시그널별 원인 분석 — 각 시그널이 실제로 왜 발생하는가

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' failedassert() 실패
*** 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순위 의심 대상이다.

관련 문서