我们必须不时编写有关检查编译器下一版本的文章。没意思。但是,如实践所示,如果长时间不这样做,人们就会开始怀疑PVS-Studio分析仪是否应被称为“良好的bug和潜在漏洞捕获者”。也许新的编译器已经知道该怎么做?是的,编译器不会停滞不前。但是,PVS-Studio也在不断开发,即使在诸如编译器之类的高质量项目的代码中也证明了发现错误的能力。
是时候仔细检查Clang代码了
老实说,我以文章“使用PVS-Studio检查GCC 10编译器”作为本文的基础。因此,如果您觉得您已经在某处阅读了一些段落,那么您就不会认为:)。
编译器具有自己的内置静态代码分析器已经不是什么秘密,并且它们也正在开发中。因此,我们不时撰写有关PVS-Studio静态分析器如何甚至在编译器内部也可以发现错误的文章,以及我们吃面包的原因:)。
实际上,您无法将经典的静态分析器与编译器进行比较。静态分析器不仅要查找代码中的错误,而且还涉及发达的基础架构。例如,这是与SonarQube,PlatformIO,Azure DevOps,Travis CI,CircleCI,GitLab CI / CD,Jenkins,Visual Studio等系统集成。这些是用于批量抑制警告的高级机制,即使在一个大型的旧项目中,也可以使您快速开始使用PVS-Studio。这是一个通知分发。等等等等。但是,这仍然是第一个要问的问题:“ PVS-Studio是否可以找到编译器无法找到的内容?”这意味着我们将一再撰写有关检查这些编译器本身的文章。
让我们回到检查Clang项目的主题。无需深入研究该项目并告诉它是什么。实际上,不仅测试了Clang 11本身的代码,还测试了基于Clang 11的LLVM 11库。从本文的角度来看,在编译器或库代码中是否发现缺陷都没有区别。
在我看来,Clang / LLVM代码比GCC代码清晰得多。至少所有这些可怕的宏都丢失了,并且积极使用了C ++语言的现代功能。
尽管如此,该项目还是很大的,并且没有对分析仪进行初步设置,查看报告仍然非常繁琐。基本上,半误报会干扰。我所说的“半错误”肯定是分析仪在形式上正确,但没有警告意义的情况。例如,许多这样的肯定结果会发布给单元测试和生成的代码。
测试示例:
Spaces.SpacesInParentheses = false; // <=
Spaces.SpacesInCStyleCastParentheses = true; // <=
verifyFormat("Type *A = ( Type * )P;", Spaces);
verifyFormat("Type *A = ( vector<Type *, int *> )P;", Spaces);
verifyFormat("x = ( int32 )y;", Spaces);
verifyFormat("int a = ( int )(2.0f);", Spaces);
verifyFormat("#define AA(X) sizeof((( X * )NULL)->a)", Spaces);
verifyFormat("my_int a = ( my_int )sizeof(int);", Spaces);
verifyFormat("#define x (( int )-1)", Spaces);
// Run the first set of tests again with:
Spaces.SpacesInParentheses = false; // <=
Spaces.SpaceInEmptyParentheses = true;
Spaces.SpacesInCStyleCastParentheses = true; // <=
verifyFormat("call(x, y, z);", Spaces);
verifyFormat("call( );", Spaces);
分析器警告说,已为变量分配了它们已经包含的相同值:
- V1048为'Spaces.SpacesInParentheses'变量分配了相同的值。FormatTest.cpp 11554
- V1048为'Spaces.SpacesInCStyleCastParentheses'变量分配了相同的值。FormatTest.cpp 11556
正式地,分析器返回了正确的响应,这是应该简化或纠正的代码片段。同时,很明显,实际上一切都很好,并且编辑任何内容也没有意义。
另一个例子:分析器向自动生成的Options.inc文件发出大量警告。您可以在文件中看到“代码表”:
PVS-Studio会向所有这些发出警告:
- V501'=='运算符的左侧和右侧有相同的子表达式:nullptr == nullptr Options.inc 26
- V501在'=='运算符的左侧和右侧有相同的子表达式:nullptr == nullptr Options.inc 27
- V501在'=='运算符的左侧和右侧有相同的子表达式:nullptr == nullptr Options.inc 28
- 等等每一行...
这一切并不可怕。所有这些都是可以克服的:禁用对不必要文件的检查,标记一些宏和功能,抑制某些类型的警报等等。可能,但是将其作为编写文章任务的一部分并不有趣。因此,我所做的与关于GCC编译器的文章完全相同。我研究了这份报告,直到有11个有趣的代码示例来撰写文章。为什么是11?我以为,由于Clang版本是11,所以让片段成为11 :)。
11个可疑代码段
片段N1,以1取模
很酷的错误!我爱这些!
void Act() override {
....
// If the value type is a vector, and we allow vector select, then in 50%
// of the cases generate a vector select.
if (isa<FixedVectorType>(Val0->getType()) && (getRandom() % 1)) {
unsigned NumElem =
cast<FixedVectorType>(Val0->getType())->getNumElements();
CondTy = FixedVectorType::get(CondTy, NumElem);
}
....
}
PVS-Studio警告:V1063以1为模的运算没有意义。结果将始终为零。 llvm-stress.cpp 631模
除法用于获取0或1的随机值。但是,显然,此值1令人迷惑,尽管有必要将其除以2,但人们仍将错误的经典模式除以1。X%1运算无意义,因为结果始终为0。正确的代码变体:
if (isa<FixedVectorType>(Val0->getType()) && (getRandom() % 2)) {
最近出现在PVS-Studio中的Diagnostics V1063非常简单,但是如您所见,它可以工作。
众所周知,编译器开发人员会查看我们在做什么,并借鉴我们的最佳实践。没有什么不妥。我们很高兴PVS-Studio是进步的动力。我们设置在Clang和GCC中出现相同的诊断量之后的时间:)。
片段N2,条件错误
class ReturnValueSlot {
....
bool isNull() const { return !Addr.isValid(); }
....
};
static bool haveSameParameterTypes(ASTContext &Context, const FunctionDecl *F1,
const FunctionDecl *F2, unsigned NumParams) {
....
unsigned I1 = 0, I2 = 0;
for (unsigned I = 0; I != NumParams; ++I) {
QualType T1 = NextParam(F1, I1, I == 0);
QualType T2 = NextParam(F2, I2, I == 0);
if (!T1.isNull() && !T1.isNull() && !Context.hasSameUnqualifiedType(T1, T2))
return false;
}
return true;
}
PVS-Studio警告:V501'&&'运算符的左侧和右侧都有相同的子表达式:!T1.isNull()&&!T1.isNull()SemaOverload.cpp 9493检查
两次!T1.isNull ()。这是一个明显的错别字,条件的第二部分应检查T2变量。
片段N3,电位超出阵列范围
std::vector<Decl *> DeclsLoaded;
SourceLocation ASTReader::getSourceLocationForDeclID(GlobalDeclID ID) {
....
unsigned Index = ID - NUM_PREDEF_DECL_IDS;
if (Index > DeclsLoaded.size()) {
Error("declaration ID out-of-range for AST file");
return SourceLocation();
}
if (Decl *D = DeclsLoaded[Index])
return D->getLocation();
....
}
PVS-Studio警告:V557阵列可能超限。“索引”索引指向数组边界之外。ASTReader.cpp 7318
假定数组包含一个元素,而Index变量也是一个。条件(1> 1)为false,结果该数组将被溢出。正确检查:
if (Index >= DeclsLoaded.size()) {
片段N4,参数评估的顺序
void IHexELFBuilder::addDataSections() {
....
uint32_t SecNo = 1;
....
Section = &Obj->addSection<OwnedDataSection>(
".sec" + std::to_string(SecNo++), RecAddr,
ELF::SHF_ALLOC | ELF::SHF_WRITE, SecNo);
....
}
PVS-Studio警告:V567未指定行为。没有为'addSection'函数定义参数评估的顺序。考虑检查“ SecNo”变量。Object.cpp 1223
请注意,SecNo参数使用了两次,并在此过程中递增。只是不可能说出参数将按什么顺序求值。因此,结果将取决于编译器版本或编译设置。
让我用一个综合的例子解释一下:
#include <cstdio>
int main()
{
int i = 1;
printf("%d, %d\n", i, i++);
return 0;
}
根据编译器的不同,可以同时打印“ 1、2”和“ 2、1”。使用编译器资源管理器,我得到以下结果:
有趣的是,对于这种简单的情况,Clang编译器会发出警告:
<source>:6:26: warning:
unsequenced modification and access to 'i' [-Wunsequenced]
printf("%d, %d\n", i, i++);
显然,在实际情况下,此警告没有帮助。由于不方便实际使用,因此禁用了诊断程序,或者编译器无法警告更复杂的情况。
N5片段,奇怪的重新测试
template <class ELFT>
void GNUStyle<ELFT>::printVersionSymbolSection(const ELFFile<ELFT> *Obj,
const Elf_Shdr *Sec) {
....
Expected<StringRef> NameOrErr =
this->dumper()->getSymbolVersionByIndex(Ndx, IsDefault);
if (!NameOrErr) {
if (!NameOrErr) {
unsigned SecNdx = Sec - &cantFail(Obj->sections()).front();
this->reportUniqueWarning(createError(
"unable to get a version for entry " + Twine(I) +
" of SHT_GNU_versym section with index " + Twine(SecNdx) + ": " +
toString(NameOrErr.takeError())));
}
Versions.emplace_back("<corrupt>");
continue;
}
....
}
PVS-Studio警告:V571重复检查。'if(!NameOrErr)'条件已在第4666行中得到验证。ELFDumper.cpp 4667
第二个检查与第一个检查重复,并且是多余的。也许可以简单地删除第二张支票。但这很可能是错字,在第二种情况下应使用其他变量。
片段N6,取消引用可能为空的指针
void RewriteObjCFragileABI::RewriteObjCClassMetaData(
ObjCImplementationDecl *IDecl, std::string &Result)
{
ObjCInterfaceDecl *CDecl = IDecl->getClassInterface();
if (CDecl->isImplicitInterfaceDecl()) {
RewriteObjCInternalStruct(CDecl, Result);
}
unsigned NumIvars = !IDecl->ivar_empty()
? IDecl->ivar_size()
: (CDecl ? CDecl->ivar_size() : 0);
....
}
PVS-Studio警告:V595在针对nullptr进行验证之前,已使用了'CDecl'指针。检查行:5275,5284。RewriteObjC.cpp 5275
在第一次检查期间,始终以粗体方式取消引用CDecl指针:
if (CDecl->isImplicitInterfaceDecl())
并且仅从下面编写的代码中可以清楚地看出该指针可以为null:
(CDecl ? CDecl->ivar_size() : 0)
最有可能的是,第一次检查应如下所示:
if (CDecl && CDecl->isImplicitInterfaceDecl())
片段N7,取消引用可能为空的指针
bool
Sema::InstantiateClass(....)
{
....
NamedDecl *ND = dyn_cast<NamedDecl>(I->NewDecl);
CXXRecordDecl *ThisContext =
dyn_cast_or_null<CXXRecordDecl>(ND->getDeclContext());
CXXThisScopeRAII ThisScope(*this, ThisContext, Qualifiers(),
ND && ND->isCXXInstanceMember());
....
}
PVS-Studio警告:V595在针对nullptr进行验证之前,已使用了'ND'指针。检查行:2803,2805。SemaTemplateInstantiate.cpp 2803
先前错误的变体。如果使用动态强制转换获得了指针的值,则在不首先检查指针的情况下取消引用是很危险的。此外,从下面的代码中,您可以看到需要进行此检查。
片段N8,尽管出现错误状态,该函数仍继续执行
bool VerifyObject(llvm::yaml::Node &N,
std::map<std::string, std::string> Expected) {
....
auto *V = llvm::dyn_cast_or_null<llvm::yaml::ScalarNode>(Prop.getValue());
if (!V) {
ADD_FAILURE() << KS << " is not a string";
Match = false;
}
std::string VS = V->getValue(Tmp).str();
....
}
PVS-Studio警告:V1004在针对nullptr对其进行验证之后,不安全地使用了“ V”指针。检查行:61,65。TraceTests.cpp 65
指针V可能为空。显然,这是一种错误情况,甚至有报道。但是,此后,该函数将继续执行,就好像什么都没发生一样,这将导致对该空指针的取消引用。也许他们忘记了中断该函数,并且正确的版本应如下所示:
auto *V = llvm::dyn_cast_or_null<llvm::yaml::ScalarNode>(Prop.getValue());
if (!V) {
ADD_FAILURE() << KS << " is not a string";
Match = false;
return false;
}
std::string VS = V->getValue(Tmp).str();
片段N9,错字
const char *tools::SplitDebugName(const ArgList &Args, const InputInfo &Input,
const InputInfo &Output) {
if (Arg *A = Args.getLastArg(options::OPT_gsplit_dwarf_EQ))
if (StringRef(A->getValue()) == "single")
return Args.MakeArgString(Output.getFilename());
Arg *FinalOutput = Args.getLastArg(options::OPT_o);
if (FinalOutput && Args.hasArg(options::OPT_c)) {
SmallString<128> T(FinalOutput->getValue());
llvm::sys::path::replace_extension(T, "dwo");
return Args.MakeArgString(T);
} else {
// Use the compilation dir.
SmallString<128> T(
Args.getLastArgValue(options::OPT_fdebug_compilation_dir));
SmallString<128> F(llvm::sys::path::stem(Input.getBaseInput()));
llvm::sys::path::replace_extension(F, "dwo");
T += F;
return Args.MakeArgString(F); // <=
}
}
PVS-Studio警告:V1001分配了“ T”变量,但在功能结束时未使用。CommonArgs.cpp 873
注意函数结束。局部变量T已更改,但未以任何方式使用。这很可能是一个错字,该函数应以以下代码行结尾:
T += F;
return Args.MakeArgString(T);
片段N10,除数为零
typedef int32_t si_int;
typedef uint32_t su_int;
typedef union {
du_int all;
struct {
#if _YUGA_LITTLE_ENDIAN
su_int low;
su_int high;
#else
su_int high;
su_int low;
#endif // _YUGA_LITTLE_ENDIAN
} s;
} udwords;
COMPILER_RT_ABI du_int __udivmoddi4(du_int a, du_int b, du_int *rem) {
....
if (d.s.low == 0) {
if (d.s.high == 0) {
// K X
// ---
// 0 0
if (rem)
*rem = n.s.high % d.s.low;
return n.s.high / d.s.low;
}
....
}
PVS-Studio警告:
- V609 Mod归零。分母'dslow'== 0.udivmoddi4.c 61
- V609除以零。分母'dslow'== 0.udivmoddi4.c 62
我不知道这是一个错误还是一些棘手的想法,但是代码很奇怪。有两个普通的整数变量,一个可以被另一个整除。有趣的是,这仅在两个变量均为零时才会发生。这是什么意思呢?
N11片段,复制粘贴
bool MallocChecker::mayFreeAnyEscapedMemoryOrIsModeledExplicitly(....)
{
....
StringRef FName = II->getName();
....
if (FName == "postEvent" &&
FD->getQualifiedNameAsString() == "QCoreApplication::postEvent") {
return true;
}
if (FName == "postEvent" &&
FD->getQualifiedNameAsString() == "QCoreApplication::postEvent") {
return true;
}
....
}
PVS-Studio警告:V581彼此并排放置的'if'语句的条件表达式相同。检查行:3108,3113。MallocChecker.cpp 3113
复制了代码段,但未进行任何更改。应该删除或修改第二个片段以开始执行一些有用的检查。
结论
让我提醒您,您可以使用此免费许可证选项来检查开源项目。顺便说一下,还有其他免费许可PVS-Studio的选项,甚至包括封闭项目。它们在此处列出:“免费的PVS-Studio许可选项”。感谢您的关注。
我们有关编译器检查的其他文章
- LLVM(Clang)分析(2011年8月),第二分析(2012年8月),第三分析(2016年10月),第四分析(2019年4月)
- GCC分析(2016年8月),第二次分析(2020年4月)
- 华为方舟编译器分析(2019年12月)
- .NET编译器平台(“ Roslyn”)分析(2015年12月),第二次分析(2019年4月)
- 罗斯林分析仪分析(2019年8月)
- PascalABC.NET分析(2017年3月)
如果您想与说英语的读者分享这篇文章,请使用翻译链接:Andrey Karpov。使用PVS-Studio检查Clang 11。