このページでは、あなたが書いたC言語ソースコードがどのように処理されて実行可能ファイルへ変換されるかについて解説します。
実行可能ファイルとはその名の通り実行可能なファイルです。例えば .exe
ファイルと聞くとイメージ付きやすい人もいるかもしれません。
例えばソースコードのファイル名が main.c
である時、gcc
を用いて下記のコマンドを実行すれば、main.c
から実行可能ファイルである main.exe
を生成することができます。
gcc main.c -o main.exe
実行するコマンドは1つですが、実はこのソースコード main.c
から実行可能ファイル main.exe
を生成する際にはさまざまな処理が行われています。
このページではこのソースコードから実行可能ファイルを生成するまでに行われる処理について解説します。
Contents
実行可能ファイルになるまでの流れ
C言語ソースコードが実行可能ファイルになるまでには4つの変換処理が行われます。
- プリプロセッサによる、C言語ソースコードを前処理済みC言語に変換するプリプロセス処理
- コンパイラによる、前処理済みC言語をアセンブリ言語に変換するコンパイル処理
- アセンブラによる、アセンブリ言語をオブジェクトファイルに変換するアセンブル処理
- リンカーによる、オブジェクトファイルに必要なライブラリをくっつけて実行可能ファイルに変換するリンク処理
この流れを図で表すと下のようになります。
つまり、細かく分けると下記の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
が下記のようなものとします。
#include "header.h"
int main(void){
int a;
int b;
a = A;
b = B;
add(a, b);
return 0;
}
さらに 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
に変換されるということになります。
# 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
に対して下記のコマンドを実行して main.s
を生成してみましょう。
gcc -S main.c
生成された 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
が生成されます。オブジェクトファイルには機械語が含まれているのでほとんど人間には読めない文字から形成されます。ただし、関数名などのシンボルはまだ読むことが可能です(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
は未定義であることを示す記号になります。
未定義の関数があると実行可能ファイルを生成することができません。
この未定義のシンボルを補うように結合するのがリンクです。
例えば main.c
で未定義の関数となっていた add
関数を含む下記のような 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.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
ライブラリのリンク
このページでは詳しい説明は省略しましたが、リンク時にライブラリをリンクすることも可能です。
その際には下記のように gcc
コマンドで -l
オプションを指定します。
gcc main.o -L . -lcalc -o main.exe
-l
の後ろにはライブラリファイル名から lib
と拡張子を取り除いたものを指定します。例えば上のコマンドの -lcalc
により libcalc.a
という名前のライブラリとリンクすることができます。また -L
の引数にはリンクするライブラリが置いているフォルダを指定します。
スポンサーリンク
まとめ
このページではC言語におけるソースコードから実行可能ファイル生成までの処理の流れ及び、そこで実行される各処理の内容について解説しました。
C言語ではソースコードに対して下記の4つの処理が行われて実行可能ファイルが生成されます。
- プリプロセス
- コンパイル
- アセンブル
- リンク
gcc
コマンドでは、オプションを付加することで、各処理で処理された結果を確認することも可能です。
実際確認してみると、自身が作成したソースコードがどのように変化して実行生成ファイルが作られるか確認できて楽しいです!
また、途中の段階のコードを見て解析したりチューニングしたりすることもできるので覚えておくと便利です!
オススメの参考書(PR)
C言語学習中だけど分からないことが多くて挫折しそう...という方には、下記の「スッキリわかるC言語入門」がオススメです!
まず学習を進める上で、参考書は2冊持っておくことをオススメします。この理由は下記の2つです。
- 参考書によって、解説の仕方は異なる
- 読み手によって、理解しやすい解説の仕方は異なる
ある人の説明聞いても理解できなかったけど、他の人からちょっと違った観点での説明を聞いて「あー、そういうことね!」って簡単に理解できた経験をお持ちの方も多いのではないでしょうか?
それと同じで、1冊の参考書を読んで理解できない事も、他の参考書とは異なる内容の解説を読むことで理解できる可能性があります。
なので、参考書は2冊持っておいた方が学習時に挫折しにくいというのが私の考えです。
特に上記の「スッキリわかるC言語入門」は、他の参考書とは違った切り口での解説が豊富で、他の参考書で理解できなかった内容に対して違った観点での解説を読むことができ、オススメです。題名の通り「なぜそうなるのか?」がスッキリ理解できるような解説内容にもなっており、C言語入門書としてもかなり分かりやすい参考書だと思います。
もちろんネット等でも色んな観点からの解説を読むことが出来ますので、分からない点は別の人・別の参考書の解説を読んで解決していきましょう!もちろん私のサイトも参考にしていただけると嬉しいです!
入門用のオススメ参考書は下記ページでも紹介していますので、こちらも是非参考にしていただければと思います。
https://daeudaeu.com/c_reference_book/