C言語で実践LLDBコマンドライン・デバッグ入門

# まえがき
こんにちは。42 Tokyo というパリ発のエンジニア養成機関の[2023年度アドベントカレンダー](https://qiita.com/advent-calendar/2023/42tokyo)１４日目を担当します、在校生の mogawa と申します。

論理的設計能力＆脳内デバッグ力の欠如から、コードにバグがあると、`printf()`がコードのいたるところにばらまかれる状態に陥っていたので、42tokyo校舎ですぐ使える`LLDB`というデバッガーの力を借りてみたく、少し勉強してみました。

# 実践LLDBデバッグ
今回は簡単なサンプルコードを利用して、コマンドラインでLLDBのデバッガーを実際に動かして初歩的なデバッグの流れを見てみたいと思います。

:::note info
今回の記事においてのコードは、`xcode command line tools`が導入されているMacOS上での実行を前提としています。
また、LLDBのコマンドの詳細には触れませんので、その場合には[オフィシャルページ](https://lldb.llvm.org/)や[こちらのページ](https://zenn.dev/hosu/articles/df1698424d780b)をご参照ください。
:::

## サンプルプロジェクトの内容
Cの文字列（`char *`）を`スペース`により分割して２次元配列(`char **`)に格納して返す劣化版 **split関数**の実装を目指します。例題には、`hello world from 42 tokyo`をインプットし、`[hello][world][from][42][tokyo]`と`char **` で返ってくることが期待される結果です。なお、エラーハンドリングやfreeなどは全部無視しています！

スターターコードは以下です。また、[GITHUB](https://github.com/masaruo/lldb_tutorial)にも上げておきました。お時間ありましたら、是非、自分の環境でも一緒にやってみてください！

```shell:SHELL
git clone git@github.com:masaruo/lldb_tutorial.git
```

<details><summary>split by spaceのスターターコード</summary>

```c:split.c

#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);
}
```
</details>

## デバッグの端緒

まずは、`cc split.c && ./a.out`で、コンパイルして、実行してみましょう。
おっと、`bus error`が出ており、実行できないようです。

```shell:shell
$ 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を走らせます。

```llvm: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`ファイルの`１５行目`でエラーが発生しているようです。ここでは、文字列の中にスペースがあったらそれをヌル文字に変更しているだけで、文字列領域も確保されているはず・・・。では、大元の呼び出し元まで辿りってみると、main内で`ft_split_by_space("hello world from 42 tokyo");`と静的領域に確保した文字列リテラルを変更しようとしていることから、怒られているのがわかります。今回は`strdup関数`によってコピーして、文字列を渡すように変更しましょう。

```diff_c:split.c
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`を押します。

```llvm:lldb
(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`を入力。

```llvm:lldb
   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])
```
メイン関数に入ってすぐ、矢印のところで実行が止まっているのがわかります

:::note info
矢印の行は、**まだ**実行されていません
:::

`next`で`ft_split_by_space`まで行き、再度`next`で当該関数を実行させてみると、`memmove`の同じエラーが発生しました。`ft_split_by_space`の中でエラーがあるようです。

## ステップイン実行
では、今度は問題を起こしている関数の中に入ってみましょう。再度、`run`して`next`すると`ft_split_by_space`に矢印がついていると思います。ここで、今度は、`step`
```llvm:lldb
   42           char    **res;
   43           int             i = 0;
   44  
-> 45           res = ft_split_by_space(strdup("hello world from 42 tokyo"));
```
そうすると、画面に`ft_split_by_space`のコードが現れ、対象関数内に入れたことがわかります。
```llvm:lldb
(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`を実行すると怒られるようです。
```llvm:LLDB
* 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に打ち込んでください。そうすると、これまでの呼び出しの履歴が見えます。
```llvm: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`が呼び出されたところまで戻れました。
```llvm:LLDB
(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]`と入力して見ましょう。
```llvm:LLDB
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を行わず文字列の領域を確保していなかったので、文字列分の領域を確保するようにコードを変更しましょう。

```diff_c:split.c
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`と打って、全部削除してみましょう。

:::note info
LLDBのコマンドが長くてうんざりしている場合には、ショートフォームもあるので、公式サイトなど見ながら探してみてください。例えば、今回の`breakpoint delete --force`は`br del -f`でもいけます。また、aliasの設定もできます。
:::


 ## 直して、コンパイルして、再度チェックのループ
 さて、コードを改変しましたので、再度コンパイルが必要です。terminalで`cc -g split.c`して、`./a.out`を叩いてください。
 ```bash:bash
$ ./a.out
[hello]
[]
[]
[]
```
 今度は以下の問題がみつかりました。
 1. helloまでましたが、それ以降が出ていません。
 1. []の枠が４個分しかない。

例文は`hello world from 42 tokyo`なので本来なら５個の枠が必要なはず。
枠の確保は`ft_split_by_space`の２９行目`malloc(sizeof(char *) * (number_of_spaces + 2));`なので、
```c:split.c
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`を打って実行してみてください

```llvm:LLDB
(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]`と打ち込んでみると、
```llvm:LLDB
(lldb) print &s[j]
(char *) 0x0000000128e055d5 ""
```
と、空のようです。では、`j`の値を`print j`で調べてみると
```llvm:LLDB
(lldb) print j
(int) 5
```
`hello'\0'world'\0'from ...`のゼロ起算の５番目を示しているということは、helloとworldの間をさししめしているようです。
試しに`print &s[j + 1]`と **一つ進めて**プリントしてみる、"world"の文字列が認識できます。

```llvm:LLDB
(lldb) print &s[j + 1]
(char *) 0x00000001236055a6 "world"
```

このことから、３４行目の`j += strlen(&s[j]);`がstrlen + **ヌル文字分**を加算する必要があるとわかるので、コードを変更しましょう。
```diff_c:split.c
	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`を叩いてみると。
```shell:shell
$ cc -g split.c
$ ./a.out
[hello]
[world]
[from]
[42]
```
４２まで印字されていますが、TOKYOがでてきません。。。
再度、一旦、`breakpoint delelte --force`ですべてのブレークポイントを消しましょう。
そして、ft_split_by_spaceに戻るため、`breakpoint set --name ft_split_by_space`で再度ブレークポイントを設定。そして`run`で新しくコンパイルされた実行ファイルを開始。

## 一回限りのブレークポイント
まずは、38行目で`res`のリターン値を確かめましょう。`tbreak　３８`でテンポラリーブレークポイントを作成し`continue`してそこまで進んでみます。

```llvm:LLDB
(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)
```

## 配列のデータをプリント
２次元配列の`res`の値を調べたいのですが、`print res`とすると`(char **) 0x0000000102d009d0`と有益な情報が出てきません。`print *res`とすると`(char *) 0x0000000128e04120 "hello"`と最初の文字列データは見えますが、全部は見えません。

```LLVM:LLDB
   36           }
   37           res[i] = NULL;
-> 38           return (res);

(lldb) print res
(char **) 0x0000000102d009d0
(lldb) p *res
(char *) 0x0000000128e04120 "hello"
```
そこで、`parray`コマンドを使ってみましょう。`parray 10 res`と打ち込むと配列全部のデータが見えます。LLDBは配列がいつまで続くかのデータがないので、人間が指定する必要があります。今回は、１０個分のデータと指定しています。
```llvm:LLDB
(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`が２次元配列に格納されていないので、この行上の`while loop`に問題があることがわかります。

## ループ内での変数値の確認
再度、`run`してwhile loopを調べましょう。今度は、`strcpy`でコピー後の状態を見たいので`breakpoint set --line 34 -command "print res[i]"`で`continue`を打っていくと、出力値が`４２`の後でループを出ているのが確認できます。

```llvm:LLDB
(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`となっています。
```c:split.c
	while (i < number_of_spaces)
	{
		res[i] = malloc(sizeof(char) * strlen(&s[j]) + 1);
```
文字と文字の間に挟まれるスペースは文字数よりも一つ少ないことから、＋１しなければいけないことに気づきます。`< number_of_spaces + 1`にコードを変えてみましょう。
```diff_c:split.c
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`としてみると、以下のように、出力されました。
```shell:shell
$ cc -g split.c && ./a.out
[hello]
[world]
[from]
[42]
[tokyo]
```
期待された出力値になっているようです！お疲れ様でした！
## LLDBの終了
今回のバグは直せたようですので、LLDBを終了させましょう。LLDBのコンソールで`quit`を打って`y`で終了です。
```llvm:LLDB
(lldb) quit
Quitting LLDB will kill one or more processes. Do you really want to proceed: [Y/n] y
```
以上となりますが、長々とお付き合いいただきありがとうございました。改善点や問題点がありましたら、どうぞ、教えて下さいませ。では、皆様、メリークリスマス＆良いお年を！

## 参考文献
Norman Matloff・Peter Salzman著、相川愛三訳（２００９）「[実践デバッグ技法](https://www.oreilly.co.jp/books/9784873114064/)」オライリー・ジャパン