gccとリンクエラーのはなし
Linux上で移植ビルドをやっているとしばしば、謎のリンクエラーに悩まされます。
start.S:84: undefined reference to `__libc_csu_init'
libcrypto.so: undefined reference to `fstat@GLIBC_2.33'
libc.so.6: undefined reference to `__tunable_is_initialized@GLIBC_PRIVATE'
Googleで検索するとQ&A事例が山のように出てきて、なんだかみんな違うことを言っていてわけがわからなかったりします。今回はこのへんの話を整理してみようと思います。
gccの基本動作
C言語の古典的なサンプルとして「Hello, world」があります。
hello.c:
#include <stdio.h>
int main(int argc, char *argv[])
{
printf("Hello, world\n");
return 0;
}
これをコンパイルするのに普通は
$ gcc -o hello hello.c
と書きますが、中間オブジェクトhello.oを経由する場合は
$ gcc -c -o hello.o hello.c
$ gcc -o hello hello.o
と書きます。
hello.oの中身をobjdumpで見るとこんな風になっています。
$ objdump --syms hello.o
SYMBOL TABLE:
0000000000000000 l df *ABS* 0000000000000000 hello.c
0000000000000000 l d .text 0000000000000000 .text
0000000000000000 l d .data 0000000000000000 .data
0000000000000000 l d .bss 0000000000000000 .bss
0000000000000000 l d .rodata 0000000000000000 .rodata
0000000000000000 l d .note.GNU-stack 0000000000000000 .note.GNU-stack
0000000000000000 l d .note.gnu.property 0000000000000000 .note.gnu.property
0000000000000000 l d .eh_frame 0000000000000000 .eh_frame
0000000000000000 l d .comment 0000000000000000 .comment
0000000000000000 g F .text 0000000000000026 main
0000000000000000 *UND* 0000000000000000 _GLOBAL_OFFSET_TABLE_
0000000000000000 *UND* 0000000000000000 puts
関数名mainがグローバルシンボル(g)として定義され、未定義の(*UND*)外部シンボルに_GLOBAL_OFFSET_TABLE_とputsが定義されています。putsは「引数なしのprintf」が自動的に置き換えられたものです。
生成されたELF実行ファイルhelloの中身を見ると、ずっと複雑になっています。
$ objdump --syms hello
SYMBOL TABLE:
0000000000000318 l d .interp 0000000000000000 .interp
0000000000000338 l d .note.gnu.property 0000000000000000 .note.gnu.property
0000000000000358 l d .note.gnu.build-id 0000000000000000 .note.gnu.build-id
000000000000037c l d .note.ABI-tag 0000000000000000 .note.ABI-tag
00000000000003a0 l d .gnu.hash 0000000000000000 .gnu.hash
00000000000003c8 l d .dynsym 0000000000000000 .dynsym
0000000000000470 l d .dynstr 0000000000000000 .dynstr
00000000000004f2 l d .gnu.version 0000000000000000 .gnu.version
0000000000000500 l d .gnu.version_r 0000000000000000 .gnu.version_r
0000000000000520 l d .rela.dyn 0000000000000000 .rela.dyn
00000000000005e0 l d .rela.plt 0000000000000000 .rela.plt
0000000000001000 l d .init 0000000000000000 .init
0000000000001020 l d .plt 0000000000000000 .plt
0000000000001040 l d .plt.got 0000000000000000 .plt.got
0000000000001050 l d .plt.sec 0000000000000000 .plt.sec
0000000000001060 l d .text 0000000000000000 .text
00000000000011e8 l d .fini 0000000000000000 .fini
0000000000002000 l d .rodata 0000000000000000 .rodata
0000000000002014 l d .eh_frame_hdr 0000000000000000 .eh_frame_hdr
0000000000002058 l d .eh_frame 0000000000000000 .eh_frame
0000000000003db8 l d .init_array 0000000000000000 .init_array
0000000000003dc0 l d .fini_array 0000000000000000 .fini_array
0000000000003dc8 l d .dynamic 0000000000000000 .dynamic
0000000000003fb8 l d .got 0000000000000000 .got
0000000000004000 l d .data 0000000000000000 .data
0000000000004010 l d .bss 0000000000000000 .bss
0000000000000000 l d .comment 0000000000000000 .comment
0000000000000000 l df *ABS* 0000000000000000 crtstuff.c
0000000000001090 l F .text 0000000000000000 deregister_tm_clones
00000000000010c0 l F .text 0000000000000000 register_tm_clones
0000000000001100 l F .text 0000000000000000 __do_global_dtors_aux
0000000000004010 l O .bss 0000000000000001 completed.8061
0000000000003dc0 l O .fini_array 0000000000000000 __do_global_dtors_aux_fini_array_entry
0000000000001140 l F .text 0000000000000000 frame_dummy
0000000000003db8 l O .init_array 0000000000000000 __frame_dummy_init_array_entry
0000000000000000 l df *ABS* 0000000000000000 hello.c
0000000000000000 l df *ABS* 0000000000000000 crtstuff.c
000000000000215c l O .eh_frame 0000000000000000 __FRAME_END__
0000000000000000 l df *ABS* 0000000000000000
0000000000003dc0 l .init_array 0000000000000000 __init_array_end
0000000000003dc8 l O .dynamic 0000000000000000 _DYNAMIC
0000000000003db8 l .init_array 0000000000000000 __init_array_start
0000000000002014 l .eh_frame_hdr 0000000000000000 __GNU_EH_FRAME_HDR
0000000000003fb8 l O .got 0000000000000000 _GLOBAL_OFFSET_TABLE_
0000000000001000 l F .init 0000000000000000 _init
00000000000011e0 g F .text 0000000000000005 __libc_csu_fini
0000000000000000 w *UND* 0000000000000000 _ITM_deregisterTMCloneTable
0000000000004000 w .data 0000000000000000 data_start
0000000000000000 F *UND* 0000000000000000 puts@@GLIBC_2.2.5
0000000000004010 g .data 0000000000000000 _edata
00000000000011e8 g F .fini 0000000000000000 .hidden _fini
0000000000000000 F *UND* 0000000000000000 __libc_start_main@@GLIBC_2.2.5
0000000000004000 g .data 0000000000000000 __data_start
0000000000000000 w *UND* 0000000000000000 __gmon_start__
0000000000004008 g O .data 0000000000000000 .hidden __dso_handle
0000000000002000 g O .rodata 0000000000000004 _IO_stdin_used
0000000000001170 g F .text 0000000000000065 __libc_csu_init
0000000000004018 g .bss 0000000000000000 _end
0000000000001060 g F .text 000000000000002f _start
0000000000004010 g .bss 0000000000000000 __bss_start
0000000000001149 g F .text 0000000000000026 main
0000000000004010 g O .data 0000000000000000 .hidden __TMC_END__
0000000000000000 w *UND* 0000000000000000 _ITM_registerTMCloneTable
0000000000000000 w F *UND* 0000000000000000 __cxa_finalize@@GLIBC_2.2.5
hello.oで「puts」だったシンボルは「puts@@GLIBC_2.2.5」に変わっていますが、これは共有ライブラリlibc.so.6を間接参照していることを意味します。シンボル「_start」はgccが「勝手に」追加するルーチンで、OS(Linux)におけるアプリケーションの初期化・終了などの「お決まり」手続きを処理しています。この「スタートアップ・オブジェクト」は通常crt1.oというファイル名が自動的にリンクされており、Linuxでは/usr/lib/x86_64-linux-gnu/に格納されています。その中身を見てみると
$ objdump --syms /usr/lib/x86_64-linux-gnu/crt1.o
SYMBOL TABLE:
0000000000000000 l d .text 0000000000000000 .text
0000000000000000 l d .note.gnu.property 0000000000000000 .note.gnu.property
0000000000000000 l d .note.ABI-tag 0000000000000000 .note.ABI-tag
0000000000000000 l d .rodata.cst4 0000000000000000 .rodata.cst4
0000000000000000 l d .eh_frame 0000000000000000 .eh_frame
0000000000000000 l d .data 0000000000000000 .data
0000000000000000 l d .bss 0000000000000000 .bss
0000000000000000 l d .note.GNU-stack 0000000000000000 .note.GNU-stack
0000000000000000 *UND* 0000000000000000 __libc_csu_fini
0000000000000030 g F .text 0000000000000005 .hidden _dl_relocate_static_pie
0000000000000000 g F .text 000000000000002f _start
0000000000000000 *UND* 0000000000000000 __libc_csu_init
0000000000000000 *UND* 0000000000000000 main
0000000000000000 w .data 0000000000000000 data_start
0000000000000000 *UND* 0000000000000000 _GLOBAL_OFFSET_TABLE_
0000000000000000 g O .rodata.cst4 0000000000000004 _IO_stdin_used
0000000000000000 *UND* 0000000000000000 __libc_start_main
0000000000000000 g .data 0000000000000000 __data_start
となっており、確かに_startが入っていてmainを外部参照しています。しかしそれだけではなく__libc_csu_initや__libc_csu_finiを外部参照しています。これらはlibc_nonshared.aに格納されています。
$ objdump --syms /usr/lib/x86_64-linux-gnu/libc_nonshared.a
SYMBOL TABLE:
0000000000000000 l d .text 0000000000000000 .text
0000000000000000 l d .data 0000000000000000 .data
0000000000000000 l d .bss 0000000000000000 .bss
0000000000000000 l d .note.GNU-stack 0000000000000000 .note.GNU-stack
0000000000000000 l d .note.gnu.property 0000000000000000 .note.gnu.property
0000000000000000 l d .eh_frame 0000000000000000 .eh_frame
0000000000000000 g F .text 0000000000000065 __libc_csu_init
0000000000000000 *UND* 0000000000000000 .hidden __init_array_start
0000000000000000 *UND* 0000000000000000 .hidden __init_array_end
0000000000000000 *UND* 0000000000000000 _GLOBAL_OFFSET_TABLE_
0000000000000000 *UND* 0000000000000000 _init
0000000000000070 g F .text 0000000000000005 __libc_csu_fini
libc.so.6とlibc_nonshared.aの両方が「勝手に」リンクされるからくりは、ライブラリディレクトリに含まれている「libc.so」が名前に反して共有ライブラリファイルではなくASCII TEXTファイルで、gccに対してlibc.so.6とlibc_nonshared.aのリンクを指示するスクリプトファイルになっているからです。
$ cat /usr/lib/x86_64-linux-gnu/libc.so
/* GNU ld script
Use the shared library, but some functions are only in
the static library, so try that secondarily. */
OUTPUT_FORMAT(elf64-x86-64)
GROUP ( /lib/x86_64-linux-gnu/libc.so.6 /usr/lib/x86_64-linux-gnu/libc_nonshared.a AS_NEEDED ( /lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 ) )
"Hello world"を動かすだけで思ったより複雑な仕掛けが動いていますが、これが今のgccとLinuxのELFバイナリ実行の仕組みです。
クロスコンパイラ環境によっては、参照先がlibc.so.6ではなくlibc.so.1で、その実体がlibuClibc-1.x.x.soにシンボリックリンクされているかも知れません。uClibc(μClibc)は標準glibcに代えて使うことのできる軽量版(機能限定版)のライブラリで、大容量メモリがまだ高価だった時代の組み込みLinuxでは多用されていました。最近は少なくなっていますが、「工業用」をうたうLinuxのBSPではまだ使われていることがあります。
よくあるトラブル
ビルド環境と実行環境が同一の場合はともかく、x86-64マシン上でaarch64バイナリを作るようなクロス環境ではしばしば、プリビルドされたrootfsイメージを用いたクロスビルドを行うことが求められます。そのクロスコンパイラを何処から持ってきて何処にインストールするかもケースバイケースです。Ubuntu Linuxでは
$ sudo apt-get install gcc-aarch64-linux-gnu
を実行することでaarch64-linux-gnu-gccが使えるようになりますが、この実行バイナリは/usr/bin/に入っており、ライブラリ群は/usr/aarch64-linux-gnu/libに入っています。この環境で
$ aarch64-linux-gnu-gcc -o hello hello.c
でビルドすればARM aarch64-ELFのhelloが作られますが、これがターゲット環境で動くとは限りません。このhelloは共有ライブラリ/usr/aarch64-linux-gnu/lib/libc.so.6とリンクすることを前提にビルドされており、ターゲットのrootfsにインストールされているlibc.so.6とバージョンが一致する保証が無いからです。もし不一致が起きると「fstat@GLIBC_2.33が見つからない」みたいな実行時エラーが出ます。
「こんなこともあろうか」と、gccにはルートディレクトリを別の場所に付け替える--sysrootオプションが用意されています。
$ aarch64-linux-gnu-gcc --sysroot=./custom-rootfs -o hello hello.c
のように指定すれば、/usr/includeや/usr/libの代わりに./custom-rootfs/usr/includeや./custom-rootfs/libが参照されるはずというオプションです。
しかしUbuntu Linuxの場合、--sysrootはネイティブコンパイラ(x86_64-linux-gnu)に対しては働くものの、どういうわけかクロスコンパイラには働きません。gcc --sysroot=で存在しないディレクトリを指定すると「stdio.hが見つからない」エラーでコンパイルも通りませんが、aarch64-linux-gnu-gccではスルーで通ってしまいます。リンク時も同様に、--sysroot(あるいは_Wl,--sysroot=)に関わらず、crt1.o・libc.so・libc.so.6・libc_nonshared.aなどのファイルは常に/usr/aarch64-linux-gnu/lib/から参照されていしまいます。
Ubuntuのクロスコンパイラで特定バージョンのlibc.so.6とリンクしたい場合は、その絶対パスを名指しで指定する必要があるようです。
$ aarch64-linux-gnu-gcc -o hello hello.c rootfs/usr/lib/aarch64-linux-gnu/libc.so.6
rootfs/usr/lib/aarch64-linux-gnu/ld-linux-aarch64.so.1
「ld-linux-aarch64.so.1」はこのバージョンのlibc.so.6から参照されている幾つかのシンボルを含む共有ライブラリです。gccのバージョンやrootfsのglibcのバージョンによって、このへんの組み合わせは違ってくるでしょう。めんどくさいですが「そういうもの」で、このめんどくささゆえに「Googleで検索するとQ&A事例が山のように出てきて、なんだかみんな違うことを言っている」状態になっています。
ランタイムトラブルの緊急回避
ターゲット環境で「libc.so.6のバージョンが不一致」というエラーが出て動かない場合、「本来なら」そのターゲットシステムのlibc.so.6に整合したビルドに修正すべきなのですが、「そんなことやってられん」という場合には裏技があります。Linuxは環境変数LD_LIBRARY_PATHで共有ライブラリの検索パスを指定できるので、
$ mkdir -p /home/root/lib/
$ cp libc.so.6 /home/root/lib/
$ export LD_LIBRARY_PATH=/home/root/lib
$ ./hello
のように、そのランタイムバイナリのビルドに参照された共有ライブラリをローカルディレクトリにコピーして、環境変数LD_LIBRARY_PATHにローカルディレクトリを指定して「無理やり」動かすことは可能です。これはあくまで「裏技」であり、あまり褒められたことではないことに注意してください。
その実行ファイルが必要とする共有ライブラリは「readelf」コマンドで知ることができます。
$ readelf -a hello
:
Dynamic section at offset 0x2dc8 contains 27 entries:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
0x000000000000000c (INIT) 0x1000
0x000000000000000d (FINI) 0x11e8
0x0000000000000019 (INIT_ARRAY) 0x3db8
:
Symbol table '.dynsym' contains 7 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterTMCloneTab
2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND puts@GLIBC_2.2.5 (2)
3: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.2.5 (2)
4: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
5: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMCloneTable
6: 0000000000000000 0 FUNC WEAK DEFAULT UND __cxa_finalize@GLIBC_2.2.5 (2)
のような表示で、”Dynamic section”からは「libc.so.6を必要としている」こと、”Symbol table”からは「putsと__libc_start_mainが外部関数(FUNC GLOBAL DEFAULT UND)として参照されている」ことがわかります。しかしながら、「どのシンボルがどのライブラリに有ることが期待されているのか」という対応一覧は無いようです。(@の後の文字列がヒントにはなります)
--sysrootオプションの謎
Ubuntuクロスコンパイラの場合、--sysrootはデフォルトライブラリ(crt1.oやlibc.so.6)のパス変更には効かない一方で、通常のリンクライブラリ(-l)の指定には効果があるようです。ここでlibsub.cというライブラリを作ってみます。
libsub.c:
int sub(void)
{
return 42;
}
$ aarch64-linux-gnu-gcc -c -o libsub.o libsub.c
$ aarch64-linux-gnu-ar r libsub.a libsub.o
hello.cをこれを呼び出すように改変して、
include <stdio.h>
extern int sub(void);
int main(int argc, char *argv[])
{
printf("Answer=%d\n", sub());
return 0;
}
ビルドしようとするとエラーになります。
$ aarch64-linux-gnu-gcc -c -o hello.o hello.c
$ aarch64-linux-gnu-gcc -o hello hello.o
/usr/lib/gcc-cross/aarch64-linux-gnu/9/../../../../aarch64-linux-gnu/bin/ld: hello.o: in function `main':
hello.c:(.text+0x10): undefined reference to `sub'
collect2: error: ld returned 1 exit status
gccでライブラリをリンクするのは、リンクリストの中に.aないし.soの絶対パスを直接指定するか、
$ aarch64-linux-gnu-gcc -o hello hello.o libsub.a
-lでライブラリ名を指定する方法がありますが、後者は「-lsubが見つからない」というエラーになります。
$ aarch64-linux-gnu-gcc -o hello hello.o -lsub
/usr/lib/gcc-cross/aarch64-linux-gnu/9/../../../../aarch64-linux-gnu/bin/ld: cannot find -lsub
collect2: error: ld returned 1 exit status
gccには「インクルードパス」と「ライブラリパス」という概念があり、#includeで指定されたファイルや-lで指定されたライブラリは複数の候補を並べて指定することで、最初に見つかったファイルを使う仕組みになっています。ずいぶんいい加減な仕組みですが、1970年代に作られたカーニハン&リッチーの最初のCコンパイラがそういう仕様だったので21世紀まで引きずっています。前者は-I・後者は-Lで指定できるので、
$ aarch64-linux-gnu-gcc -o hello hello.o -lsub -L.
とすればビルドは通ります。
余談ですが、いまのconfigureやautoconfが作るMakefileでは-Iオプションが数十個くっつくことも珍しくありません。コマンドラインをいちいち表示するとコンパイル1回あたり1画面を埋め尽くすくらいのログが流れてゆくのでエコーバック表示は抑制されていることが多く、ものによって異なりますが「V=1 make」でコマンドライン表示できるものが多いです。
さてここで--sysrootの再登場です。ローカルに仮想のライブラリディレクトリを作り、そこに作成したlibsub.aをコピーして、
$ mkdir -p ./usr/lib/aarch64-linux-gnu
$ cp libsub.a usr/lib/aarch64-linux-gnu/
-lsub --sysroot=.を付けてコンパイルすれば、なんとビルドが通ります。
$ aarch64-linux-gnu-gcc -o hello hello.o -lsub --sysroot=.
--sysroot本来の機能定義「gccにとっての/の位置を差し替える」からすればおかしな話で、/が./に変わったら/usr/aarch64-linux-gnu/libに入っていたはずのcrt1.oやlibc.so.6も「見えなく」なってエラーになるはずですが、何故かaarch64-linux-gnu-gccではこれで通ってしまいます。ネイティブのx86_64 gccも#includeに対しては「そんなファイルは見つからない」とエラーにしていたのに、リンクに関しては--sysrootが効いていないようです。
このへんの挙動はgccのビルド時のconfigによって変わるのか、同じ「Linux上のクロスgcc」でもaarch64-none-linux-gccではコンパイル時もリンク時も--sysrootが律儀に解釈され、存在しないディレクトリを指定すると「crt1.oが見つからない」とエラーにするものもありました。どうしてこんなことになっているのかわかりませんが、--sysroot, -Wl,--sysroot, -L, -lは自分が使うコンパイラの癖を把握して指定する必要があるようです。
--sysrootと-Wl,--sysroot
--sysrootは「gccに対して/の挿し替えを指示するオプション」、-Wlは「gccから呼び出すリンカに対して指示するオプション」で、-Wl,--sysroot=は「リンカに対して/の挿し替えを指示するオプション」になります。--sysrootオプションが「額面通りに効く」aarch64-none-linux-gccを使ってこれらを試したとき、妙な現象に出くわしました。
$ aarch64-none-linux-gcc -o hello hello.c
ビルド成功。生成されたhelloには__libc_csu_initが入っている。
$ aarch64-none-linux-gcc -o hello hello.c --sysroot=./custom-rootfs
ビルド成功。生成されたhelloには__libc_csu_initが入っていない。
$ aarch64-none-linux-gcc -o hello hello.c -Wl,--sysroot=./custom-rootfs
ビルド失敗。「crt1.oがから参照されている__libc_csu_initが見つからない」というエラーが大量に出る。
どうやらこのクロスコンパイラでは、--sysroot=を指定した場合はcrt1.oとlibc.soがともに指定ディレクトリから参照されるのに対し、-Wl,--sysroot=を指定した場合はcrt1.oがコンパイラのデフォルトライブラリディレクトリから・libc.soが指定ディレクトリから参照されて不整合エラーになったようです。
上に書いたように、--sysrootの効き方は「ものによって違う」ので、クロスコンパイラで出た謎エラーをセルフコンパイラで再現しようとすると挙動が違ってビルド成功してしまったり、逆にクロスコンパイラでと言っていたものがセルフではエラーになったりします。めんどくさいですが「gccとはそういうもの」として付き合うしかないのかもしれません。
chrootを使う方法
gccがどうしても言うことを聞かない場合は、Linuxの”chroot”コマンドを使って「ルートディレクトリ偽装」を行う方法もあります。
$ sudo chroot --userspec=<ユーザー名> </の代わりにしたいパス>
これを実行したあとのシェルは「指定したパスを/とみなした仮想環境の箱庭入り」になります。
chroot環境では「箱庭の外」が「見えなく」なってしまうので、ソースコードやMakefileもその仮想rootfs内にコピーしておく必要があります。chrootには不便も制限もありますが、「相対パスと絶対パスの混在問題」だとか「Makefileにおける該当パスの引用が外部アプリケーション(dpkg)に依存していて、ホスト環境とクロス環境で使い分けなければならない」のような面倒くさい問題は緩和されます。
まとめ
本業で幾つかのクロスビルド案件に関わってこの関連のエラーが出てえらく悩まされ、「そもそもcrt1.oって何処にあって何が入っているんだ」「-L, --sysroot, -Wl,--sysrootって本来の定義は何で実際のコンパイラではどう働くんだ」ということを調べ直す必要がありました。
最近のクロスビルド案件ではYoctoが使われることが多いです。YoctoはPythonスクリプトで書かれた化物みたいな自動ビルドシステムで、閉じたディレクトリ環境の中で指定されたスクリプトに従ってホストツールのリビルドから全部やり直します。1案件あたり数ギガバイトのストレージ容量を消費し、クリーンビルドからやり直すと数時間かかったりする重たいシステムですが、何から何まで指定環境に合わせてゼロから作り直すので、このような「ホストにインストールされているクロスコンパイラのlibc.so.6とターゲットrootfsのlibc.so.6のバージョン違い」のような相性問題は起きないで済みます。
しかし、「OSバージョン偽装のためのDocker」「rootfs偽装のためのchroot」「環境に応じてMakefileをカスタマイズするconfigure」「環境に応じたconfigureを生成するautomake」「特定のビルド環境をゼロから再構築するためのYocto」などを併用していると、ときどき自分が何をやっているのかわからなくなってきます。
そもそも21世紀にもなっていったい何時までネイティブバイナリコンパイラなんか使っているんだ、Java仮想マシンの仮想オブジェクトで「Write Once, Run Everywhere」とか言ってたのも30年前じゃんか!あれはどうなったんだよ!と叫びたくもなりますが、世の中得てしてそんなものであります。