O comportamento indefinido (UB) é um conceito complicado em linguagens de programação e compiladores. Ao longo dos muitos anos, fui mentor da indústria para o curso de Engenharia de Desempenho 6.172 do MIT $^1$, Já ouvi muitos equívocos sobre o que o compilador garante na presença do UB. Isso é lamentável, mas não surpreendente!

Para uma cartilha sobre comportamento indefinido e por que não podemos simplesmente “definir todos os comportamentos”, recomendo fortemente a palestra de Chandler Carruth “Garbage In, Garbage Out: Arguing about Undefined Behavior with Nasal Demons”.

Você também pode estar familiarizado com minha série de blogs Compiler Adventures sobre como funcionam as otimizações do compilador. Um próximo episódio é sobre a implementação de otimizações que aproveitam comportamentos indefinidos, como dividir por zero, onde veremos UB “do outro lado”.

Comportamento indefinido != comportamento definido pela implementação

Comportamento indefinido não é o mesmo que comportamento definido pela implementação $^2$. Os comportamentos do programa se enquadram em três grupos, não dois:

  • Especificação definida: A própria linguagem de programação define o que acontece. Esta é a grande maioria de todos os programas.
  • Implementação definida: O comportamento exato é definido pelo compilador, sistema operacional ou hardware. Por exemplo: quantos bits exatamente estão em a ou em C++.charint $^3$
  • Comportamento indefinido: tudo pode acontecer e você pode não ter mais um computador depois que tudo acontecer. Nenhum resultado é um bug se causado pelo UB. Por exemplo: estouro de inteiro assinado em C ou usando para criar duas referências aos mesmos dados em Rust.unsafe&mut $^4$

Aqui está a lista de garantias que os compiladores fazem sobre os resultados do comportamento indefinido:

Essa é a lista completa. Não, não esqueci nenhum item. Sim, sério.

É possível analisar como o UB afeta um programa específico quando compilado por um compilador específico ou executado em uma plataforma de destino específica. Por exemplo, existem compiladores, sistemas operacionais e hardware exóticos que oferecem garantias adicionais $^5$

A mentalidade para este post é a seguinte: “Se meu programa contém UB e o compilador produziu um binário que faz X, isso é um bug do compilador?”

Não é um bug do compilador.

Todas as suposições a seguir estão erradas

Falsidades sobre quando UB “acontece”

  1. O comportamento indefinido só “acontece” em altos níveis de otimização, como -O2 ou -O3.
  2. Se eu desativar as otimizações com um sinalizador como -O0, não haverá UB.
  3. Se eu incluir símbolos de depuração na compilação, não haverá UB.
  4. Se eu executar o programa em um depurador, não haverá UB.
  5. Ok, ainda há UB com tudo isso, mas meu código “fará a coisa certa” independentemente.
  6. Ele “fará a coisa certa” ou travará com um (sinal).Segmentation FaultSIGSEGV
  7. Ele vai “fazer a coisa certa” ou falhar de alguma forma.
  8. Ele vai “fazer a coisa certa” ou travar ou loop infinito ou impasse.
  9. Pelo menos ele não executará algum código não relacionado de outro lugar do programa.
  10. Pelo menos ele não executará nenhum código inacessível que o programa possa conter.

Falsidades em torno do comportamento de execução de UB

  1. Se uma linha com UB anteriormente “fez a coisa certa”, ela continuará a “fazer a coisa certa” na próxima vez que executarmos o programa.
  2. A linha UB pelo menos continuará a “fazer a coisa certa” enquanto o programa ainda estiver em execução.
  3. É possível determinar se uma linha anterior era UB e evitar que ela cause problemas.
  4. Pelo menos o impacto do UB é limitado ao código que usa valores produzidos a partir do UB.
  5. Pelo menos o impacto do UB é limitado ao código que está na mesma unidade de compilação que a linha com UB.
  6. Ok, mas pelo menos o impacto do UB é limitado ao código que é executado após a linha com UB $^6$.

Falsidades sobre os possíveis resultados do UB

  1. Pelo menos não corromperá a memória do programa.
  2. Pelo menos não corromperá a memória do programa além de onde os dados afetados pelo UB estavam localizados.
  3. Pelo menos não vai corromper a pilha.
  4. Pelo menos não vai corromper a pilha.
  5. Pelo menos não corromperá o quadro de pilha atual. (Meu nome para isso é a falácia “as variáveis locais estão seguras nos registros”.)
  6. Pelo menos não corromperá o ponteiro da pilha.
  7. Pelo menos não corromperá o registro de sinalizadores da CPU / qualquer outro estado da CPU.
  8. Pelo menos não corromperá a memória executável do programa. $^7$
  9. Pelo menos não corromperá fluxos como stdout ou stderr.
  10. Pelo menos ele não substituirá nenhum arquivo que o programa já tenha aberto.
  11. Pelo menos ele não abrirá novos arquivos e os substituirá.
  12. Pelo menos não vai limpar completamente a unidade.
  13. Pelo menos não danificará ou destruirá nenhum componente de hardware. $^8$
  14. Pelo menos ele não começará a jogar Doom se o programa ainda não tiver o código-fonte do Doom. $^9$

Falsidades como “mas funcionou bem antes”

  1. Se um programa contendo UB “funcionou bem” anteriormente, recompilar o programa sem nenhuma alteração de código ainda produzirá um binário que “funciona bem”.
  2. Recompilar sem alterações de código e com o mesmo compilador e sinalizadores produzirá um binário que ainda “funciona bem”.
  3. Recompilar como acima + na mesma máquina produzirá um binário que ainda “funciona bem”.
  4. Recompilar como acima + se você não reinicializou a máquina desde a última compilação produzirá um binário que ainda “funciona bem”.
  5. Recompilar como acima + com as mesmas variáveis de ambiente produzirá um binário que ainda “funciona bem”.
  6. Recompilar como acima + na mesma hora do dia e dia da semana de antes, durante um eclipse lunar, tendo primeiro sacrificado um novo bastão de RAM aos deuses binários, produzirá um binário que ainda “funciona bem”.

Falsidades sobre o comportamento auto-consistente do UB

  1. Várias execuções do programa compiladas como acima e com as mesmas entradas produzirão o mesmo comportamento em cada execução.
  2. Essas várias execuções produzirão o mesmo comportamento se o programa, ignorando o UB, for determinístico.
  3. Mas eles o farão se o programa também for single-threaded.
  4. Mas eles o farão se o programa também não ler nenhum dado externo (arquivos, rede, variáveis de ambiente, etc.).

  5. O uso de um depurador em um programa contendo UB mostrará o estado do programa que corresponde ao código-fonte. $^10$
  6. O comportamento indefinido é puramente um fenômeno de tempo de execução. $^11$

Falsas expectativas em torno da UB, em geral

  1. Qualquer tipo de comportamento razoável ou irracional que aconteça com qualquer consistência ou garantia de qualquer tipo.

No momento em que seu programa contém UB, todas as apostas estão canceladas. Mesmo que seja apenas um pequeno UB. Mesmo que nunca seja executado. Mesmo que você não saiba que está lá. Provavelmente mesmo se você mesmo escreveu a especificação da linguagem e o compilador. $^12$

Isso não quer dizer que todos os resultados da lista acima sejam igualmente prováveis, ou mesmo plausíveis. $^13$ Mas todos eles são permitidos, válidos, comportamento compatível com as especificações.

É perfeitamente possível que seu programa tenha UB e esteja funcionando bem há anos sem problemas. Fantástico! Fico feliz em ouvir isso! Não estou nem dizendo que você precisa voltar e reescrevê-lo para remover o UB. Mas, à medida que você toma suas decisões, é bom saber o quadro completo do que o compilador garantirá ou não para o seu programa.

Menção honrosa para uma suposição especial

“Se o programa compila sem erros, então ele não tem UB.”

Isso é 100% falso em C e C++.

Também é falso, como afirmado em Rust, mas com um ajuste é quase verdade. Se o seu programa Rust nunca usa , então ele deve estar livre de UB. Em outras palavras: causar UB sem é considerado um bug no compilador Rust. Estes são raros e é muito improvável que você os encontre.unsafeunsafe

Quando Rust é usado, todas as apostas estão canceladas, assim como em C ou C++. Mas a suposição de que “os programas Safe Rust que compilam estão livres de UB” é principalmente verdadeira.unsafe

Esta não é uma tarefa fácil. Temos uma dívida de gratidão para com as pessoas que cumulativamente colocaram séculos de engenheiros para torná-lo assim. É Dia de Ação de Graças e eu agradeço!

Errata e histórico de edições

2022-11-29: Itens 13-16 corrigidos e atualizados

A versão original deste post continha os seguintes itens nas posições 13-16 da lista:

  1. Mas se a linha com UB não for executada, o programa funcionará normalmente como se o UB não estivesse lá.
  2. Ok, mas se a linha com UB for um código inacessível (morto), então é como se o UB não estivesse lá. $^14$
  3. Se a linha com UB for um código inacessível, o programa não falhará por causa do UB.
  4. Se a linha com UB for um código inacessível, o programa pelo menos parará de funcionar de alguma forma e em algum momento.

Essa redação não era precisa o suficiente e, como resultado, as alegações eram indiscutivelmente incorretas, conforme declarado. Atualizei a postagem perto dessas alegações para capturar melhor as sutilezas envolvidas.

A seção “Falsas expectativas em torno do UB, em geral” agora contém uma seleção de itens sugeridos pela comunidade. Anteriormente, ele continha apenas um único item (o último da lista atual) na posição número 41.

Obrigado a chegar, Conrad Ludgate, sharnoff, Brian Graham e algumas pessoas que preferiram permanecer anônimas, pelo feedback sobre os rascunhos deste post. Quaisquer erros são só meus.


  1. Uma excelente aula que eu recomendo. É muito completo e prático, às custas de também exigir muito trabalho em um ritmo muito rápido. Quando fiz isso como estudante de graduação, foi uma ótima troca, mas YMMV.
  2. O comportamento indefinido também não é o mesmo que o comportamento não especificado, que é semelhante ao comportamento definido pela implementação menos o requisito de que a implementação documente suas escolhas e as cumpra. Aqui estamos nos concentrando no comportamento indefinido, não no comportamento não especificado, portanto, agruparemos o comportamento não especificado e o comportamento definido pela implementação.
  3. A especificação garante pelo menos 8 bits para e pelo menos 16 bits para . O restante é definido pela implementação.charint
  4. A Wikipedia tem uma excelente lista de exemplos, se você quiser ver mais.
  5. Como CHERI, com poderes incríveis em torno da segurança do ponteiro. em relação às plataformas mais comuns, que garantem apenas o isolamento do processo no nível do sistema operacional. Não estamos falando sobre isso neste post.
  6. O UB tem permissão explícita para alterar o comportamento de outro código, incluindo até mesmo operações anteriores! “Alterar” aqui engloba corromper, desfazer ou impedir completamente (como se nunca tivesse acontecido) os resultados desse outro código. Para saber mais e ver exemplos de UB causando “viagem no tempo”, confira esta postagem no blog. Obrigado a essas duas postagens do Reddit por sugerir uma redação melhor para esses itens. Para o texto original, consulte a seção Errata no final deste post.
  7. Os recursos de segurança do sistema operacional e do hardware, como o W^X, podem tornar isso improvável, mas programas auto-modificáveis podem ser criados, portanto, em princípio, também é possível através do UB. Certamente não há garantia de que o UB não fará isso!
  8. Nem todos os dispositivos têm o mesmo nível de autoproteção contra entradas incorretas gravadas em seus registros de controle. Este é o tipo de lição que se tende a aprender da maneira mais difícil.
  9. Eu ficaria bastante impressionado se você fizesse um compilador que fizesse os programas executarem o Doom quando encontrarem o UB. Considere isso um desafio!
  10. Este é um corrolário de falsidade # 16, explicado mais detalhadamente neste post. O UB pode corromper o comportamento do programa antes e depois do UB, de modo que o código-fonte que você vê em seu editor não corresponde mais ao programa em execução real. É claro que você ainda pode usar o depurador para percorrer as instruções de assembly e exibir o estado do registro. Mas a montagem altamente otimizada não é fácil de entender para começar, e a estranheza induzida por UB só tornará isso mais difícil. No geral, uma situação que é melhor evitar. Contribuiu aqui.
  11. Em Rust, um contra-exemplo é o uso indevido de #[no_mangle] para sobrescrever um símbolo com um tipo incorreto. Um contra-exemplo de C++ são as violações da ODR (Regra de Definição), algumas das quais o compilador não é obrigado a relatar antes de causar estragos.
  12. Falando por experiência própria. Espero que não seja um que você tenha que reviver para acreditar.
  13. Especialmente aquele sobre executar Doom.
  14. Surpreendente, certo? Não é óbvio por que o código que deveria ser perfeitamente seguro para exclusão teria algum efeito sobre o comportamento do programa. Mas acontece que às vezes as otimizações podem fazer com que algum código morto volte a funcionar. EDIT: Esta era originalmente a nota de rodapé # 6 antes de ser movida para cá.

Artigo Original