본문으로 건너뛰기
Postmortem Debugging · 3/6

GDB로 Core 분석 — backtrace·info threads·py 활용

· Hawk · 5분 읽기

core 파일이 손에 들어왔습니다. 어디서 죽었나, 왜 죽었나, 어떤 변수가 잘못된 값을 가졌나. 이 장은 GDB로 core를 분석하는 실전 흐름과 자주 만나는 패턴을 다룹니다.

#열기

Terminal window
$ 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 환경에선.

Terminal window
$ 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 bt
Thread 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) → 정수 / 0
SIGABRT → assert(), abort(), 또는 Sanitizer
SIGILL + 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 auxv
0x21 AT_SYSINFO_EHDR 0x7ffff7ffd000
0x10 AT_HWCAP 0x178bfbff
0x06 AT_PAGESZ 4096
0x11 AT_CLKTCK 100
...
(gdb) info shared
From To Syms Read Shared Object Library
0x00007ffff7da6000 0x00007ffff7dc8b9e Yes (*) /lib64/ld-linux-x86-64.so.2
0x00007ffff7c00000 0x00007ffff7da4690 Yes (*) /lib/x86_64-linux-gnu/libc.so.6
0x00007ffff7d00000 0x00007ffff7dc8000 Yes (*) /usr/lib/x86_64-linux-gnu/libstdc++.so.6
...
(gdb) info proc mappings
process 0
Mapped address spaces:
Start Addr End Addr Size Offset Perms objfile
0x0000555555554000 0x0000555555556000 0x2000 0x0 r--p /usr/local/bin/server
0x0000555555556000 0x000055555555a000 0x4000 0x2000 r-xp /usr/local/bin/server
0x000055555555a000 0x000055555555c000 0x2000 0x6000 r--p /usr/local/bin/server
0x000055555555c000 0x000055555555d000 0x1000 0x8000 r--p /usr/local/bin/server
0x000055555555d000 0x000055555555e000 0x1000 0x9000 rw-p /usr/local/bin/server
0x00007ffff7da6000 0x00007ffff7dca000 0x24000 0x0 r--p /lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
...

info proc mappingsNT_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) list
40 void process_request(Request *req) {
41 log("processing");
42 return req->process(); ← req가 NULL이면 req->process()의 vtable 접근이 0x10 등
(gdb) print req
$2 = (Request *) 0x0

si_addr = 0x10 = NULL + 멤버 offset. NULL 객체에 가상 함수 호출. req가 NULL.

#패턴 2 — 손상된 포인터

(gdb) print $_siginfo
$1 = {si_addr = 0x4141414141414141}
(gdb) bt
#0 0x... at random_function

0x4141414141414141 = "AAAAAAAA"문자열이 포인터 자리에 덮어쓰임. 버퍼 오버플로 또는 use-after-free 의심.

ASan 빌드로 다시 실행하면 정확한 위치 발견.

#패턴 3 — 스택 손상

(gdb) bt
#0 0x4242424242424242 in ?? ()
#1 0x4242424242424242 in ?? ()

return address가 “BBBBBBBB”. 스택 버퍼 오버플로가 RA 덮어씀. 마지막 정상 프레임까지의 콜스택은 추측 어려움.

(gdb) x/100xg $rsp # 스택 dump
0x7fff...0: 0x4242424242424242 0x4242424242424242
0x7fff...10: 0x4242424242424242 0x4242424242424242
0x7fff...20: 0x4242424242424242 0x424242424242420a ← \n으로 끝남
0x7fff...30: 0x0000000000000000 0x0000000000000000

“B” 다수 + \n (0x0a). gets() 또는 strcpy() 같은 길이 검사 없는 입력이 의심됨.

#패턴 4 — 데드락 (hung core)

gcore로 떨어뜨린 hung process.

(gdb) thread apply all bt
Thread 5: #0 __lll_lock_wait ... #1 pthread_mutex_lock ... cache_update
Thread 6: #0 __lll_lock_wait ... #1 pthread_mutex_lock ... logger_write
Thread 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) list
40 void do_validate(int x) {
41 assert(x >= 0 && x < MAX_SIZE);
42 ...
(gdb) print x
$1 = -1

assertion이 실패한 조건변수 값. 일반 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:10

stack 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 main

glibc가 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 on

core 분석 전에 .gdbinit에 설정해 두는 게 편함.

#auto-load pretty-printer

(gdb) info auto-load
...
python-scripts:
Loaded Script
Yes /usr/lib/libstdc++.so.6.0.32-gdb.py

libstdc++ 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가 첫 명령들.
  • $_siginfosi_code + si_addr 분석.
  • 8가지 패턴 — NULL deref / 손상된 포인터 / 스택 손상 / 데드락 / abort / vtable / 재귀 / heap.
  • print + cast로 raw 메모리 해석.
  • set print object on으로 dynamic type.
  • Python으로 자동화.
  • 함수 호출은 core에선 안 됨 — 표현식 평가만.

#다음 장 예고

Ch 4 (마지막) — debuginfod 자동 다운로드, minidump 분석, 자동화된 사고 분석.

#관련 항목