É legal para o código-fonte que contém comportamento indefinido travar o compilador?

85

Digamos que eu vá compilar algum código-fonte C ++ mal escrito que invoca um comportamento indefinido e, portanto (como eles dizem) "tudo pode acontecer".

Do ponto de vista do que a especificação da linguagem C ++ considera aceitável em um compilador "compatível", "qualquer coisa" neste cenário inclui a falha do compilador (ou roubo de minhas senhas, ou de outro modo com comportamento incorreto ou erro durante a compilação), ou é escopo do comportamento indefinido limitado especificamente ao que pode acontecer quando o executável resultante é executado?

Jeremy Friesner
fonte
22
"UB é UB. Viva com isso" ... Não, espere. "Por favor, poste um MCVE." ... Não, espere. Adoro a pergunta por todos os reflexos que ela desencadeia de maneira inadequada. :-)
Yunnosch
14
Não há realmente nenhuma limitação, e é por isso que se diz que o UB pode invocar demônios nasais .
Algum programador de
15
UB pode fazer o autor postar uma pergunta no SO. : P
Tanveer Badar
46
Independentemente do que o padrão C ++ diz, se eu fosse um escritor de compilador, certamente consideraria isso como um bug em meu compilador. Portanto, se você estiver vendo isso, apresente um relatório de defeito.
john
9
@LeifWillerts Isso foi nos anos 80. Não me lembro da construção exata, mas acho que dependia do uso de um tipo de variável complicada. Depois de colocar um substituto, tive um momento "o que eu estava pensando - as coisas não funcionam assim". Não culpei o compilador por rejeitar a construção, apenas por reiniciar a máquina. Duvido que alguém encontrasse esse compilador hoje. Foi o compilador cruzado HP C para o HP 64000 visando o microprocessador 68000.
Avi Berger de

Respostas:

71

A definição normativa de comportamento indefinido é a seguinte:

[defns.undefined]

comportamento para o qual esta Norma Internacional não impõe requisitos

[Nota: Um comportamento indefinido pode ser esperado quando esta Norma omite qualquer definição explícita de comportamento ou quando um programa usa uma construção ou dados errados. O comportamento indefinido permissível varia de ignorar a situação completamente com resultados imprevisíveis, para se comportar durante a tradução ou execução do programa de uma maneira documentada característica do ambiente (com ou sem a emissão de uma mensagem de diagnóstico), para encerrar uma tradução ou execução (com a emissão de uma mensagem de diagnóstico). Muitas construções de programa erradas não geram comportamento indefinido; eles precisam ser diagnosticados. A avaliação de uma expressão constante nunca exibe um comportamento explicitamente especificado como indefinido. - nota final]

Embora a nota em si não seja normativa, ela descreve uma variedade de implementações de comportamentos que são conhecidas por exibir. Portanto, travar o compilador (que é a tradução encerrando abruptamente), é legítimo de acordo com essa nota. Mas realmente, como diz o texto normativo, o padrão não impõe nenhum limite para a execução ou tradução. Se uma implementação roubar suas senhas, não é uma violação de nenhum contrato estabelecido no padrão.

StoryTeller - Unslander Monica
fonte
43
Dito isso, se você realmente puder fazer com que um compilador execute código arbitrário em tempo de compilação, sem qualquer sandbox, então várias pessoas de segurança estariam muito interessadas em saber sobre isso. O mesmo vale para o segfaulting do compilador.
Kevin
67
O mesmo vale para o que Kevin disse. Como engenheiro de compilador C / C ++ / etc em uma carreira anterior, nossa posição era que o comportamento indefinido poderia travar seu programa , bagunçar seus dados de saída, colocar fogo em sua casa, o que quer que seja. Mas o compilador nunca deve travar, independentemente da entrada. (Pode não dar mensagens de erro úteis, mas deve produzir algum tipo de diagnóstico e saída, em vez de apenas gritar CTHULHU PEGUE A RODA e segfaulting.)
Ti Strga
8
@TiStrga Aposto que Cthulhu seria um ótimo piloto de F1.
banda zeta de
35
"Se uma implementação roubar suas senhas, não é uma violação de nenhum contrato estabelecido no padrão." Isso é verdade independentemente de o código ter UB, não é? O padrão apenas dita o que o programa compilado deve fazer - um compilador que compila corretamente o código, mas rouba suas senhas no processo, não estaria desobedecendo o padrão.
Carmeister
8
@Carmeister, oooh, esse é um bom ponto, vou lembrar as pessoas disso sempre que aqueles argumentos "UB dá ao compilador permissão para iniciar uma guerra nuclear" aparecerem. Novamente.
ilkkachu
8

A maioria dos tipos de UB com os quais geralmente nos preocupamos, como NULL-deref ou dividir por zero, são UB em tempo de execução . Compilar uma função que causaria UB em tempo de execução, se executada, não deve causar a falha do compilador. A menos que possa provar que a função (e aquele caminho através da função) definitivamente será executada pelo programa.

(2ª reflexão: talvez eu não tenha considerado a avaliação necessária do template / constexpr em tempo de compilação. Possivelmente UB durante isso pode causar estranheza arbitrária durante a tradução, mesmo se a função resultante nunca for chamada.)

O comportamento durante a tradução da citação ISO C ++ na resposta de @ StoryTeller é semelhante à linguagem usada no padrão ISO C. C não inclui modelos ou avaliação constexprobrigatória em tempo de compilação.

Mas curiosidade : ISO C diz em uma nota que se a tradução for encerrada, deve ser com uma mensagem de diagnóstico. Ou "comportando-se durante a tradução ... de maneira documentada". Não acho que "ignorar a situação completamente" possa ser lido como incluindo a interrupção da tradução.


Resposta antiga, escrita antes de eu aprender sobre o tempo de tradução UB. É verdade para o runtime-UB, entretanto, e, portanto, ainda potencialmente útil.


Não existe UB que aconteça em tempo de compilação. Ele pode ser visível para o compilador ao longo de um determinado caminho de execução, mas em termos de C ++ não aconteceu até que a execução alcance esse caminho de execução por meio de uma função.

Defeitos em um programa que tornam impossível até mesmo compilar não são UB, são erros de sintaxe. Esse programa "não é bem formado" na terminologia C ++ (se eu entendi corretamente o padrão). Um programa pode ser bem formado, mas conter UB. Diferença entre comportamento indefinido e mal formado, nenhuma mensagem de diagnóstico necessária

A menos que eu esteja entendendo mal alguma coisa, ISO C ++ requer que este programa seja compilado e executado corretamente, porque a execução nunca atinge a divisão por zero. (Na prática ( Godbolt ), bons compiladores apenas fazem executáveis ​​funcionais. Gcc / clang avisa sobre x / 0isso, mas não, mesmo durante a otimização. Mas de qualquer forma, estamos tentando dizer o quão baixo ISO C ++ permite que a qualidade de implementação seja. Portanto, verificar o gcc / clang dificilmente é um teste útil além de confirmar que escrevi o programa corretamente.)

int cause_UB() {
    int x=0;
    return 1 / x;      // UB if ever reached.
 // Note I'm avoiding  x/0  in case that counts as translation time UB.
 // UB still obvious when optimizing across statements, though.
}

int main(){
    if (0)
        cause_UB();
}

Um caso de uso para isso pode envolver o pré-processador C, ou constexprvariáveis ​​e ramificações nessas variáveis, o que leva a um absurdo em alguns caminhos que nunca são alcançados para essas escolhas de constantes.

Caminhos de execução que causam UB visível no tempo de compilação podem ser assumidos como nunca tomados, por exemplo, um compilador para x86 poderia emitir um ud2(causar exceção de instrução ilegal) como a definição para cause_UB(). Ou dentro de uma função, se um lado de um if()levar a UB comprovável , o ramal pode ser removido.

Mas o compilador ainda precisa compilar todo o resto de uma maneira sã e correta. Todos os caminhos que não encontram (ou não podem ser encontrados) UB ainda devem ser compilados para asm que executa como se a máquina abstrata C ++ o estivesse executando.


Você poderia argumentar que o UB visível no tempo de compilação incondicional mainé uma exceção a essa regra. Ou, de outra forma, comprovável em tempo de compilação que a execução começando mainem de fato atinge UB garantido.

Eu ainda argumentaria que os comportamentos legais do compilador incluem a produção de uma granada que explode se for executada. Ou, mais plausivelmente, uma definição de mainque consiste em uma única instrução ilegal. Eu diria que, se você nunca executou o programa, ainda não houve nenhum UB. O compilador em si não pode explodir, IMO.


Funções contendo UB possíveis ou prováveis ​​dentro dos ramos

O UB ao longo de qualquer caminho de execução volta no tempo para "contaminar" todo o código anterior. Mas, na prática, os compiladores só podem tirar proveito dessa regra quando podem realmente provar que os caminhos de execução levam a UB visível no tempo de compilação. por exemplo

int minefield(int x) {
    if (x == 3) {
        *(char*)nullptr = x/0;
    }

    return x * 5;
}

O compilador deve fazer um conjunto que funcione para todos os xoutros que não 3, até os pontos em que x * 5causa estouro de sinal UB em INT_MIN e INT_MAX. Se esta função nunca for chamada com x==3, o programa obviamente não contém UB e deve funcionar como escrito.

Poderíamos também ter escrito if(x == 3) __builtin_unreachable();em GNU C para dizer ao compilador que xdefinitivamente não é 3.

Na prática, há código de "campo minado" em todos os lugares em programas normais. por exemplo, qualquer divisão por um inteiro promete ao compilador que é diferente de zero. Qualquer deref de ponteiro promete ao compilador que não é NULL.

Peter Cordes
fonte
3

O que significa "legal" aqui? Qualquer coisa que não contradiga o padrão C ou C ++ é legal, de acordo com esses padrões. Se você executar uma declaração i = i++;e, como resultado, os dinossauros dominarem o mundo, isso não contradiz os padrões. No entanto, contradiz as leis da física, por isso não vai acontecer :-)

Se o comportamento indefinido travar seu compilador, isso não viola o padrão C ou C ++. No entanto, isso significa que a qualidade do compilador pode (e provavelmente deve) ser melhorada.

Nas versões anteriores do padrão C, havia declarações que eram erros ou não dependentes de comportamento indefinido:

char* p = 1 / 0;

É permitido atribuir uma constante 0 a um char *. Permitir uma constante diferente de zero não é. Uma vez que o valor de 1/0 é um comportamento indefinido, é um comportamento indefinido se o compilador deve ou não aceitar esta instrução. (Hoje em dia, 1/0 não atende mais à definição de "expressão constante inteira").

gnasher729
fonte
4
Para ser mais preciso: os dinossauros dominando o mundo não contradizem nenhuma lei da física (por exemplo, a variação do Parque Jurássico). É altamente improvável. :)
bizarro
-1

O Padrão não imporia requisitos sobre o comportamento de uma implementação, caso fosse encontrado #include "'foo'". Se o criador do compilador julgar que seria útil processar as diretivas de inclusão dessa forma (contendo os apóstrofos no nome do arquivo), executando o programa indicado com sua saída direcionada para um arquivo temporário e, em seguida, comportando-se como um #includedesse arquivo, então uma tentativa processar um programa que contém a linha acima pode executar o programa foo, com quaisquer consequências resultantes.

Portanto, em geral não há limite para o que pode acontecer como consequência de tentar traduzir um programa C, mesmo que ninguém faça esforço para executá-lo.

supergato
fonte
Pode-se dizer o mesmo sobre qualquer tradutor ou compilador em qualquer linguagem de programação. Ou, por falar nisso, qualquer programa que seja.
Robert Harvey
@RobertHarvey: Muitas especificações de linguagens de programação são muito mais específicas sobre essas coisas. Se uma especificação de idioma diz que uma determinada diretiva lerá a entrada de um fluxo cujo caminho do sistema operacional é o especificado, e o sistema operacional faz algo estranho ao ler um determinado caminho, isso estaria fora do controle da especificação do idioma, mas eu não acho que a maioria das especificações de linguagem daria às implementações carta branca para processar tais diretivas de forma arbitrária em seu lazer, sem ter que documentar, mesmo em plataformas que, de outra forma, definiriam o comportamento.
supercat