1. 内核调试的现实:为什么用户态工具不好使
1.1 从用户态GDB到内核态:调试场景的落差
调试内核和调试普通用户程序体验完全不一样。你在用户空间用gdb,能随便断点、单步、看变量,就算程序崩了,core dump一堆寄存器、栈帧也摆在那里让你分析。可一到内核态,事情全变了:没有进程的概念,没有属于你的地址空间,没有异常处理机制兜底,一个空指针直接就是整机panic或者卡死在某个锁上。很多驱动开发、嵌入式Linux工程师的日常工作,就是靠printk打日志,改一行重编重烧一次,然后看串口输出猜问题在哪。运气好,两三轮就定位了;运气不好,一个神秘崩溃点能让你耗上一整天。
这时候就需要真正的内核级调试手段。KGDB和KDB这两个子系统,就是Linux内核官方提供的两把钥匙:KGDB允许你在一台主机上用GDB调试另一台机器上的内核,KDB则是目标机上直接运行的命令行调试器。它们解决的都不是"看一眼日志"的问题,而是"让内核停下来、把内部状态完整呈现在你面前"的问题。内核模块开发、驱动调试、嵌入式Linux移植、系统故障定位,这几类场景都会用到它们。
1.2 KGDB与KDB:一个连接器、一个现场调试器
先说KGDB。它的全称是Kernel GDB,让GDB通过串口或者网络连接到目标内核,支持的调试能力和用户态GDB非常像:打断执行、下断点、单步、读写内存和寄存器、打印调用栈。对经常用GDB的工程师来说,这套交互逻辑几乎是零学习成本。
KDB则走的是完全不同的路子。它不依赖外部主机,直接在目标机的串口控制台或者键盘上运行一套命令。什么场景下会需要它?比如你手里没有另一台开发机,或者现场环境不方便接线,又或者系统已经进入了一个网络和存储都失效的异常状态,你只想快速看看寄存器和栈,确认一下是哪个模块把系统带崩了。KDB就是这样一种"急救式"的内核调试工具,不需要远程连接,内核里内置一套命令就能看现场。
两者不是互斥关系,而是配合关系。你可以先用KDB做快速检查,再切换到KGDB模式让外部GDB接管做深度分析。后面会专门讲这个切换过程。
2. 环境搭建与内核编译配置
2.1 内核配置选项:哪些必须开、哪些推荐开
KGDB不是内核默认打开的选项,使用前必须重新配置内核。最直接的方式是make menuconfig,进入Kernel hacking菜单,找到KGDB: kernel debugging with remote gdb相关选项打开。
下面这些配置项,我的建议是直接照抄:
CONFIG_KGDB=y CONFIG_KGDB_SERIAL_CONSOLE=y CONFIG_KGDB_KDB=y CONFIG_DEBUG_INFO=y CONFIG_KALLSYMS=y CONFIG_FRAME_POINTER=y CONFIG_MAGIC_SYSRQ=y逐一说下为什么要开。CONFIG_KGDB是总开关,不开就没有后面所有功能。CONFIG_KGDB_SERIAL_CONSOLE负责把KGDB绑定到串口控制台,这是目前最稳定的连接方式。CONFIG_KGDB_KDB开启KDB命令行前端,等于给KGDB加了一个内置调试界面,强烈建议打开,后面会用到。CONFIG_DEBUG_INFO生成带完整符号信息的vmlinux文件,这是GDB能看懂内核函数的必要条件,不开的话断点全打在未知地址上,无从下手。CONFIG_KALLSYMS提供符号表,内核崩溃打印栈回溯和KDB内部的符号查找都依赖它。CONFIG_FRAME_POINTER保留栈帧指针,栈回溯会更准,代价是略有性能损耗,调试期完全可以接受。CONFIG_MAGIC_SYSRQ是进入KDB的快捷通道,也很有用。
还有一个容易被忽略的点:如果你的内核开了KASLR(内核地址空间随机化),建议在启动参数里加上nokaslr。否则每次启动内核的加载地址都不一样,你编译好的vmlinux里记录的符号地址可能对不上,断点会落在错误位置,调试体验非常坑。
2.2 启动参数与终端准备:kgdboc、kgdbwait、kgdbearly
配置好内核,编译并安装到目标机之后,还要在bootloader的启动参数里加两个东西:kgdboc和kgdbwait。kgdboc是"KGDB over Console"的缩写,指定KGDB用哪个串口、什么波特率工作。比如你的调试串口是ttyS0,波特率115200,启动参数就写成:
kgdboc=ttyS0,115200 nokaslrkgdbwait的意思是内核启动时先停在KGDB的初始化点,等外部GDB连上来。这样你就能从内核启动的最早期开始调试,非常适合排查启动阶段的崩溃。完整启动参数示例:
root=/dev/mmcblk0p2 console=ttyS0,115200 kgdboc=ttyS0,115200 kgdbwait nokaslr注意,console和kgdboc都用了同一个串口,这没问题。KGDB和内核控制台可以共存,但你得知道,控制台打印和KGDB交互共用同一个物理串口,调试期间控制台的大量日志输出可能会干扰GDB交互。我一般习惯在调试时把console的日志级别调低,减少干扰。
如果你不想改bootloader,KGDB还支持运行时加载。在内核起来之后,向/sys/module/kgdboc/parameters/kgdboc写入串口参数即可:
echo ttyS0,115200 > /sys/module/kgdboc/parameters/kgdbockgdbearly是另一个相关选项,它让KGDB在内核更早的阶段就能启用。一般常规调试用不上,但排查早期初始化问题时会需要,你知道有这个东西就行。
2.3 GDB客户端准备:vmlinux、System.map和gdb脚本
目标机内核编好之后,编译目录里会有几个关键文件:vmlinux是未压缩的内核镜像,带调试符号;System.map是符号表;还有arch/xxx/boot/下的压缩镜像如uImage、zImage。调试时,主机端GDB加载的一定是vmlinux,不是那些压缩镜像。
我的建议是,把vmlinux和System.map复制到主机端的调试目录,然后写一个.gdbinit,把常用配置固化下来:
set architecture i386:x86-64 set remotebaud 115200 set pagination off target remote /dev/ttyS0set remotebaud在部分GDB版本里不是必须的,但写上没坏处,防止默认波特率不对。set pagination off是关闭GDB分页,否则调试过程中输出一屏就停下来等你回车,在远程调试场景里会很烦。target remote /dev/ttyS0是连到主机这边的串口设备。注意,这里/dev/ttyS0指的是主机上的物理串口,不是目标机上的,别搞混。
还有个小细节:GDB连接远程内核时,推荐加上硬件断点支持的相关命令,后面实战部分会讲为什么。
3. 实战操作:用KGDB下断点调试内核
3.1 连接流程与基本GDB命令
准备工作做完,连接流程其实就三步。第一步,目标机带上kgdboc和kgdbwait启动,内核会停在KGDB的等待点,屏幕上一般会有提示,比如Waiting for connection from remote gdb...。第二步,在主机上启动GDB并加载vmlinux。第三步,执行target remote /dev/ttyS0,看到Remote debugging using /dev/ttyS0的提示,说明连接成功了。
连接成功后,内核处于暂停状态,GDB等着你的命令。最常用的操作是先看当前调用栈:
bt这个命令在用户态和内核态用法一样,能立刻打印出当前CPU上的内核函数调用链。接下来可以继续执行:
continue让内核跑起来。这是KGDB最顺手的地方——内核就像普通程序一样被GDB托管,你随时可以按Ctrl+C把运行中的内核打断,回到GDB命令行,这在排查死循环、软锁死这类问题时特别管用。
3.2 内核态断点:函数断点、条件断点、数据断点
内核里下断点,最基础的是函数断点。例如想抓住进程退出流程,直接:
break do_exit有的函数名和用户态符号冲突,或者被编译器内联优化掉了,GDB可能提示找不到。这时可以看一下info address确认符号是否存在,或者用break *地址直接下地址断点。
条件断点在排查特定场景问题时很高效。比如你只关心PID为1234的进程调用某个函数,可以写成:
break do_exit if current->pid == 1234注意数据结构字段的访问,内核里current是一个宏,GDB环境里通常能解析,但字段名需要你根据内核版本调整。
数据断点,也就是硬件观察点,主要用来查某个变量被谁改写了。例如监控一个全局变量:
watch global_counter内核模式下的数据断点要依赖硬件调试寄存器,数量有限,一般2到4个,别贪多。还有一点,软件断点会改写内核代码区域的指令,在只读内存或者指令缓存不一致的架构上可能出问题,所以内核调试我更推荐用硬件断点:
hbreak sys_synchbreak强制在内存中放置硬件断点,中断触发逻辑更可靠,代价是数量受限。
3.3 查看和修改内存、寄存器、调用栈
内核态和用户态GDB的内存查看命令是一样的。查看一段内存内容:
x/20x 0xffffffff81001230这个命令以十六进制查看从指定地址开始的20个字。想查看字符串:
x/s buffer_address查看寄存器:
info registers单条指令单步走到崩溃现场之后,info registers能告诉你所有CPU寄存器的当前值,配合bt查看栈回溯,基本能还原出代码执行路径。
修改内存和变量是KGDB另一个硬核能力。在KGDB调试中,可以直接修改内核变量的值,让程序走另一个分支:
set variable some_global = 1也可以直接写指定内存地址:
set {unsigned long}0xffffffff81001230 = 0xdeadbeef这块操作一定要谨慎。内核态的内存没有用户态那种保护机制,写错一个地址,轻则当场panic,重则让脏数据落盘,破坏文件系统。
4. KDB命令行调试:不依赖主机也能玩内核调试
4.1 KDB进入方式与常用命令
KDB的使用场景是"目标机现场直接调试"。进入KDB最常见的方式是使用魔术键SysRq。内核开了CONFIG_MAGIC_SYSRQ后,在串口控制台输入:
echo g > /proc/sysrq-trigger或者按SysRq-g组合键。内核会暂停所有CPU,进入KDB命令行。这时候你会看到类似Entering kdb on processor 0的提示,然后出现KDB命令提示符。
KDB有一套和GDB相似但更精简的命令,我把最常用的列成一张表:
| 命令 | 作用 |
|---|---|
help | 查看所有命令和用法 |
go | 让内核继续运行 |
bt | 打印当前CPU调用栈 |
regs | 显示CPU寄存器 |
md | 显示内存内容 |
mm | 修改内存内容 |
bp | 设置断点 |
bc | 清除断点 |
ss | 单步执行 |
kgdb | 切换到KGDB模式,等待外部GDB连接 |
比如查看当前调用栈:
[0]kdb> btKDB的bt输出没有GDB那么详细,但对于快速判断内核卡在哪个函数已经足够了。md命令还能跟着访问长度,比如md 0xffffffff81001230 40表示显示40个双字。
4.2 用KDB排查死锁与crash
KDB最实用的场景是排查死锁。系统卡住不动时,按SysRq-g进入KDB,先执行bt看每个CPU的调用栈。KDB会把每个CPU的当前状态列出来,你一眼就能看出哪个CPU在自旋锁上等待,哪个CPU持锁后没释放。
典型做法是:
[0]kdb> bt然后对比不同CPU的栈顶函数。如果CPU0停在raw_spin_lock,CPU1停在spin_unlock之前,那基本可以判断存在锁竞争问题。配合regs查看寄存器,还能看到$rip指向哪条指令,确认是不是在自旋循环里。
遇到内核崩溃、Oops现象但系统还没完全死透时,也可以用KDB做现场保留。进入KDB后先regs保存寄存器现场,再md读出oops地址附近的内存,最后go让系统继续,配合日志分析。
4.3 KDB与KGDB之间的切换
KDB和KGDB并不是两个割裂的系统。在KDB命令行里执行:
[0]kdb> kgdbKDB会暂停当前CPU,等待外部GDB通过之前配置的kgdboc串口连上来。这时候主机端GDB执行target remote /dev/ttyS0,就能接管调试。这个场景常用于:现场先用KDB快速判断出可疑函数,然后切成KGDB,用GDB的断点条件、观察点做进一步深入分析。
反过来,如果GDB正在调试,你想回到KDB的纯命令行界面,可以直接在GDB里断开连接,然后在目标机控制台重新触发SysRq-g。这套切换机制非常灵活,相当于一个是"现场应急",一个是"全面深入"。
5. 常见问题与避坑指南
5.1 连接失败与无响应的排查
KGDB最常见的坑,是kgdbwait之后GDB连不上自己。遇到这种情况,照着下面顺序排:
- 确认目标机的
CONFIG_KGDB_SERIAL_CONSOLE确实编译进去,不是只开了CONFIG_KGDB。可以开机后查看/proc/cmdline,确认kgdboc参数存在。 - 确认串口线是交叉线还是直连线。大部分开发板用交叉线,有些调试底板是直连,物理层不对的话信号根本不通。
- 确认串口设备权限。主机端运行GDB的用户需要有
/dev/ttyS0的读写权限,dialout组或者uucp组可以解决,不行就sudo。 - 确认波特率一致。目标机启动参数里
kgdboc=ttyS0,115200,主机GDB端set remotebaud 115200,两边不一致时会出现乱码或者干脆连不上。 - 如果用了虚拟机和USB转串口,还要确认宿主机把串口直通给了虚拟机,而不是被宿主机的终端工具占用了。
5.2 断点不触发或地址对不上
断点不触发,十有八九是内核地址对不上。主要两个原因:KASLR和镜像类型。KASLR的问题前面说了,加nokaslr启动参数。镜像类型的问题更隐蔽:你加载的是vmlinux,但目标机实际跑的是uImage或者Image,两者的加载地址可能存在偏移。这种情况下,GDB里加载vmlinux后,需要手动调整符号地址。
一个实用技巧是,先在GDB里连接上正在运行的内核,然后:
info files查看加载了哪些段,和vmlinux的段地址做对比,如果发现偏移,用add-symbol-file重新加载符号文件并指定偏移量。还有一种情况是断点设在了一个被内联掉的短函数上,编译器把它优化没影了,GDB能解析符号,但实际没有独立代码。这种可以断到调用它的父函数,或者用disassemble查找内联后的入口。
5.3 串口日志干扰与printk刷屏
KGDB和内核控制台共用串口时,如果有大量printk日志持续输出,会在主机的GDB终端上刷屏,严重时干扰远程协议交互。
解决思路有两个。第一,启动参数里调高日志级别:
loglevel=3loglevel=3表示只输出KERN_ERR以上级别的日志,能大幅度降低串口输出量。第二,如果必须保留日志,可以换一路独立串口给KGDB用,让控制台和调试通道物理分离。一些开发板有多个UART,把console配到UART1,kgdboc配到UART2,能彻底解决干扰问题。
5.4 单步执行卡死或跳飞
单步执行内核代码,尤其但在中断上下文或者访问外设寄存器的路径上,偶尔会遇到步过去之后内核直接卡死的情况。原因通常是在KGDB单步时会触发本地中断或者NMI,尤其是定时器中断,导致处理器进入异常处理路径,单步的逻辑被打破。
如果遇到单步卡死,不要硬拼,改用以下思路:
- 优先用
continue带着断点跑,用断点而不是单步来逼近问题点。 - 用硬件断点
hbreak,可以让CPU在指定地址处直接停下,比单步更稳。 - 如果只在某几条指令上需要单步,先关掉本地中断,比如在目标机进入KDB后,用
regs确认IF标志,必要时调整中断状态再做单步。
5.5 网络调试与现场遗留问题
除了串口,KGDB理论上也可以走网络。内核提供了kgdboc的kgdboe网络调试补丁和kgdbgp等方案,但整体成熟度不如串口调试,尤其是网络驱动本身如果有问题,调试工具反而先挂了。我的经验是,常规调试优先串口。只有在串口不可用、网络驱动确定正常的情况下,再考虑网络调试。
另外,KGDB和KDB调试过程中有一些"现场遗留"要注意:调试完一定要记得恢复正常状态,比如清除所有临时断点、把修改过的全局变量恢复原值,然后go让系统继续。如果有改动挂载中的文件系统缓存,尽量先sync再进调试,免得调试中断时数据没落盘。
6. 一些个人经验与学习建议
6.1 从哪里开始:建议先KDB后KGDB
如果你之前没用过内核调试工具,我的建议是先玩KDB。KDB门槛低,不需要第二台机器,在开发板上改一下启动参数就能进去。先把bt、regs、md这些基础命令练熟,理解内核是怎么把函数一层层调进去的。之后再上KGDB,你会发现GDB的远程调试交互太熟悉了,上手速度比直接从KGDB起步快得多。
6.2 调试期内核配置的取舍
带KGDB和完整调试信息的内核,性能会比release版本下降一些,而且vmlinux文件体积很大,动辄几百MB。所以在现场设备上不要长期跑这个配置。我的习惯是:日常测试用 release 内核,等出现疑似内核态问题、需要深入调试时,再刷 KGDB 版本的内核复现。复现不了就先抓系统快照,带上日志再去调。
6.3 与printk、trace工具的配合
KGDB和KDB再强,也不是所有内核问题都适合用它们查。比如性能类的瓶颈分析,用perf更合理;调试某个函数被调用频率,function trace更直接;最简单的崩溃点,可能printk打两行日志就能定位。我自己的实践是,printk做初步排查,ftrace抓函数流,KGDB/KDB做最终断点级分析。这条链路配合下来,绝大多数内核态问题都能有清晰的解法。