GDB 커널 디버깅
리눅스 커널 GDB 디버깅에 특화된 내용을 정리합니다. KGDB/QEMU 원격 연결, vmlinux 심볼 로딩, 모듈 주소 해석, 커널 GDB 스크립트(lx-*), vmcore 코어덤프 분석, 임베디드 JTAG 디버깅, 역방향 디버깅을 다룹니다. GDB 기본 기능은 GDB 가이드를 참조하세요.
커널 디버깅 빠른 시작
커널 개발자라면 아래 흐름부터 읽는 편이 효율적입니다. 즉 "명령어 사전"보다 "커널에서 어떤 심볼과 아티팩트를 열어야 하는가"를 먼저 고정해야 합니다. GDB 기본 명령어는 GDB 가이드를 참조하세요.
| 상황 | 가장 먼저 할 일 | 바로 이어질 섹션 |
|---|---|---|
| 라이브 커널 KGDB 연결 | vmlinux와 동일 빌드의 심볼 준비 | 리눅스 커널 디버깅 활용 |
| 모듈 함수 주소 해석 | 모듈 로드 주소와 심볼 오프셋(Offset) 확인 | 위치 지정자, 리눅스 커널 디버깅 활용 |
vmcore 후분석 | crash 대신 GDB로 특정 구조체(Struct)를 직접 탐색 | 리눅스 커널 디버깅 활용, Python 확장 |
| QEMU 원격 디버깅 | gdb stub 포트와 심볼 파일 정합성 확인 | 원격 디버깅, 리눅스 커널 디버깅 활용 |
vmlinux, 모듈 심볼, /proc/kallsyms, System.map, vmcore, KGDB transport가 서로 맞아야 하므로, 심볼 정합성을 먼저 확인하는 습관이 중요합니다.
리눅스 커널 디버깅 활용
maxcpus=1 커널 파라미터와 함께 사용합니다.
QEMU + GDB 커널 디버깅
# QEMU + GDB로 커널 디버깅
# -s = -gdb tcp::1234 (기본 포트 단축), -S = 시작 즉시 정지
qemu-system-x86_64 \
-kernel arch/x86/boot/bzImage \
-initrd initramfs.cpio.gz \
-append "nokaslr console=ttyS0" \
-serial tcp::1234,server,nowait \
-S -gdb tcp::1235 # -S: 시작 시 정지, GDB 포트 1235
# 호스트에서 GDB 연결
gdb vmlinux
(gdb) target remote :1235
(gdb) break start_kernel
(gdb) continue
# 멀티코어 환경 (-smp 4): GDB thread 명령으로 각 CPU 확인
# qemu-system-x86_64 -smp 4 -S -gdb tcp::1235 ...
(gdb) info threads # CPU별 스레드 목록 (Thread 1~4)
(gdb) thread 2 # CPU 1로 전환
(gdb) backtrace # 해당 CPU 스택
(gdb) thread apply all bt # 전체 CPU 스택 한 번에
# 부팅 초기 단계 디버깅 (start_kernel 이전, head.S)
(gdb) break startup_64 # x86_64 어셈블리 진입점 (arch/x86/kernel/head_64.S)
(gdb) break x86_64_start_kernel # C 코드 전환 직전
(gdb) break start_kernel # 커널 메인 C 진입점
# scripts/gdb 활용 (커널 빌드 디렉터리에서)
(gdb) source scripts/gdb/vmlinux-gdb.py
(gdb) lx-dmesg # 커널 로그 출력
(gdb) lx-ps # task_struct 기반 프로세스 목록
(gdb) lx-lsmod # 모듈 목록
(gdb) lx-symbols # 모듈 심볼 로드
(gdb) lx-mounts # 마운트 목록
# 커널 자료구조 탐색
(gdb) p init_task # 첫 번째 task_struct
(gdb) ptype struct task_struct # task_struct 레이아웃
(gdb) p $lx_current() # 현재 태스크
# 모듈 디버깅
(gdb) add-symbol-file drivers/foo/foo.ko 0x$(cat /sys/module/foo/sections/.text)
lx-* 명령
scripts/gdb/vmlinux-gdb.py를 로드하면 커널 전용 lx-* 명령이 등록됩니다. 이 명령들은 커널 자료구조를 Python으로 순회해 GDB에서 직접 커널 상태를 조회합니다.
| 명령 | 설명 |
|---|---|
lx-dmesg | 커널 링 버퍼 출력 (printk 로그) |
lx-ps | 프로세스 목록 (task_struct 기반) |
lx-lsmod | 로드된 모듈 목록 |
lx-symbols | 모듈 심볼 로드 (add-symbol-file 자동화) |
lx-mounts | 마운트(Mount) 목록 (/proc/mounts 대응) |
lx-clk-summary | 클록 트리 요약 (클록 주파수·상태) |
lx-device-list-bus | 버스별 디바이스 목록 |
lx-device-list-class | 클래스별 디바이스 목록 |
lx-device-list-tree | 디바이스 트리(Device Tree) 계층 출력 |
lx-fdtdump | 디바이스 트리 블록 덤프 (임베디드) |
lx-timerlist | 커널 타이머(Timer) 목록 (/proc/timer_list 대응) |
lx-genpd-summary | 전원 도메인(Generic PM Domain) 요약 |
lx-iomem | I/O 메모리 맵 (/proc/iomem 대응) |
lx-ioports | I/O 포트 맵 (/proc/ioports 대응) |
lx-version | 커널 버전 정보 출력 |
커스텀 lx-* 명령은 gdb.Command를 상속해 Python으로 작성합니다.
import gdb
class LxTaskWalk(gdb.Command):
"""모든 task_struct를 순회하는 커스텀 lx 명령"""
def __init__(self):
super().__init__("lx-taskwalk", gdb.COMMAND_USER)
def invoke(self, arg, from_tty):
init = gdb.parse_and_eval("init_task")
# for_each_process: tasks.next → 다음 task_struct (container_of)
# scripts/gdb/linux/tasks.py의 task_lists() 함수 참조
pid = int(init["pid"])
comm = init["comm"].string()
print(f"PID {pid}: {comm}")
# ... (링크드 리스트 순회 구현)
LxTaskWalk()
# (gdb) source lx_taskwalk.py
# (gdb) lx-taskwalk
커널 자료구조 GDB 탐색
GDB로 커널의 핵심 자료구조를 직접 탐색하는 실전 패턴입니다.
# 태스크 / 스케줄러
(gdb) p init_task.pid # init 프로세스 PID (0)
(gdb) p $lx_current()->comm # 현재 실행 중인 태스크 이름
(gdb) p $lx_current()->mm->mmap_base # 사용자 공간 mmap 베이스
(gdb) p *$lx_current()->sched_class # 스케줄러 클래스 구조체
(gdb) ptype struct task_struct # task_struct 전체 레이아웃
# 메모리 관리
(gdb) p/x init_task.mm->pgd # 페이지 디렉터리 기본 주소 (pgd_t)
(gdb) p init_mm.total_vm # 커널 VM 영역 크기 (페이지 수)
(gdb) p *virt_to_page(0xffffffff81000000) # 가상 주소 → page 구조체
(gdb) p ((struct page*)mem_map)[0] # 첫 번째 page 구조체
# VFS (가상 파일시스템)
(gdb) p $lx_current()->fs->root.dentry->d_iname # 루트 경로 이름
(gdb) p init_task.files->fdt->max_fds # 파일 디스크립터 최대 수
(gdb) p *init_task.files->fdt->fd[0] # fd 0번 (stdin) file 구조체
# 네트워크
(gdb) p init_net.dev_base_head # net_device 링크드 리스트 헤드
(gdb) p ((struct net_device*)init_net.dev_base_head.next)->name # 첫 번째 인터페이스
# 인터럽트 / IRQ
(gdb) p *irq_desc[0] # IRQ 0 디스크립터
(gdb) p irq_desc[0].action->name # IRQ 0 핸들러 이름
KASLR 처리 및 심볼 재베이스
KASLR(Kernel Address Space Layout Randomization)이 활성화된 커널에서는 심볼 주소가 매 부팅마다 달라집니다. GDB에서 올바른 심볼 조회를 위해 심볼 파일을 재베이스해야 합니다.
# KASLR 활성화 상태에서 커널 텍스트 오프셋 확인
$ cat /proc/kallsyms | grep ' _text$'
ffffffff91000000 T _text # 실제 적재 주소 (기준: 0xffffffff81000000)
# 오프셋: 0xffffffff91000000 - 0xffffffff81000000 = 0x10000000
# GDB에서 심볼 파일 재베이스 (-s 옵션으로 섹션별 주소 지정)
(gdb) add-symbol-file vmlinux \
-s .text 0xffffffff91000000 \
-s .data 0xffffffff92800000
# 자동화 Python 스크립트 (GDB 세션에서 실행)
python
kallsyms = open('/proc/kallsyms').readlines()
line = next(l for l in kallsyms if ' _text\n' in l)
actual = int(line.split()[0], 16)
offset = actual - 0xffffffff81000000
gdb.execute(f"add-symbol-file vmlinux -s .text {actual:#x}")
print(f"KASLR offset: {offset:#x}")
end
# QEMU 개발 환경: nokaslr로 KASLR 비활성화 권장
# -append "nokaslr console=ttyS0 ..."
KASAN / KCSAN + GDB 분석
커널 빌드 시 KASAN(Kernel Address Sanitizer)·KCSAN(Kernel Concurrency Sanitizer)을 활성화하면 메모리 오류와 경쟁 조건을 런타임에 감지합니다. GDB와 연계해 오류를 정밀 분석합니다.
# KASAN 빌드 설정
CONFIG_KASAN=y
CONFIG_KASAN_OUTLINE=y # 인스트루멘테이션 모드 (INLINE보다 디버깅 용이)
CONFIG_KASAN_GENERIC=y # 일반 KASAN (QEMU x86_64 지원)
# KCSAN 빌드 설정
CONFIG_KCSAN=y
CONFIG_KCSAN_REPORT_ONCE_IN_MS=3000 # 중복 보고 억제
# KASAN 오류 발생 시 GDB로 분석
(gdb) lx-dmesg # BUG: KASAN: use-after-free in ... 확인
(gdb) break kasan_report # KASAN 보고 함수에 중단점
(gdb) continue # 오류 발생 시 정지
# 오류 주소의 page 구조체 검사
(gdb) p *virt_to_page(0xffff888003b00000) # page 구조체 상태
(gdb) p ((struct page*)0xffff888003b00000)->_refcount # 참조 카운트
# KCSAN 경쟁 조건 감지 후 전체 CPU 스택 확인
(gdb) thread apply all bt # 경쟁하는 코드 경로 파악
# KASAN shadow 메모리 직접 확인 (x86_64, CONFIG_KASAN_GENERIC)
# shadow_addr = (addr >> 3) + 0xdffffc0000000000
python
addr = 0xffff888003b00000
shadow = (addr >> 3) + 0xdffffc0000000000
val = int(gdb.parse_and_eval(f"*(char*){shadow:#x}"))
print(f"Shadow: {val:#x} (0=접근가능, 음수=해제/무효)")
end
crash 도구와 병행 사용
# kdump로 생성된 vmcore 분석
crash /usr/lib/debug/lib/modules/$(uname -r)/vmlinux /var/crash/vmcore
# GDB로 vmcore 직접 분석 (crash 대안)
gdb vmlinux vmcore
(gdb) target kdump vmcore # 일부 GDB 버전에서 지원
프로세스 메모리 레이아웃 검사
GDB로 프로세스의 가상 메모리(Virtual Memory) 구조를 시각적으로 이해하고 검사하는 것은 메모리 버그, 보안 분석, 성능 최적화의 기초입니다. 리눅스 x86_64 프로세스의 메모리 레이아웃과 GDB 검사 방법을 심층 정리합니다.
섹션별 상세 검사
# 전체 메모리 맵 확인
(gdb) info proc mappings
Start Addr End Addr Size Offset Perms File
0x555555554000 0x555555556000 0x2000 0x0 r--p /tmp/prog
0x555555556000 0x555555558000 0x2000 0x2000 r-xp /tmp/prog # .text
0x555555558000 0x555555559000 0x1000 0x4000 r--p /tmp/prog # .rodata
0x55555555a000 0x55555555b000 0x1000 0x5000 rw-p /tmp/prog # .data .bss
0x55555555b000 0x55555557c000 0x21000 0x0 rw-p [heap]
0x7ffff7d90000 0x7ffff7f22000 0x192000 0x0 r-xp libc.so.6
0x7ffffffde000 0x7ffffffff000 0x21000 0x0 rw-p [stack]
# ELF 섹션 정보
(gdb) maintenance info sections
[0] 0x555555554318->0x555555554334 .interp
[1] 0x555555554338->0x555555554358 .note.gnu.build-id
...
[13] 0x555555556000->0x555555557a52 .text ALLOC LOAD READONLY CODE
[14] 0x555555558000->0x555555558120 .rodata ALLOC LOAD READONLY DATA
# 스택 프레임 상세 검사
(gdb) info frame
Stack level 0, frame at 0x7fffffffde10:
rip = 0x555555556194 in main (prog.c:42)
Arglist at 0x7fffffffde00, args: argc=1, argv=0x7fffffffdf08
Locals at 0x7fffffffde00, Previous frame's sp is 0x7fffffffde10
Saved registers: rbp at 0x7fffffffde00, rip at 0x7fffffffde08
# 스택 내용 직접 검사 (16바이트 단위, 32줄)
(gdb) x/32gx $rsp
0x7fffffffdda0: 0x0000000000000001 0x00007fffffffdf08 # argc, argv
0x7fffffffddb0: 0x0000555555556194 0x00007fffffffde00 # rip, rbp
# 스택 canary 확인 (fs_base + 0x28)
(gdb) print /x *(long*)($fs_base + 0x28)
$1 = 0x1a2b3c4d5e6f7080 # stack canary 값
# ASLR 상태 확인
(gdb) info auxv
33 AT_SYSINFO_EHDR 0x7ffff7fc1000 # vDSO 주소
25 AT_RANDOM 0x7fffffffdf79 # 랜덤 시드 주소
3 AT_PHDR 0x555555554040 # 프로그램 헤더 (PIE 베이스)
# GOT/PLT 검사 (동적 링킹 관련)
(gdb) info functions @plt # PLT 함수 목록
(gdb) x/gx 0x55555555a018 # GOT 엔트리 확인 (printf@got)
(gdb) disassemble 0x555555556020,+16 # PLT 스텁 확인
힙 구조 심층 분석
# 힙 청크 직접 검사
(gdb) print ptr # 0x55555555b260 (malloc 반환 포인터)
(gdb) x/4gx (char*)ptr - 16 # 청크 헤더부터 확인
0x55555555b250: 0x0000000000000000 # prev_size
0x55555555b258: 0x0000000000000031 # size=0x30(48바이트) | P=1
0x55555555b260: 0x4141414141414141 # user data
0x55555555b268: 0x0000000000000000
# tcache 확인 (glibc 2.26+)
# tcache_perthread_struct는 각 스레드의 힙 첫 부분에 위치
(gdb) print *(tcache_perthread_struct*)0x55555555b010
# main_arena 확인 (멀티스레드 힙)
(gdb) print main_arena
(gdb) print main_arena.top # top 청크 (brk 경계)
(gdb) print main_arena.bins[0] # unsorted bin
커널 GDB 스크립트 (lx-*) 완전 가이드
리눅스 커널 소스의 scripts/gdb/ 디렉터리에는 GDB Python 확장으로 구현된 lx-* 명령 모음이 있습니다. 이 스크립트들은 커널 자료구조를 직접 순회하여 GDB에서 커널 상태를 조회합니다. vmlinux-gdb.py가 진입점이며, 각 기능은 scripts/gdb/linux/ 하위 모듈로 구현되어 있습니다.
lx-dmesg 내부 동작
lx-dmesg는 커널 링 버퍼(struct printk_ringbuffer)를 직접 순회하여 커널 로그를 출력합니다. /proc/kmsg나 dmesg 명령 없이도 GDB만으로 커널 메시지를 확인할 수 있습니다.
# lx-dmesg 내부 동작 원리 (scripts/gdb/linux/dmesg.py 기반)
# 1. prb (printk_ringbuffer) 전역 변수 접근
# 2. desc_ring에서 각 descriptor 순회
# 3. text_data_ring에서 메시지 텍스트 추출
# 4. 타임스탬프(nsec) → 사람이 읽을 수 있는 형식 변환
import gdb
class LxDmesgDetailed(gdb.Command):
"""lx-dmesg 확장: 로그 레벨 필터링 지원"""
def __init__(self):
super().__init__("lx-dmesg-filter", gdb.COMMAND_USER)
def invoke(self, arg, from_tty):
level_filter = int(arg) if arg else 7 # 0=EMERG ~ 7=DEBUG
log_buf = gdb.parse_and_eval("prb")
# printk_ringbuffer → desc_ring → text_data_ring 순회
# 각 레코드의 level 필드 확인 후 필터링
print(f"[lx-dmesg-filter] 레벨 {level_filter} 이하 메시지만 출력")
# ... (링 버퍼 순회 구현)
LxDmesgDetailed()
lx-symbols 상세 사용법
lx-symbols는 커널 모듈의 심볼을 자동으로 로드합니다. 모듈이 동적으로 로드/언로드되는 환경에서 각 모듈의 .text 섹션 주소를 자동 감지하여 add-symbol-file을 실행합니다.
# lx-symbols 기본 사용
(gdb) lx-symbols
loading vmlinux
# scanning for modules in /path/to/kernel/build
# loading @0xffffffffa0000000: /path/to/module/foo.ko
# 모듈 빌드 디렉터리 지정 (여러 경로 가능)
(gdb) lx-symbols /path/to/modules/ /another/path/
# 모듈 심볼이 자동 로드된 후 모듈 함수에 중단점 설정
(gdb) break my_module_init
(gdb) break my_module_ioctl
# 수동으로 모듈 심볼 로드 (lx-symbols 실패 시)
# 1. 타겟에서 모듈 섹션 주소 확인
(gdb) p/x ((struct module *)$lx_module_by_name("foo"))->core_layout.base
# 또는 /sys/module/foo/sections/.text 에서 확인
# 2. add-symbol-file로 수동 로드
(gdb) add-symbol-file /path/to/foo.ko 0xffffffffa0001000
# 모듈 로드 이벤트 자동 감지 (lx-symbols 내부)
# scripts/gdb/linux/symbols.py는 do_init_module()에
# 내부 중단점을 설정하여 새 모듈 로드 시 자동 심볼 추가
# 현재 로드된 모듈 확인
(gdb) lx-lsmod
# Address Module Size Used by
# 0xffffffffa0000000 foo 16384 0
# 0xffffffffa0010000 bar 24576 1 foo
# 모듈 의존성 확인
(gdb) p ((struct module *)$lx_module_by_name("foo"))->source_list
(gdb) p ((struct module *)$lx_module_by_name("foo"))->target_list
lx-* 고급 활용 패턴
# lx-ps 확장: 특정 상태의 프로세스만 필터링
(gdb) lx-ps
# TASK_RUNNING(0), TASK_INTERRUPTIBLE(1), TASK_UNINTERRUPTIBLE(2)
# Python으로 D 상태(TASK_UNINTERRUPTIBLE) 프로세스만 추출
python
import gdb
from linux import tasks
for t in tasks.task_lists():
state = int(t["__state"])
if state == 2: # TASK_UNINTERRUPTIBLE
pid = int(t["pid"])
comm = t["comm"].string()
print(f"[D state] PID {pid}: {comm}")
end
# 특정 태스크의 커널 스택 덤프
python
task = gdb.parse_and_eval("init_task")
stack = task["stack"]
print(f"Stack base: {stack}")
# 스택 포인터에서 thread_info 추출
end
# lx-mounts 활용
(gdb) lx-mounts
# mount: /dev/sda1 -> / type: ext4
# mount: proc -> /proc type: proc
# lx-iomem / lx-ioports 로 I/O 리소스 맵 확인
(gdb) lx-iomem
# 00000000-0009ffff : System RAM
# 000a0000-000bffff : PCI Bus 0000:00
# ...
# lx-version 으로 커널 빌드 정보 확인
(gdb) lx-version
# Linux version 6.8.0-rc1 (gcc 13.2.0) #1 SMP PREEMPT_DYNAMIC ...
# lx-device-list-bus 로 PCI/USB 디바이스 확인
(gdb) lx-device-list-bus pci
(gdb) lx-device-list-bus usb
# lx-clk-summary 로 클록 트리 확인 (임베디드/SoC)
(gdb) lx-clk-summary
QEMU + GDB 커널 디버깅
QEMU는 GDB stub을 내장하고 있어 GDB Remote Protocol로 직접 연결할 수 있습니다. 실제 하드웨어 없이 커널을 완전히 제어할 수 있으므로, 커널 개발에서 가장 효율적인 디버깅 환경입니다.
QEMU 커널 디버깅 환경 구축
# 1. 디버깅용 커널 빌드 설정
make menuconfig
# 필수 설정:
CONFIG_DEBUG_INFO=y # DWARF 디버그 정보
CONFIG_DEBUG_INFO_DWARF5=y # DWARF 5 (권장)
CONFIG_GDB_SCRIPTS=y # scripts/gdb 활성화
CONFIG_FRAME_POINTER=y # 정확한 백트레이스
CONFIG_RANDOMIZE_BASE=n # KASLR 비활성화 (디버깅 편의)
CONFIG_DEBUG_INFO_REDUCED=n # 디버그 정보 축소 비활성화
CONFIG_KGDB=y # KGDB 지원 (선택)
CONFIG_STRICT_KERNEL_RWX=n # 코드 패치 허용 (SW BP용)
# 2. 최소 initramfs 생성 (BusyBox 기반, 상세: busybox.html)
mkdir -p initramfs/{bin,sbin,etc,proc,sys,dev,tmp}
# BusyBox 정적 바이너리 복사
cp busybox initramfs/bin/
cd initramfs/bin && ln -s busybox sh && ln -s busybox ls && cd ../..
# init 스크립트
cat > initramfs/init <<'INITEOF'
#!/bin/sh
mount -t proc proc /proc
mount -t sysfs sysfs /sys
mount -t devtmpfs devtmpfs /dev
echo "=== Debug Kernel Booted ==="
exec /bin/sh
INITEOF
chmod +x initramfs/init
# cpio 아카이브 생성
cd initramfs && find . | cpio -o -H newc | gzip > ../initramfs.cpio.gz && cd ..
# 3. QEMU 기동 (디버그 모드)
qemu-system-x86_64 \
-kernel arch/x86/boot/bzImage \
-initrd initramfs.cpio.gz \
-append "nokaslr console=ttyS0 loglevel=7" \
-serial stdio \
-m 1G \
-smp 4 \
-enable-kvm \
-S -gdb tcp::1234 \
-nographic # 그래픽 없이 콘솔만
# 4. GDB 연결 (별도 터미널)
gdb vmlinux
(gdb) target remote :1234
(gdb) source scripts/gdb/vmlinux-gdb.py
(gdb) break start_kernel
(gdb) continue
# → Breakpoint 1, start_kernel () at init/main.c:...
# 5. virtio 디바이스 디버깅
(gdb) break virtio_dev_probe # virtio 디바이스 프로브 감시
(gdb) break virtnet_open # virtio-net 열기 감시
(gdb) break virtblk_make_request # virtio-blk I/O 요청 감시
# 6. 9p 파일시스템으로 호스트-게스트 파일 공유
# QEMU 옵션 추가:
# -virtfs local,path=/path/to/shared,mount_tag=shared,security_model=mapped-xattr
# 게스트에서 마운트:
# mount -t 9p -o trans=virtio shared /mnt
QEMU 환경 모듈 디버깅
# 모듈을 게스트에 전달 (9p 또는 initramfs에 포함)
# 게스트에서 모듈 로드
# (guest) insmod /mnt/my_module.ko
# GDB에서 모듈 심볼 자동 로드
(gdb) lx-symbols /path/to/module/build/
# loading @0xffffffffa0000000: my_module.ko
# 모듈 함수에 중단점 설정
(gdb) break my_module_init
(gdb) break my_module_ioctl
(gdb) continue
# 모듈 로드 시점 자동 감시 (do_init_module에 잡기)
(gdb) break do_init_module
(gdb) commands
silent
lx-symbols
continue
end
# 모듈의 특정 구조체 검사
(gdb) p *((struct module *)$lx_module_by_name("my_module"))
(gdb) p/x ((struct module *)$lx_module_by_name("my_module"))->core_layout
# → {base = 0xffffffffa0000000, size = 0x4000, text_size = 0x1000, ...}
QEMU 초기 부팅 디버깅
# BIOS → 부트로더 → 커널 진입까지 추적
# -S 옵션으로 QEMU는 첫 명령 실행 전에 정지
(gdb) target remote :1234
# 현재 위치: BIOS 코드 (리얼 모드)
# x86_64 부팅 순서별 중단점
(gdb) break *0x7c00 # 부트로더 진입 (MBR)
(gdb) hbreak startup_64 # 64비트 모드 전환 (head_64.S)
(gdb) break x86_64_start_kernel # C 코드 전환점
(gdb) break start_kernel # 커널 메인 C 진입점
# 서브시스템 초기화 추적
(gdb) break mm_core_init # 메모리 관리 초기화
(gdb) break sched_init # 스케줄러 초기화
(gdb) break rest_init # init 프로세스 생성 직전
(gdb) break kernel_init # PID 1 (init) 진입
# 커널 커맨드라인 확인
(gdb) break start_kernel
(gdb) continue
(gdb) p boot_command_line
# → "nokaslr console=ttyS0 loglevel=7"
코어덤프(Core Dump) 분석
코어덤프(Core Dump)는 프로세스가 비정상 종료될 때 메모리 상태를 파일로 저장한 것입니다. GDB로 코어 파일을 로드하면 크래시 시점의 정확한 상태를 사후 분석할 수 있습니다. 커널 코어덤프(vmcore)는 시스템 패닉 시 kdump가 생성하며, crash 도구 또는 GDB로 분석합니다.
유저 공간 코어덤프 분석
# 코어덤프 활성화
ulimit -c unlimited # 현재 셸에서 코어 크기 무제한
echo "core.%e.%p" | sudo tee /proc/sys/kernel/core_pattern # 파일명 패턴
# 코어 파일로 GDB 분석
gdb ./prog core.prog.12345
# Core was generated by `./prog --input large_file.dat'.
# Program terminated with signal SIGSEGV, Segmentation fault.
# #0 0x0000000000401234 in process_data (buf=0x0) at prog.c:42
# 크래시 원인 분석 절차
(gdb) bt # 1. 콜 스택 확인
(gdb) bt full # 2. 로컬 변수 포함 스택
(gdb) info registers # 3. 레지스터 상태 (RIP, RSP 등)
(gdb) frame 0 # 4. 크래시 프레임 선택
(gdb) list # 5. 크래시 위치 소스 코드
(gdb) info locals # 6. 지역 변수 값
(gdb) info args # 7. 함수 인자
(gdb) x/16gx $rsp # 8. 스택 메모리 덤프
# gcore: 실행 중인 프로세스의 코어 파일 생성
gcore 12345 # PID 12345의 코어 덤프 (core.12345 생성)
# 또는 GDB 내에서:
(gdb) attach 12345
(gdb) generate-core-file mycore.dump # 커스텀 파일명
(gdb) detach
# coredumpctl (systemd 환경)
coredumpctl list # 최근 코어덤프 목록
coredumpctl info PID # 특정 코어덤프 상세 정보
coredumpctl debug PID # GDB로 직접 분석 시작
coredumpctl dump PID -o core.out # 코어 파일 추출
# 코어 파일 구조 확인 (readelf)
readelf -n core.prog.12345 # PT_NOTE 세그먼트 (레지스터, siginfo)
readelf -l core.prog.12345 # PT_LOAD 세그먼트 (메모리 매핑)
eu-readelf --notes core.prog.12345 # elfutils 상세 노트
커널 코어덤프 (vmcore) 분석
# kdump 설정 (커널 코어덤프 캡처)
# 1. crashkernel 메모리 예약 (부트 파라미터)
# crashkernel=256M 또는 crashkernel=256M,high
# 2. kdump 서비스 활성화
systemctl enable kdump
systemctl start kdump
# 3. 패닉 테스트 (주의: 시스템 크래시 발생!)
# echo c > /proc/sysrq-trigger
# vmcore 분석 (crash 도구)
crash /usr/lib/debug/lib/modules/$(uname -r)/vmlinux /var/crash/vmcore
crash> bt # 패닉 시점 콜 스택
crash> log # 커널 로그
crash> ps # 프로세스 목록
crash> struct task_struct ffff88800abcd000 # 구조체 덤프
# GDB로 vmcore 직접 분석
gdb vmlinux vmcore
(gdb) bt # 패닉 CPU 콜 스택
(gdb) info threads # CPU별 스레드 (각 CPU = 1 thread)
(gdb) thread apply all bt # 모든 CPU 스택
# vmcore에서 커널 자료구조 탐색
(gdb) p panic_cpu # 패닉 발생 CPU
(gdb) p init_task.comm # swapper 이름
(gdb) p system_state # 시스템 상태
# makedumpfile로 vmcore 필터링 (크기 축소)
makedumpfile -c -d 31 vmcore vmcore.filtered # 불필요한 페이지 제거
# -d 플래그: 1=제로페이지, 2=캐시, 4=사용자, 8=프리, 16=감시
역방향 디버깅
역방향 디버깅(Reverse Debugging)은 프로그램 실행을 되감아 버그의 근본 원인을 추적하는 기법입니다. GDB의 record full, Mozilla의 rr, Intel PT 각각의 내부 동작 원리와 실전 활용 패턴을 심층 분석합니다.
rr 고급 활용
# rr로 멀티스레드 레이스 컨디션 디버깅
# 1. 레이스가 발생하는 프로그램 기록
rr record ./race_program
# 여러 번 실행하여 레이스가 발생하는 기록 확보
# 2. 재생 시작
rr replay
(rr) break crash_function
(rr) continue # → 크래시 지점
# 3. 데이터 레이스 역추적
# 문제가 되는 변수의 주소를 확인
(rr) print &shared_counter
(rr) watch -l *0x604040 # 하드웨어 감시점
(rr) reverse-continue # → 마지막으로 수정한 지점!
# Inferior stopped because watchpoint 2 was triggered.
# Old value = 42, New value = 41
(rr) bt # 어떤 스레드가 수정했는지 확인
(rr) info threads # 활성 스레드 목록
# 4. rr의 chaos 모드 (레이스 유발 극대화)
rr record --chaos ./race_program # 스케줄링 무작위화
# 5. rr 기록 공유 (팀 협업)
rr pack ~/.local/share/rr/race_program-0/
# → 기록 디렉터리를 다른 머신으로 전송 가능
# 수신 측: rr replay /path/to/packed/trace
# 6. rr + 감시점 역추적 패턴 (use-after-free 추적)
(rr) break main
(rr) continue
# ... 크래시까지 진행 ...
(rr) print ptr # 댕글링 포인터 주소 확인
(rr) watch -l *(void**)&ptr # ptr 변수 감시
(rr) reverse-continue # → ptr가 마지막으로 설정된 시점
# 여기서 free() 이후 ptr이 재사용되는 패턴 확인
# 7. rr 타임라인 내비게이션
(rr) when # 현재 이벤트 번호 (예: event 5000)
(rr) run 1000 # 이벤트 1000으로 점프
(rr) run 9999 # 이벤트 9999로 점프
record full 실전 패턴
# record full로 메모리 오염 원인 추적
(gdb) break main
(gdb) run
(gdb) record full # main에서 기록 시작
(gdb) continue # → 크래시 또는 이상 동작
# 크래시 시점에서 역추적
(gdb) print corrupted_var
(gdb) watch corrupted_var # 감시점 설정
(gdb) reverse-continue # → 마지막 수정 지점으로 되감기
# 기록 한계 설정
(gdb) set record full insn-number-max 500000 # 최대 명령어 수
(gdb) set record full stop-at-limit off # 한계 시 순환 버퍼 (오래된 기록 삭제)
# 기록 상태 확인
(gdb) info record
# Active record target: record-full
# Record mode: normal (중단 없이 기록 중)
# Lowest recorded instruction number is 1.
# Highest recorded instruction number is 150000.
# Log contains 150000 instructions.
# record full 주의사항
# - 시스템 콜 결과는 기록되지만 부작용(파일 I/O)은 복원 안 됨
# - 멀티스레드에서 다른 스레드의 변경은 기록 안 됨
# - 메모리 사용량이 크게 증가할 수 있음
임베디드/JTAG 디버깅
GDB는 JTAG/SWD 디버그 프로브(Probe)를 통해 베어메탈 펌웨어(Firmware), RTOS, 부트로더(Bootloader)를 디버깅하는 표준 도구입니다. OpenOCD, J-Link GDB Server, pyOCD 등이 GDB Remote Protocol로 프로브를 추상화합니다.
OpenOCD + GDB
# OpenOCD 서버 시작 (ST-Link + STM32F4)
openocd -f interface/stlink.cfg -f target/stm32f4x.cfg
# → Info : Listening on port 3333 for gdb connections
# → Info : Listening on port 4444 for telnet connections
# GDB 연결 (ARM 크로스 컴파일러 GDB 사용)
arm-none-eabi-gdb firmware.elf
(gdb) target remote :3333
# Flash 프로그래밍
(gdb) monitor reset halt # 리셋 후 정지
(gdb) monitor flash write_image erase firmware.bin 0x08000000
(gdb) load # ELF에서 자동 Flash 기록
# 임베디드 디버깅 기본 워크플로우
(gdb) monitor reset halt
(gdb) break main
(gdb) continue
(gdb) info registers # ARM 레지스터 확인
(gdb) print /x *SCB # System Control Block 레지스터
# OpenOCD monitor 명령
(gdb) monitor reset init # 리셋 + 초기화 스크립트 실행
(gdb) monitor halt # CPU 정지
(gdb) monitor resume # CPU 재개
(gdb) monitor reg # 레지스터 (OpenOCD 형식)
(gdb) monitor mdw 0x40021000 4 # 메모리 워드 읽기 (RCC 레지스터 등)
(gdb) monitor mww 0x40021000 0x01 # 메모리 워드 쓰기
(gdb) monitor bp 0x08001234 4 hw # 하드웨어 중단점 (Flash)
# 세미호스팅 (타겟의 printf를 GDB 콘솔로 리다이렉트)
(gdb) monitor arm semihosting enable
# 타겟 코드에서 printf() 호출 → GDB 콘솔에 출력
J-Link + GDB
# J-Link GDB Server 시작
JLinkGDBServer -device STM32F407VG -if SWD -speed 4000 -port 2331
# GDB 연결
arm-none-eabi-gdb firmware.elf
(gdb) target remote :2331
(gdb) monitor reset # 타겟 리셋
(gdb) monitor halt
(gdb) load # Flash 프로그래밍
# J-Link RTT (Real-Time Transfer) — UART 없이 고속 로깅
# 타것 코드에서 SEGGER_RTT_printf() → J-Link 버퍼 → 호스트
(gdb) monitor exec SetRTTAddr 0x20000000 # RTT 컨트롤 블록 주소
# SWO (Serial Wire Output) — ITM 트레이스 출력
(gdb) monitor SWO EnableTarget 4000000 0 1 0 # SWO 활성화
RISC-V + GDB
# OpenOCD + RISC-V (FTDI 기반 프로브)
openocd -f interface/ftdi/olimex-arm-usb-tiny-h.cfg \
-f target/riscv.cfg
# RISC-V GDB 연결
riscv64-unknown-elf-gdb firmware.elf
(gdb) target remote :3333
# RISC-V 트리거 (하드웨어 중단점/감시점)
(gdb) hbreak *0x80000000 # 하드웨어 BP (tdata1 CSR 사용)
(gdb) watch *0x80001000 # 하드웨어 WP (match control)
# RISC-V CSR(Control and Status Register) 접근
(gdb) info registers csr # 모든 CSR 레지스터
(gdb) print /x $mstatus # Machine Status Register
(gdb) print /x $mcause # Machine Cause (예외/인터럽트 원인)
(gdb) print /x $mepc # Machine Exception PC
# 다중 hart(코어) 디버깅
(gdb) info threads # hart별 스레드 목록
(gdb) thread 2 # hart 1로 전환
cortex-debug 확장으로 VS Code에서 GDB+OpenOCD를 GUI로 사용할 수 있습니다. launch.json에서 "servertype": "openocd"와 "configFiles"를 설정하면 Flash 프로그래밍·브레이크포인트·변수 감시를 GUI에서 수행합니다.
- GDB 가이드 — GDB 기본 명령어, 중단점, 감시점, DWARF, Python 확장 등 범용 GDB 사용법
- 디버깅 & 트러블슈팅 — printk, KASAN, lockdep, kdump 등 커널 디버깅 전반
- ftrace / Tracepoints — 커널 동적 추적, kprobe, 함수 그래프
- 크래시 분석 — kdump vmcore 분석, crash 도구, Call Trace 읽기
- 커널 심볼(Kernel Symbol) — kallsyms, System.map, 모듈 주소 해석
- 커널 개발 도구 — QEMU, sparse, Coccinelle, KUnit
- GCC 완전 가이드 —
-g,-g3,-Og디버그 빌드 옵션
관련 문서
- GDB 가이드 — 중단점·감시점·조건식 추적, 스레드/코어덤프 분석, gdbserver 원격 세션, Python 자동화, DWARF 심볼 해석 등 범용 GDB 기능
- 디버깅 & 트러블슈팅 — printk, KASAN, lockdep, kdump 등 커널 디버깅 기법 전반
- 크래시 분석 — kdump vmcore 분석, crash 도구, Call Trace 읽기