2 puntos por GN⁺ 2024-07-28 | 1 comentarios | Compartir por WhatsApp
  • Linux Kernel Module Programming Guide es una guía gratuita para crear módulos de kernel cargables en Linux v5.10 o superior, y recorre de forma integral desde el entorno de desarrollo hasta la compilación, carga, depuración y las principales interfaces del kernel
  • Los ejemplos iniciales usan hello-*.c para aprender module_init(), module_exit(), kbuild, insmod, rmmod y dmesg, y con devtools basadas en QEMU reducen el riesgo de dañar el sistema host
  • Como los módulos del kernel se ejecutan en el espacio de direcciones del kernel, punteros incorrectos, orden de descarga erróneo, concurrencia y fallos al copiar memoria de usuario pueden provocar corrupción de memoria del kernel o inestabilidad del sistema
  • Se expande hacia dispositivos de caracteres, /proc, seq_file, IRQ con hilos, input, PCI, USB, bloque, red, Device Model, Device Tree y static key, tratando repetidamente el orden de registro y liberación y la gestión del lifetime
  • Como las API internas del kernel cambian según la versión, se verifican condiciones como LINUX_VERSION_CODE, KERNEL_VERSION, CONFIG_MODVERSIONS, firmas de SecureBoot y version magic, y los ejemplos también incluyen compilación condicional

Estructura de la guía y flujo básico

  • Esta guía es material de aprendizaje sobre módulos del kernel que ofrece un repositorio en GitHub y un documento PDF, y puede copiarse, modificarse y distribuirse bajo los términos de Open Software License 3.0
  • Actualmente la guía toma Linux v5.10 como requisito mínimo de soporte y busca mantener la compatibilidad de ejemplos e instrucciones a lo largo de los kernels con soporte de largo plazo
  • El lector necesita experiencia con lenguaje C y con la escritura de programas para procesos normales, y los módulos del kernel se cargan y descargan dinámicamente para ampliar funciones del kernel sin reiniciar
  • El flujo básico de desarrollo sigue con instalación de headers del kernel, compilación del .ko con make, verificación con modinfo, carga con insmod, revisión de logs con dmesg o journalctl -k, y descarga con rmmod
  • devtools/ compila el código fuente del kernel y un sistema de archivos raíz de BusyBox, arranca en QEMU, comparte examples/ mediante 9p virtfs y luego permite probar módulos dentro del guest
  • Para la inicialización y limpieza del módulo se prefiere usar module_init() y module_exit(), y el método anterior con init_module() y cleanup_module() puede causar fallos de compilación en ciertas condiciones a partir del kernel 6.15 con x86 IBT habilitado
  • Los módulos del kernel no usan printf() ni libc y solo pueden usar símbolos exportados por el kernel; la salida no va a la terminal sino al ring buffer de logs del kernel
  • Para mover datos entre espacio de usuario y espacio de kernel se necesitan funciones dedicadas como put_user, get_user, copy_to_user y copy_from_user
  • El ejemplo de dispositivo de caracteres muestra register_chrdev, file_operations, major number dinámico, creación de nodos en /dev, apertura exclusiva, lectura basada en put_user y manejo de escritura no soportada
  • El ejemplo de /proc cubre proc_create, proc_ops, callbacks de lectura y escritura, y la API seq_file, reflejando el cambio introducido desde Linux v5.6 donde proc_ops reemplaza a file_operations en los handlers de /proc
  • Las IRQ con hilos usan request_threaded_irq() para dividir top-half y bottom-half; el top-half realiza solo trabajo mínimo en contexto de interrupción y despierta el bottom-half basado en hilo con IRQ_WAKE_THREAD
  • Los capítulos posteriores se expanden a áreas reales de drivers como input, PCI, USB, bloque, red, Device Model y Device Tree, centrados en el método de registro de cada subsistema y la elección de ABI de userspace
  • La parte de optimización y seguridad trata likely y unlikely, static key, stacks pequeños del kernel, prohibición de usar FPU, fuga de padding no inicializado y precauciones al usar API internas con doble guion bajo

Restricciones con las que uno tropieza primero al compilar y cargar

  • Un módulo compilado para un kernel puede no cargarse en otro, y si version magic o CONFIG_MODVERSIONS no coinciden aparecerán Invalid module format o errores de versión de símbolos
  • En la mayoría de los kernels de distribuciones Linux comunes puede estar activado modversioning, así que si los ejemplos no funcionan de inmediato conviene considerar un kernel con modversioning desactivado o un entorno QEMU
  • En sistemas con SecureBoot activado, la carga de módulos sin firma puede estar restringida, y si aparece Lockdown: insmod: unsigned module loading is restricted, será necesario desactivar SecureBoot o seguir el proceso de firmado del módulo

Entorno de práctica basado en QEMU

  • devtools/setup.sh descarga y compila el tarball del kernel y BusyBox, y empaqueta el initramfs
  • devtools/build-modules.sh compila módulos para el kernel de QEMU, devtools/boot.sh proporciona un shell del guest, y devtools/test-modules.sh ejecuta pruebas automáticas de insmod y rmmod por módulo
  • La depuración con GDB se realiza compilando vmlinux con LKMPG_NO_PREBUILT=1 devtools/setup.sh, y luego usando devtools/boot.sh --gdb con una conexión remota de GDB

Reglas para escribir código de kernel

  • En la función init pueden fallar el registro y la asignación, por lo que los recursos adquiridos deben liberarse en orden inverso dentro de rutas de error basadas en goto
  • Si se registra una estructura de callbacks en el kernel, userspace puede invocarlos incluso antes de que init retorne, por lo que es importante la regla de registrar al final y desregistrar primero: terminar la inicialización interna antes de registrar y hacer la liberación en el orden inverso
  • El process context, softirq/tasklet context y hardirq context difieren en si pueden dormir, acceder a memoria de usuario, usar GFP_KERNEL o mutex; entender mal esa distinción lleva a bugs comunes del kernel

Puntos de atención por dispositivo y subsistema

  • Los dispositivos de caracteres identifican al driver con el major number y distinguen varios dispositivos internos con el minor number; en el enfoque moderno se recomienda la interfaz cdev por encima de register_chrdev()
  • Los drivers PCI no asumen direcciones fijas, sino que mapean los recursos BAR enumerados por el core PCI; en código para Linux 5.10 o posterior, pcim_enable_device() y la API de recursos administrados por el dispositivo ayudan a reducir bugs de teardown
  • Los drivers USB deben tratar hotplug y disconnect como eventos normales, y diseñarse asumiendo que la finalización de URB puede competir con disconnect, timeout, suspend y apagado desde userspace
  • Los drivers de bloque funcionan alrededor de blk-mq, request, gendisk, límites de cola y semántica de flush/FUA, y participan en un modelo de finalización asíncrona de requests en lugar de simples callbacks de lectura/escritura
  • Los drivers de red se integran con struct net_device, net_device_ops, sk_buff, NAPI, banderas de offload y reporte de estado del enlace, y declarar mal una capacidad de offload puede corromper el tráfico

Adaptación a cambios de versión del kernel

  • Los ejemplos manejan con compilación condicional cambios como la firma de class_create() en Linux 6.4, proc_ops en Linux v5.6, el cambio del tipo de retorno de remove() en Linux 6.11 y cambios de helpers de blk-mq entre Linux 5.15 y 6.9
  • Como las interfaces internas del kernel cambian con más frecuencia que las llamadas al sistema, en módulos que soportan varios kernels es difícil evitar comparaciones entre LINUX_VERSION_CODE y KERNEL_VERSION

Puntos de control de seguridad

  • El stack del kernel es mucho más pequeño que el stack de userspace y en muchos sistemas puede ser de 8 KiB o 16 KiB, por lo que los arreglos grandes deben usar kmalloc() o kzalloc()
  • Al enviar datos a userspace con copy_to_user(), todos los bytes, incluido el padding, deben estar inicializados; de lo contrario puede producirse una filtración de información de memoria del kernel
  • API que empiezan con doble guion bajo como __kmalloc() o __list_add() pueden asumir precondiciones internas, por lo que salvo que la documentación lo exija, conviene usar primero el wrapper público

Alcance omitido

  • Se indica que algunos fragmentos originales fueron omitidos durante el procesamiento de entrada por límites de longitud o costo, por lo que este resumen no cubre exhaustivamente todos los capítulos, ejemplos ni rutas de código de la guía completa

1 comentarios

 
GN⁺ 2024-07-28
Opiniones en Hacker News
  • QEMU es una buena forma de experimentar con hacking del kernel.
    Sería bueno que alguien actualizara LDD (Linux Device Drivers) y los libros sobre el kernel de Linux; como este tipo de libros técnicos difícilmente genera ganancias, quizá la Linux Foundation podría patrocinarlos.

    • Hay algunos artículos que describen el proceso de escribir un driver y crear y conectar un dispositivo personalizado con QEMU: [0] https://blog.davidv.dev/posts/learning-pcie/, [1] https://blog.davidv.dev/posts/pcie-driver-dma/
    • Usar virtme-ng https://github.com/arighi/virtme-ng hace que sea realmente fácil levantar en QEMU un kernel en desarrollo.
    • Para depuración de kernels en etapas tempranas, cuando ni siquiera hay consola, se usa mucho QEMU.
      Incluso esta semana, con QEMU + GDB reproduje en v6.8 un problema en el que el kernel se detenía de inmediato y en silencio si el parámetro de línea de comandos del kernel arm64 tenía 146 caracteres o más; emulé la compilación de un kernel arm64 en un host amd64 con Debian 12 Bookworm y fui siguiendo línea por línea el código problemático hasta encontrar la causa.
      El flujo consiste en compilar una imagen de kernel arm64 y scripts para GDB en un entorno con las dependencias de compilación y herramientas de cross-compilation listas; instalar en el host gdb, si hace falta gdb-multiarch, y qemu-system-arm; luego iniciar qemu-system-aarch64 detenido con -S -gdb tcp::1234 y conectarse desde otra terminal con gdb-multiarch ./vmlinux.
      Después, en GDB, al ejecutar target remote :1234, break __parse_cmdline y continue, se pueden usar funciones normales de GDB como revisar memoria, variables y stack, y ejecutar paso a paso.
      Para depuración del kernel con GDB y los scripts lx-*, consultar https://www.kernel.org/doc/html/latest/dev-tools/gdb-kernel-...
      Para que GDB use los scripts Python lx-*, normalmente también hace falta permitir la ruta, por ejemplo con echo "add-auto-load-safe-path ${SRC_DIR}/scripts/gdb/vmlinux-gdb.py" > ~/.gdbinit.
    • La suite de pruebas de WireGuard que ahora está dentro del kernel es un buen ejemplo para desarrollar módulos del kernel con QEMU e incluso probarlos automáticamente.
    • Greg KH dijo con bastante claridad que no habrá una 4.ª edición de LDD.
  • Hilos relacionados en HN: https://news.ycombinator.com/item?id=35782630, https://news.ycombinator.com/item?id=28283030

  • The Linux Memory Manager también vale la pena como referencia: https://linuxmemory.org/chapters
    Según la última actualización que envió el autor a principios de julio, terminó el primer borrador y ahora entró en la etapa de edición junto con la editorial.

    • El índice se ve bien, pero es una lástima que no haya precompra para apoyar la producción.
  • Algunos ejemplos parecen difíciles de ejecutar directamente.
    Por ejemplo, “Detecting button presses” asume que se puede compilar un módulo para RPi, pero eso en sí puede requerir tareas como cross-compilation, así que quizá no sea tan simple.

    • Aunque sea un poco engorroso, ¿no bastaría con correr el compilador en la Raspberry Pi?
  • Es excelente: detallado, práctico y orientado a un tutorial donde de inmediato se compila un módulo del kernel.

  • Material para ver junto con esto: https://0xax.gitbooks.io/linux-insides/content/index.html

  • Me pregunto dónde conviene mirar para aprender sobre programación del kernel de Linux en general, como sistemas de archivos o administración de memoria.
    Hace mucho estaba “Linux Kernel Development” de Robert Love, pero parece que ya no se actualiza.

  • Leí esto por primera vez hace unos 22 años :)