诸如COFF和ELF等目标文件格式有一些局限性,会对编译器产生的代码质量有很大影响。由于目标文件格式的设计问题,链接器和编译器只好经常向可执行文件插入本不需要的多余代码。本节将探讨COFF和ELF等一般目标文件格式对可执行代码的影响。
COFF和ELF等一般目标文件格式有一个麻烦,就是设计时并未针对特定CPU生成高效的可执行文件。相反,它们能支持形形色色的CPU,链接目标模块也很容易。糟糕的是,这种多面手不利于生成尽可能出色的目标文件。
COFF和ELF格式的最大难题也许是,目标文件中的重定位值必须适用于目标代码中的32位或64位指针。在某指令要对少于32位或64位的位移值或地址值编码等情形中,就会出现问题。在80x86等处理器上,小于32位的位移值是如此之小,例如80x86的位移值可为8位,以至不可能通过它们引用当前目标模块之外的代码;而在PowerPC、ARM等RISC处理器上,位移值却大得多—PowerPC分支指令可有26位位移值。这会导致代码不尽理想,比如GCC将为了调用外部函数而生成桩函数。请看下列C语言程序及GCC为其产生的PowerPC代码:
PowerPC的GCC汇编输出如下:
因为编译器并不知道以后链接器将printf()例程实际加到最终的可执行文件时,printf()会被放在哪里,所以只得产生L_printf$stub桩函数。printf()不会位于PowerPC的24位分支位移值(扩展到26位)所支持的正负32MB之外。然而,编译器并不知道这一事实。如果printf()位于运行时动态链接的共享库中,那么很可能超出了这个范围。因此,编译器需要做出稳妥选择—采用32位的位移量存放printf()函数地址。然而不幸的是,PowerPC指令不支持32位位移值,因为PowerPC指令都只有32位长。32位位移值将挤得操作码没处放。因此,编译器必须将printf()例程的32位指针存入变量,通过该变量间接跳转。可惜倘若寄存器中没有指针地址,在PowerPC上访问32位内存指针将颇需一些代码。L_printf$stub标号后跟着的那些代码就是为此而设的。
如果链接器能够采纳26位的位移值,而不用32位值,就不需要L_printf$stub例程或L_printf$lazy_ptr指针变量。假如L_printf$stub指令未超出正负32MB的范围,应能直接跳转到printf()例程这个分支。单个程序文件通常不会包含多于32MB的机器指令,所以极少为了调用外部例程而使代码这么麻烦。
糟糕的是,我们对目标文件格式无能为力,只能遵守操作系统指定的格式。操作系统指定的格式在现代32位、64位机器上通常是COFF或ELF的变种。不过,倘若遵从操作系统和CPU强制的目标文件格式限制,我们可以工作得不错。
对于无法直接将32位位移值编码到指令的CPU,如PowerPC、ARM或其他RISC处理器,如果想让代码在其上运行,可以通过尽量避免跨模块调用来优化。尽管创建单个应用程序,将其所有源代码置于一个源文件(或经一次编译处理)并非好的编程做法,但将我们自己的所有函数分开放在不同的源模块,并分别编译—特别是这些例程相互间还存在调用—也确实没有必要。通过把代码用到的某些公共例程放入一个编译单位(即源文件),能够让编译器优化这些函数间的调用,以免在PowerPC之类的处理器上生成桩函数。注意:这一建议并非单单将所有外部函数移入一个源文件而已。只有模块中的函数相互调用,或者共享其他全局变量时代码才会改善。如果函数完全是相互独立的,仅供编译单位外的代码调用,那我们的努力就徒劳无益,因为编译器依然需要为外部代码生成桩例程。