代码重定位
一、清除BSS段(ZI段)
1.1 C语言中的BSS段
程序里的全局变量,如果它的初始值为0,或者没有设置初始值,这些变量被放在BSS段里,也叫ZI段。
char g_Char = 'A';
const char g_Char2 = 'B';
int g_A = 0; // 放在BSS段
int g_B; // 放在BSS段
BSS段并不会放入bin文件中,也不会烧写到flash中,否则也太浪费空间了。
在使用BSS段里的变量之前,把BSS段所占据的内存清零就可以了。
- 注意:对于keil来说,一个本该放到BSS段的变量,如果它所占据的空间小于等于8字节自己,keil仍然会把它放在data段里。只有当它所占据的空间大于8字节时,才会放到BSS段。
int g_A[3] = {
0, 0}; // 放在BSS段
char g_B[9]; // 放在BSS段
int g_A[2] = {
0, 0}; // 放在data段
char g_B[8]; // 放在data段
打印数组的首项,显示结果是随机值,怎么修改才能让打印出的内存地址不是随机值呢?初始化BSS段,清零
所以在定义的变量值为0,或者未初始化时,将他们放到BSS段上,在使用前清零即可
1.2 BSS段在哪?多大?
在散列文件中,BSS段(ZI段)在可执行域RW_IRAM1
中描述:
LR_IROM1 0x08000000 0x00080000 {
; load region size_region
ER_IROM1 0x08000000 0x00080000 {
; load address = execution address
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+RO)
.ANY (+XO)
}
RW_IRAM1 0x20000000 0x00010000 {
; RW data
.ANY (+RW +ZI)
}
}
BSS段(ZI段)的链接地址(基地址)、长度,使用下面的符号获得:
1.3 怎么清除BSS段
1.汇编码
IMPORT |Image$$RW_IRAM1$$ZI$$Base|
IMPORT |Image$$RW_IRAM1$$ZI$$Length|
LDR R0, = |Image$$RW_IRAM1$$ZI$$Base| ; DEST
MOV R1, #0 ; VAL
LDR R1, = |Image$$RW_IRAM1$$ZI$$Length| ; Length
BL memset
2.C语言
- 方法1
声明为外部变量,使用时需要使用取址符:
extern int Image$$RW_IRAM1$$ZI$$Base;
extern int Image$$RW_IRAM1$$ZI$$Length;
memset(&Image$$RW_IRAM1$$ZI$$Base, 0, &Image$$RW_IRAM1$$ZI$$Length);
- 方法2
声明为外部数组,使用时不需要使用取址符:
extern char Image$$RW_IRAM1$$ZI$$Base[];
extern int Image$$RW_IRAM1$$ZI$$Length[];
memset(Image$$RW_IRAM1$$ZI$$Base[], 0, Image$$RW_IRAM1$$ZI$$Length);
二、代码重定位
2.1 加载地址等于链接地址
在默认散列文件中,代码段的load address = execution address
,(执行地址)
也就是加载地址和**执行地址(链接地址)**一致,所以无需重定位:
ROM对应的地址是0x08000000;RAM对应的地址是0x020000000
LR_IROM1 0x08000000 0x00080000 {
; load region size_region //加载域;加载域里面有两个执行域
ER_IROM1 0x08000000 0x00080000 {
; load address = execution address //这个可执行域里面放的主要是代码;加载地址=执行地址,所以
*.o (RESET, +First) //所有.o 文件的RESET段 //不需要重定位
*(InRoot$$Sections)
.ANY (+RO) //.o 文件,或者.h文件的只读数据段; .ANY表示所有.o文件、库文件
.ANY (+XO)
}
RW_IRAM1 0x20000000 0x00010000 {
; RW data
.ANY (+RW +ZI)
}
}
2.2 加载地址不等于链接地址
有时候,我们需要把程序复制到内存里运行,比如:
- 想让程序执行得更快:需要把代码段复制到内存里
- 程序很大,保存在片外SPI Flash中,SPI Flash上的代码无法直接执行,需要复制到内存里
这时候,需要修改散列文件,把代码段的可执行域放在内存里。
那么程序运行时,需要尽快把代码段重定位到内存。
散列文件示例:
LR_IROM1 0x08000000 0x00080000 {
; 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的可执行地址
- 需要尽快把数据段复制到可执行地址处
数据段的重定位我们做过实验,
如果代码段不重定位的话,会发生什么事?
2.3 代码段不重定位的后果
不能使用链接地址来调用函数
- 汇编中
ldr pc, =main ; 这样调用函数时,用到main函数的链接地址,如果代码段没有重定位,则跳转失败
- C语言中
void (*funcptr)(const char *s, unsigned int val);
funcptr = put_s_hex;
funcptr("hello, test function ptr", 123);
2.4 代码段重定位
①code:代码段,存放程序的所有代码
②RO:只读数据段,存放程序中定义的常量
③RW:读写数据段,存放初始化为非0值的全局变量
④ZI:零数据段,存放未初始化的全局变量,以及初始化为0的变量。
ROM:存放指令代码和一些固定数值,程序运行后不可改动。
凡是c文件及h文件中所有代码、全局变量、局部变量、’const’限定符定义的常量数据、startup.asm文件中的代码(类似ARM中的bootloader或者X86中的BIOS,一些低端的单片机是没有这个的)通通都存储在ROM中。
RAM:用于程序运行中数据的随机存取,掉电后数据消失。
凡是整个程序中,所用到的需要被改写的量,都存储在RAM中,“被改变的量”包括全局变量、局部变量、堆栈段。
程序经过编译、汇编、链接后,生成hex文件。用专用的烧录软件,通过烧录器将hex文件烧录到ROM中,这个时候的ROM中,包含所有的程序内容:无论是一行一行的程序代码,函数中用到的局部变量,头文件中所声明的全局变量,const声明的只读常量,都被生成了二进制数据,包含在hex文件中,全部烧录到了ROM里面,此时的ROM,包含了程序的所有信息,正是由于这些信息,“指导”了CPU的所有动作。
可能有人会有疑问,既然所有的数据在ROM中,那RAM中的数据从哪里来?什么时候CPU将数据加载到RAM中?
回答这个问题,首先必须明确一条:ROM是只读存储器,CPU只能从里面读数据,而不能往里面写数据,掉电后数据依然保存在存储器中;
RAM是随机存储器,CPU既可以从里面读出数据,又可以往里面写入数据,掉电后数据不保存,这是条永恒的真理,始终记挂在心。
RAM中的数据不是在烧录的时候写入的,同时也说明,在CPU运行时,RAM中已经写入了数据。关键就在这里:这个数据不是人为写入的,CPU写入的,那CPU又是什么时候写入的呢?
这里所做的工作是为整个程序的顺利运行做好准备,或者说是对RAM的初始化(注:ROM是只读不写的),工作任务有几项:
1、为全局变量分配地址空间—如果全局变量已赋初值,则将初始值从ROM中拷贝到RAM中,如果没有赋初值,则这个全局变量所对应的地址下的初值为0或者是不确定的。当然,如果已经指定了变量的地址空间,则直接定位到对应的地址就行,那么这里分配地址及定位地址的任务由“连接器”完成。
2、设置堆栈段的长度及地址-–用C语言开发的单片机程序里面,普遍都没有涉及到堆栈段长度的设置