重定位
说明
本文只是一个博主学习记录的笔记,没有参考价值,全部都是搬砖的内容,具体详情可以看韦东山老师的笔记:https://cloud.tencent.com/developer/article/1708811
必须知道的几个概念
什么是代码重定位?
把可执行程序从一个位置复制到另一个位置的过程叫做重定位
什么是位置无关码
什么是运行地址
是指程序指令真正运行的地址,是由用户指定的,用户将运行地址烧录到哪里,哪里就是运行的地址。比如有一个指令的编译地址是0x0,实际运行的地址是0x30000000,如果用户将指令烧到0x60000000上,那么这条指令的运行地址就是0x60000000。
当编译地址和运行地址不同的时候会出现什么结果?结果是不能跳转,编译后会产生跳转地址,如果实际地址和编译后产生的地址不相等,那么就不能跳转。
为什么要代码重定位?
nand flash启动的情况
S3C2440的cpu可以直接给sdram发送命令,给nor flash发送命令,但是不可以给nand flash发送命令
那么为什么还可以使用nand flash启动呢?
- 因为上电后,cpu是先操控nand flash控制器,由操控器在nand flash进行操作的,因此启动硬件时会自动把nand flash前4k复制到sram
- cpu从0地址运行sram
如果程序大于4k怎么办?
- 前4k的代码需要把整个程序读出来放到sdram也就是—代码重定位
nor flash启动的情况
如果从nor flash启动,会出现什么问题?
- 将拨动开关拨到Nor Flash启动时,此时CPU认为的
0
地址 在Nor Flash上面,片内内存SRAM的基地址就变成了0x40000000
(Nand启动时片内内存SRAM的基地址基地址是0), - 由于Nor Flash特性:可以像内存一样读,但不能像内存直接写,所以当我们需要修改变量全局变量的值时,因为该变量时存储在nor flash的,所以不修改无效的,因此需要把全局变量和静态变量重定位放到SDRAM里,所以也是需要代码重定位。
两种方式的重定位
第一种是只重定位data段到sdram内存,因为在nor flash内存中不能进行写操作的所以我们需要把数据段(data段)重定位到SDRAM上进行写的操作,所以需要代码重定位
。但是这种只是重定位某段的方式是存在弊端的,第一,我们的调试工具通常是不支持这种 分体形式(比如我们的之前的代码在0地址开始存放text和rodata段,而在间隔很远处sdram 0x30000000存放data段,这就是分体的形式),第二,这种分体方式需要能够直接运行程序的flash比如nor flash才可以工作,但是有些板子根本连nor flash都没有,那么只能通过第二种方式进行开发。
第二种是把整个程序都复制到内存sdram,这种方式所有数据都是紧挨着的。我们的bin文件时由链接脚本指定了运行地址SDRAM的0x30000000,但是我们bin文件在启动时却是选择了nor flash,也就是在该内存的0地址开始运行,那么在nor flash上的代码就需要把整个bin文件拷贝到SDRAM中去,这就是代码重定位,但这就需要我们必须做到,在重定位之前的代码必须时位置无关码(连接脚本指定我们程序的运行地址为0x30000000,为什么我们的在nor flash上的代码从0地址开始运行也能工作?这就是建立在我们这部分代码必须是位置无关码的基础上的,0地址处运行和0x30000000处运行达到同样效果,需要我们保证,在重定位之前,也就是复制操作没有完成之前的代码,必须是位置无关的)
代码重定位
例题一
我们打算用程序中写入全局变量以及局部变量,测试在Nand Flash何Nor Flash是在你们样的
main.c文件
#include "s3c2440_soc.h"
#include "uart.h"
#include "init.h"
char g_Char = 'A'; //定义一个全局变量
const char g_Char2 = 'B'; //定义固定的全局变量
int g_A = 0;
int g_B;
int main(void)
{
uart0_init();
while (1)
{
putchar(g_Char); /*让g_Char输出*/
g_Char++; /* nor启动时, 此代码无效 */
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
在但是我们查看bin文件时,可见突然就变成很大-rwxr-xr-x. 1 root root 33874 7月 18 15:06 sdram.bin
。这是因为.data
数据段放在了0x00008474
地址导致程序太大,所以我们需要修改为arm-linux-ld -Ttext 0 -Tdata 0x800 start.o led.o uart.o init.o main.o -o sdram.elf
就是加入了-Tdata 0x800
意思就是说把data数据段放到地址为0x800
- 烧写进Nand Flash时,我们可以知道输出的是:
ABCDE....
,我们可以知道,我们的全局变量是可以被修改的 - 烧写进NorFlash时,我们可以知道输出的是:
AAAA....
,我们可以知道,我们的全局变量是没有被修改成功的
可能会有点疑惑上面的.data数据段是啥意思呢。我们的一个程序里面一般有下面几个段。
链接脚本的引入与简单测试
为什么我们上面的例子烧写的nand flash 以及nor flash最后输出的数据却是不一样的呢?
- Nor Flash的话: Nor Flash就被认为是0地址,y因为我们的.data数据段时修改到0x800,所以g_Char被放在0x800后面。CPU上电后从0地址开始执行,它能读取Nor Flash上的代码,打印出A,当进行g_Char++的时候,写操作操作无效,下次读取的数据仍然是A。
- Nand Flash的话: 上电后,Nand Flash前4K代码就被自动的复制到SRAM里面,SRAM是CPU认为的0地址。CPU上电后从0地址开始执行,它读取SRAM上的代码,并g_Char++修改变量,下次读取的数据就依次增加了。
我们应该怎样修改,才能让Nor Flash修改成功呢?
我们直接把数据段.data放在地址0x30000000就好,如arm-linux-ld -Ttext 0 -Tdata 0x30000000 start.o led.o uart.o init.o main.o -o sdram.elf
,这个主要是添加了-Tdata 0x30000000
改变数据段的位置 ,但是我们make一下查看bin文件大小既然有800多:-rwxr-xr-x. 1 root root 805306369 7月 18 15:47 sdram.bin
, 这是因为我们Nor Flash是从代码段.text
0地址到数据段的.data
的0x30000000地址,文件就有了800多M了,,代码段和数据段之间有间隔,我们称之为黑洞
那我们应该怎么解决黑洞的方法?
第一种方法
- 把数据段的g_Char和代码段靠在一起;
- 烧写在Nor Flash上面;
- 运行时把g_char(全局变量)复制到SDRAM,即0x3000000位置(段重定位);
第二种方法
- 让文件直接从0x30000000开始,全局变量在0x3……;
- 烧写Nor Flash上 0地址处;
- 运行会把整个代码段数据段(整个程序)从0地址复制到SDRAM的0x30000000(全段重定位);
第一种方法的实现
修改Makefile的代码段地址,使用链接脚本sdram.lds指定。arm-linux-ld -T sdram.lds start.o led.o uart.o init.o main.o -o sdram.elf
,这里面的sdram.lds
就是链接脚本。
其中数据段放在0x700,但运行时在0x3000000:
SECTIONS{
.text 0 :{*(.text)} //所有文件的.text
.rodata :{*(.rodata)} //只读数据段
.data 0x30000000 : AT(0x700) {*(.data)} //放在0x700,但运行时在0x3000000
.bss : {*(.bss) *(.COMMON)} //所有文件的bss段,所有文件的.COMMON段
}
具体的链接脚本语法可以自己百度,这里主要说的是.data 0x30000000 : AT(0x700) {*(.data)}
,这里说明了存储地址是在0x700,运行地址却是在0x30000000。而且,我们还需要在.s文件中把0x700的数据复制到0x30000000,这样的话我们烧写运行时才有数据。
start.s
bl sdram_init
/*重定位data段*/
move r1, #0x700
ldr r0,[r1]
mov r1,#0x30000000
str r0,[r1]
上面的这种方法,只能复制0x700处的一位数据,不太通用,我们数据有多个的时候就需要下面写一个更加通用的复制方法:
链接脚本修改如下:
SECTIONS{
.text 0 : {*(.text)}
.rodata : {*(.rodata)}
.data 0x30000000 : AT(0x700){
data_load_addr = LOADADDR(.data);
data_start = .;//等于当前位置
*(.data)//等于数据段的大小
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:
ldr r4,[r1] //从r1读到r4
str r4,[r2] //r4存放到r2
add r1,r1,#1 //r1+1
add r2,r2,#1 //r2+1
cmp r2,r3 //r2 r3比较
bne cpy //如果不等则继续拷贝
bl main
最后烧写进Nor Flash,得出结果为AaBaCcDd.......
所以修改成功。
我们再去做个实验,把全局变量g_A以16进制打印出来
uart.c文件
//在这个文件,我们再添加这个函数
void printHex(unsigned int val){
int i;
unsigned char arr[8];
/*先取出每一位的值*/
for(i = 0; i < 8; i++){
arr[i] = val & 0xf;
val >>= 4;
}
/* 打印 */
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');
}
}
main.c文件
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++; /* nor启动时, 此代码无效 */
putchar(g_Char3);
g_Char3++;
delay(500000);
}
return 0;
}
上面代码因为没有清理bss,所以打印出来的值都是一些莫名其妙的值,很显然是不符合我们需要的值的,所以我们需要清理bss段。
那么为什么需要清理bss段呢?
- 全局变量与静态变量没有初始化或初始化值为0时,都会放在.bss段。
- 初始化为非0值,则放在.data段。全局变量与静态变量没有初始化或初始化值为0时,都放在.bss段会产生一个问题: 假如说我们定义一个全局的变量 int a = 0;我们知道这是一个初始化值为0 的全局变量,那么他会被放在.bss段,由于存储在bss段内的数据在下一次启动代码时不会被自动的重新初始化为0(即bss段清0),这就可能导致上一次运行代码过程中有可能修改了全局变量或静态变量的值,而修改过的值会被bss段保存下来,那么在下一次启动代码事我们定义的那个全局变量的值就可能不是我们第一次定义的 “0”值了,这样的话就有可能导致一些问题。所以我们当初始值为0,或者没有初始化时,我们需要清理bss段 那么如何对Bss段清零? 在链接脚本中标记bss段的开始和结束位置,然后在初始化代码的时候把开始标记和结束标记之间的内容清0。
sdram.lds文件
SECTIONS {
.text 0 : { *(.text) }
.rodata : { *(.rodata) }
.data 0x30000000 : AT(0x700)
{
data_load_addr = LOADADDR(.data);
data_start = . ;
*(.data)
data_end = . ;
}
bss_start = .; //bss开始地址是当前位置
.bss : { *(.bss) *(.COMMON) }
bss_end = .; //bss结束地址也是当前位置
}
start.s文件
/* 清除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
bl main
halt:
现在的代码全局变量就是为0,通过几行代码,就可以少几十个甚至上千个全局变量的存储空间。
拷贝代码和链接脚本的改进
前面重定位时,需要ldrb命令从的Nor Flash读取1字节数据,再用strb命令将1字节数据写到SDRAM里面。
cpy:
ldrb r4, [r1] /*首先从flash读出一个字节*/
strb r4, [r2] /*让后把数据写到SDRAM*/
add r1, r1, #1
add r2, r2, #1
cmp r2, r3
bne cpy
-
JZ2440上的Nor Flash是16位,SDRAM是32位。
-
假设现在需要复制16byte数据,
- 采用ldrb命令每次只能加载1byte,因此CPU需要发出16次命令,内存控制器每次收到命令后,访问硬件Nor Flash,因此需要访问硬件16次;
- 同理,访问SDRAM时,CPU需要执行strb 16次,内存控制器每次收到命令后,访问硬件SDRAM,也要16次,这样总共访问32次。
-
现在对其进行改进,使用ldr从Nor Flash中读,ldr命令每次加载4字节数据,因此CPU只需执行4次,但由于Nor Flash是16位的,内存控制器每次收到CPU命令后,需要拆分成两次访问,因此需要访问硬件8次;
-
使用str写SDRAM,CPU只需执行4次,内存控制器每次收到命令后,直接硬件访问32位的SDRAM,因此这里只需要4次,这样总共访问只需要12次。
-
在整个操作中,花费时间最长的就是硬件访问,改进后代码,减少了硬件访问的次数,极大的提高了效率
修改start.s里的部分代码
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 //如果r1 =< r2则继续拷贝
修改sdram.lds里的部分代码
SECTIONS {
.text 0 : { *(.text) }
.rodata : { *(.rodata) }
.data 0x30000000 : AT(0x700)
{
data_load_addr = LOADADDR(.data);
. = ALIGN(4);
data_start = . ;
*(.data)
data_end = . ;
}
. = ALIGN(4);//让当前地址向4对齐
bss_start = .;
.bss : { *(.bss) *(.COMMON) }
bss_end = .;
}
- 这里主要修改的是添加了
. = ALIGN(4);
,是指示编译器将接下来的代码进行4字节对齐编译,也就是在分配地址时,以4的整数倍分配。
代码重定位与位置无关码
一个程序,由代码段、只读数据段、数据段、bss段等组成。
程序一开始可以烧在Nor Flash上面,运行时代码段仍可以在Nor Flash运行,但对于数据段,就必须把数据段移到SDRAM中,因为只要在SDRAM里面,数据段的变量才能被写操作,把程序从一个位置移动到另一个位置,把这个过程就称为重定位。
前面的例子,我们只是重定位了数据段,这里我们再尝试重定位整个代码。
先梳理下把整个程序复制到SDRAM需要哪些技术细节:
- 把程序从Flash复制到运行地址,链接脚本中就要指定运行地址为SDRAM地址;
- 编译链接生成的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段。
我们以后的代码更多的采用一体式链接脚本,原因如下:
- 分体式链接脚本适合单片机,单片机自带有flash,不需要再将代码复制到内存占用空间。而我们的嵌入式系统内存非常大,没必要节省这点空间,并且有些嵌入式系统没有Nor Flash等可以直接运行代码的Flash,就需要从Nand Flash或者SD卡复制整个代码到内存;
- 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 某个值,只是起到方便查看的作用,并不是真的跳转。
怎么写位置无关码?
- 使用相对跳转命令 b或bl;
- 重定位之前,不可使用绝对地址,不可访问全局变量/静态变量,也不可访问有初始值的数组(因为初始值放在rodata里,使用绝对地址来访问);
- 重定位之后,使用ldr pc = xxx,跳转到/runtime地址;
- 重点: 反汇编文件里, B或BL 某个值,只是起到方便查看的作用,并不是真的跳转。
写位置无关码,其实就是不使用绝对地址,判断有没有使用绝对地址,除了前面的几个规则,最根本的办法看反汇编。
因此,前面的例子程序使用bl命令相对跳转,程序仍在NOR/sram执行,要想让main函数在SDRAM执行,需要修改代码:
//bl main /*bl相对跳转,程序仍在NOR/sram执行*/
ldr pc, =main/*绝对跳转,跳到SDRAM*/
重定位清除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 */
在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语言直接取参数。
修改start.S 跳转到C函数不需要任何参数
bl sdram_init
//bl sdram_init2 /* 用到有初始值的数组, 不是位置无关码 */
/* 重定位text, rodata, data段整个程序 */
bl copy2sdram
/* 清除BSS段 */
bl clean_bss
修改链接脚本,让__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?
- 在C函数中声明改变量为extern外部变量类型,比如:extern int abc;
- 使用时,要取址,比如: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符号表?
- 对于常规变量g_i,得到里面的值,使用&g_i得到addr;
- 为了保持代码的一致,对于lds中的a1,使用&a1得到里面的值;
这只是一个编译器的小技巧,不用深究。
结论:
- C程序中不保存lds文件中的变量,lds再大也不影响;
- 借助symbol table保存lds的变量,使用时加上”&”得到它的值,链接脚本的变量要在C程序中声明为外部变量,任何类型都可以;