El problema

Estas construyendo un compilador que genera LLVM IR. Todo funciona en las pruebas con entradas pequenas, pero cuando el programa intenta procesar 500 mil elementos en un loop, el proceso muere con SIGSEGV (exit code 139) — sin mensaje de error, sin stack trace, sin nada.

El culpable es una instruccion alloca emitida dentro del cuerpo de un loop.


Que es alloca

En LLVM IR, alloca reserva espacio en el stack frame de la funcion actual. Es el equivalente a declarar una variable local en C:

define i32 @ejemplo() {
entry:
  %x = alloca i32, align 4    ; reserva 4 bytes en el stack
  store i32 42, ptr %x, align 4
  %val = load i32, ptr %x, align 4
  ret i32 %val
}

El detalle crucial es que la memoria asignada por alloca solo se libera cuando la funcion retorna. No existe free para el stack — todo se reclama de una vez en el epilogo de la funcion, cuando el stack pointer se restaura.

Dentro de una funcion sin loops, esto es perfectamente seguro. El problema aparece cuando alloca se llama repetidamente.


El bug: alloca dentro de un loop

Considera un generador de codigo que necesita crear una variable temporal para cada iteracion de un loop. Si el alloca se emite dentro del cuerpo del loop, cada iteracion consume mas stack sin nunca liberarlo:

; BUG: alloca dentro del loop — el stack crece en cada iteracion
define void @llenar_vector(ptr %vector, i32 %n) {
entry:
  br label %loop

loop:
  %i = phi i32 [0, %entry], [%next, %loop]
  %tmp = alloca i32, align 4         ; ← 4 bytes mas en el stack por iteracion
  store i32 1, ptr %tmp, align 4
  call void @vector_agregar(ptr %vector, ptr %tmp)
  %next = add i32 %i, 1
  %done = icmp eq i32 %next, %n
  br i1 %done, label %fin, label %loop

fin:
  ret void
}

Con n = 800.000, este codigo consume ~3.2 MB de stack (800K x 4 bytes) — suficiente para superar el limite predeterminado de 8 MB en la mayoria de los sistemas. El proceso recibe SIGSEGV y muere silenciosamente.

El comportamiento es traicionero porque con n = 100 funciona perfectamente. El bug solo se manifiesta con entradas lo suficientemente grandes para superar el limite del stack.


Como diagnosticar

Cuando ves un segfault sin explicacion en codigo generado por LLVM, especialmente con entradas grandes, sigue estos pasos:

1. Compila con AddressSanitizer

clang -fsanitize=address programa.ll -o programa
./programa

Si el problema es stack overflow, ASan lo reporta claramente:

ERROR: AddressSanitizer: stack-overflow on address 0x7ffc2a400000

2. Inspecciona el archivo .ll generado

Busca instrucciones alloca fuera del bloque entry. Cualquier alloca dentro de un bloque que puede ser alcanzado mas de una vez (como el cuerpo de un loop) es sospechoso:

# Busca allocas fuera del entry block
grep -n "alloca" programa.ll

Si el alloca aparece en un bloque que no es entry, investiga si ese bloque forma parte de un loop.

3. Compara N pequeno vs N grande

Si el programa funciona con N=100 pero falla con N=100000, la causa casi con certeza es crecimiento descontrolado del stack.


La correccion

La solucion es mover toda instruccion alloca al bloque de entrada de la funcion. Como el bloque de entrada se ejecuta exactamente una vez, la asignacion en el stack ocurre una sola vez, y el mismo espacio se reutiliza en cada iteracion:

; CORRECTO: alloca en el entry block, reutilizado en cada iteracion
define void @llenar_vector(ptr %vector, i32 %n) {
entry:
  %tmp = alloca i32, align 4         ; ← asignado una unica vez
  br label %loop

loop:
  %i = phi i32 [0, %entry], [%next, %loop]
  store i32 1, ptr %tmp, align 4     ; reutiliza el mismo slot
  call void @vector_agregar(ptr %vector, ptr %tmp)
  %next = add i32 %i, 1
  %done = icmp eq i32 %next, %n
  br i1 %done, label %fin, label %loop

fin:
  ret void
}

En un generador de codigo, la implementacion tipica es un helper que guarda el punto de insercion actual, se mueve al final del bloque de entrada, emite el alloca, y restaura el punto de insercion original:

function crearAllocaEnBloqueEntrada(
  funcion: llvm.Function,
  builder: llvm.IRBuilder,
  tipo: llvm.Type,
  nombre: string
): llvm.AllocaInst {
  const bloqueEntrada = funcion.getEntryBlock();
  const bloqueActual = builder.getInsertBlock();

  // Mover al final del bloque de entrada
  builder.setInsertionPoint(bloqueEntrada, bloqueEntrada.end());

  const alloca = builder.createAlloca(tipo, nombre);

  // Restaurar el punto de insercion original
  if (bloqueActual) {
    builder.setInsertionPoint(bloqueActual, bloqueActual.end());
  }

  return alloca;
}

Este patron es exactamente lo que el tutorial de Kaleidoscope recomienda, y es usado por compiladores maduros como Clang.


Por que -O2 no lo resuelve

Es tentador pensar que el optimizador de LLVM corregiria esto automaticamente. En algunos casos simples, el pase mem2reg puede promover alloca a registros SSA, eliminando el problema. Pero esto no funciona cuando la direccion de la variable escapa de la funcion — como al pasar un puntero a una llamada externa:

call void @vector_agregar(ptr %vector, ptr %tmp)  ; la direccion de %tmp escapa

Cuando la direccion se pasa a otra funcion, LLVM necesita garantizar que apunte a memoria valida. No puede promover a registro, no puede eliminar el alloca. El resultado: cada iteracion del loop asigna mas stack, incluso con -O2 o -O3.

La regla es clara: no dependas del optimizador para corregir un bug en tu generador de codigo.


La leccion para autores de generadores de codigo

Si estas construyendo un compilador o transpilador que emite LLVM IR, la regla es simple:

Toda llamada a CreateAlloca (o el equivalente en la API que uses) debe emitir la instruccion en el bloque de entrada de la funcion, nunca en el punto de uso. Esto aplica para variables temporales, variables de loop, parametros copiados al stack — todo.

Esta es una de esas trampas que no aparece en ningun error de compilacion y que pasa desapercibida en todas las pruebas unitarias con entradas pequenas. Solo se manifiesta en produccion, con datos reales, y cuando ocurre, el unico sintoma es un segfault misterioso.


Referencias