我们最近完成的一个项目是移植一个大型的32位应用程序,它可在64位环境中支持11个操作系统平台..
我们最近完成的一个项目是移植一个大型的32位应用程序,它可在64位环境中支持11个操作系统平台,并且程序的源代码超过了30万行。由于此32位程序是在几年前分成几部分开发而成,所以极有可能代码是由不同的开发者编写。鉴于此,我们有理由怀疑,在64位移植中导致问题的类型不匹配,很有可能是在这几年中随着程序模块的添加与删除而引入的。
我们移植此32位程序到64位平台,是为了利用64位技术的先进之处--支持更大的文件、支持更大的内存、及64位计算,大体使用的方法是一个反复迭代的过程,不断地在一些细节问题上来来回回,如字节序、调整编译器选项等等,并时不时停下来查看是否达到了总体目标--遵从ANSI标准及源代码将来的可移植性。第一步,我们研究了64位的系统资源,以充分了解11个操作系统平台上每一个的编译器选项、内存模型和编码方面的考虑。作为全部工作的起点,我们在其中一个平台上打开了所有编译器警告,进行第一次构建,并仔细检查构建日志信息。通过这些最初的构建、使用本地调试器、及之后使用如Parasoft's Insure++ (http://www.parasoft.com/)这样的工具,我们确定了一个开发蓝图,接下来,编制了一个清晰彻底的源代码目录清单,并在每次配置构建之后进行相应的检查。
经过最初的代码修改、调试、查阅构建日志,已经有足够的信息对现实中可能碰到的事件进行排序以确定优先次序。在一个拥有所有基本功能的程序成功地通过我们的自动测试案例之后,移植工作总算到了一个转折点;此测试除了测试64位功能,也包含了向后兼容性测试。如果你所移植的项目中有几个不同的64位平台,很可能要在其上一一测试,一旦程序可在第一个平台上正确运行,接下来就要测试下一个平台,如此这般下去。然而,我们却发现了一个非常好的方法,可在同一时间,在所有的平台上进行工作,这是因为:
·每一个编译器都在它的警告信息中都提供了不同的信息,仔细查看几个编译器产生的错误,可有助于我们定位问题区域。
·不同平台上,错误也不同。同一个问题,在另一个平台上看上去无任何迹象,很可能会在当前平台上导致程序崩溃。
在此项目中,最后需要考虑的一点是为最终发布的测试阶段作提前计划。因为最近修改的代码会被32位及64位的多平台共享,所以在每一个32位平台上,必须重新进行彻底地测试,这要花上双倍的测试时间和资源。
跨平台问题 期间还有许多的问题,在把32位程序移植到多个64位操作系统平台时,这些问题涵盖了从编译器警告到读写二进制数据等等。幸运的是,编译器能帮助我们确定大多数64位移植问题,可在所有平台上把编译器警告级别设为最严,要更多地关注那些指示出数据截断及把64位数据赋给32位数据的警告。不管怎样,把编译器警告设为更严的级别,将会导致多得数不清的警告信息,当然其中大部分能被编译器自身自动解决;此处最主要的问题是,很可能最重要的警告信息被大量的次要信息淹没了,没办法做出区分。为了解决这个问题,我们在不同平台上同时进行构建,因为不同的编译器能给出不同详细级别的警告信息,这将帮助我们做出区分,以过滤掉无用的信息,找出真正需要修正的问题所在。
某些应用程序需要访问那些64位与32位共享的二进制数据或文件,在这种情况下,必须仔细检查long与指针的二进制格式,可能需要修改相关的读写函数以转换不同的大小,并在多平台间处理大字节序与小字节序问题。为得到正确的机器字节序,在64位程序中更大的数据尺寸需要更多的字节交换。
例如,一个32位的long:
Big Endian(大字节序)= (B0, B1, B2, B3) |
转换为:
Little Endian(小字节序)= (B3, B2, B1, B0) |
而一个64位的long:
Big Endian(大字节序)= (B0, B1, B2, B3, B4, B5, B6, B7) |
转换为:
Little Endian(小字节序)= (B7, B6, B5, B4, B3, B2, B1, B0) |
大多数的编译器能发现类型的不匹配,并能在构建期间纠正它们,这对如传递给函数的参数这样大多数的简单赋值来说是正确的。而真正的问题在于,对int、long、指针之间的不匹配,编译器在编译期间却视而不见,或者说编译器在编译期间所做的假设,导致了这种不匹配问题依然存在。前者涉及指针参数及函数指针,而后者主要是有关于函数原型。
传递一个int和long指针作为函数参数时,如果这个指针之后被解引用为一个不同的、不兼容的类型,也会导致问题。这种情况不是32位代码的问题,而是因为ing与long是不可互换的。但是,在64位代码中,却因为指针与生具有的可伸缩性,这种情况将导致运行时错误,大多数编译器假定你正在做的事情,就是你想要做的事情,所以会悄无声息地对此问题放行,除非打开更多的警告信息。而通常只有在程序运行时,此问题才会浮出水面。
举例来说,例1可同时在Solaris和AIX(Frote7、VAC 6)的32位与64位模式中通过编译,而没有任何警告,然而,64位的版本运行时却输出不正确的值。在如此短的示例中,这种问题很容易被发现,如果在更大型的代码中呢,恐怕会难多了吧。这种类型的问题可隐藏在现实中实际的代码里,而大多数的编译器却不能发现它们。
例1:
#include <stdlib.h> #include <stdio.h>
int Func1(char *);
int main() { long arg, ret; arg = 247; ret = Func1((char *)&arg); printf("%ld\n", ret); return(0); } int Func1(char * input) { int *tmp; tmp = (int *)input; return(*tmp); } |
当作为64位可执行程序运行于小字节序机器上时,例1显得一切正常,因为arg的值完全包含在long的四个最少有意义的字节中。然而,甚至在小字节序的x86机器上,当arg的值超出了四个最少有意义的字节时,64位的版本也会在运行期间产生一个错误。
正是因为函数指针,编译器无法获取信息,以确定哪一个函数将会调用,因此它不能纠正或警告你有关可能存在的类型不匹配问题,所有通过特定函数指针调用的函数参数与返回类型都在此范围之内。如果想从根本上纠正此问题,你必须提供一种分离的方案,在函数调用时对参数及返回值进行适当的类型转换。
第二个问题涉及隐式函数声明,如果没有对代码中调用的每一个函数提供一个原型,编译器就会做出假设。编译器产生的类似警告信息"Implicit function declaration: assuming extern returning int"在32位构建时通常是无关紧要的;但在64位构建时,这种返回值是int的假设,当函数实际上是返回long或指针(例如malloc)时,就会导致真正的问题了。为了不让编译器做出这种假设,必须确保所有需要的系统头文件都包含或提供了外部函数的函数原型。