代码重定位

Posted 今天天气眞好

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了代码重定位相关的知识,希望对你有一定的参考价值。


1.段的概念以及重定位的引入

首先看下面的图:
在这里插入图片描述
我们知道,CPU可以直接访问SDRAM,NOR,SRAM(片内4k内存)以及NOR FLASH控制器,而不能直接访问NAND,但是我们为什么又可以烧写bin文件到nand上呢?

下面来分析Nand,Nand在启动时候,会把前4k内容复制到SRAM,这是由硬件完成,然后CPU从SRAM的0地址开始执行,但是注意到一个问题:当bin文件的大小超过4k呢
这时前4k的代码就会将整个程序读出,放到SDRAM上,而这就是一个重定位的过程,即重新确定程序的地址。

同样,来分析Nor,Nor可以像内存一样读,但是不能像内存一样写
怎么理解:Nor相当于硬盘,保存有重要数据,不能随便写,容易破坏数据

看下面汇编代码(Nor上):

MOV RO,#0
LDR R1,[R0] 读出地址0上的值
STR R1,[R0]	将R1的值写进R0,此条语句无效,直接修改变量无效

因为程序中含有需要写的全局/静态变量时,他们保存在bin中,写在Nor上,因此对Nor的写需要执行特殊操作(即重定位,放到SDRAM上)

下面通过编程来进行验证
看到main

#include "s3c2440_soc.h"
#include "uart.h"
#include "init.h"

//数据段(全局变量)
char g_Char = 'A'; 

//只读数据段(rodata)
const char g_Char2 = 'B';

//初始化为0的全局变量
int g_A = 0;

//无初值的全局变量
int g_B;

int main(void)
{
	uart0_init();

	while (1)
	{
		putchar(g_Char);
		g_Char++;         /* nor启动时, 此代码无效 */
		/* 因为nor只能读,不能随便写 */
		/* 而对于nand flash就可以正常输出 */
		delay(1000000);
	}
	return 0;
}

Makefile

all:
	arm-linux-gcc -c -o led.o led.c
	arm-linux-gcc -c -o uart.o uart.c
	arm-linux-gcc -c -o init.o init.c
	arm-linux-gcc -c -o main.o main.c
	arm-linux-gcc -c -o start.o start.S
	arm-linux-ld -Ttext 0 -Tdata 0x800  start.o led.o uart.o init.o main.o -o sdram.elf
	arm-linux-objcopy -O binary -S sdram.elf sdram.bin
	arm-linux-objdump -D sdram.elf > sdram.dis
clean:
	rm *.bin *.o *.elf *.dis

注意:-Tdata 0x800是指定从0X800开始,即2M的空间位置

编译烧写之后通过串口打印如下:
在这里插入图片描述
前面的ABCDEFGH是烧写到nand上的,没有问题
后面一直输出A是烧写到nor上的,因此可以看到对全局变量的修改是无效的。

分析反汇编代码sdram.dis可以发现:
重定位,程序含有:
1.代码段(data):test
在这里插入图片描述

2.数据段(全局变量):data
在这里插入图片描述

3.只读数据段(rodata):const全局变量
在这里插入图片描述

4.bss段:初值为0或者无初值的全局变量
在这里插入图片描述

5.注释段 commen :
在这里插入图片描述

注意到:
在这里插入图片描述
发现sdram.dis的大小为2049,转换成十六进制即801,可以发现bss段和注释段都不在bin文件中。
在这里插入图片描述

2.链接脚本的引入以及简单测试

分析Makefile可以看到:

all:
	arm-linux-gcc -c -o led.o led.c
	arm-linux-gcc -c -o uart.o uart.c
	arm-linux-gcc -c -o init.o init.c
	arm-linux-gcc -c -o main.o main.c
	arm-linux-gcc -c -o start.o start.S
	arm-linux-ld -Ttext 0 -Tdata 0x800  start.o led.o uart.o init.o main.o -o sdram.elf
	arm-linux-objcopy -O binary -S sdram.elf sdram.bin
	arm-linux-objdump -D sdram.elf > sdram.dis
clean:
	rm *.bin *.o *.elf *.dis

-Ttext 0 -Tdata 0x800 :指定代码段从0开始,数据段从0X800开始

下面进行分析:
1.Nor启动:SRAM从0X40000000开始执行,Nor中存放bin文件,数据从0X800开始执行g_char,此时进行g_char++操作,Nor不能写,因此只会打印AAAA
在这里插入图片描述

2.nand启动:SRAM从0地址开始执行,nand会将前4K内容复制到SRAM,然后SRAM中从0x800开始执行代码段g_char,此时操作的是内存,可以进行读写操作,因此最终打印的是ABCD…
在这里插入图片描述

那我们能不能修改Makefile,让g_char放到SDRAM上呢?

将代码段与g_char靠在一起,如下图:
在这里插入图片描述
其中可以发现hole没有使用(黑洞)
通过测试可以发现如下sdram.dis很大,接近800多M
在这里插入图片描述
解决办法:
方法一:重定位g_char,放到0x30000000
在这里插入图片描述

方法二:重定位整个程序,放到0x30000000
在这里插入图片描述

那么怎么实现将代码段与g_char靠在一起呢?—>引入链接脚本

参考资料:
Using LD, the GNU linker
链接脚本

链接脚本:将data段与代码段拼接在一起

Makefile

all:
	arm-linux-gcc -c -o led.o led.c
	arm-linux-gcc -c -o uart.o uart.c
	arm-linux-gcc -c -o init.o init.c
	arm-linux-gcc -c -o main.o main.c
	arm-linux-gcc -c -o start.o start.S
	#arm-linux-ld -Ttext 0 -Tdata 0x30000000  start.o led.o uart.o init.o main.o -o sdram.elf
	arm-linux-ld -T sdram.lds start.o led.o uart.o init.o main.o -o sdram.elf
	arm-linux-objcopy -O binary -S sdram.elf sdram.bin
	arm-linux-objdump -D sdram.elf > sdram.dis
clean:
	rm *.bin *.o *.elf *.dis

注意:介入了-T sdram.ld,引入链接脚本
代码如下:

SECTIONS {
	#所有文件的代码段都放在0地址
   .text   0  : { *(.text) }    

   #只读数据段紧接着代码段来存放
   .rodata  : { *(.rodata) }	
   
	#将数据段放在0x30000000,g_char放在0x800
   .data 0x30000000 : AT(0x800) { *(.data) }
   .bss  : { *(.bss) *(.COMMON) }
}

start.s汇编中重定位代码如下:

	bl sdram_init	//跳到SDRAM的初始化	

	/* 重定位data段 */
	//即将0x800的位置复制一个字节的大小放在0x30000000
	//但是这是经过分析后得到的,并不通用
	mov r1, #0x800
	ldr r0, [r1]	//从r1取值放到r0
	mov r1, #0x30000000
	str r0, [r1]	//将r0的值放到r1所指的地方

要实现通用的话:
修改sdram.lds:

SECTIONS {
   .text   0  : { *(.text) }
   .rodata  : { *(.rodata) }
   .data 0x30000000 : AT(0x800) 
   { 
      data_load_addr = LOADADDR(.data);
      data_start = . ;//.就是当前位置,即data_start = 0x30000000
      *(.data) 
      data_end = . ;//data_end - data_start就是数据段的长度 (data_end由数据段确定大小)
   }
   .bss  : { *(.bss) *(.COMMON) }
}

start.s汇编中重定位代码如下:

	bl sdram_init	

	/* 重定位data段 */
	ldr r1, =data_load_addr  /* data段在bin文件中的地址, 即加载地址 */
	ldr r2, =data_start 	 /* data段在重定位地址, 运行时的地址 */
	ldr r3, =data_end 	     /* data段结束地址 */

cpy: //拷贝
	ldrb r4, [r1] //从r1读一个字节到r4
	strb r4, [r2] //将r4的值写到r2存放的位置
	add r1, r1, #1
	add r2, r2, #1
	cmp r2, r3
	bne cpy	//如果不相等,继续拷贝

3. 链接脚本的解析

先来看链接脚本的格式:

SECTIONS {
...
secname start BLOCK(align) (NOLOAD) : AT ( ldadr )
  { contents } >region :phdr =fill
...
}

secname :段名
start :起始地址,及运行时的地址(runtime addr),也叫重定位地址(relocate addr)

BLOCK(align) (NOLOAD)>region :phdr =fill一般都不会用到

AT ( ldadr ):load addr:加载地址,不写时候,runtime addr = relocate addr

contents :内容
格式
(1)整个文件指定:start.o

(2)所有文件指定:*(.test)

(3)指定某个文件在前,后面为剩下所有文件的某个段

start.o
*(.test)

下面分析链接脚本:

SECTIONS {
   .text   0  : { *(.text) }
   .rodata  : { *(.rodata) }
   .data 0x30000000 : AT(0x800) 
   { 
      data_load_addr = LOADADDR(.data);
      data_start = . ;
      *(.data) 
      data_end = . ;
   }
   .bss  : { *(.bss) *(.COMMON) }
}

这两句没有写AT,即runtime addr = relocate addr,无需重定位

   .text   0  : { *(.text) }
   .rodata  : { *(.rodata) }

分析下面这句

   .data 0x30000000 : AT(0x800) 

即:load addr = 0x800;runtime addr = 0x30000000

分析bss段:

 .bss  : { *(.bss) *(.COMMON) }

bss段紧跟着data段存放,即runtime addr = 0x3xxxxxxx

注意:在bin文件,elf文件中都不存在bss段

1.分析elf文件
(1)链接得到elf文件,含有地址信息(load addr)
(2)使用加载器(对于裸板是JTAG调试工具,对于APP,加载器本身也是一个APP)将elf文件解析读入内存(读到load addr)
(3)运行
(4)如果load addr != runtime addr,程序本身就要进行重定位

核心:程序运行前,应位于runtime addr,relocate addr,也叫链接地址

2.分析bin文件
(1)将elf转换为bin
(2)硬件机制启动
(2)如果bin文件所在位置 != runtime addr,程序本身实现重定位

下面进行代码分析:
定义的g_char = 0,我们要将他以十六进制进行打印,按理说应该打印的是0x00000000

但是最终打印却发现并不是,其实这就是bss段没有清除的原因

修改:
main.c

#include "s3c2440_soc.h"
#include "uart.h"
#include "init.h"

char g_Char = 'A';
char g_Char3 = 'a';
const char g_Char2 = 'B';
int g_A = 0;
int g_B;

int main(void)
{
	uart0_init();

	puts("\\n\\rg_A = ");
	printHex(g_A);
	puts("\\n\\r");

	while (1)
	{
		putchar(g_Char);
		g_Char++;

		putchar(g_Char3);
		g_Char3++;
		delay(1000000);
	}
	return 0;
}

uart.c串口中打印函数:

/* 0xABCDEF12 */
void printHex(unsigned int val)
{
	int i;
	unsigned char arr[8];

	/* 先取出每一位的值 */
	for (i = 0; i < 8; i++)
	{
		arr[i] = val & 0xf;
		val >>= 4;   /* arr[0] = 2, arr[1] = 1, arr[2] = 0xF */
	}

	/* 打印 */
	puts("0x");
	for (i = 7; i >=0; i--)
	{
		if (arr[i] >= 0 && arr[i] <= 9)
			putchar(arr[i] + '0');
		else if(arr[i] >= 0xA && arr[i] <= 0xF)
			putchar(arr[i] - 0xA + 'A');
	}
}

start.s汇编代码中清除bss段:

	/* 清除BSS段 */
	ldr r1, =bss_start
	ldr r2, =bss_end
	mov r3, #0
clean:
	strb r3, [r1]
	add r1, r1, #1
	cmp r1, r2
	bne clean

运行结果显示正确

4. 拷贝代码和链接脚本的改进

1. 拷贝代码要改进
即将代码段和数据段从flash中复制到sdram

在这里插入图片描述
我们看到之前的代码是ldrb和strb都是按照一字节来操作
假设复制16字节
ldrb从nor中得到数据:执行16次,访问nor16次
strb写到sdram:执行16次,访问sdram16次
发现一共32次,效率非常低

ldrb:读取一个字节,CPU会先把命令发给内存控制器,内存控制器从sdram中读取四字节(32bit)数据,挑出CPU感兴趣的1字节进行返回

strb:写一个字节,CPU会把地址和数据发送给内存控制器,内存控制器把32的数据发给sdram,同时也会发出屏蔽信号(DQM),比如只需要写一个字节,则会发出3条DQM屏蔽其他不需要写的3字节,选择最终只会写sdram中的一个字节

改进:
ldr从nor中得到数据:执行4次,访问硬件8次
str写到sdram:执行4次,访问硬件4次
一个12次,得到了改进

代码方面:

	bl sdram_init	

	/* 重定位data段 */
	ldr r1, =data_load_addr  /* data段在bin文件中的地址, 加载地址 */
	ldr r2, =data_start 	 /* data段在重定位地址, 运行时的地址 */
	ldr r3, =data_end 	     /* data段结束地址 */

cpy:
	ldr r4, [r1]
	str r4, [r2]
	add r1, r1, #4
	add r2, r2, #4
	cmp r2, r3
	ble cpy


	/* 清除BSS段 */
	ldr r1, =bss_start
	ldr r2, =bss_end
	mov r3, #0
clean:
	str r3, [r1]
	add r1, r1, #4
	cmp r1, r2
	ble clean

	bl main

都是以4字节为单位

但是最终发现还是存在问题,在于str r3, [r1]这句话清除bss的同时也把初始化的全局变量进行了清除,涉及到一个字节对齐的问题。因此对链接脚本进行改进。

2. 链接脚本要改进
代码:

SECTIONS {
   .text   0  : { *(.text) }
   .rodata  : { *(.rodata) }
   .data 0x30000000 : AT(0x800) 
   { 
      data_load_addr = LOADADDR(.data);
	  . = ALIGN(4);
      data_start = . ;
      *(.data) 
      data_end = . ;
   }
   
   . = ALIGN(4);//当前地址向4取整
   bss_start = .;
   .bss  : { *(.bss) *(.COMMON) }
   bss_end = .;
}

5. 代码重定位与位置无关码

一个程序,由代码段、只读数据段、数据段、bss段等组成。
程序一开始可以烧在Nor Flash上面,运行时代码段仍可以在Nor Flash运行,但对于数据段,就必须把数据段移到SDRAM中,因为只要在SDRAM里面,数据段的变量才能被写操作,把程序从一个位置移动到另一个位置,把这个过程就称为重定位。
前面的例子,我们只是重定位了数据段,这里我们再尝试重定位整个代码。

先梳理下把整个程序复制到SDRAM需要哪些技术细节:

  1. 把程序从Flash复制到运行地址,链接脚本中就要指定运行地址为SDRAM地址;
  2. 编译链接生成的bin文件,需要在SDRAM地址上运行,但上电后却必须先在0地址运行,这就要求重定位之前的代码与位置无关(是位置无关码);

参考Uboot修改链接脚本:

SECTIONS
{
    . = 0x30000000;

    . = ALIGN(4);
    .text      :
    {
      *(.text)
    }

    . = ALIGN(4);
    .rodata : { *(.rodata) }

    . = ALIGN(4);
    .data : { *(.data) }

    . = ALIGN(4);
    __bss_start = .;
    .bss : { *(.bss) *(.COMMON) }
    _end = .;
}

现在我们写的这个链接脚本,称为一体式链接脚本,对比前面的分体式链接脚本区别在于代码段和数据段的存放位置是否是分开的。

例如现在的一体式链接脚本的代码段后面依次就是只读数据段、数据段、bss段,都是连续在一起的。
分体式链接脚本则是代码段、只读数据段,中间相关很远之后才是数据段、bss段。

我们以后的代码更多的采用一体式链接脚本,原因如下:

  1. 分体式链接脚本适合单片机,单片机自带有flash,不需要再将代码复制到内存占用空间。而我们的嵌入式系统内存非常大,没必要节省这点空间,并且有些嵌入式系统没有Nor Flash等可以直接运行代码的Flash,就需要从Nand Flash或者SD卡复制整个代码到内存;
  2. JTAG等调试器一般只支持一体式链接脚本;

修改start.S段


    /* 重定位text, rodata, data段整个程序 */
    mov r1, #0
    ldr r2, =_start         /* 第1条指令运行时的地址 */
    ldr r3, =__bss_start    /* bss段的起始地址 */

cpy:
    ldr r4, [r1]
    str r4, [r2]
    add r1, r1, #4
    add r2, r2, #4
    cmp r2, r3
    ble cpy


    /* 清除BSS段 */
    ldr r1, =__bss_start
    ldr r2, =_end
    mov r3, #0
clean:
    str r3, [r1]
    add r1, r1, #4
    cmp r1, r2
    ble clean

    bl main  

halt:
    b halt

将修改后的代码重新编译烧写在Nor Flash上,上电运行。
对本代码的启动情况进行分析:
在这里插入图片描述
在生成的bin文件里,代码保存的位置是0x30000000。随后烧写到NOR Flash的0地址,但代码的结构没有变化。之后再重定位到SDRAM。

查看反汇编:

3000005c:   eb000106    bl  30000478 <sdram_init> 

30000060:   e3a01000    mov r1, #0  ; 0x0
30000064:   e59f204c    ldr r2, [pc, #76]   ; 300000b8 <.text+0xb8>
30000068:   e59f304c    ldr r3, [pc, #76]   ; 300000bc <.text+0xbc>

这里的bl 30000478不是跳转到30000478,这个时候sdram并未初始化;
为了验证,我们做另一个实验,修改连接脚本sdram.lds, 链接地址改为0x32000478,编译,查看反汇编:

3000005c:   eb000106    bl  30000478 <sdram_init> 

30000060:   e3a01000    mov r1, #0  ; 0x0
30000064:   e59f204c    ldr r2, [pc, #76]   ; 300000b8 <.text+0xb8>
30000068:   e59f304c    ldr r3, [pc, #76]   ; 300000bc <.text+0xbc>

可以看到现在变成了bl 30000478,但两个的机器码eb000106都是一样的,机器码一样,执行的内容肯定都是一样的。
因此这里并不是跳转到显示的地址,而是跳转到: pc + offset,这个由链接器决定。

假设程序从0x30000000执行,当前指令地址:0x3000005c ,那么就是跳到0x30000478;如果程序从0运行,当前指令地址:0x5c 调到:0x00000478

跳转到某个地址并不是由bl指令所决定,而是由当前pc值决定。反汇编显示这个值只是为了方便读代码。

重点:反汇编文件里, B或BL 某个值,只是起到方便查看的作用,并不是真的跳转。

怎么写位置无关码?
(1)使用相对跳转命令 b或bl;

(2)重定位之前,不可使用绝对地址,不可访问全局变量/静态变量,也不可访问有初始值的数组(因为初始值放在rodata里,使用绝对地址来访问);

(3)重定位之后,使用ldr pc = xxx,跳转到/runtime地址;
写位置无关码,其实就是不使用绝对地址,判断有没有使用绝对地址,除了前面的几个规则,最根本的办法看反汇编。

因此,前面的例子程序使用bl命令相对跳转,程序仍在NOR/sram执行,要想让main函数在SDRAM执行,需要修改代码:

 //bl main  /*bl相对跳转,程序仍在NOR/sram执行*/
 ldr pc, =main/*绝对跳转,跳到SDRAM*/

6. 重定位_清除BSS段的C函数实现

在前面,我们使用汇编程序来实现了重定位和清bss段,下面我们将使用C语言,实现重定位和清除bss段。

1.打开start.S把原来的汇编代码删除改为调用C函数

    /* 重定位text, rodata, data段整个程序 */
    mov r1, #0
    ldr r2, =_start         /* 第1条指令运行时的地址 */
    ldr r3, =__bss_start    /* bss段的起始地址 */

cpy:
    ldr r4, [r1]
    str r4, [r2]
    add r1, r1, #4
    add r2, r2, #4
    cmp r2, r3
    ble cpy


    /* 清除BSS段 */
    ldr r1, =__bss_start
    ldr r2, =_end
    mov r3, #0
clean:
    str r3, [r1]
    add r1, r1, #4
    cmp r1, r2
    ble clean

改为

    /* 重定位text, rodata, data段整个程序 */
    mov r0, #0
    ldr r1, =_start         /* 第1条指令运行时的地址 */
    ldr r2, =__bss_start    /* bss段的起始地址 */
    sub r2, r2, r1          /*长度*/


    bl copy2sdram  /* src, dest, len */

    /* 清除BSS段 */
    ldr r0, =__bss_start
    ldr r1, =_end

    bl clean_bss  /* start, end */

1.在init.c 实现如上两个C函数


void copy2sdram(volatile unsigned int *src, volatile unsigned int *dest, unsigned int len)  /* src, dest, len */
{
    unsigned int i = 0;

    while (i < len)
    {
        *dest++ = *src++;
        i += 4;
    }
}


void clean_bss(volatile unsigned int *start, volatile unsigned int *end)  /* start, end */
{
    while (start <= end)
    {
        *start++ = 0;
    }
}

汇编中,为C语言传入的参数,依次就是R1、R2、R3。
编译,烧写运行没有问题。

我们假设不想汇编传入参数,而是C语言直接取参数。

1.修改start.S 跳转到C函数不需要任何参数

    bl sdram_init
    //bl sdram_init2     /* 用到有初始值的数组, 不是位置无关码 */

    /* 重定位text, rodata, data段整个程序 */
    bl copy2sdram

    /* 清除BSS段 */
    bl clean_bss

2.修改链接脚本,让__code_start 等于当前地址,也就是这里的0x30000000

SECTIONS
{
    . = 0x30000000;

    __code_start = .; //定义__code_start地址位当前地址

    . = ALIGN(4);
    .text      :
    {
      *(.text)
    }

    . = ALIGN(4);
    .rodata : { *(.rodata) }

    . = ALIGN(4);
    .data : { *(.data) }

    . = ALIGN(4);
    __bss_start = .;
    .bss : { *(.bss) *(.COMMON) }
    _end = .;
}

3.修改init.c 用函数来获取参数

void copy2sdram(void)
{
    /* 要从lds文件中获得 __code_start, __bss_start
     * 然后从0地址把数据复制到__code_start
     */

    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;

    while (dest < end)
    {
        *dest++ = *src++;
    }
}


void clean_bss(void)
{
    /* 要从lds文件中获得 __bss_start, _end
     */
    extern int _end, __bss_start;

    volatile unsigned int *start = (volatile unsigned int *)&__bss_start;
    volatile unsigned int *end = (volatile unsigned int *)&_end;


    while (start <= end)
    {
        *start++ = 0;
    }
}

编译烧写运行 ,没有问题。

总结:
C函数怎么使用lds文件总的变量abc?

(1)在C函数中声明该变量为extern外部变量类型,比如:extern int abc;
(2)使用时,要取址,比如:int *p = &abc;//p的只即为lds文件中abc的值

汇编文件中可以直接使用外部链接脚本中的变量,但C函数中要加上取址符号

解释一下原因:
C函数中,定义一个全局变量int g_i;,程序中必然有4字节的空间留出来给这个变量g_i。

假如我们的lds文件中有很多变量

lds{
    a1 = ;
    a2 = ;
    a3 = ;
    ...
}

如果我们C程序只用到几个变量,完全没必要全部存储lds里面的所有变量,C程序是不保存lds中的变量的。
对于万一要用到的变量,编译程序时,有一个symbol table符号表:
在这里插入图片描述
如何使用symbol table符号表?

(1)对于常规变量g_i,得到里面的值,使用&g_i得到addr;
(2)为了保持代码的一致,对于lds中的a1,使用&a1得到里面的值;
这只是一个编译器的小技巧,不用深究。

结论:
C程序中不保存lds文件中的变量,lds再大也不影响;
借助symbol table保存lds的变量,使用时加上”&”得到它的值,链接脚本的变量要在C程序中声明为外部变量,任何类型都可以;

以上是关于代码重定位的主要内容,如果未能解决你的问题,请参考以下文章

代码重定位

CSS代码片段

CSS代码片段

代码重定位和位置无关码

代码重定位(韦东山)

代码重定位