추가로 알아야 할 것들 — dmesg·siginfo_t·ASan·예방 도구
NOTE
core dump 분석은 “이미 터진 뒤”의 사후 부검이다. 여기서는 core dump를 보완하거나, 애초에 프로덕션에서 터지기 전에 잡아내는 방법들을 정리한다.
1. 커널 로그(dmesg/journalctl)도 함께 확인한다
SIGSEGV가 나면 커널이 자체적으로 syslog에 한 줄 남긴다. core dump 설정이 꺼져 있어도 이 로그는 대부분 남기 때문에 1차 단서로 가장 빠르다.
dmesg -T | grep -i segfault
# 또는
journalctl -k | grep -i segfault출력 예:
myprog[12345]: segfault at 0 ip 0000555555555169 sp 00007fffffffe410 error 4 in myprog[555555554000+1000]
| 필드 | 의미 |
|---|---|
at 0 | 접근하려던(폴트난) 메모리 주소 — 0이면 NULL 포인터 역참조 확정 |
ip | 크래시 시점의 명령어 포인터 (어느 명령어에서 터졌는지) |
sp | 크래시 시점의 스택 포인터 |
error 4 | 비트마스크 — 1:쓰기(0=읽기), 2:유저모드(0=커널), 4:페이지 없음(0=권한문제) 등의 조합. error 4=유저모드에서 매핑 안 된 페이지 읽기 |
ip 값을 addr2line -e myprog -f -C <ip 주소>에 넣으면 core dump 없이도 대략적인 함수/라인을 뽑을 수 있다(빌드 시 -g 필요).
TIP
dmesg | grep -i "killed process"로 OOM killer 로그도 같이 확인한다. SIGKILL(137)로 죽었는데 코드에 원인이 안 보인다면 크래시가 아니라 메모리 부족으로 강제 종료된 것일 수 있다 — 1. 비정상 종료 시그널 종류의$?표 참고.
2. 죽기 직전에 스스로 스택을 찍게 만들기 — sigaction + backtrace()
운영 환경에서 core dump 저장 공간이 없거나 정책상 못 남기는 경우, 크래시 시그널 핸들러를 직접 등록해서 죽기 직전에 스택 트레이스를 로그로 남기는 패턴이 실무에서 많이 쓰인다.
#include <execinfo.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
static void crash_handler(int sig, siginfo_t *info, void *ucontext)
{
void *frames[32];
int n = backtrace(frames, 32);
dprintf(STDERR_FILENO, "signal %d, faulting address %p\n", sig, info->si_addr);
backtrace_symbols_fd(frames, n, STDERR_FILENO); /* async-signal-safe */
signal(sig, SIG_DFL); /* 기본 동작 복원 후 재발생 → 정상적으로 core도 남김 */
raise(sig);
}
void install_crash_handler(void)
{
struct sigaction sa = {0};
sa.sa_sigaction = crash_handler;
sa.sa_flags = SA_SIGINFO | SA_RESETHAND;
sigemptyset(&sa.sa_mask);
sigaction(SIGSEGV, &sa, NULL);
sigaction(SIGABRT, &sa, NULL);
sigaction(SIGBUS, &sa, NULL);
sigaction(SIGILL, &sa, NULL);
sigaction(SIGFPE, &sa, NULL);
}WARNING
크래시 핸들러 안에서는 async-signal-safe 함수만 호출 가능하다 —
malloc/free/printf는 내부적으로 락을 잡기 때문에, 크래시 시점에 마침 그 락을 들고 있었다면 핸들러 안에서 데드락에 빠진다.backtrace_symbols_fd(내부에서 malloc 안 함)와dprintf/write처럼 안전한 함수만 쓴다. 이 원칙은 [C] 실무 C 코드 관례와 UB 함정 정리의 시그널 핸들러 관례와 동일한 맥락이다.
3. 터지기 전에 잡는다 — Sanitizer / Valgrind
Core dump·gdb는 이미 터진 뒤의 사후 분석이라 정확한 순간을 못 잡을 때가 많다(특히 힙 손상은 “망가뜨린 시점”과 “터지는 시점”이 다르다). 개발/테스트 단계에서 아래 도구를 상시로 돌리면 SIGABRT/SIGSEGV로 가기 전에 정확한 원인 라인을 알려준다.
| 도구 | 빌드 방법 | 잡아내는 것 |
|---|---|---|
| AddressSanitizer(ASan) | gcc -fsanitize=address -g | 버퍼 오버플로우, Use-After-Free, Double Free — 정확한 발생 라인을 즉시 리포트 |
| UndefinedBehaviorSanitizer(UBSan) | gcc -fsanitize=undefined -g | 정수 오버플로우, 정렬 위반, NULL에 대한 산술 등 UB |
| Valgrind (memcheck) | 별도 빌드 불필요, valgrind ./a.out | 초기화 안 된 값 사용, 메모리 누수, 잘못된 free — ASan보다 느리지만 소스 재컴파일 없이 사용 가능 |
gdb watch | — | 특정 메모리 주소가 “언제, 어느 코드에서” 바뀌는지 실시간 추적 (힙 손상 범위 좁히기) |
재현이 간헐적인 크래시(레이스 컨디션, UAF)는 ASan을 켠 빌드로 스트레스 테스트를 돌리는 게 gdb로 core를 백날 들여다보는 것보다 빠르다.
4. 컴파일러/런타임 하드닝 옵션 — 예방적 방어
| 옵션 | 효과 |
|---|---|
-fstack-protector-strong | 스택 버퍼 오버플로우 시 canary로 감지 → SIGSEGV 대신 명확한 SIGABRT(stack smashing detected)로 전환 |
-D_FORTIFY_SOURCE=2 (+-O1 이상) | strcpy/sprintf/memcpy 등을 버퍼 크기 검사가 들어간 __*_chk 버전으로 치환 |
-fsanitize=address | 위 ASan (개발/테스트 빌드 전용, 런타임 오버헤드 큼 — 프로덕션에는 보통 비활성) |
5. 컨테이너 환경의 함정
core_pattern은 호스트 커널의 전역 설정이라 컨테이너 안에서ulimit -c unlimited만 해서는 부족한 경우가 있다 — 파이프(|systemd-coredump...) 패턴이면 호스트 저널로 가고, 컨테이너 안에서는 안 보인다.- 컨테이너에
--cap-drop이나 seccomp 프로파일이 걸려 있으면 SIGSYS가 늘어난다 — 호스트에서는 멀쩡하던 코드가 컨테이너에서만 죽으면 seccomp부터 의심한다. - core dump를 컨테이너 안에 남기려면 볼륨 마운트 +
core_pattern을 컨테이너에 맞게(상대경로, 절대경로 파이프 회피) 조정해야 한다.
6. 체크리스트 — 크래시 리포트 받았을 때 순서
$?또는 로그의 종료 코드로 어떤 시그널인지 역산 (128+N)dmesg/journalctl에서 커널 로그 확인 (주소, error 비트)- core dump가 있는지 확인 (
coredumpctl list또는core_pattern경로) - core가 없으면 재현 시도 +
ulimit -c unlimited로 core 생성을 유도하거나, ASan 빌드로 재현 gdb로bt full→ 크래시 지점의 변수 상태 확인- 원인 패턴을 2. 시그널별 원인 분석의 표와 대조
- 수정 후 회귀 방지: 같은 입력으로 ASan/UBSan 빌드 테스트를 CI에 추가
관련 문서
- 1. 비정상 종료 시그널 종류
- 2. 시그널별 원인 분석
- 3. Core Dump 확인 방법
- [C] 실무 C 코드 관례와 UB 함정 정리 — async-signal-safe 함수, 시그널 핸들러 관례
- [C] 리눅스 시스템 함수(System Call) 카테고리별 치트시트