Linuxカーネルのソースコードを読んで、システムコールを探る:main()関数の前には何があるのか(6)(3/3 ページ)
C言語の「Hello World!」プログラムで使われる、「printf()」「main()」関数の中身を、デバッガによる解析と逆アセンブル、ソースコード読解などのさまざまな側面から探る連載。前回まで、printf()内の中身をさまざまな方法で探り、write()やint $0x80の呼び出しまでたどり着いた。今回は、さらにその先にあるLinuxカーネル側のシステムコールを見ていく。
パラメータの渡しかたを見る
Linuxカーネルでのシステムコール処理の入口となっている、ソフトウェア割込みのハンドラの位置はわかった。
ただこれは、呼び出される手順がわかったというだけだ。実際にはシステムコール発行時には、アプリケーションからいくつかのパラメータが渡されてくるはずだ。
次はシステムコール発行時のアプリケーション側とLinuxカーネル側での、パラメータの引渡しについて見てみよう。
レジスタの値を確認する
Linuxのような汎用OSでは、アプリケーションからOSカーネルにパラメータを渡す方法として、レジスタ経由かスタック経由かが主に考えられる。
連載第4回では動的解析によりint $0x80の呼び出し箇所まで知ることができている。システムコール発行時のレジスタとスタックの値を、GDBでの動的解析によって確認してみよう。
ということでGDBでの解析に戻ろう。GDBを起動し、write()にブレークポイントを張る。
[user@localhost hello]$ gdb -q hello Reading symbols from /home/user/hello/hello...done. (gdb) break write Breakpoint 1 at 0x8053d70 (gdb)
ブレークポイントを張ったらrunで実行すると、write()まで処理が進む。
(gdb) run Starting program: /home/user/hello/hello Breakpoint 1, 0x08053d70 in write () (gdb)
さらにint命令にブレークポイントを張ろう。連載第4回の図2.32を見ると、int命令は0x110414というアドレスに配置されている。
アドレス指定でブレークポイントを張るには以下のようにする。
(gdb) break *0x110414 Breakpoint 2 at 0x110414 (gdb)
この状態でcontinueすれば、int命令まで実行を進めることができるはずだ。
(gdb) continue Continuing. Breakpoint 2, 0x00110414 in __kernel_vsyscall () (gdb)
「0x00110414」というアドレスでブレークしているので、int命令の位置まで進めることができたようだ。
ここで初めからint命令にブレークポイントを張らないのは、別のシステムコールからも当該の箇所が大量に呼ばれており、write()以外の呼び出しでもブレークしてしまうためだ。
この状態で、レジスタの状態を見てみよう。これはinfo registersというコマンドで可能だ。
(gdb) info registers eax 0x4 4 ecx 0xb7fff000 -1207963648 edx 0x26 38 ebx 0x1 1 esp 0xbffff4d8 0xbffff4d8 ebp 0xbffff4fc 0xbffff4fc esi 0xb7fff000 -1207963648 edi 0x80d68c0 135096512 eip 0x110414 0x110414 <__kernel_vsyscall> eflags 0x246 [ PF ZF IF ] cs 0x73 115 ss 0x7b 123 ds 0x7b 123 es 0x7b 123 fs 0x0 0 gs 0x33 51 (gdb)
eipが0x110414となっていることを確認しておこう。EIPは「インストラクション・ポインタ」と呼ばれるレジスタで、いわゆる「プログラム・カウンタ」のことだ。つまり実行中のアドレスを指すレジスタだ。正確には、これから実行しようとしている命令のアドレスを指す。
このようなレジスタは一般的にはプログラム・カウンタと呼ばれるが、x86ではインストラクション・ポインタと呼ばれている。もともとは「IP」というレジスタなのだが、頭に「E」が付くのは32ビット拡張されたときに「Extend」の意味で付加されたものだ。
ここでwriteシステムコールについて考えてみよう。出力先は標準出力で、そのファイルディスクリプタの値は1だ。そして出力される文字列は"Hello World! 1 /home/user/hello/hello\n"なので、38バイトになる。
そのような視点で見てみると、int $0x80が実行される直前にEBXレジスタが1、EDXレジスタが38になっている点に興味をひかれるだろう。
ECXは0xb7fff000という値になっている。これはESPと近い値になっているので、どうやらスタック上のアドレスのようだ。その先には何があるのだろうか。
(gdb) x/s $ecx 0xb7fff000: "Hello World! 1 /home/user/hello/hello\n" (gdb)
これは表示される文字列のようだ。
つまりシステムコールのパラメータは、EAX、EBX、ECX、EDXといったレジスタで渡されているらしいということがわかる。これらは汎用レジスタとして多く利用されるものだが、その値をまとめると、表3.1のようになっているようだ。
レジスタ | 値 | |
---|---|---|
EAX | 4 | |
EBX | 1 | |
ECX | 0xb7fff000 | |
EDX | 38 |
スタックの状態も確認しておく
参考までに、スタックの状態も見ておこう。
(gdb) x/16x $esp 0xbffff4d8: 0x08053d92 0x00000026 0x08067671 0x00000001 0xbffff4e8: 0xb7fff000 0x00000026 0x080d68c0 0x00000026 0xbffff4f8: 0xb7fff000 0xbffff524 0x0806819b 0x080d68c0 0xbffff508: 0xb7fff000 0x00000026 0xbffff544 0x08069732 (gdb)
スタック先頭から+12以降の位置に、1、0xb7fff000、0x26(10進数で38)という3つの値が並んでいることがわかる。
つまりスタック上にもシステムコールの引数が配置されているようなのだが、これは後述するシステムコール・ラッパーの呼び出しのためのものであり、write()が呼ばれたときにスタック経由で渡された、システムコールの引数だ。
つまりint $0x80の呼び出しによって直接参照される箇所ではない。これについては後述する。
システムコール呼び出し後のレジスタの状態
さらにstepiでint命令を実行すると、以下のようにしてメッセージが出力される。
(gdb) stepi Hello World! 1 /home/user/hello/hello 0x00110416 in __kernel_vsyscall () (gdb)
この状態で、もう一度レジスタの状態を見てみよう。
(gdb) info registers eax 0x26 38 ecx 0xb7fff000 -1207963648 edx 0x26 38 ebx 0x1 1 esp 0xbffff4d8 0xbffff4d8 ebp 0xbffff4fc 0xbffff4fc esi 0xb7fff000 -1207963648 edi 0x80d68c0 135096512 eip 0x110416 0x110416 <__kernel_vsyscall+2> eflags 0x10246 [ PF ZF IF RF ] cs 0x73 115 ss 0x7b 123 ds 0x7b 123 es 0x7b 123 fs 0x0 0 gs 0x33 51 (gdb)
eaxの値が4→38のように変化していることに注目してほしい。
これが実は、システムコールの戻り値になる。これについては後述する。
書籍紹介
ハロー“Hello, World” OSと標準ライブラリのシゴトとしくみ
坂井弘亮著
秀和システム 3,200円
C言語の入門書では、"Hello, World"と出力するプログラムを最初に作るのが定番です。"Hello, World"は、たった7行の単純なプログラムですが、printf()の先では何が行われているのか、main()の前にはいったい何があるのか、考えてみると謎だらけです。本書は、基礎中の基礎である"Hello, World"プログラムを元に、OSと標準ライブラリの仕組みをあらゆる角度からとことん解析します。資料に頼らず、自分の手で調べる方法がわかります。
Copyright © ITmedia, Inc. All Rights Reserved.
関連記事
- プログラミング言語Cについて知ろう
プログラミング言語の基本となる「C」。正しい文法や作法を身に付けよう。Cには確かに学ぶだけの価値がある(編集部) - シェルコード解析に必携の「5つ道具」
コンピュータウイルスの解析などに欠かせないリバースエンジニアリング技術ですが、何だか難しそうだな、という印象を抱いている人も多いのではないでしょうか。この連載では、「シェルコード」を例に、実践形式でその基礎を紹介していきます。(編集部) - 【 od 】コマンド――ファイルを8進数や16進数でダンプする
本連載は、Linuxのコマンドについて、基本書式からオプション、具体的な実行例までを紹介していきます。今回は、「od」コマンドです。