C言語の「Hello World!」プログラムで使われる、「printf()」「main()」関数の中身を、デバッガによる解析と逆アセンブル、ソースコード読解などのさまざまな側面から探る連載。前回まで、printf()内の中身をデバッグと逆アセンブルで探ってきたが、今回はソースコードリーディングで答え合わせをしてみる。
書籍の中から有用な技術情報をピックアップして紹介する本シリーズ。今回は、秀和システム発行の書籍『ハロー“Hello, World” OSと標準ライブラリのシゴトとしくみ(2015年9月11日発行)』からの抜粋です。
ご注意:本稿は、著者及び出版社の許可を得て、そのまま転載したものです。このため用字用語の統一ルールなどは@ITのそれとは一致しません。あらかじめご了承ください。
※編集部注:前回記事「OSのシステムコールの呼び出しとは&バイナリエディタの使い方」はこちら
printf()は指定したフォーマットに従って文字列出力を行うための標準ライブラリ関数だ。入門書では定番の関数であるため、利用したことがないという人は少ないことだろう。
しかしそのprintf()の中の処理を読んだことがあるか、となると話は変わってくるのではないだろうか。これほど多く利用されているprintf()だが、その実装はどのようになっているのだろうか?
ここではprintf()のソースコードを読むことで、標準入出力関数の動作の仕組みに迫ってみよう。
まずはprintf()のソースコードのありかだ。
printf()はC言語の標準ライブラリ関数と呼ばれる。つまりC言語の開発環境において、標準で提供されるライブラリ関数だ。
CentOSでは標準Cライブラリとしてglibcが採用されている。よってprintf()の本体は、glibcの中にあるはずだ。
まずはglibcのソースコードの中から、printf()に関連しそうなファイルを探してみよう。「printf」と名前のつくファイルはないだろうか。
[user@localhost ~]$ cd glibc-2.21 [user@localhost glibc-2.21]$ find . -name "*printf*" ./include/printf.h ./stdio-common/test-vfprintf.c ./stdio-common/tst-sprintf3.c ./stdio-common/bug-vfprintf-nargs.c ./stdio-common/reg-printf.c ./stdio-common/printf_size.c ./stdio-common/printf-parsewc.c ./stdio-common/tst-printf.sh ./stdio-common/vprintf.c ./stdio-common/tst-sprintf2.c ./stdio-common/printf_fp.c ./stdio-common/tst-wc-printf.c ./stdio-common/fxprintf.c ./stdio-common/printf.h ./stdio-common/printf.c ...
stdio-commonというディレクトリ以下に、printf()に関係しそうなファイルが大量にあるようだ。他にもsysdeps、libio、debugといったディレクトリのファイルもヒットするのだが、ディレクトリ名を見る限り、stdio-commonが本命のほうに思える。
[user@localhost glibc-2.21]$ cd stdio-common [user@localhost stdio-common]$ ls *printf.c asprintf.c printf.c test-vfprintf.c tst-swprintf.c vprintf.c dprintf.c reg-printf.c tst-obprintf.c tst-wc-printf.c fprintf.c snprintf.c tst-printf.c vfprintf.c fxprintf.c sprintf.c tst-sprintf.c vfwprintf.c [user@localhost stdio-common]$
printf.cというファイルがあるようだ。その内容を見てみると、以下のような関数の定義があった。
24:/* Write formatted output to stdout from the format string FORMAT. */ 25:/* VARARGS1 */ 26:int 27:__printf (const char *format, ...) 28:{ 29: va_list arg; 30: int done; 31: 32: va_start (arg, format); 33: done = vfprintf (stdout, format, arg); 34: va_end (arg); 35: 36: return done; 37:} 38: 39:#undef _IO_printf 40:ldbl_strong_alias (__printf, printf); 41:/* This is for libg++. */ 42:ldbl_strong_alias (__printf, _IO_printf);
関数名が__printf()になっているが、これはldbl_strong_alias()というマクロによってprintf()というエイリアスが定義されるようだ。つまり上記__printf()が、printf()の本体と言える。
以下は、ldbl_strong_alias()のマクロの定義を追ったものだ。
... 12:#define ldbl_strong_alias(name, aliasname) strong_alias (name, aliasname) ...
114:/* Define ALIASNAME as a strong alias for NAME. */ 115:# define strong_alias(name, aliasname) _strong_alias(name, aliasname) 116:# define _strong_alias(name, aliasname) \ 117: extern __typeof (name) aliasname __attribute__ ((alias (#name)));
ここで連載第3回の図2.11をもう一度見直してほしい。
実行ファイルhelloをGDBで解析したとき、printf()の内部はvfprintf()を呼び出すだけの処理になっていた。そして上記__printf()のソースコードもそのようになっている。よって確かにこれが、printf()の本体のようだ。
printf()と__printf()が配置されているアドレスを調べてみよう。
[user@localhost hello]$ readelf -a hello | grep " printf" 960: 08049360 35 FUNC GLOBAL DEFAULT 6 printf [user@localhost hello]$ readelf -a hello | grep __printf | grep -v printf_ 946: 08049360 35 FUNC GLOBAL DEFAULT 6 __printf [user@localhost hello]$
どちらも配置先は08049360であり一致している。やはり同一の関数のようだ。
Copyright © ITmedia, Inc. All Rights Reserved.