불쌍한 C 언어의 사정을 하나 설명하려 함.

// 간디 meme: 한국인 millenial 분들은 다들 알만한 meme임. 문명 초기 버전 코드에 정수 오버플로 버그가 있어서 간디가 상당히 적대적으로 변하는 quirk가 있었음(리눅스 프로세스 nice 값처럼, 값이 클수록 친절하고, 값이 낮을수록 적대적이라는 로직 - 상당히 친절하다는 것을 표현하는 큰 양수 값에 오버플로 발생해서 극한 적대적으로 변한다는 버그라고 함). 이게 반응이 좋아서 개발자들이 시리즈에 그대로 뒀다는 전설임
// 참고로 이거 어디까지나 근거 없는 도시전설이다. 더 자세한 내용은 나무위키 항목 참조. 게임 스크립팅을 오버플로 감지가 되지 않는 low-level 언어로 하는 것 자체가 말이 안되지, 아무리 엔진이 90년대로 거슬러 올라가는 게임이라 해도. 지금 쓸 수 있는 게임 엔진들 다 오버플로 나면 예외가 터지는 쪽으로 구현함. C 언어처럼 오버플로 터졌는데 조용히 넘어가는 식으로 구현하면 게임 로직에 치명적임. NES 마리오 게임이나 Pacman 같은 게임이 오버플로나서 죽는 버그는 지금 시대에 일어나기 힘듦. 소프트웨어 엔지니어링도 그 사이에 어마어마한 발전이 있었음
최근 이런 코드를 짜기 시작했다.
https://github.com/dxdxdt/exfatprogs/commit/5c9f44707176f47606dc8f441fe30153ec763a1d
static inline off_t exfat_iostat_ofsadd(off_t a, off_t b, int f)
{
#ifndef EXFAT_NO_IOSTATS
a += b;
if (a < 0)
io_stat.ovf |= f;
return a;부호 있는 정수 2개를 더해봤을 때, 결과 값이 음수로 나오면 오버플로로 간주한다는 코드임. 실제로 생성되는 기계코드를 확인해 보면(gdb 명령 disass/s ...):
147 static inline off_t exfat_iostat_ofsadd(off_t a, off_t b, int f)
148 {
149 #ifndef EXFAT_NO_IOSTATS
150 a += b;
151 if (a < 0)
0x0000000000406a4f <+143>: add 0x2058a(%rip),%rbx # 0x426fe0 <io_stat+64>
0x0000000000406a56 <+150>: js 0x406a80 <exfat_discard_blocks+192>
153 return a;
0x0000000000406a58 <+152>: mov %rbx,0x20581(%rip) # 0x426fe0 <io_stat+64>
점프를 하긴 하는데, add 명령 실행 결과물에 부호가 있으면(ie. 음수이면) 점프하라는 코드가 생성됨(js). 물론, 위 함수 호출자(caller)가 a와 b 모두 양수라는 조건을 잘 조성해 줘야 함. io_stat.ovf 변수는 한번 설정되면 다시 clear 되지 않는 추노마크 dirty bit로 사용됨. 이렇게 한번 음수를 감지하면 이 플래그가 유지되기 때문에 위 함수를 그 이후에 몇 번이든 호출하건 상관 없음.
참고로 위 코드는 프로그램 상의 중요한 기능은 아니고, I/O 성능 데이터를 수집하는 부가적인 기능을 구현하는 코드임. 오버플로 났다고 프로그램을 실행을 중단하는 예외처리를 할 필요가 없는 부분임.
이렇게 부호 있는 정수의 오버플로 감지는 비교적 간단하게 구현할 수 있음. 이식성 문제가 있는 __builtin_*_overflow()를 사용할 필요도 없고, 아직 시기상조인 C23(<stdckdint.h>)를 사용할 필요도 없음.
0x7FFFFFFF = 2147483647
0x7FFFFFFF + 1 = 0x80000000
0x80000000 = -2147483648
복습
지금 모든 컴퓨터가 음수를 2의 보수로 표현하는 이유는 하드웨어 구현이 상당히 간단해지기 때문임. 더하기와 빼기 연산을 위한 회로를 따로 두지 않아도 되는게 큰 장점임. 예로 a - b 연산을 할 때, b를 가산기에 넣기 전에 2의 보수를 취해 더하기를 하면 빼기를 하는 것과 동일한 효과가 있기 때문임. 지금이야 아주 당연한 사실이지만, 안타깝게도 60년대 디지털 컴퓨터 설계자들이 빠르게 눈치채고 표준으로 만들지 못한 부분임.
2의 보수로 빼기 연산을 하는 것은 자연스럽게 오버플로와 언더플로가 발생하면서 양수 혹은 음수로 변하는 수학적인 꼼수를 이용하는 것임. 이건 이미 컴퓨터 일반 교과서에 간략하게 나와있는 부분임.
뭐가 문제냐?
- https://lwn.net/Articles/979747/
- https://gcc.gnu.org/onlinedocs/gcc/Code-Gen-Options.html
- https://stackoverflow.com/questions/12276957/are-there-any-non-twos-complement-implementations-of-c
- https://stackoverflow.com/questions/6971886/exotic-architectures-the-standards-committees-care-about
안타깝게도, 앞서 제시한 구현은 ISO 표준 C 언어 상에서 불법으로 규정됨. 부호 없는 정수의 wrap around(0으로 되돌아가는)는 것은 정의된 동작이지만, 부호 있는 정수의 오버플로는 정의되지 않은 동작(정확히는 implementation-defined: 컴파일러 마음대로)로 규정함. 이건 다 C 언어가 현대 2의 보수를 사용하는 시스템 뿐만 아니라, 역사적으로 존재했던 모든 레거시 시스템도 생각했기 때문임.
C 언어는 우리가 지금 접하고 사용할 수 있는 2의 보수로 음수를 표현하는 아키텍처만 지원하지 않음. 역사적으로 음수를 2의 보수가 아닌 다른 방식으로 사용하던 컴퓨터가 있었음. 지금은 사실상 모두 역사속으로 사라진 컴퓨터들임. 지금 짜는 코드는 그런 exotic한 시스템을 고려하며 짜지 않아도 됨. 그러나…
현대 컴파일러는 여전히 표준 C 언어가 이게 정의되지 않은 동작인 사실을 이용한 최적화를 함. 예를 들면:
int f(int i) {
return i + 1 > i;
}이걸 항상 참으로 최적화 할 수 있음. 내가 짠 위 코드의 경우도, 만약 a와 b 중 하나라도 양수를 상수로 쓰면 이런 코드가 생성될 수 있다는 뜻임(C++의 constexpr 상황이 대표적).
대부분의 경우 의미가 없는 최적화고, 오히려 예상치 못한 버그만 터질 수 있는 최적화임. 리눅스 커널은 (언제나 그랬듯이) 한술 더 떠서 이 최적화를 꺼버림전형적인 Linus 씨의 “그런게 어딨어?” 배짱
. 대부분의 경우 이건 프로그래머가 의도하는 바가 아니기도 하고, 이 최적화를 해서 성능 이점을 얻는 경우는 거의 0에 수렴함. 이 최적화는 -fno-strict-overflow 옵션으로 끌 수 있음. 굳이 이 최적화 옵션을 끄지 않아도, 현대 컴파일러는 -Wall -Wextra 옵션이 사용되면 경고를 대부분 띄움.
// 언제나 그런 것처럼, 이 두 옵션은 항상 켜두는 것이 좋음
아이러니 한 부분은 유닉스 생태계에서 사용되는 gcc와 clang는 2의 보수 시스템만 지원한다는 것임. 생각해보면 이 최적화는 단순히 C 표준이 그렇게 되어 있으니까 그런 코드를 생성해도 된다는 무책임한 논리에 기반한 것이라 봐도 됨.
정리하자면, 결과값이 음수로 나와버리는 것으로 오버플로를 감지하는 코드를 짜고 싶으면:
-fno-strict-overflow를 사용하거나-Wall와-Wextra를 사용해서 과도한 최적화가 될 경우 경고가 나오도록 하거나 둘 다 사용
부호 있는 정수 연산의 오버플로를 감지하는 코드는 이 방법 뿐임.
이것도 역시 60년대 메인프레임 시대 때 설계된 C 언어의 결함이라 봐도 되고, gcc는 이 한계를 옵션으로 극복할 수 있도록 설계되어 있음. C/C++을 제외한 다른 신생 프로그래밍 언어들은 대부분 코드 짤 때 이런거 생각할 필요도 없다. 다른 상위 언어는 예외가 터지거나(Java) word size가 넘어가면 multi-precision arithmetic으로 자동 변환됨(Python). Rust만 해도 C23의 <stdckdint.h>와 상응한 기능이 기본적으로 제공됨.
COLOPHON
Signed-off-by: David Timber
Acked-by: 0
| DATE | COMMIT | MSG |
|---|---|---|
| 2026-09-06 | 0115e0ceba22 | detecting-integer-overflows: initial commit |