printf()のソースコードで、ソースコードリーディングのコツを身に付ける:main()関数の前には何があるのか(5)(3/3 ページ)
C言語の「Hello World!」プログラムで使われる、「printf()」「main()」関数の中身を、デバッガによる解析と逆アセンブル、ソースコード読解などのさまざまな側面から探る連載。前回まで、printf()内の中身をデバッグと逆アセンブルで探ってきたが、今回はソースコードリーディングで答え合わせをしてみる。
ファイルポインタの構造
バッファリングされた文字列が、実際に出力されるのはいつだろうか。
バッファはファイルポインタの先にあるので、ファイルポインタの構造を知る必要がある。ファイルポインタというのはいわゆるFILE型の構造体を指すポインタのことだ。例えばfopen()する際に、以下のように書くだろう。
FILE *fp;
fp = fopen("/tmp/sample.txt", "w");
このときのfpがファイルポインタで、FILE型の構造体を指している。
FILE型の定義を探してみよう。ファイルポインタを利用する際にインクルードするのはstdio.hだ。ということはglibcの中に、ユーザ・プログラマに提供するためのstdio.hがあるはずだ。
[user@localhost glibc-2.21]$ find . -name stdio.h ./include/stdio.h ./libio/stdio.h ./libio/bits/stdio.h [user@localhost glibc-2.21]$
3つあるようだ。これらをひとつひとつ見てみると、libio/stdio.hに以下のような定義が見つかった。
47:/* The opaque type of streams. This is the definition used elsewhere. */ 48:typedef struct _IO_FILE FILE;
同じファイルには以下のようにstdin/stdout/stderrの宣言もある。どうやら_IO_FILEがFILEで間違いないようだ。
167:/* Standard streams. */ 168:extern struct _IO_FILE *stdin; /* Standard input stream. */ 169:extern struct _IO_FILE *stdout; /* Standard output stream. */ 170:extern struct _IO_FILE *stderr; /* Standard error output stream. */
struct _IO_FILEの構造体の定義はどこでされているだろうか。
[user@localhost glibc-2.21]$ find . -name "*.h" | xargs grep _IO_FILE
...
./libio/libio.h:struct _IO_FILE {
...
どうやらlibio/libio.hで定義されているようだ。実際の定義部分を見てみよう。
245:struct _IO_FILE {
246: int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
247:#define _IO_file_flags _flags
248:
249: /* The following pointers correspond to the C++ streambuf protocol. */
250: /* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
251: char* _IO_read_ptr; /* Current read pointer */
252: char* _IO_read_end; /* End of get area. */
253: char* _IO_read_base; /* Start of putback+get area. */
254: char* _IO_write_base; /* Start of put area. */
255: char* _IO_write_ptr; /* Current put pointer. */
256: char* _IO_write_end; /* End of put area. */
257: char* _IO_buf_base; /* Start of reserve area. */
258: char* _IO_buf_end; /* End of reserve area. */
...
268: int _fileno;
...
メンバとして_IO_write_ptrが定義されていることが確認できるので、確かにこれがFILE型の構造体の本体だろう。
またバッファの実体は、ファイルポインタの指す先にあるということが確認できる。
ファイル構造体のバッファリング処理
struct _IO_FILEの定義を見ると、以下の3つのメンバを持っている。
... 254: char* _IO_write_base; /* Start of put area. */ 255: char* _IO_write_ptr; /* Current put pointer. */ 256: char* _IO_write_end; /* End of put area. */ ...
これらがファイル出力のためのバッファのように思える。_IO_write_baseがバッファの先頭アドレス、_IO_write_ptrがバッファの使用中の位置、_IO_write_endがバッファの終端アドレスだろうか。
標準入出力関数では、出力はバッファリングされる。このため実際の出力処理は_IO_write_baseというバッファに対して行われるはずだ。
libioの中で_IO_write_baseに触れているような処理を検索してみよう。すると、以下のようなものがヒットする。
libio/fileops.c: return _IO_do_write (f, f->_IO_write_base, libio/fileops.c: if (_IO_do_write (f, f->_IO_write_base,
つまり_IO_do_write()という関数が呼ばれているようだ。名前からして、出力を行う関数だろうか。
ヒットした箇所を見てみよう。
793:int
794:_IO_new_file_overflow (_IO_FILE *f, int ch)
795:{
...
838: if (ch == EOF)
839: return _IO_do_write (f, f->_IO_write_base,
840: f->_IO_write_ptr - f->_IO_write_base);
841: if (f->_IO_write_ptr == f->_IO_buf_end ) /* Buffer is really full */
842: if (_IO_do_flush (f) == EOF)
843: return EOF;
844: *f->_IO_write_ptr++ = ch;
845: if ((f->_flags & _IO_UNBUFFERED)
846: || ((f->_flags & _IO_LINE_BUF) && ch == '\n'))
847: if (_IO_do_write (f, f->_IO_write_base,
848: f->_IO_write_ptr - f->_IO_write_base) == EOF)
849: return EOF;
850: return (unsigned char) ch;
851:}
852:libc_hidden_ver (_IO_new_file_overflow, _IO_file_overflow)
_IO_new_file_overflow()という関数の内部のようだ。これは連載第4回のGDBによる解析時に、write()の前段階で呼ばれていた関数だ。やはりここで出力が行われているように思える。
処理の内容を読んでみよう。_IO_do_write()の呼び出しは2箇所にあるが、ひとつはEOFに達したときに呼ばれるようだ。
そしてもうひとつの箇所で_IO_do_write()が呼ばれる条件は、_IO_UNBUFFEREDのフラグが立っているときか、もしくは_IO_LINE_BUFのフラグが立ち改行コードが来たときのようだ。
そしてそれらの場合に_IO_do_write()が呼ばれ、実際の出力が行われることになるようだ。
_IO_LINE_BUFというフラグが立つのはどのようなときだろうか。フラグを立てている箇所を探すと、以下の2つの部分があった。
ひとつはlibio/iosetvbuf.cの、以下の部分だ。
33:int
34:_IO_setvbuf (fp, buf, mode, size)
35: _IO_FILE *fp;
36: char *buf;
37: int mode;
38: _IO_size_t size;
39:{
...
75: case _IOLBF:
76: fp->_IO_file_flags &= ~_IO_UNBUFFERED;
77: fp->_IO_file_flags |= _IO_LINE_BUF;
...
これはsetvbuf()で_IOLBFを指定したときのようだ。
もうひとつはlibio/filedoalloc.cの以下の箇所だ。
93:int
94:_IO_file_doallocate (fp)
95: _IO_FILE *fp;
96:{
...
116: if (
117:#ifdef DEV_TTY_P
118: DEV_TTY_P (&st) ||
119:#endif
120: local_isatty (fp->_fileno))
121: fp->_flags |= _IO_LINE_BUF;
...
つまり出力先がTTYのときに、行単位の出力になるようだ。
「./hello」のように実行する場合と、「./hello | cat」のようにパイプで別プログラムに流し込む場合では、ライブラリの動作は変わってくるということだ。
write()の呼び出し
_IO_new_file_overflow()のソースコードを見る限りでは、出力処理として_IO_do_write()が呼ばれていた。しかし連載第3回のGDBでの解析では、_IO_new_file_overflow()の中からは_IO_new_do_write()が呼ばれていた。
ということは_IO_new_do_write()が、どこかで_IO_do_write()にリネームされているのだろうか。
配置されているアドレスを比較してみよう。
[user@localhost hello]$ readelf -a hello | grep _do_write 366: 08067280 273 FUNC LOCAL DEFAULT 6 new_do_write 1408: 08068110 274 FUNC GLOBAL DEFAULT 6 _IO_new_do_write 1811: 08068110 274 FUNC WEAK DEFAULT 6 _IO_do_write [user@localhost hello]$
_IO_new_do_write()と_IO_do_write()の配置先アドレスはともに08068110で一致している。つまりこれらは同一の関数だ。
_IO_new_do_write()を探してみると、libio/fileops.cで以下のように定義されていた。
478:int
479:_IO_new_do_write (_IO_FILE *fp, const char *data, _IO_size_t to_do)
480:{
481: return (to_do == 0
482: || (_IO_size_t) new_do_write (fp, data, to_do) == to_do) ? 0 : EOF;
483:}
484:libc_hidden_ver (_IO_new_do_write, _IO_do_write)
内部ではnew_do_write()が呼ばれている。
さらにバックトレースを見ると、new_do_write()の中からは_IO_new_file_write()が呼ばれているようだ。
new_do_write()はどのように実装されているのだろうか。探してみると、_IO_new_do_write()の直後で以下のようにして定義されていた。
486:static
487:_IO_size_t
488:new_do_write (_IO_FILE *fp, const char *data, _IO_size_t to_do)
489:{
...
506: count = _IO_SYSWRITE (fp, data, to_do);
...
独自ビルドしたglibcをリンクした実行ファイルで見てみたところ、この_IO_SYSWRITE()が呼ばれているようだ。
_IO_SYSWRITE()の先は複雑なマクロになっており追跡が難しいのだが、GDBでの解析によれば、その先には_IO_new_file_write()がある。
そして_IO_new_file_write()は、libio/fileops.cで以下のように定義されている。
1242:_IO_ssize_t
1243:_IO_new_file_write (_IO_FILE *f, const void *data, _IO_ssize_t n)
1244:{
1245: _IO_ssize_t to_do = n;
1246: while (to_do > 0)
1247: {
1248: _IO_ssize_t count = (__builtin_expect (f->_flags2
1249: & _IO_FLAGS2_NOTCANCEL, 0)
1250: ? write_not_cancel (f->_fileno, data, to_do)
1251: : write (f->_fileno, data, to_do));
...
write()の呼び出しがある。最終的にはここでwrite()が呼ばれ、メッセージが出力されることになるようだ。
この過程は、独自ビルドしたglibcを用いてリンクしたhelloをGDBで解析するとソースコードと実際の関数呼び出し手順を見比べることができる。GDBで「up」「down」といったコマンドで関数呼び出しを遡ったり戻ったりすることができるので、比較してみると参考になるだろう。
書籍紹介
ハロー“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」コマンドです。
