断点是如何工作的?
原文:How do breakpoints even work?
停止程序执行的想法是70多年前作为ENIAC数字计算机的一部分发明的。当时,要暂停程序并进行调试,你必须拔掉控制程序流程的一根电缆,这就是所谓的“断点”。1
今天,断点是一种无处不在的调试程序的方式,大多数芯片架构甚至有专门的指令来触发它们!你有没有想过它们实际上是如何工作的?或者至少,当他们不这样做的时候会不高兴?
在本文中,我们将讨论断点的基本类型(硬件和软件)以及它们如何被GNU项目调试器GDB所利用。然后,我们将探讨如何使用Flash补丁和断点单元(条板)并通过一个示例应用程序来研究现实世界的配置。
注意:虽然本文的重点是关于断点以及它们如何为基于ARM Cortex-M MCU的固件工作,但是提出的一般思想适用于任何编译语言和调试器。
基本术语
有两类断点,称为“硬件断点”和“软件断点”。
硬件断点
硬件断点由正在使用的芯片组实现。在芯片级,它们是比较器,将提取的指令与外设寄存器中配置的指令进行比较。当指令匹配时,硬件将触发调试事件,暂停内核或生成异常。对于任何给定的芯片,通常都有少量固定数量的硬件断点可用。如果你曾经使用过一个没有断点的调试器,这就是原因!
软件断点
另一方面,软件断点是由调试器实现的。他们靠修补您试图通过以某种方式触发调试事件的指令来执行的代码。这是通过注入断点指令来实现的,或者在不支持断点指令时,通过插入导致中断内核的错误的指令来实现。
使用软件断点,调试器实质上可以暴露“无限”数量的断点。尽管如此,仍有一些挑战需要应对:
- 并非所有代码区域都是可修补的(即只读存储器(ROM))
- 如果调试器崩溃,代码可能会处于不良状态(打补丁的指令而不是实际的代码!).
项目设置示例
在下面几节中,我们将通过使用一个非常简单的裸机应用程序来探索断点是如何工作的,该应用程序使用以下工具在nRF52上运行:
- nRF52840-DK2(ARM Cortex-M4F)作为开发板
- SEGGER JLinkGDBServer3作为我们的gdb server(6.80 b版)
- GCC 9.3.1 / GNU Arm嵌入式工具链作为我们的编译器4
- GNU Make作为我们的构建系统
- 简单的CLI shell我们建立在以前的职位.
- PySerial的miniterm.py5连接到nRF52上的串行控制台。
编译和刷新项目
如果您有nRF52840-DK,并希望继续学习:
启动JLinkGDBServer
$ JLinkGDBServer -if swd -device nRF52840_xxAA -nogui
克隆、编译和刷新
$ git clone https://github.com/memfault/interrupt.git
$ cd examples/breakpoint
# Build
$ make
Compiling src/dummy_functions.c
Compiling src/fpb.c
Compiling src/log_port.c
Compiling src/main.c
Compiling src/minimal_nrf52_uart.c
Compiling src/shell.c
Compiling src/shell_commands.c
Compiling src/shell_port.c
Compiling src/startup.c
Linking library
Generated build/nrf52.elf
# Flash & Debug
$ make flash
(gdb)
打开到Shell的串行连接
# Start Miniterm (Separate Terminal)
$ miniterm.py /dev/cu.usbmodem* 115200 --raw
--- Miniterm on /dev/cu.usbmodem0006838593731 115200,8,N,1 ---
--- Quit: Ctrl+] | Menu: Ctrl+T | Help: Ctrl+T followed by Ctrl+H ---
==Booted==
shell>
GDB断点处理
让我们先来看看GDB是如何处理断点的。
对调试接口的全面讨论超出了本文的范围,但是可以在我们关于这个话题的帖子。我们将在下面回顾与这篇文章相关的部分。
在高层次上,gdb
(“客户端”)通过与嵌入式MCU连接gdbserver
(在我们的例子中是SEGGER的JLinkGDBServer)。gdb“客户端”和gdb“服务器”之间的协议被称为“GDB远程串行协议”(GDB·RSP).
如果我们看一下GDB·RSP的文件,我们会发现z包用于配置断点。
GDB·RSP断点命令
命令的格式是Z/z type,addr,kind
其中:
GDB远程串行数据包 | 操作 |
---|---|
Z0,addr,kind | 在以下位置添加软件断点addr |
z0,addr,kind | 移除位于的软件断点addr |
Z1,addr,kind | 在处添加硬件断点addr |
z1,addr,kind | 移除位于的硬件断点addr |
kind
是特定于架构的,但是对于ARM,它指示使用什么指令。(指令根据MCU使用ARM还是Thumb指令集而变化)。
注意:在GDB RSP使用“软件”断点有点用词不当。gdbserver可以选择如何最好地安装断点。例如,如果仍然有可用的硬件断点,服务器可以选择安装一个“软件”断点作为硬件断点。
调试GDB远程串行协议
说完这些,让我们来看看实际的GDB·RSP当我们安装断点时,通过网络发送的命令。方便的是,GDB有一个内置的调试命令,set debug remote 1
,可用于打印客户机和服务器之间发送的每一条命令!
为了方便断点测试,我添加了dummy_function_1
穿过dummy_function_9
我的程序,我们可以设置断点。所有的函数都是打印正在执行的函数的名称:
void dummy_function_3(void) {
EXAMPLE_LOG("stub function '%s' called", __func__);
}
我还补充道dump_dummy_funcs
到固件CLI以打印关于它们的所有信息:
shell> dump_dummy_funcs
dummy_function_1: Starts at 0x240. First Instruction = 0x48024901
dummy_function_2: Starts at 0x250. First Instruction = 0x48024901
dummy_function_3: Starts at 0x260. First Instruction = 0x48024901
dummy_function_4: Starts at 0x270. First Instruction = 0x48024901
dummy_function_5: Starts at 0x280. First Instruction = 0x48024901
dummy_function_6: Starts at 0x290. First Instruction = 0x48024901
dummy_function_7: Starts at 0x2a0. First Instruction = 0x48024901
dummy_function_8: Starts at 0x2b0. First Instruction = 0x48024901
dummy_function_9: Starts at 0x2c0. First Instruction = 0x48024901
dummy_function_ram: Starts at 0x20000000. First Instruction = 0x48024901
我们转储名称和地址,以便更容易地将函数地址与GDB·RSP我们转储前几条指令,这样我们可以很容易地检查是否安装了软件断点(如果代码被打了补丁,第一条指令将会改变)。
我们可以确认,在闪存启动后,通过CLI打印的信息与我们从ELF文件转储信息时看到的信息相匹配。例如:
$ arm-none-eabi-objdump --disassemble=dummy_function_1 build/nrf52.elf
[...]
00000240 <dummy_function_1>:
240: 4901 ldr r1, [pc, #4] ; (248 <dummy_function_1+0x8>)
242: 4802 ldr r0, [pc, #8] ; (24c <dummy_function_1+0xc>)
dummy_function_1
“从0x240开始”和“指令= 0x48024901”捕获前两个16位指令0x4901 (ldr r1, [pc, #4])
和0x4802 (ldr r0, [pc, #8])
.
GDB·RSP在设置断点时
让我们开始吧GDB·RSP调试跟踪,安装一个断点,看看会发生什么。
(gdb) set debug remote 1
(gdb) break dummy_function_1
Sending packet: $m80,4#35...Packet received: 2de9f341
Sending packet: $m240,40#93...Packet received:[...]
Sending packet: $m248,4#6b...Packet received: 2f730000
Sending packet: $m248,4#6b...Packet received: 2f730000
Sending packet: $m240,2#61...Packet received: 0149
Breakpoint 1 at 0x240: file stub_functions.c, line 4.
我们看到一些m小包 (m addr,length)
.这些用于请求读取内存,但没有断点设置命令。!让我们continue
看看会发生什么:
(gdb) continue
Continuing.
(gdb) c
Continuing.
==> Sending packet: $Z0,240,2#aa...Packet received: OK
==> Packet Z0 (software-breakpoint) is supported
Sending packet: $vCont?#49...Packet received:
Packet vCont (verbose-resume) is NOT supported
Sending packet: $Hc0#db...Packet received: OK
Sending packet: $c#63...
我们看到一个$Z0,240,2
发出了表示“在地址0x240安装软件断点”的请求。这与的地址匹配dummy_function_1
这就是我们所期待的!看起来断点只在我们恢复时安装。
当断点触发时,GDB·RSP
现在让我们通过执行安装断点的函数来触发断点。我已经向示例项目添加了一个CLI命令,call_dummy_funcs
它简单地调用所有的dummy_function_*
连续套路:
shell> call_dummy_funcs
[... GDB should have halted ...]
Sending packet: $c#63...Packet received: T05hwbreak:;thread:0000DEAD;
Sending packet: $g#67...Packet received:[...]
Sending packet: $qfThreadInfo#bb...Packet received: m0000dead
Sending packet: $qsThreadInfo#c8...Packet received: l
===>Sending packet: $z0,240,2#ca...Packet received: OK
Breakpoint 1, Sending packet: $m240,4#63...Packet received: 01490248
dummy_function_1 () at dummy_functions.c:4
4 EXAMPLE_LOG("stub function '%s' called", __func__);
我们可以从上面的跟踪中看到发生了一个断点调试事件。这是由T AA n1:r1;n2:r2;…
在哪里停止回复数据包T
表示收到信号,并且hwbreak
指示命中了硬件断点。
注意:在
thread:0000DEAD
传达生成信号的“线程”,其中0000DEAD
是线程id。由于我们正在运行一个裸机系统,我认为有人在赛格只是有一些乐趣和使用0xdead
作为本我。在GDB,你唯一能看到它被反射的地方是跑步时的“目标Id”info threads:
(gdb) info threads Id Target Id Frame * 1 Thread 57005 dummy_function_1 () at dummy_functions.c:4 (gdb) p/x 57005 $1 = 0xdead (gdb)
我们还看到发送了移除断点的请求($z0,240,2
).太奇怪了。我们希望断点在安装后继续触发。让我们从断点继续,看看会发生什么:
GDB RSP当一个断点触发后继续
(gdb) c
Continuing.
Sending packet: $Hcdead#39...Packet received: OK
===>Sending packet: $s#73... Packet received: T05hwbreak:;thread:0000DEAD;
Sending packet: $g#67...Packet received: [...]
===>Sending packet: $Z0,240,2#aa...Packet received: OK
Sending packet: $Hc0#db...Packet received: OK
===>Sending packet: $c#63...
分解一下:
- 一;一个s小包被发送以指示调试器正在请求单个步骤
- MCU生成调试事件并且gdb再次接收暂停信号(T05hwbreak包)。
- 我们的断点在dummy_function_1被重新安装($Z0,240,2).
- 我们首先请求的延续是用c小包.
想到这,就开始说得通了。如果GDB没有删除断点,我们设置的断点会在我们试图继续时重复触发。通过在断点被禁用时单步执行指令,我们可以跳过导致断点的指令,并在错过任何新的断点之前重新启用所有断点。
在系统暂停时移除和添加回断点的另一个优点是,它降低了调试器被终止并使系统处于断点保持安装状态的可能性。
有趣的事实:GDB不发送s对所有目标进行单步执行的数据包。一些目标(如arm-elf-linux-*)使用“软件单步执行”,其中GDB将自己确定下一条指令,并在该位置安装一个硬件断点。如果不支持本机单步执行,这可能很有用。您可以通过转储特定于gdb的arch信息来检查该模式是否正在使用,
maintenance print architecture
,看看是否gdbarch_software_single_step_p
不是空指针。
Cortex-M硬件断点
现在我们对GDB如何设置和删除断点有了一个很好的了解,让我们看看我们是否能弄清楚gdbserver实际上是如何为nRF52 Cortex-M目标安装断点的。
闪存补丁和断点单元
对于ARM Cortex-M MCU,硬件断点功能通过Flash补丁和断点单元(条板)8.
FPB有两个主要特点:
- 允许配置和启用硬件断点。
- 允许修补闪存代码和数据。从历史上看,这一直是调试ROM引导加载程序或为无法执行完全无线更新的设备提供修复的流行方式。如今,越来越多的嵌入式设备执行完整的固件更新,因此这种做法变得越来越不常见。闪存修补的艺术值得一贴,所以我们不会在这里深入探讨。
对于断点管理,在条板, FP_CTRL & FP_COMPn
.
Flash补丁控制寄存器FP_CTRL的0xE0002000
这FP_CTRL
您可以在这里找到有关支持多少硬件断点的信息,并启用条板。值得注意的是,
- REV:目前只有两个版本,版本1(REV=0)&版本2(REV=1).根据版本的不同,的布局FP_COMPn注册会略有不同。这两个版本的显著区别在于,对于版本1,断点可以仅仅如果地址在ARM代码区域范围内(0x00000000-0x1FFFFFFF)但是在版本2中,断点可以安装在整个4GB地址空间中。
- 可用硬件断点的数量可通过下式计算:FP_CTRL[6:4] << 4 | FP_CTRL[3:0]
- ENABLE打开条板单位,当设置为1时禁用它,当设置为0时。
注意:写入FP_CTRL注册只有在以下情况下才会生效KEY位为1。否则,操作将会失败。
Flash Patch比较器寄存器FP_COMPn,0xE0002008- 0xE0002008+4n
断点的比较器将总是FP_COMP0
最多支持的断点数减1。从技术上讲,最多可以实现127个硬件断点,但通常只有2-6个。
版本1布局
ENABLE
开启单个比较器REPLACE
控制行为如下:
替换 | 行为 |
---|---|
0b00 | 不适用-用于启用闪存修补 |
0b01 | 在指令上安装断点0b000:FP_COMP[28:2]:00. |
0b10 | 在指令上安装断点0b000:FP_COMP[28:2]:10. |
0b11 | 在指令上安装断点0b000:FP_COMP[28:2]:00 & 0b000:FP_COMP[28:2]:10 |
版本2布局
版本2的布局更简单一些。位0,断点使能(存在),控制是否启用断点:
位31:1是中断的地址:
注意,这能够覆盖整个32位地址空间,即使底部位用于存在因为位0用于传达正在使用ARM还是Thumb指令集,而不是编码任何地址信息和指令必须按宽度对齐。
检查FPB配置
回想一下,GDB会卸载断点时,系统暂停,所以这是一个罕见的情况下,我们可以仅仅在运行的系统上调试配置!
nRF52包括版本1条板因此,从现在开始,我们的代码示例将利用该实现。下面是可用于转储配置的最小代码片段:
转储FPB配置的c代码
#include <stdint.h>
#include <stddef.h>
typedef struct {
volatile uint32_t FP_CTRL;
volatile uint32_t FP_REMAP;
// Number Implemented determined by FP_CTRL
volatile uint32_t FP_COMP[];
} sFpbUnit;
static sFpbUnit *const FPB = (sFpbUnit *)0xE0002000;
void fpb_dump_breakpoint_config(void) {
const uint32_t fp_ctrl = FPB->FP_CTRL;
const uint32_t fpb_enabled = fp_ctrl & 0x1;
const uint32_t revision = (fp_ctrl >> 28) & 0xF;
const uint32_t num_code_comparators =
(((fp_ctrl >> 12) & 0x7) << 4) | ((fp_ctrl >> 4) & 0xF);
EXAMPLE_LOG("FPB Revision: %d, Enabled: %d, Hardware Breakpoints: %d",
revision, (int)fpb_enabled, (int)num_code_comparators);
for (size_t i = 0; i < num_code_comparators; i++) {
const uint32_t fp_comp = FPB->FP_COMP[i];
const bool enabled = fp_comp & 0x1;
const uint32_t replace = fp_comp >> 30;
uint32_t instruction_address = fp_comp & 0x1FFFFFFC;
if (replace == 0x2) {
instruction_address |= 0x2;
}
EXAMPLE_LOG(" FP_COMP[%d] Enabled %d, Replace: %d, Address 0x%x",
(int)i, (int)enabled, (int)replace, instruction_address);
}
}
我已经上钩了fpb_dump_breakpoint_config()
一直到fpb_dump
示例应用程序中的shell命令。让我们看看从这个断点中我们得到了什么dummy_function_1
仍然设置:
shell> fpb_dump
FPB Revision: 0, Enabled: 1, Hardware Breakpoints: 6
FP_COMP[0] Enabled 1, Replace: 1, Address 0x240
FP_COMP[1] Enabled 0, Replace: 0, Address 0x0
FP_COMP[2] Enabled 0, Replace: 0, Address 0x0
FP_COMP[3] Enabled 0, Replace: 0, Address 0x0
FP_COMP[4] Enabled 0, Replace: 0, Address 0x0
FP_COMP[5] Enabled 0, Replace: 0, Address 0x0
我们可以看到JLinkGDBServer
在第一个可用的比较器中安装了一个断点!
RAM中运行的函数的断点
回想一下,FPB的第1版仅适用于代码空间中的地址。让我们为运行在SRAM中的函数安装一个断点(地址0x20000000-0x3FFFFFFF
)看看会发生什么:
(gdb) break dummy_function_ram
Sending packet: $m20000000,40#7f...Packet received: [...]
Sending packet: $m20000008,4#57...Packet received: c8730000
Sending packet: $m20000008,4#57...Packet received: c8730000
Sending packet: $m20000000,2#4d...Packet received: 0149
Breakpoint 2 at 0x20000000: file dummy_functions.c, line 41.
(gdb) c
Continuing.
==> Sending packet: $Z0,240,2#aa...Packet received: OK
==> Sending packet: $Z0,20000000,2#96...Packet received: OK
Sending packet: $c#63...
酷,我们可以看到GDB已请求断点安装在地址0x240 & 0x20000000
.
shell> fpb_dump
FPB Revision: 0, Enabled: 1, Hardware Breakpoints: 6
FP_COMP[0] Enabled 1, Replace: 1, Address 0x240
FP_COMP[1] Enabled 0, Replace: 0, Address 0x0
FP_COMP[2] Enabled 0, Replace: 0, Address 0x0
FP_COMP[3] Enabled 0, Replace: 0, Address 0x0
FP_COMP[4] Enabled 0, Replace: 0, Address 0x0
FP_COMP[5] Enabled 0, Replace: 0, Address 0x0
我们可以看到断点dummy_function_1
仍在FPB中配置,但用于RAM功能的配置在哪里?!
唯一可行的方法是调试器能够安装一个“软件断点”。让我们来看看说明:
shell> dump_dummy_funcs
dummy_function_1: Starts at 0x240. First Instruction = 0x48024901
dummy_function_2: Starts at 0x250. First Instruction = 0x48024901
dummy_function_3: Starts at 0x260. First Instruction = 0x48024901
dummy_function_4: Starts at 0x270. First Instruction = 0x48024901
dummy_function_5: Starts at 0x280. First Instruction = 0x48024901
dummy_function_6: Starts at 0x290. First Instruction = 0x48024901
dummy_function_7: Starts at 0x2a0. First Instruction = 0x48024901
dummy_function_8: Starts at 0x2b0. First Instruction = 0x48024901
dummy_function_9: Starts at 0x2c0. First Instruction = 0x48024901
dummy_function_ram: Starts at 0x20000000. First Instruction = 0x4802be00
你可以看到第一条指令dummy_function_ram
已被调试器从修改0x4901
到0xbe00
.
敏锐的观察者可能马上意识到这是thumb模式中断点的编码:
BKPT指令编码
使用所有硬件断点
让我们暂停并安装另外5个断点,耗尽我们6个可用的硬件断点,看看会发生什么!
(gdb) break dummy_function_2
Breakpoint 2 at 0x250: file dummy_functions.c, line 8.
(gdb) break dummy_function_3
Breakpoint 3 at 0x260: file dummy_functions.c, line 12.
(gdb) break dummy_function_4
Breakpoint 4 at 0x270: file dummy_functions.c, line 16.
(gdb) break dummy_function_5
Breakpoint 5 at 0x280: file dummy_functions.c, line 20.
(gdb) break dummy_function_6
Breakpoint 6 at 0x290: file dummy_functions.c, line 24.
(gdb) c
Continuing.
Sending packet: $Z0,240,2#aa...Packet received: OK
Sending packet: $Z0,250,2#ab...Packet received: OK
Sending packet: $Z0,260,2#ac...Packet received: OK
Sending packet: $Z0,270,2#ad...Packet received: OK
Sending packet: $Z0,280,2#ae...Packet received: OK
Sending packet: $Z0,290,2#af...Packet received: OK
Sending packet: $Z0,20000000,2#96...Packet received: OK
Sending packet: $c#63...
正如预期的那样,我们看到在我们指定的所有位置安装断点的请求。我们可以扔掉fpb从CLI配置并确认:
shell> fpb_dump
FPB Revision: 0, Enabled: 1, Hardware Breakpoints: 6
FP_COMP[0] Enabled 1, Replace: 1, Address 0x240
FP_COMP[1] Enabled 1, Replace: 1, Address 0x250
FP_COMP[2] Enabled 1, Replace: 1, Address 0x260
FP_COMP[3] Enabled 1, Replace: 1, Address 0x270
FP_COMP[4] Enabled 1, Replace: 1, Address 0x280
FP_COMP[5] Enabled 1, Replace: 1, Address 0x290
在闪存中安装软件断点
既然我们已经使用了所有可用的硬件断点,如果我们试图为另一个基于flash的函数添加一个断点,会发生什么情况呢?
break dummy_function_7
Breakpoint 8 at 0x2a0: file dummy_functions.c, line 28.
(gdb) c
Continuing.
Sending packet: $Z0,240,2#aa...Packet received: OK
Sending packet: $Z0,250,2#ab...Packet received: OK
Sending packet: $Z0,260,2#ac...Packet received: OK
Sending packet: $Z0,270,2#ad...Packet received: OK
Sending packet: $Z0,280,2#ae...Packet received: OK
Sending packet: $Z0,290,2#af...Packet received: OK
Sending packet: $Z0,2a0,2#d7...Packet received: OK
Sending packet: $Z0,20000000,2#96...Packet received: OK
Sending packet: $c#63...
shell> fpb_dump
FPB Revision: 0, Enabled: 1, Hardware Breakpoints: 6
FP_COMP[0] Enabled 1, Replace: 1, Address 0x250
FP_COMP[1] Enabled 1, Replace: 1, Address 0x260
FP_COMP[2] Enabled 1, Replace: 1, Address 0x270
FP_COMP[3] Enabled 1, Replace: 1, Address 0x280
FP_COMP[4] Enabled 1, Replace: 1, Address 0x290
FP_COMP[5] Enabled 1, Replace: 1, Address 0x2a0
有趣的是,它看起来像是dummy_function_1
(地址0x240)已从中移除FP_COMP_0
和dummy_function_2
穿过dummy_function_7
已被配置为硬件断点。
让我们调用所有的函数,使用call_dummy_funcs
壳牌命令并确认我们在dummy_function_1..dummy_function_7
也dummy_function_ram
:
(gdb) c
Continuing.
Breakpoint 1, dummy_function_1 () at dummy_functions.c:4
4 EXAMPLE_LOG("stub function '%s' called", __func__);
(gdb) c
Continuing.
Breakpoint 3, dummy_function_2 () at dummy_functions.c:8
8 EXAMPLE_LOG("stub function '%s' called", __func__);
(gdb) c
Continuing.
[...]
Breakpoint 8, dummy_function_7 () at dummy_functions.c:28
28 EXAMPLE_LOG("stub function '%s' called", __func__);
(gdb) c
Continuing.
Breakpoint 2, dummy_function_ram () at dummy_functions.c:41
41 EXAMPLE_LOG("stub function '%s' called", __func__);
(gdb) c
很好,它们都工作正常,所以为基于闪存的功能安装了一个断点(dummy_function_1
)这是FPB配置中所缺少的。
让我们转储函数信息来检查是否有任何指令被修改:
shell> dump_dummy_funcs
dummy_function_1: Starts at 0x240. First Instruction = 0x4802be00
dummy_function_2: Starts at 0x250. First Instruction = 0x48024901
dummy_function_3: Starts at 0x260. First Instruction = 0x48024901
dummy_function_4: Starts at 0x270. First Instruction = 0x48024901
dummy_function_5: Starts at 0x280. First Instruction = 0x48024901
dummy_function_6: Starts at 0x290. First Instruction = 0x48024901
dummy_function_7: Starts at 0x2a0. First Instruction = 0x48024901
dummy_function_8: Starts at 0x2b0. First Instruction = 0x48024901
dummy_function_9: Starts at 0x2c0. First Instruction = 0x48024901
dummy_function_ram: Starts at 0x20000000. First Instruction = 0x4802be00
哇,我们能看见了dummy_function_1
也被更新以包括断点指令,0xbe00
。有趣的是,这个地址在nRF52的内部闪存空间中。这意味着要插入这个断点,地址所在的闪存页面必须已经被重新编程。
事实上,这正是所发生的事情。SEGGER JLinkGDBServer实现了一个在文档中被称为“无限闪存断点”的特性9这将修补J-Link软件知道如何写入的闪存区域bkpt
说明。为了支持这一特性,JLink软件为所有这些芯片提供了闪存驱动程序,因为它们在不同的芯片供应商之间不是标准的。
闪存软件断点优化
为了减少重写闪存扇区的次数,JLinkGDBServer还会执行一些漂亮的优化,比如缓存闪存扇区和“模拟”指令执行,而不是像gdb客户要求。
让我们看一个简单的例子dummy_function_1
在哪里0x4901
指令在0x240
已被带有断点指令的gdbserver替换。
$ arm-none-eabi-objdump --disassemble=dummy_function_1 \
example/breakpoint/build/nrf52.elf
[...]
00000240 <dummy_function_1>:
240: 4901 ldr r1, [pc, #4] ; (248 <dummy_function_1+0x8>)
242: 4802 ldr r0, [pc, #8] ; (24c <dummy_function_1+0xc>)
244: f000 b966 b.w 514 <example_log>
248: 0000732f .word 0x0000732f
24c: 00007315 .word 0x00007315
正如我们在本文中发现的,当我们在dummy_function_1
,gdb客户端将要求我们卸载所有断点。然后当我们continue
,的gdb
客户将要求对指令进行“单步”操作。在我们的例子中,这意味着在0x240执行指令,并在取指令时再次暂停0x242
。在这个时候gdb
客户端将要求服务器重新安装所有断点(包括最初位于0x240
)和简历。
相反,JLinkGDBServer忽略删除/重新安装断点的请求,当它接收到“单步”请求时,“模拟”指令,并用调试器通过调试访问端口。对于我们示例中的指令,这将涉及更新$r1
与.word
在$pc+4
(0x248
)然后推进$pc
两个字节。
使用这种技术,GDBServer很少需要对闪存扇区重新编程,从而减少了闪存写入周期,并加快了“设置”闪存断点的平均时间。
注意:没有多少调试器具有这种复杂的功能。对于OpenOCD或pyOCD这样的gdbserver,当可用的硬件断点用完时,在flash区域设置断点的请求就会失败。
结果
这种方法的一个不利的副作用是,如果您将电源拉至您的板,并且没有优雅地终止事情,断点指令可能会卡在您的设备上,直到您刷新板。当没有启用调试器时,执行断点指令将升级为严重故障,所以这可能会使您的固件处于崩溃循环的状态!
我们可以自己尝试一下:
- 拔掉nRF52上的USB电源
- 与其重新连接GDB,不如启动一个shell:
$ miniterm.py /dev/cu.usbmodem* 115200 --raw
shell>
- 转储函数和确认断点指令,
0xbe00
仍在安装。
shell> dump_dummy_funcs
dummy_function_1: Starts at 0x240. First Instruction = 0x4802be00
[...]
- 执行虚拟函数并确认这会导致重新启动!
shell> call_dummy_funcs
==Booted==
最后的想法
我希望这篇文章教会了你一些关于断点的新知识,并且希望下次你遇到调试器和设置断点的问题时,你现在已经有了一些要研究什么的想法!
我很想知道你是否在使用ARM Cortex-M条板以有趣的方式为您的产品服务,或者如果有其他您希望看到的关于断点的主题。无论哪种方式,请在下面的讨论区告诉我们!
看到你想改变的东西了吗?提交拉取请求或在以下位置打开问题开源代码库
GNU ARM Embedded toolchain for download ↩
Official GDB Remote Serial Protocol Docs & Informative Unofficial Doc ↩
See “C1.11 Flash Patch and Breakpoint unit” in ARMv7-M Specification ↩