EmptyRoom

Diary

twitterもはじめました

[384]2008/12/2(Tue) [tag: プログラミング ]
IA-32のlibc.so.6のprintfはfloatの表示を実装してない?

研究で独自アーキテクチャ用のアセンブリ言語・アセンブラを作ってそのテスト中に気づいた…。

まず,普通にCでfloatの値を表示するプログラムを書いてみる。 とりあえず,このファイルをfloat.cとしておく。

#include <stdio.h>

int main(int argc, char *argv[])
{
    float val = 3.14159;
    printf("Value: %f\n", val);

    return 0;
}

そしてこれをコンパイルして実行すると

Value: 3.141590

と出る。これは期待した動作をしている。 さて,ここでgcc -S float.cとしてアセンブリで出力してみると

    .file "float.c"
    .section .rodata
.LC1:
    .string "Value: %f\n"
    .text
.globl main
    .type main, @function
main:
    leal    4(%esp), %ecx
    andl    $-16, %esp
    pushl    -4(%ecx)
    pushl   %ebp
    movl    %esp, %ebp
    pushl   %ecx
    subl    $36, %esp
    movl    $0x40490fd0, %eax  #注: この16進数即値はfloatの3.14159
    movl    %eax, -8(%ebp)
    flds    -8(%ebp)
    fstpl   4(%esp)

    movl    $.LC1, (%esp)
    call    printf
    movl    $0, %eax
    addl    $36, %esp
    popl    %ecx
    popl    %ebp
    leal    -4(%ecx), %esp
    ret
    .size   main, .-main
    .ident  "GCC: (Ubuntu 4.3.2-1ubuntu11) 4.3.2"
    .section    .note.GNU-stack,"",@progbits

注目して欲しいのは太字の部分。 下に抜き出したものと,それぞれの命令の動作について書いてみる。

movl    $0x40490fd0, %eax  # 3.14159 を%eaxレジスタに移動
movl    %eax, -8(%ebp)     # 3.14159 を%ebp-8の指すメモリに移動
flds    -8(%ebp)           # 浮動小数点レジスタに%ebp-8の指すメモリから移動
fstpl    4(%esp)           # 浮動小数点レジスタの内容を %esp+4の指すメモリに移動(スタックに積む)

なぜいちいち浮動小数点レジスタを介すのか? 欲しい値はすでに一つ目の命令で%eaxに入っているはずで, それをそのままスタックに積めば目的の動作を実現することができるはず。 なので,浮動小数点レジスタを介さずにプログラムを書き直してみると

movl    $0x40490fd0, %eax  # 3.14159 を%eaxレジスタに移動
movl    %eax, 4(%esp)     # 3.14159 をスタックに積む

このように書ける。 ここでこのプログラムをgcc float.sでコンパイルして実行してみると

Value: -0.103642

こんなワケのわからん値が出てくる。しかもこの値,実行するたびに変わりる。 ちなみに,

movl    $0, %eax
movl    %eax, -8(%esp)

のようなコードを挟んでやると,プログラムの出力は常にValue: 0.00000になる。

なぜこのような現象が起こるのか?

実は"Value %f"は32bitの浮動小数点(float)を表示するのではなく, 64bitの浮動小数点(double)を表示するものとして実装されているっぽい(少なくともIA-32では)。 なので一度浮動小数点レジスタを介してfloatからdoubleに変換していたというワケ。 じゃあfloatをそのまま表示することはできないのか?はたぶんYes。

なぜこんな実装になっているのかはよくわからないんだけども, IA-32で浮動小数点を扱う場合,(MMXやSSEを使わなければ)必ずx87由来の80bitの精度を持った浮動小数点レジスタを使うことになる。 float同士の演算であっても,メモリへの結果の格納を除いてすべて80bitの精度で計算されるし,

float val2 = val * 2.71;

とやったとき,GCCはデフォルトで2.71はdouble型として定義する。 ここらへんの開発者はIA-32ではfloatを使うよりdoubleを使う方が自然だと判断している? もしかすると,x87は4バイトのデータを扱うのが苦手なのか? 決定的な理由はよくわからないけど,そうなっているのである…うーん

ということで,よくC言語入門テキストに書いてある

floatの値をpritfで使うには%f,doubleは%lfを使いましょう。

なんて文言は嘘なので信じないように! 俺はこの文言を信じて自分の作っている仮想CPUにバグがあるんじゃないかと数時間疑い,その時間を無駄にしたのだから! (少なくともIA-32, libc.so.6では)

# こっそり追記
# floatに %f, doubleに %lf を使うというのは,
# 他のアーキテクチャへの移植という観点から正しいかもしれない
# ただ,今回のような例外もあることを書いておかないと
# アセンブリから直接libcを叩くときに変な罠にハマってしまう
# そういうところは注意が必要


名前
コメント

Cookieに名前を保存して,次回から名前の入力を省略する
パスワード