PVS-Studio로 Clang 11 확인

(2020 년 10 월 28 일 )

때때로 우리는 컴파일러의 또 다른 새로운 버전을 확인하는 방법에 대한 기사를 작성해야합니다. 별로 재미 없어요. 그러나 실습에서 알 수 있듯이 잠시 동안 그만두면 사람들은 PVS-Studio가 버그와 취약성의 좋은 포수라는 제목의 가치가 있는지 의심하기 시작합니다. 새 컴파일러도 그렇게 할 수 있다면 어떨까요? 물론 컴파일러는 진화하지만 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 자체의 코드뿐만 아니라 그 기반이되는 LLVM 11 라이브러리의 코드도 확인했습니다. 이 기사의 관점에서 컴파일러 나 라이브러리의 코드에서 결함이 발견되었는지는 중요하지 않습니다.

Clang / LLVM의 코드가 GCC의 코드보다 훨씬 명확하다는 것을 알았습니다. 적어도 모든 끔찍한 매크로로 가득 차 있지 않고 C ++의 최신 기능을 광범위하게 사용합니다.

그래도 프로젝트는 사전 사용자 지정없이 분석 보고서를 검사하는 데 지루할만큼 충분히 큽니다. 대부분 방해가되는 것은 “반 거짓”긍정입니다. “반 거짓”긍정이란 분석기가 특정 문제를 지적하기에 기술적으로 정확하지만 이러한 경고가 실용적이지 않은 경우를 의미합니다. 예를 들어, 이러한 경고는 대부분 단위 테스트 및 생성 된 코드를 참조합니다.

다음은 단위 테스트의 예입니다.

Spaces.SpacesInParentheses = false; // <=
Spaces.SpacesInCStyleCastParentheses = true; // <=
verifyFormat("Type *A = ( Type * )P;", Spaces);
verifyFormat("Type *A = ( vector )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);

The 분석기는 변수에 이미있는 것과 동일한 값이 할당되어 있다고 경고합니다.

  • 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 There ==연산자의 왼쪽과 오른쪽에 동일한 하위 표현식입니다. nullptr == nullptr Options.inc 28
  • 등 — 한 줄에 하나의 경고…

그렇지만 큰 문제는 아닙니다. 분석에서 관련없는 파일을 제외하고, 특정 매크로 및 기능을 표시하고, 특정 진단 유형을 억제하는 등의 방법으로 문제를 해결할 수

있습니다 . 예, 가능합니다.하지만 기사를 쓸 때하는 것은별로 흥미로운 일이 아닙니다. 그래서 GCC 컴파일러 검사에 대한 기사에서와 동일한 작업을 수행했습니다. 기사에 포함 할 11 개의 흥미로운 예제를 수집 할 때까지 보고서를 계속 읽었습니다. 왜 11일까요? Clang의 11 번째 버전이기 때문에 11 개의 예제가 필요하다고 생각했습니다. :).

11 개의 의심스러운 코드 스 니펫

스 니펫 1, 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(Val0->getType()) && (getRandom() % 1)) {
unsigned NumElem =
cast(Val0->getType())->getNumElements();
CondTy = FixedVectorType::get(CondTy, NumElem);
}
....
}

PVS-Studio 진단 메시지 : V1063 모듈로 by 1 연산은 의미가 없습니다. 결과는 항상 0입니다. llvm-stress.cpp 631

프로그래머는 모듈로 연산을 사용하여 0 또는 1의 임의 값을 얻습니다. 그러나 1 값은 개발자를 혼란스럽게하는 것 같습니다. 모듈로 연산이 2가 아닌 1에서 수행되는 고전적인 안티 패턴을 작성하도록합니다. X % 1 연산은 항상 0 . 다음은 고정 버전입니다.

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

최근에 추가 된 V1063 진단은 매우 간단하지만 보시다시피 완벽하게 작동합니다.

우리는 컴파일러 개발자가 우리의 작업을 주시하고 우리의 아이디어를 빌린다는 것을 알고 있습니다. 괜찮습니다. PVS-Studio가 발전의 원동력이라는 사실을 알게되어 기쁩니다 . 유사한 진단이 Clang 및 GCC에 표시되는 데 얼마나 걸리는지 살펴 보겠습니다.

조건의 오타 인 스 니펫 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 () 검사는 두 번 수행됩니다. 이것은 분명히 오타입니다. 조건의 두 번째 부분은 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

배열이 하나의 요소를 저장하고 Index 변수의 값도 1이라고 가정합니다. 그런 다음 (1> 1) 조건 false이므로 배열이 경계를 넘어 인덱싱됩니다. 올바른 확인 방법은 다음과 같습니다.

if (Index >= DeclsLoaded.size()) {

스 니펫 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 인수는 두 번 사용되며 그 동안 증가합니다. 문제는 인수가 평가 될 정확한 순서를 알 수 없다는 것입니다. 따라서 결과는 컴파일러 버전 또는 컴파일 매개 변수에 따라 달라집니다.

다음은이 점을 설명하는 합성 예제입니다.

#include 
int main()
{
int i = 1;
printf("%d, %d\n", i, i++);
return 0;
}

컴파일러에 따라이 코드는 “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.cpp 4667

두 번째 검사는 첫 번째 검사의 복제본이므로 중복됩니다. 안전하게 제거 할 수 있습니다. 하지만 오타가 포함되어 있고 다른 변수를 확인하기위한 것일 가능성이 더 높습니다.

스 니펫 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 CDecl포인터가 nullptr에 대해 확인되기 전에 사용되었습니다. 확인 줄 : 5275, 5284. RewriteObjC.cpp 5275

첫 번째 확인을 수행 할 때 개발자는 CDecl 포인터를 역 참조하는 것을 주저하지 않습니다.

if (CDecl->isImplicitInterfaceDecl())

하지만 코드를 몇 줄 더 살펴보면 포인터가 null 일 수 있음이 분명해집니다.

(CDecl ? CDecl->ivar_size() : 0)

첫 번째 검사는 아마도 다음과 같이 보일 것입니다 :

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

스 니펫 7, 잠재적 인 널 포인터 역 참조

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 V포인터가 nullptr에 대해 확인 된 후 안전하지 않게 사용되었습니다. 체크 라인 : 61, 65. TraceTests.cpp 65

V 포인터는 널 포인터 일 수 있습니다. 이것은 분명히 오류 상태이며 오류 메시지와 함께보고됩니다. 그러나 함수는 아무 일도 일어나지 않은 것처럼 계속 실행되며 결국 널 포인터를 역 참조하게됩니다. 프로그래머는이 시점에서 함수가 중지되기를 원했을 것입니다.이 경우 다음과 같이 수정해야합니다.

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 The T 변수가 할당되었지만 함수의 끝에서 사용되지 않습니다. CommonArgs.cpp 873

함수의 마지막 줄을보십시오. 지역 변수 T 는 변경되지만 어떤 방식으로도 사용되지 않습니다. 오타 여야하며 함수는 다음과 같이 끝나야합니다.

T += F;
return Args.MakeArgString(T);

스 니펫 10, 0은 제수

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 0으로 수정합니다. 분모‘d.s.low’== 0. udivmoddi4.c 61
  • V609 0으로 나눕니다. 분모‘d.s.low’== 0. udivmoddi4.c 62

버그인지 까다로운 장치인지 모르겠지만 코드가 이상하게 보입니다. 두 개의 일반 정수 변수가 있으며, 그중 하나는 다른 것으로 나뉩니다. 그러나 흥미로운 부분은 두 변수가 모두 0 인 경우에만 나누기 연산이 발생한다는 것입니다. 어떤 작업을 수행해야합니까?

스 니펫 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 라이선스를 얻는 방법 ”을 참조하십시오. 읽어 주셔서 감사합니다!

답글 남기기

이메일 주소를 발행하지 않을 것입니다. 필수 항목은 *(으)로 표시합니다