1 重定位的概念与引入
1.1 重定位的概念
关于段的概念参考:[RT-Thread学习笔记] 程序内存分布
- 代码段(
RO-CODE
):存放程序指令二进制代码 - 可读可写的数据段(
RW-DATA
):初值非0的全局变量和静态变量,需要从ROM上复制到RAM内存 - 只读的数据段(
RO-DATA
):即常量(局部常量在栈区),可以放在ROM上,不需要复制到RAM内存 BSS
段或ZI
段:未初始化或初始化为0的全局变量和静态变量,没必要放在ROM上,只需记录起始地址和大小,再使用前清零即可- 栈(
stack
):存放局部变量(局部静态变量除外),运行时分配 - 堆(
heap
):使用malloc动态分配的空间,malloc函数可以自己写,运行时分配
重定位即将数据从一块内存复制到另一块内存中:
- 数据段重定位:保存在ROM上的全局变量的值,使用前要复制到RAM内存。
- 代码段重定位:将二进制代码复制到其他位置。
此外,因为RAM读写相对较快,如果空间特别充裕,代码段、只读数据段也都可以放在RAM中。
1.2 为什么需要重定位
MDK环境下,STM32启动文件中的__main
帮我们做了重定位等一系列操作,如果不使用编译器提供的__main
函数(主函数名称为main
也会默认调用__main
),则RW-DATA
中变量的值仅保存在ROM中,没有将值复制到RAM中,但程序访问变量时,是去从该变量所在RAM内存地址处加载值的,但此地址却并未初始化,所以读取的数值为脏数据。
start.s
PRESERVE8
THUMB
AREA RESET, DATA, READONLY
EXPORT __Vectors
__Vectors DCD 0 ; Top of Stack
DCD Reset_Handler ; Reset Handler
AREA |.text|, CODE, READONLY
; Reset handler
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
IMPORT my_main ; 修改为my_main
LDR SP, =(0x20000000 + 0xC000)
BL my_main
ENDP
END
my_main
串口打印
const char g_a = 'a'; // 常量 RO-DATA
char g_b = 'b'; // 初始化非0的全局变量 RW-DATA
int my_main()
{
USART_TypeDef *usart1 = (USART_TypeDef*)0x40013800;
uart_init(usart1, 115200);
putchar(usart1, g_a);
putchar(usart1, '\n');
putchar(usart1, g_b);
putchar(usart1, '\n');
put_s_hex(usart1, "g_a:", (uint32_t)&g_a);
putchar(usart1, '\n');
put_s_hex(usart1, "g_b:", (uint32_t)&g_b)
}
打印log:
STM32:
0x08000000
为IROM起始地址,0x20000000
为IRAM起始地址
-
常量
g_a
可以正常打印,因为其值就存放在IROM地址0x080001C4
处,cpu无需访问内存,直接立即寻址加载立即数0x61
(a的ascall码)的值(反汇编指令MOVS r1,#0x61
) -
变量
g_b
输出无法显示(非ascall码字符),因为在IROM只存放全局变量的名字和地址,cpu是从IRAM内存地址0x20000000
处读取变量值的,但是该地址还未初始化,所以读取的是随机值。(反汇编指令LDR r0,[pc,#68] ; [0x8000090] = 0x20000000
)
对于变量在编译的时候会有一个符号表(symbol table),符号表存放的是所有变量的名字和地址,反汇编代码中可以看到:
因此,IROM中都保存了g_a和g_b的名字和地址,但对于常量g_a,在IROM中还保存了它的值。
在C函数中直接向0x20000000
地址直接写入值后,可以正常print:
*((unsigned int*)(0x20000000)) = 'b';
2 重定位分析与实现
2.1 需要重定位的段
先引入两个概念:
- 加载地址(存储地址):代码存储的物理地址,如STM32F103,程序烧写到Flash上的地址即为载地址。
- 链接地址(运行地址):程序运行时应该位于的地址,由链接器根据链接文件指定,它会将程序中所有指令存放到链接地址中(链接地址与程序实际运行时地址不一定相同,若不同则需重定位)。
如果链接地址与加载地址不同,则运行时PC
得到的值与实际指令/数据存放的地址就不相同,即该处没有有效的可执行指令或读出错误的数据。因此,当加载地址!=链接地址时,就需要重定位。但不包括BSS段,因为程序不保存该段,使用前把BSS段对应空间的数据清零即可。
2.2 重定位的实现
2.2.1 位置无关码
通过程序本身将加载地址与链接地址不同的数据复制到链接地址处,即可实现重定位:
但需要解决一个问题:程序中是根据指令地址来执行程序的,当运行地址与实际指令存放地址不同时,程序为何也能正确执行?
利用位置无关指令实现,即利用程序实际运行时的地址(PC值),然后相对其进行偏移执行后续指令,即使用相对寻址。因为其偏移量在汇编时已确定,因此无需关心代码被链接后指令所处地址。
位置无关指令码的实现(不使用链接地址):
- 跳转:使用相对跳转指令(
b
,bl
指令),不能使用绝对跳转指令(ldr
伪指令,其相对pc的偏移地址是链接时确定的) - 不要访问全局变量/静态变量(访问时使用的是链接地址)
重定位完成后,再使用绝对跳转指令跳动链接地址处运行。
相对跳转(位置无关)与绝对跳转(位置相关)进一步分析:
0x08000640: BL Delay_Init ; 0x800028c
0x08000644: LDR r0,[pc,#56] ; [0x8000680] = 0x20000014
- 相对跳转
BL
:当前地址0x08000640
,相对其偏移地址为0x08000640 - 0x800028c = 0x3B4
,执行该指令会相对于当前地址向前跳转0x3B4
长度 - 绝对跳转
LDR
:pc即为当前程序运行时的地址,执行该指令会直接跳转到pc + 56 = 0x20000014
绝对地址处。
2.2.2 散列文件(armlink依赖)
复制就涉及到数据传输,数据传输的三要素:
- 源(加载地址):代码段/数据段存储地址
- 目的(链接地址):代码段/数据段要被复制到的地址
- 长度:代码段/数据段的长度
对于BSS
段的清除需知道它的地址范围:起始地址和长度。
上述信息通过下列文件指定:
- 在keil中使用散列文件(
Scatter File
)来描述 - 在GCC中使用链接脚本(
Link Script
)来描述
2.2.2.1 基本语法
一个散列文件由一个或多个Load region
组成,Load region
中含有一个或多个Execution region
,而Execution region
中含有一个或多个输入段Input section
,
-
*.o :所有objects文件
-
*:所有objects文件和库*
-
.ANY:等同于*,优先级比*低;在一个散列文件的多个可执行域中可以有多个
.ANY
LR_IROM1 0x08000000 0x00080000 {
ER_IROM1 0x08000000 0x00080000 {
*.o (RESET, +First) ; 从所有.o文件中寻找名RESET段(启动文件中用AREA伪指令定义RESET段), 利用+First放在起始位置
*(InRoot$$Sections) ; *表示从所有.o文件和库文件中抽取InRoot$$Sections段,该段为__main()中的一部分代码,主要功能将RW复制到RAM,然后再RW中创建ZI段
.ANY (+RO) ;.ANY与*功能相似,从所有.o文件和库文件中抽取RO-Data段
.ANY (+XO) ; 从所有.o文件和库文件中抽取XO段, 即只可执行的段
}
RW_IRAM1 0x20000000 0x00010000 {
.ANY (+RW +ZI) ; 从所有.o文件和库文件中抽取RW-data和ZI(bss)段(实际内容不会存入)
}
}
- 加载域
LR_IROM1
:加载起始地址0x08000000
, 长度0x00080000
,其中保存了两个可执行域ER_IROM1
&RW_IRAM1
- 可执行域
ER_IROM1
:目的链接地址0x08000000
,源加载地址0x08000000
,两者一样,因此无需重定位 - 可执行域
RW_IRAM1
:目的链接地址紧随可执行域ER_IROM1
加载地址后面,但其源加载地址0x20000000
,可加载地址 != 链接地址, 需要重定位。
综上,需要将可执行域RW_IRAM1
中的RW-data
段进行重定位,并清除bss段。
此外,也可以通过MDK设置选择来分配,如配置uart.c文件:
分别为:+RO、+ZI、+RW
2.2.2.2 region symbol含义
散列文件一般使用Two execution regions:
Symbol | Description |
---|---|
Image$$region_name$$Base | Execution address of the region. |
Image$$region_name$$Length | Execution region length in bytes excluding ZI length. |
Image$$region_name$$Limit | Address of the byte beyond the end of the non-ZI part of the execution region. |
Image$$region_name$$RO$$Base | Execution address of the RO output section in this region. |
Image$$region_name$$RO$$Length | Length of the RO output section in bytes. |
Image$$region_name$$RO$$Limit | Address of the byte beyond the end of the RO output section in the execution region. |
Image$$region_name$$RW$$Base | Execution address of the RW output section in this region. |
Image$$region_name$$RW$$Length | Length of the RW output section in bytes. |
Image$$region_name$$RW$$Limit | Address of the byte beyond the end of the RW output section in the execution region. |
Image$$region_name$$XO$$Base | Execution address of the XO output section in this region. |
Image$$region_name$$XO$$Length | Length of the XO output section in bytes. |
Image$$region_name$$XO$$Limit | Address of the byte beyond the end of the XO output section in the execution region. |
Image$$region_name$$ZI$$Base | Execution address of the ZI output section in this region. |
Image$$region_name$$ZI$$Length | Length of the ZI output section in bytes. |
Image$$region_name$$ZI$$Limit | Address of the byte beyond the end of the ZI output section in the execution region. |
Image$$
表示执行域符号,分别为执行域本身和其输出的RO、RW、XO、ZI段的起始地址、长度与结束地址。- 将
Image$$
换成Load$$
即为执行域的加载符号,与其一一对应。 Load$$region_name$$Base
:域region_name加载时的起始地址;Image$$region_name$$Base
:域region_name运行时的起始地址;Image$$region_name$$Length
:域region_name运行时的长度(为4字节的倍数);Image$$region_name$$Limit
:域region_name运行时存储域末尾的下一个字节地址(此地址不属于region_name所占的存储区域)
注意:Load$$region_name
符号只适用于执行区域。Load$$LR$$load_region_name
符号只适用于加载区域。
2.2.2.3 在程序中使用链接器符号
symbol 本身即为地址
- 汇编中使用
IMPORT |Image$$ZI$$Limit|
zi_limit DCD |Image$$ZI$$Limit|
LDR r1, zi_limit
- C函数中使用
声明为外部变量(包括指针,使用时需要使用取址符&
):
extern unsigned int Image$$RW_IRAM1$$Base; //extern unsigned int* Image$$RW_IRAM1$$Base;也一样需要使用取指符
extern unsigned int Load$$RW_IRAM1$$Base;
extern unsigned int Image$$RW_IRAM1$$Length;
memcpy(&Image$$RW_IRAM1$$Base, &Image$$RW_IRAM1$$Length, &Load$$RW_IRAM1$$Base);
声明为外部数组(使用时不需要使用取址符&
):
extern char Image$$RW_IRAM1$$Base[];
extern char Load$$RW_IRAM1$$Base[];
extern unsigned int Image$$RW_IRAM1$$Length;
memcpy(Image$$RW_IRAM1$$Base, Image$$RW_IRAM1$$Length, &Load$$RW_IRAM1$$Base);
综上,Image$$region_name$$xxx
存放的是值的地址,而&Image$$region_name$$xxx
相当于解引用,取出该地址中的值!(&symbol == *var)
2.2.2.4 C语言中指定变量输入节区
参考:野火-MDK编译过程及文件类型全解
RW_ERAM1
为外部SRAM,需要在RW段重定位前,初始化FSMC SRAM
ldr r0, =systemInit
blx r0
ldr r0, =fsmc_sram_init ; 初始化fsmc sram, 确保RW段被正常复制
blx r0
LR_IROM1 0x08000000 0x00080000 {
ER_IROM1 0x08000000 0x00080000 {
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+RO)
}
RW_IRAM1 0x20000000 0x00010000 { // on chip ram
*.o(STACK) // 外部SRAM初始化函数需要用到栈,必须放在IRAM中,不然会默认放在内存比较大的RW_ERAM1中,但此时外部sram还未初始化, 不能正常使用! (当然也可以直接操作寄存器, 无需使用stack)
.ANY (+RW +ZI)
}
RW_ERAM1 0x68000000 0x00100000 { // 外部SRAM
*.o(EXRAM)
}
}
使用__attribute__
来指定:
// 内存池32字节对齐, 指定存放起始地址为0x20001000
__align(32) uint8_t membase[MEM_MAX_SIZE] __attribute__(at(0x20001000));
uint8_t EXgroup[1024] __attribute__(section("EXRAM")) = {1, 2, 3};
__attribute__((section("EXRAMt"))) uint16_t test_func(void); // 函数
3 重定位程序编写
3.1 数据段重定位
数据段的加载地址(源): Load$$RW_IRAM1$$Base
数据段的链接地址(目的): Image$$RW_IRAM1$$Base
数据段的长度(长度): Image$$RW_IRAM1$$Length // 以字节为单位的执行区域长度,不包括ZI长度
3.1.1 纯汇编实现
relocate PROC
IMPORT |Load$$RW_IRAM1$$Base| ; 数据段的加载起始地址(源)
IMPORT |Image$$RW_IRAM1$$Base| ; 数据段的链接起始地址(目的)
IMPORT |Image$$RW_IRAM1$$Length| ; 数据段的长度(长度)
ldr r0, =|Load$$RW_IRAM1$$Base|
ldr r1, =|Image$$RW_IRAM1$$Base|
ldr r2, =|Image$$RW_IRAM1$$Length|
memcpy
ldrb r3, [r0], #1 ; r3 = *(char*)&r0, r0 += 1
strb r3, [r1], #1 ; *(char*)&r1 = r3, r1 += 1
cmp r2,#0 ; 在r2-1前进行比较, 判断其是否为0(不然会少执行一次)
sub r2, r2, #1 ; r2 -= 1
bne memcpy ; z != 0 跳转 (取决于r2 是否为 0)
bx lr ; 返回
ENDP
注意:cmp r2,#0
+sub r2, r2, #1
是先判断再将r2值-1(i--
),而subs r2, r2, #1
是先-1再判断(--i
)。
在Reset_Handler
中进行调用(必须在C库初始化前):
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
IMPORT my_main
BL relocate
LDR SP, =(0x20000000 + 0xC000)
BL my_main
ENDP
这样就能正常使用存放在RW_IRAM1
执行域中RW-Data中
的变量了。
3.1.2 C函数实现
- C函数实现memcpy
// src = r0, dest = r1, len = r2
void c_memcpy(void *src, void *dest, unsigned int len){
while (len--){
*(char*)dest++ = *(char*)src++;
}
}
- 汇编传参&调用
根据ATPCS规则,使用r0~r3给函数传参
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
IMPORT my_main
IMPORT c_memcpy
ldr r0, =|Load$$RW_IRAM1$$Base|
ldr r1, =|Image$$RW_IRAM1$$Base|
ldr r2, =|Image$$RW_IRAM1$$Length|
LDR SP, =(0x20000000 + 0xC000)
BL c_memcpy
BL my_main
ENDP
注意:调用c_memcpy
函数前需先指定SP
,因为函数调用会用栈保存/恢复现场,c_memcpy
反汇编代码如下:
当然也可以在main函数中调用(但不建议这么做,因为在重定位完成前可能会进入ISR/ESR中,使用未重定位的数据):
extern char Load$$RW_IRAM1$$Base[];
extern char Image$$RW_IRAM1$$Base[];
extern unsigned int Image$$RW_IRAM1$$Length;
int my_main(){
// ....
c_memcpy(Load$$RW_IRAM1$$Base, Image$$RW_IRAM1$$Base, &Image$$RW_IRAM1$$Length);
// ....
}
3.2 清除BSS段
keil对bss段进行了优化,如果变量所占据的空间≤8字节,则会把它放在data段,重定位后该值会被初始化为0,只有当它>8字节,才会被放到bss段,此时如果不清除,则会得到随机值:
// char g_c[8]; // data段
char g_c[9]; // bss段
int my_main()
{
static int s_c[3] = {0}; // bss段
USART_TypeDef *usart1 = (USART_TypeDef*)0x40013800;
uart_init(usart1, 115200);
put_s_hex(usart1, "\ng_c[0] = ", g_c[0]);
put_s_hex(usart1, "\ns_c[0] = ", s_c[0]);
}
因此,需要手动清除bss段。(__main
也会帮我们做),其链接器符号为:
//清除bss时只需用到起始地址和长度
bss/ZI段的链接(起始)地址: Image$$RW_IRAM1$$ZI$$Base
bss/ZI段的结束地址: Image$$RW_IRAM1$$ZI$$Limit
bss/ZI段的长度: Image$$RW_IRAM1$$ZI$$Length
3.2.1 纯汇编实现
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
IMPORT my_main
LDR SP, =(0x20000000 + 0xC000)
; relocate data section
BL relocate
; clear bss section
BL clear_bss
BL my_main
ENDP
clear_bss PROC
IMPORT |Image$$RW_IRAM1$$ZI$$Base| ; ZI段的链接起始地址
IMPORT |Image$$RW_IRAM1$$ZI$$Length| ; ZI段的长度
ldr r0, =|Image$$RW_IRAM1$$ZI$$Base|
mov r1, #0
ldr r2, =|Image$$RW_IRAM1$$ZI$$Length|
memset
strb r1, [r0], #1 ; *(char*)&r1 = r0 = 0, r0 += 1
cmp r2,#0 ; 在r2-1前进行比较, 判断其是否为0(不然会少执行一次)
sub r2, r2, #1 ; r2 -= 1
bne memset ; z != 0 跳转 (取决于r2 是否为 0)
bx lr ; 返回
mov r0, r0 ; 字节对齐
ENDP
成功将bss段清0:
3.2.2 C函数实现
- C函数实现memset
void c_memset(void *dest, char val, unsigned int len){
while (len --){
*(char*)dest++ = val;
}
}
- 汇编传参&调用
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
IMPORT my_main
LDR SP, =(0x20000000 + 0xC000)
; relocate data section
BL relocate
; clear bss section
IMPORT c_memset
ldr r0, =|Image$$RW_IRAM1$$ZI$$Base|
mov r1, #0
ldr r2, =|Image$$RW_IRAM1$$ZI$$Length|
BL c_memset
BL my_main
ENDP
3.3 代码段重定位
3.3.1 修改散列文件
之前散列文件中的代码段的加载地址=链接地址,因此无需重定位,但是想让程序执行得更快,需要把代码段复制到内存(RAM)里,下面修改散列文件:
LR_IROM1 0x08000000 0x00040000 { ; load region size_region
ER_IROM1 0x20000000{ ; load address != execution address
*.o (RESET, +First)
.ANY (+RO)
.ANY (+XO)
}
RW_IRAM1 +0 { ; RW data
.ANY (+RW +ZI)
}
}
- 可执行域
ER_IROM1
- 加载地址为0x08000000,可执行地址为0x20000000,两者不相等
- 板子上电后,从0x08000000处开始运行,需要尽快把代码段复制到0x20000000
- 可执行域
RW_IRAM1
- 加载地址:紧跟着
ER_IROM1
的加载地址 - 可执行地址:紧跟着
ER_IROM1
的可执行地址 - 需要尽快把数据段复制到可执行地址处
- 加载地址:紧跟着
3.3.2 代码段不重定位引发的后果
指令从0x08000000
位置开始存储(加载地址),但是实际运行地址(链接地址)是从0x20000000
处开始的,但此空间并没有放入指令,所以此时程序不能正常执行,分析反汇编文件:
第一列为链接地址,第二列为机器码,复位后跳到0x20000009 - 1
(LSB=1表示Thumb状态)处开始执行,然后加载pc->0x2000c000
,但此空间并没有放入指令,导致程序崩溃。
如果将程序执行第二条指令存放的Reset_Handler
地址设为0x08000009
(第一条指令为给SP赋值,这里先赋0,后面在复位isr中再进行赋值),程序全部使用位置无关码,即可正常运行。
- 修改启动文件
__Vectors DCD 0 ; Top of Stack
DCD 0x08000009 ; Reset Handler
AREA |.text|, CODE, READONLY
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
...
- 分析反汇编文件
第二条指令存放的Reset_Handler
地址变为了0x08000009
,其他地方并未发送变化,但程序却可以正常运行了:
这是因为反汇编文件中看到的都是程序的链接地址,但是在实际运行中,都是使用的位置无关码,从Reset_Handler
开始执行时,pc = 0x08000008 + 4
(因为ARM指令流水线机制),然后从pc+60 = 0x08000048
取指执行(链接地址0x20000048
也是相对pc偏移计算出来的),后面每条指令都是相对pc偏移运行的,即依然是在flash中取指的。
如果不使用位置无关码,即用链接地址来调用函数:
-
汇编中
ldr pc, =my_main ; 调用函数时,用到main函数的链接地址,如果代码段没有重定位,则跳转失败
将启动文件的BL my_main
修改成绝对跳转后,程序无法正常执行。
-
C语言中
void (*funcptr)(USART_TypeDef* usart, const char *s); funcptr = send_str; funcptr(usart1, "hello, test function ptr");
当程序运行到函数指针处时,指针指向的函数运行时的地址(链接地址)是存放在RAM中的,程序会以绝对跳转的方式到该空间取指,但并没有存入指令,程序跑飞。
3.3.3 编写程序(综合)
代码段的加载地址(源): Load$$ER_IROM1$$Base
代码段的链接地址(目的): Image$$ER_IROM1$$Base
代码段的长度(长度): Image$$ER_IROM1$$Length
注意:即使使用位置无关码,但是第一条指令地址仍然需要指定为flash中的地址,即让pc的初值为flash中的地址,后面重定位代码才能相对于pc在flash中正常运行,重定位完成后在使用绝对跳转指令到RAM中运行。
具体步骤:
- 复位向量地址设为flash中起始位置第二条指令地址
0x08000008 + 1
- 根据链接符号提供的代码段源、目的、长度进行重定位,与数据段重定位函数的实现一致
- 最后用绝对跳转指令跳到
main
函数中去执行(使用相对跳转最后不会跳到ram, 程序依然在flash中运行,因为flash中也存放着指令的,而重定位要做的就是将flash的指令拷贝到ram中)
完整代码如下:
- 汇编
PRESERVE8
THUMB
AREA RESET, DATA, READONLY
EXPORT __Vectors
__Vectors DCD 0 ; Top of Stack
DCD 0x08000009 ; Reset Handler ; pc初始值必须在flash中, 且必须为flash起始地址偏移2个指令长度
AREA |.text|, CODE, READONLY
; Reset handler
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
IMPORT my_main
IMPORT |Load$$ER_IROM1$$Base| ; 代码段的加载起始地址(源)
IMPORT |Image$$ER_IROM1$$Base| ; 代码段的链接起始地址(目的)
IMPORT |Image$$ER_IROM1$$Length| ; 代码段的长度(长度)
IMPORT |Load$$RW_IRAM1$$Base| ; 数据段的加载起始地址(源)
IMPORT |Image$$RW_IRAM1$$Base| ; 数据段的链接起始地址(目的)
IMPORT |Image$$RW_IRAM1$$Length| ; 数据段的长度(长度)
LDR SP, =(0x20000000 + 0xC000)
; relocate text section
ldr r0, =|Load$$ER_IROM1$$Base|
ldr r1, =|Image$$ER_IROM1$$Base|
ldr r2, =|Image$$ER_IROM1$$Length|
BL relocate
; relocate data section
ldr r0, =|Load$$RW_IRAM1$$Base|
ldr r1, =|Image$$RW_IRAM1$$Base|
ldr r2, =|Image$$RW_IRAM1$$Length|
BL relocate
; clear bss section
BL clear_bss
LDR R0, =my_main ; ldr pc, =my_main
BLX R0 ; X带状态跳转
; BL my_main ; 使用相对跳转最后不会跳到ram, 程序依然在flash中运行(flash中也存放着指令)
ENDP
relocate PROC
memcpy
ldrb r3, [r0], #1 ; r3 = *(char*)&r0, r0 += 1
strb r3, [r1], #1 ; *(char*)&r1 = r3, r1 += 1
cmp r2,#0 ; 在r2-1前进行比较, 判断其是否为0(不然会少执行一次)
sub r2, r2, #1 ; r2 -= 1
bne memcpy ; z != 0 跳转 (取决于r2 是否为 0)
bx lr ; 返回
ENDP
clear_bss PROC
IMPORT |Image$$RW_IRAM1$$ZI$$Base| ; ZI段的链接起始地址
IMPORT |Image$$RW_IRAM1$$ZI$$Length| ; ZI段的长度
ldr r0, =|Image$$RW_IRAM1$$ZI$$Base|
mov r1, #0
ldr r2, =|Image$$RW_IRAM1$$ZI$$Length|
memset
strb r1, [r0], #1 ; *(char*)&r1 = r0 = 0, r0 += 1
cmp r2,#0 ; 在r2-1前进行比较, 判断其是否为0(不然会少执行一次)
sub r2, r2, #1 ; r2 -= 1
bne memset ; z != 0 跳转 (取决于r2 是否为 0)
bx lr ; 返回
ENDP
ALIGN ; 填充字节使地址对齐
END
- C函数
首先需要修改启动文件:
PRESERVE8
THUMB
AREA RESET, DATA, READONLY
EXPORT __Vectors
__Vectors DCD 0 ; Top of Stack
DCD 0x08000009 ; Reset Handler ; pc初始值必须在flash中, 且必须为flash起始地址偏移2个指令长度
AREA |.text|, CODE, READONLY
; Reset handler
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
IMPORT my_main
IMPORT SystemInit
LDR SP, =(0x20000000+0xC000)
BL SystemInit
;BL mymain
LDR R0, =my_main
BLX R0
ENDP
END
而后直接使用C库提供的memset
和memcpy
:
void SystemInit(void)
{
extern int Image$$ER_IROM1$$Base[];
extern int Image$$ER_IROM1$$Length[];
extern int Load$$ER_IROM1$$Base[];
extern int Image$$RW_IRAM1$$Base[];
extern int Image$$RW_IRAM1$$Length[];
extern int Load$$RW_IRAM1$$Base[];
extern int Image$$RW_IRAM1$$ZI$$Base[];
extern int Image$$RW_IRAM1$$ZI$$Length[];
/* text relocate */
memcpy(Image$$ER_IROM1$$Base, Load$$ER_IROM1$$Base, Image$$ER_IROM1$$Length);
/* data relocate */
memcpy(Image$$RW_IRAM1$$Base, Load$$RW_IRAM1$$Base, Image$$RW_IRAM1$$Length);
/* bss clear */
memset(Image$$RW_IRAM1$$ZI$$Base, 0, Image$$RW_IRAM1$$ZI$$Length);
}
重定位完成后函数指针可以正常调用函数的链接地址:
4 总结
- 当加载地址与链接地址不同时则需要重定位
- 散列文件中描述了程序的加载域和执行域的起始地址和长度
- 重定位只能使用位置无关指令,即都是相对于当前PC偏移进行跳转的
- 重定位即复制数据,需了解源(加载地址)、目的(链接地址)、长度(复制数据的长度),这些可以通过armlink提供的symbol中获取
- keil中,变量>8字节才会放入到bss段,否则存放在rw_data中,使用bss段变量前需要将其清零
- 在重定位代码段时,需要将flash第二条指令存放的
Reset_Handler
地址修改为flash起始地址+8 +1,这样重定位程序才能在flash中正常复制数据到ram中
此外,如果将所有数据都存放到ram中,也可以:
- 修改散列文件,全部放入
IRAM
段
LR_IROM1 0x08000000 0x00040000 { ; load region size_region
IRAM 0x20000000{ ; load address = execution address
*.o (RESET, +First)
.ANY (+RO)
.ANY (+XO)
.ANY (+RW +ZI)
}
}
- 修改C函数
void SystemInit(void)
{
extern int Image$$IRAM$$Base[];
extern int Image$$IRAM$$Length[];
extern int Load$$IRAM$$Base[];
extern int Image$$IRAM$$ZI$$Base[];
extern int Image$$IRAM$$ZI$$Length[];
/* text + data relocate */
memset(Image$$IRAM$$Base, Load$$IRAM$$Base, Image$$IRAM$$Length);
/* bss clear */
memset(Image$$IRAM$$ZI$$Base, 0, Image$$IRAM$$ZI$$Length);
}
- 启动文件代码与汇编程序(综合)一致
memcpy
和memset
查看反汇编都是每次复制4个字节,因为各segment是4字节对齐的,且Image$$region_name$$Length
为4字节倍数,因此汇编代码也可以改为:
ldr r0, =|Load$$IRAM$$Base|
ldr r1, =|Image$$IRAM$$Base|
ldr r2, =|Image$$IRAM$$Length|
relocate_loop
sub r2, r2, #4 ; 每次复制4个字节
ldr r3, [r0, r2] ; r3 = *(r0 + r2)
str r3, [r1, r2] ; *(r1 + r2) = r3
cmp r2,#0
bne relocate_loop
bx lr ; 返回
; 也可以利用段的起始和结束地址
ldr r0, =|Load$$IRAM$$Base|
ldr r1, =|Image$$IRAM$$Base|
ldr r2, =|Image$$IRAM$$Limit|
relocate_loop
cmp r1, r2
ldrls r3, [r0], #4
strls r3, [r1], #4
bls relocate_loop
bx lr ; 返回
ENDP
ldr r0, =|Image$$IRAM$$ZI$$Base|
mov r1, #0
ldr r2, =|Image$$IRAM$$ZI$$Length|
clear_bss_loop
sub r2, r2, #4
str r1, [r0, r2]
cmp r2,#0
bne clear_bss_loop
bx lr ; 返回
; 也可以利用zi的起始和结束地址
ldr r0, =|Image$$IRAM$$ZI$$Base|
mov r1, #0
ldr r2, =|Image$$IRAM$$ZI$$Limit|
clear_bss_loop ; ls: r0<=r2时执行
cmp r0, r2
strls r1, [r0], #4 ; *r0 = r1, r0+=4;
bls clear_bss_loop
bx lr ; 返回
注意:是从后往前复制的,因此需要先减小长度。
参考:
END