Undefined behavior: os comportamentos que o C não define
1. O que é Undefined Behavior (UB) e por que ele existe?
No padrão C, undefined behavior é definido como "comportamento para o qual o padrão não impõe nenhum requisito". Isso significa que, ao encontrar uma operação classificada como UB, o compilador pode fazer literalmente qualquer coisa: gerar código que funciona por acaso, travar silenciosamente, ignorar a linha inteira, ou até mesmo fazer seu programa parecer funcionar corretamente em testes e falhar em produção.
Para entender o UB, é crucial distinguir quatro categorias de comportamento:
- Comportamento definido: o padrão especifica exatamente o que acontece.
- Comportamento não especificado: o padrão oferece opções, mas não exige documentação.
- Comportamento implementation-defined: o compilador deve escolher e documentar a escolha (ex:
sizeof(int)). - Undefined behavior: qualquer resultado é possível, incluindo nada.
A existência do UB não é um acidente. Ela permite que compiladores gerem código mais rápido, evitando verificações desnecessárias. Um compilador C assume que você nunca escreve UB, e otimiza agressivamente com base nessa premissa.
2. Acessos inválidos à memória
O exemplo clássico é desreferenciar um ponteiro nulo:
int *p = NULL;
*p = 42; // UB: desreferencia ponteiro nulo
Ponteiros selvagens (não inicializados) são igualmente perigosos:
int *p; // não inicializado
*p = 10; // UB: p pode apontar para qualquer lugar
Buffer overflow ocorre quando escrevemos além dos limites de um array:
int arr[5];
for (int i = 0; i <= 5; i++) {
arr[i] = i; // UB na última iteração (i=5)
}
Use-after-free é um dos UB mais traiçoeiros:
int *p = malloc(sizeof(int));
free(p);
*p = 42; // UB: memória já foi liberada
Double-free também é UB:
int *p = malloc(sizeof(int));
free(p);
free(p); // UB: segundo free em ponteiro já liberado
3. Violações de aliasing e regras de tipo
A strict aliasing rule diz que você não pode acessar um objeto através de um ponteiro de tipo incompatível. Exemplo clássico:
float f = 3.14f;
int *p = (int*)&f;
printf("%d\n", *p); // UB: acessa float como int
A exceção principal é char*, que pode acessar qualquer tipo:
float f = 3.14f;
unsigned char *p = (unsigned char*)&f;
for (int i = 0; i < sizeof(f); i++) {
printf("%02x ", p[i]); // OK: char pode acessar qualquer tipo
}
Unions podem ser usadas para type-punning, mas com cuidado:
union {
float f;
int i;
} u;
u.f = 3.14f;
printf("%d\n", u.i); // Comportamento implementation-defined, não UB
4. Operações aritméticas e shifts problemáticos
Overflow de inteiros com sinal é UB:
int a = INT_MAX;
int b = a + 1; // UB: overflow com sinal
Shifts com valores negativos ou maiores que o número de bits:
int x = 1;
int y = x << 33; // UB se int tem 32 bits
int z = x << -1; // UB: shift negativo
Divisão por zero:
int a = 10;
int b = 0;
int c = a / b; // UB
int d = a % b; // UB também
5. Sequenciamento e efeitos colaterais
Modificar uma variável mais de uma vez entre sequence points:
int i = 0;
i = i++; // UB: i é lido e modificado sem sequence point entre
Outro exemplo famoso:
int arr[] = {1, 2, 3};
int i = 0;
arr[i] = i++; // UB: ordem de avaliação de i e i++ é indefinida
A ordem de avaliação de argumentos de função é apenas não especificada, não UB:
int x = 1;
printf("%d %d\n", x, x++); // Comportamento não especificado, mas não UB
6. Ponteiros e aritmética de ponteiros
Aritmética de ponteiro só é válida dentro do array (mais um elemento após o fim):
int arr[5];
int *p = arr + 5; // OK: aponta para "um após o último"
int *q = arr + 6; // UB: além do permitido
Subtrair ponteiros de arrays diferentes:
int a[5], b[5];
int *p = a;
int *q = b;
ptrdiff_t d = q - p; // UB: ponteiros de arrays diferentes
7. Comportamentos de biblioteca padrão
Usar especificadores de formato incompatíveis com printf:
int x = 42;
printf("%f\n", x); // UB: espera double, recebe int
Chamar funções variádicas com tipos errados:
float f = 3.14f;
printf("%f\n", f); // UB: float é promovido a double em variádicas, mas se passar double* seria UB
memcpy com regiões sobrepostas:
char str[] = "Hello";
memcpy(str + 1, str, 5); // UB: regiões sobrepostas, deveria usar memmove
8. Como detectar e evitar UB no dia a dia
Compiladores modernos oferecem ferramentas excelentes. Compile sempre com:
gcc -Wall -Wextra -Wpedantic -fsanitize=undefined -fsanitize=address programa.c
-fsanitize=undefined: detecta UB em tempo de execução (overflow, shifts inválidos, etc.)-fsanitize=address: detecta acessos inválidos à memória (buffer overflow, use-after-free)-Wall -Wextra -Wpedantic: ativa avisos do compilador sobre construções suspeitas
Boas práticas para evitar UB:
- Inicialize sempre ponteiros e variáveis
- Use
size_tpara índices de arrays (tipo sem sinal, sem UB de overflow) - Evite casts desnecessários, especialmente entre tipos incompatíveis
- Prefira
memmoveamemcpyquando houver possibilidade de sobreposição - Use ferramentas de análise estática como
cppcheckouclang-tidy - Mantenha o código simples: expressões complexas com múltiplos efeitos colaterais são mais propensas a UB
Lembre-se: UB não é "comportamento imprevisível" — é "comportamento que o padrão não define". O compilador pode assumir que UB nunca ocorre e otimizar seu programa de maneiras que podem quebrá-lo silenciosamente. Um programa com UB pode funcionar por anos e falhar após uma atualização do compilador.
Referências
- ISO/IEC 9899:2024 (C23) — Draft — Documento oficial do padrão C, seção 3.4.3 define undefined behavior e lista todas as situações de UB.
- GCC: Options to Request or Suppress Warnings — Documentação oficial do GCC sobre flags de warning, incluindo
-Wall,-Wextrae-Wpedantic. - GCC: Program Instrumentation Options (Sanitizers) — Documentação oficial sobre
-fsanitize=undefinede-fsanitize=address, ferramentas essenciais para detectar UB. - Clang: UndefinedBehaviorSanitizer — Documentação oficial do Clang sobre o UBSan, com exemplos de cada tipo de UB detectável.
- CERT C Coding Standard: Undefined Behavior — Guia do SEI/CERT sobre comportamento indefinido em C, com regras e recomendações para evitar UB em código crítico.
- What Every C Programmer Should Know About Undefined Behavior (LLVM Blog) — Artigo clássico da equipe LLVM explicando como compiladores exploram UB para otimização e por que isso importa.
- cppreference.com: Undefined behavior — Referência técnica completa listando todas as situações de UB em C, com exemplos e explicações detalhadas.