ARM架构与编程6--重定位(基于百问网ARM架构与编程教程视频)

一、启动程序流程

我们之前讲过,单片机有根据boot的不同,有三种启动方式:

boot0boot1启动模式
0Xflash启动
10系统存储器
11内置SRAM

单片机上电复位后,运行main函数。以STMF103ZE芯片、flash启动为例。

首先在keil中可以看到芯片默认的ROM和RAM地址:
在这里插入图片描述

可以看到,ROM也就是flash起始地址是0x08000000,大小是512k:RAM起始地址是0x20000000,大小是64k。

下面以串口实验为例,看一下程序的反汇编代码。分析一下系统的启动流程。
在这里插入图片描述

程序被下载到flash中,首地址就是0x08000000,而且内核规定了,第一条指令必须是初始化栈指针的,第二条指令必须是初始化PC指针,PC = Rest_Handle,并且跳转过去执行,然后在Rest_Handler中调用main函数,这样程序就会执行我们自己写的函数了。注意这个函数是我们自己编写的,不是官方的,这里为了让大家更加清晰的看到程序的执行过程,简化了这个函数。

这里用mymain代替main函数,也是要简化系统启动的流程。 查看MDK的文档,会发现有这么一句说明:It is automatically created by the linker when it sees a definition of main()。就是说,当发现定义了main函数,那么就会自动创建__main,这个函数是编译器自动创建的,会在里面去进行一些初始化设置。

这样,我们就可以正常运行一个简单的程序,下面以串口实验为例:

在这里插入图片描述

可以看到,现象正常,说明我们自己的启动文件是可以运行的。

二、段

启动文件像上面那样设置是不是就没问题了?我们做个实验,还是以串口为例。

char Temp = 'O';
int mymain()
{	
    usart_init();
	putchar('1');
	putchar('1');
	putchar(Temp);
	putchar('1');
	putchar('1');
	while(1);
}

定义一个全局变量,并赋值。然后通过串口打印输出。

在这里插入图片描述

我们可以看到,输出的结果明显不对,Temp应该是输出字母O,但是结果明显不是。下面大红框是数据的16进制表示,31、32是1、2的ASCII码值,中间应该是O的,应该是4F。而且每一次复位之后,输出的也不一样。说明全局变量Temp的值不是确定的,每次上电后都发生变化。

下面定义一个const修饰的全局变量,可以看到,Temp2对应的16进制是41,也就是A,说明Temp2的值是确定的,Temp1的值是不确定的。

在这里插入图片描述

下面看一下这两个遍全局变量的地址。
在这里插入图片描述

可以看到,Temp1地址是0x2000 0000,Temp2地址是0x0800 0144,这两个地址是不是有些眼熟。没错,正是对应芯片的RAM和ROM。

在这里插入图片描述

也就是说,虽然都是全局变量,但是他们在程序运行时,保存的位置是不一样的。

对于全局变量Temp1,他是可读可写的,所以在运行时放在RAM中。Temp2是只读的,所以放在ROM(也就是Flash)中。根据程序数据存放位置的不同,内存会被分为不同的段。

1、可读可写数据段(RW-data):存放初始值不为0的全局变量、静态变量。烧录在ROM上,使用之前需要从ROM上复制到内存。

2、只读数据段(RO-data):存放不可以被修改的变量,如const修饰的变量或者字符串,烧录在ROM上,不需要复制到内存。

3、BSS或ZI段:初始值为0和未初始化的全局变量或静态变量,放在RAM中。

4、堆:一块空闲空间,存放进程运行中被动态分配的内存段,使用malloc函数来管理它。

5、栈:存放局部变量和函数调用时的参数

上面是存放数据的段,还有存放代码的段。

代码段(Code):就是目标文件的所有程序代码,不会被修改,烧录在Flash中。

烧录时:

Code+RO+RW全在flash上,RI段全是0没必要烧录,只需要在程序运行前全部清零即可。

运行时:

RW-data会搬运到RAM中,链接器会把RW的地址映射到RAM中的一片区域内,访问RW-data实际上访问的是RAM中的地址。

接下来我们分析一下,为什么上面的全局变量Temp1的值是不确定的。

因为链接器只负责映射地址,不负责数据的拷贝,所以对于RAM中的0x20000000这个地方,是没有数据的,或者说这里面的数据是随机的,因为没有对这快内存进行赋值。我们的程序上电之后,执行Rest_Handler程序,设置栈指针,然后运行main函数,使用串口打印Temp,这些流程中,并没有对0x20000000这块内存进行赋值。这就是为什么定义Temp1时候,即使赋值了,但是仍然打印不出来正确的数据初始化赋的值保存在了ROM中,而程序运行时访问的时RAM中的地址,RAM中的空间并没有被初始化。对于Temp2,CPU访问的时候访问的是ROM区域,而Temp2正是保存在这里,所以CPU可以直访问。

在这里插入图片描述

2.1 加载地址和链接地址

由上可知,程序在烧录和运行时分布是不同的。这些程序中所有初值要保存只可能存在ROM中,但是在使用的时候,访问的却是RAM。所以这中间肯定有这样一种机制 : 在上电以后把ROM中存储的这些变量初值来重新初始化到对应的RAM地址,以便后续程序指令访问。

在程序烧录时,所有数据都加载都ROM中,在ROM中为代码和数据分配内存空间,加载地址就是程序保存在ROM中的地址。

链接地址:程序在RAM中执行时的地址。执行这条指令时,PC值应该等于这个地址,也就是说,PC等于这个地址时,这条指令应该保存在这个地址内。链接地址由链接脚本文件指出,链接的时候确定

2.1.2 重定位

重定位就是把目标重新放在一个新的地址上,PC访问的时候是去新地址上访问。简单地说就是用链接地址来代替加载地址成为程序被访问时的地址。保存在ROM上的全局变量的值,在使用前要复制到RAM,这就是数据段重定位。若想把代码移动到其他位置,就是代码重定位。

假若程序不位于链接地址时程序会出现什么问题?

去访问某些全局变量时就会出错,因为访问这些全局变量时用的是它的链接地址,我去链接地址访问你,你就必须位于链接地址上。

现在我们应该知道了为什么上面的Temp1打印出来乱码,是因为Temp1的链接地址上的数据没有被赋值。

下面我们看一下官方提供的启动文件中的Rest_Handler函数中做了些什么:

在这里插入图片描述

SystemInit中主要负责初始化STM的时钟系统。

__main中的__scatterload函数负责设置内存,而rt_entry函数负责设置运行时的环境。__scatterload中负责把RW(非零)输出段从装载域地址复制到运行域地址(执行代码和数据复制、解压缩),并完成ZI段运行域数据的0初始化工作。然后跳到__rt_entry设置堆栈和堆、初始化库函数和静态数据。然后,__rt_entry跳转到应用程序的入口main()。主应用程序结束执行后,__rt_entry将库关闭,然后把控制权交换给调试器。main()函数的存在强制链接器链接到__main__rt_entry中的代码。如果没有标记为main()的函数,则没有链接到初始化序列,因而部分标准C库功能得不到支持。

到现在我们知道了程序的启动流程应该是:复位->设置堆栈->执行Reset_Handler->SystemInit(可以没有)->__main(重定位RW-data数据段、清楚ZI段)->进入真正的main函数。

可以看到,我们自己的启动函数缺少了SystemInit__main两部分,对于SystemInit是配置时钟的,如果不初始化,芯片默认使用HSI-8MHz的时钟。所以不是必须的;但是数据段的重定位是必须的,所以我们要加上这部分。

三、重定位

是否需要重定位,根据数据的加载地址和链接地址确定。若加载地址和链接地址相等,则不需要重定位,比如保存在ROM中的RO-data。

否则就需要重定位,比如RW-data。BSS(ZI)段:不需要重定位,在可执行文件中没有他,只需要在把他对应的空间清零即可。

对于重定位,还需要知道两个概念:位置无关码、位置有关码。

3.1 位置无关码

该段代码无论放在内存的哪个地址,都能正确运行。因为他使用程序当前运行的PC值进行相对跳转,PC=PC+offset,没有使用绝对地址,都是相对地址,无论代码在哪,总能达到指令的正常目的,也就是说这段代码扔在任何位置都可以运行。类似于文件的相对路径,可以把程序放在任何文件夹下面,编辑器均可以根据工程文件路径找到其他每一个文件。

例如:

bl main

3.2 位置有关码

它的地址与代码处于的位置相关,是绝对地址。如PC=0x08000000。他不依赖当前PC值,而是直接把要跳转的地址赋值给PC。这样赋给PC的地址里面,必须有正确的指令才能正常运行,否则会出现各种错误。类似绝对路径,一旦文件夹发生变动,基本上就是定位不到具体的文件了。

例如:

ldr pc, =main    

3.3 重定位实质

重定位的实质就是数据拷贝,把加载地址中的数据搬运到链接地址中去。

对于移动数据,我们要知道三个重要因素:、

1、源:数据来源,也就是加载地址中的数据。

2、目的:数据最终要保存的地址,也就是链接地址。

3、长度:要搬运的数据的长度。

在keil中,用散列文件描述以上三个方面。

3.4 散列文件

在编译过程中有多个.o文件,而最后生成的只是一个可执行文件,那么这些文件要怎么以什么方式生成一个文件呢----链接,在Keil-MDK下就是使用散列文件来指导链接的。

在这里插入图片描述

如上图所示,进行设置,然后重新编译,就会在Objects文件夹中得到一个.sct文件,他就是散列文件。

在这里插入图片描述

打开文件,可以看到下图中的代码。

在这里插入图片描述

下面我们分析一下这个散列文件中的代码。

首先看一下keil中散列文件的基础知识。

在这里插入图片描述

根据上图操作,打开文档找到第八章Scatter File Syntax。

在这里插入图片描述

根据上图可知,一个散点文件包含一个或多个加载域, 一个加载域可以包含一个或多个执行域,每一个执行域包含一个或多个Input section。 加载域中描述二进制文件中一共有哪些东西。

加载域语法

在这里插入图片描述

执行域分布:

在这里插入图片描述

.sct文件:
在这里插入图片描述

LR_IROM1 0x08000000 0x00080000  {;加载域地址从0x08000000开始,大小是0x00080000
  ER_IROM1 0x08000000 0x00080000  {  ; 执行域ER_IROM1的链接地址是0x08000000,与加载地址相同,大小也相同,所以他不需要重定位
   *.o (RESET, +First)	;所有的.o文件里的RESET段抽取出来放在最开始的位置,一般只有启动文件中有RESET段
   *(InRoot$$Sections)	;所有的文件包括库,keil添加的可执行文件,看不到源码,可以去掉
   .ANY (+RO)			;等同于*,优先级比*低,这里表示所有的只读数据段-RO
   .ANY (+XO)			;这里表示所有的只可执行段
  }
  RW_IRAM1 0x20000000 0x00010000  {  ; 执行域RW_IRAM1的链接地址是0x20000000,大小是0x00010000,他需要重定位
   .ANY (+RW +ZI)		;所有的文件的可读可写数据段和ZI段,但是ZI段并不会存在bin可执行文件里。
  }
}

上面两个执行域包含了整个工程的所有信息,两个执行域,一个把内容存放在ROM,一个把内存放在了RAM。由上面也看到了,RO-data保存在了ROM上,RW-data和ZI段放在了RAM。

实际上,对于在STM32F103这类资源紧缺的单片机芯片中:

1、代码段保存在Flash上,直接在Flash上运行(当然也可以重定位到内存里)。

2、数据段暂时先保存在Flash上,然后在使用前被复制到内存里(只读数据段不复制)。

3.4.1 确定三要素

根据上面的内容可以分析出执行域的源和目的。

目的
ER_IROM10x0800 00000x0800 0000
RW_IRAM1紧随ER_IROM1之后0x2000 0000

上面的内容是根据已知的文件生成的,但是对于任意的、未知的执行域来说,如何查找呢?

还是看上面的keil手册6.3节。

在这里插入图片描述

在这里插入图片描述

上面知道了三要素:

1、目的:Image$$region_name$$Base

2、长度:Image$$region_name$$Length

3、源:Load$$region_name$$Base

3.5 重定位代码

知道数据传输三要素之后,我们就可以自己编写重定位代码了。注意:重定位是在调用main函数之前。

先写一个数据拷贝函数memory_copy

void memory_copy(void * dest,void * src,unsigned int len)
{
	unsigned char * pcDest;
	unsigned char * pcSrc;
	while(len--)
	{
		*pcDest = *pcSrc;
		pcSrc++;
		pcDest++;
	}
}

3.5.1 RW-data重定位

ldr r0, = |Image$$RW_IRAM1$$Base|
ldr r1, = |Load$$RW_IRAM1$$Base|
ldr r2, = |Image$$RW_IRAM1$$Length|
bl memory_copy

重定位之后,再次执行之前的实验代码:

在这里插入图片描述

可以看到,全局变量Temp1已经可以正常打印出来了。

打开反汇编文件,查看Temp1和Temp2的地址。可以看到,Temp1的地址是0x2000 0000,Temp2位于RO-data段,地址是0x0800 0164。

在这里插入图片描述

3.5.2 ZI段清零

ZI段不被烧录到ROM中,也不会放入bin文件中,否则也太浪费空间了。在使用ZI段里的变量之前,把ZI段所占据的内存清零就可以了。

查看散列文件可知,ZI段在可执行域RW_IRAM1中。

在这里插入图片描述

根据手册可以看到ZI段的基地址和长度。

在这里插入图片描述

知道了ZI段的基地址和长度,我们就可以对这块空间进行清除了。

ZI段保存的是初始值为0或没有初始值的全局变量和静态变量。

int Temp1[16] = {0};
int Temp2[16];
int mymain()
{	
	static int Temp3[16] = {0};
	static int Temp4[16];
	usart_init();
	put_s_hex("Temp1 is: ",Temp1[0]);
	put_s_hex("Temp2 is: ",Temp2[0]);
	put_s_hex("Temp3 is: ",Temp3[0]);
	put_s_hex("Temp4 is: ",Temp4[0]);
	while(1);
}

输出结果:

Temp1 is: 0x6BF87F8B
Temp2 is: 0x126577A9
Temp3 is: 0xC82AA8DC
Temp4 is: 0xE5EC5EF7

可以看到,上面四个变量的值是随机的,打开keil生成的.map文件(Listings目录下),可以看到,他们都是位于BSS段。

在这里插入图片描述

由于没有进行RAM中BSS段的清零,所以他们初值是随机的。

注意:若本属于BSS段的数据比较少,编译器会进行优化,把数据放在.data段。

int Temp1 = 0;
int Temp2;
int mymain()
{	
	static int Temp3 = 0;
	static int Temp4;
	usart_init();
	put_s_hex("Temp1 is: ",Temp1);
	put_s_hex("Temp2 is: ",Temp2);
	put_s_hex("Temp3 is: ",Temp3);
	put_s_hex("Temp4 is: ",Temp4);
	while(1);
}

在这里插入图片描述

ZI段清零:

还是在启动文件中添加ZI段清零的代码:

IMPORT |Image$$RW_IRAM1$$ZI$$Base|	;ZI段基地址
IMPORT |Image$$RW_IRAM1$$ZI$$Length|	;ZI段长度
IMPORT	memory_set

ldr r0, = |Image$$RW_IRAM1$$ZI$$Base|
mov r1, 0
ldr r2, = |Image$$RW_IRAM1$$ZI$$Length|
bl memory_set

输出结果:

Temp1 is: 0x00000000
Temp2 is: 0x00000000
Temp3 is: 0x00000000
Temp4 is: 0x00000000

可以看到,ZI段已经被清零了。

3.5.3 代码重定位

一般情况下,代码都是下载到Flash中,但是有时候为了提高执行速度,也会把代码拷贝到RAM中运行。

首先设置Keil,使用自定义的散列文件。

在这里插入图片描述

然后修改Objects文件夹下的.sct文件。

LR_IROM1 0x08000000 0x00080000  {    ; load region size_region
  ER_IROM1 0x20000000  {  ; load address = execution address
   *.o (RESET, +First)
   *(InRoot$$Sections)
   .ANY (+RO)
  }
  RW_IRAM1 + 0  {  ; RW data,紧跟着ER_IROM1后面
   .ANY (+RW +ZI)
  }
}

这样,代码的链接地址就处于RAM中了。

在汇编中,使用不同的指令可以在Flash或RAM中跳转函数。

LDR PC,=main	;使用链接地址,运行RAM中代码
BL main	;运行Flash中代码

所以运行RAM中的代码,要使用第一种跳转方法,同时必须进行代码重定位,否则会跟RW-data重定位出现的问题一样,运行函数的之前,函数的链接地址中没有相应的指令,程序就会崩溃。

代码段的链接地址(基地址)、长度,使用下面的符号获得:

在这里插入图片描述

代码段的加载地址,使用下面的符号获得:

在这里插入图片描述

代码重定位的汇编代码与之前的RW-data重定位类似,

LDR R0, = |Image$$ER_IROM1$$Base|
LDR R1, = |Load$$ER_IROM1$$Base|  
LDR R2, = |Image$$ER_IROM1$$Length|
BL memory_copy
LDR  R0, =mymain
BLX  R0

注意注意:要把启动文件的第二个指令的地址改一下,否则程序上电后第二步执行Reset_Handler时,是去RAM中执行,但此时RAM中并没有相应的Reset_Handler代码,所以要先执行ROM中的Reset_Handler,就是地址0x08000008。

__Vectors       DCD     0                  
                ;DCD     Reset_Handler     ;直接跳转到RAM中执行,但是此时RAM中并没有相应的指令,所以程序崩溃
				DCD      0x08000009        ;跳转到0x08000008执行,执行Flash中的Reset_Handler

3.6 重定位之前的代码怎么运行

重定位之前的代码是使用位置无关码写的,关于位置无关码和位置有关码的区别之前介绍过,这里不再讲解。

使用位置无关码,无论程序在哪里,都可以被CPU正确访问到。

1、只使用相对跳转指令:B、BL

2、不能用绝对跳转指令:

LDR R0, =main
BLX R0

3、不访问全局变量、静态变量、字符串、数组

4、重定位完后,使用绝对跳转指令跳转到XXX函数的链接地址去

BL main         ; bl相对跳转,程序仍在Flash上运行
LDR R0, =main   ; 绝对跳转,跳到链接地址去,就是跳去内存里执行
BLX R0

四、C语言编写重定位函数

在汇编中,重定位需要的变量用|Image$$region_name$$Base|这样的格式表示。

在C语言中,对于一个变量,可以用extern关键字声明为外部变量,然后直接引用即可。

对于变量,引用的时候要加上&:

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

对于数组,数组名就相当于地址,所以不用加&:

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

现在把启动文件中的重定位代码删除,用C语言编写一个重定位函数relocate_c,然后在启动文件中调用。

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;	
void relocate_c(void)
{
	/*代码段重定位*/
    memcpy(&Image$$ER_IROM1$$Base, &Load$$ER_IROM1$$Base, &Image$$ER_IROM1$$Length);	
    /*RW-data重定位*/
    memcpy(&Image$$RW_IRAM1$$Base, &Load$$RW_IRAM1$$Base, &Image$$RW_IRAM1$$Length);
    /*清除ZI段*/
    memset(&Image$$RW_IRAM1$$ZI$$Base, 0, &Image$$RW_IRAM1$$ZI$$Length);
}

五、程序是否需要加载到RAM中?

4.1 单片机

1、把程序从flash加载到RAM需要bootloader,(其实程序也可以直接下载到RAM中运行,只不过重启程序就没有了。

2、单片机RAM较小程序太多无法加载全部程序。

3、单片机执行分三个步骤:取指令,分析指令,执行指令。取指令任务是根据PC值从地址总线上读出指令。虽然从RAM取指令速度远大于ROM,但单片机自身运行速度不高,所以程序放在哪里没有太大影响。

4.2 运行Linux系统

linux程序比较大,很少用norflash储存程序,而是用nandFlash或sd储存,这类储存不符合CPU 的指令译码执行要求。

运行linux系统的cpu,其运行频率非常高,远大于ROM读写速度,从ROM取指会严重影响速度。故系统会把程序拷贝到RAM执行。

所有运行Linux系统时程序必须加载到RAM

六、参考文章

(7条消息) STM32裸机开发(6) — Keil-MDK下散列文件的分析_Willliam_william的博客-CSDN博客

(7条消息) STM32启动详细流程之__main_非常规自我实现的博客-CSDN博客

(7条消息) STM32代码烧写到哪里去了?是ROM?还是RAM?还是flash?它们都是啥?代码具体占了多少空间?超没超芯片的范围?KEI里如何设置芯片flash、RAM可用大小呢?_越过山丘呀的博客-CSDN博客

欢迎阅读《MDK的编译过程及文件类型全解》文档-by 秉火 — FLASH 1.0 文档 (flash-rtd.readthedocs.io)

(7条消息) 【IoT】STM32 启动代码 __main 与用户主程序 main() 的区别_产品人卫朋的博客-CSDN博客

(7条消息) 什么是重定位?为什么需要重定位?_cherisegege的博客-CSDN博客_重定位是什么意思

stm32中存在rom中的全局变量初始值是怎么copy到RAM区? (amobbs.com 阿莫电子论坛)

(7条消息) MDK __main()代码执行分析_TS_up的博客-CSDN博客___main

  • 6
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
ARM体系架构编程》是杜春雷编写的一本关于ARM体系架构编程的教材。ARM处理器是一种广泛应用于嵌入式系统和移动设备的处理器架构,它具有低功耗、高性能和灵活性等特点,得到了广泛的应用和认可。 该书主要介绍了ARM体系结构的基本概念、指令集及其编程模型、流水线和缓存等方面的内容。首先,书中详细介绍了ARM处理器的发展历程以及其基本原理和体系结构的组成部分,包括寄存器、指令集、执行状态以及内存管理等。其次,书中介绍了ARM指令集的特点和编程模型,包括数据处理、访存指令、分支和跳转指令等。此外,书中还讲解了ARM处理器的流水线结构和缓存机制,帮助读者理解和优化ARM程序的性能。 《ARM体系架构编程》书写简练明了,通俗易懂,适合初学者入门。同时,书中也涵盖了一些高级主题,如ARM处理器的异常处理和浮点运算等,适合具有一定基础知识的读者进一步深入学习。此外,书中还提供了丰富的示例代码和实例,帮助读者更好地理解和应用所学知识。 总而言之,《ARM体系架构编程》是一本权威、全面且易于理解的ARM处理器教材,对于学习和应用ARM体系结构的读者来说是一本很好的参考书。无论是嵌入式系统开发者还是移动设备开发者,都可以通过这本书更好地了解和使用ARM处理器。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值