GCCの-ftrapv (3)
さて、インフルエンザで寝込んでいる最中ですがここぞとばかりに更新。
以前書いたGCCの-ftrapvをそろそろ実戦投入するかと思ったんですが、3つほど注意点を見つけたのでメモ。
問題1: 3.3.x以前でちゃんとうごかない
GCC 3.3.x 以前のlibgccの実装にはバグがあります(→ http://www.kb.cert.org/vuls/id/540517)。多分使い物になりません。-ftrapv は、GCC 3.4.x or 4.0.x 以降のなるべく新しいので利用されるのがよろしいかと。
問題2: false negative (加算のオーバーフローがチェックされないことがある)
int f(); int g(); があるとき、
int r, a, b; r = a + b; // ok r = a + f(); // ok
のオーバーフローはチェックされるんですけど、
r = f() + g(); // NG
はなぜかチェックのコードが入りません*1。__addvsi3関数の呼び出しが挿入されないということです。減算とか乗算だとチェックされるんですけどね。これ以外にもスルーされてしまうパターンがあるかもしれません。すでにバグとして登録されていますが、1年以上治っていません。→ http://gcc.gnu.org/bugzilla/show_bug.cgi?id=19020
でも、もともと符号ナシ整数がからむ演算は一切チェックされないわけだし、完全じゃなくてもまぁいいかなとは思います。
問題3: false positive (誤検知)
本当はオーバーフローしていないのに、オーバーフローが誤検出され、abortすることがあります。GCCの仰る事に納得がいかない場合は、オブジェクトを逆アセして読んだほうがいいです。
int x, y; ... x += (y - 48);
のようなコードで、xの元の値がINT_MAXで、yが48だったとしますよね。これは、x += 0; を行うことになるのでオーバーフローは発生しないはず。でも実際は、GCCは x = (x + y) - 48; に相当する*2、次のようなコードを吐くことがあって、この場合
movb (%eax), %al # *s, D.1674 movsbl %al,%eax # D.1674, D.1675 pushl -4(%ebp) # ret pushl %eax # D.1675 call __addvsi3 # leal -48(%eax), %eax #, tmp67 movl %eax, -4(%ebp) # tmp67, ret
最初のチェック付き加算のところでabortしちゃいます。いや、-ftrapvナシならこういう整数演算での順序入れ替えはもちろんアリ*3ですけど、-ftrapv時は (C99 の 5.1.2.3 Program Execution の Example 6 でいう) "On a machine in which overflows produce an explicit trap" にあたるわけですから、入れ替えるのはあんまりだと思うんですけど...どうなんですっけ。
GCCのMLとかbugzillaで話題になっているかしら。または、GCCのオプションで回避できたりするのかしら。適当にぐぐった限りでは見つかりません。誰か知っていたら教えてください。ちなみに、
int my_atoi(const char* s) { int ret = 0; while(*s != '\0') { ret *= 10; ret += (*s - '0'); ++s; } return ret; }
このような、オーバーフローチェックのない脆弱なatoiもどき関数を書いて-ftrapvが正しく動くか実験していたときに、INT_MAXに達していない my_atoi("2147483600"); でabortしてしまう挙動@ILP32環境 に出くわして気づきました。下記のように書き換えればabortしなくなります…。uum..
int tmp = *s - '0'; ret *= 10; ret += tmp;
まとめ?
GCCの-ftrapvを使うときの注意点は次の3つ: