Ir al contenido principal

Desarrollo de módulos para el kernel Linux

Una de las primeras cosas que tiene que saber un hacker del kernel es cómo crear un módulo para el kernel Linux. Para continuar leyendo este artículo es conveniente haber leído antes el anterior Entorno para desarrollo del kernel Linux.
Linux es un kernel del tipo monolítico, lo cual quiere decir, grosso modo, que todo el kernel es un gran archivo ejecutable que se carga y toma el control al iniciar el ordenador. El problema de este tipo de kernels es que al compilarlo hay que añadir todos los controladores para el hardware que queremos soportar. Esto es un problema si queremos hacer un kernel genérico que soporte una gran variedad de dispositivos, ya que su tamaño será considerable, e incluso inviable.
Linux soluciona este problema a través de los módulos, por lo que podemos decir que Linux es un kernel modular que carga en memoria aquellos módulos que necesita en cada momento. Las distribuciones genéricas, como por ejemplo, Ubuntu, compilan un núcleo muy pequeño con lo mínimo para poder arrancar y en tiempo de ejecución carga en memoria aquellos módulos necesarios para dar servicio a los dispositivos que tiene nuestro equipo.
Los módulos son, pues, la manera más sencilla y cómoda de añadir código y funcionalidad al espacio del kernel sin tener que andar recompilándolo.
En este artículo vamos a crear un módulo muy simple (de hecho el más simple posible) y lo vamos a cargar en la máquina virtual Qemu que preparamos en el artículo anteriormente citado.



Una vez creado el módulo lo copiaremos desde nuestra máquina host a la máquina virtual usando scp. Por lo tanto, tenemos que asegurarnos de que nuestra máquina virtual tiene conectividad vía red con la máquina host. En mi caso, la configuración del kernel que compilamos en el anterior artículo tiene activado el soporte de la tarjeta de red Intel e1000 (la que emula Qemu) como módulo, pero en este caso, para facilitarnos la tarea nos va a interesar que el soporte a esta tarjeta esté compilado como parte integrante del kernel. Así que nos vamos a ir al directorio donde está el código fuente del kernel y nos vamos a asegurar de que el soporte para la tarjeta ethernet Intel e1000 está activado en el archivo .config (debe aparecer como CONFIG_E1000=y) y volvemos a compilar.

cd ~/kerneldev/src/linux-$(uname -r)
sed -i s/CONFIG_E1000=m/CONFIG_E1000=y/ .config
make -j4

También tenemos que asegurarnos de que tenemos instalado el servidor ssh en nuestra máquina host para poder copiar los ficheros de una máquina a otra.

sudo apt-get install openssh-server

Hechos los preparativos, podemos entrar en harina y comenzar a desarrollar nuestro módulo. Un módulo no es más que un programa escrito en C (aunque nada nos impide hacerlo directamente en ensamblador si nos sentimos fuertes) que cumple algunas condiciones. Veamos el código de nuestro primer módulo que pondremos en un archivos llamado hello_module.c y que estará en el directorio ~kerneldev/src/modulo1.

#include <linux/module.h>

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Modulo Hola mundo");
MODULE_AUTHOR("Alberto Garcia");

static int __init hello_init(void)
{
    printk(KERN_INFO "Hola mundo.\n");
    return (0);
}

static void __exit hello_exit(void)
{
    printk(KERN_INFO "Hasta otra.\n");
}

module_init(hello_init);
module_exit(hello_exit);

De entrada nos encontramos con dos funciones de lo más normales salvo por que antes del nombre incluyen __init o __exit. Un módulo ha de tener al menos dos funciones: una que se invoca cuando se carga el módulo en memoria y que es la que lleva __init; y otra que es llamada cuando se descarga el módulo del kernel, y es la que lleva __exit. Esta función se encargará de limpiar y liberar todos los recursos que estemos usando. Si tienes curiosidad por saber como se definen __init y __exit puedes verlo en linux/init.h.
Las dos últimas líneas indican qué función es la que ha de llamarse al cargar y descargar el módulo usando module_init() y module_exit() respectivamente.
Las tres primeras líneas tras el #include son bastante autodescriptivas: en ellas indicamos el tipo de licencia del módulo (en este caso GPL), la descripción y el autor.
Otra función que no te resultará familiar es printk(). Recuerda que este código se ejecuta en el espacio del kernel y por lo tanto no tenemos acceso a la librería de C libc, es decir, no tenemos printf() así que tenemos que usar la función printk() que pertenece al código del kernel.

Para compilar el módulo vamos a usar el siguiente makefile.

ifneq ($(KERNELRELEASE),)
obj-m := hello_module.o 
else
KERNEL_VERSION = linux-3.5.0
KERNELDIR := ../$(KERNEL_VERSION)
PWD := $(shell pwd)
modules:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
endif

clean:
rm -rf *.[oas] .*.flags *.ko .*.cmd .*.d .*.tmp *.mod.c \
.tmp_versions Module.symvers

Es importante poner la variable KERNEL_VERSION a su valor correcto. En mi caso particular el directorio donde está el código fuente de Linux es linux-3.5.0. Para compilar nuestro módulo sólo hemos de ejecutar la utilidad make y, si todo va bien, obtendremos varios archivos entre los que se encuentra el que vamos a usar nosotros, que es hello_module.ko.

Para probarlo iniciamos la máquina virtual Qemu que ya habíamos preparado en el artículo anterior con nuestro recién compilado kernel y una vez dentro copiamos el módulo hello_module.ko desde dentro de la máquina virtual.

scp alberto@10.0.2.2:kerneldev/src/modulo1/hello_module.ko .

La dirección IP 10.0.2.2 es la que Qemu le da por defecto a la máquina host, así que sólo tienes que cambiar alberto por el nombre el usuario de tu máquina host.
Una vez copiado, podemos cargar el módulo en memoria usando insmod:

insmod hello_module.ko

Si todo va bien verás aparecer la frase hola mundo. Para ver los módulos cargados en memoria usamos el comando lsmod. Finalmente, para descargar el módulo de memoria usamos el comando rmmod, que nos mostrará una frase de despedida.

rmmod hello_module


En este artículo llamado Linux Kernel Modules - Load, Unload, Configure puedes encontrar más información sobre la gestión de los módulos.
Por ahora nuestro módulo no es demasiado útil, pero nos ha permitido comprender todo el proceso de creación, carga y descarga. En próximos artículos iremos ampliándolo y empezaremos a hacer cosas más interesantes dentro de nuestro módulo.

Comentarios

  1. Buenas noches. Muchas gracias por esta info tan valiosa!!!
    webnegociosargentina@gmail.com

    ResponderEliminar

Publicar un comentario

Entradas populares de este blog

Criptografía en Python con PyCrypto

A la hora de cifrar información con Python, tenemos algunas opciones, pero una de las más fiables es la librería criptográfica PyCrypto, que soporta funciones para cifrado por bloques, cifrado por flujo y cálculo de hash. Además incorpora sus propios generadores de números aleatorios. Seguidamente os presento algunas de sus características y también como se usa.


Regresión lineal y descenso de gradiente con Python

En machine learning, el objetivo principal es encontrar un modelo que explique el comportamiento de un sistema (en el amplio sentido de la palabra). A partir de unos datos de entrenamiento, un sistema de aprendizaje automático ha de ser capaz de inferir un modelo capaz de explicar, al menos en su mayoría, los efectos observados. Pero también aplicar ese aprendizaje. Por ejemplo, un sistema de machine learning muy lucrativo para las empresas anunciantes es aquél que dado un perfil de usuario (datos de entrada A), sea capaz de predecir si pinchará o no (salida B) sobre un anuncio publicitario de, por ejemplo, comida para gatos. No es sencillo crear un modelo capaz de predecir el comportamiento del usuario (o sí), pero en todo caso, existen diferentes técnicas que nos permiten abordar el problema. En el caso del ejemplo que acabamos de ver, el modelo debería ser capaz de clasificar a los usuarios en dos clases diferentes, los que pulsarán y los que no pulsarán el anuncio de comida de ga…

Desbordamiento de enteros (Integer Overflow)

Ya os he hablado en este blog de posibles problemas potenciales que se pueden dar en los programas y que son susceptibles de ser explotados para hacer que dichos programas se comporten de forma diferente a la que deberían. Uno de estos problemas es el del desbordamiento de la pila. Sin embargo, hay otros posibles errores de programación que, aunque menos obvios, son igual de peligrosos. Uno de ellos es el desbordamiento de enteros o integer overflow. Para entender cómo funciona os presento un ejemplo muy sencillo pero didáctico.