はじめに
背景
既存のソースに手を加えてちょっとしたアプリを作ろうとしていた時に、既存のソース構成をマネしているのにリンクエラーが出るという事象を体験しました。
その内容が「重複シンボルエラー」( multiple definitions ) だったのですが、なぜ既存ソースでは発生せず、マネした方だけ出たのか不思議でした。が、おおよその原因が分かったように思ったのでメモとして残します。
環境
Windows10/WSL + Ubuntu 18 + gcc7.4 + binutils 2.3 他。
なお、検証に使ったソース一式については、githubのレポジトリの以下に置いています。
https://github.com/angel-p57/qiita-sample/tree/master/linkerr
事象
発端
もともと、( 関連部分のみを抜粋すると ) 以下のようなMakefileで示されるソース依存関係を持った、appok.exe
というアプリがあったとします。
appok.exe: appok.o specific.o libcommon.a
gcc $(LDFLAGS) -o appok.exe appok.o specific.o -L. -lcommon
appok.o: appok.c
gcc -c -o appok.o appok.c
specific.o: specific.c
gcc -c -o specific.o specific.c
libcommon.a: common1.o common2.o
ar r libcommon.a common1.o common2.o
common1.o: common1.c
gcc -c -o common1.o common1.c
common2.o: common2.c
gcc -c -o common2.o common2.c
$ make appok.exe
gcc -c -o appok.o appok.c
gcc -c -o specific.o specific.c
gcc -c -o common1.o common1.c
gcc -c -o common2.o common2.c
ar r libcommon.a common1.o common2.o
ar: creating libcommon.a
gcc -o appok.exe appok.o specific.o -L. -lcommon
$ ./appok.exe
itest1=1, itest2=2, idup=3
ここに、appok.exe
をマネした appng.exe
についてMakefileの記述とソースを追加し、ビルドを実行しました。
appng.exe: appng.o specific.o libcommon.a
gcc $(LDFLAGS) -o appng.exe appng.o specific.o -L. -lcommon
appng.o: appng.c
gcc -c -o appng.o appng.c
$ make appng.exe
gcc -c -o appng.o appng.c
gcc -o appng.exe appng.o specific.o -L. -lcommon
./libcommon.a(common2.o):(.bss+0x0): multiple definition of `idup'
specific.o:(.data+0x0): first defined here
collect2: error: ld returned 1 exit status
Makefile:5: recipe for target 'appng.exe' failed
make: *** [appng.exe] Error 1
ところが、上のようにシンボル idup
の重複定義ということで、リンクに失敗してしまいました。
直接の原因
エラー内容を再掲します。
gcc -o appng.exe appng.o specific.o -L. -lcommon
./libcommon.a(common2.o):(.bss+0x0): multiple definition of `idup'
specific.o:(.data+0x0): first defined here
collect2: error: ld returned 1 exit status
直接的な原因は、エラーメッセージにある通り idup
を複数のソースで重複定義してリンクしてしまったためです。
ところがこの idup
、新しく作った appng.c
で定義したわけではありません。
次のように、リンク対象のオブジェクトのソース specific.c
と、common2.c
( オブジェクトは libcommon.a
にアーカイブされる ) に含まれていたものでした。
$ grep idup *.c
common2.c:int idup = 0;
specific.c:int idup = 3;
specific.c: printf("itest1=%d, itest2=%d, idup=%d\n", itest1, itest2, idup);
つまり、もともとシンボル重複を起こす状況だったにも関わらず、顕在化していなかったということです。
状況整理
ここで、ビルドでできる各中間ファイルのシンボルの状況をnm
コマンドで見てみます。
$ nm specific.o libcommon.a
specific.o:
U _GLOBAL_OFFSET_TABLE_
0000000000000000 T ftest
0000000000000000 D idup
U itest1
U itest2
U printf
libcommon.a:
common1.o:
0000000000000000 D itest1
common2.o:
0000000000000000 B idup
0000000000000000 D itest2
これだけを見ると、specific.o
で未定義(U)となっているitest1
,itest2
の定義を、それぞれlibcommon.a
に含まれるcommon1.o
,common2.o
で解決する必要があり、その結果、common2.o
,specific.o
の両者に含まれるidup
が重複を起こすのは必然に見えます。
この状況は、リンカーフラグ-M
( gcc経由で渡す場合は-Wl,-M
) で見ることができます。
実際、appng.exe
のリンクの際は想定通りに思えます。
$ LDFLAGS=-Wl,-M make appng.exe
gcc -Wl,-M -o appng.exe appng.o specific.o -L. -lcommon
Archive member included to satisfy reference by file (symbol)
./libcommon.a(common1.o) specific.o (itest1)
./libcommon.a(common2.o) specific.o (itest2)
…
ところが、appok.exe
の場合、違った状況になっていることが分かりました。
$ rm -f appok.exe; LDFLAGS=-Wl,-M make appok.exe
gcc -Wl,-M -o appok.exe appok.o specific.o -L. -lcommon
Archive member included to satisfy reference by file (symbol)
./libcommon.a(common1.o) specific.o (itest1)
…
実は、itest2
のシンボル解決のためにcommon2.o
を読み込んでいません。
つまり、common2.o
を読み込まないからこそ、idup
のシンボル重複にもならなかったのだと分かりました。
根本原因
では、かたやcommon2.o
を読み込む、かたや読み込まない。この違いはどこから生まれたのか。それは、おおもとのappok.c
,appng.c
にありました。
今度は、appok.o
,appng.o
も含めシンボル状況をnm
コマンドで見てみます。
$ nm appok.o appng.o specific.o libcommon.a
appok.o:
U _GLOBAL_OFFSET_TABLE_
U ftest
0000000000000000 D itest2
0000000000000000 T main
appng.o:
U _GLOBAL_OFFSET_TABLE_
U ftest
0000000000000000 T main
specific.o:
U _GLOBAL_OFFSET_TABLE_
0000000000000000 T ftest
0000000000000000 D idup
U itest1
U itest2
U printf
libcommon.a:
common1.o:
0000000000000000 D itest1
common2.o:
0000000000000000 B idup
0000000000000000 D itest2
appok.o
はappng.o
と違って、既にitest2
のシンボルがあります。
これは、( 今回の検証用ソースでは ) 意図的に itest2
の定義を抜いてappng.c
を作ったからです。
$ diff -u appok.c appng.c
--- appok.c 2020-05-04 13:06:26.593127900 +0900
+++ appng.c 2020-05-04 13:06:26.577501900 +0900
@@ -1,5 +1,4 @@
void ftest(void);
-int itest2 = 2;
int main(void) {
ftest();
なので、specific.o
をリンクした時点でitest2
が解決済みになり、common2.o
の読み込みがスキップされたということになります。
つまり、シンボル解決に関係がなければ .a
アーカイブの中の .o
ファイルは読み込まれないと見ることができます。
逆に、アーカイブではなく、直接.o
ファイルを指定した場合は、解決済みかどうかに関わらず読み込みが行われるため、appok.exe
の方もエラーになります。
$ gcc -o appok.exe appok.o specific.o common1.o common2.o
common2.o:(.data+0x0): multiple definition of `itest2'
appok.o:(.data+0x0): first defined here
common2.o:(.bss+0x0): multiple definition of `idup'
specific.o:(.data+0x0): first defined here
collect2: error: ld returned 1 exit status
結論と対処
今回の検証で、分かったことは次の2点です。
- 未解決シンボルがないオブジェクト(
.o
ファイル)は、アーカイブ(.a
ファイル)から読み込まれない。 - 今回のような重複シンボルエラーが発生する場合、未解決シンボルを (
appok.c
におけるitest2
のように ) 予め解決済みにしておいて、被疑オブジェクトの読み込みをスキップさせて回避することができる。
個人的にはあまりスッキリしない話ですが、挙動を見る限り、こういう話になるのではないかと思います。