主要原因是当您创建一个将被其他系统或许多软件使用的对象时(例如,系统库或作为软件套件的一部分的库,如MySQL),我看到PIC被用于Linux。
例如,您可以为PHP、Apache和可能是MySQL编写模块,这些模块需要被这些工具加载,并且将在某个“随机”地址加载它们,并能够以最小的代码工作来执行它们的代码。实际上,在大多数情况下,这些系统会检查您的模块是否是PIC(位置无关代码,如queen3所强调的)模块,如果不是,则拒绝加载您的模块。
这使得大部分代码都可以运行而无需进行所谓的重定位。重定位是将代码加载到基地址后添加的地址修改库代码的操作(尽管这完全安全)。对于动态库来说,这很重要,因为每次它们被不同进程加载时,它们可能会被赋予不同的地址(请注意,这与安全性无关,仅与您的进程可用地址空间有关)。然而,重定位意味着每个版本都是不同的,因为正如我刚才所说,您修改了为每个进程加载的代码,因此每个进程在内存中都有不同的版本(这意味着动态加载库并不能发挥其本应具有的作用!)
PIC机制创建一个表,如其他人所述,该表特定于您的进程,以及那些库使用的读/写内存(.data),但库的其余部分(.text和.rodata部分)保持不变,这意味着可以从一个位置使用它们,供许多进程使用(尽管该库的地址可能从每个进程的角度看是不同的,但请注意,这是所谓的MMU的副作用:内存管理单元,它可以为任何物理地址分配虚拟地址。)
在旧的系统中,例如SGI的著名IRIX系统,机制是为每个动态库预分配一个基地址。这是一种预重定位方式,使得每个进程都可以在一个特定位置找到该动态库,从而使其真正可共享。但是当你有数百个共享库时,为每个库预分配虚拟地址几乎是不可能运行像今天这样的大型系统的。更不用说一个库可能会升级,现在就会挤掉分配给它右边的那个地址了...当时的MMU比今天的MMU不够灵活,PIC也没有被视为好的解决方案。
回答你关于mysql的问题,-DWITH_PIC可能是一个好主意,因为许多工具始终在运行,所有这些库将被加载一次并由所有工具重复使用。因此,在运行时,它将更快。如果没有PIC功能,它肯定必须一遍又一遍地重新加载相同的库,浪费很多时间。因此,多几兆字节可以为您节省数百万个周期,当您24/7运行进程时,这是相当长的时间!
我认为一个汇编小例子能更好地解释我们在这里讨论的内容...
当您的代码需要跳转到某个位置时,最简单的方法是使用跳转指令:
jmp $someplace
在这种情况下,$someplace被称为绝对地址。这是一个问题,因为如果您在不同的位置(不同的基地址)加载代码,则$someplace也会发生变化。为了缓解这个问题,我们有了重定位。这是一个表格,告诉系统将基地址添加到$someplace,以便jmp实际按预期工作。
当使用PIC时,具有绝对地址的跳转指令会以以下两种方式之一转换:通过表格跳转或使用相对地址跳转。
jmp $function_offset[%ebx]
bra $someplace
如您所见,我在这里使用特殊指令bra(分支)而不是跳转来实现相对跳转。如果跳转到代码中同一部分的另一个位置,则可能会使用此方法,尽管在某些处理器中,这种跳转非常有限(即-128到+127字节!),但是在新的处理器中,限制通常为+/-2Gb。
然而,jmp(或者在英特尔上是call指令的jsr)通常用于跳转到不同的函数或超出同一节代码。这样处理函数间调用更加清晰。
在很多方面,您的大部分代码已经在PIC中了,除了:
当您调用另一个函数(除了内联或内置函数)时
当您访问数据时
对于数据,我们面临一个类似的问题,我们需要通过mov从地址加载值。
mov %eax, [$my_data]
%my_data是一个绝对地址,需要重定位(即编译器会保存$my_data相对于节开始的偏移量,在加载时将库加载的基址加到mov指令中地址的位置。)
这就是我们的表格与%ebx寄存器发挥作用的地方。地址的起始位置在表格中的某个特定偏移量处找到,可以检索出来访问数据。这需要两个指令:
mov %eax, $data_pointer[%ebx]
mov %eax, $my_data_offset[%eax]
我们首先加载指向数据缓冲区开头的指针,然后从该指针中加载数据本身。这样做会稍微慢一些,但是第一次加载将被处理器缓存,因此反复重新访问它将是瞬间完成的(没有实际的内存访问)。