Reduzindo o comportamento indefinido na linguagem C

Comportamento indefinido em C e C++ — operações para as quais a linguagem não garante nada, como overflow de inteiros com sinal ou desreferenciação de null — continua sendo uma fonte central de bugs, vulnerabilidades de segurança e otimizações surpreendentes dos compiladores. Os comentaristas ponderam as razões históricas do UB e a necessidade de otimização agressiva contra pedidos para converter mais casos em comportamento definido pela implementação, adicionar modos opcionais que gerem trap ou sanitizem UB e avançar em C2y para reduzir o “zoológico de UB”. Muitos argumentam que a verdadeira robustez virá de linguagens mais fortes (como Rust) ou de mecanismos em nível de hardware/ABI (como CHERI e Fil-C), enquanto o C deve ser reservado para código de sistemas de baixo nível, onde os desenvolvedores aceitam conscientemente esses trade-offs.

Escopo e natureza do comportamento indefinido (UB) em C

  • UB é descrito como “o que acontece quando você viola uma pré-condição obrigatória”, dando aos compiladores liberdade para assumir que esses casos nunca ocorrem.
  • Vários comentários argumentam que isso torna o raciocínio sobre o programa inteiro extremamente difícil, já que o compilador pode otimizar com base em “UB nunca acontece” e alterar o comportamento mesmo antes do ponto de UB.
  • Outros observam que o UB em C/C++ é incomumente agressivo em comparação com muitas outras linguagens, permitindo efetivamente comportamento arbitrário (“demônios nasais”) em vez de apenas “o que o hardware faz”.

Otimização vs. correção

  • Defensores do UB dizem que ele é essencial para a otimização: assumir que não há UB permite inlining, propagação de constantes, eliminação de código morto, alocação de registradores e remoção de verificações redundantes.
  • Críticos contrapõem que os compiladores agora “armazenam como arma” o UB, levando à remoção surpreendente de verificações de null ou de branches e dificultando a depuração.
  • Alguns sugerem que os compiladores deveriam ao menos emitir avisos quando o raciocínio baseado em UB elimina código, enquanto outros argumentam que isso é inviável porque quase todas as otimizações dependem de assumir que não há UB.

Definido pela implementação vs. indefinido

  • Vários კომენტadores argumentam que muitos comportamentos (por exemplo, desreferenciar null, leituras não inicializadas) deveriam ser definidos pela implementação ou garantidos para gerar trap, em vez de totalmente indefinidos.
  • Visão oposta: hardware exótico, WASM e restrições de desempenho tornam impraticável um comportamento totalmente definido; os comitês de C relutam em quebrar implementações existentes.

Overflow de inteiros

  • Há um forte debate sobre se o overflow com sinal deveria ser definido (por exemplo, wrap em complemento de dois, traps ou algo configurável).
  • Lado pró-definição: verificações em runtime mais fáceis, menos exploração de UB, comportamento consistente; flags como -fwrapv/-ftrapv já existem.
  • Lado anti-definição: UB permite que compiladores alertem sobre erros óbvios, usem sanitizers e mantenham otimizações poderosas; overflow sem sinal já é definido, mas ainda assim é propenso a bugs.

Memória não inicializada

  • Código de exemplo mostra compiladores explorando leituras de memória malloc não inicializada para otimizar de formas surpreendentes (por exemplo, folding constante para sempre tomar um branch).
  • Alguns querem valores “indeterminados, mas estáveis” ou semântica definida pela implementação; outros afirmam que o C2y não fará isso porque quebraria muitas implementações.

Alternativas e mitigação

  • As sugestões incluem: usar linguagens com segurança de memória (Rust, Go) para aplicações; manter C para kernels de baixo nível; confiar em sanitizers (UBSan, ASan, TSan), Valgrind e analisadores estáticos apesar de suas limitações.
  • CHERI e Fil-C são citados como abordagens no nível de ABI que restringem fortemente o comportamento da memória e reduzem a explorabilidade, com trade-offs em compatibilidade e implantação.

Evolução do padrão e camadas

  • O C2y supostamente remove alguns casos de UB, mas muitos permanecem por otimização e portabilidade.
  • Há discussão sobre qual camada (linguagem, SO, ABI, hardware) deve arcar com a complexidade da segurança, sem consenso claro.