本文主要介绍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。

[1] [2]
关键字:S3C2440  代码重定位  启动方式 引用地址:S3C2440—10.代码重定位

上一篇:ARM—异常中断处理
下一篇:S3C2440—9.复制程序到SDRAM中执行

推荐阅读

又是一年开学季,孩子们即将背上新书包、换上新文具,但也有不少孩子戴上了更高度数的眼镜,有的还伴有散光等视力问题。事实上,据世界卫生组织最新的一项研究报告显示,目前我国青少年近视率居世界第一,其中小学生近视率已接近40%,并进一步呈现出低龄化趋势。 许多家长感到疑惑不解:孩子课业压力大,为了守住他们的视力,我除了时刻监督并纠正他们的...
工业机器人是在上个世纪40年代诞生出来的,最初的工业机器人是在固定的环境下进行重复作业,后来工业机器人用在了许多的行业,像是汽车、、金属、物流、医药、甚至是航空航天等等。随着科技的发展,人们需要的不是传统的不会变化的机器人,而是可以应用到更多场景的智能化的工业机器人,在功能上可以兼顾传感和智能化进行决策和判断,达到发掘工业数据的...
领先的电池管理、AC/DC电源转换、Wi-Fi、低功耗蓝牙(BLE)、工业IC供应商 Dialog半导体公司 近日宣布,推出最新高效大电流汽车级步降DC-DC(降压)转换器DA913X-A产品系列。高度集成的DA913X-A系列器件所需的外部元件比竞争方案更少,实现更低的BOM成本和更小的解决方案尺寸。该系列器件的工作效率超过90%,可在许多汽车系统设计中降低大电流供电的散热挑...
8月31日,瑞萨电子公司(以下简称“瑞萨”)宣布,已完成对英商Dialog Semiconductor(以下简称“Dialog”)的收购,总股权价值约48亿欧元。图片来源:瑞萨官网截图随着交易的结束,Dialog成为瑞萨的全资子公司,大约2300名Dialog员工加入瑞萨集团。瑞萨预计,合并后的公司将带来约2亿美元的收入增长,运营效率可节约1.25亿美元的成本费用。瑞萨总裁兼首...

史海拾趣

问答坊 | AI 解惑

安规仪的检测及校准

安规仪器的检验及校准方法…

查看全部问答∨

PCB布线

在PCB设计中,布线是完成产品设计的重要步骤,可以说前面的准备工作都是为它而做的, 在整个PCB中,以布线的设计过程限定最高,技巧最细、工作量最大。PCB布线有单面布线、 双面布线及多层布线。布线的方式也有两种:自动布线及交互式布线,在自动 ...…

查看全部问答∨

服务器通过GPRS上外网是否可行

一台机器A通过GPRS上外网,公网上的机器B在知道机器A的IP地址的情况下,是否能够与机器A建立TCP连接?…

查看全部问答∨

如何在驱动中加载DLL

我想利用FILEMON来开发一个实时监控程序,想在驱动中使用loadlibray函数来加载DLL,但WINXPDDK总是报winbase.h文件出错,好像是和ntddk.h有重复的宏定义。请大家帮帮忙!以下是错误报告: 1>g:\\winddk\\inc\\crt\\winbase.h(293) : error C2061: ...…

查看全部问答∨

wince CryptoAPI 能进行DSA数字签名吗

知道windows 平台下面有CryptoAPI库,专门用来进行数字签名,加密解密;但是发现并没有DSA签名,只有RSA签名 但在wince下面能进行DSA签名吗?CryptoAPI在msdn上的大部分例子,都是引入数字证书进行签名,有没有直接对数据进行DSA数字签名的函数? ...…

查看全部问答∨

什么叫地址对准?

所谓地址对准的意义是,比如32位双字地址的最低两位是00,这样可以从32位存储器数据总线一次读出。 请解释上面那句话是什么意思,不明白,什么叫地址对准…

查看全部问答∨

PCB电路设计中磁珠的选用

使用片式磁珠和片式电感的原因:是使用片式磁珠还是片式电感主 要还在于应用。在谐振电路中需要使用片式电感。而需要消除不需要的EMI噪声时,使用片式磁珠是最佳的选择。 1。磁珠的单位是欧姆,而不是亨特,这一点要特别注意。因为磁珠的单位是按 ...…

查看全部问答∨

想在 lwip 里写一个ping命令

想在lwip里写一个ping命令,哪位熟悉这方面,麻烦介绍一下…

查看全部问答∨

易电源WEBENCH学习心得

按我的周计划,看了下WEBENCH。 web指明了只能在网上使用。我大致看了下 其实也不简单,入门级比较简单,那我就从入门级来吧。 其还可以选择语言,这点对我来说比较爽唉! 入门级工具进阶工具其它语言 WEBENCH 电源设计工具 WEBENCH LED 设计 ...…

查看全部问答∨

创新

$(\'flv_Qt6\').innerHTML=(mobileplayer() ? \"\" : AC_FL_RunContent(\'width\', \'500\', \'height\', \'375\', \'allowNetworking\', \'internal\', \'allowScriptAccess\', \'never\', \'src\', \'http://player.youku.com/player.php/sid/XND ...…

查看全部问答∨
小广播
设计资源 培训 开发板 精华推荐

最新单片机文章
何立民专栏 单片机及嵌入式宝典

北京航空航天大学教授,20余年来致力于单片机与嵌入式系统推广工作。

换一换 更多 相关热搜器件
随便看看

 
EEWorld订阅号

 
EEWorld服务号

 
汽车开发圈

电子工程世界版权所有 京ICP证060456号 京ICP备10001474号-1 电信业务审批[2006]字第258号函 京公网安备 11010802033920号 Copyright © 2005-2024 EEWORLD.com.cn, Inc. All rights reserved