OpenOCD芯片调试和固件提取-以STM32为例

OpenOCD芯片调试及固件提取-以STM32为例

前言

  • OpenOCD支持多种处理器架构、接口和仿真器,并且可拓展,是一个相对通用的开源片上调试工具,主要用于嵌入式系统的调试、编程和测试。本文介绍其用法并用它来调试STM32芯片和提取固件。

  • 除了OpenOCD,也可以用STM32CubeProgrammer等IDE读写固件 ,功能更全面,图形化程度更高,也有其它特定调试器的配套软件如针对JLINK的jflash等。

  • 相对其它工具,OpenOCD优势在于开源、灵活性强(兼容多种仿真器和芯片且可拓展)、轻量(命令行式操作),可以将其集成到其它工具或开发流程中。

  • 如果STM32开启了RDP(读保护),则无法通过该方式读写flash,需要关闭保护或利用漏洞、故障注入等手段绕过保护。

设备基本信息

PC环境:ubuntu2004虚拟机

仿真器:CMSIS-DAP

MCU芯片型号:STM32F407ZGT6

打开数据手册查看内存 映射中Flash的位置:0x0800 0000-0x080F FFFF

1

OpenOCD简介

全称Open On-Chip Debugger(开放式片上调试器),由Dominic Rath开发,是其毕业论文的一部分,最初是针对ARM7和ARM9芯片的片上调试。

OpenOCD提供对嵌入式设备的调试烧录、系统编程和边界扫描功能,可以配合仿真器连接设备的调试接口(尤其是JTAG/SWD)。

OpenOCD官网

OpenOCD Github

OpenOCD架构和原理

论文的第40页介绍了OpenOCD的架构,包含了如下几个模块

2

  • Daemon模块:守护进程(daemon)是首先被初始化的部分,它控制着其余组件。守护进程通过评估命令行参数来管理程序配置,并使用CLI模块解析配置文件。每个模块都可以向CLI模块注册命令,这些命令既可用作配置语句,也可作为用户命令,或者在配置选项后续可能更改的情况下同时用于两者。

  • JTAG模块:JTAG模块是唯一直接访问被调试硬件的部分,它为其他模块提供了与目标设备通信的接口。

  • Target模块:目标(Target)模块封装了常见的调试功能,并向CLI模块注册特定于目标的命令。守护进程在等待新连接时通过访问目标模块来跟踪目标状态,并向GDB和CLI模块提供目标调试功能。

  • GDB模块:GDB模块在守护进程接受新的GDB连接后被调用,它将GDB远程串行协议映射到目标模块提供的接口上。GDB远程协议中定义了一种监控数据包(monitor packet),允许发送应由GDB服务器解释的命令。GDB模块将这些命令字符串交给CLI模块进行评估,并将任何回复返回给远程GDB,从而将GDB会话中可访问的功能扩展到各个模块提供的所有命令。

  • CLI模块:CLI模块实现了telnet服务器,处理配置解析,并允许其他模块注册命令。

  • Flash模块:闪存(Flash)模块仅通过命令接口访问,因为GDB协议未规定闪存操作,它利用Target模块提供的内存访问功能来编程闪存芯片。

3

Target模块将目标状态分为四种:unknown(未知)、reset(重置)、running(运行)和halt(停止)。其中比较重要的是运行和停止状态。类似gdb的调试,运行状态可以通过断点或CLI中的halt指令切换到停止状态;停止状态下可以下断点、检查寄存器和读写内存,也可以恢复到运行状态,有单步执行功能。更详细的介绍请参考论文6.4节。

4

OpenOCD安装

ubuntu上用apt包管理器安装即可

1
sudo apt install openocd

要获取指定版本的OpenOCD可以编译源码或下载编译好的release包。参考Getting OpenOCD

OpenOCD使用

导入配置文件

OpenOCD需要导入正确的cfg配置文件才能连接调试目标,其针对常见硬件提供了的现成的cfg文件,位于/usr/share/openocd/scripts目录。

5

最基本的要求是导入targetinterface中对应的cfg配置文件。target指芯片型号,interface指调试器类型。

如果没有现成的,我们也可以自己编写cfg文件。编写cfg文件用到的是TCL脚本语言:OpenOCD开发文档-TCL 入门

CLI交互

openocd导入配置文件

1
openocd -f '/usr/share/openocd/scripts/interface/your_debugger.cfg' -f '/usr/share/openocd/scripts/target/your_mcu.cfg'

新开一个shell,telnet连接CLI(默认4444端口)

1
telnet localhost 4444

然后就可以和CLI交互了。一般刚连上目标处于运行状态,需要先输入halt指令进入停止状态才能进行查看寄存器或访问内存等操作。

常用命令如下(截取自该博客):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
$ telnet localhost 4444
Open On-Chip Debugger
reset # 复位
init # 初始化设备
halt # 停止运行
step # 单步执行
resume # 恢复运行
step 0x08000648 # 运行到地址0x08000648处后单步执行

reset run # 复位并立即运行
reset halt # 复位并立即停止
reset init # 复位并立即初始化

halt # 写入程序前要先停止设备
program ./target.hex # 写入 hex
program ./target.bin 0x08000000 # 写入 bin
flash write_image erase ./target.hex # 擦除后写入 hex 文件
flash write_image erase ./target.bin 0x08000000 # 擦除后写入 bin 文件

reg # 列出所有寄存器
halt # 停止设备
set_reg {pc 0 sp 0x1000} # 写PC,SP寄存器
get_reg {pc sp} # 读PC,SP寄存器
pc 0x00000000 sp 0x00001000

write_memory 0x20000000 32 {0xdeadbeef 0x00230500} # 向内存写入两个32位数据
read_memory 0x20000000 32 2 # 读取两个32位数据
0xdeadbeef 0x230500

mdd 0x08000000 1 # 读1个64位的数据
mdw 0x08000000 1 # 读1个32位的数据
mdh 0x08000000 1 # 读1个16位的数据
mdb 0x08000000 1 # 读1个 8位的数据

mwd 0x20000000 0xaaaabbbbccccdddd # 写1个64位的数据
mww 0x20000000 0xaaaabbbb # 写1个32位的数据
mwh 0x20000000 0xaaaa # 写1个16位的数据
mwb 0x20000000 0xaa # 写1个 8位的数据

bp # 查看所有断点
bp 0x08000648 2 hw # 在地址0x08000648处设置硬件断点
rbp 0x08000648 # 删除该地址的断点
rbp all # 删除所有断点

exit # 退出

更多指令参考:General Commands

GDB调试

获取交叉编译工具

需要从对应芯片的交叉编译工具链中获取专用的gdb

Arm GNU 工具链下载

STM32编译工具链下载

支持的目标架构分类

  • AArch32
    • 裸机:arm-none-eabi
    • GNU/Linux硬浮点:arm-none-linux-gnueabihf
  • AArch64
    • 裸机:aarch64-none-elf
    • GNU/Linux:aarch64-none-linux-gnu
    • GNU/Linux大端:aarch64_be-none-linux-gnu

进行调试

首先导入配置文件。

1
openocd -f '/usr/share/openocd/scripts/interface/your_debugger.cfg' -f '/usr/share/openocd/scripts/target/your_mcu.cfg'

新开一个shell,运行gdb后输入target remote localhost:3333连接到gdbserver。注意交叉编译工具链中的gdb名称可能不同。

1
2
3
$ gdb 
(gdb) file "./target.elf"
(gdb) target extended-remote localhost:3333

这里的target.elf指设备中正在运行的程序的可执行文件,通常在IDE编译后产生。

整体架构如下,Gdb Server端口是3333。

6

GDB常用指令(截取自该博客):

指令 简写 说明
help h 打印帮助信息
h next :打印 next 指令的用法说明
file 指定程序文件
load 加载 file 指定的程序文件
break b 设置断点
b main :在 main 函数处设置断点
b 10 :在第 10 行设置断点
b test.c:10 : 在 test.c 的第 10 行设置断点
b if a == 1 : 当 a 等于 1 时打断
tbreak tb 设置临时断点,只作用一次
info i 显示调试信息
i b :显示所有断点
i display :显示所有表达式
delete d 删除断点等
d :删除所有断点
d 2 :删除 2 号断点
run r 从头开始执行程序
next n 执行一条 C 语句,不进入函数
step s 执行一条 C 语句,进入函数
nexti ni 执行一条汇编语句,不进入函数
stepi si 执行一条汇编语句,进入函数
continue c 继续执行
finish fin 跳出当前函数
until u 跳出当前循环
backtrace bt 打印所有栈帧
frame f 打印当前栈帧
print p 打印变量或表达式的值
display disp 增加要显示值的表达式(disp+后缀/x=16进制,/i=汇编指令,/x=字符串等)
call 调用函数
call func(1, 2) :调用 func 函数
tui enable - 启用字符图形窗口
focus cmd :聚焦到 cmd 窗口
layout asm :显示汇编窗口,其他还有 src、regs、split
winheight src -2 :调整源码窗口高度-2
tui disable 关闭字符图形窗口
disassemble <address> 将给定的地址转换为汇编代码
info threads 显示所有线程的状态
info line * <address> 显示给定地址的 c++ 行源码信息
info all-registers 查看所有寄存器信息

STM32 固件提取

根据所用的仿真器和MCU芯片型号导入配置文件

1
openocd -f '/usr/share/openocd/scripts/interface/cmsis-dap.cfg' - f '/usr/share/openocd/scripts/target/stm32f4x.cfg'

7

然后telnet localhost 4444连接CLI,halt进入停止状态

8

最后用dump_image命令,结合数据手册中flash在内存中的映射地址范围,提取固件并命名为flash.bin

1
dump_image flash.bin 0x08000000 0x100000 

9

STM32 Gdb调试

获取Linux系统下32位arm裸机的编译工具链

10

获取编译后的ELF可执行文件,笔者用keil编译烧录了一个点灯程序

11

是否生成ELF、文件名称和位置在魔法棒中设置

12

根据所用的仿真器和MCU芯片型号导入配置文件

1
openocd -f '/usr/share/openocd/scripts/interface/cmsis-dap.cfg' - f '/usr/share/openocd/scripts/target/stm32f4x.cfg'

解压编译工具链压缩包,运行bin目录下的arm-none-eabi-gdb

1
op1n@ubuntu:~/Desktop/arm-gnu-toolchain-14.2.rel1-x86_64-arm-none-eabi/bin$ ./arm-none-eabi-gdb

导入目标程序的ELF可执行文件

1
(gdb) file '/home/op1n/Desktop/demo.axf' 

连接Gdb Server

1
(gdb) target extended-remote localhost:3333

启用字符图形窗口tui enable,显示汇编layout asm

13

显示寄存器layout regs

14

Reference

【芯片调试】OpenOCD是什么?

OpenOCD简介和下载安装(Ubuntu)

使用OpenOCD提取STM32固件

OpenOCD笔记

OpenOCD命令操作

Debugging with GDB on STM32

OpenOCD + GDB 调试

使用 OpenOCD 来调试 STM32


OpenOCD芯片调试和固件提取-以STM32为例
https://lkliki.github.io/2025/03/16/OpenOCD芯片调试和固件提取-以STM32为例/
作者
0P1N
发布于
2025年3月16日
许可协议