ARM架构与编程——6.重定位的引入

基于STM32F103的重定位

一、段的概念及重定位的引入

1.1 问题引入

在串口程序中添加全局变量,把它打印出来,看看会发生什么事。

char g_Char = 'A';
const char g_Char2 = 'B';	//const修饰的常量

//main()
putchar(g_Char);
putchar(g_Char2);

打印结果:
在这里插入图片描述
可以看到const修饰的常量和全局变量的地址空间并不一样,且变量 g_char 的值并不能打印出来,这是为什么呢?

我们先引入STM32F103的内存资源
在这里插入图片描述
可以看到g_char被保存到RAM中,而g_char2则被保存到ROM中
这是为什么呢?
RAM被称为随机存取存储器,而ROM则被成为只读存储器

因为RAM是可读可写的
ROM只可读,const修饰的是常量,只读就行,所以可以放在ROM里

所以在程序烧录的时候,.bin程序被烧写到ROM里面,变量g_char2的值就存储在ROM中,此时我们使用地址就可以访问到变量B;
而变量g_char存储在RAM里面,没人对他进行初始化操作,所以它的值就是一个随机值,打印出来就会变成乱码

为什么变量g_char的值会保存在RAM中呢,此时就需要我们引入重定位的概念。
什么是重定位呢?

烧写完程序,ROM上会有代码,变量的值等,所以在调用main函数之前,应该把数据复制到内存上去,这就是重定位(重新确定位置)

重定位为数据段重定位,代码重定位

  • 代码重定位

保存在ROM上的全局变量的值,在使用前要复制到内存,这就是数据段重定位。
代码直接放在ROM上运行即可,对于数据,可读可写的数据,我们需要把它从ROM上复制到RAM上去

  • 数据段重定位

想把代码移动到其他位置,这就是代码重定位。

在这里插入图片描述
接下来引入下一个问题:
访问可读可写的全局变量前,应该给它初始化一个值,谁来初始化?

在 start.s 启动文件里进行初始化

  • 段的详细解释

1.初始值放在ROM上,但是我们要去内存里访问它,所以在使用它前,我们需要把它的值从ROM上复制到内存里,像这类变量我们要把它们放在一块,这一段空间就称为可读可写的数据段.

2.像程序中的多个常量,我们也把它们放在一块,这一块空间就被成为只读数据段

3.指令本身,他们是属于代码段

4.对于初始值为0的变量,或者没有初始值的变量,假设有很多,我们需要把他们都保存到ROM上吗?
答案肯定是不用,对于这些变量,我们会把他们放在一块,这块空间就被称为BSS段,或者叫ZI段,使用前,只需把这段空间全部清零即可,相应的变量的值也就清零了

1.2 段的概念

代码段、只读数据段、可读可写的数据段、BSS段。

char g_Char = 'A';           // 可读可写,不能放在ROM上,应该放在RAM里
const char g_Char2 = 'B';    // 只读变量,可以放在ROM上
int g_A = 0;   // 初始值为0,干嘛浪费空间保存在ROM上?没必要
int g_B;       // 没有初始化,干嘛浪费空间保存在ROM上?没必要	放在BSS段上或者ZI段上

所以,程序分为这几个段:

  • 代码段(RO-CODE):就是程序本身,不会被修改
  • 可读可写的数据段(RW-DATA):有初始值的全局变量、静态变量,需要从ROM上复制到内存
  • 只读的数据段(RO-DATA):可以放在ROM上,不需要复制到内存
  • BSS段或ZI段:
    • 初始值为0的全局变量或静态变量,没必要放在ROM上,使用之前清零就可以
    • 未初始化的全局变量或静态变量,没必要放在ROM上,使用之前清零就可以
  • 局部变量:保存在栈中,运行时生成
  • 堆:一块空闲空间,使用malloc函数来管理它,malloc函数可以自己写

二、重定位要做的事情

2.1 程序中含有什么?

  • 代码段:如果它不在链接地址上,就需要重定位
  • 只读数据段:如果它不在链接地址上,就需要重定位
  • 可读可写的数据段:如果它不在链接地址上,就需要重定位
  • BSS段:不需要重定位,因为程序里根本不保存BSS段,使用前把BSS段对应的空间清零即可

2.2 谁来做重定位?

  • 程序本身:它把自己复制到链接地址去
  • 一开始,程序可能并不位于它的链接地址上,为什么它可以执行重定位的操作?
    • 因为重定位的代码是使用“位置无关码”写的
  • 什么叫位置无关码:这段代码扔在任何位置都可以运行,跟它所在的位置无关
  • 怎么写出位置无关码:
    • 跳转:使用相对跳转指令,不能使用绝对跳转指令
      • 只能使用branch指令(比如bl main),不能给PC直接复制,比如ldr pc, =main(这条指令是把main函数的链接地址赋给PC,这样这个程序就去链接地址那里执行代码,但是在链接地址那里还没有指令,还未重定位完)
    • 不要访问全局变量、静态变量(是使用链接地址的)
    • 不使用字符串(同上)

2. 3 怎么做重定位和清除BSS段?

  • 核心:复制 (memcpy)

  • 复制的三要素:源、目的、长度

    • 怎么知道代码段/数据段保存在哪?(加载地址)
    • 怎么知道代码段/数据段要被复制到哪?(链接地址)
    • 怎么知道代码段/数据段的长度?
  • 怎么知道BSS段的地址范围:起始地址、长度?

  • 这一切

    • 在keil中使用散列文件(Scatter File)来描述
    • 在GCC中使用链接脚本(Link Script)来描述

2. 4 加载地址和链接地址的区别

程序运行时,应该位于它的链接地址处,因为:

  • 使用函数地址时用的是"函数的链接地址",所以代码段应该位于链接地址处
  • 去访问全局变量、静态变量时,用的是"变量的链接地址",所以数据段应该位于链接地址处

但是: 程序一开始时可能并没有位于它的"链接地址":

  • 比如对于STM32F103,程序被烧录器烧写在Flash上,这个地址称为"加载地址"
  • 比如对于IMX6ULL/STM32MP157,片内ROM根据头部信息把程序读入内存,这个地址称为“加载地址”

加载地址 != 链接地址时,就需要重定位。

链接地址,运行地址,加载地址,存储地址之间的关系:
链接地址 == 运行地址,
加载地址 == 存储地址
链接地址:编译器编译时候,指定的a.out中第一条指令的地址
举例:比如,一个可执行程序a.out由a.o、b.o、c.o组成,那么最终的a.out中谁在前、谁在中间、谁在结尾,都可以通过制定链接地址来决定。
注意,对于CPU来说是不管你这个链接地址是物理地址还是虚拟地址。
链接地址是静态的,在进行程序编译的时候指定的。
总结
1、链接地址是给编译器用的,用来计算代码中相关地址偏移的
2、只要和PC值相关的就是位置无关代码(相对偏移),和PC无关的就是位置相关代码(绝对值)

在这里插入图片描述

例如上图所示,指令ldr r0, =func就是一条位置相关指令,在编译的时候,编译器根据链接地址(链接地址入口是0x40008000)将其翻译成:ldr r0, [pc, #0x80],也就是将func标号等价于地址0x40008080,然后将0x40008080这个地址数值放在a.out文件中链接地址0x50008000的位置。
当程序运行时,a.out会被加载到内存中运行,如果程序运行的地址和链接的地址都是0x40008000,那么程序运行时,没有任何问题,因为读取的func的地址是0x40008080,实际跳转的时候,跳转到0x40008080中存放的也是func对应的代码。
但是如果运行的地址和链接地址不一样(运行地址是0x20008000),这时候,func的地址还是编译的时候计算的地址0x40008080,但是实际在内存中,func的地址是0x20008080,那么当你跳转执行func的时候,取出来的是0x40008080,跳转的地址也是0x40008080,而0x40008080中存放的是什么代码我们不确定,但是一定不是func的代码(func存放在0x20008080中)。
这就是位置相关的概念

运行地址:可执行程序a.out在内存中存储的第一条指令地址
程序实际在内存中运行时候的地址,比如CPU要执行一条指令,那么必然要通过给PC赋值,从对应的地址空间中去取出来,那么这个地址就是实际的运行地址。
运行地址是动态的,如果你将程序加载到内存中时,改变存放在内存的地址,那么运行地址也就随之改变了。
注意,CPU同样不关心运行地址是虚拟地址还是物理地址。

三、散列文件的使用与分析

3.1 重定位的实质: 移动数据

把代码段、只读数据段、数据段,移动到它的链接地址处。
也就是复制
数据复制的三要素:源、目的、长度。

  • 数据保存在哪里?加载地址

  • 数据要复制到哪里?链接地址

  • 长度

这3要素怎么得到?
在keil中,使用散列文件来描述。
散列?分散排列?ROM和RAM并不是连续排列的
是的,在STM32F103这类资源紧缺的单片机芯片中,

  • 代码段保存在Flash上,直接在Flash上运行(当然也可以重定位到内存里)
  • 数据段保存在Flash上,使用前被复制到内存里
    在这里插入图片描述

上图中的目的执行的可执行域的地址就是链接地址
对于第二个可执行域,它的加载地址并不等于链接地址,所以对第二个可执行域来说,在执行前要进行重定位,指向内存(放入的有可读可写段,BSS段)

生成的散列文件位于Keil对应工程目录下Objects文件夹内,以.sct为文件后缀

生成散列文件还需对Keil进行配置
在这里插入图片描述

3.2 散列文件示例

3.2.1 示例代码
; *************************************************************
; *** Scatter-Loading Description File generated by uVision ***
; *************************************************************

LR_IROM1 0x08000000 0x00080000  {    ; load region size_region 加载域
  ER_IROM1 0x08000000 0x00080000  {  ; load address = execution address 可执行域1
   *.o (RESET, +First)
   .ANY (+RO)
   .ANY (+XO)
  }
  RW_IRAM1 0x20000000 0x00010000  {  ; RW data 可执行域2
   .ANY (+RW +ZI)
  }
}
3.2.2 散列文件语法

一个散列文件由一个或多个Load region组成:

load_region_description ::=
load_region_name (base_address | ("+" offset)) [attribute_list] [max_size]
"{"
execution_region_description+
"}

Load region中含有一个或多个Execution region
Execution region语法如下:

execution_region_description ::=
exec_region_name (base_address | "+" offset) [attribute_list] [max_size | length]
"{"
input_section_description*
"}

Execution region中含有一个或多个Input section
Input section语法如下:

input_section_description ::=
module_select_pattern [ "(" input_section_selector ( ","
input_section_selector )* ")" ]
input_section_selector ::=
"+" input_section_attr |
input_section_pattern |
input_section_type |
input_symbol_pattern |
section_properties

3.3 散列文件解析

在这里插入图片描述
*.o :所有objects文件(如RESET段,放在这个文件First最开始的地方)

*:所有objects文件和库,在一个散列文件中只能使用一个*;keil提供,如__main

.ANY:等同于*,优先级比*低;在一个散列文件的多个可执行域中可以有多个.ANY (所有的库)

3.4 怎么获得region的信息

3.4.1 可执行域信息

在这里插入图片描述

3.4.2 加载域信息

在这里插入图片描述

3.4.3 汇编代码里怎么使用这些信息

示例代码如下:

IMPORT |Image$$RW_IRAM1$$Base|		//可执行域的地址,目的
IMPORT |Image$$RW_IRAM1$$Length|	//长度
IMPORT |Load$$RW_IRAM1$$Base|		//加载域的地址,源

LDR R0, = |Image$$RW_IRAM1$$Base|    ; DEST
LDR R1, = |Load$$RW_IRAM1$$Base|     ; SORUCE
LDR R2, = |Image$$RW_IRAM1$$Length|  ; LENGTH

//

通过memcpy函数实现数据段的重定位,从而打印出变量g_char的值。未进行重定位时,未实现对数据的初始化操作,通过memcpy完成重定位,从而能顺利打印出变量g_char的值。

//start.s
; Reset handler
Reset_Handler   PROC
				EXPORT  Reset_Handler             [WEAK]
                IMPORT  main
				IMPORT |Image$$RW_IRAM1$$Base|
				IMPORT |Image$$RW_IRAM1$$Length|
				IMPORT |Load$$RW_IRAM1$$Base|
				IMPORT memcpy

				LDR SP, =(0x20000000+0x10000)

				; relocate data section
				LDR R0, = |Image$$RW_IRAM1$$Base|    ; DEST
				LDR R1, = |Load$$RW_IRAM1$$Base|     ; SORUCE
				LDR R2, = |Image$$RW_IRAM1$$Length|  ; LENGTH
				BL memcpy							 ;将数据拷贝到内存里

				BL main					

                ENDP
                
                END

//string.c
void memcpy(void *dest, void *src, unsigned int len)
{
	unsigned char *pcDest;
	unsigned char *pcSrc;
	
	while (len --)
	{
		*pcDest = *pcSrc;
		pcSrc++;
		pcDest++;
	}
}

//main.c
char g_Char = 'A';
const char g_Char2 = 'B';
putchar(g_Char);
putchar(g_Char2);

在这里插入图片描述

3.4.4 C语言里怎么使用这些信息
  • 方法1
    声明为外部变量。
    注意:使用时需要使用取址符:
extern int Image$$RW_IRAM1$$Base;
extern int Load$$RW_IRAM1$$Base;
extern int Image$$RW_IRAM1$$Length;

memcpy(&Image$$RW_IRAM1$$Base, &Image$$RW_IRAM1$$Length, &Load$$RW_IRAM1$$Base);
  • 方法2
  • 声明为外部数组。
    注意:使用时不需要使用取址符:
extern char Image$$RW_IRAM1$$Base[];
extern char Load$$RW_IRAM1$$Base[];
extern int Image$$RW_IRAM1$$Length;

memcpy(Image$$RW_IRAM1$$Base, Image$$RW_IRAM1$$Length, &Load$$RW_IRAM1$$Base);

注意:使用指针和使用外部变量方法一样,都需要进行取&地址

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值