GDB·LLDB Backtrace와 프레임 이동 — Call Stack 분석
#호출 스택은 디버깅의 지도
프로그램이 어떤 자리에 멈췄을 때, 어떻게 거기까지 왔는지를 알아야 합니다. 답이 호출 스택 (call stack)이고, 디버거가 보여 주는 게 backtrace.
void deepest() { *(int*)0 = 1; } // SIGSEGV
void deeper() { deepest(); }void deep() { deeper(); }int main() { deep(); return 0; }(gdb) runProgram received signal SIGSEGV, Segmentation fault.0x000055555555512e in deepest () at crash.c:1
(gdb) backtrace#0 0x000055555555512e in deepest () at crash.c:1#1 0x0000555555555140 in deeper () at crash.c:3#2 0x000055555555514c in deep () at crash.c:4#3 0x0000555555555158 in main () at crash.c:5충돌이 deepest에서 일어났고, 거기까지 deeper→deep→main 순으로 왔다는 사실이 한눈에. 이게 bt의 가치입니다.
#backtrace 변형
(gdb) backtrace # 전체 스택(gdb) bt # 약어(gdb) bt 5 # 안쪽 5 프레임만(gdb) bt -5 # 바깥 5 프레임만(gdb) bt full # 변수 값까지 포함(gdb) bt no-filters # 필터 비활성화 (Python 필터 제거)LLDB:
(lldb) thread backtrace(lldb) bt(lldb) bt 5(lldb) bt all # 모든 스레드#bt full — 변수 포함
(gdb) bt full#0 factorial (n=3) at hello.c:4 result = <optimized out>#1 factorial (n=4) at hello.c:5 result = 6#2 factorial (n=5) at hello.c:5 result = 24#3 main () at hello.c:10 n = 5 result = <optimized out>각 프레임의 지역 변수가 다 같이 출력. 충돌 시점의 전체 컨텍스트를 빠르게 파악.
비용: 큰 함수나 STL 객체는 출력이 매우 길어집니다. 전체보다 *특정 프레임 들어가서 info locals*가 보통 빠름.
#frame — 특정 프레임 이동
(gdb) frame 2 # 2번 프레임으로(gdb) f 2 # 약어
#2 0x000055555555514c in deep () at crash.c:4프레임을 현재 위치로 설정. 그 프레임 안의 변수와 인자를 볼 수 있게 됩니다.
(gdb) frame 2(gdb) info locals(gdb) info args(gdb) print local_var # 그 프레임의 변수LLDB:
(lldb) frame select 2(lldb) f 2(lldb) frame variable # = info locals + info args#up / down — 상대적 이동
(gdb) up # 한 단계 위 (호출자)(gdb) up 3 # 3단계 위(gdb) down # 한 단계 아래 (피호출자)(gdb) down 2up은 스택 깊이 줄임(호출자 방향), down은 깊어짐. bt에서 번호가 큰 쪽이 깊은 호출이고, up은 번호 증가, down은 감소.
이름이 헷갈리기 쉽습니다. 외울 때:
- 호출자(caller) =
up(먼저 호출한 쪽 = 위) - 피호출자(callee) =
down
#info frame — 프레임 상세 정보
(gdb) info frameStack level 0, frame at 0x7fffffffe340: rip = 0x55555555512e in deepest (crash.c:1); saved rip = 0x555555555145 called by frame at 0x7fffffffe350 source language c. Arglist at 0x7fffffffe330, args: Locals at 0x7fffffffe330, Previous frame's sp is 0x7fffffffe340 Saved registers: rbp at 0x7fffffffe330, rip at 0x7fffffffe338각 항목:
rip— 현재 명령어 포인터.saved rip— 호출자로 돌아갈 주소.called by frame at— 호출자의 프레임 위치.Arglist— 함수 인자가 위치한 메모리.Saved registers— 호출 전 저장된 레지스터들의 메모리 위치.
이 정보는 프레임 포인터를 따라가는 low-level 디버깅에서 필요. 보통은 bt로 충분.
LLDB:
(lldb) frame info#인라인 함수의 까다로움
inline int small() { return 42; }
int main() { int x = small(); // 컴파일러가 small() 인라인 가능 return x;}-O2에서 small()이 인라인되면, 디버거가 현재 자리가 어디인지 헷갈릴 수 있습니다.
(gdb) bt#0 small () at inline.c:1#1 main () at inline.c:5GDB 7+는 인라인 표시를 잘합니다. #0이 진짜 스택 프레임이 아니라 “main 안의 인라인된 자리”라는 의미.
(gdb) info frameInlined frame, no frame info.인라인 프레임은 진짜 스택이 없음. 변수 일부가 <optimized out> 가능. 정확한 분석을 위해 -O0 빌드 권장.
#set print frame-info source-and-location
(gdb) bt#0 0x... small () at inline.c:1#1 0x... main () at inline.c:5기본 출력에 소스 위치가 같이. 외부 라이브러리가 심볼만 있고 소스 없을 때 구분 도움.
#최적화 코드의 스택 — frame 안의 함정
-O2로 컴파일하면 tail call optimization이 적용될 수 있습니다.
int helper() { return compute(); } // tail call
int main() { return helper(); }(gdb) bt#0 compute () at opt.c:5#1 main () at opt.c:8 # ← helper가 사라짐!helper가 스택에 안 보입니다. tail call로 main → compute 직접 jump한 것처럼 보임. 이게 디버깅을 어렵게 합니다.
해결:
-fno-optimize-sibling-calls컴파일 옵션.- 또는
-O0/-Og빌드.
#재귀의 backtrace
(gdb) bt#0 factorial (n=0) at fact.c:4#1 factorial (n=1) at fact.c:5#2 factorial (n=2) at fact.c:5#3 factorial (n=3) at fact.c:5#4 factorial (n=4) at fact.c:5#5 factorial (n=5) at fact.c:5#6 main () at fact.c:10각 재귀 호출이 별도 프레임. 깊은 재귀에서 스택 오버플로가 나면 backtrace에서 수천 프레임 보일 수 있습니다.
(gdb) bt 20 # 안쪽 20만너무 깊을 때 최근 N만 보면 패턴 파악 가능.
#모든 스레드의 backtrace
(gdb) thread apply all backtraceThread 4 (Thread 0x7fff...):#0 ...#1 ...
Thread 3 (Thread 0x7fff...):#0 ...#1 ...
Thread 2 (Thread 0x7fff...):...
Thread 1 (Thread 0x7fff...):...모든 스레드의 현재 위치. 데드락이나 어느 스레드에서 멈췄는지 모를 때 결정적.
LLDB:
(lldb) thread backtrace all(lldb) bt all자세한 멀티스레드 디버깅은 Ch 6에서.
#부분 스택 — 깨진 스택의 복구
스택이 심하게 손상되면 GDB가 깊이 따라가지 못합니다.
(gdb) bt#0 0x000055555555512e in deepest () at crash.c:1Backtrace stopped: previous frame inner to this frame (corrupt stack?)원인: 스택 오버런이 backtrace 정보까지 망가뜨림.
해결 시도:
bt no-filters— Python 필터 비활성화. 가끔 깨진 자리 통과.info registers— 수동으로 스택 따라가기.rbp,rsp로 프레임 추적.- 메모리 검사 —
x/64xg $rsp로 스택 내용 확인.
깨진 스택은 근본 원인이 따로 있습니다 (buffer overflow). 디버거로 그 자리를 찾는 게 목표.
#외부 라이브러리 프레임 숨기기
(gdb) skip function pthread_*(gdb) skip function __libc_*bt에서 pthread_ 와 _libc* 프레임을 숨김. 우리 코드만 보고 싶을 때.
(gdb) info skip(gdb) skip disable 1 # 임시 비활성화(gdb) skip enable 1step이나 next도 건너뛴 함수는 안 들어감. 외부 라이브러리에 들어가지 않고 한 줄씩 진행.
#coredump의 backtrace
$ gdb ./myapp coreCore was generated by `./myapp'.Program terminated with signal SIGSEGV, Segmentation fault.#0 0x000055555555512e in deepest () at crash.c:1
(gdb) bt#0 0x000055555555512e in deepest () at crash.c:1#1 0x0000555555555140 in deeper () at crash.c:3#2 0x000055555555514c in deep () at crash.c:4#3 0x0000555555555158 in main () at crash.c:5코어 덤프는 프로세스 죽은 순간의 메모리 스냅샷. 거기서도 bt가 정상 동작. 프로덕션 사고 분석의 기본.
자세한 코어 덤프 분석은 Ch 7에서.
#디버깅 시나리오 — 실전 예시
#시나리오 1: 충돌 자리 찾기
SIGSEGV 발생 직후 backtrace로 null 포인터를 받은 frame을 찾고, up으로 누가 보냈는지 거슬러 올라갑니다.
$ gdb ./myapp(gdb) runProgram received signal SIGSEGV.
(gdb) bt#0 process_node (n=0x0) at tree.c:42#1 walk_tree () at tree.c:88#2 main () at main.c:15
(gdb) frame 0(gdb) info argsn = 0x0
(gdb) up#1 walk_tree () at tree.c:88(gdb) listlist로 walk_tree의 어디에서 process_node(NULL)을 호출했는지 봅니다.
#시나리오 2: 깊은 재귀 분석
100단계 깊은 재귀에서 한 단계마다 인자가 어떻게 변하는지 봅니다. 각 단계마다 잘 줄어드는지 확인.
(gdb) bt#0~99: 100단계 깊은 재귀#100: main
(gdb) frame 0(gdb) info args # n = 0(gdb) frame 10(gdb) info args # n = 10(gdb) frame 50(gdb) info args # n = 50#시나리오 3: 데드락 진단
두 스레드가 서로 다른 mutex를 기다린다 → 데드락.
(gdb) thread apply all btThread 4: 멈춰 있음#0 __lll_lock_wait ()#1 pthread_mutex_lock (mutex=0x... <m1>)#2 worker (m1, m2) ...
Thread 3: 멈춰 있음#0 __lll_lock_wait ()#1 pthread_mutex_lock (mutex=0x... <m2>)#2 worker (m2, m1) ...#공유 라이브러리 프레임의 함정
(gdb) bt#0 0x00007ffff7d... in malloc () from /lib/x86_64-linux-gnu/libc.so.6#1 0x00007ffff7d... in operator new (sz=24) at libsupc++/new_op.cc:50#2 std::vector<int, ...>::_M_default_append (this=0x...) ...라이브러리 함수의 심볼만 있고 줄 번호 없는 경우 — from /lib/.... 디버그 정보가 없는 라이브러리에서 호출됨.
해결: 디버그 심볼 패키지 설치.
# Ubuntu/Debiansudo apt install libc6-dbg libstdc++6-dbgsym
# Fedora/RHELsudo dnf debuginfo-install glibc libstdc++이러면 bt에 .c 파일과 줄 번호도 같이 나옵니다.
#정리
backtrace— 호출 스택 보기.bt full은 변수 포함.frame N/up/down— 프레임 이동. caller = up, callee = down.info frame— 프레임의 low-level 정보.- 인라인 함수는 별도 프레임으로 표시되지만 진짜 스택 아님.
- Tail call optimization으로 프레임이 사라질 수 있음.
-fno-optimize-sibling-calls또는-O0. thread apply all bt— 모든 스레드의 스택. 데드락 진단 필수.skip function— 외부 라이브러리 프레임 숨김.- 코어 덤프에서도
bt동작. - 깨진 스택은 근본 원인이 따로 — 보통 buffer overflow.
#다음 장 예고
Ch 5: Breakpoint와 Watchpoint에서는 멈출 자리를 더 깊이 다룹니다. 조건부 break의 변형, watchpoint(변수 변경 시 멈춤), hardware vs software breakpoint.
#참고 자료
GDB and LLDB · 4 of 12
- 1GDB vs LLDB 분석 — 두 디버거의 설치·차이·선택 기준
- 2GDB·LLDB 기본 명령 — break·step·next·print 동작 비교
- 3디버거로 상태 들여다보기 — 변수·메모리·레지스터·STL 추적
- 4GDB·LLDB Backtrace와 프레임 이동 — Call Stack 분석
- 5Breakpoint와 Watchpoint 분석 — Conditional·Hardware·Catchpoint
- 6멀티스레드·멀티프로세스 디버깅 — Non-Stop·Scheduler-Locking·Fork
- 7Core Dump 분석 기법 — gcore·coredumpctl·디버거 활용
- 8GDB 원격 디버깅 — gdbserver·OpenOCD·J-Link 통합
- 9GDB·LLDB Python 스크립팅 — Pretty-Printer·Custom Command
- 10GDB·LLDB TUI와 프런트엔드 — gdb-dashboard·gef·pwndbg·VS Code
- 11GDB·LLDB 실전 팁 — STL·최적화 코드·시간 역행 디버깅
- 12DWARF 디버그 정보 — 디버거가 변수와 라인을 찾는 방식
관련 글
DWARF 디버그 정보 — 디버거가 변수와 라인을 찾는 방식
DWARF 표준, DIE / abbrev / line / location, expression VM, CFI, split-DWARF.
GDB·LLDB Python 스크립팅 — Pretty-Printer·Custom Command
GDB / LLDB Python API. pretty-printer 작성, 커스텀 명령, 자동화, MI.
Breakpoint와 Watchpoint 분석 — Conditional·Hardware·Catchpoint
조건부 break, watchpoint(변수 변경 추적), catchpoint, hardware vs software.