提问人:Jeff 提问时间:8/3/2013 最后编辑:Jeff 更新时间:7/7/2017 访问量:12435
编译WITH_PIC (-DWITH_PIC, --with-pic) 到底有什么作用?
What does compiling WITH_PIC (-DWITH_PIC, --with-pic) actually do?
问:
从源代码编译二进制文件时,生成 PIC 对象与否之间的实际差异是什么?在什么时候,有人会说,“当我编译MySQL时,我应该生成/使用PIC对象。或不?
我读过Gentoo的《位置无关代码简介》、《位置无关代码内部》、《如何修复-fPIC错误》、Libtool的《创建目标文件》和《位置无关代码》。
从PHP的:./configure --help
--with-pic:尝试仅使用 PIC/非 PIC 对象 [default=use both]。
从MySQL的:cmake -LAH .
·DWITH_PIC:生成 PIC 对象
这些信息是一个好的开始,但给我留下了很多问题。
据我了解,它在编译器中打开,而编译器又在生成的二进制文件/库中生成 PIC 对象。我为什么要这样做?反之亦然。也许它的风险更大,或者可能会使二进制文件不太稳定?也许在某些架构上编译时应该避免它(在我的情况下是 amd64/x86_64)?-fPIC
默认的MySQL构建设置PIC=OFF。MySQL官方版本版本设置PIC=ON。而 PHP“试图同时使用两者”。在我的测试设置中,生成的二进制文件略大:-DWITH_PIC=ON
PIC=OFF PIC=ON
mysql 776,160 778,528
mysqld 7,339,704 7,476,024
答:
您想要以这种方式编译的原因确实有两个。
第一,如果你想创建一个共享库。通常,共享库必须是 Linux 上的 PIC。
第二,您可能希望编译主要的可执行文件“PIE”,它基本上是可执行文件的 PIC。PIE 是一项安全功能,允许将地址空间随机化应用于主可执行文件。
评论
可以在启用和禁用 PIC 代码的情况下构建共享库和可执行文件。也就是说,如果您在没有 PIC 的情况下构建它们,它们仍然可以被其他应用程序使用。但是,并非所有地方都支持非 PIC 库 - 但在 Linux 上支持,但有一些限制。
=== 这是一个简短的解释,你不需要;-) ===
PIC 的作用是使代码位置独立。每个共享库都加载到内存中的某个位置 - 出于安全原因,这个地方通常是随机的 - 因此代码中的“绝对”内存引用不能真正是“绝对的” - 实际上,它们相对于库的内存段起始地址。加载库后,必须对其进行调整。
这可以通过遍历所有这些(它们的地址将存储在文件头中)并更正来完成。但这很慢,如果基址不同,则无法在进程之间共享“更正”映像。
因此,通常使用不同的方法。对存储器的每个引用都是通过一个特殊的寄存器(通常是ebx)完成的。调用函数时,它会在开始时跳转到一个特殊的代码块,该代码块将 ebx 值调整为库的内存段地址。然后,该函数使用 [ebx + know offset] 访问其数据。
因此,对于每个程序,只需要调整此代码块,而不是每个函数和内存引用。
请注意,如果知道从同一共享库的其他函数调用函数,则编译器/链接器可以省略 PIC 寄存器 (ebx) 调整,因为已知它已经具有正确的值。在某些架构(尤其是 x86_64)中,程序可以访问相对于 IP(当前指令指针)的数据,该 IP 已经经过绝对调整,因此它限制了对特殊寄存器(如 ebx)及其调整的需求。
=== 这是该部分的结尾,无需阅读即可跳过 ===
那么,为什么要在没有 PIC 的情况下构建一些东西呢?
首先,它会使您的编程速度减慢几个百分点,因为在每个函数开始时都会运行一个额外的代码来调整寄存器,而宝贵的寄存器不适用于优化器(仅限 x86)。函数通常无法知道它是从同一个库调用还是从另一个库调用的,因此即使是内部调用也会受到惩罚。因此,如果您想优化速度 - 尝试在没有 PIC 的情况下进行编译。
然后,如您所见,代码大小会大一些,因为每个函数将包含更多用于设置 PIC 寄存器的指令。
如果我们使用链接时优化(--lto 开关)和受保护的函数可见性,则可以在一定程度上避免这种情况,以便编译器知道哪些函数根本没有在外部调用,因此它们不需要 PIC 代码。但我还没有尝试过。
为什么要使用PIC?因为它更安全(这是地址空间随机化所必需的);因为并非所有系统都支持非 PIC 库;因为非 PIC 库的启动加载时间可能较慢(必须将整个代码段调整为绝对地址,而不仅仅是表存根);如果加载的库段加载到不同的空间中,则无法共享(即,这可能会导致使用更多内存)。然后,并非所有编译器/链接器标志都与非 PIC 库兼容(据我所知,线程本地支持有一些内容),因此有时您根本无法构建非 PIC 代码。
因此,非 PIC 代码的风险更大(安全性较低),您不能总是获得它,但如果您需要它(例如速度) - 为什么不呢?
评论
我看到 PIC 在 Linux 下使用的主要原因是当您创建一个将由另一个系统或许多软件(即系统库或作为软件套件(如 MySQL)一部分的库)使用的对象时。
例如,你可以为PHP、Apache和MySQL编写模块,这些模块需要由这些工具加载,这将发生在某个“随机”地址,它们将能够以最少的代码工作来执行他们的代码。实际上,在大多数情况下,这些系统会检查您的模块是否是 PIC(位置无关代码,如 queen3 下划线所示)模块,如果不是,它们会拒绝加载您的模块。
这样一来,大多数代码都可以运行,而无需执行所谓的重定位操作。重定位是对加载代码的基址地址的添加,并修改了库的代码(尽管它是完全安全的。这对于动态库很重要,因为每次由不同的进程加载它们时,它们可能会被赋予不同的地址(请注意,这与安全性无关,只有进程可用的地址空间)。但是,重定位意味着每个版本都是不同的,因为正如我刚才所说,您修改了为每个进程加载的代码,因此每个进程在内存中都有不同的版本(这意味着动态加载库的事实不会像其他方式那样多!
正如其他人所提到的,PIC 机制会创建一个特定于您的进程的表,就像这些库使用的读/写内存 (.data) 一样,但库的其余部分(.text 和 .rodata 部分)保持不变,这意味着它可以被来自该位置的许多进程使用(尽管该库的地址可能与每个进程的观点不同, 请注意,这是所谓的 MMU(内存管理单元)的副作用,它可以将虚拟地址分配给任何物理地址。
在过去,在SGI著名的IRIX系统等系统下,其机制是为每个动态库预先分配一个基址。这是一个预定位,这样每个进程都会在那个特定位置找到那个动态库,使其真正可共享。但是,当您拥有数百个共享库时,为每个库预先分配一个虚拟地址将使得像我们今天这样运行大型系统几乎是不可能的。我什至不会谈论这样一个事实,即一个图书馆可能会被升级,现在又撞上了被分配了地址的图书馆......只有当时的MMU的通用性不如今天的MMU,PIC还不被视为一个好的解决方案。
为了回答您关于 mysql 的问题,-DWITH_PIC 可能是个好主意,因为许多工具一直在运行,所有这些库都会加载一次并被所有工具重用。因此,在运行时,它会更快。如果没有 PIC 功能,它肯定必须一遍又一遍地重新加载相同的库,从而浪费大量时间。因此,多几 Mb 可以为您每秒节省数百万个周期,当您 24/7 全天候运行进程时,这是相当多的时间!
我在想,也许组装中的一个小例子会更好地解释我们在这里谈论的内容......
当你的代码需要跳转到某个位置时,最简单的方法是使用跳转指令:
jmp $someplace
在这种情况下,$someplace称为绝对地址。这是一个问题,因为如果您在不同的位置(不同的基址)加载代码,那么$someplace也会发生变化。为了缓解,我们有搬迁。这是一个表,告诉系统将基址添加到$someplace以便JMP实际按预期工作。
使用 PIC 时,具有绝对地址的跳转指令以以下两种方式之一进行转换:跳转表或使用相对地址跳转。
jmp $function_offset[%ebx] ; jump to the table where function is defined at function_offset
bra $someplace ; this is relative to IP so no need to change anything
正如你在这里看到的,我使用特殊的指令胸罩(分支)而不是跳跃来获得相对跳跃。如果您跳转到同一代码段中的另一个位置,这是可能的,尽管在某些处理器中,这种跳转非常有限(即 -128 到 +127 字节!),但对于较新的处理器,限制通常是 +/-2Gb。
但是,jmp(或jsr,用于跳转到子例程,在INTEL上是调用指令)通常在跳转到其他函数或同一部分代码之外时使用。这对于处理函数间调用来说要干净得多。
在许多方面,您的大多数代码都已在 PIC 中,但以下情况除外:
- 调用其他函数(内联函数或内部函数除外)时
- 访问数据时
对于数据,我们有一个类似的问题,我们想从一个带有 mov 的地址加载一个值:
mov %eax, [$my_data]
这里 %my_data 将是一个绝对地址,需要重新定位(即编译器将保存 $my_data 与部分开头的偏移量,并且在加载时,加载库的基址将被添加到 mov 指令中的地址位置。
这就是我们的表与 %ebx 寄存器一起发挥作用的地方。地址的开头位于表中的某个特定偏移量处,可以检索它以访问数据。这需要两个指令:
mov %eax, $data_pointer[%ebx]
mov %eax, $my_data_offset[%eax]
我们首先加载指向数据缓冲区开头的指针,然后从该指针加载数据本身。它有点慢,但第一次加载将由处理器缓存,因此无论如何,一遍又一遍地重新访问它将是即时的(没有实际的内存访问)。
评论
有两个概念不应混淆:
- 可重定位的二进制文件
- 与位置无关的代码
他们都处理类似的问题,但在不同的层面上。
问题
大多数处理器体系结构都有两种寻址:绝对寻址和相对寻址。寻址通常用于两种类型的访问:访问数据(读取、写入等)和执行代码的不同部分(跳转、调用等)。两者都可以绝对完成(调用位于固定地址的代码,在固定地址读取数据)或相对(跳转到五条指令后退,相对于指针读取)。
相对寻址通常需要速度和内存。速度,因为处理器必须计算指针的绝对地址和相对值,然后才能访问实际内存位置或实际指令。内存,因为必须存储一个额外的指针(通常在寄存器中,速度非常快,但内存也非常稀缺)。
绝对寻址并不总是可行的,因为如果天真地实现,则必须在编译时知道所有地址。在许多情况下,这是不可能的。从外部库调用代码时,可能不知道操作系统将在哪个内存位置加载库。在对堆上的数据进行寻址时,不会事先知道操作系统将为此操作保留哪个堆块。
然后有很多技术细节。例如,处理器架构只允许相对跳转到某个限制;然后,所有更宽的跳跃都必须是绝对的。或者在具有非常宽的地址范围(例如 64 位甚至 128 位)的架构上,相对寻址将导致更紧凑的代码(因为可以使用 16 位或 8 位作为相对地址,但绝对地址必须始终是 64 位或 128 位)。
可重定位的二进制文件
当程序使用绝对地址时,它们会对地址空间的布局做出非常强烈的假设。操作系统可能无法满足所有这些假设。为了缓解这个问题,大多数操作系统都可以使用一个技巧:二进制文件使用其他元数据进行扩充。然后,操作系统使用此元数据在运行时更改二进制文件,以便修改后的假设适合当前情况。通常,元数据描述指令在二进制文件中的位置,二进制文件使用绝对定位。当操作系统加载二进制文件时,它会在必要时更改存储在这些指令中的绝对地址。
这些元数据的一个示例是 ELF 文件格式的“重定位表”。
一些操作系统使用技巧,因此它们不需要在运行之前总是处理每个文件:它们对文件进行预处理并更改数据,因此它们的假设很可能适合运行时的情况(因此不需要修改)。此过程在 Mac OS X 上称为“预绑定”,在 Linux 上称为“预链接”。
可重定位的二进制文件在链接器级别生成。
位置无关代码 (PIC)
编译器可以生成仅使用相对寻址的代码。这可能意味着数据和代码的相对寻址,或者仅针对这些类别之一。例如,gcc 上的选项“-fPIC”表示强制执行代码的相对寻址(即仅相对跳转和调用)。然后,代码可以在任何内存地址上运行,而无需进行任何修改。在某些处理器架构上,这样的代码并不总是可行的,例如,当相对跳转的范围受到限制时(例如,允许最多 128 条指令宽相对跳转)。
与位置无关的代码在编译器级别进行处理。仅包含 PIC 代码的可执行文件不需要重定位信息。
何时需要 PIC 代码
在某些特殊情况下,绝对需要 PIC 代码,因为在装载过程中重新定位是不可行的。一些例子:
- 某些嵌入式系统可以直接从文件系统运行二进制文件,而无需先将它们加载到内存中。当文件系统已经在内存中时,例如在ROM或FLASH存储器中,通常会出现这种情况。然后,执行程序的启动速度要快得多,并且不需要额外的(通常是稀缺的)RAM。此功能称为“就地执行”。
- 您正在使用一些特殊的插件系统。一个极端的情况是所谓的“shell代码”,即使用安全漏洞注入的代码。然后,您通常不知道代码在运行时的位置,并且有问题的可执行文件不会为您的代码提供重定位服务。
- 操作系统不支持可重定位的二进制文件(通常是由于资源稀缺,例如在嵌入式平台上)
- 操作系统可以在正在运行的程序之间缓存公共内存页。当二进制文件在重新定位期间发生更改时,此缓存将不再起作用(因为每个二进制文件都有自己的重新定位代码版本)。
何时应避免 PIC
- 在某些情况下,编译器可能无法使所有内容独立于位置(例如,因为编译器不够“聪明”,或者因为处理器架构过于受限)
- 由于指针操作较多,与位置无关的代码可能太慢或太大。
- 优化器可能在许多指针操作方面存在问题,因此它不会应用必要的优化,并且可执行文件将像糖蜜一样运行。
建议/结论
由于某些特殊限制,可能需要 PIC 代码。在所有其他情况下,请坚持使用默认值。如果您不知道这些约束,则不需要“-fPIC”。
评论