2 pontos por GN⁺ 2024-07-28 | 1 comentários | Compartilhar no WhatsApp
  • O Linux Kernel Module Programming Guide é um guia gratuito para criar módulos carregáveis do kernel no Linux v5.10 ou superior, abordando em um único fluxo desde o ambiente de desenvolvimento até build, carregamento, depuração e as principais interfaces do kernel
  • Os exemplos iniciais usam hello-*.c para ensinar module_init(), module_exit(), kbuild, insmod, rmmod e dmesg, e reduzem o risco de danificar o sistema host com devtools baseadas em QEMU
  • Como módulos do kernel executam no espaço de endereçamento do kernel, ponteiros incorretos, ordem de unload, concorrência e erros ao copiar memória de usuário podem levar a corrupção da memória do kernel ou instabilidade do sistema
  • O guia avança para dispositivos de caractere, /proc, seq_file, threaded IRQ, input, PCI, USB, bloco, rede, Device Model, Device Tree e static key, tratando repetidamente da ordem de registro/liberação e do gerenciamento de lifetime
  • Como APIs internas do kernel mudam conforme a versão, ele verifica condições como LINUX_VERSION_CODE, KERNEL_VERSION, CONFIG_MODVERSIONS, assinatura para SecureBoot e version magic, e os exemplos também incluem compilação condicional

Estrutura do guia e fluxo básico

  • Este guia é um material de estudo sobre módulos do kernel que oferece um repositório no GitHub e um documento em PDF, podendo ser copiado, modificado e distribuído sob os termos da Open Software License 3.0
  • Atualmente, o guia adota Linux v5.10 como requisito mínimo de suporte e busca manter a compatibilidade dos exemplos e instruções em toda a família de kernels de suporte de longo prazo
  • O estudante precisa ter experiência com a linguagem C e com a escrita de programas para processos comuns; módulos do kernel são carregados e descarregados dinamicamente para estender funcionalidades do kernel sem reinicialização
  • O fluxo básico de desenvolvimento passa por instalar os headers do kernel, compilar o .ko com make, verificar com modinfo, carregar com insmod, consultar logs com dmesg ou journalctl -k e descarregar com rmmod
  • devtools/ compila o código-fonte do kernel e um sistema de arquivos raiz com BusyBox, faz boot no QEMU, compartilha examples/ via 9p virtfs e permite testar os módulos no guest
  • Para inicialização e limpeza de módulos, prefere-se usar module_init() e module_exit(); a forma antiga com init_module() e cleanup_module() pode causar falhas de build em algumas condições a partir do kernel 6.15 com x86 IBT habilitado
  • Módulos do kernel não usam printf() nem libc, apenas símbolos exportados pelo kernel, e a saída vai para o buffer circular de logs do kernel, não para o terminal
  • A movimentação de dados entre o espaço de usuário e o espaço do kernel exige funções específicas como put_user, get_user, copy_to_user e copy_from_user
  • O exemplo de dispositivo de caractere mostra register_chrdev, file_operations, major number dinâmico, criação de nó em /dev, open exclusivo, read baseado em put_user e tratamento de write não suportado
  • Os exemplos de /proc cobrem proc_create, proc_ops, callbacks de read/write e a API seq_file, refletindo a mudança introduzida a partir do Linux v5.6, quando proc_ops substituiu file_operations para handlers de /proc
  • Threaded IRQ separa top-half e bottom-half com request_threaded_irq(); o top-half executa apenas o mínimo necessário em interrupt context e acorda o bottom-half baseado em thread com IRQ_WAKE_THREAD
  • Os capítulos seguintes avançam para áreas de drivers reais como input, PCI, USB, bloco, rede, Device Model e Device Tree, seguindo com foco nos métodos de registro de cada subsystem e na escolha da ABI de userspace
  • A parte de otimização e segurança trata de likely/unlikely, static key, pilha pequena do kernel, proibição de uso da FPU, vazamento de padding não inicializado e cuidado ao usar APIs internas com double underscore

Restrições encontradas primeiro no build e no carregamento

  • Um módulo compilado para um kernel pode não carregar em outro; se version magic e CONFIG_MODVERSIONS não coincidirem, podem ocorrer Invalid module format ou incompatibilidade de versão de símbolos
  • A maioria dos kernels de distribuições Linux comuns pode ter modversioning habilitado; se os exemplos não funcionarem de imediato, deve-se considerar um kernel com modversioning desativado ou um ambiente QEMU
  • Em sistemas com SecureBoot ativado, o carregamento de módulos não assinados pode ser restrito; se aparecer Lockdown: insmod: unsigned module loading is restricted, é necessário desativar o SecureBoot ou seguir o procedimento de assinatura de módulos

Ambiente de prática baseado em QEMU

  • devtools/setup.sh baixa e compila o tarball do kernel e o BusyBox, e empacota o initramfs
  • devtools/build-modules.sh compila os módulos destinados ao kernel do QEMU, devtools/boot.sh fornece um shell no guest, e devtools/test-modules.sh executa testes automatizados de insmod e rmmod para cada módulo
  • A depuração com GDB é feita compilando vmlinux com LKMPG_NO_PREBUILT=1 devtools/setup.sh, depois usando devtools/boot.sh --gdb e conectando-se via GDB remoto

Regras para escrever código de kernel

  • Como registros e alocações podem falhar na função init, os recursos obtidos devem ser liberados na ordem inversa nos caminhos de erro baseados em goto
  • Ao registrar uma estrutura de callbacks no kernel, o espaço de usuário pode chamar callbacks até mesmo antes do retorno de init; por isso, é importante a regra registrar por último, desregistrar primeiro, fazendo o registro apenas após terminar a inicialização interna e a liberação antes de tudo
  • Process context, softirq/tasklet context e hardirq context diferem quanto à possibilidade de sleep, acesso à memória de usuário, uso de GFP_KERNEL e uso de mutex; entender mal essa distinção leva a bugs comuns no kernel

Cuidados por dispositivo e subsystem

  • Dispositivos de caractere identificam o driver pelo major number e distinguem vários dispositivos internos pelo minor number; na abordagem moderna, a interface cdev é recomendada em vez de register_chrdev()
  • Drivers PCI não assumem endereços fixos e mapeiam recursos BAR enumerados pelo PCI core; em código para Linux 5.10 ou posterior, pcim_enable_device() e APIs de recursos gerenciados pelo dispositivo ajudam a reduzir bugs de teardown
  • Drivers USB devem tratar hotplug e disconnect como eventos normais, e seu design deve partir da premissa de que URB completion pode concorrer com disconnect, timeout, suspend e shutdown iniciado por userspace
  • Drivers de bloco operam em torno de blk-mq, request, gendisk, limites de fila e semântica de flush/FUA, participando de um modelo de conclusão assíncrona de requests, não de simples callbacks de read/write
  • Drivers de rede combinam struct net_device, net_device_ops, sk_buff, NAPI, flags de recursos de offload e reporte de link-state; declarações incorretas de offload podem corromper tráfego

Como lidar com mudanças de versão do kernel

  • Os exemplos tratam por compilação condicional mudanças como a assinatura de class_create() no Linux 6.4, proc_ops no Linux v5.6, a alteração do tipo de retorno de remove() no Linux 6.11 e mudanças em helpers de blk-mq entre Linux 5.15 e 6.9
  • Interfaces internas do kernel mudam com mais frequência do que chamadas de sistema, então módulos que dão suporte a vários kernels dificilmente conseguem evitar comparações com LINUX_VERSION_CODE e KERNEL_VERSION

Checkpoints de segurança

  • A pilha do kernel é muito menor que a pilha do espaço de usuário e, em muitos sistemas, pode ser de 8 KiB ou 16 KiB; portanto, arrays grandes devem usar kmalloc() ou kzalloc()
  • Ao enviar dados ao espaço de usuário com copy_to_user(), todos os bytes, incluindo padding, precisam estar inicializados; caso contrário, pode ocorrer vazamento de informações da memória do kernel
  • APIs que começam com double underscore, como __kmalloc() e __list_add(), podem assumir pré-condições internas; se a documentação não exigir seu uso, deve-se priorizar o wrapper público

Escopo omitido

  • Como está indicado que alguns chunks do original foram omitidos por limites de tamanho e custo no processo de entrada, este resumo não cobre exaustivamente todos os capítulos, exemplos e caminhos de código do guia

1 comentários

 
GN⁺ 2024-07-28
Opiniões no Hacker News
  • QEMU é uma boa forma de experimentar hacking no kernel
    Seria ótimo se alguém atualizasse o LDD (Linux Device Drivers) e os livros sobre o kernel Linux; como é difícil ganhar dinheiro com livros técnicos desse tipo, talvez fizesse sentido a Linux Foundation patrocinar

    • Há alguns posts sobre escrever drivers e criar/anexar dispositivos customizados com 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 torna realmente fácil subir no QEMU um kernel em desenvolvimento
    • Para depuração de kernel em estágio inicial, quando nem há console, QEMU é muito usado
      Ainda nesta semana, em v6.8, reproduziram com QEMU + GDB um problema em que o kernel parava imediatamente e em silêncio se o parâmetro de linha de comando do kernel arm64 tivesse 146 caracteres ou mais; em um host Debian 12 Bookworm amd64, emularam o build de um kernel arm64 e foram acompanhando o código problemático linha por linha para encontrar a causa
      O fluxo consiste em, em um ambiente com dependências de build e ferramentas de cross-compilação prontas, compilar a imagem do kernel arm64 e os scripts para GDB; instalar no host gdb, se necessário gdb-multiarch, e qemu-system-arm; depois iniciar qemu-system-aarch64 pausado com -S -gdb tcp::1234 e, em outro terminal, conectar com gdb-multiarch ./vmlinux
      Em seguida, no GDB, ao executar target remote :1234, break __parse_cmdline e continue, é possível usar recursos comuns do GDB, como inspecionar memória, variáveis e pilha, além de executar passo a passo
      Para depuração do kernel com GDB e scripts lx-*, consulte https://www.kernel.org/doc/html/latest/dev-tools/gdb-kernel-...
      Para que o GDB use os scripts Python lx-*, em geral também é preciso permitir o caminho, por exemplo com echo "add-auto-load-safe-path ${SRC_DIR}/scripts/gdb/vmlinux-gdb.py" > ~/.gdbinit
    • A suíte de testes do WireGuard que hoje está no kernel é um bom exemplo para desenvolver módulos de kernel com QEMU e até rodar testes automatizados
    • Greg KH disse de forma bastante clara que não haverá uma 4ª edição do LDD
  • Threads relacionadas no HN: https://news.ycombinator.com/item?id=35782630, https://news.ycombinator.com/item?id=28283030

  • The Linux Memory Manager também vale como referência: https://linuxmemory.org/chapters
    Segundo a última atualização enviada pelo autor no começo de julho, ele concluiu o primeiro rascunho e agora entrou na fase de edição com a editora

    • O sumário parece bom, mas é uma pena não haver pré-venda para apoiar a produção
  • Alguns exemplos parecem difíceis de executar por conta própria
    Por exemplo, “Detecting button presses” parte do pressuposto de que é possível compilar um módulo para RPi, mas isso por si só pode exigir tarefas como cross-compilação, então talvez não seja simples

    • Mesmo sendo um pouco trabalhoso, acho que bastaria rodar o compilador no Raspberry Pi, não?
  • É excelente: detalhado, focado na prática e um tutorial que já leva a compilar módulos de kernel na hora

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

  • Fico curioso sobre onde seria bom aprender sobre programação do kernel Linux em geral, como sistemas de arquivos ou gerenciamento de memória
    Antigamente havia “Linux Kernel Development”, de Robert Love, mas parece que ele não é mais atualizado

  • Li isso pela primeira vez há cerca de 22 anos :)