我们最近完成的一个项目是移植一个大型的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)时,就会导致真正的问题了。为了不让编译器做出这种假设,必须确保所有需要的系统头文件都包含或提供了外部函数的函数原型。
隐藏的问题
当然,也有一些问题不会在一开始就很容易地被发现,例如,在64位程序中,long与指针尺寸更大了,随之也会带来包含它们的结构大小上的增长。结构元素的排列方式决定了结构将占用多大的空间,举例来说,一个包含了int其后跟着一个long的结构,在32位程序中占用8字节,但64位程序在结构的第一个元素上加入了4个字节的填充数据,以使第二个元素在边界上排列得更自然,见图1:
图1:32位与64位结构中数据的排列方式 |
为最小化填充数据所带来的影响,可把结构中的数据元素按从大到小重新排列。但是,如果数据元素是通过字节流访问的,还必须调整代码中的逻辑部分,以适应结构中的新的数据排列方式。
在那些重新排列结构数据不起作用,或数据元素是通过字节流访问的情况中,就要小心计算填充数据,我们对此的解决方案是,实现一个帮助函数,在把数据写到字节流之前,从结构中消去多余的填充数据;而由此带来的另一个好处是,在读取数据时就不需要作任何修改了,参见例2:
例2:
typdef struct demo{ int i; long j; } DEMO;
DEMO test; /*pout_raw输出原始数据到一个文件中*/ /*输出结构的每一个元素并避免了填充数据*/ pout_raw ((int) file_unit, (char *) test.i, sizeof (test.i)); pout_raw ((int) file_unit, (char *) test.j, sizeof (test.j));
/*下行包含了填充数据*/ pout_raw ((int) file_unit, (char *) test,sizeof(test)); |
数组问题 64位上的long型数组或结构中的数组,不只是比它们32位的对等体包含更大的数值,而且可包含更多的元素。回头看一下前面用来定义数组边界和分配数组大小的4字节变量,它们可被转换成long。如果为了让64程序获得更好的性能,需决定现存的long数组是否应转换成int类型,请参考http://developers.sun.com/prodtech/cc/articles/ILP32toLP64Issues.html。
编码惯例与移植考虑
除了遵循操作系统编译器文档所推荐的标准64位编码惯例之外,以下还有一些意见及小提示,也许在计划向64位移植时,可帮得上忙:
·如果可能且现实,把源代码转换为ANSI C/C++。这将简化64位移植过程,甚至将来的移植也会受益。
·你的目标操作系统同时支持32位与64位应用程序吗?应及早知道答案,因为它会对是否移植产生影响。例如:在Solaris上,使用系统命令isainfo检查32位与64位程序的兼容性。
% isainfo -v 64-bit sparcv9 applications 32-bit sparc applications |
·如果你的源代码已经被如CVS之类的版本控制系统管理,在移植之前,将会有助于实现第一个程序。由于为了移植,我们常常需要对全局代码作大量的改动,而有时需要回返到前一个版本的代码,版本控制系统正好是这方面的能手。
·你的应用程序使用或加载32位的第三方库吗?如果有,在计划期间就最好决定,是否这些库也应该升级为64位。如果long与指针不能在你的主程序与第三方库之间传递,那么只要操作系统可以同时运行32位与64位程序,就没有必要移植为64位了;如果操作系统不支持,那么就要计划把第三方程序也移植为64位了。
·如果你的程序在运行时动态地加载库,并仍然使用对load()的老式调用,那么应使用dlopen()来纠正在主程序与第三方模块之间的数据传递问题,尤其是在dlopen()出现之前的那些老式AIX程序更应如此。为在AIX上打开运行时链接,链接器中必须加上 -brtl选项,并带上 -L ":"以指定库的位置。为提高兼容性,你的主程序和所有通过dlopen()加载的库,都必须使用运行时链接重新编译。
·考虑向后兼容性。当移植到64位平台时,向后兼容性问题变得尤为重要,必须对现有测试方案进行改进,以包含老式32位测试和新式64位测试。
工具 对大型的源代码编制一个详细目录清单,特别是共享于跨32位与64位平台的,而且要对每一处修改进行评估,不管它是多么微不足道,都是一项令人畏惧的任务,因为忽略转换问题的潜在性及引入新错误的概率都非常高。可是,使用一些64位工具及技巧,许多的这种潜在性问题能在预编译阶段、编译阶段、和运行时捕捉到。下面是一些可用的工具:
·预编译阶段。在编译器中使用 -errchk=longptr64选项,可启用lint,对于捕捉类型转换的不匹配、隐式函数声明、及参数不匹配等非常有效,例3演示了典型的lint警告,当然也有其他lint类型的程序,如FlexLint(http://www.gimpel.com/html/products.htm)。
·编译时技巧。调整编译器警告级别,以保证至少在项目的初始阶段,没有抑制任何警告信息。对多平台环境而言,在不同的操作系统上,编译同一源代码,将会产生不同的问题,要充分利用这一点,对比分析出问题所在。
·编译时和运行时工具。先进的工具,如 Insure++ 或 Purify for 64-bit,都针对至少一个平台,在任何针对运行时和编译时的开发环境中,都派得上用场。
·运行时工具。试一下dbx,其由每一个Unix编译器提供;或者ddd(数据显示调试器),它是Unix上的dbx和gdb的图形接口(http://www.gnu.org/software/ddd/)。
例3:典型lint警告
warning: implicit function declaration: main warning: argument does not match remembered type: arg #1 warning: passing 64-bit integer arg, expecting 32-bit integer: MyProc(arg 7) warning: assignment of 64-bit integer to 32-bit integer warning: function argument ( number ) used inconsistently warning: comparing 32-bit integer with 64-bit integer |
结论 花一些时间来做前期的计划和调查,虽说不一定能达到事半功倍的效果,但从获得的结果来看还是值得的。而当程序中没有任何一处工作正常时,也不要气馁,此时,应仔细并系统地检查代码并找出问题所在。随着内存及数据数量的逐年高速增长,我们有理由相信,跟64位程序所带来的好处相比,那些移植转换时的艰难过程根本算不了什么。
查看本文来源