web-dev-qa-db-ja.com

C / C ++での符号付きオーバーフローの検出

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

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

最も明白でありながら単純な方法は、次のようなものです。

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)コンパイラーにオーバーフローチェックを最適化する機会を与えないことが保証されているもの私は両方のオペランドを符号なしにキャストし、独自の2の補数演算をロールしてチェックを実行することで何らかの方法があると考えていましたが、それを行う方法は本当にわかりません。

75
Channel72

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

より大きな整数型が利用可能な場合、別の正しいアプローチは、より大きな型で算術演算を実行し、それを元に戻すときに結果がより小さな型に収まることを確認することです

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サイズの加算と単一の条件付きジャンプオンオーバーフローに変換し、実際に大きな加算を実行することはありません。

編集: Stephenが指摘したように、私は(あまり良くない)コンパイラーgccを取得して正常なasmを生成するのに問題があります。それが生成するコードはひどく遅くはありませんが、確かに最適ではありません。 gccに正しいことをさせるこのコードのバリアントを知っている人がいれば、ぜひ見たいと思います。

23
R..

いいえ、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を記号で追加する必要があります。これにより、結果が範囲外であるという算術条件が正確に得られます。

34
Jens Gustedt

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

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

16
JaredPar

Gccの場合、 gcc 5.0リリースノート から、__builtin_add_overflowさらにオーバーフローをチェックするため:

オーバーフローチェック付きの算術演算用の新しい組み込み関数セットが追加されました。これらの組み込み関数には2つの整数の引数があり(同じ型である必要はありません)、引数は無限精度の符号付き型に拡張され、+、-、または*がそれらに対して実行され、結果が指す整数変数に格納されます最後の引数によって。格納された値が無限精度の結果と等しい場合、組み込み関数はfalseを返します。それ以外の場合はtrueを返します。結果を保持する整数変数の型は、最初の2つの引数の型と異なる場合があります。

例えば:

__builtin_add_overflow( rhs, lhs, &result )

Gccドキュメントから見ることができます オーバーフローチェックで算術を実行する組み込み関数

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

clangはまた、一連の チェック済み算術ビルトイン を提供します。

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

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

__builtin_sadd_overflow( rhs, lhs, &result )
13
Shafik Yaghmour

インラインアセンブラを使用する場合、 オーバーフローフラグ を確認できます。別の可能性は、 safe int data type を使用できることです。 整数セキュリティ に関するこのペーパーを読むことをお勧めします。

10
rook

最速の方法は、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
_
6
tbodt

どうですか:

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_MINおよびINT_MAX(対称かどうか);クリップのように機能しますが、他の動作を取得する方法は明らかです。

2
supercat

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;
}

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

1
Jonathan

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

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

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

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

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

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

そして、以来、命令

add eax, ebx 

未定義の動作につながりますか? Intel x86命令セットの参照にはそのようなものはありません。

0
ruslik

明白な解決策は、明確に定義された符号なしオーバーフロー動作を得るために、符号なしに変換することです:

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コンパイラです。

0
Chris Dodd

2つのlong値を追加する場合、移植可能なコードはlong値を低い_と高いint部分(またはshort部分の場合はlong部分に分割できます。 $ var] _は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'
0
atomsymbol