소프트웨어공학 퀴즈 및 중간고사 대비
The Debugging Book에서 핵심 내용만 요약 정리해서 미사카 미코토 공부법으로 만점 쟁취해 봅시다.
Intro_Debugging
- Understand the code
- Fix the problem, not the symptom
- Proceed systematically
which is what we will apply for the rest of this chapter.
Since the code was originally written by a human, any defect can be related to some original mistake the programmer made. This gives us a number of terms that all are more precise than the general “error” or the colloquial “bug”:
- A mistake is a human act or decision resulting in an error.
- A defect is an error in the program code. Also called bug.
- A fault is an error in the program state. Also called infection.
- A failure is an externally visible error in the program behavior. Also called malfunction.
The cause-effect chain of events is thus
- Mistake → Defect → Fault → … → Fault → Failure
Note that not every defect also causes a failure, which is despite all testing, there can still be defects in the code looming around until the right conditions are met to trigger them. On the other hand, though, every failure can be traced back to the defect that causes it. Our job is to break the cause-effect chain.
Defect에 의해 발생한 Fault들로 형성되는 연쇄의 사슬에서 가장 처음 이른 state에서 발생한 fault를 찾아내는 것이 핵심입니다.
Correct State -> Faulty state로 넘어가는 transition 지점을 찾아야 합니다.
Debugging strategy 실제 프로그램이 갖는 state의 수는 매우 많기 때문에 사람이 일일이 추적하기 어렵습니다. 즉, 의심되는 부분을 집중 분석하고 그 다음 덜 중요한 부분을 분석하는 식으로 debugging하는 전략이 필요합니다.
프로그램 중간에 assertion을 삽입하는 것도 도움이 됩니다.
assert condition
Python에서는 위의 statement를 사용할 수 있습니다.
The Scientific Method
- Formulate a question, as in “Why does this apple fall down?”.
- Invent a hypothesis based on knowledge obtained while formulating the question, that may explain the observed behavior.
- Determining the logical consequences of the hypothesis, formulate a prediction that can support or refute the hypothesis. Ideally, the prediction would distinguish the hypothesis from likely alternatives.
- Test the prediction (and thus the hypothesis) in an experiment. If the prediction holds, confidence in the hypothesis increases; otherwise, it decreases.
- Repeat Steps 2–4 until there are no discrepancies between hypothesis and predictions and/or observations.
Checking Diagnoses
In debugging, you should start to fix your code if and only if you have a diagnosis that shows two things:
- Causality. Your diagnosis should explain why and how the failure came to be. Hence, it induces a fix that, when applied, should make the failure disappear.
- Incorrectness. Your diagnosis should explain why and how the code is incorrect (which in turn suggests how to correct the code). Hence, the fix it induces not only applies to the given failure, but also to all related failures.
Tracer
Tracer
class: 프로그램 실행 중 발생하는 이벤트를 로깅함
EventTracer
class: 특정 조건에서만 로깅하도록 제한
위 클래스들은 subclassing을 허용한다.
>>> with EventTracer(condition='line == 223 or len(out) >= 6'):
>>> remove_html_markup('<b>foo</b>bar')
특정 condition
(breakpoint)뿐만 아니라, 주어진 events
내 값들이 변화할 때(watchpoint)만 로깅하도록 제한할 수도 있습니다.
>>> with EventTracer(events=["c == '/'"]): # c == '/' 평가 결과 True, False 값이 변화할 때만 로깅
>>> remove_html_markup('<b>foo</b>bar')
sys.settrace()
sys.settrace()
함수에 매 line마다 호출될 tracing function을 전달하여 프로그램 실행 중 발생하는 이벤트를 로깅할 수 있습니다. None
을 전달하면 tracing을 끕니다.
tracing function은 아래와 같은 형태
# tracing function
def traceit(frame: FrameType, event: str, arg: Any) -> Optional[Callable]:
...
Here, event
is a string telling what has happened in the program – for instance,
'line'
– a new line is executed (정확히는, 실행 전에line
이벤트가 먼저 발생함)'call'
– a function just has been called'return'
– a function returns'exception'
– an exception has been raised
The frame
argument holds the current execution frame – that is, the function and its local variables:
frame.f_lineno
– the current lineframe.f_locals
– the current variables (as a Python dictionary)frame.f_code
– the current code (as a Code object), with attributes such asframe.f_code.co_name
– the name of the current function
tracing function의 리턴값은 다음 이벤트 때 실행될 함수에 해당하며, 일반적으로는 자기 자신을 리턴합니다.
여기서 arg
는 이벤트에 따라 다른 의미를 가집니다.
event |
arg |
---|---|
call |
호출된 함수 |
return |
반환값 |
exception |
예외 객체 |
line |
None |
여기서 호출된 함수라는 건, 함수 이름이 아니라 함수 객체를 의미하는 것 같습니다. 이름에 접근하려면 frame.f_code.co_name
을 사용해야 합니다.
Tracer
클래스 만들기
The typical usage of Tracer
, however, is as follows:
with Tracer():
# Code to be traced
...
# Code no longer traced
...
When the with
statement is encountered, the __enter__()
method is called, which starts tracing. When the with
block ends, the __exit__()
method is called, and tracing is turned off. We take special care that the internal __exit__()
method is not part of the trace, and that any other tracing function that was active before is being restored.
미리 정의된 StackInspector
라는 클래스를 상속받아서 Tracer
클래스를 정의하고 있는데, our_frame()
이랑 is_internal_error()
라는 메소드가 에러 상황에서 더 나은 정보를 준다고 하네요. -> 직접 구현하려면 StackInspector
는 이용 불가하니 무시
class Tracer(StackInspector):
"""A class for tracing a piece of code. Use as `with Tracer(): block()`"""
def __init__(self, *, file: TextIO = sys.stdout) -> None:
"""Trace a block of code, sending logs to `file` (default: stdout)"""
self.original_trace_function: Optional[Callable] = None # 이따 __enter__에서 기존 tracing function이 저장된다.
self.file = file
def traceit(self, frame: FrameType, event: str, arg: Any) -> None:
"""Tracing function. To be overridden in subclasses."""
self.log(event, frame.f_lineno, frame.f_code.co_name, frame.f_locals)
def _traceit(self, frame: FrameType, event: str, arg: Any) -> Optional[Callable]: # private
"""Internal tracing function."""
if self.our_frame(frame):
# Do not trace our own methods
pass
else:
self.traceit(frame, event, arg)
return self._traceit
# 굳이 이렇게 구현하지 말고 traceit 함수 내에서 frame.f_code.co_name을 이용해 현재 함수가 Tracer 클래스의 메소드인지 확인하면 될 듯
def log(self, *objects: Any,
sep: str = ' ', end: str = '\n',
flush: bool = True) -> None:
"""
Like `print()`, but always sending to `file` given at initialization,
and flushing by default.
"""
print(*objects, sep=sep, end=end, file=self.file, flush=flush)
def __enter__(self) -> Any: # with 구문 진입 시 호출
"""Called at begin of `with` block. Turn tracing on."""
self.original_trace_function = sys.gettrace() # 현재 사용 중인 tracing function을 저장
sys.settrace(self._traceit)
# This extra line also enables tracing for the current block ?? (그냥 convention에 가깝다고 함. 중요하지는 않은 듯)
# inspect.currentframe().f_back.f_trace = self._traceit
return self
def __exit__(self, exc_tp: Type, exc_value: BaseException,
exc_traceback: TracebackType) -> Optional[bool]:
"""
Called at end of `with` block. Turn tracing off.
Return `None` if ok, not `None` if internal error.
"""
sys.settrace(self.original_trace_function)
# Note: we must return a non-True value here,
# such that we re-raise all exceptions
if self.is_internal_error(exc_tp, exc_value, exc_traceback):
return False # internal error
else:
return None # all ok
간단히 해석하자면, __enter__
와 __exit__
로 with
블록 내에서만 사용할 tracing function을 지정하고, with
블록을 나갈 때 원래 tracing function을 복원하는 기능을 구현했네요.
그리고 file
argument로 log를 출력할 스트림을 지정 가능하구요.
_traceit
은 자기 함수를 제외하고 다른 함수들만 적용된다는 조건 하에 traceit
을 호출하는 wrapper 함수처럼 보이는데… 이것이 private
이고 traceit
이 public
이라는 것은 어떤 의미를 가질까요? 그냥 보기에는 _traceit
이 먼저 호출되고 그 안에서 traceit
을 호출해야 하는 것처럼 보이는데 말이죠… 외부에선 직접 traceit
을 호출한다는 소리일까요?
__enter__
함수 보면 답이 나옵니다. 여기서 아예 sys.settrace()
에 _traceit
을 전달하고 있네요. 그래서 with
블록 내에서 tracing function으로 _traceit
이 호출되고, _traceit
이 traceit
을 호출하는 방식으로 동작하는 것이죠.
소스 코드 접근
inspect
모듈을 사용하면 소스 코드에 접근할 수 있습니다.
import inspect
...
module = inspect.getmodule(frame.f_code)
source = inspect.getsource(module)
이제 줄번호만 알면 되겠네요.
줄번호는 frame.f_lineno
에 있으니, source
를 줄 단위로 나누고 frame.f_lineno
와 함께 그 값에 해당하는 줄을 출력하면 되겠죠.
Python hack: 메소드를 추가적으로 정의하기
class C(C):
def new_method(self):
pass
이런 식으로 자기 자신을 상속하면 기존 클래스를 상속하는 새로운 클래스가 생성되며, shadowing이 일어나면서 메소드 정의하는 코드를 연속적으로 작성하지 않고도 새 메소드를 추가할 수 있어요.
여기서는 한 눈에 보기 쉽게 그냥 클래스가 업데이트될 때마다 Tracer
클래스 전체 정의를 보여주도록 하겠습니다.
class Tracer(StackInspector):
"""A class for tracing a piece of code. Use as `with Tracer(): block()`"""
def __init__(self, *, file: TextIO = sys.stdout) -> None:
"""Trace a block of code, sending logs to `file` (default: stdout)"""
self.original_trace_function: Optional[Callable] = None # 이따 __enter__에서 기존 tracing function이 저장된다.
self.file = file
def traceit(self, frame: FrameType, event: str, arg: Any) -> None:
"""Tracing function; called at every line. To be overloaded in subclasses."""
if event == 'line':
module = inspect.getmodule(frame.f_code)
if module is None:
source = inspect.getsource(frame.f_code)
else:
source = inspect.getsource(module)
current_line = source.split('\n')[frame.f_lineno - 1]
self.log(frame.f_lineno, current_line)
def _traceit(self, frame: FrameType, event: str, arg: Any) -> Optional[Callable]: # private
"""Internal tracing function."""
if self.our_frame(frame):
# Do not trace our own methods
pass
else:
self.traceit(frame, event, arg)
return self._traceit
# 굳이 이렇게 구현하지 말고 traceit 함수 내에서 frame.f_code.co_name을 이용해 현재 함수가 Tracer 클래스의 메소드인지 확인하면 될 듯
def log(self, *objects: Any,
sep: str = ' ', end: str = '\n',
flush: bool = True) -> None:
"""
Like `print()`, but always sending to `file` given at initialization,
and flushing by default.
"""
print(*objects, sep=sep, end=end, file=self.file, flush=flush)
def __enter__(self) -> Any: # with 구문 진입 시 호출
"""Called at begin of `with` block. Turn tracing on."""
self.original_trace_function = sys.gettrace() # 현재 사용 중인 tracing function을 저장
sys.settrace(self._traceit)
# This extra line also enables tracing for the current block ?? (그냥 convention에 가깝다고 함. 중요하지는 않은 듯)
# inspect.currentframe().f_back.f_trace = self._traceit
return self
def __exit__(self, exc_tp: Type, exc_value: BaseException,
exc_traceback: TracebackType) -> Optional[bool]:
"""
Called at end of `with` block. Turn tracing off.
Return `None` if ok, not `None` if internal error.
"""
sys.settrace(self.original_trace_function)
# Note: we must return a non-True value here,
# such that we re-raise all exceptions
if self.is_internal_error(exc_tp, exc_value, exc_traceback):
return False # internal error
else:
return None # all ok
inspect.getmodule(frame.f_code)
: frame의 code object가 속한 module을 반환합니다. 만약 module이 없다면 code object 자체를 반환합니다.
inspect.getsource(frame.f_code)
: frame의 code object의 소스 코드를 반환합니다.
inspect.getsource(module)
: module의 소스 코드를 반환합니다.
이런 식으로 traceit
을 재정의 가능합니다.
이제 다음과 같이 사용할 수 있습니다.
with Tracer():
remove_html_markup('<b>foo</b>bar')
변수 값 변화 추적
class Tracer(StackInspector):
"""A class for tracing a piece of code. Use as `with Tracer(): block()`"""
def __init__(self, file: TextIO = sys.stdout) -> None:
"""
Create a new tracer.
If `file` is given, output to `file` instead of stdout.
"""
"""Trace a block of code, sending logs to `file` (default: stdout)"""
self.original_trace_function: Optional[Callable] = None # 이따 __enter__에서 기존 tracing function이 저장된다.
self.file = file
self.last_vars: Dict[str, Any] = {}
super().__init__(file=file)
def changed_vars(self, new_vars: Dict[str, Any]) -> Dict[str, Any]:
"""Track changed variables, based on `new_vars` observed."""
changed = {}
for var_name, var_value in new_vars.items():
if (var_name not in self.last_vars or
self.last_vars[var_name] != var_value):
changed[var_name] = var_value
self.last_vars = new_vars.copy()
return changed
def traceit(self, frame: FrameType, event: str, arg: Any) -> None:
"""Tracing function; called at every line. To be overloaded in subclasses."""
if event == 'line':
module = inspect.getmodule(frame.f_code)
if module is None:
source = inspect.getsource(frame.f_code)
else:
source = inspect.getsource(module)
current_line = source.split('\n')[frame.f_lineno - 1]
self.log(frame.f_lineno, current_line)
def _traceit(self, frame: FrameType, event: str, arg: Any) -> Optional[Callable]: # private
"""Internal tracing function."""
if self.our_frame(frame):
# Do not trace our own methods
pass
else:
self.traceit(frame, event, arg)
return self._traceit
# 굳이 이렇게 구현하지 말고 traceit 함수 내에서 frame.f_code.co_name을 이용해 현재 함수가 Tracer 클래스의 메소드인지 확인하면 될 듯
def log(self, *objects: Any,
sep: str = ' ', end: str = '\n',
flush: bool = True) -> None:
"""
Like `print()`, but always sending to `file` given at initialization,
and flushing by default.
"""
print(*objects, sep=sep, end=end, file=self.file, flush=flush)
def __enter__(self) -> Any: # with 구문 진입 시 호출
"""Called at begin of `with` block. Turn tracing on."""
self.original_trace_function = sys.gettrace() # 현재 사용 중인 tracing function을 저장
sys.settrace(self._traceit)
# This extra line also enables tracing for the current block ?? (그냥 convention에 가깝다고 함. 중요하지는 않은 듯)
# inspect.currentframe().f_back.f_trace = self._traceit
return self
def __exit__(self, exc_tp: Type, exc_value: BaseException,
exc_traceback: TracebackType) -> Optional[bool]:
"""
Called at end of `with` block. Turn tracing off.
Return `None` if ok, not `None` if internal error.
"""
sys.settrace(self.original_trace_function)
# Note: we must return a non-True value here,
# such that we re-raise all exceptions
if self.is_internal_error(exc_tp, exc_value, exc_traceback):
return False # internal error
else:
return None # all ok
위와 같이 변수들의 값을 저장해두고 변화가 있는 변수들만 감지하는 메소드를 구현 가능합니다. 이를 이용해서 tracing function에 값이 변한 변수만 로깅하도록 만들 수도 있겠죠.
class Tracer(StackInspector):
"""A class for tracing a piece of code. Use as `with Tracer(): block()`"""
def __init__(self, file: TextIO = sys.stdout) -> None:
"""
Create a new tracer.
If `file` is given, output to `file` instead of stdout.
"""
"""Trace a block of code, sending logs to `file` (default: stdout)"""
self.original_trace_function: Optional[Callable] = None # 이따 __enter__에서 기존 tracing function이 저장된다.
self.file = file
self.last_vars: Dict[str, Any] = {}
super().__init__(file=file)
def changed_vars(self, new_vars: Dict[str, Any]) -> Dict[str, Any]:
"""Track changed variables, based on `new_vars` observed."""
changed = {}
for var_name, var_value in new_vars.items():
if (var_name not in self.last_vars or
self.last_vars[var_name] != var_value):
changed[var_name] = var_value
self.last_vars = new_vars.copy()
return changed
def print_debugger_status(self, frame: FrameType, event: str, arg: Any) -> None:
"""Show current source line and changed vars"""
changes = self.changed_vars(frame.f_locals)
changes_s = ", ".join([var + " = " + repr(changes[var])
for var in changes])
if event == 'call':
self.log("Calling " + frame.f_code.co_name + '(' + changes_s + ')')
elif changes:
self.log(' ' * 40, '#', changes_s)
if event == 'line':
try:
module = inspect.getmodule(frame.f_code)
if module is None:
source = inspect.getsource(frame.f_code)
else:
source = inspect.getsource(module)
current_line = source.split('\n')[frame.f_lineno - 1]
except OSError as err:
self.log(f"{err.__class__.__name__}: {err}")
current_line = ""
self.log(repr(frame.f_lineno) + ' ' + current_line)
if event == 'return':
self.log(frame.f_code.co_name + '()' + " returns " + repr(arg))
self.last_vars = {} # Delete 'last' variables
def traceit(self, frame: FrameType, event: str, arg: Any) -> None:
"""Tracing function; called at every line. To be overloaded in subclasses."""
self.print_debugger_status(frame, event, arg)
def _traceit(self, frame: FrameType, event: str, arg: Any) -> Optional[Callable]: # private
"""Internal tracing function."""
if self.our_frame(frame):
# Do not trace our own methods
pass
else:
self.traceit(frame, event, arg)
return self._traceit
# 굳이 이렇게 구현하지 말고 traceit 함수 내에서 frame.f_code.co_name을 이용해 현재 함수가 Tracer 클래스의 메소드인지 확인하면 될 듯
def log(self, *objects: Any,
sep: str = ' ', end: str = '\n',
flush: bool = True) -> None:
"""
Like `print()`, but always sending to `file` given at initialization,
and flushing by default.
"""
print(*objects, sep=sep, end=end, file=self.file, flush=flush)
def __enter__(self) -> Any: # with 구문 진입 시 호출
"""Called at begin of `with` block. Turn tracing on."""
self.original_trace_function = sys.gettrace() # 현재 사용 중인 tracing function을 저장
sys.settrace(self._traceit)
# This extra line also enables tracing for the current block ?? (그냥 convention에 가깝다고 함. 중요하지는 않은 듯)
# inspect.currentframe().f_back.f_trace = self._traceit
return self
def __exit__(self, exc_tp: Type, exc_value: BaseException,
exc_traceback: TracebackType) -> Optional[bool]:
"""
Called at end of `with` block. Turn tracing off.
Return `None` if ok, not `None` if internal error.
"""
sys.settrace(self.original_trace_function)
# Note: we must return a non-True value here,
# such that we re-raise all exceptions
if self.is_internal_error(exc_tp, exc_value, exc_traceback):
return False # internal error
else:
return None # all ok
여기서는 traceit()
함수에서 직접 디버깅 메시지를 출력하던 것이 새 메소드 print_debugger_status()
로 분리되었네요.
자 주의할 점은 line
이벤트 이후에 statement의 evaluation이 일어나기 때문에 변수 변화의 출력은 line
이벤트 이후에 이루어집니다.
Conditional Tracing
class ConditionalTracer(Tracer):
def __init__(self, *, condition: Optional[str] = None, file: TextIO = sys.stdout) -> None:
"""Constructor. Trace all events for which `condition` (a Python expr) holds."""
if condition is None:
condition = 'False'
self.condition: str = condition
self.last_report: Optional[bool] = None
super().__init__(file=file)
def eval_in_context(self, expr: str, frame: FrameType) -> Optional[bool]:
frame.f_locals['function'] = frame.f_code.co_name
frame.f_locals['line'] = frame.f_lineno
try:
cond = eval(expr, None, frame.f_locals)
except NameError: # (yet) undefined variable
cond = None
return cond
def do_report(self, frame: FrameType, event: str, arg: Any) -> Optional[bool]: # report 할지 말지 결정
return self.eval_in_context(self.condition, frame)
def traceit(self, frame: FrameType, event: str, arg: Any) -> None:
report = self.do_report(frame, event, arg)
if report != self.last_report:
# report == False 이고 self.last_report == True인 경우에는 조건 만족 연속구간이 끊겼으므로 다음 조건 만족시에 ... 출력함
if report:
self.log("...") # 조건 만족이 시작되는 지점에서 리포트를 시작함을 표시
self.last_report = report # 연속적으로 조건이 참이면 ...을 출력하지 않음
if report:
self.print_debugger_status(frame, event, arg)
이런 식으로 조건을 만족하는 경우에만 tracing을 하도록 만들 수 있습니다. 여기서 핵심은 eval_in_context
함수입니다. 이것으로 조건 만족 여부에 따라, tracing을 할지 말지 결정할 수 있게 되는 것이죠.
eval_in_context
함수에서 임의로 function
과 line
변수를 추가한 것을 주목하세요. 이렇게 하면 함수 이름과 줄번호를 조건으로 줄 수가 있는 것이죠. 주의할 점은, 이 시점에서 원래의 지역 변수에 function
이나 line
이 존재했다면 값이 덮어씌워지기 때문에 원래의 지역 변수에 function
과 line
이 있었다면 Tracer에서 접근할 수 없게 됩니다.
Event Tracing
class EventTracer(ConditionalTracer):
"""Log when a given event expression changes its value"""
def __init__(self, *, condition: Optional[str] = None,
events: List[str] = [], file: TextIO = sys.stdout) -> None:
"""Constructor. `events` is a list of expressions to watch."""
self.events = events
self.last_event_values: Dict[str, Any] = {}
super().__init__(file=file, condition=condition)
def events_changed(self, events: List[str], frame: FrameType) -> bool:
"""Return True if any of the observed `events` has changed"""
change = False
for event in events:
value = self.eval_in_context(event, frame)
if (event not in self.last_event_values or
value != self.last_event_values[event]):
self.last_event_values[event] = value
change = True
return change
def do_report(self, frame: FrameType, event: str, arg: Any) -> bool:
"""Return True if a line should be shown"""
return (self.eval_in_context(self.condition, frame) or
self.events_changed(self.events, frame))
events
로 전달된 expression(변수의 값, 함수 자체)이 변화할 때만 로깅하도록 만들 수 있습니다.
아래와 같이 사용 가능합니다.
with EventTracer(events=["c == '/'"]):
remove_html_markup('<b>foo</b>bar')
Efficient Tracing
동적으로 condition을 만족하는지 일일이 확인하는 Tracing은 매우 overhead가 큰 작업이라 tracing을 하지 않을 때보다 훨씬 실행 시간이 길어집니다. Frida 때를 생각해 보면 답 나오죠?
아예 tracing 코드를 특정 함수에만 정적으로 삽입하는 방식을 쓰면 부하를 훨씬 줄일 수가 있어요. 이때는 tracing 대상 코드가 아닌 부분은 원래의 실행 속도가 유지 가능하거든요. 방법을 알아봅시다.
함수와 breakpoint의 위치를 인자로 받아 거기에 tracing statements를 인젝션하는 insert_tracer
라는 함수를 정의해 봅시다.
TRACER_CODE = \
"TRACER.print_debugger_status(inspect.currentframe(), 'line', None); "
TRACER = Tracer()
def insert_tracer(function: Callable, breakpoints: List[int] = []) -> Callable:
"""Return a variant of `function` with tracing code `TRACER_CODE` inserted
at each line given by `breakpoints`."""
source_lines, starting_line_number = inspect.getsourcelines(function)
breakpoints.sort(reverse=True)
for given_line in breakpoints:
# Set new source line
relative_line = given_line - starting_line_number + 1
inject_line = source_lines[relative_line - 1]
indent = len(inject_line) - len(inject_line.lstrip())
source_lines[relative_line - 1] = ' ' * indent + TRACER_CODE + inject_line.lstrip()
# Rename function
new_function_name = function.__name__ + "_traced"
source_lines[0] = source_lines[0].replace(function.__name__, new_function_name)
new_def = "".join(source_lines)
# For debugging
print_content(new_def, '.py', start_line_number=starting_line_number)
# We keep original source and filename to ease debugging
prefix = '\n' * starting_line_number # Get line number right
new_function_code = compile(prefix + new_def, function.__code__.co_filename, 'exec')
exec(new_function_code)
new_function = eval(new_function_name)
return new_function
이러면 sys.settrace()
함수의 힘을 빌리지 않아도 특정 라인(breakpoints)에서 강제로 debugging information을 출력하도록 함으로써, 결과적으로 그 부분만 tracing을 할 수 있게 되는 것이죠.
이제 다음과 같이 사용할 수 있습니다.
_, remove_html_markup_starting_line_number = inspect.getsourcelines(remove_html_markup)
breakpoints = [(remove_html_markup_starting_line_number - 1) + 7,
(remove_html_markup_starting_line_number - 1) + 18]
remove_html_markup_traced = insert_tracer(remove_html_markup, breakpoints)
참고로, 인터프리터 언어가 아닌 컴파일 언어에서는 위와 비슷한 static한 방식으로 디버깅이 이루어집니다. Binary code 내 특정 위치에 있는 instruction을 break instruction으로 바꾸고 debugger에게 control을 넘기는 방식이죠. 그리고 debugger는 실행을 재개하기 전 그 instruction을 원래대로 복구하는 방식으로 동작합니다. 물론 read-only 영역이라 코드를 대체할 수 없는 경우는 한 줄씩 실행하고 상태를 살펴보는 dynamic한 방식으로 디버깅이 이루어집니다. 한편, 실제 레지스터의 값이나 메모리의 특정 영역의 값의 변화에 따라 interrupt가 발생하는 hardware breakpint 또는 hardware watchpoint도 있습니다.
Lessons Learned
- Interpreted languages can provide debugging hooks that allow controlling program execution and access program state.
- Tracing can be limited to specific conditions and events:
- A breakpoint is a condition referring to a particular location in the code.
- A watchpoint is an event referring to a particular state change.
- Compiled languages allow instrumenting code at compile time, injecting code that allows handing over control to a tracing or debugging tool.
Low-Level Debugging Interfaces
Linux는 ptrace()
라는 시스템 call을 제공하는데 이를 이용하면 프로세스의 실행을 추적하거나 제어할 수 있습니다. 이를 이용해서 gdb와 같은 디버깅 툴을 만들 수 있죠. 그리고 컴파일러는 -g
옵션을 통해 디버깅 정보를 포함한 binary를 생성할 수 있습니다. 이때 디버깅 정보는 instruction을 original statement로 맵핑하고 메모리의 내용물을 변수의 값에 매칭시킬 수 있도록 도와줍니다.
High-Level Debugging Interfaces
Python에서처럼 코드 위치나 변수 High-Level에서 프로그램의 동향을 추적하는 데 도움을 주는 라이브러리도 있습니다. 예를들어 JDI라는 Java Debug Interface가 있죠.
Excercises
Exception
추적하기
class Tracer(Tracer):
def print_debugger_status(self, frame: FrameType, event: str, arg: Any) -> None:
if event == 'exception':
exception, value, tb = arg
self.log(f"{frame.f_code.co_name}() "
f"raises {exception.__name__}: {value}")
else:
super().print_debugger_status(frame, event, arg)