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.