Clang 11(GCC 10も)で様々なアプリケーションのビルドが失敗する問題の原因と対応策について

# 初めに

協定世界時2020年8月16日19時14分04秒の[リビジョン364284のコミット](https://svnweb.freebsd.org/base?view=revision&revision=364284)で、FreeBSDのbase systemのheadにClang 11.0.0-rc1がマージされたのですが、その後FreeBSD Ports Collectionの多数のportが、13-CURRENTで同様のエラーでビルドに失敗するようになりました。これはClang 11で、`-fcommon`/`-fno-common`オプションに関する振る舞いが変わったことが原因ですが、そのことについて簡単に書いてみようと思います。

# -fcommon/-fno-commonオプションとは

`-fcommon`/`-fno-common`オプションについては、clang(1)のmanページで以下のように説明されています。

>  This flag specifies that variables without initializers get common linkage.  It can be disabled with -fno-common.

"variables without initializers get common linkage"というのがよく判らないのですが、検索した結果から判断すると、大体「別々のソースファイルで同じ名前のグローバル変数が定義されていた場合、それらは同じものとして扱われる」ということのようです。これはC標準の「グローバル変数の定義はソース全体で一度だけ」という規則に明らかに反しているわけですが、多分K&Rの時代からの後方互換性とかでこういうオプションが存在しているのだと思います。それで、Clang 10までは`-fcommon`がデフォルトだったのですが、11でデフォルトが`-fno-common`に変わりました。その結果、昔から存在しているアプリケーションを中心にビルドエラーが発生しているようです。

# 具体例

でもってこれだけだと私自身が今一つピンとこなかったので、これまた検索した結果を参考にして、以下のような簡単なサンプルプログラムを書いてみました。

```make:Makefile
RM=             rm -f

TARGET=         fcommon_test
OBJECTS=        get.o get_char.o set.o main.o

all: ${TARGET}

${TARGET}: ${OBJECTS}
        ${CC} -o ${TARGET} ${OBJECTS}

clean:
        ${RM} ${TARGET} ${OBJECTS}
```

```c:main.c
#include <stdio.h>

extern int get(void);
extern char get_char(void);
extern void set(int);

int
main(int argc, char** argv)
{
    set(65);
    printf("%d\n", get());
    printf("%c\n", get_char());
    return 0;
}
```

```c:get.c
int foo;

int 
get(void)
{
    return foo;
}
```

```c:get_char.c
char foo;

char
get_char(void)
{
    return foo;
}
```

```c:set.c
int foo;

void
set(int val)
{
    foo = val;
}
```

グローバル変数である`foo`は`get.c`,`get_char.c`,`set.c`の三か所で定義されていて、しかも`get.c`と`set.c`ではint型で`get_char.c`ではchar型として定義されています。このような明らかにおかしいコードなのですが、11以前のClangでは問題なくビルド出来て生成されたバイナリもそれっぽく動いてしまいます。

```zsh
yasu@eastasia[3981]% clang --version
FreeBSD clang version 8.0.1 (tags/RELEASE_801/final 366581) (based on LLVM 8.0.1)
Target: x86_64-unknown-freebsd12.1
Thread model: posix
InstalledDir: /usr/bin
yasu@eastasia[3982]% make
cc  -O2 -pipe -c get.c -o get.o
cc  -O2 -pipe -c get_char.c -o get_char.o
cc  -O2 -pipe -c set.c -o set.o
cc  -O2 -pipe -c main.c -o main.o
cc -o fcommon_test get.o get_char.o set.o main.o
yasu@eastasia[3983]% ./fcommon_test
65
A
yasu@eastasia[3984]%
```

一方11では以下のようにリンク時のエラーが発生します。

```zsh
yasu@rolling-vm-freebsd1[1038]% clang --version
FreeBSD clang version 11.0.0 (git@github.com:llvm/llvm-project.git llvmorg-11.0.0-rc1-47-gff47911ddfc)
Target: x86_64-unknown-freebsd13.0
Thread model: posix
InstalledDir: /usr/bin
yasu@rolling-vm-freebsd1[1039]% make
cc  -O2 -pipe -c get.c -o get.o
cc  -O2 -pipe -c get_char.c -o get_char.o
cc  -O2 -pipe -c set.c -o set.o
cc  -O2 -pipe -c main.c -o main.o
cc -o fcommon_test get.o get_char.o set.o main.o
ld: error: duplicate symbol: foo
>>> defined at get.c
>>>            get.o:(foo)
>>> defined at get_char.c
>>>            get_char.o:(.bss+0x0)

ld: error: duplicate symbol: foo
>>> defined at get.c
>>>            get.o:(foo)
>>> defined at set.c
>>>            set.o:(.bss+0x0)
cc: error: linker command failed with exit code 1 (use -v to see invocation)
*** Error code 1

Stop.
make: stopped in /home/yasu/work/test/fcommon
yasu@rolling-vm-freebsd1[1040]%
```

同じ名前のグローバル変数を別々のソースファイルで異なった型で定義してしまうというのは、デバッグの難しいバグにつながりやすいと思うので、Clang 11でのこの変更は妥当なものだと思いますし、むしろなぜもっと早く変更されなかったのかという気さえします。

# GCCは?

実は同様の変更がGCCでも行われています。GCCでは10でデフォルトが`-fcommon`から`-fno-common`に変わったようです。なので、大半のLinux系OSのように、GCCをシステムコンパイラとして使っているOSでも、いずれ同様の問題が発生するはずです。現在GCC 10が各Linux系OSでどの程度採用されているかは判りませんが、やがてあらゆるLinux系OSのパッケージメンテナがこの問題に直面するもとの思われます。

# -fno-commonが原因のビルドエラーの修正方法

前述のように-fno-commonが原因のアプリケーションのビルドエラーは、いずれFreeBSD以外でも発生するものと思われます。そこで大した内容ではないのですが、私や他のFreeBSD Ports Collectionのメンテナが行った-fno-commonが原因のビルドエラーの修正方法を、簡単に説明しておきます。

## Upstreamがパッチや新しいバージョンをリリースしてないか確認する

現在FreeBSD Ports Collectionはこの問題への対応の真っ最中なのですが、メンテナの中にはこの問題を修正するパッチを作成して、upstreamにフィードバックしている人もいるようです。なので、Upstreamのリポジトリやバグ追跡システムを確認すれば、この問題を修正するパッチが見つかるかもしれません。あるいはもうしばらくすれば、この問題に対応した新しいバージョンがリリースされることもあるでしょう。

## FreeBSD Ports Collection(あるいは他のOS)が問題を修正してないか確認する

開発が停滞したり終了しているアプリケーションの場合には、upstreamがこの問題に対応する可能性が低いでしょう。しかしその場合でも個々のOSが個別に問題に対応している可能性はあります。たとえば私の場合、FreeBSD Ports Collectionの[`sysutils/htop`](https://svnweb.freebsd.org/ports/head/sysutils/htop/)というportのビルドエラーに対応しましたが、そのために必要なパッチは[ここ](https://svnweb.freebsd.org/ports/head/sysutils/htop/)からダウンロードすることができます。各portsのコミットログを見て`-fno-common`や`Clang 11`などのキーワードがあれば、きっとそのコミットで問題を修正するパッチが追加されているはずです。

## 自分でアプリケーションのソースコードを修正する

Upstreamでも他のOSでも対応がされていないのなら、自分で対応するしかありません。前述のエラーメッセージを見て分かる通りエラーメッセージには、重複して定義されたグローバル変数とその変数が定義されているソースファイル名が含まれています。なので、該当するすべてのソースコードを確認して、それらに含まれているグローバル変数の定義に、一つを残し後のすべての先頭に`extern`をつけて定義を宣言に変えてしまえば、ほとんどの場合ビルドが通るようになります。

## CFLAGSに-fno-commonを追加する

修正箇所が少ない場合には前述の方法で修正できますが、大規模なアプリケーションの場合には、修正箇所が膨大でとてもいちいち修正していられない場合もあるかもしれません。そのような場合にはコンパイル時に`-fcommon`オプションを指定すればClang 10以前の振る舞いになりますので、ビルドが通るようになるはずです。

# 最後に

Clang 11(及びGCC 10)における、`-fcommon`/`-fno-common`オプションに関するデフォルト値の変更に由来する、アプリケーションのビルドエラーの原因とその対応策について説明しました。パッケージメンテナの方で、Clang/GCCが11/10になっていきなり自分の管理しているパッケージがビルド出来なくなったという方は、この記事の事を思い出していただけると幸いです。