转自:https://blog.youkuaiyun.com/myselfzhangji/article/details/80379887
一 代码的组成
程序至少包含:代码段+数据段
代码段:.text
数据段:一般的全局变量,初值不为0的经过初始化的全局变量
如:char g_char ='A'; //初值为A的字符型全局变量
rodata:const的全局变量,只读数据段
bss:初值为0,或者没有初值的全局变量,不保存在bin文件中
如:int g_A =0; //初值为0的整形全局变量
int g_B; //无初值的整形全局变量
commen:注释,不保存在bin文件中
关于bss段和comment段为什么不保存在bin文件中,可以这样来理解,如果我们的代码中有非常多的全局变量(比如100万个char型变量),如果这些数据都保存在bin文件中,那么我们的bin文件得有多大,显然这是不可能的。
所以,一般地,在链接脚本中,我们都需要指出这几个段在内存中的分配情况。如下所示:
二 重定位的相关概念
1. Nand flash和nor flash
以前使用单片机时,没有仔细思考过这个问题,都是认为程序是烧写到芯片内部的flash中的,也没有仔细思考过,程序是怎么跳转到flash取指令并执行的。
对于嵌入式系统来说,它的程序可能会比较大,超出它内部的flash大小,我们的程序无法整个放入到芯片内部的flash中;甚至有些SoC芯片内部根本就没有flash,代码无法放入到芯片内部,只能放在片外flash芯片上,这时芯片就需要外挂flash芯片了---nand flash或者nor flash。
| NAND flash | NOR flash |
地址线数据线 | 地址线、数据线复用,所有信息通过一根线传输 | 单独地址线 单独数据线 |
| 价格低,容量大 | 价格高,容量小 |
坏块 | 容易有坏块,数据的有效性不能保证 | 无坏块 |
访问操作 | 因为没有单独的地址线,所以pc无法直接访问它,也就无法取指令并执行 | 因为有单独的地址线,pc可以通过地址线取指令 |
读写操作 | 可以像内存一样读,也可以像内存一样写 | 可以像内存一样读,但是不能向内存一样直接写,写入需要单独设置操作 |
sdram:主要用于程序执行时的程序存储、执行或计算,类比于PC 的内存
2. 为什么需要重定位
Flash | 需要重定位的原因 |
NAND flash | 因为pc不能直接访问它,要想执行它内部的程序,需要将nand中的代码拷贝到sdram等ram中,所以需要重定位 |
NOR flash | 虽然pc可以直接访问nor,但是因为不能像内存一样写nor flash,我们程序中的全局变量、静态变量等都是存在bin文件中的,也就是说存在nor上的,当程序需要修改这些变量时,无法修改他们,所以需要重定位到sdram等ram中 |
3. 重定位之前的状态
重定位决定了pc取指令的地址,但是很显然SoC芯片刚刚上电时,重定位还没有实现,SoC需要执行一段代码来实现重定位,所以这时PC指针并没有指向重定位地址的地方,那么这个过程是如何实现的呢?
3.1 上电后,PC指向哪里
对于任何一种SoC芯片来说,上电后PC指针的位置是有硬件设计决定的,一般地,对于cortex-M系列内核的芯片,上电后PC指针都为0,指向0地址处。
对于nand启动,0地址对应片内sram,因为nand控制器上电后自动将nand中的前4k大小拷贝到了sram中,所以此时sram中存在代码(即nand中前4K代码),内核就可以取指令并执行了。
对于nor启动,0地址对应nor芯片,我们已经将程序下载到nor上了,所以此时pc也可以取指令并执行了。
4. 重定位过程和方法
4.1 nor flash启动
示例:
char g_char ='A';
constchar g_char2 ='B';
int g_A =0;
int g_B;
int main(void)
{
uart0_init();
while(1)
{
putchar(g_char);
g_char++; /*nor启动,此代码无效*/
delay(1000000);
}
return0;
}
我们的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.ostart.S
arm-linux-ld -Ttext 0 -Tdata 0x800[zj1] start.o led.ouart.o init.o main.o -o sdram.elf
arm-linux-objcopy -O binary -Ssdram.elf sdram.bin
arm-linux-objdump -D sdram.elf> sdram.dis
clean:
rm *.bin *.o *.elf *.dis
我们按照上面的Makefile进行编译后,将bin文件下载到nor flash中,发现输出的字符一直都是‘A’,可是我们明明在程序中,对全局字符变量g_char进行++操作,这就说明,我们保存在nor flash中的g_char是不能进行写操作的
为了实现修改,我们考虑将g_char保存在外部的sdram中,修改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.ostart.S
arm-linux-ld -Ttext 0 -Tdata 0x30000000[zj2] start.o led.ouart.o init.o main.o -o sdram.elf
arm-linux-objcopy -O binary -Ssdram.elf sdram.bin
arm-linux-objdump -D sdram.elf> sdram.dis
clean:
rm *.bin *.o *.elf *.dis
编译之后,发现bin文件为800多M,显然这是不合理的
BIN文件的数值为什么是805306369?我们发现805306369=0x30000001,的确,我们在Makefile中就是指明了全局变量保存在SDRAM中,所以BIN文件的保存地址是从0~0x30000000,其大小正好是0x30000001,因此,这个时候,我们的代码段和数据段的存储格式如下:
为了解决上面的方法,代码过大的问题,有两种方式来解决:
A. 将data段重定位到SDRAM中,text段仍在NOR Flash中
1. 仍然将全局变量数据段和代码段烧写到nor flash中
2. 在运行时,代码段代码要能实现将数据段拷贝(重定位)到SDRAM中;
3. 以后每次访问全局变量,都是去SDRAM中去访问,不去nor flash中访问
这个方式是通过链接脚本(sdram.lds[zj3] 文件)来实现的,如下所示
SECTIONS {
.text 0 : { *(.text) }
.rodata : { *(.rodata) }
.data 0x30000000[zj4] : AT(0x800[zj5] )
{
data_load_addr =LOADADDR(.data);
data_start = . ;
*(.data)
data_end = . ;
}
.bss : { *(.bss) *(.COMMON) }
}这样在makefile文件中,通过指定这个lds文件的方式来编译代码
arm-linux-ld-T sdram.lds[zj6] start.o led.ouart.o init.o main.o -o sdram.elf
对于elf文件
l elf文件含有地址信息,bin文件不含有地址信息,
l 使用加载器(对于裸板,就是JTAG调试工具;对于APP,加载器也是一个APP),使用加载器把elf文件读入内存的加载地址(load addr);
l 然后运行程序;
l 如果load addr不等于runtime addr,程序本身需要重定位
核心:程序运行时,应该位于runtime addr(或者是重定位地址,或者链接地址);
对于bin文件
l 将elf文件转换成bin文件
l 硬件机制的启动(没有加载器)
l 如果bin文件所在位置不等于runtime addr,程序本身实现重定位
但是,我们发现,如果只是按照makefile和sdram.lds文件内容的话,还是不能将data段重定位到SDRAM中,我们需要一个程序将一开始处于nor中的data段拷贝到sdram中,这个程序是在start.S文件来实现的,如下:
ldr r1, =data_load_addr /*data段在bin文件中的地址, 加载地址*/
ldr r2, =data_start /*data段在重定位地址, 运行时的地址*/
ldr r3, =data_end
cpy:
ldrb r4, [r1]
strb r4, [r2]
add r1, r1, #1
add r2, r2, #1
cmp r2, r3
bne cpy
B. 将text段和data段都重定位在SDRAM中
为了解决上面的问题,解决办法:
A. 链接脚本指定runtime addr为SDRAM的地址;
B. 重定位之前的代码,与位置无关,即用位置无关码写成
C. 在nor flash中最开始的代码,需要完成这样的动作,将整个nor中的代码拷贝到SDRAM中(上面的方式是分体的程序,即代码段和数据段是分开来存放的,对于我们上面的示例,就是代码段保存在nor flash,数据段保存在sdram中,这种方式适用于单片机,它们本身含有内部flash,内存空间较小,如果将代码段也拷贝到内存中,会造成内存剩余可用空间变小;合体的方式就是我们这个示例中的链接脚本,它适用于嵌入式系统,因为嵌入式系统的内存空间较大)
SECTIONS
{
. = 0x30000000[zj7] ;
. = ALIGN(4);
.text :
{
*(.text)
}
. = ALIGN(4);
.rodata : { *(.rodata) }
. = ALIGN(4);
.data : { *(.data) }
. = ALIGN(4);
__bss_start = .;
.bss : { *(.bss) *(.COMMON) }
_end = .;
}
链接脚本只是告诉程序需要将某些段拷贝到哪些内存位置,但是它本身不完成拷贝的动作,这个动作是start.S完成的,因此在start.S文件中,我们需要将text段、rodata段,data段整个拷贝到sdram对应的位置
/*重定位text,rodata,data段整个程序*/
mov r1, #0 /* */
ldr r2, =_start /*第一条指令运行地址*/
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
怎么写位置无关码(地址在任何地方都能执行)???
a. 使用相对跳转命令,B/BL
b. 重定位之前,不可使用绝对地址,比如不可访问全局变量、静态变量、有初始值的数组(rodata,data,使用绝对地址来访问),初始值放在rodata里面,使用绝对地址来访问。
c. 重定位之后,可以使用绝对地址ldr pc, =xxx跳转到runtime addr
d. 不使用绝对地址!最根本的办法是看反汇编
这里穿插讲一下什么是位置无关指令,什么是位置相关指令,在我们上面的链接脚本中,我们需要代码段和数据段都被保存在sdram的起始地址0x30000000位置(. = 0x30000000[zj8] ;),这样编译出来的反汇编代码如下:
请注意上面的bl sdram_init处的机器码是eb000102
如果我们希望将代码段和数据段保存在sdram的起始地址为0x3200000位置(. = 0x32000000;),我们再次编译代码,发现bl sdram_init处的机器码仍然是eb000102,按照常规的理解,bl指令现在希望跳转的地址是3200046c(之前的是3000046c),地址不同,按道理来说机器码肯定不同呀(机器码就是对指令的翻译),那这里为什么机器码是一样的呢,这就说明bl指令是位置无关的,它和需要跳转到哪里的地址是没有关系的(只和需要跳转的指令与代码段起始地址的差值有关系,就是说他会在代码段起始位置处查找需要跳转到的地址)
4.2 nand flash启动
很显然,对于nand启动时,.text段和.data段都需要重定位到sdram中,也就是和nor flash中第2种重定位方式一样,这里就不在展开解释了。