100个开源C/C++项目中的bugs

大东哥 发布于 2012/03/22 20:03
阅读 14K+
收藏 141

 100个开源C/C++项目中的bugs

摘要

本文演示静态代码分析的能力. 提供了100个已在开源C/C++项目中发现的错误例子给读者研究。所有的错误已经被PVS-Studio静态代码分析工具发现。

介绍

我们不想让你阅读文档感到厌倦,并马上给你传达错误例子.

那些想了解什么是静态代码分析的读者,请跟随链接。想了解什么是PVS-Studio的读者,可下载试用版.请看该页:http://www.viva64.com/en/pvs-studio/.

当然,还有一件事,请查看我们的帖子: "FAQ for those who have read our articles"

发现错误样本的各个开源项目

错误实例將被归为几类。这种分法是很相对性的。 同一个错误常常同时属于打印错误和不正确的数组操作错误.

当然我们仅从每个项目中列出少数几个错误。如果我们描述所以已发现的错误,那將要编写一本指南书了。以下是被分析的项目:

 数组和字符串处理的错误

数组和字符串处理的错误是C/C++程序中最大的一类。它以程序员可以高效处理低级内存问题为代价. 本文中,我们將展示一小部分被PVS-Studio分析器发现的此类错误。但我们相信任何C/C++程序员都了解他们有多巨大和阴险.

例 1. Wolfenstein 3D项目。对象仅被部分清除:

void CG_RegisterItemVisuals( int itemNum ) {
  ...
  itemInfo_t *itemInfo;
  ...
  memset( itemInfo, 0, sizeof( &itemInfo ) );
  ...
}

The error was found through the V568 diagnostic: It's odd that the argument of sizeof() operator is the '&itemInfo' expression. cgame cg_weapons.c 1467.

sizeof() 操作符计算的是指针大小而非‘itemInfo_t’结构体大小。必须写成"sizeof(*itemInfo)"。

例 2. Wolfenstein 3D项目。矩阵仅被部分清除:

ID_INLINE mat3_t::mat3_t( float src[ 3 ][ 3 ] ) {
  memcpy( mat, src, sizeof( src ) );
}

The error was found through the V568 diagnostic: sizeof()操作返回的是指针的大小,非数组的大小,在'sizeof(src)'表达式中. Splines math_matrix.h 94.

通常开发者期望 'sizeof(src)' 操作返回"3*3*sizeof(float)"字节的数组大小。但根据语言规范,'src'仅是一个指针,而不是一个数组。因此,该矩阵只被部分的复 制。‘'memcpy'’函数將拷贝4或8字节(指针大小),这依赖于代码是32位还是64位的.

如果你希望整个矩阵都被拷贝,你可以给函数传递该数组的引用。以下是正确的代码:

ID_INLINE mat3_t::mat3_t( float (&src)[3][3] )
{
  memcpy( mat, src, sizeof( src ) );
}

例 3. FAR Manage 项目。数组仅被部分清除:

The error was found through the V579: memset 函数接收指针和它的大小作为参数. 这可能会引发错误。检测第三个参数。far treelist.hpp 66.

最有可能的是, 计算待清除的元素数量的乘法操作丢失了, 而该代码须如下所示:

"memset(Last, 0, LastCount * sizeof(*Last));".

例 4. ReactOS. 不正确的字符串长度计算.

static const PCHAR Nv11Board = "NV11 (GeForce2) Board";
static const PCHAR Nv11Chip = "Chip Rev B2";
static const PCHAR Nv11Vendor = "NVidia Corporation";

BOOLEAN
IsVesaBiosOk(...)
{
  ...
  if (!(strncmp(Vendor, Nv11Vendor, sizeof(Nv11Vendor))) &&
      !(strncmp(Product, Nv11Board, sizeof(Nv11Board))) &&
      !(strncmp(Revision, Nv11Chip, sizeof(Nv11Chip))) &&
      (OemRevision == 0x311))
  ...
}

该错误经由V579诊断:strncmp 函数接收了指针和它的大小做为参数。这可能是个错误。查看第三个参数。vga vbe.c 57

'strncmp'的函数的调用仅仅比较了前几个字符,而不是整个字符串。这里的错误是:sizeof()操作,在这种情况下用来计算字符串长度绝对不适宜。sizeof()操作实际上只计算了指针的大小而不是string的字节数量。

关于该错误最讨厌和阴险的是,该代码大多数时候都如预期的工作。99%的情况下,比较前几个字符就足够了。但剩下的1%能带给你愉快和长时间的调试过程。

例 5. VirtualDub 项目. 数据越界(明确的下标).

 struct ConvoluteFilterData {
 long m[9];
 long bias;
 void *dyna_func;
 DWORD dyna_size;
 DWORD dyna_old_protect;
 BOOL fClip;
};

static unsigned long __fastcall do_conv(
  unsigned long *data,
  const ConvoluteFilterData *cfd,
  long sflags, long pit)
{
  long rt0=cfd->m[9], gt0=cfd->m[9], bt0=cfd->m[9];
  ...
} 

The code was found through the V557 diagnostic: 数组可能越界。下标‘9’已经指向数组边界外. VirtualDub f_convolute.cpp 73

这不是一个真正的错误,但是个好的诊断。解释: http://www.viva64.com/go.php?url=756.

例 6. CPU Identifying Tool 项目. 数组越界(宏中的下标)

 #define FINDBUFFLEN 64  // Max buffer find/replace size
...
int WINAPI Sticky (...)
{
  ...
  static char findWhat[FINDBUFFLEN] = {'\0'};
  ...
  findWhat[FINDBUFFLEN] = '\0';
  ...
} 

The error was found through the V557 diagnostic:数组可能越界.下标‘64’已经指向数组边界外。stickies stickies.cpp 7947

该错误与上例类似。末端 null 已被写到数组外。正确代码是:"findWhat[FINDBUFFLEN - 1] = '\0';".

例 7. Wolfenstein 3D 项目. 数组越界(不正确的表达式).

 typedef struct bot_state_s
{
  ...
  char teamleader[32]; //netname of the team leader
  ...
}  bot_state_t;

void BotTeamAI( bot_state_t *bs ) {
  ...
  bs->teamleader[sizeof( bs->teamleader )] = '\0';
  ...
} 

The error was found through the V557 diagnostic: 数组可能越界. 'sizeof (bs->teamleader)' 下标已指向数组边界外。game ai_team.c 548

这是又一个数组越界的例子, 当使用了明确声明的下标时候. 这些例子说明了,这些不起眼的错误比看上去更广泛.

末端的 null 被写到了 'teamleader' 数组的外部。下面是正确代码:

 bs->teamleader[
  sizeof(bs->teamleader) / sizeof(bs->teamleader[0]) - 1
  ] = '\0'; 

Example 8. Miranda IM 项目. 仅字符串的部分被拷贝.

 typedef struct _textrangew
{
  CHARRANGE chrg;
  LPWSTR lpstrText;
} TEXTRANGEW;

const wchar_t* Utils::extractURLFromRichEdit(...)
{
  ...
  ::CopyMemory(tr.lpstrText, L"mailto:", 7);
  ...
} 

The error was found through the V512 diagnostic: 一个 'memcpy' 函数的调用將导致缓冲区上溢或下溢。tabsrmm utils.cpp 1080

如果使用Unicode字符集,一个字符占用2或4字节(依赖于编译器使用的数据模型)而不是一字节。不幸的是,程序员们很容易忘记,你常常会在程序中看到类似该例子的污点。

'CopyMemory' 函数將只拷贝L"mailto:"字符串的部分,因为他处理字节,而不是字符。你可以使用一个更恰当的字符串拷贝函数修复该代码,或者,至少是,将 sizeof(wchar_t) 与 数字7 相乘.

例 9. CMake 项目. 循环中的数组越界.

 static const struct {
  DWORD   winerr;
  int     doserr;
} doserrors[] =
{
  ...
};

static void
la_dosmaperr(unsigned long e)
{
  ...
  for (i = 0; i < sizeof(doserrors); i++)
  {
    if (doserrors[i].winerr == e)
    {
      errno = doserrors[i].doserr;
      return;
    }
  }
  ...
} 

The error was found through the V557 diagnostic: 数组可能越界.'i'下标值可到达 367. cmlibarchive archive_windows.c 1140, 1142

该错误处理本身就包含了错误。sizeof() 操作符以字节数返回数组大小而不是里面的元素的数量。因此,该程序將在循环中试图搜索多于本应搜索的元素.下面是正确的循环:

 for (i = 0; i < sizeof(doserrors) / sizeof(*doserrors); i++) 

例 10. CPU Identifying Tool 项目. A string is printed into itself.

 char * OSDetection () 
{
  ...
  sprintf(szOperatingSystem, 
          "%sversion %d.%d %s (Build %d)",
          szOperatingSystem,
          osvi.dwMajorVersion,
          osvi.dwMinorVersion,
          osvi.szCSDVersion,
          osvi.dwBuildNumber & 0xFFFF);
  ...
  sprintf (szOperatingSystem, "%s%s(Build %d)",
           szOperatingSystem, osvi.szCSDVersion,
           osvi.dwBuildNumber & 0xFFFF);
  ...
} 

This error was found through the V541 diagnostic: It is dangerous to print the string 'szOperatingSystem' into itself. stickies camel.cpp 572, 603

一个格式化字符串打印到自身的企图,可能会导致不良后果。

代码的执行结果依赖于输入数据,结果未知。可能的,结果会是一个无意义字符串或一个 Access Violation 將发生。

该错误可以归为 "脆弱代码" 一类.在某些程序中,向代码提供特殊字符,你可以利用这些代码片段触发缓冲区溢出或者被入侵者利用.

例 11. FCE Ultra 项目。某字符串未获得足够内存

 int FCEUI_SetCheat(...)
{
  ...
  if((t=(char *)realloc(next->name,strlen(name+1))))
  ...
} 

The error was found through the V518 diagnostic: 'realloc' 函数通过 'strlen(expr)' 申请了数量奇怪的内存. 可能正确的变体是 'strlen(expr) + 1'. fceux cheat.cpp 609

该错误由一个错误打印导致。strlen() 函数的参数必须是 "name" 指针而不是 "name+1" 指针。因此,realloc 函数比实际需要少申请了两个字节:1个字节丢失了,因为1没有被加入到字符串长度中。另一个字节也丢失了,因为'strlen'函数计算长度时跳过了第一 个字符。

例 12. Notepad++ 项目。部分的数组清理。

 #define CONT_MAP_MAX 50
int _iContMap[CONT_MAP_MAX];
...
DockingManager::DockingManager()
{
  ...
  memset(_iContMap, -1, CONT_MAP_MAX);
  ...
} 

The error was found through the V512 diagnostic:memset 函数的调用將导致缓冲区上溢或下溢. notepadPlus DockingManager.cpp 60

这是又一个数组元素数量与数组大小混淆的例子。一个通过 sizeof(int) 的乘法操作丢失了。

我们可以继续给你指出更多我们在各种项目中发现的数组处理错误例子。但我们不得不停止了。

未定义行为

首先,一小段理论知识

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

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

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

 void AccessibleContainsAccessible(...)
{
  ...
  auto_ptr<VARIANT> child_array(new VARIANT[child_count]);
  ...
} 

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 操作释放:

 ~auto_ptr() {
  delete _Myptr;
} 

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

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

 template<typename T, Ipp32s size> void HadamardFwdFast(...)
{
  Ipp32s *pTemp;
  ...
  for(j=0;j<4;j++) {
    a[0] = pTemp[0*4] + pTemp[1*4];
    a[1] = pTemp[0*4] - pTemp[1*4];
    a[2] = pTemp[2*4] + pTemp[3*4];
    a[3] = pTemp[2*4] - pTemp[3*4];
    pTemp = pTemp++;
    ...
  }
  ...
} 

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 项目。复杂的表达式。

 uint32 CUnBitArrayOld::DecodeValueRiceUnsigned(uint32 k) 
{
  ...
  while (!(m_pBitArray[m_nCurrentBitIndex >> 5] &
    Powers_of_Two_Reversed[m_nCurrentBitIndex++ & 31])) {}
  ...
} 

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

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

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

 short ezxml_internal_dtd(ezxml_root_t root,
  char *s, size_t len)
{
  ...
  while (*(n = ++s + strspn(s, EZXML_WS)) && *n != '>') {
  ...
} 

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

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

与运算优先级相关的错误

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

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

 int ha_innobase::create(...)
{
  ...
  if (srv_file_per_table
      && !mysqld_embedded
      && (!create_info->options & HA_LEX_CREATE_TMP_TABLE)) {
  ...
} 

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

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

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

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

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

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

 STDMETHODIMP
CCustomAutoComplete::Next(..., ULONG *pceltFetched)
{
  ...
  if (pceltFetched != NULL)
    *pceltFetched++;
  ...
} 

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

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

 TMP = pceltFetched + 1;
*pceltFetched;
pceltFetched = TMP; 

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

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

 #define FILE_ATTRIBUTE_DIRECTORY 0x00000010

bool GetPlatformFileInfo(PlatformFile file, PlatformFileInfo* info) {
  ...
  info->is_directory =
    file_info.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY != 0;
  ...
} 

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

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

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

让我们简化它:

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

再次简化它:

 info->is_directory = file_info.dwFileAttributes & 1; 

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

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

 void BCMenu::InsertSpaces(void)
{
  if(IsLunaMenuStyle())
    if(!xp_space_accelerators) return;
  else
    if(!original_space_accelerators) return;
  ...
} 

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

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

 if(IsLunaMenuStyle()) {
  if(!xp_space_accelerators) return;
} else {
  if(!original_space_accelerators) return;
} 

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

 if(IsLunaMenuStyle())
{
   if(!xp_space_accelerators) {
     return;
   } else {
     if(!original_space_accelerators) return;
   }
} 

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

 vm_file* vm_file_fopen(...)
{
  ...
  mds[3] = FILE_ATTRIBUTE_NORMAL |
           (islog == 0) ? 0 : FILE_FLAG_NO_BUFFERING;
  ...
} 

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

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

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

让我们简化该表达式:

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

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

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

 dgInt32 CalculateConvexShapeIntersection (...)
{
  ...
  den = dgFloat32 (1.0e-24f) *
        (den > dgFloat32 (0.0f)) ?
          dgFloat32 (1.0f) : dgFloat32 (-1.0f);
  ...
} 

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".

格式化输出错误

列举这种类型的错误是重复而乏味的,因此我们將只检查少数几个例子。关键点是,函数声明的可变参数数量与实际接受的格式化字符串的参数数量不符. 任何使用如 printf() 这类函数函数的程序员所犯的错误都相似。

例 1. ReactOS 项目. 不正确的打印WCHAR-character。

 static void REGPROC_unescape_string(WCHAR* str)
{
  ...
  default:
    fprintf(stderr,
      "Warning! Unrecognized escape sequence: \\%c'\n",
      str[str_idx]);
  ...
} 

The error was found through the V576 diagnostic: 不正确的格式化。考虑检查 'fprintf' 函数的的第三个实参。它期望一个字符类型的参数。

fprintf() 函数必须以char类型打印字符。但是第三个参数是WCHAR type类型的字符。用户將得到一个错误生成的消息。为修正该代码,我们需要在字符串格式化中將'%c'换成'%C'。

例 2. Intel AMT SDK 项目. 字符 '%' 丢失

 void addAttribute(...)
{
  ...
  int index = _snprintf(temp, 1023, 
    "%02x%02x:%02x%02x:%02x%02x:%02x%02x:"
    "%02x%02x:02x%02x:%02x%02x:%02x%02x",
    value[0],value[1],value[2],value[3],value[4],
    value[5],value[6],value[7],value[8],
    value[9],value[10],value[11],value[12],
    value[13],value[14],value[15]);
  ...
} 

The error was found through the V576 diagnostic: 不正确的格式化。当调用 'printf' 时, 实际参数数量和预期值不同. 期望:18.实际:19.

咋看之下很难发现错误。然而,PVS-Studio分析器并未疲于应对和通知,该函数携带了更多实际参数比指定的字符串格式化参数。原因是,'%'已在某个地方丢失了。让我们指出该片段:

 "%02x%02x:[HERE]02x%02x:%02x%02x:%02x%02x", 

例 3. Intel AMT SDK 项目. 未使用的参数.

 bool GetUserValues(...)
{
  ...
  printf("Error: illegal value. Aborting.\n", tmp);
  return false;
} 

The error was found through the V576 diagnostic: 不正确的格式化。当调用 'printf' 时, 实际参数数量和预期值不同. 期望:1.实际:2. RemoteControlSample remotecontrolsample.cpp 792

这里的错误是:当打印消息时, 'tmp' 变量未以任何方式使用。

例 4. G3D Content Pak 项目. 打印无效数据

 class Matrix3 {
  ...
  inline float* operator[] (int iRow) {
  ...
};
void AnyVal::serialize(G3D::TextOutput& t) const {
  ...
  const Matrix3& m = *(Matrix3*)m_value;
  ...
  t.printf("%10.5f, %10.5f, %10.5f,\n
           %10.5f, %10.5f, %10.5f,\n
           %10.5f, %10.5f, %10.5f)",
           m[0, 0], m[0, 1], m[0, 2],
           m[1, 0], m[1, 1], m[1, 2],
           m[2, 0], m[2, 1], m[2, 2]);
  ...
} 

The error was found through the V520 diagnostic:  逗号操作符 ',',放在数组了下标表达式中 '[0,0]'. graphics3D anyval.cpp 275

该程序打印无效值而不是矩阵值。当你使用不同的编程语言工作时, 你可能会编写类似的代码,并且有时候会忘记怎么在C语言中访问二维数组的元素。

让我们看看 'm[0,1]' 是怎样工作的。首先,表达式"0,1" 被运算。表达式结果是 1 。然后,'operator[]' 函数在 Matrix3 class中被调用. 函数取得实参值 1 并返回指向矩阵中的第一个字符串的指针. 

下面是正确代码:

 t.printf("%10.5f, %10.5f, %10.5f,\n
         %10.5f, %10.5f, %10.5f,\n
         %10.5f, %10.5f, %10.5f)",
         m[0][0], m[0][1], m[0][2],
         m[1][0], m[1][1], m[1][2],
         m[2][0], m[2][1], m[2][2]); 


代码中打印错误的例子


很多的编程错误由打印错误导致。大多数这类错误能很快的在测试阶段的早期发现。也有一些这类错误被保留在代码中很长时间, 为开发者和用户制造麻烦.

你可以借助PVS-Studio分析器少犯这样的错误。它將在测试前发现这些错误,这将会显助减少缺陷阱和排错时间。

例 1. Miranda IM 项目. 在 IF 中赋值

 void CIcqProto::handleUserOffline(BYTE *buf, WORD wLen)
{
  ...
  else if (wTLVType = 0x29 && wTLVLen == sizeof(DWORD))
  ...
} 

The error was found through the V560 diagnostic: 条件的某部分总为 true : 0x29. icqoscar8 fam_03buddy.cpp 632

由于错误打印,在 if 条件中的发生了赋值。正确的条件:"if (wTLVType == 0x29 && wTLVLen == sizeof(DWORD))".

例 2. ReactOS 项目. 赋值错误

 BOOL WINAPI GetMenuItemInfoA(...)
{
  ...
  mii->cch = mii->cch;
  ...
} 

The error was found through the V570 diagnostic: 'mii->cch' 变量值被赋值给了本身. user32 menu.c 4347

变量值被赋给了本身。程序员显然是想写成:"mii->cch = miiW->cch;".

例 3. Clang 项目. 对象名打印错误

 static Value *SimplifyICmpInst(...) {
  ...
  case Instruction::Shl: {
    bool NUW =
      LBO->hasNoUnsignedWrap() && LBO->hasNoUnsignedWrap();
    bool NSW =
      LBO->hasNoSignedWrap() && RBO->hasNoSignedWrap();
  ...
} 

The error was found through the V501 diagnostic: 在 '&&' 操作的左边和右边有个相同的子表达式 'LBO->hasNoUnsignedWrap ()'. LLVMAnalysis instructionsimplify.cpp 1891

这是一个使用相似变量名导致的打印错误。在第一行,LBO 和 RBO 都必须使用。正确代码:

 bool NUW = LBO->hasNoUnsignedWrap() && RBO->hasNoUnsignedWrap(); 

例 4. Notepad++ 项目. 不正确的状态测试

 bool _isPointXValid;
bool _isPointYValid;
...
bool isPointValid() {
  return _isPointXValid && _isPointXValid;
}; 

The error was found through the V501 diagnostic:在 '&&' 操作的左边和右边有个相同的子表达式 '_isPointXValid && _isPointXValid'

'_isPointXValid' 名被使用了两次。该函数实际返回的代码应是:"_isPointXValid && _isPointYValid".

例 5. StrongDC++ 项目. 未成功检查 \r\n.

 static void getContentLengthAndHeaderLength(...)
{
  ...
  while(line[linelen] != '\r' && line[linelen] != '\r')
  ...
} 

The error was found through the V501 diagnostic:  在 '&&' 操作的左右两边两个相同的子表达式 'line [linelen] != '\r''. miniupnpc miniupnpc.c 153

因为打印错误,字符 '\r' 被检查了两次。实际上字符 '\n' 也要被检查。

例 6. G3D Content Pak 项目. 右括号放错位置.

 bool Matrix4::operator==(const Matrix4& other) const {
  if (memcmp(this, &other, sizeof(Matrix4) == 0)) {
    return true;
  }
  ...
} 

The error was found through the V575 diagnostic: 'memcmp' 函数处理了 '0' 个元素。审查第三个参数。graphics3D matrix4.cpp 269

某个右括号放错位置。事实证明被比较的内存区域大小值通过"sizeof(Matrix4) == 0"表达计算了。该表达式总为 'false' 值。接着 'false' 转型为一个整型值 0 。正确的代码:

 if (memcmp(this, &other, sizeof(Matrix4)) == 0) { 

例 7. 项目. 错误的结构体成员拷贝。

 PassRefPtr<Structure>
Structure::getterSetterTransition(Structure* structure)
{
  ...
  transition->m_propertyStorageCapacity =
    structure->m_propertyStorageCapacity;
  transition->m_hasGetterSetterProperties =
    transition->m_hasGetterSetterProperties;
  transition->m_hasNonEnumerableProperties =
    structure->m_hasNonEnumerableProperties;
  transition->m_specificFunctionThrashCount =
    structure->m_specificFunctionThrashCount;
  ...
} 

The error was found through the V570 diagnostic: 'transition->m_hasGetterSetterProperties' 变量被赋给了自身. QtScript structure.cpp 512

查看该代码不容易发现错误。但它是存在的。‘m_hasGetterSetterProperties’ 被拷贝给了自身。以下是正确代码:

 transition->m_hasGetterSetterProperties =
  structure->m_hasGetterSetterProperties; 

例 8. Apache HTTP Server 项目. 多余的 sizeof 操作。

 PSECURITY_ATTRIBUTES GetNullACL(void)
{
  PSECURITY_ATTRIBUTES sa;
  sa  = (PSECURITY_ATTRIBUTES)
    LocalAlloc(LPTR, sizeof(SECURITY_ATTRIBUTES));
  sa->nLength = sizeof(sizeof(SECURITY_ATTRIBUTES));
  ...
} 

The error was found through the V568 diagnostic: 有个奇怪的 sizeof()  'sizeof (SECURITY_ATTRIBUTES)' 表达式参数. libhttpd util_win32.c 115

属性 'nLength' 必须包含'SECURITY_ATTRIBUTES'结构体大小。这是一个代码中的打印错误:'sizeof' 操作被使用了两次。因此,属性 'nLength' 保存了 'size_t' 类的大小。以下是正确代码:

 sa->nLength = sizeof(SECURITY_ATTRIBUTES); 

例 9. FCE Ultra 项目. 重复的变量声明

 int iNesSaveAs(char* name)
{
  ...
  fp = fopen(name,"wb");
  int x = 0;
  if (!fp)
    int x = 1;
  ...
} 

The error was found through the V561 diagnostic:將值赋给 'x' 可能会更好,而不是重新声明. Previous daclaration: ines.cpp, line 960. fceuxines.cpp 962

变量 'x' 必须保存信息,无论某文件是否成功打开。由于一个打印错误,一个新 'x' 变量被创建和初始化为 1 并取代了现有值。下面是正确代码:

 if (!fp)
  x = 1; 

例 10. Notepad++ 项目. 使用了 && 操作 替代 &.

 TCHAR GetASCII(WPARAM wParam, LPARAM lParam)
{
  ...
  result=ToAscii(wParam,
    (lParam >> 16) && 0xff, keys,&dwReturnedValue,0);
  ...
} 

The error was found through the V560 diagnostic: 条件表达式的某部分总为 true: 0xff. notepadPlus babygrid.cpp 694

"(lParam >> 16) && 0xff"表达式无意义,总是等于 1 (true)。这里的打印错误是使用了 '&&' 替代 '&'。

例 11. WinDjView 项目. 不完备的条件.

 inline bool IsValidChar(int c)
{
  return c == 0x9 || 0xA || c == 0xD || c >= 0x20 &&
         c <= 0xD7FF || c >= 0xE000 && c <= 0xFFFD ||
         c >= 0x10000 && c <= 0x10FFFF;
} 

The error was found through the V560 diagnostic:条件表达式的某部分总为 true: 0xA. WinDjView xmlparser.cpp 45 False

该IsValidChar函数总是返回 'true'。比较语义在某个部分丢失了,缘于一个打印错误:"... || 0xA || ...".

例 12. Fennec Media 项目. 多余的分号.

 int settings_default(void)
{
  ...
  for(i=0; i<16; i++);
    for(j=0; j<32; j++)
    {
      settings.conversion.equalizer_bands.boost[i][j] = 0.0;
      settings.conversion.equalizer_bands.preamp[i]   = 0.0;
    }
} 

The error was found through the V529 diagnostic: 'for' 操作后面有个多余的分号 ';'. settings.c 483

所有的 C 和 C++ 程序员都知道多余分号 ';'的危险性。不幸的是,该知识并为避免他们犯此类打印错误。这里有一个'for' 操作, 后面有多余的分号 ';',使该程序片段无法执行。

例 13. QT 项目. break 运算丢失.

 int QCleanlooksStyle::pixelMetric(...)
{
  ...
  case PM_SpinBoxFrameWidth:
    ret = 3;
    break;
  case PM_MenuBarItemSpacing:
    ret = 6;
  case PM_MenuBarHMargin:
    ret = 0;
    break;
  ...
} 

The error was found through the V519 diagnostic: 'ret'被成功赋值两次。这可能是个错误。检查行数: 3765, 3767. QtGui qcleanlooksstyle.cpp 3767

这是个经典的错误 - 'break' 在 'switch' 操作中丢失,我想你不需要任何评论了。

例 14. Miranda IM 项目. 赋值取代了比较

 int FindItem(...)
{
  ...
  int ret;
  ret=FindItem(hwnd,dat,hItem,
               (struct ClcContact ** )&z,
               (struct ClcGroup ** )&isv,NULL);
  if (ret=0) {return (0);}
  ...
} 

The error was found through the V559 diagnostic: 在 'if' 条件表达式中有一个可疑的赋值: ret = 0. clist_mw clcidents.c 179

这是个条件判断 'if' 操作符中的打印错误:'==' 被写成了 '='。当某个值不存在时,函数將会错误的处理该情形。

例 15. IPP Samples 项目. 错误的下标

 struct AVS_MB_INFO
{
  ...
  Ipp8u refIdx[AVS_DIRECTIONS][4];
  ...
};

void AVSCompressor::GetRefIndiciesBSlice(void){
  ...
  if (m_pMbInfo->predType[0] & predType)
  {
    m_refIdx[iRefNum] = m_pMbInfo->refIdx[dir][0];
    iRefNum += 1;
  }
  if (m_pMbInfo->predType[1] & predType)
  {
    m_refIdx[iRefNum] = m_pMbInfo->refIdx[dir][1];
    iRefNum += 1;
  }
  if (m_pMbInfo->predType[2] & predType)
  {
    m_refIdx[iRefNum] = m_pMbInfo->refIdx[dir][2];
    iRefNum += 1;
  }
  if (m_pMbInfo->predType[3] & predType)
  {
    m_refIdx[iRefNum] = m_pMbInfo->refIdx[dir][30];
    iRefNum += 1;
  }
  ...
} 

The error was found through the V557 diagnostic: 数组可能越界. 下标 '30' 已指向数组边界外. avs_enc umc_avs_enc_compressor_enc_b.cpp 495

考虑该片段: "m_pMbInfo->refIdx[dir][30]". 由于一个打印错误,数字 30 被写成了 3 .顺便说下,该例子很好的说明了我们是怎样相对的將错误分类的. 该错误也能很好的与 "数组和字符串处理的错误" 分类关联. 分类只是相对的, 并展示PVS-Studio 分析器能发现的多种错误.

例 16. ReactOS 项目. 宏定义中的打印错误

 #define SWAP(a,b,c)  c = a;\
                     a = b;\
                     a = c 

The error was found through the V519 diagnostic: 'v2'变量被成功赋值两次。这可能是个错误。检查行号: 343, 343. win32k gradient.c 343

这是个相当荒唐的宏定义打印错误,本意是想交换两个变量值。仔细观察代码,你就知道我的意思。下面是正确代码:

 #define SWAP(a,b,c)  c = a;\
                     a = b;\
                     b = c 

例 17. Quake-III-Arena 项目. 打印错误. 逗号取代了乘法操作。

 void Q1_AllocMaxBSP(void)
{
  ...
  q1_allocatedbspmem +=
    Q1_MAX_MAP_CLIPNODES * sizeof(q1_dclipnode_t);
  ...
  q1_allocatedbspmem +=
    Q1_MAX_MAP_EDGES , sizeof(q1_dedge_t);
  ...
  q1_allocatedbspmem +=
    Q1_MAX_MAP_MARKSURFACES * sizeof(unsigned short);
  ...
} 

The error has been found with rule V521: 该表达式使用 ',' 操作符是危险的. 请确保该表达式正确. bspc l_bsp_q1.c 136

它是个荒唐的打印错误。检查中间的代码行。',' 取代了 '*'。因此,'sizeof(q1_dedge_t)'值总是添加给‘q1_allocatedbspmem’。对于该打印错误將发生什么,我无从得知。

例 18. LibXml 项目. 错误打印 =+.

 static int 
xmlXPathCompOpEvalFirst(...)
{
  ...
  total += xmlXPathCompOpEvalFirst(...);
  ...
  total =+ xmlXPathCompOpEvalFilterFirst(ctxt, op, first);
  ...
} 

The error has been found with rule V588: 使用了表达式 'A =+ B'. 考虑复查它, 可能是 'A += B'的意思. libxml xpath.c 12676

在某个地方,"=+" 错误的替代了 "+="。它们看起来看相似,但结果会非常不同。这类错误非常难以发现,只能复查代码。

很多软件错误是由于打印错误导致的。还有比程序员认为的更多这此类错误。我们可以在本章中继续列出, 但我们决定在列举第18个例子后结束战斗。

不正确的使用基本函数和类

例 1. Fennec Media 项目. 需要两个末端 nulls.

 int JoiningProc(HWND hwnd,UINT uMsg,
  WPARAM wParam,LPARAM lParam)
{
  ...
  OPENFILENAME  lofn;
  memset(&lofn, 0, sizeof(lofn));
  ...
  lofn.lpstrFilter = uni("All Files (*.*)\0*.*");
  ...
} 

The error was found through the V540 diagnostic: 'lpstrFilter'成员应当使用两个 0 字符指向字符串末端. base windows.c 5309

在 Windows 的API中,某些结构体的字符串成员指针必须以两个 null 字符结束. OPENFILENAME 结构体的成员'lpstrFilter' 指向的是一个非同寻常的字符串. 

MSDN中对 'lpstrFilter' 的描述

LPCTSTR

A buffer containing pairs of null-terminated filter strings. The last string in the buffer must be terminated by two NULL characters.

一个包含成对的 null-terminated 过滤字符串缓冲区。该缓冲区中的最后一个字符串必须以两个 NULL 字符结尾。

如果你忘记在末端添加上另一个 null ,文件处理的对话框可能会包含一些在过滤属性中的垃圾数据。

 lofn.lpstrFilter = uni("All Files (*.*)\0*.*\0"); 

例 2. TortoiseSVN 项目. 错误的使用 'remove' 函数。

 STDMETHODIMP CShellExt::Initialize(....)
{
  ...
  ignoredprops = UTF8ToWide(st.c_str());
  // remove all escape chars ('\\')
  std::remove(ignoredprops.begin(), ignoredprops.end(), '\\');
  break;
  ...
} 

The error was found through the V530 diagnostic: 'remove' 的返回值需要被有效利用. contextmenu.cpp 442

std::remove函数不会將容器内的元素移除。它仅仅反转元素和返回一个指向垃圾数据开始端的迭代器。假设我们有一个 vector<int> 容器,包含元素 1,2,3,1,2,3,1,2,3。如果我们执行代码 "remove( v.begin(), v.end(), 2 )",该容器將包含元素 1,3,1,3,X,X,X,X是一些垃圾数据。该函数將返回一个指向第一个垃圾数据的迭代器,因此如果我们想移除这些垃圾元素,我们需要编写为: "v.erase(remove(v.begin(), v.end(), 2), v.end())".

例 3. TortoiseSVN 项目. 使用 'empty' 函数替代了 'clear'.

 CMailMsg& CMailMsg::SetFrom(string sAddress,
                            string sName)
{
   if (initIfNeeded())
   {
      // only one sender allowed
      if (m_from.size())
         m_from.empty();
      m_from.push_back(TStrStrPair(sAddress,sName));
   }
   return *this;
} 

The error was found through the V530 diagnostic: 'empty' 的返回值需要被有效利用. mailmsg.cpp 40

这里的错误是:vector::empty()函数被错误的调用而取代 vectory::clear(),而该数组的内容保存不变。这是一个常见的错误,因为单词 'clear' 和 'empty' 意思非常的接近。

例 4. WinMerge 项目. 使用了 'empty' 函数替代 'clear'。

 void CDirView::GetItemFileNames(int sel,
  String& strLeft, String& strRight) const
{
  UINT_PTR diffpos = GetItemKey(sel);
  if (diffpos == (UINT_PTR)SPECIAL_ITEM_POS)
  {
    strLeft.empty();
    strRight.empty();
  }
  else
  {
     ...
  }
} 

The error was found through the V530 diagnostic: 'empty' 的返回值需要被有效利用. WinMerge DirActions.cpp 1307, 1308

再一次,使用了 empty() 函数替代 clear()。我们还可以从其它项目中引用这类错误示例: InstantVNC, IPP Samples, Chromium, Intel AMT SDK等等。遗憾的是,所有的这些例子都相同,我没有兴趣去检查他们。但请相信我,你也会在由专业程序员开发的工业级项目中看到此类缺陷。

例 5. Pixie 项目. 在循环中使用 'alloca'.

 inline  void  triangulatePolygon(...) {
  ...
  for (i=1;i<nloops;i++) {
    ...
    do {
      ...
      do {
        ...
        CTriVertex  *snVertex =
         (CTriVertex *)alloca(2*sizeof(CTriVertex));
        ...
      } while(dVertex != loops[0]);
      ...
    } while(sVertex != loops[i]);
    ...
  }
  ...
} 

The error was found through the V505 diagnostic: 在循环中使用 'alloca'. 这会很快使用栈溢出. ri polygons.cpp 1120

alloca 函数是在栈中申请内存,因此如果在循环中多次调用它的话,可能会突然导致栈溢出。而我们这里有几个嵌套循环。该代码將很快的耗尽栈内存。

例 6. Miranda IM 项目. 多参数混淆

 static BOOL ImageArray_Alloc(LP_IMAGE_ARRAY_DATA iad, int size)
{
  ...
  memset(&iad->nodes[iad->nodes_allocated_size], 
    (size_grow - iad->nodes_allocated_size) *
       sizeof(IMAGE_ARRAY_DATA_NODE),
    0);
  ...
} 

The error was found through the V575 diagnostic:  函数接收一个多余的参数. clist_modern modern_image_array.cpp 59

'memset'函数处理 0 个无素,实际什么也没做。原因是参数混淆了。下面是调用 memset 函数的正确写法:

memset(&iad->nodes[iad->nodes_allocated_size],
  0,
  (size_grow - iad->nodes_allocated_size) *
     sizeof(IMAGE_ARRAY_DATA_NODE));

无效代码示例

例 1.IPP Samples 项目. 不完备的条件

 void lNormalizeVector_32f_P3IM(Ipp32f *vec[3],
  Ipp32s* mask, Ipp32s len)
{
  Ipp32s  i;
  Ipp32f  norm;

  for(i=0; i<len; i++) {
    if(mask<0) continue;
    norm = 1.0f/sqrt(vec[0][i]*vec[0][i]+
             vec[1][i]*vec[1][i]+vec[2][i]*vec[2][i]);
    vec[0][i] *= norm; vec[1][i] *= norm; vec[2][i] *= norm;
  }
} 

The error was found through the V503 diagnostic: 一个无意义的比较:pointer < 0 .ipprsample ippr_sample.cpp 501

I do not know how it happened, but there are 3 characters "[i]" missing in this code. As a result, the code performs a meaningless check that the pointer is below zero instead of checking the mask array.

我不知道它会发生什么,但该代码有 3 个字符 "[i]" 丢失了。因此,该代码执行了个无效的检查,指针为0 而不是检查 mask 数组。

正确的检查应该写成这样:if(mask[i] < 0).

例 2. Pc Ps2 Emulator 项目. 无效分支

 LRESULT CALLBACK IOP_DISASM(...)
{
  ...
  switch(LOWORD(wParam))
  {
    case (IDOK || IDCANCEL):
      EndDialog(hDlg,TRUE);
      return(TRUE);
      break;
  }
  ...
} 

The error was found through the V560 diagnostic: 条件表达式的某部分总为 true. pcsx2 debugger.cpp 321

该代码没有任何意义。程序员一定是想写成这样的方式:

 switch(LOWORD(wParam))
{
  case IDOK: //no break
  case IDCANCEL:
    EndDialog(hDlg,TRUE);
    return(TRUE);
    break;
} 

例 3. CPU Identifying Tool 项目. 一个太严格的条件

 void projillum(short* wtab, int xdots, int ydots, double dec)
{
  ...
  s = sin(-dtr(dec));
  x = -s * sin(th);
  y = cos(th);
  ...
  lon = (y == 0 && x == 0) ? 0.0 : rtd(atan2(y, x));
} 

The error was found through the V550 diagnostic: 一个多余的条件比较:x == 0. 可能更好的比较方式是:fabs(A - B) '<'. Epsilon. clock_dll sunalgo.cpp 155

 令人奇怪的是,经过使用 'sin' 和 'cos' 这些复杂的运算函数, 最终的期望结果是 0. 可能这里必须要执行某个精确值的比较.

例 4. Lugaru 项目. 重复赋值

 int Game::DrawGLScene(void)
{ 
  ...
  radius=fast_sqrt(maxdistance);
  radius=110;
  ...
} 

The error was found through the V519 diagnostic: 'radius' 对象被成功的赋值了两次. 这可能是个错误. Lugaru gamedraw.cpp 1505

程序员一定是为了试验, 故意的给 'radius' 写入 110, 然后忘记移除该行了. 因此,我们得到一个无用的甚至可能是无效的代码.

例 5. QT 项目. 重复检查。

 Q3TextCustomItem* Q3TextDocument::parseTable(...)
{
  ...
  while (end < length
    && !hasPrefix(doc, length, end, QLatin1String("</td"))
    && !hasPrefix(doc, length, end, QLatin1String("<td"))
    && !hasPrefix(doc, length, end, QLatin1String("</th"))
    && !hasPrefix(doc, length, end, QLatin1String("<th"))
    && !hasPrefix(doc, length, end, QLatin1String("<td"))
    && !hasPrefix(doc, length, end, QLatin1String("</tr"))
    && !hasPrefix(doc, length, end, QLatin1String("<tr"))
    && !hasPrefix(doc, length, end, QLatin1String("</table"))) {

  ...
} 

The error was found through the V501 diagnostic: 在 '&&' 操作的左边和右边有个相同的子表达式. Qt3Support q3richtext.cpp 6978

"<td" 前缀在条件中被检查了两次。这是无意义的。可能这是额外的检查或者需要用其它前缀代替第二个 "<td"。

例 6. Audacity 项目. 怪异的检查.

 int sf_error (SNDFILE *sndfile)
{
  ...
  if (!sndfile)
  {
    if (sf_error != 0)
      return sf_errno;
    return 0;
  } ;
  ...
} 

The error was found through the V516 diagnostic: 考虑审核一个奇怪的表达式. 非空函数指针被用于与 null 比较:'sf_error != 0'。

"sf_error != 0" 检查总是返回 true,因为被执行的代码里 'sf_error' 是函数的名字.

例 7. IPP Samples 项目. 循环中怪异的代码

 static IppStatus mp2_HuffmanTableInitAlloc(Ipp32s *tbl, ...)
{
  ...
  for (i = 0; i < num_tbl; i++) {
    *tbl++;
  }
  ...
} 

The error was found through the V532 diagnostic: 考虑审核一个奇怪的模式 '*pointer++' .可能意思是:'(*pointer)++'. mpeg2_dec umc_mpeg2_dec.cpp 59

循环体可能不完备因为在循环体中它是无效的.


总为 true 或 false 的条件


这是一类非常广泛的错误类型。这些错误也很依赖于严重程度。不那么危险的错误,我们可能只是用ASSERT做不正确的断言,实际上并未检查任何东西。而对于危险的错误,是那些不正确的缓冲区检查或下标指向。

例 1. Shareaza 项目. char类型的范围

 void CRemote::Output(LPCTSTR pszName)
{

  ...
  CHAR* pBytes = new CHAR[ nBytes ];
  hFile.Read( pBytes, nBytes );
  ...
  if ( nBytes > 3 && pBytes[0] == 0xEF &&
       pBytes[1] == 0xBB && pBytes[2] == 0xBF )
  {
    pBytes += 3;
    nBytes -= 3;
    bBOM = true;
  }
  ...
} 

The error was found through the V547 diagnostic: 表达式 'pBytes [ 0 ] == 0xEF' 总为 false。无符号 char 类型的范围是: [-128, 127].

In this code, the 'TCHAR' type is the 'char' type. The value range of char is from -128 to 127 inclusive. Value 0xEF in the variable of the char type is nothing else than number -17. When comparing the char variable with number 0xEF, its type is extended up to the 'int' type. But the value still lies inside the range [-128..127]. The "pBytes[0] == 0xEF" ("-17 == 0xEF") condition is always false, and the program does not work as intended.

在该代码中,'TCHAR' 是 'char' 类型。char的值范围从 -128 到 127 。值0xEF 在char 类型中只不过是 -17。当比较 char 变量与数字 0xEF时,它的类型被向上转型为 'int' 类型。但该值仍处于 [-128..12]区间中。"pBytes[0] == 0xEF" ("-17 == 0xEF")条件总为 false,程序不会按照意图那样工作。

下面是正确的比较:

 if ( nBytes > 3 && pBytes[0] == TCHAR(0xEF) &&
                   pBytes[1] == TCHAR(0xBB) &&
                   pBytes[2] == TCHAR(0xBF) ) 

例 2. TortoiseSVN 项目. char类型的范围。

 BOOL TortoiseBlame::OpenFile(const TCHAR *fileName)
{
  ...
  // check each line for illegal utf8 sequences.
  // If one is found, we treat
  // the file as ASCII, otherwise we assume
  // an UTF8 file.
  char * utf8CheckBuf = lineptr;
  while ((bUTF8)&&(*utf8CheckBuf))
  {
    if ((*utf8CheckBuf == 0xC0)||
        (*utf8CheckBuf == 0xC1)||
        (*utf8CheckBuf >= 0xF5))
    {
      bUTF8 = false;
      break;
    }

   ...
  }
  ...
} 

The error was found through the V547 diagnostic: 表达式 '* utf8CheckBuf == 0xC0' 总为 false. 无符号 char 类型的范围是: [-128, 127]. tortoiseblame.cpp 310

While the defect in the previous example seems to be caused through mere inattention, in this case it is not so. Here is another identical example where a condition is always false. This is a very widely-spread type of errors in various projects.

上个例子看起是由于粗心导致, 但这里并非如此。这是另一个相同的例子,条件总为 false。这是一个非常广泛的错误类型见于各类项目中。

例 3. VirtualDub 项目. 无符号类型总是 >=0.

 typedef unsigned short wint_t;
...
void lexungetc(wint_t c) {
  if (c < 0)
    return;
   g_backstack.push_back(c);
} 

The error was found through the V547 diagnostic: 表达式 'c < 0' 总为 false. 无效号类型的值不可能 < 0. Ami lexer.cpp 225

"c < 0" 条件总为 false ,因为无符号数的变量总是大于或等于0.

例 4.  Swiss-Army Knife of Trace 项目. Socket处理.

 static UINT_PTR m_socketHandle;

void TTrace::LoopMessages(void) 
{
  ...
  // Socket creation
  if ( (m_socketHandle = socket(AF_INET,SOCK_STREAM,0)) < 0)
  {
    continue;
  }
  ...
} 

The error was found through the V547 diagnostic: 表达式 '(m_socketHandle = socket (2, 1, 0)) < 0' 总为 false。无符号类型的值从不 <0 . Vs8_Win_Lib tracetool.cpp 871

一个企图检查socket是否成功创建被错误的执行了. 如果socket无法创建,这种情况没有被任何方式处理。为使检查工作正确,我们应使用

INVALID_SOCKET 常量:

 m_socketHandle = socket(AF_INET,SOCK_STREAM,0); 

例 5. Chromium 项目. 时间处理

 IdleState CalculateIdleState(...) {
  ...
  DWORD current_idle_time = 0;
  ...
  // Will go -ve if we have been idle for
  // a long time (2gb seconds).
  if (current_idle_time < 0)
    current_idle_time = INT_MAX;
  ...
} 

The error was found through the V547 diagnostic: 表达式 'current_idle_time < 0' 总为 false. 无符号类型的值从不 <0. 浏览 idle_win.cc 23

为处理时间,使用了个无符号类型变量。因此,太大的值检查不能工作。这是正确代码:

 if (current_idle_time > INT_MAX)
  current_idle_time = INT_MAX; 

例 6. ICU 项目. 条件出错

 U_CDECL_BEGIN static const char* U_CALLCONV
_processVariableTop(...)
{
  ...
  if(i == locElementCapacity &&
     (*string != 0 || *string != '_'))
  {
    *status = U_BUFFER_OVERFLOW_ERROR;
  }
  ...
} 

The error was found through the V547 diagnostic: 表达式 "*string != 0 || *string != '_'" 总为 true. 可能这里应使用 '&&' 操作符. icui18n ucol_sit.cpp 242

条件包含了逻辑错误。"(*string != 0 || *string != '_')" 子表达式总为 true。同一个string字符不可能同时等于 '0' 和 '_'。

例 7. QT 项目. 危险的循环

 bool equals( class1* val1, class2* val2 ) const{
{
  ...
  size_t size = val1->size();
  ...
  while ( --size >= 0 ){
    if ( !comp(*itr1,*itr2) )
      return false;
    itr1++;
    itr2++;
  }
  ...
} 

The error was found through the V547 diagnostic: 表达式 '--size >= 0' 总为 true. 无符号类型的值总是 >=0. QtCLucene arrays.h 154

The (--size >= 0) condition is always true, since the size variable has the unsigned type. It means that if two sequences being compared are alike, we will get an overflow that will in its turn cause Access Violation or other program failures.

(--size >= 0) 条件总为 true,既然变量的大小是无符号类型. 意味着, 如果两个相同序列被比较,我们將得到溢出, 它將导致Access Violation 或其它的软件错误.

这是正确代码:

 for (size_t i = 0; i != size; i++){
  if ( !comp(*itr1,*itr2) )
    return false;
  itr1++;
  itr2++;
} 

例 8. MySQL 项目. 条件出错

 enum enum_mysql_timestamp_type
str_to_datetime(...)
{
  ...
  else if (str[0] != 'a' || str[0] != 'A')
    continue; /* Not AM/PM */
  ...
} 

The error was found through the V547 diagnostic:  表达式 "str [0] != 'a' || str [0] != 'A'" 总为 true. 可能这里应该用 '&&' 操作符. clientlib my_time.c 340

条件总为 true,因此字符即不等于 'a' 也不等于 'A'.这是正确代码:

 else if (str[0] != 'a' && str[0] != 'A') 

例 9. QT 项目. 不正确的引用计数

 STDMETHODIMP QEnumPins::QueryInterface(const IID &iid,void **out)
{
  ...
  if (S_OK)
    AddRef();
  return hr;
} 

The error was found through the V545 diagnostic: Such conditional expression of 'if' operator is incorrect for the HRESULT type value '(HRESULT) 0L'. The SUCCEEDED or FAILED macro should be used instead. phonon_ds9 qbasefilter.cpp 60

这个 'if' 表达式操作是不正确的,因为是HRESULT 类型的值 value '(HRESULT) 0L'. 应该用 SUCCESSED 或 FAILED 宏指令替换.

检查被 S_OK 代表了. 既然 S_OK 是 0,AddRef()函数將会从未被调.正确的检查必须是这样:if (hr == S_OK).

例 10 . TickerTape 项目. Incorrect tornado.

 void GetWindAtSingleTornado(...)
{
  ...
  if(radius < THRESH * 5)
      *yOut = THRESH * 10 / radius;
  else if (radius < THRESH * 5)
      *yOut = -3.0f / (THRESH * 5.0f) * 
             (radius - THRESH * 5.0f) + 3.0f;
  else
      *yOut = 0.0f;
  ...
} 

The error was found through the V517 diagnostic:一个使用了 'if (A) {...} else if (A) {...}' 的模式被查出. 这可能有一个逻辑错误. TickerTape wind.cpp 118

第二个条件总为 false. 原因是第一个条件与第二个条件恰恰相同. 这肯定是个打印错误.

例 11. Apache HTTP Server 项目. Windows中的 socket 处理错误

 typedef UINT_PTR SOCKET;

static unsigned int __stdcall win9x_accept(void * dummy)
{
  SOCKET csd;
  ...
  do {
      clen = sizeof(sa_client);
      csd = accept(nsd, (struct sockaddr *) &sa_client, &clen);
  } while (csd < 0 && APR_STATUS_IS_EINTR(apr_get_netos_error()));
  ...
} 

The error was found through the V547 diagnostic: 表达式 'csd < 0' 总为 false. 无符号类型值从不 <0. libhttpd child.c 404

Socket handling errors very often emerge in crossplatform programs built under Windows. In Linux, socket descriptors are represented by the signed type, while in Windows it is the unsigned type. Programmers often forget about this and check the error status by comparing the value to 0. This is incorrect; you must use specialized constants.

构建于Windows平台下的Socket错误处理常常破坏程序的跨平台性. 在Linux中,socket描述符常常使用有符号类型代表,而在Windows中是无符号类型.程序员们常常忘记,而常常使用 =0 判断错误状态.这是不正确的;你必须使用特殊的常量.

例 12. QT 项目. 比较中的打印错误

 QStringList ProFileEvaluator::Private::values(...)
{
  ...
  else if (ver == QSysInfo::WV_NT)
    ret = QLatin1String("WinNT");
  else if (ver == QSysInfo::WV_2000)
    ret = QLatin1String("Win2000");
  else if (ver == QSysInfo::WV_2000)  <<--
    ret = QLatin1String("Win2003");
  else if (ver == QSysInfo::WV_XP)
    ret = QLatin1String("WinXP");
  ...
} 

The error was found through the V517 diagnostic: 被查出使用了 'if (A) {...} else if (A) {...}' 模式. 这可能是个逻辑错误. 查检行号: 2303, 2305. lrelease profileevaluator.cpp 2303

字符串中我们已标记了它必须是 "ver == QSysInfo::WV_2003" .由于该错误,"ret = QLatin1String("Win2003")"块將会从不被执行.

代码漏洞

当然, 代码漏洞实际由打印错误, 不正确的条件和不正确的数组操作导致. 但我们决定特别的分类指出这些错误, 因为它们与软件漏洞相关. 入侵者,利用这些错误,能试图破坏程序执行, 获取特别权限执行攻击或任何他/她需要的操作.

例 1. Ultimate TCP/IP 项目. 不正确的检查空字符串

 char *CUT_CramMd5::GetClientResponse(LPCSTR ServerChallenge)
{
  ...
  if (m_szPassword != NULL)
  {
    ...
    if (m_szPassword != '\0')
    {
  ...
} 

The error was found through the V528 diagnostic: 奇怪的 'char' 类型指针与 '\0' 比较.  可能意思是: *m_szPassword != '\0'. UTMail ut_crammd5.cpp 333

该代码片断必须检查指向 password 的指针不为 NULL 且该字符串不为空.但替代,该代码对指针不为 NULL 检查了两次. s字符串的检查无法工作. "if (m_szPassword != '\0')" 条件意图检查在字符串最开始处是否有一个终止符 null, 意思是字符串为空. 但这里缺少指针解引用操作,仅仅是指针自己和 0 比较. 下面是正确代码:

 if (m_szPassword != NULL)
{
  ...
  if (*m_szPassword != '\0') 

例 2. Chromium 项目. 空指针操作.

 bool ChromeFrameNPAPI::Invoke(...)
{
  ChromeFrameNPAPI* plugin_instance =
    ChromeFrameInstanceFromNPObject(header);
  if (!plugin_instance &&
      (plugin_instance->automation_client_.get()))
    return false;
  ...  
} 

The error was found through the V522 diagnostic: 可能发生对空指针 plugin_instance 解引用. 请检查逻辑条件. chrome_frame_npapi chrome_frame_npapi.cc 517

检查空指针的条件被不正确的编写. 因此,我们得到了一个 segmentation error. 下面是正确代码:

 if (plugin_instance &&
    (plugin_instance->automation_client_.get()))
  return false; 

例 3. SMTP Client with SSL/TLS 项目. 不完全缓冲区清理.

 void MD5::finalize () {
  ...
  uint1 buffer[64];
  ...
  // Zeroize sensitive information
  memset (buffer, 0, sizeof(*buffer));
  ...
} 

The error was found through the V512 diagnostic: 'memset' 函数的调用將导致缓冲区上溢或下溢. CSmtp md5.cpp 212

出于安全考虑,该函数试图清理包含些敏感信息的缓冲. 但它失败了.仅仅缓冲的第一个字节被清除.错误是: 'sizeof' 操作计算了 'uint1' 类型的大小而非缓冲大小. 这是正确代码:

 memset (buffer, 0, sizeof(buffer)); 

通常,不完全的内存清理常常发生. 请认真考虑其它的类似情况.

例 4. Chromium. 不完全缓冲区清理.

 void Time::Explode(..., Exploded* exploded) const {
  ...
  ZeroMemory(exploded, sizeof(exploded));
  ...
} 

The error was found through the V512 diagnostic: 'memset' 函数的调用將导致缓冲区上溢或下溢. base time_win.cc 227

ZeroMemory 函数仅仅清理Exploded结构体的一部分.原因是 'sizeof' 操作符返回的是指针大小.修复该错误,我们必须解引用指针:

 ZeroMemory(exploded, sizeof(*exploded)); 

例 5. Apache HTTP Server 项目. 不完全缓冲区清理.

 #define MEMSET_BZERO(p,l)       memset((p), 0, (l))

void apr__SHA256_Final(..., SHA256_CTX* context) {
  ...
  MEMSET_BZERO(context, sizeof(context));
  ...
} 

The error was found through the V512 diagnostic: 'memset' 函数的调用將导致缓冲区 '(context)' 上溢或下溢. apr sha2.c 560

该错误与前例完全相同. 'sizeof' 计算的是指针大小.改正他,我们必须写:"sizeof(*context)".

例 6. Miranda IM 项目. 不正确的字符串处理

 static char *_skipblank(char * str)
{
  char * endstr=str+strlen(str);
  while ((*str==' ' || *str=='\t') && str!='\0') str++;
  while ((*endstr==' ' || *endstr=='\t') &&
         endstr!='\0' && endstr<str)
    endstr--;
  ...
} 

The error was found through the diagnostics V528: 奇怪的 'char' 类型指针与 '\0' 比较. 可能意思是:*str != '\0'. clist_modern modern_skinbutton.cpp 282

该代码非常危险,因为它不正确的定义了字符串结尾. 它可能导致字符串溢出,以一个 Access Violation 异常为代价. 错误在这里:"str!='\0'" 和这里:"endstr!='\0'". 指针解引用丢失了. 这是正确代码:

 while ((*str==' ' || *str=='\t') && *str!='\0') str++;
while ((*endstr==' ' || *endstr=='\t') &&
       *endstr!='\0' && endstr<str)
  endstr--; 

例 7. PNG library 项目. 意外的指针清除

 png_size_t
png_check_keyword(png_structp png_ptr, png_charp key,
  png_charpp new_key)
{
  ...
  if (key_len > 79)
  {
    png_warning(png_ptr, "keyword length must be 1 - 79 characters");
    new_key[79] = '\0';
    key_len = 79;
  }
  ...
} 

The error was found through the V527 diagnostic:奇怪的將 '\0' 赋给一个 'char' 类型的指针. 可能意思:*new_key [79] = '\0'. graphics3D pngwutil.c 1283

该例子展示了一种错误,当程序员不小心清理了指针而不是截断字符串长度. 'new_key' 是指向字符串的指针. 这意味着我们应该像下面这样编写我们的代码,將它截断到 79 个字符:

 (*new_key)[79] = '\0'; 

例 8. Intel AMT SDK 项目. 未验证用户名.

 static void
wsman_set_subscribe_options(...)
{
  ...
  if (options->delivery_certificatethumbprint ||
     options->delivery_password ||
     options->delivery_password) {
  ...
} 

The error was found through the V501 diagnostic: 在 '||' 操作符的左边和右边有两个相同的子表达式 'options->delivery_password'. OpenWsmanLib wsman-client.c 631

由于开发者的粗心,password 被检查了两次,而用户名未被检查.这是正确代码:

 if (options->delivery_certificatethumbprint ||
   options->delivery_username ||
   options->delivery_password) { 

例 9. Ultimate TCP/IP 项目. 不正确的处理空字符串.

 void CUT_StrMethods::RemoveCRLF(LPSTR buf)
{
  // v4.2 changed to size_t
  size_t  len, indx = 1;
  if(buf != NULL){
    len = strlen(buf);
    while((len - indx) >= 0 && indx <= 2) {
      if(buf[len - indx] == '\r' ||
         buf[len - indx] == '\n')
         buf[len - indx] = 0;
      ++indx;
    }
  }
} 

The error was found through the V547 diagnostic: 表达式 '(len - indx) >= 0' 总为 true.无符号类型总是 >=0 . UTDns utstrlst.cpp 58

"len - indx" 表达式是无符号类型 'size_t' , 它总是 >=0 . 让我们看看如果发送个空字符串,会返回什么.

如果字符串为空,则:len = 0, indx = 1.

len - indx 表达式等于 0xFFFFFFFFu.

既然 0xFFFFFFFFu > 0 而 indx <=2, 一个数组访问被执行

"buf[len - indx]".

"buf[0xFFFFFFFFu]" 操作將导致 Access Violation.

例 10. Miranda IM 项目. 溢出保护不起作用

 void Append( PCXSTR pszSrc, int nLength )
{
  ...
  UINT nOldLength = GetLength();
  if (nOldLength < 0)
  {
    // protects from underflow
    nOldLength = 0;
  }
  ...
} 

The error was found through the V547 diagnostic: 表达式 'nOldLength < 0' 总为 true. 无符号数从不 <0 . IRC mstring.h 229

"if (nOldLength < 0)"的检查不起作用, 因为 nOldLength 是无符号类型.

例 11. Apache HTTP Server 项目. 不正确的处理负数.

 typedef  size_t      apr_size_t;
APU_DECLARE(apr_status_t) apr_memcache_getp(...)
{
  ...
  apr_size_t len = 0;
  ...
  len = atoi(length);
  ...
  if (len < 0) {
    *new_length = 0;
    *baton = NULL;
  }
  else {
    ...  
  }
} 

The error was found through the V547 diagnostic:表达式 'len < 0' 总为 false. 无符号数从不 <0 . aprutil apr_memcache.c 814

"if (len < 0)" 检查不起作用, 'len' 变量是无符号类型.

例 12. Ultimate TCP/IP 项目. 不正确的循环终止条件

 void CUT_StrMethods::RemoveSpaces(LPSTR szString) {
  ...
  size_t loop, len = strlen(szString);
  // Remove the trailing spaces
  for(loop = (len-1); loop >= 0; loop--) {
    if(szString[loop] != ' ')
      break;
  }
  ...
} 

The error was found through the V547 diagnostic: 表达式 'loop >= 0' 总为 true. 无符号数总是 >=0. UTDns utstrlst.cpp 430

Suppose the whole string consists only of spaces. While searching the characters, the program will reach the null item of the string, and the 'loop' variable will equal to zero. Then it will be decremented once again. Since this variable is of unsigned type, its value will be 0xFFFFFFFFu or 0xFFFFFFFFFFFFFFFFu (depending on the architecture). This value is 'naturally >= 0', and a new loop iteration will start. There will be an attempt of memory access by szString[0xFFFFFFFFu] address - the consequences of this are familiar to every C/C++ programmer.

假设整个字符串只包含空格. 当搜索字符的时候,程序到达字符串的 null 元素,并且 'loop' 变量等于 0. 然后再次递减. 既然变量是无符号类型,它的值將是 0xFFFFFFFFu 或 0xFFFFFFFFFFFFFFFFu (依赖于体系结构).该表达式为 'naturally >= 0',这将会启动一个新循环. 有个操作试图通过szString[0xFFFFFFFFu]地址的内存访问 -这样的后果是每一个C/C++程序员所熟知的.

例 13. Crypto++ 项目. 基本类型数据清除错误.

 void CAST256::Base::UncheckedSetKey(const byte *userKey,
  unsigned int keylength, const NameValuePairs &)
{
  AssertValidKeyLength(keylength);
  word32 kappa[8];
  ...
  memset(kappa, 0, sizeof(kappa));
} 

The error has been found with rule V597: 编译器可以删除 'memset' 函数调用, 这是用来清洗 'kappa' 缓冲区.RtlSecureZeroMemory() 函数应被用来擦除基本类型数据. cryptlib cast.cpp 293

问题出在 memset() 函数中. 传递给函数的参数是正确的. 如果开发者在调试器中查看该代码的Debug版本如何工作的话, 他/她也不会注意到这类麻烦. 该错误发生在项目的发布版中. 应该被清除的数据被留在了内存中. 原因是编译器有权在优化阶段删除 memset() 函数的调用, 这就是它做的事. 如果你想知道为什么会发生,请阅读该文章"Overwriting memory - why?".


复制粘贴


开发者们不应该低估复制粘贴的错误, 而认为他也是一种普通的打印错误. 它们数量巨大. 程序员花费很多时间来调试它们.

当然,打印错误和复制粘贴很相似, 但也有不同, 造成我们在本文中將它们放到了不同的分组. 打印错误常常由于使用错误的变量取代所需的变量. 而在 copy-paste 的情形中, 开发者们是忘记编辑复制粘贴的行了.

例 1. Fennec Media 项目. 错误处理数组元素

 void* tag_write_setframe(char *tmem,
  const char *tid, const string dstr)
{
  ...
  if(lset)
  {
    fhead[11] = '\0';
    fhead[12] = '\0';
    fhead[13] = '\0';
    fhead[13] = '\0';
  }
  ...
} 

The error was found through the V525 diagnostic: 代码包含了一系列相似的块. 在 716, 717, 718, 719 行中查检元素. id3 editor.c 716

四个相似的行通过复制粘贴的方法必须出现在代码中. 当开发者开始编程下标的时候,他/她犯了个错误导致 0 被写入 'fhead[13]' 两次而没被写到 'fhead[14]'中.

例 2. MySQL 项目. 操作数组元素出错

 static int rr_cmp(uchar *a,uchar *b)
{
  if (a[0] != b[0])
    return (int) a[0] - (int) b[0];
  if (a[1] != b[1])
    return (int) a[1] - (int) b[1];
  if (a[2] != b[2])
    return (int) a[2] - (int) b[2];
  if (a[3] != b[3])
    return (int) a[3] - (int) b[3];
  if (a[4] != b[4])
    return (int) a[4] - (int) b[4];
  if (a[5] != b[5])
    return (int) a[1] - (int) b[5];
  if (a[6] != b[6])
    return (int) a[6] - (int) b[6];
  return (int) a[7] - (int) b[7];
} 

The error was found through the V525 diagnostic: 代码包含了一系列相似的代码块. 在 680, 682, 684, 689, 691, 693, 695 行检查元素 '1', '2', '3', '4', '1', '6'. sql records.cc 680

咋看之下不那么明显,让我们指出来:

 return (int) a[1] - (int) b[5]; 

实际上应该是下面的代码:

 return (int) a[5] - (int) b[5]; 

例 3. TortoiseSVN 项目. 文件名不正确

 BOOL GetImageHlpVersion(DWORD &dwMS, DWORD &dwLS)
{
  return(GetInMemoryFileVersion(("DBGHELP.DLL"),
                                dwMS,
                                dwLS)) ;
}

BOOL GetDbgHelpVersion(DWORD &dwMS, DWORD &dwLS)
{
  return(GetInMemoryFileVersion(("DBGHELP.DLL"),
                                dwMS,
                                dwLS)) ;
} 

The error was found through the V524 diagnostic: 很奇怪 'GetDbgHelpVersion' 函数与 'GetImageHlpVersion' (SymbolEngine.h) line 98 函数完全相同. symbolengine.h 105

'GetImageHlpVersion' 函数肯定是通过复制粘贴 'GetInMemoryFileVersion' 函数出现的. 错误是: 程序员在被复制粘贴函数中忘记修改文件名了. 下面是正确代码:

 BOOL GetImageHlpVersion(DWORD &dwMS, DWORD &dwLS)
{
  return(GetInMemoryFileVersion(("IMAGEHLP.DLL"),
                                dwMS,
                                dwLS)) ;
} 

例 4. Clang 项目. 函数体相同

 MapTy PerPtrTopDown;
MapTy PerPtrBottomUp;

void clearBottomUpPointers() {
  PerPtrTopDown.clear();
}

void clearTopDownPointers() {
  PerPtrTopDown.clear();
} 

The error was found through the V524 diagnostic: 很奇怪, 'clearTopDownPointers' 函数体和 'clearBottomUpPointers'(ObjCARC.cpp, line 1318) 函数体完全一样. LLVMScalarOpts objcarc.cpp

clearBottomUpPointers 函数看起来不正确;该函数应写成下面这样:

 void clearBottomUpPointers() {
  PerPtrBottomUp.clear();
} 

例 5. QT 项目. 不成功的交换.

 bool qt_testCollision(...)
{
  ...
  t=x1; x1=x2; x2=t;
  t=y1; x1=y2; y2=t;
  ...
} 

The error was found through the V519 diagnostic: 'x1' 变量被成功赋值了两次.可能这是个错误. 检查行号: 2218, 2219. Qt3Support q3canvas.cpp 2219

第一行是绝对正确的, 并交换了 x1 和 x2 变量的值. 在第二行, x1 和 x2 必须被交换. 该行可以能是前一行的拷贝.所有的 'x' 字母需要被替换为 'y'.不幸的是,程序员忘记做了:"... x1=y2; ...".

正确代码:

 t=x1; x1=x2; x2=t;
t=y1; y1=y2; y2=t; 

例 6. Crystal Space 3D SDK. 子表达式相同.

 inline_ bool Contains(const LSS& lss)
{
  return Contains(Sphere(lss.mP0, lss.mRadius)) &&
         Contains(Sphere(lss.mP0, lss.mRadius));
} 

The error was found through the V501 diagnostic:在 '&&' 操作符的两边有个相同的子表达式. plgcsopcode icelss.h 69

错误是:'lss.mP0.'值被使用了两次.表达式的第一部份必须是 'lss.mP1'.

例 7. Notepad++ 项止. 不正确的风格设置.

 void KeyWordsStyleDialog::updateDlg() 
{
  ...
  Style & w1Style =
    _pUserLang->_styleArray.getStyler(STYLE_WORD1_INDEX);
  styleUpdate(w1Style, _pFgColour[0], _pBgColour[0],
    IDC_KEYWORD1_FONT_COMBO, IDC_KEYWORD1_FONTSIZE_COMBO,
    IDC_KEYWORD1_BOLD_CHECK, IDC_KEYWORD1_ITALIC_CHECK,
    IDC_KEYWORD1_UNDERLINE_CHECK);

  Style & w2Style =
    _pUserLang->_styleArray.getStyler(STYLE_WORD2_INDEX);
  styleUpdate(w2Style, _pFgColour[1], _pBgColour[1],
    IDC_KEYWORD2_FONT_COMBO, IDC_KEYWORD2_FONTSIZE_COMBO,
    IDC_KEYWORD2_BOLD_CHECK, IDC_KEYWORD2_ITALIC_CHECK,
    IDC_KEYWORD2_UNDERLINE_CHECK);

  Style & w3Style =
    _pUserLang->_styleArray.getStyler(STYLE_WORD3_INDEX);
  styleUpdate(w3Style, _pFgColour[2], _pBgColour[2],
    IDC_KEYWORD3_FONT_COMBO, IDC_KEYWORD3_FONTSIZE_COMBO,
    IDC_KEYWORD3_BOLD_CHECK, IDC_KEYWORD3_BOLD_CHECK,
    IDC_KEYWORD3_UNDERLINE_CHECK);

  Style & w4Style =
    _pUserLang->_styleArray.getStyler(STYLE_WORD4_INDEX);
  styleUpdate(w4Style, _pFgColour[3], _pBgColour[3],
    IDC_KEYWORD4_FONT_COMBO, IDC_KEYWORD4_FONTSIZE_COMBO,
    IDC_KEYWORD4_BOLD_CHECK, IDC_KEYWORD4_ITALIC_CHECK,
    IDC_KEYWORD4_UNDERLINE_CHECK);
  ...
} 

The error was found through the V525 diagnostic: 代码包含了一系列相似的代码块. 检查元素 '7', '7', '6', '7' 在 576, 580, 584, 588 行.

肉眼几乎很难发现错误, 让我们缩减文本挑选感兴趣的片段:

 styleUpdate(...
  IDC_KEYWORD1_BOLD_CHECK, IDC_KEYWORD1_ITALIC_CHECK,
  ...);
styleUpdate(...
  IDC_KEYWORD2_BOLD_CHECK, IDC_KEYWORD2_ITALIC_CHECK,
  ...);
styleUpdate(...
  IDC_KEYWORD3_BOLD_CHECK, IDC_KEYWORD3_BOLD_CHECK, <<--
  ...);
styleUpdate(...
  IDC_KEYWORD4_BOLD_CHECK, IDC_KEYWORD4_ITALIC_CHECK,
  ...); 

出错了, IDC_KEYWORD3_BOLD_CHECK被用来取代IDC_KEYWORD3_ITALIC_CHECK了.

例 8. ReactOS 对象. 选择了错误的对象类型

 void CardButton::DrawRect(HDC hdc, RECT *rect, bool fNormal)
{
  ...
  HPEN hhi = CreatePen(0, 0, MAKE_PALETTERGB(crHighlight));
  HPEN hsh = CreatePen(0, 0, MAKE_PALETTERGB(crShadow));
  ...
  if(fNormal)
    hOld = SelectObject(hdc, hhi);
  else
    hOld = SelectObject(hdc, hhi);
  ...
} 

The error was found through the V523 diagnostic:  'then' 被当做 'else' 了. cardlib cardbutton.cpp 83

'hsh' 对象未被使用,而 'hhi'被使用了两次.以下是正确代码:

 if(fNormal)
  hOld = SelectObject(hdc, hhi);
else
  hOld = SelectObject(hdc, hsh); 

例 9. IPP Samples 项目. 不正确的检查.

 Status VC1VideoDecoder::ResizeBuffer()
{
  ...
  if(m_pContext && m_pContext->m_seqLayerHeader &&
     m_pContext->m_seqLayerHeader->heightMB &&
     m_pContext->m_seqLayerHeader->heightMB)  
  ...
} 

The error was found through the V501 diagnostic: 在 '&&' 操作的两边有两个相同的子表达式 'm_pContext->m_seqLayerHeader->heightMB'. operator. vc1_dec umc_vc1_video_decoder.cpp 1347

正确代码:

 if(m_pContext && m_pContext->m_seqLayerHeader &&
   m_pContext->m_seqLayerHeader->heightMB &&
   m_pContext->m_seqLayerHeader->widthMB)   

例 10. ReactOS 项目. 变量名错误.

 BOOL APIENTRY
GreStretchBltMask(...)
{
  ...
  MaskPoint.x += DCMask->ptlDCOrig.x;
  MaskPoint.y += DCMask->ptlDCOrig.x;
  ...
} 

The error was found through the V537 diagnostic: 认真审查 'x' 元素的使用. win32k bitblt.c 670

这是一个非常好的例子, 你可以看出一行被复制和粘贴了. 之后, 程序员修复了第一个名称 'x' 但忘记了改正第二个了. 下面是正确代码:

MaskPoint.x += DCMask->ptlDCOrig.x;
MaskPoint.y += DCMask->ptlDCOrig.y;

Late check of null pointers

逾期空指针检查


C/C++程序员不得不在任何时候大量的检查指针, 确保他们不等于 0 . 既然该类检查数量巨大的, 犯错的机会也就很大. 常常发生在一个指针先被使用而后才和 NULL 比较.这类错误很少显露出来. 通常程序在标准模式下工作良好,而仅在非标准模下失败. 不同于正常模式下的空指针处理, Access Violation 将发生且一个异常会被抛出.

例 1. Quake-III-Arena 项目. 逾期检查.

 void Item_Paint(itemDef_t *item) {
  vec4_t red;
  menuDef_t *parent = (menuDef_t*)item->parent;
  red[0] = red[3] = 1;
  red[1] = red[2] = 0;
  if (item == NULL) {
    return;
  }
  ...
} 

The error has been found with rule V595: 'item' 在未验证空指针前被使用. Check lines: 3865, 3869. cgame ui_shared.c 3865

'item' 指针先被使用而后才与 NULL 比较.

例 2. LAME Ain't an MP3 Encoder 项目. 逾期检查.

 static int
check_vbr_header(PMPSTR mp, int bytes)
{
  ...
  buf  = buf->next;
  pos = buf->pos;
  if(!buf) return -1; /* fatal error */
  ...
} 

The error has been found with rule V595: 'buf' 指针在空指针验证前被使用. Check lines: 226, 227. mpglib interface.c 226

'buf' 等于 NULL,一个异常將被抛出而不是返回一个错误码. 如果异常未被捕获,该程序将崩溃.

例 3. daoParanoia library 项目. 逾期检查.

 static long i_stage2_each(root_block *root,
  v_fragment *v, void(*callback)(long,int))
{
  cdrom_paranoia *p=v->p;
  long dynoverlap=p->dynoverlap/2*2;
  if (!v || !v->one) return(0);
  ...
} 

The error has been found with rule V595: 'v' 指针在空指针验证前被使用.Check lines: 532, 535. daoParanoia paranoia.c 532

这里的情况和前例一样.

例 4. TrinityCore 项目. 逾期检查.

 bool OnCheck(Player* player, Unit* /*target*/)
{
  bool checkArea =
    player->GetAreaId() == AREA_ARGENT_TOURNAMENT_FIELDS ||
    player->GetAreaId() == AREA_RING_OF_ASPIRANTS ||
    player->GetAreaId() == AREA_RING_OF_ARGENT_VALIANTS ||
    player->GetAreaId() == AREA_RING_OF_ALLIANCE_VALIANTS ||
    player->GetAreaId() == AREA_RING_OF_HORDE_VALIANTS ||
    player->GetAreaId() == AREA_RING_OF_CHAMPIONS;

  return player && checkArea && player->duel &&
         player->duel->isMounted;
}
The error has been found with rule : The 'player' pointer was utilized before it was verified against nullptr. Check lines: 310, 312. scripts achievement_scripts.cpp 310 

你可以从 "player && ..."条件中看出, 'player' 指针可能等于 0. 然后像前面几个例子一样的检查, 逾期了.

我们还可以引用很多这类例子, 但它们都很相似. 如果你看到了一些这类例子, 确保你是见过他们的.

杂项

例 1. Image Processing SDK 项目. 八进制数.

 inline 
void elxLuminocity(const PixelRGBus& iPixel,
  LuminanceCell< PixelRGBus >& oCell)
{
  oCell._luminance = uint16(0.2220f*iPixel._red +
    0.7067f*iPixel._blue + 0.0713f*iPixel._green);
  oCell._pixel = iPixel;
} 

inline 
void elxLuminocity(const PixelRGBi& iPixel,
  LuminanceCell< PixelRGBi >& oCell)
{
  oCell._luminance = 2220*iPixel._red +
    7067*iPixel._blue + 0713*iPixel._green;
  oCell._pixel = iPixel;
} 

The error was found through the V536 diagnostic: 被告知, 一个常量值被八进制的形式代表了. Oct: 0713, Dec: 459. IFF plugins pixelservices.inl 146

如果你检查第二个函数, 你会看到程序员的意图是使用数字 713 而不是 0713. 0713 是定义八进制数字的. 如果你很少使用八进制常量, 你会很容易忘记它.

Example 2. IPP Samples 项目. One variable for two loops.

例 2. IPP Samples 项目. 一个变量双循环.

 JERRCODE CJPEGDecoder::DecodeScanBaselineNI(void)
{
  ...
  for(c = 0; c < m_scan_ncomps; c++)
  {
    block = m_block_buffer + (DCTSIZE2*m_nblock*(j+(i*m_numxMCU)));

    // skip any relevant components
    for(c = 0; c < m_ccomp[m_curr_comp_no].m_comp_no; c++)
    {
      block += (DCTSIZE2*m_ccomp[c].m_nblocks);
    }
  ...
} 

The error was found through the V535 diagnostic: 'c' 变量被当前循环和外层循环使用了. jpegcodec jpegdec.cpp 4652

内层循环和外层循环使用同名的变量. 因此,该代码仅处理部分数据或导致一个死循环.

例 3. Quake-III-Arena 项目. 缺少 return.

 static ID_INLINE int BigLong(int l)
{ LongSwap(l); } 

The error has been found with rule V591: Non-void function should return a value. botlib q_shared.h 155

该代码使用C编写. 意味着编译器不需要显示的返回值. 但这里相当必要. 然而,该代码能工作完全靠运气.一切都依赖于 EAX 寄存器包含了什么.但这仅仅是运气,无他. 函数体应该被写成这样:{ return LongSwap(l) };

例4. Notepad++ 项目. 冗余条件.

 int Notepad_plus::getHtmlXmlEncoding(....) const
{
  ...
  if (langT != L_XML && langT != L_HTML && langT == L_PHP)
    return -1;
  ...
} 

The error has been found with rule V590: Consider inspecting this expression. The expression is excessive or contains a misprint. Notepad++ notepad_plus.cpp 853

能这仅仅是个打印错误, 但它也可能在运算分解中出现. 然而,这里很明显. 条件可以更简单: if (langT == L_PHP). 代码必须看起来像这样:

if (langT != L_XML && langT != L_HTML && langT != L_PHP)

英文原文OSCHINA原创翻译
加载中
0
Lunar_Lin
Lunar_Lin
相当霸气的广告! 被该工具折服了. 这样的公司真是牛.
0
Jackarain
Jackarain

今天下午刚看到, vlc的compat/lldiv.c

lldiv_t lldiv (long long num, long long denom)
{
    lldiv_t d = { num / denom, num % demon, };
    return d;
}

0
中山野鬼
中山野鬼
这工具真的很一般。别给看花眼了,诸位。我以为是动态检测呢。原来是静态文本分析。函数与函数的组合方式中的静态错误,未必你就能实现,特别是函数指针的方式折腾。这需要理解源码还不是简单的文本识别。
0
我土鳖
不过文中用的一些开源项目代码有点老,是出于安全性的考虑吗?
大东哥
大东哥
不知道,C/C++不熟,只是对着原文翻译.
0
轻风抚翼
轻风抚翼
基础很重要
0
hokim
hokim
这样的广告才是好广告    
0
开心Nemo
开心Nemo

引用来自“Jack.arain”的答案

今天下午刚看到, vlc的compat/lldiv.c

lldiv_t lldiv (long long num, long long denom)
{
    lldiv_t d = { num / denom, num % demon, };
    return d;
}

没很明白,是不是赋值最后多了个逗号?
Jackarain
Jackarain
注意看了, 硬生生的错误. 只有denom, 后面那个是demon...
0
阿昭
阿昭
很好很强大 
0
代码会说话
代码会说话

引用来自“hokim”的答案

这样的广告才是好广告    
是啊,我也有同感!
返回顶部
顶部