Uma implementação de C ++ que detecta comportamento indefinido?

Um grande número de operações em C ++ resulta em um comportamento indefinido, em que a especificação é completamente muda em relação ao comportamento do programa e permite que qualquer coisa aconteça. Por causa disso, existem todos os tipos de casos em que as pessoas têm código que compila no modo de debugging, mas não de liberação, ou que funciona até que uma alteração aparentemente não relacionada seja feita, ou que funcione em uma máquina, mas não em outra, etc.

Minha pergunta é se existe um utilitário que analisa a execução do código C ++ e sinaliza todas as instâncias em que o programa invoca o comportamento indefinido. Embora seja bom que tenhamos ferramentas como valgrind e implementações de STL verificadas, elas não são tão fortes quanto o que estou pensando – o valgrind pode ter falsos negativos se você lixar a memory que você ainda alocou, por exemplo, e verificar implementações do STL não vai pegar apagar através de um ponteiro de class base.

Essa ferramenta existe? Ou seria útil até tê-lo por aí?

EDIT : Estou ciente de que, em geral, é indecidível para verificar estaticamente se um programa C ++ pode nunca executar algo que tem um comportamento indefinido. No entanto, é possível determinar se uma execução específica de um C ++ produziu um comportamento indefinido. Uma maneira de fazer isso seria criar um interpretador C ++ que percorra o código de acordo com as definições definidas na especificação, em cada ponto determinando se o código possui ou não um comportamento indefinido. Isso não detectará um comportamento indefinido que não ocorra na execução de um determinado programa, mas encontrará qualquer comportamento indefinido que realmente se manifeste no programa. Isso está relacionado a como ele é reconhecível por Turing para determinar se uma TM aceita alguma input, mesmo que ainda seja indecidível em geral.

Obrigado!

Esta é uma ótima pergunta, mas deixe-me dar uma ideia do porque eu acho que pode ser impossível (ou pelo menos muito difícil) em geral.

Presumivelmente, tal implementação seria quase um interpretador de C ++, ou pelo menos um compilador para algo mais parecido com Lisp ou Java. Ele precisaria manter dados extras para cada ponteiro para garantir que você não executasse a aritmética fora de uma matriz ou cancelasse a referência a algo que já estava liberado ou o que quer que seja.

Agora, considere o seguinte código:

 int *p = new int; delete p; int *q = new int; if (p == q) *p = 17; 

O comportamento *p = 17 indefinido? Por um lado, desreferencia depois de ter sido liberado. Por outro lado, dereferenciamento q é bom e p == q

Mas isso não é realmente o ponto. O ponto é que, se o if avaliado como verdadeiro, depende dos detalhes da implementação do heap, que pode variar de implementação para implementação. Então, substitua *p = 17 por algum comportamento indefinido, e você tem um programa que pode muito bem explodir em um compilador normal, mas funciona bem no seu hipotético “detector UB”. (Uma implementação típica de C ++ usará uma lista livre do LIFO, portanto os pointers têm uma boa chance de serem iguais. Um “detector de UB” hipotético pode funcionar mais como uma linguagem coletada pelo lixo para detectar problemas de uso após a liberação.)

Em outras palavras, a existência de um comportamento meramente definido pela implementação torna impossível escrever um “detector de UB” que funcione para todos os programas, suspeito.

Dito isso, um projeto para criar um “compilador C + uber-strict” seria muito interessante. Deixe-me saber se você quer começar um. 🙂

John Regehr em Finding Undefined Behavior Bugs por Finding Dead Code aponta uma ferramenta chamada STACK e eu cito do site ( ênfase minha ):

O código de otimização instável (código instável) é uma class emergente de erros de software: código que é inesperadamente eliminado por otimizações de compilador devido ao comportamento indefinido no programa. O código instável está presente em muitos sistemas, incluindo o kernel Linux e o servidor de database Postgres. As consequências do código instável variam de funcionalidade incorreta a verificações de segurança ausentes.

STACK é um verificador estático que detecta código instável em programas C / C ++ . A aplicação do STACK em sistemas amplamente utilizados descobriu 160 novos bugs que foram confirmados e corrigidos pelos desenvolvedores.

Também em C ++ 11 para o caso de variables ​​e funções constexpr, o comportamento indefinido deve ser detectado em tempo de compilation .

Nós também temos o gcc ubsan :

Recentemente, o GCC (versão 4.9) ganhou o Undecined Behavior Sanitizer (ubsan), um verificador em tempo de execução para as linguagens C e C ++. Para verificar seu programa com o ubsan, compile e vincule o programa com a opção -fsanitize = undefined. Esses binários instrumentados devem ser executados; Se o ubsan detectar algum problema, ele emitirá uma mensagem “erro de execução:” e, na maioria dos casos, continuará executando o programa.

e o Clang Static Analyzer, que inclui muitas verificações de comportamento indefinido. Por exemplo clangs -fsanitize verifica que inclui -fsanitize=undefined :

-fsanitize = undefined: verificador de comportamento indefinido rápido e compatível. Ativa as verificações de comportamento indefinidas que possuem um pequeno custo de tempo de execução e nenhum impacto no layout do espaço de endereço ou no ABI. Isso inclui todas as verificações listadas abaixo, com exceção do não-inteiro-estouro de estouro.

e para C , podemos dar uma olhada no artigo dele, It’s Time To Get Serious, sobre Explorando Comportamento Indefinido, que diz:

[..] Confesso que não tenho pessoalmente o bom senso necessário para encher o GCC ou o LLVM através dos melhores verificadores dynamics de comportamento indefinidos: KCC e Frama-C . […]

Aqui está um link para o kcc e cito:

[…] Se você tentar executar um programa que é indefinido (ou um para o qual nós estamos perdendo a semântica), o programa ficará preso. A mensagem deve dizer onde ela ficou presa e pode dar uma dica do motivo. Se você quiser ajudar a decifrar a saída, ou ajudar a entender por que o programa é indefinido, por favor envie seu arquivo .kdump para nós. […]

e aqui está um link para Frama-C , um artigo onde o primeiro uso de Frama-C como um interpretador de C é descrito e um adendo ao artigo.

Usando g++

 -Wall -Werror -pedantic-error 

(de preferência com um argumento -std apropriado também) vai pegar um bom número de UB


Coisas que você recebe incluem:

-pedante
Emitir todos os avisos exigidos pelo rigoroso ISO C e ISO C ++; rejeitar todos os programas que usam extensões proibidas e alguns outros programas que não seguem ISO C e ISO C ++. Para ISO C, segue a versão do padrão ISO C especificada por qualquer opção -std usada.

-Winit-self (apenas C, C ++, Objective-C e Objective-C ++)
Avisar sobre variables ​​não inicializadas que são inicializadas com elas mesmas. Note que esta opção só pode ser usada com a opção -Wuninitialized, que por sua vez só funciona com -O1 e acima.

-Wuninitialized
Avisa se uma variável automática é usada sem primeiro ser inicializada ou se uma variável pode ser destruída por uma chamada “setjmp”.

e várias coisas não permitidas que você pode fazer com especificadores para funções da família printf e scanf .

A Clang tem um conjunto de desinfetantes que captam várias formas de comportamento indefinido. Seu objective final é ser capaz de capturar todo o comportamento indefinido da linguagem principal do C ++, mas as verificações de algumas formas complicadas de comportamento indefinido estão faltando no momento.

Para um conjunto decente de higienizadores, tente:

 clang++ -fsanitize=undefined,address 

-fsanitize=address checagem de -fsanitize=address para uso de pointers ruins (não apontando para memory válida), e -fsanitize=undefined habilita um conjunto de verificações de UB leves (estouro de inteiro, turnos ruins, pointers desalinhados, …).

-fsanitize=memory (para detectar leituras de memory não inicializadas) e -fsanitize=thread (para detecção de raças de dados) também são úteis, mas nenhuma delas pode ser combinada com -fsanitize=address nem entre si porque todos os três têm um impacto invasivo sobre o espaço de endereço do programa.

Você pode querer ler sobre o SAFECode .

Este é um projeto de pesquisa da Universidade de Illinois, o objective é indicado na primeira página (link acima):

A finalidade do projeto SAFECode é ativar a segurança do programa sem garbage collection e com verificações mínimas de tempo de execução usando análise estática, quando possível, e verificações em tempo de execução, quando necessário. O SAFECode define uma representação de código com restrições semânticas mínimas projetadas para permitir a imposição estática da segurança, usando técnicas agressivas de compilador desenvolvidas neste projeto.

O que é realmente interessante para mim é a eliminação das verificações de tempo de execução sempre que se pode provar que o programa está correto estaticamente, por exemplo:

 int array[N]; for (i = 0; i != N; ++i) { array[i] = 0; } 

Não deve incorrer em mais sobrecarga do que a versão regular.

De uma forma mais leve, Clang tem algumas garantias sobre o comportamento indefinido também, tanto quanto me lembro, mas não consigo colocar as mãos nele …

O compilador clang pode detectar alguns comportamentos indefinidos e avisá-los. Provavelmente não tão completo quanto você quer, mas é definitivamente um bom começo.

Infelizmente não tenho conhecimento de nenhuma ferramenta desse tipo. Normalmente, o UB é definido como tal precisamente porque seria difícil ou impossível para um compilador diagnosticá-lo em todos os casos.

Na verdade, sua melhor ferramenta é provavelmente os avisos do compilador: eles geralmente avisam sobre itens do tipo UB (por exemplo, o destruidor não-virtual em classs base, abusando das regras de restrição de alias, etc).

A revisão de código também pode ajudar a detectar casos em que o UB é invocado.

Então você tem que confiar em valgrind para capturar os casos restantes.

Assim como uma observação lateral, de acordo com a teoria da computabilidade, você não pode ter um programa que detecte todos os possíveis comportamentos indefinidos.

Você só pode ter ferramentas que usam heurística e detectar alguns casos específicos que seguem certos padrões. Ou você pode, em certos casos, provar que um programa se comporta como você deseja. Mas você não pode detectar comportamento indefinido em geral.

Editar

Se um programa não terminar (trava, faz um loop para sempre) em uma determinada input, sua saída é indefinida.

Se você concordar com esta definição, então determinar se um programa termina é o conhecido “Problema de Interrupção”, que provou ser indecidível, ou seja, não existe nenhum programa (Máquina de Turing, programa C, programa C ++, programa Pascal, em qualquer que seja a linguagem) que possa resolver este problema em geral.

Simplificando: não existe nenhum programa P que possa receber como input qualquer programa Q e dados de input I e imprimir como saída TRUE se Q (I) terminar, ou então imprimir FALSE se Q (I) não terminar.

Para mais informações, você pode consultar http://en.wikipedia.org/wiki/Halting_problem .

O comportamento indefinido é indefinido . O melhor que você pode fazer é seguir o padrão pedantemente, como outros sugeriram, no entanto, você não pode testar o que é indefinido, porque você não sabe o que é. Se você soubesse o que era e os padrões especificavam, não seria indefinido.

No entanto, se por algum motivo, realmente depender do que a norma diz ser indefinido , e resultar em um resultado particular, então você pode optar por defini-lo e escrever alguns testes de unidade para confirmar que, para sua compilation em particular, é definiram. É muito melhor, no entanto, simplesmente evitar comportamentos indefinidos sempre que possível.

Dê uma olhada no PCLint é bastante decente em detectar muitas coisas ruins em C ++.

Aqui está um subconjunto do que captura