PVS-StudioでClang11を確認する

(2020年10月28日) )

時々、コンパイラの別の新しいバージョンをどのようにチェックしたかについての記事を書かなければなりません。それはあまり楽しいことではありません。しかし、実践が示すように、しばらくそれをやめると、人々はPVS-Studioがバグや脆弱性の優れたキャッチャーの称号に値するかどうか疑問に思い始めます。新しいコンパイラでもそれができるとしたらどうでしょうか?確かに、コンパイラーは進化しますが、PVS-Studioも進化します。そして、コンパイラーなどの高品質のプロジェクトでもバグをキャッチできることを何度も証明しています。

Clangを再確認する時間

実を言うと、私はこの記事をベースに書いています以前の投稿「

PVS-Studioを使用したGCC10コンパイラの確認」。したがって、一部の段落がおなじみのように思われる場合は、以前に読んだことがあるためです:)

コンパイラが独自の組み込み静的コードアナライザを採用していることは周知の事実であり、それらも開発中です。そのため、静的アナライザーであるPVS-Studioがコンパイラー内でもバグを検出できること、そして私たちが塩の価値があることを示すために、時々記事を書いています:)

実際、あなたはできません。従来の静的アナライザーとコンパイラーを比較してください。静的アナライザーは、ソースコードのバグを検出するだけでなく、高度に開発されたインフラストラクチャも含みます。 1つには、SonarQube、PlatformIO、Azure DevOps、Travis CI、CircleCI、GitLab CI / CD、Jenkins、VisualStudioなどのシステムとの統合が含まれます。警告を大量に抑制するメカニズムが含まれているため、大規模なプロジェクトでもすぐにPVS-Studioの使用を開始できます。これには、電子メールによる通知の送信が含まれます。などなど。しかし、開発者がまだ尋ねる最初の質問は、「あなたのPVS-Studioは、コンパイラーが見つけられないものを見つけることができるか?」です。つまり、コンパイラ自体を何度もチェックする方法についての記事を書く運命にあるということです。

Clangに戻りましょう。このテーマにこだわって、プロジェクトの内容を説明する必要はありません。実際、Clang 11自体のコードだけでなく、それが基づいているLLVM11ライブラリのコードもチェックしました。この記事の観点からは、コンパイラまたはライブラリのコードに欠陥が見つかったかどうかは関係ありません。

Clang / LLVMのコードはGCCのコードよりもはるかに明確であることがわかりました。少なくとも、これらすべてのひどいマクロでいっぱいではなく、C ++の最新機能を幅広く採用しています。

それでも、プロジェクトは十分に大きいため、事前にカスタマイズしなくても分析レポートを調べるのは面倒です。主に邪魔になるのは、「半偽」の陽性です。 「半偽」陽性とは、特定の問題を指摘するためにアナライザーが技術的に正しいが、それらの警告が実用的でない場合を意味します。たとえば、このような警告の多くは、単体テストと生成されたコードを参照しています。

単体テストの例を次に示します。

アナライザーは、変数に既に持っているのと同じ値が割り当てられていることを警告します。

  • V1048「Spaces.SpacesInParentheses」変数に同じ値が割り当てられました。 FormatTest.cpp 11554
  • V1048「Spaces.SpacesInCStyleCastParentheses」変数に同じ値が割り当てられました。 FormatTest.cpp 11556

技術的には、この警告は要点であり、スニペットを単純化または修正する必要があります。ただし、このコードはそのままで問題がなく、何も修正しても意味がないことも明らかです。

別の例:アナライザーは、自動生成されたファイルOptions.incに大量の警告を出力します。含まれているコードの「壁」を見てください:

この大量のコードは、大量の警告をトリガーします:

  • V501 ==演算子の左側と右側に同一の部分式があります:nullptr == nullptr Options.inc 26
  • V501「==」演算子の左側と右側に同一の部分式があります。nullptr== nullptr Options.inc 27
  • V501あります==演算子の左側と右側にある同一の部分式です:nullptr == nullptr Options.inc 28
  • など—1行に1つの警告…

それでも大したことではありません。 は、関係のないファイルを分析から除外したり、特定のマクロや関数にマークを付けたり、特定の診断タイプを抑制したりすることで解決できます。はい、できますが、記事を書くときに行うのはあまり興味深い仕事ではありません。そのため、GCCコンパイラのチェックに関する記事と同じことを行いました。記事に含める11の興味深い例を収集するまで、レポートを読み続けました。なぜ11? Clangの11番目のバージョンなので、11個の例が必要だと思いました:)。

11個の疑わしいコードスニペット

スニペット1、1 <のモジュロ演算/ p>

これはすばらしいものです!私はそのようなバグが好きです!

PVS-Studio診断メッセージ:V10631つのモジュロ演算は無意味です。結果は常にゼロになります。 llvm-stress.cpp 631

プログラマーはモジュロ演算を使用して0または1のランダムな値を取得していますが、値 1 は開発者を混乱させているようです。 モジュロ演算が2ではなく1で実行される古典的なアンチパターンを記述させます。 X%1 演算は、常に 0 。これは修正バージョンです:

if (isa(Val0->getType()) && (getRandom() % 2)) {

最近追加されたV1063診断は非常に単純ですが、ご覧のとおり、完全に機能します。

コンパイラ開発者は私たちの仕事に目を光らせ、私たちのアイデアを借りていることを私たちは知っています。それはまったく問題ありません。 PVS-Studioが進歩の原動力であることを知っておくとよいでしょう。同様の診断がClangとGCCに表示されるまでにかかる時間を見てみましょう:)

Snippet 2、条件のタイプミス

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()チェックが2回実行されます。これは明らかにタイプミスです。条件の2番目の部分では、 T2 変数をチェックする必要があります。

スニペット3、潜在的なarray-index-out-of- bounds

std::vector 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

配列に1つの要素が格納され、 Index 変数の値も1であるとします。次に、(1> 1)条件はfalseであるため、配列はその境界を超えてインデックスが付けられます。正しいチェックは次のとおりです。

スニペット4、引数の評価順序

void IHexELFBuilder::addDataSections() {
....
uint32_t SecNo = 1;
....
Section = &Obj->addSection(
".sec" + std::to_string(SecNo++), RecAddr,
ELF::SHF_ALLOC | ELF::SHF_WRITE, SecNo);
....
}

PVS-Studio診断メッセージ:V567指定されていない動作。引数の評価の順序は、「addSection」関数では定義されていません。 「SecNo」変数を調べることを検討してください。 Object.cpp 1223

SecNo 引数が2回使用され、その間にインクリメントされることに注意してください。問題は、引数が評価される正確な順序がわからないことです。したがって、結果はコンパイラのバージョンまたはコンパイルパラメータによって異なります。

この点を説明するための合成例を次に示します。

コンパイラに応じて、このコードは「1、2」または「2、1」のいずれかを出力する場合があります。コンパイラエクスプローラーで実行すると、次の出力が得られました。

  • Clang 11.0.0でコンパイルすると、プログラム出力 1 、1。
  • GCC 10.2でコンパイルすると、プログラム出力 2、1。

興味深いことに、この単純なケースにより、Clangは警告を発行します:

:6:26: warning:
unsequenced modification and access to "i" [-Wunsequenced]
printf("%d, %d\n", i, i++);

ただし、何らかの理由で、この警告は実際のコードでは発行されませんでした。あまり実用的ではないために無効にされているか、その場合はコンパイラが対処するには複雑すぎます。

スニペット5、奇妙な重複チェック

template <class ELFT>
void GNUStyle::printVersionSymbolSection(const ELFFile *Obj,
const Elf_Shdr *Sec) { ....
Expected 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("");
continue;
}
....
}

PVS-Studio診断メッセージ:V571定期的なチェック。 「if(!NameOrErr)」条件は4666行目ですでに確認されています。ELFDumper.cpp4667

2番目のチェックは最初のチェックのクローンであるため、冗長です。多分それは安全に取り除くことができます。しかし、より可能性が高いのは、タイプミスが含まれていて、他の変数をチェックするためのものであるということです。

スニペット6、潜在的なnullポインター逆参照

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になる可能性があることが明らかになります。

最初のチェックはおそらく次のようになっているはずです:

if (CDecl && CDecl->isImplicitInterfaceDecl())

スニペット7、潜在的なnullポインタの逆参照

bool
Sema::InstantiateClass(....)
{
....
NamedDecl *ND = dyn_cast(I->NewDecl);
CXXRecordDecl *ThisContext =
dyn_cast_or_null(ND->getDeclContext());
CXXThisScopeRAII ThisScope(*this, ThisContext, Qualifiers(),
ND && ND->isCXXInstanceMember());
....
}

PVS-Studio診断メッセージ:V595「ND」ポインターが検証される前に使用されましたnullptr。チェック行:2803、2805。SemaTemplateInstantiate.cpp 2803

このエラーは、前のエラーと同様です。動的型キャストを使用して値を取得する場合、事前のチェックなしにポインタを逆参照するのは危険です。それ以上に、後続のコードはそのようなチェックが必要であることを確認します。

スニペット8、エラー状態にもかかわらず実行を続ける関数

bool VerifyObject(llvm::yaml::Node &N,
std::map Expected) {
....
auto *V = llvm::dyn_cast_or_null(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 ポインタはnullポインタである可能性があります。これは明らかにエラー状態であり、エラーメッセージで報告されることもあります。しかし、関数は何も起こらなかったかのように実行を続け、その非常にnullポインターを逆参照することになります。プログラマーはおそらくこの時点で関数を停止したいと考えていました。その場合、次のように修正する必要があります。

auto *V = llvm::dyn_cast_or_null(Prop.getValue());
if (!V) {
ADD_FAILURE() << KS << " is not a string";
Match = false;
return false;
}
std::string VS = V->getValue(Tmp).str();

スニペット9、タイプミス

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);

スニペット10、ゼロ除数

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。分母「d.s.low」==0。udivmoddi4.c61
  • V609ゼロで除算します。分母「d.s.low」==0。udivmoddi4.c62

これがバグなのか、トリッキーな矛盾なのかはわかりませんが、コードは奇妙に見えます。これには2つの正規整数変数があり、一方が他方で除算されます。しかし、興味深い部分は、両方の変数がゼロの場合にのみ除算演算が行われることです。実行するタスクは何ですか?

スニペット11、コピー&ペースト

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ライセンスを取得する方法」を参照してください。読んでいただきありがとうございます!

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です