问题来源
- 在 I.MX6ull 的启动流程中,u-boot会将自身从内存一开始的位置拷贝到其他位置,以便给linux留出内存空间,防止 u-boot被覆盖
- 如果代码中包含直接引用其链接时地址的指令,那么当代码被移动到新的地址时,这些引用将不再正确。这包括通过绝对地址直接调用函数或访问全局变量
什么是位置无关码
位置无关码(Position Independent Code,简称 PIC)是一种特殊类型的编码方式,用于生成在内存中可以任意位置运行的代码。这种代码在编译时不会固定任何特定的内存地址,使得程序或库在运行时可以被加载到内存的任意位置而无需修改。这对于操作系统中的动态链接库(DLLs)和共享库(如 Linux 中的 .so 文件)尤为重要。
实现方式
在编写位置无关的代码时,通常采取以下技术:
- 相对寻址:使用基于程序计数器(PC)的相对地址而非绝对地址。这允许代码引用距当前执行点的相对位置,而不是固定的内存地址。
- 全局偏移表(GOT):用于存储全局变量和函数的地址。程序通过 GOT 来间接访问这些全局资源,GOT 在程序启动时由动态链接器填充。
- 过程链接表(PLT):在调用动态链接库中的函数时使用。每次函数调用都通过 PLT 间接进行,这使得函数地址可以在运行时解析。
bl指令是如何实现位置无关码的(函数相对地址跳转)
BL
(Branch with Link) 指令在 ARM 架构中是用来实现函数调用的,它通过链接寄存器 (LR) 保存返回地址,并执行一个分支到目标函数的地址。这个指令通常使用相对地址进行跳转,这意味着它计算目标地址作为当前程序计数器(PC)加上一个偏移量(PC+offset)。这种方式本身就为位置无关代码(PIC)提供了一定的支持,但是如果需要将代码移动到内存中的不同位置,是否还能工作取决于代码是如何编写和链接的。
相对跳转
BL
指令使用相对地址进行跳转,这是实现代码位置无关的关键。相对跳转意味着跳转的目标地址是基于当前执行指令的位置计算的。这种方式保证了,只要代码整体移动,相对偏移保持不变,跳转仍然有效。
示例
假设有如下的指令序列:
00008000: MOV R0, #0
00008004: BL 0000800C ; 假设跳转到 0000800C
00008008: MOV R1, #1
0000800C: ADD R0, R0, R1
如果整个代码块移动到地址 00009000
,跳转指令会相应地调整偏移量,但跳转的行为不变(可以看到BL 跳转的地址随着代码整体被拷贝而随之改变了):
00009000: MOV R0, #0
00009004: BL 0000900C ; 偏移量仍然正确地指向下一条指令
00009008: MOV R1, #1
0000900C: ADD R0, R0, R1
限制
当 BL
指令足够应对内部函数调用的相对跳转时,它不适用于以下情况:
- 外部函数调用:如果需要调用其他模块或库中的函数,单纯的
BL
不能解决问题,因为其它模块或库可能不与主程序一起移动。 - 绝对地址引用:对于全局变量或者需要通过绝对地址访问的资源,
BL
无法提供解决方案。
解决方案
对于上述限制,通常需要额外的机制,如:
- 动态链接:使用动态链接器来解决库函数的位置问题,通常涉及到 GOT(全局偏移表)和 PLT(程序链接表)。
- 重定位:生成重定位表,使得在程序加载到新位置时能够调整内存中的绝对地址引用。
结论
BL
指令通过使用相对地址的方式,为实现位置无关代码提供了基础支持。它适用于代码内部的函数调用,但对于跨模块或需要绝对地址的引用,需要其他机制和支持。在设计嵌入式系统或操作系统引导程序如 u-boot 时,这些考虑是关键的,以确保系统的灵活性和可移植性。
如何实现全局变量的位置无关码
- 有如下的C代码
static int rel_a = 0;
void rel_test(void) {
rel_a = 100;
printf("rel_test\r\n");
}
- 编译后进行反汇编,左边为地址
1 8785dcf8 <rel_a>:
2 8785dcf8: 00000000 andeq r0, r0, r0
3
4 878042b4 <rel_test>:
5 878042b4: e59f300c ldr r3, [pc, #12] ; 878042c8 <rel_test+0x14>
6 878042b8: e3a02064 mov r2, #100 ; 0x64
7 878042bc: e59f0008 ldr r0, [pc, #8] ; 878042cc <rel_test+0x18>
8 878042c0: e5832000 str r2, [r3]
9 878042c4: ea000d64 b 87839bfc <printf>
10 878042c8: 8785dcf8 .word 0x8785dcf8
11 878042cc: 87842aaf strhi r2, [r4, pc, lsr #21]
- 程序计数器(PC)的值为当前地址+8(ARM的三级流水线)
- 看第5行,这里设置 r3 为 878042b4 + 8 + 12 = 878042c8 此处(878042b4+8)就是PC的值
- 878042c8 在第 12 行,指向全局变量的位置为 8785dcf8
- 878042c8 也被称为 Label
- 这样就通过 PC 的相对地址指向了全局变量地址,代码在内存中的位置移动后如果全局变量的地址移动了,那么只要对全局变量加上一个整体的偏移地址,那么对全局变量的引用也不会出错
在编译的时候添加 -pie 选项
在编译程序时,使用 -pie
选项(Position Independent Executable,位置无关可执行文件)是一种常见的方法,用于生成可以在内存中任意位置执行的可执行文件。这是创建与地址空间布局随机化(ASLR, Address Space Layout Randomization)兼容的二进制文件的关键步骤,增强了程序的安全性。
使用 -pie
选项的效果
当你为编译器(如 GCC 或 Clang)添加 -pie
选项时,编译器会生成位置无关的代码。这意味着生成的可执行文件不依赖于固定的内存地址,从而可以在加载时由操作系统重新定位到内存的任何位置。这与生成动态库时使用的 -fPIC
(Position Independent Code)选项类似。
比较 -fPIC
和 -pie
-fPIC
:生成位置无关的代码,通常用于编译动态链接库(如.so
文件)。它确保代码可以在任何地址被加载和执行。-pie
:使整个可执行文件位置无关。当系统启用 ASLR 时,这允许操作系统在每次程序启动时将可执行文件加载到随机的内存地址。
注意事项
- 当使用
-pie
时,确保所有的库也支持位置无关代码,否则可能会遇到链接错误。 - 在一些系统上,使用
-pie
可能会导致轻微的性能开销,因为地址计算现在需要额外的间接寻址步骤。
添加位置偏移的代码
8785dcec: 87800020 strhi r0, [r0, r0, lsr #32]
8785dcf0: 00000017 andeq r0, r0, r7, lsl r0
……
8785e2fc: 878042c8 strhi r4, [r0, r8, asr #5]
8785e300: 00000017 andeq r0, r0, r7, lsl r0
- 878042c8+offset = 读取新的Label处的数据+offset
- 这里的 00000017 用于标识其上的数据为一个Label,需要加上偏移地址
这段代码涉及到动态链接过程中的一个关键步骤:地址重定位。代码片段主要执行地址重定位,以确保程序中的引用指向正确的内存地址。这是位置无关代码(PIC)在实际运行时进行调整以适应其加载位置的一个实例。
代码解析
以下是代码的详细分析:
ldr r2, =__rel_dyn_start /* r2 <- SRC &__rel_dyn_start */
ldr r3, =__rel_dyn_end /* r3 <- SRC &__rel_dyn_end */
- 这两行加载动态重定位表的开始和结束地址到寄存器
r2
和r3
。__rel_dyn_start
和__rel_dyn_end
是由链接器定义的符号,分别指向重定位表的开始和结束。
fixloop:
ldmia r2!, {r0-r1} /* (r0,r1) <- (SRC location,fixup) */
ldmia r2!, {r0-r1}
是一个“加载多个寄存器”指令,它从r2
指向的地址加载数据到寄存器r0
和r1
,同时r2
自增以指向下一个位置。这里,r0
获得需要修正的内存位置,r1
获得修正方式(或偏移)。
and r1, r1, #0xff
cmp r1, #23 /* relative fixup? */
bne fixnext
- 这段代码检查
r1
中的修正类型是否是相对修正(类型代码为23)。如果不是,跳转到fixnext
继续处理下一个条目。
/* relative fix: increase location by offset */
add r0, r0, r4
ldr r1, [r0]
add r1, r1, r4
str r1, [r0]
- 如果是相对地址修正,使用寄存器
r4
中的偏移量来调整r0
指向的位置。这里r4
应该包含了加载地址和基地址之间的偏差。读取r0
指向的当前值,增加偏差,然后将新值写回。
fixnext:
cmp r2, r3
blo fixloop
- 检查是否已处理完所有重定位条目。如果
r2
(指向当前条目的指针)仍然低于r3
(结束地址),则继续循环。