33
20

More than 3 years have passed since last update.

C++ユーザーの為のリンクの話1

Last updated at Posted at 2020-12-05

C++と書きましたが C でも Fortran でもだいたい同じです。とりあえず gcc (g++) で Linux の話をします。目標としては次の二つです

  • リンク時の undefined reference エラーとは何なのかを理解する(この記事)
  • 実行時の cannot open libXXX.so エラーとは何なのかを理解する(次回

コンパイルとリンク

C++ではプログラムを実行可能な形式にするまでにいくつかの形をとります

  • ソースコード (*.cpp, *.cxx 等)
  • オブジェクト (*.o)
  • アーカイブ (*.a)
  • 共有ライブラリ (*.so)
  • 実行可能ファイル (a.out, これには普通拡張子はつけない)

ソースコードから実行可能ファイルや共有ライブラリを作る操作を一般にコンパイルと呼びますが、特に複数のオブジェクトファイルを単一のアーカイブや共有ライブラリ、実行可能ファイルにまとめ上げる作業をリンクと言います。

ソースコード (*.cpp) → オブジェクト (*.o)

次の二つの C++ ファイルがあるとします

a.cpp
#include <iostream>
void func_a() { std::cout << "This is func_a!" << std::endl; }
b.cpp
#include <iostream>
void func_b() { std::cout << "This is func_b!" << std::endl; }

これをまずオブジェクトにコンパイルします

g++ -c a.cpp # a.o が出来る
g++ -c b.cpp # b.o が出来る

a.ob.o が出来上がったので中身を見てみましょう。 nm コマンドを使います。これはオブジェクトの中で定義されているシンボルを見るためのコマンドです。

$ nm -C a.o   # -C は C++の名前修飾を人間に読みやすくしてくれる
                 U __cxa_atexit
                 U __dso_handle
                 U _GLOBAL_OFFSET_TABLE_
0000000000000078 t _GLOBAL__sub_I__Z6func_av
000000000000002f t __static_initialization_and_destruction_0(int, int)
0000000000000000 T func_a()
                 U std::ostream::operator<<(std::ostream& (*)(std::ostream&))
                 U std::ios_base::Init::Init()
                 U std::ios_base::Init::~Init()
                 U std::cout
                 U std::basic_ostream<char, std::char_traits<char> >& std::endl<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&)
0000000000000000 r std::piecewise_construct
0000000000000000 b std::__ioinit
                 U std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)
^^^^^^^^^^^^^^^^ ^ 
シンボルの番地     シンボルの種類

シンボルの種類は次のとおりです

type 説明
U The symbol is undefined.
T The symbol is in the text (code) section. (Global)
t The symbol is in the text (code) section. (Local)
b The symbol is in the BSS data section. This section typically contains zero-initialized or uninitialized data, although the exact behavior is system dependent.
r The symbol is in a read only data section.

ちょっと情報量が多いですが、肝心なのは a.cpp 内で定義されている関数が T (in text section) として含まれているのに対して、 iostream 内の関数が U (undefined) になっている事です。これの実体は libstdc++.so に含まれているわけですが、このオブジェクトには「外にあるどこかのシンボルにジャンプする」という事しか書いていません。これを実際どこに飛べばいいか教えてあげるのがリンクの操作です。

オブジェクトファイルにはプログラムをCPUが読める機械語に変換したバイナリが含まれています。これを見てみましょう objdump -d を使います

$ objdump -dC a.o

a.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <func_a()>:
   0:   55                      push   %rbp
   1:   48 89 e5                mov    %rsp,%rbp
   4:   48 8d 35 00 00 00 00    lea    0x0(%rip),%rsi        # b <func_a()+0xb>
   b:   48 8d 3d 00 00 00 00    lea    0x0(%rip),%rdi        # 12 <func_a()+0x12>
  12:   e8 00 00 00 00          callq  17 <func_a()+0x17>
  17:   48 89 c2                mov    %rax,%rdx
  1a:   48 8b 05 00 00 00 00    mov    0x0(%rip),%rax        # 21 <func_a()+0x21>
  21:   48 89 c6                mov    %rax,%rsi
  24:   48 89 d7                mov    %rdx,%rdi
  27:   e8 00 00 00 00          callq  2c <func_a()+0x2c>
  2c:   90                      nop
  2d:   5d                      pop    %rbp
  2e:   c3                      retq   

  ^^    ^^^^^^^^^^^^^^^^^^^^^   ^^^^^^^^^^^^^^^^^^^^^^^^^^  ^^^^^^^^^^^^^^^^^^^^^
  番地   機械語(x86_64)         人間に読めるように翻訳した命令    注釈

(以下略)

元の C++ プログラムを見る限り、ここでは std::ostream::operator<<(std::ostream& (*)(std::ostream&)) を実行しないといけないはずですが、func_a() + 0x2c のように相対的なアドレス値しか入っていません。なのでオブジェクトファイル a.o は単独では実行できません。

オブジェクト (*.o) → 実行可能ファイル (a.out)

では実行可能なファイルを作っていきましょう。 C++ では実行可能ファイルを作るには実行を開始する関数である main 関数を定義する必要があります。次のファイルを用意しましょう

main.cpp
void func_a();

int main() {
  func_a();
}

func_a() を呼び出すだけのプログラムです。多くの場合では関数宣言 void func_a(); の部分はヘッダーファイル (*.h, *.hpp) に記述してプリプロセッサで展開させる事が多いですが、ここでは素の挙動がわかりやすい様に *.cpp の方に書いています。
まず一旦オブジェクトファイルにしてみましょう

$ g++ -c main.cpp
$ nm -C main.o
                 U _GLOBAL_OFFSET_TABLE_
0000000000000000 T main
                 U func_a()

このように宣言だけしてあり実装が無い関数 func_a()U (undefined) になります。これを実行ファイルにしましょう。g++ コマンドは -c を付けないと実行ファイルを作ろうとするのでそれを使います

$ g++ main.o 
/usr/bin/ld: main.o: in function `main':
main.cpp:(.text+0x5): undefined reference to `func_a()'
collect2: error: ld returned 1 exit status

Undefined な関数 func_a() があると言って失敗しますね。Undefined な関数があると、プログラムは途中でどこに処理を移せばいいか分からなくなってしまうので、このままでは実行ファイルに出来ないわけです。 func_a() は上で見た通り a.o に入っているのでしたね

$ g++ main.o a.o
$ ./a.out
This is func_a!

実行出来ました!

ここで気をつけないといけないことは、 main.cppa.cpp の間で func_a() を結びつけているのは名前でしかないという事です。main.o を作った段階ではこれはどの func_a() を呼び出すか分からない、a.cpp で実装されている func_a() なのか、それとも別のファイル a2.cpp で実装された func_a() かもしれないということです。これがいつ決まるのかを把握するとリンクエラーを自力で解決出きる可能性がぐっとあがります。

オブジェクト (*.o) → アーカイブ (*.a)

ややこしい共有ライブラリに行く前に(共有ライブラリは次回やります)まず簡単なアーカイブの方を見てみましょう。アーカイブを生成するには ar コマンドを使います

$ ar rcs libab.a a.o b.o

artar と同じような、圧縮をせずにファイルに結合するだけのアーカイブ形式で、

$ ar tv libab.a
rw-r--r-- 0/0   2760 Jan  1 09:00 1970 a.o
rw-r--r-- 0/0   2760 Jan  1 09:00 1970 b.o

のように t コマンドで中身を確認できます。nmobjdump はそのまま対応しており、

$ nm -C libab.a

a.o:
                 U __cxa_atexit
                 U __dso_handle
                 U _GLOBAL_OFFSET_TABLE_
0000000000000078 t _GLOBAL__sub_I__Z6func_av
000000000000002f t __static_initialization_and_destruction_0(int, int)
0000000000000000 T func_a()
                 U std::ostream::operator<<(std::ostream& (*)(std::ostream&))
                 U std::ios_base::Init::Init()
                 U std::ios_base::Init::~Init()
                 U std::cout
                 U std::basic_ostream<char, std::char_traits<char> >& std::endl<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&)
0000000000000000 r std::piecewise_construct
0000000000000000 b std::__ioinit
                 U std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)

b.o:
                 U __cxa_atexit
                 U __dso_handle
                 U _GLOBAL_OFFSET_TABLE_
0000000000000078 t _GLOBAL__sub_I__Z6func_bv
000000000000002f t __static_initialization_and_destruction_0(int, int)
0000000000000000 T func_b()
                 U std::ostream::operator<<(std::ostream& (*)(std::ostream&))
                 U std::ios_base::Init::Init()
                 U std::ios_base::Init::~Init()
                 U std::cout
                 U std::basic_ostream<char, std::char_traits<char> >& std::endl<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&)
0000000000000000 r std::piecewise_construct
0000000000000000 b std::__ioinit
                 U std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)

のようにそれぞれのオブジェクトファイル毎に出してくれます。

これは素の a.o と同じように使えます

$ g++ main.o libab.a
$ ./a.out
This is func_a!

参考文献

33
20
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
33
20