1 引入代码重定位
s3c2440的cpu默认是从0地址开始取指令执行,当从nor启动时,0地址对应nor, nor可以像内存一样读,但不能像内存一样写,前面s3c2440裸机编程-内存控制器 讲过,写入norflash要进行按照spec进行命令表写入。因此我们能够从nor上取指令执行。
当nand启动的时候,我们nand中的前4K指令会变自动加载到sram中去,这时的0地址对应sram。那么我们的程序如果大于4K,要从nand启动,sram只拷贝了nand中的前4K代码,那么如何解决这个问题呢?
就需要重定位代码到dram(ddr)中去,dram的容量较大,又可以直接被cpu访问。
1.1 程序地址空间
我们知道,程序包含:
1 2 3 4 5
| 代码段(.text) 数据段(.data):存放初始值不为0的全局变量/静态变量 rodata段(.rodata):const修饰的全局变量或静态变量 bss段(.bss):存放初始值为0或者未初始化的全局变量/静态变量 commen段(.commen):注释
|
下面展开一个实验引入为什么要代码重定位。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| #include "s3c2440_soc.h" #include "uart.h" #include "init.h" int g_Char = 'A'; int g_CharB = 'B'; int g_CharC = 'C'; int g_CharD = 'D'; const int g_roval = 'C'; int g_A = 0; int g_B; int main(void) { uart0_init(); while (1) { putchar(g_Char); g_Char++; delay(1000000); } return 0; }
|
我们将上面的代码编译出的裸机程序分别烧录到nand和nor flash,看看结果:
1 2
| 1. 烧录到nor flash, 程序一直输出`AAAA`。 2. 烧录到nand flash,程序无任何输出。
|
我们发现nor启动时, 对全局变量g_char++无效, nand启动程序无任何输出。我们对程序进行反汇编处理:
可以看到.text段是从0地址开始的,证明cpu的确从0地址取指令进行译码、执行。
当从nor启动时,0地址对应nor;当从nand启动时,0地址对应sram,所以无论从nand还是从nor启动cpu都能取指令执行。
再进一步分析反汇编:
可以看到.data段的起始地址是0x8474(即g_Char变量的地址为0x8474)。
1 2
| 1. 当把程序烧录进nor, .data段在nor上的某一段区域, 由于nor能像内存一样读,但不能像内存一样直接写,因此对'g_Char'修改无效。 2. 当把程序烧录进nand, .data段在nand的某一区域,nand启动时硬件会自动把nand上的前4K数据copy到SRAM,然后cpu从sram取指令执行。但是.data段的起始地址0x8474>0x1000,超过了4K, cpu没法把.data段也copy到SRAM,所以当访问'g_Char'时,发生了异常(abt数据访问终止,这个异常后面有在下一节“异常与中断”里面专门讲解),因此程序卡死。
|
再仔细看看反汇编,发现.rodata段和.text段是连续的,但是.rodata段和.data段中间有一段”空洞”。用图形表示更形象,bin文件的内容分布如下所示:
那么我们怎么去掉空洞,让.data段了紧接着.rodata段呢?
1 2
| 1. 链接脚本(后面1.2有专门讲) 2. 直接在编译的时候用 "-Tdata 0x800",这样指定.data段基地址为0x800,这样nand启动时.data就能自动copy到SRAM了。
|
我们现在使用-Tdata 0x800
编译出裸机程序,对应反汇编如下:
这时我们烧录程序到nand,从nand启动,发现能输出ABCDEFG
了,这就对了,因为.data段数据从nand自动拷贝到了sram。
有人说为什么不吧.data段指向到dram呢,这样无论时nor启动还是nand启动不就都能对全局变量写了?
当然这个没错,我做了这个尝试,编译时用-Tdata 0x30000000
, 发现编译出来的bin文件有800多M,为什么有这么大呢?由于我们指定.data段存放在0x30000000(sdram的基地址),这时bin文件的内部结构如下所示:
这么大的bin文件根本无法烧录。通过上面的例子,现在总结下为什么要代码重定位:
1 2
| 1.nand启动,前4K代码被自动copy到sram,当程序大于4K的时候需要重定位代码到sdram。 2.nor启动, 全局变量在nor上,不能像内存一样直接写该全局变量,那么也需要重定位到sdram。
|
1.2 链接脚本
1.2.1 重定位data段
我们发现arm-linux-ld -Ttext 0 -Tdata 0x30000000
这种方式编译出来的bin文件有800多M,这肯定是不行的。可以通过AT参数指定.data段在编译时的存放位置,我们发现这样指定太不方便了,而且不好确定要放在bin文件的哪个位置。这里就要引入链接脚本,它可以帮我们解决这个不必要的麻烦。
1.2.1.1 链接脚本格式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| SECTIONS { . = 0x00000000; . = ALIGN(4); .text : { cpu/arm920t/start.o (.text) board/lyb2440/boot_init.o (.text) *(.text) }
. = ALIGN(4); .rodata : { *(.rodata) }
. = ALIGN(4); .data : 0x30000000 : AT(0x800) { *(.data) }
__bss_start = .; .bss : { *(.bss) } _end = .; }
|
这是从uboot中裁剪过来的链接脚本,注释已经链接脚本的结构讲解的差不多了。这里.data段指定了程序的运行(链接)地址为sdram的base_addr(0x30000000),通过AT指定加载(在bin文件的存放)地址0x800。
1.2.1.2 重定位data段例子
对于nor启动时,我们可以直接从nor上取指令执行,所以可以只进行数据段的重定位(数据段需要写入),我们编写链接脚本sdram.lds如下所示:
1 2 3 4 5 6
| SECTIONS { .text 0 : { *(.text) } .rodata : { *(.rodata) } .data 0x30000000 : AT(0x800) { *(.data) } .bss : { *(.bss) *(.COMMON) } }
|
将程序烧录进nor flash,程序运行时会将.data拷贝到0x0x30000000也就是sdram中去。只重定位数据段的过程用下图更直观:
因此就可以对g_char进行写入了。Makefile如下:
1 2 3 4 5 6 7 8 9 10
| 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 -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
|
修改start.s进行.data段的重定位。我们需要将以0x800为.data段基地址的整个数据段copy到0x30000000处:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| .text .global _start _start: /* 关闭看门狗 */ /* 初始化时钟 */ /* 设置栈 */ /*初始化sdram*/ ... /* 重定位data段,把加载地址0x800(bin文件中在nor中)的数据段的内容重定位到sdram的baseaddr */ mov r1, #0x800 ldr r0, [r1] mov r1, #0x30000000 str r0, [r1] bl main halt: b halt
|
用几行简单的数据加载存储指令即可实现数据段的重定位,这里是用的相对跳转指令bl main,因为还没有重定位整个完整的代码,所以不能用ldr绝对跳转。前面的初始化时钟、sdram我就不写了,参考s3c2440裸机编程-时钟体系 , s3c2440裸机编程-内存控制器。
缺点:
这里只是人为的对.data段写死了,那么当我有多个全局变量时,还要计算重定位的次数,而且我们也不知道有多少个全局变量,所以这重定位方式有缺陷。那么我们对这种重定位.data断的方法做一个改进,将链接脚本修改如下:
1 2 3 4 5 6 7 8 9 10 11 12
| SECTIONS{ .text 0 : { *(.text) } .rodata : { *(.rodata) } .data 0x30000000 : AT(0x800) { data_load_addr = LOADADDR(.data); /* data段在bin文件中的地址, 加载地址 */ data_start = . ; /* data段在重定位地址, 运行时的地址 */ *(.data) data_end = . ; /* data段结束地址 */ } .bss : { *(.bss) *(.COMMON) } }
|
链接脚本用一个变量data_load_addr指定了加载地址(data段在bin文件中的地址,即0x800),用变量data_start指定了运行地址(即为0x30000000),那么用data_end - data_start就是我们数据段的总长度。
对start.s重定位过程做出如下修改:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| /* 重定位data段 */ ldr r1, =data_load_addr /* data段在bin文件中的地址, 加载地址 */ ldr r2, =data_start /* data段在重定位地址, 运行时的地址 */ ldr r3, =data_end /* data段结束地址 */ cpy: ldrb r4, [r1] strb r4, [r2] /*r2存入data_load_addr 0x400, 然后,r2,r1依次自加*/ add r1, r1, #1 add r2, r2, #1 cmp r2, r3 ble cpy
bl main halt: b halt
|
这里start.s中用到了链接脚本中的label地址。
1.2.2 重定位整个程序段
由于我们的程序可能会大于SRAM或者nor的容量,那么就必须连代码段也一起进行重定位,这种重定位方式更好,在实际应用中也是用的这种方式去做的重定位。
1 2 3 4 5 6 7 8 9 10 11 12 13
| SECTIONS { . = 0x30000000; . = ALIGN(4); .text : { *(.text) } . = ALIGN(4); .rodata : { *(.rodata) } . = ALIGN(4); .data : { *(.data) } . = ALIGN(4); __bss_start = .; .bss : { *(.bss) *(.COMMON) } _end = .; }
|
我们将代码段的地址设置为0x3000_0004,然后紧接着放.rodata段,然后再紧接着放.data段。这样我们的bin文件就不再有“空洞”了。再来看重定位代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| .text .global _start _start: ... /* 重定位text, rodata, data段整个程序 */ mov r1, #0 ldr r2, =_start /* 第1条指令运行时的地址,也就是.text段的runtime addr,在这里是0x3000_0004*/ ldr r3, =__bss_start /* bss段的起始地址,也就是整个程序的结束地址 */ cpy: ldrb r4, [r1] strb r4, [r2] add r1, r1, #1 add r2, r2, #1 cmp r2, r3 ble cpy bl main halt: b halt
|
整个bin文件程序的长度(.text + .rodata + .data)为__bss_start - _start,那么我们是把bin文件从存储介质的0地址copy到程序的运行地址0x3000_0004,这样我们访问.data段时就是访问sdram中重定位后的数据段了。
1.2.3 重定位代码优化
1.2.3.1 strb/ldrb替换成str/ldr
前面重定位时,我们使用的是ldrb命令从的Nor Flash读取1字节数据,再用strb命令将1字节数据写到SDRAM里面。
我们开发板的Nor Flash是16位,SDRAM是32位。 假设现在需要复制16byte数据。
不同的读写指令 |
cpu读取nor的次数 |
cpu写入sdram的次数 |
ldrb、strb |
16 |
16 |
ldr、str |
8 |
4 |
可以看出我们更换读写指令后读写次数变少了,提升了cpu的访问效率。修改如下:
1 2 3 4 5 6 7 8 9
| ... cpy: ldr r4, [r1] str r4, [r2] add r1, r1, #4 //r1加4 add r2, r2, #4 //r2加4 cmp r2, r3 //如果r2 =< r3继续拷贝 ble cpy ...
|
1.2.3.2 改成c代码重定位
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| SECTIONS { . = 0x30000000; __code_start = .; . = ALIGN(4); .text : { *(.text) } . = ALIGN(4); .rodata : { *(.rodata) } . = ALIGN(4); .data : { *(.data) } . = ALIGN(4); __bss_start = .; .bss : { *(.bss) *(.COMMON) } _end = .; }
|
1 2 3 4 5 6 7 8 9 10 11 12
| 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;
while (dest < end) { *dest++ = *src++; } }
|
在start.s中设置栈指针sp后,即可执行bl copy2sdram进行重定位代码。如何设置栈指针参考s3c2440裸机编程-时钟体系 有实现,重复代码我就不贴上来了。
2 清除bss
2.1 bss段介绍
bss段是什么?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| #include "s3c2440_soc.h" #include "uart.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; }
|
把程序烧进去,然后打印g_A,但是发现g_A这个值并不是0,而是一个随机值。我们学习linux时知道全局变量g_A, g_B输出肯定是0,裸机输出不是0,为什么呢?
1 2
| 原因:程序执行汇编过程做完了重定位后把代码copy到了sdram上,然后sdram上紧接着的地址就是.bss的基地址了,这时候bss段的这块内存没有经过任何处理,所以是随机的。 那么我们重定位完代码后需要进行清除sdram上.bss段的数据,因为我们知道bss是未初始化和初始值为0的全局变量。
|
2.2 清bss
1 2 3 4 5 6 7 8 9 10 11 12 13
| SECTIONS { . = 0x30000000; . = ALIGN(4); .text :{ *(.text) } . = ALIGN(4); .rodata : { *(.rodata) } . = ALIGN(4); .data : { *(.data) } . = ALIGN(4); __bss_start = .; .bss : { *(.bss) *(.COMMON) } _end = .; }
|
清除bss段的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13
| /* 清除BSS段 */ ldr r1, =__bss_start ldr r2, =_end mov r3, #0 clean: strb r3, [r1] add r1, r1, #1 cmp r1, r2 ble clean
bl main halt: b halt
|
我们把程序再烧进去,然后打印g_A,但是发现g_A的值是0了。本质上就是对重定位后的bss段数据清0。
2.3 清bss优化
2.3.1 strb/ldrb替换成str/ldr
1 2 3 4 5 6 7 8 9 10 11
| 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
|
2.3.2 改成c代码清bss
1 2 3 4 5 6 7 8 9 10
| void clean_bss(void) { 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语言代码中获取的是链接脚本中的变量的值,所以这里的用C语言改进重定位还是清bss都是要加取址符。
2.3.3 每个段地址4字节对齐
前面为了加快重定位和清bss的速度,用到了ldr,str这样以4字节为单位进行读写,但是还可能导致一个问题,假设现在链接脚本没有进行用ALIGN(4)让不同的段以4字节对齐,那么就会出现访问错乱的情况。举个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| #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"); putchar(g_Char); return 0; }
|
将链接脚本中.data段和.bss之间的**ALIGN(4)**去掉。那么我们会发现程序执行的时候输出的g_A=0,为什么呢,我们明明初始化g_A=‘A’呀?
打开反汇编分析:
我们的.bss段紧接着.data段后面,可知在对bss段进行清除的时候,由于我们是以4字节为单位操作的,所以我们清除g_A的时候,连带g_Char,g_Char的值也一起清除了。
所以data段和数据段之间添加ALIGN(4)。修改后就会发现bss段的地址以0x30000248开始了,如下图:
3 位置无关码
3.1 绝对跳转与相对跳转指令
3.1.1 相对跳转
使用b, bl跳转指令。
对bl sdram_test
指令进行分析,查看反汇编, 代码段的链接地址为0x3000,0000。
这里的bl 3000036c不是跳转到3000036c,这个时候sdram并未初始化,那么这个物理地址是无法访问的.
为了验证,我们做另一个实验,修改连接脚本sdram.lds, 链接地址改为0x3000,0800,编译查看反汇编:
可以看到现在变成了bl 300003ec,但两个的机器码e1a0c00d都是一样的,机器码一样,执行的内容肯定都是一样的。 因此这里并不是跳转到显示的地址,而是跳转到: pc + offset,这个由链接器决定。
假设程序从0x30000000执行,当前指令地址:0x3000005c ,那么就是跳到0x3000036c;如果程序从0运行,当前指令地址:0x5c 跳到:0x000003ec。
因此:跳转到某个地址并不是由bl指令所决定,而是由当前pc值和offset偏移量决定。反汇编显示这个值只是为了方便读代码。
结论: 反汇编文件里, B或BL 某个值,只是起到方便查看的作用,并不是真的跳转。
3.1.2 绝对跳转
1 2
| //bl main /*bl相对跳转,程序仍在NOR/sram执行*/ ldr pc, =main/*绝对跳转,跳到SDRAM*/
|
3.1.3 相对跳转与决定跳转比较
怎么写位置无关码?
1 2 3 4
| 使用相对跳转命令 b或bl; 重定位之前,不可使用绝对地址(因为你sdram还没初始化,没有重定位代码过去,跳转过去不就死机了),也不可访问有初始值的数组(因为初始值放在rodata里,使用绝对地址来访问); 重定位之后,使用ldr pc = xxx,跳转到/runtime地址; 写位置无关码,其实就是不使用绝对地址
|
因此,前面的重定位和清bss例子,程序使用bl命令相对跳转,程序仍在NOR/sram执行,要想让main函数在SDRAM执行,需要修改代码:
1 2
| //bl main /*bl相对跳转,程序仍在NOR/sram执行*/ ldr pc, =main/*绝对跳转,跳到SDRAM*/
|