「Hello World!」の主役printf()の内部動作をデバッガGDBで追う:main()関数の前には何があるのか(2)(2/3 ページ)
C言語の「Hello World!」プログラムで使われる、「printf()」「main()」関数の中身を、デバッガによる解析と逆アセンブル、ソースコード読解などのさまざまな側面から探る連載。今回は、「Hello World!」の主役printf()の内部動作をデバッガGDBで追う。
gdbserverで画面崩れを防ぐ
さてこれで実行はできたわけだが、「Hello World!」の文字列はGDBを起動しているスクリーン上に出力されている。
これは実は、後述するソースコードやアセンブラの表示を行うと、画面が崩れてしまい確認しにくくなるという問題がある。場合によってはそもそも視認できない、ということもあるようだ。
これを回避するためには、gdbserverを利用してサンプル・プログラムの起動コンソールとGDBの操作コンソールを分けるとよい。以下にgdbserverによるデバッグ手法を説明しておくので、「Hello World!」のメッセージ出力が確認できない、という場合には参考にしてほしい。
なお本書では説明を1画面で行いたいという都合上、gdbserver は利用せずにひとつのコンソール上で作業を行っている。このため「Hello World!」の文字列表示で画面が崩れているような場合もあるので注意していただきたいと思う。
まずgdbserverがインストールされていない場合には、スーパーユーザで以下を実行することでgdbserverをインストールする。
[user@localhost ~]$ gdbserver -bash: gdbserver: command not found [user@localhost ~]$ su Password: [root@localhost user]# yum install gdb-gdbserver
インストールできたら、gdbserver 経由でサンプル・プログラムを起動しよう。
[user@localhost ~]$ cd hello [user@localhost hello]$ gdbserver localhost:12345 ./hello Process ./hello created; pid = 2324 Listening on port 12345
ここでは接続用ポート番号を「12345」という適当なものにしているが、すでに使われている場合には以下のようなエラーになるので、ポート番号を12345以外に変更して再度試してみてほしい。
[user@localhost hello]$ gdbserver localhost:12345 ./hello Process ./hello created; pid = 2466 Can't bind address: Address already in use. Killing process(es): 2466 [user@localhost hello]$
gdbserverが起動できたら、次は別のターミナルでGDBを起動する。
[user@localhost ~]$ cd hello [user@localhost hello]$ gdb -q hello Reading symbols from /home/user/hello/hello...done. (gdb)
gdbの「-q」オプションは、起動時のメッセージ出力を抑制するためのものだ。本書ではページ数が限られていることもあり、以降は-qオプションを利用することにする。
GDB側で「target extended-remote」というコマンドでgdbserverに接続する。これでgdbserver側で起動しているサンプル・プログラムを、GDB側から操作できる。
(gdb) target extended-remote localhost:12345 Remote debugging using localhost:12345 0x080481c0 in _start () Created trace state variable $trace_timestamp for target's variable 1. (gdb)
なお接続には通常は「target remote 」コマンドを利用するのだが、それだとgdbserverを利用しない場合と比べて様々な操作の違いが出てくる。例えば起動時にrunでなくcontinueで動作開始する、runでリスタートができない、終了すると接続が切断されてしまいやはりリスタートできない、といったものだ。「targetextended-remote 」で接続することで、gdbserverを経由せずにgdb上でプログラムを動作させているときと同じ感覚で操作できる。
あとは通常のGDBの操作と同じだ。runで実行してみよう。
(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)
GDB 側にはメッセージは出力されていない。プログラムの実行はgdbserver 側で行われ、メッセージもそちらに出力されることになる。
[user@localhost hello]$ gdbserver localhost:12345 ./hello Process ./hello created; pid = 2324 Listening on port 12345 Remote debugging from host 127.0.0.1 Process ./hello created; pid = 2353 Hello World! 1 ./hello Child exited with status 0
図2.1はGUI 上でターミナルを2つ開き、片方でgdbserver、もう片方でGDBを動作させているところだ。左下のウィンドウがgdbserver、右上のウィンドウがGDBになる。左下のgdbserver 側で「Hello World!」のメッセージ出力がされていることを確認してほしい。
プログラムの終了は、「quit」ではgdbserver側が終了せず、GDB側のみ終了する。以下のように「monitor exit」というコマンドで、gdbserver側を終了できる。GDB側を終了させてしまったときには、GDBを再起動しtarget extended-remoteで再接続できるので、再接続した状態でmonitor exitを実行する。
(gdb) monitor exit
gdbserverは本来は組込み機器のようなプアなプログラム実行環境で、ターゲット機器上ではGDBを動作させずにプログラムのデバッグを行うためのものなのだが、プログラムの実行とGDBの操作を分離できるため、たとえばテキストエディタのようなプログラムのデバッグにも重宝する。
ブレークポイントを張ってみる
さて、ただ実行するだけではデバッガを使う意味が無い。
GDBの操作方法の説明に戻ろう。次はmain()関数の先頭に「ブレークポイント」を張って、そこでブレークしてみる。ブレークポイントを張るコマンドは「break」だ。
(gdb) break main Breakpoint 1 at 0x80482c5: file hello.c, line 5. (gdb)
もう一度実行してみよう。
(gdb) run Starting program: /home/user/hello/hello Breakpoint 1, main (argc=1, argv=0xbffffc14) at hello.c:5 5 printf("Hello World! %d %s\n", argc, argv[0]); (gdb)
今度はmain()関数の内部、hello.cの5 行目でブレークしたようだ。
これで、どうやらprintf()の呼び出し位置でブレークしているらしいということはわかる。ソースコードを表示させてみよう。
(gdb) layout src
すると、図2.2のような画面になるだろう。
printf()の行が反転表示されているので、そこで停止しているようだ。
反転表示されている位置は「実行完了した行」ではなく「これから実行しようとしている行」を指す。つまりprintf()の呼び出し前で停止していることになる。
このようにデバッガではブレークポイントを張ることで、任意の場所でプログラムの実行を一時停止することができる。
この「ブレークポイント」が、デバッガの代表的な2つの機能のうちのひとつだ。
ステップ実行してみる
デバッガの代表的な機能のもうひとつは、ステップ実行だ。
この状態でステップ実行してみよう。ステップ実行は「next」というコマンドで行える。
図2.3ではreturnの行が反転表示されている。表示位置が崩れてしまってはいるが、「Hello World!」のメッセージも出力されていることがわかる。
なおここで表示崩れのために「Hello World!」のメッセージが確認できない、という場合があるかもしれない。そのような場合には、先述したgdbserver 経由での実行を試してみてほしい。
さて現在はreturnの実行前でブレークしている状態だが、「continue」というコマンドを実行すると、ブレーク状態を解除して実行を継続する。
やってみよう。
(gdb) continue Continuing. Program exited normally. (gdb)
実行が継続され、プログラムが終了した。画面は図2.4のようになっている。
GDBではこのように、プログラムを適宜ブレークさせてはそのときの状態を見て、デバッグをしていく。
さらにプログラムの実行とコマンド実行は、排他になっている。プログラムを実行している最中はコマンド実行できないし、コマンドを実行させるときにはプログラムの実行を一時的に停止する。プログラム実行とコマンド実行を交互に行う感じだ。
まずはこのようなデバッガの操作に慣れてほしい。
Copyright © ITmedia, Inc. All Rights Reserved.
関連記事
- プログラミング言語Cについて知ろう
プログラミング言語の基本となる「C」。正しい文法や作法を身に付けよう。Cには確かに学ぶだけの価値がある(編集部) - シェルコード解析に必携の「5つ道具」
コンピュータウイルスの解析などに欠かせないリバースエンジニアリング技術ですが、何だか難しそうだな、という印象を抱いている人も多いのではないでしょうか。この連載では、「シェルコード」を例に、実践形式でその基礎を紹介していきます。(編集部) - 【 od 】コマンド――ファイルを8進数や16進数でダンプする
本連載は、Linuxのコマンドについて、基本書式からオプション、具体的な実行例までを紹介していきます。今回は、「od」コマンドです。