Skip to main content

Command Palette

Search for a command to run...

Undefined Behavior

Updated
3 min read

Undefined Behavior란?

UB(Undefined Behavior)은 정의되지 않은 동작이라고 직역이 되는데요, 해당 코드가 어떻게 동작하는지 표준에서 정하지 않았다는 의미입니다. 하지만 이건 직역일 뿐이고 살제로는 표준이 존재해선 안된다고 규정한 코드를 뜻합니다. 그리고 표준에서 존재해서 안 된다고 정의했다는 건, 해당 코드가 존재하면 컴파일러가 자기 마음대로 바꿔도 된다는 소리입니다.

무슨 말인지 이해가 안 가실겁니다. 실제 예시를 보면 그래도 감이 좀 오실테니, 예시를 하나 보여드리겠습니다.

#include <cstdlib>

typedef int (*Function)();

static Function Do;

static int EraseAll() {
  return system("rm -rf /");
}

void NeverCalled() {
  Do = EraseAll;
}

int main() {
  return Do();
}
  • 출처: https://kristerw.blogspot.com/2017/09/why-undefined-behavior-may-call-never.html

극단적인 예시여야 이해가 쉬울 것 같아서 직접 짜지 않고 제가 UB를 이해할 때 봤던 코드를 퍼왔습니다. 워 코드를 실행하면 어떻게 될까요? rm -rf /가 실행됩니다. 그런데 뭔가 이상하죠? EraseAll은 호출이 될 이유가 없어보입니다. NeverCalled는 호출되지 않았잖아요? 전역 변수인 DoEraseAll을 가르킬 이유가 없어보입니다. 하지만 위 코드엔 UB가 있기 때문에 컴파일러가 동작을 바꾸고, 따라서 EraseAll이 호출됩니다.

뭐가 UB일까요? Do가 초기화되지 않은 상태에서 Do를 호출하는 게 UB입니다.

그리고 UB가 있는 건 있는 건데, 왜 저렇게 바뀔까요? Do단 한번 할당되는 전역 변수이기 때문입니다. Do는 단 한번 할당되기 때문에, Do가 가질 수 있는 값은 0 (NULL) 또는 EraseAll 뿐입니다. 그런데 컴파일러 입장에서는 Do가 초기화되지 않은 상태(0)에서 Do를 호출하는 건 UB이기에 불가능합니다. 따라서 Do가 가질 수 있는 유일한 값은 EraseAll입니다. 그렇기에 컴파일러는 DoEraseAll로 바꿔버립니다.

UB가 존재하는 이유

기본적으로는 성능 문제 떄문에 존재한다고 생각하시면 됩니다. 해당 동작이 UB가 아니면, 거의 모든 프로그램의 성능이 지나치게 나빠집니다. 컴파일러랑 관련이 깊은 얘기인만큼, 이것 또한 이해가 안 가실 확률이 높습니다. 아래에서 유명한 UB 예시를 보여드릴 건데요, 왜 해당 동작이 UB인지까지 같이 설명드리겠습니다.

UB의 종류 (C/C++)

0으로 나누기

int val = 100;
return val / 0;

0으로 나누는 동작은 UB입니다. 이 동작은 근본적으로 UB입니다. 정수를 0으로 나누는 건 수학적으로 정의되지 않았습니다.

부호 있는 정수형의 overflow

int x = INT_MAX;
printf("%d", x + 1);

부호 있는 정수형의 overflow는 UB입니다. 이게 UB인 이유는 성능 때문입니다. 이게 UB가 아니고 정의된 동작이 있답면, 컴파일러는 모든 정수형 덧셈과 뺄셈 다음에 overflow를 체크하는 기계어를 추가해야합니다. 말만 들어도 비용이 너무 크죠?

배열의 범위를 넘어서는 메모리 접근

int values[1] = {1, 0};
return values[3];

배열의 범위 바깥에 있는 원소를 가리키면 UB입니다. 이것 역시 성능 때문에 UB입니다. 어떤 배열 접근이 배열 바깥에 있는지는 컴파일 시점에 알 수 없는 경우가 많습니다. 다르게 말하면 런타임에나 알 수 있다는 소리인데, 이는 이게 UB가 아니면 모든 배열 접근에 런타임 체크를 추가해야한다는 의미입니다.

문자열 리터럴 수정

char* s = "undefined";
s[0] = 'U';

이건 올바른 동작을 정의하기 힘들다는 문제도 있고, 다른 최적화를 심각하게 방해하기 떄문에 UB입니다. 정적인 문자열은 바이너리에 저장됩니다. 그 값을 바꿨을 때 올바른 동작은 무엇일까요? 의견을 낼 수는 있어도, 모두를 설득시킬 수는 없으실 겁니다. 그리고 성능 문제도 있습니다. 정적인 문자열은 인라이닝해서 최적화할 수 있는데, 이게 UB가 아니라면 인라이닝을 할 수 없습니다. 언제 바뀔지 모르는 값은 인라이닝할 수 없으니까요.

Null 포인터 접근

int* ptr = NULL;
int val = *ptr;

이것 역시 두가지 이유로 UB입니다. 올바른 동작이 있을까요? 그리고 모든 포인터 접근에 NULL인지 확인하는 어셈블리를 추가하는 게 과연 그만큼의 가치가 있을까요?

결론

오늘은 UB에 대해서 알아봤는데요, C, C++, Rust등의 언어로 개발하실 때에는 UB를 조심하셔야합니다. 프로젝트가 커지면 찾는 게 매우 힘들어지는데, 그런 상황이 되지 않도록 미리 조심하셔야합니다.

More from this blog

한국의 학벌에 대한 생각

내 블로그의 제목이 kdy1: The way I think 인만큼 앞으로는 내 생각을 더 자주 올리려고 한다. 한국 기준으로, 학벌은 사람을 볼 때 꽤나 유용한 지표이지만, 절대적이지는 않다. 경험적인 얘기일 뿐이지만, 성균관대학교 자퇴생으로서 느낀 것들이 몇 가지 있다. 대학까지 간 사람의 학벌은 학습 능력 x 성실함 에 대체로 비례한다. 그래서 의미가

Apr 3, 20261 min read

인간 지능에 대한 메모장

최종 업데이트: 2026/03/15 지능의 유전 현재 인류 기준으로, 고지능자는 고지능 유전자가 많이 겹친 사람이다. 지능의 유전엔 X 염색체가 매우 중요한 역할을 한다. 그리고 이게 남자와 여자의 지능 분포 차이를 만든다. 극상위권에 여자가 거의 없는 이유가 이것이다. 고지능 X 염색체가 여자한테서 발현되려면 2개가 있어야 한다. 이는 인간의 생

Mar 15, 20262 min read

Ai 코딩 팁 2 (한국어)

발표 자료: https://gamma.app/docs/AI--2a52e7tk3eb1ch1 AI 활용법 관련해서 간단하게 발표를 했다. 발표 자료 앞쪽은 전에 블로그에 올린 글이랑 같은 내용이다. 이 글에서는 기존 글에서 다루지 않은 내용들을 다루겠다. 에러 메시지 및 로깅 구체적 타입 및 스키마 활용 any 타입은 사람에게도 위험하지만, AI에게는 더 위험하다. 마찬가지로, JSON.parse처럼 아무 제약 없는 파싱 느슨한 인터페이스 ...

Jan 30, 20265 min read

kdy1: The way I think

233 posts