IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 游戏开发 -> [012] [STM32] 代码重定位与清除BSS段深入分析 -> 正文阅读

[游戏开发][012] [STM32] 代码重定位与清除BSS段深入分析

STM32
Contents
重定位的概念与引入
重定位的概念
为什么需要重定位
重定位分析与实现
需要重定位的段
重定位的实现
重定位程序编写
数据段重定位
清除BSS段·
代码段重定位
总结

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:

image-20220314235930120

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),符号表存放的是所有变量的名字和地址,反汇编代码中可以看到:

image-20220316112839987

因此,IROM中都保存了g_a和g_b的名字和地址,但对于常量g_a,在IROM中还保存了它的值。

在C函数中直接向0x20000000地址直接写入值后,可以正常print:

*((unsigned int*)(0x20000000)) = 'b';

image-20220315003102300

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文件:

image-20220317120216209

分别为:+RO、+ZI、+RW

2.2.2.2 region symbol含义

散列文件一般使用Two execution regions:image-20220315152046670

SymbolDescription
Image$$region_name$$BaseExecution address of the region.
Image$$region_name$$LengthExecution region length in bytes excluding ZI length.
Image$$region_name$$LimitAddress of the byte beyond the end of the non-ZI part of the execution region.
Image$$region_name$$RO$$BaseExecution address of the RO output section in this region.
Image$$region_name$$RO$$LengthLength of the RO output section in bytes.
Image$$region_name$$RO$$LimitAddress of the byte beyond the end of the RO output section in the execution region.
Image$$region_name$$RW$$BaseExecution address of the RW output section in this region.
Image$$region_name$$RW$$LengthLength of the RW output section in bytes.
Image$$region_name$$RW$$LimitAddress of the byte beyond the end of the RW output section in the execution region.
Image$$region_name$$XO$$BaseExecution address of the XO output section in this region.
Image$$region_name$$XO$$LengthLength of the XO output section in bytes.
Image$$region_name$$XO$$LimitAddress of the byte beyond the end of the XO output section in the execution region.
Image$$region_name$$ZI$$BaseExecution address of the ZI output section in this region.
Image$$region_name$$ZI$$LengthLength of the ZI output section in bytes.
Image$$region_name$$ZI$$LimitAddress of the byte beyond the end of the ZI output section in the execution region.
▲ Image$$ 可执行域 symbols
  • 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反汇编代码如下:

image-20220315191513840

当然也可以在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]);
}

image-20220315205848670

因此,需要手动清除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:

image-20220315212508535

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处开始的,但此空间并没有放入指令,所以此时程序不能正常执行,分析反汇编文件:

image-20220315220737309

第一列为链接地址,第二列为机器码,复位后跳到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]
				...
  • 分析反汇编文件

image-20220315222115330

第二条指令存放的Reset_Handler地址变为了0x08000009,其他地方并未发送变化,但程序却可以正常运行了:

image-20220315222709364

这是因为反汇编文件中看到的都是程序的链接地址,但是在实际运行中,都是使用的位置无关码,从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库提供的memsetmemcpy

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);
}

重定位完成后函数指针可以正常调用函数的链接地址:

image-20220316002644181

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);
}

memcpymemset查看反汇编都是每次复制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

  游戏开发 最新文章
6、英飞凌-AURIX-TC3XX: PWM实验之使用 GT
泛型自动装箱
CubeMax添加Rtthread操作系统 组件STM32F10
python多线程编程:如何优雅地关闭线程
数据类型隐式转换导致的阻塞
WebAPi实现多文件上传,并附带参数
from origin ‘null‘ has been blocked by
UE4 蓝图调用C++函数(附带项目工程)
Unity学习笔记(一)结构体的简单理解与应用
【Memory As a Programming Concept in C a
上一篇文章      下一篇文章      查看所有文章
加:2022-03-17 22:31:30  更:2022-03-17 22:33:32 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2025年1日历 -2025/1/16 19:05:06-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码