検索
連載

printf()のソースコードで、ソースコードリーディングのコツを身に付けるmain()関数の前には何があるのか(5)(1/3 ページ)

C言語の「Hello World!」プログラムで使われる、「printf()」「main()」関数の中身を、デバッガによる解析と逆アセンブル、ソースコード読解などのさまざまな側面から探る連載。前回まで、printf()内の中身をデバッグと逆アセンブルで探ってきたが、今回はソースコードリーディングで答え合わせをしてみる。

Share
Tweet
LINE
Hatena

連載目次

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

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

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


※編集部注:前回記事「OSのシステムコールの呼び出しとは&バイナリエディタの使い方」はこちら

標準入出力関数の実装を見る

 printf()は指定したフォーマットに従って文字列出力を行うための標準ライブラリ関数だ。入門書では定番の関数であるため、利用したことがないという人は少ないことだろう。

 しかしそのprintf()の中の処理を読んだことがあるか、となると話は変わってくるのではないだろうか。これほど多く利用されているprintf()だが、その実装はどのようになっているのだろうか?

 ここではprintf()のソースコードを読むことで、標準入出力関数の動作の仕組みに迫ってみよう。

printf()のソースコードを読む

 まずはprintf()のソースコードのありかだ。

 printf()はC言語の標準ライブラリ関数と呼ばれる。つまりC言語の開発環境において、標準で提供されるライブラリ関数だ。

 CentOSでは標準Cライブラリとしてglibcが採用されている。よってprintf()の本体は、glibcの中にあるはずだ。

printf()の本体を探す

 まずは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)
...
sysdeps/generic/math_ldbl_opt.h
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)));
include/libc-symbols.h

 ここで連載第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.

       | 次のページへ
ページトップに戻る