编译WITH_PIC(-DWITH_PIC, - with-pic)实际上做了什么?

时间:2013-08-02 21:20:50

标签: gcc compilation cmake libtool

从源代码编译二进制文件时,生成PIC objects之间的实际差异是什么?在某个时刻,有人会说,“我应该在编译MySQL时生成/使用PIC对象。”或者不是?

我看过Gentoo's Introduction to Position Independent CodePosition Independent Code internalsHOWTO fix -fPIC errorsLibtool's Creating object filesPosition Independent Code

来自PHP的./configure --help

  

- with-pic:尝试仅使用PIC /非PIC对象[默认=同时使用]。

来自MySQL的cmake -LAH .

  

-DWITH_PIC:生成PIC对象

这个信息是一个好的开始,但给我留下了很多问题。

据我所知,它在编译器中打开-fPIC,然后在生成的二进制文件/库中生成PIC对象。我为什么要那样做?或相反亦然。也许风险更大或者可能使二进制文件更不稳定?编译某些体系结构时可能应该避免(在我的情况下是amd64 / x86_64)?

默认的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

4 个答案:

答案 0 :(得分:18)

有两个概念不应该混淆:

  1. 可重定位二进制文​​件
  2. 位置独立代码
  3. 他们都处理类似的问题,但处于不同的层面。

    问题

    大多数处理器架构都有两种寻址:绝对和相对。寻址通常用于两种类型的访问:访问数据(读,写等)和执行代码的不同部分(跳转,调用等)。两者都可以绝对完成(调用位于固定地址的代码,读取固定地址的数据)或相对(跳转到五条指令,相对于指针读取)。

    相对寻址通常需要成本,速度和内存。速度,因为处理器必须先计算指针的绝对地址和相对值才能访问实际内存位置或实际指令。内存,因为必须存储一个额外的指针(通常在寄存器中,这是非常快但内存非常稀缺)。

    绝对寻址并不总是可行的,因为当天真地实现时,必须在编译时知道所有地址。在许多情况下,这是不可能的。从外部库调用代码时,可能不知道操作系统将在哪个内存位置加载库。在堆上寻址数据时,不会事先知道操作系统将为此操作保留哪个堆块。

    然后有很多技术细节。例如。处理器架构只允许相对跳跃达到一定限度;所有更广泛的跳跃必须是绝对的。或者在具有非常宽的地址范围(例如64位或甚至128位)的架构上,相对寻址将导致更紧凑的代码(因为相对地址可以使用16位或8位,但绝对地址必须始终为64位或128位)。

    可重定位二进制文​​件

    当程序使用绝对地址时,它们会对地址空间的布局做出非常强烈的假设。操作系统可能无法满足所有这些假设。为了解决这个问题,大多数操作系统都可以使用技巧:二进制文件丰富了额外的元数据。然后,操作系统使用此元数据在运行时更改二进制文件,因此修改后的假设适合当前情况。通常元数据描述二进制中指令的位置,它使用绝对定位。当操作系统然后加载二进制文件时,它会在必要时更改存储在这些指令中的绝对地址。

    这些元数据的一个例子是"重定位表"以ELF文件格式。

    某些操作系统使用技巧,因此它们无需在运行之前始终处理每个文件:它们预处理文件并更改数据,因此它们的假设很可能适合运行时的情况(因此无需修改) 。这个过程被称为"预绑定"在Mac OS X和" prelink"在Linux上。

    可重定位二进制文​​件在链接器级别生成。

    位置无关代码(PIC)

    编译器可以生成仅使用相对寻址的代码。这可能意味着对数据和代码的相对寻址,或仅针对这些类别中的一个。选项" -fPIC"在gcc上,例如表示强制执行代码的相对寻址(即仅相对跳转和调用)。然后代码可以在任何内存地址上运行而无需任何修改。在某些处理器体系结构中,这样的代码并不总是可能的,例如,当相对跳跃的范围有限时(例如,允许最多128个指令宽的相对跳跃)。

    位置无关代码在编译器级别处理。仅包含PIC代码的可执行文件不需要重定位信息。

    何时需要PIC代码

    在某些特殊情况下,绝对需要PIC代码,因为在加载过程中重新定位是不可行的。一些例子:

    1. 某些嵌入式系统可以直接从文件系统运行二进制文件,而无需先将其加载到内存中。当文件系统已经在存储器中时,通常就是这种情况。在ROM或FLASH存储器中。然后执行的启动速度更快,并且不需要(通常是稀缺的)RAM的额外部分。此功能称为" execute in place"。
    2. 您正在使用一些特殊的插件系统。一个极端的例子就是所谓的shell代码",即使用安全漏洞注入的代码。然后,您通常不会知道代码在运行时的位置,并且相关的可执行文件不会为您的代码提供重定位服务。
    3. 操作系统不支持可重定位的二进制文件(通常由于资源稀缺,例如在嵌入式平台上)
    4. 操作系统可以在正在运行的程序之间缓存公共内存页面。在重定位期间更改二进制文件时,此缓存将不再起作用(因为每个二进制文件都有自己的重定位代码版本)。
    5. 应避免使用PIC

      1. 在某些情况下,编译器可能无法使所有位置独立(例如,因为编译器不是"聪明"足够或因为处理器架构太受限制)
      2. 由于许多指针操作,与位置无关的代码可能太慢或太大。
      3. 优化器可能在许多指针操作中出现问题,因此它不会应用必要的优化,并且可执行文件将像molasse一样运行。
      4. 建议/结论

        由于某些特殊限制,可能需要PIC代码。在所有其他情况下,坚持使用默认值。如果你不了解这些限制,你就不需要" -fPIC"。

答案 1 :(得分:1)

您希望以这种方式编译有两个原因。

一,如果你想创建一个共享库。通常,共享库必须是Linux上的PIC。

二,您可能想要编译主要的可执行文件“PIE”,它基本上是可执行文件的PIC。 PIE是一种安全功能,允许将地址空间随机化应用于主可执行文件。

答案 2 :(得分:1)

可以在启用和禁用PIC代码的情况下构建共享库和可执行文件。即如果你在没有PIC的情况下构建它们,它们仍可以被其他应用程序使用但是,到处都不支持非PIC库 - 但在Linux上存在一些限制。

===这是您不需要的简要说明;-) ===

PIC的作用是,它使代码位置无关。每个共享库都加载到内存中的某个位置 - 出于安全考虑,这个地方通常是随机的 - 因此代码中的“绝对”内存引用实际上并不是“绝对的” - 事实上它们是相对于库的内存段开始的地址。加载库后,必须对其进行调整。

这可以通过遍历所有这些(它们的地址将存储在文件头中)并更正来完成。但这很慢,如果基址不同,则无法在进程之间共享“已更正”的映像。

因此通常使用不同的方法。每次对存储器的引用都是通过一个特殊寄存器(通常是ebx)完成的。调用函数时,它会在开始时跳转到一个特殊的代码块,该代码块将ebx值调整为库的内存段地址。然后该函数使用[ebx + know offset]访问其数据。

因此,对于每个程序,只需调整此代码块,而不是每个函数和内存引用。

请注意,如果知道函数是从同一个共享库的其他函数调用的,则编译器/链接器可以省略PIC寄存器(ebx)调整,因为已知它已具有正确的值。在某些体系结构(最值得注意的是x86_64)中,程序可以访问相对于IP(当前指令指针)的数据,该数据已经过绝对调整,因此它可以消除对特殊寄存器(如ebx及其调整)的需求。

===以下是可以在不阅读===

的情况下跳过的部分的结尾

那你为什么要在没有PIC的情况下建造东西?

好吧,首先它会使你的程序减慢几个百分点,因为在每个函数的开始处运行一个额外的代码来调整寄存器,并且优化器没有一个珍贵的寄存器(仅限x86)。功能通常无法知道它是从同一个库还是从另一个库中调用,因此即使是内部调用也会受到惩罚。因此,如果您想优化速度 - 尝试在没有PIC的情况下进行编译。

然后,正如您所注意到的那样,代码大小有点大,因为每个函数都会包含一些设置PIC寄存器的指令。

如果我们使用链接时优化(--lto切换)和受保护的函数可见性,这可以在某种程度上避免,以便编译器知道哪些函数根本不在外部调用,因此它们不需要PIC代码。但我还没试过(还)。

为什么要使用PIC?因为它更安全(这是地址空间随机化所必需的);因为并非所有系统都支持非PIC库;因为非PIC库的启动加载时间可能较慢(整个代码段必须调整为绝对地址而不仅仅是表存根);如果将加载的库段加载到不同的空间(即可能导致使用更多的内存),则无法共享它们。然后,并非所有的编译器/链接器标志都与非PIC库兼容(从我记得有关于线程本地支持的东西),所以有时你根本无法构建非PIC代码。

所以非PIC代码风险更大(安全性更低)而你总是无法获得它,但是如果你需要它(例如速度) - 为什么不呢。

答案 3 :(得分:1)

我在Linux下使用PIC的主要原因是当你创建一个将被另一个系统或许多软件使用的对象时(即系统库或作为MySQL等软件套件一部分的库)。

例如,您可以为PHP,Apache和MySQL编写模块,这些模块需要通过这些工具加载,这些模块将在某些"随机"地址,他们将能够以最少的代码工作执行他们的代码。实际上,在大多数情况下,这些系统检查您的模块是否为PIC(位置无关代码,作为queen3下划线)模块,如果不是,则拒绝加载模块。

这允许您的大多数代码无需执行所谓的重定位即可运行。重定位是对加载代码的基址的地址的补充,并且修改了库的代码(虽然它非常安全。)这对于动态库很重要,因为每次它们都由不同的进程加载,它们可能会被赋予不同的地址(请注意,这与安全无关,只能解决您的流程可用的地址空间。)但是,重定位意味着每个版本都不同,因为正如我刚才所说,您修改为每个进程加载的代码,因此每个进程在内存中都有不同的版本(这意味着动态加载库的事实并没有那么多!)

PIC机制创建一个表,正如其他人所提到的那样,特定于您的进程,这些库使用的读/写内存(.data),但库的其余部分(.text和.rodata)。 (段)保持完整意味着它可以被来自那个位置的许多进程使用(尽管该库的地址可能与每个进程的观点不同,请注意这是所谓的MMU的副作用:内存管理单元,可以为任何物理地址分配虚拟地址。)

在过去,在SGI着名的IRIX系统等系统下,机制是为每个动态库预先分配一个基地址。这是一个预先重新定位,因此每个进程都会在该特定位置找到动态库,使其真正可共享。但是当你拥有数百个共享库时,为每个库预先分配一个虚拟地址将使我们几乎无法像现在这样运行大型系统。而且我甚至不会谈论这样一个事实,即一个库可能会升级,现在正好碰到那个分配了地址的那个库......只有当时的MMU比现在的MMU更不通用而且PIC是尚未被视为一个好的解决方案。

要回答关于mysql的问题,-DWITH_PIC可能是一个好主意,因为许多工具一直在运行,所有这些库都将被加载一次并被所有工具重用。所以在运行时,它会更快。如果没有PIC功能,它肯定会一遍又一遍地重新加载同一个库,浪费了大量时间。因此,更多的Mb可以为您节省每秒数百万个周期,当您全天候运行流程时,这需要相当多的时间!


我认为在集会中可能有一个小例子可以更好地解释我们在这里谈论的内容......

当您的代码需要跳转到某个地方时,最简单的方法是使用跳转指令:

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]

我们首先将指针加载到数据缓冲区的开头,然后从该指针加载数据本身。它有点慢,但第一次加载将由处理器缓存,因此无论如何重新访问它将是瞬时的(没有实际的内存访问。)