2019年在C ++项目中发现的十大bug

图片7

又一年即将结束,这是个完美的时机,让自己喝杯咖啡,重新阅读今年跨开放源代码项目收集的bug的评论。 当然,这将花费相当长的时间,因此我们准备了本文以使您更轻松。 今天,我们将回想起2019年在开源C / C ++项目中遇到的最有趣的暗点。

不行 10.我们正在运行什么操作系统?


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

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

__MINGW32 _宏的名称中有一个错字(MINGW32实际上是由__MINGW32__声明的)。 在项目的其他地方,检查是否正确编写:

图片3


顺便说一句,此错误不仅是文章“ CMake:项目质量不可原谅的情况 ”中首次描述,而且还是V1040诊断程序在一个真正的开源项目中发现的第一个真正错误(8月19日) ,2019)。

不行 9.谁先?


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不为null,因此条件将一直返回true ,这意味着else分支的主体是不可访问的代码。

该错误在“ 检查华为最近使开源的方舟编译器 ”中有描述。

不行 8.危险的按位操作


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列表上有效迭代器的数量。 也就是说,添加非null迭代器将导致列表的大小逐渐增加:1、2、3、4,依此类推。 这是该错误起作用的地方:

 ret &= AddFunction(*f); 

由于该函数返回的是int类型而不是bool的值 ,因此'&='操作将为偶数返回false ,因为偶数的最低有效位始终设置为零。 这是一个微妙的错误可以破坏SetFunctionsList的返回值的方式,即使其参数有效。

如果您正在仔细阅读该代码段(当时是,不是吗?),您可能已经注意到它来自项目ROOT。 是的,我们也进行了检查:“ 分析ROOT代码,科学数据分析框架”

不行 7.变量混合


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中发现了此错误。 我们有不时检查此项目的传统。 今年,我们又检查了一次。

不行 6. 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()方法找到的对象位置。 但实际上,此条件表达式检查f0是否等于f1 ,然后检查m_fractureBodies.size()是否等于表达式f0 == f1的结果 。 也就是说,此处的第三个操作数将根据0或1进行检查。

那是一个不错的错误! 而且,幸运的是,这是非常罕见的。 到目前为止,我们仅在三个开源项目中看到了它,有趣的是,这三个都是游戏引擎。 这不是Bullet中发现的唯一错误; 文章“ PVS-Studio看着Red Dead Redemption的子弹引擎 ”中描述了最有趣的引擎

不行 5.行尾是什么?


如果您知道一个棘手的细节,这很容易。

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)的字符。 与EOF比较时,此字符将变为-1,从而使条件成立。

在今年的前10名中,有很多错误是在计算机游戏软件中发现的:引擎或开源游戏。 正如您已经猜到的那样,这一个人也来自该地区。 更多的错误在文章“大灾变未来:静态分析和Roguelike游戏 ”中描述。

不行 4.魔术常数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
错误的百万分之一数字几乎不会引起任何明显的危害,但是最好使用库中的现有常量,这样才能保证其正确性。 例如,Pi编号由标头math.h中的常数M_PI表示。

您已经在文章“ PVS-Studio深入研究Red Dead Redemption的Bullet Engine ”中阅读了有关此错误的信息,该错误在此列第六。 如果您尚未阅读,这是您的最后机会。

一个小转移


我们正在处理最有趣的3个错误。 正如您可能已经注意到的那样,我对bug的排序不是根据它们的影响,而是要由人工审核者才能找到它们。 毕竟,静态分析优于代码审查的优势基本上是软件工具无法累或忘了事情。 :)

现在,让我们看看前三名中有什么。

图片8


不行 3.难以捉摸的例外


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 ::异常将导致程序丢失CalcException类型的异常。 此行为源于以下事实:私有继承禁止隐式类型转换。

您肯定不希望由于缺少公共修饰符而导致程序崩溃。 顺便说一句,我敢打赌您一生中至少使用过此应用程序一次,因为它是旧的Windows计算器 ,我们也在今年早些时候对其进行了检查

不行 2.未封闭的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 ++代码内。 :)

该错误不仅排在第二位,而且还是我们的前10名名单中的第二个计算器。 要了解我们在该项目中发现了哪些其他错误,请参阅文章“ 跟随计算器的脚步:SpeedCrunch ”。

不行 1.难以捉摸的标准功能


这是首先放置的错误。 这是一个令人印象深刻的怪异错误,它通过代码审查得以成功实现。

尝试自己找到它:

 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

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


All Articles