検索
連載

「Hello World!」の主役printf()の内部動作をデバッガGDBで追うmain()関数の前には何があるのか(2)(3/3 ページ)

C言語の「Hello World!」プログラムで使われる、「printf()」「main()」関数の中身を、デバッガによる解析と逆アセンブル、ソースコード読解などのさまざまな側面から探る連載。今回は、「Hello World!」の主役printf()の内部動作をデバッガGDBで追う。

Share
Tweet
LINE
Hatena
前のページへ |       

デバッガで動作を追ってみる

 デバッガの操作に慣れたら、いよいよprintf()の内部の処理を追ってみよう。

printf()の中に入っていく

 nextコマンドではステップ実行によりプログラムの動作を1行ずつ確認しながら進められるのだが、printf()のような関数呼び出しも1行として数えてしまう。

 つまりnextでは、関数内部の処理を一気に実行してしまうことになる。このままではprintf()の処理の内部が追えない。

 関数の中に入っていくためのステップ実行の方法は、また別にある。

 試してみよう。まずはもう一度runを実行し、プログラムの動作を開始する。

(gdb) run

 画面は図2.5のようになった。ブレークポイントはまだ有効のまま残っているので、先ほどと同様にmain()の先頭で止まっている。

図2.5: runの実行画面
図2.5: runの実行画面

 ここで「stepi」というコマンドを実行してみよう。1回実行しただけでは変化は無いのだが、8回ほど実行すると、図2.6のような画面になった。

図2.6: stepiの実行画面
図2.6: stepiの実行画面

 ステップ実行にはいくつかのコマンドがある。

 まず「next」と「step」がある。これらはどちらもステップ実行なのだが、「next」は関数呼び出しの際に関数の処理をすべて行った上で呼び出し後の次の行でブレークする。つまり関数呼び出しも、1行の実行として扱う。それに対して「step」は、関数呼び出しの中に入っていく。

 さらに「nexti」「stepi」がある。これらは機械語の命令単位でのステップ実行を行う。

アセンブラベースで処理を見る

 さて図2.6では、 画面上にソースコードが表示されていない。 これは実行がprintf()の内部に入っていったのだが、printf()が標準Cライブラリに含まれており、ソースコードが参照できないためだ。

 つまりソースコードをベースとした、いわゆる「シンボリック・デバッグ」は、できないということだ。

 しかしC言語のソースコードが参照できなくても、実行ファイルには機械語コードが含まれている。よってそれを逆アセンブルして、アセンブラベースで処理を追うことは可能だ。

 ということで、アセンブラの表示に切り替えてみよう。

(gdb) layout asm

 すると図2.7のような画面になるだろう。

図2.7: layout asmの実行画面
図2.7: layout asmの実行画面

 これは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と標準ライブラリのシゴトとしくみ

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

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

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


注文ページへ


Copyright © ITmedia, Inc. All Rights Reserved.

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