4.数据段重定位
4.1. 怎么获得各个段的信息
数据复制3要素:源、目的、长度。
怎么知道某个段的加载地址、链接地址、长度?
4.1.1 怎么确定源?
可以用ADR伪指令获得当前代码的地址,对于这样的代码:
.text .global _start _start: ...... adr r0, _start
adr是伪指令,它最终要转换为真实的指令。它怎么获得_start代码的当前所处地址呢?
实际上,adr r0, _start指令的本质是r0 = pc - offset,offset是在链接时就确定了。
4.1.2 怎么确定目的地址?
也就是怎么确定链接地址?可以用LDR伪指令。
对于这样的代码:
.text .global _start _start: ...... ldr r0, =_start
ldr是伪指令,它最终要转换为真实的指令。它怎么获得_start的链接地址呢?
_start的链接地址在链接时,由链接脚本确定。
4.1.3 如何获得更详细的信息
在链接脚本里可以定义各类符号,在代码里读取这些符号的值。
比如对于下面的链接脚本,可以使用__bss_start、__bss_end得到BSS段的起始、结束地址:
__bss_start = .; .bss : { *(.bss) *(.COMMON) } __bss_end = .;
上述代码里,有一个".“,它被称为"Location Counter”,表示当前地址:可读可写。
它表示的是链接地址。
. = 0xABC; /* 设置当前地址为0xABC */ _abc_addr = . ; /* 设置_abc_addr等于当前地址 */ . = . + 0x100; /* 当前地址增加0x100 */ . = ALIGN(4); /* 当前地址向4对齐 */
注意:"Location Counter"只能增大,不能较小。
4.2. 编写程序重定位数据段
4.2.1 修改链接脚本
我们故意只重定位数据段,在后面的课程再来重定位代码段并引入更多知识。
数据段要被复制到哪去?需要在链接脚本里确定一下:增加了__data_start
SECTIONS { . = 0xC0200000; /* 对于STM32MP157设置链接地址为0xC0200000, 对于IMX6ULL设为0x80200000 */ . = ALIGN(4); .text : { *(.text) } . = ALIGN(4); __rodata_start = .; .rodata : { *(.rodata) } . = ALIGN(4); .data : { *(.data) } . = ALIGN(4); __bss_start = .; .bss : { *(.bss) *(.COMMON) } __bss_end = .; }
4.2.2 编写程序
修改start.S:
ldr r0, =__data_start /* 目的: 链接地址 */ /* 计算data段的当前地址: * _start的链接地址 - _start的当前地址 = __rodata_start的链接地址 - rodata段的当前地址 * data段的当前地址 = __rodata_start的链接地址 - (_start的链接地址 - _start的当前地址) */ ldr r0, =__rodata_start ldr r2, =_start /* link addr */ adr r3, _start /* load addr */ sub r2, r2, r3 sub r1, r0, r2 /* 源 */ ldr r3, =__bss_start sub r2, r3, r0 bl memcpy /* r0: 目的, r1: 源, r2:长度 */
5.清除BSS段
5.1. C语言中的BSS段
程序里的全局变量,如果它的初始值为0,或者没有设置初始值,这些变量被放在BSS段里。
char g_Char = 'A'; const char g_Char2 = 'B'; int g_A = 0; // 放在BSS段 int g_B; // 放在BSS段
BSS段并不会放入bin文件中,否则也太浪费空间了。
在使用BSS段里的变量之前,把BSS段所占据的内存清零就可以了。
5.2. 清除BSS段
5.2.1 BSS段在哪?多大?
在链接脚本中,BSS段如下描述:
SECTIONS { . = 0xC0200000; /* 对于STM32MP157设置链接地址为0xC0200000, 对于IMX6ULL设为0x80200000 */ . = ALIGN(4); .text : { *(.text) } . = ALIGN(4); __rodata_start = .; .rodata : { *(.rodata) } . = ALIGN(4); .data : { *(.data) } . = ALIGN(4); __bss_start = .; .bss : { *(.bss) *(.COMMON) } __bss_end = .; }
BSS段的起始地址、结束地址,使用__bss_start和__bss_end来获得,它们是链接地址。
5.2.2 怎么清除BSS段
ldr r0, =__bss_start /* 目的 */ mov r1, #0 /* 值 */ ldr r2, =__bss_end sub r2, r2, r1 /* 长度 */ bl memset /* r0: 目的, r1: 值, r2: 长度 */
6.代码段重定位
6.1. 代码段不重定位的后果
谁执行了数据段的重定位?
谁清除了BSS段?
都是程序自己做的,也就是代码段那些指令实现的。
代码段并没有位于它的链接地址上,并没有重定位,为什么它也可以执行?
因为重定位之前的代码是使用位置无关码写的,后面再说。
如果代码段没有重定位,则不能使用链接地址来调用函数:
汇编中
ldr pc, =main ;
这样调用函数时,用到main函数的链接地址,如果代码段没有重定位,则跳转失败
C语言中
void (*funcptr)(const char *s, unsigned int val); funcptr = put_s_hex; funcptr("hello, test function ptr", 123);
6.2. 代码段重定位
6.2.1 代码段在哪?多大?
这要看链接脚本,对于MPU的程序,代码段、数据段一般是紧挨着排列的。
所以重定位时,干脆把代码段、数据段一起重定位。
链接脚本
SECTIONS { . = 0xC0200000; /* 对于STM32MP157设置链接地址为0xC0200000, 对于IMX6ULL设为0x80200000 */ . = ALIGN(4); .text : { *(.text) } . = ALIGN(4); __rodata_start = .; .rodata : { *(.rodata) } . = ALIGN(4); .data : { *(.data) } . = ALIGN(4); __bss_start = .; .bss : { *(.bss) *(.COMMON) } __bss_end = .; }
对于这样的代码:
.text .global _start _start:
确定目的
ldr r0, =_start
确定源
adr r1, _start
确定长度
ldr r3, =__bss_start sub r2, r3, r0
6.2.2 怎么重定位
ldr r0, =_start adr r1, _start ldr r3, =__bss_start sub r2, r3, r0 bl memcpy
6.3 为什么重定位之前的代码也可以正常运行?
因为重定位之前的代码是使用位置无关码写的:
只使用相对跳转指令:b、bl
不只用绝对跳转指令:
ldr pc, =main
不访问全局变量、静态变量、字符串、数组
重定位完后,使用绝对跳转指令跳转到XXX函数的链接地址去
bl main // bl相对跳转,程序仍在原来的区域运行 ldr pc, =main // 绝对跳转,跳到链接地址去运行 ldr r0, =main // 更规范的写法,支持指令集切换 blx r0
7.重定位的纯C函数实现
7.1. 怎么得到链接脚本里的值
对于这样的链接脚本,怎么得到其中的__bss_start和 __bss_end:
SECTIONS { . = 0xC0200000; /* 对于STM32MP157设置链接地址为0xC0200000, 对于IMX6ULL设为0x80200000 */ . = ALIGN(4); .text : { *(.text) } . = ALIGN(4); __rodata_start = .; .rodata : { *(.rodata) } . = ALIGN(4); .data : { *(.data) } . = ALIGN(4); __bss_start = .; .bss : { *(.bss) *(.COMMON) } __bss_end = .; }
7.1.1 汇编代码
ldr r0, =__bss_start ldr r1, =__bss_end
7.1.2 C语言
方法1
声明为外部变量,使用时需要使用取址符:
extern unsigned int __bss_start; extern unsigned int __bss_end; unsigned int len; len = (unsigned int)&__bss_end - (unsigned int)&__bss_start; memset(&__bss_start, 0, len);
方法2
声明为外部数组,使用时不需要使用取址符:
extern char __bss_start[]; extern char __bss_end[]; unsigned int len; len = __bss_end - __bss_start; memset(__bss_start, 0, len);
7.2. 怎么理解上述代码
对于这样的C变量:
int g_a;
编译的时候会有一个符号表(symbol table),如下:
Name | Address |
g_a | xxxxxxxx |
对于链接脚本中的各类Symbol,有2中声明方式:
extern unsigned int __bss_start; // 声明为一般变量 extern char __bss_start[]; // 声明为数组
不管是哪种方式,它们都会保存在符号表里,比如:
Name | Address |
g_a | xxxxxxxx |
__bss_start | yyyyyyyy |
对于int g_a变量
使用&g_a得到符号表里的地址。
对于extern unsigned int __bss_start变量
要得到符号表中的地址,也是使用&__bss_start。
对于extern char __bss_start[]变量
要得到符号表中的地址,直接使用__bss_start[],不需要加&
为什么?`__bss_start本身就表示地址啊