话不多说,先看例子
test.c
#include
int a = 100;
void main()
{
printf("main = %p\n", main);
printf("&a = %p\n", &a);
printf("a = %d\n", a);
*(int *)0x4000010 = 20;
printf("a = %d\n", a);
}
test.lds
SECTIONS
{
.text 0x30000000:
{
*(.text)
}
.data 0x4000000:
{
*(.data)
}
.bss :
{
*(.bss)
}
}
$ gcc test.c test.lds -o test -no-pie
$ ./test
main = 0x300000e6
&a = 0x4000010
a = 100
a = 20
PIE(position-independent executable,位置无关码),no-pie 就是位置有关码。
PIE 还有个孪生兄弟 PIC(position-independent code)。其作用和 PIE 相同,都是使被编译后的程序能够随机的加载到某个内存地址。区别在于 PIC 是在生成动态链接库时使用(Linux 中的 so),PIE 是在生成可执行文件时使用。
链接地址 ≠ 运行地址:位置无关码
链接地址 = 运行地址:位置有关码
上述示例,编译时使用了 -no-pie 选项,故编译出来的代码是位置有关码,即,链接地址 = 运行地址。我们来检查下是否符合预期。
通过 readelf -h test
看到,程序的入口地址为 0x30000000,和我们链接脚本中的 .text 0x30000000
一致。
$ readelf -h test
ELF 头:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
类别: ELF64
数据: 2 补码,小端序 (little endian)
Version: 1 (current)
OS/ABI: UNIX - System V
ABI 版本: 0
类型: EXEC (可执行文件)
系统架构: Advanced Micro Devices X86-64
版本: 0x1
入口点地址: 0x30000000
程序头起点: 64 (bytes into file)
Start of section headers: 22848 (bytes into file)
标志: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 15
Size of section headers: 64 (bytes)
Number of section headers: 31
Section header string table index: 30
从下面的反汇编可以看到,main 函数的地址为 0x300000e6,和打印一致,全局变量 a 的地址为 0x4000010,和打印一致。
$ objdump -d test
test: 文件格式 elf64-x86-64
...
Disassembly of section .text:
0000000030000000 <_start>:
30000000: f3 0f 1e fa endbr64
30000004: 31 ed xor %ebp,%ebp
30000006: 49 89 d1 mov %rdx,%r9
30000009: 5e pop %rsi
3000000a: 48 89 e2 mov %rsp,%rdx
3000000d: 48 83 e4 f0 and $0xfffffffffffffff0,%rsp
30000011: 50 push %rax
30000012: 54 push %rsp
30000013: 49 c7 c0 d0 01 00 30 mov $0x300001d0,%r8
3000001a: 48 c7 c1 60 01 00 30 mov $0x30000160,%rcx
30000021: 48 c7 c7 e6 00 00 30 mov $0x300000e6,%rdi
30000028: ff 15 c2 2f 00 00 callq *0x2fc2(%rip) # 30002ff0 <__libc_start_main@GLIBC_2.2.5>
3000002e: f4 hlt
3000002f: 90 nop
...
00000000300000e6 :
300000e6: f3 0f 1e fa endbr64
300000ea: 55 push %rbp
300000eb: 48 89 e5 mov %rsp,%rbp
300000ee: 48 8d 35 f1 ff ff ff lea -0xf(%rip),%rsi # 300000e6
300000f5: 48 8d 3d 08 0f 00 00 lea 0xf08(%rip),%rdi # 30001004 <_IO_stdin_used+0x4>
300000fc: b8 00 00 00 00 mov $0x0,%eax
30000101: e8 3a 0f 40 d0 callq 401040
30000106: 48 8d 35 03 ff ff d3 lea -0x2c0000fd(%rip),%rsi # 4000010
3000010d: 48 8d 3d fb 0e 00 00 lea 0xefb(%rip),%rdi # 3000100f <_IO_stdin_used+0xf>
30000114: b8 00 00 00 00 mov $0x0,%eax
30000119: e8 22 0f 40 d0 callq 401040
3000011e: 8b 05 ec fe ff d3 mov -0x2c000114(%rip),%eax # 4000010
30000124: 89 c6 mov %eax,%esi
30000126: 48 8d 3d eb 0e 00 00 lea 0xeeb(%rip),%rdi # 30001018 <_IO_stdin_used+0x18>
3000012d: b8 00 00 00 00 mov $0x0,%eax
30000132: e8 09 0f 40 d0 callq 401040
30000137: b8 10 00 00 04 mov $0x4000010,%eax
3000013c: c7 00 14 00 00 00 movl $0x14,(%rax)
30000142: 8b 05 c8 fe ff d3 mov -0x2c000138(%rip),%eax # 4000010
30000148: 89 c6 mov %eax,%esi
3000014a: 48 8d 3d c7 0e 00 00 lea 0xec7(%rip),%rdi # 30001018 <_IO_stdin_used+0x18>
30000151: b8 00 00 00 00 mov $0x0,%eax
30000156: e8 e5 0e 40 d0 callq 401040
3000015b: 90 nop
3000015c: 5d pop %rbp
3000015d: c3 retq
3000015e: 66 90 xchg %ax,%ax
...
由于是位置有关码,运行地址 = 链接地址,我们能够很清楚地知道在运行时代码所处的地址,全局变量所处的地址。在示例代码中,我们将 data 段的地址设置为了 0x4000000,则全局变量 a 就落在这个区间,为 0x4000010。这样,甚至,我们可以在代码中直接操作这个地址,将该地址储存的值 100 改为 20,也就是示例中的效果。
test.c
#include
int a = 100;
void main()
{
printf("main = %p\n", main);
printf("&a = %p\n", &a);
// printf("a = %d\n", a);
// *(int *)0x4000010 = 20;
// printf("a = %d\n", a);
}
test.lds 和示例一保持不变
去除位置有关码编译选项,编译得到位置无关码,运行
$ gcc test.c test.lds -o test
liyongjun@Box:~/project/c/ld/4$ ./test
main = 0x564ec6e1e0e9
&a = 0x564e9ae1e010
liyongjun@Box:~/project/c/ld/4$ ./test
main = 0x55a23683f0e9
&a = 0x55a20a83f010
liyongjun@Box:~/project/c/ld/4$ ./test
main = 0x558bf273a0e9
&a = 0x558bc673a010
可以看到,每次运行,运行地址都不一样,都是加载器随机选择内存的。
因为可执行程序的代码段只有读和执行属性没有写属性,而数据段具有读写属性。要实现地址无关代码,就要将代码段中需要改变的值分离到数据段中,而程序加载时可以保存代码段不变,通过改变数据段中的内容,实现地址无关代码。
执行程序会在固定的地址开始加载。系统的动态链接器库 ld.so 会首先加载,接着 ld.so 会通过 .dynamic 段中类型为 DT_NEED 的字段查找其他需要加载的共享库,并依次将它们加载到内存。
注意:因为是 Non-PIE 模式,这些动态链接库每次加载的顺序和位置都一样。
而对于通过 PIE 方式生成的执行程序,因为没有绝对地址引用,所以每次加载的地址都不尽相同。
不仅动态链接库的加载地址不固定,就连执行程序每次加载的地址也不一样。
这就要求 ld.so 首先被加载,然后它不仅要负责重定为其它共享库,还要对可执行文件重定位。