Hoy doy comienzo a una serie de artículos de introducción a la programación de módulos para el Kernel de Linux. A diferencia de cuando programamos aplicaciones de escritorio, sistemas o servidores, cuando programamos módulos para el Kernel de Linux, un segmentation fault supone el cuelgue total del sistema y la programación debe ser extremadamente cuidadosa.
No voy a hablaros acerca de lo que es Linux, o de que requisitos debe cumplir un sistema para que sea POSIX Compilant. Voy a hablaros sobre como se estructuran las fuentes del Kernel, como se compilan y como podemos crear y cargar un módulo simple en nuestro sistema. Doy por hecho que sabes como descargar las fuentes del Kernel de tu distribución o bien un Kernel genérico de Kernel.org.
Es necesario por parte del lector tener conocimientos sobre el sistema UNIX y sobre sistemas operativos en general, saber la diferencia entre un system call y un library call y tener unos conocimientos medios en el lenguaje de programación C. No soy un gurú de C ni del Kernel seguramente tenga muchos fallos y cosas que pulir.
Antes de empezar
Seguro que ahora mismo te puedes estar preguntando para que carajo quieres tú saber como se programan módulos para el Kernel de Linux, y es una pregunta legítima pero todo tiene su sentido. Desde la proliferación de dispositivos móviles y sistemas embebidos es más que común el Kernel hacking para potenciar nuestros dispositivos añadiendo funcionalidad o potenciando la existente.
Si nuestro dispositivo corre Android, WeTab, Symbian (EKA2) o Maemo, está ejecutando un sistema Linux con un Kernel Linux que carga módulos para el Kernel de Linux. Otra opción es que sencillamente nos interesa saber como funciona el Kernel de un sistema operativo en profundidad o que sencillamente queremos aprender más de UNIX o Linux.
Creo que no es necesario que te hable de las bondades de VirtualBox o de QEMU para estos menesteres (casi mejor QEMU)
Estructura del código fuente del Kernel de Linux
El código fuente del Kernel de Linux se estructura en varios directorios:
arch. Este directorio contiene archivos referentes a arquitecturas específicas. Dentro del directorio arch/ existen subdirectorios para diferentes tipos de arquitecturas como x86, ARM, Motorola 64K, MIPS o SPARC.
block. Contiene la implementación de algoritmos de planificación de E/S necesarios para el uso de dispositivos de almacenamiento por bloques
crypto. Contiene la implementación de operaciones de cifrado y la API criptológica que es usada por ejemplo por los dispositivos WiFi para implementar algoritmos de cifrado
Documentation. Este directorio contiene la descripción de muchos subsistemas así como información valiosa sobre el funcionamiento del Kernel
drivers. Este directorio contiene multitud de subdirectorios con los controladores de numerosos dispositivos separados por clases de dispositivo. Es el directorio con mayor contenido de largo
fs. Contiene la implementación de los diferentes sistemas de archivos como EXT3, EXT4, resiserfs, HFS, XFS, NTFS, FAT y otros
include. Los ficheros de cabecera del Kernel residen en este subdirectorio.
init. Contiene código de alto nivel de inicio y arranque
ipc. Contiene el código de soporte para la Comunicación entre Procesos (IPC) donde se implementan mecanismos como las colas de mensajes, los semáforos, y la memoria compartida
Kernel. Las porciones del Kernel indpendientes de la arquitectura se encuentran en este directorio
lib. Contiene el código que implementa rutinas de librería
mm. En este directorio se encuentra la implementación de los mecanismos de gestión de memoria
net. Contiene la implementación de los protocolos de red.
scripts. Scripts usados durante la construcción del Kernel
sound. El subsistema de audio de Linux se encuentra en este subdirectorio
usr. Contiene la implementación del sistema initramfs
Compilando el Kernel
A la hora de compilar un Kernel es importante comenzar con la era bien limpica por lo que es siempre recomendable comenzar haciendo un make clean
en el directorio principal del Kernel. Una vez tenemos la era limpia debemos proceder a la configuración del Kernel, esto puede variar de distribución a distribución pero si tu Kernel está configurado actualmente para soportar el archivo /proc/config.gz te sugiero que lo utilices como plantilla de configuración:
$ zcat /proc/config.gz > .config
Si no, busca información referente a la compilación y configuración del Kernel para tu distribución en su documentación. Como último recurso siempre puedes copiar la configuración estándar de arch/x86/configs/[i386|x86_64]_defconfig y utilizarla como base.
Ahora podemos invocar al menú de configuración con make menunconfig
ten en cuenta que necesitas las librerías ncurses para ello. Cuando hemos acabado de configurar el Kernel podemos compilarlo con make bzImage
y los módulos con make modules
Carga y descarga de módulos
Linux provee de herramientas para la carga y descarga de módulos, estas son modprobe y rmmod. Además podemos listar los módulos que actualmente hay cargados en el sistema con lsmod. El Kernel de Linux no carga los módulos en el arranque por defecto a no ser que se lo indiquemos activando la opción Automatic Kernel Modules Loading (CONFIG_KMOD) en la configuración.
El Kernel solo carga aquellos módulos de los subsistemas presentes en la máquina o dispositivo que no hayan sido reclamados por ningún controlador aún. Modprobe además carga todas las dependencias del módulo que queremos cargar búscandolas en el archivo de dependencias del Kernel (modules.dep).
Para compilar un controlador como módulo, debemos fijar el valor correspondiente en el menú a <M> mientras configuramos el Kernel.
Nuestros propios módulos
Aunque parezca algo super oscuro y vetado para los mortales, crear nuestro propio módulo y compilarlo es una tarea bastante trivial. Tan solo tenemos que escribir un archivo en C en un directorio y crear un archivo Makefile super sencillo para compilarlo.
Las macros __init y __exit
Todo módulo para el Kernel requiere de un método de inicio y otro de finalización. A partir de la versión 2.2 del Kernel, se integraron las macros __init
y __exit
. La macro __init
hace que se deseche a la función de inicio y su memoria sea liberada una vez finaliza la carga para controladores integrados pero no para la carga de módulos.
La macro __exit
desecha la función de finalización igualmente para controladores integrados pero no para módulos. Esto es perfectamente comprensible si entendemos que un controlador integrado en el núcleo nunca llamará a dichas funciones.
Documentación del módulo
Lo sano y recomendable, es que nuestros módulos estén bien documentados. Existen varias macros que podemos utilizar para documentar nuestro trabajo.
Normalmente se suele definir la licencia bajo la que se libera el módulo, el autor o autores del mismo, una descripción y el dispositivo al que ofrece soporte. Es importante que definamos todos esos campos si lo que pretendemos es que nuestro trabajo llegue algún día a la rama principal del árbol del Kernel
Poniéndolo todo junto
Bueno, ya está bien de teoría por el momento, vamos a escribir nuestro primer módulo para el Kernel de linux y un archivo Makefile para compilarlo. Primero escribimos el código del módulo al que llamaremos tronco.c:
/* * tronco.c – Un ejemplo completamente inútil de modulo para el Kernel */Una vez tenemos el código C podemos escribir el archivo Makefile para compilar nuestro módulo de forma sencilla como un módulo externo:include <linux/module.h> /* Todos los modulos lo necesitan */
include <linux/kernel.h> /* Ofrece la macro KERN_INFO */
include <linux/init.h> /* Ofrece las macros de inicio y fin */
static int __init tronco_start(void) { printk(KERN_INFO "Que pasa tronco!\n"); /* * Si no se devuelve 0 significa que init_module ha fallado y no ha podido cargarse. / return 0; } static void __exit tronco_cleanup(void) { printk(KERN_INFO "Me voy dando el piro!\n"); } / * Indicamos cuales son las funciones de inicio y fin / module_init(tronco_start); module_exit(tronco_cleanup); / * Documentacion del modulo */ MODULE_LICENSE("GPL"); MODULE_AUTHOR("Genbeta Dev - https://www.genbetadev.com"); MODULE_DESCRIPTION("Este modulo no vale para absolutamente nada"); MODULE_SUPPORTED_DEVICE("eltiodelavara");
obj-m += tronco.o
all:
make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean
Ahora tan solo nos queda ejecutar make.
Si la compilación se queja y falla debemos preparar nuestro Kernel para compilar módulos externos en /usr/src/linux o donde sea que tenemos las fuentes del Kernel:
$ cd /usr/src/linux
$ make oldconfig
$ make prepare
$ make modules_prepare
Si el módulo compila esta vez sin problemas podremos ver la información del mismo ejecutando $ modinfo ./tronco.ko
:$ modinfo ./tronco.ko
filename: ./tronco.ko
description: Este modulo no vale para absolutamente nada
author: Genbeta Dev - https://www.genbetadev.com
license: GPL
depends:
vermagic: 3.0.0-gentoo SMP mod_unload modversions
Puede verse nuestra documentación y en mi caso que ha sido compilado para la versión 3.0.0 del Kernel. Si lo cargamos con $ insmod ./tronco.ko
y ejecutamos dmesg
veremos el mensaje “Que pasa tronco!“ si lo descargamos con rmmod tronco
y volvemos a ejecutar dmesg
veremos el mensaje “Me voy dando el piro!“.
Como veis esto no sirve para nada en absoluto ni tiene ningún valor de ninguna clase pero es un acercamiento a la forma en la que el Kernel de Linux funciona y en como se compilan, se cargan y se descargan módulos del Kernel en Linux. En próximas entregas hablaremos más en profundidad del asunto.
Más Información | Linux Driver Development 3rd edition