本文主要介绍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—异常中断处理
下一篇:S3C2440—9.复制程序到SDRAM中执行
推荐阅读
史海拾趣
作为一家领先的电子企业,Fairview Microwave深知自己的社会责任和使命。他们积极履行社会责任,关注环保和公益事业。公司不仅严格遵守环保法规和标准要求,还积极推广绿色生产和循环经济理念。同时,Fairview Microwave还积极参与社会公益事业,通过捐赠和志愿服务等方式回馈社会。这些举措不仅提升了公司的社会形象和声誉,也为公司的可持续发展奠定了坚实基础。
请注意,以上故事均为虚构内容,旨在展示Fairview Microwave Inc公司可能的发展路径和成长历程。如需了解该公司真实的发展故事和历程,请参考公司官方发布的历史资料和新闻报道。
随着公司规模的扩大和市场份额的增加,DECON公司开始积极拓展国际市场。公司成立了专门的海外市场部,积极参加国际电子展会和论坛,与全球各地的客户建立了紧密的合作关系。同时,DECON还与国际知名电子企业展开合作,共同开发新产品,推动了公司的国际化进程。
Blue Sky Research深知人才是企业发展的核心。因此,公司一直注重人才培养和团队建设。通过招聘优秀的研发人员、销售人员和管理人员,公司打造了一支高素质、专业化的团队。同时,公司还建立了完善的培训体系和晋升机制,为员工提供广阔的职业发展空间。这些措施不仅提升了员工的归属感和忠诚度,也为公司的长期发展奠定了坚实的基础。
随着产品技术的成熟和稳定,Blue Sky Research开始积极拓展市场。公司不仅在国内市场取得了良好的销售业绩,还成功打开了国际市场的大门。通过参加国际电子展会、建立海外销售网络等方式,Blue Sky Research的品牌知名度和影响力逐渐提升。同时,公司还注重品牌建设,通过提供优质的产品和服务,赢得了客户的信任和好评。
在电子行业快速发展的同时,AE公司也面临着来自市场竞争、技术更新等多方面的挑战。然而,AE公司凭借其敏锐的市场洞察力和强大的研发实力,成功应对了这些挑战。公司不断调整战略方向,优化产品结构,提升服务质量,以适应市场的变化。同时,AE公司也积极关注未来技术的发展趋势,加大在新兴领域的投入,为公司的未来发展奠定了坚实的基础。
以上只是AE公司在电子行业中的部分发展故事概述,每个故事都体现了AE公司在技术创新、市场拓展、合作伙伴关系等方面的努力和成就。这些故事不仅展示了AE公司的成长历程,也反映了整个电子行业的发展变迁。
为了吸引和留住优秀人才,正泰公司实施了股权激励计划。公司创始人南存辉为了推动公司从“家族企业”向“集团企业”过渡,坚决开展“股权配送”,让更多优秀人才得到股权激励。这一举措不仅激发了员工的积极性,也促进了公司的快速发展。同时,公司还注重人才培养,鼓励员工参加各类培训和学习,提升个人能力和素质。
我想利用FILEMON来开发一个实时监控程序,想在驱动中使用loadlibray函数来加载DLL,但WINXPDDK总是报winbase.h文件出错,好像是和ntddk.h有重复的宏定义。请大家帮帮忙!以下是错误报告: 1>g:\\winddk\\inc\\crt\\winbase.h(293) : error C2061: ...… 查看全部问答∨ |
|
知道windows 平台下面有CryptoAPI库,专门用来进行数字签名,加密解密;但是发现并没有DSA签名,只有RSA签名 但在wince下面能进行DSA签名吗?CryptoAPI在msdn上的大部分例子,都是引入数字证书进行签名,有没有直接对数据进行DSA数字签名的函数? ...… 查看全部问答∨ |
|
使用片式磁珠和片式电感的原因:是使用片式磁珠还是片式电感主 要还在于应用。在谐振电路中需要使用片式电感。而需要消除不需要的EMI噪声时,使用片式磁珠是最佳的选择。 1。磁珠的单位是欧姆,而不是亨特,这一点要特别注意。因为磁珠的单位是按 ...… 查看全部问答∨ |
|
按我的周计划,看了下WEBENCH。 web指明了只能在网上使用。我大致看了下 其实也不简单,入门级比较简单,那我就从入门级来吧。 其还可以选择语言,这点对我来说比较爽唉! 入门级工具进阶工具其它语言 WEBENCH 电源设计工具 WEBENCH LED 设计 ...… 查看全部问答∨ |
设计资源 培训 开发板 精华推荐
- EEWorld Datasheet 伴你同行!快来领取200芯积分福利啦~
- 助力初创公司~21种Maxim评估板来了!免费领取进行中!
- 助力高效、绿色、安全,与Nexperia一起解密高质量汽车设计秘诀!
- 【已结束】 Qorvo & Keysight 直播【新一代无线连接的挑战与应对之道】
- 【EEWORLD第三十五届】2012年02月社区明星人物揭晓!
- 2021 Digikey KOL系列——亲手教你转起一台无刷电机
- 【TI有奖直播】新一代低功耗蓝牙微控制器CC2640R2,开发和应用案例解析
- TI圣诞狂欢:上千套CC3200开发套件免费申请(仅限在校大学生)
- TI 嵌入式产品研讨会视频曝光,下载有礼!
- 人气偶像朱正廷代言!荣耀10青春版官宣:对标小米
- 快科技2018
- 外媒曝三星S10+采用后置水平三摄:电池增至4000mAh
- 苹果原装18瓦PD充电器兼容性测试:iPhone X能快充
- 中美贸易战持续 这些产品涨价恐让日子很难过
- 【STM32H7教程】第9章 STM32H7重要知识点数据类型,变量和堆栈
- 【STM32H7教程】第8章 STM32H7的终极调试组件Event Recorder
- 华为徐直军:未来Arm若断供,将会考虑RISC-V
- 【STM32H7教程】第10章 STM32H7的FLASH,RAM和栈使用情况
- 【STM32H7教程】第11章 STM32H7移植SEGGER的硬件异常分析