GDB로 Core 분석 — backtrace·info threads·py 활용
core 파일이 손에 들어왔습니다. 어디서 죽었나, 왜 죽었나, 어떤 변수가 잘못된 값을 가졌나. 이 장은 GDB로 core를 분석하는 실전 흐름과 자주 만나는 패턴을 다룹니다.
#열기
$ gdb /usr/local/bin/server /var/crash/core.server.1234순서가 실행 파일, core. 실행 파일을 안 주면 GDB는 함수 이름을 모름 (주소만 보임).
GDB는 core의 NT_PRSTATUS, NT_FILE, NT_AUXV를 읽어 자동으로:
- 스레드 정보 (각 NT_PRSTATUS로).
- 매핑된 라이브러리 (NT_FILE).
- 실행 환경 (NT_AUXV).
Reading symbols from /usr/local/bin/server...[New LWP 12346][New LWP 12347][New LWP 12348][Thread debugging using libthread_db enabled]Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".Core was generated by `/usr/local/bin/server --config=prod.yaml'.Program terminated with signal SIGSEGV, Segmentation fault.#0 0x00005555... in std::__throw_length_error at stdexcept.cc:42마지막 줄에 어디서 죽었는지. 콜스택 가장 안쪽 프레임.
#coredumpctl debug — 자동
systemd 환경에선.
$ coredumpctl list$ coredumpctl debug 12345[GDB 자동 실행 + 매칭]debug 명령이 실행 파일·debuginfo 자동 매칭. 가장 편한 진입점.
#첫 명령들
#bt — 죽은 곳
(gdb) bt#0 0x00005555... in std::__throw_length_error at stdexcept.cc:42#1 0x00005555... in std::vector::reserve at vector.h:281#2 0x00005555... in load_config at config.cpp:88#3 0x00005555... in main at main.cpp:23가장 아래가 프로그램의 진입점 (main), 위로 갈수록 깊은 호출. #0이 죽은 함수.
#bt full
(gdb) bt full#0 0x00005555... in std::__throw_length_error at /usr/include/c++/11/bits/stdexcept_msg.cc:42 msg = "vector::reserve"#1 0x00005555... in std::vector<...>::reserve (this=0x7fff1234, n=18446744073709551615) at /usr/include/c++/11/bits/vector.tcc:281 ...#2 0x00005555... in load_config (path=0x7fff5678 "...") at config.cpp:88 f = ... line = "..." items = std::vector of size 0#3 ...각 프레임의 모든 지역 변수. 인자 값에서 문제 파악: n=18446744073709551615 (= (unsigned)-1) — 너무 큰 reserve 요청. 어디서 그 값이 왔는지 위 프레임 탐색.
#info threads — 다른 스레드
(gdb) info threads Id Target Id Frame* 1 Thread 0x... (LWP 12345) "main" std::__throw_length_error 2 Thread 0x... (LWP 12346) "worker" __pthread_cond_wait 3 Thread 0x... (LWP 12347) "worker" __pthread_cond_wait 4 Thread 0x... (LWP 12348) "io" epoll_wait죽은 건 main (*). 다른 스레드들은 대기 중이었음.
#thread apply all bt
(gdb) thread apply all btThread 4 (LWP 12348): #0 epoll_wait #1 io_loop ...Thread 3 (LWP 12347): #0 __pthread_cond_wait #1 worker_loop ...Thread 2 (LWP 12346): #0 __pthread_cond_wait #1 worker_loop ...Thread 1 (LWP 12345): #0 std::__throw_length_error #1 ... #2 load_config #3 main모든 스레드의 콜스택을 동시에. 멀티스레드 사고 분석의 첫 명령.
#siginfo — 죽음의 정체
(gdb) print $_siginfo$1 = {si_signo = 11, si_errno = 0, si_code = 1, _sifields = {_sigfault = {si_addr = 0xdeadbeef}}}si_signo = 11 (SIGSEGV). si_code = 1 (SEGV_MAPERR — 매핑 안 됨). si_addr = 0xdeadbeef (접근 주소).
#si_code 해독
$_siginfo.si_code:
- SIGSEGV: 1=SEGV_MAPERR, 2=SEGV_ACCERR, 6=SEGV_BNDERR, 7=SEGV_PKUERR
- SIGBUS: 1=BUS_ADRALN, 2=BUS_ADRERR, 3=BUS_OBJERR, 4=BUS_MCEERR_AR
- SIGILL: 1=ILL_ILLOPC, 2=ILL_ILLOPN, 3=ILL_ILLADR, 4=ILL_ILLTRP,
- 5=ILL_PRVOPC, 6=ILL_PRVREG, 7=ILL_COPROC, 8=ILL_BADSTK
- SIGFPE: 1=FPE_INTDIV (정수 / 0), 2=FPE_INTOVF, 3=FPE_FLTDIV,
- 4=FPE_FLTOVF, 5=FPE_FLTUND, 6=FPE_FLTRES, 7=FPE_FLTINV,
- 8=FPE_FLTSUB
#정확한 진단
SIGSEGV + si_code=1 (MAPERR) + si_addr=0x0 → NULL 역참조SIGSEGV + si_code=1 + si_addr=high address → 손상된 포인터SIGSEGV + si_code=2 (ACCERR) → 권한 (RO에 쓰기)SIGBUS + si_code=1 (ADRALN) → unaligned access (ARM에서 흔함)SIGFPE + si_code=1 (INTDIV) → 정수 / 0SIGABRT → assert(), abort(), 또는 SanitizerSIGILL + si_code=1 → 손상된 함수 포인터 또는 코드si_addr이 프로그램 주소면 그 메모리에 무엇이 있나 확인.
(gdb) info proc mappings 0xdeadbeef영역에 없으면 완전 잘못된 포인터. 매핑 안에 있으면 권한 또는 stale data.
#프레임 이동
(gdb) frame 2(gdb) bt#2 0x00005555... in load_config (path=0x7fff5678 "...") at config.cpp:88
(gdb) list # 그 줄 주변 소스83 file f(path);84 if (!f) {85 throw std::runtime_error(...);86 }87 std::string line;88 items.reserve(get_size(line)); # 죽은 곳89 while (std::getline(f, line)) {90 items.push_back(parse(line));
(gdb) print get_size("...") # 안 됨 — 라이브 호출 불가print 표현식은 그 시점의 메모리만 가능. 함수 호출은 불가 — core에선 CPU가 정지 상태가 아니라 사망 상태라 새 코드 실행이 안 됨.
(gdb) print items # 변수 보기$2 = std::vector of size 0(gdb) print line$3 = "garbage value here"(gdb) print path$4 = 0x7fff5678 "/etc/myapp/config.yaml"get_size(line)이 (size_t)-1 같은 거대한 값을 반환했음을 추정 가능.
#info — 메타 정보
(gdb) info auxv0x21 AT_SYSINFO_EHDR 0x7ffff7ffd0000x10 AT_HWCAP 0x178bfbff0x06 AT_PAGESZ 40960x11 AT_CLKTCK 100...
(gdb) info sharedFrom To Syms Read Shared Object Library0x00007ffff7da6000 0x00007ffff7dc8b9e Yes (*) /lib64/ld-linux-x86-64.so.20x00007ffff7c00000 0x00007ffff7da4690 Yes (*) /lib/x86_64-linux-gnu/libc.so.60x00007ffff7d00000 0x00007ffff7dc8000 Yes (*) /usr/lib/x86_64-linux-gnu/libstdc++.so.6...
(gdb) info proc mappingsprocess 0Mapped address spaces:
Start Addr End Addr Size Offset Perms objfile0x0000555555554000 0x0000555555556000 0x2000 0x0 r--p /usr/local/bin/server0x0000555555556000 0x000055555555a000 0x4000 0x2000 r-xp /usr/local/bin/server0x000055555555a000 0x000055555555c000 0x2000 0x6000 r--p /usr/local/bin/server0x000055555555c000 0x000055555555d000 0x1000 0x8000 r--p /usr/local/bin/server0x000055555555d000 0x000055555555e000 0x1000 0x9000 rw-p /usr/local/bin/server0x00007ffff7da6000 0x00007ffff7dca000 0x24000 0x0 r--p /lib/x86_64-linux-gnu/ld-linux-x86-64.so.2...info proc mappings이 NT_FILE 노트에서 옴. 모든 매핑을 그대로.
info shared이 동적 라이브러리만. Syms Read가 심볼 로드 여부 — No (*)면 debuginfo 없음.
#변수 검사
#단순
(gdb) print x(gdb) print obj.field(gdb) print *p # deref(gdb) print arr[5](gdb) print arr@10 # array 10개#메모리 dump
(gdb) x/16xb 0x7ffd1234 # 16 바이트 hex(gdb) x/4xw 0x7ffd1234 # 4 워드 (4바이트) hex(gdb) x/16i 0x401200 # 16 명령어 디스어셈블(gdb) x/s 0x... # NUL-terminated 문자열#Cast
(gdb) print (char *)0x7ffd1234(gdb) print (struct sockaddr_in *)0x7ffd1234(gdb) print *(MyClass *)0x7ffd1234타입이 사라진 raw 메모리를 해석. 자체 자료 구조 분석에 매우 유용.
#자주 만나는 패턴
#패턴 1 — NULL 역참조
(gdb) bt#0 0x... in process_request at handler.cpp:42
(gdb) print $_siginfo$1 = {si_signo = 11, si_code = 1, si_addr = 0x10}
(gdb) frame 0(gdb) list40 void process_request(Request *req) {41 log("processing");42 return req->process(); ← req가 NULL이면 req->process()의 vtable 접근이 0x10 등
(gdb) print req$2 = (Request *) 0x0si_addr = 0x10 = NULL + 멤버 offset. NULL 객체에 가상 함수 호출. req가 NULL.
#패턴 2 — 손상된 포인터
(gdb) print $_siginfo$1 = {si_addr = 0x4141414141414141}
(gdb) bt#0 0x... at random_function0x4141414141414141 = "AAAAAAAA" — 문자열이 포인터 자리에 덮어쓰임. 버퍼 오버플로 또는 use-after-free 의심.
ASan 빌드로 다시 실행하면 정확한 위치 발견.
#패턴 3 — 스택 손상
(gdb) bt#0 0x4242424242424242 in ?? ()#1 0x4242424242424242 in ?? ()return address가 “BBBBBBBB”. 스택 버퍼 오버플로가 RA 덮어씀. 마지막 정상 프레임까지의 콜스택은 추측 어려움.
(gdb) x/100xg $rsp # 스택 dump0x7fff...0: 0x4242424242424242 0x42424242424242420x7fff...10: 0x4242424242424242 0x42424242424242420x7fff...20: 0x4242424242424242 0x424242424242420a ← \n으로 끝남0x7fff...30: 0x0000000000000000 0x0000000000000000“B” 다수 + \n (0x0a). gets() 또는 strcpy() 같은 길이 검사 없는 입력이 의심됨.
#패턴 4 — 데드락 (hung core)
gcore로 떨어뜨린 hung process.
(gdb) thread apply all btThread 5: #0 __lll_lock_wait ... #1 pthread_mutex_lock ... cache_updateThread 6: #0 __lll_lock_wait ... #1 pthread_mutex_lock ... logger_writeThread 7: #0 __lll_lock_wait ... #1 pthread_mutex_lock ... cache_update...여러 스레드가 락 대기.
(gdb) thread 5(gdb) frame 1(gdb) print *mutex$1 = {__data = {__owner = 12350, ...}} # Thread N (LWP 12350)__owner가 그 락을 든 LWP. 각 스레드의 어느 락을 기다리는지 + 어느 락을 들고 있는지 그래프.
#패턴 5 — abort() / assert
(gdb) bt#0 __pthread_kill_implementation at pthread_kill.c#1 __GI_raise at raise.c#2 __GI_abort at abort.c#3 __assert_fail_base at assert.c#4 __GI___assert_fail at assert.c#5 do_validate at validator.cpp:42#6 process at handler.cpp:60
(gdb) frame 5(gdb) list40 void do_validate(int x) {41 assert(x >= 0 && x < MAX_SIZE);42 ...(gdb) print x$1 = -1assertion이 실패한 조건과 변수 값. 일반 SIGSEGV보다 명확한 진단.
#패턴 6 — 가상 함수 호출 — vtable 손상
(gdb) bt#0 0x4141414141414141 in ??
(gdb) frame 1(gdb) print *obj$1 = {_vptr.MyClass = 0x4141414141414141, ...} # vtable 포인터가 garbage객체의 vtable이 덮어쓰임. use-after-free의 표준 증상.
#패턴 7 — 무한 재귀
(gdb) bt#0 recursive_fn at f.cpp:5#1 recursive_fn at f.cpp:5...#499 recursive_fn at f.cpp:5#500 main at main.cpp:10stack overflow로 SIGSEGV. bt가 수천 프레임.
(gdb) bt 5 # 처음 5개만(gdb) bt -5 # 끝 5개만 (가장 오래된)마지막 정상 프레임을 보고 언제 재귀 시작했는지.
#패턴 8 — heap corruption
(gdb) bt#0 __libc_message#1 malloc_printerr "free(): invalid pointer"#2 free#3 ~MyClass#4 mainglibc가 heap 무결성 검사에서 실패 → abort. free 받은 포인터가 이상함. double free 또는 heap 헤더 손상.
ASan으로 다시 실행 → 정확한 alloc 사이트 + free 사이트 찾기.
#set print 옵션
(gdb) set print pretty on # 들여쓰기(gdb) set print object on # vtable로 dynamic type(gdb) set print array on # 배열 한 줄로(gdb) set print array-indexes on(gdb) set print elements 100 # 컨테이너 표시 한도(gdb) set print depth 5 # 중첩 깊이(gdb) set print null-stop on # NUL에서 string 자르기(gdb) set print address oncore 분석 전에 .gdbinit에 설정해 두는 게 편함.
#auto-load pretty-printer
(gdb) info auto-load...python-scripts:Loaded ScriptYes /usr/lib/libstdc++.so.6.0.32-gdb.pylibstdc++ pretty-printer가 자동 로드. print v 했을 때 std::vector of size 3 = {1, 2, 3} 형태.
자체 라이브러리에 libfoo.so-gdb.py를 같이 두면 자동 적용.
#Python 명령으로 자동화
import gdb
class CoreInfo(gdb.Command): """core dump 종합 정보.""" def __init__(self): super().__init__("core-info", gdb.COMMAND_USER) def invoke(self, arg, from_tty): # 죽음의 정체 siginfo = gdb.parse_and_eval("$_siginfo") print(f"\n=== Death info ===") print(f"signal: {int(siginfo['si_signo'])}") print(f"si_code: {int(siginfo['si_code'])}") try: addr = int(siginfo['_sifields']['_sigfault']['si_addr']) print(f"si_addr: {addr:#x}") except gdb.error: pass
# 모든 스레드 print(f"\n=== Threads ===") for thread in gdb.selected_inferior().threads(): thread.switch() frame = gdb.newest_frame() print(f"Thread {thread.num}: {frame.name() or '??'}")
# 메모리 매핑 print(f"\n=== Mappings ===") gdb.execute("info proc mappings", to_string=False)
CoreInfo()(gdb) core-info 한 번에 모든 정보.
#정리
gdb <exe> <core>로 열기 또는coredumpctl debug.bt/bt full/info threads/thread apply all bt가 첫 명령들.$_siginfo로 si_code + si_addr 분석.- 8가지 패턴 — NULL deref / 손상된 포인터 / 스택 손상 / 데드락 / abort / vtable / 재귀 / heap.
print+ cast로 raw 메모리 해석.set print object on으로 dynamic type.- Python으로 자동화.
- 함수 호출은 core에선 안 됨 — 표현식 평가만.
#다음 장 예고
Ch 4 (마지막) — debuginfod 자동 다운로드, minidump 분석, 자동화된 사고 분석.
#관련 항목
- Ch 2: ELF core 포맷
- Ch 4: debuginfod / minidump / 자동화
- GDB Examining Core Files
- Concurrency Debugging Ch 4: 데드락 방법론
- Sanitizers Ch 1: AddressSanitizer — heap 손상 자동 검출
Postmortem Debugging · 3 of 6
관련 글
CXL 디바이스 Core Dump 분석 — Device State·Mailbox Log·NUMA 토폴로지
CXL 디바이스가 fail한 후 core dump에서 device state·mailbox 명령 이력·NUMA 토폴로지를 복원하는 분석 흐름.
ELF Core 파일 포맷 분해 — NT_PRSTATUS·NT_PRPSINFO·NT_FILE
core dump의 내부 구조. PT_NOTE/PT_LOAD, NT_PRSTATUS, NT_FILE, NT_AUXV.
CXL Fabric Postmortem — 분산 디바이스·Multi-Host Pool 장애 추적
CXL 2.0/3.x fabric에서 multi-host pooled 디바이스 fail 분석 — Fabric Manager log·LD 상태·cross-host correlation.