まえがき
こんにちは。42 Tokyo というパリ発のエンジニア養成機関の2023年度アドベントカレンダー14日目を担当します、在校生の mogawa と申します。
論理的設計能力&脳内デバッグ力の欠如から、コードにバグがあると、printf()
がコードのいたるところにばらまかれる状態に陥っていたので、42tokyo校舎ですぐ使えるLLDB
というデバッガーの力を借りてみたく、少し勉強してみました。
実践LLDBデバッグ
今回は簡単なサンプルコードを利用して、コマンドラインでLLDBのデバッガーを実際に動かして初歩的なデバッグの流れを見てみたいと思います。
サンプルプロジェクトの内容
Cの文字列(char *
)をスペース
により分割して2次元配列(char **
)に格納して返す劣化版 split関数の実装を目指します。例題には、hello world from 42 tokyo
をインプットし、[hello][world][from][42][tokyo]
とchar **
で返ってくることが期待される結果です。なお、エラーハンドリングやfreeなどは全部無視しています!
スターターコードは以下です。また、GITHUBにも上げておきました。お時間ありましたら、是非、自分の環境でも一緒にやってみてください!
git clone git@github.com:masaruo/lldb_tutorial.git
split by spaceのスターターコード
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
static int ft_utils(char *s)
{
int i = 0;
int num_spaces = 0;
while (s[i])
{
if (s[i] == ' ')
{
num_spaces++;
s[i] = '\0';
}
i++;
}
return (num_spaces);
}
char **ft_split_by_space(char *s)
{
char **res;
int i = 0;
int j = 0;
int const number_of_spaces = ft_utils(s);
res = malloc(sizeof(char *) * (number_of_spaces + 2));
while (i < number_of_spaces)
{
strcpy(res[i], &s[j]);
j += strlen(&s[j]);
i++;
}
res[i] = NULL;
return (res);
}
int main(void)
{
char **res;
int i = 0;
res = ft_split_by_space("hello world from 42 tokyo");
while (res[i])
{
printf("[%s]\n", res[i]);
i++;
}
return (0);
}
デバッグの端緒
まずは、cc split.c && ./a.out
で、コンパイルして、実行してみましょう。
おっと、bus error
が出ており、実行できないようです。
$ cc split.c && ./a.out
[1] 32418 bus error ./a.out
デバッグに必要な情報を含めコンパイルを実行
デバッグの開始にあたり、コンパイラにデバッグに必要な情報も作成してもらうように、-gオプションを付けてcc -g split.c
とコンパイルします。そうすると同じ階層にa.out.dSYMというデバッグシンボルを格納するフォルダが作成されていると思います。
LLDBの実行
シェルにおいて、lldb ./a.out
で、lldbに実行ファイルを読み込ませます。画面上に、(lldb)
のプロンプトが現れたら、立ち上げ成功です。その後、run
を押して、lldbを走らせます。
$ lldb a.out
(lldb) target create "a.out"
(lldb) run
* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=2, address=0x100003f85)
frame #0: 0x0000000100003ea8 a.out`ft_utils(s="hello world from 42 tokyo")
at split.c:15:9
12 if (s[i] == ' ')
13 {
14 num_spaces++;
-> 15 s[i] = '\0';
16 }
17 i++;
18 }
a.out ft_utils(s="hello world from 42 tokyo") at split.c:15:9
とあるのでsplit.c
ファイルの15行目
でエラーが発生しているようです。ここでは、文字列の中にスペースがあったらそれをヌル文字に変更しているだけで、文字列領域も確保されているはず・・・。では、大元の呼び出し元まで辿りってみると、main内でft_split_by_space("hello world from 42 tokyo");
と静的領域に確保した文字列リテラルを変更しようとしていることから、怒られているのがわかります。今回はstrdup関数
によってコピーして、文字列を渡すように変更しましょう。
int main(void)
{
char **res;
int i = 0;
- res = ft_split_by_space("hello world from 42 tokyo");
+ res = ft_split_by_space(strdup("hello world from 42 tokyo"));
while (res[i])
{
printf("[%s]\n", res[i]);
i++;
}
return (0);
}
コード変更後の再コンパイル
コード変更後は再コンパイルが必要ですが、LLDBをエグジットする必要はありません。cc -g split.c
と再度コンパイルを行い、LLDB
のシェルに戻ってrun
を押してください。
There is a running process, kill it and restart?: [Y/n] y
と聞かれますので、y
を押します。
(lldb) run
There is a running process, kill it and restart?: [Y/n] y
* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0x0)
frame #0: 0x00000001830e5810 libsystem_platform.dylib`_platform_memmove + 448
libsystem_platform.dylib`_platform_memmove:
-> 0x1830e5810 <+448>: strb w6, [x3], #0x1
0x1830e5814 <+452>: subs x2, x2, #0x1
0x1830e5818 <+456>: b.ne 0x1830e580c ; <+444>
0x1830e581c <+460>: ret
frame #0: 0x00000001830e5810 libsystem_platform.dylib
_platform_memmove + 448 libsystem_platform.dylib
_platform_memmove:
また、エラーで怒られていますが、今度はmemmove
というライブラリ関数が出てきました。memmove
は使用した覚えがなく、コードがアセンブラのようになっておりよくわかりません。
ブレークポイントの設定
実際にコードを一行づつ動かして、悪さをしている箇所を探しましょう。
LLDBにbreakpoint set --name main
と打ってメイン関数直下にブレークポイントを配置し、プログラムの実行を一時停止させてみましょう。
ステップ実行
そしてrun
を入力。
40 int main(void)
41 {
42 char **res;
-> 43 int i = 0;
44
45 res = ft_split_by_space(strdup("hello world from 42 tokyo"));//strdup added
46 while (res[i])
メイン関数に入ってすぐ、矢印のところで実行が止まっているのがわかります
矢印の行は、まだ実行されていません
next
でft_split_by_space
まで行き、再度next
で当該関数を実行させてみると、memmove
の同じエラーが発生しました。ft_split_by_space
の中でエラーがあるようです。
ステップイン実行
では、今度は問題を起こしている関数の中に入ってみましょう。再度、run
してnext
するとft_split_by_space
に矢印がついていると思います。ここで、今度は、step
42 char **res;
43 int i = 0;
44
-> 45 res = ft_split_by_space(strdup("hello world from 42 tokyo"));
そうすると、画面にft_split_by_space
のコードが現れ、対象関数内に入れたことがわかります。
(lldb) step
Process 16754 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = step in
frame #0: 0x0000000100003d78 a.out`ft_split_by_space(s="hello world from 42 tokyo") at split.c:25:7
22 char **ft_split_by_space(char *s)
23 {
24 char **res;
-> 25 int i = 0;
26 int j = 0;
27 int const number_of_spaces = ft_utils(s);
変数値のプリント
そのまま何度もnext
を打っていくと、strcpy
を実行すると怒られるようです。
* thread #1, queue = 'com.apple.main-thread', stop reason = step over
frame #0: 0x0000000100003dc4 a.out`ft_split_by_space(s="hello") at split.c:33:3
30 while (i <= number_of_spaces)
31 {
32
-> 33 strcpy(res[i], &s[j]);
34 j += strlen(&s[j]);
35 i++;
36 }
(lldb)
Process 16650 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0x0)
frame #0: 0x0000000188f45810 libsystem_platform.dylib`_platform_memmove + 448
libsystem_platform.dylib`_platform_memmove:
-> 0x188f45810 <+448>: strb w6, [x3], #0x1
0x188f45814 <+452>: subs x2, x2, #0x1
0x188f45818 <+456>: b.ne 0x188f4580c ; <+444>
0x188f4581c <+460>: ret
* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0x0)
とエラーが印字されていますが、コードがアセンブリ言語のようになってよくわかりません。このままでは、変数などのデバッグができないので、strcpy
を呼び出したスタックまで戻りましょう。thread backtrace
をLLDBに打ち込んでください。そうすると、これまでの呼び出しの履歴が見えます。
(lldb) thread backtrace
* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0x0)
* frame #0: 0x0000000188f45810 libsystem_platform.dylib`_platform_memmove + 448
frame #1: 0x0000000188db8490 libsystem_c.dylib`stpcpy + 56
frame #2: 0x0000000188db8410 libsystem_c.dylib`__strcpy_chk + 36
frame #3: 0x0000000100003de4 a.out`ft_split_by_space(s="hello") at split.c:33:3
frame #4: 0x0000000100003ee0 a.out`main at split.c:46:8
frame #5: 0x0000000188b950e0 dyld`start + 2360
ここでは、frame #3に自分のft_split_by_space
の情報がありそうです。
frame select 3
と入力すると、strcpy
が呼び出されたところまで戻れました。
(lldb) frame select 3
frame #3: 0x0000000100003de4 a.out`ft_split_by_space(s="hello") at split.c:33:3
30 while (i <= number_of_spaces)
31 {
32
-> 33 strcpy(res[i], &s[j]);
34 j += strlen(&s[j]);
35 i++;
36 }
strcpy(res[i], &s[j]);
を実行したら怒られるのはわかっているので、まずは、正しい値がわたっているか引数の値を調べるべく、print res[i]
とprint &s[j]
と入力して見ましょう。
frame #0: 0x0000000100003dc0 a.out`ft_split_by_space(s="hello") at split.c:32:3
29 res = malloc(sizeof(char *) * (number_of_spaces + 2));
30 while (i < number_of_spaces)
31 {
-> 32 strcpy(res[i], &s[j]);
33 j += strlen(&s[j]);
34 i++;
35 }
(lldb) print res[i]
(char *) 0x0000000000000000
(lldb) print &s[j]
(char *) 0x000000014a004080 "hello"
res[i]
の数値が0x00000....とNULLとなっています。
どこも指していない領域にデータをコピーしようとしたことが原因だとわかりました。mallocを行わず文字列の領域を確保していなかったので、文字列分の領域を確保するようにコードを変更しましょう。
while (i < number_of_spaces)
{
+ res[i] = malloc(sizeof(char) * (strlen(&s[j]) + 1));
strcpy(res[i], &s[j]);
j += strlen(&s[j]);
i++;
}
ブレークポイントの削除
ブレークポイントも増えてきて、不必要なものも増えてきたので、いっそ全て消してみたいと思います。
breakpoint delete --force
と打って、全部削除してみましょう。
LLDBのコマンドが長くてうんざりしている場合には、ショートフォームもあるので、公式サイトなど見ながら探してみてください。例えば、今回のbreakpoint delete --force
はbr del -f
でもいけます。また、aliasの設定もできます。
直して、コンパイルして、再度チェックのループ
さて、コードを改変しましたので、再度コンパイルが必要です。terminalでcc -g split.c
して、./a.out
を叩いてください。
$ ./a.out
[hello]
[]
[]
[]
今度は以下の問題がみつかりました。
- helloまでましたが、それ以降が出ていません。
- []の枠が4個分しかない。
例文はhello world from 42 tokyo
なので本来なら5個の枠が必要なはず。
枠の確保はft_split_by_space
の29行目malloc(sizeof(char *) * (number_of_spaces + 2));
なので、
char **ft_split_by_space(char *s)
{
char **res;
int i = 0;
int j = 0;
int const number_of_spaces = ft_utils(s);
res = malloc(sizeof(char *) * (number_of_spaces + 2));//29行目
while (i < number_of_spaces)
{
res[i] = malloc(sizeof(char) * strlen(&s[j]) + 1);
strcpy(res[i], &s[j]);
j += strlen(&s[j]) + 1;
i++;
}
res[i] = NULL;
return (res);
}
LLDBでチェックするためにbreakpoint set --file split.c --line 29
を叩いて、いつものrun
をしましょう。
ブレークポイント・コマンド
では、whileループの一回ごとにどのように、res[i]
が変化するか、見ていきたいをチェックしたいのですが、毎回、print
するのも面倒なので、ブレークポイントをヒットすると自動的にコマンドを実行するようにしたいと思います。
breakpoint set --command "print res[i]" --line 35
と打ってrun
して、また、ブレークポイントにヒットしたらcontinue
を打って実行してみてください
(lldb) breakpoint set --command "print res[i]" --line 35
Breakpoint 3: where = a.out`ft_split_by_space + 200 at split.c:35:4, address = 0x0000000100003e00
(lldb) run
(lldb) print res[i]
(char *) 0x0000000128e04120 "hello"
Process 41497 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 3.1
frame #0: 0x0000000100003e00 a.out`ft_split_by_space(s="hello") at split.c:35:4
32 res[i] = malloc(sizeof(char) * strlen(&s[j]) + 1);
33 strcpy(res[i], &s[j]);
34 j += strlen(&s[j]);
-> 35 i++;
36 }
37 res[i] = NULL;
38 return (res);
(lldb) continue
Process 41497 resuming
(lldb) print res[i]
(char *) 0x0000000129904080 ""
Process 41497 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 3.1
frame #0: 0x0000000100003e00 a.out`ft_split_by_space(s="hello") at split.c:35:4
32 res[i] = malloc(sizeof(char) * strlen(&s[j]) + 1);
33 strcpy(res[i], &s[j]);
34 j += strlen(&s[j]);
-> 35 i++;
36 }
37 res[i] = NULL;
38 return (res);
(lldb)
2回目のループで、空文字になっているようです。
では、strcpyでコピーされているはずの&s[j]
の値を見てみましょう。
print &s[j]
と打ち込んでみると、
(lldb) print &s[j]
(char *) 0x0000000128e055d5 ""
と、空のようです。では、j
の値をprint j
で調べてみると
(lldb) print j
(int) 5
hello'\0'world'\0'from ...
のゼロ起算の5番目を示しているということは、helloとworldの間をさししめしているようです。
試しにprint &s[j + 1]
と 一つ進めてプリントしてみる、"world"の文字列が認識できます。
(lldb) print &s[j + 1]
(char *) 0x00000001236055a6 "world"
このことから、34行目のj += strlen(&s[j]);
がstrlen + ヌル文字分を加算する必要があるとわかるので、コードを変更しましょう。
while (i < number_of_spaces)
{
res[i] = malloc(sizeof(char) * strlen(&s[j]) + 1);
strcpy(res[i], &s[j]);
- j += strlen(&s[j]);
+ j += strlen(&s[j]) + 1;
i++;
}
res[i] = NULL;
再度、cc -g split.c && ./a.out
を叩いてみると。
$ cc -g split.c
$ ./a.out
[hello]
[world]
[from]
[42]
42まで印字されていますが、TOKYOがでてきません。。。
再度、一旦、breakpoint delelte --force
ですべてのブレークポイントを消しましょう。
そして、ft_split_by_spaceに戻るため、breakpoint set --name ft_split_by_space
で再度ブレークポイントを設定。そしてrun
で新しくコンパイルされた実行ファイルを開始。
一回限りのブレークポイント
まずは、38行目でres
のリターン値を確かめましょう。tbreak 38
でテンポラリーブレークポイントを作成しcontinue
してそこまで進んでみます。
(lldb) tbreak 38
(lldb) continue
frame #0: 0x0000000100003e20 a.out`ft_split_by_space(s="hello") at split.c:38:10
35 i++;
36 }
37 res[i] = NULL;
-> 38 return (res);
39 }
40
41 int main(void)
配列のデータをプリント
2次元配列のres
の値を調べたいのですが、print res
とすると(char **) 0x0000000102d009d0
と有益な情報が出てきません。print *res
とすると(char *) 0x0000000128e04120 "hello"
と最初の文字列データは見えますが、全部は見えません。
36 }
37 res[i] = NULL;
-> 38 return (res);
(lldb) print res
(char **) 0x0000000102d009d0
(lldb) p *res
(char *) 0x0000000128e04120 "hello"
そこで、parray
コマンドを使ってみましょう。parray 10 res
と打ち込むと配列全部のデータが見えます。LLDBは配列がいつまで続くかのデータがないので、人間が指定する必要があります。今回は、10個分のデータと指定しています。
(lldb) parray 10 res
(char **) $1 = 0x0000000102d009d0 {
[0] = 0x0000000102c006f0 "hello"
[1] = 0x0000000102c006d0 "world"
[2] = 0x0000000102c006b0 "from"
[3] = 0x0000000102c00690 "42"
[4] = 0x0000000000000000
[5] = 0xbebebebebebebebe ""
[6] = 0x0000002f00001103 ""
[7] = 0x000000000000007e ""
[8] = 0x000000000000008d ""
[9] = 0xffffffffffffffff <no value available>
}
やはり、最後の要素のTokyo
が2次元配列に格納されていないので、この行上のwhile loop
に問題があることがわかります。
ループ内での変数値の確認
再度、run
してwhile loopを調べましょう。今度は、strcpy
でコピー後の状態を見たいのでbreakpoint set --line 34 -command "print res[i]"
でcontinue
を打っていくと、出力値が42
の後でループを出ているのが確認できます。
(lldb) print res[i]
(char *) 0x0000000102c006f0 "hello"
frame #0: 0x0000000100003918 a.out`ft_split_by_space(s="hello") at split.c:34:16
31 {
32 res[i] = malloc(sizeof(char) * (strlen(&s[j]) + 1));//!malloc added
33 strcpy(res[i], &s[j]);
-> 34 j += strlen(&s[j]) + 1;//! +1 added
35 i++;
36 }
37 res[i] = NULL;
(lldb) continue
(lldb) print res[i]
(char *) 0x0000000102c006d0 "world"
frame #0: 0x0000000100003918 a.out`ft_split_by_space(s="hello") at split.c:34:16
31 {
32 res[i] = malloc(sizeof(char) * (strlen(&s[j]) + 1));//!malloc added
33 strcpy(res[i], &s[j]);
-> 34 j += strlen(&s[j]) + 1;//! +1 added
35 i++;
36 }
37 res[i] = NULL;
(lldb) continue
(lldb) print res[i]
(char *) 0x0000000102c006b0 "from"
frame #0: 0x0000000100003918 a.out`ft_split_by_space(s="hello") at split.c:34:16
31 {
32 res[i] = malloc(sizeof(char) * (strlen(&s[j]) + 1));//!malloc added
33 strcpy(res[i], &s[j]);
-> 34 j += strlen(&s[j]) + 1;//! +1 added
35 i++;
36 }
37 res[i] = NULL;
(lldb) continue
(lldb) print res[i]
(char *) 0x0000000102c00690 "42"
Process 25431 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 13.1 14.1
frame #0: 0x0000000100003918 a.out`ft_split_by_space(s="hello") at split.c:34:16
31 {
32 res[i] = malloc(sizeof(char) * (strlen(&s[j]) + 1));//!malloc added
33 strcpy(res[i], &s[j]);
-> 34 j += strlen(&s[j]) + 1;//! +1 added
35 i++;
36 }
37 res[i] = NULL;
このことから、while loopの条件が一つ足りていないことがわかります。
よくよく見てみると< number_of_spaces
となっています。
while (i < number_of_spaces)
{
res[i] = malloc(sizeof(char) * strlen(&s[j]) + 1);
文字と文字の間に挟まれるスペースは文字数よりも一つ少ないことから、+1しなければいけないことに気づきます。< number_of_spaces + 1
にコードを変えてみましょう。
char **ft_split_by_space(char *s)
{
char **res;
int i = 0;
int j = 0;
int const number_of_spaces = ft_utils(s);
res = malloc(sizeof(char *) * (number_of_spaces + 2));
- while (i < number_of_spaces)
+ while (i < number_of_spaces + 1)
{
res[i] = malloc(sizeof(char) * strlen(&s[j]) + 1);
strcpy(res[i], &s[j]);
j += strlen(&s[j]) + 1;
i++;
}
res[i] = NULL;
return (res);
}
これまでの変更をSAVEしたあと、ターミナルで、cc -g split.c && ./a.out
としてみると、以下のように、出力されました。
$ cc -g split.c && ./a.out
[hello]
[world]
[from]
[42]
[tokyo]
期待された出力値になっているようです!お疲れ様でした!
LLDBの終了
今回のバグは直せたようですので、LLDBを終了させましょう。LLDBのコンソールでquit
を打ってy
で終了です。
(lldb) quit
Quitting LLDB will kill one or more processes. Do you really want to proceed: [Y/n] y
以上となりますが、長々とお付き合いいただきありがとうございました。改善点や問題点がありましたら、どうぞ、教えて下さいませ。では、皆様、メリークリスマス&良いお年を!
参考文献
Norman Matloff・Peter Salzman著、相川愛三訳(2009)「実践デバッグ技法」オライリー・ジャパン