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

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

実行可能ファイルとはその名の通り実行可能なファイルです。例えば .exe ファイルと聞くとイメージ付きやすい人もいるかもしれません。

例えばソースコードのファイル名が main.c である時、gcc を用いて下記のコマンドを実行すれば、main.c から実行可能ファイルである main.exe を生成することができます。

gcc main.c -o main.exe

実行するコマンドは1つですが、実はこのソースコード main.c から実行可能ファイル main.exe を生成する際にはさまざまな処理が行われています。

このページではこのソースコードから実行可能ファイルを生成するまでに行われる処理について解説します。

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

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

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

この流れを図で表すと下のようになります。

つまり、細かく分けると下記の4つの処理が行われています。

  • プリプロセス
  • コンパイル
  • アセンブル
  • リンク

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

つまり、下記のようにコマンド1つでソースコードから実行可能ファイルを生成することができますが、内部では上記の4つの処理が行われているということです。

gcc main.c -o main.exe

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

プリプロセス

では一つ目の処理である「プリプロセス」について解説していきたいと思います。

スポンサーリンク

プリプロセスとは

プリプロセスとは、ユーザーの記述した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 でプリプロセスを行うには下記コマンドを実行します。

gcc -E main.c > main.p

このコマンドで出力される main.p は下記のようになりました。つまり、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.cmain 関数内で使用していたマクロ AB が、header.h#define により定義されている定数そのものに置き換えられていることが分かると思います。

さらに、header.h 内の add 関数の宣言が、main.c に組み込まれていることが分かると思います。

このようにプリプロセスでは #define で定義されたマクロの展開、#include で指定したヘッダーファイルの組み込み等、プリプロセッサ指令(#define#include など)を解釈してコンパイラが理解できるコードに変換します。

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

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

プリプロセッサ解説ページアイキャッチ【C言語】プリプロセッサについて解説!#includeや#defineの意味が理解できる!

スポンサーリンク

コンパイル

続いて「コンパイル」について解説していきます。

コンパイルとは

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

コンパイルを実行するコマンド

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

gcc -S ファイル名.c

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

スポンサーリンク

コンパイル結果

プリプロセスで用いた main.c に対して下記のコマンドを実行して main.s を生成してみましょう。

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 によって結果は変わると思います。

意味不明なコードに感じる方もいるかもしれませんが、これが main.c をアセンブリ言語に変換した結果になります。

一応下記で定数 3 と定数 5 が使用されていることや、

定数利用箇所
	movl	$3, -8(%rbp)
	movl	$5, -12(%rbp)

下記で add 関数が呼び出されていることが確認でき、main.c の名残があることが分かると思います。

関数呼び出し箇所
	callq	_add

コンパイル時の最適化オプション(-O1-O2 など)によって生成されるアセンブリコードが変わります。

ですので、どのように最適化が効いているかを確認できますし、ガチで高速化を行う場合などはこのコードを見ながらチューニングしたりすることもあります。

アセンブル

3つ目に解説する処理は「アセンブル」になります。

アセンブルとは

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

スポンサーリンク

アセンブルを実行するコマンド

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

gcc -c ファイル名.c

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

アセンブル結果

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

gcc -c main.c

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

ちなみにコンパイラで生成した 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 は未定義であることを示す記号になります。

未定義の関数があると実行可能ファイルを生成することができません。

この未定義のシンボルを補うように結合するのがリンクです。

例えば main.c で未定義の関数となっていた add 関数を含む下記のような calc.c を用意してみます。

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.c ののオブジェクトファイル calc.o を生成すると、こちらの calc.oadd 関数が定義されます。

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

ちなみに main.cadd 関数を定義してやればリンクが不要かというとそういうわけではありません。

実行可能ファイルを生成するためには、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

ライブラリのリンク

このページでは詳しい説明は省略しましたが、リンク時にライブラリをリンクすることも可能です。

その際には下記のように gcc コマンドで -l オプションを指定します。

gcc main.o -L . -lcalc -o main.exe

-l の後ろにはライブラリファイル名から lib と拡張子を取り除いたものを指定します。例えば上のコマンドの -lcalc により libcalc.a という名前のライブラリとリンクすることができます。また -L の引数にはリンクするライブラリが置いているフォルダを指定します。

スポンサーリンク

まとめ

このページではC言語におけるソースコードから実行可能ファイル生成までの処理の流れ及び、そこで実行される各処理の内容について解説しました。

C言語ではソースコードに対して下記の4つの処理が行われて実行可能ファイルが生成されます。

  • プリプロセス
  • コンパイル
  • アセンブル
  • リンク

gcc コマンドでは、オプションを付加することで、各処理で処理された結果を確認することも可能です。

実際確認してみると、自身が作成したソースコードがどのように変化して実行生成ファイルが作られるか確認できて楽しいです!

また、途中の段階のコードを見て解析したりチューニングしたりすることもできるので覚えておくと便利です!

コメントを残す

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