news 2026/4/15 7:20:47

带你搞懂BootLoader(三)-第二个BootLoader

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
带你搞懂BootLoader(三)-第二个BootLoader

这篇文章将带你写第二个BootLoader程序,对应的是以下那篇博文的第二种启动方式:APP原本设计在Flash中运行,但实际执行时会先将自身代码复制到RAM,然后在RAM中运行。

带你搞懂BootLoader(一)

引言

那么是谁将APP程序从Flash复制到内存呢?又复制到哪里呢?

  • APP程序从Flash复制到内存可以由BootLoader复制也可以由APP自我复制

关于复制到哪里

先通过分析APP程序的反汇编文件来引入几个概念

int mymain() { char c = 'A'; while (1) { putchar(c++); delay(1000000); if (c == 'Z') c = 'A'; } return 0; }

这里APP程序为了它的反汇编文件代码更精简,我将它的main函数修改成了mymain函数,去掉系统自带的其他代码

这里要引入一个概念:BL是相对跳转指令

相对跳转是什么意思呢?

  • 相对跳转是指跳转目标地址是相对于当前程序计数器(PC)的位置来计算的。具体来说,BL指令执行时,处理器会计算当前PC值与偏移量(OFFSET)的和,得到目标地址。计算公式为:当前PC = 当前PC + OFFSET

    举个例子: 假设当前PC值为0x1000,偏移量OFFSET为0x200,那么执行BL指令后,程序将跳转到0x1200地址处执行。

拿上面这个图来说,处理器执行putchar函数怎么去跳转到它的地址呢?就是根据当前PC值加上一个偏移值跳转到putchar函数的地址,那么如果使用的是函数指针呢?如下图

如果使用的是函数指针来调用putchar函数,使用的就是绝对跳转,再来看看它的反汇编文件:

首先,PC(程序计数器)将0x20000034地址处的值0x2000003d加载到寄存器r5中,然后跳转到r5的地址来执行putchar函数,加载的地址值0x2000003d的最低位是1,这是ARM架构中Thumb指令集的标志位,去掉最低位1后,实际地址是0x2000003c,这个0x2000003c就是putchar函数的真实入口地址。

这种跳转方式属于绝对跳转,与相对跳转不同,绝对跳转直接指定目标地址,在ARM架构中,函数指针调用通常采用这种绝对跳转方式。

这个mymain函数能够调用putchar函数的前提是,在内存地址0x2000003c处必须存在有效的机器码指令。这里涉及到几个关键的技术细节:

  • 当APP程序的链接地址被指定在RAM区域0x20000000时,这意味着编译器生成的机器码是按照这个基地址进行地址计算的。

  • 所有函数调用和变量访问的地址都是基于这个基地址的偏移量。

  • 如果程序没有被实际加载到0x20000000开始的RAM区域,那么通过函数指针进行的绝对地址跳转(如跳转到0x2000003c)就会失败。

  • 该地址必须包含有效的putchar函数机器码,否则CPU会尝试执行无效指令,导致系统崩溃。

  • 如果没有正确复制程序:

    • 函数指针跳转会访问到随机数据或全0区域

    • 可能触发硬件异常(如HardFault)

    • 在Cortex-M架构中会导致进入异常处理程序

  • 可以通过调试器检查0x2000003c地址内容:

    • 验证内存保护单元(MPU)设置是否允许访问该区域

    • 检查程序是否被完整复制到目标区域

    • 确认该地址是否包含预期的机器码

实验

现在来做一个实验,如果APP程序不复制到指定的链接地址来运行程序,而是烧写bin文件在Flash上就地运行,会发生什么事呢?

现在我修改一下APP程序的链接参数,将它的只读地址(ro)和读写地址(rw)都定位到RAM区域

APP程序就要将它的ro段复制到0x20000000地址,rw段复制到0x20000800地址

第二个Bootloader程序就是APP程序从Flash自我复制到RAM区域

但是一般程序都不会允许代码段(ro)和数据段(rw)中间会有这么大的空内存空间,所以就要用散列文件来指定代码段和数据段的链接地址

所以这里写一个散列文件来指定APP程序的链接地址

; ************************************************************* ; *** Scatter-Loading Description File generated by uVision *** ; ************************************************************* LR_IROM1 0x20000000 0x10000 { ; load region size_region ER_IROM1 0x20000000 0x10000 { ; load address = execution address *.o (RESET, +First) .ANY (+RO) .ANY (+XO) .ANY (+RW +ZI) } }
  • LR_IROM1:Load Region 名称(可自定义),通常表示“加载区域”。

  • 0x20000000:该 Load Region 的加载地址

  • 0x10000:区域大小(64KB)。

  • ER_IROM1:Execution Region 名称(执行区域)。

  • 0x20000000执行地址

  • 此处加载地址=执行地址,表示“加载后无需搬移,直接在加载地址执行”。

  • *.o (RESET, +First):将所有目标文件中名为RESET的段(通常是向量表)放在执行区域最前面;

  • .ANY (+RO):放置只读代码(如函数);

  • .ANY (+XO):可执行的只读数据(较少用);

  • .ANY (+RW +ZI):读写数据(初始化变量)和零初始化数据(未初始化全局变量)。

只需要理解散列文件就是将APP程序的运行地址改为我自定义指定的地址

然后在APP程序里面添加一个静态全局数组,这个数组会保存在数据段,并且使用函数指针来执行putchar函数打印数组里面的字符串

#include "uart.h" static char buf[100] = "this is a test"; void delay(int d) { while(d--); } int mymain() { char c = 'A'; int (*fp)(char c); fp = putchar; //putstr(buf); while (1) { fp(c++); //putchar(c++); delay(1000000); if (c == 'Z') c = 'A'; } return 0; }

编译程序,然后先烧写Bootloader程序,让它跳转到0x08040000地址,再烧写编译APP程序生成的.bin文件到0x08040000地址,看看会发生什么

结果意料之中,串口只打印了bootloader的字符串,并没有打印APP程序里面的字符串,再看看APP程序的反汇编文件

BLX r5

这行代码就是跳转putchar函数的绝对跳转指令,跳转过去之后发现指定的地址根本就没有指令来运行,所以这就导致了系统崩溃

那么注释掉函数指针的代码,程序是不是就可以正常运行了,修改程序

重新编译,烧写.bin文件,运行

结果还是只打印了bootloader程序的字符串,这是为什么呢?

bootloader程序会跳转到0x08040000地址来取APP程序的向量表中的Reset_Handler地址来执行Reset_Handler,但是Reset_Handler现在的地址是在RAM区域,APP程序并没有将程序复制过去,所以RAM区域没有可运行的指令,导致系统崩溃

所以APP程序的start.s文件需要将Reset_Handler改为0x08040009地址,系统会将这个地址赋给Reset_Handler函数来执行Reset_Handler函数的代码,这样程序就可以运行了

PRESERVE8 THUMB ; Vector Table Mapped to Address 0 at Reset AREA RESET, DATA, READONLY EXPORT __Vectors __Vectors DCD 0x20000000+0x10000 DCD 0x08040009 ;Reset_Handler ; Reset Handler AREA |.text|, CODE, READONLY ; Reset handler Reset_Handler PROC EXPORT Reset_Handler [WEAK] IMPORT mymain ;LDR SP, =(0x20000000+0x10000) BL mymain ENDP END

这样程序就可以正常运行了

总结

我使用散列文件修改了APP程序的链接地址,如下图

那么app.bin文件就应该在指定的链接地址来运行,程序必须被复制到该地址才能正确运行,所有函数调用和变量访问都会基于这个基地址

如果使用的是函数指针来进行函数调用,使用的就是绝对跳转来跳转到函数的地址来执行函数,函数的地址都是在链接地址的范围里面,如果程序没有被复制到该地址,就无法正常运行

如果不是用函数指针来进行函数调用,C语言编译器会优先使用的就是相对跳转来执行函数相对跳转依赖于当前指令指针的偏移量,这种使用相对跳转的程序可以放在任何地址都可以正常运行

还有如果是长距离调用,比如main函数地址是A,fun函数地址是B,如果B的地址远大于A,也是使用的绝对跳转,但这种情况几乎不太可能

链接地址就是程序运行的地址,程序不在这个地址就无法运行

第二个Bootloader程序就是将跳转到APP程序,APP程序将链接地址修改成RAM区域,再自我复制程序到RAM来运行

Bootloader程序

设置跳转到指定地址0x08040000运行APP程序

----------------------main.c----------------------- #include "uart.h" extern void start_app(unsigned int new_vector); void delay(int d) { while(d--); } int mymain() { unsigned int new_vector = 0x08040000; uart_init(); putstr("bootloader\r\n"); /* start app */ start_app(new_vector); return 0; } ------------------start.s--------------------------- PRESERVE8 THUMB ; Vector Table Mapped to Address 0 at Reset AREA RESET, DATA, READONLY EXPORT __Vectors __Vectors DCD 0 DCD Reset_Handler ; Reset Handler AREA |.text|, CODE, READONLY ; Reset handler Reset_Handler PROC EXPORT Reset_Handler [WEAK] IMPORT mymain LDR SP, =(0x20000000+0x10000) BL mymain ENDP start_app PROC EXPORT start_app ; set vector base address as 0x08040000 ldr r3, =0xE000ED08 str r0, [r3] ldr sp, [r0] ; read val from addr 0x08040000 ldr r1, [r0, #4] ; read val from addr 0x08040004 BX r1 ENDP END

这个程序在带你搞懂BootLoader(二)-第一个BootLoader里面有讲解,这里就不多说了

APP程序

现在分析一下整体架构:

  1. 芯片上电→ CPU 从0x08040000(Flash)读取向量表;

  2. 跳转到Reset_Handler(仍在 Flash 中执行)

  3. 调用copy_myself将整个 App 镜像从 Flash 复制到 RAM

  4. 跳转到 RAM 中的mymain函数执行

第一部分:App 主逻辑

#include "uart.h" static char buf[100] = "this is app"; void copy_myself(int *from, int *to, int len) { // 从哪里到哪里, 多长 ? int i; for (i = 0; i < len/4+1; i++) { to[i] = from[i]; } } void delay(int d) { while(d--); } int mymain() { char c = 'A'; int (*fp)(char c); fp = putchar; putstr(buf); while (1) { fp(c++); putchar(c++); delay(1000000); if (c == 'Z') c = 'A'; } return 0; }
static char buf[100] = "this is app";

定义一个初始化的全局字符串。
关键点:这是一个RW 数据(已初始化),会被链接器放入.data段,在 Flash 中有初始值,在 RAM 中有运行副本。
当 App 被复制到 RAM 后,这个变量也会在 RAM 中存在,且值正确。

void copy_myself(int *from, int *to, int len) { int i; for (i = 0; i < len/4+1; i++) { to[i] = from[i]; } }

功能:将len字节从from(Flash)复制到to(RAM)。

App 主循环(mymain)

  • 打印预定义字符串;

  • 循环打印递增字母(A→Z);

  • 使用函数指针fp调用putchar,验证 RAM 中代码可正常执行函数指针;

  • 注意:此函数将在RAM 中执行,而非 Flash!

第二部分:汇编代码(自搬移启动)

PRESERVE8 THUMB ; Vector Table Mapped to Address 0 at Reset AREA RESET, DATA, READONLY EXPORT __Vectors __Vectors DCD 0x20000000+0x10000 DCD 0x08040009 ;Reset_Handler ; Reset Handler AREA |.text|, CODE, READONLY ; Reset handler Reset_Handler PROC EXPORT Reset_Handler [WEAK] IMPORT mymain IMPORT copy_myself IMPORT |Image$$ER_IROM1$$Length| adr r0, Reset_Handler ; r0=0x08040000 bic r0, r0, #0xff ldr r1, =__Vectors ; r1=0x20000000 ldr r2, = |Image$$ER_IROM1$$Length| ; LENGTH BL copy_myself ;LDR SP, =(0x20000000+0x10000) ;BL mymain ldr pc, =mymain ENDP END
_Vectors DCD 0x20000000+0x10000 DCD 0x08040009 ;Reset_Handler
  • 第 0 项(MSP):0x20010000→ RAM 顶部,作为栈顶;
  • 第 1 项(Reset_Handler):硬编码为0x08040009(Flash 地址);

为什么 Reset_Handler 地址写死?
因为此时 App 还在 Flash 中,必须先执行 Flash 中的Reset_Handler来完成自搬移。
搬移完成后,才会跳到 RAM 中的mymain

EXPORT Reset_Handler [WEAK] IMPORT mymain IMPORT copy_myself IMPORT |Image$$ER_IROM1$$Length|
  • 导出Reset_Handler

  • 导入 C 函数mymaincopy_myself

  • 关键:导入链接器生成的符号|Image$$ER_IROM1$$Length|,表示当前镜像长度(单位:字节)。

|Image$$...$$Length|是什么?
这是 ARM 链接器自动生成的符号,表示某个执行区域(ER)的大小。
在 scatter 文件中若定义了ER_IROM1,链接器会生成:

  • Image$$ER_IROM1$$Base

  • Image$$ER_IROM1$$Limit

  • Image$$ER_IROM1$$Length = Limit - Base

adr r0, Reset_Handler ; r0 = 当前 PC 相对地址(即 Reset_Handler 的地址) bic r0, r0, #0xff ; 清除低 8 位,对齐到 256 字节边界

目的:获取 App 在 Flash 中的起始地址(即0x08040000)。

原理

  • adr r0, Reset_Handler:将Reset_Handler的地址加载到r0(例如0x08040008);

  • bic r0, r0, #0xff:清除低 8 位(即& ~0xFF),得到0x08040000

为什么可行?
因为向量表必须位于 256 字节对齐地址(VTOR 要求),所以 App 起始地址低 8 位必为 0。

ldr r1, =__Vectors ; r1 = __Vectors 的链接地址(应为 0x20000000)

__Vectors在链接时被分配到 RAM 起始地址(如0x20000000),所以r1 = 0x20000000
这就是 RAM 中的目标地址

ldr r2, = |Image$$ER_IROM1$$Length| ; LENGTH

将 App 镜像总长度(字节)加载到r2

BL copy_myself

调用 C 函数copy_myself(r0, r1, r2),即:

copy_myself(0x08040000, 0x20000000, image_length);

整个 App(包括向量表、代码、RW 数据)从 Flash 复制到 RAM。

注意:此时 ZI 段(未初始化变量)不会被复制(因为 Flash 中无内容),但 RAM 中原本就是 0(上电清零或 Bootloader 清过),通常可接受。

ldr pc, =mymain

直接将 PC 设置为mymain的地址,跳转到 RAM 中执行。

为什么不用BL mymain

  • BL会保存返回地址到lr,但这里不需要返回;

  • 更重要的是:mymain现在在 RAM 中,而BL mymain会跳转到Flash 中的 mymain(链接地址)

ldr pc, =mymain的妙处

  • =mymain是 mymain 的链接地址(比如0x20000100);

  • 因为我们刚把整个镜像复制到 RAM,RAM 中0x20000100处就是 mymain 的代码

  • 所以ldr pc, =mymain实际跳转到RAM 中的 mymain

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/12 3:49:26

39M参数撬动百亿市场:Whisper-Tiny.en引领嵌入式语音交互革命

39M参数撬动百亿市场&#xff1a;Whisper-Tiny.en引领嵌入式语音交互革命 【免费下载链接】whisper-tiny.en 项目地址: https://ai.gitcode.com/hf_mirrors/openai/whisper-tiny.en 导语 OpenAI推出的Whisper-Tiny.en模型以3900万参数实现8.44%的英语语音识别错误率&a…

作者头像 李华
网站建设 2026/4/11 20:08:47

终极指南:M3 Pro芯片运行CosyVoice性能优化技巧

想要在M3 Pro芯片的MacBook上流畅运行CosyVoice多语言语音生成模型吗&#xff1f;本文将为你揭示从卡顿到丝滑的完整性能优化方案&#xff0c;让Apple Silicon发挥最大潜能&#xff0c;实现语音合成效率的显著提升。 【免费下载链接】CosyVoice Multi-lingual large voice gene…

作者头像 李华
网站建设 2026/4/15 7:03:23

Citybound道路规划系统深度解析:从新手到专家的进阶指南

在城市模拟游戏的世界里&#xff0c;道路系统往往决定了整个城市的命运。Citybound作为一款开源的多玩家城市模拟游戏&#xff0c;其道路规划系统以其智能化和易用性而著称。本文将带您深入探索这一系统的奥秘&#xff0c;掌握从基础操作到高级技巧的全套技能。 【免费下载链接…

作者头像 李华
网站建设 2026/4/13 8:25:34

容器和进程,自动化的关系概论

容器化技术&#xff08;如Docker&#xff09;与进程、自动化之间有着密切的关系。容器本质上是进程的隔离环境&#xff0c;而自动化则是容器技术的重要应用场景和优势所在。 一、容器与进程的关系 容器是进程的扩展 容器实际上是一个或多个进程的集合&#xff0c;这些进程运行在…

作者头像 李华
网站建设 2026/4/1 23:44:12

放弃30万年薪后,他用AI求职中介精准匹配海外岗位?

“放弃30万年薪&#xff0c;他靠AI求职中介‘越级匹配’英国算法岗&#xff0c;回国反而‘水土不服’&#xff1f;”“不是所有30万的‘体面’&#xff0c;都抵得上一次被看见的精准对焦。”引言 最近&#xff0c;一位服务超过5年、帮助超百名留学生进入顶尖科技公司的资深算法…

作者头像 李华
网站建设 2026/4/12 13:35:02

31、创建程序文档:man 页面与 SGML 文档的实践指南

创建程序文档:man 页面与 SGML 文档的实践指南 在软件开发过程中,为程序创建清晰、准确的文档是至关重要的。它不仅有助于其他开发者理解和使用你的程序,还能在后续的维护和扩展中提供重要的参考。本文将介绍如何使用 groff 创建 man 页面,以及如何利用功能更强大、更灵活的…

作者头像 李华