본문으로 건너뛰기
GDB and LLDB · 10/12

GDB·LLDB TUI와 프런트엔드 — gdb-dashboard·gef·pwndbg·VS Code

· Hawk · 8분 읽기

GDB는 기본적으로 명령줄입니다. 강력하지만 한 화면에 지금 어디서 멈췄는지, 변수가 얼마인지, 콜스택, 레지스터를 동시에 보여 주지 못합니다. 이 장은 TUI(터미널 UI)와 외부 프런트엔드로 그 한계를 메우는 방법입니다.

내장 TUI에서 출발해 cgdb·gdb-dashboard·gef/pwndbg 같은 터미널 확장, VSCode·nvim-dap·Emacs 같은 IDE 통합, 마지막으로 모든 IDE가 공유하는 DAP(Debug Adapter Protocol)의 정체까지 살펴봅니다.

#GDB 내장 TUI

(gdb) tui enable
(gdb) layout src # 소스 + 명령
(gdb) layout asm # 어셈블리 + 명령
(gdb) layout regs # 레지스터 패널 추가
(gdb) layout split # 소스 + 어셈블리 + 명령

토글 단축키 Ctrl-x a. 한 번 켜면 화면이 분할되어 위쪽엔 소스가, 아래쪽엔 명령창이 뜹니다. 화살표/PgUp으로 소스 스크롤, Ctrl-x o로 패널 포커스 전환, Ctrl-l로 다시 그리기.

#동작 원리

TUI는 ncurses 위에 구현됐습니다. 정지·재개 이벤트마다 GDB가 현재 PC·소스·레지스터를 다시 받아 화면을 그립니다.

레이아웃은 고정된 윈도 셋의 조합.

윈도단축내용
srcs소스 코드
asma어셈블리
regsr레지스터
cmdc명령창 (항상 표시)

#자주 쓰는 단축키

단축키동작
Ctrl-x aTUI on/off 토글
Ctrl-x 1한 윈도
Ctrl-x 2두 윈도
Ctrl-x o다음 패널로 포커스
Ctrl-l다시 그리기
Ctrl-x sTUI SingleKey 모드(s=step, n=next 등)
+ / -활성 윈도 크기 조절
< / >소스 스크롤 좌우
PgUp / PgDn소스 스크롤

#제약

  • 윈도 깨짐이 자주 일어남(Ctrl-l 자주 누르게 됨). 이유는 GDB가 ncurses 화면 위에 그대로 print 출력을 던지면서 화면이 망가지기 때문입니다. Python으로 직접 ncurses와 통합한 cgdb·dashboard가 더 안정적인 이유.
  • 마우스 안 됨.
  • 일부 환경에서 color 안 나옴.
  • 변수·watch 패널 없음.

가벼운 디버깅에는 충분. 큰 코드베이스에선 곧 한계를 느낍니다.

#cgdb — 분할 화면 GDB

cgdb는 위에 vi 키 바인딩이 적용된 소스 뷰어, 아래에 GDB 명령창이 있는 진짜 TUI.

Terminal window
$ cgdb ./my_program
  • Esc로 소스 패널, i로 명령 패널.
  • 소스 패널에서 vi 키(j/k//)로 이동.
  • 마우스 휠 스크롤 가능.
  • 색상·구문 강조가 깔끔.

#동작 원리

cgdb는 GDB를 서브프로세스로 띄우고 stdin/stdout을 가로챕니다. MI(머신 인터페이스) 명령을 보내 현재 PC, 소스 파일, 콜스택을 구조적으로 받아 자체 ncurses UI를 그립니다. GDB 내장 TUI보다 ncurses와 충돌이 적은 이유.

GDB 내장 TUI보다 훨씬 안정적입니다. 패키지 매니저로 바로 설치되므로 첫 선택지로 추천.

~/.cgdb/cgdbrc # 설정
set winminheight=3
set winsplit=top_big
set syntax=on
set color=on
map <C-n> :gdb next<CR>

#gdb-dashboard — 깔끔한 단일 파이썬 스크립트

cyrus-and/gdb-dashboard — 한 파이썬 스크립트(~/.gdbinit에 source)로 콘솔에 멀티 패널을 그립니다.

Terminal window
$ wget -O ~/.gdbinit https://raw.githubusercontent.com/cyrus-and/gdb-dashboard/master/.gdbinit
$ gdb ./my_program

기본 패널: source / assembly / stack / registers / variables / breakpoints / expressions / threads / memory / history.

>>> dashboard -layout source assembly stack registers variables
>>> dashboard memory watch 0x7fff0000 64
>>> dashboard expressions watch "i + j"
>>> dashboard source -style compact
>>> dashboard -style syntax_highlighting 'monokai'

#어떻게 그리는가

gdb.events.stop에 훅을 걸어 정지마다 각 패널의 콘텐츠를 Python으로 수집해 print. ncurses를 쓰지 않으므로 화면 깨짐 없음. 매 정지마다 위에서 아래로 다시 그려지는 식이라 스크롤백을 잃지 않습니다.

장점 — Python 한 파일이라 설정/이식 쉬움. TUI 모드처럼 화면이 깨지지 않고, 매 정지마다 위쪽에 패널이 다시 그려집니다.

커스텀 모듈을 직접 추가할 수 있어 팀별 디버깅 환경을 표준화하기 좋습니다.

~/.gdb/my_module.py
class MyPanel(Dashboard.Module):
def label(self):
return "my-counters"
def lines(self, term_width, term_height, style_changed):
cnt = int(gdb.parse_and_eval("g_counter"))
return [f"counter = {cnt}"]

#gef / pwndbg / peda — 보안·exploit 디버깅

세 가지 모두 보안 분석가의 작업을 GDB 위에서 빠르게 하기 위한 Python 확장입니다. 설치하면 정지할 때마다 레지스터·스택·디스어셈블·코드 컨텍스트가 자동으로 한 화면에 뜹니다.

Terminal window
# gef
$ wget -O ~/.gdbinit-gef.py https://gef.blah.cat/py
$ echo "source ~/.gdbinit-gef.py" >> ~/.gdbinit
# pwndbg
$ git clone https://github.com/pwndbg/pwndbg
$ cd pwndbg && ./setup.sh

이 장의 관심사는 TUI이므로 여기서는 셋의 성격만 구분해 둡니다. gef는 한 파일짜리라 가볍고 ARM·MIPS·PPC까지 따라오며, pwndbg는 힙 분석이 가장 깊어 CTF·exploit에서 사실상 표준입니다. peda는 x86 중심의 오래된 도구라 지금은 옛 자료를 따라갈 때 씁니다.

셋의 상세 비교와 context·vmmap·heap·checksec 같은 공통 명령표는 GDB Extension and IDE Ch 6: GDB 프런트엔드 비교에 정리돼 있습니다.

주의할 점은 하나입니다. 이 확장들은 매 정지마다 큰 컨텍스트를 쏟아내기 때문에, 보안 분석이 아닌 일반 애플리케이션 디버깅에서는 화면이 오히려 시끄러워집니다. 그럴 때는 앞의 gdb-dashboard나 cgdb 쪽이 맞습니다.

#VSCode + cppdbg / CodeLLDB

터미널을 벗어나면 VSCode가 가장 많이 쓰는 프런트엔드입니다. GDB 사용자 입장에서 알아 둘 것은 두 익스텐션이 GDB와 대화하는 방식이 다르다는 점입니다.

cppdbg는 Microsoft가 만든 C/C++ 익스텐션으로, GDB와 MI 프로토콜로 통신합니다. GDB의 명령이 그대로 전달되므로 .gdbinit의 pretty-printer 설정이 살아 있고, setupCommands로 GDB 명령을 직접 밀어 넣을 수도 있습니다. CodeLLDB는 LLDB의 DAP 서버를 직접 띄우는 쪽이라 macOS·Rust·Swift에서 강합니다.

MI를 한 겹 거치기 때문에 생기는 어긋남도 있습니다. 조건부 breakpoint의 표현식이 GDB에서 직접 칠 때와 미묘하게 다르게 평가되는 경우가 있고, fork 추적은 GDB에서 set follow-fork-mode로 다루던 만큼 정교하지 않습니다. 콘솔 입력이 필요한 프로그램은 "externalConsole": true가 필요하고, core dump는 coreDumpPath를 따로 줘야 합니다.

launch.json 전체 예시(launch·attach·gdbserver 원격), 단축키표, Logpoint와 Watch 패널 사용법은 GDB Extension and IDE Ch 6에 정리돼 있습니다.

#Neovim — nvim-dap

nvim-dap이 Debug Adapter Protocol(DAP) 클라이언트, nvim-dap-ui가 UI. VSCode와 같은 디버그 어댑터(cppdbg, CodeLLDB)를 그대로 씁니다.

local dap = require('dap')
dap.adapters.lldb = {
type = 'executable',
command = '/usr/bin/lldb-vscode',
name = 'lldb',
}
dap.configurations.cpp = {
{
name = 'Launch',
type = 'lldb',
request = 'launch',
program = function()
return vim.fn.input('Path to executable: ', vim.fn.getcwd() .. '/', 'file')
end,
cwd = '${workspaceFolder}',
stopOnEntry = false,
args = {},
},
{
name = 'Attach to PID',
type = 'lldb',
request = 'attach',
pid = require('dap.utils').pick_process,
args = {},
},
}
dap.configurations.c = dap.configurations.cpp
dap.configurations.rust = dap.configurations.cpp
vim.keymap.set('n', '<F5>', dap.continue)
vim.keymap.set('n', '<F10>', dap.step_over)
vim.keymap.set('n', '<F11>', dap.step_into)
vim.keymap.set('n', '<S-F11>', dap.step_out)
vim.keymap.set('n', '<F9>', dap.toggle_breakpoint)
vim.keymap.set('n', '<leader>B', function()
dap.set_breakpoint(vim.fn.input('Condition: '))
end)
vim.keymap.set('n', '<leader>dl', dap.run_last)
vim.keymap.set('n', '<leader>dt', dap.terminate)

dap-ui가 VSCode와 거의 같은 패널 레이아웃을 그립니다.

local dapui = require('dapui')
dapui.setup()
dap.listeners.after.event_initialized['dapui_config'] = dapui.open
dap.listeners.before.event_terminated['dapui_config'] = dapui.close

Neovim 안에서 디버그 패널 띄우고, REPL로 표현식 평가. Vim 키를 유지하면서 IDE 수준의 경험을 가져갑니다.

#DAP — 표준 프로토콜의 의미

VSCode·Neovim·Emacs(dap-mode)·Sublime·Helix가 모두 같은 DAP를 씁니다. 따라서 한 어댑터(cppdbg)를 설치하면 어느 에디터든 동일한 디버깅 경험을 얻을 수 있습니다. RSP가 디버거 ↔ 타깃 표준이라면 DAP는 IDE ↔ 디버거 표준.

[에디터] ← DAP → [adapter (예: cppdbg)] ← MI → [GDB]
↑ RSP
[gdbserver / OpenOCD]

#DAP 메시지

JSON-RPC over TCP/stdio. 모든 메시지가 request / response / event 셋.

// 요청
{"seq": 1, "type": "request", "command": "initialize",
"arguments": {"clientID": "vscode", "linesStartAt1": true}}
// 응답
{"seq": 2, "type": "response", "request_seq": 1, "success": true,
"command": "initialize",
"body": {"supportsConfigurationDoneRequest": true, ...}}
// 이벤트
{"seq": 3, "type": "event", "event": "stopped",
"body": {"reason": "breakpoint", "threadId": 1}}

주요 명령.

Command동작
initialize핸드셰이크
launch / attach시작 / 부착
setBreakpoints파일별 BP 일괄 설정
setFunctionBreakpoints함수명 BP
setDataBreakpointswatchpoint
threads스레드 목록
stackTrace콜스택
scopes한 프레임의 스코프 (Local/Arg/Reg)
variables한 스코프의 변수들
continue / next / stepIn / stepOut진행
evaluate표현식 평가

VSCode의 변수 트리는 재귀적인 variables 호출입니다. 사용자가 트리 노드를 펼칠 때마다 그 노드의 variablesReference를 인자로 다시 호출. 결과적으로 깊은 객체도 지연 로드되어 IDE가 빠릅니다.

#Emacs — dap-mode / gud

전통적으로 Emacs는 M-x gdb로 GUD(GDB UI mode)를 썼습니다. 요즘은 dap-mode로 같은 DAP 어댑터를 공유.

(use-package dap-mode
:after lsp-mode
:config (dap-auto-configure-mode))
(use-package dap-cpptools) ; cppdbg
(use-package dap-lldb)
(dap-register-debug-template
"C++ Launch"
(list :type "cppdbg"
:request "launch"
:name "C++ Launch"
:MIMode "gdb"
:program "${workspaceFolder}/build/my_program"
:cwd "${workspaceFolder}"))

자세한 설정은 dap-mode 문서. Doom Emacs는 lsp +debugger 모듈로 한 번에.

#데이터·메모리 시각화

  • VSCode의 Hex Editor + memoryview — 메모리 영역을 hex 그리드로.
  • gef의 memoryview — 메모리 영역을 색칠해 표시.
  • gdb의 dashboard memory — 직접 watch 영역 지정.
  • GDB 자체x/100bx 0x... 또는 Python으로 binary blob 시각화.

큰 버퍼·이미지 디버깅에서는 hex 뷰가 콜스택보다 더 자주 쓰입니다.

#NumPy/이미지 자체 시각화

GDB Python으로 NumPy 배열을 PNG로 떨어뜨려 외부 뷰어로 보기.

class ViewImage(gdb.Command):
def __init__(self):
super().__init__("view_image", gdb.COMMAND_USER)
def invoke(self, arg, from_tty):
v = gdb.parse_and_eval(arg)
w = int(v['width']); h = int(v['height'])
data = v['data']
# 메모리를 numpy로
import numpy as np
buf = gdb.selected_inferior().read_memory(int(data), w*h*3)
arr = np.frombuffer(buf, dtype=np.uint8).reshape((h, w, 3))
from PIL import Image
Image.fromarray(arr).save('/tmp/view.png')
import subprocess
subprocess.Popen(['xdg-open', '/tmp/view.png'])
ViewImage()

컴퓨터 비전·비디오 코덱·머신러닝 디버깅에서 가시화는 콜스택을 100번 들여다보는 것보다 빠릅니다.

#Cortex-Debug — Cortex-M을 붙일 때

Cortex-M 펌웨어라면 Cortex-Debug가 IDE 쪽 표준입니다. OpenOCD·J-Link·ST-Link·Black Magic Probe를 자동으로 띄우고 GDB를 붙여 주므로, Ch 8에서 손으로 하던 gdbserver 연결이 설정 한 덩어리로 줄어듭니다.

이 시리즈의 관심사는 GDB 쪽이므로 여기서는 GDB 사용자가 얻는 것만 짚습니다. SVD 파일을 읽어 peripheral 레지스터를 비트 단위로 펼쳐 주고, Segger RTT 출력을 콘솔에 얹어 주며, J-Link에서는 CPU를 세우지 않고도 변수를 갱신합니다.

launch.json 전체 예시와 servertype별 차이는 GDB Extension and IDE Ch 6에 있습니다.

#어느 걸 골라야 하나

상황추천
가벼운 한 번 디버깅GDB TUI(Ctrl-x a)
일상 — 터미널만cgdb 또는 gdb-dashboard
일상 — IDE 같이VSCode + cppdbg/CodeLLDB
Vim 사용자nvim-dap + dapui
CTF / exploitpwndbg 또는 gef
임베디드 GDB (Cortex-M)VSCode + Cortex-Debug
자동화 / 헤드리스gdb -batch + Python
원격 + 콘솔만cgdb over SSH

#정리

  • 내장 TUI(Ctrl-x a)는 빠른 시각화, 화면 깨짐만 감수하면 충분.
  • cgdb·gdb-dashboard가 터미널에서 가장 안정적.
  • gef/pwndbg는 보안·CTF 용으로 강력하지만 일반 작업엔 시끄러움.
  • IDE 통합은 VSCode + cppdbg/CodeLLDB 또는 nvim-dap.
  • DAP가 IDE↔디버거 표준 — request/response/event 셋의 JSON-RPC.
  • 변수 트리는 variables 재귀 호출로 지연 로드.
  • Cortex-M은 VSCode Cortex-Debug 익스텐션이 자동으로 OpenOCD/J-Link까지 묶어 줌. SVD/RTT 통합.

#다음 장 예고

Ch 11(시리즈 마지막) — 실전 팁. STL pretty-printer, -O2로 빌드된 코드 디버깅, .gdbinit 추천 설정, time-travel(rr)으로 마무리.

#관련 항목