g++によるhello world再訪


このエントリーをはてなブックマークに追加

g++でhello.cppから実行ファイルを生成するときに何が起こっているかを調べてまとめました。全体的に gcc Compilation Process and Steps of C Program in Linux を参考にしています。元記事はCファイルを対象にしていますが、本記事ではC++ファイルを対象としています。

ソースコード

以下のコード hello.cpp をステップバイステップでビルドしていきます。このコードはC++11相当のコードです。

#include <iostream>
#include <vector>
#define MESSAGE "hello  "

int main(void)
{
    std::vector<int> vec{1,2,3,4,5};
    for(auto e: vec) {
        std::cout << MESSAGE << __FILE__ << " " << e << std::endl;
    }
    
    return 0;
}

1. プリプロセッシング

プリプロセッシングの結果を見る

まず最初に、プリプロセッシングと呼ばれる前処理が行われます。具体的には、

  • #includeでインクルードしたヘッダファイルの展開
  • #defineで定義した文字列の置換処理
  • __FILE____LINE__などの記号定数の置換処理

が行われます。(参考:マクロ展開の例

$ cpp -x c++ -std=c++11 hello.cpp > hello.i

または

$ g++ -E -std=c++11 hello.cpp > hello.i

で、プロプロセッシング後のファイルをhello.iという名前で保存できます。

hello.iの抜粋を以下に示します。

# 1 "hello.cpp"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 1 "<command-line>" 2
# 1 "hello.cpp"
# 1 "/usr/include/c++/5/iostream" 1 3
# 36 "/usr/include/c++/5/iostream" 3
...(中略)...
# 5 "hello.cpp"
int main(void)
{
    std::vector<int> vec{1,2,3,4,5};
    for(auto e: vec) {
        std::cout << "hello  " << "hello.cpp" << " " << e << std::endl;
    }

    return 0;
}

ヘッダファイルが検索されるディレクトリの場所を調べる

ヘッダファイルが捜索されるディレクトリの場所は、Cの場合は

$ gcc -xc -E -v -

C++の場合は

$ gcc -xc++ -E -v -

で調べることができます(参考:c++ - What are the GCC default include directories? - Stack Overflow)。私の場合は以下のように表示されました。

()
#include "..." search starts here:
#include <...> search starts here:
 /usr/include/c++/5
 /usr/include/x86_64-linux-gnu/c++/5
 /usr/include/c++/5/backward
 /usr/lib/gcc/x86_64-linux-gnu/5/include
 /usr/local/include
 /usr/lib/gcc/x86_64-linux-gnu/5/include-fixed
 /usr/include/x86_64-linux-gnu
 /usr/include
End of search list.

includeされるヘッダファイルのフルパスを調べる

ヘッダファイルは芋づる式にincludeされていくわけですが、具体的にどのパスにあるどのヘッダファイルがincludeされたかは、

$ g++ -std=c++11 -H hello.cpp

で調べることができます(参考:c++ - How to tell where a header file is included from? - Stack Overflow)。私の環境だと以下のようにツリーが表示されました。

$ g++ -H hello.cpp       
. /usr/include/c++/5/iostream
.. /usr/include/x86_64-linux-gnu/c++/5/bits/c++config.h
... /usr/include/x86_64-linux-gnu/c++/5/bits/os_defines.h
.... /usr/include/features.h
..... /usr/include/x86_64-linux-gnu/sys/cdefs.h
...... /usr/include/x86_64-linux-gnu/bits/wordsize.h
..... /usr/include/x86_64-linux-gnu/gnu/stubs.h
...... /usr/include/x86_64-linux-gnu/gnu/stubs-64.h
...(略)...

2. アセンブリコードへの変換

次に、コンパイラは、hello.iアセンブリコードに変換します。

$ g++ -std=c++11 -S hello.i

とすると、アセンブリコードhello.sが生成されます。

        .file   "hello.cpp"
        .section        .rodata
        .type   _ZStL19piecewise_construct, @object
        .size   _ZStL19piecewise_construct, 1
_ZStL19piecewise_construct:
        .zero   1
        .local  _ZStL8__ioinit
        .comm   _ZStL8__ioinit,1,1
...(略)...

3. オブジェクトファイルへの変換

次に、asコマンドを使って、アセンブリコードhello.sからマシン語に変換し、オブジェクトファイルhello.oを得ます。

$ as hello.s -o hello.o

hello.oの中身は

$ objdump -d hello.o

で調べることができます。以下は実行例です。

0000000000000000 <main>:
   0:   55                      push   %rbp
   1:   48 89 e5                mov    %rsp,%rbp
   4:   41 55                   push   %r13
   6:   41 54                   push   %r12
   8:   53                      push   %rbx
   9:   48 83 ec 58             sub    $0x58,%rsp
   d:   64 48 8b 04 25 28 00    mov    %fs:0x28,%rax
...(略)...

4. リンク

実行ファイルを生成する

最後に、さっき生成したオブジェクトファイルhello.oおよび、他に必要なオブジェクトファイルを集めてリンクし、実行ファイルを生成します。

$ g++ hello.o -o hello

とすると、実行ファイルhelloが生成されます。

ライブラリが検索されるディレクトリの場所、リンクされるライブラリのフルパスを調べる

$ g++ -v hello.o -o hello

とすると、以下のように詳細情報が表示されます。

LIBRARY_PATH=/usr/lib/gcc/x86_64-linux-gnu/5/:/usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/5/../../../../lib/:/lib/x86_64-linux-gnu/:/lib/../lib/:/usr/lib/x86_64-linux-gnu/:/usr/lib/../lib/:/usr/lib/gcc/x86_64-linux-gnu/5/../../../:/lib/:/usr/lib/

は、ライブラリが検索されるディレクトリの場所を表していると思います。

また、

COLLECT_GCC_OPTIONS='-v' '-o' 'hello' '-shared-libgcc' '-mtune=generic' '-march=x86-64'
 /usr/lib/gcc/x86_64-linux-gnu/5/collect2 -plugin /usr/lib/gcc/x86_64-linux-gnu/5/liblto_plugin.so -plugin-opt=/usr/lib/gcc/x86_64-linux-gnu/5/lto-wrapper -plugin-opt=-fresolution=/tmp/ccvdOxv1.res -plugin-opt=-pass-through=-lgcc_s -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lc -plugin-opt=-pass-through=-lgcc_s -plugin-opt=-pass-through=-lgcc --sysroot=/ --build-id --eh-frame-hdr -m elf_x86_64 --hash-style=gnu --as-needed -dynamic-linker /lib64/ld-linux-x86-64.so.2 -z relro -o hello /usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu/crt1.o /usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_64-linux-gnu/5/crtbegin.o -L/usr/lib/gcc/x86_64-linux-gnu/5 -L/usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu -L/usr/lib/gcc/x86_64-linux-gnu/5/../../../../lib -L/lib/x86_64-linux-gnu -L/lib/../lib -L/usr/lib/x86_64-linux-gnu -L/usr/lib/../lib -L/usr/lib/gcc/x86_64-linux-gnu/5/../../.. hello.o -lstdc++ -lm -lgcc_s -lgcc -lc -lgcc_s -lgcc /usr/lib/gcc/x86_64-linux-gnu/5/crtend.o /usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu/crtn.o

は、リンク時に使われたコマンドの詳細を表していると思います。collect2とはgcc/g++が内部で呼び出すリンクのためのコマンドのようです。ldとの違いはよくわかりません…。

その他の情報

$ g++ -save-temps -std=c++11 hello.cpp

とすると、上記でステップバイステップで生成した中間ファイル hello.ii, hello.s, hello.o が一気に生成されます。