共享库的一个要求是代码应该可以在不同的进程中共享,这就要求在重定向时不能修改代码段的内容。共享库也实现成可加载到任何地址执行。对于同一个共享库,不同进程,其运行地址可能不同。
位置无关代码,就是访问对象时,不使用对象的绝对地址,而是使用相对地址,通常是相对PC的地址,通过例子分析:
代码b.c
1 2 3 4 5 6 7 | $ cat b.c static int foo = 100; int function (void) { return foo; } |
编译x64和x86目标文件:
1 2 | $ cc -c -fPIC -o b2.o b.c $ cc -m32 -fPIC -c b.c |
目标文件elf信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | $ readelf -a b2.o 。。。 Section Headers: [Nr] Name Type Address Offset Size EntSize Flags Link Info Align [ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0 [ 1] .text PROGBITS 0000000000000000 00000040 000000000000000c 0000000000000000 AX 0 0 1 [ 2] .rela.text RELA 0000000000000000 000001c8 0000000000000018 0000000000000018 I 10 1 8 [ 3] .data PROGBITS 0000000000000000 0000004c 0000000000000004 0000000000000000 WA 0 0 4 。。。 Relocation section '.rela.text' at offset 0x1c8 contains 1 entries: Offset Info Type Sym. Value Sym. Name + Addend 000000000006 000300000002 R_X86_64_PC32 0000000000000000 .data - 4 |
x64目标文件反汇编信息
1 2 3 4 5 6 7 8 9 10 11 12 13 | $ objdump -d b2.o b2.o: file format elf64-x86-64 Disassembly of section .text: 0000000000000000 <function>: 0: 55 push %rbp 1: 48 89 e5 mov %rsp,%rbp 4: 8b 05 00 00 00 00 mov 0x0(%rip),%eax # a <function+0xa> a: 5d pop %rbp b: c3 retq |
分析:
可以看到,指令使用相对rip寄存器的地址访问。重定向类型为R_X86_64_PC32。是PC相对地址。
目标文件的elf信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | $ readelf -a b.o Section Headers: [Nr] Name Type Addr Off Size ES Flg Lk Inf Al [ 0] NULL 00000000 000000 000000 00 0 0 0 [ 1] .group GROUP 00000000 000034 000008 04 12 12 4 [ 2] .text PROGBITS 00000000 00003c 000015 00 AX 0 0 1 [ 3] .rel.text REL 00000000 000200 000018 08 I 12 2 4 [ 4] .data PROGBITS 00000000 000054 000004 00 WA 0 0 4 [ 5] .bss NOBITS 00000000 000058 000000 00 WA 0 0 1 [ 6] .text.__x86.get_p PROGBITS 00000000 000058 000004 00 AXG 0 0 1 ... Relocation section '.rel.text' at offset 0x200 contains 3 entries: Offset Info Type Sym.Value Sym. Name 00000004 00000c02 R_386_PC32 00000000 __x86.get_pc_thunk.ax 00000009 00000d0a R_386_GOTPC 00000000 _GLOBAL_OFFSET_TABLE_ 0000000f 00000309 R_386_GOTOFF 00000000 .data ... Symbol table '.symtab' contains 14 entries: Num: Value Size Type Bind Vis Ndx Name ... 5: 00000000 4 OBJECT LOCAL DEFAULT 4 foo ... 12: 00000000 0 FUNC GLOBAL HIDDEN 6 __x86.get_pc_thunk.ax 13: 00000000 0 NOTYPE GLOBAL DEFAULT UND _GLOBAL_OFFSET_TABLE ... |
目标文件的反汇编信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | $ objdump -d b.o b.o: file format elf32-i386 Disassembly of section .text: 00000000 <function>: 0: 55 push %ebp 1: 89 e5 mov %esp,%ebp 3: e8 fc ff ff ff call 4 <function+0x4> 8: 05 01 00 00 00 add $0x1,%eax d: 8b 80 00 00 00 00 mov 0x0(%eax),%eax 13: 5d pop %ebp 14: c3 ret Disassembly of section .text.__x86.get_pc_thunk.ax: 00000000 <__x86.get_pc_thunk.ax>: 0: 8b 04 24 mov (%esp),%eax 3: c3 |
分析:
有3条重定向,386架构不允许直接访问当前指令地址,因此通过__x86.get_pc_thunk.ax函数间接访问,返回地址被压入栈中,通过栈来读取当前指令地址,存储到%eax中,然后通过两种重定向R_386_GOTPC和R_386_GOTOFF来确定相对偏移。
这里关注共享库访问全局符号的情况,因为对于本地符号(static),链接共享库时,符号地址和访问地址的相对位置是可以确定的。通过位置无关代码就可以满足共享库的要求。在动态加载(动态链接阶段)的视角来看,这些符号是不需要再次解析的。
注意:共享库中的全局符号,可以被外部全局符号覆盖,设计如此,这样用户就可以提供自己的变量和函数。
代码c.c
1 2 3 4 5 6 7 | $ cat c.c extern int foo; int function() { return foo; } |
编译
1 | $ cc -shared -fPIC -o libc.so c.c |
反汇编信息:
1 2 3 4 5 6 7 8 9 | $ objdump -d libc.so ... 0000000000000690 < function >: 690: 55 push %rbp 691: 48 89 e5 mov %rsp,%rbp 694: 48 8b 05 45 09 20 00 mov 0x200945(%rip),%rax # 200fe0 <_DYNAMIC+0x1a0> 69b: 8b 00 mov (%rax),%eax 69d: 5d pop %rbp 69e: c3 retq |
地址为相对rip偏移0x200945,反汇编给出地址为0x200fe0。查看elf信息,它落到.got section里,而且存在一条foo的重定向信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | $ readelf -a libc.so ... Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flags Align LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x000000000000072c 0x000000000000072c R E 200000 LOAD 0x0000000000000e28 0x0000000000200e28 0x0000000000200e28 0x00000000000001f8 0x0000000000000200 RW 200000 Section to Segment mapping: Segment Sections...
01 .init_array .fini_array .jcr .dynamic .got .got.plt .data .bss ... Section Headers: [Nr] Name Type Address Offset Size EntSize Flags Link Info Align [19] .got PROGBITS 0000000000200fd0 00000fd0 0000000000000030 0000000000000008 WA 0 0 8 Relocation section '.rela.dyn' at offset 0x470 contains 9 entries: Offset Info Type Sym. Value Sym. Name + Addend 000000200fe0 000400000006 R_X86_64_GLOB_DAT 0000000000000000 foo + 0 |
.got和.data是在同一个可读写的段里面的。
总结:
访问外部变量时,使用一个中间层.got,重定向将符号的patch到.got而不是.text,这样就避免了修改代码段。
代码d.c及编译
1 2 3 4 5 6 7 8 | $ cat d.c int foo(); int function { return foo(); } $ cc -shared -fPIC -o libd.so d.c |
编译
1 | $ cc -shared -fPIC -o libd.so d.c |
反汇编:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | $ objdump -d libd.so ... Disassembly of section .plt: 0000000000000570 <foo@plt-0x10>: 570: ff 35 92 0a 20 00 pushq 0x200a92(%rip) # 201008 <_GLOBAL_OFFSET_TABLE_+0x8> 576: ff 25 94 0a 20 00 jmpq *0x200a94(%rip) # 201010 <_GLOBAL_OFFSET_TABLE_+0x10> 57c: 0f 1f 40 00 nopl 0x0(%rax) 0000000000000580 <foo@plt>: 580: ff 25 92 0a 20 00 jmpq *0x200a92(%rip) # 201018 <_GLOBAL_OFFSET_TABLE_+0x18> 586: 68 00 00 00 00 pushq $0x0 58b: e9 e0 ff ff ff jmpq 570 <_init+0x28> ... 00000000000006a0 <function>: 6a0: 55 push %rbp 6a1: 48 89 e5 mov %rsp,%rbp 6a4: b8 00 00 00 00 mov $0x0,%eax 6a9: e8 d2 fe ff ff callq 580 <foo@plt> 6ae: 5d pop %rbp 6af: c3 retq |
elf信息
1 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 | $ readelf -a libd.so Section Headers: [Nr] Name Type Address Offset Size EntSize Flags Link Info Align [ 7] .rela.dyn RELA 0000000000000470 00000470 00000000000000c0 0000000000000018 A 3 0 8 [ 8] .rela.plt RELA 0000000000000530 00000530 0000000000000018 0000000000000018 AI 3 21 8 [ 9] .init PROGBITS 0000000000000548 00000548 0000000000000020 0000000000000010 AX 0 0 16 [11] .plt.got PROGBITS 0000000000000590 00000590 0000000000000010 0000000000000000 AX 0 0 8 [12] .text PROGBITS 00000000000005a0 000005a0 0000000000000110 0000000000000000 AX 0 0 16 [20] .got PROGBITS 0000000000200fd8 00000fd8 0000000000000028 0000000000000008 WA 0 0 8 [21] .got.plt PROGBITS 0000000000201000 00001000 0000000000000020 0000000000000008 WA 0 0 8 Section to Segment mapping: Segment Sections... 00 .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .plt.got .text .fini .eh_frame_hdr .eh_frame 01 .init_array .fini_array .jcr .dynamic .got .got.plt .data .bss Relocation section '.rela.plt' at offset 0x530 contains 1 entries: Offset Info Type Sym. Value Sym. Name + Addend 000000201018 000400000007 R_X86_64_JUMP_SLO 0000000000000000 foo + 0 |
为了实现懒加载,函数调用实现成如下框架[1]
.plt section包含n个PLT片,第一个PLT[0]用于解析符号。后续的PLT都对应一个外部函数。如上述反汇编0x6a9通过call指令调用0x580处指令,也即PLT[1]。PLT[1]包括3条指令,第一条指令是jmp指令[3],它跳转到指定内存地址存储的地址。这个内存地址是0x201018。这个地址在.got.plt section,其值为0x00000586,这就是PLT[1]的第二条指令的地址,如下:
1 2 3 4 5 6 7 8 | Disassembly of section .got.plt: 0000000000201000 <_GLOBAL_OFFSET_TABLE_>: 201000: 18 0e sbb %cl,(%rsi) 201002: 20 00 and %al,(%rax) ... 201018: 86 05 00 00 00 00 xchg %al,0x0(%rip) # 20101e <_GLOBAL_OFFSET_TABLE_+0x1e> ... |
第二条指令准备符号解析的参数,第三条指令跳转到PLT[0]执行,PLT[0]最终将调用动态链接器中的函数执行解析任务。在符号解析后,将修改0x201018内存处的值,使其执行正确的地址,这样就可以直接跳转到函数处执行。
位置无关总是与共享库联系在一起,可执行文件不需要共享代码段,因此通常是位置相关的。
访问本地函数,总是使用相对PC地址,因为他们在一个.text section,偏移在编译阶段就可以确定,这样不需要任何重定向信息。
位置无关代码和got实现是两回事。不需要got也可以实现位置无关代码。但是got通常是位置无关的。
链接-fPIC共享库时,通常也要求目标文件是-fPIC编译的。
参考
【1】https://eli.thegreenplace.net/2011/11/03/position-independent-code-pic-in-shared-libraries
【2】https://eli.thegreenplace.net/2011/11/11/position-independent-code-pic-in-shared-libraries-on-x64
【3】https://stackoverflow.com/questions/20251097/what-does-this-intel-jmpq-instruction-do