Introdução a módulos no Linux Kernel

Este post é uma continuação do artigo anterior: Introdução ao Linux Kernel.

Em um sistema modular, módulo é um componente que tem uma funcionalidade e é desacoplado do sistema como um todo. Apesar de ser desacoplado, o módulos podem ser integrados ao sistema, interagirem um com os outros e criarem configurações específicas e distintas de acordo com quais módulos estão sendo usados.

Quem é programador entende de forma intuitiva o conceito de módulos, ou plugins, add-ons etc. E no Linux Kernel módulos são exatamente o que você espera de um módulo.

  • São desacoplados do Kernel em funcionalidade, porém são sempre específicos a um Kernel. Se a versão do Kernel muda, todos os módulos precisam ser recompilados para este Kernel.
  • Performam uma função específica;
  • Podem trabalhar em conjunto, criando assim dependência entre eles;
  • Podem ser inseridos/removidos no/do sistema em tempo de execução.

O Linux suporta duas formas de compilar seu módulo: embutido (built-in) ou módulo (module). A única diferença entre os dois para o usuário é que no modo built-in o módulo é compilado e embutido dentro da vmlinux (imagem do Kernel), enquanto no modo module o módulo será compilado como um Kernel Object (.ko) e poderá ser inserido em tempo de execução com a ferramenta insmod ou modprobe , que checa aliases (definidas no arquivo /etc/modprobe.conf e /etc/modprobe.d/*.conf ) do módulo e suas dependências (definidas no arquivo /lib/modules/`uname -r`/modules.dep  gerado pelo depmod ) e então executa o insmod  para cada módulo. Você pode confirmar esse processo com o comando modprobe -n -v module.

Existem outras importantes características do código no modo module:

  1. É necessário que a configuração CONFIG_MODVERSIONS do Kernel esteja habilitada (comumente habilitada) para que seja possível inserir módulos em tempo de execução;
  2. É possível compilar módulos como Kernel Objects fora da árvore de código fonte original do Kernel. Ou seja, módulos podem ter suas próprias arvores de código e ser compiladas para um Kernel específico apenas apontando para a árvore que geral esse Kernel. Isto é chamado de out-of-tree kernel module;
  3. Módulos como Kernel Objects precisam de um root filesystem para serem executados, já que o módulo é inserido no Kernel pela ferramenta modprobe à partir do caminho /lib/modules/`uname -r`/kernel/* . Isso é meio que óbvio, já que o Kernel por si só não serve de nada.

Mão na massa: escrevendo um módulo

O Linux tem uma filosofia de nunca quebrar o user-space, entretanto dentro do Kernel a API interna é quebrada com uma frequência até que grande. Porém a API para módulos é relativamente bem estável e simples, o que torna fácil de mantê-la.

Basicamente precisamos criar um arquivo de código fonte em C e modificar dois arquivos, Kconfig e Makefile, exigidos pelo KBuild.

OBS: Todo nosso código será escrito em inglês, já que é a língua franca no mundo do software.

Crie uma pasta dentro de drivers chamada hello (drivers/hello) e o arquivo de código fonte em drivers/hello/hello-world.c (lembrando que nossa raíz do Linux é em ~/devel/linux):

Este é o módulo clássico que todo mundo começando no Linux escreve. É bem simples de entender, certo? A função hello_init()  é chamada qual o módulo é carregado, e a função hello_exit()  é chamada quando o módulo é removido.

A função helo_init()  retorna um int, isso é uma pratica comum no Linux Kernel e em API’s POSIX no geral. O int representa um erro. O valor 0 significa que a função rodou sem dar nenhum erro, e um valor negativo representa um erro. Normalmente erros são definidos nos cabeçalhos errno-base.h e errno.h.

Isso tudo é simples, mas tem uns poréns.

  1. module_init()  e module_exit()  são macros helpers que facilitam a declaração das funções em diferentes modos de compilação, built-in ou module. Lembre-se que no Kernel, não existe um sistema operacional e uma biblioteca padrão para facilitar sua vida, tudo precisa ser feito à mão.
  2. A macro __init causa a função hello_init() ser descartada depois de der chamada pelo kernel no caso de ser built-in. Porém não tem efeito no caso de ser module. O mesmo serve para variáveis, que usam __initdata ou __initconst.
  3. A macro __exit  causa a omissão da função no Kernel quando ela é compilada como built-in. Fazendo com que a função hello_exit()  nunca seja chamada.

Essas macros são importantes porque causam o Kernel limpar memória quando termina o boot (já viram uma mensagem do tipo “Freeing unused kernel memory: 280K (80693000 – 806d9000)”?) Também simplificam o código possibilitando assim que o arquivo fonte seja compilado como built-in ou module de forma simples.

Perguntinha teste: Por que o __init  não faz sentido quando a função é compilado como module?

O MODULE_LICENSE("GPL v2")  é necessário se não o Kernel vai chiar quando for carregar o módulo via modprobe. O módulo será marcado como não GPL e não terá acesso a API internas exportadas como GPL. Além de ser o óbvio a se fazer. 🙂

Agora modifique os arquivos Kconfig e Makefile do diretório drivers (drivers/Kconfig e drivers/Makefile) adicionando nosso diretório de desenvolvimento.

drivers/Kconfig

Certifique-se de que essa modificação está foi inserida antes do endmenu .

drivers/Makefile

Crie um Kconfig no diretório de seu módulo (drivers/hello/Kconfig) com o seguinte:

Repare que o tipo da configuração é tristate (ou de três estados). Isso significa que ela pode ser selecionada para não ser configurada, para ser configurada como built-in ou ser configurada como module. Para saber por mais tipos suportados pelo KBuild, leia a documentação e procure por “Menu attributes”.

Também, repare na sintaxe. O KBuild exige esse tipo de sintaxe que usa tabulações e espaços para poder fazer o parse correto das configurações.

E por último, apenas crie um arquivo Makefile dentro do diretório do seu módulo (drivers/hello/Makefile) com o seguinte:

Bom, acredito que essas duas últimas modificações são auto-explanatórias, não?

Disponibilizei o 0001-blog-hello-world.c.patch para quem quiser aplicar na sua árvore do Linux. O commit foi em cima da tag v4.4-rc4. Use o comando git am  para aplicar.

Como executar um módulo

Agora na raíz do seu Linux, execute o comando (considerando que você já selecionou um defconfig):

Habilite o nosso Hello World módulo em:

Repare que o KBuild irá habilitar 3 possíveis configurações: excluded, built-in ou module. Bem simples.

Executando um módulo compilado como built-in

Habilite nosso módulo como built-in no menuconfig e recompile o Kernel.

Repare que nosso arquivo foi compilado junto com a imagem do Linux:

Agora rode nosso Kernel fresquinho no QEMU novamente com:

E repare como nosso módulo é carregado procurando pela linha “################# Hello, World!“. Bem tranqüilo.

Executando um módulo compilado como module

Habilite nosso módulo como module no menuconfig e depois recompile o Kernel e compile os módulos:

Pergunta para o atento: Por que foi preciso recompilar o Kernel?

Repare que nosso módulo agora foi compilado como module.

Após ser compilado, o binário passa por um passo chamado MODPOST  no qual é feita a verificação dos símbolos externos do Kernel, presentes no arquivo Module.symvers dentro do diretório de build. Além disso, se necessário, o arquivo Module.symvers é modificado por ser adicionado as funções exportadas pelo módulo compilado.

Construindo Uma Pequena Distribuição

É importante lembrar que o foco dos artigos é sobre o Linux Kernel. Mas como o Kernel em si não faz nada de interessante (o objetivo de um Kernel é servir o user-space), precisamos rodar uma distribuição (user-space) para tirar proveito do Kernel, por isso esta etapa é necessária e será usada nos futuros artigos.

Chegou a hora mais temida, pra ser sincero. 😯 Vamos precisar de um rootfs e para isso vamos construir uma distribuição bem pequena usando o BusyBox apenas! Também vamos usar o suporte a NFS no Linux para podermos montar o rootfs via NFS e assim ter uma distribuição para fácil desenvolvimento onde podemos atualizar nosso rootfs no host (seu computador) e automaticamente refletir o mesmo rootfs no target (máquina virtual no QEMU).

Para quem não sabe o que é BusyBox eu recomendo ler o site oficial. Mas por cima, é um único utilitário que implementa boa parte dos utilitários necessários para rodar um Sistema Operacional Unix em user-space.

Primeiro passo é baixar a última versão do código fonte do busybox no site oficial, compilar e instalar. Para isso vamos simplificar os comandos e se houver qualquer dúvida, poste nos comentários.

Selecione o modo de linkagem como estática. Isso garante que as dependências (i.e., glibc) do busybox não precisem estar instaladas no sistema target (alvo).

Agora selecione a seguinte opção para instalarmos o BusyBox no diretório /rootfs-bb do seu sistema. Na realidade você pode instalar onde quiser, só que eu optei por usar esse diretório.

Saia com ESQ  e não se esqueça de salvar.

Agora compile e instale (no meu caso precisa de sudo, já que estou instalando no /rootfs-bb).

Repare que boa parte do rootfs está pronta no /rootfs-bb, porém ainda faltam alguns toques finais. Eu não vou explicar tudo, fica a critério do leitor se informar ou perguntar.

É isso aí, nossa distribuição Linux minimalista está prontinha! 😀

O último detalhe é configurar seu servidor NFS local. Isso eu não vou explicar pois existem milhões de tutoriais por aí na internet em como o fazer. Mas é necessário que você configure seu /etc/exportfs da seguinte forma:

Agora é o momento de rufarem os tambores pois vamos executar nossa máquina virtual com um monte de parâmetros que antes não tínhamos usado. E isso significa que algo sempre dá errado. 👿  Mas não se desespere, tente se certificar de todos passos e qualquer coisa estamos aí nos comentários!

ANTES DE EXECUTAR: Leia os comentários abaixo!

Eu recomendo vocês executarem o QEMU num outro terminal já que vamos continuar usando o terminal na árvore do Linux.

Bom, vamos considerar alguns parâmetros antes de carregar o módulo em nosso Kernel. O -m 128M  é a quantidade de memória RAM alocada para a máquina virtual. O -append  são parâmetros passados para o Linux, e para isso leia a documentação. O importante lá é entender que vamos usar o rootfs to tipo NFS pelo parâmetro root=/dev/nfs e configuramos o NFS com o argumento nfsroot=10.0.2.2:/rootfs-bb  onde o IP é o IP da máquina host que o QEMU roteia e o caminho na máquina host do rootfs.

Também é importante notar que estamos avisando o Linux para carregar o programa /sbin/init como sendo o programa init (ou PID 1). Este programa é responsável por carregar todos os outros programas no sistemas. Na verdade, todos os outros programas em user-space são filhos do init (criados através da system call fork()). Em sistemas atuais o programa init é muitas vezes o SystemD, porém em muitos sistemas embarcados, e o nosso, ainda usa-se uma implementação do SysV Init que é bem mais antiga (1983) porém simples, poderosa e bem elegante.

Agora sim, pode executar o QEMU! 😎

Se você viu esta mensagem de boas vindas, então fique tranquilo que o mais chato e mais difícil já passou! 😉

Agora sim vamos instalar nosso módulo no rootfs e carregá-lo:

No host (~/devel/linux)

Como citado no início do artigo, repare que a ferramenta do Linux depmod  foi chamada para gerar o arquivo de dependências dos módulos no target.

O arquivo gerado é o modules.dep no caminho $INSTALL_MOD_PATH/lib/modules/<kernel-version>/modules.dep. O depmod consegue linkar dependências de módulos lendo as funções que o módulo usa e e exporta (para dependências de outros módulos) e comparando com o arquivo System.map (que é uma tabela de símbolos do Kernel) que fica no diretório output do Linux (~/devel/linux/build/System.map). Por sua vez, o modprobe  pode carregar as dependências corretas do módulo em tempo de execução. Fantástico! 😛

Agora no target (QEMU)

Viram as mensagens?

Significa que seu módulo foi inserido no e removido do Kernel com sucesso! Parabéns!

Repare que a mensagem impressa foi direta do Kernel, já que contém o timestamp de quanto tempo o Kernel está rodando.

Eu recomendo você brincar um pouco com seu módulo, tentando mudar o valor de retorno da função hello_init()  entre outras coisas.

Espero que este artigo não tenha sido longo demais ou muito cansativo. Eu tentei não explicar assuntos não muito relevantes, como NFS e outras coisas. Se quiser saber mais ou estiver em dúvidas pesquise a respeito ou deixe um comentário. Mas eu quero deixar claro aqui que o objetivo destes artigos é falar sobre o Linux Kernel e não distribuições Linux. Por favor, caso tenha críticas e sugestões, fique à vontade em seus comentários.


Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.3 or any later version published by the Free Software Foundation; with no Invariant Sections, no Front-Cover Texts and no Back-Cover Texts.

  • Pingback: Introdução ao Linux Kernel | Hacking Linux Thoughts()

  • Muito bom!

  • Pingback: Introdução a módulos no Linux Kernel - Peguei do()

  • Ótimo artigo Felipe!!

  • Igor Cavalcante

    Parabéns pela iniciativa colega, aguardo os próximos posts 🙂

  • Oswaldo Fratini Filho

    Mais um post bacana!

    Assim que chegar em casa vou fazer o checkout da versão do kernel apontada no post, baixar o Busybox e tentar montar minha pequena distro.

    • Felipe Tonello

      Legal! Poste seus resultados. 🙂

  • Oswaldo Fratini Filho

    Acabei de concluir o passo a passo e executar a minha distro!

    Aqueles que tentarem o mesmo, atentem-se, na execução do QEMU, para as referências do diretório onde foi salva a imagem compilada “bzimage”. Substitua o “blog/arch/x86_64/boot/bzImage” por “build/arch/x86_64/boot/bzImage”.

    Dicas de utlização do QEMU:
    1. Utilize os comandos “SHIFT + Page Up” e “SHIFT + Page Down” para navegar pelas mensagens do kernel depois da inicialização.

    2. Se está encontrando alguma dificuldade em ver as mensagens depois de um “kernel panic”, sugiro abrir um novo terminal e executa o QEMU assim: “qemu-system-x86_64 -nographic -serial mon:stdio -append ‘console=ttyS0’ -kernel build/arch/x86_64/boot/bzImage”.

    Espero que tenha colaborado.

    🙂

    • Felipe Tonello

      Ótimo Oswaldo! Que bom que deu tudo certo aí.
      Arrumei o typo no caminho do Kernel, valeu.
      Ótima dica a do terminal.

  • Pingback: QEMU para desenvolvimento do Linux Kernel | Hacking Linux Thoughts()