C / C ++에서 서명 된 오버플로 감지 서명 된 오버플로가 발생하는 즉시 널 포인터를

언뜻보기 에이 질문은 정수 오버플로를 감지하는 방법 의 중복처럼 보일 수 있습니다 . 그러나 실제로는 상당히 다릅니다.

부호없는 정수 오버플로를 감지하는 것은 매우 사소한 일이지만 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 표준에 따르면 부호있는 정수 오버플로가 정의되지 않은 동작이라는 것입니다. 즉, 표준에 따르면 서명 된 오버플로가 발생하는 즉시 널 포인터를 역 참조한 것처럼 프로그램이 유효하지 않습니다. 따라서 정의되지 않은 동작을 유발할 수 없으며 위의 사후 조건 검사 예제에서와 같이 사실 후에 오버플로를 감지하려고 시도합니다.

위의 검사가 많은 컴파일러에서 작동 할 가능성이 있지만 믿을 수는 없습니다. 실제로 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;
}

이러한 추가를 수행해도 오버플로가 발생하지 않는지 미리 확인할 때까지 실제로 두 정수를 함께 추가하지 않기 때문에 이것은 더 유망 해 보입니다. 따라서 정의되지 않은 동작은 발생하지 않습니다.

그러나이 솔루션은 더하기 연산이 작동하는지 테스트하기 위해 빼기 연산을 수행해야하므로 초기 솔루션보다 훨씬 덜 효율적입니다. 그리고이 (작은) 성능 저하에 대해 신경 쓰지 않더라도이 솔루션이 적절하다고 확신하지는 않습니다. 이 표현식 lhs <= INT_MIN - rhs은 컴파일러가 최적화 할 수있는 표현식과 똑같이 보입니다. 서명 된 오버플로가 불가능하다고 생각합니다.

여기에 더 나은 해결책이 있습니까? 1) 정의되지 않은 동작을 일으키지 않고 2) 컴파일러에게 오버플로 검사를 최적화 할 기회를 제공하지 않는 것이 보장되는 것입니까? 두 피연산자를 unsigned로 캐스팅하고 자신의 2의 보수 산술을 굴려서 검사를 수행하는 방법이있을 수 있다고 생각했지만 어떻게 해야할지 모르겠습니다.



답변

빼기에 대한 접근 방식은 정확하고 잘 정의되어 있습니다. 컴파일러는이를 최적화 할 수 없습니다.

더 큰 정수 유형을 사용할 수있는 경우 또 다른 올바른 접근 방식은 더 큰 유형에서 산술을 수행 한 다음 다시 변환 할 때 결과가 더 작은 유형에 맞는지 확인하는 것입니다.

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이 지적했듯이 정상적인 asm을 생성하기 위해 (그다지 좋지 않은) 컴파일러 gcc를 얻는 데 문제가 있습니다. 생성되는 코드는 그렇게 느리지는 않지만 확실히 차선책입니다. gcc가 옳은 일을하도록하는이 코드의 변형을 아는 사람이 있다면보고 싶습니다.


답변

아니요, 두 번째 코드가 올바르지 않지만 가까이 있습니다.

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부등식의 양쪽에 상징적으로 추가 해야합니다. 그러면 결과가 범위를 벗어난 산술적 조건을 정확히 알 수 있습니다.


답변

IMHO, 오버플로 센시티브 C ++ 코드를 처리하는 가장 쉬운 방법은 SafeInt<T>. 이것은 여기에서 원하는 안전 보장을 제공하는 코드 플렉스에서 호스팅되는 크로스 플랫폼 C ++ 템플릿입니다.

일반적인 수치 연산과 동일한 사용 패턴을 많이 제공하고 예외를 통해 흐름의 위와 아래를 표현하므로 사용하는 것이 매우 직관적입니다.


답변

gcc 케이스의 경우 gcc 5.0 릴리스 노트에서 이제 __builtin_add_overflow오버플로 확인을위한 추가 기능을 제공하는 것을 볼 수 있습니다 .

오버플로 검사를 사용하는 산술을위한 새로운 내장 함수 세트가 추가되었습니다 : __builtin_add_overflow, __builtin_sub_overflow 및 __builtin_mul_overflow 및 clang과의 호환성을 위해 다른 변형도 추가되었습니다. 이러한 내장 기능에는 두 개의 정수 인수 (동일한 유형일 필요 없음)가 있으며, 인수는 무한 정밀도 부호있는 유형으로 확장되고, +,-또는 *가 수행되며 결과는 가리키는 정수 변수에 저장됩니다. 마지막 주장으로. 저장된 값이 무한 정밀도 결과와 같으면 내장 함수는 false를 리턴하고 그렇지 않으면 true를 리턴합니다. 결과를 보유 할 정수 변수의 유형은 처음 두 인수의 유형과 다를 수 있습니다.

예를 들면 :

__builtin_add_overflow( rhs, lhs, &result )

gcc 문서 에서 Overflow Checking 을 사용 하여 산술을 수행하는 내장 함수를 볼 수 있습니다 .

[…] 이러한 내장 함수에는 모든 인수 값에 대해 완전히 정의 된 동작이 있습니다.

clang은 또한 확인 된 산술 내장 집합을 제공합니다 .

Clang은 C에서 빠르고 쉽게 표현할 수있는 방식으로 보안에 중요한 애플리케이션에 대해 검사 된 산술을 구현하는 내장 세트를 제공합니다.

이 경우 내장은 다음과 같습니다.

__builtin_sadd_overflow( rhs, lhs, &result )

답변

인라인 어셈블러를 사용하는 경우 오버플로 플래그를 확인할 수 있습니다 . 또 다른 가능성은 safeint 데이터 유형을 사용할 수 있다는 것입니다 . Integer Security 에 대한이 문서를 읽는 것이 좋습니다 .


답변

가장 빠른 방법은 내장 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 내장을 사용하는 것이 괜찮지 않다면 다음으로 빠른 방법은 부호 비트에서 비트 연산을 사용하는 것입니다. 추가로 서명 된 오버플로는 다음과 같은 경우에 발생합니다.

  • 두 피연산자의 부호가 같고
  • 결과는 피연산자와 다른 부호를 갖습니다.

의 부호 비트는 ~(lhs ^ rhs)피연산자에 동일한 부호가 있으면 on이고 부호 비트 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

답변

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

여기서 기호 확장이 어떻게 작동하는지 자세히 살펴보고 싶을 수도 있지만 올바른 것 같습니다.