検索
連載

試行錯誤のデバッグで探る、printf()内のポインタ経由での関数呼び出しが行き着く先とはmain()関数の前には何があるのか(3)(4/4 ページ)

C言語の「Hello World!」プログラムで使われる、「printf()」「main()」関数の中身を、デバッガによる解析と逆アセンブル、ソースコード読解などのさまざまな側面から探る連載。今回は、printf()内のポインタ経由での関数呼び出しが行き着く先を試行錯誤のデバッグで探る。

Share
Tweet
LINE
Hatena
前のページへ |       

ブレークポイントを整理する

 このようにcall命令を探してはnextiでメッセージが出力されるかどうか確認し、新たにブレークポイントを設定して実行し直してということを繰り返していけば、どの関数の先でメッセージ出力が行われているのかを突き詰めていくことができる。

 そしていずれはメッセージ出力の核の部分、実際の出力が行われる最後の1命令まで迫ることができるはずだ。

 今までわかったことをまとめてみよう。hello.cのメッセージ出力処理は、以下のような関数呼び出しによって行われているということになる。

main()→printf()→vfprintf()→_IO_new_file_xsputn()→(不明)→メッセージ出力処理

 次は_IO_new_file_xsputn()にブレークポイントを張り、再びrunで実行する。

(gdb) break _IO_new_file_xsputn
Breakpoint 4 at 0x80673a8
(gdb) run
The program being debugged has been started already.
Start it from the beginning? (y or n)
Starting program: /home/user/hello/hello
 
Breakpoint 1, main (argc=1, argv=0xbffffc14) at hello.c:5
(gdb)

 ブレークポイントが増えてきたので、ここらで一度整理しておこう。

 まずinfo breakpointsでブレークポイント一覧を出力する。アセンブラ表示をしているため画面に入りきらない場合には、「Ctrl+X」の後に続けて「A」を押すことで、リスト表示のON / OFFを一時的に切替えられる。

(gdb) info breakpoints
Num	Type		Disp Enb Address	What
1	breakpoint	keep y	0x080482c5 in main at hello.c:5
	breakpoint already hit 1 time
2	breakpoint	keep y	0x080591d3 <vfprintf+19>
3	breakpoint	keep y	0x08059735 <vfprintf+1397>
4	breakpoint	keep y	0x080673a8 <_IO_new_file_xsputn+8>
(gdb)

 _IO_new_file_xsputn()以外のブレークポイントは一時的に無効にしてしまおう。これには以下のようにdisableコマンドでブレークポイントの番号(上記info breakpointsの出力の左端の値)を指定すればいい。

(gdb) disable 1 2 3

 もう一度、ブレークポイントの一覧を見てみよう。

(gdb) info breakpoints
Num	Type		Disp Enb Address	What
1	breakpoint	keep n	0x080482c5 in main at hello.c:5
	breakpoint already hit 1 time
2	breakpoint	keep n	0x080591d3 <vfprintf+19>
3	breakpoint	keep n	0x08059735 <vfprintf+1397>
4	breakpoint	keep y	0x080673a8 <_IO_new_file_xsputn+8>
(gdb)

 1〜3番までは「Enb」の部分が「n」になっており、無効化されていることがわかる。

さらに深く追っていく

 あとはさらに調査を進めていくだけだ。実際の作業はブレークポイントを張ってはステップ実行でメッセージ出力が行われるかどうかを確認するだけの単調なものになるので読み飛ばしていただいても構わないが、ここでは筆者が実施した手順について、いちおう説明しておこう。

 「Ctrl+X」→「A」でリスト表示を元に戻し、continueしてみよう。今度は図2.23のようにして、_IO_new_file_xsputn()の先頭まで一気に進むことだろう。

図2.23: _IO_new_file_xsputn()でブレークする
図2.23: _IO_new_file_xsputn()でブレークする

 そしてcontinueしてみるが、メッセージは出力されず、再度_IO_new_file_xsputn()でブレークする。さらにcontinueしても同じだ。3回目と4回目のcontinueも同様だ。

 そして5回目のcontinueでようやく図2.24のようになり、メッセージが出力された。

図2.24: 5回目のcontinueでメッセージが出力される
図2.24: 5回目のcontinueでメッセージが出力される

 ということは_IO_new_file_xsputn()での5回目のブレークの際に、nextiで処理を進めればいいことになる。

 runで再実行し、_IO_new_file_xsputn()でブレークするのでcontinueを4回実行する。そしてnextiで処理を進めていく。

 すると、図2.25の箇所でメッセージ出力がされた。

図2.25: ポインタ経由の関数呼び出しでメッセージが出力される
図2.25: ポインタ経由の関数呼び出しでメッセージが出力される

 ポインタ経由で関数呼び出しされているので、先ほどと同様にcall命令にブレークポイントを設定する。call命令のアドレスは0x80673f1だ。

(gdb) break *0x80673f1
Breakpoint 5 at 0x80673f1
(gdb)

 先ほどのブレークポイントは無効化しておく。

(gdb) disable 4

 runで再実行しよう。

(gdb) run
The program being debugged has been started already.
Start it from the beginning? (y or n)
Starting program: /home/user/hello/hello
 
Breakpoint 5, 0x080673f1 in _IO_new_file_xsputn ()
(gdb)

 continueを実行すると同じ場所でまたブレークするのだが、繰り返すと2回目のcontinueでメッセージが出力される。

 runで再実行し、1回continueを行い、2回目のブレークはstepiで関数呼び出しの中に入る。すると_IO_new_file_overflow()という関数の内部に入った。

 nextiで処理を進めよう。すると図2.26の位置でメッセージが出力された。

図2.26: _IO_new_do_write()の呼び出しでメッセージが出力されている
図2.26: _IO_new_do_write()の呼び出しでメッセージが出力されている

 _IO_new_do_write()という関数の先でメッセージ出力が行われているようだ。

 _IO_new_do_write()にブレークポイントを張り、他のブレークポイントは無効にする。

(gdb) break _IO_new_do_write
Breakpoint 6 at 0x8068116
(gdb) disable 5
(gdb) run
The program being debugged has been started already.
Start it from the beginning? (y or n)
Starting program: /home/user/hello/hello
 
Breakpoint 6, 0x08068116 in _IO_new_do_write ()
(gdb)

 ブレーク後にcontinueしてみると、2回目の_IO_new_do_write()の呼び出しでメッセージが出力されているようだ。

 ということでrunで再実行し、1回はcontinueでスキップし、2回目のブレークの際にnextiで処理を進めていく。

 すると図2.27の位置でメッセージが出力された。

図2.27: ポインタ経由の関数呼び出しでメッセージが出力される
図2.27: ポインタ経由の関数呼び出しでメッセージが出力される

 ポインタ経由で関数呼び出しが行われている。そこでcall命令(0x8068198)にブレークポイントを設定し、先ほどのブレークポイントを無効化する。

(gdb) break *0x8068198
Breakpoint 7 at 0x8068198
(gdb) disable 6
(gdb)

 runで再実行してみると、1回目のブレークでメッセージ出力されるようだ。ということでもう一度runで再実行し、stepiで関数の中に入る。

 すると図2.28のようになった。

図2.28: _IO_new_file_write()の中に入る
図2.28: _IO_new_file_write()の中に入る

 _IO_new_file_write()が呼ばれているので、_IO_new_file_write()にブレークポイントを張り、先ほどのブレークポイントは無効化する。

(gdb) break _IO_new_file_write
Breakpoint 8 at 0x8067638
(gdb) disable 7
(gdb)

 runで再実行する。ブレークするのでcontinueすると、どうやら1回目のブレークでメッセージが表示されるようだ。ということでrunで再実行し、ブレークしたらnextiで処理を進める。

 すると、図2.29のようなwrite()の呼び出しがあった。

図2.29: write()の呼び出し
図2.29: write()の呼び出し

書籍紹介

ハロー“Hello,World” OSと標準ライブラリのシゴトとしくみ

ハロー“Hello, World” OSと標準ライブラリのシゴトとしくみ

坂井弘亮著
秀和システム 3,200円

C言語の入門書では、"Hello, World"と出力するプログラムを最初に作るのが定番です。"Hello, World"は、たった7行の単純なプログラムですが、printf()の先では何が行われているのか、main()の前にはいったい何があるのか、考えてみると謎だらけです。本書は、基礎中の基礎である"Hello, World"プログラムを元に、OSと標準ライブラリの仕組みをあらゆる角度からとことん解析します。資料に頼らず、自分の手で調べる方法がわかります。


注文ページへ


Copyright © ITmedia, Inc. All Rights Reserved.

前のページへ |       
ページトップに戻る