初めに
協定世界時2020年8月16日19時14分04秒のリビジョン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
に変わりました。その結果、昔から存在しているアプリケーションを中心にビルドエラーが発生しているようです。
具体例
でもってこれだけだと私自身が今一つピンとこなかったので、これまた検索した結果を参考にして、以下のような簡単なサンプルプログラムを書いてみました。
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}
#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;
}
int foo;
int
get(void)
{
return foo;
}
char foo;
char
get_char(void)
{
return foo;
}
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では問題なくビルド出来て生成されたバイナリもそれっぽく動いてしまいます。
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では以下のようにリンク時のエラーが発生します。
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
というportのビルドエラーに対応しましたが、そのために必要なパッチはここからダウンロードすることができます。各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になっていきなり自分の管理しているパッケージがビルド出来なくなったという方は、この記事の事を思い出していただけると幸いです。