历史上的今天
今天是:2024年09月01日(星期日)
2021年09月01日 | S3C2440—10.代码重定位
2021-09-01 来源:eefocus
本文主要介绍ARM裸机代码重定位的相关知识,以及重定位的实现过程。
下面将由ARM裸机(S3C2440)的启动方式开始分析,引入段的概念,随后介绍链接脚本的使用以及代码重定位的操作,首先会使用汇编语言验证代码重定位的可行性,最后将使用C语言实现代码重定位。
一.启动方式
S3C2440的启动方式有俩种:
NOR FLASH启动
NAND FLASH启动
说起ARM裸机的启动方式,就是将程序的bin文件烧写在ARM的存储空间中,ARM从这些地址中读取指令到CPU中执行,需要数据的时候去数据的存储地址取数据。说白了启动方式的不同就是bin文件烧写地址的不同,可以烧在NOR FLASH中,也可以烧在NAND FLASH中,俩种FLASH本质上都是是存储程序的,那为什么要区别呢?因为俩种FALSH的性能不一样,具体的不同在下面分析。
1.1 NAND FLASH 启动
下图是S3C2440的内存关系框图:CPU(存储控制器忽略)、SRAM、NAND FLASH控制器,SDRAM、NOR FLASH、NAND FLASH。

可以看出CPU可以直接对地址进行读写的外设有:SRAM、NOR FLASH、SDRAM等,不可以直接对NAND FLASH进行读写。
要知道,CPU直接对地址进行读写意味着CPU可以直接去执行此地址中的机器代码,所以以NAND FLASH方式启动的时候,bin文件虽然烧写在NAND FLASH中,但是CPU无法直接去执行程序,所以硬件会在自动将NAND FLASH中的前4KB代码拷贝在SRAM中(SRAM的大小为4KB),CPU去SRAM中执行代码。
简单来说,CPU无法直接从NAND FLASH中取代码来运行:
1.上电后,硬件自动把NAND FLASH的前4K内容拷贝到SRAM中。
2.CPU从0地址开始运行代码(NAND FLASH启动时SRAM的地址为0x00000000)。
当程序的大小超过4KB时,SRAM就不足以放下整个程序,这时候就要用到代码重定位了,简单来讲就是由程序自身将程序的代码重新拷贝到SDRAM中去执行程序,接下来我们将仔细讲解代码重定位。
1.2 NOR FLASH 启动
俩种启动方式的对比:
NAND FLASH虽然内存大,但是CPU不可以直接去读写,所以需要将前4KB代码拷贝到SRAM中执行。
NOR FLASH可以被CPU直接读写,意味着代码可以直接在NOR FLASH中运行,而且NOR FLASH大小为2MB内存足够大,但是写入NOR FLASH中的数据不可以被修改(写进去的数据不可通过程序的代码改变),这样一来,如果有变量存储在NOR FLASH中,那岂不是成了常量了。
简单来说,虽然可以在NOR FLASH执行程序,但是其中的变量却不可以被改变,所以我们有俩种解决方法:
1.将整个程序重定位到SDRAM中执行
2.只将NOR FLASH中的变量重定位到SDRAM中(使变量可以改变)
注意:
并不是程序中的所有变量都随程序放在NOR FLASH中,局部变量是放在栈中的,而栈指向SRAM,所以局部变量不存在上述的情况。然鹅,全局变量是包含在bin文件中烧写在NOR FLASH中,所以这样看来全局变量是不可以被修改的,需要将全局变量重定位到SDRAM中,这涉及到段的概念,下面会讲。
以下的讨论是以NOR FLASH启动作为基础的,因为NAND FLASH启动只要代码小于4KB就可以不用重定位,NOR FLASH启动时,只要程序中有全局变量,就要进行重定位,所以重定位的使用频率较高。
二. 段的概念
上面说了,程序的局部变量存储在栈(SRAM)中,全局变量跟着代码包含在bin中烧写在NOR FLASH中,而且上面说了要把全局变量单独重定位在SDRAM中,所以我们知道,程序的bin文件是分段的:
.text:代码段,存放代码
.data:数据段,已初始化的全局变量
.rodata:只读数据段,const修饰的全局变量,和代码段一起写在bin里,值不需要改变
.bss:初值为0以及无初值的全局变量,不保存在bin中( 并不给该段的数据分配空间,只是记录数据所需空间的大小 bss段的大小从可执行文件中得到 ,然后链接器得到这个大小的内存块,紧跟在数据段后面 )
.commen:注释段,不保存在bin中
2.1 重定位数据段
以前我们通过Makefile中的链接指令来决定代码段的位置:
arm-linux-ld -Ttext 0 start.o main.o -o relocation.elf
指令的意思是:通过链接指令,将start.o、 main.o俩个文件链接在一起生成relocation.elf文件,且代码段.text存放在0地址。
这里的-Ttext 0所指的地址是代码的运行地址,即CPU运行程序时,就去运行地址中取指令、数据。
注意:
这里只是-Ttext 0,虽然只确定了代码段的位置,但其他段的存储地址都紧跟在.text的后面
现在我们将数据段的存储地址添加进去,将数据段重定位到SDRAM(0x30000000)中:
arm-linux-ld -Ttext 0 -Tdata 0X30000000 start.o main.o -o relocation.elf
意思为将代码段放在0地址,将数据段放在0x30000000地址中,也就是SDRAM。(使用SDRAM前要初始化)
2.2 加载地址的引出
经过编译后发现,生成的bin文件竟然大小为800多MB,约为0x30000001Byte,可以看出这是从代码段到数据段的所有的内存大小,代码段和数据段之间有一个0x30000000多Byte的内存空间,原因是-Ttext 0 -Tdata 0X30000000间接确定.text和.data在bin文件中的地址,即确定加载地址。
加载地址:arm-linux-ld -Ttext 0 -Tdata 0X30000000 start.o main.o -o relocation.elf中确定的是.text和.data的运行地址,Makefile中默认加载地址=运行地址,加载地址是.text和.data在bin文件中的分布地址,所以默认.data段在bin文件中的存储地址就为0x30000000。
由于bin文件中的段的加载地址,所以.data加载地址的大小影响了bin文件的大小,导致bin文件产生了0x30000000的地址。这样的bin文件800多M,想都不要想了,肯定是行不通的!
看来Makefile中修改链接指令中的地址只能影响运行地址,默认运行地址=加载地址,而加载地址又影响了bin文件的大小,所以为了不让加载地址影响bin文件的大小,我们要找出另一种方法来进行重定位!!!这就引出了链接脚本!!!
三.链接脚本
参考文章:GUN ld
3.1 链接脚本的引入
首先要知道链接脚本的主要作业是链接,有输入文件,有输出文件,将输入文件按照配置链接成为输出文件,一般输入文件是.o文件,输出为.elf文件。
链接脚本的主要格式为:
SECTIONS
{
...
secname start BLOCK(align)(NOLOAD) : AT ( ldadr )
{ contents}
...
}
解释如下:
secname:描述输出文件的段,比如.text、.data
start:规定输出段的运行地址,即规定CPU从哪个地址去取指令、数据
BLOCK(align):地址对齐,一般4Byte对齐,ALIGN(4)
AT(ldadr):段在输出文件中的物理地址,如果没有使用AT(ldadr),加载地址=start
contents:描述输入文件的段从哪里来,一般来自全部输入文件的段,比如*(.data)
先用链接脚本的方法实现上面那个Makefile的链接指令:
arm-linux-ld -Ttext 0 -Tdata 0X30000000 start.o main.o -o relocation.elf
建立链接脚本文件:relocation.lds
SECTIONS{
.text 0 : {*(.text)}
.rodata : {*(.rodata)}
.data 0x30000000 : {*(.data)}
.bss : {*(.bss) *(.COMMON)}
}
在Make file中使用relocation.lds 进行链接:
arm-linux-ld -T relocation.lds start.o uart.o main.o -o relocation.elf
得到的bin文件大小为:0x30000001Byte,也证实了上面的分析。

3.2 链接脚本的正确打开方法
如果链接脚本仅仅是上面那种使用,那就和Makefile的链接命令没有区别了,下面正式介绍链接脚本的正确打开方法:
现在修改relocation.lds:
SECTIONS{
.text 0 : {*(.text)}
.rodata : {*(.rodata)}
.data 0x30000000 : AT0x800 {*(.data)}
.bss : {*(.bss) *(.COMMON)}
}
值得注意的是:
.data 0x30000000 : AT0x800 {*(.data)}
将数据段,data的运行地址放在0x30000000,这代表CPU去0x30000000的地址去取.data,也就是SDRAM的地址;然后.data的加载地址则是0x800,即.data实际在bin文件中的位置是0x800,这样的话现在bin文件的大小为:0x801Byte(只定义了一个字符全局变量)

康起来好像没毛病,运行一下,发现此时的运行结果还是输出乱码!
原因是.data 加载地址是0x800,但是运行地址是0x30000000,此时还没有将数据段拷贝到SDRAM(0x30000000),所以CPU按照,data的运行地址直接去取数据,肯定是乱码!
那要怎么办才可以把 .data 拷贝到运行地址中呢,这才涉及到代码重定位!说白了就是程序自己把.data从加载地址复制到运行地址!
3.3 链接脚本测试
重定位:start.S中,在进入main之前进行重定位,将0x800的内容复制到0x30000000(前提得初始化SDRAM)
修改relocation.lds来控制链接:
SECTIONS{
.text 0 : {*(.text)}
.rodata : {*(.rodata)}
.data 0x30000000 : AT
{
data_load_addr = LOADADDR(.data);
data_start = .;
*(.data)
data_end = .;
}
bss_start = .;
.bss :
{
*(.bss)
*(.COMMON)
}
bss_end = .;
}
. 代表当前地址
data_load_addr:.data段在bin文件中的源地址,即加载地址
data_start:是重定位地址,即运行时的地址
data_end:是结束地址
重定位就是将.data从data_load_addr地址拷贝到data_start地址
3.4 elf文件
链接脚本生成的文件是elf文件
elf文件里含有这些地址信息,生成的bin文件中已经不含有地址信息了
1.链接得到elf文件,含有地址信息:加载地址(AT指定)
2.使用加载器把elf文件解析一下,写入内存的加载地址:load addr
3.运行程序
4.若加载地址不是运行地址,程序本身要进行重定位
核心:程序运行时应该位于运行地址(或者说是relocate addr、链接地址)
3.5 bin文件
elf文件生成bin文件,bin文件可以直接烧写在ARM中
1.elf生成bin文件
2.烧入裸机后(裸机没有加载器)硬件机制来启动
3.若加载地址位置不等于运行地址,程序本身实现重定位
四.重定位
重定位就是将.data从data_load_addr地址拷贝到data_start地址,即从加载地址拷贝到运行地址。
重定位根据连接脚本中的变量来确定各段的加载地址和运行地址:
SECTIONS{
.text 0 : {*(.text)}
.rodata : {*(.rodata)}
.data 0x30000000 : AT
{
data_load_addr = LOADADDR(.data);
data_start = .;
*(.data)
data_end = .;
}
bss_start = .;
.bss :
{
*(.bss)
*(.COMMON)
}
bss_end = .;
}
4.1 start.S 重定位数据段
对数据段.data进行重定位,从加载地址拷贝到运行地址:
ldr r1, = data_load_addr
ldr r2, = data_start
ldr r3, = data_end
cpy:
ldrb r4, [r1]
strb r4, [r2]
add r1, r1, #1
add r2, r2, #1
cmp r2,r3
bne cpy ;等于
看出来是ldrb从NOR FLASH中读取1Byte,再strb写入SDRAM,因为NOR FLASH位宽16位,SDRAM是32位,所以在俩者之间拷贝数据会耗费CPU的,而且是以Byte为单位的。
假设拷贝16Byte的数据,则会访问16次NOR FLASH、访问16次SDRAM。
利用位宽优势进行改进:
我们可以使用ldr命令和str命令开拷贝程序,这样就是以32Bit即4Byte为单位进行读写,可以省很多事。
这样情况下,拷贝16Byte数据时,执行4次ldr和str命令,访问8次NOR FLASH、访问4次SDRAM
这样就充分利用了 NOR FLASH和SDRAM的位宽优势,在数据段量大的时候,改进的优势就会体现出来。
ldr r1, = data_load_addr
ldr r2, = data_start
ldr r3, = data_end
cpy:
ldr r4, [r1]
stb r4, [r2]
add r1, r1, #4
add r2, r2, #4
cmp r2,r3
ble cpy ;小于
这样的话,加载地址就得对齐了,以4Byte对齐
4.2 start.S 清零.bss段
.bss段存放的是:未初始化的全局变量和初始值为0的全局变量,实际bin文件中是不会存储.bss段的,所以要对.bss段清零,不然未初始化的全局变量会打印一些乱码,就是因为.bss所指空间不为零。
然鹅,在运行的过程中遇到问题,.data段的全局变量被清零了,原因是清零BSS段的时候把DATA段也清零了,原BSS段清零代码如下:
ldr r1, =bss_start
ldr r2, =bss_end
mov r3, #0
clean:
str r3, [r1]
add r1, #1
cmp r1, r2
bne clean
因为使用了str,str操作的单位是4Byte
比如BSS段的地址是30000002,这样我们就需要 str r3, [30000002],但是实际上str会4Byte对齐的情况下进行str命令,即实际上str r3, [30000000],这样一来就把.data段的数据也清零了一部分。
处理方法是:以四字节对齐进行清除!!!这就需要改进以下链接脚本了,因为只有链接脚本中的加载地址以4Byte对齐,才不会出现这种情况!
现在全部以4Byte为单位进行拷贝和清除,提高效率
4.3 链接脚本改进
修改链接脚本来解决:
SECTIONS{
.text 0 : {*(.text)}
.rodata : {*(.rodata)}
.data 0x30000000 : AT
{
data_load_addr = LOADADDR(.data);
. = ALIGN(4)
data_start = .;
*(.data)
data_end = .;
}
. = ALIGN(4)
bss_start = .;
.bss :
{
*(.bss)
*(.COMMON)
}
bss_end = .;
}
. = ALIGN(4):先将当前地址向4取整,然后将当前地址给bss_start,这样进行str命令就不会波及到其他段了。
4.4 C语言实现重定位
上面实现的代码重定位和BSS段清除都是基于汇编语言的,而且也是比较简单的汇编,这里以C语言实现这些操作。
可以利用C语言的函数实现之后,在start.S中bl这些函数,通过r0、r1、r2等向C函数传递参数。
C语言实现.data重定位需要三个条件:
加载地址
运行地址
长度
void copy2sdram( volatile unsigned int *src, volatile unsigned int *dest, unsigned int len )
{
unsigned int i=0;
while( i *dest++ = *src++; i += 4; } } 但是这样需要汇编向C函数传递参数,可以改进一下,不需要汇编传参。 需要去链接脚本里获取参数 可以在链接脚本首里加入 _code_start = 0 要从lds文件中获得_code_start,_bss_start /*要从lds文件中获得_code_start,_bss_start *然后从0地址把数据复制到_code_start */ void copy2sdram( void ) { extern int _code_start, _bss_start; /* 利用符号表获取加载地址 */ volatile unsigned int *dest = ( volatile unsigned int * )&_code_start; volatile unsigned int *end = ( volatile unsigned int * )&_bss_start; volatile unsigned int *src = ( volatile unsigned int * )0; //从0地址复制 while( dest *dest++ = *src++; } } 4.5 C语言实现清零.bss段 需要俩个条件: .bss的加载地址的起始 结束地址 void clean_bss( volatile unsigned int *start, volatile unsigned int *end ) { while( start <= end ) { *start++ = 0; } } 从链接脚本获取参数: /*从lds文件中获取_bss_start、_bss_end */ void clean_bss( void ) { extern int _bss_start, _bss_end; /* 利用符号表获取加载地址 */ volatile unsigned int *start = ( volatile unsigned int * )&_bss_start; volatile unsigned int *end = ( volatile unsigned int * )&_bss_end; while( start <= end ) { *start++ = 0; } } 4.6 符号表 要注意的几个点: 调用链接脚本里面的变量时要声明为外部变量extern 使用链接脚本里的变量时要加上取地址符号&(变量指段的地址) 汇编可以直接使用lds文件里面的变量。 借助符号表保存lds文件的变量,使用时加上&才可以得到变量的值 符号表: C程序中不保存lds文件中的变量,编译程序时,有一个symbol table(符号表),包含了变量的名称和地址。在本来放地址的地方可以放值,这样就可以使用符号表保存lds的变量,这里可以看作常量。使用的时候,常规变量是取地址来得到的,为了保持代码的一致,对于lds的变量取值,也使用取值操作得到变量的值,即volatile unsigned int *end = ( volatile unsigned int * )&_bss_end;,这些变量的值来自链接脚本,在链接的时候确定。.符号表只是在编译链接的时候辅助一下,最终不会存放在程序中的,所以符号表的大小无所谓。 五.位置无关码(相对跳转与绝对跳转) ARM启动过程分析: bin一开始是.text,紧接着是.rodata,然后是.data,bin文件烧在NOR FLASH上(从0地址开始),上电后从0地址开始运行。.text的前面一部分代码会把程序拷贝到SDRAM实现重定位(整个程序的重定位)。然后start.S中实现了cpy和clean。
上一篇:ARM—异常中断处理
史海拾趣
|
我的步骤是这样的: 1、将新字体库拷贝到windows字体目录下。字体名是Square721 Dm 2、 myFont1.CreateFont( 20, &nbs ...… 查看全部问答> |
|
我的毕业设计要求做嵌入式的开发,将一些专业参数的计算方法嵌入到GPS得RTK手簿(WINCE系统)中,这些参数的计算与原系统中软件功能无关,VC++太深,我怕来不及学习,想用C#进行开发,可看到很少有用C#做嵌入式的,而且听说不能做底层,迷茫中,希 ...… 查看全部问答> |
|
新手初次编写c#的wince程序,图片显示异常,请高人指点 问题描述: 简单实现一个按钮点后,pictureBox显示图片的问题,困绕了3天了,请高人指点一二,开发环境visual studio 2005 c# 智能程序 wince5 ,连接硬件上调试 代码实现:   ...… 查看全部问答> |
|
首先说一下提这个问题的原因: 我在做一个文件系统,做完后需要对这个系统的性能进行一下评测,比如,往硬盘写1M文件需要多少时间,但我对这个时间也没有概念,不知道多少时间算是比较优异的,因此,想看看windows的文件系统性能,再与我的进行对 ...… 查看全部问答> |
|
本教程为北京化工大学何宾教授亲自编写的培训教程,这个教程结合大量的图表对赛灵思ISE13.1工具的使用进行了讲解,ISE® 13设计套件是赛灵思最新推出的工具,主要针对Spartan®-6、Virtex®-6和7系列FPGA以及行业领先的容量高达200万个逻 ...… 查看全部问答> |
|
1、解决温饱问题。即考虑后路,在寻找喜欢的事过程中,前提必须是保证正常生活,如果确实觉得找到了喜欢的事,很兴奋,很想马上去从事,HOLD住,考虑下放弃眼前工作能维持几个月的经济,别悲剧了。 2、喜欢(适合)的事可以有很多。可能以前的经验积 ...… 查看全部问答> |




