代码重定位
一、清除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语言开发的单片机程序里面,普遍都没有涉及到堆栈段长度的设置,但这不意味着不用设置。堆栈段主要是用来在中断处理时起“保存现场”及“现场还原”的作用,其重要性不言而喻。而这么重要的内容,也包含在了编译器预设的内容里面,确实省事,可并不一定省心。
3、分配数据段data,常量段const,代码段code的起始地址。代码段与常量段的地址可以不管,它们都是固定在ROM里面的,无论它们怎么排列,都不会对程序产生影响。但是数据段的地址就必须得关心。数据段的数据时要从ROM拷贝到RAM中去的,而在RAM中,既有数据段data,也有堆栈段stack,还有通用的工作寄存器组。通常,工作寄存器组的地址是固定的,这就要求在绝对定址数据段时,不能使数据段覆盖所有的工作寄存器组的地址。必须引起严重关注。
通常的做法是:普通的flashMCU是在上电时或复位时,c指针里面的存放的是“0000”,表示CPU从ROM的0000地址开始执行指令,在该地址处放一条跳转指令,使程序跳转到_main函数中,然后根据不同的指令,一条一条的执行,当中断发生时(中断数量也很有限,2~5个中断),按照系统分配的中断向量表地址,在中断向量里面,放置一条跳转到中断服务程序的指令,如此如此,整个程序就跑起来了。决定CPU这样做,是这种ROM结构所造成的。
2.5 代码段在哪?多大?
在散列文件中,代码段在可执行域ER_IROM1
中描述:
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)
}
}
代码段的链接地址(基地址)、长度,使用下面的符号获得:
代码段的加载地址,使用下面的符号获得:
2.6 怎么重定位
1.汇编代码
IMPORT |Image$$ER_IROM1$$Base|
IMPORT |Image$$ER_IROM1$$Length|
IMPORT |Load$$ER_IROM1$$Base|
LDR R0, = |Image$$ER_IROM1$$Base| ; DEST
LDR R1, = |Load$$ER_IROM1$$Base| ; SORUCE
LDR R2, = |Image$$ER_IROM1$$Length| ; LENGTH
BL memcpy
2.C语言代码
- 方法1
声明为外部变量,使用时需要使用取址符:
extern int Image$$ER_IROM1$$Base;
extern int Load$$ER_IROM1$$Base;
extern int Image$$ER_IROM1$$Length;
memcpy(&Image$$ER_IROM1$$Base, &Image$$ER_IROM1$$Length, &Load$$ER_IROM1$$Base);
- 方法2
声明为外部数组,使用时不需要使用取址符:
extern char Image$$ER_IROM1$$Base[];
extern char Load$$ER_IROM1$$Base[];
extern int Image$$ER_IROM1$$Length;
memcpy(Image$$ER_IROM1$$Base, Image$$ER_IROM1$$Length, &Load$$ER_IROM1$$Base);&Load$$ER_IROM1$$Base);
2.7 为什么重定位之前的代码也可以正常运行?
因为重定位之前的代码是使用位置无关码写的:
- 只使用相对跳转指令:B、BL
相对跳转,新的PC值 = 老的PC值 + 事先算好的偏移值
两条指令是放在一块的,中间差个offset(个人理解,就是只能在ROM上跳转,不能从ROM上跳转到内存中)
相对跳转程序只会在flash上执行main函数,如果想要跳到内存上去执行main函数,就需要执行以下指令:
- 不只用绝对跳转指令:
LDR R0, =main
BLX R0
- 不访问全局变量、静态变量、字符串、数组
- 重定位完后,使用绝对跳转指令跳转到XXX函数的链接地址去
BL main ; bl相对跳转,程序仍在Flash上运行
LDR R0, =main ; 使用链接地址,绝对跳转,跳到链接地址去,就是跳去内存里执行
BLX R0
反汇编代码解析:
; Vector Table Mapped to Address 0 at Reset
AREA RESET, DATA, READONLY
EXPORT __Vectors
//程序从向量表这里开始执行
__Vectors DCD 0 ; //这个数据用来设置栈,但我们是自己手工设置的
DCD 0x08000009; //cpu来这里取出值,把它作为一个函数地址跳转过去 ; Reset Handler
AREA |.text|, CODE, READONLY
; Reset handler
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
IMPORT mymain
IMPORT |Image$$ER_IROM1$$Base|
IMPORT |Image$$ER_IROM1$$Length|
IMPORT |Load$$ER_IROM1$$Base|
; relocate text section
LDR R0, = |Image$$ER_IROM1$$Base| ; DEST
LDR R1, = |Load$$ER_IROM1$$Base| ; SORUCE
LDR R2, = |Image$$ER_IROM1$$Length| ; LENGTH
BL memcpy
CPU一上电,会用第一个值设置栈,但现在我们在后面手动设置栈了,所以这里值可以为0,然后在下一个位置,也就是2000,0009,取出这个位置的值,把它作为函数的地址跳转过去执行,bit0并不使用,只是用来表示说是Thumb指令集,实际地址是pc = 2000,0008;但是在2000,0008这个位置上,内存这里,还没有放任何东西,程序就崩掉了
所以我们需要在2000,0009这个位置放0800,0009;把代码改为DCD 0x08000009
;
三、重定位纯C函数的实现
3.1 难点
难点在于,怎么得到各个域的加载地址、链接地址、长度。
- 方法1
声明为外部变量,使用时需要使用取址符:
extern int Image$$ER_IROM1$$Base;
extern int Load$$ER_IROM1$$Base;
extern int Image$$ER_IROM1$$Length;
memcpy(&Image$$ER_IROM1$$Base, &Image$$ER_IROM1$$Length, &Load$$ER_IROM1$$Base);
- 方法2
声明为外部数组,使用时不需要使用取址符:
extern char Image$$ER_IROM1$$Base[];
extern char Load$$ER_IROM1$$Base[];
extern int Image$$ER_IROM1$$Length;
memcpy(Image$$ER_IROM1$$Base, Image$$ER_IROM1$$Length, &Load$$ER_IROM1$$Base);
3.2 怎么理解上述代码
对于这样的C变量:
int g_a;
编译的时候会有一个符号表(symbol table),如下:
Name | Address |
---|---|
g_a | xxxxxxxx |
对于散列文件中的各类Symbol,有2中声明方式:
extern int Image$$ER_IROM1$$Base; // 声明为一般变量
extern char Image$$ER_IROM1$$Base[]; // 声明为数组
不管是哪种方式,它们都会保存在符号表里,比如:
Name | Address |
---|---|
g_a | xxxxxxxx |
Image$$ER_IROM1$$Base | yyyyyyyy |
- 对于
int g_a
变量- 使用
&g_a
得到符号表里的地址。
- 使用
- 对于
extern int Image$$ER_IROM1$$Base
变量- 要得到符号表中的地址,也是使用
&Image$$ER_IROM1$$Base
。
- 要得到符号表中的地址,也是使用
- 对于
extern char Image$$ER_IROM1$$Base[]
变量- 要得到符号表中的地址,直接使用
Image$$ER_IROM1$$Base
,不需要加&
- 为什么?
mage$$ER_IROM1$$Base
本身就表示地址
- 要得到符号表中的地址,直接使用
3.3 代码实现
- start.s
PRESERVE8
THUMB
; Vector Table Mapped to Address 0 at Reset
AREA RESET, DATA, READONLY
EXPORT __Vectors
__Vectors DCD 0
DCD 0x08000009; //Reset_Handler ; Reset Handler 对应ROM上的地址,从ROM上开始执行程序
AREA |.text|, CODE, READONLY
; Reset handler
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
IMPORT mymain
IMPORT SystemInit
LDR SP, =(0x20000000+0x10000)
BL SystemInit
;BL mymain
LDR R0, =mymain
BLX R0
ENDP
END
- init.c
#if 0
void SystemInit_bak_ok(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);
}
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);
}
#endif
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);
}
- main.c
#include "uart.h"
#include "string.h"
char g_Char = 'A';
const char g_Char2 = 'B';
int g_A[3] = {0, 0};
char g_B[9];
void delay(volatile int d)
{
while(d--);
}
int mymain()
{
char c;
void (*funcptr)(const char *s, unsigned int val);
static int s_C[16] = {0, 0};
funcptr = put_s_hex;
uart_init();
delay(1);
putchar('1');
putchar('0');
putchar('0');
putchar('a');
putchar('s');
putchar('k');
putchar('\n');
putchar('\r');
funcptr("test for text relocate ", 123);
put_s_hex("g_Char's addr = ", &g_Char);
put_s_hex("g_Char2's addr = ", &g_Char2);
put_s_hex("g_A[0]'s val = ", g_A[0]);
put_s_hex("g_B[0]'s val = ", g_B[0]);
put_s_hex("s_C[0]'s val = ", s_C[0]);
putchar(g_Char);
putchar(g_Char2);
while (1)
{
c = getchar();
putchar(c);
putchar(c+1);
}
return 0;
}
- string.c
#include "uart.h"
int puts(const char *s)
{
while (*s)
{
putchar(*s);
s++;
}
return 0;
}
void puthex(unsigned int val)
{
/* 0x76543210 */
int i, j;
puts("0x");
for (i = 7; i >= 0; i--)
{
j = (val >> (i*4)) & 0xf;
if ((j >= 0) && (j <= 9))
putchar('0' + j);
else
putchar('A' + j - 0xA);
}
}
void put_s_hex(const char *s, unsigned int val)
{
puts(s);
puthex(val);
puts("\r\n");
}
void memcpy(void *dest, void *src, unsigned int len)
{
unsigned char *pcDest = dest;
unsigned char *pcSrc = src;
while (len --)
{
*pcDest = *pcSrc;
pcSrc++;
pcDest++;
}
}
void memset(void *dest, unsigned char val, unsigned int len)
{
unsigned char *pcDest = dest;
while (len --)
{
*pcDest = val;
pcDest++;
}
}
- uart.c
#include "uart.h"
typedef unsigned int uint32_t;
typedef struct
{
volatile uint32_t SR; /*!< USART Status register, Address offset: 0x00 */
volatile uint32_t DR; /*!< USART Data register, Address offset: 0x04 */
volatile uint32_t BRR; /*!< USART Baud rate register, Address offset: 0x08 */
volatile uint32_t CR1; /*!< USART Control register 1, Address offset: 0x0C */
volatile uint32_t CR2; /*!< USART Control register 2, Address offset: 0x10 */
volatile uint32_t CR3; /*!< USART Control register 3, Address offset: 0x14 */
volatile uint32_t GTPR; /*!< USART Guard time and prescaler register, Address offset: 0x18 */
} USART_TypeDef;
void uart_init(void)
{
USART_TypeDef *usart1 = (USART_TypeDef *)0x40013800;
volatile unsigned int *pReg;
/* 使能GPIOA/USART1模块 */
/* RCC_APB2ENR */
pReg = (volatile unsigned int *)(0x40021000 + 0x18);
*pReg |= (1<<2) | (1<<14);
/* 配置引脚功能: PA9(USART1_TX), PA10(USART1_RX)
* GPIOA_CRH = 0x40010800 + 0x04
*/
pReg = (volatile unsigned int *)(0x40010800 + 0x04);
/* PA9(USART1_TX) */
*pReg &= ~((3<<4) | (3<<6));
*pReg |= (1<<4) | (2<<6); /* Output mode, max speed 10 MHz; Alternate function output Push-pull */
/* PA10(USART1_RX) */
*pReg &= ~((3<<8) | (3<<10));
*pReg |= (0<<8) | (1<<10); /* Input mode (reset state); Floating input (reset state) */
/* 设置波特率
* 115200 = 8000000/16/USARTDIV
* USARTDIV = 4.34
* DIV_Mantissa = 4
* DIV_Fraction / 16 = 0.34
* DIV_Fraction = 16*0.34 = 5
* 真实波特率:
* DIV_Fraction / 16 = 5/16=0.3125
* USARTDIV = DIV_Mantissa + DIV_Fraction / 16 = 4.3125
* baudrate = 8000000/16/4.3125 = 115942
*/
#define DIV_Mantissa 4
#define DIV_Fraction 5
usart1->BRR = (DIV_Mantissa<<4) | (DIV_Fraction);
/* 设置数据格式: 8n1 */
usart1->CR1 = (1<<13) | (0<<12) | (0<<10) | (1<<3) | (1<<2);
usart1->CR2 &= ~(3<<12);
/* 使能USART1 */
}
int getchar(void)
{
USART_TypeDef *usart1 = (USART_TypeDef *)0x40013800;
while ((usart1->SR & (1<<5)) == 0);
return usart1->DR;
}
int putchar(char c)
{
USART_TypeDef *usart1 = (USART_TypeDef *)0x40013800;
while ((usart1->SR & (1<<7)) == 0);
usart1->DR = c;
return c;
}