LV010-段的重定位
一、重定位的概念
1. 什么是重定位
接触过 S3C2440 的话应该会了解到,在程序运行之前我们需要手动将.bin 文件上的全部代码从 Nor Flash 或 Nand Flash 拷贝到 SDRAM 上。对于 imx6ull 来说,这部分拷贝代码的操作由 Boot Rom 自动完成,板子上电后 boot Rom 会将映像文件从启动设备(TF 卡、 eMMC)自动拷贝到 DDR3 内存上。上述拷贝代码的过程就是 重定位。
那么 boot Rom 应该将映像文件拷贝到内存的哪个位置呢?这部分内容已经《25-裸机开发/05-imx6ull 启动/LV020-映像文件.md》中学习过了 。映像文件包含多个部分,其中.bin 文件的起始地址由地址 entry 决定,需要在 Makefile 中手动配置。
./mkimage -n imximage.cfg.cfgtmp -T imximage -e 0x80100000 -d demo.bin demo.imx按照上述的配置,整个映像文件被自动重定位到 DDR 内存上,其中.bin 文件的起始地址为 0x80100000。重定位结束后, CPU 会从这个地址读取第一条指令开始执行程序。
2. 为什么要重定位?
int main(int argc, const char * argv[])
{
Uart_Init(); //初始化 uart 串口
printf("\n\r");
/* 在串口上输出 g_charA */
while (1)
{
PutChar(g_charA);
g_charA++;
delay(1000000);
}
return 0;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
上述代码在程序运行时, CPU 需要不断地访问 DDR3 内存来获取 g_charA 的值,访问 DDR3 会花费大量的时间,那么如何提升访问的效率呢?
答:在程序运行先前将 data 段的数据重定位到 imx6ull 的片内 RAM 上,因为 CPU 访问片内 RAM 的速度远快于访问 DDR3 的速度。
二、汇编重定位 data 段
完整代码可以看这里:
1. 参考芯片手册确定片内 RAM 位置
我们可以查看参考手册的《Chapter 2: Memory Maps》 :

参考芯片手册得到片内 RAM 的地址为: 0x900000 ~ 0x91FFFF。所以我们将.data 段重定位后的地址设置为 0x900000。
2. 链接脚本修改
创建一个变量用来存储.data 段的起始加载地址。
. = ALIGN(4);
.rodata : { *(.rodata) }
. = ALIGN(4);
data_load_addr = .;//将当前地址存储在变量中(大概的值为 0x8880xxxx)2
3
4
将.data 段的运行地址(runtime address)设定为 0x900000。加载地址由变量 data_load_addr 确定。这样设置后,在.bin 文件中“ .data”段仍旧存储在 “ .rodata ” 段之后。但在程序运行时, CPU 会从 0x900000 开始的空间内读取 “.data 段” 的值。
.data 0x900000 : AT(data_load_addr)下面我们将重定位后.data 段的起始地址存储在变量 data_start,重定位后的.data 段的结束地址存储在变量 data_end,这两个变量将供汇编文件调用。
{
data_start = . ;
*(.data)
data_end = . ;
}2
3
4
5
修改后的链接脚本如下所示:
SECTIONS {
. = 0x80100000;
. = ALIGN(4);
.text :
{
*(.text)
}
. = ALIGN(4);
.rodata : { *(.rodata) }
. = ALIGN(4);
data_load_addr = .;
.data 0x900000 : AT(data_load_addr)
{
data_start = . ;
*(.data)
data_end = . ;
}
. = ALIGN(4);
__bss_start = .;
.bss : { *(.bss) *(.COMMON) }
__bss_end = .;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
通过上述操作, CPU 虽然会去片内 RAM 中读取.data 段数据,但 实际上片内 RAM 并没有准备好.data 段的数据,如下图所示。下面我们将通过汇编将 DDR 内存上的.data 段数据重定位到片内 RAM 上。

3. 修改汇编文件重定位.data 段
3.1 测试一下在 DDR 内部的 data 段重定位
这里完整的测试代码可以看这里:09_RELOCATION/03_without_relocation · sumumm/imx6ull-bare-demo。我们可以将之前定义的各个变量的地址打印出来:
#include "uart.h"
#include "my_printf.h"
char g_charA = 'A'; //存储在 .data 段
const char g_charB = 'B'; //存储在 .rodata 段
const char g_charC; //存储在 .bss 段
int g_intA = 0; //存储在 .bss 段
int g_intB; //存储在 .bss 段
void delay (volatile int time)
{
while(time--);
}
int main(int argc, const char * argv[])
{
Uart_Init(); //初始化 uart 串口
printf("g_charA=%d &g_charA=0x%x\n\r", g_charA, &g_charA);
printf("g_charB=%d &g_charB=0x%x\n\r", g_charB, &g_charB);
printf("g_charC=%d &g_intA=0x%x\n\r", g_charC, &g_charC);
printf("g_intA=%d &g_intA=0x%x\n\r", g_intA, &g_intA);
printf("g_intB=%d &g_intB=0x%x\n\r", g_intB, &g_intB);
/* 在串口上输出 g_charA */
while (1)
{
//printf("g_intA =%d &g_intA = 0x%x\n\r", g_intA, &g_intA);
g_charA++;
delay(1000000);
}
return 0;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
我们的链接文件如下:
SECTIONS {
. = 0x80100000;
. = ALIGN(4);
.text :
{
*(.text)
}
. = ALIGN(4);
.rodata : { *(.rodata) }
. = ALIGN(4);
.data 0x80200000 : { *(.data) }
. = ALIGN(4);
__bss_start = .;
.bss : { *(.bss) *(.COMMON) }
__bss_end = .;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
可以看到打印信息如下:

然后我们修改链接文件如下:
SECTIONS {
. = 0x80100000;
. = ALIGN(4);
.text :
{
*(.text)
}
. = ALIGN(4);
.rodata : { *(.rodata) }
. = ALIGN(4);
.data 0x80140000 : { *(.data) }
. = ALIGN(4);
__bss_start = .;
.bss : { *(.bss) *(.COMMON) }
__bss_end = .;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
会看到变量的地址也随着发生了变化:

3.2 重定位到片内 RAM
完整代码可以看这里:09_RELOCATION/04_manual_relocate_data · sumumm/imx6ull-bare-demo。设置完栈后直接跳转到 copy_data 函数重定位 data 段。
.text
.global _start
_start:
/* 设置栈 */
ldr sp,=0x80200000
/* 重定位data段 */
bl copy_data
/* 清除bss段 */
bl clean_bss2
3
4
5
6
7
8
9
10
11
12
13
燃弧我们来实现这个 copy_data 函数 :
copy_data:
/* 重定位data段 */
ldr r1, =data_load_addr /* data段的加载地址 (0x8010....) */
ldr r2, =data_start /* data段重定位地址, 0x900000 */
ldr r3, =data_end /* data段结束地址(重定位后地址 0x90....) */
cpy:
ldr r4, [r1] /* 从r1读到r4 */
str r4, [r2] /* r4存放到r2 */
add r1, r1, #4 /* r1+1 */
add r2, r2, #4 /* r2+1 */
cmp r2, r3 /* r2 r3比较 */
bne cpy /* 如果不等则继续拷贝 */
mov pc, lr2
3
4
5
6
7
8
9
10
11
12
13
14
3.3 测试结果
我们烧写到开发板,会有以下打印信息:
可以看到.data 已经被定位到 0x00900000。
三、C 函数重定位 data 段和清除 bss 段
目前为止我们已经通过汇编实现了重定位 data 段和清除 bss 段。为了让汇编程序更加简洁,这一节中我们将通过 C 语言实现重定位 data 段和清除 bss 段。
1. 通过汇编传递链接脚本变量
这一部分我们来学习通过汇编文件获得链接脚本中的变量,再将这些变量传递给 C 函数。 完整代码可以看这里:09_RELOCATION/05_relocate_data_with_c_use_start · sumumm/imx6ull-bare-demo
1.1 修改汇编文件
打开 start.S 将之前的汇编函数 copy_data, clean_bss 删除,改为直接调用 C 函数。在调用对应的 C 函数之前,需要通过寄存器 r0~r4 将 C 函数的参数准备好。
.text
.global _start
_start:
/* 设置栈 */
ldr sp,=0x80200000
/* 重定位data段 */
ldr r0, =data_load_addr /* data段的加载地址 (0x8010....) */
ldr r1, =data_start /* data段重定位地址, 0x900000 */
ldr r2, =data_end /* data段结束地址(重定位后地址 0x90....) */
sub r2, r2, r1 /* r2的值为data段的长度 */
bl copy_data /* 跳转到函数copy_data并将r0,r1,r2作为函数参数传入 */
/* 清除bss段 */
ldr r0, =__bss_start
ldr r1, =__bss_end
bl clean_bss /* 跳转到函数clean_bss并将r0, r1作为函数参数传入*/
/* 跳转到主函数 */
bl main
halt:
b halt2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
1.2 实现 copy_data, clean_bss 函数
我们创建程序文件 init.c ,在里面实现 copy_data, clean_bss 函数 :
void copy_data (volatile unsigned int *src, volatile unsigned int *dest, unsigned int len) /* src, dest, len */
{
unsigned int i = 0;
while (i < len)
{
*dest++ = *src++;
i += 4;
}
}
void clean_bss (volatile unsigned int *start, volatile unsigned int *end) /* start, end */
{
while (start <= end)
{
*start++ = 0;
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
对于 copy_data 函数来说,参数 src, dest, len 分别对应汇编文件中 r0, r1, r2 的值。
对于 clean_bss 函数来说,参数 start, end 分别对应汇编文件中 r0, r1 的值
1.3 修改 Makefile
修改 Makefile 文件,编译 init.c 并链接 init.o。
1.4 测试效果
我们编译并烧写到开发板中,看到以下打印信息说明我们在 C 语言实现了重定位和清除 BSS 段:

2. C 函数直接调取链接脚本变量
上一节中 C 函数需要通过汇编文件传入参数,在这一节我们将进一步改进 C 函数,使得 C 函数跳过汇编文件,直接从链接脚本中调用所需变量。完整代码可以看这里:09_RELOCATION/06_relocate_data_with_c_use_lds · sumumm/imx6ull-bare-demo
2.1 修改汇编文件为直接调用 C 函数
.text
.global _start
_start:
/* 设置栈 */
ldr sp,=0x80200000
/* 重定位data段 */
bl copy_data
/* 清除bss段 */
bl clean_bss
/* 跳转到主函数 */
bl main
halt:
b halt2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2.2 修改 init.c 通过函数来获取参数
void copy_data (void)
{
/* 从链接脚本中获得参数 data_load_addr, data_start, data_end */
extern int data_load_addr, data_start, data_end;
volatile unsigned int *dest = (volatile unsigned int *)&data_start;
volatile unsigned int *end = (volatile unsigned int *)&data_end;
volatile unsigned int *src = (volatile unsigned int *)&data_load_addr;
/* 重定位数据 */
while (dest < end)
{
*dest++ = *src++;
}
}
void clean_bss(void)
{
/* 从 lds 文件中获得 __bss_start, __ bss_end */
extern int __bss_end, __bss_start;
volatile unsigned int *start = (volatile unsigned int *)&__bss_start;
volatile unsigned int *end = (volatile unsigned int *)&__bss_end;
while (start <= end)
{
*start++ = 0;
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
2.3 测试效果
我们编译并烧写到开发板中可以看到如下打印信息,说明我们重定位成功:

3. 总结:如何在 C 函数中使用链接脚本变量
我们来总结一下如何在 C 函数中使用链接脚本中定义的变量:
- (1)在 C 函数中声明该变量为外部变量,用 extern 修饰,例如: extern int _start;
- (2)使用取址符号(&)得到该变量的值,例如: int *p = &_start;//p 的值为 lds 文件中_start 的值。
为什么在汇编文件中可以直接使用链接脚本中的变量,而在 C 函数中需要加上取址符号呢?
原因: C 函数中定义一个全局变量 int g_i = 10;,程序中必然有 4 字节的空间留出来给这个变量 g_i,然而链接脚本中的变量并不像全局变量一样都保存在.bin 文件中。如果我们在 C 程序中只用到链接脚本变量 a1, a2, a3,那么程序中并不保存这 3 个变量。 但是这些变量的符号,都会保存在 symbol_table 符号表中,如下图所示

从上图中我们注意到:
- 对于全局变量, symbol table 里面存储的是变量的地址;可以通过&g_i 得到变量的地址 addr。
- 对于链接脚本变量, symbol table 里面存储的是变量的值;为了取出这个值, C 代码要通过&a1。
四、C 语言重定位全部代码
对于 imx6ull,它的 boot ROM 功能强大,会帮我们把程序重定位到 DDR3 内存上。但对于一些采用其他芯片的板子,这一部分的操作可能需要我们手动去完成。例如 S3C2440 上电后,因为硬件的限制, .bin 文件的前 4k 程序需要将整个程序重定位到大小能够执行整个程序的 SDRAM 上。
为了学习代码重定位所需知识,在这一节中我们将重定位整个.bin 文件到片内 RAM 上。需要注意,虽然将全部代码重定位到片内 RAM 上可以加快命令的执行、数据的读取写入,但是这样的做法并不适合体积较大的程序,因为片内 RAM 只有 128KB 空间。
1. 重定位步骤
完整代码可以看这里:09_RELOCATION/07_relocate_all_with_c · sumumm/imx6ull-bare-demo
1.1 修改链接脚本
① 修改链接地址为 0x900000
② 删除与.data 段相关的链接脚本变量。
③ 添加变量_load_addr 并将它的值设置为 Makefile 中 entry 地址的值,供 C 函数调用。
SECTIONS {
_load_addr = 0x80100000;
. = 0x900000;
. = ALIGN(4);
.text :
{
*(.text)
}
. = ALIGN(4);
.rodata : { *(.rodata) }
. = ALIGN(4);
.data : { *(.data) }
. = ALIGN(4);
__bss_start = .;
.bss : { *(.bss) *(.COMMON) }
__bss_end = .;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
1.2 修改 init.c
重定位全部代码和重定位.data 段原理相同。在这里只需要修改 copy_data 函数中调用的外部变量。
void copy_data (void)
{
/* 从链接脚本中获得参数 _start, __bss_start, */
extern int _load_addr, _start, __bss_start;
volatile unsigned int *dest = (volatile unsigned int *)&_start; //_start = 0x900000
volatile unsigned int *end = (volatile unsigned int *)&__bss_start; //__bss_start = 0x9xxxxx
volatile unsigned int *src = (volatile unsigned int *)&_load_addr; //_load_addr = 0x80100000
/* 重定位数据 */
while (dest < end)
{
*dest++ = *src++;
}
}
void clean_bss(void)
{
/* 从 lds 文件中获得 __bss_start, __ bss_end */
extern int __bss_end, __bss_start;
volatile unsigned int *start = (volatile unsigned int *)&__bss_start;
volatile unsigned int *end = (volatile unsigned int *)&__bss_end;
while (start <= end)
{
*start++ = 0;
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
1.3 修改汇编文件
重定位之后,需要使用绝对跳转命令 ldr pc, = xxx,跳转到重定位后的地址。
.text
.global _start
_start:
/* 设置栈 */
ldr sp,=0x80200000
/* 重定位 text, rodata, data 段 */
bl copy_data
/* 清除 bss 段 */
bl clean_bss
/* 跳转到主函数 */
// bl main /* 相对跳转,程序仍在 DDR3 内存中执行 */
ldr pc, =main /* 绝对跳转,程序在片内 RAM 中执行 */
halt:
b halt2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
1.4 测试效果
我们能看到以下打印信息说明重定位成功,可以看到所有的变量都被重定义到片内 RAM 了。

2. 位置无关码
查看上述程序的反汇编发现,在重定位函数 copy_data 执行之前,已经涉及到了片内 RAM 上的地址,但此时片内 RAM 上并没有任何程序,那为什么程序还能正常运行呢?

dis 文件中左边的 90000xx 是链接地址,表示程序运行“应该位于这里”。但是实际上,我们一上电, boot ROM 把程序放到 0x80100000 去了。所以一开始运行这些指令时,它们是位于 DDR 里的。
第 9 行的 blx 命令,并不是跳到 0x9006a4。这要根据当前的 PC 值来计算,在 dis 里写成 0x9006a4,这只是表示“如果程序从 0x900000 开始运行的话,第 9 行就会跳到 0x9006a4”。现在程序被 boot ROM 复制到 0x80100000,从 0x80100000 开始运行,我们需要根据机器码来计算出实际跳转的地址。
blx 是相对跳转指令,要跳到“ pc + offset”这个地址去。程序从 0x8010000 运行,运行到第 9 行时,如下计算新地址:
PC = 当前地址+8=0x8010004+8=0x801000C
offset = 机器码 fa00016f 里的 bit[23:0]*4=0x16f*4=0x5BC
new PC = PC + offset = 0x80105C82
3
在 0x80105C8 这个位置,确实存有 copy_data 函数(这里没看懂,从哪知道之类有这个函数的?反正韦东山的裸机开发教程这么写的,我反正没理解,后面明白了再补充吧),所以:即使程序并不在链接地址 0x900000 上,它也可以运行。因为 blx 是相对跳转指令,它用的不是链接地址,它是“位置无关”的。使用“位置无关码”写出的代码,它可以在任何位置上运行,不一定要在“链接地址”上运行。
下面我们来分析一下实际板子上电后,程序是如何执行的 :
(1)程序被 boot ROM 重定位到 0x80100000,并从这个地址开始执行第一条指令, 此时 pc = 0x80100000 + 8 = 0x80100008。
(2)执行到第 2 条指令“fa00016f”时,根据上述算法,它跳到地址 0x80105C8 去执行 copy_data 函数。
(3)在执行完 copy_data 和 clean_bss 函数后,片内 RAM 0x900000 上已经有程序了。
(4)执行绝对跳转命令“ldr pc, = main”,它是一条伪指令,真实指令是“ldr pc, [pc, #4] ; 900018 <halt+0x8>”。
从 dis 文件里很容易看出,执行完这条指令后, pc 等于 dis 文件中“ 900018”上的值“009001d1”,所以程序跳到片内 RAM 去执行 main 函数了。
注意:在 dis 文件中, main 函数的链接地址是 0x009001d1,往 pc 寄存器里赋值 0x009001d1 时, bit0 为 1,表示 main 函数的代码是用 Thumb 指令写的。
3. 如何写位置无关码?
那么我们应该如何写位置无关码呢?答:使用相对跳转命令 b 或 bl,并注意:
- 重定位之前,不可使用绝对地址
(a) 不可访问全局类变量(全局变量或 static 修饰的局部变量)。
(b) 不可访问有初始值的数组(初始值放在 rodata 里,需要绝对地址来访问)。
- 重定位之后,使用 ldr pc = xxx,跳转到绝对地址( runtime address) 。