AVISO

AVISO: ESTE É MEU ANTIGO BLOG, QUE NÃO É MAIS ESCRITO DESDE 2011. O CONTEÚDO AQUI EXPRESSO PODE NÃO REPRESENTAR MEUS PENSAMENTOS E OPINIÕES DE IDADE ADULTA. PARA CONTEÚDOS NOVOS E RELEVANTES ACESSE BLOG.BRUNO.TODAY




terça-feira, 28 de outubro de 2008

Desenvolvimento de Módulos Para o Kernel do Linux II

Palavras-chave: Linux, C, Kernel


Empregos Manager Online


Após o "incrível sucesso" do primeiro artigo, decidi transformá-lo em uma "série" de artigos, e este será o nosso segundo. Primeiramente gostaria de agradecer ao pessoal que me incentivou. Eu imaginava ter uns 100 acessos, mas o primeiro artigo teve aproximadamente 900 visualizações, fora o pessoal que comentou me incentivando.

Só prestando um esclarecimento, eu nem imaginava que este tema seria de tanto agrado do publico. Imaginei que isso fosse alvo de interesse apenas dos meus amigos mais nerds :-) Mas vou aproveitar que me interesso bastante pelo tema e vou procurar escrever com freqüência(ou nem tanta assim) sobre o tema, tudo dependerá de como andam as coisas para mim(fim da semana passada e esta agora estão corridinhas!!).


Agora chega de lero-lero e vamos ao que interessa: eu deixei muita coisa "em aberto" no primeiro artigo. Uma pessoa deixou(anonimamente) o comentário dizendo que faltava comentar o código. Bom, quando eu enviei aquele post eu estava com um pouco de pressa(como agora hehehe), mas agora vamos explicá-lo melhor!!

Primeiramente, um pouco do "geral" sobre as bibliotecas do kernel. Obviamente, um sistema operacional requer "rotinas diversas" para uso interno do próprio kernel. São rotinas básicas que usamos em diversos diferentes locais. Um destes exemplos é o printk(), que é um "analogo" ao printf() da libc.

Antes de mais nada, alguns podem ter a dúvida: e porque não utilizar as funções da libc mesmo, que são padronizadas com a linguagem, mais completas e de utilização mais fácil?? Bom, a resposta é simples: o kernel não faz a mínima idéia de que exista uma libc. Quando você está no kernel space só existe o kernel. Bibliotecas, para o ver do kernel, são meros arquivos em formato ELF, referenciados por outros meros arquivos também em formato ELF. A única coisa que o kernel sabe é executar system calls[1] nestes arquivos.

Estas bibliotecas são documentadas naquela API do kernel, cujo link coloquei no outro artigo. Retomando o foco do assunto, no post anterior eu não havia escrito muitos detalhes do código. Revirando meus links, encontrei o link do 'The Linux Kernel Module Programming Guide', que eu não me recordava, mas havia sido minha inspiração na escrita daquele código de exemplo. Todos os detalhes que menciono aqui são semelhantes aos do guia, que é uma das fontes deste artigo, juntamente com os fontes do kernel.

Primeiramente, na estrutura do código, temos nossos dois arquivos: errcon.h e errcon.c. Prezumo que todos que estejam lendo este arquivo conheçam razoavelmente os padrões que comumente são adotados para a divisão de códigos entre arquivos de cabeçalho(.h, com declarações) e arquivos de implementação(os .c ou .cpp no caso de C++). Caso não conheçam, recomendo "googlar" sobre o assunto

Em nosso header, primeiro incluimos alguns headers com declarações de símbolos que utilizaremos. Logo após, definimos algumas "macros úteis". São apenas para concentrar o valor de algumas coisas ali ao começo. Nesta época eu não era tão "avesso" ao uso de macros.

Depois temos nossaas variáveis globais e funções. Quero que atentem bastante às funções 'ec_dev*' e à estrutura 'opers', do tipo file_operations. Elas são as responsáveis pelo funcionamento da parte do arquivo de dispositivo(a entrada no /dev) que nosso módulo nos disponibiliza.

Como vocês podem notar, registramos em uma estrutura file_operations todas as operações relacionadas a arquivo que nosso módulo realiza. Mas neste módulo não utilizamos todas as possibilidades. Mesmo assim, quero que vocês conheçam a declaração da estrutura para verificar as operações disponíveis para manipulação:
struct file_operations {
 struct module *owner;
 loff_t (*llseek) (struct file *, loff_t, int);
 ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
 ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
 ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
 ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
 int (*readdir) (struct file *, void *, filldir_t);
 unsigned int (*poll) (struct file *, struct poll_table_struct *);
 int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
 long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
 long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
 int (*mmap) (struct file *, struct vm_area_struct *);
 int (*open) (struct inode *, struct file *);
 int (*flush) (struct file *, fl_owner_t id);
 int (*release) (struct inode *, struct file *);
 int (*fsync) (struct file *, struct dentry *, int datasync);
 int (*aio_fsync) (struct kiocb *, int datasync);
 int (*fasync) (int, struct file *, int);
 int (*lock) (struct file *, int, struct file_lock *);
 ssize_t (*sendfile) (struct file *, loff_t *, size_t, read_actor_t, void *);
 ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
 unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
 int (*check_flags)(int);
 int (*dir_notify)(struct file *filp, unsigned long arg);
 int (*flock) (struct file *, int, struct file_lock *);
 ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
 ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
};
Então, basicamente, registramos as funções que iriamos utilizar(ec_devopen, ec_devclose, ec_devread e ec_devwrite) nos ponteiros de função(open, close, read e write) da estrutura.

Agora, reparem também, ainda no noso arquivo de cabeçalho, que temos um ponteiro para uma declaração do tipo proc_dir_entry. Ela é uma "análoga", se é que podemos dizer isso, da estrutura file_operations. Mais adiante compreenderemos porque ela é declarada na forma de um ponteiro aqui em cima. Por enquanto, adianto apenas que as funções ec_procread e ec_procwrite serão associadas a ela, juntamente com algumas outras informações. Veja declaração da estrutura:

struct proc_dir_entry {
 unsigned int low_ino;
 unsigned short namelen;
 const char *name;
 mode_t mode;
 nlink_t nlink;
 uid_t uid;
 gid_t gid;
 loff_t size;
 const struct inode_operations *proc_iops;
 const struct file_operations *proc_fops;
 get_info_t *get_info;
 struct module *owner;
 struct proc_dir_entry *next, *parent, *subdir;
 void *data;
 read_proc_t *read_proc;
 write_proc_t *write_proc;
 atomic_t count;  /* use count */

 int deleted;  /* delete flag */
 void *set;
};
Repare que nesta estrutura temos diversos elementos além de ponteiros de funções, diferentemente da file_operations que tinha apenas ponteiros de função(exceto pelo owner, ponteiro que nem utilizamos manualmente).

Agora vamos finalmente ao nosso arquivo de implementação(errcon.c). A primeira coisa que vemos(após a inclusão do cabeçalho errcon.h) é implementação da função ec_devwrite. O que ela faz, basicamente, é implementar a operação write no dispositivo. Simplesmente lê o buffer de leitura(localizado no user-space, por isso lido com get_user e não diretamente) e o salva em nossa lista dos ultimos 4 erros.

Quanto ao local para onde os ponteiros de nosso array de ponteiro apontam, entenderemos mais tarde. De momento saiba apenas que é para um buffer de dados. Repare que como este módulo é apenas um teste, NENHUM TIPO DE PREVENÇÃO CONTRA BUFFER OVERFLOW FOI TOMADO. Para ocorrer um buffer overflow, basta escrevermos um número de bytes maior do que o tamanho do buffer. E, para este buffer overflow ser mais grave, basta que isso ocorra no ultimo dos 4 buffers, ou então que seja grande o suficiente para passar do fim do ultimo buffer no caso dos outros 3 buffers. Isso tudo se deve a grande preguiça que o autor tem de implementar um contador de caracteres(int i=buf_limit e buf_limit-- dentro do loop :-) hehehe).

Agora prosseguindo, temos nossa função ec_procread, que responde à operação read do arquivo de dispositivo. Ela basicamente escreve os dados do buffer do módulo(com a mensagem do ultimo erro recebido pelo módulo) no buffer do user-space(lá aonde o processo está fazendo a leitura. A "idéia" do put_user é a mesma do get_user, com diferença apenas operacional(um coloca no usuário e o outro pega do usuário, :-) entenda este comentário como quiser).

Agora vamos para a ec_open. Ela é responsável pela abertura do arquivo (citação de Dr. Obvio). Para evitar multiplos processos efetuando I/O no módulo, fazemos um "controle de abertura". No mais, a função também reseta o buffer de leitura. Como vocês viram, na ec_read nós apenas liamos apartir do buffer. Mas aí ficava no ar a pergunta: daonde vinha a posição correta do buffer?

É simples. Imagine que o processo não leia todos os dados de uma vez só. Suponhamos que nós tenhamos uma string de erro "hahehihohu". O tamanho de buffer utilizado pelo processo que lê o dispositivo é 2. Acompanhe a seqüência:
  • Processo efetua open() no dispositivo;
  • Processo lê 2 bytes;
  • Processo "trabalha" os 2 bytes lidos;
  • Processo lê mais 2 bytes;
  • Processo "trabalha" mais 2 bytes lidos;
  • Processo lê mais 2 bytes;
  • ...
A posição do buffer deve ser trabalhada na função da operação read, incrementando os bytes que são enviados para o user-space. Também devemos apontar, na primeira chamada à read, para o início do buffer. Mas como vamos saber quando é a primeira chamada?? Simples: na chamada da operação open realizada no arquivo, nós setamos o ponteiro para NULL, e verificamos se ele é nulo no momento da operação read.

Só para constar, vejamos a linha aonde fazemos a chamada:
try_module_get(THIS_MODULE);
Nesta chamada simplesmente incrementamos o contador de uso do módulo, gerenciado pelo kernel. Não confunda com nosso contador interno(ec_openc), que é para uso dentro do módulo. Ao executarmos um comando lsmod no shell, veremos uma tela similar a esta:

bruno@bw1:~$ lsmod
Module Size Used by
parport_pc 27812 0
parport 34760 1 parport_pc
yenta_socket 27148 1

Repare no terceiro campo(Used By), aonde vemos um número, algumas vezes seguido de uma lista de módulos. Este número é o contador de usos. Os módulos listados são outros módulos que utilizam recurso do listado. Isso significa que o try_module_get não serve apenas para indicar quantas vezes o arquivo de dispositivo foi aberto, mas para n outras coisas.

A função ec_devclose dispensa comentários. Apenas esclareço que o try_module_put decrementa o contador de uso do módulo. Na função ec_procread, simplesmente escrevemos no buffer uma mensagem formatada, dizendo o número de erros e o ultimo erro que foram registrados. Na ec_procwrite, utilizamos a função copy_from_user para ler 1 caractere do buffer recebido. Caso este caractere lido seja 'c', é efetuado um loop percorrendo o buffer de mensagens de erro e apagando seus dados.

Bom, agora que todos já entenderam "cada pedaço" da coisa, vamos ao "ponto de junção", aonde a gente "junta toda essa cosia e faz virar uma só". É o nosso "main 1", o init_module. Como eu já havia citado anteriormente, este "cara" é o responsável por disponibilizar aquilo que o módulo fornece, e inicializar o "serviço" do módulo.

A primeira coisa que ele faz é preparar os nossos buffers. Como você pode ver no código, de acordo com minha explicação sobre ponteiros, criamos um array global, de 16386 bytes(de acordo com a macro EC_PROCFILE_BUFF_SIZE) apontado por ec_errors, ainda no nosso header(errcon.h). Agora nós dividimos a área apontada por ec_errors em 4 partes, cada uma com seu início apontado por um dos ponteiros do array de ponteiros apontado por ec_error(esta frase é meio confusa, mas tente reler umas 2 vezes, ou se tiver raciocínio visual e precisar, não tenha vergonha de desenhar num papel ao longo da leitura para entender a frase - e se ainda não entender, recomendo ler novamente a explicação sobre ponteiros). Observe que temos 2 poréms nesta história: o primeiro é que ao longo do programa não utilizamos nenhuma mensagem além da ultima. Isso foi uma falha minha, pois na época eu não tinha noção alguma do que significava "requisito de software".

O segundo porém, é um pequeno errinho meu. Se vocês acompanharem o loop aonde é feita essa divisão da área, vocês vão ver que todos os 4 ponteiros apontam para a mesma área, que seria "a segunda posição" das 4 na qual dividimos. Apartir de agora, quando vocês olharem o código ele estará atualizado, e o valor:
(EC_PROCFILE_BUF_SIZE / 4)
será multiplicado por i.

Após dividirmos o nosso array em 4 buffers "distintos", criamos nossa entrada no sistema de arquivos proc. Fazemos um tratamento de erro, e caso ocorra realmente algo errado, nós efetuamos a tentativa de remoção da entrada recém criada(ou não criada, não temos como ter certeza, removemos para evitar uma entrada desagradável e permanente no /proc).

Logo depois, criamos nossa entrada no /dev. Só para constar, caso o retorno do register_chrdev seja negativo(erro) em vez do major, temos certeza de que o device não foi criado(diferentemente da entrada no /proc). E, então, removemos a entrada.

Em ambos os tratamentos de erro, retornamos negativo. Ou seja: indicamos pro kernel que ocorreram erros e ele deve cancelar o carregamento do módulo. O kernel, por sua vez, informa o modutils disso, e este por sua vez joga uma mensagem de erro para o usuário. É vital fazermos isso para evitar problemas!

Observem a diferença entre as abordagens da criação do arquivo de dispositivo e da criação da entrada no /proc. No primeiro caso, criamos a estrutura com as informações das operações de arquivo, e depois chamamos a função responsável "registrando" esta estrutura. No segundo, criamos apenas um ponteiro e utilizamos uma função que retorna a área da estrutura. Nesta função, passamos apenas o nome do arquivo a ser criado.

Uma vez registradas, estas estruturas podem ser modificadas a qualquer momento. É o que fazemos com a estrutura do nosso arquivo no /proc. Ao remover, passamos como parâmetro o endereço de proc_root. A entrada proc_root nada mais é do que outra estrutura proc_dir_entry, que representa a raiz do /proc.

Veja que depois nós modificamos nossa entrada no /proc acessando diretamente a estrutura. Logo após isso, o restante do código é apenas complementar: imprimimos com printk as mensagens informando alguns detalhes do módulo.

Retornamos 0(o valor de SUCCESS) para indicar sucesso na operação. Valores negativos indicam erro e cancelam o carregamento do módulo.

Quase no fim, temos nosso cleanup_module. Basicamente, ele desregistra nosso dispositivo de caractere, e remove nossa entrada do /proc.

Ao fim do código, utilizamos algumas macros para registrar algumas informações sobre o módulo: sua licensa, seu autor, o nome de seu dispositivo, e sua descrição. Você pode visualizar suas informações com o comando:

$ modinfo errcon.ko

Finalmente, peço desculpas pela demora a postar algo novo. É que esta semana está corrida!! Espero que gostem, em breve mais conteúdo sobre o kernel!!

OBS: se alguém quiser que eu escreva sobre algum tema específico, e estiver ao meu alcance, é só dizer que se tiver no meu domínio e se encaixar no meu "time" eu escrevo.

Agradeço a atenção de todos os meus leitores. Caso não saibam, tem um milhão lendo agora esse blog. E em seguida esse milhão vai ser cozido :P

[1] System Calls, ou chamadas de sistema, são "funções" disponibilizadas pelo sistema operacional aos aplicativos. Geralmente bem básicas. Vide man page for syscalls(2)

Classificação do conteúdo: SÉRIO
Sobre Bruno Moreira Guedes:
Curriculum Vitae
Site Pessoal

Nenhum comentário:

Sobre Bruno Moreira Guedes