i007.cc

i007.cc

优先队列-降维打击

05.价值资料

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 面试题?

发表回复