C言語がコンパイルされて実行可能になるまでの流れ

コンパイルの処理は大きく分けて下記の処理にわけられる。

この記事では、C言語Hello World を過程毎に追い、プログラムが出来上がるまでの流れを追う。

登場するファイル

  • main.c - ソースファイル
  • main.s - ソースファイルからコンパイルされたアセンブラソースファイル
  • main.o - アセンブル後のオブジェクトファイル
  • main - 出来上がったバイナリ

ソースコード

普通の C 言語の Hello World. 下記コードを main.c として保存する。

#include <stdio.h>
int main(){
  printf("Hello world!\n");
  return 0;
}

プリプロセス

プリプロセスとは、マクロの展開や #include や #ifdef などのディレクティブの処理が行われることを指す。
ソースファイルのプリプロセス後のソースを見るには以下のコマンドを使う。

$ gcc -E main.c

結果、下記のように大きなファイルが出来上がる。(コメント行と空白行を取り除いてある)

typedef unsigned char __u_char;
typedef unsigned short int __u_short;
typedef unsigned int __u_int;
typedef unsigned long int __u_long;
typedef signed char __int8_t;
typedef unsigned char __uint8_t;
typedef signed short int __int16_t;
typedef unsigned short int __uint16_t;
typedef signed int __int32_t;
typedef unsigned int __uint32_t;
typedef signed long int __int64_t;
typedef unsigned long int __uint64_t;
typedef long int __quad_t;
typedef unsigned long int __u_quad_t;
...
extern FILE *popen (__const char *__command, __const char *__modes) ;
extern int pclose (FILE *__stream);
extern char *ctermid (char *__s) __attribute__ ((__nothrow__ , __leaf__));
extern void flockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__));
extern int ftrylockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__)) ;
extern void funlockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__));
int main(){
  printf("Hello world!\n");
  return 0;
}

このファイルは空行とコメント行を取り除いても 344 行もあった。


コンパイル

プリプロセスが終わったら、晴れて次の作業でアセンブラに変換される。
アセンブラソースに変換された時点で処理を止めるには、-E オプションをつける。すると、拡張子が s のファイルが出来上がる。
この処理はコンパイラが行ういくつかの内、「コンパイル」と呼ばれているので「狭義のコンパイル」と呼ばれる。

$ gcc -S main.c

ちなみに、上のプリプロセスの段で表示されたプリプロセス後のソースを適当なファイルに保存し、それを gcc -S しても結果は変わらない。

$ gcc -E main.c > preprocess.c
$ gcc -S preprocess.c # 上記コマンドと同一結果(ファイル名は異なる)


さて、アセンブラに変換されたソースを見てみよう。
"Hello world!" という文字列にラベルが付けられ、puts 関数の呼び出し時に使用されていることがわかる。

$ cat main.s
	.file	"main.c"
	.section	.rodata
.LC0:
	.string	"Hello world!"
	.text
	.globl	main
	.type	main, @function
main:
.LFB0:
	.cfi_startproc
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	movl	$.LC0, %edi
	call	puts
	movl	$0, %eax
	popq	%rbp
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc
.LFE0:
	.size	main, .-main
	.ident	"GCC: (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3"
	.section	.note.GNU-stack,"",@progbits


ここで、gcc のオプションをあれこれつけてアセンブラソースを見てみると結果が変わるのが見て取れる。

例えばデバッグオプション -g. 上記のアセンブラは 26 行だったのが、デバッグオプションをつけると 226 行に膨れ上がる。

$ gcc -g -S main.c
$ cat main.s
	.file	"main.c"
	.text
.Ltext0:
	.section	.rodata
.LC0:
	.string	"Hello world!"
	.text
	.globl	main
	.type	main, @function
main:
.LFB0:
	.file 1 "main.c"

...

.LASF10:
	.string	"/home/user/work"
.LASF7:
	.string	"char"
	.ident	"GCC: (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3"
	.section	.note.GNU-stack,"",@progbits

アセンブル

狭義のコンパイルで得た main.s をアセンブルし、オブジェクトファイル(バイナリ)の生成を行う。
アセンブラソースファイルからオブジェクトファイルの生成は、as コマンドで行う。

下記コマンドでは -o オプションで出力ファイル名を main.o と指定している。指定しなかった場合、a.out という名前になる。この a.out は Assembler Output の略。

$ as -o main.o main.s


as コマンドを通したら、実行はできないもののもうバイナリです。

$ file main.o
main.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
$ hexdump -C main.o
00000000  7f 45 4c 46 02 01 01 00  00 00 00 00 00 00 00 00  |.ELF............|
00000010  01 00 3e 00 01 00 00 00  00 00 00 00 00 00 00 00  |..>.............|
00000020  00 00 00 00 00 00 00 00  30 01 00 00 00 00 00 00  |........0.......|
00000030  00 00 00 00 40 00 00 00  00 00 40 00 0d 00 0a 00  |....@.....@.....|
00000040  55 48 89 e5 bf 00 00 00  00 e8 00 00 00 00 b8 00  |UH..............|
00000050  00 00 00 5d c3 00 00 00  48 65 6c 6c 6f 20 77 6f  |...]....Hello wo|
00000060  72 6c 64 21 00 00 47 43  43 3a 20 28 55 62 75 6e  |rld!..GCC: (Ubun|
00000070  74 75 2f 4c 69 6e 61 72  6f 20 34 2e 36 2e 33 2d  |tu/Linaro 4.6.3-|
00000080  31 75 62 75 6e 74 75 35  29 20 34 2e 36 2e 33 00  |1ubuntu5) 4.6.3.|
00000090  14 00 00 00 00 00 00 00  01 7a 52 00 01 78 10 01  |.........zR..x..|
000000a0  1b 0c 07 08 90 01 00 00  1c 00 00 00 1c 00 00 00  |................|
000000b0  00 00 00 00 15 00 00 00  00 41 0e 10 86 02 43 0d  |.........A....C.|
000000c0  06 50 0c 07 08 00 00 00  00 2e 73 79 6d 74 61 62  |.P........symtab|
000000d0  00 2e 73 74 72 74 61 62  00 2e 73 68 73 74 72 74  |..strtab..shstrt|
000000e0  61 62 00 2e 72 65 6c 61  2e 74 65 78 74 00 2e 64  |ab..rela.text..d|
000000f0  61 74 61 00 2e 62 73 73  00 2e 72 6f 64 61 74 61  |ata..bss..rodata|
00000100  00 2e 63 6f 6d 6d 65 6e  74 00 2e 6e 6f 74 65 2e  |..comment..note.|
00000110  47 4e 55 2d 73 74 61 63  6b 00 2e 72 65 6c 61 2e  |GNU-stack..rela.|
00000120  65 68 5f 66 72 61 6d 65  00 00 00 00 00 00 00 00  |eh_frame........|
00000130  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
...


バイナリから情報を得るには objdump, readelf が大活躍。

$ objdump --header main.o

main.o:     file format elf64-x86-64

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .text         00000015  0000000000000000  0000000000000000  00000040  2**2
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
  1 .data         00000000  0000000000000000  0000000000000000  00000058  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  2 .bss          00000000  0000000000000000  0000000000000000  00000058  2**2
                  ALLOC
  3 .rodata       0000000d  0000000000000000  0000000000000000  00000058  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  4 .comment      0000002b  0000000000000000  0000000000000000  00000065  2**0
                  CONTENTS, READONLY
  5 .note.GNU-stack 00000000  0000000000000000  0000000000000000  00000090  2**0
                  CONTENTS, READONLY
  6 .eh_frame     00000038  0000000000000000  0000000000000000  00000090  2**3
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
$ readelf --syms main.o

Symbol table '.symtab' contains 11 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS main.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 
     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    3 
     4: 0000000000000000     0 SECTION LOCAL  DEFAULT    4 
     5: 0000000000000000     0 SECTION LOCAL  DEFAULT    5 
     6: 0000000000000000     0 SECTION LOCAL  DEFAULT    7 
     7: 0000000000000000     0 SECTION LOCAL  DEFAULT    8 
     8: 0000000000000000     0 SECTION LOCAL  DEFAULT    6 
     9: 0000000000000000    21 FUNC    GLOBAL DEFAULT    1 main
    10: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND puts


objdump コマンドで逆アセンブルなんぞも可能。

$ objdump -d main.o

main.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <main>:
   0:	55                   	push   %rbp
   1:	48 89 e5             	mov    %rsp,%rbp
   4:	bf 00 00 00 00       	mov    $0x0,%edi
   9:	e8 00 00 00 00       	callq  e <main+0xe>
   e:	b8 00 00 00 00       	mov    $0x0,%eax
  13:	5d                   	pop    %rbp
  14:	c3                   	retq  

リンク

ではいよいよこのオブジェクトファイルを実行可能にしてやりましょう。
リンクとは、必要なライブラリとの結合を行い、実行可能な形式のファイルを生成する処理のこと。

$ gcc -o main main.o

リンクを行うと、晴れて実行可能なバイナリのできあがり。改めて過程を眺めたあと動くのを見ると喜びもひとしおというもの。

$ ./main
Hello world!

ldd で依存ライブラリを見る。

$ ldd main
	linux-vdso.so.1 =>  (0x00007fff67ea4000)
	libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fd7722a7000)
	/lib64/ld-linux-x86-64.so.2 (0x00007fd772688000)

ld-linux-x86-64.so.2 はリンクローダーと呼ばれるもので、実行時に依存ライブラリをメモリ上に持ってきて結合してくれるもの。
libc.so は定番中の定番の C ライブラリ。
linux-vdso.so は Linux 上で動くバイナリに付与される仮想的なライブラリ。実体は存在しない。高速化のために存在しているらしい。


まとめ

ソースコードが実行可能になるまでの過程を一度見ておくと色々捗る

C++11/14 コア言語

C++11/14 コア言語