又一年即将结束,这是个完美的时机,让自己喝杯咖啡,重新阅读今年跨开放源代码项目收集的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__声明的)。 在项目的其他地方,检查是否正确编写:
顺便说一句,此错误不仅是文章“
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_intrinsiccall和
OP_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中的元素
数 。 可能是要检查
f0和
f1是否位于
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”。
错误的百万分之一数字几乎不会引起任何明显的危害,但是最好使用库中的现有常量,这样才能保证其正确性。 例如,Pi编号由标头math.h中的常数M_PI表示。
您已经在文章“
PVS-Studio深入研究Red Dead Redemption的Bullet Engine ”中阅读了有关此错误的信息,该错误在此列第六。 如果您尚未阅读,这是您的最后机会。
一个小转移
我们正在处理最有趣的3个错误。 正如您可能已经注意到的那样,我对bug的排序不是根据它们的影响,而是要由人工审核者才能找到它们。 毕竟,静态分析优于代码审查的优势基本上是软件工具无法累或忘了事情。 :)
现在,让我们看看前三名中有什么。
不行 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 ++源代码本身并不能说太多,所以让我们看一下从上面的代码片段生成的预处理代码:
分析器发现未关闭的
<div>标签。 这里有许多html代码片段,因此作者需要对其进行修改。
我们可以诊断出此类错误感到惊讶吗? 当我第一次看到它时,我也印象深刻。 因此,是的,我们确实了解有关分析html代码的知识。 好吧,只有它在C ++代码内。 :)
该错误不仅排在第二位,而且还是我们的前10名名单中的第二个计算器。 要了解我们在该项目中发现了哪些其他错误,请参阅文章“
跟随计算器的脚步:SpeedCrunch ”。
不行 1.难以捉摸的标准功能
这是首先放置的错误。 这是一个令人印象深刻的怪异错误,它通过代码审查得以成功实现。
尝试自己找到它:
static int EatWhitespace (FILE * InFile) { int c; for (c = getc (InFile); isspace (c) && ('\n' != c); c = getc (InFile)) ; return (c); }
现在,让我们看看分析器怎么说:
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中查找功能中的错误 ”。
结论
在今年,我们发现了许多错误。 这些是常见的复制粘贴错误,不正确的常量,未封闭的标签以及许多其他缺陷。 但是我们的分析器在不断发展,正在
学习诊断越来越多的问题,因此我们当然不会放慢脚步,并且将像以前一样定期发布有关项目中发现的错误的新文章。
以防万一您以前没有阅读过我们的文章,所有这些错误都是使用我们的PVS-Studio静态分析器发现的,欢迎您
下载并尝试使用自己的项目。 它检测用C,C ++,C#和Java编写的程序中的错误。
您终于到了终点线! 如果您错过了前两个级别,我建议您抓住机会并与我们一起完成这些级别:
C#和
Java 。