张喜俊
(中国电子科技集团公司 第四十一研究所,青岛266555)
程序员都希望尽可能地重用自己的代码,即不需要任何修改,只是简单地重新编译就可以在其他系统上运行。但是,处理器架构、汇编器语法、C编译器实现、操作系统接口都会对代码的可移植性产生不同程度的影响。首先,汇编代码是不可移植的,例如ARM汇编语言编写的代码不可能直接运行在x86处理器上,这是因为ARM和x86的指令/机器码不同。其次,虽然MASM和NASM汇编器都可以生成x86机器码,但是由于它们的语法并不相同,因此也不能直接重用。最后,不同操作系统的系统调用/应用程序编程接口相差甚远,也严重地阻碍了代码重用。
C标准通过规定C编译器的行为为最大化代码重用提供了条件,但这并不等于说C代码就是可移植的,操作系统的差异、代码质量、以及编译器的实现和扩展都会对可移植性产生影响。本文主要讨论影响C代码可移植性的因素,以及如何编写可移植的C代码。
C语言标准可以看作是C语言使用者和C编译器实现者之间的协议。如果使用者遵守标准规定的语法,而且编译器实现了标准规定的行为,那么使用者可以得到期望的输出。这样,C程序员就能够在不了解底层硬件和操作系统等细节的情况下编写出具有指定行为的程序。
C语言标准定义了C语言的语法和语义、运行时环境、预处理器和标准库等。为了提高C代码的执行效率,C标准并没有试图定义C语言的每个实现细节,而是为编译器实现者提供了一定的自由,由此导致了可移植性问题。
C语言标准只是规定了每种内置数据类型的最小尺寸,而没有定义它的确切尺寸。例如,规定int类型至少16位,但是通常为32位;long类型至少32位,但在某些系统上却为64位。因此一定不要假设int或long具有一个特定的尺寸,否则它们会在某个时刻突然溢出。应该使用typedef类型而不是int或long类型,例如:
或者使用宏定义:
虽然这两种方法的效果相同,但是推荐使用 typedef类型,这样可以充分利用编译器的静态检查功能。
C99标准在头文件inttypes.h中提供了一系列固定尺寸类型,表1列出了其中的几个。只是到目前为止支持C99标准的编译器仍然很少,于是程序员不得不自己定义它们。
表1 C99固定尺寸类型
除了自定义的typedef类型以外,C标准也定义了一些特殊用途的typedef类型,如表2所列。在编码过程中应该尽可能使用这些typedef类型,而不是int等。
表2 特殊的typedef类型
1.2.1 编译器行为
为了给编译器实现者提供灵活性以生成效率更高的代码,C标准故意定义了3种与可移植性密切相关的行为,它们分别为未指定行为、实现定义行为和未定义行为。
①未指定行为。实现者需要从标准规定的几种选项中选择一种,但是不必在文档中说明所选择的行为,例如函数调用时实参的计算次序、宏替换时预处理器连接操作符#和##的计算次序等。
该语句依赖于n在调用power之前还是之后递增,不同的编译器可能产生不同的结果,而下面的代码则并不存在二义性:
②实现定义行为。实现者需要从几种选项中选择一种,而且必须在文档中说明所选择的行为。例如,char类型可能是signed char也可能是unsigned char,因此不能对char的符号性做任何假设。如果需要特定类型的char变量,则必须显式地使用signed char或者unsigned char,但是更好的方法是使用自定义的typedef类型。编译器实现者一般在编译器手册中详细说明它的行为。以ARM公司的RealView编译工具为例,在编译程序和库指南的附录B(标准C实现方法定义)中描述了其实现定义行为。
③未定义行为。标准不对其施加任何要求,程序既可以立即崩溃,也可以好像什么事情都没有发生过一样继续运行。例如,使用不可移植或错误的程序构造,或者使用错误的数据(如溢出或除数为零)。
在ISO/IEC 9899:1999文档的附录J(移植问题)中详细地描述了这3种行为。显然,任何依赖于这3种行为之一的程序本质上都是不可移植的。如果必须依赖它们,那么应该将这部分代码隔离起来,并且在项目文档中说明。
1.2.2 编译器扩展
C标准允许编译器实现者对C语言进行扩展,这在嵌入式系统编程中尤为明显。以 RealView为例,它使用__irq将一个C函数声明为中断处理器,使用__asm在C函数体中嵌入一段汇编代码,使用__packed声明压缩的结构体,以及支持“//”注释等。虽然这些扩展为程序员提供了一定的便利,但是与此同时也引入了可移植性问题,这是因为不同的编译器有不同的扩展,即使相同的扩展也极少是兼容的。
许多编译器都提供一些编译选项用来检查代码是否严格符合ISO标准,例如 RealView提供了-strict选项,GCC提供了-ansi选项。打开严格编译选项后,如果使用了任何特定于编译器的特性,那么编译器将会给出相应的警告/错误。
在字节寻址的存储器中,存在小端字节序和大端字节序两种方式存储多字节数据。大端字节序(big endian)也称为“网络字节序”,在最低地址处存储最高有效字节,而小端字节序(little endian)则在最低地址处存储最低有效字节。假设一个32位整型数据存储在自然对齐的地址A处,如图1所示。如果为小端格式,那么地址A处的字为0x78563412,地址A+2处的半字为0x7856;相反,如果为大端格式,那么地址A处的字为0x12345678,地址A+2处的半字为0x5678。
图1 字节序
不同的处理器的字节序可能并不相同,例如x86使用小端字节序,PowerPC使用大端字节序,而ARM同时支持大端格式和小端格式。如果不清楚处理器的字节序,那么可以使用下面的is_big_endian函数判断它是否为大端字节序。
一般情况下,程序员并不需要考虑处理器的字节序,但是当编写需要在计算机间交换数据时的应用程序(特别是网络应用程序),则需要特别关注字节序问题。例如,sockaddr_in结构体中的端口成员就要求使用网络字节序。为了增强网络应用程序的可移植性,定义了两类在本地字节序和网络字节序之间进行转换的函数:操作32位整数的 htonl和 ntohl,以及操作16位整数的 htons和ntohs。ntoh函数将网络字节序转换为本机字节序,而hton函数将主机字节序转换为网络字节序。
Linux支持数目众多的处理器,与处理器字节序相关的操作都定义在各自的byteorder.h中。以PowerPC处理器为例,在linux-2.6.24includeasm-powerpcyteorder.h中包含了位于linux-2.6.24includelinuxyteorder中的big_endian.h。它定义了__BIG_ENDIAN宏(表明PowerPC处理器为大端字节序),以及一些用来在本机字节序和大/小端字节序之间进行转换的宏。例如下面的宏专门用来操作32位数:
对齐的目的是为了提高处理器的执行效率。不同的处理器有不同的存储器访问特点,Intel x86处理器允许非对齐的存储器访问,但是这会造成一定的性能损失,而ARM处理器进行非对齐访问时竟然得到一个不正确的数据。为了保证可移植性,应该确保数据的对齐性,同时避免滥用指针操作以避免非对齐的存储器访问。对于ARM处理器,执行下面的语句后,变量l将等于奇怪的0x01040302。
如果一个C语言原生类型T的变量在存储器中的地址为sizeof(T)的整数倍,那么称它是自然对齐的。一般情况下,编译器通过自然对齐所有数据类型以解决对齐问题。对C原生类型来说,这不存在任何问题;而struct类型的对齐要求与对齐要求最严格的成员一致,这可能导致结构体中两个相邻的、不同尺寸数据类型的成员之间存在填充。例如,对结构体foo来说,它的对齐要求与y一致,随着处理器字长的不同,x和y之间可能存在1个或3个甚至7个字节的填充。
C标准在stddef.h中定义了offsetof宏,用来返回结构体成员的偏移值。为了查看成员y的偏移值可以使用下面的语句:
GCC编译器为此提供了更多的扩展。关键字__alignof__返回对象的对齐需求,例如如果目标机器要求double值对齐于8字节边界,那么__alignof__(double)返回8。关键字__attribute__可以用来指定变量或者结构体成员的最低对齐需求(以字节为单位),例如int x__attribute__((aligned(16)))=0;可以通知编译器为变量x分配一个16字节对齐的地址。如果打开了-Wpadded开关,那么当结构体存在填充时,GCC编译器还会给出警告。如果编译器没有提供类似GCC那样的扩展,那么另一个常用的技巧是使用union提升较低数据类型的对齐要求。例如对于下面的联合u,假设sizeof(int)=4,那么可以保证c也是4字节对齐的。
总之,在实践中应该编写符合标准的代码,隔离与特定处理器或者操作系统相关的代码,甚至尝试使用不同的编译器编译你的代码,只有这样才能确保代码的可移植性。
[1]Horton M ark.Portable C Software[M].London:Prentice Hall,1990.
[2]Jones Derek M.The New C Standard an Economic and Cultural Commentary[M].NewYork:Addison-Wesley Professional,2003.
[3]ISO/IEC 9899:1999[EB/OL].[2010-03].www.open-std.org/JTC1/SC22/wg14/www/docs/n1124.pdf.
[4]Brian Kernighan W.程序设计实践[M].裘宗燕 ,译 .北京 :机械工业出版社,2003.
[5]Harbison Samuel P,Steele Guy L.C:A Reference Manual[M].5版.北京:人民邮电出版社,2007.
[6]Stallman Richard M,the GCC Developer Community.Using the GNU Compiler Collection:For GCC version 4.4.1[EB/OL].[2010-03].http://gcc.gnu.org/onlinedocs/gcc-4.4.1/gcc.pdf.