5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

リンク時重複シンボルエラーの明暗が分かれる例

Posted at

はじめに

背景

既存のソースに手を加えてちょっとしたアプリを作ろうとしていた時に、既存のソース構成をマネしているのにリンクエラーが出るという事象を体験しました。
その内容が「重複シンボルエラー」( 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 というアプリがあったとします。

Makefile(元)
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
appok.exeビルド・実行
$ 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の記述とソースを追加し、ビルドを実行しました。

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
appng.exeビルド
$ 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 の重複定義ということで、リンクに失敗してしまいました。

直接の原因

エラー内容を再掲します。

makeログ抜粋
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 にアーカイブされる ) に含まれていたものでした。

idupの含まれるソース
$ 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コマンド出力
$ 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のリンクの際は想定通りに思えます。

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 の場合、違った状況になっていることが分かりました。

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で全部見る
$ 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.oappng.oと違って、既にitest2のシンボルがあります。

これは、( 今回の検証用ソースでは ) 意図的に itest2の定義を抜いてappng.cを作ったからです。

appok.c,appng.cのdiff
$ 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の方もエラーになります。

.aを使わず.oを直接リンク
$ 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のように ) 予め解決済みにしておいて、被疑オブジェクトの読み込みをスキップさせて回避することができる。

個人的にはあまりスッキリしない話ですが、挙動を見る限り、こういう話になるのではないかと思います。

5
3
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
5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?