C言語ソースコードが実行可能ファイルになるまでの流れ

このページでは、あなたが書いたC言語ソースコードがどのような処理が行われて実行可能ファイルに変換されるかについて解説します。

実行可能ファイルになるまでの流れ

C言語ソースコードが実行可能ファイルになるまでには4つの変換処理が行われます。

  1. プリプロセッサによる、C言語ソースコードを前処理済みC言語に変換するプリプロセス処理
  2. コンパイラによる、前処理済みC言語をアセンブリ言語に変換するコンパイル処理
  3. アセンブラによる、アセンブリ言語をオブジェクトファイルに変換するアセンブル処理
  4. リンカーによる、オブジェクトファイルに必要なライブラリをくっつけて実行可能ファイルに変換するリンク処理

細かく分けるとこのように、プリプロセス、コンパイル、アセンブル、リンクの4つの処理が行われています。文脈によってはコンパイラがプリプロセッサからリンカーまで全て実行するものであったり、プリプロセッサからアセンブラまでを実行するものを指す場合がありますが、正確にはこれらの4つの処理が実行されます。

ここからは、それぞれの処理でどのような変換が行われるか、変換が行われるとどのようなコードに変わるのかを解説していきたいと思います。

プリプロセス

プリプロセスとは、ユーザーの記述したC言語ソースコードをコンパイラが理解できるプリミティブなC言語コードに変換する(コンパイル前の)前処理です。例えば #include や #define といったプリプロセッサ指令を変換します。

gcc でC言語ソースコードに対してプリプロセスのみを実行するには -E オプションを付加します。

gcc -E ファイル名.c

このコマンドをターミナル等から実行することによりプリプロセスのみ実行した結果が表示されます。

ターミナル

Mac OS Xであれば、ターミナルアプリは「アプリケーション」フォルダの下の「ユーティリティ」フォルダ内にあります

具体的にどのように処理が行われるか見ていきましょう。例えば main.c が下記のようなものとします。

main.c
#include "header.h"

int main(void){
    int a;
    int b;
    a = A;
    b = B;
    add(a, b);
    return 0;
}

さらに header.h が下記のようなものとします。

header.h
#define A 3
#define B 5
int add(int, int);

この時、下記コマンドにより main.c に対してプリプロセッサ処理を行うと、

gcc -E main.c > main.p

下記のように変換されます。

main.p
# 1 "main.c"
# 1 "" 1
# 1 "" 3
# 361 "" 3
# 1 "" 1
# 1 "" 2
# 1 "main.c" 2
# 1 "./header.h" 1



int add(int, int);
# 2 "main.c" 2

int main(void){
    int a;
    int b;
    a = 3;
    b = 5;
    add(a, b);
    return 0;
}

main.c の main関数内で使用していたマクロ A や B が、header.h 内で #define で定義されていた値に変換されていることが分かると思います。さらに、header.h 内の add関数の宣言が、main.c に組み込まれていることが分かると思います。このようにプリプロセスでは #define で定義されたマクロの展開、#include で指定したヘッダーファイルの組み込み等、プリプロセッサ指令(#define, #include など)を解釈してコンパイラが理解できるコードに変換します。

プリプロセスにより、人間にとってはちょっと読みにくくなりますが、コンパイラが理解するために必要な処理になります。

プリプロセッサの処理については下記ページで詳細に解説していますので、プリプロセス・プリプロセッサについて詳しく知りたい方は読んでみてください。

C言語の「#include」「#define」の意味は?プリプロセッサについて解説!

スポンサーリンク

コンパイル

コンパイルとは、プリプロセス処理後のC言語ソースコードを解釈し、アセンブリ言語のコードに変換する処理です。この処理はコンパイラによって実行されます。

gcc でコンパイルまでの処理のみを実行するためには -S オプションを付加します。

gcc -S ファイル名.c

これにより ファイル名.s のファイルが生成されます。これがアセンブリ言語のコードに変換されたファイルです。

プリプロセスで用いた main.c に対して下記のコマンドを実行すると、

gcc -S main.c

コンパイルまでの処理を実行すると下記のような main.s が生成されます。

main.s
	.section	__TEXT,__text,regular,pure_instructions
	.build_version macos, 10, 14	sdk_version 10, 14
	.globl	_main                   ## -- Begin function main
	.p2align	4, 0x90
_main:                                  ## @main
	.cfi_startproc
## %bb.0:
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset %rbp, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register %rbp
	subq	$16, %rsp
	movl	$0, -4(%rbp)
	movl	$3, -8(%rbp)
	movl	$5, -12(%rbp)
	movl	-8(%rbp), %edi
	movl	-12(%rbp), %esi
	callq	_add
	xorl	%esi, %esi
	movl	%eax, -16(%rbp)         ## 4-byte Spill
	movl	%esi, %eax
	addq	$16, %rsp
	popq	%rbp
	retq
	.cfi_endproc
                                        ## -- End function

.subsections_via_symbols

おそらく使用しているPCによって結果は変わると思います。

かなり意味不明なコードになってしまいましたが、これがアセンブリ言語に変換された結果です。一応下記で定数3と定数5が使用されていることや、

	movl	$3, -8(%rbp)
	movl	$5, -12(%rbp)

下記でadd関数が呼び出されていることが確認できます。

	callq	_add

コンパイル時の最適化オプション( -O や -O2 など)によって生成されるアセンブリコードが変わります。ですので、どのように最適化が効いているかを確認できますし、ガチで高速化を行う場合などはこのコードを見ながらチューニングしたりすることもあります。

アセンブル

アセンブルとは、アセンブリ言語のコードを解釈し、オブジェクトファイルに変換する処理です。この処理はアセンブラによって実行されます。

gcc でC言語ソースコードのファイルに対してアセンブルまでの処理を実行するためには -c オプションを付加します。

gcc -c ファイル名.c

これにより ファイル名.c のファイルが生成されます。これがオブジェクトファイルです。

こちらでもプリプロセスで用いた main.c に対してアセンブルまでの処理を下記コマンドで実行してみましょう。

gcc -c main.c

すると下記のような main.o が生成されます。オブジェクトファイルには機械語が含まれているのでほとんど人間には読めない文字から形成されます。ただし、関数名などのシンボルはまだ読むことが可能です(main や add)。

ちなみにコンパイラで生成した main.s に対して下記を実行することでも、アセンブルを実行することが可能です。

as main.s -o main.o

もしくは

gcc -c main.s

リンク

リンクとは、オブジェクトファイルをライブラリや他のオブジェクトファイルと結合し、実行可能ファイルに変換する処理です。この処理はリンカーによって実行されます。

gcc では引数にオブジェクトファイルを他のオブジェクトファイルと並べて書いてやることで、その2つのオブジェクトファイルをくっつけて実行可能ファイルを生成してくれます。

gcc ファイル名1.o ファイル名2.o -o 実行ファイル名

これにより -o で指定した名前のファイルが生成されます。このファイルは実行可能なファイルになります。

例えば、アセンブルで生成した main.o について考えてみましょう。この main.o はもともとプリプロセスで紹介した main.c をプリプロセス・コンパイル・アセンブルしたものになります。この main.c では add関数を実行していますが、main.c にはこのadd関数は定義されていません。

このことは main.o に対して「nmコマンド」を実行することでも確認することができます。nmコマンドとは、オブジェクトファイルに含まれるシンボル情報(変数・関数)を表示するコマンドです。実際に main.o に対して nmコマンドを実行した結果は下記の通りになります。

nm main.o
                 U _add
0000000000000000 T _main

詳しい説明は割愛しますが、_add の前の U は未定義であることを示す記号になります。未定義の関数があると実行可能ファイルを生成することができません。

この未定義のシンボルを補うように結合するのがリンクです。例えば下記のような calc.c を用意し、

int add(int x, int y){
    return x + y;
}

int mul(int x, int y){
    return x * y;
}

int div(int x, int y){
    return x / y;
}

int sub(int x, int y){
    return x - y;
}

このオブジェクトファイル calc.o を生成すると、こちらの calc.o にadd関数が定義されます。そして、この add関数を持つ calc.o と main.o とをリンクしてやれば、お互いに未定義のシンボルがなくなるので実行可能ファイルを生成することができます。

ちなみに main.c にadd関数を定義してやればリンクが不要かというとそういうわけではありません。実行可能ファイルを生成するためには、OSがそのファイルを実行できるようにするためのライブラリに対してもリンクが必要だからです。なので実行可能ファイルを生成するためには必ずリンクが必要です。ただし、OS実行のためのライブラリはリンクを実行すると勝手にリンクしてくれるので、何かオプションをつけたりする必要はありません。

ライブラリについては下記ページで解説していますので、こちらもぜひ合わせて読んでみてください。

ライブラリについて解説!静的・動的とは?それぞれのメリットは?

スポンサーリンク

コマンド

ここまで gcc のコマンドをいくつか紹介しましたので、ここでそのコマンドをまとめておきます。

プリプロセスのみ実行

gcc -E main.c

コンパイルまで実行

gcc -S main.c

アセンブルまで実行

gcc -c main.c
gcc -c main.s
ar main.s

リンクまで実行

gcc main.c calc.c -o main.exe
gcc main.o calc.o -o main.exe

ライブラリ(libcalc.a or libcalc.so)を使用した場合のリンク

gcc main.o -L . -lcalc -o main.exe
※-L の引数にはライブラリが置いているフォルダを指定

まとめ

C言語ソースコードは、プリプロセッサ、コンパイラ、アセンブラ、リンカーが処理を行うことで実行可能なファイルに変換されます。gcc であればオプションを付加することで、途中の段階のコードを出力することが可能です。途中の段階のコードを見て解析したりチューニングしたりすることもできるので覚えておくと便利です!

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です