Part10: 代码重定位与位置无关码
1 代码重定位
什么是代码重定位呢?
来一个例子,对于S3C2440这款开发板,Nor Flash可以直接读,但不能直接写(可通过代码验证,此处忽略)。
但是如果要写的话,如何实现?
这时候就要用“代码重定位”了。有两个办法,一是重定位需写的数据到SDRAM 二 是重定位整个程序到SDRAM
下面依次讲解这两个方法。
1.1 重定位需写的数据到SDRAM
直接上代码,再解释。如下
char g_Char = 'A';
int main(void)
{
uart0_init(); // 串口初始化
while (1)
{
putchar(g_Char);
g_Char++; /* nor启动时, 此代码无效 */
delay(1000000);
}
return 0;
对于g_Char 这个全局变量,烧写到Nor Flash是不能直接修改的,故g_Char++会无效
因此,这里需重定位g_Char到sdram(即0x3000,0000处), 如何实现呢?
- 使用链接脚本 sdram.lds
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
clean:
rm *.bin *.o *.elf *.dis
2.修改start.S文件,将g_Char 写到 0x30000000,如下
bl sdram_init //注意,使用sdram前,需初始化
mov r1, #0x800
ldr r0, [r1]
mov r1, #0x30000000
str r0, [r1]
bl main
- sdram.s 链接脚本内容如下
SECTIONS {
.text 0 : { *(.text) }
.rodata : { *(.rodata) }
.data 0x30000000 : AT(0x800) { *(.data) }
.bss : { *(.bss) *(.COMMON) }
}
补充下链接脚本的语法
SECTIONS {
...
secname start BLOCK(align) (NOLOAD) : AT ( ldadr )
{ contents } >region :phdr =fill
...
}
解释:secname:段名 start:运行时地址(runtime addr) 或 重定位地址(relocate addr)
BLOCK(align) (NOLOAD )很少用,用到再查 AT(ldadr): ldadr即load addr加载地址,当未指定时,= runtime addr
{ contents } 即这个段的内容,可以指定某个目标文件对应的段 如 .text 0 : { start.o }即代码段为start.o的代码段
且放在运行时地址0. 还可指定所有目标文件的对应的段,如 .data 0xXXX : { *(.data) } 表示所有目标文件的data段
放在0xXXX开始处
>region:phdr = fill 基本不用,忽略
更详细的用法可参考官方文档:http://ftp.gnu.org/old-gnu/Manuals/ld-2.9.1/html_mono/ld.html
以上面的链接脚本为例,解释如下
可能难点在运行地址 和 加载地址的 理解,对于这两个,可理解为,加载地址指 代码在“可执行程序”中的地址,
而 运行地址 即 程序加载进内存运行的地址。
1.2 重定位整个程序到SDRAM
- sdram.lds 链接脚本如下
SECTIONS
{
. = 0x30000000;
. = ALIGN(4);
.text :
{
*(.text)
}
. = ALIGN(4);
.rodata : { *(.rodata) }
. = ALIGN(4);
.data : { *(.data) }
. = ALIGN(4);
__bss_start = .;
.bss : { *(.bss) *(.COMMON) }
_end = .;
}
- 修改start.S 文件
bl sdram_init
/* 下面是复制整个程序到SDRAM的0x3000,0000开始处 */
mov r1, #0
ldr r2, =_start
ldr r3, =__bss_start
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
- main函数如下
char g_Char = 'A';
int g_A = 0;
int main(void)
{
puts("\n\rg_A=");
printHex(g_A);
puts("\n\r");
while (1)
{
putchar(g_Char);
g_Char++; /* nor启动时, 此代码无效 */
delay(1000000);
}
return 0;
}
上面两个实验的效果都是一样的,如下
2 位置无关码
其实,上面将整个程序重定位到SDRAM的0x30000000开始的地方,就实现了位置无关码
怎么理解位置无关码呢?看下图
如图所示,整个程序的前面部分代码需将整个程序复制到SDRAM的开始处0x3000,0000。
那红框部分代码就是位置无关码? 其实这样理解是不对的(我一开始也这样认为:)
实际,“位置无关码”是这整个程序。因为整个程序实际可复制到SDRAM任意地方,但是这整个程序里面的
代码有要求:
1) 在重定位整个程序到SDRAM前,不可使用绝对地址。因为还未将整个程序复制到SDRAM,那引用
SDRAM里面地址,肯定不行的啊。 具体来说,重定位前避免使用全局变量/全局静态变量,或已初始化的数组
因为这些变量在rodata 或 data段,这些段的地址是绝对地址。
2)在重定位前的代码,尽量使用相对跳转指令,如B/BL。为什么呢?整个程序重定位到新地址后,基址改变了,
但是相对偏移量offset是不变的,即寻址 PC + offset。 不管怎么修改PC,offset不变,那访问时只需修改PC即可
3)重定位之后, 使用绝对跳转命令跳到Runtime Addr,比如:
ldr pc, =main
上面三点,也就是如何写“位置无关码”的三大技巧
2.1 C中如何使用链接脚本的变量
OK,理解了“代码重定位” 和 “位置无关码”,那怎么完善上面第二个程序呢?即使用ldr的变量,用C替换汇编实现
整个程序的复制和BSS段的清零工作呢?
实现如下
对于 复制整个程序,如下
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++;
}
}
对于 清零BSS段,如下
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;
}
}
因此,start.S 即可修改为
bl sdram_init
/* 重定位text, rodata, data段整个程序 */
bl copy2sdram
/* 清除BSS段 */
bl clean_bss
上面C中的变量来自sdram.lds链接脚本,如下
SECTIONS
{
. = 0x30000000;
__code_start = .;
. = ALIGN(4);
.text :
{
*(.text)
}
. = ALIGN(4);
.rodata : { *(.rodata) }
. = ALIGN(4);
.data : { *(.data) }
. = ALIGN(4);
__bss_start = .;
.bss : { *(.bss) *(.COMMON) }
_end = .;
}
总结一下
C函数怎么使用lds文件中的变量abc?
a. 在C函数中声明改变量为extern类型, 比如:
extern int abc;
b. 使用时, 要取址, 比如:
int *p = &abc; // p的值即为lds文件中abc的值
OK,以上就是这篇文章的所有内容(我觉得讲得不够详细,后面有时间再完善,有问题可以提出来,一起进步呀:)