关于将动态链接库函数拷贝到共享存储中运行的调研历程

想要将动态链接库拷贝到共享存储里运行。

相关编译过程概述

概述 - GCC工作的前三个阶段

预编译

  • 处理宏定义和文件包含。
    • 宏定义:可以理解为递归地替换
    • 文件包含:将包含在头文件中的声明拷贝到同一个文件内(extern)。但是这一步的本质是为了让编译器在生成.S文件时的符号表管理和语义检查不会出错。

编译

编译器完成的工作有词法分析、语法分析、语义分析和代码生成(编译优化)。其中词法分析、语法分析和语义分析中涉及到错误处理。语法分析和语义分析还有代码生成涉及到符号表管理。其中,词法分析通过有限状态自动机分析、语法分析通过语法树的生成构建、语义分析则是对一些语义的检查(声明与定义、表达式和语句的正确性等)、代码生成就是翻译为汇编代码的过程。我们重点关注符号表管理。

  • 符号表管理:一个比较典型的符号表保存三个数据结构:变量表、函数表和串表。一种设计是,变量表和函数表使用哈希散列。串表保存的是程序中用到的字符串常量,用链表保存。

汇编

汇编器的构造与编译器大同小异,因为它本质上是汇编的编译器,汇编器的符号来源有四种:数据、标签、宏、外部符号。

其中

  • 标签用于表示一段内存区域的地址,可能来自于函数名
  • 外部符号用于表示引用的其他文件的数据或标签。

此时,已经开始生成ELF格式文件,所以我们比较关心汇编器的符号表和重定位表的操作。

汇编允许符号的后置定义,所以汇编器需要对程序进行两次扫描。第一次扫描的时候,汇编器将引用的不在符号表中的符号作为外部符号处理,添加到符号表。当扫描到符号的定义时,使用该符号的定义信息替换原有的符号信息。

表信息生成

由前面的“ELF文件格式”一节可以知道可重定位目标文件的结构。其中段表、符号表和重定位表是最重要的三个表。

汇编器在第一次扫描时,将汇编信息的段信息导出为段表项,填充到段表。在第二遍扫描时,将符号信息导出为符号表项,填充到符号表,并在产生重定位的位置生成重定位项,填充重定位表。这里重点关注符号表和重定位表。

  • 符号表
    • 符号表中记录的符号信息包括数据定义符号、宏符号、代码标签、函数名等。是为链接器、调试器、反汇编器等服务等。
    • 符号表中记录符号名、所在段、段内偏移、类型(global, local)
    • 对于外部符号,统一设定为全局符号(因为链接器会对全局符号进行符号解析,忽略局部符号的内容)(这是否是普遍的做法?),所在段设为未知。
  • 重定位表
    • 重定位表中记录重定位符号名,重定位位置所在段,重定位位置段内偏移,重定位类型(相对/绝对)。【这里需要注意的是,和符号表的所在段和段内偏移不一样,符号表的这两个属性是符号被定义时的位置,而重定义表中的所在段和段内偏移的属性是在被使用时的位置

链接重定位和位置无关代码

在预编译、编译中所操作的符号表,并不是可执行文件所生成的符号表或重定位表。他们所用的符号表只是在编译过程中,为了执行语法和语义的错误检查而维护的中间变量。我们所真正关心的是最终生成的可执行文件的符号表和重定位表。链接主要负责的工作有地址空间分配、符号解析和重定位。链接器会将同名的段合并。

链接过程中的重定位操作

相对重定位和绝对重定位

  • 相对重定位:使用一个相对地址的引用
  • 绝对重定位,使用一个绝对地址的引用
    • 相对重定位地址=重定位符号地址-重定位位置+重定位位置数据内容=重定位符号地址-下一条指令地址
  • 凡是对数据符号的引用都需要重定位,而对标签符号的引用,只有外部引用时才需要重定位。】调用本地声明的函数时,无论链接器如何调整.text字段,调用者和被调用者的相对位置都不会改变,所以不需要进行重定位。【如call funcall指令内保存的相对地址不会变化。】
  • 具体操作
    • 对于一般的直接引用符号地址的指令会使用绝对地址重定位,对于函数调用或者跳转指令会使用相对地址重定位。(这是否是普遍的做法?)

位置无关代码

位置无关代码,可以加载而无需重定位代码段部分

PIC数据引用

无论在内存中的何处加载一个目标模块,数据段与代码段的距离总是保持不变。所以PIC数据引用利用了这个特点。在数据段开始的地方设置了全局偏移量表(Global Offset Table,GOT)。每个被当前目标模块引用的全局数据目标都有一个8字节条目,编译器为GOT中的每个条目生成一个重定位记录。加载的时候,动态链接器会重定位【GOT中的每个条目】。这意味着动态链接器并不会重定位代码段的信息。

每个引用全局目标的目标模块都有自己的GOT。

PIC函数调用(延迟绑定)

一个典型的应用程序常常使用共享库的很少一部分,而延迟绑定被提出解决这一问题。一个目标模块调用在共享库中的任何函数,就会有自己的GOT和PLT。延迟绑定依赖于两个部分

  • 过程连接表(PLT,Procedure Linkage Table)
    • PLT是以16字节代码为单位(一个条目)构成的数组。PLT[0]跳转到动态链接器中,PLT[1]初始化执行环境,PLT[2]开始的条目调用用户代码调用的函数。
  • 全局偏移量表(GOT)
    • GOT是以8字节的地址为单位构成的数组。GOT[0]和[1]包含动态链接器在解析函数地址时会使用的信息(addr of .dynamic / addr of reloc entries)。GOT[2]是动态链接器在ld-linux.so中的入口点。其余每一条对应于一个被调用的函数,其地址需要在运行时被解析。每个条目都有一条对应的PLT条目。初始时,每个GOT条目都指向对应PLT条目的第二条指令。

整个过程如下(如被调用函数为func):

第一次执行call func

  • 程序经过三次寻址(跳转到PLT,查询GOT条目,跳转到GOT条目对应地址),程序跳转到PLT对应表项的第二条指令。
  • PLT表项的第二条指令,将函数的ID压入栈中,然后跳转到PLT[0]。
  • PLT[0]压入GOT[1]存放的参数(是什么?),然后通过GOT[2]跳转到动态链接器中。动态链接器通过函数ID和GOT[1]参数确定函数func地址。用得到的地址重写对应GOT表中的地址。

第二次执行call func

  • 程序仍然经过上面三次寻址(跳转到PLT,查询GOT条目,跳转到GOT条目对应地址),此时GOT条目对应地址为func地址。

一个问题:延迟绑定修改后的GOT地址是相对地址还是绝对地址?经过思考,应为绝对地址

位置无关代码的函数内部调用

使用一些简单的程序进行实验,对函数内部调用有基本的认识。使用-R查看dynamic relocation entries,使用-d查看反汇编结果,使用-t查看符号表内容,使用-T查看动态符号表内容。RIP寄存器存放着当前指令的地址。

64位RIP寻址

在没有RIP寻址之前,ELF文件的PIC实现大费周章。必须先调用一个函数拿到PC,再加上偏移地址来获得变量的地址或者GOT位置。RIP寻址则解决了这一问题,这让代码可以通过offset(%rip)的形式偏移寻址。

调用全局数据符号(RIP寻址) √

和前面的讲述一致。完成编译的位置无关代码动态链接库调用static数据符号时,采用offset(%rip)的形式找到GOT对应表项。加载的时候GOT对应表项被链接器重定位。而符号本身(也许)是存放在全局变量区,属于数据段的一部分。同时,动态符号表中将产生对应的表项。尚不清楚全局数据符号的实际地址在什么得放,猜测存放在全局变量区。

调用static数据符号(RIP寻址) √

和前面的讲述一致。但是在动态符号表中没有相应表项。猜测符号本身是存放在全局变量区,属于数据段的一部分。

调用函数内部数据符号 √

栈内创建,《编译原理》书上说应通过程序运行时指针sp访问。实际是用栈底+偏移的方式。(offset(%rbp)

补充:call汇编指令(intel user manual. P694)

call汇编指令有许多种形式,其中e8 cd代表字节 0xE8 后面跟着一个2字节操作数表示要跳转到的地址与当前地址的偏移量。 下文中的call在64位机器上启用-fPIC等编译选项后的执行方式正好为e8 cd格式。可以归纳在下面的情景下,都为相对寻址。

调用外部函数符号

调用外部函数符号时,从汇编文件产生时,就已经有的call xxx@PLT的调用形式了。反编译得到的结果可以验证这一点,同时可以肯定,含有@PLT的应为绝对地址,因为最终是通过GOT获得了外部函数地址,而GOT本身是绝对地址。前提是使用位置无关方法

调用static函数符号

调用static函数符号时,汇编文件产生的调用的形式为call xxx,同时汇编文件中没有.globl字段。程序运行时应为相对地址调用。

  • 假如开了-O2选项,编译器有可能(未验证过是否都会)将static声明的函数优化为硬代码插入调用其的函数中。凡是全局声明的函数,都不会被这样优化。

全局函数符号直接拷贝运行失败的原因

对于调用了全局函数符号的代码,直接拷贝最根本的原因应该在于PLT表和GOT表可能产生的错误:

  • 函数首先通过相对偏移跳转到对应的PLT表。此时,【能否找到PLT表】成为第一个问题。问题的关键在于【PLT表是否已经被复制过来】。
  • 接下来需要通过查找GOT表中对应的条目,跳转到对应的GOT表位置。【能否找到GOT表】成为第二个问题。(确定GOT表的位置时采用的寻址方式需要确定)
  • 接下来GOT表记录的绝对地址应该是已经被修改过的。但是因为是绝对地址,所以【这个地址一定是错误的】,这是第三个问题。
  • 如果调用任何数据符号,通过偏移寻址能否找到的关键则在于【有没有将数据段也一并拷贝过来】。

全局函数符号的拷贝解决

首先需要保证【PLT表和数据段被拷贝到代码段的正确相对位置】,其次要保证【PLT表能够正确访问GOT表】,最后要保证【GOT表存储的值被全部修改】。

  • 预编译阶段:全部修改为static函数,这样所有东西都是通过相对寻址访问的了。
  • 编译或汇编阶段:全部修改为static函数解析。
  • 链接或加载阶段:编写llvm pass,将动态链接库中函数按照static方式解析。
  • 程序运行阶段:将动态链接库加载到我们想要拷贝到的地址。