gdb 常用命令汇总持续补充如果你写 C/C可能迟早会遇到这样一种情况程序跑了几十个小时在某次特定输入下突然崩了而日志停在最后一条正常输出之后什么都没有。我这些年排查过不少这类问题最顺手的工具始终是 gdb。它不挑场景不管是段错误、死锁还是内存被悄悄改写只要手上有一个带调试符号的程序和一份 core 文件大部分情况下都能顺着栈追到根因。这也是一份我长期维护的 gdb 常用命令笔记。我不按官方手册的顺序罗列命令而是按真实调试流程来组织先启动、再打断点、然后看栈和内存之后是条件断点和多线程场景最后是配置与踩坑。后续我会持续往里面补充命令和案例如果你也经常和崩溃问题打交道可以直接把这篇当作速查手册来用。1. 为什么遇到疑难崩溃我第一反应还是 GDB1.1 一次日志里根本看不出来的段错误排查很多新手觉得调试是程序出错了再加 printf 重跑一遍但有些 bug 不是多打两行日志就能定位的。我之前处理过一个模块它的日志很完整几乎把每个函数的入口和出口都打了。可崩溃发生的时候最后一条日志指向的是一段很正常的初始化代码后面调用的函数连日志都没来得及打就没了。重跑十次崩溃位置都不太一样典型的未定义行为。当时我把程序编译成带调试信息的版本然后用 gdb 打开崩溃时生成的 core 文件第一件事就是执行bt。几秒钟内调用栈清清楚楚列出来问题出在一个字符串拷贝函数里而往上翻两层栈帧发现某个指针已经被清零了。顺着这个线索再去看代码找到了一个提前释放对象但没置空引用的典型错误。整个过程没有加一行日志也没有反复重跑。这类经历多了之后我养成了一个习惯程序能跑通但结果不对先用日志程序直接崩溃或卡死先用 gdb。它不是万能的但它是把现象变成证据的最短路径。1.2 GDB 到底适合谁、能解决什么问题GDB 适合的不只是 C/C 开发者。只要是能产生调试符号和 core 文件的语言和场景它都能派上用场。比如 Rust 程序、部分 Go 程序甚至嵌入式交叉编译环境里的目标程序都可以用 gdb 或它的变体来调试。日常运维排查、开源项目二次开发、线上事故复盘都会用到它。它能解决的核心问题大概有四类崩溃定位程序为什么死是段错误、非法指令还是直接 abort看栈和信号就能定位。状态查看某个变量、寄存器、内存地址在当前时刻到底是什么值比猜或者加日志更直接。执行流控制让程序停在你关心的地方逐步执行观察状态变化。异常捕获在特定条件发生时停下来比如内存被改写、特定入参出现、某线程进入死锁。我常跟团队里人说一句话日志是事后调查gdb 是现场勘查。信息足够多的时候用日志就够了但到了需要精确知道那一瞬间发生了什么的阶段调试器是不可替代的。2. 启动与断点先让程序跟着你的节奏走2.1 编译时别忘了 -g优化级别越低越好调试很多人到了程序崩溃才想起来编译产物里没有调试信息。GDB 能读到的符号表、源码行号、变量名全靠编译时加入的调试信息。GCC 和 Clang 都认-g这个选项gcc -g -o app main.c utils.c g -g -O0 -o app main.cpp这里有一个关键取舍优化级别。如果用了-O2甚至-O3编译器会做内联、重排、变量优化GDB 看到的源码行号和寄存器里的变量可能对不上。我之前见过最离谱的情况是print一个局部变量GDB 直接报value has been optimized out。所以本地复现问题的建议是用-O0配合-g先把问题找回再考虑优化环境下的表现。如果在排查一个 release 版本问题至少用-g -O2重新编一份保留 DWARF 符号。虽然行号会有偏差但通常还能判断大致位置。顺便提醒交叉编译时要用对应的 gdb。比如 ARM 目标板上的程序本地 x86 的 gdb 往往打不开 core得用aarch64-linux-gnu-gdb这类工具。2.2 启动 GDB 的两种姿势直接调试与分析 coredump启动 GDB 最基础的方式是跟着一个可执行文件gdb ./app进入交互界面之后用run启动程序也可以带参数(gdb) run --port 8080 --config /etc/app.conf如果希望每次启动都固定参数用set args(gdb) set args --port 8080 (gdb) run另一种更实用的方式是分析 core 文件。系统会在程序崩溃时把内存现场写进 core默认路径通常在可执行文件所在目录或/var/lib/systemd/coredump/。用法是gdb ./app core进入之后不需要再run因为程序已经死了你只需要查看现场。有时你会发现core文件缺失多半是/proc/sys/kernel/core_pattern被改过或 shell 的限制把它们挡掉了ulimit -c unlimited我建议在开发机的~/.bashrc里默认加上这个限制省得崩溃发生时什么都没有。2.3 断点管理的四件套break、info、delete、disable调试图的起点就是断点。GDB 里设置断点最常见的方式有四种命令作用示例break 函数名在函数入口停break mallocbreak 文件名:行号在源码指定行停break main.c:42break 文件名:函数名精确到具体文件break src/net.cpp:handle_packetbreak *地址在地址停break *0x4012c0列出现有断点用info breakpoints每条断点都有编号。删除用delete 编号临时禁用用disable 编号重新启用用enable 编号。这三个动作我每天都用尤其是调试时经常要临时关掉一个无关紧要的断点避免反复命中。(gdb) info breakpoints Num Type Disp Enb Address What 1 breakpoint keep n 0x0000000000401200 main.c:42 2 breakpoint keep y 0x00000000004012a0 handle_packet注意缩进和列对齐实际输出可能更长。关键是看Enb列n表示这个断点当前是关着的。断点按编号管理理解这一点后很多操作都会顺手很多。2.4 next/step/continue/finish理解执行控制的粒度一进入卡住的场景新手经常分不清next、step、continue、finish到底用哪个。我的理解很简单nextn不进入函数在当前函数内执行到下一行。steps进入函数如果当前行调用了函数会停在被调函数的第一行。finish直接跑完当前函数停到调用者的下一行。continuec继续全速运行直到下一个断点或程序结束。还有一个常用的是until它可以在不需要临时断点的情况下跳出循环(gdb) until比如在一个 10000 次的循环里你已经在循环体里停了但不想一次次next走完整个循环until会一直执行到当前函数的下一条源码行对跳出循环很有用。控制执行的粒度要结合你当前的目标是想看子函数的内部逻辑还是只关心返回值。如果不需要进去千万别用step否则一旦碰上不懂的实现会浪费大量时间。3. 状态查看backtrace、print、x 和 info 的配合3.1 backtrace 永远是排查崩溃的第一把钥匙打开 GDB 或者加载 core 之后我做的第一件事永远是bt全称backtrace。它会把当前线程的调用栈一层层列出来从当前帧到最外层入口(gdb) bt #0 __GI_raise (sig11) at ../sysdeps/unix/sysv/linux/raise.c:50 #1 0x00007f... in __GI_abort () at abort.c:79 #2 0x00007f... in __assert_fail_base () ... #3 0x00007f... in __assert_fail () ... #4 0x00000000004015f0 in process_request (req0x0) at server.c:88 #5 0x000000000040199b in loop_poll (event0x7fff...) at server.c:214崩溃位置在process_request里入参req是空指针。看到这一眼事情就明朗了一半。bt带一个full参数时会额外打印每帧的局部变量和入参对分析更有利(gdb) bt full栈很深的时候用frame 编号切换上下文。比如我想看loop_poll里event变量的值(gdb) frame 5 (gdb) print eventframe不会退出第 0 帧的现场只是把视角切过去。这个技巧在处理多层函数嵌套的问题时非常有用比重新continue再断一次快得多。3.2 print 的格式化输出比普通日志可靠得多print可以打印变量、表达式、数组元素甚至函数返回值。但让我真正觉得它好用的是格式控制能力。默认打印十进制。想看十六进制用/x(gdb) print/x errno (gdb) print/x ptr看字符串用/s看浮点用/f看单个字符用/c看反汇编则用disassemble而不是 print。组合示例(gdb) print/d count (gdb) print/f score (gdb) print/s name_buf指针和结构体的打印也有技巧。直接print p只能看到指针地址要看它指向的内容用*p(gdb) print *p (gdb) print *req数组可以通过指针 长度的方式打印全部元素(gdb) print *buf10这个技巧在排查缓冲区内容被改写的场景里尤其好用。还有两个配置我强烈建议打开否则结构体输出会挤在一行里看不清(gdb) set print pretty (gdb) set print array onset print pretty让嵌套结构体按层级缩进set print array on让数组元素一行一个。日志打印做不到这种实时格式化GDB 却可以。3.3 内存查看 x 命令从变量到指针指向的数据print适合看解释后的值但有时候你更想看内存里的原始字节尤其是排查数组越界、结构体对齐、字节序问题时x命令是首选。它的格式是x/数量格式单位 地址单位有四类b字节、h半字2 字节、w字4 字节、g八字8 字节。格式有x十六进制、d十进制、s字符串、i指令、f浮点。最常用的几种(gdb) x/16xw 0x603020 # 打印从该地址开始的 16 个字十六进制 (gdb) x/20i main # 反汇编 main 函数前 20 条指令 (gdb) x/s name_buf # 查看字符串 (gdb) x/4gx array_ptr # 查看 4 个八字节内存常用于 64 位指针数组有一次排查内存被改写我用x/64xw把可疑缓冲区整个打印出来发现中间夹了一段不是程序写入的数据顺着地址对应的分配记录很快找到了越界写入的代码。这种视角是print给不了的。3.4 info 系列给程序做一次全面体检info是 GDB 里一个大类可以说它承担了程序体检的功能。我最常用的是这么几个命令看什么info registers所有通用寄存器和标志位info threads当前所有线程概况info locals当前栈帧的局部变量info args当前函数的入参info sharedlibrary动态库加载情况info files可执行文件和 core 的信息崩溃或卡死时先info registers看rip和rsp有时候比bt还快。比如非法跳转导致的崩溃bt可能显示的是很正常的栈但rip指向了一个根本不属于任何函数的位置这就说明栈或返回地址已经被破坏了。info localsbt full的组合是我每次定位崩溃的固定动作。一次拿到函数列表、入参和局部变量基本可以把问题范围缩得很小。4. 条件断点、观察点和断点命令让调试半自动化4.1 条件断点在百万次循环里精确命中第 N 次如果程序在一个循环里反复执行而问题只会在特定条件下触发手动一次次continue会让人崩溃。GDB 支持条件断点格式是break 位置 if 条件(gdb) break server.c:120 if req_id 42 (gdb) break handle_packet if packet_len 1024条件可以是任意能求值的表达式包括函数调用。只要条件满足程序才会停。更精细的写法是直接指定第几次命中才停(gdb) break server.c:120 (gdb) ignore 1 1000ignore会让 1 号断点前 1000 次命中都不停第 1001 次才生效。有些时候我需要统计条件触发的次数而不是一触发就停这时可以在条件里调用计数变量(gdb) break server.c:120 if hits 50不过要注意条件表达式里的副作用有时候会让程序逻辑变复杂不到万不得已我不这么做。4.2 观察点抓出偷改我变量的元凶有一类问题特别反直觉变量在代码里明明没有任何写入运行一圈回来却变了。这往往是缓冲区越界、野指针或者并发写入的结果。print在事后只能看到结果抓不住谁改的。这时用观察点。观察点分三种命令含义watch 变量或地址写入时停rwatch 变量或地址读取时停awatch 变量或地址读写都停最常用的场景是盯一个全局变量或堆地址(gdb) watch global_flag (gdb) watch *0x603020程序执行到任何一条试图写目标位置的指令时GDB 都会立刻停下并告诉你当前栈。搭配bt基本可以直接看到元凶。硬件观察点数量有限但多数 CPU 支持同时监视几个地址对排查实际问题足够了。这里有个经验观察局部变量时如果发现 GDB 报错或始终没有命中往往是变量已经被编译器优化掉或者它的生命周期已经结束。尽量观察具有固定地址的全局变量、堆内存。4.3 commands 命令列表断点触发时自动打印和续跑GDB 允许为断点绑定一组命令命中时自动执行。这是我调试高频率代码时最喜欢的半自动化手段。(gdb) break handle_packet (gdb) commands 2 silent printf packet len: %d\n, packet_len x/4xw packet_buf continue end解释一下commands 2给 2 号断点绑定命令序列silent让 GDB 不再打印命中信息printf按格式打印需要的值x/4xw打印关键内存continue自动继续执行。这样断点命中后不会打断运行节奏但我能持续收集数据。我经常用这个思路做低成本轨迹日志程序跑一天把关键函数的入参变化和返回值全部收集下来第二天直接分析。相比改代码加日志再重新编译部署这个方式对线上复现场景更省事。5. 多线程与多进程别死盯主线程5.1 线程切换与死锁定位程序是多线程时GDB 默认只显示当前线程。列出所有线程(gdb) info threads每行前面的星号表示当前线程。切换到指定线程(gdb) thread 3如果程序表现出卡死不退出的迹象第一时间对所有线程打栈(gdb) thread apply all bt这条命令会把每个线程的调用栈全部打出来。死锁的本质是两个线程各自持有锁、又等对方的锁。thread apply all bt一下两个栈会同时出现在视野里一个停在pthread_mutex_lock上另一个也停在pthread_mutex_lock上对应的锁地址如果相同基本就是锁等待问题。我还会配合info threads看线程状态。如果线程一直处于运行状态但栈没变化优先怀疑它陷入了死循环。调试过程中如果来源只有一个线程需要单步而其他线程也在运行可以用(gdb) set scheduler-locking on这样next或step时只有当前线程执行其他线程都冻结避免上下文干扰。调试完记得关掉否则continue会把其他线程锁死(gdb) set scheduler-locking off5.2 follow-fork-mode多进程程序的跟随策略用fork派生子进程的程序GDB 默认只跟父进程调试子进程会直接跑飞。想要调试子进程需要在启动前设置跟踪模式(gdb) set follow-fork-mode child (gdb) run如果父子进程都要守护用set detach-on-fork off这样 fork 后两个进程都会被 GDB 控制通过info inferiors切换。注意这个选项会显著增加调试复杂度非必要不开启。我踩过的一个坑是程序里 fork 比较频繁忘了设置follow-fork-mode导致子进程里出现的段错误无法拦截core 文件也经常存不下来。后来我固定在一个项目级.gdbinit里维护统一配置每个新同事拉下来就能直接用比口头说明可靠得多。5.3 attach 到运行中进程的几个注意点除了从启动开始调试GDB 还可以附着到正在运行的进程上。这在排查已经跑起来的服务为何异常时很有用gdb -p 12345进入之后程序会被临时暂停你可以设置断点、打印变量然后continue让它继续服务。但有几个问题必须提前想清楚权限普通用户通常不能 attach 他人进程需要用sudo或让进程与调试者同属一个用户。信号干扰attach 后程序可能收到SIGSTOP这会短暂中断服务对线上环境是有影响的最好在非高峰窗口操作。符号匹配如果运行中的可执行文件已经被 strip 或版本对不上attach 后很多变量名都不可用。我对生产环境的建议是能用 core 分析就不 attach实在要 attach先备份现场再找低峰期操作。6. 从崩溃到根因内存问题的完整排查链路6.1 段错误到底是怎么出来的段错误SIGSEGV是 C/C 调试中出现频率最高的信号。它的本质是程序访问了无权限或无效的内存地址。常见触发原因空指针解引用通过已释放的指针访问内存栈溢出数组越界写入破坏了返回地址多线程竞争导致的悬空指针定位段错误的标准流程是加载 core →bt→ 看当前帧与变量 → 必要时看x内存和反汇编。举例来说(gdb) bt #0 0x0000000000401300 in strcpy (dest0x0, src0x6030a0) ... #1 0x0000000000401348 in handle_msg (msg0x603080) at server.c:77第 0 帧在strcpy目标地址是0x0说明传入的dest是空指针。回看第 1 帧server.c:77大概率是分配内存失败后没有判空就直接拷贝。更刁钻的情况是栈顶和bt看起来都正常但info registers里的rip指向了无法识别的地址。这种情况十有八九是堆或栈缓冲区越界把返回地址写坏了。除了观察点还可以用x/20i $pc-20看崩溃点附近的反汇编判断真正执行的指令。6.2 检测内存泄漏时 GDB 能当临时拐杖很多人以为 GDB 能直接报告内存泄漏其实它不具备 valgrind 或 AddressSanitizer 那样的追踪能力。但有些环境下不方便跑外部工具时GDB 可以做粗糙的辅助采样。思路是断在malloc上用命令脚本统计分配次数和调用栈(gdb) break malloc (gdb) commands silent printf malloc called from %p\n, __builtin_return_address(0) continue end这只能给你哪里频繁调用 malloc的堆栈分布不能告诉你哪块内存在泄漏后没释放。真要精确检测泄漏我建议优先用 AddressSanitizer编译时加-fsanitizeaddress或者 valgrindgcc -g -fsanitizeaddress -o app app.c ./appASAN 在崩溃或严重越界时能直接给出栈和问题地址比 GDB 反汇编猜地址高效得多。GDB 更适合的场景是已经知道有问题想办法理解为啥这么写会触发而不是做内存工具的替代品。6.3 release 版本崩溃时的处理套路生产环境里跑的多半是-O2编译、带-g但可能被 strip 过的二进制。遇到这种现场我的处理顺序是保留现场的 core 文件和原始二进制哪怕没有符号。用file和info files确认二进制与 core 是否匹配。如果二进制被 strip尝试加载单独的 symbol 文件symbol-file app.debug。在带调试信息的环境重新编译一份同版本的二进制用gdb 新二进制 旧core打开部分 symbol 能重新对上。一句很关键的话永远别扔 release 二进制的构建产物和符号文件。它们平时看着没用等线上崩了就是唯一的救命稻草。我给团队定过一条规矩所有版本归档至少保存一份带完整调试符号的二进制哪怕体积很大。7. 我日常顺手用的 GDB 配置和几个大坑7.1 .gdbinit 里的增量配置GDB 启动时会读取用户目录下的.gdbinit也可以在项目目录放一个.gdbinit按优先级加载。我的基础配置长这样set pagination off set print pretty set print array on set print elements 100 set charset UTF-8 set disassembly-flavor intel set confirm off解释一下这几个配置的价值set pagination off输出一长串栈时不会被Press Enter to continue打断对复制日志和自动化分析特别关键。set disassembly-flavor intel反汇编用 Intel 语法可读性比 ATT 强很多多数教程和工具也用 Intel。set confirm off执行删除断点这类操作时不再反复确认。我还会在.gdbinit里定义几个自定义命令比如一个打印数组的快捷方式define parr print *$arg0$arg1 end用起来就是parr buf 16省去记print *buf16的格式。配置文件的本质是把高频动作固化成肌肉记忆值得花半小时整理。7.2 汇总几个真实踩过的坑下面这些坑我基本都亲身踩过写在这里提醒大家问题现象对策strip 掉符号的二进制bt全是地址没有函数名保留带符号的副本用symbol-file加载-O2行号漂移断点落不到预期位置本地用-O0复现或编译-g -O2watch 局部变量失效观察点不命中或报错观察全局变量或指定内存的绝对地址条件断点表达式太复杂命中奇慢或条件反复求值拆成多个简单断点 ignoreattach 线上进程导致服务中断进程短暂暂停或收到信号优先 core 分析attach 时选低峰期忘记设置 core 大小限制崩溃后找不到 coreulimit -c unlimited常态化交叉编译环境符号不匹配打开的 core 完全识别不了使用对应的交叉 gdb 版本其中行号漂移最隐蔽。-O1/-O2下某些代码会被重排序断点打在源码 120 行实际触发位置可能在旁边几行。我会用disas对照看几条汇编确认是不是预期逻辑再继续单步。7.3 我会持续往这个清单里加什么这份 gdb 常用命令笔记对我来说还在持续生长。接下来优先补充的方向有三个一是 Python 扩展脚本GDB 支持用 Python 编写调试指令用来处理复杂数据结构的自动分析很顺手二是更细的汇编级调试包括layout asm、reverse-step这类反向执行能力三是嵌入式场景里的远程调试也就是target remote和 gdbserver 的组合用法。调试这事工具命令背得再多不如真正在崩溃现场多观察几次。你第一次用bt就找到根因的那天就会明白为什么老开发者宁可少打一版日志也要留好一份 core。如果你有自己常用的 gdb 命令组合也欢迎交流补充我回头整理进这份清单。