作者:John Regehr
原作:http://blog.regehr.org/archives/226
当像边界检查GCC,Purify,Valgrind这样的工具第一次出现时,在它们下面运行任意一个UNIX应用程序是有趣的。检查器的输出显示这些应用程序,尽管工作得很好,执行了大量的内存安全性错误,比如使用未初始化数据、数组访问越界等。仅运行grep或不管什么将导致数以十计或百计的这些错误发生。
发生了什么?基本上,C/UNIX执行环境的附带属性使得这些错误(通常)是温和的。例如,由malloc()返回的块通常在之前及/或之后包含一些填充字节;这些填充字节可以吸收越界储存,只要它们不是离分配区域太远。值得消除这些bug吗?是的。首先,一个带有不同属性的执行环境,比如一个提供减少填充字节的嵌入系统的malloc(),会把温和的近失(near-miss)数组写变为凶险的堆讹误bug。其次,在不同的情形下,同一个温和的bug甚至可能,在同一个执行环境里,导致一个崩溃或讹误。开发者通常发现这些类型的争论是令人信服的,目前大多数UNIX程序是相对Valgrind净化的。
用于查找整数未定义行为的工具不像内存不安全性检查器那么成熟。在C及C++中坏的整数行为包括有符号数溢出、除0、偏移超出比特宽度等。近年来,这些成为了更加严重的问题,因为:
最近我的学生Peng Li实现了一个整数未定义行为的检查工具。使用它,我们发现许多程序包含这些bug。例如,超过半数的SPECINT2006基准测试执行了或这或那的整数未定义行为。今天在许多方面,整数bug的情形与1995年左右内存bug的情形类似。说得更明确点,整数检查工具确实存在,但看起来它们没有广泛使用,而且它们中的许多工作在2机制文件上,太晚了。在编译器有机会利用未定义行为之前,你必须看一下源代码——然后消除它。
本贴的余下部分探讨我们在LLVM:一个中等大小(~800KLOC)的开源C++代码库中发现的几个整数未定义行为。当然我这里不是挑剔LLVM:它是质量非常高的代码。想法是通过看一下,在这个经过良好测试的代码中,未检测出的、潜藏的某些问题,我们可以有望学会在将来然后避免写出这些bug。
作为一个无目的的注解(asa random note),如果我们把LLVM代码视为C++0x而不是C++98,那么会出现大量额外的偏移相关的未定义行为。在后续贴中我将谈及新的偏移限制(它等同于C99中的限制)。
我稍微整理了工具的输出以提高可读性。
错误消息:
UNDEFINED at <BitcodeWriter.cpp, (740:29)> :
Operator: -
Reason: Signed Subtraction Overflow
left (int64): 0
right (int64): -9223372036854775808
代码:
int64_t V = IV->getSExtValue();
if (V >= 0)
Record.push_back(V << 1);
else
Record.push_back((-V << 1) | 1); <<----- bad line
在运行在2进制编码机器上的所有现代C/C++变种中,对值是INT_MIN(或在这个情形里,INT64_MIN)的int求负是未定义的行为。对这个情形,修改是添加一个显式的检查。
编译器会利用这个未定义的行为吗?它们会:
[regehr@gamow ~]$ cat negate.c
int foo (int x) __attribute__ ((noinline));
int foo (int x)
{
if (x < 0) x = -x;
return x >= 0;
}
#include <limits.h>
#include <stdio.h>
int main (void)
{
printf (“%d\n”, -INT_MIN);
printf (“%d\n”, foo(INT_MIN));
return 0;
}
[regehr@gamow ~]$ gcc -O2 negate.c -o negate
negate.c: In function ‘main’:
negate.c:13:19: warning: integer overflow in expression [-Woverflow]
[regehr@gamow ~]$ ./negate
-2147483648
1
在C编译器有矛盾的想法中,-INT_MIN即是负的又是非负的。如果第一个真正的AI(译注:人工智能)以C或C++编写,我想它会立即推导出自由即是奴役、爱即是恨、和平即是战争。
错误消息:
UNDEFINED at <InitPreprocessor.cpp, (173:39)> :
Operator: -
Reason: Signed Subtraction Overflow
left (int64): -9223372036854775808
right (int64): 1
代码:
MaxVal = (1LL << (TypeWidth – 1)) – 1;
在C/C++中,像这样计算最大有符号整数值是非法的。有更好的方式,比如创建一个全1的向量,然后清除高位的比特。
错误消息:
UNDEFINED at <TargetData.cpp, (629:28)> :
Operator: *
Reason: Signed Multiplication Overflow
left (int64): 142998016075267841
right (int64): 129
代码:
Result += arrayIdx * (int64_t)getTypeAllocSize(Ty);
这里分配的大小是貌似合理的,但对于任何可想象的数组,数组索引超出了边界。
错误消息:
UNDEFINED at <InstCombineCalls.cpp, (105:23)> :
Operator: <<
Reason: Unsigned Left Shift Error: Right operand is negative or is greater than or equal to the width of the promoted left operand
left (uint32): 1
right (uint32): 63
代码:
unsigned Align = 1u << std::min(BitWidth – 1, TrailZ);
这完全是一个bug:BitWidth被设置为64,但应该是32。
错误消息:
UNDEFINED at <Instructions.h, (233:15)> :
Operator: <<
Reason: Signed Left Shift Error: Right operand is negative or is greater than or equal to the width of the promoted left operand
left (int32): 1
right (int32): 32
代码:
return (1 << (getSubclassDataFromInstruction() >> 1)) >> 1;
当getSubclassDataFromInstruction()返回在范围128-131内的一个值时,左移的右实参值为32(译注:上面的代码应是getSubclassDataFromInstruction()>> 2)。(在任一方向)移动这个比特宽度或更大是一个错误,因此这个函数要求getSubclassDataFromInstruction()返回不大于127的值。
使得某些程序行动错误,但没有给开发者任何方式来告知他们的代码是否执行这些行动,如果是在何处,基本上是邪恶的。C的设计点之一是“相信程序员”。这很好,但有信心然后才有信任(there’strust and then there’s trust)。我的意思是,我信任我5岁的孩子,但我仍然不会让他独自穿过一条繁忙的街道。以C或C++创建一大段安全性关键或保密性关键的代码,在编程方面等同于蒙上眼睛穿越8车道高速公路。