変数に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オプションのフラグが立っているべきだと思うのですが...
ああ,でもグローバル変数はそもそも"定義されていない"のだから,
使い方が間違っている,ってことになるのか.
reference
http://pc11.2ch.net/test/read.cgi/tech/1206196600/637-665
http://www.kouno.jp/home/c_faq/c10.html#6
http://www.kouno.jp/home/c_faq/c1.html#7
http://rina.jpn.ph/~rance/c_language/p11.html
google:gcc "-Wall"
http://www.sra.co.jp/wingnut/ld/ld-ja_2.html
http://www.unixuser.org/~euske/doc/gccopts/index.html
info gcc / man gcc / info ld / man ld
http://www.oklab.org/language_c/gcc_manual.html
http://dbkun.cs.shinshu-u.ac.jp/cai/c2/text/e_10-01-02.html
http://dbkun.cs.shinshu-u.ac.jp/cai/c2/text/e_10-05-01.html
google:crtn.o
http://www.globe.to/~oka326/archive/elf_doc_sgml_ja/elf_doc-3.html
http://www.bookshelf.jp/texi/binutils/binutils-ja_2.html