undefined reference to `__GOTT_BASE__' undefined reference to `__GOTT_INDEX__'

问题:

编译的静态库或者动态库没有添加-fpic的属性

 

分析:全局偏移表GOT(Global Offset Table)索引是在链接器建立的,属于.data数据段,是gp寄存器管理。

PIC介绍

    PIC 代码在调用函数和对数据变量进行操作的方式上与传统代码截然不同。它将通过间接表"全局偏移表"(GOT)访问这些函数和数据,通过使用保留名称"_GLOBAL_OFFSET_TABLE_"可访问的软件约定。用于此用途的确切机制取决于硬件体系结构,但通常保留一个特殊的计算机寄存器,用于在输入函数时设置 GOT 的位置。此间接寻址背后的原理是生成可以独立访问实际负载地址的代码。在文本段没有重定位的真正 PIC 库中,只有"全局偏移表"中导出的符号需要运行时更新,具体取决于正在运行的进程地址空间中各种共享库的当前加载地址。

同样,对全局定义的函数的过程调用通过位于核心映像数据段中的"过程链接表"(PLT)重定向。同样,这样做是为了避免对文本段进行运行时修改。

链接器编辑器在将 PIC 对象文件合并到适合映射到进程地址空间的图像中时,分配全局偏移表和过程链接表。它还收集运行时链接编辑器可能需要的所有符号,并将这些符号与图像的文本和数据位一起存储。另一个保留符号 @DYNAMIC 用于指示运行时链接器结构的存在。每当 _DYNAMIC 重新定位到 0 时,就无需调用运行时链接编辑器。如果此符号是非零,则指向数据结构,可以从中派生必要的重新定位和符号信息的位置。这在启动模块 crt0、crt1S 和最近的 Scrt1 中最为显著。_DYNAMIC 结构通常位于与它相关的图像的数据段的开头。

在大多数体系结构上,将源代码编译为对象代码时,需要指定对象代码是否应独立于位置。偶尔有一些体系结构没有区别,通常是因为所有对象代码都根据应用程序二进制接口ABI) 独立定位,或者较少出现,因为对象的负载地址在编译时是固定的,这意味着此类平台不支持共享库。如果将对象编译为独立于位置的代码 (PIC),则操作系统可以在任何地址加载该对象,以便准备执行。这涉及在编译时用相对地址替换直接地址引用的时间开销,以及维护信息以帮助运行时加载程序在运行时填充未解析的地址时的时间开销。因此,PIC 对象在运行时通常比等效的非 PIC 对象稍大且速度慢。重新使用共享库中的 PIC 对象代码后,在磁盘和内存中共享库代码的优势就超过了这些问题。

PIC 编译正是将成为共享库一部分的对象所需的内容。因此,libtool 生成 PIC 对象,用于共享库和非 PIC 对象,用于静态库。每当 libtool 指示编译器生成 PIC 对象时,它还会定义预处理器符号"PIC",以便程序集代码可以知道它是否驻留在 PIC 对象中。

通常,由于 libtool 正在编译源,它将生成 .lo 对象(作为 PIC)和 .o 对象(作为非 PIC),然后在链接各种可执行文件和库时使用相应的对之一。在没有区别的体系结构上,.lo 文件只是指向 .o 文件的软链接。

实际上,您可以将 PIC 对象链接到静态存档中,以便在执行和加载速度方面获得少量开销,并且通常也可以类似地将非 PIC 对象链接到共享存档。

使用与位置无关的代码时,可重定位引用将作为间接生成,使用共享对象的数据段中的数据。文本段代码保持只读状态,并且所有重定位更新都应用于数据段中的相应条目。

如果共享对象是从不独立于位置的代码构建的,则文本段通常需要在运行时执行大量重定位。尽管运行时链接器已具备处理此情况的能力,但这种情况造成的系统开销可能会导致性能严重下降。

您可以使用 readelf -d foo 等工具标识需要对其文本段进行重定位的共享对象,并检查任何 TEXTREL 条目的输出。TEXTREL 条目的值不相关。它在共享对象中的存在表示存在文本重定位。

    简单地说,当链接器创建共享库时,它事先不知道它可能加载到何处。这会对库中的数据和代码引用造成问题,这应该以某种方式指向正确的内存位置。

在 Linux ELF 共享库中,有两种主要方法可以解决此问题:

  • 加载时重新定位 Load-time relocation
  • 定位独立代码 (PIC)Position independent code

已涵盖加载时间重新定位。在这里,我想解释第二种方法 - PIC。

    因此,它只解释 PIC 在 x86 上是如何工作的,特别是选择这个较旧的体系结构,因为(与 x64 不同),它不是在设计时考虑到 PIC,因此在它上实现 PIC 有点棘手。

加载时重定位的一些问题
正如我们在上一篇文章中所看到的,加载时重定位是一个相当简单的方法,它的工作原理。但是,PIC 现在更受欢迎,并且通常是构建共享库的推荐方法。为什么会这样?

加载时重定位有几个问题:执行需要时间,并且它使库的文本部分不可共享。

首先,性能问题。如果共享库与加载时重定位条目链接,则加载应用程序时实际执行这些重定位需要一些时间。您可能认为成本不应太大 - 毕竟,加载程序不必扫描整个文本部分 - 它应该只查看重新定位条目。但是,如果一个复杂的软件在启动时加载多个大型共享库,并且每个共享库必须首先应用其加载时重定位,则这些成本可能会累积,并导致应用程序的启动时间明显延迟。

二是不可共享文本部分问题,比较严重。首先,共享库的要点之一是保存 RAM。多个应用程序使用一些常见的共享库。如果共享库的文本部分(代码位于何处)只能加载到内存中一次(然后映射到许多进程的虚拟内存),则可以保存大量 RAM。但是,在加载时重定位中这是不可能的,因为使用此技术时,必须在加载时修改文本部分以应用重定位。因此,对于加载此共享库的每个应用程序,必须再次将其完全置于 RAM 中。不同的应用程序将无法真正共享它。

此外,具有可写文本部分(必须保持可写性,以便动态加载程序执行重定位)会带来安全风险,从而更容易利用应用程序。

正如我们在本文中看到的,PIC 主要缓解了这些问题。

PIC - 简介
PIC 背后的理念很简单 - 向代码中的所有全局数据和函数引用添加额外的间接级别。通过巧妙地利用链接和加载过程的某些工件,可以使共享库的文本部分真正独立,因为它可以轻松地映射到不同的内存地址,而无需更改一个位。在接下来的几节中,我将详细解释如何实现这一壮举。

关键洞察#1 - 文本和数据部分之间的偏移
PIC 所依赖的关键见解之一是文本和数据部分之间的偏移,链接器在链接时就知道。当链接器将多个对象文件组合在一起时,它会收集其节(例如,所有文本节都统一到单个大文本节中)。因此,链接器知道节的大小及其相对位置。

例如,文本节的后跟数据节可能紧跟在数据节后面,因此文本部分中任何给定指令到数据节开头的偏移量只是文本节的大小减去从e 文本部分 - 链接器知道这两个数量。

全局偏移表 (GOT)
有了这个,我们终于可以在x86上实现与位置无关的数据处理。它通过"全局偏移表"或简称 GOT 来实现。

GOT 只是一个地址表,位于数据部分中。假设代码部分中的一些指令想要引用变量。它不是直接引用绝对地址(这需要重新定位),而是引用 GOT 中的条目。由于 GOT 位于数据部分的已知位置,因此此引用是相对的,并且链接器已知。GOT 条目将包含变量的绝对地址:

    通过通过 GOT 重定向变量引用,从而摆脱了代码部分中的重定位。但是,我们还在数据部分创建了一个重定位。为什么?因为 GOT 仍必须包含变量的绝对地址,以便上述方案正常工作。那么,获得了什么?

事实证明,很多。数据部分中的重定位问题远小于代码部分中的问题,原因有二(这直接解决了本文开头所述代码加载时重定位的两个主要问题):

代码部分中的重新定位是每个变量引用所必需的,而在 GOT 中,我们只需要每个变量重新定位一次。对变量的引用可能比变量多得多,因此效率更高。
数据部分是可写化的,并且无论如何都不会在进程之间共享,因此向其添加重定位不会造成伤害。但是,从代码部分移动重定位允许将其为只读并在进程之间共享。

PIC 中的函数调用
好了,这就是数据寻址在位置独立代码中的工作方式。但是函数调用呢?从理论上讲,完全相同的方法也可以适用于函数调用。与其实际调用实际包含要调用的函数的地址,不如让它包含已知 GOT 条目的地址,并在加载过程中填写该条目。

但这不是函数调用在 PIC 中的工作方式。实际发生的情况要复杂一些。在我解释它是如何做的之前,先说几句关于这种机制的动机。

惰性绑定优化
当共享库引用某些函数时,该函数的实际地址直到加载时间才知道。解析此地址称为绑定,这是动态加载程序在将共享库加载到进程的内存空间时所做的。此绑定过程不分项,因为加载程序必须实际查找特殊表中的函数符号 。

因此,解析每个函数需要时间。时间不多,但加起来,因为库中的函数量通常远远大于全局变量的数量。此外,大多数这些解决方法都是徒劳的,因为在程序的典型运行中,只有一小部分函数实际被调用(想想处理错误的各种函数和特殊条件,这些函数通常不会调用)。

因此,为了加快这一过程,设计了一个聪明的惰性绑定方案。{"Lazy"是计算机编程中一系列优化的通用名称,其工作被延迟到实际需要的最后一刻,如果程序的特定运行过程中不需要其结果,则避免执行此操作。懒惰的好例子是写文复制和惰性评估。

此惰性绑定方案是通过添加另一个间接级别 - PLT 来实现的。

过程链接表 (PLT)
PLT 是可执行文本部分的一部分,由一组条目组成(共享库调用的每个外部函数一个条目)。每个 PLT 条目都是可执行代码的短块。代码不是直接调用函数,而是在 PLT 中调用一个条目,然后该条目会注意调用实际函数。这种安排有时被称为"跳绳"。每个 PLT 条目在 GOT 中也有一个相应的条目,其中包含对函数的实际偏移量,但仅当动态加载程序解析它时。我知道这很令人困惑。

PIC效率
但PIC也并非没有问题。一个显而易见的成本是 PIC 中所有外部引用数据和代码所需的额外间接。对于对全局变量的每个引用以及每次对函数的调用,这是额外的内存负载。这在实践中有多成问题取决于编译器、CPU 体系结构和特定应用程序。

另一个不太明显的成本是实现 PIC 所需的寄存器使用量增加。为了避免过于频繁地定位 GOT,编译器生成将其地址保存在寄存器(通常是 ebx)的代码是有意义的。但是,为了GOT,整个登记册都结下了不上。虽然对于往往具有大量通用寄存器的 RISC 体系结构来说,这不是一个大问题,但它对具有少量寄存器的 x86 等体系结构提出了性能问题。PIC 意味着少一个通用寄存器,这增加了间接成本,因为现在必须进行更多的内存引用。

结论
PIC正变得越来越流行。某些非英特尔架构(如 SPARC64)强制共享库仅使用 PIC 代码,而许多其他体系结构(例如 ARM)则包含 IP 相关寻址模式,以使 PIC 更高效。

 

生成的ELF或者so文件,可以查看相关的具体的文件格式定义

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

道格拉斯范朋克

播种花生牛奶自留田

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值