WWDC 2018のAdvanced Debugging with Xcode and LLDBのセッションがとても良かったので、コマンドを作ってみた。
デバッグ時、ある関数を通ったかどうか知るためにNSLogとかprintとかを書いたりするが、リポジトリには入れたくないし、消すのが面倒である。
そこでLLDBのスクリプト機能を使って簡易トレースしてみた。
使い方は、
- Xcode上で、トレースしたい関数にブレークポイントを貼る
- アプリケーションの先頭 (application:didFinishLaunchingWithOptionsなど)にブレークポイントを貼る
- 実行するとアプリケーションの先頭で止まる
- LLDBコマンドプロンプトでtraceコマンドを実行
- 実行を続行する
という感じ。トレースが不要になったらブレークポイントをすべて削除すればよい。
XcodeはデバッガとしてLLDBを使っている。LLDBは~/.lldbinitというファイルを用意しておくと、デバッグの起動時に読み込んでくれる。このファイル中は(Pythonではなく)LLDBのコマンドだけが実行できる。その中でPythonスクリプトを読み込むことができる。
command script import ~/bin/lldbutils.py
スクリプトの中でコマンドや、コマンドで使うクラスを定義する。コマンドを追加するには、クラスを定義してもよいし、関数だけでも実装できる。(WWDCのセッションのサンプルだと、引数をきちんとパースしている)
import lldb
class TraceCommand:
def trace(self, target, debugger, result):
for breakpoint in target.breakpoint_iter():
debugger.HandleCommand("br command add -F lldbutils.show_function_name %d" % breakpoint.id)
return None
def get_short_help(self):
return "Change all breakpoints into trace points."
def get_long_help(self):
return self.get_short_help()
def __init__(self, debugger, unused):
return
def __call__(self, debugger, command, exe_ctx, result):
self.trace(exe_ctx.target, debugger, result)
def show_function_name(frame, bp_loc, dict):
thread = frame.GetThread()
debugger = thread.GetProcess().GetTarget().GetDebugger()
lineEntry = frame.GetLineEntry()
options = lldb.SBExpressionOptions()
reg = frame.EvaluateExpression("self", options)
print('%s %s : %d' % (str(reg.GetValue() or ''), frame.GetFunctionName(), lineEntry.GetLine()))
return False
def __lldb_init_module(debugger, dict):
debugger.HandleCommand('command script add -c lldbutils.TraceCommand trace')
traceコマンドでは、全てのブレークポイントを列挙して、show_function_name関数を実行するようにしている。show_function_nameの中では関数名と行番号をprintする。ブレークポイントで呼び出された関数にてFalseを返すと、(コマンドプロンプトを表示せずに)実行を継続するという仕様になっている。(詳細はhelp br command addに書いてある)
ブレークしたときのコマンドやauto-continueなどの属性をPythonのオブジェクトで直接設定する方法がわからなかったので、デバッガ経由でコマンド実行して設定している。
微妙なのは、デバッガインスタンスはデバッグを開始したあとに作成される点である。どうやら、Xcode上で設定したブレークポイントはXcodeが管理していて、デバッグを開始した時点でLLDBに伝えるようになっているようだ。LLDBのコマンドプロンプトで設定したブレークポイントの情報はXcodeには伝わらず、実行を停止してからもう一度実行すると、Xcode上で設定したブレークポイントだけが設定される。