「Hello World!」の中身を探る意義と環境構築、main(C言語)のアセンブラコードの読み方:main()関数の前には何があるのか(1)(3/4 ページ)
C言語の「Hello World!」プログラムで使われる、printfやmainの中身を、デバッガによる解析と逆アセンブル、ソースコード読解などのさまざまな側面から探る連載。初回は、「Hello World!」の中身を探る意義と環境構築について触れ、mainのアセンブラコードを読んでみる。
実行ファイルの生成の手順
上述したように実行ファイルを自前で生成しなおした場合、生成される実行ファイルには差異があり、本書の内容を同じようにトレースできない可能性がある。
しかしそれを理解した上でならば、もちろんソースコードを自前の環境でコンパイルし、実行ファイルを生成してもいい。
ここでは自分の環境で実行ファイルを生成する場合について説明しておこう。本書で扱う実行ファイルがどのような手順で作成されているのか、知りたい場合には参考にしてほしい。
実行ファイルを生成する
まずはなんらかのテキストエディタによって、リスト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.
関連記事
- プログラミング言語Cについて知ろう
プログラミング言語の基本となる「C」。正しい文法や作法を身に付けよう。Cには確かに学ぶだけの価値がある(編集部) - シェルコード解析に必携の「5つ道具」
コンピュータウイルスの解析などに欠かせないリバースエンジニアリング技術ですが、何だか難しそうだな、という印象を抱いている人も多いのではないでしょうか。この連載では、「シェルコード」を例に、実践形式でその基礎を紹介していきます。(編集部) - 【 od 】コマンド――ファイルを8進数や16進数でダンプする
本連載は、Linuxのコマンドについて、基本書式からオプション、具体的な実行例までを紹介していきます。今回は、「od」コマンドです。