原作者:Eli Bendersky

http://eli.thegreenplace.net/2011/11/11/position-independent-code-pic-in-shared-libraries-on-x64

之前的文章。以为x86架构编译的代码为样例,解释了位置无关代码(PIC)怎样工作。我承诺在还有一篇文章里涉及x64[1]上的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 =
;

 

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[2]

x64PIC函数调用——一个样例

如今让我们看一下在x64上PIC代码怎样进行函数调用。再次,我们将使用之前文章里的样例:

int myglob =
;

 

intml_util_func(int a)

{

    return a +
;

}

 

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相对取址不须要这种寄存器[3]。总而言之,x64PIC导致的性能影响远小于x86,使得它更有吸引力。

其实。如此有吸引力,这是这个架构上编写共享库的缺省方法。

额外的学分:x64上的非PIC代码

gcc不仅鼓舞你在x64上对共享库使用PIC,它缺省地要求它。比如,假设我们没有使用-fpic[4]编译第一个样例。然后尝试将它链接入一个共享库(使用-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的反汇编代码[5]

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标记。

代码模型的议题是有趣的。但解释它会使我们离题太远[6]

因此我仅仅能说代码模型是程序猿与编译器之间的一种协议,当中程序猿向编译器做出某种关于程序将要使用偏移大小的承诺。

作为回报,编译器能够生成更好的代码。

结果是要使得编译器在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相对的[7]

还有将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代码。假设你有不论什么关于未来研究方向的问题或建议,请通过评论或邮件让我知道。


[1]一如既往,我使用x64作为被称为x86-64,AMD64或Intel 64的架构的一个方便短名。

[2] 放入eax而不是rax是由于myglob的类型是int,在x64上这仍然是32位大小。

[3] 随便提一下。在x64束缚一个寄存器远没有那么“痛苦”,由于它的通用寄存器两倍于x86。

[4]假设我们通过向gcc传递-fno-pic显式指定我们不希望PIC,也会发生这种情形。

[5] 注意到不像我们在这篇及之前文章里看过的反汇编代码,这是一个目标文件。不是一个共享库或可运行文件。

因此它会包括链接器使用的重定位。

[6] 这个议题某些好的资料,參考AMD64 ABI,及man gcc。

[7] 某些汇编器称这个指令为movabs以差别于接受一个相对參数的mov。

只是Intel架构手冊还是称之为mov。

它的操作码格式是REX.W + B8 + rd。

x64共享库中的位置无关代码(PIC)的更多相关文章

  1. JZ2440开发笔记(9)——位置无关代码设计【转】

    b MAIN 和 ldr pc,=MAIN 的区别(谈到代码位置无关性) 看bootloader的时候经常看到这两种写法,不太明白区别,网上查了查.其实看了之后还是一头雾水? 其中,2和3 似乎是一个 ...

  2. uboot之位置无关代码解析

    在之前的话 新年过去了,那么久没有好好学习,感觉好颓废,现在就uboot的一些基础问题做一些笔记,顺便分享给大家,不过由于见识有限,如果有不足之处请多多指教. 位置无关?什么意思?我们先了解一些基础知 ...

  3. 在Team Foundation Server (TFS)的代码库或配置库中查找文件或代码

    [update 2017.2.11] 最新版本的TFS 2017已经增加了代码搜索功能,可以参考这个链接 https://blogs.msdn.microsoft.com/visualstudioal ...

  4. Java小题,通过JNI调用本地C++共享库中的对应方法实现杨辉三角的绘制

    1.在Eclipse中配置Javah,配置如下 位置是你javah.exe在你电脑磁盘上的路径 位置:C:\Program Files\Java\jdk1.8.0_112\bin\javah.exe ...

  5. C 标准库 中 操作 字符串 的 代码

    1)字符串操作 strcpy(p, p1) 复制字符串 strncpy(p, p1, n) 复制指定长度字符串 strcat(p, p1) 附加字符串 strncat(p, p1, n) 附加指定长度 ...

  6. 2015.7.24 CAD库中列举五字代码点所属航路及终端区图,左连接的累加

    select decode(fb.tupr,null,'仅航路',decode(fc.aw,null,'仅终端区','航路及终端区')) 范围,pt 五字代码点,fb.tupr 终端区图及程序,fc. ...

  7. 如何统计TFS代码库中的团队项目所占用的磁盘空间

    在一个开发团队较多的研发中心,当开发人员的代码数据积累到一定程度,TFS系统的磁盘空间的使用率会逐渐成为系统管理员关注的问题.你可能会关注代码库中每个团队项目,甚至每个目录占用的的磁盘空间.不幸的,即 ...

  8. 链接(extern、static关键词\头文件\静态库\共享库)

    原文链接:http://www.orlion.ga/781/ 一. 多目标文件的链接 假设有两个文件:stack.c: /* stack.c */ char stack[512]; int top = ...

  9. linux下制作共享库.a和 .so

    接触linux时间不长,总是感觉底气不足,很多东西总是感到迷迷糊糊,其实是因为没找拿到linux C的两把钥匙: makefile和动态库.共享库.linux C中几乎所有的程序都是以库的形式给出,如 ...

随机推荐

  1. 转:CentOS系统yum源配置修改、yum安装软件包源码包出错解决办法!

    yum安装包时报错: Could not retrieve mirrorlist http://mirrorlist.repoforge.org/el6/mirrors-rpmforge error ...

  2. java 如何使用多线程调用类的静态方法?

      1.情景展示 静态方法内部实现:将指定内容生成图片格式的二维码: 如何通过多线程实现? 2.分析 之所以采用多线程,是为了节省时间 3.解决方案 准备工作 logo文件 将生成的文件保存在F盘te ...

  3. V-rep学习笔记:并联机构正逆运动学

    Solving the FK problem of simple kinematic chains is trivial (just apply the desired joint values to ...

  4. 常见pip方法

    pip search 包名   查询 pip install  包名   安装包 pip show--files 包名 pip list --outdated  检查哪些包需要更新 pip insta ...

  5. java new关键字

    //new关键字://1.表示创建一个对象//2.表示实例化对象//3.表示申请内存空间 在python中其实就是一个实例化的过程

  6. Qt5中创建临时的后台线程。

    有个需求就是,GUI图形界面在上传文件到服务器的时候,需要用zip命令行打包,因为文件很多的时候,zip命令打包需要计算很长时间,所以把这样计算量大的任务分离到后台线程比较合适,然后任务完成,以信号槽 ...

  7. IDEA导入项目jar包红线, cannot resolve symbol xxxx问题

    简单来说遇到的这种情况2种方式解决了, 不知道还有没有其他原因的. 1.reimport包 2.清缓存重启 针对1方法: a.确实不缺包: 可以先注释掉pom文件中的jar包, 此时idea会提示im ...

  8. SQL语句之order by 、group by、having、where

    百度知道:1.order by是 按字段进行排序.. 字段后面可跟desc降序..asc升序..默认为升序2.group by是进行分组查询3.having和where都属于条件过滤 区别在于一般ha ...

  9. [转]nonlocal和global

    在Python中,当引用一个变量的时候,对这个变量的搜索是按找本地作用域(Local).嵌套作用域(Enclosing function locals).全局作用域(Global).内置作用域(bui ...

  10. Linux 操作MySQL常用命令行(转)

    注意:MySQL中每个命令后都要以分号:结尾. 1.显示数据库 mysql> show databases; +----------+ | Database | +----------+ | m ...