変数にexternをつけるってどういうこと?

次のようなコードを書いてコンパイルしたときのお話.

/* src1.c */
#include <stdio.h>

int a;
int sub(int n) {
	printf(" sub::a: %d\n", a);
	printf(" sub::a: %p\n", &a);
	return n+1;
}

/* src2.c */
#include <stdio.h>

int a;
int main() {
	int x;
	x = sub(a);
	fprintf(stdout, "x: %d\n", x);
	printf("a: %p\n", &a);
	return 0;
}

まず,間違っていることに気づかず行っていたこと

グローバル変数は0で初期化されます.つまり,定義が同時に行われます.
extern記述子を使うことで,どこかで定義してあることを伝えられます.
つまり,externは同じグローバル変数を2回以上定義することをチェックする役割を持っているでしょうか.
# 実際は他のところで定義してあることの意思表示だけなのかもしれませんが

しかし,上のソースはコンパイルすることができ,実行も可能です.
では,aのアドレスは?


上で述べたように,グローバル変数は同時に初期化されるはずなので,
2つのソースにおけるaはそれぞれでメモリの割付が行われると思います.


しかし,表示されるaのアドレスは同じでした.
つまり,2回定義してしまっても,1回しか定義してないことになったわけです.


ということは、extern記述子は冗長な表現ってことになってしまうんでしょうか?
# externって他のソースファイルで定義してるから
# ここでは宣言のみですよってことだったと認識していたのですが.


と,ここまでのことを某掲示板で訊いてみたところ,

リンカがエラー出してない?
シンボルが重複してますよって


シンボルの重複はリンカの担当だから、ldの問題。
--allow-multiple-definitionあたりが渡ってないか?

最初,ldはgccから間接的に呼び出されていると認識していたので,ldのオプションはgccから渡せると勘違いしていました.
"--allow-multiple-definition"をgccのオプションとしてつけてみました。
するとこんなエラーが。

$ gcc --allow-multiple-definition src?.c
cc1: error: unrecognized command line option "-fallow-multiple-definition"
cc1: error: unrecognized command line option "-fallow-multiple-definition"

つけたオプションと違うオプションが認識されない、と出てきたのは謎ですが,
次は--fallow-multiple-definitionオプションをつけてコンパイルしてみました。
すると同じエラーメッセージがでてきました。

manやinfoコマンドでgccのオプションを調べてみると、上のオプションはgccには無いとのこと。
gcc経由では渡せないオプションだったのですね。


次にinfo ldで調べると,ldにはあるとのこと.そこで,

$ gcc -c src?.c

コンパイル直前まで終わらせて、出てきた中間ファイルをldでリンクしてみました.

$ ld --allow-multiple-definition src?.o
src1.o: In function `main':
src1.c:(.text+0x23): undefined reference to `stdout'
src1.c:(.text+0x3a): undefined reference to `fprintf'
src1.c:(.text+0x4e): undefined reference to `printf'
src2.o: In function `sub':
src2.c:(.text+0x17): undefined reference to `printf'
src2.c:(.text+0x2b): undefined reference to `printf'

どうやら,標準ライブラリ関数もリンクされてないようです.


この先のgccの詳細を-vオプションをつけて何をしているか調べてみました。
めちゃくちゃ長いので折り返しています.

$ gcc -v src?.o -o a.out 
Using built-in specs.
Target: i386-redhat-linux
コンフィグオプション: ../configure --prefix=/usr --mandir=/usr/share/man
 --infodir=/usr/share/info --enable-shared --enable-threads=posix
 --enable-checking=release --with-system-zlib --enable-__cxa_atexit
 --disable-libunwind-exceptions --enable-libgcj-multifile
 --enable-languages=c,c++,objc,obj-c++,java,fortran,ada --enable-java-awt=gtk
 --disable-dssi --with-java-home=/usr/lib/jvm/java-1.4.2-gcj-1.4.2.0/jre
 --with-cpu=generic --host=i386-redhat-linux
スレッドモデル: posix
gcc バージョン 4.1.1 20070105 (Red Hat 4.1.1-51)
 /usr/libexec/gcc/i386-redhat-linux/4.1.1/collect2 --eh-frame-hdr -m elf_i386
 -dynamic-linker /lib/ld-linux.so.2 -o a.out /usr/lib/gcc/i386-redhat-linux/4.1.1/../../../crt1.o
 /usr/lib/gcc/i386-redhat-linux/4.1.1/../../../crti.o /usr/lib/gcc/i386-redhat-linux/4.1.1/crtbegin.o
 -L/usr/lib/gcc/i386-redhat-linux/4.1.1 -L/usr/lib/gcc/i386-redhat-linux/4.1.1
 -L/usr/lib/gcc/i386-redhat-linux/4.1.1/../../.. src1.o src2.o -lgcc --as-needed
 -lgcc_s --no-as-needed -lc -lgcc --as-needed -lgcc_s --no-as-needed
 /usr/lib/gcc/i386-redhat-linux/4.1.1/crtend.o /usr/lib/gcc/i386-redhat-linux/4.1.1/../../../crtn.o

/usr/libexec/gcc/i386-redhat-linux/4.1.1/collect2というのはリンカです.
ldとは異なるようですが,次の結果により同じオプションが使えるみたいです.
どうやら--allow-multiple-definitionオプションは入っていないようです。


ldに対して,上の長いオプションをコピペして実行してみると,エラーメッセージなくリンクが済みました.
敢えて--allow-multiple-definitionをつけてリンクしてみましたが、
どちらにしても、グローバル変数aは同じアドレスのようです。

"--allow-multiple-definition"自体は多重定義を許すオプションなので,
デフォルトでフラグが立っていたってことなんでしょう.


すると,次のようなレスをいただきました.

-fno-common オプションをつけるといいらしい

試してみると,

$ gcc -fno-common src?.c
/tmp/ccuJF0uA.o:(.bss+0x0): multiple definition of `a'
/tmp/ccU9yxgD.o:(.bss+0x0): first defined here

晴れて求めていたエラーメッセージが出てきました.
ただし,これはgccのオプションです.
ldのオプションでは何かな,,,と調べてみると,どうやら"--warn-common"オプションがそのようでした.
上の長いコマンドにこのオプションをつけて実行してみると,

$ ld --eh-frame-hdr -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 /usr/lib/crt1.o
 /usr/lib/crti.o /usr/lib/gcc/i386-redhat-linux/4.1.1/crtbegin.o
 -L/usr/lib/gcc/i386-redhat-linux/4.1.1 -L/usr/lib/gcc/i386-redhat-linux/4.1.1
 -L/usr/lib/gcc/i386-redhat-linux/4.1.1/../../.. src1.o src2.o -lgcc --as-needed
 -lgcc_s --no-as-needed -lc -lgcc --as-needed -lgcc_s --no-as-needed
 /usr/lib/gcc/i386-redhat-linux/4.1.1/crtend.o
 /usr/lib/gcc/i386-redhat-linux/4.1.1/../../../crtn.o -warn-common
src2.o: warning: multiple common of `a'
src1.o: warning: previous common is here

でましたね.

しかし,実は間違った認識をしていたことに気づく

bssという言葉を見かけました.
そういえば,以前エキスパートCプログラミング―知られざるCの深層 (Ascii books)
"グローバル変数bss領域に確保されるため実行時に0で初期化される"
という旨の文章を読んだことがある...


ここで,ソースを次のように書き換えてみました.

#include <stdio.h> 

int a=0;
int main() {
        int x;
        x = sub(a);
        fprintf(stdout, "x: %d\n", x);
        printf("a: %p\n", &a);
        return 0;
}

つまり,src1.cのグローバル変数だけ明示的に定義してみました.
もちろんこれは実行でき,最初に述べたような実行結果になります.
ただ,src2.cも明示的に定義すると変わってきます.

#include <stdio.h>

int a=4;
int sub(int n) {
        printf(" sub::a: %d\n", a);
        printf(" sub::a: %p\n", &a);
        return n+1;
}

どっちのaが優先されるか調べるために定義の値を変えてみました.
コンパイル結果.

$ gcc src?.c
/tmp/cc4a7Yre.o:(.data+0x0): multiple definition of `a'
/tmp/ccwrP69I.o:(.bss+0x0): first defined here
collect2: ld はステータス 1 で終了しました

なるほど.
つまり,

グローバル変数は宣言と同時に定義が行われる

というのは勘違いだったわけですね.


最後に,nmを使ってシンボルのリストを取得してみます.
まず,記事冒頭のソースです.
グローバル変数を多重宣言し,明示的な初期化を行っていないもの

$ nm a.out 

       ~snip~

080482c4 T _init
08048330 T _start
08049658 B a
08048354 t call_gmon_start
08049654 b completed.5757
08049648 W data_start
         U fprintf@@GLIBC_2.0
080483b0 t frame_dummy
080483d4 T main
0804964c d p.5755
         U printf@@GLIBC_2.0
08049650 B stdout@@GLIBC_2.0
08048434 T sub

そして,片方を宣言のみ行い,もう片方にexternを用いたオブジェクトファイルです.

$ nm a.out 

       ~snip~

080482c4 T _init
08048330 T _start
08049650 D a
08048354 t call_gmon_start
08049658 b completed.5757
08049648 W data_start
         U fprintf@@GLIBC_2.0
080483b0 t frame_dummy
080483d4 T main
0804964c d p.5755
         U printf@@GLIBC_2.0
08049654 B stdout@@GLIBC_2.0
08048434 T sub

変数aがBからDになっています.
これは.bss領域から.data領域に移ったことを示します.
http://www.bookshelf.jp/texi/binutils/binutils-ja_2.html

結論

グローバル変数は明示的な初期化がされないと,
デフォルトでは多重宣言が許されてしまいます.


つまり次のようなコードが許されてしまいます.

/* src1.c */
#include <stdio.h>

int a;
int main() {
        int x;
        a = 5;
        x = sub(&a);
        fprintf(stdout, "x: %d\n", x);
        printf("a: %p\n", &a);
        return 0;
}

/* src2.c */
#include <stdio.h>

int a;
int sub(int *n) {
        a += 10;
        printf(" sub::a: %d\n", a);
        printf(" sub::a: %p\n", &a);
        return *n+a;
}

わざと,ポインタを渡して書き換えられてしまうようにしました.


src1.cのコーダーはsub()が10を足してくれることを期待して,15を待ってます.
src2.cでは,自らが宣言したaが0で初期化されることを期待して,
渡された変数に10を足して返しています.
しかし実際は30が返ってきてしまいます.


上のようなソースは意味のあるものではありませんが,それでも
2人で意思疎通を行わず,同じ名前のグローバル変数が宣言を知らず,
"初期化せずに"それぞれで使用してしまうと,相手先で変更されてしまいます.怖いですね.


通常は,部署などでは初期化するよう義務付けられているのでしょう.
でも,間違ってしまうことはあるでしょうし,
gccの--fno-commonオプションのフラグが立っているべきだと思うのですが...


ああ,でもグローバル変数はそもそも"定義されていない"のだから,
使い方が間違っている,ってことになるのか.