• [tutorial] programación de drivers/kernel module para linux.

 #435711  por NvK
 30 Ene 2014, 23:52
Hace algún tiempo vengo escribiendo pequeños kernel modules y publicandolos, hoy vengo con algo
distinto.. un pequeño tutorial con el objetivo que ustedes puedan programar sus propios drivers.

En éste tutorial no se profundizara acerca del manejo de memoria, la relación entre el hardware y el kernel,
el control de las interrupciones, etc. Si no que se centrara en como poder programar tu kernel module de una manera practica,
y las medidas basicas que se deberian tomar cuando se programa a éste nivel ya que un problema podría propagarse
a todo el kernel lo que te llevaría a un reinicio, Panic, o un Oops.

Que necesitarás:
[+] Una máquina virtual(también opcional, puedes hacerlo con una fisica).
[+] Backtrack 5 r3 o r2(éste es opcional ya que tratare con una versión del kernel 2.6 y superiores).
[+] Un editor de texto(yo usaré gedit).
[+] Conocimientos en C.

Introducción:
Los modulos del kernel son piezas de código escritas por un programador, que sirven para extender la funciónalidad
del kernel, sin ellos cada vez que quisieramos extender el kernel tendríamos que reconstuir todo de nuevo.
Ésto tiene muchas ventajas, ya que ademas de ahorrar tiempo, reducimos muchos líneas de código y memoria.


¿Que puedes hacer con ellos?:
Muchas cosas, realmente no existe un "límite", desde programar malware, hasta hacer dispositivos fisicos para
guardar algún tipo de información, interacción con los dispositivos de internet(como por el ejemplo el modelo OSI),
hooks, etc...

Preparando el entorno:
Vamos a crear una carpeta en el escritorio(o donde quieras), y vamos a llamarla mis_kernels_module y crearemos
2 archivos más, 1 de nombre mi_module.c y otro de nombre Makefile.
Aca te dejo un pequeño resumen de "Makefile" por si no sabes lo que es: [ Debe registrarse para ver este enlace ]
Makefile
obj-m += mi_module.o
 
all:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clear
Claro que en "obj-m += mi_module.o" puedes poner el nombre de tu archivo en c(con el que se compilara),y debe terminar en .o

Precauciones con Makefile:
Me ha pasado muchas veces que por un "error" de tabulación o identación, make no compila o nos muestra un mensaje raro,
Aca un ejemplo:

Ten ésto en cuenta cuando compiles y asegúrate de que los espacios estén corregidos.

Programando nuestro primer kernel module.
Desde éste momento comienza la programación del modulo en sí, así que te mostrare lo basico y mi formula de programación,
pero recuerda que ésta no es la única manera, si te interesa dejare que lo busques por tu cuenta.
mi_module.c
/* Includes necesarias para el kernel */
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>

static int __section(.init.text) __cold notrace INIT_KERNEL(void)
{
	printk(KERN_INFO "Hola mundo desde el kernel!!!\n");
	
	return 0;
}

static void __section(.exit.data) EXIT_KERNEL(void)
{
	printk(KERN_ALERT "Driver desinstalado correctamente.");
}

module_init(INIT_KERNEL);
moduel_exit(EXIT_KERNEL);
Antes de continuar voy a explicar un poco lo que hice y que significa cada cosa.
vamos por partes como dijo jack el destripador:
static int __section(.init.text) __cold notrace INIT_KERNEL(void)
static:
Es comun que siempre veas ésto en programción de modulos, ya que la mayoria de veces el
kernel usa macros como EXPORT_SYMBOL, y recordemos que al ser estatico puede exportar
la dirección de la función(por que ciertamente la dirección sería la misma) sin tener que redeclararlas
en el archivo de cabezera, entonces a reducidas cuenta se puede "llamar" a esa función de una manera
más comoda.
Otro uso es que muchas operaciónes del kernel(como algúnas estructuras) necesitan esas funciónes estaticas
para que puedan ser usadas(un claro ejemplo son las operaciones en dispositivos de caracteres).

NOTA:el nucleo tiene miles de modulos que importa de forma dinámica(como por ejemplo los archivos .so linkeandolos)
si éstas direcciones no serían estaticas en el espacio del kernel imaginense la cantidad de conflictos que habría al
importarse por nombre, sería un caos, las funciónes podrían ser confundidas, lo que llevaría a una polución de la información.


__section(.init.text) && __section(.exit.data):
Éste al igual que .exit.data(y muchos otros) tienen que ver con la acomodación de datos dentro del código, si
tienes conocimientos en el formato PE de windows, deberas saber que en linux el formato más manejado es el ELF y que
estos datos se acomodan en segmentos, por ejemplo sección de código(.text), sección de solo lectura(.rodata),
sección de datos sin inicializar(.bss), sección de datos inicializados(.data), etc...
Aca te dejo un buen artículo al respecto: [ Debe registrarse para ver este enlace ]

__cold:
No me expandire mucho en ésto solo dire que seran las funciónes menos usadas(y consumirá menos), ya que la inicializacion se dara 1 ves.

printk:
Así como en el espacio de usuario tenemos printf, en el kernel tenemos algo parecido llamado printk el cual cumple la misma
función, reportar algún tipo de información, existen varios tipos de alertas(ésto significa la prioridad,
con la que se dara el mensaje).
--------------------------------------------------------------
KERN_EMERG - condición de emergencia
KERN_ALERT - Evento que requiere una atención inmediata
KERN_CRIT - condición crítica
KERN_ERR - condición de error
KERN_WARNING - condición de alerta
KERN_NOTICE - condición normal
KERN_INFO - mensaje de informacion
KERN_DEBUG - el mensaje sera de tipo "depuración"
--------------------------------------------------------------
module_init(INIT_KERNEL) y moduel_exit(EXIT_KERNEL):
Con ésto simplemente incializamos las funciónes antes programadas, el kernel hara el resto al cargarse dinamicamente.
del mismo modo podemos usar __init y __exit
static int __init INIT_KERNEL(void)
{
	printk(KERN_INFO "Hola mundo desde el kernel!!!\n");
	return 0;
}

static void __exit EXIT_KERNEL(void)
{
	printk(KERN_ALERT "Driver desinstalado correctamente.");
}
Compilando:
Llego la hora de probar nuestro pequeño module, así como habiamos hecho el makefile es hora de compilarlo,
sigue mis pasos:
Abre la shell y ve a la carpeta antes creada(mis_kernel_module) y tan solo introduce el siguiente comando.
make

Podemos observar como se han creado unos cuantos archivos, de los cuales el que nos importa a nosotros es mi_module.ko

Cargando:
Ahora es tan fácil como cargarlo dinamicamente, para ésto haremos uso del comando:
insmod mi_module.ko

Hola mundo desde el kernel:
Ya lo hemos cargado, pero como podemos ver nuestro progreso, saber que printk hizo su correcto trabajo.
es muy facil...
Solo tenemos que hacer uso del comando:
dmesg

Hola kernel!!!

¿Pero que hace realmente hace éste comando?. dmesg o diagnostic message muestra los mensajes emitidos
por el kernel, es muy útil para depuración y controlar nuestro progreso.


Descargando:
Remover el kernel es fácil si todo se a hecho correctamente, es muy útil si estamos programando algo y queremos
instalar y desinstalar para ver la contiguidad de nuestro modulo y así avanzar en nuestro trabajo.
comando:
rmmod mi_module.ko (aun que también podemos hacer "rmmod mi_module")

Perfecto, pero ahora vamos a hacer la comprobación.
para ello hagamos de nuevo dmesg

desinstalado perfectamente ;)

Algo más...
Existen otros tipos de comandos que podemos introducir por ejemplo para ver los modules ya cargados, veamos.
lsmod

como ven me ha pasado la lista de todos los modulos incluyendo el nuestro(antes de la desinstalación obviamente).

algunas macros sirven para "personalizar" más nuestro modulo:
MODULE_LICENSE("GPL"); // el modulo será de licencia GPL
MODULE_AUTHOR("NvK"); // El autor de que ha escrito éste modulo
MODULE_DESCRIPTION("Mi primer kernel module"); // ¿Para que sirve éste modulo?
entonces el código te debería quedar así:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>

static int __section(.init.text) __cold notrace INIT_KERNEL(void)
{
	printk(KERN_INFO "Hola mundo desde el kernel!!!\n");
	
	return 0;
}

static void __section(.exit.data) EXIT_KERNEL(void)
{
	printk(KERN_ALERT "Driver desinstalado correctamente.");
}

module_init(INIT_KERNEL);
moduel_exit(EXIT_KERNEL);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("NvK");
MODULE_DESCRIPTION("Mi primer kernel module");
¿Como podemos comprobar éstos datos?
tan fácil como usar el comando modinfo

Existe más tipo de información adicional que podemos agregar, pero supongo que lo puedes buscar por tu cuenta...

Antes de despedirme voy a dejar un último ejemplo practico para que lo puedas poner a prueba.
La estructura task_struct:
Una de las estructuras más importantes para el kernel, contiene toda la información acerca de los procesos,
con ella se puede listarlos y conseguir información realmente importante que de manera convenciónal no tendríamos
acceso.
Ésta es definida en <sched.h> para estudiar más acerca de ella puedes hacer lo siguiente:
locate sched.h
y luego abrirla con el gedit(o el editor de texto favorito que uses).
en mi caso empieza en la línea 1220.

comm es un array con una longitud de 16 elementos(la macro definida de nombre TASK_COMM_LEN) así que no esperes que
contenga la ruta completa del proceso...
y pid simplemente devuelve el idenficador del proceso.
Código de ejemplo:
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
// necesaria para la estructura task_struct
#include <linux/sched.h>
// definimos el puntero a la estructura
static struct task_struct *kern_ts= NULL;

static int __section(.init.text) __cold notrace INIT_KERNEL(void)
{
	kern_ts = &init_task; // iniciamos la estructura para no producir fallos de segmentos;
	for (;(kern_ts = next_task(kern_ts)) != &init_task; ) // hasta que liste todos los procesos
	{
		printk(KERN_INFO "Proceso: %s PID: %d", kern_ts->comm, kern_ts->pid);
	}
  
  return 0x0;
}

static void __section(.exit.data) EXIT_KERNEL(void)
{
	printk(KERN_ALERT "Driver desinstalado correctamente.\n"); // al desinstalar el driver
}

module_init(INIT_KERNEL); // la funcion inicializadora
module_exit(EXIT_KERNEL); // la funcion de salida

// datos extras
MODULE_LICENSE("GPL");
MODULE_AUTHOR("NvK");
MODULE_DESCRIPTION("Mi primer kernel module");
Resultado:


Recuerda inicializar la estructura sino se producirían fallos de segmentos lo que simplemente haría el proceso del modulo inmatable,
o peor aun, de no encontrar la región de memoria alocada podría resultar en un caos total lo que llevaría a un oops, o
reinicio del sistema.

Y con ésto ultimo me despido, espero que el tutorial les haya gustado y podido entender, es cierto que no hable
de lo más importante del kernel, como los mecanismos de interrupciónes los accesos a la memoria, la paginación,
etc... pero recuerda que éste es un tutorial practico, tal vez para otra ocación hable más acerca del hardware y
su estrecha relación con el kernel.

Para finalizar te dejo éste articulo de como funcióna la estructa task_struct, entenderas más como funcionan los procesos
y sus caracteristicas, sus estados, habla algo acerca de los incializadores por medio del slab allocator(necesario para inicializar
la estructura) y como se hace cargo la MMU y mucho más... muy interesante y ojalá le tomes interes.
Artículo de interes: [ Debe registrarse para ver este enlace ]

Saludos NvK.
 #435846  por $DoC
 01 Feb 2014, 15:38
Que maravilla , muy buena info NvK, para quien esté estudiando esto le va a servir mucho. Abre un sin fin de posibilidades .

Saludos