
在本文中,您将学习有关 Linux™ on POWER™ 中 GNU 工具链的知识。本文将重点介绍可用于使用 Linux on POWER 上的 GNU 编译器(compiler)、链接器(linker)和加载器(loader)的一些常用选项,还将讨论 GNU binutils,重点考虑特定于 Linux on POWER 的一些考虑事项,连同 SUSE Linux Enterprise Server Version 9 和 Red Hat Enterprise Linux Version 4 中提供的一些新特性。
简介 GNU 软件的一个显著长处是他来自严格的研发者社区,具备很好的可移植性。GNU 研发工具链(toolchain)是指 GNU Compiler Collection、GNU libc 连同用来编译、测试和分析软件的 GNU binutils。这些工具遵守 PowerOpen ABI 和 64 位的 PowerPC® ELF ABI Supplement 规范,从而确保和其他类似工具的二进制兼容性。此外,他们是 Linux on POWER 的默认研发工具链。
尽管 GNU 工具重点强调的是兼容性,但是和其他通用研发平台相比,他们在 POWER 架构上仍然有一些特有的区分。POWER 上的指令集和其他架构有所不同,GNU toolchain 负责处理这些区分。研发人员应该理解使用 GNU toolchain for Linux on POWER 的细节。例如,在 Linux on POWER ABI 和其他通用的 ABI 之间有所差异,研发人员必须知道在研发和移植软件时,这些差异会对自己的代码产生什么影响。
值得一提的是 IBM XL C/C++ 和 Fortran 编译器也会使用 GNU toolchain 来生成二进制文档。在这些相关的地方,我们将针对 XL C/C++ 对 GNU toolchain 进行讨。通过逐步解决基本操作、特定于 POWER 的操作、潜在的缺陷和 GNU toolchain 一些新研发的特性,本文解决了 GNU toolchain for Linux on POWER 的软件研发和可移植性需求问题。本文还对 GCC C/C++ 编译器、GNU 链接器和汇编程式连同其他一些 GNU binutils 进行了探索。虽然更有很多平台都没有介绍,但 GNU 手册通常都会引用一些完整介绍这些主题的资料。本文并没有讨论 Linux on POWER 系统中其他 GCC 编译器、IBM XL C/C++、Fortran 或 Java 研发的一些细节问题。
我们从 GCC 编译器和编译驱动器入手开始介绍,依次介绍 GNU 链接器、GNU 汇编程式和其他的 binutils。本文没有明确地对 GNU C Library 进行讨论,因为特定于 POWER 的变体很少,应该不会影响到用户,C Library 通常也被认为是 GNU Toolchain 的一部分。在合适的地方,我们给出了在 Linux on POWER 上发现的一些细微区分,为那些熟悉其他平台上的 Linux 或在 POWER 架构上运行的 AIX 的研发人员提供帮助。(例如,x86 和 POWER 之间就有一些区分:前者的汇编程式不同,后者在 ELF 和 XCOFF ABI 之间也有所不同。)
GNU Compiler Collection 从历史传统上来说,GCC 是代表 GNU C Compiler,但是现在他代表 GNU Compiler Collection。GCC 是用于 C、C++、Objective-C、Java™、Fortran 和 Ada 编程语言的一个编译器集。但是本文的讨论仅仅局限于 C 和 C++ 编译器,尤其是那些 GCC 的操作和选项、特定于 POWER 架构的 GCC 特性操作和受 GCC 支持的最新特性,这些特性是随 Linux for POWER 架构的两个领先发行版本(Red Hat Enterprise Server AS 和 SUSE LINUX Enterprise Server)一起打包的。
GCC 的基本操作 GCC 的基本操作驱动着预处理、编译、汇编和链接的过程。传递给 GCC 的大部分选项实际上都会被重定向给工具链中的其他组件。有些选项,例如平台的选择、调试和优化标记,会为编译器和其他组件同时提供参数。
输入选项 编译器必须知道要处理的是哪种类型的输入数据。例如,C 源程式文档的处理就和 C++ 文档不同。对于编译器来说,有一个基本的隐式选项:源程式文档的扩展名。该选项将决定调用哪一个 GCC 编译器。例如,file.c 调用的是 C 编译器,而 foo.C 或 foo.cxx 则会调用 C++ 编译器。在 GCC 手册中列出了可接收源代码文档的扩展名的完整列表。
下图展示了 GNU Toolchain 生成可执行程式或共享对象所应采取的步骤。GCC 驱动器通过接受一些选项对整个 toolchain 间接进行控制。
图 1. 生成可执行程式或共享对象的步骤 生成可执行程式或共享对象的步骤
输出选项 编译器还必须知道用户期望获得哪种类型的输出。GCC 驱动器能够为整个工具链的其他部分产生一些指令,从而生成最终的可执行程式,或在生成一些中间文档时就停止。您能够使用源程式的文档扩展名或命令行选项来控制编译器的输出。
表 1 对输出文档的选项进行了总结。
表 1. 输出文档的选项 标记 操作 输出格式-c 汇编,但不链接 .o 文档 -S 编译,但不汇编 .s 文档 -E 预处理,但不编译 对 .c 文档进行预处理后的结果
例如,下面的命令能够对源文档 hello.c 进行预处理和编译,生成 hello.s 文档:
$ gcc -S hello.c
所生成的文档 hello.s 就已能够进行汇编生成对象文档了。
另外一组输出现象影响的是编译器的输出信息,而不是输出文档的格式。-v 选项会显示编译过程的周详信息,-### 选项的作用是显示这些周详信息,但是并不执行这些命令。后面这个选项在创建编译脚本时尤其有用。
Dialect 和标准选项 正如前面介绍的相同,GCC 能够为很多种语言编译二进制文档。这些语言都有一些特定变体,即方言,这些变体被概括为特别的语言约定。GCC 能够接受一些选项来指定编译器所使用的方言。这包括标准选项,连同对标准限定进行修改的更具体的标记。GCC 支持 1990 年发布的 ANSI C Standard,并支持 1995 年所添加的扩展,连同部分支持 C99 修正标准。
-ansi 和 -std 标记能够用来对编译器强制施加一些限制。-ansi 标记会禁用 GNU 的扩展(请参看下面的 表 2),例如 inline 和 asm。注意其他的 GNU 关键字,例如 __inline__ 和 __asm__ 依然能够继续使用,但是这会破坏对语言标准规范的遵守。-ansi 选项还会禁止在 C 语言中使用 C++ 风格的注释。
-std= 选项能够指定标准等级或 GNU 对该标准的扩展等级。这些选项在下表中进行了总结。能够在 GCC 手册的 3.4 节中看到任何可用规范的完整列表。(请参阅 参考资料。)
表 2. 标准和标准扩展选项 -std= 标记 标准或标准扩展c89 或 iso9899:1990 ISO C90(和 -ansi 相同) iso9899:199409 ISO C90,1994 amendment c99, c9x, iso9899:1999, or iso9899:199x ISO C99 (并不完全支持) gnu89 ISO C90 with GNU extensions(默认) gnu99 或 gnu9x ISO C99 with GNU extensions c++98 Amended 1998 ISO C++ standard gnu++98 Amended 1998 ISO C++ standard, with GNU extensions (默认)
更新标准的特性假如和以前的 C 标准并不冲突,那就能够使用。即使不指定 -std= 标记,情况也是如此。例如,即使不指定 -std=c99,也能够使用 __restrict__ ,条件是他不会和所指定的旧标准冲突。
有一些更加具体的标记,例如 -fno-asm,能够用来强制是否遵从某种标准。这种类型的标记提供了一些强制关键字、通用有符号数据类型,并能够显示数据元素是保存在二进制文档的什么地方,连同其他一些信息。关于 C 和 C++ 的 dialect 选项的完整信息,请分别参阅 GCC 手册的 3.4 节和 3.5 节(请参阅 参考资料)。
除了 ANSI 标准之外,GCC 还支持 GNU 对 C 和 C++ 语言的一些扩展,这些扩展在 ANSI 标准中并不支持。在本文中并没有对这些扩展过多进行讨论,但是假如研发人员希望自己的代码能够在其他 GNU 平台之间能够很好地移植,我们强烈建议研发人员要了解这些区分。在下面的 清单 1 中给出了一个主要的扩展,这是因为这种用法很常见,而且这是个很好的可移植扩展的例子。
GNU 扩展能够使用前后的双下划线进行标记。例如,__asm__ 就是 GNU 对 asm 的扩展,他能够在规范中允许指令的操作数使用 C 表达式。
现在请考虑下面这个例子,他来自于 Linux 内核的源代码,使用了 GNU 的扩展: 清单 1. GNU 扩展
static __inline__ void atomic_sub(int a, atomic_t *v) { int t; __asm__ __volatile__( "1: lwarx %0,0,%3 # atomic_sub\n\ subf %0,%2,%0\n" PPC405_ERR77(0,%3) " stwcx. %0,0,%3 \n\ bne- 1b" : "=&r" (t), "=m" (v->counter) : "r" (a), "r" (&v->counter), "m" (v->counter) : "cc"); }
注意,在这个例子中使用了 inline、asm 和 volatile 关键字的另外一种可替代形式:前后都使用了双下划线。
__GNUC__ 宏定义通常是为 GCC 进行预定义的,能够用来检查在编译代码时是否使用了对 GNU 扩展的支持。您能够认为这就像是在询问某一个编译器是否是 GCC 相同。我们建议您使用 GNU 的扩展关键字来检查是否存在 GNU 扩展,从而确保和其他编译器的可移植能力。例如:
#ifndef __GNUC__ #define __asm__ asm #endif
这会检查是否存在 GNU 的扩展,假如不存在,就将后面的代码中会出现的一些关键字定义为其他编译器能够识别的关键字。
当使用 pedantic 警告信息调用 GNU 编译器时,任何的 GNU 扩展都会被报告为警告信息。通过在任何使用 GNU 扩展关键字的表达式之前都加上 __extension__ 关键字,能够解决这个问题。
记住,这是 GNU 扩展及其用法的一个例子。其他编译器也有很多特性,其中有一些都是类似的非标准特性。严谨的研发计划包括对这些扩展在可移植性和性能影响方面的理解。在 GCC 手册中完整介绍了任何的可用扩展。(请参阅 参考资料。)
警告选项 GCC 在报告错误消息时能够使用不同的粒度。在 GCC 手册中的 3.8 节列出了警告和错误选项。请参阅 参考资料。)除了能够打开或关闭每种类型的警告信息之外, 更有几个元选项来控制警告和错误消息的类型。这样能够让编译器进行一些严格的检查。
-fsyntax-only 选项会通知编译器只关心那些真正不符合语法规发的地方。这是编译器最松散的一种状态,和 -pedantic 选项正好相反。
-pedantic 选项会产生选定的 ISO 标准所希望的任何警告信息。这个标记还会禁用扩展,__keywords__ 和以 __extension__ 标识符开始的表达式除外。-pedantic-errors 选项会导致编译器编译器在碰到每个警告信息时就停止,仿佛碰到的是个错误。
-Wall 现象会使用很多警告信息,但是只有警告信息,没有其他信息。-Wall 究竟会产生哪些消息是个经验问题。正如在 GCC 手册中介绍的相同,这个选项 " 会启用任何有关那些某些用户认为存在问题而且很容易避免的结构的警告信息 " 请参阅 参考资料)。-Wextra 选项则会通知编译器要报告任何其他认为可能较少出现问题的警告消息。在 GCC 手册的 3.8 节中,能够看到这些标记能够捕获到的任何警告信息的完整清单。(请参阅 参考资料。)
假如一个标准能够同时使用 -std= 选项和 -pedantic 选项来测试代码是否遵守某个标准,那会很有吸引力。然而,这在实践中却并不怎样。虽然 -pedantic 选项实际上会对标准所希望产生警告的每个结构都产生警告信息,但是他并没有解决标准中的任何情况,因此他会替换成实际的标准遵守测试。
调试选项 交互式调试程式能够极大地加速代码中问题的分析。GNU Project Debugger,或称为 gdb,能够在程式中任何给定的地点停止,跟踪程式的执行过程,检查程式执行过程中到底发生了什么;假如程式没有正常结束,还能够修改程式的执行过程。
为了在 Linux on POWER 上使用 gdb,必须使用调试选项来编译代码。在使用 -g 或 -ggdb 选项编译代码时,会产生 DWARF 和 DWARF2 格式的信息。-pedantic 标记会产生 DWARF 信息供 gdb 使用。但是和大部分调试器不同,gdb 能够同时使用 -g 和 -O 标记(基本优化标记)。这是 gdb 特有的一个特性,在其他很多调试器中都没有这个特性,因为经过优化之后,代码的组织和执行都会发生变化。然而您也要有所准备,因为同时使用 -O 和 -g 会可能对代码重新进行调整,这在使用调试器时会受到影响。假如给 GCC 传递 -ggdb 选项,GDB 就能够使用意义更加丰富的 DWARF 2 格式。这同时会启用 gdb 的扩展。
-g 和 -ggbd 选项都能够接受一个级别参数(例如,-ggdb1 标识级别 1)。从直观上来看,这个级别越高(一共有 3 个),调试器中能够使用的调试信息就越多。这是个折中:调试信息越多,程式就越大,执行速度也就越慢。gcc 更有很多标记能够用来进行配置。这包括 gprof 和 gcov 用来搜集数据的一些选项,连同用来报告内存使用情况、优化信息的标记,等等。很多这种选项通常都是用来调试程式的,而不是用来编译代码的。最后一组调试标记是有关编译环境的。诸如 -print-file-name= 之类的选项用来验证所提供的编译环境是不是我们所希望的环境。
完整的调试标记,请参阅 GCC 手册的 3.9 节。(请参阅 参考资料。)
优化选项 GCC 为 C/C++ 代码和所支持的其他语言提供了一个后台优化器。由于 Linux on POWER 是个性能很高的平台,因此对于研发人员来说,理解这个优化器的操作很重要。注意,并不是任何的优化程式都被周详记录,而是只有那些能够使用命令行选项调用的程式会有被记录。此处,介绍的这些选现并不是从他们性能的角度进行讨论的,而是从他们的移植和调试角度进行讨论的。完整的优化选项,请参考 GCC 手册的 3.10 节。(请参阅 参考资料。)
分组优化标记能够让编译器使用一个标记来选择一组优化选项。他们用来对代码的性能或大小进行不同级别的优化。-O 或 -O1 标记是第一个组优化标记,他包括包括了一些不用多少编译时间就能够实现的优化步骤。-O2 实现的是那些能够用来提高性能但却不会增加程式大小的优化;比较 -O3 和 -O2,针对性能进一步进行优化,而不管是否会影响程式的大小。-Os 是对程式的大小进行优化,即使会影响程式的性能也在所不惜。注意,并没有一个分组选项能够选择使用编译器中任何可用的性能优化选项。更有一个值得注意的事项是,对于 GCC 来说,-O 和 -O1 是完全相同的。但对于其他一些编译器来说,情况并非如此,这些编辑器包括 IBM XL C/C++ 编译器,该编辑器也能够在 Linux on POWER 上使用。
注意,有些选项对于编译器来说是明智的,因为他们会影响到研发的各个方面,例如调试。-fomit-frame-pointer 就是个这种选项,在任何的 -O 级别中都会启用这个选项,他会在无需的地方删除 frame 指针。这个选项能够提高性能,但是这个标记的规范却并不担保实现了优化。在 -fomit-frame-pointer 的例子中,这是因为 POWER 平台上的堆栈指针就是个 frame 指针,在使用 alloca() 机制在堆栈中动态分配内存时更是如此。在这些情况中,frame 指针必须保留,以便提供反向的跟踪。这是个启发式优化有害的例子,因此编译器会将其忽略。
POWER 架构中有一个分支计数寄存器,GCC 有一个利用这种特性的选项:-fbranch-count-reg。该特性利用了 POWER 架构能够对计数寄存器进行消耗和分支的能力,而不是采用消耗(和 0 作比较)和有条件的分支。在从 SPARC 往 Linux on POWER 上移植程式时,要确认启用了这个选项,因为 SPARC 并不支持这种特性。假如启用了 -O2,那么 -fbranch-count-reg 标记默认情况下是启用的。再次重申,在某些上下文中,编译器是不会应用这种优化的。在这种情况中,不包含函数调用的循环能够从这个选项中获益良多,但是在函数调用之前复制计数寄存器的内容所带来的负载可能会超过使用计数寄存器操作所带来的长处。
预处理选项 GNU 的预处理器是和编译器一起对源文档进行处理,他们将源程式中的文本转换成一些指令用来进行编译。GNU 预处理器是在 GCC 驱动器的控制之下自动进行操作的,GCC 驱动器中有几个选项能够用来引导预处理器的行为。
和 GCC 所驱动的其他工具链工具相同,预处理器能够使用 -Wp 选项向预处理器传递预定义的选项,或使用 -Xpreprocessor 选项向预处理器显式地传递外部指定的选项。Linux on POWER 没有 GCC 驱动器标记不能使用的预处理器选项。也就是说,我们没有什么理由去使用 -Xpreprocessor 标记,而不使用 -Wp 标记。
-undef 选项能够取消 GCC 或目标平台所特有的宏定义。在计划进行移植时,这很有用,因为他能够帮助确定移植的问题。
-M 选项通常也很有用,因为他能够用来显示源文档所需要的任何依赖关系。这种依赖关系乐意用来检查源代码和目标平台的可用性。
-I 选项用来包含要搜索的头文档所在的目录。假如一个依赖关系是使用 -I 选项直接指定的,并且这个包含的路径没有成功,那么需要确保系统中已安装了任何的更新包,并应用了任何的服务包。在较低版本的 Linux on POWER 上,安装程式有些问题,没有完全安装部分系统库和头文档。具体地说,Red Hat Enterprise Linux Version 3 上 64 位的标准和兼容库就存在这个问题。RHEL3 的任何问题在 RHEL3 Update 3 中已得以修正。
汇编程式选项 和预处理器类似,GNU 汇编程式也能够接受 GCC 能够识别和不能识别的一些选项:他们分别是 -Wa 和 -Xassembler 标记。
这两个选项在后文的“GNU 汇编程式”一节中会深入进行介绍。
链接器选项 链接器也能够使用类似于 -Wa 和 -Xassembler 的选项:-Wl 和 -Xlinker。然而,和汇编程式选项不同,GCC 驱动器能够接受一些直接对链接器行为产生影响的选项。
Linux on POWER 上有两种编译器:GCC 和 XL C/C++ for Linux on POWER。由于这两种编译器都使用了 GNU 工具链,因此他们生成的对象是完全兼容的。对象名的清单能够作为参数传递给 GCC 驱动器,这会告诉链接器在链接成可执行文档时包含这个对象文档。在将多个编译器产生的对象链接为可执行程式时,这个选项很有用。例如,能够考虑这样一个应用程式:他有很多进程组件,其中一些进程需要考虑可移植性的问题。那些对性能至关重要的代码能够使用 XL C/C++ 进行编译,以便获得更好的性能;其余的代码能够使用 GCC 进行编译。这种方法最大限度地利用了编译器的性能、GNU 工具链的灵活性连同能够将这两个编译器编译出来的对象链接成可执行程式的长处。
GCC 驱动器也能够接受一些参数,指定在链接时包含哪些库。-l 选项能够接受库的名称,能够直接使用,也能够作为遵守 POSIX 规范的一个单独参数。这个参数的排序规则很重要。库是按照指定的顺序进行搜索的。因此要确保其他库或对象所依赖的库要首先列出。
虽然从理论上来说,将 32 位对象和 64 位对象链接在一起是可行的,但是现在 GNU 工具链还没有实现这种功能。从架构上来说,存在一些原因能够解释为什么没有实现这种功能。在 POWER 平台上,将整个应用程式都编译成 64 位的并不会降低程式的性能。
代码生成选项 有些和机器平台无关的选项能够影响生成代码的方式,在 Linux on POWER 可能会有一些特别的影响。在将程式从 Linux on Intel 移植到 Linux on POWER 时,一个常见的变化就是对位置无关的代码的考虑。-fpic 选项用来生成和位置无关的代码,用来在 GCC 中使用共享库使用。在很多平台上,这个选项是必须指定的,但是在 POWER 架构上,这要取决于到底是使用 32 位模式还是采用 64 位模式来编译应用程式。在 64 位模式中,任何的对象都是和位置无关的;而在 32 位模式中,您必须指定 -fpic 选项。Makefile 和编译脚本在为 Linux on POWER 配置编译标记时,就应该考虑这个问题。
堆栈的反向跟踪(stack backtrace)能够用来研究程式究竟是怎样执行的。假如程式出错,那么反向跟踪能够提供一些内幕信息,说明究竟是哪个函数调用应该对这个问题负责。即使程式执行成功了,反向调用也能够用来研究程式是否是按照程式员所想象得那样执行。反向跟踪依赖于构成程式的对象中的 frame 指针。
为对象生成 frame 指针,既无需 32 位的 ABI for Linux on POWER,也无需 64 位的 ABI for Linux on POWER。这会使得调试变得更为复杂,尤其是建立反向跟踪之后。frame 的信息在 GCC 中能够使用 -fexceptions 标记来产生。这个选项会告诉 GCC 来实现在使用调试程式展开堆栈时所需要的反向跟踪信息。虽然这会增加数据的大小,但是应该不会影响程式的执行。-fexceptions 标记对于 C++ 程式默认是启用的,而对于 C 程式默认是禁用的(这可能会需要和 C++ 对象中的异常处理程式进行交互)。因此,假如假如 C 代码希望处理 C++ 程式所触发的异常,那么就必须在 C 程式的编译标记中加上 -fexceptions 标记。
在某些架构上,异常处理必须使用 Call Frame Information 指令进行管理。这些指令用来在汇编中引导异常的处理。在 Linux on POWER 上也能够使用这些指令,由于某些原因(例如,代码的可移植性),GCC 所成城的异常处理信息还是不够的。
特定于 POWER 架构的选项 尽管 GNU 工具链的重点是可移植性,但是每种架构都有自己特别的长处,研发人员必须理解怎样充分地利用这些长处来实现最好的性能。这一节将介绍 GCC Linux on POWER 能够接受的特定于架构的一些选项。
Power 架构家族和 GCC POWER 和 PowerPC 处理器从 IBM 801 到 POWER5 已经历了一段漫长的路程,并广泛应用于各个领域:从手表到企业级的服务器无所不在。POWER 是最初的架构的名字,PowerPC 则起源于 Apple、IBM、Motorola 在 1990 年的合作项目。这两个系列是单独进行研发的,但是从发布以来,POWER5 和 PowerPC970FX 的结果就领先了业界 10 年之上。不管在多么广泛的产品中采用,这两个架构系列所具备的通用指令都能够为代码提供很好的可移植性。虽然 IBM 还会不断推出更新的和 POWER 架构类似的芯片,但是对于 GCC 来说,PowerPC 也包含了任何最新的处理器。
架构标记 GCC 对于 POWER 架构能够使用两类扩展指令。第一个集合是为早期的 RS/6000 架构设计的,能够使用 -mpower 标记启用。最近的 POWER 或 PowerPC 硬件不能使用这个标记。相反,他们使用 -mpowerpc 选项,或他们的 64 位对应的选项 -mpowerpc64,来使用现代 POWER 和 PowerPC 硬件所通用的指令。对于那些希望仍然对传统的 POWER 硬件提供支持的研发人员来说,要么同时使用 -mpower 和 -mpowerpc 标记,要么一个标记也不使用,这是因为这两个标记每个都只启用了针对每一个处理器系列的扩展。假如这两个标记一个都不使用,那么就只会使用那些这两种架构所通用的那些指令。然而,要想对性能进行优化,我们建议您使用 CPU 特有的标记。
CPU 特有的架构标记 CPU 特有的优化标记比处理器系列标记更能提高程式的性能。这些标记会通知编译器为某个特定的 CPU 生成最优化的代码,但是这些代码可能并不能在其他平台上运行。-mtune= 标记用来为一种给定的 CPU 指定调度参数,但是他并不会配置架构的类型、寄存器的用法连同记忆变量。这些是通过 -mcpu= 标记进行控制的。
-mtune= 标记的用法如下:
$ gcc -O3 -mtune=power5 -o foo foo.c foo2.c
这个例子引导编译器将源文档 foo.c 和 foo2.c 编译成一个可执行文档,使用优化级别 3,并使用为 POWER5 CPU 定制的调度参数。
-mcpu= 标记的用法如下:
$ gcc -O3 -mcpu=power5 -o foo foo.c foo2.c
这个例子引导编译器将源文档 foo.c 和 foo2.c 编译成一个可执行文档,使用优化级别 3,并使用为 POWER5 CPU 定制的调度参数、记忆变量、架构类型连同寄存器的用法。
熟悉 IBM XL C/C++ 的研发人员可能会注意到此处的一个差别。使用 XL C/C++ 时,对应的标记是 -qtune= 和 -qarch=,分别对应 -mtune= 和 -mcpu=。
-mtune= 和 -mcpu= 都能够使用 common 选项,这样会选择 Power 和 PowerPC 架构处理器通用的一个指令集。例如:
$ gcc -O3 -mtune=common -mcpu=common -o foo foo.c foo2.c
这个例子会从源文档 foo.c 和 foo2.c 生成一个使用级别 3 进行优化的二进制文档,他能够在任何 POWER 和 PowerPC 处理器上运行,只使用了这两个架构所通用的一些指令。然而,这个二进制文档的性能可能不如使用 CPU 特有的标记进行优化所得到的二进制文档的性能好。另外,powerpc、powerpc64 和 power 选项都能够用来指定自己的 CPU 属性。
这两个选项的全部合法范围包括对旧集合和新机器的支持。然而,POWER 架构上的企业级 Linux 发行版只支持常见的 power3、power4、power5、970、powerpc 和 powerpc64 选项,因为他们都有为企业级硬件所设计的相关指令集。请参阅 GCC 手册中对所支持的指令集和处理器的周详清单。(请参阅 参考资料。)
Vector Multimedia eXtension Vector Multimedia eXtension,简称 VMX,是 POWER 处理器的单指令多数据(SIMD)架构的扩展。VMX 是由 Apple、IBM、Motorola PowerPC 联合研发的,每个成员都使用一个不同的名字在市场上进行销售。IBM 称之为 VMX,这是这种技术最原始的代号;而 Apple 称之为 Velocity Engine®;Motorola 使用的名字是 AltiVec®。GCC 并不能识别这种技术的商标名,但是他实现了对这些处理器指令的支持,他使用 AltiVec 作为相关标记的名字。
VMX 提供了 32 个附加 128 位的寄存器来保存向量数据,这能够提供 16 个 8 位的值、8 个 16 位的值或 4 个 32 位的值。(注意 VMX 寄存器不能操作 64 位的值。)这些寄存器和对其进行操作的 162 条处理器指令就是为什么在处理特别类型数据时 VMX 能够极大提高性能的原因。例如,我们能够使用向量来并行处理 4 个整数,这需要为此目的而特别编写代码。
虽然 VMX 为某些算法提供了巨大的长处,但是代码的编写必须实现向量化。不幸的是,对于编译器来说,没有什么魔力能够在后台实现 "自动向量化(autovectorization)"。C 语言中有一些指令能够对底层寄存器的选择提供控制。这些指令、原型连同宏都是在 GNU 的 altivec.h 文档中定义的,能够用来帮助实现这个任务。注意在使用 IBM XL C/C++ 时,并不用显式地在源程式中包含 altivec.h,而在使用 GCC 时,则必须显式地包含这个文档。GCC 为 AltiVec 的内置函数在 GCC 手册的 5.4 节中进行了介绍。(请参阅 参考资料。)
向量通常需要采用新颖的方法才能形成一个好的算法,只有程式员仔细规划代码才能够实现这种功能。实际上,编写得不好的向量代码的运行速度通常比普通的代码都慢,因为他进行对齐的效率很低。VMX 寄存器中处理的数据必须按照 4 个字进行对齐(128 位),程式员要负责确保这些指令所处理的数据缓冲区都是对适当的地址进行对齐的。这需要特别注意通过在 32 位模式中动态内存分配所创建的缓冲区,因为他们只能确保是按照双字进行对齐的。然而,在 64 位模式中,malloc 子系统返回的是按照 4 个字进行对齐的地址。
在线教程“Introduction to Altivec - Ten Easy Ways to Vectorize Your Code”(PDF)对代码的向量化详尽地进行了介绍。 (请参阅 参考资料。)这篇文章讨论了向量化编码对循环展开和像素操作等所带来的巨大好处。
在 GCC 中,VMX 的功能是使用 -maltivec 和 -mabi=altivec 标记来启用的。
值得注意的是,Apple 的 Mac OS X 操作系统上的 GCC 也支持了他们自己的 AltiVec 扩展的实现,但是稍有区分。区分之一是向量声明的语法,这会影响代码在这些平台之间的迁移。在 GCC for Linux on POWER 中,向量位于花括号 { } 中,而在 GCC for OS X 中,向量则是位于圆括号 ( ) 中。IBM XL C/C++ 能够同时支持这两种方法。有关这个问题连同 VMX 实现之间的差异的更多信息,请参阅 “About Compilers with VMX Support” 的 Web 主页。(请参阅 参考资料。)
表 3. 向量的语法 VMX 向量语法 GCC for Linux IBM XLC C/C++ GCC for Mac OS X花括号 {...} 支持 支持 不支持 圆括号 (...) 不支持 支持 支持
对齐选项 GNU toolchain for Linux on POWER 能够使用数据对齐选项来调整基于 POWER 和 PowerPC 实现(例如,嵌入式处理器)的数据宽度范围,连同和其他架构之间的互操作能力。尽管默认的对齐模式是针对 64 位的 POWER 架构进行优化的,但是实际上还能够使用一种自然的对齐选项。自然对齐方式放弃了平台上根据 long double 类型进行对齐的长处,因此是最有效的一种内存对齐方式。但是,为了确保和其他平台上编译的对象之间的互操作能力,可能会需要使用自然对齐方式。-malign-power 标记为 POWER 架构指定优化的对齐方式,这对于 GCC 来说是默认的。?malign-natural 标记用来指定自然对齐方式。
TOC 选项 POWER 处理器在执行模式中利用了一个称为 Table of Contents(简称 TOC)的概念。TOC 是每个模块的一个定位点,用作查找该模块中全局数据、静态数据连同和位置无关的代码的一个引用点。每个模块(例如,主程式或共享库)都包含有自己的 TOC。这个系统特有的属性是由 64 位的 PowerPC ELF ABI 定义的,能够使用 r2 寄存器进行访问。在构造 TOC 项时,GCC 驱动器能够接受一些选项来控制链接器的行为。这些选项会在后文中的 “GNU 链接器” 一节中进行介绍。
GNU Binutils GNU binutils 包括一套用来构造和使用二进制文档所需要的工具。其中两个最为关键的 binutils 是 GNU 链接器 ld 和 GNU 汇编程式 as。这两个工具是 GNU 工具链中的两个完整部分,通常是由 GCC 前端进行驱动的。然而,了解怎样直接引导 GNU 工具链的这些组件将很有用。本节将介绍怎样控制这两个工具并选择其他 binutils 以便易于移植,并介绍 POWER 架构特有的一些功能,连同常见的一些问题和误解。
GNU 链接器 链接是创建一个可执行程式的最后一个步骤。GNU 链接器可执行程式,或称为链接编辑器,是 ld,他的角色是将对象文档合并成可执行程式,同时指定程式在运行时是怎样执行的。GNU 链接器使用一个命令语言脚本来控制链接过程;默认情况下,ld 是由一组内部命令进行控制的,这些命令能够进行扩展或覆盖。强调可移植性和灵活性在 GCC 的功能中是很明显的一条,他能够为很多不同的编译环境生成链接脚本,并向 ld 传递定制过的链接脚本,而不用手工进行干预。相反,在其他某些系统中,链接编辑器则很难实现这一点。例如,AIX 的链接器就需要自己处理定制过程,而不是在一个预链接的编译步骤中来完成。
本节将介绍 GCC 驱动器和链接器之间的交互,提供 64 位 PowerPC ELF ABI 的一些周详信息,并探索 Linux on POWER 上链接器实现的一些异常之处。
由于链接器的角色是将对象代码集合在一起,并产生一个可执行模块,因此编译器驱动器会将所期望的参数传递给 ld。这包括:应用程式对象文档,启动代码(在进程启动且调用 main() 之前所运行的代码),可能有用的包和所依赖的共享模块清单,库搜索路径(在链接时和调用时使用),已平台所需要的实现特有的选项。ld 会从左到右遍历命令行,并使用这个顺序来确定怎样查找/引用哪些符号。
Linux on POWER 对象文档 Linux on POWER 使用 ELF 对象文档格式。这种灵活且可扩展的格式能够很好地满足平台的需要,另外更有几个特别的段。表 4 列出了这些特别的段,并对其进行了介绍:
表 4. 特别的段 段 描述.glink 包含对全局链接代码的支持。模块间的函数调用(例如主程式和 libc.so)需要加载一个函数描述符,其中这个描述符波包含了目标程式的地址和目标模块的 TOC 值。(周详信息请参看下面的 TOC。)这种机制是由过程链接表(.plt)段和 .glink 段实现的。 .toc 这是 TOC 的一部分,每个模块都有这样一个段,他是加载全局信息使用的一个字典。作为 TOC 的一部分,.toc 段包含了初始化信息。 .tocbss 其作用类似于一个 .bss 段,但是为 TOC 保存了一个尚未初始化的数据区。 .got 全局偏移量表(Global Offset Table)保存在 .got 段中,由此能够访问全局数据项(在本模块之外也是可见的)。注意,尽管数据能够加载到 .toc 或 .tocbss 段中,但是全局数据通常都是通过 GOT 进行寻址的。 .plt 包含了对模块间调用的支持。每个模块(主程式,共享对象)都包含了自己的 TOC,因此也都有自己的 TOC 定位点。这个段的内容是由动态链接器进行填充的,他支持一个用来支持惰性符号解析的简单函数描述符集合(特例)。
以下节选自“64-bit PowerPC ELF ABI Supplement”(请参阅 参考资料):
ELF 处理器特有的实现通常会定义一个 GOT (‘Global Offset Table’)段用来保存位置无关的代码。有些 ELF 处理器特有的实现,包括 32 位的 PowerPC Processor Supplement,定义了一个小的数据段。有时会使用相同的寄存器来对 GOT 和这个小的数据段进行寻址。
64 位的 PowerOpen ABI 定义了一个 TOC (‘Table of Contents’)段。TOC 结合了 GOT 和这个小数据段的功能。
您能够看到虽然 32 位和 64 位的模块提供了相同的功能,但是他们的组织方式却是不同的。由于 64 位模块在 ELF 中引入了这个 TOC 的概念(这来自于 AIX),请考虑以下细节。
内容列表 正如前文中介绍的相同,每个 64 位模块都包含一个 TOC。这就意味着您的 "hello, world!" 至少包含两个模块:main 程式和 libc,因此包含两个 TOC。每个 TOC 都有一个 "知名的" TOC 定位点,这通常能够在进程运行时从寄存器 2 中找到;TOC 寄存器的值会随着执行过程从一个模块跳到另外一个模块而发生变化。这个定位点支持访问一个模块的各种全局数据的机制(例如全局的外部变量、全局静态变量连同函数描述符)。
使用 TOC 意味着要使用两级间接寻址。例如,要访问一个全局变量,程式要使用 TOC 定位点(rc2)来查找指向这个变量的指针的位置。在为一个模块外部的调用查找函数描述符时,又会发生同样的操作。但是能够将数据保存在 TOC(而不是使用一个指向数据的指针),这样就避免了另外一级的间接寻找。记住,(在 64 位模式中)TOC 中保存的每个数据不管是指针还是实际数据,都必须是 8 个字节或少于 8 个字节。现在请考虑一下这对 TOC 大小的影响。
TOC 相对寻址使用了一条指令,他限定于只能使用 16 位的偏移量。TOC 能够保存 65,536 个字节,在 64 位模式中,这能够用来存放 8,192 个 GOT 项。对于大型应用程式来说,您可能会看到这些空间还不够用。GNU 链接器有几个选项来处理超过 TOC 最大值的应用程式。从 2.15.90 版本的 GNU Binutils 开始,TOC 假如溢出,在链接时会自动被分隔为多个 TOC,但是对于一个特别的结果,我们也能够使用在 表 5 中列出的选项。此处,这些选项通常都是作为 GCC 驱动程式的参数指定的。
表 5. TOC 选项 TOC 选项 描述-mfull-toc 这个选项是处理 TOC 使用的默认值。他会让链接器为可执行程式或共享对象(换而言之,就是个模块)分配一个 TOC。假如现在的 64K 空间不足以实现链接编辑的功能,那么链接器就会报告一个错误消息说 TOC 空间已溢出了。 -mno-fp-in-toc 这个标记能够减少 TOC 中使用的空间。通常,编译器会将浮点值直接放到 TOC 中。(AIX/GCC 研发人员可能会在自己的 32 位研发经验中见过这个选项。)这个标记能够防止将这些值保存到小数据段中,从而空出一些 TOC 空间给其他项使用。和此有关的一个标记是 no-sum-in-toc。 -mno-sum-in-toc 这个标记通知 GCC 在运行时生成代码来计算一个地址和一个常量之和,而不是将他们的和放到 TOC 中。这个选项(更有 no-fp-in-toc 选项)都能够用来保留一部分 TOC 空间,但是所生成的代码更大、速度更慢。 -mminimal-toc 假如使用 no-fp-in-toc 和 no-sum-in-toc 标记还不能释放足够的 TOC 空间,那么就能够使用 minimal-toc 标记为每个(对象)文档生成一个单独的 TOC。这会生成很多很小的 TOC 项。虽然这解决了 TOC 溢出的问题(因为现在有无穷个 TOC 了,每个都是 64K),但是这样所生成的代码会更大、速度也更慢。使用 TOC 来指定函数地址的长处从根本上被遗弃了。
longcall POWER 指令集提供了 bl(或称为分支链接)指令给模块内部的子程式调用使用。这种格式的指令能够使相对寻址范围达到 64MB,或 2^26(从调用位置开始计算)。然而,有时可能会达到这个限制,因此必须使用另外一种机制实现从 A 处到 B 处的寻址。?mlongcall 选项就能够使用函数指针机制(用于模块间的调用)。换言之,每个函数调用都类似于一个模块外调用。这突破了 64M 相对地址的限制,代价是可能会稍微增加函数调用的负载。他还支持 longcall 的用法,其优先级比 -mlongcall 选项更高。longcall (1) 会对任何后续函数声明都应用这个属性;longcall (0) 能够停止对后续函数应用这个属性。
对于研发人员来说,幸运的是 GNU for Linux on POWER 上的链接器能够快速生成需要实现这个解决方案所需要的代码。正如 AIX 链接器相同,在 64 位模式中,并无需担心 ?mlongcall。这个特性对于 32 位的 GNU 链接器来说是不可用的,包括 SLES9 和 RHEL4。然而,在能够自由下载的 GCC 源代码中包括了这个特性。假如您有一个 32 位的应用程式在 SLES9 或 RHEL4 上运行,他调用的位置超过了 64MB 的限制,那么您就只能重新编写代码,或将其编译为 64 位模式。由于 32 位和 64 位应用程式在 Linux on POWER 运行时是能够并存的,因此在这种情况中,我们建议您将其编译为 64 位模式。
链接器脚本 GNU 链接器提供了一种命令语言,能够用来控制链接编辑的操作。虽然对于那些早已熟悉 GCC 研发工具的人来说,这并没有什么奇怪;但是对于 AIX 研发人员来说,他们需要理解 AIX 和 Linux on POWER 上所存在的区分。尽管(XCOFF)对象文档的定义和 AIX 链接器的行为都是自动的,但是 GNU ld 能够对怎样连同在何处合并对象文档的各个段进行更加灵活的控制。让我们来考虑一下这种脚本的基本属性。
GNU ld 会自动对一个内部脚本进行操作。您能够添加或替换这些内部命令。在特定的条件下(例如,使用只出现一次的命令),必须使用定制的链接器脚本提供完整的命令集;否则,能够使用脚本为链接器添加常用的操作。在添加定制操作时,您能够在命令行中对链接器脚本简单地进行命令(链接器假设任何非对象文档都是链接器脚本)。在替换链接器默认的(内部)命令时,您能够使用 --T 或 --script= 选项。
检查默认的链接器脚本的工作能够通过一个链接器选项发送到标准错误设备上。使用一个简单的 "hello, world" 程式,加上 verbose 选项,您就能够捕获这些内部的命令:
$ cc -o hello hello.c -Wl,--verbose 2> hello.ldscript
这会创建一个 256 行的脚本,他能够处理最常见的链接情况(我们期望如此)。AIX 研发人员应该注意这是 ?bbindcmds: 选项的结果,但是我们早已说过,AIX 链接器要更加自动化。
因此您可能会问:为什么会有人对这种功能感兴趣呢?有些研发工作需要使用一些 "链接器欺骗(linker trick)" 的技巧,这是另外一种声明项目对怎样构建应用程式和分布内存创建约束的方法。这种需要能够通过聪明地链接编辑操作实现,GNU 链接器的命令行选项就提供了这种机制。
GNU 汇编程式 汇编程式的任务是接受使用人们可读的格式编写的输入文档,并生成包含机器级指令的文档。然后将一个或多个文档中的对象代码传递给链接器,后者负责生成可执行模块。GNU 汇编程式能够支持很多平台;虽然每个平台都能够使用很多命令行选项,但是也有一些平台只能支持一些系统特有的特性。
和其他平台相同, GCC 驱动器能够接受使用高级语言编写的输入文档 -- 他能够产生汇编代码,然后再使用 GNU 汇编程式转换成目标代码。幸运的是,大部分研发人员都无需直接使用汇编语言,GCC 驱动器会为研发人员隔离这些具体的实现细节。然而,当需求提高时,理解怎样和汇编程式进行交互会很有用;GCC 能够让汇编程式接受一些使用逗号分隔的选项来支持这种功能,他使用的语法为 -Wa [选项]。例如:
$ gcc -O -Wa,-Z,-v foo.c -o foo
这条命令使用 GCC 驱动器将文档 foo.c 编译成二进制文档 foo,并打印汇编程式的版本,即便碰到错误也是如此。
虽然您可能会发现在 Linux on POWER 上,GCC、链接器、汇编程式等的用法都和其他平台很类似,但是汇编语言本身却有很大的区分。注意一些细节问题:RISC 架构和 CISC 架构相比,通常需要多条指令来完成一个特定的任务;POWER 也不例外。假如您查看一下 GCC 所生成的汇编代码,就会注意到其中会有很多 load 和 store 指令,同时更有其他类型的指令。熟悉汇编语言的一种有用的方法是使用高级语言(例如 C)来编写代码,并使用 GCC 的 -S 选项来保存所生成的汇编文档,或使用 objdump 对目标代码进行反汇编。
二进制文档的组织 在上文中有关 ld 的 “GNU 链接器” 一节中,我们曾介绍了 ELF 对象文档的一些段;这些段就是链接器最感兴趣的地方,他们就是用来生成可执行模块的。然而,几乎编译器所生成的每个对象文档中都有这些有趣的段,他们构成了您的程式的各个部分(使用对象代码格式),如 表 6 所示,因此链接器知道怎样对他们进行合并,从而创建一个程式或共享模块。
表 6. 二进制文档的组织 段 描述.text 可执行指令(汇编代码所转换成的指令)通常就保存在这个段中。只读常量(例如字符串常量)也能够在这个段中找到。这个平台上的文本段是只读的,这就意味着程式不能对自己进行修改;这个属性允许可执行文档的文本区能够在此程式的任何运行实例之间共享。例如,bash shell 的每个正在运行的拷贝都会共享 bash 文档的 .text 段的一份拷贝。 .data 这就是为可执行程式保存初始化过的数据的地方。从定义中我们能够看出,数据段是可读写的,进程能够对这个段中的内容(在内存中)怎样操作进行完全控制。举例来说,C 语言中那些使用编译时的值进行初始化的全局变量都能够在这个段中找到。 .bss bss 段中包含了尚未初始化的数据;这个段不在对象文档中占据任何空间,但是他定义了一段内存区域,他们在运行时被清零。C 语言中那些没有给定初始化值的全局变量在程式启动时都被定义为 0;这些变量都能够在 bss 段中找到。
链接编辑器会从指定的对象文档中搜集任何的 .text 段的内容,并合并他们来创建可执行程式的代码,任何的 .data 段都被搜集起来用来创建最终的数据段,任何的 .bss 段被合并起来创建最终的 bss 段。还要注意,每个可执行模块都包含自己的段;例如,共享模块就有自己的 .text 段和 .data 段。
汇编版本的 Hello, World! 考虑一下这个 32 位的非 PIC 汇编版本的 "Hello, World!" 他首次出现在 Hollis Blanchard 所编写的“PowerPC assembly: Introduction to assembly on the PowerPC” 一文中(developerWorks,2002 年 7 月): 清单 2. ia32 assembly
.data # section declaration - variables only .align 3 # make double-word aligned msg: .string "Hello, world!\n" len = . - msg # length of our dear string
.text # section declaration - begin code .align 3 # ensure alignment .global _start _start:
# write our string to stdout
li 0,4 # syscall number (sys_write) li 3,1 # first arg: file descriptor (stdout) # second arg: pointer to message to write lis 4,msg@ha # load top 16 bits of &msg addi 4,4,msg@l # load bottom 16 bits li 5,len # third argument: message length sc # call kernel
# and exit
li 0,1 # syscall number (sys_exit) li 3,1 # first argument: exit code sc # call kernel
注意数据段位于最上面,其中定义了 printf 要打印的字符串 -- 他能够于 .text 段中的内容相同简单,因为他是被当作一个常量来对待的。
文本段是有许多指令组成的,因为这个程式要使用几个系统调用(而不是使用 libc 中的程式,例如 printf)来完成工作。write 系统调用是在寄存器 0 中指定的,目标文档是在寄存器 3 中指定的,指向字符串的指针是在寄存器 4 中指定的,字符串的长度是在寄存器 5 中指定的。在输出字符串之后,又要将寄存器领配置为 exit 的系统调用,并传递值 "1" 作为退出代码。
这个例子会被传递给汇编程式,从而创建一个对象文档;所生成的对象文档又被传递给链接器,后者生成一个可执行文档的映像。这一系列操作对于具备 AIX 和 Linux 背景的程式员来说会感觉很熟悉。
汇编中的 CFI 指令 假如您发现自己要将 C++ 程式和汇编程式合并在一起,那么就能够使用 CFI(Call Frame Information)指令来使用异常处理的支持。这些指令会生成一些堆栈展开信息,这样使用汇编编写的程式就能够和使用 C++ 或其他语言编写的程式很好地结为一体了。
GNU 汇编程式中所支持的 CFI 指令在“CFI support for GNU assembler (GAS)” Web 页面上进行了周详的介绍(请参阅 参考资料)。对于 Linux on POWER 来说,CFI 指令所感兴趣的是 start_proc 和 end_proc。当把这些指令放到汇编代码中的每个函数两端时,就会生成 .eh_frame 信息。
更多的 Binutils 除了链接器和汇编程式之外,有些研发人员还会发现其他 binutils 也很有用。在本文中我们不会讨论每个工具的功能,这在 GNU Binary Utilities 的 Web 站点上(请参阅 参考资料)已周详介绍过了,下一节的讨论将局限于两个类似的工具。objdump 和 readelf 的出现或多或少都是很自然的,这取决于您所使用的平台。AIX 研发人员能够认识 objdump 的风格,ELF 的老用户也早就对 readelf 很熟悉了。这两个工具在 Linux on POWER 上都能够使用。
objdump 熟悉 dump 命令的 AIX 研发人员可能会希望了解有关 objdump 的知识。这个工具提供了类似的功能,而且功能比您所熟悉的功能更加丰富。objdump 的一个有用特性是能够对对象文档进行反汇编,并查看机器代码。但是,这并不适合对汇编程式进行反馈。 readelf 另外一个有用的工具是 readelf,他能够显示符号、段信息、二进制文档格式的信息等等。这在分析编译器怎样从源代码创建二进制文档时很有用,尤其是对于所链接的共享对象更是如此。AIX 研发人员会发现这和使用 dump 命令来查看所加载的段信息是一致的。
结束语 Linux on POWER 上的研发和 Linux on x86 架构连同在 POWER 架构上运行的 AIX 上的开放有所不同。通常来说,这些不同对于研发人员来说并不明显,因为 GNU 软件的目标就是实现可移植性。然而,在某些情况中,需要更多信息,我们试图提供这些信息,因为他们是属于 GNU 研发工具链的,包括 GCC 编译器、GNU 链接器和汇编程式,连同其他 GNU binutils。
我们已回顾了 GNU Compiler 的功能,连同他的能力:不但能够用来编译代码,而且能够用来控制其他 binutils。我们对链接器和汇编程式所采用的内部格式和操作进行了回顾,这重点是为了那些熟悉其他架构和操作系统的读者准备的。在本文整篇文章的内容中参考了很多其他有关该主题的资料,这些在 参考资料 中都列出了。我们在撰写本文时参考了当时最新的 GCC 手册。可能在您阅读本文时已有了更新的发布版。请查看参考资料,获取最新的 GCC 手册。
致谢 我们很感谢 Steven Munroe、Alan Modra、Hollis Blanchard 和 David Edelsohn,连同任何对我们理解本文主题有所贡献的研发人员。尤其要感谢 Hollis,本文中使用了他的 Hello, World 的例子。
|