Usando o aprendizado de máquina na análise estática do código-fonte do programa

Usando o aprendizado da máquina na análise estática do código-fonte do programa

O aprendizado de máquina está profundamente enraizado em várias áreas da atividade humana: do reconhecimento de fala ao diagnóstico médico. A popularidade dessa abordagem é tão grande que eles tentam usá-la sempre que possível. Algumas tentativas de substituir as abordagens clássicas por redes neurais não são tão bem-sucedidas. Vamos dar uma olhada no aprendizado de máquina do ponto de vista da criação de analisadores de código estático eficazes para encontrar bugs e possíveis vulnerabilidades.

A equipe do PVS-Studio é frequentemente questionada se queremos começar a usar o aprendizado de máquina para encontrar erros no código fonte dos programas. Resposta curta: sim, mas muito limitada. Acreditamos que, com o uso do aprendizado de máquina em problemas de análise de código, existem muitas armadilhas. Na segunda parte do artigo, falaremos sobre eles. Vamos começar com uma revisão de novas soluções e idéias.

Novas abordagens


Atualmente, já existem muitas implementações de analisadores estáticos baseados em ou usando aprendizado de máquina, incluindo aprendizado profundo e PNL para detecção de erros. Não apenas os entusiastas, mas também as grandes empresas, como Facebook, Amazon ou Mozilla, chamaram a atenção para o potencial do aprendizado de máquina ao procurar erros. Alguns projetos não são analisadores estáticos completos, mas apenas encontram erros específicos durante as confirmações.

Curiosamente, quase todos estão posicionados como produtos que mudam o jogo que, com a ajuda da inteligência artificial, mudarão o processo de desenvolvimento.


Considere alguns exemplos conhecidos:

  1. Deepcode
  2. Infer, Sapienz, SapFix
  3. Embold
  4. Origem {d}
  5. Assistente inteligente de confirmação, confirmação
  6. CodeGuru

Deepcode


O Deep Code é uma ferramenta de busca de vulnerabilidades no código de programas escritos em Java, JavaScript, TypeScript e Python, nos quais o aprendizado de máquina está presente como um componente. Segundo Boris Paskalev, mais de 250 mil regras já funcionam. Essa ferramenta é treinada com base nas alterações feitas pelos desenvolvedores no código fonte de projetos abertos (um milhão de repositórios). A própria empresa diz que o projeto deles é Grammarly para desenvolvedores.



Em essência, este analisador compara sua solução com seu banco de dados de projetos e oferece a melhor solução estimada com a experiência de outros desenvolvedores.

Em maio de 2018, os desenvolvedores escreveram que o suporte à linguagem C ++ estava sendo preparado, no entanto, essa linguagem ainda não é suportada. Embora seja indicado no próprio site que um novo idioma pode ser adicionado em questão de semanas, devido ao fato de que apenas uma etapa depende da análise do idioma.





Um grupo de publicações sobre os métodos nos quais o analisador se baseia também é publicado no site.

Inferir


O Facebook está tentando amplamente introduzir novas abordagens em seus produtos. Eles não ignoraram sua atenção e aprendizado de máquina. Em 2013, eles compraram uma startup que estava desenvolvendo um analisador estático baseado em máquina. E em 2015, o código fonte do projeto tornou-se aberto .

Infer é um analisador estático para projetos escritos em Java, C, C ++ e Objective-C, desenvolvidos pelo Facebook. Segundo o site, ele também é usado no Amazon Web Services, Oculus, Uber e outros projetos populares.

O Infer atualmente é capaz de detectar erros relacionados à desreferenciação de um ponteiro nulo, vazamentos de memória. Infer baseia-se na lógica de Hoar, lógica de separação e bi-abdução, bem como na teoria da interpretação abstrata. O uso dessas abordagens permite ao analisador dividir o programa em pequenos blocos (pedaços) e analisá-los independentemente um do outro.

Você pode tentar usar o Infer em seus projetos, no entanto, os desenvolvedores alertam que, embora em projetos do Facebook, os hits úteis representem 80% dos resultados, em outros projetos, um número baixo de falsos positivos não é garantido. Alguns dos erros que a Infer ainda não conseguiu encontrar, mas os desenvolvedores estão trabalhando na introdução desses gatilhos:

  • saindo da matriz;
  • exceções tipográficas;
  • vazamento de dados não verificados;
  • condição de corrida.

Sapfix


SapFix é uma ferramenta de edição automatizada. Ele recebe informações da Sapienz, uma ferramenta de automação de teste e do analisador estático Infer, e com base nas alterações e mensagens mais recentes, o Infer escolhe uma das várias estratégias para corrigir erros.



Em alguns casos, o SapFix reverte todas ou parte das alterações. Em outros casos, ele tenta resolver o problema gerando um patch a partir de seu conjunto de padrões de correção. Esse conjunto é formado a partir dos modelos de edição compilados pelos próprios programadores a partir do conjunto de edições já feitas uma vez. Se esse modelo não corrigir o erro, o SapFix tentará ajustá-lo à situação, fazendo pequenas modificações na árvore de sintaxe abstrata até encontrar uma solução em potencial.

Mas como uma solução em potencial não é suficiente, o SapFix coleta várias soluções selecionadas com base em três perguntas: existem erros de compilação, há um travamento, a edição introduz novos travamentos. Após as edições serem totalmente testadas, as correções são enviadas para revisão ao programador que decide qual das edições melhor resolve o problema.

Embold


O Embold é uma plataforma de inicialização para análise estática do código fonte dos programas, que antes da renomeação era chamada Gamma. A análise estática é realizada com base em nossos próprios diagnósticos, bem como com base em analisadores internos, como Cppheck, SpotBugs, SQL Check e outros.



Além dos próprios diagnósticos, a ênfase está na capacidade de exibir visualmente os infográficos pela carga da base de código e visualizar convenientemente os erros encontrados, bem como procurar refatoração. Além disso, esse analisador possui um conjunto de antipadrões que permite detectar problemas na estrutura do código no nível de classes e métodos, além de várias métricas para calcular a qualidade do sistema.



Uma das principais vantagens é o sistema inteligente de sugestões de soluções e revisões, que, além dos diagnósticos usuais, verifica as revisões com base em informações sobre alterações anteriores.



Usando a PNL, o Embold divide o código em partes e procura interconexões e dependências entre funções e métodos entre elas, o que economiza tempo de refatoração.



Assim, o Embold oferece principalmente uma visualização conveniente dos resultados da análise do seu código-fonte por vários analisadores, bem como seus próprios diagnósticos, alguns dos quais baseados no aprendizado de máquina.

Origem {d}


A fonte {d} é a mais aberta em termos de como implementá-la nos analisadores que examinamos. É também uma solução de código aberto . No site deles, você pode (em troca do seu endereço de e-mail) obter um livreto com uma descrição das tecnologias que eles usam. Além disso, ele contém um link para a base de publicação que eles coletaram relacionados ao uso de aprendizado de máquina para análise de código, além de um repositório com um conjunto de dados para treinamento em código. O produto em si é uma plataforma inteira para analisar o código-fonte e o software, e se concentra, não nos desenvolvedores, mas no link dos gerentes. Entre suas capacidades, há uma funcional para identificar o volume de dívida técnica, gargalos no processo de desenvolvimento e outras estatísticas globais do projeto.



Eles baseiam sua abordagem da análise de código assistida por máquina na Hipótese Natural, formulada no artigo " Sobre a naturalidade do software ".

“As linguagens de programação, em teoria, são complexas, flexíveis e poderosas, mas os programas que as pessoas reais realmente escrevem são, na maioria das vezes, simples e razoavelmente repetitivos e, portanto, possuem propriedades estatísticas úteis e previsíveis que podem ser expressas em estatísticas. modelos de linguagem e uso para tarefas de desenvolvimento de software. ”

Com base nessa hipótese, quanto maior a base de código para o treinamento do analisador, mais propriedades estatísticas se destacam e maior a precisão das métricas alcançadas pelo treinamento.

Para analisar o código, a origem {d} usa o serviço Babelfish, que pode analisar o arquivo de código em qualquer um dos idiomas disponíveis, obter uma árvore de sintaxe abstrata e convertê-lo em uma árvore de sintaxe universal.



No entanto, a fonte {d} não procura erros no código. Com base na árvore, usando o aprendizado de máquina com base em todo o projeto, a fonte {d} revela como o código é formatado, qual estilo de codificação é usado no projeto e ao confirmar, e se o novo código não corresponder ao estilo de código do projeto, ele fará as alterações apropriadas.





O treinamento é guiado por vários elementos básicos: espaços, guias, quebras de linha etc.



Você pode ler mais sobre isso em sua publicação: " STYLE-ANALYZER: corrigindo inconsistências de estilo de código com algoritmos não supervisionados interpretáveis ".

Em geral, a fonte {d} é uma ampla plataforma para coletar uma ampla variedade de estatísticas sobre o código-fonte e o processo de desenvolvimento do projeto, desde o cálculo da eficácia dos desenvolvedores até a identificação dos custos de tempo para as revisões de código.

Confirmação inteligente


O Clever-Commit é um analisador criado pela Mozilla em colaboração com a Ubisoft. Ele é baseado no estudo CLEVER da Ubisoft (Combinando níveis de técnicas de prevenção e resolução de bugs) e em seu Commit Assistant baseado em produto, que identifica confirmações suspeitas que provavelmente contêm um erro. Devido ao fato de o CLEVER se basear na comparação de códigos, ele não apenas indica um código perigoso, mas também faz sugestões sobre possíveis correções. De acordo com a descrição, em 60-70% dos casos, o Clever-Commit encontra áreas problemáticas e com a mesma frequência oferece correções corretas para elas. Em geral, há pouca informação sobre esse projeto e sobre os erros que ele consegue encontrar.

CodeGuru


E, mais recentemente, a lista de analisadores que usam aprendizado de máquina foi reabastecida com um produto da Amazon chamado CodeGuru. Este serviço é baseado no aprendizado de máquina, que permite encontrar erros no código e identificar seções dispendiosas. Até agora, a análise é apenas para código Java, mas eles escrevem sobre o suporte a outras linguagens no futuro. Embora tenha sido anunciado recentemente, o CEO da AWS (Amazon Web Services), Andy Jassi, diz que o usa há muito tempo na própria Amazon.

O site diz que o treinamento foi realizado na base de código da própria Amazon, bem como em mais de 10.000 projetos de código aberto.

De fato, o serviço é dividido em duas partes: CodeGuru Reviewer, treinado pela pesquisa de regras associativas e pela procura de erros no código, e CodeGuru Profiler, que monitora o desempenho do aplicativo.



Em geral, poucas informações foram publicadas sobre este projeto. O site afirma que, para aprender a detectar desvios das "melhores práticas", o Reviewer analisa as bases de código da Amazon e procura solicitações de recebimento contendo chamadas da API da AWS. Em seguida, ele analisa as alterações feitas e as compara com os dados da documentação, que é analisada em paralelo. O resultado é um modelo de "melhores práticas".

Também é dito que as recomendações para código personalizado melhoram depois de receber feedback sobre as recomendações.

A lista de erros aos quais o Revisor responde é bastante borrada, pois nenhuma documentação específica para erros foi publicada:
  • Práticas recomendadas da AWS
  • Concorrência
  • Vazamentos de recursos
  • Vazamento de informações confidenciais
  • "Práticas recomendadas" comuns para codificação

Nosso ceticismo


Agora, vamos analisar o problema de encontrar erros através dos olhos de nossa equipe, que desenvolve analisadores estáticos há muitos anos. Vemos vários problemas de alto nível na aplicação do treinamento, sobre os quais queremos falar. Porém, no começo, dividimos aproximadamente todas as abordagens de ML em dois tipos:

  1. Treine manualmente um analisador estático para procurar vários problemas usando exemplos de código sintético e real;
  2. Treine os algoritmos em um grande número de código-fonte aberto (GitHub) e altere o histórico, após o qual o próprio analisador começará a detectar erros e até sugerir correções.

Falaremos sobre cada direção separadamente, pois elas terão várias deficiências inerentes. Depois disso, acho que ficará claro para os leitores por que não negamos a possibilidade de aprendizado de máquina, mas também não compartilhamos entusiasmo.

Nota Analisamos a perspectiva de desenvolver um analisador estático universal de uso geral. Nosso foco é o desenvolvimento de um analisador que não se concentre em uma base de código específica, mas que qualquer equipe possa usar em qualquer projeto.

Treinamento manual do analisador de estática


Suponha que desejemos usar o ML para que o analisador comece a procurar anomalias no seguinte formato no código:

if (A == A) 

É estranho comparar uma variável consigo mesma. Podemos escrever muitos exemplos de código correto e incorreto e treinar o analisador para procurar esses erros. Além disso, é possível adicionar exemplos reais de erros já encontrados nos testes. A questão, é claro, é onde conseguir esses exemplos. Mas consideraremos que é possível. Por exemplo, acumulamos vários exemplos desses erros: V501 , V3001 , V6001 .

Portanto, é possível procurar esses defeitos no código usando algoritmos de aprendizado de máquina? Você pode. Mas não está claro por que fazer isso!

Veja, para treinar o analisador, precisamos dedicar muito esforço na preparação de exemplos para treinamento. Ou marque o código de aplicativos reais, indicando onde jurar e onde não. De qualquer forma, muito trabalho terá que ser feito, pois deve haver milhares de exemplos de treinamento. Ou dezenas de milhares.

Afinal, queremos procurar não apenas casos (A == A), mas também:

  • if (X e&A == A)
  • if (A + 1 == A + 1)
  • if (A [i] == A [i])
  • se ((A) == (A))
  • e assim por diante.



Agora vamos ver como um diagnóstico tão simples seria implementado no PVS-Studio:

 void RulePrototype_V501(VivaWalker &walker, const Ptree *left, const Ptree *right, const Ptree *operation) { if (SafeEq(operation, "==") && SafeEqual(left, right)) { walker.AddError(" , !", left, 501, Level_1, "CWE-571"); } } 

E é isso. Nenhuma base de treinamento de amostra é necessária!

No futuro, os diagnósticos devem ser ensinados para levar em consideração várias exceções e entender que você precisa jurar em (A [0] == A [1-1]). No entanto, tudo isso é muito fácil de programar. Mas apenas com a base de exemplos de treinamento, tudo será ruim.

Observe que em ambos os casos ainda será necessário um sistema de teste, documentação de redação etc. No entanto, o esforço para criar um novo diagnóstico está claramente do lado da abordagem clássica, onde a regra é simplesmente codificada no código.

Vamos olhar agora para alguma outra regra. Por exemplo, que o resultado de algumas funções deve ser usado. Não faz sentido chamá-los sem usar o resultado. Aqui estão alguns desses recursos:
  • malloc
  • memcmp
  • string :: vazio

Em geral, é isso que os diagnósticos do V530 implementados no PVS-Studio fazem .

Portanto, queremos procurar chamadas para essas funções em que o resultado de seu trabalho não seja usado. Para fazer isso, você pode gerar muitos testes. E achamos que tudo vai funcionar bem. Mas, novamente, não está claro por que isso é necessário.

A implementação do diagnóstico V530 com todas as exceções no analisador PVS-Studio é de 258 linhas de código, das quais 64 são comentários. Além disso, há uma tabela com anotações de funções, onde se observa que o resultado delas deve ser usado. Reabastecer esta tabela é muito mais fácil do que criar exemplos sintéticos.

A situação será ainda pior com diagnósticos que usam análise de fluxo de dados. Por exemplo, o analisador PVS-Studio pode rastrear o valor dos ponteiros, o que permite encontrar esse vazamento de memória:

 uint32_t* BnNew() { uint32_t* result = new uint32_t[kBigIntSize]; memset(result, 0, kBigIntSize * sizeof(uint32_t)); return result; } std::string AndroidRSAPublicKey(crypto::RSAPrivateKey* key) { .... uint32_t* n = BnNew(); .... RSAPublicKey pkey; pkey.len = kRSANumWords; pkey.exponent = 65537; // Fixed public exponent pkey.n0inv = 0 - ModInverse(n0, 0x100000000LL); if (pkey.n0inv == 0) return kDummyRSAPublicKey; // <= .... } 

Um exemplo é retirado do artigo " Chromium: memory leaks ". Se a condição (pkey.n0inv == 0) for atendida , a função sairá sem liberar o buffer, cujo ponteiro está armazenado na variável n .

Do ponto de vista do PVS-Studio, não há nada complicado. O analisador estudou a função BnNew e lembrou que ele retorna um ponteiro para um bloco de memória alocada. Em outra função, ele notou que é possível uma situação em que o buffer não é liberado e o ponteiro para ele é perdido quando a função sai.

Um algoritmo de rastreamento de valor geral funciona. Não importa como o código está escrito. Não importa o que mais há na função que não esteja relacionada ao trabalho com ponteiros. O algoritmo é universal e o diagnóstico do V773 encontra muitos erros em vários projetos. Veja como os fragmentos de código são diferentes onde os erros são detectados!

Não somos especialistas em aprendizado de máquina, mas parece que haverá grandes problemas. Existem inúmeras maneiras de escrever código com vazamentos de memória. Mesmo que a máquina seja treinada para rastrear o valor das variáveis, será necessário treiná-la para entender que existem chamadas de função.

Há uma suspeita de que tantos exemplos serão necessários para o treinamento que a tarefa se torna assustadora. Não dizemos que é irrealizável. Duvidamos que os custos de criação de um analisador sejam compensadores.

Analogia. Uma analogia vem à mente com uma calculadora, onde, em vez de diagnósticos, é necessário programar operações aritméticas. Temos certeza de que você pode ensinar uma calculadora baseada em ML a adicionar números, introduzindo uma base de conhecimento sobre o resultado das operações 1 + 1 = 2, 1 + 2 = 3, 2 + 1 = 3, 100 + 200 = 300 e assim por diante. Como você sabe, a conveniência de desenvolver essa calculadora é uma grande questão (se uma subvenção não for alocada para ela :). Uma calculadora muito mais simples, rápida, precisa e confiável pode ser escrita usando a operação "+" comum no código.

Conclusão O método irá funcionar. Mas usá-lo, em nossa opinião, não faz sentido prático. O desenvolvimento consumirá mais tempo e o resultado será menos confiável e preciso, principalmente se se tratar da implementação de diagnósticos complexos com base na análise do fluxo de dados.

Aprendendo com muita fonte aberta


Bem, descobrimos exemplos sintéticos manuais, mas existe o GitHub. Você pode acompanhar o histórico de confirmações e derivar padrões de alterações / correções de código. Depois, você pode apontar não apenas seções do código suspeito, mas até sugerir uma maneira de corrigi-lo.

Se você parar nesse nível de detalhe, tudo ficará bem. O diabo, como sempre, está nos detalhes. Vamos falar sobre esses detalhes.

A primeira nuance. Fonte de dados.

As edições no GitHub são bastante caóticas e variadas. As pessoas costumam ter preguiça de fazer confirmações atômicas e fazer várias alterações no código de uma só vez. Você mesmo sabe como isso acontece: eles corrigiram o erro e, ao mesmo tempo, refatoraram um pouco ("E aqui vou adicionar o processamento de um caso desse tipo ao mesmo tempo ..."). Mesmo assim, pode não estar claro para uma pessoa se essas alterações estão relacionadas ou não.

O problema é como distinguir os erros reais da adição de novas funcionalidades ou outra coisa. Obviamente, você pode plantar 1.000 pessoas manualmente para marcar confirmações. As pessoas terão que indicar que corrigiram o erro aqui, refatorando aqui, nova funcionalidade aqui, requisitos alterados aqui e assim por diante.

Essa marcação é possível? Possível. Mas preste atenção na rapidez com que a mudança ocorre. Em vez de "aprender o próprio algoritmo com base no GitHub", já estamos discutindo como confundir centenas de pessoas por um longo tempo. Os custos de mão-de-obra e o custo de criar uma ferramenta aumentam bastante.

Você pode tentar identificar automaticamente onde exatamente os erros foram corrigidos. Para fazer isso, você deve analisar os comentários sobre confirmações, prestar atenção às pequenas edições locais, que, provavelmente, são exatamente a revisão de erros. É difícil dizer o quão bem você pode procurar automaticamente por correções de bugs. De qualquer forma, essa é uma grande tarefa que requer pesquisa e programação separadas.

Portanto, ainda não atingimos o treinamento, mas já existem nuances :).

A segunda nuance. Atraso no desenvolvimento.

Os analisadores que serão treinados com base em bancos de dados como o GitHub sempre estarão sujeitos a uma síndrome como "retardo mental". Isso ocorre porque as linguagens de programação mudam com o tempo.

O C # 8.0 introduziu os tipos de Referência Nula para ajudar a lidar com as Exceções de Referência Nula (NREs). O JDK 12 apresenta uma nova instrução switch ( JEP 325 ). No C ++ 17, tornou-se possível executar construções condicionais no estágio de compilação ( constexpr if ). E assim por diante

Linguagens de programação estão evoluindo. Além disso, como o C ++, são muito rápidos e ativos. Novos designs aparecem neles, novas funções padrão são adicionadas e assim por diante. Juntamente com os novos recursos, também aparecem novos padrões de erro que gostaríamos de identificar usando a análise de código estática.

E aqui o método de ensino em questão tem um problema: o padrão de erro já pode ser conhecido, existe um desejo de identificá-lo, mas não há nada com o que aprender.

Vejamos esse problema com um exemplo específico. O loop for baseado em intervalo apareceu no C ++ 11. E você pode escrever o seguinte código, repetindo todos os elementos no contêiner:

 std::vector<int> numbers; .... for (int num : numbers) foo(num); 

O novo ciclo trouxe um novo padrão de erro. Se o contêiner for alterado dentro do loop, isso levará à invalidação dos iteradores "shadow".

Considere o seguinte código incorreto:

 for (int num : numbers) { numbers.push_back(num * 2); } 

O compilador irá transformá-lo em algo como isto:

 for (auto __begin = begin(numbers), __end = end(numbers); __begin != __end; ++__begin) { int num = *__begin; numbers.push_back(num * 2); } 

Durante a operação push_back , a invalidação dos iteradores __begin e __end pode ocorrer se ocorrer alocação de memória dentro do vetor. O resultado será um comportamento indefinido do programa.

Portanto, o padrão de erro já é conhecido e descrito na literatura. O analisador PVS-Studio o diagnostica usando o diagnóstico V789 e já encontrou erros reais em projetos abertos.

Em quanto tempo haverá código novo suficiente no GitHub para perceber esse padrão? Boa pergunta ... Você precisa entender que, se o loop for baseado em intervalo aparecer, isso não significa que todos os programadores começaram imediatamente a usá-lo em massa. Pode levar anos até que muito código apareça usando um novo loop. Além disso, muitos erros devem ser cometidos e, em seguida, devem ser corrigidos para que o algoritmo possa observar o padrão nas alterações.

Quantos anos devem passar? Cinco? Dez?

Dez é demais e somos pessimistas? Nem um pouco. Oito anos se passaram no momento em que este artigo foi escrito, pois o loop for baseado em intervalo apareceu no C ++ 11. Mas até agora, apenas três casos desse erro foram gravados em nosso banco de dados. Três erros não são muitos e nem um pouco. Nenhuma conclusão deve ser tirada do seu número. O principal é que você pode confirmar que esse padrão de erro é real e faz sentido detectá-lo.

Agora compare essa quantidade, por exemplo, com este padrão de erro: o ponteiro é desreferenciado antes da verificação . No total, ao verificar projetos de código aberto, já identificamos 1716 casos.

Talvez você não deva procurar erros de loop com base no intervalo? Não. Apenas os programadores são inerciais, e esse operador está lentamente ganhando popularidade. Gradualmente, haverá muito código com sua participação e, consequentemente, também haverá mais erros.

Provavelmente, isso acontecerá somente após 10 a 15 anos a partir do momento em que o C ++ 11 apareceu. E agora uma pergunta filosófica. Já conhecendo o padrão de erro, esperaremos muitos anos até que muitos erros se acumulem em projetos abertos?

Se a resposta for "sim", é possível diagnosticar razoavelmente para todos os analisadores com base no ML o diagnóstico "retardo mental".

Se a resposta for não, então o que devo fazer? Não há exemplos. Para escrevê-los manualmente? Mas então voltamos ao capítulo anterior, onde consideramos escrever uma pessoa para muitos exemplos de aprendizado.

Isso pode ser feito, mas novamente surge a questão da conveniência. A implementação do diagnóstico V789 com todas as exceções no analisador PVS-Studio é de apenas 118 linhas de código, das quais 13 são comentários. I.e. Este é um diagnóstico muito simples que pode ser facilmente obtido e programado de maneira clássica.

Uma situação semelhante ocorrerá com outras inovações que aparecerem em outros idiomas. Como se costuma dizer, há algo em que pensar.

A terceira nuance. Documentação

Um componente importante de qualquer analisador estático é a documentação que descreve cada diagnóstico. Sem ele, o uso do analisador será extremamente difícil ou até impossível. Na documentação do PVS-Studio, temos uma descrição de cada diagnóstico, que fornece um exemplo de código incorreto e como corrigi-lo. Há também um link para o CWE, onde você pode ler uma descrição alternativa do problema. E, mesmo assim, às vezes algo é incompreensível para os usuários, e eles nos fazem perguntas esclarecedoras.

No caso de analisadores estáticos, que são baseados em algoritmos de aprendizado de máquina, o problema da documentação é, de alguma forma, silencioso. Supõe-se que o analisador simplesmente indique um local que lhe parece suspeito e, talvez, até sugira como consertá-lo. A decisão de fazer uma alteração ou não permanece com a pessoa. E aqui ... ahem ... Não é fácil tomar uma decisão, não sendo capaz de ler, com base na qual o analisador parece desconfiar de um ou outro local no código.

Obviamente, em alguns casos, tudo será óbvio. Suponha que o analisador aponte para este código:

 char *p = (char *)malloc(strlen(src + 1)); strcpy(p, src); 

E oferecerá substituí-lo por:

 char *p = (char *)malloc(strlen(src) + 1); strcpy(p, src); 

Fica imediatamente claro que o programador fechou e adicionou 1 ao lugar errado. Como resultado, menos memória será alocada.

Aqui, sem documentação, tudo está claro. No entanto, esse nem sempre será o caso.

Imagine que o analisador silenciosamente aponte para este código:

 char check(const uint8 *hash_stage2) { .... return memcmp(hash_stage2, hash_stage2_reassured, SHA1_HASH_SIZE); } 

E sugere alterar o tipo do valor de retorno de char para int:

 int check(const uint8 *hash_stage2) { .... return memcmp(hash_stage2, hash_stage2_reassured, SHA1_HASH_SIZE); } 

Não há documentação para o aviso. E, aparentemente, o texto do aviso em si, como o entendemos, também não será, se estivermos falando de um analisador completamente independente.

O que fazer Qual a diferença? Devo fazer uma substituição?

Em princípio, aqui você pode ter uma chance e concordar em corrigir o código. Embora aceite edições sem entendê-las, essa é uma prática mais ou menos ... :) Você pode examinar a descrição da função memcmp e ler que a função retorna int : 0 valores maiores que zero e menores que zero. Mesmo assim, pode não estar claro por que fazer alterações se o código já estiver funcionando com êxito.

Agora, se você não sabe qual é o objetivo dessa edição, leia a descrição dos diagnósticos do V642 . Torna-se imediatamente claro que este é um erro real. Além disso, pode causar vulnerabilidade.

Talvez o exemplo não parecesse convincente. Afinal, o analisador propôs um código que provavelmente seria melhor. Ok Vejamos outro exemplo de pseudocódigo, desta vez, para uma mudança, em Java.

 ObjectOutputStream out = new ObjectOutputStream(....); SerializedObject obj = new SerializedObject(); obj.state = 100; out.writeObject(obj); obj.state = 200; out.writeObject(obj); out.close(); 

Existe algum tipo de objeto. É serializado. O estado do objeto muda e é serializado novamente. Tudo parece estar bem. Agora imagine que o analisador, de repente, não gosta desse código, e ele sugere substituí-lo por:

 ObjectOutputStream out = new ObjectOutputStream(....); SerializedObject obj = new SerializedObject(); obj.state = 100; out.writeObject(obj); obj = new SerializedObject(); //    obj.state = 200; out.writeObject(obj); out.close(); 

Em vez de alterar o objeto e regravá-lo, um novo objeto é criado e já está serializado.

Não há descrição do problema. Nenhuma documentação. O código ficou mais longo. Por alguma razão, a criação de um novo objeto foi adicionada. Você está pronto para fazer essa edição no seu código?

Você dirá que não está claro. Na verdade, não está claro. E assim será incompreensível o tempo todo. Trabalhar com um analisador "silencioso" será um estudo interminável, na tentativa de entender por que o analisador não gosta de algo.

Se houver documentação, tudo ficará transparente. A classe java.io.ObjectOuputStream , usada para serialização, armazena em cache objetos graváveis. Isso significa que o mesmo objeto não será serializado duas vezes. Depois que a classe serializa o objeto, e na segunda vez, simplesmente grava um link no mesmo primeiro objeto no fluxo. Leia mais: V6076 - A serialização recorrente usará o estado do objeto em cache desde a primeira serialização.

Esperamos poder explicar a importância de ter documentação. E agora a pergunta. Como será exibida a documentação para um analisador baseado em ML?

Quando um analisador de código clássico é desenvolvido, tudo é simples e claro. Há um certo padrão de erros. Nós o descrevemos na documentação e implementamos o diagnóstico.

No caso de ML, o oposto é verdadeiro. Sim, o analisador pode observar uma anomalia no código e apontar para ele. Mas ele não sabe nada sobre a essência do defeito. Ele não entende e não diz por que o código não pode ser escrito assim. Essas são abstrações de nível muito alto. Em seguida, o analisador também deve aprender a ler e entender a documentação para as funções.

Como eu disse, como o tópico da documentação é abordado em artigos sobre aprendizado de máquina, não estamos prontos para conversar mais. Apenas outra grande nuance que trouxemos para revisão.

Nota Pode-se argumentar que a documentação é opcional. O analisador pode levar a muitos exemplos de correções no GitHub e uma pessoa, analisando confirmações e comentários sobre elas, descobrirá o que é o quê. É sim. Mas a ideia não parece atraente. Em vez de um assistente, o analisador atua como uma ferramenta que irá confundir ainda mais o programador.

A quarta nuance. Idiomas altamente especializados.

A abordagem descrita não é aplicável a linguagens altamente especializadas para as quais a análise estática também pode ser extremamente útil. O motivo é que o GitHub e outras fontes simplesmente não têm uma base de código fonte grande o suficiente para fornecer treinamento eficaz.

Considere isso com um exemplo específico. Para começar, acesse o GitHub e pesquise repositórios para a popular linguagem Java.

Resultado: idioma: “Java”: 3.128.884 resultados disponíveis do repositório

Agora vamos usar o idioma especializado "1C Enterprise" usado em aplicativos de contabilidade emitidos pela empresa russa 1C .

Resultado: idioma: “1C Enterprise”: 551 resultados disponíveis no repositório

Talvez não sejam necessários analisadores para esse idioma? São necessários. Existe uma necessidade prática para a análise de tais programas e já existem analisadores correspondentes. Por exemplo, existe um plug-in SonarQube 1C (BSL) fabricado pela Silver Bullet .

, - , .

. C, C++, #include .

, ML, , Java, JavaScript, Python. . C C++ - , .

, /, , C C++ . «» .

c/cpp- . , GitHub, - cpp- . , ML.

, . GitHub . , . , . , .cpp- .

. . . , , . .

. :

 bool Class::IsMagicWord() { return m_name == "ML"; } 

:

 bool Class::IsMagicWord() { return strcmp(m_name, "ML") == 0; } 

, (x == «y») strcmp(x, «y»)?

, , m_name . , , :

 class Class { .... char *m_name; }; class Class { .... std::string m_name; }; 

, . , ( std::string ).

, , .h . , . , C C++.

- , , , C C++.

, . , , . , cpp-.

. (, , ). , . , , .



, GitHub . , , . - . - , . . « ». , , .cpp (.i) . .

, , , . , . . , - , , .

, . . C C++ , GitHub, . , .

. , . GitHub C++ , .cpp . :).

, C C++ .

. .

, .

V789 , Range-based for loop. , , . , , , . , :

 std::vector<int> numbers; .... for (int num : numbers) { if (num < 5) { numbers.push_back(0); break; // , , return } } 

, . . PVS-Studio 26 .

, . , , , .

, . , , , ML. I.e. .

. .

, . (, WinAPI, ..).

C, strcmp , . GitHub, available code results:

  • strcmp — 40,462,158
  • stricmp — 1,256,053

, . , , , :
  • , . .
  • , NULL. .
  • , . .
  • E assim por diante

? Não. « ». « » . Top50 . , , , 100 , . , , , . , - Amazon.com , 130 « ».

. , . , :

  • g_ascii_strncasecmp — 35,695
  • lstrcmpiA — 27,512
  • _wcsicmp_l — 5,737
  • _strnicmp_l — 5,848
  • _mbscmp_l — 2,458
  • ..

, , . . . , , . « ».

PVS-Studio . , C ++ 7200 . :

  • WinAPI
  • C,
  • (STL),
  • glibc (GNU C Library)
  • Qt
  • MFC
  • zlib
  • libpng
  • OpenSSL
  • ..

, . . , .

. ML? , .

, , ML, . , . , strcmp malloc .

C . , . , , , .

, _fread_nolock . , , fread . . , . , . :

 int buffer[10]; size_t n = _fread_nolock(buffer, size_of(int), 100, stream); 

PVS-Studio:

 C_"size_t _fread_nolock" "(void * _DstBuf, size_t _ElementSize, size_t _Count, FILE * _File);" ADD(HAVE_STATE | RET_SKIP | F_MODIFY_PTR_1, nullptr, nullptr, "_fread_nolock", POINTER_1, BYTE_COUNT, COUNT, POINTER_2). Add_Read(from_2_3, to_return, buf_1). Add_DataSafetyStatusRelations(0, 3); 

, , , , . , write-only . . .

ML. GitHub . 15000 . . :

 #define fread_unlocked _fread_nolock 

?

  1. . .
  2. , , . , , . .
  3. , , . , . ML :). .

, ML .

, ML, , , , . , , , .

. , , WinAPI. , , ? , Google, , . , . _fread_nolock , . , , C++. , 20.

, . , memmove . - :

 void *memmove (void *dest, const void *src, size_t len) { return __builtin___memmove_chk(dest, src, len, __builtin_object_size(dest, 0)); } 

__builtin___memmove_chk ? intrinsic , . .

memmove - : . , - .

Ok, . , . , ML , , .

. . . , , . , AI? , AI , . , . , 20 .



, , . . , .
  • . , , . , - . . C++ auto_ptr . unique_ptr .
  • . , C C++ , . , . , . , long Windows 32/64 32 . Linux 32/64 . , . -. , , . , ( ). , .
  • . ML, , , . I.e. , — , , . , . , , — , . . , / , , , . . : " PVS-Studio: ". , , .


, , . ML , , ( ) . , , ML .

, , ML. , , .

, ML . , . ML «» .

, , , , .

ML, .

PS


, - , ML, .

Unicórnios Ludditas


, . PVS-Studio. ML. , . , , , if- :). , :).

, -, .

. " PVS-Studio ".



, : Andrey Karpov, Victoria Khanieva. Machine Learning in Static Analysis of Program Source Code .

Source: https://habr.com/ru/post/pt484208/


All Articles