検索
連載

「Hello World!」の中身を探る意義と環境構築、main(C言語)のアセンブラコードの読み方main()関数の前には何があるのか(1)(3/4 ページ)

C言語の「Hello World!」プログラムで使われる、printfやmainの中身を、デバッガによる解析と逆アセンブル、ソースコード読解などのさまざまな側面から探る連載。初回は、「Hello World!」の中身を探る意義と環境構築について触れ、mainのアセンブラコードを読んでみる。

Share
Tweet
LINE
Hatena

実行ファイルの生成の手順

 上述したように実行ファイルを自前で生成しなおした場合、生成される実行ファイルには差異があり、本書の内容を同じようにトレースできない可能性がある。

 しかしそれを理解した上でならば、もちろんソースコードを自前の環境でコンパイルし、実行ファイルを生成してもいい。

 ここでは自分の環境で実行ファイルを生成する場合について説明しておこう。本書で扱う実行ファイルがどのような手順で作成されているのか、知りたい場合には参考にしてほしい。

実行ファイルを生成する

 まずはなんらかのテキストエディタによって、リスト1.1のC言語プログラムを作成し、hello.cというファイル名で保存しよう。

$ mkdir hello
$ cd hello
$ vi hello.c

 hello.cが準備できたら、作成したhello.cをコンパイルして実行してみよう。まずは以下を実行してみる。

$ ls
hello.c
$ gcc hello.c -o hello -Wall -g -O0 -static
$

 コンパイル・オプションが多数指定されているが、生成される実行ファイルの調整のためなので、ひとまずは気にしなくて構わない。

なお「-O0」というオプションがあるが、「-O0」はハイフンの後に「オー」と「ゼロ」を続けている。つまり「オーゼロ」であって、「オーオー」や「ゼロゼロ」ではないということに注意してほしい。


 そしてコンパイルの際に、もしかしたら以下のようなエラーが出るかもしれない。

$ gcc hello.c -o hello -Wall -g -O0 -static
-bash: gcc: command not found
$

 この場合はコンパイラであるgccがインストールされていないので、インストールしてほしい。CentOSならば、スーパーユーザで以下を実行する。

# yum install gcc

 さらに、以下のようなエラーが出ることもあるかもしれない。

$ gcc hello.c -o hello -Wall -g -O0 -static
/usr/bin/ld: cannot find -lc
collect2: ld returned 1 exit status
$

 これは-staticにより静的ライブラリをリンクしようとするが、標準Cライブラリであるいわゆるlibcの静的ライブラリ版がインストールされていないためだ。

 このような場合には、たとえばCentOSならばスーパーユーザで以下のようにしてライブラリを追加インストールしてから、再度コンパイルを試してみてほしい。

# yum install glibc-static

 うまくコンパイルできると、実行ファイルとして「hello」というファイルが作成される。確認してみよう。

$ gcc hello.c -o hello -Wall -g -O0 -static
$ ls
hello hello.c
$

 どうだろうか。まずはプログラムをコンパイルして、実行ファイルを生成することができただろうか?

コンパイル・オプション

 コンパイル時には、いくつかのオプションを指定している。以下のコンパイル時の、「-Wall」や「-g」といったものだ。

$ gcc hello.c -o hello -Wall -g -O0 -static

 このようなコンパイル・オプションの指定に慣れない読者のかたもいるかもしれないが、オプションによって出力される実行ファイルが様々に変化するので、コンパイル・オプションは重要だ。

 ここでコンパイル・オプションについて説明しておこう。

 まず「-Wall」はすべてのワーニングを出力するためのチェック用だ。ワーニングは単なる警告の場合もあるが、バグかバグすれすれのことも多いものだ。ワーニングはなるべく出力させ、可能な限り(というよりは、基本的にはすべてを)減らしておく癖をつけるといいだろう。

 「-g」はデバッガによるデバッグを可能にするためのオプションだ。これを付加することで実行ファイル中にデバッグ用の情報が組み込まれ、C言語のソースコードと対応させてのデバッグができるようになる。

 また「-O0」は最適化を行わないようにするためのオプションだ。これは先述したように「オー」に続けて「ゼロ」を付加しているわけだが、「ゼロ」は最適化の度合を表していて、「-O1」にすると最適化を、「-O2」にするとさらに進んだ最適化を行うようになる。

 さらに「-static」はリンクを静的に行うためのオプションだ。「静的リンク」「スタティックリンク」などのように呼ばれる。これらはこの後の解析をやりやすくするために指定している。

 ただしこれらは、書籍中で徐々に詳しく説明していく。よくわからなければ、まあそのようなものとして深くは考えずに読み飛ばしていただいてもかまわない。

プログラムの動作を確認する

 実行ファイルが生成されたら、プログラムを実行してみよう。以下のように実行ファイルを指定することで、カレントディレクトリ上の実行ファイルを実行することができる。

$ ./hello
Hello World! 1 ./hello
$

 無事に実行できたようだ。printf()が実行されて、「Hello World!」のメッセージが出力されている。

 さらにprintf()に引数として渡したargcとargv[0]が表示されている。argcにはコマンドラインから渡した引数の数、argv[]には引数列が渡されるが、先頭のゼロ番目には実行コマンド名が格納されるので、argcは1となり、argv[0]として「./hello」が渡されていることがわかる。

 ためしに、実行時にコマンドライン引数を与えてみよう。

[user@localhost hello]$ /home/user/hello/hello abc def
Hello World! 3 /home/user/hello/hello
[user@localhost hello]$

 まず引数として「abc」「def」の2つを与えているため、実行コマンド名と合わせてargcが3になっている。さらに実行コマンドはフルパスで指定したため、argv[0]がやはりフルパスで表示されていることがわかる。

 さて、これだけの動作のプログラムをどれだけ深く堀り下げることができるのだろうか?

 しかしこのような単純なプログラムであっても、動作のためには様々な要素が複雑に連携している。そしてその上でプログラムを書いているからこそ我々は「プログラムをコンパイルして実行してメッセージを出力」などというものすごく複雑な処理を、簡単に実現することができている。

 その「ものすごく複雑な処理」をひとつひとつ解き明かしていくことが、本書のテーマになる。

逆アセンブル結果を見る

 本書では実行ファイルを逆アセンブルした結果を参照することがある。

 逆アセンブルというのは、実行ファイル中の機械語コードから元になったニーモニックを復元することだ。

 ここで、逆アセンブル方法についても簡単に説明しておこう。

 逆アセンブルはobjdumpというコマンドによって行える。

[user@localhost hello]$ objdump -d hello | head
 
hello: file format elf32-i386
 
 
Disassembly of section .init:
 
08048140 <_init>:
 8048140:	55	push	%ebp
 8048141:	89 e5	mov	%esp,%ebp
 8048143:	53	push	%ebx
[user@localhost hello]$

 実際に実行すると大量の出力がされてしまうため、先頭付近のみ抽出してみた。lessなどに入力して検索すると、main()関数の先頭部分を見ることもできる。

 lessは「/」を押し、続けて検索文字列を入力することで検索ができる。

 実際に探すと、main()に相当する部分は以下のようになっていた。

080482bc <main>:
 80482bc:	55		push	%ebp
 80482bd:	89 e5		mov	%esp,%ebp
 80482bf:	83 e4 f0	and	$0xfffffff0,%esp
 80482c2:	83 ec 10	sub	$0x10,%esp
 80482c5:	8b 45 0c	mov	0xc(%ebp),%eax
 80482c8:	8b 10		mov	(%eax),%edx
 80482ca:	b8 0c 36 0b 08	mov	$0x80b360c,%eax
 80482cf:	89 54 24 08	mov	%edx,0x8(%esp)
 80482d3:	8b 55 08	mov	0x8(%ebp),%edx
 80482d6:	89 54 24 04	mov	%edx,0x4(%esp)
 80482da:	89 04 24	mov	%eax,(%esp)
 80482dd:	e8 7e 10 00 00	call	8049360 <_IO_printf>
 80482e2:	b8 00 00 00 00	mov	$0x0,%eax
 80482e7:	c9		leave
 80482e8:	c3 		et
...

 なおアセンブラについてはまた後ほど説明するので、よくわからないかたは、ここではひとまずはobjdumpコマンドにより逆アセンブルが可能であるということだけ知っておいていただければと思う。

実行ファイルの解析結果を見る

 また本書では、実行ファイルを解析するためにreadelfというコマンドを利用する。

 以下のようにreadelfコマンドを実行することで、実行ファイル上の様々な情報が出力される。

[user@localhost hello]$ readelf -a hello | head
ELF Header:
  Magic:	7f 45 4c 46 01 01 01 03 00 00 00 00 00 00 00 00
  Class:	ELF32
  Data:		2's complement, little endian
  Version:	1 (current)
  OS/ABI:	UNIX - Linux
  ABI Version:	0
  Type:		EXEC (Executable file)
  Machine:	Intel 80386
  Version:	0x1
[user@localhost hello]$

 readelfコマンドに関しても、必要になった箇所でその都度説明する。よってここではそのようなコマンドがある、ということだけ知っておいていただければ十分だ。

Copyright © ITmedia, Inc. All Rights Reserved.

ページトップに戻る