[compiler] an introduction to gcc

この本がなかなか面白かったので、7割ぐらい読んでしまった。

gccコンパイラはよく使用する機会があるけれども、あまり知らない機能が多いことに気がつきました。

この本の中でも、11章のAn overview of the compilation process は結構面白いです。普段コンパイルで省略している部分をあえて省略せずに、冗長に実行することでコンパイルの流れを知ることが出来ます。というわけでその内容について少し紹介してみます。

この本によると、コンパイルの流れは以下のようになるということが書かれています。

  • preprocessing (to expand macros)
  • compilation (from source code to assembly language)
  • assembly (from assembly language to machine code)
  • linking (to create the final executable)

この流れを実際のコマンドを利用して体感することが出来ます。

まず、preprocessingだが、これはマクロの展開である。マクロの展開とはどういうことかというと、例えば#include を例にとると、このファイルを全て展開する!!ということを意味している。実際にマクロ展開されたファイルは.iファイルというものになるのであるが、このソースが結構面白い。

実際に例に出して流れを追ってみてみます。

まずサンプルソース. hello.c

#include <stdio.h>
{
    printf("hello world\n");
    return 0;
}

はじめに、processingというわけでマクロ展開します。

これをマクロ展開するには以下のようなコマンドを実行する。

cpp hello.c > hello.i

このコマンドを実行することで、hello.cファイルをマクロ展開することが出来る。マクロ展開されたファイルは次のような形に変化する。

# 1 "test.c"
# 1 ""
# 1 ""
# 1 "test.c"
# 1 "/usr/include/stdio.h" 1 3 4
# 28 "/usr/include/stdio.h" 3 4
# 1 "/usr/include/features.h" 1 3 4
# 295 "/usr/include/features.h" 3 4
# 1 "/usr/include/sys/cdefs.h" 1 3 4
# 296 "/usr/include/features.h" 2 3 4
# 318 "/usr/include/features.h" 3 4
# 1 "/usr/include/gnu/stubs.h" 1 3 4
# 319 "/usr/include/features.h" 2 3 4
# 29 "/usr/include/stdio.h" 2 3 4





# 1 "/usr/lib/gcc-lib/i486-linux/3.3.5/include/stddef.h" 1 3 4
# 213 "/usr/lib/gcc-lib/i486-linux/3.3.5/include/stddef.h" 3 4
typedef unsigned int size_t;
# 35 "/usr/include/stdio.h" 2 3 4

# 1 "/usr/include/bits/types.h" 1 3 4
# 28 "/usr/include/bits/types.h" 3 4
# 1 "/usr/include/bits/wordsize.h" 1 3 4
# 29 "/usr/include/bits/types.h" 2 3 4


# 1 "/usr/lib/gcc-lib/i486-linux/3.3.5/include/stddef.h" 1 3 4
# 32 "/usr/include/bits/types.h" 2 3 4


typedef unsigned char __u_char;
typedef unsigned short int __u_short;
typedef unsigned int __u_int;
typedef unsigned long int __u_long;

こんな感じ。 stdio.hファイルを開いてるからものすごい量になります! hello.cファイルが5行なのに対し、hello.iファイルはなんと910行!にもなります。マクロ展開恐るべしですね。

次に、マクロ展開したファイルをコンパイルさせアセンブリファイルへと変換させます。

実行コードは以下のとおり

gcc -Wall -S hello.i

これを実行すると、hello.sファイルが出来ます。

hello.sファイルの中身は以下のようになります。

	.file	"test.c"
	.section	.rodata
.LC0:
	.string	"hello world\n"
	.text
.globl main
	.type	main, @function
main:
	pushl	%ebp
	movl	%esp, %ebp
	subl	$8, %esp
	andl	$-16, %esp
	movl	$0, %eax
	subl	%eax, %esp
	movl	$.LC0, (%esp)
	call	printf
	movl	$0, %eax
	leave
	ret
	.size	main, .-main
	.section	.note.GNU-stack,"",@progbits
	.ident	"GCC: (GNU) 3.3.5 (Debian 1:3.3.5-13)"

22行と実にシンプルです。910行もあった.iファイルとはえらい違いです(まぁ使わないライブラリとコメントが大半ってのが原因だけど(ノ∀`))

というわけで、アセンブリファイルができたので、次にアセンブリアセンブラーを使って機械語へと翻訳させます。

実行するコマンドは以下のとおり

as hello.s -o hello.o

これでオブジェクトファイルが出来ました。

最後にlinkerの出番です。これはオブジェクトファイルを実行ファイルへと変換させるために必要です。

実行ファイルを作るためにはさまざまな外部関数をC run-time(crt)ライブラリから拝借する使う必要があります。

例えば、hello.oを実行ファイルにするには以下のようなコマンドを実行する必要があります。

$ ld -dynamic-linker /lib/ld-linux.so.2 /usr/lib/crt1.o
/usr/lib/crti.o /usr/lib/gcc-lib/i686/3.3.1/crtbegin.o
-L/usr/lib/gcc-lib/i686/3.3.1 hello.o -lgcc -lgcc_eh
-lc -lgcc -lgcc_eh /usr/lib/gcc-lib/i686/3.3.1/crtend.o
/usr/lib/crtn.o

とまぁこんな感じで、非常にめんどくさいです。

しかしながら、これらのプロセスはgccコマンドが自動的で提供してくれるので、実際に必要とするコマンドは以下のようになります。

gcc hello.o

これだけです。ほんとにgccは優秀なコンパイルですよね(ノ∀`) ldコマンドのめんどくさいことといったら..

といったわけでgcc hello.oを実行したら、a.outファイルが作成されます。

以下のコマンドを入力すると

./a.out

hello world と端末に出力されます。

といったわけでgccコンパイルの実行手順を追ってみました。こういったコンパイラ追体験をすることで、マクロファイルの展開だとか、アセンブリファイルの作成だとかgccは内部で処理が完結していて非常に優秀なコンパイルであるということが再認識できました。

というわけで、一度くらいはこういった体験をしてみるとプログラムの内部動作についてのちょっとした素養が少しぐらい身につくかもしれないと思いました。