当前位置:首页 > 嵌入式 > 嵌入式分享
[导读]嵌入式Linux内核出问题时,printk和dump_stack往往不够用——你需要在代码执行到某个点时停下、查看变量、单步跟踪。JTAG调试器配合GDB,能让你像调试普通应用程序一样操作内核,甚至能在中断上下文、调度器内部下断点。本文以OpenOCD + GDB + ARM Cortex-A平台为例,演示内核态单步调试的完整链路。


嵌入式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开发或驱动移植的工程师来说,这套技能的价值远超学习成本。



本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除( 邮箱:macysun@21ic.com )。
关闭