Linux 程序调试最佳实践
GDB 命令行确实反人类,但它是基础。现代开发者都在用更好的工具层叠在 GDB 之上。
一、先解决 GDB 难用的问题
TUI 模式 —— GDB 自带的图形化
bash
gdb -tui ./game_server # 或进入 GDB 后 (gdb) tui enable (gdb) layout src # 上方显示源码,下方命令行 (gdb) layout asm # 显示汇编 (gdb) layout split # 源码 + 汇编并排 (gdb) layout regs # 显示寄存器
但 TUI 还是很丑,真正好用的是下面这些。
GDB Dashboard —— 免费,效果接近 IDE
bash
# 安装(一行命令) wget -P ~ https://git.io/.gdbinit # 之后 gdb 自动加载,显示: # ─── Source ────────────────────────── # 当前执行位置高亮 # ─── Variables ─────────────────────── # 局部变量实时显示 # ─── Stack ─────────────────────────── # 调用栈 # ─── Threads ───────────────────────── # 所有线程状态
pwndbg / peda —— 功能更强的 GDB 增强
bash
pip install pwndbg # 或 git clone https://github.com/pwndbg/pwndbg && cd pwndbg && ./setup.sh
二、真正好用的方案:IDE 集成调试
VS Code + GDB(最推荐)
安装 C/C++ Extension,然后配置 .vscode/launch.json:
json
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug Game Server",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/build/game_server",
"args": ["--config", "dev.json"],
"stopAtEntry": false,
"cwd": "${workspaceFolder}",
"environment": [
{"name": "LOG_LEVEL", "value": "DEBUG"}
],
"MIMode": "gdb",
"setupCommands": [
{
"description": "Enable pretty-printing",
"text": "-enable-pretty-printing"
}
]
},
{
"name": "Attach to Process", // 附加到正在运行的进程
"type": "cppdbg",
"request": "attach",
"program": "${workspaceFolder}/build/game_server",
"processId": "${command:pickProcess}",
"MIMode": "gdb"
},
{
"name": "Debug Core Dump", // 分析 coredump
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/build/game_server",
"coreDumpPath": "/tmp/core.12345",
"MIMode": "gdb"
}
]
}
之后就是正常 IDE 体验:点击行号设断点、Watch 窗口看变量、Call Stack 面板看调用栈,F5/F10/F11 步进。
CLion(付费,最完整)
JetBrains 出品,CMake 项目直接打开就能调试,智能感知 + 调试体验最接近 Visual Studio。EA 这种大厂内部很多人用 CLion。
三、不同场景的调试策略
场景1:本地开发调试
bash
# 编译时必须加调试信息,关闭优化 cmake -DCMAKE_BUILD_TYPE=Debug .. # 等价于:g++ -g -O0 -DDEBUG # -g3 包含宏定义信息(调试宏时有用) g++ -g3 -O0 game_server.cpp -o game_server
关键:-O2 优化后变量可能被优化掉、循环被展开、函数被内联,GDB 看到的代码行顺序会乱掉,调试体验极差。开发阶段一定用 -O0。
场景2:生产环境 Crash 分析(coredump)
bash
# 1. 生产环境开启 coredump(加到启动脚本) ulimit -c unlimited echo "/var/cores/core.%e.%p.%t" > /proc/sys/kernel/core_pattern # 2. 关键:生产编译用 -O2 但保留符号 g++ -O2 -g game_server.cpp -o game_server # 或者分离符号文件(不把调试信息放进二进制) g++ -O2 game_server.cpp -o game_server objcopy --only-keep-debug game_server game_server.debug strip game_server # 生产二进制不带调试信息,体积小 # 崩溃后加载符号: gdb game_server -s game_server.debug -c core.12345 # 3. 用 VS Code 打开 coredump 分析 # launch.json 里配置 coreDumpPath 即可
场景3:内存问题(最头疼)
AddressSanitizer —— 首选,开发期必用
bash
# 编译时开启,无需改代码
g++ -fsanitize=address,undefined -fno-omit-frame-pointer -g -O1 \
game_server.cpp -o game_server_asan
# 运行时自动检测:
# - 堆越界(heap buffer overflow)
# - 栈越界(stack buffer overflow)
# - use-after-free
# - double-free
# - 内存泄漏(进程退出时报告)
# 输出示例:
# ERROR: AddressSanitizer: heap-use-after-free on address 0x...
# READ of size 4 at 0x... thread T0
# #0 0x... in PlayerManager::update() player.cpp:42
# #1 0x... in GameLoop::tick() gameloop.cpp:87
ThreadSanitizer —— 检测数据竞争
bash
g++ -fsanitize=thread -g -O1 game_server.cpp -o game_server_tsan # 运行时报告: # WARNING: ThreadSanitizer: data race # Write of size 8 at 0x... by thread T2: # Previous read of size 8 at 0x... by thread T1:
Valgrind —— 更全面但很慢
bash
# 检测内存泄漏(程序跑慢 10-50 倍,适合功能测试阶段)
valgrind --leak-check=full \
--show-leak-kinds=all \
--track-origins=yes \
--log-file=valgrind.log \
./game_server
# 输出:
# LEAK SUMMARY:
# definitely lost: 1,024 bytes in 3 blocks ← 真正的泄漏
# indirectly lost: 2,048 bytes in 8 blocks
# possibly lost: 512 bytes in 1 blocks
场景4:性能问题
bash
# perf + 火焰图 —— 定位 CPU 热点
perf record -F 99 -g -p $(pgrep game_server) -- sleep 30
perf script | \
stackcollapse-perf.pl | \
flamegraph.pl > /tmp/flame.svg
# 在浏览器打开 flame.svg,横轴=占用比例,纵轴=调用深度
# 宽的块 = 热点函数,直接锁定优化目标
四、日志调试 —— 很多时候比断点调试更实用
生产环境不能接 GDB,结构化日志是主力。
spdlog(C++ 游戏后端首选日志库)
cpp
#include <spdlog/spdlog.h>
#include <spdlog/sinks/rotating_file_sink.h>
// 初始化:按大小滚动的日志文件
auto logger = spdlog::rotating_logger_mt(
"game", "logs/game.log",
100 * 1024 * 1024, // 100MB per file
5 // 保留5个文件
);
logger->set_level(spdlog::level::debug);
logger->set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%l] [tid:%t] %v");
// 使用
spdlog::info("Player {} joined match {}", player_id, match_id);
spdlog::warn("Match queue timeout, elapsed={}ms", elapsed);
spdlog::error("DB connection failed: {}", e.what());
// 关键:异步日志,不阻塞游戏线程
spdlog::init_thread_pool(8192, 1); // 队列8192条,1个后台写入线程
auto async_logger = spdlog::basic_logger_async_mt(...);
条件编译调试输出
cpp
// 开发期详细日志,生产期零开销
#ifdef DEBUG
#define DBG_LOG(fmt, ...) \
spdlog::debug("[{}:{}] " fmt, __FILE__, __LINE__, ##__VA_ARGS__)
#else
#define DBG_LOG(fmt, ...) do {} while(0) // 完全消除,零开销
#endif
// 带上下文的断言
#define GAME_ASSERT(cond, msg) \
do { \
if (!(cond)) { \
spdlog::critical("ASSERT FAILED: {} at {}:{}", \
msg, __FILE__, __LINE__); \
spdlog::dump_backtrace(); \
std::abort(); \
} \
} while(0)
五、实用 GDB 命令速查(必须记的那些)
真正需要上 GDB 时,就这几条最常用:
bash
# 启动 gdb ./game_server gdb -p 12345 # 附加到进程 # 断点 b player.cpp:42 # 文件行号 b PlayerManager::update # 函数名 b player.cpp:42 if id==5 # 条件断点(只在 id==5 时停) watch g_player_count # 变量变化时停 # 执行控制 r # run 启动 c # continue 继续 n # next 下一行(不进函数) s # step 进入函数 fin # finish 执行完当前函数 until 50 # 运行到第50行 # 查看状态 bt # backtrace 调用栈 bt full # 带局部变量的完整调用栈 frame 3 # 切换到第3帧 info locals # 当前帧所有局部变量 p player->health # print 打印变量 p/x addr # 十六进制打印 x/10xw 0x7fff... # 查看内存(10个word,十六进制) # 多线程 info threads # 列出所有线程 thread 3 # 切换到线程3 thread apply all bt # 所有线程打印调用栈(死锁分析必用)
死锁分析的固定动作:
bash
gdb -p <pid> (gdb) thread apply all bt # 看所有线程卡在哪里 # 通常会看到多个线程互相等 futex
六、推荐工具组合
日常开发:VS Code + CMake(Debug) + ASan
F5 启动 → 点击断点 → Watch 窗口
内存问题:ASan(首选)→ Valgrind(兜底)
多线程Bug:TSan(数据竞争) + 日志时序分析
生产Crash:coredump + VS Code/GDB 离线分析
+ 结构化日志回溯现场
性能瓶颈:perf 火焰图 → 定位热点 → 针对性优化
核心原则:让工具帮你找问题,而不是靠盯代码猜。ASan 能直接告诉你 use-after-free 在第几行,TSan 能告诉你哪两个线程在竞争哪个变量,花时间配好工具链比反复加 printf 调试高效十倍。
有特定调试场景想深入的话可以继续,比如多线程死锁定位、内存泄漏排查流程,或者直接转到下一个 EA 面试题?
