はじめに
こんにちは。42 Tokyo というパリ発のエンジニア養成機関の2023年度アドベントカレンダー昨日に続いて15日目も担当します、在校生の mogawa と申します。引き続き、LLDB
ネタで投稿させていただきます。
LLDBカスタムコマンド
そもそもLLDBはすでに、複数のコマンドを用意してくれており、通常そちらで十分だと思うのですが、痒いところに微妙に手が届かないことがあり、少し勉強してみました。
カスタムコマンドを必要とした端緒
42東京の本科生になると、多くの課題でシステムコールしか使うのを許されていないため、まず、自分用汎用関数群(strlen
やmemmove
などなど)を作成する課題が与えられます。
そして、自分で作成したかわいい自作汎用関数群を携えて、難易度が上がっていく次への課題へと対応していくことになります。
連結リスト構造体(t_list
)
その最初の課題で連結リスト関係の一連の関数群も作成する機会があるのですが、構造は以下のt_list
の構造体となります。
typedef struct s_list
{
struct s_list *next;
void *content;
} t_list;
void*型の問題
t_list
構造体において、実際のデータは、メンバ変数のcontent
に預けることになりますが、こちらはどんな型でも受け入れられるようにvoid *
となっています。
何でも受け入れられる忍耐と寛容のある型なのですが、おかげでデバッガーは、void*型
をどう表示していいのかわからない状態となり怒られます。
(lldb) print *content_head->next
(s_list) {
next = 0x0000600001e44040
prev = 0x0000600001e44000
content = 0x0000600001044000
}
(lldb) print *content_head->next->content
error: <user expression 4>:1:1: indirection not permitted on operand of type 'void *'
1 | *content_head->next->content
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~
(lldb) print *content_head->next->content->_str
error: <user expression 5>:1:29: member reference base type 'void' is not a structure or union
1 | *content_head->next->content->_str
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~^ ~~~~
(lldb)
対応策
デバッガーにvoid *
型が指している先の型をcast
して優しく教えてあげれば、デバッガも答えてくれます。
(lldb) print ((t_content *)(content_head->next->content))->_str
(char *) 0x0000600001c44000 "One"
- でも毎回こんなコマンド打ってられない
- しかもリスト構造なら何個も
next->next->next->next->...
となる
のでカスタムコマンドで解決したいと思います。
カスタムコマンドを実装してみよう
前提
Python
LLDB
はPython
のモジュールを通じて多くの機能をコントロールできるようになっているので、カスタムコマンドの利用には、Pythonがインストールされている必要があります。
.lldbinit
設定は、.lldbinit
ファイルに保存します。LLDBは~/.lldbinit
ファイルを起動時に読み込みますので、ファイルを作成しておいてください。
今回のサンプルリスト構造
t_listのvoid *
の先に、intとchar*型を持つt_data
構造体を格納します。
typedef struct s_data
{
int _int;
char *_str;
} t_data;
/*
typedef struct s_list
{
struct s_list *next;
void *data;
} t_list;
*/
なお、テストのためにt_data
を有するリスト構造を作成するコードをGITHUBに上げておいたので、そちらもご参照ください。
作成したい機能
リスト構造を順次最後までループしつつ、格納された情報を印字しデバックできるコードを作成します。
まずはハローワールド実行
print_tlist.py
を作成してください。その中で、lldbのモジュールのインポートなどを行い、print_tlist()
関数を作成してみましょう。
import lldb
def print_tlist(debugger, command, result, internal_dict):
print("hello world!")
また、.lldbinit
にprint_tlist.py
を登録しましょう。
command script import ~/your//print_tlist/file/location.py
lldb
を立ち上げて、command script add -f print_tlist.print_tlist greeting
と打ち込み、続けて、greeting
と打ってみると、hello world!
とプリントされていると思います。
[~]$ lldb
(lldb) command script add -f print_tlist.print_tlist print_tlist
(lldb) print_tlist
hello world!
(lldb)
print_tlist
関数の実装
hello world!が成功したので、正しく、print_tlist.py
はLLDBに読み込まれているようです。まずは、 LLDBで立ち上げ時に毎回command script add -f print_tlist.print_tlist print_tlist
でコマンドの登録は面倒なので、こちらをprint_tlist.py
に入れましょう。
import lldb
def print_tlist(debugger, command, result, internal_dict):
print("hello world!")
+def __lldb_init_module(debugger, internal_dict):
+ debugger.HandleCommand("command script add -f print_tlist.print_tlist print_tlist")
LLDBをquit
し再度lldb
で立ち上げ、print_tlistと打ち込んで、hello world!
が出ると思います。
LLDB Python APIのクラス
次に、print_tlist関数
にlldbの重要なクラスにアクセスできるようにしましょう。
import lldb
def print_tlist(debugger, command, result, internal_dict):
- print("hello world!")
+ target = debugger.GetSelectedTarget()
+ process = target.GetProcess()
+ thread = process.GetSelectedThread()
+ frame = thread.GetSelectedFrame()
print_tlistの引数にdebugger
とあるように、debuggerのクラスを関数の引数として受け取れるので、そこから、他のクラスに順次アクセスして、それぞれのクラスが持つ情報にアクセスできるようです。コードでは、
debugger
->target
->process
->thread
->frame
をそれぞれのGetSelected*()
で格納しています。
-
target
はデバッグ対象やデバッグ情報などを担当 -
process
は、メモリーアクセスや複数のスレッドを担当 -
thread
は、スタックフレーム群や実行ステップなどを担当 -
frame
は、当該スタックフレームを担当し、ローカル変数などを担当
上記は著者の拙い理解ですので、実態と乖離している可能性がかなーりあります!大きな間違えがありましたら、教えていただけましたら、助かります!また、LLDB公式サイトを確認してください。
void*型のキャスト対応
デバッガーに渡すべきデータは、
- t_list連結リストの
先頭ノード
(head_of_tlist
) - t_listの
void *content
が指している先の型情報(t_data
)
です。本来は引数などで渡すのがいいのでしょうが、今回は、コードにベタ打ちとします。
1の先頭ノードはframe
に情報がありそうなので、その中で、FindVariable()
して発見します。
+ list_head = frame.FindVariable("head_of_tlist")
また、2の型情報は全体の情報を持つtarget
のFindFirstType()
で見つけて、GetPointerType()
で型タイプをゲットしたいと思います。
+ content_type = target.FindFirstType("t_data").GetPointerType()
連結リストの最後までループ
連結リストの最後まで進めるループを作成して、リストの最後まで順次データを印字するようにしたいと思います。以下のコードを付け加えてください。この中ではcurrent
の値がNULL
になるまで、ループを回します。
current = list_head
while current.GetValueAsUnsigned() != 0:
data = current.GetChildMemberWithName('content').Cast(content_type)
int_data = data.GetChildMemberWithName('_int').GetValueAsSigned()
str_data = data.GetChildMemberWithName('_str').GetValue()
print (f'[:{int_data}:{str_data}]')
current = current.GetChildMemberWithName('next')
この中で、data
にはt_listのcontent
が指している先のデータは、t_data
の型だとCast(content_type)
で渡してあげます。そして、デレファレンスされたdata
から、子要素の_int & _str
をGetChildMemberWithName()
で確保し、int型にはGetValueAsSigned()
で、また、char*型にはGetSummary()
で人間の情報に転換し、順次print
することにより、t_list型連結リストのすべての情報を吐き出すようにしました。
以下が最終のコードです。
import lldb
def print_tlist(debugger, command, result, internal_dict):
target = debugger.GetSelectedTarget()
process = target.GetProcess()
thread = process.GetSelectedThread()
frame = thread.GetSelectedFrame()
list_head = frame.FindVariable("head_of_tlist")
content_type = target.FindFirstType("t_data").GetPointerType()
current = list_head
while current.GetValueAsUnsigned() != 0:
data = current.GetChildMemberWithName('content').Cast(content_type)
int_data = data.GetChildMemberWithName('_int').GetValueAsSigned()
str_data = data.GetChildMemberWithName('_str').GetSummary()
print (f'[:{int_data}:{str_data}]')
current = current.GetChildMemberWithName('next')
def __lldb_init_module(debugger, internal_dict):
# command script add -f <filename>.<function_name> <cmd_name>
debugger.HandleCommand("command script add -f print_tlist.print_tlist print_tlist")
実行テスト
テストコードGITHUBにて、-g
フラグ付きでmain.c
をコンパイルか、または、ただ単にmake
してください。そして、生成されて実行ファイルをlldb
で読み込んでください。b main
でブレークポイントを設定し、n
でステップ実行を実行し、head_of_tlist
のデータが作られたところで、作成したカスタムコマンドprint_tlist
を実行してみてください。
$ lldb print_tlist
(lldb) target create "print_tlist"
Current executable set to '/Users/masaru/SynologyDrive/document/code/lldb/script_for_tlist/print_tlist' (arm64).
(lldb) b main
Breakpoint 1: where = print_tlist`main + 24 at main.c:50:18, address = 0x0000000100003c28
(lldb) run
Process 27664 launched: '/Users/masaru/SynologyDrive/document/code/lldb/script_for_tlist/print_tlist' (arm64)
Process 27664 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
frame #0: 0x0000000100003c28 print_tlist`main at main.c:50:18
47 {
48 t_list *head_of_tlist;
49
-> 50 head_of_tlist = get_data();
51 (void)head_of_tlist;// surpress a compiler's warning
52 return (0);
53 }
(lldb) next
Process 27664 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = step over
frame #0: 0x0000000100003c38 print_tlist`main at main.c:52:2
49
50 head_of_tlist = get_data();
51 (void)head_of_tlist;// surpress a compiler's warning
-> 52 return (0);
53 }
(lldb) print_tlist
[:0:None]
[:-100:"Hello World"]
[:0:"from 42 Tokyo!"]
[:42:"This programe will create memory leak!"]
t_data
に格納した情報が順次作成されているので、一旦完成です。
引数を受け取り汎用性を高めたptlist
の実装例
42生向けになりますが、引数を受取り汎用性を高めたものを、こちらのGITHUBに格納していますので、見てみてください。
参考文献
LLDB公式サイト
Kodeco Team & Walter Tyree. Advanced Apple Debugging & Reverse Enginnering. Kodeco, 2023年5月