C / C ++での署名付きオーバーフローの検出


82

一見すると、この質問は整数オーバーフローを検出する方法の複製のように見えるかもしれません。ただし、実際には大幅に異なります。

符号なし整数オーバーフローの検出は非常に簡単ですが、C / C ++での符号付きオーバーフローの検出は実際にはほとんどの人が考えるよりも難しいことがわかりました。

それを行う最も明白でありながら素朴な方法は、次のようになります。

int add(int lhs, int rhs)
{
 int sum = lhs + rhs;
 if ((lhs >= 0 && sum < rhs) || (lhs < 0 && sum > rhs)) {
  /* an overflow has occurred */
  abort();
 }
 return sum; 
}

これに伴う問題は、C標準によれば、符号付き整数オーバーフローは 未定義の動作であるということです。 言い換えると、標準によれば、符号付きオーバーフローが発生するとすぐに、プログラムはnullポインターを逆参照した場合と同じように無効になります。したがって、上記の事後条件チェックの例のように、未定義の動作を引き起こして、事後にオーバーフローを検出しようとすることはできません。

上記のチェックは多くのコンパイラで機能する可能性がありますが、信頼することはできません。実際、C標準では符号付き整数オーバーフローは未定義であるとされているため、一部のコンパイラ(GCCなど)は上記のチェック最適化します。、最適化フラグが設定されると、符号付きオーバーフローは不可能であると想定するため、を最適化します。これにより、オーバーフローをチェックする試みが完全に中断されます。

したがって、オーバーフローをチェックする別の可能な方法は次のとおりです。

int add(int lhs, int rhs)
{
 if (lhs >= 0 && rhs >= 0) {
  if (INT_MAX - lhs <= rhs) {
   /* overflow has occurred */
   abort();
  }
 }
 else if (lhs < 0 && rhs < 0) {
  if (lhs <= INT_MIN - rhs) {
   /* overflow has occurred */
   abort();
  }
 }

 return lhs + rhs;
}

このような加算を実行してもオーバーフローが発生しないことを事前に確認するまで、実際には2つの整数を加算しないため、これはより有望に思えます。したがって、未定義の動作を引き起こすことはありません。

ただし、このソリューションは、加算操作が機能するかどうかをテストするためだけに減算操作を実行する必要があるため、残念ながら最初のソリューションよりもはるかに効率が低くなります。そして、この(小さな)パフォーマンスの低下を気にしなくても、このソリューションが適切であると完全に確信しているわけではありません。表現lhs <= INT_MIN - rhsは、符号付きオーバーフローは不可能であると考えて、コンパイラーが最適化するようなとまったく同じように見えます。

それで、ここにもっと良い解決策はありますか?1)未定義の動作を引き起こさないこと、および2)オーバーフローチェックを最適化する機会をコンパイラに提供しないことが保証されているものはありますか?両方のオペランドをunsignedにキャストし、独自の2の補数演算をロールしてチェックを実行することで、それを行う方法があるのではないかと考えていましたが、その方法がよくわかりません。


1
検出しようとするよりも、オーバーフローの可能性のないコードを書くほうがいいのではないでしょうか。
アルン

9
@ArunSaha:計算をしてオーバーフローしないようにするのは本当に難しいですし、一般的なケースで証明することは不可能です。通常の方法は、可能な限り幅の広い整数型と希望を使用することです。
David Thornley 2010年

6
@Amardeep:nullポインターの逆参照は、符号付きオーバーフローと同様に未定義です。未定義の振る舞いは、標準に関する限り、何でも起こり得ることを意味します。署名されたオーバーフローの後、システムが無効で不安定な状態にならないことを想定することはできません。OPは、これの1つの結果を指摘しました。オプティマイザーが、署名されたオーバーフローが発生すると検出するコードを削除することは完全に合法です。
David Thornley 2010年

16
@Amardeep:そのような実装について触れました。最適化フラグが設定されると、GCCはオーバーフローチェックコードを削除します。だからそれは基本的にあなたのプログラムを壊します。これは、nullポインターの逆参照よりも間違いなく悪いです。これは、微妙なセキュリティ上の欠陥が発生する可能性があるためです。一方、nullを逆参照すると、プログラムがセグメンテーション違反でぶっきらぼうになります。
channel72 2010年

2
@Amardeep:コンパイラの設定によっては、オーバーフローによってトラップが発生する実装のようです。特定の符号なし変数または数量を(1)クリーンにラップするか、(2)障害を起こすか、または(3)便利なことを行うかを言語で指定できると便利です。変数がマシンのレジスタサイズよりも小さい場合、符号なしの量をクリーンにラップする必要があると、最適なコードの生成が妨げられる可能性があることに注意してください。
スーパーキャット2010年

回答:


26

減算によるアプローチは正しく、明確に定義されています。コンパイラはそれを最適化することはできません。

より大きな整数型を使用できる場合の別の正しいアプローチは、より大きな型で算術演算を実行し、変換するときに結果がより小さな型に適合することを確認することです。

int sum(int a, int b)
{
    long long c;
    assert(LLONG_MAX>INT_MAX);
    c = (long long)a + b;
    if (c < INT_MIN || c > INT_MAX) abort();
    return c;
}

優れたコンパイラは、加算とifステートメント全体をintサイズの加算と単一の条件付きジャンプオンオーバーフローに変換する必要があり、実際に大きな加算を実行することはありません。

編集:スティーブンが指摘したように、私は(あまり良くない)コンパイラgccを取得して正常なasmを生成するのに問題があります。それが生成するコードはそれほど遅くはありませんが、確かに最適ではありません。gccに正しいことをさせるこのコードの変種を誰かが知っているなら、私はそれらを見てみたいです。


1
これを使いたい人は、私の編集したバージョンを見ていることを確認してください。オリジナルではlong long、追加前のキャストをばかげて省略しました。
R .. GitHub STOP

3
好奇心から、コンパイラにこの最適化を実行させることに成功しましたか?いくつかのコンパイラに対する簡単なテストでは、それを実行できるコンパイラは見つかりませんでした。
スティーブンキヤノン

2
x86_64では、32ビット整数の使用について非効率的なことは何もありません。パフォーマンスは64ビットのものと同じです。ネイティブワードサイズよりも小さいタイプを使用する動機の1つは、オーバーフロー/キャリーが直接アクセス可能な場所で発生するため、オーバーフロー条件またはキャリー(任意精度の演算の場合)を処理することが非常に効率的であることです。
R .. GitHub STOP HELPING ICE

2
@ R。、@ Steven:OPが提供した減算コードは正しくありません。私の答えを参照してください。また、最大2つの比較でそれを実行するコードも提供します。おそらく、コンパイラーはそれでうまくいくでしょう。
Jens Gustedt 2010年

3
このアプローチは、珍しいプラットフォームでは機能しません sizeof(long long) == sizeof(int)。Cはそれだけを指定しsizeof(long long) >= sizeof(int)ます。
chux -復活モニカ

36

いいえ、2番目のコードは正しくありませんが、近いです:設定した場合

int half = INT_MAX/2;
int half1 = half + 1;

加算の結果はINT_MAXです。(INT_MAX常に奇数です)。したがって、これは有効な入力です。しかし、あなたのルーチンでは、あなたは持っINT_MAX - half == half1ているでしょう、そしてあなたは中絶するでしょう。誤検知。

このエラー<<=、両方のチェックの代わりに置くことで修復できます。

しかし、コードも最適ではありません。次のようになります。

int add(int lhs, int rhs)
{
 if (lhs >= 0) {
  if (INT_MAX - lhs < rhs) {
   /* would overflow */
   abort();
  }
 }
 else {
  if (rhs < INT_MIN - lhs) {
   /* would overflow */
   abort();
  }
 }
 return lhs + rhs;
}

これが有効であることを確認するlhsには、不等式の両側をシンボリックに追加する必要があります。これにより、結果が範囲外であるという算術条件が正確に得られます。


ベストアンサーは+1。マイナー:/* overflow will occurred */全体のポイントは、コードがlhs + rhs実際に合計を行わずに発生した場合にオーバーフローが発生したことを検出することであることを強調することをお勧めします。
chux -復活モニカ

16

IMHO、オーバーフローセンシティブC ++コードを処理する最も簡単な方法は、を使用することSafeInt<T>です。これは、コードプレックスでホストされているクロスプラットフォームのC ++テンプレートであり、ここで必要な安全性を保証します。

通常の数値演算と同じ使用パターンの多くを提供し、例外を介してフローの上下を表現するため、非常に直感的に使用できます。


14

gccの場合、gcc 5.0リリースノートから、__builtin_add_overflowオーバーフローをチェックするためのが追加で提供されることがわかります。

オーバーフローチェックを備えた算術演算用の組み込み関数の新しいセットが追加されました:__ builtin_add_overflow、__ builtin_sub_overflow、および__builtin_mul_overflow、およびclangとの互換性のために他のバリアントもあります。これらのビルトインには2つの整数引数(同じ型である必要はありません)があり、引数は無限精度の符号付き型に拡張され、+、-、または*がそれらに対して実行され、結果はを指す整数変数に格納されます最後の引数で。格納された値が無限精度の結果と等しい場合、組み込み関数はfalseを返し、そうでない場合はtrueを返します。結果を保持する整数変数のタイプは、最初の2つの引数のタイプとは異なる場合があります。

例えば:

__builtin_add_overflow( rhs, lhs, &result )

gccドキュメントから、オーバーフローチェックで算術演算を実行するための組み込み関数を確認できます。できます。

[...]これらの組み込み関数には、すべての引数値に対して完全に定義された動作があります。

clangは、チェックされた算術ビルトインのセットも提供します

Clangは、Cで高速かつ簡単に表現できる方法で、セキュリティが重要なアプリケーションのチェック演算を実装する一連の組み込み関数を提供します。

この場合、組み込みは次のようになります。

__builtin_sadd_overflow( rhs, lhs, &result )

この関数は、1つのことを除いて、非常に便利であるよう見えます。オーバーフロー時にint result; __builtin_add_overflow(INT_MAX, 1, &result);何が格納されるかを明示的に示さずresult、残念ながら、未定義の動作が発生しないことを指定すると静かになります。確かにそれが意図でした-UBはありません。それを指定した方が良いです。
chux -復活モニカ

1
@chux良い点、ここでは結果が常に定義されていると述べています。私は答えを更新しました。そうでなければ、それはかなり皮肉なことです。
Shafik Yaghmour 2015

興味深いあなたの新しい参照には(unsigned) long long *resultforがありません__builtin_(s/u)addll_overflow。確かにこれらは誤りです。他の側面の信憑性について不思議に思う。IAC、これらを見て良かった__builtin_add/sub/mull_overflow()。彼らがいつかC仕様に到達することを願っています。
chux -復活モニカ

1
+1これは、少なくともコンパイラのオプティマイザに依存して何をしているのかを理解することなく、標準Cで取得できるものよりもはるかに優れたアセンブリを生成します。そのような組み込みが利用可能になると検出し、コンパイラが提供しない場合にのみ標準ソリューションを使用する必要があります。
AlexReinking19年

11

インラインアセンブラを使用する場合は、オーバーフローフラグを確認できます。もう1つの可能性は、safeintデータ型を使用できることです。整数セキュリティに関するこのペーパーを読むことをお勧めします。


6
+1これは、「Cがそれを定義しない場合、プラットフォーム固有の動作を強いられる」という別の言い方です。組み立てで簡単に処理できるものの多くはCで定義されておらず、移植性の名の下にモグラヒルから山を作成しています。
Mike DeSimone 2010年

5
私はCの質問に対するasmの回答に反対票を投じました。私が言ったように、Cでチェックを書くための正しい、ポータブルな方法があります。それはあなたが手で書くのとまったく同じasmを生成します。当然、これらを使用すると、パフォーマンスへの影響は同じになり、これも推奨したC ++のsafeintのものよりもはるかに影響が少なくなります。
R .. GitHub STOP HELPING ICE

1
@Matthieu:1つの実装でのみ使用されるコードを記述していて、その実装によって何かが機能することが保証され、優れた整数パフォーマンスが必要な場合は、実装固有のトリックを使用できます。しかし、それはOPが求めていたものではありません。
David Thornley 2010年

3
Cは、正当な理由で実装定義の動作と未定義の動作を区別します。現在のバージョンの実装でUBを使用したものが「機能」しとしても、将来のバージョンでも機能し続けるとは限りません。... gccと符号付きオーバーフローの動作を考えてみましょう
R .. GitHubのSTOPはICE手助け

2
私はベースますので、私の-1、我々はCコードが同一のasmを生成するために得ることができるという主張に、私はそれがすべての主要なコンパイラは、この点でジャンクであることが判明したときに、それを撤回するだけ公正だと思う...
R .. GitHubのSTOP HELPING ICE

6

可能な最速の方法は、GCCビルトインを使用することです。

int add(int lhs, int rhs) {
    int sum;
    if (__builtin_add_overflow(lhs, rhs, &sum))
        abort();
    return sum;
}

x86では、GCCはこれを次のようにコンパイルします。

    mov %edi, %eax
    add %esi, %eax
    jo call_abort 
    ret
call_abort:
    call abort

これは、プロセッサの組み込みオーバーフロー検出を使用します。

GCCビルトインの使用に問題がある場合、次に速い方法は、符号ビットでビット演算を使用することです。さらに、次の場合に符号付きオーバーフローが発生します。

  • 2つのオペランドの符号は同じであり、
  • 結果の符号はオペランドとは異なります。

の符号ビットは~(lhs ^ rhs)、オペランドの符号が同じである場合にオンになり、の符号ビットはlhs ^ sum、結果の符号がオペランドと異なる場合にオンになります。したがって、未定義の動作を回避するために符号なし形式で加算を実行してから、次の符号ビットを使用できます~(lhs ^ rhs) & (lhs ^ sum)

int add(int lhs, int rhs) {
    unsigned sum = (unsigned) lhs + (unsigned) rhs;
    if ((~(lhs ^ rhs) & (lhs ^ sum)) & 0x80000000)
        abort();
    return (int) sum;
}

これは次のようにコンパイルされます。

    lea (%rsi,%rdi), %eax
    xor %edi, %esi
    not %esi
    xor %eax, %edi
    test %edi, %esi
    js call_abort
    ret
call_abort:
    call abort

これは、32ビットマシン(gccを使用)で64ビットタイプにキャストするよりもはるかに高速です。

    push %ebx
    mov 12(%esp), %ecx
    mov 8(%esp), %eax
    mov %ecx, %ebx
    sar $31, %ebx
    clt
    add %ecx, %eax
    adc %ebx, %edx
    mov %eax, %ecx
    add $-2147483648, %ecx
    mov %edx, %ebx
    adc $0, %ebx
    cmp $0, %ebx
    ja call_abort
    pop %ebx
    ret
call_abort:
    call abort

1

64ビット整数に変換し、そのような同様の条件をテストする方が幸運かもしれません。例えば:

#include <stdint.h>

...

int64_t sum = (int64_t)lhs + (int64_t)rhs;
if (sum < INT_MIN || sum > INT_MAX) {
    // Overflow occurred!
}
else {
    return sum;
}

ここで符号拡張がどのように機能するかを詳しく調べたいと思うかもしれませんが、それは正しいと思います。


ビット単位の-andを削除し、returnステートメントからキャストします。それらは書かれているように正しくありません。大きな符号付き整数型から小さな型への変換は、値が小さな型に収まる限り完全に明確に定義されており、明示的なキャストは必要ありません。警告を出し、値がオーバーフローしないことを確認したときにキャストを追加することを提案するコンパイラは、壊れたコンパイラです。
R .. GitHub STOP HELPING ICE

@Rあなたは正しいです、私は自分のキャストについて明確にするのが好きです。ただし、正確を期すために変更します。将来の読者のために、戻り行はを読みますreturn (int32_t)(sum & 0xffffffff);
ジョナサン

2
を書くとsum & 0xffffffffsum暗黙的に型に変換されることに注意してくださいunsigned int(32ビットを想定しintているため)0xffffffffタイプがありますunsigned int。次に、ビット単位のとの結果はでありunsigned intsum負の場合は、でサポートされてint32_tいる値の範囲外になります。への変換にint32_tは、実装定義の動作があります。
R .. GitHub STOP

これは、が64ビットであるILP64環境では機能しないことに注意してくださいint
rtx 1320

1

どうですか:

int sum(int n1, int n2)
{
  int result;
  if (n1 >= 0)
  {
    result = (n1 - INT_MAX)+n2; /* Can't overflow */
    if (result > 0) return INT_MAX; else return (result + INT_MAX);
  }
  else
  {
    result = (n1 - INT_MIN)+n2; /* Can't overflow */
    if (0 > result) return INT_MIN; else return (result + INT_MIN);
  }
}

私はそれが合法INT_MININT_MAX(対称的であろうとなかろうと)どんなものでもうまくいくはずだと思います。示されているクリップのように機能しますが、他の動作を取得する方法は明らかです)。


+1は、おそらくより直感的な代替アプローチです。
R .. GitHub STOP HELPING ICE

1
result = (n1 - INT_MAX)+n2;n1が小さく(たとえば0)、n2が負の場合、これはオーバーフローする可能性があると思います。
davmac 2013

@davmac:うーん... 3つのケースを分割する必要があるかもしれません:(n1 ^ n2) < 02の補数マシンでは、値が反対の符号を持ち、直接追加される可能性があることを意味する、の1から始めます。値の符号が同じである場合、上記のアプローチは安全です。一方、標準の作成者が、2の補数のサイレントオーバーフローハードウェアの実装が、オーバーフローの場合に、プログラムの即時の異常終了を強制しない方法でレールをジャンプすることを期待していたかどうかに興味がありますが、他の計算の予測できない中断。
スーパーキャット2016

0

明らかな解決策は、unsignedに変換して、明確に定義されたunsignedオーバーフロー動作を取得することです。

int add(int lhs, int rhs) 
{ 
   int sum = (unsigned)lhs + (unsigned)rhs; 
   if ((lhs >= 0 && sum < rhs) || (lhs < 0 && sum > rhs)) { 
      /* an overflow has occurred */ 
      abort(); 
   } 
   return sum;  
} 

これにより、未定義の符号付きオーバーフロー動作が、符号付きと符号なしの間の範囲外の値の実装定義の変換に置き換えられるため、コンパイラのドキュメントをチェックして、何が起こるかを正確に知る必要がありますが、少なくとも十分に定義されている必要があります。変換時にシグナルを生成しない2の補数マシンで正しいことを行う必要があります。これは、過去20年間に構築されたほとんどすべてのマシンとCコンパイラです。


あなたはまだ結果をに保存していますsum、それはintです。その結果、の値が。(unsigned)lhs + (unsigned)rhsより大きい場合、実装定義の結果または実装定義のシグナルが発生しますINT_MAX
R .. GitHub STOP HELPING ICE

2
@R:それが要点です。動作は未定義ではなく実装定義であるため、実装はその機能を文書化し、一貫して実行する必要があります。シグナルは、実装がそれを文書化した場合にのみ発生させることができます。その場合、常に発生させる必要があり、その動作を使用できます。
クリスドッド

0

2つのlong値を追加する場合、ポータブルコードはlong値を低いint部分と高い部分に分割できます(または、と同じサイズのshort場合longは部分に分割できますint)。

static_assert(sizeof(long) == 2*sizeof(int), "");
long a, b;
int ai[2] = {int(a), int(a >> (8*sizeof(int)))};
int bi[2] = {int(b), int(b >> (8*sizeof(int))});
... use the 'long' type to add the elements of 'ai' and 'bi'

特定のCPUをターゲットにする場合は、インラインアセンブリを使用するのが最速の方法です。

long a, b;
bool overflow;
#ifdef __amd64__
    asm (
        "addq %2, %0; seto %1"
        : "+r" (a), "=ro" (overflow)
        : "ro" (b)
    );
#else
    #error "unsupported CPU"
#endif
if(overflow) ...
// The result is stored in variable 'a'

-1

私はこれがうまくいくと思います:

int add(int lhs, int rhs) {
   volatile int sum = lhs + rhs;
   if (lhs != (sum - rhs) ) {
       /* overflow */
       //errno = ERANGE;
       abort();
   }
   return sum;
}

volatileを使用すると、コンパイラーはテストを最適化できないと考えます。 sumは、加算と減算の間が変更された可能性ができなくなります。

x86_64にgcc4.4.3を使用すると、このコードのアセンブリは加算、減算、およびテストを実行しますが、スタックおよび不要なスタック操作のすべてを格納します。私も試しましたregister volatile int sum =が、組み立ては同じでした。

int sum =(揮発性またはレジスタがない)バージョンのみの場合、関数はテストを実行せず、1つのlea命令のみを使用して加算を実行しました(lea実効アドレスのロードであり、フラグレジスタに触れずに加算を実行するためによく使用されます)。

あなたのバージョンはより大きなコードであり、より多くのジャンプがありますが、どちら良いかわかりません


4
volatile未定義の動作をマスクするための誤用の場合は-1 。それが「機能する」場合、あなたはまだ「幸運を得る」だけです。
R .. GitHub STOP HELPING ICE

@R:それが機能しない場合、コンパイラはvolatile正しく実装されていません。私が試していたのは、すでに回答済みの質問に関する非常に一般的な問題に対するより簡単な解決策でした。
nategoose 2010年

ただし、失敗する可能性があるのは、整数のオーバーフロー時に数値表現がより低い値にラップされるシステムです。
nategoose 2010年

その最後のコメントには、「しなかった」または「しなかった」が含まれている必要があります。
nategoose 2010年

@nategoose、「それが機能しない場合、コンパイラはvolatileを正しく実装していない」というあなたの主張は間違っています。1つには、2の補数演算では、オーバーフローが発生した場合でも、lhs = sum-rhsであることが常に当てはまります。そうでない場合でも、この特定の例は少し工夫されていますが、コンパイラは、たとえば、加算を実行し、結果値を格納し、値を別のレジスタに読み取り、格納された値を読み取り値と比較するコードを生成する場合があります。値とそれらが同じであることに気づき、したがってオーバーフローが発生していないと想定します。
davmac 2013

-1

私の場合、最も簡単なチェックは、オペランドと結果の符号をチェックすることです。

合計を調べてみましょう。オーバーフローは、両方のオペランドの符号が同じである場合にのみ、+または-の両方向で発生する可能性があります。そして、明らかに、結果の符号がオペランドの符号と同じにならない場合にオーバーフローが発生します。

したがって、このようなチェックで十分です。

int a, b, sum;
sum = a + b;
if  (((a ^ ~b) & (a ^ sum)) & 0x80000000)
    detect_oveflow();

編集:ニルスが示唆したように、これは正しいif条件です:

((((unsigned int)a ^ ~(unsigned int)b) & ((unsigned int)a ^ (unsigned int)sum)) & 0x80000000)

そして、命令がいつから

add eax, ebx 

未定義の振る舞いにつながりますか?Intelx86命令セットリファレンスにはそのようなものはありません。


2
あなたはここでポイントを逃しています。コードの2行目でsum = a + bは、未定義の動作が発生する可能性があります。
channel72 2010年

あなたは合計をキャストした場合、aとbをunsignedにテスト-添加の間、あなたのコードが...ところで動作します
ニルスPipenbrinck

プログラムがクラッシュしたり、動作が異なるためではなく、未定義です。これは、プロセッサがOFフラグを計算するために行っていることとまったく同じです。標準は、非標準のケースから自分自身を保護しようとしているだけですが、これを行うことが許可されていないという意味ではありません。
ruslik 2010年

@Nilsええ、私はそれをやりたかったのですが、4(usngined int)秒でもっと読みにくくなると思いました。(ご存知のとおり、最初に読んで、気に入った場合にのみ試してください)。
ruslik 2010年

1
未定義の動作はCであり、アセンブリにコンパイルした後ではありません
phuclv 2015
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.