Saltos no locales con setjmp y longjmp

setjmp y longjmp proporcionan un mecanismo de salto no local, lo que permite que la ejecución del programa salte a través de múltiples niveles de la pila de llamadas, ignorando los retornos convencionales de las funciones. setjmp captura el contexto de ejecución actual —incluyendo el contador de instrucciones, el puntero de pila y los registros de propósito general— y lo almacena en una estructura jmp_buf. Cuando se invoca longjmp, el procesador restaura dicho estado, provocando que la ejecución “regrese” al punto donde se llamó originalmente a setjmp, pero con un valor de retorno distinto de cero.

Este mecanismo es extremadamente potente para implementar una semántica de excepciones en C, siendo útil en escenarios donde un error profundo en una estructura de datos altamente anidada (como un analizador sintáctico recursivo) requiere abortar toda la operación sin pasar por cada nivel de retorno intermedio. Sin embargo, su uso debe limitarse estrictamente a la gestión de errores críticos y nunca para el flujo de control normal de la aplicación. Si se utiliza incorrectamente, se puede corromper la integridad del estado de la máquina abstracta de C: no hay un proceso de “desenrollado de la pila” (stack unwinding), lo que significa que los recursos locales (memoria, descriptores de archivos, mutexes) quedan huérfanos, y las variables locales pueden presentar valores inconsistentes si el compilador ha optimizado su almacenamiento en registros en lugar de en la memoria principal.

#include <stdio.h>
#include <setjmp.h>
#include <stdlib.h>

/* El buffer que almacena el contexto de ejecución */
jmp_buf env;

/* 
 * Esta variable DEBE ser volatile. 
 * Si no lo es, el compilador podría mantener su valor en un registro 
 * que se restaurará al estado original de setjmp, perdiendo el cambio.
 */
volatile int error_status = 0;

/* Esta variable NO es volatile. Su valor tras un longjmp es impredecible. */
int local_counter = 0;

void deep_level(int depth) {
    if (depth > 3) {
        printf("[deep] Error crítico en nivel %d. Lanzando longjmp...\n", depth);
        error_status = 1;  /* Modificación segura: se escribe en memoria */
        local_counter = 99; /* Modificación peligrosa: probablemente en un registro */
        
        /* El segundo argumento es el valor que recibirá el setjmp */
        longjmp(env, 1);
    }
    printf("[deep] Nivel %d\n", depth);
    deep_level(depth + 1);
}

void middle_level(int depth) {
    printf("[middle] Entrando nivel %d\n", depth);
    deep_level(depth);
    
    /* Este código nunca se ejecutará si ocurre el longjmp */
    printf("[middle] Salida nivel %d\n", depth);
}

int main(void) {
    int rc;

    /* 
     * setjmp retorna 0 la primera vez que es llamado.
     * Retorna un valor distinto de 0 cuando se invoca longjmp desde algún nivel.
     */
    rc = setjmp(env);

    if (rc == 0) {
        printf("[main] Estableciendo punto de restauración.\n");
        middle_level(1);
        printf("[main] Si llegas aquí, la ejecución fue normal.\n");
    } else {
        /* rc contiene el valor pasado en longjmp (en este caso, 1) */
        printf("[main] Recuperado de longjmp. Código de retorno: %d\n", rc);
        printf("[main] error_status (volatile): %d\n", error_status);
        printf("[main] local_counter (no volatile): %d\n", local_counter);
    }

    return 0;
}

Análisis del mecanismo

El comportamiento observado en el ejemplo depende críticamente de la interacción entre el compilador y el modelo de memoria.

  1. jmp_buf env: Actúa como el “snapshot” del estado del CPU. Al llamar a setjmp(env), guardamos el estado actual de la pila.
  2. El flujo de salto: Cuando deep_level detecta el error, longjmp(env, 1) no simplemente retorna a middle_level, sino que “teletransporta” el puntero de instrucción directamente de vuelta a la línea rc = setjmp(env) en main. Esto salta por encima de los marcos de pila (stack frames) de middle_level y las llamadas pendientes de deep_level.
  3. El problema de la optimización y volatile: Fíjate en local_counter. En una compilación optimizada (como -O2 o -O3), el compilador suele asignar variables locales a registros de la CPU para mejorar la velocidad. Cuando longjmp restaura el contexto, restaura también el contenido de esos registros al estado que tenían en el momento de setjmp. Como local_counter se modificó después de setjmp, su nuevo valor (99) se pierde si el registro fue sobrescrito por el estado restaurado, volviendo a 0. En cambio, error_status, al estar marcada como volatile, obliga al compilador a realizar la escritura y lectura directamente en la memoria RAM, asegurando que el cambio sobreviva al salto.
  4. Retorno de setjmp: La primera llamada siempre devuelve 0. Si longjmp se invoca con un valor val, setjmp retorna val. Si val es 0, setjmp retornará 1 por convención para evitar la ambigüedad.

El error frecuente

Un error clásico y extremadamente sutil es intentar usar longjmp para saltar fuera de un manejador de señales (signal handler). Esto produce un comportamiento indefinido porque longjmp no realiza la limpieza necesaria del estado interno del manejador de señales.

/* ERROR: Código peligroso que causa Comportamiento Indefinido */
void signal_handler(int sig) {
    // longjmp desde un manejador de señales es inseguro
    longjmp(env, 1); 
}

Si necesitas saltar desde un manejador de señales para recuperar la ejecución, debes usar la extensión [POSIX] sigsetjmp y siglongjmp, las cuales están diseñadas para restaurar también la máscara de señales, evitando dejar al proceso en un estado de bloqueo o con señales bloqueadas permanentemente. Además, recuerda que longjmp no ejecuta código de limpieza; cualquier malloc realizado en las funciones saltadas resultará en una fuga de memoria (memory leak) inmediata.

90

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio