ChatGPT解决这个技术问题 Extra ChatGPT

GCC -fPIC 选项

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

Clang 也使用 -fPIC。
相关,但不是骗子:stackoverflow.com/questions/23225566/…

D
Drew Dormann

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

例如,跳跃将产生为相对而非绝对。

伪组装:

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 以增强安全性。
B
Błażej Michalik

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

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

在上面的例子中,非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 - 因为每个函数调用都需要一次计算(函数地址)。所以在性能方面,如果不需要,最好不要使用它。
J
Jonathan Leffler

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


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

进一步添加...

每个进程都有相同的虚拟地址空间(如果在 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 文件时,所有标签都会相应更新。这不能对共享库进行。
K
Konstantin Burlachenko

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

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

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

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

“位置无关代码”这个名称实际上意味着以下内容:

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

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

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

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

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

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

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


u
user1016759

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

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

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

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

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

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