程序段与重定位
S3C2440的CPU可以直接给SDRAM发送命令、给Nor Flash发送命令、给4K的片内SDRAM 发送命令,但是不能直接给Nand Flsh发送命令
Nand Flash 启动需要重定位的原因
假如把程序烧写到Nand Flash上,即向Nand Flash烧入 bin 文件,CPU是无法从Nand Flash中取代码执行的。上电后,Nand启动硬件会自动把Nand Flash前4K复制到片内SDRAM;CPU从0地址运行, 0地址是在片内SDRAM上;如果程序大于4K, 前4K的代码需要把整个程序从nand flash 上读出来放到内存SDRAM中(即代码重定位)。
Nor Flash启动需要重定位的原因
Nor Flash启动时,此时CPU认为的 0地址 在Nor Flash上面,片内SDRAM的基地址就变成了0x40000000(Nand启动时片内SDRAM的基地址基地址是0),由于Nor Flash特性:可以像内存一样读,但不能像内存直接写,因此需要把全局变量和静态变量重定位放到内存SDRAM里。因为程序运行时,要初始化全局变量和静态变量,写这些变量,确定他们的值。
例如执行如下几条汇编指令
MOV R0, #0 //0地址
LDR R1, [R0] @读有效 //读NOR零地址
STR R1, [R0] @写无效 //写NOR零地址
当程序中含有需要写的全局变量或静态变量时,假如是在Nand Flash可以正常操作,如果是在Nor Flash,修改无效。因此需要把全局变量和静态变量重定位放到内存SDRAM。
写一个测试程序来测试:
#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;
}
烧写程序:
烧写在NORFlash 和 烧写在NANDFlash观察这两种的效果。
设置成NANDFlash启动没有问题 显示ABCDE...
设置成NORFlash启动显示AAA...
对于NOR启动时g_Char++; /* nor启动时, 此代码无效 */
程序段
一个程序里面有
- .text 代码段
- .data 数据段
- rodata 只读数据段(const全局变量)
- bss段 (初始值为0,无初始值的全局变量)
- commen 注释
其中bss段和commen 注释不保存在bin文件中。
上面测试程序的各个段的位置如下:
Disassembly of section .data:
00000700 <__data_start>:
700: Address 0x700 is out of bounds. //数据段
Disassembly of section .rodata:
//放在只读数据段内
00000474 : //const char g_Char2 = 'B';
474: Address 0x474 is out of bounds.
Disassembly of section .bss: //bss段
00000804 : //int g_A = 0;
804: 00000000 andeq r0, r0, r0
00000808 : //int g_B;
808: 00000000 andeq r0, r0, r0
Disassembly of section .comment:
链接脚本
链接脚本的语法:
SECTIONS {
...
secname start BLOCK(align) (NOLOAD) : AT ( ldadr )
{ contents } >region :phdr =fill
...
}
需要依次排列 代码段、只读数据段、数据段、.bss段、.common。
从加载地址0x700重定位到运行时地址0x30000000
其中数据段放在0x700,但运行时在0x3000000:
SECTIONS {
.text 0 : { *(.text) }//所有文件的.text代码段
.rodata : { *(.rodata) } //只读数据段
.data 0x30000000 : AT(0x700) { *(.data) } //数据段放在0x700,但运行时在0x3000000!!!!!!
.bss : { *(.bss) *(.COMMON) }//所有文件的bss段,所有文件的.COMMON段
}
重新编译后烧写bin文件,发现启动后显示乱码。原因是我们从0x30000000处获取g_Char,但在这之前,并没有在0x30000000处准备好数据。因此需要重定位数据段,将0x700的数据移动到0x30000000处,在start.S加入:
bl sdram_init
/* 重定位data段 */
mov r1, #0x700
ldr r0, [r1]
mov r1, #0x30000000
str r0, [r1]
bl main
上面的这种方法,只能复制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:
ldrb r4, [r1] //从r1读到r4
strb 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
链接脚本的语法
SECTIONS {
...
secname start BLOCK(align) (NOLOAD) : AT ( ldadr )
{ contents } >region :phdr =fill
...
}
解释:
secname :段名
start :起始地址:运行时的地址(runtime addr);重定位地址(relocate addr)
AT ( ldadr ) :可有可无(load addr:加载地址) 不写时LoadAddr = runtime addr
{ contents } 的内容:
start.o //内容为start.o文件
*(.text)所有的代码段文件
start.o *(.text)文件
bin文件重定位
1 elf生成bin文件
2 硬件机制启动
3 如果bin文件所在位置 不等于runtimeaddr ,程序本身实现重定位
bin文件/elf文件都不保存bss段 这些都是初始值为0 或者没有初始化的全局变量
程序运行时要把bss段对应的空间清零,否则未初始化的g_A会等于莫名奇妙的值,并不等于0
清零bss段
修改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段
/* 清除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:
b halt
现在的代码全局变量就是为0,通过几行代码,就可以少几十个甚至上千个全局变量的存储空间。
ALIGN(4)向4取整
SECTIONS
{
. = 0x30000000;
. = ALIGN(4);
.text :
{
*(.text)
}
. = ALIGN(4);
.rodata : { *(.rodata) }
. = ALIGN(4);
.data : { *(.data) }
. = ALIGN(4);
__bss_start = .;
.bss : { *(.bss) *(.COMMON) }
_end = .;
}
Uboot是裸机的集大成者,可以参考uboot链接脚本也是类似的。
代码重定位与位置无关码
一个程序,由代码段、只读数据段、数据段、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 = .;
}
现在写的这个链接脚本,称为一体式链接脚本,对比前面的分体式链接脚本区别在于代码段和数据段的存放位置是否是分开的。
修改start.S段
/* 重定位text, rodata, data段整个程序 */
mov r1, #0
ldr r2, =_start /* 第1条指令运行时的地址 0x30000000*/
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
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
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 某个值,只是起到方便查看的作用,并不是真的跳转。