시그널별 원인 분석 — 각 시그널이 실제로 왜 발생하는가
NOTE
시그널 이름은 “무엇으로 죽었는지”만 알려준다. “왜” 죽었는지는 코드 패턴을 알아야 좁힐 수 있다. 아래는 시그널별로 실무에서 가장 자주 만나는 원인들을 빈도순으로 정리한 것이다.
SIGSEGV — Segmentation Fault (번호 11)
매핑되지 않았거나 접근 권한이 없는 메모리 주소에 접근했을 때 발생한다. 가장 흔한 크래시 시그널.
si_code로 보는 2가지 하위 유형
sigaction(SA_SIGINFO)로 잡으면 siginfo_t.si_code로 구분 가능하다.
| si_code | 의미 |
|---|---|
SEGV_MAPERR | 주소 자체가 어떤 영역에도 매핑돼 있지 않음 (NULL 역참조가 대표적) |
SEGV_ACCERR | 주소는 매핑돼 있지만 접근 권한이 없음 (읽기전용 영역에 쓰기 등) |
원인 후보 (빈도순)
1. NULL 포인터 역참조
char *p = NULL;
strcpy(p, "hi"); /* p가 가리키는 주소 0 근처는 항상 매핑 안 됨 → SEGV_MAPERR */malloc/strdup 등의 반환값을 NULL 체크 없이 바로 쓰는 경우가 대부분.
2. Use-After-Free (댕글링 포인터)
free(p);
/* ... */
printf("%s", p->name); /* p가 가리키던 메모리는 이미 반납됨 */free 직후엔 우연히 값이 남아있어 안 죽다가, 다른 malloc이 그 자리를 재사용한 뒤에야 죽는 경우가 많아 재현이 불규칙한 게 특징이다. 동적 할당 소유권의 소유권 규칙을 어겼을 때 전형적으로 나타난다.
3. 버퍼 오버플로우로 매핑 밖까지 침범
char buf[16];
strcpy(buf, very_long_untrusted_string); /* 스택 인접 영역을 넘어 매핑 끝까지 밀고 나감 */작은 오버플로우는 스택의 다른 변수만 깨뜨려 조용히 넘어가지만, 충분히 크면 매핑 안 된 페이지까지 넘어가 SIGSEGV로 이어진다.
4. 스택 오버플로우 (재귀/거대 지역 변수)
void recurse(int n) { char big[1024]; recurse(n + 1); } /* 종료조건 없음 */스택 끝에는 가드 페이지(guard page) 가 붙어 있어서, 스택이 다 자라 가드 페이지를 건드리는 순간 SIGSEGV가 난다. gdb에서 bt를 찍었을 때 같은 프레임이 수백~수천 번 반복되면 바로 이 케이스로 판단할 수 있다.
5. 읽기 전용 영역에 쓰기 (SEGV_ACCERR)
char *s = "literal"; /* .rodata 섹션 */
s[0] = 'X'; /* 쓰기 권한 없음 */6. 함수 포인터 오염 / 구조체 캐스팅 실수
handler_t h = table[bad_index]; /* 배열 밖의 쓰레기 값을 함수포인터로 오인 */
h();7. 초기화 안 된 포인터(wild pointer)
struct node *n; /* 초기화 없음 — 스택의 쓰레기 값 */
n->next = NULL; /* 임의 주소에 쓰기 */SIGABRT — Abort (번호 6)
프로그램/런타임이 스스로 abort()를 호출해서 죽는 경우다. SIGSEGV와 달리 원인 메시지가 stderr에 같이 찍히는 경우가 많아 여기 나온 문자열이 원인 분석의 가장 확실한 단서가 된다.
| stderr 메시지 | 원인 |
|---|---|
Assertion 'x' failed | assert() 실패 |
*** stack smashing detected ***: terminated | 스택 버퍼 오버플로우로 canary 훼손 (-fstack-protector) |
*** buffer overflow detected *** | _FORTIFY_SOURCE가 strcpy/sprintf 등에서 오버플로우 탐지 |
free(): invalid pointer | 이미 free된 포인터를 또 free하거나, malloc으로 받지 않은 포인터를 free |
double free or corruption | 같은 포인터를 두 번 free (Double Free) |
malloc(): corrupted top size / corrupted double-linked list | 힙 메타데이터가 버퍼 오버플로우로 훼손됨 — 실제 원인은 훨씬 이전의 힙 오버플로우 |
terminate called after throwing an instance of ... | (C++ 혼용 시) 처리 안 된 예외 |
WARNING
SIGABRT의 스택 트레이스 최상단은 대부분
abort()/malloc/free내부다. 진짜 원인 코드는 그보다 훨씬 이전에 실행된 다른 함수인 경우가 많다 — 힙 손상은 “터진 시점”과 “망가뜨린 시점”이 다르다는 걸 항상 염두에 둔다.
SIGBUS — Bus Error (번호 7)
하드웨어(버스) 레벨에서 접근이 거부된 경우다. SIGSEGV보다 드물지만 원인이 더 명확한 편이다.
- 정렬(alignment) 안 된 접근 — 일부 아키텍처(ARM 구버전, SPARC)는 4/8바이트 경계에 안 맞는 주소 접근 시 하드웨어가 즉시 거부한다. x86_64는 대체로 관대해서 잘 안 보인다.
mmap한 파일의 실제 크기를 넘어선 접근 — 파일을 매핑한 뒤 파일이 다른 프로세스에 의해 잘렸거나(truncate), 매핑 크기보다 작은 파일을 매핑해놓고 뒷부분을 읽는 경우.- 물리적으로 존재하지 않는 메모리 접근 (임베디드/드라이버 환경)
SIGILL — Illegal Instruction (번호 4)
CPU가 디코딩할 수 없거나 현재 권한에서 실행 불가능한 명령어를 만났을 때 발생한다.
- 함수 포인터가 코드가 아닌 데이터/쓰레기 값을 가리켜 그리로 점프 — 대부분 스택/힙 오버플로우로 리턴 주소나 함수 포인터가 덮어써진 결과다.
- 스택 오버플로우로 조작된 리턴 주소가 NX(실행 방지) 영역을 가리키는 경우 — 데이터 영역(스택/힙)으로 점프시켰는데 실행 보호가 걸려 있어 즉시 SIGILL로 이어진다.
- 손상된 바이너리/라이브러리, 아키텍처가 다른 명령어 세트 실행 (드묾)
SIGFPE — 이름과 달리 정수 연산도 포함 (번호 8)
- 정수를 0으로 나누기 —
x / 0,x % 0 INT_MIN / -1— 결과가INT_MAX를 넘어가는 오버플로우라 하드웨어 예외가 발생한다 (부호 있는 정수 나눗셈의 함정으로, 은근히 자주 놓친다)- 부동소수점 자체는 기본적으로
NaN/Inf를 반환할 뿐 예외를 안 던지는 경우가 많아, 실무에서 SIGFPE는 거의 항상 정수 나눗셈 문제다.
SIGSYS — 허용 안 된 시스템 콜 (번호 31)
seccomp 필터(컨테이너, 샌드박스)가 화이트리스트에 없는 시스템 콜 호출을 차단할 때 발생한다. 컨테이너 환경으로 옮긴 뒤에만 재현되는 크래시라면 1순위 의심 대상이다.
관련 문서
- 1. 비정상 종료 시그널 종류
- 3. Core Dump 확인 방법 — 활성화·수집·gdb 분석 — 여기 나온 후보들을 실제 증거로 좁히는 법
- [C] 동적 할당 소유권 — caller free vs callee create·destroy 쌍 — UAF·Double Free가 생기는 소유권 위반 패턴
- [C] 문자열 비교와 strcasecmp — strcmp 차이, 대소문자 무시 및 컴파일러 속성 해독 — 런타임 SIGSEGV 방어 패턴 예시