gdb (GNU Debugger) のコマンド、わりと毎回調べ直してる気がするのでとりあえずまとめとく. シェル内での操作は先頭に $、gdb 内での操作は先頭に (gdb) と付けているが、これらは実際に入力する必要はないので注意. なお、基本的にすべて 32 bit の Linux 環境での実行結果となる.

準備

C プログラムを gdb でデバッグしたい場合、gcc などのコンパイラにデバッグオプションを渡してからそのファイルをコンパイルする. hello.c を gcc でコンパイルするなら、以下のようにして -g フラグを渡してコンパイルする.

$ gcc -o hello hello.c -g -Wall

-Wall フラグは警告を可能な限り多く表示するフラグ. とりあえず有効にしておいたほうが無難らしい.

なお、実は -g オプションを付けずにコンパイルした実行ファイルを gdb でデバッグ (ブレークポイント設定やステップ実行) することも可能ではある.

ただし、実行ファイル中には “各機械語命令が元となった C 言語ソースファイルのどの部分に対応するのか” というシンボル情報が含まれなくなるため、C 言語ソースコードレベルのデバッグはできなくなる.

よってアセンブリコードレベルのデバッグだけが可能である.

このため、C 言語ではなく全てアセンブリで書かれたコードをアセンブル・リンクして得られる実行ファイルも gdb を用いてデバッグすることができる. x64 アーキテクチャの Linux において hello.asm を nasm と ld を用いてアセンブル・リンクして実行ファイルを得るには、以下のコマンドを実行すれば良い.

$ nasm -o hello.o -f elf64 hello.asm
$ ld -o hello hello.o

gcc のデバッグオプションについての補足

低レベルプログラミング では、gdb でデバッグする場合には (-g オプションではなく) -ggdb オプションが推奨されている. これは .line セクションやローカル変数のシンボルなどといった、gdb が利用できる情報が生成されるらしい. man gcc 中には -ggdb は “可能な限り全ての GDB 拡張を含む” と書かれているため、通常の -g よりも多くの情報を gdb 内から扱えるらしい. (未検証)

コンパイル・アセンブル・リンクについての補足

アセンブリコードから実行ファイルを作成するために nasmld というコマンドを使ったが、ここで nasm はアセンブラであり、ここではアセンブリコード hello.asm からオブジェクトファイル hello.o を生成するために使用している. また、ld はリンカであり、オブジェクトファイル同士をリンクして実行ファイルを生成するために使用する.

このとき、アセンブリコード中にはプログラムのエントリポイントを示すために _start というラベルが定義されている必要があり、この _start というラベルを設定されているアドレスが実行ファイルである ELF ファイルのヘッダでエントリポイントとして設定されるようになっている.

なお、C 言語のプログラムをコンパイルする際に gcc に -S オプションや -c オプションを与えることで、それぞれ gcc による処理をコンパイルまでとアセンブルまでに限定することができる. したがって、gcc からアセンブリコードやオブジェクトファイルを得ることができるが、このようにして得られたオブジェクトファイルを ld でリンクを行おうとしても、そのままでは普通は実行できるファイルを作成できない.

これは、gcc による処理で得られたアセンブリコードやオブジェクトファイル中には “gcc に渡した C 言語ファイル中で記述された内容だけ” が含まれており、printf() をはじめとする標準 C ライブラリの内容は当然含まれていないためである. したがって、実行可能なファイルを作るためには普通はリンカにそのようなライブラリも指定する必要がある.

また、C 言語のプログラムを単にコンパイル・アセンブルして得られたオブジェクトファイル中には実行可能ファイルのエントリポイントとなる _start ラベルが存在しないため、やはり単に ld でリンクしただけでは ELF ファイルのエントリポイントをリンカが見つけられないのでうまく実行ファイルを作成できない.

通常の C 言語プログラムを gcc を呼び出すことでコンパイル・アセンブル・リンク処理を行う時、C 言語プログラムのエントリポイントとなる _start の処理は実は標準 C ライブラリ (glibc など) によって提供されている. すなわち、C 言語のプログラムから実行ファイルを得る場合には普通は glibc などが提供してくれる _start の定義が含まれるアセンブリコードも一緒にリンクしているのである.

このため、“gcc から C 言語プログラムのアセンブリコードを出力できる” からといって、そのコードをそのままアセンブル・リンクすることで実行可能なファイルを生成できるわけではない. 通常は _start の定義を含むアセンブリで書かれたスタートアップ処理を行うためのオブジェクトファイルが暗黙的に gcc によって一緒にリンクされているのである.

すると gcc によって C 言語ファイルからアセンブリコードやオブジェクトファイルを生成するのは全く意味がなさそうに見えるが、そういうわけでもない.

例えばアセンブリコードでプログラムを作る場合を考える. 少なくとも _start を含むアセンブリコードは自分で準備する必要があるが、プログラムの全ての処理を全てアセンブリで記述するのは非常に労力がかかるため、処理の一部は C 言語で書きたいとする. このような場合、C 言語で関数を定義し、そのコードを gcc でコンパイルする際に -S オプションを付与すればその関数の処理をアセンブリコードで出力ができるので、得られたコードを一緒にアセンブル・リンクしてやれば、アセンブリコードでのプログラミングの一部を C 言語で記述できることとなる.

ただし、C 言語で記述した関数内で標準 C ライブラリの関数を参照しているような場合には、やはりリンクする際に標準 C ライブラリも含める必要が出てくるので注意が必要である.

起動と終了

起動は単に gdb コマンドの後ろにデバッグしたい実行ファイル名を続ければ OK. -q フラグを与えると、起動時のライセンスに関するメッセージが表示されずに起動する. (quiet の q かな)

$ gdb hello
$ gdb -q hello

終了は quit. exit ではダメっぽいね.

(gdb) quit

実は q だけでも OK. 多くの gdb のコマンドははじめの 1 文字だけでも認識される. (すべてのコマンドがそうなのかは知らないけど…)

(gdb) q

TAB キーを押せば入力途中のコマンドを補完することもできる. したがって q とだけ入力した状態で TAB キーを押せば quit と補完されるはず.

なお、先に gdb をシェルから起動させた後でから実行ファイルを読み込むこともでき、その場合には file を使用する.

# 以下は $ gdb -q hello と同じ.
$ gdb -q
(gdb) file hello

実行

実行は run あるいは r.

(gdb) run
(gdb) r

なお、コマンドライン引数を渡したい場合には run の後に引数を続けて記述するか、set args コマンドで run の際の引数を設定しておくことができる.

# run の後ろに直接引数を記述する.
(gdb) run 1 2 3

# あるいは予め set args コマンドで引数を設定してから run する.
(gdb) set args 1 2 3
(gdb) run

# set args で設定した引数を解除したい場合には、単に set args と入力すれば良い.
(gdb) set args

関数の先頭にブレークポイントを設定する

単に実行するだけならデバッガ使う意味がないので、まぁブレークポイントを張ったりしたい. 特定の関数内に入ったらプログラムの動作を停止させたいなら、関数名に対してブレークポイントを貼ればよい. その場合、break あるいは b の後ろに続けて関数名を記述する. 例えば、main 関数の先頭でプログラムを停止させる場合には以下のようにすればよい.

(gdb) break main
(gdb) b main

問題なくブレークポイントが脹れたら Breakpoint 1 at 0x80482c5: file hello.c, line 5. のように表示される.

この状態で run あるいは r でプログラムを実行すれば、設定したブレークポイントの位置でプログラムの動作が停止する.

実行の再開

ブレークポイントの位置で停止したプログラムの実行を再開する場合には continue あるいは c を使用する.

(gdb) continue
(gdb) c

さらにブレークポイントが設定されている場合にはその行でプログラムが再び停止することとなる. ブレークポイントが設定された行を実行しない場合にはそのままプログラム終了まで処理が進む.

なお、continue の代わりに run を実行するとプログラムを最初から実行し直すことになるので注意.

ソースコードを表示する

ブレークポイントで止めても流石にソースコードが見られないと不便. ソースコードを表示する場合には list あるいは l を使用する.

(gdb) list
(gdb) l

なお、list は基本的に 現在のソースファイルの先頭から 10 行分ずつソースコードを表示する 仕様である. 当然普通のソースコードは 10 行以上あるだろうが、表示されなかった分のコードは list を 実行するごとに順番に 10 行ずつ表示される.

特定の行の周辺のソースコードを確認したい場合には list の後ろに行番号を付与する. 例えば、以下のように記述すればソースコードの 13 行目を中心に 10 行分のソースが表示される.

(gdb) list 13
(gdb) l 13

また、特定の関数の周辺のソースコードを確認したい場合には list の後ろに関数名を付与すればよい. 例えば、以下のように記述すれば func 関数の先頭を中心に 10 行分のソースが表示される.

(gdb) list func
(gdb) l func

表示しきれなかった残りのソースコードは list あるいは l を実行するごとに 10 行ずつ表示される. ソースコードを逆方向に表示したい (すなわち下から上の方向に表示する) 場合には listl の 代わりに list -l - が使用できる.

(gdb) list -
(gdb) l -

layout 表示とステップ実行

listl でソースコードが確認できると言っても、正直このコマンドを毎回実行するのはちょっと面倒. もう少し視覚的にデバッグを行うための方法として、layout コマンドがある.

layout src あるいは la src と入力することで gdb の画面表示が変化し、ソースコードを表示しながら デバッガを動かすことができるようになる.

(gdb) layout src
(gdb) la src

これにより、例えば main 関数の先頭でブレークポイントで停止しているデバッグ状態が以下のように 表示される.

(gdb) layout src

現在停止中の行が反転表示されていてわかりやすい. なお、左端の B+ はブレークポイントのマークである.

さて、この状態でプログラムをステップ実行をしてみる. ステップ実行はプログラムを 1 命令ずつ実行する ことができるデバッガの機能だ.

gdb では stepnext という 2 種類のステップ実行コマンドが使える. step は関数呼び出しの際に 関数の内部に入っていく (ステップイン) が、next はその関数呼び出しそのものを 1 命令とみなしてそのまま次の行に移る (ステップオーバー).

(gdb) step
(gdb) next

例えば上の図の場合、関数呼び出し func() の直前で停止している. この時に next を実行すると func() の終了後の行、すなわち 8 行目の func2() に移動する. (step では func 関数の内部まで入っていく.)

関数呼び出しによって関数の内側に入った際、外側の関数に戻るには step および next でステップ実行していって return を実行する他に、finish を使用することで即座に現在の関数内の処理を実行して外側の関数に戻る (すなわちステップアウトする) ことができる.

(gdb) finish

ちなみに、layout には C のソースコードレベルでの表示機能以外に、アセンブリ命令レベルでの表示を行う layout asm も存在する.

(gdb) layout asm

上の図と同じ状態を layout asm で表示すると以下のようになる.

(gdb) layout asm

Intel 構文ではなく AT&T 構文なので (GNU 系列なので当然 nasm ではなく as で採用している記法が使われる) mov による代入の方向は 2nd arg -> 1st arg ではなく、1st arg -> 2nd arg の方向となるので注意. また、数値リテラルには $、レジスタ名には % が接頭辞として与えられるようである. すなわち、mov $0x0, %eax はレジスタ EAX に対して数値 0 を代入するという命令である.

さらに、アセンブリレベルでのステップ実行のために stepinexti という命令が用意されている. それぞれ、省略形は si および ni である.

(gdb) stepi
(gdb) nexti

これらの違いは stepnext の違いと同様である.

さらに、layout regs という現在のレジスタの状態を列挙してくれる機能もある. 超便利.

(gdb) layout regs

(gdb) layout asm と (gdb) layout regs

このように layout で複数のパネルが表示されている場合、Ctrl-x + o で選択中のパネルを切り替えることができ、カーソルキーの上と下で現在選択中のパネルの内容をスクロールすることができる.

なお、layout srclayout asm を使っている状態でプログラムの標準出力によって画面が崩れてしまった場合には、Ctrl-l で表示を直すことができるようになっている.

layout srclayout asm による表示を終了したい場合には Ctrl-x + a を押せばよいらしい.

たまに Ctrl-x をメタキーとして受け付けてくれないときもあってちょっと困ってる…

同じコマンドを繰り返し実行する

何も入力せずにエンターキーだけを押すことで直前に実行したコマンドを繰り返し実行することができる.

nextstep の際に重宝する.

任意の行にブレークポイントを設定する

関数の先頭以外でもブレークポイントを設定したい場合には、breakb の後ろにブレークポイントを 設定したい位置を書けばよい.

ただし、layout srclayout asm の例からわかるように、プログラムの実行ファイルを C 言語レベルで デバッグするのかアセンブリレベルでデバッグするのかによってブレークポイントの設定単位も変わってくる.

まず、layout src を設定しているときのように C 言語レベルでデバッグしている際に、現在の ソースファイルの 10 行目にブレークポイントを貼る場合には以下のように書けばよい.

(gdb) break 10

単に行番号を書けばよいだけなので簡単だ.

逆に layout asm を設定しているときのようにアセンブリレベルでデバッグしている際に、例えば メモリアドレス 0x80482d4 の位置でブレークポイントを設定したいという場合には、以下のように書く.

(gdb) break *0x80482d4

アドレスの先頭に * を書くのを忘れないようにする.

ブレークポイントの一覧確認とブレークポイントの有効化/無効化

現状設定されているブレークポイントの一覧を表示したい場合には、info breakpoints が使用できる. これによって各ブレークポイントが 1 から順番に番号が振られた状態で列挙される.

(gdb) info breakpoints

列挙されたブレークポイントのうち、特定のブレークポイントを一時的に無効にしたい場合には disable コマンドが使える. disable の後ろに無効にしたいブレークポイントの番号を書けばよい.

(gdb) disable 1

スペース区切りで番号を並べることで複数のブレークポイントを一気に無効化できる.

(gdb) disable 1 2 3

ブレークポイントを無効にすると info breakpoints で表示した際に Enb という項目が n と表示されるようになる.

逆に無効にしておいたブレークポイントを有効にしたい場合には enable コマンドを使う.

(gdb) enable 1

また、設定したすべてのブレークポイントを削除する場合には delete breakpoints を使用する.

(gdb) delete breakpoints

簡単な変数の中身の確認 (C 言語)

gdb 上で変数の中身を表示することができる. 基本となるコマンドは print あるいは p である.

(gdb) print 変数名
(gdb) p 変数名

例えば int i = 10; のように定義されている int 型変数 i があった場合には、 p i のように入力すれば、その時点での変数 i の中身を表示することができる.

また、int a[3] = {10, 20, 30}; のような配列変数の場合には、p a とすれば配列のすべての要素が一度に表示される. 特定の要素だけを表示したい場合には、p a[1] のようにして添字を指定すれば良い. なお、C 言語においては配列変数の名前はその先頭の領域を参照するポインタと見なせるので、p *(a+1) のようなポインタ演算を用いた記述も可能である.

なお、display という便利なコマンドもある. このコマンドはステップ実行のたびに指定しておいた変数の値を毎回表示してくれる. これにより、特定の変数の中身をステップ実行のたびに追いかけることができる.

(gdb) display 変数名

display i しておけば、next などによるステップ実行のたびに i の値が毎回表示されることになる.

ウォッチポイントを置いて特定の変数が書き換えられた瞬間にブレークさせる

GDB のウォッチポイントという機能を使うことで、特定の変数の値が書き換えられた箇所でプログラムの動作をブレークさせることができる. ウォッチポイントの設定には watch を使用する.

(gdb) watch 変数名

コールスタックの確認とフレームの切り替え

関数内部に設定したブレークポイントでプログラムが停止している際、その関数がどの関数から呼び出されているのかという関数呼び出しのスタック (コールスタック) を確認するためには、wherebacktraceinfo stack のいずれかのコマンドを使えば良い. (いずれもエイリアスとなっている)

(gdb) where

(gdb) backtrace

(gdb) info stack

また、例えば特定の関数内部のブレークポイントで停止している際にその関数を呼び出している外側の関数内で定義された変数を確認したい場合には、frame コマンドを使用することで着目しているフレームの切り替えを行えば良い.

例えば以下の例では write() 関数の内部でブレークしている状態で main() 関数の argc の値を確認しているが、write() 関数内で print argc を実行しても No symbol "argc" in current context. というエラーが発生している. したがって、frame コマンドを使ってフレームを main() 関数のフレームである 8 番 (フレーム番号は where などのコマンドの結果からわかる) へと切り替えることで argc の値を確認できるようにしている.

(gdb) where
#0  write () at ../sysdeps/unix/syscall-template.S:81
#1  0x08050f08 in _IO_new_file_write (f=0x80df1e0, data=<value optimized out>, n=44) at fileops.c:1251
#2  0x08050bcc in new_do_write (fp=0x80df1e0,
    data=0xb7fff000 "Hello World! 1 /home/user/build/hello/hello\n", to_do=<value optimized out>)
    at fileops.c:506
#3  0x08050e95 in _IO_new_do_write (fp=0x80df1e0,
    data=0xb7fff000 "Hello World! 1 /home/user/build/hello/hello\n", to_do=44) at fileops.c:482
#4  0x08051a3d in _IO_new_file_overflow (f=0x80df1e0, ch=-1) at fileops.c:839
#5  0x08050d07 in _IO_new_file_xsputn (f=0x80df1e0, data=0x80be37e, n=1) at fileops.c:1319
#6  0x0807ce8d in _IO_vfprintf_internal (s=0x80df1e0, format=<value optimized out>,
    ap=0xbffff53c "\210\201\004\b\030\361\r\b\210\201\004\b\270\365\377\277v\205\004\b\001")
    at vfprintf.c:1673
#7  0x0804deb1 in __printf (format=0x80be36c "Hello World! %d %s\n") at printf.c:33
#8  0x080483c2 in main (argc=1, argv=0xbffff5e4) at hello.c:5

(gdb) print argc
No symbol "argc" in current context.

(gdb) frame 8
#8  0x080483c2 in main (argc=1, argv=0xbffff5e4) at hello.c:5
5         printf("Hello World! %d %s\n", argc, argv[0]);

(gdb) print argc
$1 = 1

各レジスタの値の一括表示

アセンブリコードのデバッグを行っている際、info registers コマンドで各レジスタ中の値を一括表示することができる.

(gdb) info registers

指定した関数の命令を逆アセンブルする

指定した関数の処理内容を逆アセンブルするには disassemble コマンドが使用できる. 例えば main() 関数の内容を逆アセンブルする場合は以下のように書けばよい.

(gdb) disassemble main

また、関数の指定はその関数のアドレスで行うこともでき、例えば main() 関数が 0x80482bc というアドレスに存在する場合には以下のように書くこともできる.

(gdb) disassemble 0x80482bc

なお、layout asm によるアセンブリコードの表示中に disassemble コマンドを実行すると、layout asm によるアセンブリコードの表示が disassemble コマンドで指定した関数のものに変更される.

layout asm によるアセンブリコード表示の構文を変更する

layout asm を実行した場合、デフォルトではアセンブリコードは as で採用されている AT&T 構文で表示される. これを nasm などで採用されている Intel 構文で表示するようにするには、以下のコマンドを実行すればよい.

(gdb) set disassembly-flavor intel

すでに layout asm でコードを表示している状態でこのコマンドを実行した場合、一度他の関数内に入るなどして表示が更新されないと反映されないため注意すること.

なお、AT&T 構文に戻したい場合には以下のコマンドを実行すれば良い.

(gdb) set disassembly-flavor att

このような設定を毎回入力するのは手間であるので、設定ファイル ~/.gdbinit を作成しておきその中にこれらの設定を記述しておくとよい.

コアダンプを解析する

コアダンプを gdb で解析することでプログラムの停止箇所を判定することができる.

$ gdb -q binary_file core_dump_file

この状態で where を実行すれば停止した箇所のスタックトレースが確認できるほか、layout asm を実行することで実行停止した命令を確認することができる.

高度な変数の中身の確認 (C 言語 / アセンブリ)

もう少し細かい値表示のコマンドについて.

まず、値の表示には print (省略名は p) と x という 2 種類のコマンドが使える. これらのコマンドは、print には表示したい値そのものを渡し、x には表示したい値のアドレスを渡すという違いがある.

print /FMT <val> : レジスタまたはメモリの値を参照できる. レジスタ名は先頭に $ をつけて $eax のようにする.

x /FMT <address> : print と同様に値のチェックに使えるが、ポインタを受け取る点が異なる.

(FMT にはフォーマットを指定する. 後述.)

例えば、/home/user/hello/hello abc def のようにして実行ファイル hello を実行したとする.

argc の値を表示したい場合には print および x を用いてそれぞれ以下のように書くことができ、どちらの結果からも argc に 3 が入っていることを確認できる. なお、変数のアドレスを得るには C 言語と同様に & を変数名の前に付与すればよい.

(gdb) print argc
$1 = 3

(gdb) x &argc
0xbffff550:     0x00000003

また、参照先のアドレスからその中の値を得るにはやはり C 言語と同様に * を変数名の前に付与すればよいので、以下のように書けば argc のアドレスを得た後に、そのアドレスの中身を確認することになり、print argc と書いた場合と全く同じ結果が得られる.

(gdb) print *(&argc)
$2 = 3

なお、printx には表示の仕方をフォーマット指定することができるようになっており、コマンド名と表示したい値やアドレスの間に / から始まるフォーマット文字列を書くことができる.

例えば、print コマンドで argv[0] をフォーマットを指定して表示するには以下のようにする.

(gdb) print argv[0]
$1 = 0xbffff732 "/home/user/hello/hello"

# アドレスとして表示したい場合.
(gdb) print /a argv[0]
$2 = 0xbffff732

# 文字列として表示したい場合.
(gdb) print /s argv[0]
$3 = 0xbffff732 "/home/user/hello/hello"

# 文字として表示したい場合.
(gdb) print /c argv[0]
$4 = 50 '2'

ここで /c を指定した場合には結果が '2' という 1 文字となっているが、これは argv[0] の値、すなわちポインタ配列 argv の 0 番目に格納されている「"/home/user/hello/hello" という文字列の先頭のアドレス 0xbffff732」を char 型の値として見なそうとした結果が 50 (16 進数では 0x32) となり、この数値に対応する文字が '2' となることを示している. すなわち、char ch = 0xbffff732; という代入処理をしたあとに ch の値を調べていることに相当する.

print コマンドで使われるフォーマット文字には以下のようなものがある.

フォーマット文字 効果 使用例
o 8 進数表示 print /o 8 # => 010
x 16 進数表示 print /x 16 # => 0x10
u 符号なし 10 進数表示 print /u -1 # => 4294967295
t 2 進数表示 print /t 127 # => 1111111
f 浮動小数点数表示 print /f 0x40000000 # => 2
a アドレス表示 print /a argv # => 0xbffff5f4
c 文字表示 print /c 65 # => 65 'A'
s 文字列表示 print /s argv[1] # => 0xbffff749 "abc"

x コマンドも print コマンドと同様に / から始まるフォーマット文字列の指定が可能となっているが、x コマンドでのフォーマット指定では print コマンドにはなかったサイズ修飾子 (size modifier) も指定できるようになっている.

これはアドレスを直接扱ったり、アセンブリコードのデバッグを行うようになると非常に重要となってくる. 例えば、print argv[0] の結果は以下のようになっていた.

(gdb) print argv[0]
$1 = 0xbffff732 "/home/user/hello/hello"

この結果から、コマンドライン引数として渡した "/home/user/hello/hello" という文字列の先頭アドレスが 0xbffff732 であることがわかる. C 言語でこのアドレス argv[0] のデリファレンスを行う場合、*argv[0] と書くのであった. 文字列配列の先頭アドレスの中にはその文字列の最初の文字が格納されていたので、以下のような結果が得られる.

(gdb) print *argv[0]
$5 = 47 '/'

コマンドライン引数 "/home/user/hello/hello" の最初の文字 / が得られている.

さて、今 argv[0] の値は 0xbffff732 であるとわかっているのであるから、このアドレスを使って *0xbffff732 とデリファレンスを行っても同様の結果が得られそうに思える. 試してみよう.

(gdb) print *0xbffff732
$6 = 1836017711

(gdb) print /c *0xbffff732
$7 = 47 '/'

すると、/c というフォーマット指定を行わないと正しく結果が表示できていないことがわかる. print *argv[0] としたときには正しく表示できたのに print *0xbffff732 というフォーマット指定が必須となるようだ.

なぜだろうか. これは、0xbffff732 というアドレス情報だけではそのアドレスの値の表示はできず、その値が保存されているメモリ領域の大きさまで分からないと値を正しく表示できない ということを示している.

C 言語のデバッグで値を確認する場合、各変数にはその変数の型が定まっている. したがって、argv[0] と書いた場合にはその値である参照先のアドレスが 0xbffff732 であるという情報以外に、argv[0] の型が char * 型であるという情報から、参照先が char 型であるということがわかっている. そして char 型は 1 byte であるというメモリ領域の大きさの情報がわかっていることから、print *argv[0] だけで argv[0] が参照している文字列の先頭文字を表示することができる.

これに対し、print *0xbffff732 だけではアドレスは分かってもそのメモリ領域の大きさが 1 byte なのか 4 byte なのかあるいは 8 byte なのか全くわからない. print *0xbffff732 の結果が 1836017711 となっているが、恐らくこれは 32 bit マシンの標準サイズである DWORD、すなわち 4 byte と解釈するのがデフォルトとなっていることによるものであろう. この 1836017711 という 10 進数値を 16 進数値に変換してみると 0x6d6f682f となる. これを 1 byte データの列とみなせば、0x6d0x6f0x680x2f となる. アスキーコード表を参照してみると、0x6d'm'0x6f'o'0x68'h'0x2f'/' にそれぞれ対応することがわかる. ということは、0xbffff732 というアドレスから 'm', 'o', 'h', '/'、という文字列が順番に並んでいるのであろうか …というとそうではない. これは Intel 系の CPU がメモリ上では数値をリトルエンディアンで管理していることによるものであり、本来は 0xbffff732 というアドレスから '/', 'h', 'o', 'm' という順で文字が並んでいるのだが、これを 0x2f, 0x68, 0x6f, 0x6d という 4 byte のバイナリ列からなる数値として CPU が認識すると、CPU はこれを 0x6d, 0x6f, 0x68, 0x2f すなわち 0x6d6f682f という数値を表していると解釈するためである.

混乱するが、とにかく 0xbffff732 というアドレスの 1 byte 領域には 0x2f すなわち '/' という文字が格納されていることは確かであるため、print /c *0xbffff732 のようにフォーマットを指定してメモリ領域の大きさを決定することで適切に表示することができるわけである.

文字の場合には char 型が 1 byte であるので print でもうまく表示できるが、では 2 byte の short 型の値が特定のメモリアドレスにあるということがわかっているような場合はどのようなフォーマットを指定したらよいだろうか?

このように適切なフォーマット指定が難しくなるような場合は print よりも x でサイズ修飾子を指定するほうが便利だろう.

(gdb) x /b 0xbffff732
0xbffff732:     0x2f

(gdb) x /cb 0xbffff732
0xbffff732:     47 '/'

最初に /b という指定を行っているが、これは 1 byteを意味するサイズ修飾子である. したがって、x /b 0xbffff7320xbffff732 というアドレスに格納されている 1 byte のデータを表示できる.

この場合には結果は 0x2f という 16 進数値となっているが、これをアスキーコードと解釈して対応する文字を表示したい場合にはフォーマット指定文字 c を加えて /cb と書けばよい. これにより、0xbffff732 に格納されている 1 byte の値を文字として解釈することになり、'/' という文字が表示される.

なお、x でのフォーマットやサイズの指定は本来は /cb のように組み合わせて使うことになるはずだが、/c/b のように片方のみ、あるいは全く指定せずに単に x 0xbffff732 のようにして実行することもできる. その場合のフォーマットやサイズの指定は直前に行ったフォーマット指定やサイズ指定をそのまま使い回すようであるので、x /cb 0xbffff732 を実行した後に x /b 0xbffff732x 0xbffff732 を実行しても x /cb 0xbffff732 を実行した場合と同様のフォーマットで結果が表示される.

x コマンドで使われるサイズ修飾子には以下のようなものがある.

サイズ修飾子 サイズ
b 1 バイト
h ハーフワード (2 バイト)
w ワード (4 バイト)
g ジャイアントワード (8 バイト)

また、x コマンドで使われるフォーマット文字には以下のようなものがある.

フォーマット文字 効果 使用例
o 8 進数表示 x /ob 0xbffff732 # => 057
x 16 進数表示 x /xb 0xbffff732 # => 0x2f
d 10 進数表示 x /db 0xbffff732 # => 47
u 符号なし 10 進数表示 x /uh 0xbffff732 # => 26671
t 2 進数表示 x /th 0xbffff732 # => 0110100000101111
f 浮動小数点数表示 x /fw 0xbffff732 # => 4.63080422e+27
a アドレス表示 x /aw 0xbffff732 # => 0x6d6f682f
c 文字表示 x /cw 0xbffff732 # => 47 '/'
s 文字列表示 x /sb 0xbffff732 # => "/home/user/hello/hello"
i 命令列表示 x /ib 0x80482e8 # => <main+44>: ret

フォーマット文字列 i は指定したメモリアドレスに格納されている内容を命令として解釈した内容を表示する. 例えば、0x80482e9 というアドレスに 0x90 という値が保存されているとすると、x /ib を使用することで x86 で 0x90 という数値に割り当てられている命令である nop を表示することができる.

(gdb) x /xb 0x80482e9
0x80482e9:      0x90

(gdb) x /ib 0x80482e9
   0x80482e9:   nop

なお、printx でレジスタの値やレジスタが参照している内容を表示する場合には、レジスタの名前の先頭に $ を付与することでレジスタを指定する.

例えば、EAX レジスタの中身を確認する場合には以下のように書けばよい.

(gdb) print $eax
$8 = 4

(gdb) print /x $eax
$9 = 0x4

また、ECX レジスタが文字列の先頭アドレスを格納している場合、以下のようにしてその値と文字列を確認できる. /s だけでは正しく表示されない場合は x コマンドのサイズ指定が正しく設定されていない状態となっているため、サイズ修飾子 b を使って /sb と指定する必要がある.

(gdb) x /s $ecx
0xb7fff000:      "Hello World! 3 /home/user/hello/hello\n"

(gdb) x /sb $ecx
0xb7fff000:      "Hello World! 3 /home/user/hello/hello\n"

これにより、ECX レジスタの値は 0xb7fff000 であり、そのアドレスからスタートする文字列が表示される.

同様にしてスタックポインタ ESP の値を確認することもできる. スタックポインタの値は 32 bit マシンでは 4 byte の大きさとなっているはずなので、正しく表示されない場合にはサイズ修飾子 w を使って 4 byte ずつ表示するように認識させる必要がある.

(gdb) x $esp
0xbfffeeb8:     0x92

(gdb) x /xw $esp
0xbfffeeb8:     0x08053d92

これによってスタックポインタ ESP の値は 0xbfffeeb8 であり、そこから 4 byte の領域に 0x08053d92 という値が保存されているということが分かった.

また、関数への引数がスタック経由で渡される場合、スタック上の複数の値を一気に表示できると便利である. その場合には / の直後に表示したい値の個数を書くことで、その個数分だけ連続して値を表示することができる.

以下の場合、スタックポインタ ESP の値である 0xbfffeeb8 から 4 byte ずつ 16 個、すなわち 0xbfffeef7 までのメモリ領域の値を表示している.

(gdb) x /16xw $esp
0xbfffeeb8:     0x08053d92      0x00000026      0x08067671      0x00000001
0xbfffeec8:     0xb7fff000      0x00000026      0x080d68c0      0x00000026
0xbfffeed8:     0xb7fff000      0xbfffef04      0x0806819b      0x080d68c0
0xbfffeee8:     0xb7fff000      0x00000026      0xbfffef24      0x08069732

printx については以下の URL にわかりやすくまとまっている.

例題 1: 特定のアドレスに格納された “関数のアドレス” から参照先の関数を知る

ハロー“Hello,World”OSと標準ライブラリのシゴトとしくみ の 2 章では printf() の処理を gdb を使って辿っていく. その中で、write() システムコールラッパー内から実際に int $0x80 というソフトウェア割り込みを生じる関数へとジャンプする call *0x80d6750 という処理が出てくる.

   ┌──────────────────────────────────────────────────────────────────────┐
   │0x8053d70 <write>               cmpl   $0x0,%gs:0xc                   │
   │0x8053d78 <write+8>             jne    0x8053d9f <write+47>           │
   │0x8053d7a <__write_nocancel>    push   %ebx                           │
   │0x8053d7b <__write_nocancel+1>  mov    0x10(%esp),%edx                │
   │0x8053d7f <__write_nocancel+5>  mov    0xc(%esp),%ecx                 │
   │0x8053d83 <__write_nocancel+9>  mov    0x8(%esp),%ebx                 │
   │0x8053d87 <__write_nocancel+13> mov    $0x4,%eax                      │
  >│0x8053d8c <__write_nocancel+18> call   *0x80d6750                     │
   │0x8053d92 <__write_nocancel+24> pop    %ebx                           │
   │0x8053d93 <__write_nocancel+25> cmp    $0xfffff001,%eax               │
   │0x8053d98 <__write_nocancel+30> jae    0x8056650 <__syscall_error>    │
   │0x8053d9e <__write_nocancel+36> ret                                   │

この処理について考えてみる. まず、0x80d6750 というアドレスの中に格納されている値について調べてみる. call 命令の引数になっているので、32 bit のジャンプ先アドレスが格納されていると考えられる. すなわち、0x80d6750 から 4 byte の領域は関数ポインタ変数に相当するはずである.

(gdb) print /x *0x80d6750
$10 = 0x110414

すると、0x80d6750 というアドレスには 0x110414 という 4 byte の数値が格納されていることがわかる.

x コマンドでも確認してみよう.

(gdb) x /xw 0x80d6750
0x80d6750 <_dl_sysinfo>:        0x00110414

x コマンドでも 0x00110414、すなわち 0x110414 という同じ結果が得られた. x コマンドの結果を見てみると、どうやら 0x80d6750 というアドレスには _dl_sysinfo というラベルが設定されているらしい.

では、0x110414 というアドレスはどのようなアドレスとなっているのだろうか.

(gdb) x /xw *0x80d6750
0x110414 <__kernel_vsyscall>:   0x00c380cd

(gdb) x /xw _dl_sysinfo
0x110414 <__kernel_vsyscall>:   0x00c380cd

0x110414 というアドレスには __kernel_vsyscall というラベルが設定されているということがわかった.

これにより、call *0x80d6750 という命令によって __kernel_vsyscall 内へとジャンプすることがわかる. 実際、ステップ実行してみると確かにこの命令によって __kernel_vsyscall へとジャンプすることを確認できる.

   ┌──────────────────────────────────────────────────────────────────────┐
  >│0x110414 <__kernel_vsyscall>    int    $0x80                          │
   │0x110416 <__kernel_vsyscall+2>  ret                                   │

さらに、disassemble コマンドを使えば 0x110414 というアドレスに書かれている命令を逆アセンブルして確かめることもできる.

(gdb) disassemble 0x110414
Dump of assembler code for function __kernel_vsyscall:
=> 0x00110414 <+0>:     int    $0x80
   0x00110416 <+2>:     ret
End of assembler dump.

したがって、確かに 0x110414 というアドレスは __kernel_vsyscall というラベルを指しており、その中で int $0x80 によるソフトウェア割り込みが実行されていることがわかる.

disassemble コマンドに対して *0x80d6750 を渡しても同様の結果が表示される.

(gdb) disassemble *0x80d6750
Dump of assembler code for function __kernel_vsyscall:
=> 0x00110414 <+0>:     int    $0x80
   0x00110416 <+2>:     ret
End of assembler dump.

例題 2: コマンドライン引数 argv 周りのメモリを調べる

コマンドライン引数 argv 周りのメモリの参照関係を調べてみる. ここでも、/home/user/hello/hello abc def のようにして実行ファイル hello を実行した場合を考えている.

まず、argv[0]argv[1]argv[2] の値をアドレスとして表示してみる.

(gdb) print /a argv[0]
$11 = 0xbffff732
(gdb) print /a argv[1]
$12 = 0xbffff749
(gdb) print /a argv[2]
$13 = 0xbffff74d

さらに、argv[0]argv[1]argv[2] によって参照されているそれぞれの文字列中の各文字のアドレスも確認してみる.

# argv[0] は文字列 "/home/user/hello/hello" を参照している.
(gdb) print argv[0][0]
$14 = 47 '/'
(gdb) print /a &argv[0][0]
$15 = 0xbffff732

(gdb) print argv[0][1]
$16 = 104 'h'
(gdb) print /a &argv[0][1]
$17 = 0xbffff733

(gdb) print argv[0][2]
$18 = 111 'o'
(gdb) print /a &argv[0][2]
$19 = 0xbffff734

(gdb) print argv[0][3]
$20 = 109 'm'
(gdb) print /a &argv[0][3]
$21 = 0xbffff735

# argv[1] は文字列 "abc" を参照している.
(gdb) print argv[1][0]
$22 = 97 'a'
(gdb) print /a &argv[1][0]
$23 = 0xbffff749

(gdb) print argv[1][1]
$24 = 98 'b'
(gdb) print /a &argv[1][1]
$25 = 0xbffff74a

# argv[2] は文字列 "def" を参照している.
(gdb) print argv[2][0]
$26 = 100 'd'
(gdb) print /a &argv[2][0]
$27 = 0xbffff74d

(gdb) print argv[2][1]
$28 = 101 'e'
(gdb) print /a &argv[2][1]
$29 = 0xbffff74e

確かにコマンドライン引数 1 文字 1 文字がどのアドレスに格納されているかを表示できている.

次に文字列へのポインタの配列 argv のアドレスについて調べてみる. アドレスを表示するだけではわからないので、* を付与してデリファレンスした結果がきちんとそれぞれのコマンドライン引数文字列の先頭アドレスになっているかを確認する.

(gdb) print argv
$30 = (char **) 0xbffff5f4
(gdb) print *argv
$31 = 0xbffff732 "/home/user/hello/hello"

(gdb) print argv+1
$32 = (char **) 0xbffff5f8
(gdb) print *(argv+1)
$33 = 0xbffff749 "abc"

(gdb) print argv+2
$34 = (char **) 0xbffff5fc
(gdb) print *(argv+2)
$35 = 0xbffff74d "def"

ここまでをまとめて図示すると以下のようになるだろうか.

         Addr.       Addr.              Addr.        Addr.       Addr.       Addr.
         0xbffff732  0xbffff734         0xbffff740   0xbffff748  0xbffff749  0xbffff74d
              |       |                   |           |            |           |
              |   +---+                   |           +----+ +-----+  +--------+
              |   |                       |                | |        |
              v   v                       v                v v        v
             +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+--+-+-+-+--+-+-+-+--+
             |/|h|o|m|e|/|u|s|e|r|/|h|e|l|l|o|/|h|e|l|l|o|\0|a|b|c|\0|d|e|f|\0|
             +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+--+-+-+-+--+-+-+-+--+
              ^ ^   ^                                        ^        ^
              | |   |                                        |        |
              | |   +------------+                           |        |
              | +-------+        |                           |        |
              |         |        |                           |        |
              |     Addr.       Addr.                        |        |
              |     0xbffff733  0xbffff735                   |        |
              |                                              |        |
              |                                              |        |
              |                                              |        |
              |              +-------------------------------+        |
              |              |                                        |
              +-+            |            +---------------------------+
                |            |            |
          +-----|------+-----|------+-----|------+
          |            |            |            |
argv ---->| 0xbffff732 | 0xbffff749 | 0xbffff74d |
          |            |            |            |
          +------------+------------+------------+
              ^            ^            ^
              |            |            |
            Addr.        Addr.        Addr.
            0xbffff5f4   0xbffff5f8   0xbffff5fc

すると、3 つのコマンドライン引数はすべて連続したメモリ領域に置かれているということがわかる. また、ポインタ配列 argv はそこから大体 0x200 byte 程度離れたところにあるようだ.

続けて、スタックとの関係を調べよう.

まず、このプログラムの元々のソースコードは以下のようになっていたとする.

#include <stdio.h>

int main(int argc, char *argv[]) {
    printf("Hello World! %d %s\n", argc, argv[0]);
    return 0;
}

そして、このプログラムの main() 関数の処理が以下のようなアセンブリコードに変換されていたとする. そして、printf() 関数への call 命令へのジャンプが行われる直前のスタックの状態を考える.

   ┌──────────────────────────────────────────────────────────────────────┐
   │0x80482bc <main>                        push   %ebp                   │
   │0x80482bd <main+1>                      mov    %esp,%ebp              │
   │0x80482bf <main+3>                      and    $0xfffffff0,%esp       │
   │0x80482c2 <main+6>                      sub    $0x10,%esp             │
   │0x80482c5 <main+9>                      mov    0xc(%ebp),%eax         │
   │0x80482c8 <main+12>                     mov    (%eax),%edx            │
   │0x80482ca <main+14>                     mov    $0x80b360c,%eax        │
   │0x80482cf <main+19>                     mov    %edx,0x8(%esp)         │
   │0x80482d3 <main+23>                     mov    0x8(%ebp),%edx         │
   │0x80482d6 <main+26>                     mov    %edx,0x4(%esp)         │
   │0x80482da <main+30>                     mov    %eax,(%esp)            │
   │0x80482dd <main+33>                     call   0x8049360 <printf>     │
   │0x80482e2 <main+38>                     mov    $0x0,%eax              │
   │0x80482e7 <main+43>                     leave                         │
   │0x80482e8 <main+44>                     ret                           │

すると、printf() 関数への引数の準備の仕方から、0x4(%esp) すなわち $esp + 0x4 から 4 byte の領域に argc の値、0x8(%esp) すなわち $esp + 0x8 から 4 byte の領域に argv[0] の値が保存されていることがわかる.

ここから、このアセンブリコードを逆に読んでいくと、argc は元々は 0x8(%ebp) すなわち $ebp + 0x8 から 4 byte の領域に、argv0xc(%ebp) すなわち $ebp + 0xc から 4 byte の領域に保存されていたことがわかる.

また、$ebp のアドレスには main() 関数の呼び出し元となっている関数 (32 bit Linux では標準 C ライブラリ glibc によるスタートアップ処理の一部である __libc_start_main() 関数) のベースポインタの値、$ebp + 0x4 のアドレスには main() 関数終了後に ret 命令によって戻るジャンプ先 (__libc_start_main()main() 関数実行後の処理のアドレス) が保存されている.

元々の argcargv の値が $ebp + 0x8$ebp + 0xc のようにベースポインタ EBP を介して取得されていることから、argcargv の値は main() 実行前のスタートアップ処理 __libc_start_main() 関数によって引数として渡されてきているということがわかる.

まずは EBP レジスタの値を調べてみる.

(gdb) print $ebp
$36 = (void *) 0xbffff548

(gdb) print ($ebp + 0x4)
$37 = (void *) 0xbffff54c

(gdb) print ($ebp + 0x8)
$38 = (void *) 0xbffff550

(gdb) print ($ebp + 0xc)
$39 = (void *) 0xbffff554

次に、これらのアドレス中に含まれている値を確認してみる. これらはそれぞれ 4 byte のアドレスあるいは整数値となっている.

(gdb) x /aw $ebp
0xbffff548:     0xbffff5c8

(gdb) x /aw ($ebp + 0x4)
0xbffff54c:     0x8048478 <__libc_start_main+392>

(gdb) x /xw ($ebp + 0x8)
0xbffff550:     0x00000003

(gdb) x /aw ($ebp + 0xc)
0xbffff554:     0xbffff5f4

これらの結果から、main() 関数の呼び出し元のベースポインタの値は 0xbffff5c8main() 関数の ret 命令によるジャンプ先は 0x08048478argc の値は 3、argv の値は 0xbffff5f4 となっていることが分かった. argv の値は確かに argv[0] の値が格納されているアドレスと一致している.

(gdb) x /aw ($ebp + 0xc)
0xbffff554:     0xbffff5f4

(gdb) x /aw *($ebp + 0xc)
Attempt to dereference a generic pointer.

(gdb) x /aw *((char **) ($ebp + 0xc))
0xbffff5f4:     0xbffff732

(gdb) x /aw 0xbffff5f4
0xbffff5f4:     0xbffff732

(gdb) x /sb 0xbffff732
0xbffff732:      "/home/user/hello/hello"

単に *($ebp + 0xc) としてデリファレンスしようとするとポインタの大きさが分からないためにうまくデリファレンスできないため、char ** 型に一旦キャストしてからデリファレンスを行う必要がある場合がある.

さらに、今度はスタックポインタ ESP との関連を見てみる. 今、printf() 関数への call 命令実行の直前でブレークしているとすると、アセンブリコードの処理内容から、$esp のアドレスには printf() 関数への第一引数であるフォーマット文字列、$esp + 0x4 には argc の値、$esp + 0x8 には最初のコマンドライン引数文字列の先頭アドレス argv[0] が入っているはずである.

確認してみる. まず、現在のスタックポインタの先頭を確認する.

(gdb) print $esp
$40 = (void *) 0xbffff530

続けて、スタック中の値を 4 byte ずつ連続して表示してみる.

(gdb) x /16xw $esp
0xbffff530:     0x080b360c      0x00000003      0xbffff732      0x00000000
0xbffff540:     0x08048be0      0x08048c20      0xbffff5c8      0x08048478
0xbffff550:     0x00000003      0xbffff5f4      0xbffff604      0x00000000
0xbffff560:     0x00000000      0x00000000      0x00000000      0x00000000

すると、スタックの先頭 0xbffff530 から 4 byte には 0x080b360c という値が入っている. これは printf() 関数への第一引数であるフォーマット文字列の先頭アドレスとなっているはずである.

(gdb) x /sb 0x080b360c
0x80b360c <__dso_handle+4>:      "Hello World! %d %s\n"

また、0xbffff5340xbffff538 には argcargv[0] の値が入っていることがわかる.

まとめてみると、printf() 関数実行の直前のメモリの状態は以下のようになっていることとなる.

         Addr.       Addr.              Addr.        Addr.       Addr.       Addr.
         0xbffff732  0xbffff734         0xbffff740   0xbffff748  0xbffff749  0xbffff74d
              |       |                   |           |            |           |
              |   +---+                   |           +----+ +-----+  +--------+
              |   |                       |                | |        |
              v   v                       v                v v        v
             +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+--+-+-+-+--+-+-+-+--+
       +---->|/|h|o|m|e|/|u|s|e|r|/|h|e|l|l|o|/|h|e|l|l|o|\0|a|b|c|\0|d|e|f|\0|
       |     +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+--+-+-+-+--+-+-+-+--+
       |      ^ ^   ^                                        ^        ^
       |      | |   |                                        |        |
       |      | |   +------------+                           |        |
       |      | +-------+        |                           |        |
       |      |         |        |                           |        |
       |      |     Addr.       Addr.                        |        |
       |      |     0xbffff733  0xbffff735                   |        |
       |      |                                              |        |
       |      |              +-------------------------------+        |
       |      |              |                                        |
       |      +-+            |            +---------------------------+
       |        |            |            |
       |  +-----|------+-----|------+-----|------+
       |  |            |            |            |
    +---->| 0xbffff732 | 0xbffff749 | 0xbffff74d |
    |  |  |            |            |            |
    |  |  +------------+------------+------------+
    |  |      ^            ^            ^
    |  |      |            |            |
    |  |    Addr.        Addr.        Addr.
    |  |    0xbffff5f4   0xbffff5f8   0xbffff5fc
    |  |
    |  +---------------------------------------+ argv[0]
    |                                          |
    +------------------------------------------|-+ argv
                                               | |
                    Stack                      | |
                    +---------------------+    | |
Addr. 0xbffff530 -->| (arg1 for printf()) | <------- ESP
                    | 0x080b360c         ----+ | |
                    +---------------------+  | | |
Addr. 0xbffff534 -->| argc                |  | | |    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+--+
                    | (arg2 for printf()) |  +------> |H|e|l|l|o| |W|o|r|l|d|!| |%|d| |%|s|\|n|\0|
                    | 0x00000003          |    | |    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+--+
                    +---------------------+    | |     ^
Addr. 0xbffff538 -->| argv[0]             |    | |     |
                    | (arg3 for printf()) |    | |     +------- Addr.
                    | 0xbffff732         ------+ |              0x080b360c
                    +---------------------+      |
Addr. 0xbffff53c -->| 0x00000000          |      |
                    +---------------------+      |
Addr. 0xbffff540 -->| 0x08048be0          |      |
                    +---------------------+      |
Addr. 0xbffff544 -->| 0x08048c20          |      |
                    +---------------------+      |
Addr. 0xbffff548 -->| base ptr value of   |      |
                    | caller of main()    |      |
                    | 0xbffff5c8          | <------- EBP
                    +---------------------+      |
Addr. 0xbffff54c -->| dest adder of `ret` |      |
                    | inst of main()      |      |
                    | 0x08048478          |      |
                    +---------------------+      |
Addr. 0xbffff550 -->| argc                |      |
                    | (arg1 for main())   |      |
                    | 0x00000003          |      |
                    +---------------------+      |
Addr. 0xbffff554 -->| argv                |      |
                    | (arg2 for main())   |      |
                    | 0xbffff5f4         --------+
                    +---------------------+
Addr. 0xbffff558 -->| envp                |
                    | (arg3 for main())   |
                    | 0xbffff604          |
                    +---------------------+
Addr. 0xbffff55c -->| 0x00000000          |
                    +---------------------+
Addr. 0xbffff560 -->| 0x00000000          |
                    +---------------------+
Addr. 0xbffff564 -->| 0x00000000          |
                    +---------------------+
Addr. 0xbffff568 -->| 0x00000000          |
                    +---------------------+
Addr. 0xbffff56c -->| 0x00000000          |
                    +---------------------+

スタック上の 0xbffff53c から 0xbffff547 までの 12 byte は特に使われておらず、この中に格納されている値は単に何の意味もなさないゴミデータが残っているだけと考えられる.

gdbserver でデバッガ操作とプログラムの出力を分ける

gdbserver というツールを使うことで、gdb でのデバッガの操作とデバッグ対象のプログラムの標準出力の 出力先を分けることができる. これにより、デバッグ対象のプログラムの出力によって gdb の画面が崩れて しまうといったトラブルを避けられる.

使い方は単純で、まず gdb コマンドの他に gdbserver コマンドを同じマシンに別途インストールしておく. 次に、gdb でのデバッグを行うシェルとは別の gdbserver 用のシェルを立ち上げておく.

その後、まずは gdbserver 側のシェルで以下のようにして gdbserver が動作するマシンのアドレスとポート番号を指定し、デバッグ対象のプログラムを読み込む.

$ gdbserver localhost:9000 ./hello

これによって gdbserver がサーバーとして指定されたアドレスとポート番号で立ち上がる.

次にクライアントとなる gdb からこのサーバーに接続を行うこととなる. $ gdb -q ./hello のようにして gdb 側でもデバッグ対象のプログラムを読み込むが、こちらは起動後に target extended-remote コマンドを発行する.

(gdb) target extended-remote localhost:9000

これによって、クライアント側の gdb からサーバーである gdbserver に接続することができた. 後はこの状態で gdb 上で通常のデバッグ操作を行うことができるようになる.

この状態ではデバッグ対象のプログラムによる画面出力は gdbserver が動作している側のシェルに対して出力されるため、 gdb によるデバッガ操作とデバッグ対象のプログラムの出力を分離させることができる. 結構便利かもしれない.

  • gdb 側の画面出力
[user@localhost hello]$ gdb -q hello
Reading symbols from /home/user/hello/hello...done.
(gdb) target extended-remote localhost:9000
Remote debugging using localhost:9000
0x080481c0 in _start ()
Created trace state variable $trace_timestamp for target's variable 1.
(gdb) run
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/user/hello/hello

Program exited normally.
(gdb)
  • gdbserver 側の画面出力
[user@localhost hello]# gdbserver localhost:9000 ./hello
Process ./hello created; pid = 4283
Listening on port 9000
Remote debugging from host 127.0.0.1
Process ./hello created; pid = 4290
Hello World! 1 ./hello

Child exited with status 0

なお、gdb および gdbserver を終了させる場合には、gdb を終了させる前に monitor exit コマンドを入力する必要がある.

(gdb) monitor exit

これを忘れてしまうと、gdbserver がいつまで経っても終了せずに困ることになる. (最初は気づかずに ps ax | grep gdbserver して PID を調べて kill してしまった…)

もしも gdb を先に quit してしまった場合には、再度 gdb を起動して target extended-remotegdbserver に接続してから monitor exit をすればよい.

ヘルプ

gdb の各コマンドのヘルプを見るには help コマンドを使用する. 例えば help info と入力すれば、info breakpointsinfo registers などの info のサブコマンド一覧を確認できる.

(gdb) help <コマンド名>

参考文献