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.


Referências