news 2026/4/12 19:24:38

带你搞懂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/11 10:52:36

app逆向(1)

由于初学&#xff0c;找的无反frida&#xff0c;无加壳的app 一、绕过强制更新 直接使用JADX反编译源码&#xff0c;搜索关键字&#xff1a;更新APP程序。 发现只要让mUpgradeBean.getUpdateType不等于1就可以绕过强制更新 右键跳转到声明 那么就可以编写hook脚本了&a…

作者头像 李华
网站建设 2026/4/12 17:56:55

PowerShell 7.5启动崩溃问题:从诊断到根治的完整解决方案

PowerShell 7.5启动崩溃问题&#xff1a;从诊断到根治的完整解决方案 【免费下载链接】PowerShell PowerShell/PowerShell: PowerShell 是由微软开发的命令行外壳程序和脚本环境&#xff0c;支持任务自动化和配置管理。它包含了丰富的.NET框架功能&#xff0c;适用于Windows和多…

作者头像 李华
网站建设 2026/3/29 6:05:37

29、深入探索GDB调试工具

深入探索GDB调试工具 1. 为GDB编译程序 调试程序时,为了创建增强的符号表,需要使用 -g 选项编译源代码。例如,使用以下命令编译程序: $ gcc -g file1.c file2.c -o prog此命令会使 prog 程序的符号表中包含调试符号。如果需要生成更多(特定于GDB)的调试信息,可以…

作者头像 李华
网站建设 2026/4/11 23:50:02

GLM-4-9B完全指南:如何快速上手智谱AI最强开源大模型

GLM-4-9B完全指南&#xff1a;如何快速上手智谱AI最强开源大模型 【免费下载链接】glm-4-9b 项目地址: https://ai.gitcode.com/zai-org/glm-4-9b 想要在本地部署一个功能强大的中文大语言模型&#xff0c;却担心硬件要求和部署复杂度&#xff1f;智谱AI推出的GLM-4-9B…

作者头像 李华
网站建设 2026/4/6 2:32:56

Hermes引擎完整指南:终极JavaScript优化工具链解析

Hermes引擎完整指南&#xff1a;终极JavaScript优化工具链解析 【免费下载链接】hermes A JavaScript engine optimized for running React Native. 项目地址: https://gitcode.com/gh_mirrors/hermes/hermes Hermes引擎是Facebook专门为React Native优化的JavaScript引…

作者头像 李华