我看到在 Linux 下使用 PIC 的主要原因是当您创建一个将由另一个系统或许多软件使用的对象时(即系统库或作为软件套件一部分的库,如 MySQL。)
例如,您可以为 PHP、Apache 和可能的 MySQL 编写模块,这些模块需要由这些工具加载,这将发生在某个“随机”地址,他们将能够以最少的工作量执行其代码代码。实际上,在大多数情况下,这些系统会检查您的模块是否是 PIC(位置独立代码,如queen3 下划线)模块,如果不是,它们会拒绝加载您的模块。
这允许您的大部分代码运行而无需执行所谓的重定位。重定位是对加载代码的基地址的地址的补充,它会修改库的代码(尽管它是非常安全的。)这对于动态库很重要,因为每次它们都由不同的进程加载,它们可能被赋予不同的地址(请注意,这与安全性无关,仅与您的进程可用的地址空间有关。)但是,重定位意味着每个版本都是不同的,因为正如我刚才所说,您修改的代码是为每个进程加载,因此每个进程在内存中都有不同的版本(这意味着动态加载库的事实并没有像其他方式那样做!)
正如其他人所提到的,PIC 机制会创建一个特定于您的进程的表,这些库使用的读/写内存 (.data) 也是如此,但库的其余部分(.text 和 .rodata 部分)仍然存在完整意味着它可以被来自那个位置的许多进程使用(尽管该库的地址可能与每个进程的观点不同,请注意这是所谓的 MMU:内存管理单元的副作用,它可以将虚拟地址分配给任何物理地址。)
过去,在 SGI 著名的 IRIX 系统等系统中,机制是为每个动态库预先分配一个基地址。这是一个预重定位,这样每个进程都会在那个特定位置找到动态库,使其真正可共享。但是,当您拥有数百个共享库时,为每个共享库预先分配一个虚拟地址将使我们几乎不可能运行像我们今天这样的大型系统。而且我什至不会谈论这样一个事实,即一个库可能会升级,然后会碰到一个被分配地址的库……只有当时的 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
正如您在此处看到的,我使用特殊指令 bra (branch) 而不是跳转来获得相对跳转。如果您要跳转到同一段代码中的另一个位置,这是可能的,尽管在某些处理器中这种跳转非常有限(即 -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]
我们首先加载指向数据缓冲区开头的指针,然后从该指针加载数据本身。它有点慢,但是第一次加载将由处理器缓存,因此一遍又一遍地重新访问它无论如何都是瞬时的(没有实际的内存访问。)