O problema
Você está construindo um compilador que gera LLVM IR. Tudo funciona nos testes com entradas pequenas, mas quando o programa tenta processar 500 mil elementos em um loop, o processo morre com SIGSEGV (exit code 139) — sem mensagem de erro, sem stack trace, sem nada.
O culpado é uma instrução alloca emitida dentro do corpo de um loop.
O que é alloca
Na LLVM IR, alloca reserva espaço na stack frame da função atual. É o equivalente a declarar uma variável local em C:
define i32 @exemplo() {
entry:
%x = alloca i32, align 4 ; reserva 4 bytes na stack
store i32 42, ptr %x, align 4
%val = load i32, ptr %x, align 4
ret i32 %val
}
O detalhe crucial é que a memória alocada por alloca só é liberada quando a função retorna. Não existe free para stack — tudo é reclamado de uma vez no epílogo da função, quando o stack pointer é restaurado.
Dentro de uma função sem loops, isso é perfeitamente seguro. O problema aparece quando alloca é chamado repetidamente.
O bug: alloca dentro de um loop
Considere um gerador de código que precisa criar uma variável temporária para cada iteração de um loop. Se o alloca é emitido dentro do corpo do loop, cada iteração consome mais stack sem nunca liberar:
; BUG: alloca dentro do loop — stack cresce a cada iteração
define void @preencher_vetor(ptr %vetor, i32 %n) {
entry:
br label %loop
loop:
%i = phi i32 [0, %entry], [%next, %loop]
%tmp = alloca i32, align 4 ; ← 4 bytes a mais na stack por iteração
store i32 1, ptr %tmp, align 4
call void @vetor_adicionar(ptr %vetor, ptr %tmp)
%next = add i32 %i, 1
%done = icmp eq i32 %next, %n
br i1 %done, label %fim, label %loop
fim:
ret void
}
Com n = 800.000, esse código consome ~3.2 MB de stack (800K × 4 bytes) — o suficiente para estourar o limite padrão de 8 MB na maioria dos sistemas. O processo recebe SIGSEGV e morre silenciosamente.
O comportamento é traiçoeiro porque com n = 100 funciona perfeitamente. O bug só se manifesta com entradas grandes o suficiente para ultrapassar o limite da stack.
Como diagnosticar
Quando você vê um segfault sem explicação em código gerado por LLVM, especialmente com entradas grandes, siga estes passos:
1. Compile com AddressSanitizer
clang -fsanitize=address programa.ll -o programa
./programa
Se o problema for stack overflow, o ASan reporta claramente:
ERROR: AddressSanitizer: stack-overflow on address 0x7ffc2a400000
2. Inspecione o arquivo .ll gerado
Procure por instruções alloca fora do bloco entry. Qualquer alloca que esteja dentro de um bloco que pode ser alcançado mais de uma vez (como o corpo de um loop) é suspeito:
# Procure allocas fora do entry block
grep -n "alloca" programa.ll
Se o alloca aparece em um bloco que não é o entry, investigue se esse bloco faz parte de um loop.
3. Compare N pequeno vs N grande
Se o programa funciona com N=100 mas falha com N=100000, a causa quase certamente é crescimento descontrolado da stack.
A correção
A solução é mover toda instrução alloca para o bloco de entrada da função. Como o bloco de entrada executa exatamente uma vez, a alocação na stack acontece uma vez só, e o mesmo espaço é reutilizado a cada iteração:
; CORRETO: alloca no entry block, reutilizado a cada iteração
define void @preencher_vetor(ptr %vetor, i32 %n) {
entry:
%tmp = alloca i32, align 4 ; ← alocado uma única vez
br label %loop
loop:
%i = phi i32 [0, %entry], [%next, %loop]
store i32 1, ptr %tmp, align 4 ; reutiliza o mesmo slot
call void @vetor_adicionar(ptr %vetor, ptr %tmp)
%next = add i32 %i, 1
%done = icmp eq i32 %next, %n
br i1 %done, label %fim, label %loop
fim:
ret void
}
Em um gerador de código, a implementação típica é um helper que salva o ponto de inserção atual, move para o final do bloco de entrada, emite o alloca, e restaura o ponto de inserção original:
function criarAllocaNoBlocoEntrada(
funcao: llvm.Function,
builder: llvm.IRBuilder,
tipo: llvm.Type,
nome: string
): llvm.AllocaInst {
const blocoEntrada = funcao.getEntryBlock();
const pontoAtual = builder.getInsertBlock();
// Move para o final do bloco de entrada
builder.setInsertionPoint(blocoEntrada, blocoEntrada.end());
const alloca = builder.createAlloca(tipo, nome);
// Restaura o ponto de inserção original
if (pontoAtual) {
builder.setInsertionPoint(pontoAtual, pontoAtual.end());
}
return alloca;
}
Esse padrão é exatamente o que o tutorial do Kaleidoscope recomenda, e é usado por compiladores maduros como o Clang.
Por que -O2 não resolve
É tentador pensar que o otimizador do LLVM corrigiria isso automaticamente. Em alguns casos simples, o passe mem2reg consegue promover alloca para registradores SSA, eliminando o problema. Mas isso não funciona quando o endereço da variável escapa da função — como ao passar um ponteiro para uma chamada externa:
call void @vetor_adicionar(ptr %vetor, ptr %tmp) ; endereço de %tmp escapa
Quando o endereço é passado para outra função, o LLVM precisa garantir que ele aponte para memória válida. Não pode promover para registrador, não pode eliminar o alloca. O resultado: cada iteração do loop aloca mais stack, mesmo com -O2 ou -O3.
A regra é clara: não dependa do otimizador para corrigir um bug no seu gerador de código.
A lição para geradores de código
Se você está construindo um compilador ou transpilador que emite LLVM IR, a regra é simples:
Toda chamada a CreateAlloca (ou equivalente na API que você usa) deve emitir a instrução no bloco de entrada da função, nunca no ponto de uso. Isso vale para variáveis temporárias, variáveis de loop, parâmetros copiados para a stack — qualquer coisa.
Essa é uma daquelas armadilhas que não aparece em nenhum erro de compilação e que passa despercebida em todos os testes unitários com entradas pequenas. Só se manifesta em produção, com dados reais, e quando acontece, o único sintoma é um segfault misterioso.