sábado, 5 de mayo de 2012

POSIX: INT_MAX, INT_MIN and SIGFPE

En sistemas operativos modernos, los límites de direccionamiento de memoria y cantidad de procesos/threads está estipulada de dos formas: la primera acorde al hardware específico con que cuenta el equipo, y la segunda, mediante configuración específica del sistema operativo.

A modo de ejemplo, y para dejar las cosas más en claro, vamos a hablar un poco sobre los límites de los procesos: Cada proceso en el sistema operativo ocupa un slot en una estructura de datos denominada kernel process table. Esta tabla contiene toda la información necesaria que el kernel mantiene a fin de poder manejar los procesos, schedulear las distintas tareas (acorde a sus prioridades) y estados de los mismos. Cuando el OS bootea, el kernel inicializa una estructura de datos denominada process_cache, donde comenzará a direccionar la estructura de procesos que luego serán empleada por el kernel y el userspace. Esta tabla es una lista doblemente enlazada con dos punteros, uno a su elemento anterior y otro a su elemento posterior, la cual tiene una capacidad máxima que se define acorde a la cantidad de memoria física instalada en el equipo. El sistema operativo, para esto asigna la cantidad de memoria instalada a una variable MAX_MAXUSERS (que no tiene nada que ver con el número máximo de usuarios que el sistema soporta), que luego será utilizada por dos variables más del propio kernel max_nprocs y maxuprc. max_nprocs define el número máximo de procesos del sistema operativo, y maxuprc determina el número máximo de procesos que usuarios no privilegiados (que no sean root), puedan crear y ocupar slots en la kernel process table. Y basándose en el valor de MAX_MAXUSERS, poder definir el tamaño en memoria de cada segmento del proceso (región .text, .stack, .heap, .bss, etc.).

INT_MIN e INT_MAX:
Como el número máximo de procesos, otros limites se encuentran configurados en el sistema operativo, y en muchos casos son estándares POSIX, como es el caso de INT_MAX e INT_MIN. INT_MAX representa el máximo valor de un número entero, sin signo que puede ser almacenado en una variable. Y caso contrario INT_MIN representa el valor mínimo de un numero entero que puede ser almacenado en una variable. El mismo se encuentra definido en Linux, en el header include/limits.h, como podemos ver a continuación:


En caso de que este valor sea excedido, la variable hace overflow, y comienza su cuenta nuevamente desde cero. Podemos comprobar esto mismo, de una manera bastante sencilla, con un simple programa en ANSI C, llamando a imprimir el valor de INT_MAX haciendo el include de limits.h


¿Pero que sucede, si asignamos a una variable, un valor mayor al de INT_MAX?. Podemos verlo, compilando el siguiente ejemplo:


Si ejecutamos esto, obtendremos el valor -2147483648. El número se hizo negativo, debido a que se produjo un overflow, y la cuenta empezó pero alrevez.

SIGFPE:
Las señales, son un mecanismo de IPC en entornos Unix, los procesos necesitan comunicarse entre sí, y para ello utilizan un mecanismo basado en señales, que pueden ser sincrónicas. Osea aquellas originadas por alguna condición de trap tal como segmentation fault (cuando accedemos a una dirección de memoria invalida), floating point exception (cuando dividimos por cero), entre otras. Por su parte, las señales asincrónicas, para notificar o recibir eventos externos, no necesariamente relacionados con el flujo del programas. Un ejemplo de esto sería la señal SIGKILL (kill) (para matar un proceso), SIGHUP, SIGABR. Por cada señal que es enviada, hay tres posibles acciones que un proceso puede realizar: Puede ser atrapada, puede ser ignorada o se puede realizar la acción por default (cada señal tiene una acción por default, por ejemplo terminar un proceso con SIGKILL). En Linux, estas señales se encuentran definidas en include/asm/signal.h, y como seguramente ya sabrán, pueden ser especificadas desde el userland mediante el comando kill(2), seguido de nombre de la señal, o el número. Este número de señal, se define en el header include/bits/signum.h.
Como comenté anteriormente dividir por cero, produciría una señal sincrónica, denominada Floating Point Exception o también SIGFPE. Veamos el siguiente ejemplo:


Primero lo compilaremos utilizando gcc(2) y luego lo ejecutaremos solo, y por último utilizando strace(2), cuya salida es más extensa, pero fue acortada para no hacer tan largo este post :-).
# gcc example.c -o example
# ./example 
Floating point exception

# strace ./example 
execve("./example", ["./example"], [/* 27 vars */]) = 0
brk(0)                                  = 0x22d9000
munmap(0x7f81bed4e000, 108846)          = 0
--- {si_signo=SIGFPE, si_code=FPE_INTDIV, si_addr=0x400494} (Floating point exception) ---
+++ killed by SIGFPE +++
Floating point exception
Bien, vemos que el proceso fue terminado mediante la señal SIGFPE, que en este caso se produjo por dividir INT_MIN por -1. ya que al dividir un número negativo por otro número negativo da un número positivo. Ahora bien, ya que sabemos esto, estamos en condiciones de resolver el siguiente desafío:



Nuestro objetivo, es ejecutar la shell (/bin/sh), que se encuentra dentro de la función catcher.
Al ejecutar, lo primero que hace este programa, es verificar la cantidad de argumentos que le son pasados al mismo. Por lo cual, sabiendo que el primer argumento es el nombre del programa, debemos especificarle dos más, donde el último argumento no debe ser cero (debido a que utiliza atoi). Siendo esto correcto, se establece un handler para entrar en la función catcher, y para entrar en la misma debemos producir la ya famosa Floating Point Exception, osea SIGFPE. ¿Como podemos producirlo?, si no tenemos la capacidad de dividir por cero. Bien, podemos hacerlo utilizando la última línea que divide ambos argumentos con un valor distinto a cero: return atoi(argv[1]) / atoi(argv[2]);
. Entonces, sabiendo que si dividimos INT_MIN por -1 podremos producir SIGFPE, veamos:
# gcc challenge.c -o challenge
# ./challenge  -2147483648 -1

WIN!
sh-4.2# 

1 comentario: