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/-ftrapvjá 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
mallocnã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.