原作者:Eli Bendersky
。以为x86架构编译的代码为样例,解释了位置无关代码(PIC)怎样工作。我承诺在还有一篇文章里涉及x64上的PIC,如今就是了。本文将不会太进入细节,由于假定读者已经理解了理论上PIC怎样工作。
总之。对于这两个平台想法是相似的,但由于每一个架构独有的特性,某些细节是不同的。
RIP相对取址
在x86上,虽然訪问函数(使用call指令)使用指令指针的相对偏移,数据訪问(使用mov指令)仅支持绝对地址。正如我们在之前的文章里看到的。这使得PIC代码效率下降,由于PIC天然地要求全部的偏移是IP相对的;绝对地址与位置无关不能非常好地走在一起。
x64以新的“RIP相对取址”修正了这,它是全部64位訪问内存的mov指令的缺省模式(该模式也用于其它指令。比方lea)。以下援引自“Intel架构手冊卷2a”:
在64位模式里实现了一个新的取址形式,RIP相对取址(相当于指令指针)。
通过向指向下一条指令的64位RIP加入位移来构成一个有效的地址。
在RIP相对模式里使用的位移是32位大小的。由于它应该可用于正负偏移,这个取址模式支持大约最大+/-2GB的RIP偏移。
x64PIC数据訪问——一个样例
为了更easy比較,我将使用与前一篇文章同样的C源码作为数据訪问1样例:
int myglob = 42;
intml_func(int a, int b)
{
return myglob + a + b;
}
让我们看一眼ml_func的反汇编代码:
00000000000005ec <ml_func>:
5ec: 55 push rbp
5ed: 48 89 e5 mov rbp,rsp
5f0: 89 7d fc mov DWORD PTR [rbp-0x4],edi
5f3: 89 75 f8 mov DWORD PTR [rbp-0x8],esi
5f6: 48 8b 05 db 09 20 00 mov rax,QWORD PTR [rip+0x2009db]
5fd: 8b 00 mov eax,DWORD PTR [rax]
5ff: 03 45 fc add eax,DWORD PTR [rbp-0x4]
602: 03 45 f8 add eax,DWORD PTR [rbp-0x8]
605: c9 leave
606: c3 ret
这里最有趣的指令在0x5f6:通过訪问GOT中的一个项,它将myglob的地址放入rax。正如我们看到的,它使用RIP相对取址。由于它相对于下一个指令的地址,我们实际得到的是0x5fd+ 0x2009db = 0x200fd8。因此保存myglob地址的GOT项在0x200fd8。
让我们检查一下这是否合理:
$ readelf -S libmlpic_dataonly.so
There are 35 section headers, starting at offset 0x13a8:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[...]
[20] .got PROGBITS 0000000000200fc8 00000fc8
0000000000000020 0000000000000008 WA 0 0 8
[...]
GOT始于0x200fc8,因此myglob是其第三个项。我们还能够看到为GOT訪问myglob而插入的重定位:
$ readelf -r libmlpic_dataonly.so
Relocation section '.rela.dyn' at offset 0x450 contains 5entries:
Offset Info Type Sym. Value Sym. Name + Addend
[...]
000000200fd8 000500000006R_X86_64_GLOB_DAT 0000000000201010 myglob + 0
[...]
的确,0x200fd8的重定位项告诉动态加载器,一旦知道myglob的终于地址。把它放入0x200fd8。
因此在代码里myglob的地址怎样得到应该相当清楚。
汇编代码里下一条指令(0x5fd处)解引用这个地址将myglob的值放入eax。
x64PIC函数调用——一个样例
如今让我们看一下在x64上PIC代码怎样进行函数调用。再次,我们将使用之前文章里的样例:
int myglob = 42;
intml_util_func(int a)
{
return a + 1;
}
intml_func(int a, int b)
{
int c = b +ml_util_func(a);
myglob += c;
return b + myglob;
}
反汇编ml_func,我们得到:
000000000000064b <ml_func>:
64b: 55 push rbp
64c: 48 89 e5 mov rbp,rsp
64f: 48 83 ec 20 sub rsp,0x20
653: 89 7d ec mov DWORD PTR [rbp-0x14],edi
656: 89 75 e8 mov DWORD PTR [rbp-0x18],esi
659: 8b 45 ec mov eax,DWORD PTR [rbp-0x14]
65c: 89 c7 mov edi,eax
65e: e8 fd fe ff ff call 560 <ml_util_func@plt>
[... snip more code ...]
如前,这是对ml_util_func@lt的调用。看一下那里有什么:
0000000000000560 <ml_util_func@plt>:
560: ff 25 a2 0a 20 00 jmp QWORD PTR [rip+0x200aa2]
566: 68 01 00 00 00 push 0x1
56b: e9 d0 ff ff ff jmp 540 <_init+0x18>
因此保存ml_util_func实际地址的GOT项在0x200aa2+ 0x566 = 0x201008。
就像期望的那样。有一个重定位用于它:
$ readelf -r libmlpic.so
Relocation section '.rela.dyn' at offset 0x480 contains 5entries:
[...]
Relocation section '.rela.plt' at offset 0x4f8 contains 2entries:
Offset Info Type Sym. Value Sym. Name + Addend
[...]
000000201008 000600000007R_X86_64_JUMP_SLO 000000000000063c ml_util_func + 0
性能影响
在这两个样例里,能够看到PIC在x64上比在x86上要求更少的指令。在x86上,GOT地址以两步被加载到某些基址寄存器(依据惯例ebx)——首先以一个特殊的函数调用获取指令的地址,然后加上到GOT的偏移。在x64上这两步都不须要。由于到GOT的相对偏移对链接器是已知的。并且能够简单地使用RIP相对取址编码在指令本身。
在调用一个函数时,也不须要为弹簧垫(trampoline)在ebx里准备GOT地址。就像x86代码做的那样。由于弹簧垫仅仅是直接通过RIP相对取址訪问其GOT项。
因此虽然PIC在x64上,相比非PIC代码。仍然要求额外的指令,但这额外的代价更小。束缚一个寄存器作为GOT指针的间接代价(在x86上令人痛苦)也没有了,由于使用RIP相对取址不须要这种寄存器。总而言之,x64PIC导致的性能影响远小于x86,使得它更有吸引力。
其实。如此有吸引力,这是这个架构上编写共享库的缺省方法。
额外的学分:x64上的非PIC代码
gcc不仅鼓舞你在x64上对共享库使用PIC,它缺省地要求它。比如,假设我们没有使用-fpic编译第一个样例。然后尝试将它链接入一个共享库(使用-shared),我们将从链接器得到一个错误。就像这样:
/usr/bin/ld: ml_nopic_dataonly.o: relocation R_X86_64_PC32against symbol `myglob' can not be used when making a shared object; recompilewith -fPIC
/usr/bin/ld: final link failed: Bad value
collect2: ld returned 1 exit status
发生了什么?让我们看一下ml_nopic_dataonly.o的反汇编代码:
0000000000000000 <ml_func>:
0: 55 push rbp
1: 48 89 e5 mov rbp,rsp
4: 89 7d fc mov DWORD PTR [rbp-0x4],edi
7: 89 75 f8 mov DWORD PTR [rbp-0x8],esi
a: 8b 05 00 00 00 00 mov eax,DWORD PTR [rip+0x0]
10: 03 45 fc add eax,DWORD PTR [rbp-0x4]
13: 03 45 f8 add eax,DWORD PTR [rbp-0x8]
16: c9 leave
17: c3 ret
注意如今在地址0xa处的指令里,myglob是怎样被訪问的。
它期望链接器在该指令的操作数里填补一个到myglob实际位置的重定位(因此不须要GOT重定位):
$ readelf -r ml_nopic_dataonly.o
Relocation section '.rela.text' at offset 0xb38 contains 1entries:
Offset Info Type Sym. Value Sym. Name + Addend
00000000000c 000f00000002R_X86_64_PC32 0000000000000000 myglob- 4
[...]
这里链接器抱怨的是R_X86_64_PC32重定位。它不能将带有这样重定位的对象链接进一个共享库。
为什么?由于mov的移位(加到rip的部分)必须能装入32比特,当代码进入共享库时,我们不能预先知道32比特是足够的。
毕竟。这是一个全然的64位架构,带有巨大的地址空间。
终于可能在某个超过32比特所允许距离的共享库里找到该符号。这使得R_X86_64_PC32对x64共享库无效。
但我们仍然能够在x64上创建非PIC代码?是的。我们应该指引编译器使用“大代码模型”。通过加入-mcmodel=larger标记。
代码模型的议题是有趣的。但解释它会使我们离题太远。
因此我仅仅能说代码模型是程序猿与编译器之间的一种协议,当中程序猿向编译器做出某种关于程序将要使用偏移大小的承诺。
作为回报,编译器能够生成更好的代码。
结果是要使得编译器在x64上生成能取悦链接器的非PIC代码,仅仅有大代码模型是合适的,由于它是限制最少的。
记住我怎样解释为什么在x64上简单的重定位不够好,操心在链接时偏移会超出32比特。好吧,大代码模型基本上放弃了对偏移的假设。对全部的代码訪问使用最大的64位比特。
这使得加载时重定位总是安全的,使得x64上的非PIC代码生成成为可能。让我们看一下不使用-fpic,使用-mcmodel=large编译第一个样例的反汇编代码:
0000000000000000 <ml_func>:
0: 55 push rbp
1: 48 89 e5 mov rbp,rsp
4: 89 7d fc mov DWORD PTR [rbp-0x4],edi
7: 89 75 f8 mov DWORD PTR [rbp-0x8],esi
a: 48 b8 00 00 00 00 00 mov rax,0x0
11: 00 00 00
14: 8b 00 mov eax,DWORD PTR [rax]
16: 03 45 fc add eax,DWORD PTR [rbp-0x4]
19: 03 45 f8 add eax,DWORD PTR [rbp-0x8]
1c: c9 leave
1d: c3 ret
在地址0xa处的指令将myglob的地址放入eax。注意到它的操作数当前是0,它告诉我们期待一个重定位。还注意到它具有一个完整的64位地址參数。
另外。这个參数是绝对。非RIP相对的。
还有将myglob的值放入eax,这里实际须要两条指令。
这是为什么大代码模型效率更低的一个原因。
如今让我们看一下重定位:
$ readelf -r ml_nopic_dataonly.o
Relocation section '.rela.text' at offset 0xb40 contains 1entries:
Offset Info Type Sym. Value Sym. Name + Addend
00000000000c 000f00000001R_X86_64_64 0000000000000000 myglob+ 0
[...]
注意重定位类型变为R_X86_64,这是一个能够具有64比特值的绝对重定位。它是链接器可接受的,它如今欣然允许将这个目标文件链接入一个共享库。
一些推断性的思考可能让你沉思为什么编译器缺省生成不适合加载时重定位的代码。
答案是简单的。不要忘记代码倾向于直接链接入全然不要求加载时重定位的可运行文件。因此。缺省的编译器假定小代码模型以生成最高效的代码。假设你知道你的代码将进入一个共享库,并且你不希望PIC,那么仅仅要明白告诉它使用大代码模型。我觉得这里gcc的行为是合理的。
还有一件须要考虑的事是为什么PIC代码使用小代码模型就没有问题。原因是GOT总是与訪问它的代码位于同一个共享库里。除非单个共享库超过32位地址空间。使用32位RIP相对偏移訪问PIC是没有问题的。这样巨大的共享库是差点儿不可能的。但万一你碰上一个,AMD64ABI实用于此目的的“大PIC代码模型”。
结论
通过展示PIC怎样在x64架构上工作,本文补充了之前文章没有触及的内容。 X64架构有一个辅助PIC代码更快运行的新的取址模型,因此使得它比x86上代价更高的共享库更令人期待。由于x64眼下是server、桌面及膝上电脑中最流行的架构,知道这些非常重要。因此我尝试关注将代码编译为共享库的另外方面,比方非PIC代码。假设你有不论什么关于未来研究方向的问题或建议,请通过评论或邮件让我知道。 |
一如既往,我使用x64作为被称为x86-64,AMD64或Intel 64的架构的一个方便短名。
放入eax而不是rax是由于myglob的类型是int,在x64上这仍然是32位大小。
随便提一下。在x64束缚一个寄存器远没有那么“痛苦”,由于它的通用寄存器两倍于x86。
假设我们通过向gcc传递-fno-pic显式指定我们不希望PIC,也会发生这种情形。
注意到不像我们在这篇及之前文章里看过的反汇编代码,这是一个目标文件。不是一个共享库或可运行文件。
因此它会包括链接器使用的重定位。
这个议题某些好的资料,參考AMD64 ABI,及man gcc。
某些汇编器称这个指令为movabs以差别于接受一个相对參数的mov。
只是Intel架构手冊还是称之为mov。
它的操作码格式是REX.W + B8 + rd。