2023-08-05 10:00:07

GCC -fPIC选项

我读过GCC的代码生成约定选项,但不明白“生成位置无关代码(PIC)”是做什么的。请举个例子给我解释一下是什么意思。


位置独立代码意味着生成的机器代码不依赖于位于特定地址才能工作。

例如,跳跃是相对的,而不是绝对的。

Pseudo-assembly:

PIC:无论代码在地址100还是1000,这都有效

100: COMPARE REG1, REG2
101: JUMP_IF_EQUAL CURRENT+10
...
111: NOP

非pic:这将只在代码在地址100的情况下工作

100: COMPARE REG1, REG2
101: JUMP_IF_EQUAL 111
...
111: NOP

编辑:回应评论。

如果你的代码是用- fpic编译的,它适合包含在库中——库必须能够从内存中的首选位置重新定位到另一个地址,在你的库首选的地址可能有另一个已经加载的库。


构建在共享库中的代码通常应该是与位置无关的代码,这样共享库就可以轻松地(或多或少)加载到内存中的任何地址。-fPIC选项确保GCC生成这样的代码。


我将试着用更简单的方式解释已经说过的话。

无论何时加载共享库,加载器(在操作系统上加载任何你运行的程序的代码)都会根据对象加载到的位置更改代码中的一些地址。

在上面的例子中,非pic代码中的“111”是加载程序在第一次加载时编写的。

对于非共享对象,您可能希望它是这样的,因为编译器可以对该代码进行一些优化。

对于共享对象,如果另一个进程想要“链接”到该代码,它必须将其读取到相同的虚拟地址,否则“111”将没有意义。但是虚拟空间可能已经在第二个过程中被使用了。


添加更多…

每个进程都有相同的虚拟地址空间(如果在linux操作系统中使用标志停止虚拟地址的随机化) (有关详细信息,仅为自己禁用和重新启用地址空间布局随机化)

因此,如果它是一个没有共享链接的exe(假设场景),那么我们可以始终给相同的asm指令相同的虚拟地址而不会有任何损害。

但是当我们想要将共享对象链接到exe时,我们不确定分配给共享对象的起始地址,因为它将取决于共享对象被链接的顺序。也就是说,.so内部的asm指令总是有不同的虚拟地址,这取决于它所链接的进程。

因此,一个进程可以在自己的虚拟空间中给出起始地址为。So . as 0x45678910,而其他进程同时可以给出起始地址为0x12131415,如果它们不使用相对寻址,. So将根本不起作用。

所以他们总是必须使用相对寻址模式,因此有了fpic选项。


对已经发布的答案的一个小补充:未编译为位置独立的目标文件是可重定位的;它们包含重定位表项。

这些条目允许加载器(将程序加载到内存中的代码位)重写绝对地址,以调整虚拟地址空间中的实际加载地址。

操作系统将尝试与链接到同一共享对象库的所有程序共享加载到内存中的“共享对象库”的单个副本。

由于代码地址空间(与数据空间的部分不同)不需要是连续的,而且大多数链接到特定库的程序都有一个相当固定的库依赖树,因此大多数情况下都能成功。在极少数情况下,如果存在差异,内存中可能需要有两个或多个共享对象库的副本。

显然,任何在程序和/或程序实例之间随机分配库加载地址的尝试(以减少创建可利用模式的可能性)都会使这种情况变得普遍,而不是罕见,因此当系统启用了这种功能时,应该尝试将所有共享对象库编译为与位置无关的。

由于从主程序主体对这些库的调用也可以被重新定位,这使得必须复制共享库的可能性大大降低。


到动态库中函数的链接在库加载时或运行时解析。因此,当程序运行时,可执行文件和动态库都加载到内存中。 无法确定加载动态库的内存地址 因为固定地址可能与另一个需要相同地址的动态库冲突。


有两种常用的方法来处理这个问题:

1.搬迁。如果需要,代码中的所有指针和地址都会被修改,以适应实际的加载地址。重定位是由链接器和加载器完成的。

2.位置无关代码。代码中的所有地址都相对于当前位置。类unix系统中的共享对象默认使用位置无关代码。如果程序运行很长时间,这比重定位效率低,特别是在32位模式下。


“位置无关代码”的名称实际上意味着:

The code section contains no absolute addresses that need relocation, but only self relative addresses. Therefore, the code section can be loaded at an arbitrary memory address and shared between multiple processes. The data section is not shared between multiple processes because it often contains writeable data. Therefore, the data section may contain pointers or addresses that need relocation. All public functions and public data can be overridden in Linux. If a function in the main executable has the same name as a function in a shared object, then the the version in main will take precedence, not only when called from main, but also when called from the shared object. Likewise, when a global variable in the main has the same name as a global variable in the shared object, then the instance in main will be used, even when accessed from the shared object. This so-called symbol interposition is intended to mimic the behavior of static libraries.


共享对象有一个指向其函数的指针表,称为过程链接表(PLT)和一个表 为了实现这个“覆盖”特性,我们需要使用全局偏移表(GOT)。

所有对函数和公共变量的访问都要经过这些表。

p.s.在无法避免动态链接的情况下,有各种方法可以避免位置无关代码的耗时特性。

你可以从这篇文章中阅读更多:http://www.agner.org/optimize/optimizing_cpp.pdf