2024年07月04日

SoftFloatの未定義動作バグ(1)signedのunsignedな絶対値を求める際にINT_MIN

NetBSD の libc は FPU を持たないターゲット向けの fp(浮動小数点数)ソフトウェアエミュレーションに John R. Hauser 氏が開発している Berkeley SoftFloat のバージョン 2a を使用しています。
http://www.jhauser.us/arithmetic/SoftFloat.html

弊社のコンパイラツールチェーン製品 exeClang は NetBSD の libc を使用しているのですが、LLVM 18 版の開発中に RISC-V の RV64 IMA(noFPU)ターゲットをいつものように商用コンパイラテストスイートにかけると、新規 fail が 1 つ発生しました。

その原因を追究して行く過程で、この SoftFloat の未定義動作(Undefined Behavior; UB)を 2 つ発見したので紹介します。今回はそのうち 1 つ目です。



一つ一つ原因を切り分けて行った結果、SoftFloat の本体 bits64/softfloat.c を Clang 18 で -O2 でビルドすると fail が再現、最適化を切る(-O0)と pass することがわかりました。O0 以外の最適化オプションは全て同じ結果でした。

次に 2a には既知のバグがあり、64-bit ターゲットで問題があるとウェブサイトに記載があったので、2c にアップデートしました。しかしここらへんの diff はわずかですし、NetBSD 側でも独自の修正が加わっていて、あまり問題は無さそうでした。実際変わりませんでした。

ただし、2c にアップデートしたおかげで、SoftFloat-2c のテストスイート TestFloat-2c を実行可能になりました。(2a はもうダウンロードできませんでした。)

TestFloat を実行して内容を精査すると(いろいろありましたが割愛)以下の関数に問題があることがわかりました。
float64 int32_to_float64( int32 a )
{
    flag zSign;
    uint32 absA;
    int8 shiftCount;
    bits64 zSig;

    if ( a == 0 ) return 0;
    zSign = ( a < 0 );
    absA = zSign ? - a : a;
    shiftCount = countLeadingZeros32( absA ) + 21;
    zSig = absA;
    return packFloat64( zSign, 0x432 - shiftCount, zSig<<shiftCount );

}
TestFloat では 0x80000000 つまり int32_t の最小値(INT_MIN)-2147483648 が渡された時に fail していました。実は問題は以下の絶対値を求めるコードでした。
    absA = zSign ? - a : a;
実は a が INT_MIN の時に - a のように単項マイナス演算子を適用すると INT_MAX(2147483647)を超えてしまい未定義動作となるのです。例えば C 標準ライブラリ stdlib.h の絶対値を求めるはずの int abs(int x) は、多くの実装では INT_MIN を渡すとマイナスの値が返ってきます。(以下は手元の MSYS2 UCRT64 gcc の結果です。)
$ cat abs.c
#include <stdlib.h>
#include <stdio.h>

int main() {
  fprintf(stderr, "abs(INT_MIN)=%d\n", abs(INT_MIN));
  return 0;
}
$ gcc -Wall -Wextra abs.c
$ ./a.exe
abs(INT_MIN)=-2147483648
最終的に、問題のコードを以下のように書き直して pass しました。
    absA = a;
    if (zSign) {
      absA = - (a + 1) + 1U;
    }
(参考:https://qiita.com/fujitanozomu/items/52ad9ae4dc038f231bad
StackOverflow などを見ると
(参考:https://stackoverflow.com/questions/12231560/correct-way-to-take-absolute-value-of-int-min
    absA = zSign ? - (uint32)a : a;
のようにキャストを入れるだけでも良さそうでしたが、SoftFloat のように異なる型で同じようなコードが続く場合は、コピペした時に型をミスして非常にわかりにくいバグが発生してしまうリスクが嫌だったので、コピペがしやすい上記の形を選択しました。

さて、これで TestFloat-2c は pass(※)したわけですが、肝心の商用コンパイラテストスイートの方の fail は直りませんでした。次回(2)に続きます。

※ 正確には比較対象をちゃんと実装せず、C 言語で実装された付属の systfloat.c で代用したため、丸め方式が C 言語と合わないテストは fail したままです。(NetBSD の実装では libgcc/compiler-rt 関数の多くは round to zero 固定なので、他の丸め方式のテストは fail します。)そのため G ターゲット(FPU あり)でもテストと行い、SoftFloat と FPU の結果を比較して全て pass することを確認しました。

kmckk at 20:10コメント(0)Clang | 若槻 

コメントする

名前
 
  絵文字
 
 
記事検索
最新コメント
アクセスカウンター
  • 今日:
  • 昨日:
  • 累計:

QRコード
QRコード