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

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

```text: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
```

```shell-session: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の記述とソースを追加し、ビルドを実行しました。

```text: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
```

```shell-session: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` の重複定義ということで、リンクに失敗してしまいました。

##直接の原因

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

```text: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` にアーカイブされる ) に含まれていたものでした。

```shell-session: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`コマンドで見てみます。

```shell-session: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`のリンクの際は想定通りに思えます。

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

```shell-session: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`コマンドで見てみます。

```shell_session: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`を作ったからです。

```shell-session: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`の方もエラーになります。

```shell-session:.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`のように ) 予め解決済みにしておいて、被疑オブジェクトの読み込みをスキップさせて回避することができる。

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