C言語がコンパイル~実行されるまで

記事の内容

以下のソースコードがコンパイル~実行されるまでに、何が行われるのかを理解する。 細かいオプションや処理の詳細は追わない。

#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.

処理の流れ

以下の流れで処理される。

  1. プリプロセス
  2. コンパイル
  3. アセンブル
  4. リンク
  5. ロード

f:id:kimulla:20191201180900j:plain

プリプロセス

# から始まる文は、プリプロセッサが処理する。 代表的なものに、#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'

最適化の結果として、printfputsに置き換えられている。

]$ 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を想定してビルドされたか、プログラムをどのメモリ位置にロードするか、メモリの書き込みは必要か、(後で出てくる)動的ロードで何をリンクするか、再配置はどうするか、など、実行に必要な様々な情報が実行可能ファイルに含まれる。

そのため、役割単位にセクションを作り、メモリに割り付ける単位にセグメントを作る。また、セクションを動的に増やせるようにセクションヘッダーを用意する。具体的には、以下のような構造になっている。

f:id:kimulla:20191201181714p:plain 引用 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)

セクションごとの説明は下記を参考にする。

f:id:kimulla:20191201181657p:plain 引用 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)を参考にした。

  1. execve システムコールの実行(kernelのfs/exec.c#SYSCALL_DEFINE3(execve,...))
  2. 引数の設定や環境変数の設定(kernelのfs/exec.c#__do_execve_file)
  3. ELFファイルの読み込み(kernelのfs/binfmt_elf.c#load_elf_binary)
  4. .interp セクションをチェック
  5. 指定されたローダを利用し、共有ライブラリをメモリに展開(mmap)
  6. .interp が存在しなければ何もしない
  7. ELFファイルの実行(kernelのarch/x86/kernel/process_64.c#start_thread)
  8. エントリポイント(glibcのsysdeps/x86_64/start.S) の実行
  9. glibの __libc_start_main(glibcのcsu/libc-start.c) の実行
  10. 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_PATHldconfig を利用する。
参考 Linux: ライブラリの動的リンクでエラーが出た場合の対処方法

スタートアップルーチン

動的/静的どちらも glibcのsysdeps/x86_64/start.S が実行される。

これはリンク時にgccによってリンクされている。(crt1.o)

GDBでエントリポイントで止めると以下のようになっていた。

f:id:kimulla:20191201181733p:plain

アセンブラは読めないのでコメントだけ読むと、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に対する後方互換性はかなり厳密に守られている