連載
» 2017年06月08日 05時00分 UPDATE

main()関数の前には何があるのか(4):OSのシステムコールの呼び出しとは&バイナリエディタの使い方 (1/3)

C言語の「Hello World!」プログラムで使われる、「printf()」「main()」関数の中身を、デバッガによる解析と逆アセンブル、ソースコード読解などのさまざまな側面から探る連載。今回は、printf()内のポインタ経由での関数呼び出しが行き着く先にあるシステムコールの呼び出しとバイナリエディタの使い方について。

[坂井弘亮,著]

連載目次

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

書籍の中から有用な技術情報をピックアップして紹介する本シリーズ。今回は、秀和システム発行の書籍『ハロー“Hello, World” OSと標準ライブラリのシゴトとしくみ(2015年9月11日発行)』からの抜粋です。

ご注意:本稿は、著者及び出版社の許可を得て、そのまま転載したものです。このため用字用語の統一ルールなどは@ITのそれとは一致しません。あらかじめご了承ください。


※編集部注:前回記事「試行錯誤のデバッグで探る、printf()内のポインタ経由での関数呼び出しが行き着く先とは」はこちら

write()が呼ばれている

 write()はファイル出力を行うためのシステムコールの呼び出しだ。

 ということは、ここでメッセージ出力が行われているのではないだろうか。stepiで関数の内部に入ってみよう。

図2.30: write()の内部 図2.30: write()の内部

 メッセージ出力がされないかどうか、ステップ実行を注意深く進めよう。すると図2.31のようなcall命令が見つかる。

図2.31: write()の内部からの関数呼び出し 図2.31: write()の内部からの関数呼び出し

 write()の内部なので、かなり核心に近付いているように思われる。ここはstepiで関数の内部に入っていこう。

 すると図2.32のようになった。「int」という、今まで出てきていない命令があるようだ。

図2.32: int命令の呼び出し 図2.32: int命令の呼び出し

メッセージが出力される瞬間

 さてint $0x80という命令にたどりついたら、stepiでステップ実行してみよう。

 すると、図2.34のようにしてメッセージが出力された。

図2.34: メッセージが出力された瞬間 図2.34: メッセージが出力された瞬間

 つまり「int $0x80」という命令が実行された瞬間に、ハロー・ワールドのメッセージが出力されているということになる。

 さて、これでメッセージ出力の核心まで進めることができた。ここで「where」というコマンドを実行してみよう。

(gdb) where
#0  0x00110416 in __kernel_vsyscall ()
#1  0x08053d92 in __write_nocancel ()
#2  0x08067671 in _IO_new_file_write ()
#3  0x0806819b in _IO_new_do_write ()
#4  0x080683ea in _IO_new_file_overflow ()
#5  0x080673f4 in _IO_new_file_xsputn ()
#6  0x08059738 in vfprintf ()
#7  0x08049381 in printf ()
#8  0x080482e2 in main (argc=1, argv=0xbffffc14) at hello.c:5
(gdb)

 whereは関数の呼び出し手順を遡って調べて表示するコマンドだ。関数呼び出しは実際には戻り先や関数内のさまざまなパラメータをスタック上に保存することで実現されており、このような逆算処理は一般にスタックのバックトレースと呼ばれる。

 よってこれがmain()からprintf()が呼ばれ、実際にメッセージが出力されるまでの一連の関数の呼び出し手順になる。

 なお図2.30を見ると、__write_nocancelの直前にwriteというシンボルがあり、write()の呼び出しではwriteの先頭部分が実行されていることに注目してほしい。つまりwhereでは__write_nocancelが表示されているのだが、これはおそらくその後の__kernel_vsyscall()の呼び出しが__write_nocancelの部分から行われているためGDBがそのように表示しているだけであり、実際には__write_nocancelではなくwriteが呼び出されているわけだ。

 つまりprintf()の先では、最終的にはシステムコールのwrite()が呼ばれていることになる。

システムコールの呼び出し

 ここでOSのシステムコールについて考えてみよう。

 アプリケーション・プログラムとOSカーネルの間のインターフェースは、システムコールだ。アプリケーション・プログラムは、最終的にはシステムコールを呼び出すことになる。

 CentOS環境ではopen()やread()といったシステムコールが利用できる。これらはUNIXライクなOSでのシステムコールAPIであり、POSIXという仕様で定義されている。

 なおここで「Linux」と呼ばずに「CentOS環境」と言っているのは、Linuxが持っているのは「open」というシステムコールであり、「open()」というAPIを提供しているのはglibcであるためだ。

 ハロー・ワールドのサンプル・プログラムからは、どのようなシステムコールが呼ばれているのだろうか。

straceによるトレース

 CentOS環境ではstraceというコマンドで、プログラムが呼び出しているシステムコールをトレースすることができる。

 ハロー・ワールドに対してstraceを試してみよう。

[user@localhost hello]$ strace ./hello
execve("./hello", ["./hello"], [/* 26 vars */]) = 0
uname({sys="Linux", node="localhost.localdomain", ...}) = 0
brk(0)					= 0x9e39000
brk(0x9e39cd0)				= 0x9e39cd0
set_thread_area({entry_number:-1 -> 6, base_addr:0x9e39830, limit:1048575, seg_32bit:1
, contents:0, read_exec_only:0, limit_in_pages:1, seg_not_present:0, useable:1}) = 0
brk(0x9e5acd0)				= 0x9e5acd0
brk(0x9e5b000)				= 0x9e5b000
fstat64(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 0), ...}) = 0
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb777f000
write(1, "Hello World! 1 ./hello\n", 23Hello World! 1 ./hello
) = 23
exit_group(0)				= ?
[user@localhost hello]$

 いくつかのシステムコールが呼ばれていることがわかる。そして注目すべきは、最後のほうにあるwrite()の呼び出しだ。ここでwriteシステムコールによって、ハロー・ワールドのメッセージが出力されていることが確認できる。

       1|2|3 次のページへ

Copyright© 2017 ITmedia, Inc. All Rights Reserved.

@IT Special

- PR -

TechTargetジャパン

この記事に関連するホワイトペーパー

Focus

- PR -

RSSについて

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

メールマガジン登録

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