本文使用的开发板是九鼎创展的X210 iNand版本。
本文要完成的功能是:在SRAM中将代码从0xd0020010重定位到0xd0024000(本来代码是运行在0xd0020010的,但我们又希望代码实际是在0xd0024000位置运行的,这时就需要重定位了)。
一、思路分析
(1)通过链接脚本将代码链接到0xd0024000
(2)dnw下载时将bin文件下载到0xd0020010
(3)代码执行时,通过代码前段的少量PIC位置无关码将整个代码搬移到0xd0024000
(4)使用一个长跳转,跳转到0xd0024000处的代码继续执行,重定位完成
通过(1)和(2)就保证了代码实际下载运行在0xd0020010,但是却被链接在0xd0024000,从而为重定位奠定了基础。当我们把代码链接地址设置为0xd0024000时,实际隐含意思就是这个代码将来必须放在0xd0024000位置才能正确执行。如果实际运行地址不是0xd0024000就要出事(除非代码是PIC位置无关码)。
当我们执行完代码重定位后,实际上在SRAM中有2份代码的镜像:一份是我们下载到0xd0020010处开头的,另一份是重定位代码复制到0xd0024000处开头的,这两份内容完全相同,仅仅地址不同。重定位之后使用ldr pc, =led_blink这句长跳转直接从0xd0020010处的代码跳转到0xd0024000开头的那份代码的led_blink函数去执行(实际上此时在SRAM中有2个led_blink函数的镜像,两个都能执行。如果短跳转bl led_blink则执行的就是0xd0020010开头的这一份,如果长跳转ldr pc, =led_blink则执行的就是0xd0024000开头的这一份)。
当链接地址和运行地址相同时,短跳转和长跳转实际效果是一样的。当链接地址和运行地址不同时,短跳转实际执行的是运行地址处的那一份代码,而长跳转执行的是链接地址处的那一份代码。
通过以上信息,就知道重定位代码的作用是:在PIC位置无关码执行完之前(代码中第一句位置有关码执行之前)必须将整个代码搬移到0xd0024000位置去执行。
二、代码实现
1、Makefile
led.bin: start.o led.o arm-linux-ld -Tlink.lds -o led.elf $^ arm-linux-objcopy -O binary led.elf led.bin arm-linux-objdump -D led.elf > led_elf.dis gcc mkv210_image.c -o mkx210 ./mkx210 led.bin 210.bin %.o : %.S arm-linux-gcc -o $@ $< -c -nostdlib %.o : %.c arm-linux-gcc -o $@ $< -c -nostdlib clean: rm *.o *.elf *.bin *.dis mkx210 -f
2、链接脚本(link.lds)
SECTIONS { . = 0xd0024000; .text : { start.o * (.text) } .data : { * (.data) } bss_start = .; .bss : { * (.bss) } bss_end = .; }
3、start.S
#define SVC_STACK 0xD0037D80 .global _start _start: // 设置SVC栈 ldr sp, =SVC_STACK // 重定位 // adr指令用于加载_start当前的运行地址 adr r0, _start // ldr指令用于加载_start的链接地址:0xd0024000 ldr r1, =_start // bss段的起始地址 ldr r2, =bss_start // 重定位代码的结束地址,重定位只需重定位代码段和数据段即可 // 比较_start的运行地址和链接地址是否相等 cmp r0, r1 // 如果相等说明不需要重定位,所以跳过copy_loop,直接到clean_bss beq clean_bss // 用汇编实现的一个while循环 copy_loop: ldr r3, [r0], #4 // 源 str r3, [r1], #4 // 目标 这两句代码就完成了4个字节内容的拷贝 cmp r1, r2 // r1和r2都是用ldr加载的,都是链接地址,所以r1不断+4总能等于r2 bne copy_loop // 清bss段,其实就是在链接地址处把bss段全部清零 clean_bss: ldr r0, =bss_start ldr r1, =bss_end cmp r0, r1 // 如果r0等于r1,说明bss段为空 beq run_on_dram // 清除完bss之后的地址 mov r2, #0 clear_loop: str r2, [r0], #4 // 先将r2中的值放入r0所指向的内存地址,然后r0 = r0 + 4 bne clear_loop run_on_dram: // 长跳转到led_blink开始第二阶段 ldr pc, =led_blink b .
4、led.c
#define GPJ0CON 0xE0200240 #define GPJ0DAT 0xE0200244 #define rGPJ0CON *((volatile unsigned int *)GPJ0CON) #define rGPJ0DAT *((volatile unsigned int *)GPJ0DAT) void delay(void); void led_blink(void) { rGPJ0CON = 0x11111111; while(1) { // led亮 rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5)); // 延时 delay(); // led灭 rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5)); // 延时 delay(); } } void delay(void) { volatile unsigned int i = 900000; while (i--); }
重定位就是汇编代码中的copy_loop函数,代码的作用是使用循环结构来逐句复制代码到链接地址。复制的源地址是SRAM的0xd0020010,复制的目标地址是SRAM的0xd0024000,复制的长度是bss_start - _start,所以复制的长度就是整个重定位需要重定位的长度,也就是整个程序中代码段 + 数据段的长度,bss段(bss段是初始化为0的全局变量)不需要重定位。
清除bss段是为了满足C语言的运行时要求(C语言要求显式初始化为0的全局变量,或者未显式初始化的全局变量的值为0),实际上C语言编译器就是通过清bss段来实现C语言的这个特性的。一般情况下我们的程序是不需要负责清bss段的,因为C语言编译器和链接器会帮我们的程序自动添加一段头程序,这段程序会在main函数之前运行,这段代码就负责清除bss。但是在我们代码重定位了之后,因为编译器帮我们附加的代码只是帮我们清除了运行地址那一份代码中的bss,而未清除重定位地址处的那一份代码的bss,所以重定位之后需要自己去清除bss。
清理完bss段后重定位就结束了。此时的状况是:
(1)当前运行地址还在0xd0020010开头的那份代码中运行着
(2)SRAM中已经有了2份代码,1份在0xd0020010开头,另一份在0xd0024000开头
最后执行ldr pc, =led_blink这句长跳转直接从0xd0020010处的代码跳转到0xd0024000开头的那份代码的led_blink函数去执行。