G A C E T A   D E   L I N U X
...haciendo a Linux un poco más divertido!
setjmp/longjmp Ilustrados
Por Raghu J Menon
Traducción al español por José Gregorio Del Sol Cobos
el día 19 de Mayo 2003, para La Gaceta de Linux
 

El conjunto de macros setjmp/longjmp implementadas en el lenguaje de programación C proporcionan la plataforma perfecta para realizar control de flujo complejo, pero esté seguro de que ha obtenido el conocimiento adecuado sobre ellas antes de que efectivemente las use, o si no  sus programas podrían hacerse tan complejos que sería imposible descifrarlos.

¿Qué hacen?

La función setjmp salva el estado de un programa. El estado de un programa, para ser precisos, consiste en los valores de sp ("stack pointer", puntero de pila), fp ("frame pointer2, puntero de marco), pc ("program counter", contador de programa). El estado de un programa está completamente definido por este conjunto de registros y los contenidos de la memoria, lo que incluye la pila y la cola. La siguiente pregunta obvia sería ¿para qué necesito salvar el estado? Bien, sencillamente para restaurarlo más tarde mediante longjmp. Así estas dos funciones cazan en pareja, esto es, setjmp salva el estado, lonjmp lo restaura.

La sintaxis...

La sintaxis es bastante sencilla. setjmp almacena el estado del programa en una variable de tipo jmp_buf (definida en el archivo de cabecera setjmp.h). Incluya siempre el archivo de cabecera cuando trabaje con estas funciones.

int setjmp (jmp_buf env);

int longjmp(jmp_buf env , int val);

La función longjmp restaura el estado del programa que es almacenado en env. El propósito del parámetro val será explicado más tarde. ¿Así que a qué se añade todo esto? Sencillamente que la función longjmp nunca retorna (otra después de exec). Antes de encontrar una longjmp tiene que haber una setjmp que salve el estado en env y retorne un valor 0. Cuando usted encuentra longjmp la siguiente vez el estado almacenado en env es restaurado y la ejecución del programa se retoma en la instrucción después de setjmp. Así es como longjmp retorna a través de setjmp. Este retorno debería producir un valor y ese valor es lo que se especifica mediante el parámetro val.

i = setjmp (env);//Almacena el estado en env y retorna 0

...........      //Retoma la ejecución en este punto tras la llamada a longjmp como si fuera la llamada a setjmp

.......         //retornado.

 

longjmp(env,val)

Como un último punto, intente imprimir el valor de i. Debería obtener dos valores, el primero es el obtenido cuando setjmp salva el estado y será 0 siempre. El segundo será el valor que usted le pasa a longjmp a través del parámetro val. Así el código después de setjmp parece ejecutarse más de una vez. Esto reclama alguna exploración. Por lo tanto, tenemos nuestro primer código y uno también interesante. if-else.c

  Compílelo y ejecútelo. Espero que se dé cuenta de ello: ¡se han ejecutado ambas partes de la condición, el if y el else! Ahora, esto no es como se espera que se comporte la condición if-else. Parece como fork() (el padre ejecuta la parte if y el hijo la else, o viceversa). Bien, en fork tenemos dos hilos diferentes en ejecución, que no es el caso aquí. La llamada a setjmp salva el estado en env y devuelve 0. La condición if lo evalúa como cierto y usted obtiene el primer mensaje. Ahora más adelante en el código, cuando se ejecuta longjmp el estado se restaura y usted vuelve a la sentencia siguiendo a setjmp con un valor de retorno 2.

Este valor de retorno es especificado en la llamada a longjmp. Ahora ve por qué la condición if fallaba y la else se ejecutaba. Además el programa mostraba una disparidad al no ejecutar la última línea. Bueno, como dije antes, longjmp nunca devuelve y así es bastante obvio por qué no se ejecuta la última línea. Si usted toma la sentencia de salida el código cae en un ciclo sin fin, alternando entre la parte del else y la llamada a longjmp.

Algo más útil, por favor...

Como programador, usted puede haber escrito código dividiéndolo en funciones o subrutinas (Si no, aprenda el arte de la programación funcional. Yo comencé escribiendo un programa de C como una gran función principal ("main"), sin embargo, gradualmente fui pudiendo dividir mi programa en funciones. ¿Por qué? Es más fácil de revisar, he ahí el porqué). Al implementar su programa como funciones, hay enlaces que han de ser llamadas a funciones que están anidadas, y que asimismo tienen un flujo complejo. Cada vez que ocurra un error, se necesita encontrar la función que lo causó. De este modo es más fácil revisar el programa. El código de abajo muestra el uso del par setjmp/longjmp al revisar tales programas.nest.c

Bien, el programa no hace nada útil más que servir al propósito de ilustrar el manejo elegante de errores. El código define cuatro funciones, cada una de las cuales, aparte de aceptar el númeo especificado de parámetros neteros, también tiene a env como parámetro. La env mantiene el estado del programa salvado por la llamada a setjmp de la función principal. El fallo al ejecutar cada función es especificado en la condición if. Compile el programa y ejecútelo. Ingrese los siguientes conjuntos de valores para l, m, n.

Ingrese valores (enteros) para l, m y n, por favor

1

4

7

Las funciones s eejecutaron normalmente


Ingrese valores (enteros) para l, m y n, por favor

0

0

0

Hay un error en la función 1. Saliendo...


Ingrese valores (enteros) para l, m y n, por favor

1

1

2

Hay un error en la función 2. Saliendo...


Ingrese valores (enteros) para l, m y n, por favor

0

1

2

Hay un error en la función 3. Saliendo...



Ingrese valores (enteros) para l, m y n, por favor

1

2

3

Hay un error en la función 4. Saliendo...


 

 Bien, supongo que esto era útil. El mensaje de error podria decirle dónde ocurrió el error. Sigamos el código. La setjmp de la función principal salva el estado del programa y devuelve 0. La condición if se iguala a falso y por tanto no se ejecuta. La siguiente sentencia llama a la función fun1 con los parámetros env, l, m, n; fun1 entonces llama a la función fun2, y así sucesivamente. Cuando ocurra un error en cualqueira de estas funciones se ejecuta longjmp, siendo el parámetro val el número de la función donde se ejecutó la longjmp. El programa vuelve a la función principal (a la sentencia después de setjmp) cada vez que se ejecuta una longjmp. El valor de s ahora es bien 1, 2, 3 ó 4, dependiendo de dónde se hizo la llamada a longjmp. La condición if se iguala ahora a verdadero y por tanto de despliega un mensaje apropiado de error, indicando la función en la que se produjo el error. Si no ocurrió ninguno durante la ejecución del programa, la función vuelve normalmente y la última sentencia de la función principal es ejecutada. ¿Por qué no empleé simplemente la sentencia goto para hacer un salto cuando ocurriese un error? Intente compilar el código de goto.c. El error saltó porque el goto se puede usar sólo para saltos locales. Los saltos en el programa previo hechos por longjmp no eran locales, "goto" busca etiquetas locales y de aquí que no puedan hacer saltos no locales.

Vulnerabilidad del Programador...

Hay un fallo sutil en setjmp/longjmp, no en su implementación, pero sí en la forma como las usamos. La mayoría de nosotros somos bastante ignorantes, y con toda la razón, del estado de la pila cuando escribimos un programa. Es cuando ocurre un error que intentamos trazarlo inspeccionando la pila (mediante gdb). Cada vez que hay una llamada a una función la pila es manipulada. Primero los argumentos para la función reclamada son empujados en sentido inverso. Después el JSR es llamado para empujar la dirección de retorno (pc) y después el fp, el fp y el sp se vacían para hacer un nuevo marco de pila para la función llamada. A la entrada ésta inmediatamente crea espacio en la pila para las variables locales que se pudieran declarar ne la función. Ahora que ya tiene una idea de la estructura de la pila, intente ejecutar el código de seg.c.  Se compila bien, pero desgraciadamente falla completamente. ¿Podría encontrar la razón?

        Sigamos el código. La función principal llama  a me_first con dos argumentos, éstos son empujados a la pila de env seguidos por la cadena "IC-Labs", el JSR entonces empuja los valores de pc y fp en la pila. A la entrada la función crea una variable local i en la pila. Esto es seguido por una llamada a la función setjmp, que salva el estado actual, el de la función me_first. La variable local ahora contiene el valor 0, el devuelto por setjmp. Después de retornar desde me_first la pila es devuelta a su estado original, en el que dejó a la función principal. La función i_follow se llama entonces con un valor 3 y la variable env. La pila es modificada como arriba (cuando se llamó a me_first). En la función, el estado almacenado en env es restaruado por longjmp. Los valores en la pila siguen siendo los mismos, como durante la ejecución de la función i_follow. Así pues el estado es el de la función me_first. El marco de la pila de este estado tiene una variable de tipo (char *) que previamente tenía una cadena "IC-Labs". Ahora, después de que el estado ha sido restaurado, el valor que mantiene la variable s es 3 (el valor que i_follow recibió desde la principal). Como resultado de longjmp la sentencia que sigue a setjmp en me_first es ejecutada. Al ejecutar la sentencia después de setjmp (printf), hay un acceso ilegal a memoria pues al intentar imprimir s, el programa intenta encontrar una cadena en el sitio de memoria 0x3 que causa un error de protección de memoria y hace que el programa falle. Este fallo es muy sutil y a menudo no se hace notar, ya que los marcos de pila de las funciones parecen casi el mismo. En los casos en que los marcos de pila son el mismo no hay ese error. Intente reemplazar el argumento "char *" con uno de tipo entero, y vuelva a ejecutarlo. ¿Falló?

Manejo de señales...

Una de las bellezas de estas funciones es que puede hacer longjmp desde un manejador de señales y volver a su programa y cazar esas señales de nuevo. Compruebe el programa sig.c

La función principal instala un manejador de señales usando el sistema de llamadas a señales, los parámetros son el signo(SIGALRM) que indica la señal para la que está instalando el manejador de señales y la rutina manejadora que se ejecuta cuando se da la señal. La llamada de alarma envía la señal SIGALRM al programa cada segundo. Básicamente el alrarm_handler hace longjmp después de que han pasado 8 segundos.

 

[BIO] Estoy con mis estudios de graduado en ciencia de computadores e ingeniería. Soy de Trichur (un apequeña villa en el propio pueblo de dios, Kerala). Cualquier crítica constructiva referente al estilo y al contenido será bienvenida. Siéntase libre de contactar onmigo por email.


Copyright © 2003, Raghu J Menon. Licencia de copia http://www.linuxgazette.com/copying.html
Publicado en el número 90 de Linux Gazette, Mayo de 2003