2019年C ++项目中的十大bug

图片7

下一年将要结束,因此现在该煮咖啡并重新阅读过去一年的错误报告了。 当然,这将花费很多时间,因此撰写了这篇文章。 我建议看看用C和C ++编写的项目中我们在2019年遇到的最有趣的黑暗地方。

第十名:“我们的操作系统是什么?”


V1040预定义的宏名称的拼写可能有错字。 “ __MINGW32_”宏类似于“ __MINGW32__”。 winapi.h 4112

#if !defined(__UNICODE_STRING_DEFINED) && defined(__MINGW32_) #define __UNICODE_STRING_DEFINED #endif 

此处,在宏名称__MINGW32 _中打了错字(MINGW32声明__MINGW32__)。 在项目的其他地方,验证编写正确:

图片3


顺便说一句,这不仅是文章“ CMake:由于代码质量而无法原谅的项目 ”中的第一个错误,而且通常是V1040诊断程序在一个真正的开放项目中发现的第一个实际错误(2019年8月19日)。

第九名:“谁是第一个?”


V502也许'?:'运算符的工作方式与预期的不同。 '?:'运算符的优先级低于'=='运算符。 第884章

 enum Opcode : uint8 { kOpUndef, .... OP_intrinsiccall, OP_intrinsiccallassigned, .... kOpLast, }; bool MIRParser::ParseStmtIntrinsiccall(StmtNodePtr &stmt, bool isAssigned) { Opcode o = !isAssigned ? (....) : (....); auto *intrnCallNode = mod.CurFuncCodeMemPool()->New<IntrinsiccallNode>(....); lexer.NextToken(); if (o == !isAssigned ? OP_intrinsiccall : OP_intrinsiccallassigned) { intrnCallNode->SetIntrinsic(GetIntrinsicID(lexer.GetTokenKind())); } else { intrnCallNode->SetIntrinsic(static_cast<MIRIntrinsicID>(....)); } .... } 

我们对此代码的以下部分感兴趣:

 if (o == !isAssigned ? OP_intrinsiccall : OP_intrinsiccallassigned) { .... } 

运算符'=='的优先级高于三元运算符(?:)。 因此,条件表达式不能正确求值。 编写的代码等效于以下内容:

 if ((o == !isAssigned) ? OP_intrinsiccall : OP_intrinsiccallassigned) { .... } 

并且考虑到常量OP_intrinsiccallOP_intrinsiccallassigned具有非零值的事实,此条件始终返回真实值。 else分支的主体是不可访问的代码。

该错误来自“ 检查华为最近打开的Ark编译器代码 ”一文。

第八名:“按位操作的危险”


V1046在操作'&='中不安全地使用bool'和'int'类型。 GSLMultiRootFinder.h 175

 int AddFunction(const ROOT::Math::IMultiGenFunction & func) { ROOT::Math::IMultiGenFunction * f = func.Clone(); if (!f) return 0; fFunctions.push_back(f); return fFunctions.size(); } template<class FuncIterator> bool SetFunctionList( FuncIterator begin, FuncIterator end) { bool ret = true; for (FuncIterator itr = begin; itr != end; ++itr) { const ROOT::Math::IMultiGenFunction * f = *itr; ret &= AddFunction(*f); } return ret; } 

根据代码, SetFunctionList函数应该绕过迭代器列表。 并且如果其中至少有一个无效,则返回值将为false ,否则为true

但是,实际上,即使对于有效的迭代器, SetFunctionList函数也可以返回false 。 让我们看一下情况。AddFunction函数返回fFunctions列表中有效迭代器的数量。 即 当添加非零迭代器时,此列表的大小将按顺序增加:1、2、3、4等。 这是代码中的错误开始显现的地方:

 ret &= AddFunction(*f); 

因为 由于该函数返回的是int类型而不是bool的结果,因此带有偶数的'&='操作将给出值false 。 毕竟,偶数的最低有效位将始终为零。 因此,即使对于有效的参数,这种明显的错误也会破坏SetFunctionsList函数的结果。

如果您仔细阅读了示例中的代码(并且仔细阅读了,对吗?),那么您可能会注意到这是来自ROOT项目的代码。 当然,我们对其进行了测试:“ ROOT代码分析-科学研究数据分析的框架 。”

第七名:“变量混乱”


V1001 [CWE-563]分配了'Mode'变量,但在功能结束时未使用。 SIModeRegister.cpp 48

 struct Status { unsigned Mask; unsigned Mode; Status() : Mask(0), Mode(0){}; Status(unsigned Mask, unsigned Mode) : Mask(Mask), Mode(Mode) { Mode &= Mask; }; .... }; 

为函数参数提供与类成员相同的名称是非常危险的。 很容易感到困惑。 摆在我们面前的就是这种情况。 此表达式没有意义:

 Mode &= Mask; 

函数的参数会更改。 就是这样。 该参数不再使用。 最有可能的是,有必要这样写:

 Status(unsigned Mask, unsigned Mode) : Mask(Mask), Mode(Mode) { this->Mode &= Mask; }; 

这是LLVM的错误。 我们有不时分析此项目的传统。 今年,我们还有一篇关于验证的文章

第六名:“ C ++有自己的规律”


由于C ++规则并不总是与数学规则或“常识”一致,因此在代码中出现以下错误。 自己注意到错误是在一小段代码中吗?

V709发现可疑比较:“ f0 == f1 == m_fractureBodies.size()”。 请记住,“ a == b == c”不等于“ a == b && b == c”。 btFractureDynamicsWorld.cpp 483

 btAlignedObjectArray<btFractureBody*> m_fractureBodies; void btFractureDynamicsWorld::fractureCallback() { for (int i = 0; i < numManifolds; i++) { .... int f0 = m_fractureBodies.findLinearSearch(....); int f1 = m_fractureBodies.findLinearSearch(....); if (f0 == f1 == m_fractureBodies.size()) continue; .... } .... } 

似乎该条件检查f0等于f1并等于m_fractureBodies中的元素 。 似乎此比较应该检查f0f1是否m_fractureBodies数组的末尾,因为它们包含由findLinearSearch()方法找到的对象的位置。 但是,实际上,此表达式变成一个测试,以查看f0f1是否相等,然后检查m_fractureBodies.size()是否等于f0 == f1的结果。 结果,此处的第三个操作数将与0或1进行比较。

美丽的错误! 而且,幸运的是,非常罕见。 到目前为止,我们仅在三个开放项目中遇到了她,有趣的是,所有这些仅仅是游戏引擎。 顺便说一句,这不是我们在Bullet中发现的唯一错误。 最有趣的是进入我们的文章“ PVS-Studio研究了Red Dead Redemption-Bullet引擎 ”。

第五名:“行的结尾是什么?”


如果您知道一个细微之处,则很容易检测到以下错误。

V739 EOF不应与“ char”类型的值进行比较。 “ ch”应为“ int”类型。 json.cpp 762

 void JsonIn::skip_separator() { signed char ch; .... if (ch == ',') { if( ate_separator ) { .... } .... } else if (ch == EOF) { .... } 

如果您不知道EOF定义为-1,这就是很难注意到的错误之一。 因此,如果您尝试将其与具有符号char类型的变量进行比较,则该条件几乎总是false 。 唯一的例外是字符代码为0xFF(255)。 比较时,此类符号将变为-1,并且条件为true。

在此顶部,与游戏相关的错误很多:从引擎到开放游戏。 您可能已经猜到了,这个地方也来自这个地区。 您可以在文章“ 未来大灾变,静态分析和百吉饼 ”中看到更多错误。

第四名:“ Pi的魔力”


V624'3.141592538 '常量中可能存在打印错误。 考虑使用<math.h>中的M_PI常量。 PhysicsClientC_API.cpp 4109

 B3_SHARED_API void b3ComputeProjectionMatrixFOV(float fov, ....) { float yScale = 1.0 / tan((3.141592538 / 180.0) * fov / 2); .... } 


数字Pi(3.141592653 ...)的一个小错字,在小数部分的第7个位置缺少数字“ 6”。

图片4
也许小数点后第1百万位的错误不会导致明显的后果,但是您仍然应该使用现有的库常量而不会出现错别字。 例如,对于Pi,在math.h标头中有一个M_PI常量。

该错误来自第六位已经为我们所熟悉的文章“ PVS-Studio调查了Red Dead Redemption-Bullet引擎 ”。 如果您没有推迟,那么这是最后一次机会。

有点题外话


因此,我们接近三个最有趣的错误。 您可能已经注意到,它们的存在不是根据灾难性后果来分类,而是根据检测的复杂性来分类。 归根到底,静态分析相对于代码审查而言,最重要的优势在于,机器不会感到疲劳,也不会忘记任何东西。 :)

现在,我提请您注意前三个。

图片8


第三名:“难以捉摸的例外”


V702类应始终从std :: exception(类似)派生为“ public”(未指定关键字,因此编译器默认将其设置为“ private”)。 CalcManager CalcException.h 4

 class CalcException : std::exception { public: CalcException(HRESULT hr) { m_hr = hr; } HRESULT GetException() { return m_hr; } private: HRESULT m_hr; }; 

分析器通过private修饰符(如果未指定任何内容,则为默认修饰符)检测到从std :: exception类继承的类。 这段代码的问题是,当您尝试捕获通用异常std :: exception时,将跳过CalcException类型异常。 发生此现象的原因是,私有继承会阻止隐式类型转换。

是的,我不想由于缺少公共修饰符而导致程序崩溃 顺便说一句,我敢肯定,您肯定在自己的生活中至少使用过一次该应用程序,而我们刚刚查看了该源代码。 这是我们测试过的老式标准Windows计算器

第二名:未封闭的HTML标记


V735可能是不正确的HTML。 遇到“ </ body>”结束标记,而应使用“ </ div>”标记。 book.cpp 127

 static QString makeAlgebraLogBaseConversionPage() { return BEGIN INDEX_LINK TITLE(Book::tr("Logarithmic Base Conversion")) FORMULA(y = log(x) / log(a), log<sub>a</sub>x = log(x) / log(a)) END; } 

正如C / C ++代码经常发生的那样,从源头上还不清楚什么,所以让我们来看一下此片段的预处理代码:

图片6


分析器检测到未关闭的<div>标签 。 该文件中有许多html代码片段,现在开发人员应该对其进行额外检查。

我们可以检查等感到惊讶吗? 当我第一次看到这个时,我印象深刻。 因此,我们分析了一些html代码。 是的,仅在C ++代码中。 :)

这不仅是第二名,也是我们排名第二的计算器。 您可以在“ 跟随计算器的踪迹:SpeedCrunch ”一文中找到所有错误的列表。

第一名:“难以捉摸的标准功能”


因此,我们获得了第一名。 经过代码审查的一个令人印象深刻的怪异问题。

尝试自己发现它:

 static int EatWhitespace (FILE * InFile) /* ----------------------------------------------------------------------- ** * Scan past whitespace (see ctype(3C)) and return the first non-whitespace * character, or newline, or EOF. * * Input: InFile - Input source. * * Output: The next non-whitespace character in the input stream. * * Notes: Because the config files use a line-oriented grammar, we * explicitly exclude the newline character from the list of * whitespace characters. * - Note that both EOF (-1) and the nul character ('\0') are * considered end-of-file markers. * * ----------------------------------------------------------------------- ** */ { int c; for (c = getc (InFile); isspace (c) && ('\n' != c); c = getc (InFile)) ; return (c); } /* EatWhitespace */ 

现在,让我们看看分析仪的誓言:

V560条件表达式的一部分始终为true:('\ n'!= C)。 c。第136章

奇怪,不是吗? 让我们看一下在同一项目中但在不同文件(charset.h)中的一些有趣的东西:

 #ifdef isspace #undef isspace #endif .... #define isspace(c) ((c)==' ' || (c) == '\t') 

因此,这确实已经很奇怪了......事实证明,如果变量c等于'\ n',那么乍一看isspace(c)函数绝对无害将返回false,并且由于短路评估而将不执行该测试的第二部分。 如果执行isspace(c) ,则变量c'''\ t',并且显然不等于'\ n'

当然,您可以说此宏就像#define true false ,并且这样的代码将永远不会通过代码审查。 但是,此代码通过了审核,并在项目存储库中安全地等待着我们。

如果您需要对错误进行更详细的分析,请在文章“ 对于那些想要扮演侦探的人:从Midnight Commander的函数中找到错误 ”中进行。

结论


图片9


在过去的一年中,我们发现了许多错误。 这些是常见的复制粘贴错误,常量错误,未关闭的标签以及许多其他问题。 但是我们的分析器正在不断改进,正在学习寻找越来越多的错误,因此这还远远没有结束,有关检查项目的新文章将像以前一样频繁地发布。

如果有人第一次阅读我们的文章,以防万一,我将澄清所有这些错误是使用PVS-Studio静态代码分析器发现的,建议您下载并尝试使用。 分析器可以检测使用以下语言编写的程序代码中的错误:C,C ++,C#和Java。

这样我们就结束了! 如果您错过了前两个级别,那么我建议您不要错过机会,并与我们一起经历它们: C#Java



如果您想与讲英语的读者分享这篇文章,请使用以下链接:Maxim Zvyagintsev。 2019年在C ++项目中发现的十大bug

Source: https://habr.com/ru/post/zh-CN481190/


All Articles