카테고리 보관물: Linux

GCC에서 integer arithmetic overflow 방지 대책

Arithmetic overflow는 부호 있는 변수(signed variable)가 표시 할 수 있는 최댓값 / 최솟값을 넘어 섰을 때 부호가 바뀌면서 원하지 않는 결과가 나타나는 경우를 말한다.

#include <iostream>
#include <climits>

using namespace std;
int main(void) {
    int r = 0;
    int op = INT_MAX;
    cout << op << " + 1 = " << op + 1 << endl;
    return 0;
}

위의 코드를 다음과 같이 컴파일하고 실행하면 overflow가 발생한다.

$ g++ ./overflow.cpp
$ ./a.out
2147483647 + 1 = -2147483648

이와 같은 경우를 방지 하려면 물론 꼼꼼한 코드 리뷰도 중요 하지겠만 컴파일러에서 제공하는 overflow를 점검하는 빌트인 함수를 사용하거나 Safeint 혹은 이와 유사한 Boost의 Safe numerics의 사용을 고려해 볼 수도 있겠다. 하지만 이 방법들은 처음부터 사용이 고려되어야 하거나 이미 작성한 코드를 수정해야 하는 단점이 있다.

-ftrapv compiler switch

GCC 컴파일러에서 제공하는 -ftrapv switch를 사용하면 코드 수정 없이 arithmetic overflow가 발생 했을 때 SIGABRT를 발생 시키도록 컴파일러가 trap을 추가하게 할 수 있는데, 발견 되지 않은 overflow를 일으킨 후 오동작을 하게 되는 것 보다는 프로세스 중단이 낫다고 생각하면 이 switch를 고려해 볼 수 있다.

$ g++ -ftrapv ./overflow.cpp
$ ./a.out
Aborted (core dumped)

-fwrapv compiler switch

또 다른 경우로 최적화 옵션에 따라 실행 결과가 달라 지는 경우도 있다. 다음의 예제는 Stack overflow의 What does -fwrapv do?를 약간 변형한 것이다.

#include <iostream>
#include <climits>
 
using namespace std;
int optFunc(int i) {
    return i+1 > i;
}
 
int main(void) {
    int v = INT_MAX;
 
    if (optFunc(v)) {
        cout << "Unexpected." << endl;
    } else {
        cout << "Terminated OK." << endl;
    }
	
    return 0;
}

현실에서 쓰일 법한 코드는 아니지만 optFunc()를 작성한 사람의 의도가 INT_MAX가 인자로 주어질 때 “INT_MAX + 1“이 음수가 되면서 “INT_MAX + 1 > INT_MAX”가 false(0)를 반환하도록 하는 것이었다 하더라도 이 코드는 최적화 옵션에 따라 다른 결과 값을 내게 된다.

디버깅 등의 이유로 컴파일 할 때 최적화를 옵션을 -O0로 주면 원하는 대로 동작한다.

$ g++ -O0 ./fwrapv.cpp
$ ./a.out 
Terminated OK.

하지만, -O3 옵션을 주면, 컴파일러는 optFunc()가 항상 true(1)을 반환 할 것이라 가정하고 컴파일러가 이를 최적화 시켜 버려서 코드가 의도한 대로 동작하지 않게 된다.

$ g++ -O3 ./fwrapv.cpp
$ ./a.out 
Unexpected.

이러한 경우를 방지 하기 위해 -fwrapv switch를 주면 -O3 이상의 최적화 옵션에서도 해당 부분이 ‘wrapping’ 되어서 의도한 대로 코드가 동작하게 된다.

$ g++ -O3 -fwrapv ./fwrapv.cpp
$ ./a.out 
Terminated OK.

-Wconversion compiler switch

보다 쉬운 경우로, 표현 범위가 큰 타입에서 작은 타입으로 변환 될 때 값을 잃어 버려서 원하지 않는 결과가 생길 수도 있는데, 이 경우는 -Wconversion switch를 추가하면 컴파일시간에 잡아내서 경고를 출력하도록 할 수 있다. 예를 들어 64bit 시스템에서 unsigned long type인 size_t를 실수로 unsigned int에 할당한 것과 같은 경우들을 컴파일 시간에 찾아 경고를 출력해 주는데 -Weror와 함께 사용하면 컴파일이 멈추게 된다.

결론

코딩의 처음부터 안전한 연산을 고려 한다면 이를 지원하기 위한 라이브러리들을 고려해 볼 수 있겠지만, 컴파일러 스위치를 활용하여 arithmetic integer overflow 발생에 대한 대비를 할 수도 있다.

* 위의 예제들은 Ubuntu 18.04에서 g++ 7.5.0으로 시험 되었다.
** 위의 fwrapv 예제에 -fwrapv와 -ftrapv switch를 모두 사용하면 -fwrapv로 동작한다.