デバッガgdbの使い方
デバッガの概要
なぜデバッガ?
gdbなどのデバッガの使用をお薦めする理由は「プログラムのデバッグ」をとても楽にしてくれるからです!!- デバッグが難しいのは実行中のプログラムの中身(実行状態)が外からでは見えにくいからです.
printfを埋め込むことでも変数の値や実行パスを調べられますが, デバッガを使うともっと効率的に調べることができます. - デバッガは「簡単に習得できて,効果も高いお得な開発ツール」です. 慣れることが大事です.
- デバッガはつまみ食いOKです. 最初は「自分が使いたい機能,使える機能」だけを使えばいいのです. デバッガを使うために「デバッガの全て」を学ぶ必要はありません.
デバッガとは
デバッガは主に以下の機能を組み合わせて,プログラムの実行状態を調べることで, バグの原因を探します.
- ① プログラム実行の一時停止: 「実行を止めたい場所(ブレークポイント)」や止めたい条件を設定できます. ブレークポイントには関数名や行番号やアドレスなどを指定できます.
- ② ステップ実行: ①でプログラムの実行を一時停止した後, ステップ実行の機能を使って,ちょっとずつ実行を進めます.
- ③ 実行状態の表示: 変数の値,現在の行番号,スタックトレース(バックトレース)などを表示できます.
- ④ 実行状態の変更: 変数に別の値を代入したり,関数を呼び出したりして, 「ここでこう実行したら」を試せます.
gdbとは
- Linux上で使える代表的で高性能なデバッガです.
- C/C++/アセンブリ言語/Objective-C/Rustなど,多くの言語をサポートしています.
- Linux/x86-64を含む,多くのOSやプロセッサに対応しています.
- オープンソースで無料で使えます (GNU GPLライセンス).
- 一次情報: GDB: The GNU Project Debugger
gdbの実行例 (C言語編)
起動 run と終了 quit
// hello.c
#include <stdio.h>
int main ()
{
printf ("hello\n");
}
$ gcc ❶ -g hello.c
$ ./a.out
hello
$ ❷ gdb ./a.out
❸(gdb) ❹ run
hello
(gdb) ❺ quit
A debugging session is active.
Inferior 1 [process 20186] will be killed.
Quit anyway? (y or n) ❻ y
$
gdbでデバッグする前に,gccのコンパイルに❶-gオプションを付けます.-gはa.outにデバッグ情報を付加します. デバッグ情報がなくてもデバッグは可能ですが, ファイル名や行番号などの情報が表示されなくなり不便です.
gcc -g -Og オプションがベスト
gdbのマニュアルに以下の記述があります.
-O2などの最適化オプションを付けてコンパイルしたプログラムでもgdbで デバッグできるが,行番号がずれたりする.なので可能なら 最適化オプションを付けない方が良い.gdbでデバッグするベストな最適化オプションは-Ogであり,-Ogは-O0よりも良い.
ですので,デバッグ時には gcc -g -Ogオプションがベストなようです.
Inferior はデバッグ対象のプログラムのこと
上の実行例中のInferior 1 [process 20186] will be killed.は
「gdbの終了に伴って,デバッグ対象のプログラムも実行終了させます」という
ことを意味しています.
inferiorは「下位の」「劣った」という意味ですね
(劣等感は英語で inferiority complex).
gdbのマニュアルでもデバッグ対象のプログラムを一貫してinferiorと呼んでいます.
なお他の文献ではデバッグ対象のプログラムのことをデバッギ (debuggee)と呼ぶことがあります.
- ❷
gdb ./a.outと,引数にデバッグ対象のバイナリ(ここでは./a.out)を指定してgdbを起動します.
gdb起動メッセージの抑制
デフォルトでgdbを起動すると以下のような長い起動メッセージが出ます.
$ gdb ./a.out
GNU gdb (Ubuntu 12.1-0ubuntu1~22.04) 12.1
Copyright (C) 2022 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from ./a.out...
(gdb)
これを抑制するには,gdb -qオプションを付けるか,
~/.gdbearlyinitファイルに以下を指定します.
set startup-quietly on
- ❸
(gdb)はgdbのプロンプトです.gdbのコマンドが入力可能なことを示します. - ❹
runはgdb上でプログラムの実行を開始します. ここではブレークポイントを指定していないため,そのままhelloを出力して プログラムは終了しました. - ❺
quitはgdbを終了させます. (ここではすでにデバッグ対象のプログラムの実行は終了していますが) デバッグ対象のプログラムが終了しておらず, 「本当に終了して良いか?」と聞かれたら,❻yと答えて終了させます.
コマンドの省略名
gdbのコマンドは(区別できる範囲で)短く省略できます.
例えば,runはr,quitはq,それぞれ1文字でコマンドを指定できます.
慣れてきたらコマンドの省略名を使いましょう.
コマンドライン引数argvを指定して実行
// argv.c
#include <stdio.h>
int main (int argc, char *argv[])
{
for (int i = 0; i < argc; i++) {
printf ("argv[%d]=%s\n", i, argv [i]);
}
}
$ gcc -g argv.c
$ gdb ./a.out
Reading symbols from ./a.out...
(gdb) ❶ run a b c d
argv[0]=/mnt/hgfs/gondow/project/linux-x86-64-programming/src/asm/a.out
argv[1]=a
argv[2]=b
argv[3]=c
argv[4]=d
[Inferior 1 (process 20303) exited normally]
(gdb)
- コマンドライン引数を与えてデバッグしたい場合は,
runコマンドに続けて引数を与えます(ここではa b c d).
標準入出力を切り替えて実行
// cat.c
#include <stdio.h>
int main ()
{
int c;
while ((c = getchar ()) != EOF) {
putchar (c);
}
}
$ gcc -g cat.c
$ cat foo.txt
hello
byebye
$ gdb ./a.out
(gdb) run ❶ < foo.txt ❷ > out.txt
(gdb) quit
$ cat out.txt
hello
byebye
- 標準入出力をリダイレクトして(切り替えて)実行したい場合は,
通常のシェルのときと同様に
runコマンドの後で, ❶<や❷>を使って行います.
segmentation fault (あるいは bus error)の原因を探る
// segv.c
#include <stdio.h>
int main ()
{
int *p = (int *)0xDEADBEEF; // アクセスNGそうなアドレスを代入
printf ("%d\n", *p);
}
$ gcc -g segv.c
$ ./a.out
❶ Segmentation fault (core dumped)
$ gdb ./a.out
(gdb) r
Program received signal SIGSEGV, ❷ Segmentation fault.
0x0000555555555162 in ❸ main () at ❹ segv.c:6
6 ❺ printf ("%d\n", *p);
(gdb) ❻ print/x p
$1 = ❼ 0xdeadbeef
(gdb) ❽ print/x *p
❾ Cannot access memory at address 0xdeadbeef
(gdb) quit
segv.cをコンパイルして実行すると❶ segmentation fault が起きました. segumentation fault や bus error は正しくないポインタを使用して メモリにアクセスすると発生します.gdb上でa.outを実行すると,gdb上でも ❷ segmentation fault が起きました. 発生場所は ❹ ファイルsegv.cの6行目,❸main関数内と表示されています. また,6行目のソースコード ❺printf ("%d\n", *p);も表示されています.- 変数
pが怪しいので,❻print/x pコマンドで変数pの値を表示させます./xは「16進数で表示」を指示するオプションです. 怪しそうな0xDEADBEEFという値が表示されました. (printコマンドはpと省略可能です).
怪しいアドレスとは
まず,8の倍数ではないアドレスは怪しいです(正しいアドレスのこともあります). 特に奇数のアドレスは怪しいです(正しいこともありますが). アラインメント制約を守るため, 多くのデータが4の倍数や8の倍数のアドレスに配置されるからです.
また慣れてくると,例えば「0x7ffde9a98000はスタックのアドレスっぽい」と
感じるようになります.
「これ,どこのメモリだろう」と思ったら
メモリマップ
やgdb上でinfo proc mapの結果を見て調べるのが良いです.
- 念のため,
printコマンドで*pを表示させると (❽print/x *p), この番地にはアクセスできないことが確認できました (❾Cannot access memory at address 0xdeadbeef).
変数の値を表示 (print)
// calcx.c
int main ()
{
int x = 10;
x += 3;
x += 4;
return x;
}
$ gcc -g calcx.c
$ gdb ./a.out
(gdb) ❶ b main
Breakpoint 1 at 0x1131: file calcx.c, line 3.
(gdb) ❷ r
❸ Breakpoint 1, main () at calcx.c:3
3 ❹ int x = 10;
(gdb) ❺ s
❻ 4 x += 3;
(gdb) ❼ p x
❽ $1 = 10
(gdb) s
5 x += 4;
(gdb) p x
$2 = 13
(gdb) q
- 使った短縮コマンド:
b(break),r(run),s(step),p(print),q(quit) - ❶
b mainで,main関数にブレークポイントを設定し, ❷rで実行を開始すると,❸main関数で実行が一時停止しました. ❹int x = 10;`は次に実行する文です(まだ実行していません).
breakで設定できる場所
ここではb mainと関数名を指定しました.
他にも以下のように行番号やファイル名も使えます.
| 場所の指定 | 説明 |
|---|---|
b 10 | (今実行中のファイルの)10行目 |
b +5 | 今の実行地点から5行後 |
b -5 | 今の実行地点から5行前 |
b main | (今実行中のファイルの)関数main |
b main.c:main | ファイルmain.c中のmain関数 |
b main.c:10 | ファイルmain.cの10行目 |
- ❺
sで,1行だけ実行を進めます. 4行目 (❻4 x += 3;)を実行する手前で実行が止まります. - ここで
❼ p xとしてxの値を表示させます.❽ $1 = 10と表示され,xの値は10と分かりました. ($1はgdb中で使える変数ですが,ここでは使っていません).
変数の値を自動表示 (display)
// calcx.c
int main ()
{
int x = 10;
x += 3;
x += 4;
return x;
}
$ gcc -g calcx.c
$ gdb ./a.out
(gdb) b main
Breakpoint 1 at 0x1131: file calcx.c, line 4.
(gdb) r
Breakpoint 1, main () at calcx.c:4
4 int x = 10;
(gdb) ❶ disp x
❷ 1: x = 21845
(gdb) s
5 x += 3;
❸ 1: x = 10
(gdb) s
6 x += 4;
❹ 1: x = 13
(gdb) s
7 return x;
❺ 1: x = 17
(gdb) q
- 使った短縮コマンド:
b(break),r(run),s(step),disp(display),q(quit) - 「何度も
p xと入力するのが面倒」という人はdisplayを使って, 変数の値を自動表示させましょう.displayは実行が停止するたびに, 指定した変数の値を表示します. - ここでは❶
disp xとして,変数xの値を自動表示させます. (❷1: x = 21845と出てるのは,変数xが未初期化のため,ゴミの値が表示されたからです). sで1行ずつ実行を進めるたびに, 変数xの値が,❸1: x = 10→ ❹1: x = 13→ ❺1: x = 17と変化するのが分かります.
条件付きブレークポイントの設定とバックトレース表示 (break if, backtrace)
// fact.c
#include <stdio.h>
int fact (int n)
{
if (n <= 0)
return 1;
else
return n * fact (n - 1);
}
int main ()
{
printf ("%d\n", fact (5));
}
$ gcc -g fact.c
$ gdb ./a.out
(gdb) ❶ b fact if n==0
Breakpoint 1 at 0x1158: file fact.c, line 5.
(gdb) ❷ r
❸ Breakpoint 1, fact (n=0) at fact.c:5
5 if (n <= 0)
(gdb) ❹ bt
#0 fact (n=0) at fact.c:5
#1 0x0000555555555172 in fact (n=1) at fact.c:8
#2 0x0000555555555172 in fact (n=2) at fact.c:8
#3 0x0000555555555172 in fact (n=3) at fact.c:8
#4 0x0000555555555172 in fact (n=4) at fact.c:8
#5 0x0000555555555172 in fact (n=5) at fact.c:8
❺#6 0x000055555555518a in main () at fact.c:13
(gdb) q
- 使った短縮コマンド:
b(break),r(run),bt(backtrace),q(quit) - 階乗を計算する
factは再帰的に何度も呼び出されますが, ここでは「引数nの値が0のときだけブレークしたい」とします. - ❶
b fact if n==0で,引数nが0の時だけfactの実行を停止する設定をして, ❷rで実行を開始すると,意図通り ❸fact (n=0)で実行停止できました. - ここで,❹
btとしてバックトレースを表示させます. バックトレースとは「今,実行中の関数から遡ってmain関数に至るまでの 関数呼び出し系列」のことです. ❺main関数から,fact(n=5)→fact(n=4)→(中略) →fact(n=0)と呼び出されたことが分かります. - なお,
backtrace fullとすると, バックトレースに加えて,局所変数の値も表示されます.
注: Ubuntu 20.04 LTSなど,少し古いLinuxを使っている人は バックトレース中の引数の値が間違った表示 になることがあります(私はなりました). これは古い
gdbがendbr64命令に非対応だったからです. Ubuntu 22.04 LTSなど最新のLinuxにすることをお勧めします (2023年8月現在).
変数や式の変更監視 (watch)
// calcx.c
int main ()
{
int x = 10;
x += 3;
x += 4;
return x;
}
$ gcc -g calcx.c
$ gdb ./a.out
(gdb) b main
Breakpoint 1 at 0x1131: file calcx.c, line 4.
(gdb) r
Breakpoint 1, main () at calcx.c:4
4 int x = 10;
(gdb) ❶ wa x
Hardware watchpoint 2: x
(gdb) ❷ c
Continuing.
Hardware watchpoint 2: x
❸ Old value = 21845
❹ New value = 10
main () at calcx.c:5
5 x += 3;
(gdb) c
Continuing.
Hardware watchpoint 2: x
Old value = 10
New value = 13
main () at calcx.c:6
6 x += 4;
(gdb) c
Continuing.
Hardware watchpoint 2: x
Old value = 13
New value = 17
main () at calcx.c:7
7 return x;
(gdb) q
-
使った短縮コマンド:
b(break),r(run),wa(watch),c(continue),q(quit) -
watchは指定した変数や式の変化(書き込み)を監視します. 「どこで値が変わるのかわからない」という場合に便利です. ここでは ❶wa xとして変数xを監視する設定を行い, 実行を再開します (❷c). 変更箇所で自動的にブレークされて, 変更前後の値が表示されました(❸Old value = 21845,❹New value = 10). -
breakと同様に,watchにもifで条件を指定できます. 例えば,wa x if x==13とすると,変数の値が13になった時点でブレークできます. -
watchはハードウェア機能を使うため, 高速ですが指定できる個数に限りがあります. -
watchには-lというオプションを指定可能です. このオプションを指定すると,指定した変数や式の左辺値(つまりアドレス)を計算して, そのアドレスへの書き込みを(変数のスコープを無視して)監視します. 計算結果がアドレスでなかった場合(つまり左辺値を持たない式だった場合)はgdbはエラーを表示します.例えば,
staticなしの局所変数(自動変数)に対してwatchを指定すると, この局所変数のスコープから出る際に,以下のメッセージが出て ウォッチポイントは削除されてしまいます.-lオプションをつけると削除されません.Watchpoint 2 deleted because the program has left the block in which its expression is valid. -
watchは「書き込み」を監視します. 「読み込み」を監視したい時はrwatch, 「読み書き」の両方を監視したい時はawatchを使って下さい.
実行中断と,実行途中での変数の値の変更
// inf-loop.c
#include <stdio.h>
int main ()
{
int x = 1, n = 0;
while (x != 0) {
n++;
}
printf ("hello, world\n");
}
$ gcc -g inf-loop.c
$ gdb ./a.out
(gdb) r
❶ ^C
❷ Program received signal SIGINT, Interrupt.
main () at inf-loop.c:7
7 while (x != 0) {
(gdb) ❸ p x=0
$1 = 0
(gdb) ❹ c
Continuing.
❺ hello, world
(gdb) q
- 使った短縮コマンド:
r(run),p(print),c(continue),q(quit) - このプログラムは無限ループがあるため,実行を開始すると
gdbに制御が戻ってきません.そこで,ctrl-c(❶^C)を入力して プログラムを一時停止します. - 変数
xの値をゼロにすれば無限ループを抜けるので,printコマンドで ❸p x=0とすることで,変数xにゼロを代入します. このようにprintコマンドは変数を変更したり, 副作用のある関数を呼び出すことができます(例えば,p printf("hello\n")として). - 実行を再開すると (❹
c),❺hello, worldが表示され, 無事に無限ループを抜けることができました.
再開場所の変更 (jump)
// inf-loop2.c
#include <stdio.h>
#include <time.h>
int main ()
{
int n = 0;
while (time (NULL)) {
n++;
}
printf ("hello, world\n");
}
$ gcc -g inf-loop2.c
$ gdb ./a.out
(gdb) r
^C
Program received signal SIGINT, Interrupt.
main () at inf-loop2.c:8
8 n++;
(gdb) ❶ l
3 #include <time.h>
4 int main ()
5 {
6 int n = 0;
7 while (time (NULL)) {
8 n++;
9 }
❷ 10 printf ("hello, world\n");
11 }
(gdb) ❸ j 10
Continuing at 0x555555555191.
❹ hello, world
(gdb) q
- 使った短縮コマンド:
r(run),l(list),j(jump),q(quit) - 先程と異なり,今回,無限ループを抜けるのに,
単純に変数の値を変える方法は使えません.
(システムコール
timeは1970/1/1からの経過秒数を返します). そこで,ここではjumpコマンドを使います.jumpは「指定した場所から実行を再開」します. (一方,continueは「実行を一時停止した場所から実行を再開」します).
別の方法
別の方法として,timeが返した戻り値は%raxレジスタに入っているので,
timeからのリターン直後にp $rax=0とする方法もあります
(レジスタ%raxの値が0になります).
また p $rip=0x0000555555555191として,直接 %ripレジスタの値を
変更する方法もあります(jumpコマンドの中身はまさにこれでしょう).
- 何行目から実行を再開すればよいかを調べるために,
listコマンドを使ってソースコードの一部を表示します. (listに表示する行番号や関数名を指定することもできます). 10行目から再開すれば良さそうと分かります. - 10行目から実行を再開すると(❷
j 10), 無事に無限ループを抜けられました (❸hello, world).
なお,layout srcとすると,ソースコードを表示するウインドウが現れます.
ソースコードと現在の実行位置を見ながらデバッグできるので便利です.
(時々画面が乱れるので,その時はctrl-l(コントロールL)を押して,
画面を再描画して下さい).
このモードから抜けるには,tui disableあるいはctrl-x aを入力します.
型の表示 (whatis, ptype)
// struct2.c
#include <stdio.h>
#include <stddef.h> // for size_t
struct foo {
int a1;
char a2;
size_t a3;
};
int main ()
{
struct foo f = {10, 'a', 20};
printf ("%d\n", f.a1);
}
$ gcc -g struct2.c
$ gdb ./a.out
Reading symbols from ./a.out...
(gdb) b main
Breakpoint 1 at 0x1155: file struct2.c, line 12.
(gdb) r
Breakpoint 1, main () at struct2.c:12
12 struct foo f = {10, 'a', 20};
(gdb) ❶ whatis f
type = struct foo
(gdb) ❷ ptype f
type = struct foo {
int a1;
char a2;
size_t a3;
}
(gdb) ❸ ptype/o f
/* offset | size */ type = struct foo {
/* 0 | 4 */ int a1;
/* 4 | 1 */ char a2;
/* XXX 3-byte hole */
/* 8 | 8 */ size_t a3;
/* total size (bytes): 16 */
}
(gdb) ❹ ptype struct foo
type = struct foo {
int a1;
char a2;
size_t a3;
}
(gdb) ❺ whatis f.a3
type = size_t
(gdb) ❻ ptype f.a3
type = unsigned long
(gdb) ptype size_t
type = unsigned long
(gdb) ❼ info types foo
All types matching regular expression "foo":
File struct2.c:
4: struct foo;
(gdb) q
-
whatisやptypeは式や型名の型情報を表示します. -
whatisは構造体の中身を表示しませんが (❶whatis f),ptypeは表示します (❷ptype f)./oオプションを付けると,構造体のフィールドのオフセットとサイズ, 構造体中のパディング(ホール,穴)も表示してくれます (❸ptype/o f). -
whatisやptypeには型名も指定できます (❹ptype struct foo). -
whatisはtypedefを1レベルまでしか展開しませんが (❺whatis f.a3),ptypeは全て展開します (❻ptype f.a3). -
info typesを使うと,正規表現にマッチする型名一覧を表示します (❼info types foo).
gdbの実行例 (アセンブリ言語編)
アドレス指定でブレイク,レジスタの値を表示
// hello.c
#include <stdio.h>
int main ()
{
printf ("hello\n");
}
$ gcc -g hello.c
$ gdb ./a.out
(gdb) ❶ b main
Breakpoint 1 at 0x1151: file hello.c, line 5.
(gdb) r
Breakpoint 1, main () at hello.c:5
5 printf ("hello\n");
(gdb) ❷ disas
Dump of assembler code for function main:
0x0000555555555149 <+0>: endbr64
0x000055555555514d <+4>: push %rbp
0x000055555555514e <+5>: mov %rsp,%rbp
❸ => 0x0000555555555151 <+8>: lea 0xeac(%rip),%rax # 0x555555556004
0x0000555555555158 <+15>: mov %rax,%rdi
0x000055555555515b <+18>: call 0x555555555050 <puts@plt>
0x0000555555555160 <+23>: mov $0x0,%eax
0x0000555555555165 <+28>: pop %rbp
0x0000555555555166 <+29>: ret
End of assembler dump.
(gdb) ❹ b *0x0000555555555149
Breakpoint 2 at 0x555555555149: file hello.c, line 4.
(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Breakpoint 2, main () at hello.c:4
4 {
(gdb) ❺ disp/i $rip
1: x/i $rip
❻ => 0x555555555149 <main>: endbr64
(gdb) ❼ si
0x000055555555514d 4 {
❽ 1: x/i $rip
❾ => 0x55555555514d <main+4>: push %rbp
(gdb) si
0x000055555555514e 4 {
1: x/i $rip
=> 0x55555555514e <main+5>: mov %rsp,%rbp
(gdb) ❿ p/x $rbp
$1 = 0x1
(gdb) ⓫ i r
rax 0x555555555149 93824992235849
rbx 0x0 0
rcx 0x555555557dc0 93824992247232
rdx 0x7fffffffdfb8 140737488347064
rsi 0x7fffffffdfa8 140737488347048
rdi 0x1 1
rbp 0x1 0x1
rsp 0x7fffffffde90 0x7fffffffde90
(長いので中略)
(gdb) q
-
使った短縮コマンド:
b(break),r(run),disas(disassemble),si(stepi),p(print),i r(info registers),q(quit) -
まず,
main関数にブレークポイントを設定して(❶b main), 実行を開始すると実行が一時停止するのですが, 逆アセンブル (❷disas)して確かめると, 機械語命令レベルではmain関数の先頭で実行を一時停止していません.- ❸
=> 0x0000555555555151 <+8>:の<+8>が 「main関数の先頭から8バイト目」であることを示しています.gdbは関数名を指定してブレークした場合, 関数プロローグが終わった場所でブレークします. disassembleは逆アセンブル結果を表示します. 何も指定しないと実行中の場所付近の逆アセンブル結果を表示します. 関数名やアドレスを指定することも可能です. また,disassembleには以下のオプションを指定可能です.
- ❸
| オプション | 説明 |
|---|---|
/s | ソースコードも表示 (表示順は機械語命令の順番) |
/m | ソースコードも表示 (表示順はソースコードの順番) |
/r | 機械語命令の16進ダンプも表示 |
-
ここでは
main関数の先頭番地を指定してブレークポイントを設定してみます (❹b *0x0000555555555149).行番号と区別するため*が必要です. 実行を開始するとブレークしました. -
ブレークした番地と,その番地の機械語命令を表示すると便利です. そのため,❺
disp/i $ripとしました. これはプログラムカウンタ%ripの値を命令として(i, instruction)自動表示せよ, という意味です.(gdbではレジスタを指定するのに%ripではなく,$ripのようにドルマーク$を使います). これにより,❻=> 0x555555555149 <main>: endbr64が表示されました.- 次に実行する番地は
0x555555555149番地 - その番地の命令は
endbr64命令
- 次に実行する番地は
-
stepi(❼si)を使うと,1行ではなく,機械語命令を1つ実行する ステップ実行になります. ❺disp/i $ripの効果で, 次に実行される命令の番地とニモニックが表示されました (❾=> 0x55555555514d <main+4>: push %rbp). なお,❽1: x/i $ripとあるのは, ❺disp/i $ripはprintではなく,x/i $ripコマンドで機械語命令を出力するからです (xはメモリ中の値を表示するコマンドです./iはフォーマット指定で「機械語命令」(instruction)を意味します). -
レジスタの値を表示するには
printを使います (❿p/x $rbp)/xはフォーマット指定で「16進数」(hexadecimal)を意味します. 値は1でした ($1 = 0x1). -
なお,
info registers(⓫i r)で,全ての汎用レジスタの値を 一括表示できます.
メモリ中の値(機械語命令)を表示する (x)
// hello.c
#include <stdio.h>
int main ()
{
printf ("hello\n");
}
$ gdb ./a.out
Reading symbols from ./a.out...
(gdb) b main
Breakpoint 1 at 0x1151: file hello.c, line 5.
(gdb) disp/x $rip
1: /x $rip = <error: No registers.>
(gdb) r
Breakpoint 1, main () at hello.c:5
5 printf ("hello\n");
1: /x $rip = 0x555555555151
(gdb) ❶ disas/r
Dump of assembler code for function main:
0x0000555555555149 <+0>: f3 0f 1e fa endbr64
0x000055555555514d <+4>: 55 push %rbp
0x000055555555514e <+5>: 48 89 e5 mov %rsp,%rbp
❷ => 0x0000555555555151 <+8>: 48 8d 05 ac 0e 00 00 lea 0xeac(%rip),%rax # 0x555555556004
0x0000555555555158 <+15>: 48 89 c7 mov %rax,%rdi
0x000055555555515b <+18>: e8 f0 fe ff ff call 0x555555555050 <puts@plt>
0x0000555555555160 <+23>: b8 00 00 00 00 mov $0x0,%eax
0x0000555555555165 <+28>: 5d pop %rbp
0x0000555555555166 <+29>: c3 ret
End of assembler dump.
(gdb) ❸ x/7xb 0x0000555555555151
❹ 0x555555555151 <main+8>: 0x48 0x8d 0x05 0xac 0x0e 0x00 0x00
(gdb) ❺ x/7xb $rip
0x555555555151 <main+8>: 0x48 0x8d 0x05 0xac 0x0e 0x00 0x00
(gdb) ❻ x/7xb $rip+7
0x555555555158 <main+15>: 0x48 0x89 0xc7 0xe8 0xf0 0xfe 0xff
(gdb) q
-
使った短縮コマンド:
b(break),r(run),disp(display),disas(disassemble),x(x),q(quit) -
16進ダンプ付き(
/r)で逆アセンブルすると (❶disas/r),
❷ => 0x0000555555555151 <+8>: 48 8d 05 ac 0e 00 00 lea 0xeac(%rip),%rax # 0x555555556004
と表示されました.
0x0000555555555151番地には lea 0xeac(%rip),%raxという命令があり,
機械語バイト列としては 48 8d 05 ac 0e 00 00だと分かりました.
-
xコマンドでメモリ中の値を表示できます. 例えば,❸x/7xb 0x0000555555555151は, 「0x0000555555555151番地のメモリの値を表示せよ. 表示は1バイト単位(b),16進表記(x)のものを7個,表示せよ」という意味です. その結果,逆アセンブル結果と同じ値が表示されました (❹0x555555555151 <main+8>: 0x48 0x8d 0x05 0xac 0x0e 0x00 0x00). -
なお,
xコマンドに与える指定は NFT という形式です.
N は表示個数(デフォルト1),Fはフォーマット,Uは単位サイズの指定です. FとUの順番は逆でもOKです. (例:4gxは「8バイトデータを16進数表記で4個表示」を意味する). FとUで指定できるものは以下の通りです.
| フォーマット F | 説明 |
|---|---|
x | 16進数 (hexadecimal) |
d | 符号あり10進数 (decimal) |
u | 符号なし10進数 (unsigned) |
t | 2進数 (two) |
c | 文字 (char) |
s | 文字列 (string) |
i | 機械語命令 (instruction) |
| 単位サイズ U | 説明 |
|---|---|
b | 1バイト (byte) |
h | 2バイト (half-word) |
w | 4バイト (word) |
g | 8バイト (giant) |
xへのアドレス指定にレジスタの値 (❺x/7xb $rip)や レジスタ値を使った足し算 (❻x/7xb $rip+7)も指定できます.
メモリ中の値(スタック)を表示する (x)
// fact.c
#include <stdio.h>
int fact (int n)
{
if (n <= 0)
return 1;
else
return n * fact (n - 1);
}
int main ()
{
printf ("%d\n", fact (5));
}
$ gcc -g fact.c
$ gdb ./a.out
Reading symbols from ./a.out...
(gdb) b main
Breakpoint 1 at 0x1180: file fact.c, line 13.
(gdb) b fact
Breakpoint 2 at 0x1158: file fact.c, line 5.
(gdb) r
Breakpoint 1, main () at fact.c:13
13 printf ("%d\n", fact (5));
(gdb) ❶ p/x $rbp
$1 = ❷ 0x7fffffffde90
(gdb) c
Continuing.
Breakpoint 2, fact (n=5) at fact.c:5
5 if (n <= 0)
(gdb) ❸ x/1xg $rbp + 8
0x7fffffffde88: ❹ 0x000055555555518a
(gdb) disas main
Dump of assembler code for function main:
0x0000555555555178 <+0>: endbr64
0x000055555555517c <+4>: push %rbp
0x000055555555517d <+5>: mov %rsp,%rbp
0x0000555555555180 <+8>: mov $0x5,%edi
0x0000555555555185 <+13>: call 0x555555555149 <fact>
❺ 0x000055555555518a <+18>: mov %eax,%esi
0x000055555555518c <+20>: lea 0xe71(%rip),%rax # 0x555555556004
0x0000555555555193 <+27>: mov %rax,%rdi
0x0000555555555196 <+30>: mov $0x0,%eax
0x000055555555519b <+35>: call 0x555555555050 <printf@plt>
0x00005555555551a0 <+40>: mov $0x0,%eax
0x00005555555551a5 <+45>: pop %rbp
0x00005555555551a6 <+46>: ret
End of assembler dump.
(gdb) ❻ x/1gx $rbp
0x7fffffffde80: ❼ 0x00007fffffffde90
(gdb) q
-
使った短縮コマンド:
b(break),r(run),p(print),c(continue),x(x),disas(disassemble),q(quit) -
mainとfactにブレークポイントを設定し,main関数でブレークした時点で,%rbpの値を調べると (❶p/x $rbp), ❷0x7fffffffde90と分かりました. これはmain関数のスタックフレームの一番下のアドレスです.
-
factでブレークした時点で,スタックフレームは上図になっているはずです. まずメモリ参照8(%rbp)に正しく戻り番地が入っているか調べます.$rbp+8番地のメモリの値を調べると (❸x/1xg $rbp+8), ❹0x000055555555518aが入っていました. (1xgは,8バイトデータを16進数で1個分出力する,を意味します).main関数を逆アセンブルすると,
0x0000555555555185 <+13>: call 0x555555555149 <fact>
❺ 0x000055555555518a <+18>: mov %eax,%esi
この番地(❹ 0x000055555555518a)はcall factの次の命令なので,
戻り番地として正しいことを確認できました.
- 次に
fact(5)のスタックフレーム中の「古い%rbp」の値が正しいかを調べます.%rbpが指すメモリの値を調べると(❻x/1gx $rbp), ❼0x00007fffffffde90が入っていました. これは ❷0x7fffffffde90と一致するので, 「古い%rbp」が正しいことを確認できました.
シンボルテーブル (info address, info symbol)
// hello.c
#include <stdio.h>
int main ()
{
printf ("hello\n");
}
$ gcc -g hello.c
$ gdb ./a.out
Reading symbols from ./a.out...
(gdb) ❶ info address main
Symbol "main" is a function at address ❷ 0x1149.
(gdb) b main
Breakpoint 1 at 0x1151: file hello.c, line 5.
(gdb) r
Breakpoint 1, main () at hello.c:5
5 printf ("hello\n");
(gdb) ❸ info address main
Symbol "main" is a function at address ❹ 0x555555555149.
(gdb) info address printf
Symbol "printf" is at 0x7ffff7c60770 in a file ❺ compiled without debugging.
(gdb) disas main
Dump of assembler code for function main:
0x0000555555555149 <+0>: endbr64
0x000055555555514d <+4>: push %rbp
0x000055555555514e <+5>: mov %rsp,%rbp
=> 0x0000555555555151 <+8>: lea 0xeac(%rip),%rax # 0x555555556004
0x0000555555555158 <+15>: mov %rax,%rdi
0x000055555555515b <+18>: call 0x555555555050 <puts@plt>
0x0000555555555160 <+23>: mov $0x0,%eax
0x0000555555555165 <+28>: pop %rbp
0x0000555555555166 <+29>: ret
End of assembler dump.
(gdb) ❻ info symbol 0x0000555555555149
main in section .text of /mnt/hgfs/gondow/project/linux-x86-64-programming/src/asm/a.out
(gdb) ❼ info symbol 0x0000555555555166
main + 29 in section .text of /mnt/hgfs/gondow/project/linux-x86-64-programming/src/asm/a.out
(gdb) q
-
使った短縮コマンド:
b(break),r(run),disas(disassemble),q(quit) -
info addressは指定したシンボルのアドレスを表示します. プログラム実行前の場合(❶info address main), ファイルa.out中のアドレスを表示します(❷0x1149). これはnmコマンドやobjdump -dで得られるアドレスと同じです.
$ nm ./a.out | egrep main
U __libc_start_main@GLIBC_2.34
0000000000001149 T main
-
一方,実行後では (❸
info address main),main関数のメモリ上でのアドレスが得られます (❹0x555555555149). なお,printfのアドレスを調べると, デバッグ情報無しでコンパイルされた旨のメッセージも表示されました (❺compiled without debugging). -
info symbolは指定したアドレスを持つシンボルを返します. 例えば,main関数の先頭アドレスを指定すると ( ❻info symbol 0x0000555555555149),mainを表示しました. アドレスはmain関数の途中のアドレスでも大丈夫です ( ❼info symbol 0x0000555555555166).
お便利機能
helpコマンド
help (h)はコマンドのヘルプ(説明)を表示します.
(gdb) help step
step, s
Step program until it reaches a different source line.
Usage: step ❶ [N]
Argument N means step N times (or till program stops for another reason).
例えば,help stepとすると,stepに回数を指定できる❶ことが分かりました.
[N]のカギカッコは省略可能な引数を意味します.
aproposコマンド
apropos(apr)は指定した正規表現をヘルプに含むコマンドを表示します.
(gdb) apropos break
advance -- Continue the program up to the given location (same form as args for break command).
break, brea, bre, br, b -- Set breakpoint at specified location.
break, brea, bre, br, b -- Set breakpoint at specified location.
break-range -- Set a breakpoint for an address range.
breakpoints -- Making program stop at certain points.
clear, cl -- Clear breakpoint at specified location.
commands -- Set commands to be executed when the given breakpoints are hit.
(以下略)
例えば,apropos breakとすると,breakをヘルプに含むコマンド一覧を表示します.
breakに関係するコマンドを知りたい場合に便利です.
補完とヒストリ機能
| コマンド | 省略名 | 説明 |
|---|---|---|
ctrl-p | 1つ前のコマンドを表示 | |
ctrl-n | 1つ後のコマンドを表示 | |
show commands | 自分が入力したコマンド履歴を表示 | |
ctrl-i | コマンド等を補完 (TABキーでも同じ) 2回押すと候補一覧を表示 | |
ctrl-l | 画面をクリア・リフレッシュ |
(gdb) br TAB (br とTABの間にはスペースを入れない)
(gdb) break (breakまで補完)
(gdb) break TAB (ここで2回TABを押すと)
break break-range (breakで始まるコマンドの一覧を表示)
(gdb) b main
(gdb) r
(gdb) step
(gdb) ctrl-p (ctrl-p を押すと)
(gdb) step (1つ前のコマンド step が表示された)
TUI (テキストユーザインタフェース)
layoutコマンドで,TUIの表示モードを使えます.
src (ソースコード),asm (アセンブリコード),
regs (レジスタ表示)を選べます.
上図はlayout asm後にlayout regsとした時の画面です.
元の表示方法に戻るにはctrl-x aとして下さい.
ブレークポイントの設定
場所の指定
| 場所の指定 | 説明 |
|---|---|
b 10 | (今実行中のファイルの)10行目 |
b +5 | 今の実行地点から5行後 |
b -5 | 今の実行地点から5行前 |
b main | (今実行中のファイルの)関数main |
b main.c:main | ファイルmain.c中のmain関数 |
b main.c:10 | ファイルmain.cの10行目 |
条件付きブレークポイント
// fact.c
#include <stdio.h>
int fact (int n)
{
if (n <= 0)
return 1;
else
return n * fact (n - 1);
}
int main ()
{
printf ("%d\n", fact (5));
}
$ gcc -g fact.c
$ gdb ./a.out
Reading symbols from ./a.out...
(gdb) ❶ b fact if n==0
Breakpoint 1 at 0x1158: file fact.c, line 5.
(gdb) ❷ i b
Num Type Disp Enb Address What
1 breakpoint keep y 0x0000000000001158 in fact at fact.c:5
stop only if ❸ n==0
(gdb) ❹ cond 1 n==1
(gdb) i b
Num Type Disp Enb Address What
1 breakpoint keep y 0x0000000000001158 in fact at fact.c:5
stop only if n==1
(gdb) ❺ cond 1
Breakpoint 1 now unconditional.
(gdb) i b
Num Type Disp Enb Address What
1 breakpoint keep y 0x0000000000001158 in fact at fact.c:5
(gdb) q
- 使った短縮コマンド:
b(break),i b(info breakpoints),cond(condition),q(quit) - 条件付きブレークポイントは
ifを使って指定します (❶b fact if n==0). i bで,現在のブレークポイントの状況を確認できます (❷i b). 番号1のブレークポイントとして,❸n==0という条件が設定されています.condで,指定した番号のブレークポイントの条件を変更できます. ここでは ❹cond 1 n==1として,条件をn==1に変更しました.condで新しい条件を指定しないと,条件が外れます(❺cond 1).
コマンド付きブレークポイント
commandsで「ブレークした時に実行するコマンド列」を指定できます.
$ gcc -g fact.c
$ gdb ./a.out
(gdb) b fact
Breakpoint 1 at 0x1158: file fact.c, line 5.
(gdb) ❶ commands
Type commands for breakpoint(s) 1, one per line.
End with a line saying just "end".
>print n
>c
❷>end
(gdb) ❸ r
Breakpoint 1, fact (n=5) at fact.c:5
5 if (n <= 0)
$1 = 5
Breakpoint 1, fact (n=4) at fact.c:5
5 if (n <= 0)
$2 = 4
Breakpoint 1, fact (n=3) at fact.c:5
5 if (n <= 0)
$3 = 3
Breakpoint 1, fact (n=2) at fact.c:5
5 if (n <= 0)
$4 = 2
Breakpoint 1, fact (n=1) at fact.c:5
5 if (n <= 0)
$5 = 1
Breakpoint 1, fact (n=0) at fact.c:5
5 if (n <= 0)
$6 = 0
120
(gdb)
- 引数無しで❶
commandsとすると,最後に設定したブレークポイントに対して コマンドを設定します.commands 2やcommands 5-7などブレークポイントの番号や範囲の指定もできます. commandsに続けて,実行したいコマンドを入力します. 最後に❷endを指定します.- ❸実行すると,全ての
factの呼び出しが一気に表示できました. 指定したコマンド中にcontinueを指定できるのがとても便利です. - ここでは不使用ですが,コマンド列の最初に
silentを使用すると, ブレーク時のメッセージを非表示にできます.
ステップ実行
ステップ実行の種類
| ステップ実行の種類 | gdbコマンド | 短縮形 | 説明 |
|---|---|---|---|
| ステップイン | step | s | 1行実行を進める(関数呼び出しは中に入って1行を数える) |
| ステップオーバー | next | n | 1行実行を進める(関数呼び出しはまたいで1行を数える) |
| ステップアウト | finish | fin | 今の関数がリターンするまで実行を進める |
| 実行再開 | continue | c | ブレークされるまで実行を進める |
- 上図で,今,
B();を実行する直前でブレークしているとします. stepすると,関数Bのprintf("B\n");まで実行を進めます.nextすると,関数Aのprintf("A\n");まで実行を進めます.finishすると,関数mainのprintf("main\n");まで実行を進めます.
ステップインの実行例 (step)
#include <stdio.h>
void B ()
{
printf ("B\n");
}
void A ()
{
B ();
printf ("A\n");
}
int main ()
{
A ();
printf ("main\n");
}
$ gcc -g step.c
$ gdb ./a.out
Reading symbols from ./a.out...
(gdb) b A
Breakpoint 1 at 0x116b: file step.c, line 8.
(gdb) r
Breakpoint 1, A () at step.c:8
8 B ();
(gdb) ❶ s
B () at step.c:4
4 ❷ printf ("B\n");
step(❶ s)すると,❷ printf ("B\n");まで実行しました.
ステップオーバーの実行例 (next)
$ gcc -g step.c
$ gdb ./a.out
Reading symbols from ./a.out...
(gdb) b A
Breakpoint 1 at 0x116b: file step.c, line 8.
(gdb) r
Breakpoint 1, A () at step.c:8
8 B ();
(gdb) ❸ n
B
9 ❹ printf ("A\n");
next(❸ n)すると,❹ printf ("A\n");まで実行しました.
ステップオーバーの実行例 (finish)
$ gdb ./a.out
Reading symbols from ./a.out...
(gdb) b A
Breakpoint 1 at 0x116b: file step.c, line 8.
(gdb) r
Starting program: /mnt/hgfs/gondow/project/linux-x86-64-programming/src/asm/a.out
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Breakpoint 1, A () at step.c:8
8 B ();
(gdb) ❺ fin
Run till exit from #0 A () at step.c:8
B
A
main () at step.c:14
14 ❻ printf ("main\n");
finish(❺ fin)すると,❻ printf ("main\n");まで実行しました.
実行再開の実行例 (continue)
$ gdb ./a.out
Reading symbols from ./a.out...
(gdb) b A
Breakpoint 1 at 0x116b: file step.c, line 8.
(gdb) r
Breakpoint 1, A () at step.c:8
8 B ();
(gdb) ❼ c
Continuing.
B
A
main
continue(❼ c)すると,ブレークポイントがなかったので,
最後まで実行して実行終了しました.
変数の値の表示
配列 (@)
// array2.c
#include <stdlib.h>
int main (int argc, char **argv)
{
int arr [4] = {0, 10, 20, 30};
int *p = malloc (sizeof (int) * 4);
p [0] = 40;
p [1] = 50;
p [2] = 60;
p [3] = 70;
}
$ gcc -g array2.c
(gdb) b 11
Breakpoint 1 at 0x5555555551ee: file array2.c, line 11.
(gdb) r aa bb cc dd
Starting program: /mnt/hgfs/gondow/project/linux-x86-64-programming/src/asm/a.out aa bb cc dd
Breakpoint 1, main (argc=5, argv=0x7fffffffdf78) at array2.c:11
11 }
(gdb) ❶ p arr
❷ $1 = {0, 10, 20, 30}
(gdb) ❸ p *p
❹ $2 = 40
(gdb) ❺ p *p@4
❻ $3 = {40, 50, 60, 70}
(gdb) ❼ p *argv@5
❽ $4 = {
0x7fffffffe2f7 "/mnt/hgfs/gondow/project/linux-x86-64-programming/src/asm/a.out", 0x7fffffffe337 "aa", 0x7fffffffe33a "bb", 0x7fffffffe33d "cc",
0x7fffffffe340 "dd"}
- 普通の配列は
printでそのまま表示できます. 例えば ❶p arrとすると,❷$1 = {0, 10, 20, 30}と表示されます. mallocで配列を確保した場合, 単純に ❸p *pとすると,pの型はint *なので, ❹$2 = 40しか表示されません. この場合は@を使って ❺p *p@4とすると, 4要素の配列としてうまく表示できます(❻$3 = {40, 50, 60, 70}).- 同様に
argvも ❼p *argv@5とすると,うまく表示できます(❽).
スコープの指定 ('::')
#include <stdio.h>
int x = 111;
int main ()
{
static int x = 222;
{
int x = 333;
printf ("hello\n");
}
}
$ gcc -g scope.c
$ gdb ./a.out
Reading symbols from ./a.out...
(gdb) b 8
Breakpoint 1 at 0x115c: file scope.c, line 8.
(gdb) r
Breakpoint 1, main () at scope.c:8
8 printf ("hello\n");
(gdb) p x
$1 = 333
(gdb) ❶ p 'scope.c'::x
$2 = 111
(gdb) ❷ p main::x
$3 = 222
'::'を使うと,特定のファイルや関数中の変数の値を表示できます.
- ❶
p 'scope.c'::xはscope.cのグローバル変数xの値を表示します. (ファイル名をクオート文字'で囲む必要があります). - ❷
p main::xは関数mainの静的変数xの値を表示します.
構造体 (リスト構造)
// list.c
#include <stdio.h>
struct list {
int data;
struct list *next;
};
int main ()
{
struct list n1 = {10, NULL};
struct list n2 = {20, &n1};
struct list n3 = {30, &n2};
struct list *p = &n3;
}
$ gdb ./a.out
Reading symbols from ./a.out...
(gdb) b 14
Breakpoint 1 at 0x119e: file list.c, line 14.
(gdb) r
Breakpoint 1, main () at list.c:14
14 }
(gdb) p p
$1 = (struct list *) 0x7fffffffde70
(gdb) ❶ p *p
❷ $2 = {data = 30, next = 0x7fffffffde60}
(gdb) ❸ p *p->next
$3 = {data = 20, next = 0x7fffffffde50}
(gdb) ❹ p *p->next->next
$4 = {data = 10, next = 0x0}
pが指すリスト構造は上図のようになっています.printを使って(❶p *p), 構造体の中身を普通に表示できます(❷{data = 30, next = 0x7fffffffde60}).*p->nextなどのC言語の式と(ほぼ)同じ記法で, リスト構造をたどって中身を表示できます (❸p *p->next,❹p *p->next->next).
共用体
// union.c
#include <stdio.h>
union foo {
int u1;
float u2;
};
int main ()
{
union foo f;
f.u1 = 999;
f.u2 = 123.456;
}
(gdb) b 13
Breakpoint 3 at 0x555555555138: file union.c, line 13.
(gdb) r
Breakpoint 3, main () at union.c:13
13 f.u2 = 123.456;
(gdb) ❶ p f
❷ $1 = {u1 = 999, u2 = 1.39989717e-42}
(gdb) ❸ p f.u1
❹ $2 = 999
(gdb) s
14 }
(gdb) p f
$3 = {u1 = 1123477881, u2 = 123.456001}
(gdb) p f.u2
$4 = 123.456001
- 共用体を
printすると (❶p f),u1とu2のどちらのメンバが使われているかgdbは分からないので, 両方の可能性を表示します (❷{u1 = 999, u2 = 1.39989717e-42}). - メンバ名を
u1と指定すると (❸p f.u1), そのメンバに対する値を表示します (❹$2 = 999).
特定の値をメモリ中から探す (find)
// find.c
#include <stdio.h>
int arr [1000];
int main ()
{
arr [500] = 0xDEADBEEF;
printf ("%p\n", &arr [500]);
}
(gdb) b 8
Breakpoint 1 at 0x115b: file find.c, line 8.
(gdb) r
Breakpoint 1, main () at find.c:8
8 printf ("%p\n", &arr [500]);
(gdb) ❶ p/x &arr[500]
❷ $1 = 0x555555558810
(gdb) ❸ find /w arr, arr+4000, 0xDEADBEEF
❹ 0x555555558810 <arr+2000>
1 pattern found.
- 上のプログラムでは
arr[500]に0xDEADBEEFという値が入っています. この値が格納されている場所のアドレスはprintで(❶p/x &arr[500]), ❷0x555555558810番地と分かります. - ここで仮に,配列のどこに
0xDEADBEEFが入っているか分からず, この配列に入っているか調べたいとします.findコマンドで調べられます. ❸find /w arr, arr+4000, 0xDEADBEEFは, 指定したアドレス範囲 (arr番地からarr+4000番地まで), 4バイト (/w)の値0xDEADBEEFを探せ,という意味になります. 正しく結果が表示されました (❹0x555555558810 <arr+2000>).
変数,レジスタ,メモリに値をセット (set)
setを使うと,変数,レジスタ,メモリに値をセットできます.
// calcx.c
int main ()
{
int x = 10;
x += 3;
x += 4;
return x;
}
$ gcc -g calcx.c
$ gdb ./a.out
(gdb) b main
Breakpoint 1 at 0x1131: file calcx.c, line 4.
(gdb) r
Breakpoint 1, main () at calcx.c:4
4 int x = 10;
(gdb) s
5 x += 3;
(gdb) ❶ set x = 20
(gdb) p x
$1 = 20
(gdb) ❷ p x = 30
$2 = 30
(gdb) p x
$3 = 30
(gdb) ❸ p/x &x
$4 = 0x7fffffffde8c
(gdb) ❹ set {int}0x7fffffffde8c = 40
(gdb) p x
❺ $5 = 40
(gdb) p/x $rax
$6 = 0x555555555129
(gdb) ❻ set $rax = 0xDEADBEEF
(gdb) p/x $rax
$7 = 0xdeadbeef
- ❶
set x = 20で,変数xに20を代入しています. - ❸
p/x &xで変数xのアドレスを調べて, そのアドレスに代入してみます(❹set {int}0x7fffffffde8c = 40). 変数xの値が40に変わりました (❺$5 = 40). - ❺
set $rax = 0xDEADBEEFで,レジスタ%raxの値を変更しました. - なお,変数,メモリ,レジスタのどの場合でも,
printコマンドを使っても代入できます (❷p x = 30).
gdb の主なコマンドの一覧
起動・終了
| コマンド | 省略名 | 説明 |
|---|---|---|
gdb ./a.out | gdbの起動 | |
run | r | 実行開始 |
quit | q | gdbの終了 |
ctrl-c | 実行中のプログラムを一時停止 (シグナル SIGINTを使用) | |
ctrl-z | 実行中のプログラムを一時停止 (シグナル SIGTSTPを使用) | |
| ⏎ (改行) | 前と同じコマンドを再実行 |
ctrl-cでSIGINTをgdbではなく実行中のプログラムに渡すには,handle SIGINT nostop passとします.gdbのシグナル処理状態はinfo signalsで見られます.gdbのプロンプト(gdb)が出ている状態で,ctrl-zを入力すると,gdb自体の実行を一時停止します.再開するにはfgコマンドなどを使います.
ヘルプ
| コマンド | 省略名 | 説明 |
|---|---|---|
help コマンド | h | コマンドのヘルプ(説明)を表示 |
apropos [-v] 正規表現 | apr | 正規表現をヘルプに含むコマンドを表示(-vは詳細表示) |
ヒストリ(コマンド履歴)と補完(コンプリーション)など
| コマンド | 省略名 | 説明 |
|---|---|---|
ctrl-p | 1つ前のコマンドを表示 | |
ctrl-n | 1つ後のコマンドを表示 | |
show commands | 自分が入力したコマンド履歴を表示 | |
ctrl-i | コマンド等を補完 (TABキーでも同じ) 2回押すと候補一覧を表示 | |
ctrl-l | 画面をクリア・リフレッシュ |
ブレークポイント・ウォッチポイント
| コマンド | 省略名 | 説明 |
|---|---|---|
break 場所 | b | ブレークポイントの設定 |
rbreak 正規表現 | rb | 正規表現にマッチする全関数にブレークポイントの設定 |
watch 場所 | wa | ウォッチポイント(書き込み)の設定 |
rwatch 場所 | rw | ウォッチポイント(読み込み)の設定 |
awatch 場所 | aw | ウォッチポイント(読み書き)の設定 |
info break | i b | ブレークポイント・ウォッチポイント一覧表示 |
break 場所 if 条件 | b | 条件付きブレークポイントの設定 |
condition 番号 条件 | cond | ブレークポイントに条件を設定 |
commands [番号] | comm | ブレークした時に実行するコマンド列を設定(endで終了) |
delete 番号 | d | ブレークポイントの削除 |
delete | d | 全ブレークポイントの解除 (clearでも同じ) |
| 場所の指定方法 | 例 |
|---|---|
| 関数名 | main |
| 行番号 | 6 |
| ファイル名:行番号 | main.c:6 |
| ファイル名:関数名 | main.c:main |
*アドレス | *0x55551290 |
ステップ実行
| コマンド | 省略名 | 説明 |
|---|---|---|
step | s | 次の行までステップ実行(関数コールに入って1行を数える) |
next | n | 次の行までステップ実行(関数コールはまたいで1行を数える) |
finish | fin | 今の関数を終了するまで実行 |
continue | c | ブレークポイントに当たるまで実行 |
until 場所 | u | 指定した場所まで実行(ループを抜けたい時に便利) |
jump 場所 | j | 指定した場所から実行を再開(%ripを書き換えて再開に相当) |
stepi | si | 次の機械語命令を1つだけ実行して停止(関数コールに入って1命令を数える) |
nexti | ni | 次の機械語命令を1つだけ実行して停止(関数コールはまたいで1命令を数える) |
式,変数,レジスタ,メモリの表示
| コマンド | 省略名 | 説明 |
|---|---|---|
print/フォーマット 式 | p | 式を実行して値を表示 |
display/フォーマット 式 | disp | 実行停止毎にprintする |
info display | i di | displayの設定一覧表示 |
undisplay 番号 | und | displayの設定解除 |
x/NFU アドレス | x | メモリの内容を表示 (examine) |
info registers | i r | 全汎用レジスタの内容を表示 |
info all-registers | i al | 全汎用レジスタの内容を表示 |
- 表示する「式」は副作用があっても良い.
代入式でも良いし,副作用のある関数呼び出しやライブラリ関数呼び出しでも良い.
(例:
p x = 999,p printf ("hello\n")). このためprintfコマンドは単なる「実行状態の表示コマンド」ではなく 「実行状態の変更」も可能 (このためにgdbは裏で結構すごいことやってる).
| 式 | 説明 |
|---|---|
$レジスタ名 | レジスタ参照 |
アドレス@要素数 | 配列「アドレス[要素数]」として処理 |
- N は表示個数(デフォルト1),Fはフォーマット,Uは単位サイズを指定する.
FとUの順番は逆でも良い.
(例:
4gxは「8バイトデータを16進数表記で4個表示」を意味する)
| フォーマット F | 説明 |
|---|---|
x | 16進数 (hexadecimal) |
z | 16進数 (上位バイトのゼロも表示) |
o | 8進数 (octal) |
d | 符号あり10進数 (decimal) |
u | 符号なし10進数 (unsigned) |
t | 2進数 (two) |
c | 文字 (char) |
s | 文字列 (string) |
i | 機械語命令 (instruction) |
a | アドレス (address) |
f | 浮動小数点数 (float) |
| 単位サイズ U | 説明 |
|---|---|
b | 1バイト (byte) |
h | 2バイト (half-word) |
w | 4バイト (word) |
g | 8バイト (giant) |
変数,レジスタ,メモリの変更
| コマンド | 省略名 | 説明 |
|---|---|---|
set 変数 = 式 | set | 変数に式の値を代入する |
- 変数には通常の変数(
x),レジスタ($rax), メモリ ({int}0x0x1200), デバッガ変数 ($foo)が指定できます.
スタック表示
| コマンド | 省略名 | 説明 |
|---|---|---|
backtrace | bt, ba | コールスタックを表示 where, info stackでも同じ |
backtrace full | bt f, ba f | コールスタックと全局所変数を表示 |
プログラム表示
| コマンド | 省略名 | 説明 |
|---|---|---|
list 場所 | l | ソースコードを表示 |
disassemble 場所 | disas | 逆アセンブル結果を表示 |
disassembleへのオプション
| オプション | 説明 |
|---|---|
/s | ソースコードも表示 (表示順は機械語命令の順番) |
/m | ソースコードも表示 (表示順はソースコードの順番) |
/r | 機械語命令の16進ダンプも表示 |
TUI (テキストユーザインタフェース)
| コマンド | 省略名 | 説明 |
|---|---|---|
layout レイアウト | la | TUIレイアウトを変更 |
| レイアウト | 説明 |
|---|---|
asm | アセンブリコードのウインドウを表示 |
regs | レジスタのウインドウを表示 |
src | ソースコードのウインドウを表示 |
split | ソースとアセンブリコードのウインドウを表示 |
next | 次のレイアウトを表示 |
prev | 前のレイアウトを表示 |
| キーバインド | 説明 |
|---|---|
ctrl-x a | TUIモードのオン・オフ |
ctrl-x 1 | ウインドウを1つにする |
ctrl-x 2 | ウインドウを2つにする |
ctrl-x o | 選択ウインドウを変更 |
ctrl-x s | シングルキーモードのオン・オフ |
ctrl-l | ウインドウをリフレッシュ(再表示) |
| シングルキーモードの キーバインド | 説明 |
|---|---|
c | continue |
d | down |
f | finish |
n | next |
o | nexti |
q | シングルキーモードの終了 |
r | run |
s | step |
i | stepi |
v | info locals |
v | where |
シンボルテーブル
| コマンド | 省略名 | 説明 |
|---|---|---|
info address シンボル | i ad | シンボルのアドレスを表示 |
info symbol アドレス | i s | そのアドレスを持つシンボルを表示 |
型の表示
| コマンド | 省略名 | 説明 |
|---|---|---|
whatis 式または型名 | wha | その式や型名の型情報を表示 |
ptype 式または型名 | pt | その式や型名の型情報を詳しく表示 |
info types 正規表現 | i types | 正規表現にマッチする型を表示 |
whatisはtypedefを1レベルだけ展開します.ptypeはtypedefを全て展開します.ptypeに/oオプションを付けると,構造体のフィールドの オフセットとサイズも表示します.
その他の使い方
どんなものがあるか,ごく簡単に説明します(詳しくは説明しません). 詳しくはGDBマニュアルを参照下さい.
初期化ファイル
| ファイル名 | 説明 |
|---|---|
~/.gdbearlyinit | gdbの初期化前に読み込まれる初期化ファイル |
~/.gdbinit | gdbの初期化後に読み込まれる初期化ファイル |
./.gdbinit | 最後に読み込まれる初期化ファイル |
- よく使う
gdbの設定,ユーザ定義コマンドや コマンドエイリアスは 初期化ファイルに記述しておくと便利です. gdbの起動メッセージを抑制するset startup-quietly onは~/.gdbearlyinitに指定する必要があります../.gdbinitは個別の設定の記述に便利です. ただしデフォルトでは許可されていないので,add-auto-load-safe-path パスやset auto-load safe-path /を~/.gdbinitに書く必要があります.
ユーザ定義コマンド
defineとendでユーザ定義コマンドを定義できます.
$ cat ~/.gdbinit
define hello
echo hello, ❶ $arg0\n
end
❷ define start
b main
r
end
define ❸ hook-print
echo size: b (1 byte), h (2 byte), w (4 byte), g (8 byte)\n
end
define ❹ hook-stop
x/i $rip
end
$ gdb ./a.out
(gdb) hello gdb
hello, gdb
(gdb) start
=> 0x555555555151 <main+8>: lea 0xeac(%rip),%rax # 0x555555556004
Breakpoint 1, main () at hello.c:5
5 printf ("hello\n");
(gdb) p main
size: b (1 byte), h (2 byte), w (4 byte), g (8 byte)
$1 = {int ()} 0x555555555149 <main>
(gdb) ❺ help user-defined
User-defined commands.
The commands in this class are those defined by the user.
Use the "define" command to define a command.
List of commands:
hello -- User-defined.
hook-print -- User-defined.
hook-stop -- User-defined.
start -- User-defined.
(gdb)
- ユーザ定義コマンドの引数は,❶
$arg0,$arg1... と参照します. - 例えば「毎回
b mainとrを2回打つのは面倒だ」 という場合はユーザ定義コマンド❷startを定義すると便利かも知れません. (ここでは使っていませんが)if,while,setを組み合わせて スクリプト的なユーザ定義コマンドも定義可能です. hook-で始まるコマンド名は特別な意味を持ちます. 例えば,❸hook-printはprintを実行するたびに実行されるユーザ定義コマンドになります.(ここでは試しにサイズ指定bhwgの意味を表示しています).hook-stopはプログラムが一時停止するたびに実行されるユーザ定義コマンドです.help user-definedで,ユーザ定義コマンドの一覧を表示できます.
コマンドエイリアス
$ cat ~/.gdbinit
❶ alias di = disassemble
$ gdb ./a.out
(gdb) b main
Breakpoint 1 at 0x1151: file hello.c, line 5.
(gdb) r
Breakpoint 1, main () at hello.c:5
5 printf ("hello\n");
(gdb) ❷ di
Dump of assembler code for function main:
0x0000555555555149 <+0>: endbr64
0x000055555555514d <+4>: push %rbp
0x000055555555514e <+5>: mov %rsp,%rbp
=> 0x0000555555555151 <+8>: lea 0xeac(%rip),%rax # 0x555555556004
0x0000555555555158 <+15>: mov %rax,%rdi
0x000055555555515b <+18>: call 0x555555555050 <puts@plt>
0x0000555555555160 <+23>: mov $0x0,%eax
0x0000555555555165 <+28>: pop %rbp
0x0000555555555166 <+29>: ret
End of assembler dump.
(gdb) ❸ help di
disassemble, di
Disassemble a specified section of memory.
Usage: disassemble[/m|/r|/s] START [, END]
(以下略)
(gdb) ❹ help aliases
User-defined aliases of other commands.
List of commands:
di -- Disassemble a specified section of memory.
aliasコマンドでコマンドの別名を定義できます. ここではalias di = disassembleとして,❷diで逆アセンブルができるようにしました.- 素晴らしいことに,❸
helpがユーザ定義のエイリアスに対応していて,help diでヘルプを表示できます. - また,❹
help aliasesでエイリアスの一覧を表示できます. (-aオプションで定義したエイリアスは,補完の対象にならず, エイリアス一覧にも表示されません).
プロセスのアタッチとデタッチ (attach, detach)
gdb -pオプションやattachを使うと,すでに実行中のプログラムを
gdbの支配下に置けます(これをプロセスにアタッチするといいます).
// inf-loop.c
#include <stdio.h>
int main ()
{
int x = 1, n = 0;
while (x != 0) {
n++;
}
printf ("hello, world\n");
}
$ ❶ sudo sysctl -w kernel.yama.ptrace_scope=0
$ gcc -g inf-loop.c
$ ./a.out
❷^Z
[1]+ Stopped ./a.out
$ ❸ bg
[1]+ ./a.out &
$ ps | egrep a.out
❹ 27373 pts/0 00:00:10 a.out
$ ❺ gdb -p 27373
Attaching to process 27373
main () at inf-loop.c:6
6 while (x != 0) {
(gdb) bt
#0 main () at inf-loop.c:6
(gdb) ❻ kill
Kill the program being debugged? (y or n) y
[Inferior 1 (process 27373) killed]
(gdb) q
- まず ❶
sudo sysctl -w kernel.yama.ptrace_scope=0として, プロセスへのアタッチを許可します.デフォルトでは以下のメッセージが出て アタッチができません.❶の操作はLinuxを再起動するまで有効です.
$ gdb -p 27373
Attaching to process 27373
Could not attach to process. If your uid matches the uid of the target
process, check the setting of /proc/sys/kernel/yama/ptrace_scope, or try
again as the root user. For more details, see /etc/sysctl.d/10-ptrace.conf
ptrace: Operation not permitted.
- ここでは無限ループする
inf-loop.cをコンパイルして実行します. ❷ctrl-zで実行をサスペンド(一時中断)して,❸bgで バックグラウンド実行にします. (別の端末からgdbを起動するなら,❷❸の作業は不要です) psコマンドでa.outのプロセス番号を調べると❹27373と分かりました. ❺gdb -p 27373とすると,プロセス番号27373のプロセスをgdbが支配下に置きます(これをプロセスにアタッチすると言います).- ここでは単に
killコマンドでa.outを終了させました. 終了させたくない場合は,調査後にdetachするかgdbを終了すれば,a.outはそのまま実行を継続できます. gdb起動後に,attachコマンドを使ってもアタッチできます.
コアファイルによる事後デバッグ
コアファイルとは
コアファイル(core file)あるいはコアダンプ(core dump)とは, 実行中のプロセスのメモリやレジスタの値を記録したファイルのことです. 再現性がないバグに対してコアファイルがあると, 後から何度でもそのコアファイルを使ってデバッグできるので便利です.
コアファイルのコアはメモリを意味する
コアファイルのコア (core)はメモリを意味します. これはかつて(大昔)のメモリが磁気コアだったことに由来します.
コアファイルを生成する設定
セキュリティ等の理由で,デフォルトの設定ではコアファイルが生成されません. 以下でコアファイルを生成する設定にできます.
$ ❶ ulimit -c unlimited
$ ❷ sudo sysctl -w kernel.core_pattern=core
❶でコアファイルのサイズを無制限に設定します.
❷で,コアファイル名のパターンをcoreにします
(生成されるファイル名は core.<pid> となります.<pid>はそのプロセスのプロセス番号です).
❶の設定はそのシェル内のみ,❷の設定はLinuxを再起動するまで有効です.
コアファイルで事後解析してみる
segmentation faultでクラッシュしたプログラムの事後解析をしてみます.
$ gcc -g segv.c
$ ./a.out
❶ Segmentation fault (core dumped)
$ ls -l core*
❷ -rw------- 1 gondow gondow 307200 8月 25 10:54 core.2224
$ ❸ gdb ./a.out core.2224
Reading symbols from ./a.out...
Core was generated by `./a.out'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0 0x0000559ad81bb162 in main () at segv.c:6
6 printf ("%d\n", *p);
(gdb) p p
❹ $1 = (int *) 0xdeadbeef
(gdb) bt
#0 0x0000559ad81bb162 in main () at segv.c:6
segv.cをコンパルして実行すると,segmentation fault を起こし,
コアファイルが作成されました(❷).
gdbにコアファイル名も指定して起動すると(❸),
segmentation faultが起きた状態でデバッグが可能になりました.
例えば,変数pの値を表示できています
(❹ $1 = (int *) 0xdeadbeef).
注: 著者の環境(仮想マシンVMWare Fusion 上のUbuntu 22.04LTS,ホストマシン macOS 13.4)の共有フォルダ上で上記を行った場合, 作られたコアファイルのサイズが0になってしまいました. 共有フォルダではなく
/tmpなどでは問題なくコアファイルが作られました.
動作中のプロセスのコアファイルを生成する
gcoreコマンドや,gdbのgcoreコマンドで,
動作中のプロセスのコアファイルを生成できます.
$ gcc -g inf-loop.c
$ ./a.out &
[1] 2325
$ ❶ sudo sysctl -w kernel.yama.ptrace_scope=0
kernel.yama.ptrace_scope = 0
$ ❷ gcore 2325
0x0000561775b05169 in main ()
Saved corefile core.2325
$ ❸ gdb ./a.out core.2325
Reading symbols from ./a.out...
Core was generated by `./a.out'.
#0 main () at inf-loop.c:6
6 while (x != 0) {
❶でアタッチを可能にする設定が必要です.
gcoreコマンドが対象プログラムにアタッチするからです.
gcoreコマンドでコアファイルを生成し(❷),gdbでコアファイルを指定すると(❸),
無事にデバッグ可能になりました.
$ gcc -g inf-loop.c
$ gdb ./a.out
(gdb) r
Starting program: /tmp/a.out
❶ ^C
Program received signal SIGINT, Interrupt.
main () at inf-loop.c:6
6 while (x != 0) {
(gdb) ❷ gcore
Saved corefile core.2369
gdb上でもコアファイルを生成できます.
gdb上でa.outを実行後,このプログラムは無限ループしてるので,
ctrl-c (❶)で実行を中断してから,
gcoreコマンドを使うと,コアファイルを生成できました.
キャッチポイント (catch)
キャッチポイントは様々なイベント発生時にブレークする仕組みです.
キャッチポイントが扱えるイベントには,
例外,exec,fork,vfork,
システムコール(syscall),
ライブラリのロード・アンロード(load, unload),
シグナル (signal)などがあります.
システムコールをキャッチしてみる
$ gcc -g hello.c
$ gdb ./a.out
(gdb) ❶ catch syscall write
Catchpoint 1 (syscall 'write' [1])
(gdb) r
❷ Catchpoint 1 (call to syscall write), 0x00007ffff7d14a37 in __GI___libc_write (fd=1, buf=0x5555555592a0, nbytes=6) at ../sysdeps/unix/sysv/linux/write.c:26
26 ../sysdeps/unix/sysv/linux/write.c: No such file or directory.
(gdb) ❸ bt
#0 0x00007ffff7d14a37 in __GI___libc_write (fd=1, buf=0x5555555592a0,
nbytes=6) at ../sysdeps/unix/sysv/linux/write.c:26
#1 0x00007ffff7c8af6d in _IO_new_file_write (
f=0x7ffff7e1a780 <_IO_2_1_stdout_>, data=0x5555555592a0, n=6)
at ./libio/fileops.c:1180
#2 0x00007ffff7c8ca61 in new_do_write (to_do=6,
data=0x5555555592a0 "hello\n", fp=0x7ffff7e1a780 <_IO_2_1_stdout_>)
at ./libio/libioP.h:947
#3 _IO_new_do_write (to_do=6, data=0x5555555592a0 "hello\n",
fp=0x7ffff7e1a780 <_IO_2_1_stdout_>) at ./libio/fileops.c:425
#4 _IO_new_do_write (fp=fp@entry=0x7ffff7e1a780 <_IO_2_1_stdout_>,
data=0x5555555592a0 "hello\n", to_do=6) at ./libio/fileops.c:422
#5 0x00007ffff7c8cf43 in _IO_new_file_overflow (
f=0x7ffff7e1a780 <_IO_2_1_stdout_>, ch=10) at ./libio/fileops.c:783
#6 0x00007ffff7c8102a in __GI__IO_puts (str=0x555555556004 "hello")
at ./libio/ioputs.c:41
#7 0x0000555555555160 in main () at hello.c:5
(gdb)
❶ catch syscall writeで,writeシステムコールをキャッチしてみます.
printfが最終的にはwriteシステムコールを呼ぶはずです.
やってみたら,無事にキャッチできました(❷).
バックトレースを見ると(❸),main関数からwriteが呼ばれるまでの
関数呼び出しを表示できました.
シグナルをキャッチしてみる (handle, catch signal)
handleを使う
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void handler (int n)
{
fprintf (stderr, "I am handler\n");
}
int main (void)
{
signal (SIGUSR1, handler);
while (1) {
fprintf (stderr, ".");
sleep (1);
}
}
$ gcc -g sigusr1.c
$ gdb ./a.out
Reading symbols from ./a.out...
(gdb) ❶ handle SIGUSR1
Signal Stop Print Pass to program Description
SIGUSR1 Yes Yes Yes User defined signal 1
(gdb) r
❷ ...........
❸ Program received signal SIGUSR1, User defined signal 1.
0x00007ffff7ce57fa in __GI___clock_nanosleep (clock_id=clock_id@entry=0, flags=flags@entry=0, req=req@entry=0x7fffffffde50, rem=rem@entry=0x7fffffffde50) at ../sysdeps/unix/sysv/linux/clock_nanosleep.c:78
78 ../sysdeps/unix/sysv/linux/clock_nanosleep.c: No such file or directory.
(gdb) ❹ handle SIGUSR1 nostop noprint pass
Signal Stop Print Pass to program Description
SIGUSR1 No No Yes User defined signal 1
(gdb) ❺ c
Continuing.
❻ I am handler
......I am handler
.....❼ ^C
Program received signal SIGINT, Interrupt.
0x00007ffff7ce57fa in __GI___clock_nanosleep (clock_id=clock_id@entry=0, flags=flags@entry=0, req=req@entry=0x7fffffffde50, rem=rem@entry=0x7fffffffde50) at ../sysdeps/unix/sysv/linux/clock_nanosleep.c:78
78 in ../sysdeps/unix/sysv/linux/clock_nanosleep.c
(gdb)
-
❶
handle SIGUSR1とすると,gdbがシグナルSIGUSR1を受け取った時の処理設定が表示されます.- Stop Yes:
gdbはa.outの実行を一時停止します. - Print Yes:
gdbはSIGUSR1を受け取ったことを表示します. - Pass Yes:
gdbはa.outにSIGUSR1を渡します.
- Stop Yes:
-
❷ 実行を開始すると,
a.outは1秒ごとに.を出力しながらSIGUSR1を待ちます. -
別端末から
a.outのプロセス番号を調べて(ここでは2696),kill -USR1 2696として,a.outにSIGUSR1を送信しました. その結果,a.outの実行が一時停止し(❸),gdbに制御が戻りました. -
今度は
SIGUSR1の設定を変えてやってみます ❹handle SIGUSR1 nostop noprint passは, 「SIGUSR1で一時停止しない,表示もしない,a.outにSIGUSR1を渡す」 という設定を意味します (stop,nostop,print,noprint,pass,nopassを指定可能です).gdbがSIGUSR1を受け取った時,gdbはa.outを一時停止させず,SIGUSR1をa.outに渡すはずです. -
実行を再開すると (❺
c), ❻I am handlerが表示されています. これは先程送ったSIGUSR1に対してa.outのシグナルハンドラが出力した表示です. ここでもう一度,kill -USR1 2696として,a.outにSIGUSR1を送信すると, (gdbはa.outを一時停止させること無く) 再度I am handlerが表示されました.期待した通りの動作です. -
ctrl-C(❼^C)を押して,a.outの動作を一時停止しました.
catch signalを使う
$ gcc -g sigusr1.c
$ gdb ./a.out
Reading symbols from ./a.out...
(gdb) ❶ catch signal SIGUSR1
Catchpoint 1 (signal SIGUSR1)
(gdb) r
..........
❷ Catchpoint 1 (signal SIGUSR1), 0x00007ffff7ce57fa in __GI___clock_nanosleep (clock_id=clock_id@entry=0, flags=flags@entry=0, req=req@entry=0x7fffffffde50, rem=rem@entry=0x7fffffffde50) at ../sysdeps/unix/sysv/linux/clock_nanosleep.c:78
78 ../sysdeps/unix/sysv/linux/clock_nanosleep.c: No such file or directory.
(gdb)
- ❶
catch signal SIGUSR1で,SIGUSR1をキャッチする設定をします. - 別端末から
kill -USR1 2696として,a.outにSIGUSR1を送信すると, 期待通り,SIGUSR1をキャッチしてa.outの実行が一時停止されました (❷Catchpoint 1 (signal SIGUSR1)). handleもcatchもシグナルをキャッチできるのですが,catchがhandleより嬉しいのは,catchを使うと 停止する条件や 停止時に実行する コマンドを設定できることです.- なお
catchを設定すると,handleのnostop設定は無視されます.
GDBのPythonプラグイン
PythonでGDBのユーザ定義コマンドを定義できます.
# gdb-script.py
class python_test (❶ gdb.Command):
"""Python Script Test"""
def __init__ (self):
super (python_test, self).__init__ (
"python_test", gdb.COMMAND_USER
)
def invoke (self, args, from_tty):
val = ❷ gdb.parse_and_eval (args)
print ("args = " + args)
print ("val = " + str (val))
❸ gdb.execute ("p/x" + str (val) + "\n");
python_test ()
- 例えば上の
gdb-script.pyはpython_testというユーザ定義コマンドを定義します.~/.gdbinitなどでこのファイルをsource gdb-script.pyとして読み込む必要があります. - 定義するコマンドは❶
gdb.Commandのサブクラスとして定義します. ❷gdb.parse_and_evalを使えば与えられた引数をgdbの下で評価できます. ❸gdb.executeを使えば,gdbのコマンドとして実行できます.
$ gdb ./a.out
(gdb) b main
Breakpoint 1 at 0x1180: file fact.c, line 13.
(gdb) r
Breakpoint 1, main () at fact.c:13
13 printf ("%d\n", fact (5));
(gdb) ❹ python_test $rsp
args = $rsp
val = 0x7fffffffde60
$1 = 0x7fffffffde60
(gdb) help user-defined
User-defined commands.
The commands in this class are those defined by the user.
Use the "define" command to define a command.
List of commands:
❺ python_test -- Python Script Test
(gdb)
gdb上で定義したpython_testというコマンドを実行すると(❹) 意図通り実行できました(%rspの値が評価されて0x7fffffffde60になっています).help user-definedすると,ちゃんと登録されていました(❺).
GDB/MI machine interface
gdbのMI(マシンインタフェース)とは
gdbとのやり取りをプログラムで処理しやすくするためのモードです.
$ gdb --interpreter=mi ./a.out
=thread-group-added,id="i1"
=cmd-param-changed,param="auto-load safe-path",value="/"
~"Reading symbols from ./a.out...\n"
(gdb)
❶ b main
❷ &"b main\n"
❸ ~"Breakpoint 1 at 0x1180: file fact.c, line 13.\n"
❹ =breakpoint-created,bkpt={number="1",type="breakpoint",disp="keep",enabled="y",addr="0x0000000000001180",func="main",file="fact.c",fullname="/mnt/hgfs/gondow/project/linux-x86-64-programming/src/asm/fact.c",line="13",thread-groups=["i1"],times="0",original-location="main"}
^done
(gdb)
r
&"r\n"
~"Starting program: /mnt/hgfs/gondow/project/linux-x86-64-programming/src/asm/a.out \n"
=thread-group-started,id="i1",pid="5171"
=thread-created,id="1",group-id="i1"
=breakpoint-modified,bkpt={number="1",type="breakpoint",disp="keep",enabled="y",addr="0x0000555555555180",func="main",file="fact.c",fullname="/mnt/hgfs/gondow/project/linux-x86-64-programming/src/asm/fact.c",line="13",thread-groups=["i1"],times="0",original-location="main"}
=library-loaded,id="/lib64/ld-linux-x86-64.so.2",target-name="/lib64/ld-linux-x86-64.so.2",host-name="/lib64/ld-linux-x86-64.so.2",symbols-loaded="0",thread-group="i1",ranges=[{from="0x00007ffff7fc5090",to="0x00007ffff7fee335"}]
^running
*running,thread-id="all"
(gdb)
=library-loaded,id="/lib/x86_64-linux-gnu/libc.so.6",target-name="/lib/x86_64-linux-gnu/libc.so.6",host-name="/lib/x86_64-linux-gnu/libc.so.6",symbols-loaded="0",thread-group="i1",ranges=[{from="0x00007ffff7c28700",to="0x00007ffff7dbaabd"}]
~"[Thread debugging using libthread_db enabled]\n"
~"Using host libthread_db library \"/lib/x86_64-linux-gnu/libthread_db.so.1\".\n"
=breakpoint-modified,bkpt={number="1",type="breakpoint",disp="keep",enabled="y",addr="0x0000555555555180",func="main",file="fact.c",fullname="/mnt/hgfs/gondow/project/linux-x86-64-programming/src/asm/fact.c",line="13",thread-groups=["i1"],times="1",original-location="main"}
~"\n"
~"Breakpoint 1, main () at fact.c:13\n"
~"13\t printf (\"%d\\n\", fact (5));\n"
*stopped,reason="breakpoint-hit",disp="keep",bkptno="1",frame={addr="0x0000555555555180",func="main",args=[],file="fact.c",fullname="/mnt/hgfs/gondow/project/linux-x86-64-programming/src/asm/fact.c",line="13",arch="i386:x86-64"},thread-id="1",stopped-threads="all",core="1"
(gdb)
quit
&"quit\n"
=thread-exited,id="1",group-id="i1"
gdbのMIは「CSVのようなもの」です.- ❶
b mainとブレークポイントの設定をすると, ❷&"b main\n"と入力したコマンドが返り, その結果 ❸~"Breakpoint 1 at 0x1180: file fact.c, line 13.\n"と 付属情報が表示されます ❹=breakpoint-created,bkpt={number="1",type="breakpoint",disp="keep",enabled="y",addr="0x0000000000001180",func="main",file="fact.c",fullname="/mnt/hgfs/gondow/project/linux-x86-64-programming/src/asm/fact.c",line="13",thread-groups=["i1"],times="0",original-location="main"}各行は1行で,カンマ,などの区切り子(デリミタ)で区切られており, プログラムで処理しやすい出力になっています. - JSONで出力してくればいいのにと思ったり.
gdbのMI出力をJSONに変換するツールpygdbmi はあるようです(試していません).
遠隔デバッグ (gdbserver, target remote)
gdbは遠隔デバッグが可能です.
遠隔デバッグとは,デバッグ対象のプログラムが動作しているマシンとは
異なるマシン上でデバッグすることです.
リソースが貧弱な組み込みシステムなどで,遠隔デバッグは有用です.
ここでは(簡単のため)ローカルホスト,つまり同じマシン上で遠隔デバッグをしてみます
まず予め gdbserverをインストールしておく必要があります.
$ sudo apt install gdbserver
gdbserverを使ってデバッグしたいプログラムa.outを起動します.
$ gdbserver :1234 ./a.out
Process ./a.out created; pid = 5195
Listening on port 1234
:1234は遠隔でバッグに使用するポート番号です.
$ ❶ gdb ./a.out
Reading symbols from ./a.out...
(gdb) ❷ target remote localhost:1234
Remote debugging using localhost:1234
Reading /lib64/ld-linux-x86-64.so.2 from remote target...
(gdb) ❸ c
Continuing.
Reading /lib/x86_64-linux-gnu/libc.so.6 from remote target...
[Inferior 1 (process 5195) exited normally]
(gdb)
gdbを起動して(❶),デバッグ対象を 遠隔で対象はlocalhost:1234と指定します(❷). (localhostを省略して:1234だけ指定してもOKです).- デバッグ対象のプログラムはすでに実行を開始しているので,
❸
cで実行を再開します.その後は通常のgdbと同様の操作が可能です.
トレースポイント (trace, actions, collect, tstart, tstop, tfind, tdump, tstatus)
通常,gdbを使う時はブレークポイントを使ってプログラムを一時的に停止させて,
対話的にデバッグ作業を行います.
一方,トレースポイントを使うとプログラムを一時停止させずに,
プログラムの動作を観察できます.
手順は以下の通りです.
- 遠隔デバッグでプログラムを
gdbの監視下に置きます. (現在,トレースポイントは遠隔デバッグでのみ有効です). traceとcollectを使って,観察したい場所とデータを事前に設定します.tstartとtstopを使って,プログラムのデータ収集の開始と停止を指示します.- 事後に
tfind,tdump,tstatusで収集したデータを調査します.
$ gcc -g -static fact.c
$ gdbserver :1234 ./a.out
Process ./a.out created; pid = 5696
Listening on port 1234
ここでは簡単のため静的リンクでコンパイルしたa.outを使って
遠隔でバッグの準備をします.
$ gdb ./a.out
Reading symbols from ./a.out...
(gdb) target remote :1234
Remote debugging using :1234
0x0000000000401620 in _start ()
(gdb) ❶ trace fact
Tracepoint 1 at 0x401754: file fact.c, line 5.
(gdb) ❷ actions
Enter actions for tracepoint 1, one per line.
End with a line saying just "end".
>❸ collect n
>end
(gdb) b 14
Breakpoint 2 at 0x4017a1: file fact.c, line 14.
(gdb) ❹ tstart
(gdb) c
Continuing.
Breakpoint 2, main () at fact.c:14
14 }
(gdb) ❺ tstop
(gdb) ❻ tstatus
Trace stopped by a tstop command ().
Collected 6 trace frames.
Trace buffer has 5237852 bytes of 5242880 bytes free (0% full).
Trace will stop if GDB disconnects.
Not looking at any trace frame.
Trace started at 135843.311816 secs, stopped 5.701432 secs later.
(gdb) ❼ tfind start
Found trace frame 0, tracepoint 1
#0 fact (n=5, n@entry=<error reading variable: PC not available>) at fact.c:5
5 if (n <= 0)
(gdb) ❽ tdump
Data collected at tracepoint 1, trace frame 0:
n = 5
(gdb) ❾ tfind
Found trace frame 1, tracepoint 1
#0 fact (n=4, n@entry=<error reading variable: PC not available>) at fact.c:5
5 if (n <= 0)
(gdb) tfind
Found trace frame 2, tracepoint 1
#0 fact (n=3, n@entry=<error reading variable: PC not available>) at fact.c:5
5 if (n <= 0)
(gdb) tfind
Found trace frame 3, tracepoint 1
#0 fact (n=2, n@entry=<error reading variable: PC not available>) at fact.c:5
5 if (n <= 0)
(gdb) tfind
Found trace frame 4, tracepoint 1
#0 fact (n=1, n@entry=<error reading variable: PC not available>) at fact.c:5
5 if (n <= 0)
(gdb) tfind
Found trace frame 5, tracepoint 1
#0 fact (n=0, n@entry=<error reading variable: PC not available>) at fact.c:5
5 if (n <= 0)
(gdb) quit
-
事前準備をします. 関数
factにトレースポイントを設定します(❶trace fact). コマンド付きブレークポイントのcomandsと同じ要領で, ❷actionsを使って,トレースポイントで収集するデータや動作を指定します. ここでは単に引数$n$の値を収集します (❸collect n). -
❹
tstartでデータの収集を開始します.continueでプログラムの実行を再開すると, トレースポイントにヒットした情報が集められます. ❺tstopで収集を終了します. -
事後の調査をします. ❻
tstatusで収集状況を調べると, 6回トレースポイントにヒットしてデータを収集していました (Collected 6 trace frames). ❼tfind startで最初の収集データを見ます. ❽tdumpとするとその収集データの内容を全て表示します (が,ここでは引数nの値しか表示されません). 引数無しで ❾tfindとすると,次の収集データを表示します. 引数nの値が,6から0まで変化したことが分かりました.
トレースポイントに関する付記:
traceにはifを使って ヒットする条件を指定可能です.traceはint3などのトラップ命令を使って計装(instrumentation)するので遅いです.ftraceを使うと5バイト長のジャンプ命令を使って計装するので高速になります (が,計装位置の命令長が5バイト以上必要です). (試していませんが)静的計装を行うstraceもあります.
実行の記録とリプレイ,逆実行 (record full, reverse-step)
gdbでは実行状態の記録とリプレイが可能です.
またリプレイ機能を使って逆実行も可能です.
リプレイでは実際には機械語命令の実行を行わず,
実行ログの内容を使って,メモリやレジスタの値を変化させます.
$ gcc -g fact.s
$ gdb ./a.out
Reading symbols from ./a.out...
(gdb) b main
Breakpoint 1 at 0x40177c: file fact.c, line 13.
(gdb) r
Breakpoint 1, main () at fact.c:13
13 printf ("%d\n", fact (5));
(gdb) ❶ record full
(gdb) b fact if n==0
Breakpoint 2 at 0x401754: file fact.c, line 5.
(gdb) c
Continuing.
Breakpoint 2, fact (n=0) at fact.c:5
5 if (n <= 0)
(gdb) reverse-TABTAB
reverse-continue reverse-next reverse-search reverse-stepi
reverse-finish reverse-nexti reverse-step
(gdb) ❷ reverse-next
8 return n * fact (n - 1);
(gdb) (改行のみ入力,以下も同様)
5 if (n <= 0)
(gdb)
8 return n * fact (n - 1);
(gdb)
5 if (n <= 0)
(gdb)
8 return n * fact (n - 1);
(gdb) ❸ print n
$1 = 3
(gdb) quit
-
❶
record fullで実行状態の記録を開始します. ソフトウェア的に全実行状態を保存します. (当然,メモリを激しく消費します). -
❷
reverse-nextなど逆実行用のステップ実行を行うと, 逆実行できます(実際には実行はせず,元の状態に戻すだけですが). ❸nの値が3の状態まで戻りました.
実行の記録とリプレの付記:
record fullではなく,record btrace ptなどとすると, ハードウェア機能(例えば,Intel PT)を使った高速な記録になりますが, リングバッファを使うため,バッファがあふれると古いデータは捨てられます.record stopとすると実行の記録を止めて実行ログは破棄されます. 実行ログはrecord save ファイル名,record restore ファイル名で 保存や回復が可能です.上の例だとファイルサイズは約700KBでした.- x86-64ではAVX命令などが非対応のようです.
例えば,AVX512の
vmovdqu命令を試すと,以下のエラーとなりました(2023/8/27現在).
main () at movdqu.s:23
23 vmovdqu (%rsp), %ymm0
(gdb)
Process record does not support instruction 0xc5 at address 0x555555555171.
Process record: failed to record execution log.
(gdb)
glibcなどのライブラリは-O2などでコンパイルされているため,
AVX命令が使われることが多くあります.
試しにhello.cで試した所,同じエラーとなりました.
(gdb) n
5 printf ("hello\n");
(gdb) n
Process record does not support instruction 0xc5 at address 0x7ffff7d9d969.
Process record: failed to record execution log.
macOS上でgdbを使うには?
2024/3/11現在,Apple Silicon Mac上ではgdbは使えないようです.
lldbを使いましょう.