您现在的位置: 天下网吧 >> 网吧天地 >> 天下码农 >> 微信小程序 >> 正文

64位开发中去除64位平台的内存错误

2011-1-5vczx佚名

   对新平台上应用程序的开发者来说,64位平台的稳定和可靠,是吸引他们的关键;而任何内存错误问题都会导致开发工作的失败,内存错误最棘手之处在于它是难以捉摸的,找出它们非常困难且要花费大量时间。内存错误不会在通常意义上的测试中暴露出来,正是因为它们潜在的有害性,所以在程序定型之前,去除所有的内存问题就显得非
 
常必要了。

  目前有一些强大的内存错误检测工具,它们可以在运行于双核心处理器的应用程序中,找出导致线程内存错误的原因;它可在传统测试技术找不出问题的地方,找出并修正那些难以捉摸、导致程序崩溃的"元凶"。错误检测工具可帮助你在发布程序之前,找出并修正那些C/C++内存错误,而在移植程序之前修正这些问题,可提高在新平台新架构上的程序质量,使移植过程更加流水线化,并且使老程序更加健壮可靠。

  为何移植如此之难?

  在向64位处理器或新硬件移植代码时产生的问题当中,大多数开发者是负有主要责任的。就此来说,代码在移植到新平台或新架构之上时,内存问题似乎也成倍增长了。

  在过渡到64位架构时最基本的问题,就是对各种不同的int和指针在比特位长度上假定。在从long转换到int时,不管是赋值还是显式转换,都存在着一定的隐含限制。前者可能产生一个编译器警告,而后者可能被无声地接受,就此导致了运行时的各种错误。另一个问题就是int常量并不总是与int同样大小,这是混淆有符号和无符号常量的问题,同时,适当地使用有关的后缀可以减少此类问题的发生。

  另一些问题的主要原因是各种指针类型的不匹配。举例来说,在多数64位架构上,指针类型不能再放入一个int中,而那些把指针值储存在int变量中的代码,此时当然就会出错了。

  这些问题通常会在移植过程中暴露出来,因为移植从本质上来说是一种变体测试。当你在移植代码时,实际上是在创建一种"同等变体"(对原始代码的小幅改动,不会影响到测试的结果),而通过这些"同等变体",可找出许多不常见的错误。在C/C++中,创建和运行"同等变体",可揭示出以下问题:

  1、缺少拷贝构造函数或错误的拷贝构造函数

  2、缺少或不正确的构造函数

  3、初始化代码的错误顺序

  4、指针操作的问题

  5、依赖未定义的行为,如求值的顺序

  在准备移植应用程序时,有以下几个相关步骤

  第1步、在移植之前,要保证原始代码中没有诸如内存崩溃、内存泄露等问题,找出指针类型和int错误的最有效的一个方法是,采用平衡变体测试,来达到运行时错误检测的目的。

  变体测试最先是为解决无法计量测试的准确性问题而产生的,大致如下:假定已有了一个完美的测试方案,它已经覆盖了所有的可能性,再假定已有一个完美的程序通过了这个测试,接下来修改代码(称之为变异),在测试方案中运行这个"变异"后的程序(称之为变体),将会有两个可能的情况:

  一是程序会受代码改变的影响,并且测试方案检测到了,在此假定测试方案是完美的,这意味着它可以检测一切改变。此时变体被称作"已死的变体"。

  二是程序没受改变的影响,而测试方案也没有检测到这个变体。此时变体称作"同等变体"。

  如果拿"已死变体"和已生成的变体作对比,就会发现这个比率要小于1,这个数字表示程序对代码改变有多敏感。事实上,完美的测试方案和完美的程序都不存在,这就说上面的两种情况可能会有一个发生。

  程序受影响的结果因个体而异,如果测试方案不适当,将无法检测到。"已经变体"和"生成变体"的比率小于1同时也揭示了测试方案有多精确。

  在实践中,往往无法区分测试方案不精确与同等变体之间的关系。由于缺乏其他的可能性,在此我们只好把"已死变体"对所有变体的比率,看成是测试方案的精确程度。

  例1(test1.c)证实了以上的说法(此处所有的代码均在Linux下编译),test1.c用以下命令编译:

cc -o test1 test1.c.

main(argc, argv) /* line 1 */
int argc; /* line 2 */
char *argv[]; /* line 3 */
{ /* line 4 */
int c=0; /* line 5 */
/* line 6 */
if(atoi(argv[1]) < 3){ /* line 7 */
printf("Got less than 3\n"); /* line 8 */
if(atoi(argv[2]) > 5) /* line 9 */
c = 2; /* line 10 */
} /* line 11 */
else /* line 12 */
printf("Got more than 3\n"); /* line 13 */
exit(0); /* line 14 */
} /* line 15 */


  例1:程序test1.c

  这个简单的程序读取输入的参数,并打印出相关的信息。现在假定用一个测试方案来测试此程序:

Test Case 1:
input 2 4
output Got less than 3
Test Case 2:
input 4 4
output Got more than 3
Test Case 3:
input 4 6
output Got more than 3
Test Case 4:
input 2 6
output Got less than 3
Test Case 5:
input 4
output Got more than 3

  这个测试方案在业界是有一定代表性的,它进行正则测试,表示它将测试对所有正确的输入,程序是否有正确的输出,而忽视非法的输入。程序test1完全通过测试,但它也许隐藏着严重的错误。

  现在,对程序进行"变体",用以下简单的改变:

Mutant 1: change line 9 to the form
if(atoi(argv[2]) <= 5)
Mutant 2: change line 7 to the form
if(atoi(argv[1]) >= 3)
Mutant 3: change line 5 to the form
int c=3;


  如果在测试方案中运行此修改后的程序,Mutants 1和3完全通过测试,而Mutant 2则无法通过。

    Mutants 1和3没有改变程序的输出,所以是同等变体,而测试方案没有检测到它们。Mutant 2不是同等变体,故Test Cases 1-4将会检测到程序的错误输出,而Test Case 5在不同的电脑上可能会有不同的表现。以上表明,程序的错误输出,可看作是程序可能会崩溃的一个信号。

  我们统计一下,共创建了三个变体,而只被发现了一个,这说明表示测试方案的质量为1/3,正如你看到的,1/3有点低,之所以低是因为产生了两个同等变体。这个数字应当作是测试不足的一个警告,实际上,测试方案应检测到程序中的
 
两个严重错误。

  再回到Mutant 2,在Test Case 5中运行它,如果程序崩溃了,那这个变体测试不但计量到了测试方案的质量,还检测到了严重的错误,这就是变体测试发现错误的方法。

main(argc, argv) /* line 1 */
int argc; /* line 2 */
char *argv[]; /* line 3 */
{ /* line 4 */
int c=0; /* line 5 */
int a, b; /* line 6 */
/* line 7 */
a = atoi(argv[1]); /* line 8 */
b = atoi(argv[2]); /* line 9 */
if(a < 3){ /* line 10 */
printf("Got less than 3\n"); /* line 12 */
if(b > 5) /* line 13 */
c = 2; /* line 14 */
} /* line 15 */
else /* line 16 */
printf("Got more than 3\n"); /* line 17 */
exit(0); /* line 18 */
} /* line 19 */

  例2:同等变体

  在例2中的同等变体(Mutant 4),它和前一个变体的不同之处在于,Mutant 4是同等变体,这意味着它在构建时的目的,就是要使修改后的程序如同原始程序一样运行。如果在测试方案中运行Mutant 4,那么Test Case 5大概会失败--程序将崩溃。此处表明,通过创建一个同行变体,实际上是增强了测试方案的检测力度,由此得出的结论是,有以下两种方法,可提高测试方案的精确性:

  ·在测试方案中增加测试数量

  ·在测试方案中运行同等变体

  这两点是非常重要的,尤其是第二点,因为它证明了变体可提高测试的有效性。在这些例子中,是由手工创建了每一个变体,并且对每一个程序都作了单独的修改,这个步骤费时又费力,但是自动生成同等变体是有可能的,正如例3所演示的,这个程序没有输入,只有一个输出,原则上来说,它只需要一次测试:

int doublew(x)
int x;
{ return x*2; }

int triple( y)
int y;
{ return y*3; }

main() {
int i = 2;
printf("Got %d \n", doublew(i++)+ triple(i++));
}

  例3:自动生成变体

Test Case 1:
input none
output 12

  有意思的是,这个程序因编译器的差异,而分别给出答案13或12(注:译者在Visual C++ 2005中,得出的结果是10)。假设你要编写一个这样的程序,还要能在两个不同的平台上运行,如果不同平台上的编译器有所差异,此时你会察觉到这个程序的不同,疑问由此而生:"是哪错了?"这有可能就是导致问题产生的原因。

  试想你在例4中创建了一个同等变体,此时这个程序的结果不依赖于编译器,实际上应是13,这也是在预料之中的。但一旦运行变体测试,就会发现错误了。

int doublew(x)
int x;
{ return x*2; }

int triple( y)
int y;
{ return y*3; }

main() {
int i = 2;
int a, b;

a = doublew(i++);
b = triple(i++);
printf("Got %d \n", a+b);
}

  例4:一个变体

  在变体测试中,最让人惊奇的是,它能找出正常看来是不可能检测到的错误,通常,这些错误隐藏得很深,直到程序崩溃时,才可能发现,但对此,程序员经常不能理解。同等变体是找出错误的机会,而不是其他。但普遍来说,程序员期望同等变体能得出与原程序一样的结果,但如果总是这样的话,那同等变体是没有任何作用了。

  第2步:当清除最致命的错误之后,要把那些可能会出错的代码在移植之前,用静态分析工具再确认一遍。在静态分析时,有两个主要的工作要做:

  ·找出并修正那些移植到新平台之后可能会出错的代码

  ·找出并修正那些可能不能很好地被移植的代码

    首先,要用业界推荐的C/C++编码标准来检查那些可能在新平台上出错的代码,以确认其编码结构没有问题。通过确认代码符合编码标准,可防止不必要的错误发生,还能减少在新平台上的调试工作量,并降低在最终产品中出现bug的机率。

  以下是一些可用的编码标准:

  不要返回对一个局部对象或对在函数内用"new"初始化的指针
 
的引用。对一个局部对象返回一个引用,可能会导致堆栈崩溃;而返回一个对在函数内用"new"初始化的指针的引用,可能会引起内存泄漏。

  不要转换一个常量到非常量。这可能会导致数值被改变,从而破坏数据的完整性。这也会降低代码的可读性,因为你不能再假定常量不被改变。

  如果某个类有虚拟成员函数,它最好也带有一个虚拟析构函数。这能在继承类中防止内在泄漏。带有任何虚拟成员函数的类,通常被用作基类,此时它应有一个虚拟析构函数,以保证继承类通过一个指向基类的指针来引用时,相应的析构函数会被调用。

  公共成员函数必须为成员数据返回常量句柄。当把一个非常量的句柄提供给成员数据时,此时调用者可在成员函数之外修改成员数据,这就破坏了类的封装性。

  不要把指向一个类的指针,转换成指向另一个类的指针,除非它们之间有继承关系。这种无效的转换将导致不受控的指针、数据崩溃等问题,或者其他错误。

  不要从一个构造函数中直接访问一个全局变量。C++语言的定义之中,没有规定在不同的代码单元中定义的静态对象初始化的顺序。因此,在从一个构造函数中访问一个全局变量时,这个变量可能还没有初始化。

  当找到并修正有错误的代码之后,从那些在当前平台上运行良好的代码中再继续找,因为它们可能不能被很好地移植。以下是一些对大多数64位移植项目都适用的规则:

  尽量使用标准类型。比如说,使用size_t而不是int。如果想要一个无符号的64位int,那么请使用uint64_t。这个习惯不但有助于找出和防止代码中的bug,还能在将来向128位处理器移植程序时,帮上大忙。

  检查现有代码中long数据类型的用法。如果变量、域、参数中数值的变化范围,只在2Gig-1到-2Gig或4Gig到0之间,那么最好分别使用int32_t或uint32_t。

  检查所有的"窄向"赋值。应该避免这种情况出现,因为把一个long赋值给一个int,在64位数值上会导致截断。

  找出"窄向"转换。应只在表达式中使用窄向转换,而不是在操作数中。

  找出那些把long*转换成int*,或把int*转换成long*的地方。在32位环境下,这也许是可交替的,但在64位中不行,并检查所有的不匹配指针赋值。

  找出那些在乘法符号的两端,没有long操作数的表达式。要使int型表达式将产生64位结果,至少其中的一个操作数是long或unsigned long。

  找出long型值用int初始化的地方。应避免这种类型的初始化,因为甚至在64位类型的表达式中,int常量也可能只是代表一个32位类型。

  找出那些对int进行移位操作,又把结果赋给long的地方。如果结果是64位值,最好使用64位乘法。

  找出那些64位表达式中的int常量。在64位表达式中应使用64位值。

  找出把指针转换成int的地方。涉及指针与int互转换的代码,应仔细检查。

  检查内联汇编语句。因为它不可能被很好地移植。

  第3步:重复一遍运行时错误检测,以确认所有的修改都没有引入新的运行时错误。

  第4步:此时,你可选择进行更多的测试步骤,以保证在移植之前,所有的代码都完全正确。这个额外的步骤是单元测试,单元测试是在每一个软件单元完成之后进行的传统测试,它在开发阶段的后期,也是有益的。因为在单元级别,很容易设计出每个函数的输入,它将有助于更快地找出那些在应用级别测试中无法发现的错误。

  找出64位处理器上的问题

  也许64位处理器本身就有问题,如果是这样的话,下面的步骤应该有用: 第1步:在64位处理器上重新编译应用程序。在编译中如果有问题,应考虑是不是因编译器的不同而产生的。

  第2步:一旦重新编译代码,应进行代码检查,以确保新代码都遵循适当的编码标准。在这一点上,任何人都不希望每一次修改都带来一个错误,此时解决好过在程序运行时才发现。

  第3步:链接并生成应用程序。

  第4步:应试着运行程序。如果在64位处理器上,运行程序时发现了问题,应使用单元测试方法一个函数一个函数地去找,这样能确定哪些代码没有正确地被移植;修正这些问题直到程序可以运行。

  第5步:重复运行时错误检测。

  一旦程序可以运行,一定要重复一遍运行时错误检测,因为移植过程很可能导致新的问题产生,例如新的内存崩溃或程序工作方式有所不同。如果运行时错误检测发现了错误,那么此时赶快修正它。

  结论

  遵循此文中提及的方法,可在程序发布之前,找到并修正C/C++内存错误,并可以节省下数周的调试时间,使用户免受"灾难"之苦。

欢迎访问最专业的网吧论坛,无盘论坛,网吧经营,网咖管理,网吧专业论坛 https://bbs.txwb.com

关注天下网吧微信/下载天下网吧APP/天下网吧小程序,一起来超精彩

本文来源:vczx 作者:佚名

声明
声明:本站所发表的文章、评论及图片仅代表作者本人观点,与本站立场无关。若文章侵犯了您的相关权益,请及时与我们联系,我们会及时处理,感谢您对本站的支持!联系邮箱:support@txwb.com,系统开号,技术支持,服务联系QQ:1175525021本站所有有注明来源为天下网吧或天下网吧论坛的原创作品,各位转载时请注明来源链接!
天下网吧 网吧天下