S3C2440—10.代码重定位


本文主要介绍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<len )
    {
		*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<end )
    {
		*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。

注意:

反汇编文件中,b或者bl某个值,只是起方便产看的作用,并不是实际跳到这个地址的,但是我们可以在反汇编文件中根据这个地址来查看跳转到的部分是什么样子的,也就是说这个值只是反汇编文件中的位置参考值。实际中b或者bl跳转的地址由当前的地址和一个固定的地址偏移决定(实际中采用偏移地址)。

重定位之前不使用绝对地址

就像上面分析的一样,怎么写出与当前位置无关的代码,也就是可以在任何条件允许的内存中运行的代码:

1.使用相对跳转命令:b、bl

但是有一个问题:我们在NOR FLASH(SRAM)中的代码已经通过重定位拷贝在SDRAM中了,但是在start.S中使用的 bl main命令却跳转到了NOR FLASH(SRAM)中的main函数,没有跳转到SDRAM中的main函数,这是因为bl main使用了相对跳转命令,若想跳转到SDRAM中,就要使用绝对跳转命令:

ldr pc, =main

2.重定位之前不可以使用绝对地址,不可访问全局变量,因为全局变量是通过绝对地址来访问的

3.重定位之后才可以使用 ldr pc, =xxx来跳转到加载地址,即RunTimeAddr,若执意使用bl命令,则会跳转到NOR FLASH(SRAM)上

使用绝对地址后,程序在SDRAM中运行,相对FLASH上快了一些

重定位之前一定要初始化SDRAM,重定位之后,才使用绝对地址,因为此时已经将代码拷贝到SDRAM上了!

小提示:

在写位置无关码的时候,初始化SDRAM不要使用有初始值数组的形式,因为数组的初始值要用绝对地址来访问,初始值放在.rodata

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值