当前位置:首页 > 嵌入式 > 嵌入式分享
[导读]嵌入式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 )。
换一批
延伸阅读

在之前的系列底层技术文章中,我们先后深入探讨过Linux最大并发数、高效内存池设计、内存模型与内存序等核心系统主题,而GDB作为Linux平台最经典的原生调试工具,是所有C/C++开发者排查底层问题、分析程序异常的必备利...

关键字: GDB Linux程序

在Linux C/C++开发的世界里,GDB是每一个开发者都绕不开的核心调试工具。很多人对GDB的认知还停留在“断点、单步、打印变量”的基础操作层面,遇到复杂问题时依然只会靠加日志、重启程序试错,调试效率极低。实际上GD...

关键字: Linux C/C++ GDB

在C/C++服务端开发中,多线程死锁是最让人头疼的问题之一。它不像段错误那样直接触发程序崩溃留下明确的调用栈,而是让程序卡在原地完全失去响应,CPU占用率可能为0也可能居高不下,常规的日志打点很难精准定位到问题根源。很多...

关键字: GDB 多线程

在后端服务、嵌入式系统的长期运行过程中,多线程死锁是最让人头疼的疑难问题之一。很多时候程序没有任何崩溃迹象,CPU占用率也很低,日志输出突然完全停滞,所有业务请求全部卡死,进程就像进入了永久休眠状态。不少开发者遇到这类问...

关键字: GDB 嵌入式

在嵌入式开发的“战场”上,调试器就像工程师的听诊器,而JTAG与cJTAG接口则是连接这颗听诊器的核心探针。当工程师按下IDE里的“Download”按钮,或是开启实时变量观察窗口时,一行行代码和一个个数据正以惊人的速度...

关键字: JTAG cJTAG 调试

在嵌入式开发、硬件调试和芯片测试的世界里,JTAG是一个无处不在却又常常被开发者一知半解的核心存在。很多工程师天天用JTAG下载程序、调试芯片,却未必能说清它的本质,遇到下载失败、连接中断的问题时,只会盲目更换仿真器,找...

关键字: JTAG 嵌入式

在电子工程、芯片调试、嵌入式开发等领域,JTAG是绕不开的核心技术术语。很多从业者刚接触时总会疑惑:这个常被挂在嘴边的JTAG到底是什么?实际使用中又该怎么判断一套JTAG接口或设备是否正常?要理清这些问题,得从它的诞生...

关键字: JTAG 嵌入式

在嵌入式裸机开发(无OS、无C库)中,GDB + 远程调试协议(RSP) 是定位启动代码崩溃、MMU配置错误或外设初始化异常的核心手段。根据调试适配器与目标板接口的不同,通常有OpenOCD + GDB、J-Link G...

关键字: GDB 裸机开发

在软件开发过程中,调试是定位和解决问题的关键环节。GDB(GNU Debugger)作为Linux平台下最常用的调试工具,支持对C、C++等多种语言程序的调试,能够帮助开发者监控程序执行、检查变量值、定位崩溃原因。然而,...

关键字: GDB Linux

在多线程编程的世界里,死锁就像潜伏在代码中的幽灵,时不时就会出来作祟。它让线程们陷入互相等待的僵局,程序看似运行却毫无进展,CPU使用率骤降,排查起来更是让人头疼不已。GDB(GNU调试器)作为Linux平台下的调试利器...

关键字: GDB CPU
关闭