100个开源C/C++项目中的bugs(二)未定义行为、与运算优先级相关的错误

from:http://www.oschina.net/question/1579_45444

未定义行为

首先,一小段理论知识

未定义行为是某些编程语言的特性(尤其在C和C++中),在某些情形下产生的结果將依赖于编译器的实现或指定的优化选项。换句话说,规范并没有定义 某情况下该语言的行为,仅仅是说:“在 A 条件下,B 结果是未定义的”。在这种情况下错误在你的程序中被认为是允许的,甚至在一些特别的编译器中执行良好。这样的程序不能跨平台,并有可能在不同的电脑,不同 的操作系统甚至不同的编译器设置中导致失败。

 一个时序点可以是程序中的任意点,它保证在它之前所有运算的副作用已完成,而后继运算的副作用未开始.学习更多关于时序和跟时序点相关的未定义行为,查看该贴:http://www.viva64.com/en/t/0065/.

例 1. Chromium项目。不正确的使用智能指针。

1 void AccessibleContainsAccessible(...)
2 {
3   ...
4   auto_ptr<VARIANT> child_array(new VARIANT[child_count]);
5   ...
6 }

The error was found through the V554 diagnostic: Incorrect use of auto_ptr. The memory allocated with 'new []' will be cleaned using 'delete'. interactive_ui_tests accessibility_win_browsertest.cc 171

该例子演示了使用智能指针的时候,有可能导致未定义的行为。它可能通过堆破坏,程序崩溃,未完全的对象析构函数或任何其它的错误传达. 该错误是:内存被new [] 操作分配,而被‘auto_ptr'类构析函数里的 delete 操作释放:

1 ~auto_ptr() {
2   delete _Myptr;
3 }

为修复这类问题,你应为实例使用一个更恰当的类,boost::scoped_array.

例 2. IPP Samples 项目。经典的未定义行为。

01 template<typename T, Ipp32s size> void HadamardFwdFast(...)
02 {
03   Ipp32s *pTemp;
04   ...
05   for(j=0;j<4;j++) {
06     a[0] = pTemp[0*4] + pTemp[1*4];
07     a[1] = pTemp[0*4] - pTemp[1*4];
08     a[2] = pTemp[2*4] + pTemp[3*4];
09     a[3] = pTemp[2*4] - pTemp[3*4];
10     pTemp = pTemp++;
11     ...
12   }
13   ...
14 }

The error was found through the V567 diagnostic: 未定义行为. 'pTemp' 变量被修改且在时序点间使用了两次. me umc_me_cost_func.h 168

这是一个关于未定义程序行为的经典例子. 各类文章中都拿它来演示未定义行为. ‘pTemp'自增与否是未知的。两个改变 pTemp 变量值的操作位于一个时序点中.这意味着编译器可能创建如下的代码:

pTemp = pTemp + 1;

pTemp = pTemp;

也可能创建另一版本的代码:

TMP = pTemp;

pTemp = pTemp + 1;

pTemp = TMP;

创建何种版本的代码依赖于编译器和优化选项。

例 3.Fennec Media Project 项目。复杂的表达式。

1 uint32 CUnBitArrayOld::DecodeValueRiceUnsigned(uint32 k)
2 {
3   ...
4   while (!(m_pBitArray[m_nCurrentBitIndex >> 5] &
5     Powers_of_Two_Reversed[m_nCurrentBitIndex++ & 31])) {}
6   ...
7 }

The error was found through the V567 diagnostic: 'm_nCurrentBitIndex' 变量被修改并在单个时序点中被使用了两次.MACLib unbitarrayold.cpp 78

 'm_nCurrentBitIndex' 变量的使用在两者间并没有时序点. 意味着标准并未指定该变量何时增长. 情况不同, 该代码可能工作方式不同,依赖于编译器和优化选项.

例 4.Miranda IM 项目. 复杂的表达式.

1 short ezxml_internal_dtd(ezxml_root_t root,
2   char *s, size_t len)
3 {
4   ...
5   while (*(n = ++s + strspn(s, EZXML_WS)) && *n != '>') {
6   ...
7 }

The error was found through the V567 diagnostic: 未定义行为. 's' 变量被修改且在时序点间使用了两次. msne zxml.c

这里使用了前缀++. 但不代表什么:无法保证 's' 变量值会在 strspn() 函数调用前增长。 

与运算优先级相关的错误

为了更容易理解例子,让我们先回顾运算符优先级列表。

例 1.MySQL 项目。! 和 & 运算的优先级

1 int ha_innobase::create(...)
2 {
3   ...
4   if (srv_file_per_table
5       && !mysqld_embedded
6       && (!create_info->options & HA_LEX_CREATE_TMP_TABLE)) {
7   ...
8 }

The error was found through the V564 diagnostic: '&' 操作被用到了bool类型的值上. 你很可能忘记加入圆括号或有意的使用'&&'操作。innobase ha_innodb.cc 6789

程序员想用表达式的某部分来检查 'create_info->options' 变量中的某个特定的比特位等于是否为 0 . 但 '!' 操作的优先级高于 '&' 操作,这就是该表达式使用下面运算规则的原因:

1 ((!create_info->options) & HA_LEX_CREATE_TMP_TABLE)
2 We should use additional parentheses if we want the code to work properly:
3 (!(create_info->options & HA_LEX_CREATE_TMP_TABLE))

或者,我们发现更好的方式,用下面的方式编写代码:

1 ((create_info->options & HA_LEX_CREATE_TMP_TABLE) == 0)

例 2. Emule 项目. * 和 ++ 优先级.

1 STDMETHODIMP
2 CCustomAutoComplete::Next(..., ULONG *pceltFetched)
3 {
4   ...
5   if (pceltFetched != NULL)
6     *pceltFetched++;
7   ...
8 }

The error was found through the V532 diagnostic:考虑审查 '*pointer++' 部分. 可能的意思是:'(*pointer)++'. emule customautocomplete.cpp 277

如果 'pceltFetched' 不是一个空指针,该函数必须增加该指针指向的ULONG类型变量的值。错误是:'++' 运算符的优先级高于 '*' 运算符的优先级(指针解引用)。该 "*pceltFetched++;" 行等同于下面的代码:

1 TMP = pceltFetched + 1;
2 *pceltFetched;
3 pceltFetched = TMP;

实际上它仅仅增加了指针的值。为使代码正确,我们必须添加括号:"(*pceltFetched)++;".

例 3.Chromium 项目。& 和 != 运算符的优先级

1 #define FILE_ATTRIBUTE_DIRECTORY 0x00000010
2  
3 bool GetPlatformFileInfo(PlatformFile file, PlatformFileInfo* info) {
4   ...
5   info->is_directory =
6     file_info.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY != 0;
7   ...
8 }

The error was found through the V564 diagnostic: '&' 操作被用到了bool类型的值上. 你很可能忘记加入圆括号或有意的使用'&&'操作。base platform_file_win.cc 216

程序员们很容易忘记 '!=' 的优先级高于 '&' 的优先级。这就在我们的例子中发生了。因此,我们使用下面的表达式:

1 info->is_directory =
2  file_info.dwFileAttributes & (0x00000010 != 0);

让我们简化它:

1 info->is_directory = file_info.dwFileAttributes & (true);

再次简化它:

1 info->is_directory = file_info.dwFileAttributes & 1;

原来, 我们是测试了第一个比特位而不是第5个比特位. 为修正它,我们需要添加圆括号。

例 4.BCmenu 项目。IF 和 ELSE 混乱。

1 void BCMenu::InsertSpaces(void)
2 {
3   if(IsLunaMenuStyle())
4     if(!xp_space_accelerators) return;
5   else
6     if(!original_space_accelerators) return;
7   ...
8 }

The error was found through the V563 diagnostic: 可能,这里的 'else' 分支必须与前一个 'if' 关联。 fire bcmenu.cpp 1853

这不是一个优先级的错误,但与它有关。程序员没有顾及 'else' 分支是于最近的 'if' 操作符关联。我们有充分的理由认为代码將以下面的算法工作:

1 if(IsLunaMenuStyle()) {
2   if(!xp_space_accelerators) return;
3 else {
4   if(!original_space_accelerators) return;
5 }

但实际上它等同于下面的结构:

1 if(IsLunaMenuStyle())
2 {
3    if(!xp_space_accelerators) {
4      return;
5    else {
6      if(!original_space_accelerators) return;
7    }
8 }

例 5.IPP Samples 项目. ?: 和 | 的优先级。

1 vm_file* vm_file_fopen(...)
2 {
3   ...
4   mds[3] = FILE_ATTRIBUTE_NORMAL |
5            (islog == 0) ? 0 : FILE_FLAG_NO_BUFFERING;
6   ...
7 }

The error was found through the V502 diagnostic: 可能 '?:' 操作不会像期望的那样工作。'?:' 操作具有比 '|' 操作更低的优先级。 vm vm_file_win.c 393

依赖于 'islog' 变量值,该表达式必须等于 "FILE_ATTRIBUTE_NORMAL" 或 "FILE_ATTRIBUTE_NORMAL"。但这不会发生。'?:' 的优先级比 '|' 优先级低。因此,该代码会变成下面这样:

1 mds[3] = (FILE_ATTRIBUTE_NORMAL | (islog == 0)) ?
2  0 : FILE_FLAG_NO_BUFFERING;

让我们简化该表达式:

1 mds[3] = (0x00000080 | ...) ? 0 : FILE_FLAG_NO_BUFFERING;

既然 FILE_ATTRIBUTE_NORMAL等于0x00000080,该条件永远为真。意味着 0 总是被写入 mds[3] 中。

例 6.Newton Game Dynamics 项目。?: 和 * 的优先级。

1 dgInt32 CalculateConvexShapeIntersection (...)
2 {
3   ...
4   den = dgFloat32 (1.0e-24f) *
5         (den > dgFloat32 (0.0f)) ?
6           dgFloat32 (1.0f) : dgFloat32 (-1.0f);
7   ...
8 }

The error was found through the V502 diagnostic: 可能 '?:' 操作不会像期望的那样工作。'?:' 具有比 '*' 运算更低的优先级。physics dgminkowskiconv.cpp 1061

该代码的错误再一次与 '?:' 去处符相关。'?:' 运算符的条件被一个无效的表达式 "dgFloat32 (1.0e-24f) * (den > dgFloat32 (0.0f))"表示了. 添加圆括号將解决该问题.

顺便说明,程序员们常常忘记 '?:' 运算符是多么狡猾。这里有个关于该主题贴子:"How to make fewer errors at the stage of code writing. Part N2".


上一篇:对技术的态度


下一篇:2017年的golang、python、php、c++、c、java、Nodejs性能对比(golang python php c++ java Nodejs Performance)