連載
» 2017年06月22日 05時00分 公開

Linuxカーネルのソースコードを読んで、システムコールを探るmain()関数の前には何があるのか(6)(3/3 ページ)

[坂井弘亮,著]
前のページへ 1|2|3       

パラメータの渡しかたを見る

 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のようになっているようだ。

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

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

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

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


注文ページへ


前のページへ 1|2|3       

Copyright © ITmedia, Inc. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。