3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

42 TokyoAdvent Calendar 2023

Day 14

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

Last updated at Posted at 2023-12-14

まえがき

こんにちは。42 Tokyo というパリ発のエンジニア養成機関の2023年度アドベントカレンダー14日目を担当します、在校生の mogawa と申します。

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

実践LLDBデバッグ

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

今回の記事においてのコードは、xcode command line toolsが導入されているMacOS上での実行を前提としています。
また、LLDBのコマンドの詳細には触れませんので、その場合にはオフィシャルページこちらのページをご参照ください。

サンプルプロジェクトの内容

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

スターターコードは以下です。また、GITHUBにも上げておきました。お時間ありましたら、是非、自分の環境でも一緒にやってみてください!

SHELL
git clone git@github.com:masaruo/lldb_tutorial.git
split by spaceのスターターコード
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);
}

デバッグの端緒

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

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を走らせます。

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関数によってコピーして、文字列を渡すように変更しましょう。

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を押します。

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を入力。

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])

メイン関数に入ってすぐ、矢印のところで実行が止まっているのがわかります

矢印の行は、まだ実行されていません

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

ステップイン実行

では、今度は問題を起こしている関数の中に入ってみましょう。再度、runしてnextするとft_split_by_spaceに矢印がついていると思います。ここで、今度は、step

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のコードが現れ、対象関数内に入れたことがわかります。

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を実行すると怒られるようです。

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に打ち込んでください。そうすると、これまでの呼び出しの履歴が見えます。

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
(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]と入力して見ましょう。

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を行わず文字列の領域を確保していなかったので、文字列分の領域を確保するようにコードを変更しましょう。

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と打って、全部削除してみましょう。

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

直して、コンパイルして、再度チェックのループ

さて、コードを改変しましたので、再度コンパイルが必要です。terminalでcc -g split.cして、./a.outを叩いてください。

bash
$ ./a.out
[hello]
[]
[]
[]

今度は以下の問題がみつかりました。

  1. helloまでましたが、それ以降が出ていません。
  2. []の枠が4個分しかない。

例文はhello world from 42 tokyoなので本来なら5個の枠が必要なはず。
枠の確保はft_split_by_spaceの29行目malloc(sizeof(char *) * (number_of_spaces + 2));なので、

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を打って実行してみてください

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]と打ち込んでみると、

LLDB
(lldb) print &s[j]
(char *) 0x0000000128e055d5 ""

と、空のようです。では、jの値をprint jで調べてみると

LLDB
(lldb) print j
(int) 5

hello'\0'world'\0'from ...のゼロ起算の5番目を示しているということは、helloとworldの間をさししめしているようです。
試しにprint &s[j + 1]一つ進めてプリントしてみる、"world"の文字列が認識できます。

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

このことから、34行目のj += strlen(&s[j]);がstrlen + ヌル文字分を加算する必要があるとわかるので、コードを変更しましょう。

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
$ 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
(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"と最初の文字列データは見えますが、全部は見えません。

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は配列がいつまで続くかのデータがないので、人間が指定する必要があります。今回は、10個分のデータと指定しています。

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が2次元配列に格納されていないので、この行上のwhile loopに問題があることがわかります。

ループ内での変数値の確認

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

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となっています。

split.c
	while (i < number_of_spaces)
	{
		res[i] = malloc(sizeof(char) * strlen(&s[j]) + 1);

文字と文字の間に挟まれるスペースは文字数よりも一つ少ないことから、+1しなければいけないことに気づきます。< number_of_spaces + 1にコードを変えてみましょう。

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
$ cc -g split.c && ./a.out
[hello]
[world]
[from]
[42]
[tokyo]

期待された出力値になっているようです!お疲れ様でした!

LLDBの終了

今回のバグは直せたようですので、LLDBを終了させましょう。LLDBのコンソールでquitを打ってyで終了です。

LLDB
(lldb) quit
Quitting LLDB will kill one or more processes. Do you really want to proceed: [Y/n] y

以上となりますが、長々とお付き合いいただきありがとうございました。改善点や問題点がありましたら、どうぞ、教えて下さいませ。では、皆様、メリークリスマス&良いお年を!

参考文献

Norman Matloff・Peter Salzman著、相川愛三訳(2009)「実践デバッグ技法」オライリー・ジャパン

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?