세그멘테이션 오류란 무엇인가요?
세그멘테이션 오류(segmentation fault, segfault)는 널 포인터를 통해 주소 0에 접근하는 것처럼, 프로그램이 접근이 허용되지 않은 메모리를 읽거나 쓰려 할 때 일어나는 비정상 종료입니다. 운영체제가 SIGSEGV 시그널로 프로그램을 멈춥니다.
업데이트: 2026년 9월 24일
C 프로그램이 첫 줄을 출력하고는 아무도 작성하지 않은 메시지와 함께 멈춰요.
#include <stdio.h>
int main(void) {
int *score = NULL;
printf("About to read the score\n");
printf("Score: %d\n", *score);
return 0;
}
About to read the score
bash: line 1: 64030 Segmentation fault: 11 ./seg
이 출력은 macOS의 bash에서 나온 것으로, 64030은 프로세스 ID이고 11은 시그널 번호예요. Linux에서는 같은 비정상 종료가 보통 Segmentation fault (core dumped)로 표시돼요. 어느 쪽이든 프로그램은 점수를 출력하지 못했어요. score에는 주소 0이 들어 있고, *score는 프로세서에게 그 주소의 메모리를 읽으라고 요청하는데, 어떤 프로그램에도 그것은 허용되지 않아요.
세그멘테이션 오류는 어떻게 일어나나요
모든 프로그램은 자신만의 가상 주소 공간에서 실행돼요. 이는 운영체제가 조금씩 채워 나가는 거대한 주소 범위예요. 운영체제는 프로그램의 코드, 전역 변수, 스택, 힙을 페이지라는 블록 단위로 이 범위에 매핑해요(대부분의 x86 Linux 시스템에서는 4 KB, Apple 실리콘 Mac에서는 16 KB). 대부분의 주소는 매핑되지 않은 채로 남고, 널 포인터 버그를 잡을 수 있도록 주소 0은 항상 그중에 포함돼요.
- 프로그램이 어떤 주소를 읽거나 쓰는 명령어를 실행해요. 여기서는 주소 0인
*score를 읽어요. - 프로세서의 메모리 관리 장치(MMU)가 페이지 테이블에서 그 주소를 찾아요. 페이지가 매핑되어 있지 않거나, 프로그램이 읽기 전용 페이지에 쓰려고 해요.
- 프로세서는 명령어를 멈추고 페이지 폴트로 커널에 제어를 넘겨요.
- 커널은 그 접근이 합법일 수 있는지, 예를 들어 스택이 커져야 하는 상황인지 확인해요. 그렇지 않으므로 커널은 프로세스에 시그널 11,
SIGSEGV를 보내요. SIGSEGV의 기본 동작은 프로세스를 종료하고, 시스템이 허용하면 코어 덤프를 저장하는 거예요. 그다음 셸이 메시지를 출력하고 종료 상태를 128에 시그널 번호를 더한 139로 설정해요.
따라서 세그멘테이션 오류는 컴파일러가 보고하는 것도 아니고 언어가 발생시키는 예외도 아니에요. 하드웨어와 커널이 메모리를 보호하는 것이며, 그래서 가장 갑작스러운 종류의 런타임 오류예요. Windows는 같은 사건을 예외 코드 0xC0000005인 액세스 위반으로 처리해요.
세그멘테이션 오류의 흔한 원인
C와 C++는 프로그램이 어떤 주소든 계산해서 사용할 수 있게 하므로, 원인은 결국 유효하지 않은 주소를 사용하는 것으로 귀결돼요.
- 널 포인터 역참조.
int *p = NULL; *p = 5;malloc이나fopen처럼 실패하면NULL을 반환하는 함수의 결과를 확인하지 않으면 이렇게 돼요. - 배열 끝을 한참 넘는 인덱스. C는 범위를 검사하지 않으므로
arr[1000000]은 그저 원소 백만 개만큼 뒤에 있는 주소일 뿐이에요. free후에 메모리 사용. 포인터에는 여전히 예전 주소가 들어 있지만, 그 메모리는 더 이상 여러분의 것이 아니에요.- 초기화하지 않은 포인터.
int *p; *p = 5;는p에 우연히 들어 있는 쓰레기 값을 통해 써요. - 스택 오버플로. 멈추는 조건이 없는 재귀 함수는 스택 프레임을 계속 쌓다가 스택 끝을 넘어가요. 아래 예제는 macOS에서
Segmentation fault: 11과 종료 상태 139로 비정상 종료했어요. - 문자열 리터럴에 쓰기.
char *name = "coddy"; name[0] = 'C';는 읽기 전용 메모리를 바꾸려고 해요. Linux에서는 세그폴트예요. macOS에서는 같은 프로그램이 관련된 시그널인Bus error: 10으로 멈췄어요.
#include <stdio.h>
int depth(int n) {
return depth(n + 1) + 1; /* never stops calling itself */
}
int main(void) {
printf("%d\n", depth(0));
return 0;
}
작은 실수는 비정상 종료를 일으키지 않는 경우가 많고, 그래서 더 위험해요. 이 반복문은 원소 세 개짜리 배열의 끝을 넘어 원소 하나를 더 읽어요.
#include <stdio.h>
int main(void) {
int scores[3] = {72, 88, 95};
int total = 0;
for (int i = 0; i <= 3; i++) { /* <= reads scores[3] */
total += scores[i];
}
printf("Total: %d\n", total);
return 0;
}
Mac에서 Clang으로 컴파일하자 Total: 256이 출력되었어요. 올바른 합계는 255예요. scores[3]은 스택의 다음 4바이트였고 그 메모리는 프로그램 소유였기 때문에, 폴트는 일어나지 않았고 쓰레기 값이 조용히 더해졌어요. 운영체제는 프로그램이 전혀 소유하지 않은 메모리에 대한 접근만 막아요.
두 실수의 해결책은 같은 습관이에요. 원소가 몇 개인지 알고, 포인터를 따라가기 전에 확인하세요.
Best: 95
No scores, nothing to read
"core dumped"는 무슨 뜻인가요
코어 덤프는 프로그램이 비정상 종료한 순간의 메모리 사본을 담은 파일이에요. 디버거로 나중에 이 파일을 열어 프로그램이 정확히 어디에 있었고 변수에 무엇이 들어 있었는지 볼 수 있어요. gdb ./app core처럼 써요. 많은 Linux 배포판에서는 systemd-coredump가 이 파일을 모으며, coredumpctl list로 볼 수 있어요. ulimit -c 0 등으로 코어 덤프를 끄면 괄호 안의 말 없이 Segmentation fault만 표시돼요.
비정상 종료한 줄을 찾는 방법
프로그램이 비정상 종료한 줄이 버그가 있는 줄이 아닌 경우가 많아요. 포인터는 한 함수에서 무효가 되고 한참 뒤 다른 함수에서 사용될 수 있어요. 아래 도구들은 두 곳을 모두 보여 줘요.
디버거. 디버그 정보를 넣어 컴파일하고 디버거 안에서 프로그램을 실행해요. 멈추면 bt(백트레이스)가 파일 이름과 줄 번호와 함께 함수 호출 연쇄를 출력해요. Linux의 디버거는 보통 gdb이고, macOS에서는 lldb이며 bt도 똑같이 동작해요.
gcc -g app.c -o app
gdb ./app
(gdb) run
(gdb) bt
AddressSanitizer. -fsanitize=address로 컴파일하고(GCC와 Clang 모두 지원) 프로그램을 평소처럼 실행해요. 맨 세그폴트 대신 heap-use-after-free나 stack-buffer-overflow처럼 실수의 종류를 알려 주고, 접근한 줄과 메모리를 할당한 줄을 담은 보고서를 출력해요. 스스로는 절대 비정상 종료하지 않는 위의 조용한 끝 넘어 읽기도 잡아내요.
Valgrind. Linux에서 valgrind ./app는 수정하지 않은 프로그램을 실행하고 Invalid read of size 4처럼 잘못된 읽기와 쓰기를 모두 보고해요.
다른 언어의 세그멘테이션 오류
Python, Java, JavaScript는 모든 인덱스와 참조를 사용하기 전에 검사하므로, 같은 실수가 분명한 메시지를 가진 예외가 돼요. Python에서는 IndexError나 AttributeError, Java에서는 ArrayIndexOutOfBoundsException이나 NullPointerException, JavaScript에서는 TypeError예요. 이런 예외는 예외 처리로 잡을 수 있어요. 세그폴트는 그렇게 처리할 수 없어요. 시그널이기 때문에 C++의 catch 블록은 이를 보지 못해요.
Python 프로그램도 그 아래의 C 코드가 실패하면 세그폴트를 낼 수 있어요. 이 코드는 ctypes에게 주소 0을 읽으라고 요청해요.
import ctypes
ctypes.string_at(0)
python3 -X faulthandler로 실행하자 Python 3.12는 Fatal Python error: Segmentation fault와 함께 실행 중이던 Python 줄을 출력했어요. C 확장이나 네이티브 라이브러리에 메모리 버그가 있을 때도 같은 비정상 종료가 일어나요. Rust는 다른 방식을 택해요. 컴파일러가 유효하지 않은 메모리에 접근할 수 있는 코드 대부분을 프로그램을 빌드하기 전에 거부해요.
다음 단계
C 세그멘테이션 오류 가이드는 각 원인을 최소한의 프로그램과 해결책으로 하나씩 살펴봐요. 애초에 이런 실수를 피하려면 포인터, 널 포인터, 스택과 힙을 읽고 C 강좌에서 연습하세요. 메모리를 대신 검사해 주는 언어에서 오류가 어떻게 보고되는지는 런타임 오류 페이지를 참고하세요.
자주 묻는 질문
세그멘테이션 오류는 어떻게 고치나요?
-g로 컴파일해서 gdb나 lldb에서 프로그램을 실행하고 비정상 종료 후 bt를 입력하거나, 자세한 보고서를 얻으려면 -fsanitize=address로 컴파일해요. 그다음 그 줄이 사용하는 포인터나 인덱스를 고쳐요. 포인터가 NULL인지 확인하고, 배열 인덱스를 길이보다 작게 유지하고, free 뒤에는 메모리를 쓰지 말고, 재귀가 반드시 끝나게 하세요.세그멘테이션 오류와 메모리 누수는 같은 것인가요?
free를 잘못 쓰면 세그폴트가 나고, free를 잊으면 누수가 생겨요.왜 세그멘테이션 오류라고 부르나요?
SIGSEGV는 그대로 남았어요. Windows에서는 같은 사건을 액세스 위반(access violation)이라고 불러요.Python에서도 세그멘테이션 오류가 날 수 있나요?
ctypes가 그래요. python -X faulthandler script.py로 실행하면 비정상 종료 때 실행 중이던 Python 줄을 출력해요.종료 코드 139는 무슨 뜻인가요?
SIGSEGV로 종료되었다는 뜻이에요. 셸은 시그널로 인한 종료를 128에 시그널 번호를 더한 값으로 보고하며, 128 + 11 = 139예요. Docker와 Kubernetes에서 139로 종료된 컨테이너는 메인 프로세스가 세그폴트를 낸 거예요.