仙石浩明の日記

2009年10月8日

__sync_bool_compare_and_swap_4 とは何か? ~ glibc をビルドする場合は、 gcc の –with-arch=i686 configure オプションを使ってはいけない

glibc-2.10.1 をビルドしようとしたら、 「__sync_bool_compare_and_swap_4 が定義されていない」 というエラーが出た:

senri:/usr/local/src/glibc-2.10.1.i386 % ../glibc-2.10.1/configure
        ...
senri:/usr/local/src/glibc-2.10.1.i386 % make
        ...
/usr/local/src/glibc-2.10.1.i386/libc_pic.os: In function `__libc_fork':
/usr/local/src/glibc-2.10.1/posix/../nptl/sysdeps/unix/sysv/linux/i386/../fork.c:79: undefined reference to `__sync_bool_compare_and_swap_4'
/usr/local/src/glibc-2.10.1.i386/libc_pic.os: In function `__nscd_drop_map_ref':
/usr/local/src/glibc-2.10.1/nscd/nscd-client.h:320: undefined reference to `__sync_fetch_and_add_4'
        ...
/usr/local/src/glibc-2.10.1.i386/libc_pic.os: In function `*__GI___libc_freeres':
/usr/local/src/glibc-2.10.1/malloc/set-freeres.c:39: undefined reference to `__sync_bool_compare_and_swap_4'
collect2: ld returned 1 exit status
make[1]: *** [/usr/local/src/glibc-2.10.1.i386/libc.so] Error 1
make[1]: Leaving directory `/usr/local/src/glibc-2.10.1'
make: *** [all] Error 2

__sync_bool_compare_and_swap_4 は gcc の組み込み関数なので、 関数が未定義であることを示す 「undefined reference to ...」 というエラーメッセージは、 誤解を招く不親切なメッセージだと思う。

__sync_bool_compare_and_swap_4(mem, oldval, newval) は、 mem が指し示すメモリの値 (4バイト分) が oldval であれば newval に変更する、 という操作をアトミックに行なう組み込み関数。 アトミック (不可分) 操作とは、 操作の途中が存在してはいけない操作のことで、 この例なら比較 (メモリの値が oldval か?) と代入 (newval に変更) が必ず 「いっぺん」 に行なわれ、 「比較だけ行なったけどまだ代入は行なわれていない」 という状態が存在しないことを意味する。

アトミックに行なうためには、 当然ながら CPU でその操作をサポートしている必要がある (複数個の命令の列で実現しようとすると、 命令列の半ばを実行中の状態が必ず存在してしまう) わけだが、 残念ながら Intel 386 プロセッサでは、 この compare_and_swap (CMPXCHG 命令) をサポートしておらず、 サポートするのは Intel 486 以降の CPU である。 テストプログラムを書いて試してみる:

#include <stdio.h>

int main() {
    int mem[1], oldval, newval;
    oldval=0;
    newval=1;
    mem[0] = 0;
    __sync_bool_compare_and_swap(mem, oldval, newval);
    printf("mem[0]=%d\n", mem[0]);
    return 0;
}

見ての通り、 mem[0] の値を oldval の値 (0) と比較し、 一致していたら newval の値 (1) を代入し、 mem[0] の値を表示するだけのプログラムである。

関数名が 「__sync_bool_compare_and_swap」 であって、 後ろに 「_4」 がついていないことに注意。 gcc が引数の型 (この例では int) を見て、 その型のビット長を後ろにつけてくれる (この例では int 型は 4 バイトなので 「_4」 をつけてくれる)。

gcc では 「-march=タイプ」 オプションを指定することによって CPU タイプを指定できる。 -march オプションを指定しなかったり (この場合は全 CPU でサポートされる組み込み関数のみ利用できる)、 あるいは -march=i386 を指定したりすると、 コンパイル時にエラーになる:

% gcc -Wall test.c
/tmp/cc4eNX6L.o: In function `main':
test.c:(.text+0x3b): undefined reference to `__sync_bool_compare_and_swap_4'
collect2: ld returned 1 exit status
% gcc -Wall -march=i386 test.c
/tmp/cc6chtFj.o: In function `main':
test.c:(.text+0x36): undefined reference to `__sync_bool_compare_and_swap_4'
collect2: ld returned 1 exit status
% gcc -Wall -march=i486 test.c
% ./a.out
mem[0]=1

いまさら i486 というのもアレなので、 今なら i686 を指定するのがよさげ。 私の手元にはいまだ PentiumIII マシンがあるものの、 PentiumIII より古いマシンはない (昨年 ML115 と SC440 を買ったとき PentiumII マシンを引退させた) ので、 pentium3 を指定すれば SSE (Streaming SIMD Extensions) が利用できるようになるが、 glibc をビルドするときに必要かというと、 たぶん必要ない。

というわけでエラーの原因は分かったが、 では glibc をビルドするときは、 どうすればいいだろうか?

とりあえず google で検索してみたら、 gcc の configure オプションに 「--with-arch=i686」 を指定して gcc をビルドする必要がある、 と書いてあるページが見つかった。

--with-arch オプションは、 -march のデフォルトを設定するための configure オプションである。 つまり 「--with-arch=i686」 を指定して gcc を再インストールすると、 gcc に -march オプションをつけなくてもデフォルトが i686 になる。 なるほど確かにそうすれば、 glibc 側で何も変更せずに __sync_bool_compare_and_swap_4 関数が使えるようになりそうである。

いまどき i686 以前の CPU 用のコードが必要になりそうなケースは滅多にないだろうから、 -march オプションのデフォルトを i686 にするのも悪い選択ではないように思えた。 gcc をビルドし直すのは面倒だなーと思いつつも、 ついでに gcc のバージョンを上げておこうと gcc-4.3.4 をダウンロードしてきて 「--with-arch=i686」 付でビルドしてみた。

ところが!

「--with-arch=i686」付でビルドした gcc を使って glibc をビルドしようとすると、 先ほどコンパイラエラーが出た場所より前の段階で、 アセンブラがエラーを出力して make が止まってしまった:

senri:/usr/local/src/glibc-2.10.1.i386 % make
        ...
../sysdeps/i386/fpu/s_frexp.S: Assembler messages:
../sysdeps/i386/fpu/s_frexp.S:66: Error: invalid identifier for ".ifdef"
../sysdeps/i386/fpu/s_frexp.S:66: Error: junk at end of line, first unrecognized character is `1'
        ...
../sysdeps/i386/fpu/s_frexp.S:66: Error: ".endif" without ".if"
../sysdeps/i386/fpu/s_frexp.S:66: Error: junk `.get_pc_thunk.dx' after expression
make[2]: *** [/usr/local/src/glibc-2.10.1.i386/math/s_frexp.os] Error 1
make[2]: Leaving directory `/usr/local/src/glibc-2.10.1/math'
make[1]: *** [math/subdir_lib] Error 2
make[1]: Leaving directory `/usr/local/src/glibc-2.10.1'
make: *** [all] Error 2

こちら (コンパイラ側) を立てれば、あちら (アセンブラ側) が立たず、 な二律背反状態。

「invalid identifier for ".ifdef"」 というエラーメッセージが (ぱっと見には) 意味不明である。 続いて 「junk at end of line, first unrecognized character is `1'」 と言っているから、 「.ifdef 1」 みたいなコードなのだろうか? エラーが起きた sysdeps/i386/fpu/s_frexp.S の 66行目あたりは以下のようになっている。 LOAD_PIC_REG の行が問題の 66行目:

        cmpl        $0x00100000, %eax
        jae        2f

        fldl        VAL0(%esp)
#ifdef        PIC
        LOAD_PIC_REG (dx)
#endif
        fmull        MO(two54)
        movl        $-54, %ecx
        fstpl        VAL0(%esp)
        fwait
        movl        VAL1(%esp), %eax
        movl        %eax, %edx
        andl        $0x7fffffff, %eax

おそらく LOAD_PIC_REG マクロを展開した結果がおかしいのだろうと、 LOAD_PIC_REG マクロの定義 (sysdeps/i386/sysdep.h) を調べてみる:

# define SETUP_PIC_REG(reg) \
  .ifndef __i686.get_pc_thunk.reg;                                              \
  .section .gnu.linkonce.t.__i686.get_pc_thunk.reg,"ax",@progbits;              \
  .globl __i686.get_pc_thunk.reg;                                              \
  .hidden __i686.get_pc_thunk.reg;                                              \
  .type __i686.get_pc_thunk.reg,@function;                                      \
__i686.get_pc_thunk.reg:                                                      \
  movl (%esp), %e##reg;                                                              \
  ret;                                                                              \
  .size __i686.get_pc_thunk.reg, . - __i686.get_pc_thunk.reg;                      \
  .previous;                                                                      \
  .endif;                                                                      \
  call __i686.get_pc_thunk.reg

# define LOAD_PIC_REG(reg) \
  SETUP_PIC_REG(reg); addl $_GLOBAL_OFFSET_TABLE_, %e##reg

プリプロセッサの出力を確認してみると、 66行目は次のようにマクロ展開されている:

 .ifndef 1 .get_pc_thunk.dx; .section .gnu.linkonce.t. 1 .get_pc_thunk.dx,"ax",@progbits; .globl 1 .get_pc_thunk.dx; .hidden 1 .get_pc_thunk.dx; .type 1 .get_pc_thunk.dx,@function; 1 .get_pc_thunk.dx: movl (%esp), %edx; ret; .size 1 .get_pc_thunk.dx, . - 1 .get_pc_thunk.dx; .previous; .endif; call 1 .get_pc_thunk.dx; addl $_GLOBAL_OFFSET_TABLE_, %edx

確かに、いきなり 「.ifndef 1」 となっているので、 エラーになるのは明らか。 上記 SETUP_PIC_REG マクロ定義内の 「__i686」 がことごとく 「1」 に展開されていて、 意味不明なコードになってしまっている。

というわけで、 原因に思い当たった。 プリプロセッサで自動的に定義されるマクロを表示させてみる:

% gcc -E -dM -x c /dev/null
#define __DBL_MIN_EXP__ (-1021)
#define __pentiumpro__ 1
#define __FLT_MIN__ 1.17549435e-38F
#define __DEC64_DEN__ 0.000000000000001E-383DD
#define __CHAR_BIT__ 8
        ...
#define __i686 1
        ...
#define __i686__ 1
        ...

「#define __i686 1」 だから当然そういうマクロ展開が行なわれる、 というわけ。 gcc と glibc では違うソフトウェアであるとはいえ、 gcc のプリプロセッサで自動的に定義される 「#define __i686 1」 とバッティングするようなラベル 「__i686.get_pc_thunk.reg」 をマクロ中で使うというのは、 いかがなものか。> glibc

「--with-arch=i686」付でビルドした gcc, あるいは 「-march=i686」 オプション付で実行した gcc は、 -march を指定しない gcc と比べて、 プリプロセッサにおいて以下の定義が追加されるようだ:

#define __GCC_HAVE_SYNC_COMPARE_AND_SWAP_1 1
#define __GCC_HAVE_SYNC_COMPARE_AND_SWAP_2 1
#define __GCC_HAVE_SYNC_COMPARE_AND_SWAP_4 1
#define __GCC_HAVE_SYNC_COMPARE_AND_SWAP_8 1
#define __i686 1
#define __i686__ 1
#define __pentiumpro 1
#define __pentiumpro__ 1

ちなみに、 SETUP_PIC_REG マクロ定義内で定義されている __i686.get_pc_thunk.reg は、 eip の値をレジスタ reg へ代入するための関数 (というかサブルーチン)。 サブルーチン名の末尾の 「reg」 はマクロ引数で置換される。 この例では dx。 つまり SETUP_PIC_REG マクロを展開することにより __i686.get_pc_thunk.dx というサブルーチンが定義される。

そして、 このサブルーチンを呼び出す (call __i686.get_pc_thunk.dx) と、 スタックトップをレジスタへ代入 (movl (%esp), %edx) して復帰 (ret) する。 つまりプログラムカウンタ (スタックトップに積まれた戻りアドレス) の値がレジスタ edx へ代入される、というわけ。

続いて、 edx に $_GLOBAL_OFFSET_TABLE_ を加算することにより edx にグローバル変数領域へのポインタが代入される (addl $_GLOBAL_OFFSET_TABLE_, %edx)。

__i686.get_pc_thunk.bx は、 gccが用意してくれる関数で、 PC (プログラムカウンタ、EIP レジスタの値) を ebx にコピーします。
 (中略)
なんでここで PC が必要かというと、 グローバル変数にアクセスしたいからです。 PIC/PIE の場合、 実行時に自分自身 (ELF バイナリ) がどの仮想アドレスに mmap されるかはわかりませんので、 アドレス決め打ちで変数にアクセスはできません。 でも、 いま実行している命令 (61d:) から変数までのオフセットは *.o をリンクして so にした時点で判明します (so ファイルのナカミがほぼそのままメモリに貼られるわけなんで)。
 (中略)
mov 0xほげ(%eip) ふが; とか出来れば一番いいんですが(x86_64はできる)、 x86はそういうPC相対のアドレス指定はできません。 というか、eipを直接的に得ることすら出来ません。 なんで、仕方なく一度call命令を発行して、 リターンアドレス(callを発行した命令の次の命令のアドレス) をスタック上に自動push させて、 __i686.get_pc_thunk.bx内でebxレジスタにpopしてます。 x86だと、PIC/PIEなコードでグローバル変数を触るだけで、 関数呼び出しと同じようなコストがかかるんざますよ!奥様。

glibc でどんなグローバル変数にアクセスしているのかと思って、 ちょっと調べてみた:

この sysdeps/i386/fpu/s_frexp.S は、 そのファイル名の通り frexp(3) のアセンブリ言語による実装。 C による実装は sysdeps/ieee754/dbl-64/s_frexp.c にある。 ディレクトリ名から分かるように、 IEEE 754 (IEEE 浮動小数点数演算標準) に基づいている。

double frexp(double x, int *exp) は、 浮動小数点実数 x を正規化小数と指数に分解し、 指数を *exp に格納した上で正規化小数を返す。 浮動小数点実数 x が非正規化数であるとき frexp は x に 2^54 を乗じた上で *exp に -54 を返すのだが、 この 2^54 = 18014398509481984 という定数を double 型で保持するために、 グローバル変数 two54 を使用している。

話が脱線したので元に戻すと、 要は 「--with-arch=i686」 付でビルドした gcc だと、 C のソースだろうとアセンブリ言語で書かれたソースだろうと見境なく 「#define __i686 1」 を定義してしまうので、 相手が C のソースの時のみ 「-march=i686」 を付けて gcc を実行すればよい。 つまり、 gcc は 「--with-arch=i686」 付でビルドしてはいけない。

ではどうすればいいか? いろいろ方法はあると思うが、 glibc をビルドするときに CFLAGS を 「CFLAGS="-g -O2 -march=i686"」 などと指定して configure を実行するのも一つの解決策。 CFLAGS は C のソースをコンパイルするときだけ使われて、 アセンブルするときには使われないので、 前述したようなコンパイラ/アセンブラ二律背反問題を回避できる。 というわけで、 無事 glibc 2.10.1 をビルドすることができた:

senri:/usr/local/src/glibc-2.10.1.i386 % ../glibc-2.10.1/configure CFLAGS="-g -O2 -march=i686"
        ...
senri:/usr/local/src/glibc-2.10.1.i386 % make
        ...
Filed under: システム構築・運用,プログラミングと開発環境 — hiroaki_sengoku @ 09:39

1 Comment »

  1. [FreeBSD]Linux互換用クロス開発環境構築

    FreeBSD上のLinux互換環境で動かすバイナリをビルドするため、クロス開発環境を構築しました。手順は以下の通りです。gccとglibcが相互依存しているため行ったり来たりします。途中でエラーを無視して進めますが、最終的にはすべてなくします。 binutils-2.20 gcc-4.4.2 (1

    Comment by 七誌の開発日記 — 2009年11月16日 @ 10:39

RSS feed for comments on this post.

Leave a comment