Linux嵌入式内核调试技巧:通过JTAG+GDB实现内核态单步调试
嵌入式Linux内核出问题时,printk和dump_stack往往不够用——你需要在代码执行到某个点时停下、查看变量、单步跟踪。JTAG调试器配合GDB,能让你像调试普通应用程序一样操作内核,甚至能在中断上下文、调度器内部下断点。本文以OpenOCD + GDB + ARM Cortex-A平台为例,演示内核态单步调试的完整链路。
硬件准备
你需要一块带JTAG/SWD接口的目标板和一个调试器。推荐J-Link或ST-Link/V2,它们对OpenOCD支持良好。连接方式:
调试器引脚 目标板引脚
TMS/SWDIO SWDIO
TCK/SWCLK SWCLK
GND GND
TDO? 可选
对于多核SoC,确保调试器连接到调试APB接口(通常叫"Debug Port")。部分板子需按住复位键再上电才能进入调试模式。
软件环境搭建
宿主机安装OpenOCD和交叉编译版的GDB:
sudo apt install openocd gdb-multiarch
对于ARM64目标,使用gdb-multiarch或aarch64-linux-gnu-gdb。
OpenOCD配置与启动
创建一个板级配置文件target.cfg,以STM32MP1为例:
source [find interface/stlink.cfg]
transport select hla_swd
source [find target/stm32mp15x.cfg]
# 设置工作频率
adapter speed 1000
# 复位后暂停
reset_config srst_only
init
halt
启动OpenOCD:
openocd -f target.cfg
看到Info : Listening on port 3333 for gdb connections表示成功。
连接GDB并加载内核符号
另开终端,启动GDB并连接OpenOCD的GDB服务器:
gdb-multiarch vmlinux # 必须是未strip的内核映像,含调试符号
(gdb) target remote localhost:3333
(gdb) monitor reset halt
(gdb) flushregs
此时GDB已经控制了CPU,PC停在复位向量处。但内核尚未解压,我们需要先让内核正常启动,然后在感兴趣的地方打断点。
设置断点与单步
内核调试常用断点位置:
# 在start_kernel入口打断点
(gdb) hbreak start_kernel
(gdb) continue
hbreak是硬件断点,内核代码可能位于只读区域,硬件断点更可靠。当内核执行到start_kernel时,GDB会停下来。
此时可以查看调用栈、局部变量:
(gdb) bt
(gdb) info locals
(gdb) print init_task
(gdb) list *$pc
单步执行:
(gdb) stepi # 单步一条汇编指令
(gdb) nexti # 跳过函数调用
(gdb) finish # 执行到当前函数返回
注意:内核态单步时,如果遇到中断或上下文切换,GDB可能会失去控制。建议在单步前关闭本地中断(cli指令),或使用stepi逐条汇编指令前进。
调试内核模块
动态加载的模块也需要符号。先在宿主机上找到模块的.ko文件,用add-symbol-file命令添加:
(gdb) add-symbol-file mydriver.ko 0xbf000000
模块加载地址可通过/sys/module/mydriver/sections/.text获得,或在目标板上执行cat /proc/modules查看。
常用调试技巧
1. 查看内核数据结构
(gdb) print ((struct task_struct *)0xc0a00000)->comm
(gdb) print *(struct list_head *)0xc0b00000
2. 条件断点
(gdb) break do_fork if nr_threads > 100
3. 修改内存
(gdb) set variable current->state = 0
(gdb) set {int}0xc0008000 = 0xdeadbeef
4. 查看MMU页表
(gdb) monitor mw 0x80000000 0x12345678 # 向物理地址写值
(gdb) monitor md 0x80000000 4 # 读物理地址
注意事项
Watchdog:内核调试时WDT可能复位系统,建议在设备树或内核配置中禁用。
MMU映射:内核使用虚拟地址,GDB看到的地址也是虚拟的。如果需要操作物理地址,使用OpenOCD的monitor命令。
SMP多核:GDB默认只连接一个核心。用info threads查看所有核,thread N切换。
KGDB替代方案:如果没有JTAG,可以用KGDB通过串口或以太网调试,但无法在启动早期打断点。
写在最后
JTAG+GDB的组合是嵌入式内核调试的终极武器——你能在调度器代码中单步、在中断处理函数里查看寄存器、甚至在系统调用入口捕获参数。虽然初始配置稍显繁琐,但一旦打通链路,定位那些“printk永远打不出来”的bug会变得无比直观。对于从事BSP开发或驱动移植的工程师来说,这套技能的价值远超学习成本。





