GCC -fPIC 选项

Posted

技术标签:

【中文标题】GCC -fPIC 选项【英文标题】:GCC -fPIC option 【发布时间】:2011-07-15 18:05:34 【问题描述】:

我已阅读有关 GCC's Options for Code Generation Conventions 的信息,但无法理解“生成与位置无关的代码 (PIC)”的作用。请举个例子来解释一下这是什么意思。

【问题讨论】:

Clang 也使用 -fPIC。 相关:-fpie:***.com/questions/2463150/… 相关,但不是骗子:***.com/questions/23225566/… 【参考方案1】:

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

例如跳跃会以相对而非绝对的形式生成。

伪组装:

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 编译,则它适合包含在库中 - 库必须能够从其在内存中的首选位置重定位到另一个地址,在您的库地址处可能有另一个已加载的库喜欢。

【讨论】:

这个例子很清楚,但作为一个用户,如果我创建一个没有该选项的共享实验室 (.so) 文件会有什么不同?在某些情况下,如果没有 -fPIC,我的库会无效吗? 是的,构建一个不是 PIC 的共享库可能是一个错误。 更具体地说,共享库应该在进程之间共享,但可能并不总是可以在两个进程中加载​​相同地址的库。如果代码不是位置独立的,那么每个进程都需要自己的副本。 @Narek:如果一个进程想要在同一虚拟地址加载多个共享库,则会发生错误。由于库无法预测可以加载哪些其他库,因此传统的共享库概念无法避免这个问题。虚拟地址空间在这里没有帮助。 编译程序或静态库时可以省略-fPIC,因为一个进程中只会存在一个主程序,因此不需要运行时重定位。在某些系统上,程序仍然是position independent 以增强安全性。【参考方案2】:

我将尝试以更简单的方式解释已经说过的内容。

每当加载共享库时,加载程序(操作系统上加载您运行的任何程序的代码)都会根据对象加载到的位置更改代码中的某些地址。

在上面的例子中,非PIC代码中的“111”是加载器第一次加载时写入的。

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

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

【讨论】:

Whenever a shared lib is loaded, the loader changes some addresses in the code depending on where the object was loaded to. 我认为如果使用 -fpic 编译以及 -fpic 存在的原因(即出于性能原因或因为您有一个无法重定位的加载程序或因为您需要多个副本)这是不正确的在不同的位置或出于更多原因。 为什么不总是使用-fpic? @Jay - 因为每次函数调用都需要一次计算(函数地址)。所以在性能方面,如果不需要,最好不要使用它。【参考方案3】:

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

【讨论】:

如果没有-fPIC 标志,为什么不能在内存中的任何地址加载共享库?是不是和程序没有联系?当程序运行时,操作系统会将其上传到内存中。我错过了什么吗? 是否使用了-fPIC 标志,以确保该库可以加载到链接它的进程中的任何虚拟地址?抱歉,双 cmets 5 分钟过去了,无法编辑前一个。 区分构建共享库(创建libwotnot.so)和链接它(-lwotnot)。链接时,您无需对-fPIC 大惊小怪。过去在构建共享库时,您需要确保将-fPIC 用于要构建到共享库中的所有目标文件。规则可能已经改变,因为现在编译器默认使用 PIC 代码构建。所以,我相信,20 年前很重要,7 年前可能很重要的东西,现在已经不那么重要了。操作系统内核之外的地址“总是”虚拟地址。 所以之前你必须添加-fPIC。如果不传递此标志,构建 .so 时生成的代码是否需要加载到可能正在使用的特定虚拟地址? 是的,因为如果您不使用 PIC 标志,代码就不能可靠地重定位。如果代码不是 PIC,就不可能实现像 ASLR(地址空间布局随机化)这样的事情(或者,至少,很难实现以至于它们实际上是不可能的)。【参考方案4】:

进一步添加...

每个进程都有相同的虚拟地址空间(如果在 linux OS 中通过使用标志来停止虚拟地址的随机化) (更多详情Disable and re-enable address space layout randomization only for myself)

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

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

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

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

【讨论】:

感谢虚拟地址的解释。 谁能解释一下这对静态库来说不是问题,为什么你不必在静态库上使用 -fPIC ?我知道链接是在编译时完成的(或者实际上是在编译时完成的),但是如果你有 2 个带有位置相关代码的静态库,它们将如何链接? @MichaelP 目标文件有一个位置依赖标签表,当链接特定 obj 文件时,所有标签都会相应更新。无法对共享库执行此操作。【参考方案5】:

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


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

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

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


与位置无关的代码”这个名称实际上暗示了以下内容:

代码段不包含需要重定位的绝对地址,只有自相关 地址。因此,代码段可以加载到任意内存地址并在多个进程之间共享。

数据部分不会在多个进程之间共享,因为它通常包含 可写数据。因此,数据部分可能包含指针或地址 需要搬家。

所有公共函数和公共数据都可以在 Linux 中被覆盖。如果一个函数 在主可执行文件中与共享对象中的函数具有相同的名称,则 main 中的版本将优先,不仅在从 main 调用时,而且在 从共享对象调用。同样,当 main 中的全局变量具有相同的 name 作为共享对象中的全局变量,那么 main 中的实例将是 使用,即使从共享对象访问时也是如此。这种所谓的符号插入旨在模仿静态库的行为。


共享对象有一个指向其函数的指针表,称为过程链接表 (PLT),以及一个表 指向其变量的指针称为全局偏移表 (GOT),以实现此“覆盖”功能。

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

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

您可以从这篇文章中了解更多信息:http://www.agner.org/optimize/optimizing_cpp.pdf

【讨论】:

【参考方案6】:

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

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

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

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

显然,任何在程序和/或程序实例之间随机化库加载地址的尝试(以减少创建可利用模式的可能性)都会使这种情况变得普遍,而不是罕见,因此在系统启用的情况下这种能力,人们应该尽一切努力将所有共享对象库编译为位置独立。

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

【讨论】:

以上是关于GCC -fPIC 选项的主要内容,如果未能解决你的问题,请参考以下文章

arm gcc 内嵌汇编,gcc该是啥选项呢

gcc 常用编译选项

Gcc常用选项及编译过程

gcc 的 -static 选项?

为啥 gcc -O1 的行为与 gcc -O0 + 选项不同

GCC 编译选项