はじめに
こんにちは。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月