
De vez em quando, temos que escrever artigos sobre como verificar a próxima versão de um compilador. Não é interessante. No entanto, como mostra a prática, se isso não for feito por muito tempo, as pessoas começam a duvidar se o analisador PVS-Studio merece o título de um bom pegador de bugs e vulnerabilidades potenciais. Talvez o novo compilador já saiba fazer isso? Sim, os compiladores não param. No entanto, o PVS-Studio também está desenvolvendo, demonstrando repetidamente a capacidade de encontrar erros até mesmo no código de projetos de alta qualidade como compiladores.
É hora de verificar novamente o código do Clang
Para ser honesto, peguei o artigo " Verificando o compilador GCC 10 usando PVS-Studio " como base para este texto . Então, se você acha que já leu alguns parágrafos em algum lugar, então não pense :).
Não é nenhum segredo que os compiladores têm seus próprios analisadores de código estático integrados, e eles também estão sendo desenvolvidos. Portanto, de vez em quando escrevemos artigos sobre como o analisador estático PVS-Studio pode encontrar erros mesmo dentro de compiladores e que não estamos comendo pão à toa :).
Na verdade, você não pode comparar analisadores estáticos clássicos com compiladores. Os analisadores estáticos não são apenas para encontrar erros no código, mas também uma infraestrutura desenvolvida. Por exemplo, esta é a integração com sistemas como SonarQube, PlatformIO, Azure DevOps, Travis CI, CircleCI, GitLab CI / CD, Jenkins, Visual Studio. Esses são mecanismos avançados para supressão em massa de avisos, que permitem que você comece a usar o PVS-Studio rapidamente, mesmo em um grande projeto antigo. Esta é uma distribuição de notificação. E assim por diante. No entanto, ainda é a primeira pergunta a ser feita: "O PVS-Studio pode encontrar algo que os compiladores não podem encontrar?" Isso significa que iremos escrever artigos sobre como verificar esses compiladores.
Vamos voltar ao tópico de verificação do projeto Clang. Não há necessidade de insistir neste projeto e dizer o que é. Na verdade, não apenas o código do Clang 11 em si foi testado, mas também a biblioteca LLVM 11 na qual ele foi construído. Do ponto de vista deste artigo, não faz diferença se um defeito for encontrado no compilador ou no código da biblioteca.
O código Clang / LLVM me pareceu muito mais claro do que o código GCC. Pelo menos todas essas macros terríveis estão faltando, e os recursos modernos da linguagem C ++ são usados ativamente.
Apesar disso, o projeto é grande e sem configurações preliminares do analisador ainda é muito tedioso para visualizar o relatório. Basicamente, meio-falsos positivos interferem. Por semi-falsos positivos, quero dizer situações em que o analisador está formalmente certo, mas não faz sentido avisar. Por exemplo, muitos desses positivos são emitidos para testes de unidade e código gerado.
Exemplo para testes:
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);
O analisador avisa que as variáveis são atribuídas aos mesmos valores que já contêm:
- V1048 A variável 'Spaces.SpacesInParentheses' foi atribuída ao mesmo valor. FormatTest.cpp 11554
- V1048 A variável 'Spaces.SpacesInCStyleCastParentheses' foi atribuída ao mesmo valor. FormatTest.cpp 11556
Formalmente, o analisador retornou a resposta correta, e este é o fragmento de código que deve ser simplificado ou corrigido. Ao mesmo tempo, é claro que de fato está tudo bem e que também não adianta editar algo.
Outro exemplo: o analisador emite um grande número de avisos para o arquivo Options.inc gerado automaticamente. Você pode ver a "folha de código" no arquivo:

E o PVS-Studio envia avisos para tudo isso:
- V501 Existem subexpressões idênticas à esquerda e à direita do operador '==': nullptr == nullptr Options.inc 26
- V501 Existem subexpressões idênticas à esquerda e à direita do operador '==': nullptr == nullptr Options.inc 27
- V501 Existem subexpressões idênticas à esquerda e à direita do operador '==': nullptr == nullptr Options.inc 28
- e assim por diante para cada linha ...
Tudo isso não é assustador. Tudo isso pode ser derrotado: desabilite a verificação de arquivos desnecessários, marque algumas macros e funções, suprima certos tipos de alarmes e assim por diante. É possível, mas fazer isso como parte da tarefa de escrever um artigo não é interessante. Portanto, fiz exatamente o mesmo que no artigo sobre o compilador GCC. Estudei o relatório até ter 11 exemplos de código interessantes para escrever um artigo. Por que 11? Eu pensei que como a versão do Clang é 11, então os fragmentos são 11 :).
11 snippets de código suspeitos
Fragmento N1, divisão do módulo por um

Erro legal! Eu amo estes!
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);
}
....
}
Aviso PVS-Studio: V1063 A operação módulo por 1 não faz sentido. O resultado sempre será zero. llvm-stress.cpp 631 A
divisão do módulo é usada para obter um valor aleatório de 0 ou 1. Mas, aparentemente, esse valor de 1 é confuso, e as pessoas fazem o padrão de erro clássico, dividindo por um, embora seja necessário dividir por dois. A operação X% 1 não tem sentido, pois o resultado é sempre 0 . Versão correta do código:
if (isa<FixedVectorType>(Val0->getType()) && (getRandom() % 2)) {
Diagnostics V1063, que apareceu recentemente no PVS-Studio, é absurdamente simples, mas, como você pode ver, funciona.
Como sabemos, os desenvolvedores de compiladores observam o que estamos fazendo e tomam emprestadas nossas melhores práticas. Não há nada de errado com isso. Estamos satisfeitos que PVS-Studio seja o motor do progresso . Definimos o tempo após o quanto o mesmo diagnóstico aparecerá no Clang e no GCC :).
Fragmento N2, erro de digitação na condição
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;
}
Aviso do PVS-Studio: V501 Existem subexpressões idênticas à esquerda e à direita do operador '&&' :! T1.isNull () &&! T1.isNull () SemaOverload.cpp 9493 Verificando
duas vezes ! T1.isNull () . Este é um erro de digitação óbvio e a segunda parte da condição deve verificar a variável T2 .
Fragmento N3, potencial fora dos limites da matriz
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();
....
}
Aviso do PVS-Studio: A saturação da matriz V557 é possível. O índice 'Índice' está apontando além do limite da matriz. ASTReader.cpp 7318
Suponha que a matriz contenha um elemento e a variável Index também seja um. A condição (1> 1) é falsa e, como resultado, o array será saturado. Verificação correta:
if (Index >= DeclsLoaded.size()) {
Fragmento N4, a ordem de avaliação dos argumentos
void IHexELFBuilder::addDataSections() {
....
uint32_t SecNo = 1;
....
Section = &Obj->addSection<OwnedDataSection>(
".sec" + std::to_string(SecNo++), RecAddr,
ELF::SHF_ALLOC | ELF::SHF_WRITE, SecNo);
....
}
Aviso PVS-Studio: V567 comportamento não especificado. A ordem de avaliação do argumento não está definida para a função 'addSection'. Considere inspecionar a variável 'SecNo'. Object.cpp 1223
Observe que o argumento SecNo é usado duas vezes e incrementado ao longo do caminho. É simplesmente impossível dizer em que ordem os argumentos serão avaliados. Portanto, o resultado irá variar dependendo da versão do compilador ou das configurações de compilação.
Deixe-me explicar isso com um exemplo sintético:
#include <cstdio>
int main()
{
int i = 1;
printf("%d, %d\n", i, i++);
return 0;
}
Dependendo do compilador, tanto "1, 2" como "2, 1" podem ser impressos. Usando o Compiler Explorer, obtenho os seguintes resultados:
- um programa compilado com Clang 11.0.0 produz : 1, 1.
- um programa compilado com GCC 10.2 produz : 2, 1.
Curiosamente, para este caso simples, o compilador Clang emite um aviso:
<source>:6:26: warning:
unsequenced modification and access to 'i' [-Wunsequenced]
printf("%d, %d\n", i, i++);
Aparentemente, em condições reais, esse aviso não ajudou. O diagnóstico está desabilitado, por ser inconveniente para o uso prático, ou o compilador não foi capaz de alertar sobre um caso mais complexo.
Fragmento N5, novo teste estranho
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;
}
....
}
Aviso PVS-Studio: Verificação recorrente V571. A condição 'if (! NameOrErr)' já foi verificada na linha 4666. ELFDumper.cpp 4667
A segunda verificação duplica a primeira e é redundante. Talvez o segundo cheque possa simplesmente ser removido. Mas isso é provavelmente um erro de digitação e uma variável diferente deve ser usada na segunda condição.
Snippet N6, desreferenciando um ponteiro potencialmente nulo
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);
....
}
Aviso do PVS-Studio: V595 O ponteiro 'CDecl' foi utilizado antes de ser verificado em relação a nullptr. Verifique as linhas: 5275, 5284. RewriteObjC.cpp 5275
Durante a primeira verificação, o ponteiro CDecl é sempre desreferenciado ousadamente:
if (CDecl->isImplicitInterfaceDecl())
E apenas a partir do código escrito abaixo fica claro que esse ponteiro pode ser nulo:
(CDecl ? CDecl->ivar_size() : 0)
Provavelmente, a primeira verificação deve ter a seguinte aparência:
if (CDecl && CDecl->isImplicitInterfaceDecl())
Snippet N7, desreferenciando um ponteiro potencialmente nulo
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());
....
}
Aviso do PVS-Studio: V595 O ponteiro 'ND' foi utilizado antes de ser verificado em relação ao nullptr. Verifique as linhas: 2803, 2805. SemaTemplateInstantiate.cpp 2803
Uma variação do erro anterior. É perigoso cancelar a referência de um ponteiro sem primeiro verificá-lo se seu valor for obtido usando uma conversão dinâmica. Além disso, pelo código abaixo, você pode ver que essa verificação é necessária.
Fragmento N8, a função continua a execução apesar de um estado de erro
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();
....
}
Aviso do PVS-Studio: V1004 O ponteiro 'V' foi usado de maneira insegura após ter sido verificado em relação ao nullptr. Verifique as linhas: 61, 65. TraceTests.cpp 65 O
ponteiro V pode ser nulo. Esta é uma condição claramente errônea, que até é relatada. Porém, depois disso, a função continua a execução como se nada tivesse acontecido, o que levará ao desreferenciamento deste ponteiro muito nulo. Talvez eles tenham se esquecido de interromper a função, e a opção correta deve ser assim:
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();
Snippet N9, erro de digitação
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); // <=
}
}
Aviso do PVS-Studio: V1001 A variável 'T' é atribuída, mas não é usada no final da função. CommonArgs.cpp 873
Observe o fim da função. A variável local T é alterada, mas não é usada de forma alguma. Provavelmente, isso é um erro de digitação e a função deve terminar com as seguintes linhas de código:
T += F;
return Args.MakeArgString(T);
Fragmento N10, divisor é zero
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;
}
....
}
Avisos do PVS-Studio:
- V609 Mod por zero. Denominador 'dslow' == 0.udivmoddi4.c 61
- V609 Divide por zero. Denominador 'dslow' == 0.udivmoddi4.c 62
Não sei se isso é um erro ou uma ideia inteligente, mas o código é muito estranho. Existem duas variáveis inteiras comuns, e uma é divisível pela outra. Curiosamente, isso só acontece se ambas as variáveis forem zero. O que tudo isso significa?
Fragmento N11, Copiar e Colar
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;
}
....
}
Aviso do PVS-Studio: V581 As expressões condicionais das instruções 'if' situadas lado a lado são idênticas. Verifique as linhas: 3108, 3113. MallocChecker.cpp 3113
O trecho de código foi copiado, mas não alterado de nenhuma forma. O segundo trecho deve ser removido ou modificado para começar a realizar alguma verificação útil.
Conclusão

Deixe-me lembrá-lo de que você pode usar esta opção de licença gratuita para verificar projetos de código aberto . Aliás, existem outras opções de licenciamento gratuito do PVS-Studio, inclusive para projetos encerrados. Eles estão listados aqui: " Opções de licenciamento grátis do PVS-Studio ". Obrigado pela atenção.
Nossos outros artigos sobre verificação do compilador
- Análise LLVM (Clang) (agosto de 2011), segunda análise (agosto de 2012), terceira análise (outubro de 2016), quarta análise (abril de 2019)
- Análise GCC (agosto de 2016), segunda análise (abril de 2020)
- Análise do Huawei Ark Compiler (dezembro de 2019)
- Análise da plataforma do compilador .NET ("Roslyn") (dezembro de 2015), segunda análise (abril de 2019)
- Análise do Roslyn Analyzers (agosto de 2019)
- Análise PascalABC.NET (março de 2017)

Se você deseja compartilhar este artigo com um público que fala inglês, por favor, use o link de tradução: Andrey Karpov. Verificando o Clang 11 com PVS-Studio .