next up previous contents index
Next: Procesos bloqueantes Up: Guía de Programación de Previous: Parámetros de inicio   Contents   Index


Llamadas al sistema

Hasta ahora lo único que hemos hecho ha sido usar mecanismos bien definidos del núcleo para registrar ficheros proc y manejadores de dispositivos. Esto está muy bien si quieres hacer algo que los programadores del núcleo pensaron que querrías hacer, como escribir un controlador de dispositivo. Pero ¿y si quieres escribir algo inusual, cambiar el comportamiento del sistema de alguna forma? Entonces, te encuentras solo.

Aquí es dónde la programación del núcleo se vuelve peligrosa. Al escribir el ejemplo siguiente eliminé la llamada al sistema open. Esto significa que no podría abrir ningún fichero, no podría ejecutar ningún programa, y no podría apagar la computadora. Tuve que pulsar el interruptor. Afortunadamente, no se murió ningún fichero. Para asegurate de que tú tampoco pierdas ningún fichero, por favor ejecuta sync justo antes de hacer el insmod y el rmmod.

Olvídate de los ficheros /proc, olvídate de los ficheros de los dispositivos. Son sólo detalles menores. El mecanismo real de comunicación entre los procesos y el núcleo, el que usan todos los procesos, son las llamadas al sistema. Cuando un proceso pide un servicio al núcleo (tal como abrir un fichero, ramificarse en un nuevo proceso o pedir más memoria), éste es el mecanismo que se usa. Si quieres cambiar el comportamiento del núcleo de formas interesantes, éste es el sitio para hacerlo. Por cierto, si quieres ver las llamadas al sistema que usa un programa, ejecuta strace $\langle$orden$\rangle$ $\langle$argumentos$\rangle$.

En general, un proceso se supone que no puede acceder al núcleo. No puede acceder a la memoria del núcleo y no puede llamar a las funciones del núcleo. El hardware de la CPU fuerza esto (por eso se le llama `modo protegido'). Las llamadas al sistema son una excepción a esta regla general. Lo que sucede es que el proceso rellena los registros con los valores apropiados y entonces llama a una instrucción especial, que salta a una posición previamente definida dentro del núcleo (por supuesto, la posición es legible por los procesos de usuario, pero no pueden escribir en ella). Bajo las CPUs de Intel, esto se hace por medio de la interrupción 0x80. El hardware sabe que una vez que saltas a esta localización, ya no te estarás ejecutando en el modo restringido de usuario, sino como el núcleo del sistema operativo. Y entonces se te permite hacer todo lo que quieras.

A la posición en el núcleo a la que un proceso puede saltar se le llama system_call. El procedimiento en esa posición verifica el número de la llamada al sistema, que le dice al núcleo qué servicio ha pedido el proceso. Después mira en la tabla de llamadas al sistema (sys_call_table) para ver la dirección de la función del núcleo a llamar. A continuación llama a la función, y después de retornar hace unas pocas comprobaciones del sistema y luego regresa al proceso (o a un proceso diferente, si el tiempo del proceso ha finalizado). Si quieres leer este código, está en el fichero fuente arch/$<$architecture$>$/kernel/entry.S, después de la línea ENTRY(system_call).

Por lo tanto, si queremos cambiar la forma en que funciona una cierta llamada al sistema, lo que tenemos que hacer es escribir nuestra propia función para implementarla (normalmente añadiendo un poco de nuestro código y después llamando a la función original) y entonces cambiar el puntero que está en sys_call_table para que apunte a nuestra función. Como es posible que seamos eliminados más tarde y no queremos dejar el sistema en un estado inestable, es importante que cleanup_module restaure la tabla a su estado original.

El presente código fuente es un ejemplo de módulo del núcleo. Queremos `espiar' a un cierto usuario e imprimir un mensaje (con printk) cuando el usuario abra un fichero. Para conseguir dicha meta, reemplazamos la llamada al sistema que abre un fichero con nuestra propia función, llamada our_sys_open. Esta función verifica el uid (identificación del usuario) del proceso actual, y si es igual al uid que queremos espiar, llama a printk para mostrar el nombre del fichero que se va a abrir. Luego llama a la función original open con los mismos parámetros, para realmente abrir el fichero.

La función init_module sustituye la localización apropiada que está en sys_call_table y mantiene el puntero original en una variable. La función cleanup_module utiliza dicha variable para devolver todo a su estado normal. Esta aproximación es peligrosa, por la posibilidad de que dos módulos del núcleo cambien la misma llamada al sistema. Imagínate que tenemos dos módulos del núcleo, A y B. La llamada al sistema de A será A_open y la de B será B_open. Ahora, cuando A se inserta en el núcleo, la llamada al sistema es reemplazada con A_open, la cual llamará a la sys_open original cuando haya acabado. A continuación, B es insertado en el núcleo, que reemplaza la llamada al sistema con B_open, que a su vez ejecutará la llamada al sistema que él piensa que es la original, A_open, cuando haya terminado.

Ahora, si B se quita primero, todo estará bien: simplemente restaurará la llamada al sistema a A_open, la cual llama a la original. En cambio, si se quita A y después se quita B, el sistema se caerá. El borrado de A restaurará la llamada original al sistema, sys_open, sacando a B fuera del bucle. Entonces, cuando B es borrado, restaurará la llamada al sistema a la que él piensa que es la original, A_open, que ya no está en memoria. A primera vista, parece que podríamos resolver este problema particular verificando si la llamada al sistema es igual a nuestra función open y si lo es no cambiándola (de forma que B no cambie la llamada al sistema cuando se borre), lo que causará un problema peor aún. Cuando se borra A, a él le parece que la llamada al sistema fue cambiada a B_open y así que ya no apunta a A_open, y por lo tanto no la restaurará a sys_open antes de ser borrado de memoria. Desgraciadamente B_open aún intentará llamar a A_open, que ya no está allí, por lo que incluso sin quitar B el sistema se caerá.

Se me ocurren dos formas de prevenir este problema. La primera es restaurar la llamada al valor original, sys_open. Desgraciadamente, sys_open no es parte de la tabla del sistema del núcleo que está en /proc/ksyms, por tanto no podemos acceder a ella. La otra solución es usar el contador de referencias para evitar que root pueda borrar el módulo una vez cargado. Esto es bueno para de módulos de producción, pero malo para un ejemplo de aprendizaje (que es por lo que no lo hice aquí).

syscall.c

/* syscall.c 
 * 
 * Ejemplo de llamada al sistema "robando" 
 */


/* Copyright (C) 1998-99 por Ori Pomerantz */


/* Los ficheros de cabeceras necesarios */

/* Estándar en los módulos del núcleo */
#include <linux/kernel.h>   /* Estamos haciendo trabajo del núcleo */
#include <linux/module.h>   /* Específicamente, un módulo */

/* Distribuido con CONFIG_MODVERSIONS */
#if CONFIG_MODVERSIONS==1
#define MODVERSIONS
#include <linux/modversions.h>
#endif        

#include <sys/syscall.h>  /* La lista de llamadas al sistema */

/* Para el actual estructura (proceso), necesitamos esto
 * para conocer quién es el usuario actual. */
#include <linux/sched.h>  




/* En 2.2.3 /usr/include/linux/version.h se incluye
 * una macro para esto, pero 2.0.35 no lo hace - por lo
 * tanto lo añado aquí si es necesario */
#ifndef KERNEL_VERSION
#define KERNEL_VERSION(a,b,c) ((a)*65536+(b)*256+(c))
#endif



#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)
#include <asm/uaccess.h>
#endif



/* La tabla de llamadas al sistema (una tabla de funciones).
 * Nosotros justamente definimos esto como externo, y el
 * núcleo lo rellenerá para nosotros cuando instalemos el módulo
 */
extern void *sys_call_table[];


/* UID que queremos espiar - será rellenado desde la
 * linea de comandos */
int uid;  

#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)
MODULE_PARM(uid, "i");
#endif

/* Un puntero a la llamada al sistema original. El motivo para
 * mantener esto, mejor que llamar a la función original
 * (sys_open), es que alguien quizás haya reemplazado la 
 * llamada al sistema antes que nosotros. Destacar que esto
 * no es seguro al 100%, porque si otro módulo reemplaza sys_open
 * antes que nosotros, entonces cuando insertemos llamaremos 
 * a la función en ese módulo - y quizás sea borrado
 * antes que nosotros.
 *
 * Otro motivo para esto es que no podemos tener sys_open.
 * Es una variable estática, por lo tanto no es exportada. */
asmlinkage int (*original_call)(const char *, int, int);



/* Por algún motivo, en 2.2.3 current-uid me da cero, en vez de
 * la ID real del usuario. He intentado encontrar dónde viene mal,
 * pero no lo he podido hacer en un breve periodo de tiempo, y 
 * soy vago - por lo tanto usaremos la llamada al sistema para 
 * obtener el uid, de la forma que un proceso lo haría.
 *
 * Por algún motivo, después de que recompilara el núcleo este
 * problema se ha ido.
 */
asmlinkage int (*getuid_call)();



/* La función con la que reemplazaremos sys_open (la
 * función llamada cuando llamas a la llamada al sistema open).
 * Para encontrar el prototipo exacto, con el número y tipo de
 * argumentos, encontramos primero la función original (es en
 * fs/open.c).
 *
 * En teoría, esto significa que estamos enlazados a la versión
 * actual del núcleo. En la práctica, las llamadas al sistema nunca
 * cambian (se destruirían naufragando y requerirían que los programas
 * fuesen recompilados, ya que las llamadas al sistema son las 
 * interfaces entre el núcleo y los procesos).
 */
asmlinkage int our_sys_open(const char *filename, 
                            int flags, 
                            int mode)
{
  int i = 0;
  char ch;

  /* Checkea si este es el usuario que estamos espiando */
  if (uid == getuid_call()) {  
   /* getuid_call es la llamada al sistema getuid,
    * la cual nos da el uid del usuario que ejecutó
    * el proceso que llamó a la llamada al sistema
    * que tenemos. */

    /* Indica el fichero, si es relevante */
    printk("Fichero abierto por %d: ", uid); 
    do {
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)
      get_user(ch, filename+i);
#else
      ch = get_user(filename+i);
#endif
      i++;
      printk("%c", ch);
    } while (ch != 0);
    printk("\n");
  }

  /* Llamamos a la sys_open original - en otro caso, perdemos
   * la habilidad para abrir ficheros */
  return original_call(filename, flags, mode);
}



/* Inicializa el módulo - reemplaza la llamada al sistema */
int init_module()
{
  /* Peligro - muy tarde para él ahora, pero quizás 
   * la próxima vez... */
  printk("Soy peligroso. Espero que hayas hecho un ");
  printk("sync antes de insertarme.\n");
  printk("Mi duplicado, cleanup_module(), es todavía"); 
  printk("más peligroso. Si\n");
  printk("valoras tu sistema de ficheros, será mejor ");
  printk("que hagas \"sync; rmmod\" \n");
  printk("cuando borres este módulo.\n");

  /* Mantiene un puntero a la función original en
   * original_call, y entonces reemplaza la llamada al sistema
   * en la tabla de llamadas al sistema con our_sys_open */
  original_call = sys_call_table[__NR_open];
  sys_call_table[__NR_open] = our_sys_open;

  /* Para obtener la dirección de la función para la
   * llamada al sistema foo, va a sys_call_table[__NR_foo]. */

  printk("Espiando el UID:%d\n", uid);

  /* Coje la llamada al sistema para getuid */
  getuid_call = sys_call_table[__NR_getuid];

  return 0;
}


/* Limpieza - libera el fichero apropiado de /proc */
void cleanup_module()
{
  /* Retorna la llamada al sistema a la normalidad */
  if (sys_call_table[__NR_open] != our_sys_open) {
    printk("Alguien más jugó con la llamada al sistema ");
    printk("open\n");
    printk("El sistema quizás haya sido dejado ");
    printk("en un estado iniestable.\n");
  }

  sys_call_table[__NR_open] = original_call;
}


next up previous contents index
Next: Procesos bloqueantes Up: Guía de Programación de Previous: Parámetros de inicio   Contents   Index
2003-08-27