デバッガの操作に慣れたら、いよいよprintf()の内部の処理を追ってみよう。
nextコマンドではステップ実行によりプログラムの動作を1行ずつ確認しながら進められるのだが、printf()のような関数呼び出しも1行として数えてしまう。
つまりnextでは、関数内部の処理を一気に実行してしまうことになる。このままではprintf()の処理の内部が追えない。
関数の中に入っていくためのステップ実行の方法は、また別にある。
試してみよう。まずはもう一度runを実行し、プログラムの動作を開始する。
(gdb) run
画面は図2.5のようになった。ブレークポイントはまだ有効のまま残っているので、先ほどと同様にmain()の先頭で止まっている。
ここで「stepi」というコマンドを実行してみよう。1回実行しただけでは変化は無いのだが、8回ほど実行すると、図2.6のような画面になった。
ステップ実行にはいくつかのコマンドがある。
まず「next」と「step」がある。これらはどちらもステップ実行なのだが、「next」は関数呼び出しの際に関数の処理をすべて行った上で呼び出し後の次の行でブレークする。つまり関数呼び出しも、1行の実行として扱う。それに対して「step」は、関数呼び出しの中に入っていく。
さらに「nexti」「stepi」がある。これらは機械語の命令単位でのステップ実行を行う。
さて図2.6では、 画面上にソースコードが表示されていない。 これは実行がprintf()の内部に入っていったのだが、printf()が標準Cライブラリに含まれており、ソースコードが参照できないためだ。
つまりソースコードをベースとした、いわゆる「シンボリック・デバッグ」は、できないということだ。
しかしC言語のソースコードが参照できなくても、実行ファイルには機械語コードが含まれている。よってそれを逆アセンブルして、アセンブラベースで処理を追うことは可能だ。
ということで、アセンブラの表示に切り替えてみよう。
(gdb) layout asm
すると図2.7のような画面になるだろう。
これはstepiによって関数呼び出しの中に入っていった状態だ。
アセンブラの解読には慣れない読者の方も多いかもしれないが、本書ではそれほど難しい解析は行わない。アセンブラが初見のかたも、気負わずそんなものかくらいの感覚で話を聞いてみてほしい。
objdumpにより出力されるアセンブラの読みかたについては前回説明したが、ここではGDB上で出力されるアセンブラの読み方を簡単に説明しておこう。
まず図2.7の一番左の列の「0x8049360」「0x8049361」のような16 進数の値は、命令である機械語コードが配置されているアドレスだ。
左から2番目の列には「<printf>」「<printf+1>」のようにして、機械語コードが配置されている関数が表示されているようだ。つまり機械語コードはprintf()の関数内部にある、ということを示していることになる。「+1」などの数値は、関数の先頭からのオフセット値だろう。
さらに右の列の「push %ebp」「mov %esp,%ebp」といったものは、機械語コードを逆アセンブルした結果のニーモニックだ。
そして反転表示されている行が、実行を停止している位置だ。どうやらprintf()の先頭で「push %ebp」という命令を実行しようとしているところで停止しているようだ。
GDBによる表示を、逆アセンブルした結果と比較してみよう。
以下のようにして実行ファイルを逆アセンブルする。出力が大量に発生するため、lessなどに入力するといいだろう。
[user@localhost hello]$ objdump -d hello | less
lessでprintf()の先頭を探してみよう。図2.7では「8049360」というアドレスに配置されているようなので、アドレスで検索すると手っ取り早い。
すると、当該の箇所は以下のようになっていた。
08049360 <_IO_printf>: 8049360: 55 push %ebp 8049361: 89 e5 mov %esp,%ebp 8049363: 83 ec 0c sub $0xc,%esp 8049366: 8d 45 0c lea 0xc(%ebp),%eax 8049369: 89 44 24 08 mov %eax,0x8(%esp) 804936d: 8b 45 08 mov 0x8(%ebp),%eax 8049370: 89 44 24 04 mov %eax,0x4(%esp) 8049374: a1 f0 66 0d 08 mov 0x80d66f0,%eax 8049379: 89 04 24 mov %eax,(%esp) 804937c: e8 3f fe 00 00 call 80591c0 <_IO_vfprintf> 8049381: c9 leave 8049382: c3 ret ...
ニーモニックが、図2.7のそれと一致していることを確認してほしい。ここではGDBの出力とobjdumpの出力を比較しているわけだが、このようにひとつのツールによる出力だけでなく、複数のツールの出力を比較しながら進めることは、確実に理解しながら動作を追うために意外に重要だ。
なお関数名はprintf()でなく_IO_printf()になっているようだが、エイリアスになっているのだろうか。関数の配置アドレスは、前回説明したreadelfを用いることで確認することができる。見てみよう。
[user@localhost hello]$ readelf -a hello | grep 8049360 946: 08049360 35 FUNC GLOBAL DEFAULT 6 __printf 960: 08049360 35 FUNC GLOBAL DEFAULT 6 printf 1233: 08049360 35 FUNC GLOBAL DEFAULT 6 _IO_printf [user@localhost hello]$
どうやら同じアドレスに__printf()、printf()、_IO_printf()の3つの関数名がエイリアスとして配置されているようだ。
ハロー“Hello, World” OSと標準ライブラリのシゴトとしくみ
坂井弘亮著
秀和システム 3,200円
C言語の入門書では、"Hello, World"と出力するプログラムを最初に作るのが定番です。"Hello, World"は、たった7行の単純なプログラムですが、printf()の先では何が行われているのか、main()の前にはいったい何があるのか、考えてみると謎だらけです。本書は、基礎中の基礎である"Hello, World"プログラムを元に、OSと標準ライブラリの仕組みをあらゆる角度からとことん解析します。資料に頼らず、自分の手で調べる方法がわかります。
Copyright © ITmedia, Inc. All Rights Reserved.