原作者: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. Android学习笔记四:activity的四种启动模式

    Activity有四种启动模式:standard,singleTop,singleTask,singleInstance. 1.standard:标准启动模式 默认模式,这个模式下启动的Activit ...

  2. Linux下设置oracle环境变量

    Linux设置Oracle环境变量 方法一:直接运行export命令定义变量,该变量只在当前的shell(BASH)或其子shell(BASH)下是有效的,shell关闭了,变量也就失效了,再打开新s ...

  3. Android tree应用框架

    简单介绍 一个好的Android应用开发框架,能够加快Android开发速度,今天笔记基于很多开源项目自写了一款Android应用框架. 内容 框架包含:界面管理(Activity管理).数据库操作( ...

  4. 〖Linux〗简单的将Shell和一些文件打包成一个单独的“可执行文件”

    有时候给别人分享一个工具的时候,同时需要提供的文件比较多: 如果分享一个压缩包还得教会对方如何解压.执行哪个脚本,感觉需要传输的内容多了就不方便: 把几个Shell脚本和文件打包成一个“单独的可执行文 ...

  5. topshelf 开发windows 服务资料

    官方配置 http://docs.topshelf-project.com/en/latest/configuration/config_api.html#service-start-modes to ...

  6. Struts1与Struts2的那些事

    一.概述 Struts1以ActionServlet作为核心控制器,由ActionServlet负责拦截用户的全部请求.Struts1框架有3个重要组成部分:Action.ActionForm和Act ...

  7. Ubuntu下安装软件、卸载

    Ubuntu下安装软件.卸载 一般的安装程序有三种: .deb和.rpm这2中安装文件 .boudle这是二进制安装文件 .tar.gz文件是压缩包,与.rar和.zip压缩包一样,安装此类文件需要先 ...

  8. Unable to instantiate application com.txrj.sms.activity.TxrjApplication

    07-18 12:04:57.413: E/AndroidRuntime(4448): FATAL EXCEPTION: main 07-18 12:04:57.413: E/AndroidRunti ...

  9. AWVS扫描工具使用教程

    上文AppScan扫描工具-工作原理&操作教程有写到Web安全漏洞扫描工具之一的APPScan,除此,还有另外一款国内使用比较主流的Web安全漏洞扫描工具——AWVS.相较于大容量的AppSc ...

  10. iOS 自动布局框架 – Masonry 详解

    目前iOS开发中大多数页面都已经开始使用Interface Builder的方式进行UI开发了,但是在一些变化比较复杂的页面,还是需要通过代码来进行UI开发的.而且有很多比较老的项目,本身就还在采用纯 ...