記事の内容
以下のソースコードがコンパイル~実行されるまでに、何が行われるのかを理解する。 細かいオプションや処理の詳細は追わない。
#include <stdio.h>
#define MESSAGE "hello world\n"
int main(int argc, char *argv[]) {
printf(MESSAGE);
return 0;
}
検証環境
]$ uname -rm
3.10.0-957.10.1.el7.x86_64 x86_64
]$ gcc --version
gcc (GCC) 4.8.5 20150623 (Red Hat 4.8.5-36)
Copyright (C) 2015 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
処理の流れ
以下の流れで処理される。
- プリプロセス
- コンパイル
- アセンブル
- リンク
- ロード
プリプロセス
#
から始まる文は、プリプロセッサが処理する。 代表的なものに、#include
の展開と、#define
のマクロ処理がある。
]$ gcc -E main.c -o main.i -v
組み込み spec を使用しています。
COLLECT_GCC=gcc
ターゲット: x86_64-redhat-linux
configure 設定: ../configure --prefix=/usr --mandir=/usr/share/man --infodir=/usr/share/info --with-bugurl=http://bugzilla.redhat.com/bugzilla --enable-bootstrap --enable-shared --enable-threads=posix --enable-checking=release --with-system-zlib --enable-__cxa_atexit --disable-libunwind-exceptions --enable-gnu-unique-object --enable-linker-build-id --with-linker-hash-style=gnu --enable-languages=c,c++,objc,obj-c++,java,fortran,ada,go,lto --enable-plugin --enable-initfini-array --disable-libgcj --with-isl=/builddir/build/BUILD/gcc-4.8.5-20150702/obj-x86_64-redhat-linux/isl-install --with-cloog=/builddir/build/BUILD/gcc-4.8.5-20150702/obj-x86_64-redhat-linux/cloog-install --enable-gnu-indirect-function --with-tune=generic --with-arch_32=x86-64 --build=x86_64-redhat-linux
スレッドモデル: posix
gcc バージョン 4.8.5 20150623 (Red Hat 4.8.5-36) (GCC)
<< プリプロセッサ(cc1)の実行 >>
COLLECT_GCC_OPTIONS='-E' '-o' 'main.i' '-v' '-mtune=generic' '-march=x86-64'
/usr/libexec/gcc/x86_64-redhat-linux/4.8.5/cc1 -E -quiet -v main.c -o main.i -mtune=generic -march=x86-64
存在しないディレクトリ "/usr/lib/gcc/x86_64-redhat-linux/4.8.5/include-fixed" を無視します
存在しないディレクトリ "/usr/lib/gcc/x86_64-redhat-linux/4.8.5/../../../../x86_64-redhat-linux/include" を無視します
#include "..." の探索はここから始まります:
#include <...> の探索はここから始まります:
/usr/lib/gcc/x86_64-redhat-linux/4.8.5/include
/usr/local/include
/usr/include
探索リストの終わりです。
COMPILER_PATH=/usr/libexec/gcc/x86_64-redhat-linux/4.8.5/:/usr/libexec/gcc/x86_64-redhat-linux/4.8.5/:/usr/libexec/gcc/x86_64-redhat-linux/:/usr/lib/gcc/x86_64-redhat-linux/4.8.5/:/usr/lib/gcc/x86_64-redhat-linux/
LIBRARY_PATH=/usr/lib/gcc/x86_64-redhat-linux/4.8.5/:/usr/lib/gcc/x86_64-redhat-linux/4.8.5/../../../../lib64/:/lib/../lib64/:/usr/lib/../lib64/:/usr/lib/gcc/x86_64-redhat-linux/4.8.5/../../../:/lib/:/usr/lib/
COLLECT_GCC_OPTIONS='-E' '-o' 'main.i' '-v' '-mtune=generic' '-march=x86-64'
$ cat main.i
...
<< include <stdio.h> がファイル内に展開される >>
...
extern void funlockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__));
# 943 "/usr/include/stdio.h" 3 4
# 2 "main.c" 2
int main(int argc, char *argv[]) {
<< マクロが展開される >>
printf("hello world\n");
return 0;
}
includeヘッダの検索パス
実行ログから、以下のパスが検索対象になっていることがわかる。
#include "..." の探索はここから始まります:
#include <...> の探索はここから始まります:
/usr/lib/gcc/x86_64-redhat-linux/4.8.5/include
/usr/local/include
/usr/include
探索リストの終わりです。
検索パスが man コマンドに書いてることと微妙に違った。が、gcc の man は最新情報を反映したものではないので、正しい仕様を把握したければ公式マニュアルを見る必要があるらしい。
参考 gcc onlinedocs
このマニュアルに書かれた情報は GNU C コンパイラの完全な ドキュメンテーションからの抜粋であり、オプションの意味の記述にとどめます。 このマニュアルはボランティアのメンテナンスが行なわれた時にのみ更新され るもので、常に最新の情報を示しているわけではありません。
検索パスを追加したければ、gccの -Idirname
オプションを利用する。
参考 3.14 Options for Directory Search
デバッグコードを埋め込むためのマクロ定義
以下のように条件付きマクロを定義することで、ソースコードレベルでログ処理の切り替えをするテクニックが多用される。
#include <stdio.h>
int main(void) {
#ifdef DEBUG
printf("debug message\n");
#endif
return 0;
}
こうすることで、コンパイル時のマクロ定義の有無によって、ソースコードレベルでログの埋め込みを切り替えられる。
]$ gcc -E main.c
...
int main(void) {
return 0;
}
]$ gcc -E main.c -D DEBUG
...
int main(void) {
printf("debug message\n");
return 0;
}
定義済みマクロ
以下の3種類が存在する。
参考 3.7 Predefined Macros
- c言語標準の定義済みマクロ
- GNU C extensions の定義済みマクロ
- システム/マシン 固有の定義済みマクロ
例えば以下のように記述すると、時刻(プリプロセスした時刻)、ファイル名、行数が取得できるようになる。
#include <stdio.h>
int main(void) {
printf("%s %s %d \n", __TIME__, __FILE__, __LINE__);
// プリプロセス時に
// printf("%s %s %d \n", "06:00:22", "main.c", 4);
// と置き換えられる
return 0;
}
また、-dE
オプションでプリプロセス終了時に有効だったマクロの定義を表示できる。
]$ gcc -dM -E main.c
#define _IO_CURRENTLY_PUTTING 0x800
#define __DBL_MIN_EXP__ (-1021)
#define _IO_peekc_unlocked(_fp) (_IO_BE ((_fp)->_IO_read_ptr >= (_fp)->_IO_read_end, 0) && __underflow (_fp) == EOF ? EOF : *(unsigned char *) (_fp)->_IO_read_ptr)
#define __UINT_LEAST16_MAX__ 65535
#define _STDBOOL_H
...
2重インクルード防止
ヘッダーファイルは、以下のように#ifndef
で囲むことが多い。
#ifndef MY_H
#define MY_H
...
#endif
これをしておかないと、cファイル単位で2重に同じヘッダーファイルが読み込まれたときに(ヘッダーファイルのネストを含む)構文エラーになる。
例えば以下のように。
void func(){}
#include "my.h"
#include "my.h"
int main(int argc, char *argv[]) {
return 0;
}
]$ gcc main.c
In file included from main.c:2:0:
my.h:1:6: エラー: ‘func’ が再定義されました
void func(){}
^
In file included from main.c:1:0:
my.h:1:6: 備考: 前の ‘func’ の宣言はここです
void func(){}
^
複数のcファイルからinclude
されたら、各cファイルにヘッダーがインクルードされる。そのため、ヘッダーファイルでは以下のように グローバル変数/関数 の extern 宣言だけを記述し、各cファイルでインクルードすることでプロトタイプ宣言を共通化するのが基本的な使い方。
#ifndef MY_H
#define MY_H
extern int x;
extern void func(void);
#endif
そのほかのヘッダーファイルの記載方法については、下記を参照する。
参考 C言語の正しいヘッダファイルの書き方
コンパイル
アセンブリに変換する。この段階で最適化オプションに応じた最適化が行われる。
]$ gcc -S main.i -o main.s -v
組み込み spec を使用しています。
COLLECT_GCC=gcc
ターゲット: x86_64-redhat-linux
configure 設定: ../configure --prefix=/usr --mandir=/usr/share/man --infodir=/usr/share/info --with-bugurl=http://bugzilla.redhat.com/bugzilla --enable-bootstrap --enable-shared --enable-threads=posix --enable-checking=release --with-system-zlib --enable-__cxa_atexit --disable-libunwind-exceptions --enable-gnu-unique-object --enable-linker-build-id --with-linker-hash-style=gnu --enable-languages=c,c++,objc,obj-c++,java,fortran,ada,go,lto --enable-plugin --enable-initfini-array --disable-libgcj --with-isl=/builddir/build/BUILD/gcc-4.8.5-20150702/obj-x86_64-redhat-linux/isl-install --with-cloog=/builddir/build/BUILD/gcc-4.8.5-20150702/obj-x86_64-redhat-linux/cloog-install --enable-gnu-indirect-function --with-tune=generic --with-arch_32=x86-64 --build=x86_64-redhat-linux
スレッドモデル: posix
gcc バージョン 4.8.5 20150623 (Red Hat 4.8.5-36) (GCC)
<< コンパイラ(cc1)の実行 >>
COLLECT_GCC_OPTIONS='-S' '-o' 'main.s' '-v' '-mtune=generic' '-march=x86-64'
/usr/libexec/gcc/x86_64-redhat-linux/4.8.5/cc1 -fpreprocessed main.i -quiet -dumpbase main.i -mtune=generic -march=x86-64 -auxbase-strip main.s -version -o main.s
GNU C (GCC) version 4.8.5 20150623 (Red Hat 4.8.5-36) (x86_64-redhat-linux)
compiled by GNU C version 4.8.5 20150623 (Red Hat 4.8.5-36), GMP version 6.0.0, MPFR version 3.1.1, MPC version 1.0.1
GGC heuristics: --param ggc-min-expand=100 --param ggc-min-heapsize=131072
GNU C (GCC) version 4.8.5 20150623 (Red Hat 4.8.5-36) (x86_64-redhat-linux)
compiled by GNU C version 4.8.5 20150623 (Red Hat 4.8.5-36), GMP version 6.0.0, MPFR version 3.1.1, MPC version 1.0.1
GGC heuristics: --param ggc-min-expand=100 --param ggc-min-heapsize=131072
Compiler executable checksum: 1d991baef7d22a1cfb4879366b74b684
COMPILER_PATH=/usr/libexec/gcc/x86_64-redhat-linux/4.8.5/:/usr/libexec/gcc/x86_64-redhat-linux/4.8.5/:/usr/libexec/gcc/x86_64-redhat-linux/:/usr/lib/gcc/x86_64-redhat-linux/4.8.5/:/usr/lib/gcc/x86_64-redhat-linux/
LIBRARY_PATH=/usr/lib/gcc/x86_64-redhat-linux/4.8.5/:/usr/lib/gcc/x86_64-redhat-linux/4.8.5/../../../../lib64/:/lib/../lib64/:/usr/lib/../lib64/:/usr/lib/gcc/x86_64-redhat-linux/4.8.5/../../../:/lib/:/usr/lib/
COLLECT_GCC_OPTIONS='-S' '-o' 'main.s' '-v' '-mtune=generic' '-march=x86-64'
最適化の結果として、printf
が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
subq $16, %rsp
movl %edi, -4(%rbp)
movq %rsi, -16(%rbp)
movl $.LC0, %edi
<< 最適化(printf -> puts) >>
call puts
movl $0, %eax
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-36)"
.section .note.GNU-stack,"",@progbits
最適化
-Olevel
(オー)オプションで指定する。-O2
がよく利用される。
参考 GCCの最適化オプション
参考 最適化オプションのまとめ
実際に試してみると、 -O0
と-O2
では生成されるコードにかなりの違いがある。最適化の具体的な内容は 3.10 Options That Control Optimization を参考にする。
]$ gcc -O0 -S -o main.O0.s main.c
]$ gcc -O2 -S -o main.O2.s main.c
]$ diff -u main.O0.s main.O2.s
--- main.O0.s 2019-04-28 01:22:21.660557147 +0900
+++ main.O2.s 2019-04-28 01:22:38.955690180 +0900
@@ -1,29 +1,24 @@
.file "main.c"
- .section .rodata
+ .section .rodata.str1.1,"aMS",@progbits,1
.LC0:
.string "hello world"
- .text
+ .section .text.startup,"ax",@progbits
+ .p2align 4,,15
.globl main
.type main, @function
main:
-.LFB0:
+.LFB11:
.cfi_startproc
- pushq %rbp
+ subq $8, %rsp
.cfi_def_cfa_offset 16
- .cfi_offset 6, -16
- movq %rsp, %rbp
- .cfi_def_cfa_register 6
- subq $16, %rsp
- movl %edi, -4(%rbp)
- movq %rsi, -16(%rbp)
movl $.LC0, %edi
call puts
- movl $0, %eax
- leave
- .cfi_def_cfa 7, 8
+ xorl %eax, %eax
+ addq $8, %rsp
+ .cfi_def_cfa_offset 8
ret
.cfi_endproc
-.LFE0:
+.LFE11:
.size main, .-main
.ident "GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-36)"
.section .note.GNU-stack,"",@progbits
デバッグシンボル
-glevel
オプションでデバッグ情報(GDB 等のデバッガが利用する)を付与できる。
gcc は-O
オプションと併用してデバッグ情報を付与できる。(とはいえ、-O0
を利用したほうがはまらないと思う)
レベル/含まれるデバッグ情報 の関係は以下の通り。
レベル | 含まれる情報 |
---|---|
0 | デバッグ情報を付与しない |
1 | バックトレースのための最低限の情報(関数と外部変数) |
2 | level 1 + ローカル変数や行番号。デフォルト。 |
3 | level2 + マクロ定義。当然だが、プリプロセス前のファイルを gcc の引数に渡す必要がある |
実際に試してみると、以下のようにデバッグ情報が付与されることがわかる。
]$ gcc -S main.i -o main.s -g
]$ cat main.s
...
.LASF3:
.string "unsigned int"
.LASF11:
.string "GNU C 4.8.5 20150623 (Red Hat 4.8.5-36) -mtune=generic -march=x86-64 -g"
.LASF12:
.string "main.c"
.LASF0:
.string "long unsigned int"
.LASF8:
.string "char"
.LASF13:
.string "/home/kimura/work/clang/z"
.LASF1:
.string "unsigned char"
.LASF14:
.string "main"
...
アセンブル
cファイルごとにオブジェクトファイル(=ELF形式)を作る。 この段階では、外部のファイルの変数/関数の呼び出しは未定義になる。
]$ gcc -c main.s -o main.o -v
組み込み spec を使用しています。
COLLECT_GCC=gcc
ターゲット: x86_64-redhat-linux
configure 設定: ../configure --prefix=/usr --mandir=/usr/share/man --infodir=/usr/share/info --with-bugurl=http://bugzilla.redhat.com/bugzilla --enable-bootstrap --enable-shared --enable-threads=posix --enable-checking=release --with-system-zlib --enable-__cxa_atexit --disable-libunwind-exceptions --enable-gnu-unique-object --enable-linker-build-id --with-linker-hash-style=gnu --enable-languages=c,c++,objc,obj-c++,java,fortran,ada,go,lto --enable-plugin --enable-initfini-array --disable-libgcj --with-isl=/builddir/build/BUILD/gcc-4.8.5-20150702/obj-x86_64-redhat-linux/isl-install --with-cloog=/builddir/build/BUILD/gcc-4.8.5-20150702/obj-x86_64-redhat-linux/cloog-install --enable-gnu-indirect-function --with-tune=generic --with-arch_32=x86-64 --build=x86_64-redhat-linux
スレッドモデル: posix
gcc バージョン 4.8.5 20150623 (Red Hat 4.8.5-36) (GCC)
COLLECT_GCC_OPTIONS='-c' '-o' 'main.o' '-v' '-mtune=generic' '-march=x86-64'
as -v --64 -o main.o main.s
GNU アセンブラ バージョン 2.27 (x86_64-redhat-linux)、BFD バージョン version 2.27-34.base.el7 を使用
<< アセンブラの実行 >>
COMPILER_PATH=/usr/libexec/gcc/x86_64-redhat-linux/4.8.5/:/usr/libexec/gcc/x86_64-redhat-linux/4.8.5/:/usr/libexec/gcc/x86_64-redhat-linux/:/usr/lib/gcc/x86_64-redhat-linux/4.8.5/:/usr/lib/gcc/x86_64-redhat-linux/
LIBRARY_PATH=/usr/lib/gcc/x86_64-redhat-linux/4.8.5/:/usr/lib/gcc/x86_64-redhat-linux/4.8.5/../../../../lib64/:/lib/../lib64/:/usr/lib/../lib64/:/usr/lib/gcc/x86_64-redhat-linux/4.8.5/../../../:/lib/:/usr/lib/
COLLECT_GCC_OPTIONS='-c' '-o' 'main.o' '-v' '-mtune=generic' '-march=x86-64'
]$ file main.o
main.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
ELF形式
オブジェクトファイル/実行ファイル の形式のひとつ。
参考 Wikipedia Executable and Linkable Format
実行可能ファイルにはCPUが理解できる機械語だけが入っている、という雑な理解をしていたけど、どうやらそうではないらしい。
どんなCPUを想定してビルドされたか、プログラムをどのメモリ位置にロードするか、メモリの書き込みは必要か、(後で出てくる)動的ロードで何をリンクするか、再配置はどうするか、など、実行に必要な様々な情報が実行可能ファイルに含まれる。
そのため、役割単位にセクションを作り、メモリに割り付ける単位にセグメントを作る。また、セクションを動的に増やせるようにセクションヘッダーを用意する。具体的には、以下のような構造になっている。
引用 ELFの動的リンク
ELFへッダー
ファイルの基本的な情報が埋め込まれている。 型
はオブジェクトファイルの場合にREL (再配置可能ファイル)
になり、実行可能ファイルの場合にEXEC (実行可能ファイル)
になる。 エントリポイントアドレス
は、プログラムの実行を開始するアドレスのため、実行可能ファイルで有効な値になる。
]$ readelf -h main.o
ELF ヘッダ:
マジック: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
クラス: ELF64
データ: 2 の補数、リトルエンディアン
バージョン: 1 (current)
OS/ABI: UNIX - System V
ABI バージョン: 0
型: REL (再配置可能ファイル)
マシン: Advanced Micro Devices X86-64
バージョン: 0x1
エントリポイントアドレス: 0x0
プログラムの開始ヘッダ: 0 (バイト)
セクションヘッダ始点: 680 (バイト)
フラグ: 0x0
このヘッダのサイズ: 64 (バイト)
プログラムヘッダサイズ: 0 (バイト)
プログラムヘッダ数: 0
セクションヘッダ: 64 (バイト)
セクションヘッダサイズ: 13
セクションヘッダ文字列表索引: 12
セクションヘッダー
セクションの構造を定義する。
]$ readelf --section-headers main.o
13 個のセクションヘッダ、始点オフセット 0x2a8:
セクションヘッダ:
[番] 名前 タイプ アドレス オフセット
サイズ EntSize フラグ Link 情報 整列
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS 0000000000000000 00000040
0000000000000020 0000000000000000 AX 0 0 1
[ 2] .rela.text RELA 0000000000000000 000001f8
0000000000000030 0000000000000018 I 10 1 8
[ 3] .data PROGBITS 0000000000000000 00000060
0000000000000000 0000000000000000 WA 0 0 1
[ 4] .bss NOBITS 0000000000000000 00000060
0000000000000000 0000000000000000 WA 0 0 1
[ 5] .rodata PROGBITS 0000000000000000 00000060
000000000000000c 0000000000000000 A 0 0 1
[ 6] .comment PROGBITS 0000000000000000 0000006c
000000000000002e 0000000000000001 MS 0 0 1
[ 7] .note.GNU-stack PROGBITS 0000000000000000 0000009a
0000000000000000 0000000000000000 0 0 1
[ 8] .eh_frame PROGBITS 0000000000000000 000000a0
0000000000000038 0000000000000000 A 0 0 8
[ 9] .rela.eh_frame RELA 0000000000000000 00000228
0000000000000018 0000000000000018 I 10 8 8
[10] .symtab SYMTAB 0000000000000000 000000d8
0000000000000108 0000000000000018 11 9 8
[11] .strtab STRTAB 0000000000000000 000001e0
0000000000000012 0000000000000000 0 0 1
[12] .shstrtab STRTAB 0000000000000000 00000240
0000000000000061 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
l (large), p (processor specific)
セクションごとの説明は下記を参考にする。
引用 ELFの動的リンク
各セクションは個別のコマンドで確認できる。
オブジェクトファイルはファイル単位でコンパイルされた結果なので、外部の関数呼び出しはまだ解決されていない(索引名がUND
になる)
]$ readelf --syms main.o
シンボルテーブル '.symtab' は 11 個のエントリから構成されています:
番号: 値 サイズ タイプ Bind Vis 索引名
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
プログラムヘッダー
セクションをまとめてセグメントとして定義する。
アドレス位置と読み込み属性を記述し、実行時にローダが使う。基本的にリンカが設定する値なので、オブジェクトファイルには存在しない。
]$ readelf --program-headers main.o
このファイルにはプログラムヘッダはありません。
リンク
オブジェクト/ライブラリ をリンクし、実行可能ファイルを作る。 この段階で、未定義のシンボルの参照先が解消される。
リンクには2種類ある。
静的リンク
- コンパイル時にリンクする
- リンクするオブジェクトがすべて実行可能ファイルに含まれるので、ビルド後のファイルサイズが大きい
- 特定ライブラリをアップデートするときに再ビルドが必要
- リンクするライブラリは
libxxx.a
動的リンク
- 実行時にリンクする
- 実行時にリンクするので、ビルド後のファイルサイズが小さい
- 特定ライブラリをアップデートするときに再ビルドが必要ない
- リンクするライブラリは
libxxx.so
自環境では、動的リンク用と静的リンク用のライブラリがそれぞれ用意されていた。
]$ ls -al /lib64/libc.*
-rw-r--r--. 1 root root 5089008 4月 10 02:07 /lib64/libc.a
-rw-r--r--. 1 root root 253 4月 10 01:38 /lib64/libc.so
lrwxrwxrwx. 1 root root 12 4月 22 00:44 /lib64/libc.so.6 -> libc-2.17.so
]$ rpm -iqf /lib64/libc.a
glibc-static-2.17-260.el7_6.4.x86_64
]$ rpm -iqf /lib64/libc.so
glibc-devel-2.17-260.el7_6.4.x86_64
リンカの検索パス
LIBRARY_PATH
環境変数や -Ldirname
オプションをもとに、検索パスを決定する。
参考 3.19 Environment Variables Affecting GCC
静的リンク
未定義シンボルを オブジェクトファイルや libxxx.a からコピーしてバイナリに埋め込む。
]$ gcc -static main.o -o main -v
組み込み spec を使用しています。
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/libexec/gcc/x86_64-redhat-linux/4.8.5/lto-wrapper
ターゲット: x86_64-redhat-linux
configure 設定: ../configure --prefix=/usr --mandir=/usr/share/man --infodir=/usr/share/info --with-bugurl=http://bugzilla.redhat.com/bugzilla --enable-bootstrap --enable-shared --enable-threads=posix --enable-checking=release --with-system-zlib --enable-__cxa_atexit --disable-libunwind-exceptions --enable-gnu-unique-object --enable-linker-build-id --with-linker-hash-style=gnu --enable-languages=c,c++,objc,obj-c++,java,fortran,ada,go,lto --enable-plugin --enable-initfini-array --disable-libgcj --with-isl=/builddir/build/BUILD/gcc-4.8.5-20150702/obj-x86_64-redhat-linux/isl-install --with-cloog=/builddir/build/BUILD/gcc-4.8.5-20150702/obj-x86_64-redhat-linux/cloog-install --enable-gnu-indirect-function --with-tune=generic --with-arch_32=x86-64 --build=x86_64-redhat-linux
スレッドモデル: posix
gcc バージョン 4.8.5 20150623 (Red Hat 4.8.5-36) (GCC)
<< リンカを実行する >>
COMPILER_PATH=/usr/libexec/gcc/x86_64-redhat-linux/4.8.5/:/usr/libexec/gcc/x86_64-redhat-linux/4.8.5/:/usr/libexec/gcc/x86_64-redhat-linux/:/usr/lib/gcc/x86_64-redhat-linux/4.8.5/:/usr/lib/gcc/x86_64-redhat-linux/
LIBRARY_PATH=/usr/lib/gcc/x86_64-redhat-linux/4.8.5/:/usr/lib/gcc/x86_64-redhat-linux/4.8.5/../../../../lib64/:/lib/../lib64/:/usr/lib/../lib64/:/usr/lib/gcc/x86_64-redhat-linux/4.8.5/../../../:/lib/:/usr/lib/
COLLECT_GCC_OPTIONS='-static' '-o' 'main' '-v' '-mtune=generic' '-march=x86-64'
/usr/libexec/gcc/x86_64-redhat-linux/4.8.5/collect2 --build-id --no-add-needed --hash-style=gnu -m elf_x86_64 -static -o main /usr/lib/gcc/x86_64-redhat-linux/4.8.5/../../../../lib64/crt1.o /usr/lib/gcc/x86_64-redhat-linux/4.8.5/../../../../lib64/crti.o /usr/lib/gcc/x86_64-redhat-linux/4.8.5/crtbeginT.o -L/usr/lib/gcc/x86_64-redhat-linux/4.8.5 -L/usr/lib/gcc/x86_64-redhat-linux/4.8.5/../../../../lib64 -L/lib/../lib64 -L/usr/lib/../lib64 -L/usr/lib/gcc/x86_64-redhat-linux/4.8.5/../../.. main.o --start-group -lgcc -lgcc_eh -lc --end-group /usr/lib/gcc/x86_64-redhat-linux/4.8.5/crtend.o /usr/lib/gcc/x86_64-redhat-linux/4.8.5/../../../../lib64/crtn.o
動的リンク
未定義シンボルを オブジェクトファイルや libxxx.so で解決する。
]$ gcc main.o -o main -v
組み込み spec を使用しています。
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/libexec/gcc/x86_64-redhat-linux/4.8.5/lto-wrapper
ターゲット: x86_64-redhat-linux
configure 設定: ../configure --prefix=/usr --mandir=/usr/share/man --infodir=/usr/share/info --with-bugurl=http://bugzilla.redhat.com/bugzilla --enable-bootstrap --enable-shared --enable-threads=posix --enable-checking=release --with-system-zlib --enable-__cxa_atexit --disable-libunwind-exceptions --enable-gnu-unique-object --enable-linker-build-id --with-linker-hash-style=gnu --enable-languages=c,c++,objc,obj-c++,java,fortran,ada,go,lto --enable-plugin --enable-initfini-array --disable-libgcj --with-isl=/builddir/build/BUILD/gcc-4.8.5-20150702/obj-x86_64-redhat-linux/isl-install --with-cloog=/builddir/build/BUILD/gcc-4.8.5-20150702/obj-x86_64-redhat-linux/cloog-install --enable-gnu-indirect-function --with-tune=generic --with-arch_32=x86-64 --build=x86_64-redhat-linux
スレッドモデル: posix
gcc バージョン 4.8.5 20150623 (Red Hat 4.8.5-36) (GCC)
<< リンカを実行する >>
COMPILER_PATH=/usr/libexec/gcc/x86_64-redhat-linux/4.8.5/:/usr/libexec/gcc/x86_64-redhat-linux/4.8.5/:/usr/libexec/gcc/x86_64-redhat-linux/:/usr/lib/gcc/x86_64-redhat-linux/4.8.5/:/usr/lib/gcc/x86_64-redhat-linux/
LIBRARY_PATH=/usr/lib/gcc/x86_64-redhat-linux/4.8.5/:/usr/lib/gcc/x86_64-redhat-linux/4.8.5/../../../../lib64/:/lib/../lib64/:/usr/lib/../lib64/:/usr/lib/gcc/x86_64-redhat-linux/4.8.5/../../../:/lib/:/usr/lib/
COLLECT_GCC_OPTIONS='-o' 'main' '-v' '-mtune=generic' '-march=x86-64'
/usr/libexec/gcc/x86_64-redhat-linux/4.8.5/collect2 --build-id --no-add-needed --eh-frame-hdr --hash-style=gnu -m elf_x86_64 -dynamic-linker /lib64/ld-linux-x86-64.so.2 -o main /usr/lib/gcc/x86_64-redhat-linux/4.8.5/../../../../lib64/crt1.o /usr/lib/gcc/x86_64-redhat-linux/4.8.5/../../../../lib64/crti.o /usr/lib/gcc/x86_64-redhat-linux/4.8.5/crtbegin.o -L/usr/lib/gcc/x86_64-redhat-linux/4.8.5 -L/usr/lib/gcc/x86_64-redhat-linux/4.8.5/../../../../lib64 -L/lib/../lib64 -L/usr/lib/../lib64 -L/usr/lib/gcc/x86_64-redhat-linux/4.8.5/../../.. main.o -lgcc --as-needed -lgcc_s --no-as-needed -lc -lgcc --as-needed -lgcc_s --no-as-needed /usr/lib/gcc/x86_64-redhat-linux/4.8.5/crtend.o /usr/lib/gcc/x86_64-redhat-linux/4.8.5/../../../../lib64/crtn.o
静的リンク/動的リンク の違い
ファイルコマンドでリンクの種別がわかる。
// 静的リンク
]$ file main-static
main-static: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, for GNU/Linux 2.6.32, BuildID[sha1]=4654b4ebc0af3ba0cdf8cdb240425b164379af30, not stripped
// 動的リンク
]$ file main-dynamic
main-dynamic: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=23f593b4db3f8a4189402c5d150a82d9b795e85a, not stripped
静的リンクは、全てがバイナリに含まれるのでファイルサイズが大きい。
]$ ls -al
-rwxrwxr-x. 1 kimura kimura 8440 4月 28 07:14 main-dynamic
-rwxrwxr-x. 1 kimura kimura 856848 4月 28 07:13 main-static
動的リンクは、他ライブラリへの参照情報が含まれる。
// 静的リンク
]$ ldd main-static
動的実行ファイルではありません
// 動的リンク
]$ ldd main-dynamic
linux-vdso.so.1 => (0x00007ffc8cf0a000)
libc.so.6 => /lib64/libc.so.6 (0x00007f8c2b148000)
/lib64/ld-linux-x86-64.so.2 (0x00007f8c2b515000)
これらの情報は、リンク時に ELF ファイルに追加される。
// 静的リンク
$ readelf --dynamic main
// 動的リンク
$ readelf --dynamic main
Dynamic section at offset 0xe28 contains 24 entries:
タグ タイプ 名前/値
0x0000000000000001 (NEEDED) 共有ライブラリ: [libc.so.6]
0x000000000000000c (INIT) 0x4003c8
0x000000000000000d (FINI) 0x4005b4
0x0000000000000019 (INIT_ARRAY) 0x600e10
...
ちなみに
lddはbashになっている。
$ file $(which ldd)
/usr/bin/ldd: Bourne-Again shell script, ASCII text executable
LD_TRACE_LOADED_OBJECTS=1
を設定すれば、同じことが再現できる。
参考 ldd man page
$ LD_TRACE_LOADED_OBJECTS=1 ./main
linux-vdso.so.1 => (0x00007fff0c45e000)
libc.so.6 => /lib64/libc.so.6 (0x00007fbef2e70000)
/lib64/ld-linux-x86-64.so.2 (0x00007fbef323d000)
ロード
Linuxカーネルが ELF ファイルを読み込み、プログラムを実行する。
strace でざっくりと実行時に呼ばれるシステムコールを見てみると、
// 静的リンク
]$ strace ./main-static
// execveの実行
execve("./main-static", ["./main-static"], [/* 31 vars */]) = 0
uname({sysname="Linux", nodename="localhost.localdomain", ...}) = 0
// よくわからない
brk(NULL) = 0x1b91000
brk(0x1b921c0) = 0x1b921c0
arch_prctl(ARCH_SET_FS, 0x1b91880) = 0
brk(0x1bb31c0) = 0x1bb31c0
brk(0x1bb4000) = 0x1bb4000
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 0), ...}) = 0
// mainメソッドが実行されてるっぽい
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f711ca8e000
write(1, "hello world\n", 12hello world
) = 12
exit_group(0) = ?
+++ exited with 0 +++
// 動的リンク
]$ strace ./main-dynamic
// execveの実行
execve("./main-dynamic", ["./main-dynamic"], [/* 31 vars */]) = 0
brk(NULL) = 0x1df1000
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fd86f3b4000
// 動的リンクの処理
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=88584, ...}) = 0
mmap(NULL, 88584, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7fd86f39e000
close(3) = 0
// ライブラリの読み込みとメモリへの配置
open("/lib64/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\340$\2\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=2151672, ...}) = 0
mmap(NULL, 3981792, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7fd86edc7000
mprotect(0x7fd86ef89000, 2097152, PROT_NONE) = 0
mmap(0x7fd86f189000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1c2000) = 0x7fd86f189000
mmap(0x7fd86f18f000, 16864, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7fd86f18f000
close(3) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fd86f39d000
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fd86f39b000
arch_prctl(ARCH_SET_FS, 0x7fd86f39b740) = 0
mprotect(0x7fd86f189000, 16384, PROT_READ) = 0
mprotect(0x600000, 4096, PROT_READ) = 0
mprotect(0x7fd86f3b5000, 4096, PROT_READ) = 0
munmap(0x7fd86f39e000, 88584) = 0
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 0), ...}) = 0
// mainメソッドの実行
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fd86f3b3000
write(1, "hello world\n", 12hello world
) = 12
exit_group(0) = ?
+++ exited with 0 +++
なんだかよくわからなかったので、ソースコード/GDB で調査した。 調べ方はプログラムの実行はどのようにして行われるのか、Linuxカーネルのコードから探る (1/2)を参考にした。
- execve システムコールの実行(kernelの
fs/exec.c#SYSCALL_DEFINE3(execve,...)
) - 引数の設定や環境変数の設定(kernelの
fs/exec.c#__do_execve_file
) - ELFファイルの読み込み(kernelの
fs/binfmt_elf.c#load_elf_binary
) - .interp セクションをチェック
- 指定されたローダを利用し、共有ライブラリをメモリに展開(
mmap
) - .interp が存在しなければ何もしない
- ELFファイルの実行(kernelの
arch/x86/kernel/process_64.c#start_thread
) - エントリポイント(glibcの
sysdeps/x86_64/start.S
) の実行 - glibの __libc_start_main(glibcの
csu/libc-start.c
) の実行 - mainメソッドの実行
ローダの指定
.interp セクションで指定されている。
// 静的リンク
$ readelf -p .interp main-static
readelf: main-static: 警告: セクション '.interp' は存在しないためダンプされませんでした!
// 動的リンク
]$ readelf -p .interp main-dynamic
セクション '.interp' の文字列ダンプ:
[ 0] /lib64/ld-linux-x86-64.so.2
ローダの検索パス
LD_LIBRARY_PATH
や ldconfig
を利用する。
参考 Linux: ライブラリの動的リンクでエラーが出た場合の対処方法
スタートアップルーチン
動的/静的どちらも glibcのsysdeps/x86_64/start.S
が実行される。
これはリンク時にgccによってリンクされている。(crt1.o)
GDBでエントリポイントで止めると以下のようになっていた。
アセンブラは読めないのでコメントだけ読むと、kernel から渡された値を レジスタ/スタック から取り出して、最終的に __libc_start_main
を呼び出すらしい。カーネルとライブラリの橋渡しをしてくれる処理っぽい。
/* Extract the arguments as encoded on the stack and set up
the arguments for __libc_start_main (int (*main) (int, char **, char **),
int argc, char *argv,
void (*init) (void), void (*fini) (void),
void (*rtld_fini) (void), void *stack_end).
The arguments are passed via registers and on the stack:
main: %rdi
argc: %rsi
argv: %rdx
init: %rcx
fini: %r8
rtld_fini: %r9
stack_end: stack. */
互換性について
各種仮想化技術を考えなければ、以下のようになると思う。
マシン語は、CPUアーキテクチャごとに異なる
- x86_64向けに作られたバイナリはarmでは実行できない
- x86_64がx86と互換がある、といった例外はある
- 参考 Wikipedia 機械語と互換性
サポートする実行可能形式はOSによってまちまち
- Windows は exe
- MAC は Mach-O
- Linux は a.out や ELF
- Unix系 は a.out や ELF
カーネルのAPIは、OSごとに異なる
- Unix系 <-> Linux のバイナリ互換性はない
- ただし、POSIXに準拠していればC言語APIの互換性はある(=ソースコードを基にビルドすれば実行できる)
- 参考 Wikipedia POSIX
LinuxカーネルのABIは、同一CPUアーキテクチャでのバイナリ互換性を保証する
- CPUアーキテクチャとLinuxカーネルバージョン(とリンク先のライブラリバージョン)が合っていれば、異なるディストリビューションでも実行できる
- ただし現実的には、リンク先のライブラリバージョン/参照可能な環境変数/ディレクトリ構造 などの差異があるため、正常に動かないことが多い。このディストリビューションごとの差異を埋めるためにLSB(Linux Standard Base)がある
- APIとABIの差は APIとかABIとかシステムコールとか を参考にする
LinuxカーネルのABIに対する後方互換性はかなり厳密に守られている
- 同一マシン内でカーネルバージョンを上げるならバイナリ互換がある
- 同一マシン内でカーネルバージョンを下げると、存在しないLinuxカーネルAPIがバイナリに含まれる場合に動かない
- 参考 第43回 「Dockerイメージ」のポータビリティとLinuxカーネルのABI (中井悦司)