理解位置无关性和位置相关性
- 本文的目的是通过案例分析程序链接过程中位置无关性和位置相关性。
1.代码如下:
1>.symbol.c
// symbol.c
int my_var = 42;
int my_func(int a, int b)
{
return a + b;
}
编译为动态链接库:
gcc -g -m32 -shared -fPIC symbol.c -o libsymbol.so
2>.main.c
// main.c
int var = 10;
extern int my_var;
extern int my_func(int, int);
int main() {
int a, b;
a = var;
b = my_var;
return my_func(a, b);
}
使用libsymbol.so编译出两个版本, 分别是位置相关的main和位置无关的main_pie:
位置相关
gcc -g -m32 -L. -lsymbol main.c libsymbol.so -o main
位置无关
gcc -g -m32 -L. -lsymbol -pie -fpie main.c libsymbol.so -o main_pie
2.分析位置相关性
根据链接器约定,32位程序会加载到地址0x08048000,所以以这个地址为基础, 对变量进行绝对地址寻址。 以main为例:
albert/symbol$ readelf -S ./main | grep “.data”
[24] .data PROGBITS 0804a018 001018 00000c 00 WA 0 0 4
关于加载地址0x08048000:
albert/symbol$ readelf -l main
Elf file type is EXEC (Executable file)
Entry point 0x8048470
There are 9 program headers, starting at offset 52
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
…
LOAD 0x000000 0x08048000 0x08048000 0x0070c 0x0070c R E 0x1000
LOAD 0x000f00 0x08049f00 0x08049f00 0x00124 0x0012c RW 0x1000
如上所示:
在可执行文件中 .data section的偏移量为0x1018,加载到虚拟内存中的地址是0x8048000+0x1018=0x804a18, 和显示的结果一样。查看main函数的汇编代码:
albert/symbol$ objdump -d ./main | grep "<main>" -A 15
0804856d <main>:
804856d: 55 push %ebp
804856e: 89 e5 mov %esp,%ebp
8048570: 83 e4 f0 and $0xfffffff0,%esp
8048573: 83 ec 20 sub $0x20,%esp
8048576: a1 20 a0 04 08 mov 0x804a020,%eax
804857b: 89 44 24 18 mov %eax,0x18(%esp)
804857f: a1 24 a0 04 08 mov 0x804a024,%eax
8048584: 89 44 24 1c mov %eax,0x1c(%esp)
8048588: 8b 44 24 1c mov 0x1c(%esp),%eax
804858c: 89 44 24 04 mov %eax,0x4(%esp)
8048590: 8b 44 24 18 mov 0x18(%esp),%eax
8048594: 89 04 24 mov %eax,(%esp)
8048597: e8 a4 fe ff ff call 8048440 <my_func@plt>
804859c: c9 leave
804859d: c3 ret
注意该行:
8048576: a1 20 a0 04 08 mov 0x804a020,%eax
该行的作用是获取变量,使用的是绝对地址0x804a020(在.data范围内)。
通过gdb查看该地址正是var变量的地址, 且初始值为0xa:
$ gdb ./main
(gdb) x/xw 0x804a020
0x804a020 : 0x0000000a
3.分析位置无关性
如2所述,由于一个进程只有一个主函数,所以变量按绝对地址寻址, 对于可执行文件来说不是什么问题;但是对于动态链接库而言, 如果每个.so文件都要求加载到某个绝对地址,那简直是噩梦,因为无法保证不和其他的.so加载地址发生冲突。 所以就产生了位置无关性代码。
查看main_pie:
albert/symbol$ readelf -S ./main_pie | grep “.data”
[24] .data PROGBITS 0000201c 00101c 00000c 00 WA 0 0 4
如上所示:
偏移量固定, 但Addr部分不是绝对地址。即程序可以加载到虚拟内存的任意位置。继续看看main的汇编:
albert/symbol$ objdump -d main_pie | grep "<main>" -A 20
0000067b <main>:
67b: 55 push %ebp
67c: 89 e5 mov %esp,%ebp
67e: 53 push %ebx
67f: 83 e4 f0 and $0xfffffff0,%esp
682: 83 ec 20 sub $0x20,%esp
685: e8 c6 fe ff ff call 550 <__x86.get_pc_thunk.bx>
68a: 81 c3 76 19 00 00 add $0x1976,%ebx
690: 8b 83 24 00 00 00 mov 0x24(%ebx),%eax
696: 89 44 24 18 mov %eax,0x18(%esp)
69a: 8b 83 f0 ff ff ff mov -0x10(%ebx),%eax
6a0: 8b 00 mov (%eax),%eax
6a2: 89 44 24 1c mov %eax,0x1c(%esp)
6a6: 8b 44 24 1c mov 0x1c(%esp),%eax
6aa: 89 44 24 04 mov %eax,0x4(%esp)
6ae: 8b 44 24 18 mov 0x18(%esp),%eax
6b2: 89 04 24 mov %eax,(%esp)
6b5: e8 16 fe ff ff call 4d0 <my_func@plt>
6ba: 8b 5d fc mov -0x4(%ebp),%ebx
6bd: c9 leave
6be: c3 ret
注意:685~690处,和之前的区别是这次通过ebx寄存器来对变量进行寻址,对于__x86.get_pc_thunk.bx函数:
objdump -d main_pi | grep “__x86.get_pc_thunk.bx” -A 2
00000550 <__x86.get_pc_thunk.bx>:
550: 8b 1c 24 mov (%esp),%ebx
553: c3 ret
该函数作用就是把esp(即返回地址)的值保存在ebx中,然后寻址用。经过685和68a两条指令后, ebx的值等于相对当前PC指针的固定位移。
只看静态代码的话,可知:ebx=0x68a+0x1976=0x2000,这个地址位于:
albert/symbol$ readelf -S ./main_pie | grep 2000 -C 1
[22] .got PROGBITS 00001fe4 000fe4 00001c 04 WA 0 0 4
[23] .got.plt PROGBITS 00002000 001000 00001c 04 WA 0 0 4
[24] .data PROGBITS 0000201c 00101c 00000c 00 WA 0 0 4
它是.got.plt的起始地址。 现在先看汇编的690处,通过ebx+0x24=0x2024获取了变量的值,这个地址已经进入到了.data之中:
gdb ./main_pie
(gdb) x/xw 0x2000+0x24
0x2024 : 0x0000000a
所以, 位置无关代码实际上就是通过运行时PC指针的值来找到代码所引用的其他符号的位置, 不管二进制文件被加载到哪个位置, 都可以正确执行。