簡単な C プログラムを AMD64 向けにコンパイルして radare2 でディスアセンブルしてみよう!(Part 3)
June 24, 2020

ビルドとは



この辺の記事 を見ると 「コンパイル」 という行為により、人間が C 言語に込めた思いがコンピュータのわかる言語に翻訳され、コンピュータがそれを実行し、人間の夢を叶える…。

…とかわけわからん宗教じみたこと言ってましたけど、あれはたぶん厳密には少し正しくなくて…。

とりあえず適当に書いた C プログラムと、それをコンパイルした出力結果を並べて表示してみます。

compile


なにかおかしいですよね?

だって 別の記事 で 「人間には読むことがかなり困難な機械語に翻訳する行為をコンパイル」 と紹介したにも関わらず、この出力結果まあまあ読めませんか…?(まあ読みづらいんですけど)
そもそも英語で text とか size とか書いてますし…普通の ASCII Text フォーマット、今読んでいただいているこのブログと同じような文字で記述されていますよね?

実はこの 「コンパイル」 を内包した 「ビルド」 という一連のプロセスがあり、その 「ビルド」 を経てはじめてあなたの書いた C プログラムは機械語に翻訳され、自動的に必要なファイルがさらに足され (そう、 C 言語で書かれたあなたのプログラムを翻訳するだけではまだコンピュータはプログラムを実行できないんです…) 実行可能なファイルとなります。

ただし文脈によっては 「ビルド」 のことをコンパイルといっていることもあるし(僕も別の記事でこれと似たようなことをした気がする)、このへん 「コンパイル」 という言葉は個人的には非常に紛らわしく、正直なにが正解かわかってないです。

とりあえず、 「コンパイル」 はともかく、 「ビルド」 という言葉を見たら 「自分が書いたプログラムを翻訳してコンピュータが理解できてさらに実行もできるファイルを作るんだな…」 くらいに思っておいてください。



4 つのビルドステップ



ここで少し踏み込んで 「C 言語で書いたプログラムが実行できる機械語に翻訳されるまで」 、つまり 「ビルド」 の主な 4 つのステップについて書いてみます。

1. プリプロセス
2. コンパイル
3. アセンブル
4. リンク


上記についてひとつひとつ丁寧に説明を試みます。

1. プリプロセス

test_c

上に C で書かれたどうしようもないプログラムがありますが、このようなプログラムにおいてプリプロセスでは、 #include#define に関する処理をします。

#include って 「あとで読む予定のファイルの名前をここに置いておく(でも今はめんどくさいからまだ読まなくていいや、みたいな。積ん読みたいな)」 みたいな意味合いで書かれてい…ます(ちょっと怪しいかも)。
プリプロセスでは実際に積ん読に積んであるファイルの内容を読みこんでファイルの実体に置き換える感じです。

また #define では人間がコードをより生産的に書くことができるようにマクロを定義できますが、コンピュータには不要なのでプリプロセスでマクロは実際の中身に置き換えられます。

ここで「マクロ」について補足なのですが、この言葉、もしかしたらプログラマなどでない限りあまりなじみのない言葉かもしれません。
ですが、なんら難しいものではありません。ここでは「略称」くらいに考えておいてください!

たとえば僕の本名は Vane11ope Von Schw33tz ですが、毎回いちいち Vane11ope Von Schw33tz と綴っていたら一週間で一日くらい損しそうです。
マクロがあれば #define Vane11ope "Vane11ope Von Schw33tz" と定義し、 プログラムで必要な部分には Vane11ope とだけ書けば、あとでプリプロセスがすべての Vane11ope を Vane11ope Von Schw33tz に置き換えてくれます。

…つ、伝わりましたかね…伝わるといいな…。

下記プログラムでは GREETING"Hello World.\n" と定義されていますが GREETING と書いてある箇所がすべて "Hello World.\n" で置き換えられる感じです。

test_c

ちなみにこれらプリプロセスで処理されるような #include だの #define だのは directive と呼ばれます。まあ現状は特に覚える必要もないかも。まあいいや。

下に軽くまとめた図を置いておきます。

directive

directive_replace


ちなみにプリプロセスが行われるとファイルがどうなるのか見たいときのコマンドは gcc -E ファイル名.c です!
僕の場合だと gcc -E test.c と入力し、以下のような結果が得られました(…長すぎるため抜粋した)。

元ファイル test.c

test_c

gcc -E test.c の出力結果

gcc__E


2. コンパイル

いよいよコンパイルの登場なのですが、さきほども紹介したように、コンパイルの段階ではまだ機械語に翻訳は行われません。(なんなんだいったい)

でもでも、繰り返しになるんですが、文脈次第では機械語に翻訳し、さらには実行可能なファイルを作成することまで含めてコンパイルとも呼ぶようです。このへん僕は🔰なので正直よくわかりません。

とりあえず、ビルドを語る文脈におけるコンパイルでは、機械語にはまだ翻訳されず、C 言語と機械語の中間地点のような言語に翻訳される、と思ってください。下記のように。

左: 元ファイル test.c
右: コンパイル後ファイル test.s

compile


ちなみに test.s を作成するコマンドは gcc -S あなたのファイル.c となり、僕の場合は gcc -S test.c となります。

このステップが存在する理由は インラインアセンブラ を書くことができるようにするためであったり、それからこのあとのステップ 「アセンブル」 で出てくる道具アセンブラを gcc 標準のものではないものを用いることができるようにするためであったり、などあるようですが、このブログの想定読者的にたぶん何言ってるかわからないと思いますし、僕自身も正直そのメリットを享受できるほどのプログラミングは経験したことがありません…。

まあこれは結構よくある考え方なのですが、一連の作業を部品のように細かくしておくことで、それぞれの段階においていろいろカスタマイズが効く、というのはあります。

たとえば車を作る工程を考えたときに、一つの工程ですべてを組み上げてしまうより、それぞれの工程を細かく分割しておくことで、たとえばある工程において違うパーツを使いたくなったとき、あるいはその工程自体を他社にお願いしたくなったときなどに、対応しやすくなります…よね?

そんな感じで今回のこのビルドプロセスでも、細かく工程を分け、それぞれに別のツールを使える余地を残した、みたいな感じかと想像します。

とりあえず今は、こういう手順があるんだなー、こういうことが行われるんだなー、というのをイメージでだいたいつかみ取ってもらえれば十分です。繰り返しますがこの段階ではまだ人間に読める文字で書かれているのでなんとかコードは解読できます。

3. アセンブル

この工程を経てようやくコードがコンピュータ側に近づきます。
…何言ってるかわかりませんが、要は人間には読むのがかなり困難になってきます。

またよくわからないたとえを用いてみます。

たとえばどこかに旅行するとします。その場合、下記のような行程になるかと思います。

1. どこか目的地に移動する。
2. 観光地など楽しむ。
3. やがて現地の人と恋に落ちる。
4. デートを重ねる。
5. 将来を約束する。
6. 別れを惜しみつつ帰路に就く。


...完全に余計な行程が入っている気もしますが気のせいでしょう。

それはともかくとして C 言語でプログラムを書くというのはたいてい上記の旅行でいえば旅行のメインの部分、つまり 2 ~ 5 の部分を記述することに相当します。

残りの部分、たとえば現地に移動したり、そこから帰ったりというような、旅行の本質とはあまり関係ない面倒な部分は書いてません。(まあ個人的には移動時間にこそ旅行の本質はあると思ってますが)

そこらへんはリンカという人が次のリンクという工程で自動的にやってくれます。

つまりそのリンクという工程なくしてはプログラムはたいていまだ不完全であり(だって旅行のたとえでいえば、行きの手段や帰りの手段を確保していないようなものなので)、そのためプログラムの実行はできません。

具体的にアセンブルを実行するコマンドは gcc -c test.c となります。そうすると test.o というファイルが出てくると思います。試しにエディタで普通に開いてみますか…。

test_o
ところどころしか読めませんね…。

これあとで読めるようになるので少し待ってくださいね!こういうの読むために radare2 があるんです!

4. リンク

3 のアセンブルでも書いたのですが、この工程では、旅行のたとえでいうと(しつこい)、現地に移動するという部分と現地から帰る部分がしっかりと旅行に組み込まれます。交通手段も確保され、旅行が実行可能になりました。

…もう少し真面目に説明すると、たとえばプログラムが実行されるとき、まずローダという働き者がプログラムをメモリにロードし、その後プログラムがローダから処理を引き継ぎ、プログラムの中身(=あなたが書いた、プログラムのメインの部分)を実行する、という流れになります。
この「ローダから引き継ぐ処理」がプログラムの中になくては、プログラムは正常に実行されません。めんどくさいですね…。

そういったプログラムのメインではない、だけどプログラムを実行するのに必要、みたいな処理を自分で書かなくてもいいようにしてくれるのが、このリンクというステップです!

具体的には、パソコンにはかつて同じような処理を書いた賢い人の資産がすでにあったりするのですが(気付いてないとは思いますが)、そのような資産をリンクが探してきて、自分のプログラムに取り込んでくれます。

ちなみに実行可能なプログラムが実際にどのようにメモリにロードされ実行されるか、などについて興味が出てきた人もいるでしょう。そんな人は この記事 とか読むといいことが起こるかもしれません!

これを実行するのはシンプルに gcc あなたのファイル.c でいけます。僕の場合は gcc test.c です。
すると a.out という、翻訳も終わり、実行可能となったファイルが作られます!

どのようなコンパイラやコマンド、またはファイルが使用されたかなどは

gcc -### あなたのファイル.c


などでチェックしてみることができます。僕の場合こんな感じでした!

detail


ではさっそく実行してみましょうか。
…と思ったら実行は この記事 の最後のほうですでにしていましたね。
a.out の中身をエディタで見てみるとどうでしょうか。

a_out


「アセンブル」 の test.o のような様相を呈していますが、もっとひどいですね。リンクによりいろいろなプログラムが加わった結果かもしれません。これも radare2 であとで見ます!

…また今回もどうでもいい説明が長くなりすぎて radare2 を使うところまでいかなかった😭

…次回こそは…



最後に



今日の内容がより上手にかつ専門的にまとめられたすごい記事 (そんなのがあるならじゃあなんで今回この記事を書いたんだって感じだけど) を貼っておくので興味ある人は読んでみてください。

x86 においてプログラムはどのようにしてロードされ、 main 関数まで到達するのか

もっと専門的なやつ


…なんか今回の記事、ほんとになにをしたかったのかよくわからないのですが、次回は絶対に radare2 の説明します…絶対に。