Driver de dispositivo Linux (4) - tecnologia de depuração


prefácio

Como o kernel é uma coleção de funções não relacionadas a um processo específico, o código do kernel não pode ser facilmente executado em um depurador e também é difícil de rastrear e rastrear.Este capítulo apresentará técnicas para monitorar o código do kernel e rastrear erros.


1. Tecnologia de depuração no kernel

Listamos as opções de configuração que devem ser habilitadas para um kernel de desenvolvimento e, salvo indicação em contrário, todas essas opções estão no menu "kernel hacking" da ferramenta de configuração do kernel.注意:并非所有体系架构都支持其中的某些选项

  • CONFIG_DEBUG_KERNEL
    • Esta opção apenas disponibiliza outras opções de depuração . Ele deve estar ativado, mas por si só não ativará todos os recursos de depuração.
  • CONFIG_DEBUG_SLAB
    • Esta é uma opção muito importante. Ela abre vários tipos de verificações na função de alocação de memória do kernel. Depois que esta verificação é ativada, muitos erros de estouro de memória e inicialização esquecida podem ser detectados. Cada byte de memória alocada é submetido ao chamador é definido como 0xa5 antes, mas é definido como 0x6b após o lançamento.Se você vir o caractere "laje" acima na saída de seu próprio driver ou nas informações oops, poderá determinar facilmente o problema. Após a opção de depuração ser ativada, o kernel também definirá alguns valores especiais de proteção antes e depois de cada objeto de memória alocado; dessa forma, quando esses valores de proteção mudarem, o kernel poderá saber que alguns códigos excedem o acesso normal de escopo de memória.
  • CONFIG_DEBUG_PAGEALLOC
    • Quando liberadas, todas as páginas de memória são removidas do espaço de endereço do kernel . Esta opção retardará bastante a operação, mas pode localizar rapidamente o local de corrupção de memória específica.
  • CONFIG_DEBUG_SPINLOCK
    • Com esta opção ativada, o kernel interceptará operações em spinlocks não inicializados, bem como outros erros, como desbloquear o mesmo bloqueio duas vezes .
  • CONFIG_DEBUG_SPINLOCK_SLEEP
    • Esta opção verificará as tentativas de hibernação enquanto mantém um spinlock . Na verdade, esta opção também entra em vigor se uma função que pode causar uma suspensão for chamada, mesmo que a função não cause realmente uma suspensão.
  • CONFIG_INIT_DEBUG
    • Os símbolos marcados com __init (ou __initdata) serão descartados após a inicialização do sistema ou carregamento do módulo. Esta opção pode ser usada para verificar as tentativas de acesso ao espaço de memória usado para inicialização após a conclusão da inicialização .
  • CONFIG_DEBUG_INFO
    • Esta opção faz com que o kernel seja construído com informações completas de depuração, que você precisará se quiser depurar o kernel com gdb . Se você planeja usar o gdb, também precisa habilitar o CONFIG FRAME POINTER.
  • CONFIG_MAGIC_SYSRQ
    • Ative o botão "SysRq magic (magic SysRq)". Abordaremos essa chave na seção "Suspensões do sistema" mais adiante neste capítulo.
  • CONFIG_DEBUG_STACKOVERFLOW
  • CONFIG_DEBUG_STACK_USAGE
    • Essas opções podem ajudar a rastrear problemas de estouro da pilha do kernel . Um sinal claro de um estouro de pilha é um manifesto oops que não contém nenhuma informação razoável de backtrace. A primeira opção adicionará verificações de estouro explícitas no kernel; enquanto a segunda opção permitirá que o kernel monitore o uso da pilha e gere algumas estatísticas por meio da chave SysRq .
  • CONFIG_KALLSYMS
    • Esta opção aparece no menu "Configuração geral/Recursos padrão" para incluir informações de símbolos no kernel; esta opção está ativada por padrão. Esta informação de símbolo é usada para contexto de depuração ; sem este símbolo, o manifesto oops só pode fornecer informações de backtrace do kernel em hexadecimal, o que geralmente não é muito útil.
  • CONFIG_IKCONFIG
  • CONFIG_IKCONFIG_PROC
    • Essas opções (no "Menu de configuração geral") fazem com que o estado completo da configuração do kernel seja incorporado ao kernel, que pode ser disponibilizado via /proc, a maioria dos desenvolvedores do kernel sabe qual configuração está usando e não precisa dessas opções ( o que faria kernels maiores) Mas eles podem ser úteis se você estiver tentando depurar problemas em kernels construídos por outras pessoas .
  • CONFIG_ACPI_DEBUG
    • Esta opção aparece no menu "Gerenciamento de energia/ACPI (gerenciamento de energia/ACPI)". Esta opção ativará informações detalhadas de depuração em ACPI (Configuração avançada e interface de energia, configuração avançada e interface de energia) . Use esta opção se suspeitar que o problema que está enfrentando está relacionado à ACPI.
  • CONFIG_DEBUG_DRIVER
    • No menu "Drivers de dispositivo (driver de dispositivo)". Essa opção ativa as informações de depuração no núcleo do driver, o que pode ajudar a rastrear problemas no código de suporte subjacente.
  • CONFIG_SCSI_CONSTANTS
    • Essa opção aparece no menu "Drivers de dispositivo/suporte a dispositivos SCSI (driver de dispositivo/suporte a dispositivos SCSI)", que abrirá mensagens de erro SCSI detalhadas . Esta opção pode ser usada se o leitor quiser escrever um driver SCSI.
  • CONFIG_INPUT_EVBUG
    • Esta opção pode ser encontrada em "Drivers de dispositivo/Suporte de dispositivo de entrada (driver de dispositivo/suporte a dispositivo de entrada), ativará a gravação detalhada de eventos de entrada . Se o leitor quiser escrever um driver para o dispositivo de entrada, você pode usar esta opção Esteja ciente dos problemas de segurança que esta opção pode causar: ela registrará tudo o que você digitar, incluindo senhas.
  • CONFIG_PROFILING
    • Esta opção está em "Suporte de criação de perfil". A criação de perfil geralmente é usada para ajustar o desempenho do sistema, mas também pode ser útil para rastrear algumas interrupções do kernel e problemas relacionados .

As opções acima serão encontradas novamente à medida que percorremos as diferentes abordagens para o rastreamento de problemas do kernel.

2. Depuração por impressão

Ao depurar o código do kernel, você pode usar printk para realizar o mesmo trabalho.

1、printk

A diferença do printk em relação ao printf: Uma das diferenças é que ao anexar diferentes níveis de log (logevel), ou prioridades de mensagens, o printk pode ser usado para classificar as mensagens de acordo com a gravidade indicada por esses níveis . Geralmente usamos macros para indicar o nível de log, como KERN_INFO, a macro que indica o nível de log será expandida em uma string e será costurada junto com o texto da mensagem pelo pré-processador em tempo de compilação; é por isso que a prioridade em o exemplo a seguir e a string de formato sem vírgula. Aqui estão dois exemplos de printk, um para informações de depuração e outro para informações críticas:

printk(KERN_DEBUG "Here I am: %s:%i\n", __FILE__, __LINE__);
printk(KERN_CRIT "I'm trashed; giving up on %p\n", ptr);

Oito strings de nível de log disponíveis são definidas no arquivo de cabeçalho <linux/kernel.h>, listadas abaixo em ordem decrescente de gravidade:

  • KERN_EMERG
    • Usados ​​para mensagens de emergência, geralmente são as mensagens que são solicitadas antes que o sistema falhe .
  • CORE_ALERT
    • Usado em situações onde é necessária ação imediata .
  • KERN_CRIT
    • Um estado crítico, geralmente envolvendo uma falha operacional grave de hardware ou software.
  • CORE_ERR
    • Usado para relatar status de erro. Os drivers de dispositivo geralmente usam KERN_ERR para relatar problemas do hardware .
  • KERN_WARNING
    • Alerta sobre possíveis condições problemáticas, mas tais condições geralmente não causam problemas sérios ao sistema .
  • KERN_NOTICE
    • Uma situação normal em que a solicitação é necessária. Muitas condições relacionadas à segurança são relatadas nesse nível .
  • CORE_INFO
    • Informação informativa. Muitos drivers imprimem informações sobre o hardware que encontram nesse nível na inicialização .
  • KERN_DEBUG
    • Usado para informações de depuração.

Cada string (expandida como uma macro) representa um inteiro entre parênteses. O intervalo de valores inteiros é de 0 a 7, quanto menor o valor, maior a prioridade . O nível padrão
usado pelas instruções printk sem uma prioridade especificada é DEFAULT_MESSAGE_LOGLEVEL, que é especificado como um número inteiro em kernel/printk.c. No kernel 2.6.10, DEFAULT_MESSAGE_LOGLEVEL é KERN_WARNING .

Dependendo do nível de log, o kernel pode imprimir mensagens para o console atual, que pode ser um terminal de modo de caractere, uma impressora serial ou uma impressora paralela. Quando a prioridade é menor que o valor da variável inteira console_loglevel, a mensagem pode ser exibida e cada linha de saída ( 如果不以newline字符结尾,则不会输出). Se o sistema executar klogd e syslogd ao mesmo tempo, independentemente do valor de console_loglevel, as mensagens do kernel serão anexadas a /var/log/messages (caso contrário, serão processadas de acordo com a configuração de syslogd) . Se o klogd não estiver em execução, essas mensagens não serão passadas para o espaço do usuário, caso em que apenas o arquivo /proc/kmsg poderá ser visualizado (fácil de fazer com o comando dmesg) . Se você usar klogd, você deve entender que ele não salva linhas de informação consecutivas idênticas, ele apenas salva a primeira linha idêntica consecutiva e imprime o número de repetições desta linha no final.

A variável console_loglevel tem um valor inicial de DEFAULT_CONSOLE_LOGLEVEL e também pode ser modificada pela chamada de sistema sys_syslog . Essa variável pode ser modificada especificando a opção -c ao chamar klogd. 注意,要修改其当前值,必须先杀掉 klogd,然后再用新的 -c 选项重新启动它. Além disso, os programas podem ser escritos para alterar o nível de log do console. A nova prioridade é especificada como um valor inteiro entre 1-8. Se o valor for definido como 1, somente mensagens com nível 0 (KERN_EMERG) chegarão ao console; se for definido como 8, todas as mensagens, incluindo mensagens de depuração, serão exibidas.

Também podemos ler e modificar o nível de log do console acessando o arquivo de texto /procsys/kernel/printk . Este arquivo contém 4 valores inteiros: o nível de log atual, o nível de mensagem padrão quando nenhum nível de log é explicitamente especificado, o nível de log mínimo permitido e o nível de log padrão na inicialização. Gravar um único valor inteiro neste arquivo alterará o nível de log atual para esse valor. Por exemplo, você pode simplesmente digitar o seguinte comando para que todas as mensagens do kernel sejam impressas no console:

echo 8 > /proc/sys/kernel/printk

2. Redirecione as mensagens do console

O Linux permite alguma flexibilidade com a estratégia de log do console: o kernel pode enviar mensagens para um console virtual específico (desde que o console seja uma tela de texto). Por padrão, "console" é o terminal virtual atual. Você pode chamar ioctl(TIOCLINUX) em qualquer dispositivo de console para especificar outros terminais virtuais para receber mensagens . O seguinte programa setconsole pode selecionar um console especialmente usado para receber mensagens do kernel. Este programa deve ser executado pelo superusuário e pode ser encontrado no diretório misc-progs.
Abaixo está a lista completa do programa. Ao chamar o programa, anexe um argumento especificando o número do console para receber a mensagem.

int main(int argc, char **argv)
{
    
    
	char bytes[2] = {
    
    11,0}; /* 11 is the TIOCLINUX cmd number */
	if (argc==2) bytes[1] = atoi(argv[1]); /* the chosen console */
	else {
    
    
		fprintf(stderr, "%s: need a single arg\n",argv[0]); exit(1);
		}
	if(ioctl(STDIN_FILENO, TIOCLINUX, bytes)<0) {
    
     /* use stdin */
		fprintf(stderr,"%s: ioctl(stdin, TIOCLINUX): %s\n",
		argv[0], strerror(errno));
		exit(1);
	}
	exit(0);
}

setconsole usa um comando ioctl especial: TIOCLINUX, este comando pode completar algumas funções específicas do Linux. Ao usar o TIOCLINUX, você precisa passar para ele um parâmetro de ponteiro apontando para a matriz de bytes. O primeiro byte da matriz especifica o número do subcomando solicitado e a função dos bytes subsequentes é determinada por esse subcomando . No setconsole, o subcomando usado é 11, e o último byte (armazenado em bytes[1]) é usado para identificar o console virtual. Uma descrição completa do TIOCLINUX está disponível no arquivo drivers/char/ttyio.c no código-fonte do kernel.

3. Como a mensagem é gravada

A função printk escreve mensagens em um buffer circular de comprimento __LOG_BUP_LEN bytes (podemos especificar um valor entre 4 KB-1MB para __LOG_BUP_LEN ao configurar o kernel). A função então ativa todos os processos que estão esperando por mensagens, ou seja, aqueles que estão dormindo na chamada do sistema syslog ou lendo /proc/kmsg . As interfaces dos dois mecanismos de log de acesso são quase equivalentes, 不过请注意,对 /proc/kmsg 进行读操作时,日志缓冲区中被读取的数据就不再保留,而 syslog 系统调用却能通过选项返回日志数据并保留这些数据,以便其他进程也能使用. Em geral, é mais fácil ler o arquivo /proc, que é o método padrão do klogd. O comando dmesg pode obter o conteúdo de um buffer sem eliminá-lo; na verdade, o comando retorna todo o conteúdo do buffer para sdout, independentemente de o buffer ter sido lido ou não.

Se você ler as mensagens do kernel manualmente após parar o klogd, o leitor descobrirá que o arquivo /proc/kmsg se parece com um FIFO e o processo de leitura será bloqueado no arquivo, aguardando mais dados . Obviamente, se já houver klogd ou outros processos lendo os mesmos dados, esse método não pode ser usado para ler a mensagem, pois competiria com esses processos.

Se o buffer circular ficar cheio, printk retorna ao início do buffer para preencher os novos dados, que sobrescrevem os dados mais antigos, de modo que o processo de registro perde os dados mais antigos . Mas esse problema é insignificante em comparação com os benefícios de usar um buffer circular.

Quando o klogd é executado, ele lê as mensagens do kernel e as distribui para o syslogd, que então examina /etc/syslog.conf para descobrir o que fazer com os dados . syslogd diferencia mensagens por função e prioridade; valores opcionais para ambos são definidos em <sys/syslog.h>. As mensagens do kernel são registradas pelo recurso LOG_KERN e são registradas com a prioridade correspondente em printk (por exemplo, KERN_ERR usado em printk corresponde a LOG_ERR em syslogd). Sem a execução do klogd, os dados permanecerão em um buffer circular até que algum processo os leia ou o buffer estoure.

Se você deseja evitar sobrecarregar o log do sistema com uma grande quantidade de informações de monitoramento do driver, pode especificar a opção -f(arquivo) para klogd, instruindo klogd a salvar a mensagem em um arquivo específico ou modificar /etc/syslog .conf para satisfazer as próprias necessidades.

4. Abra e feche a mensagem

Aqui está uma maneira codificada de chamar printk que alterna instruções printk individualmente ou globalmente; o truque é definir uma macro que se expande para uma chamada printk (ou printf) quando necessário:

  • Cada instrução de impressão pode ser ativada ou desativada excluindo ou adicionando uma letra ao nome da macro.
  • Você pode desabilitar todas as mensagens de uma vez modificando a variável CFLAGS antes de compilar.
  • As mesmas instruções de impressão podem ser usadas no código do kernel como no código de nível de usuário, para que drivers e programas de teste possam gerenciar essas informações de depuração adicionais da mesma maneira.
    Os trechos de código a seguir do arquivo de cabeçalho scull.h implementam essas funções:
#undef PDEBUG /* undef it, just in case */
#ifdef SCULL_DEBUG
#	ifdef __KERNEL__
/* This one if debugging is on, and kernel space */
#		define PDEBUG(fmt, args...) printk( KERN_DEBUG "scull: " fmt, ## args)
#	else
/* This one for user space */
#		define PDEBUG(fmt, args...) fprintf(stderr, fmt, ## args)
#	endif
#else
#	define PDEBUG(fmt, args...) /* not debugging: nothing */
#endif
#undef PDEBUGG 
#define PDEBUGG(fmt, args...) /* nothing: it's a placeholder */

A definição do símbolo PDEBUG depende se SCULL_DEBUG está definido, e pode escolher uma forma apropriada para exibir informações de acordo com o ambiente em que o código está sendo executado: no modo kernel, ele usa o kernel para chamar printk; no espaço do usuário, ele usa libc para chamar fprintf e Output to standard error device . O símbolo PDEBUGG, por outro lado, não faz nada; ele pode comentar as instruções de impressão sem removê-las completamente.
Para simplificar ainda mais o processo, as seguintes linhas podem ser adicionadas ao makefile:

# Comment/uncomment the following line to disable/enable debugging
DEBUG = y
# Add your debugging flag (or not) to CFLAGS
ifeq ($(DEBUG),y)
DEBFLAGS = -O -g -DSCULL_DEBUG # "-O" is needed to expand inlines
else
DEBFLAGS = -O2
endif
CFLAGS += $(DEBFLAGS)

Instruções condicionais pré-processadas (e expressões constantes em seu código) são executadas apenas no tempo de compilação, portanto, você deve recompilar para ativar ou desativar as mensagens novamente. Outra abordagem é usar instruções condicionais C, que são executadas em tempo de execução, para que as mensagens possam ser ativadas ou desativadas enquanto o programa está em execução . Este é um ótimo recurso, mas o sistema precisa fazer um processamento extra toda vez que o código é executado e, mesmo após desabilitar as mensagens, ainda afeta o desempenho e, às vezes, essa perda de desempenho é inaceitável.

5. Limite de velocidade

Às vezes, você acidentalmente usa printk para gerar milhares de mensagens, preenchendo o console com informações de log e, mais provavelmente, sobrecarregando o arquivo de log do sistema. Se você usar um dispositivo de console lento (como uma porta serial), a alta velocidade de saída da mensagem fará com que o sistema fique mais lento ou até mesmo impossibilite que o sistema responda normalmente.

Em muitos casos, a melhor coisa a fazer é definir um sinalizador que diga "Eu disse" e não imprimir nada quando o sinalizador for definido. Mas, em alguns casos, ainda há um motivo para uma mensagem ocasional "Este dispositivo parou de funcionar". O kernel fornece uma função útil para este caso:

int printk_ratelimit(void);

A função acima deve ser chamada antes de imprimir uma mensagem que pode ser repetida. Se a função retornar um valor diferente de zero, podemos continuar e imprimir nossa mensagem, caso contrário, devemos pular. Como tal, uma chamada típica deve ser semelhante a esta:

if (printk_ratelimit())
	printk(KERN_NOTICE "The printer is still on fire\n");

printk ratelimit funciona controlando o número de mensagens enviadas ao console. Se a taxa de saída exceder um limite, printk ratelimit retornará zero, evitando assim o envio de mensagens duplicadas. Podemos personalizar
modificando /proc/sys/kernel/printk ratelimit ( o número de segundos que deve esperar antes de reabrir a mensagem ) e /proc/sys/kernel/printk ratelimit burst ( o número de mensagens que podem ser aceitas antes da limitação de velocidade ) Comportamento de printk ratelimit .

6. Imprima o número do dispositivo

Às vezes, ao imprimir mensagens de um driver, gostaríamos de imprimir o número do dispositivo associado ao hardware. O kernel fornece um par de macros auxiliares (definidas em <linux/kdev th>):

int print_dev_t(char *buffer, dev_t dev);
char *format_dev_t(char *buffer, dev_t dev);

Essas duas macros imprimem o número do dispositivo no buffer fornecido, a única diferença é que print_dev_t retorna o número de caracteres impressos, enquanto format_dev_t retorna o buffer, portanto, seu valor de retorno pode ser usado diretamente como os parâmetros usados . 不能忘记只有在结尾处存在newline(新行)字符时,printk才将消息刷新到控制台。O buffer passado para a macro acima deve ser grande o suficiente para conter um número de dispositivo. Como a possibilidade de usar números de dispositivo de 64 bits é muito aparente em versões futuras do kernel, o tamanho desse buffer deve ser de pelo menos 20 bytes .

3. Depuração por consulta

O uso pesado de printk ainda reduzirá significativamente o desempenho do sistema, no entanto, diminuir o desempenho do sistema devido ao processamento de informações de depuração é indesejável. Esse problema pode ser resolvido prefixando o nome do arquivo de log em /etc/syslogd.conf com um sinal de menos . O problema com a modificação de arquivos de configuração é que essas alterações persistirão após a conclusão da depuração; se você não quiser fazer essas alterações persistentes, outra opção é executar um programa não-klogd (como cat /proc/kmsg descrito anteriormente) , mas isso não fornece um ambiente adequado para a operação normal do sistema.

Na maioria dos casos, a melhor maneira de obter informações relevantes é consultar as informações do sistema quando necessário, em vez de gerar dados continuamente. Na verdade, todo sistema Unix fornece muitas ferramentas para obter informações do sistema, como ps, netstat, vmstat e assim por diante.

Os desenvolvedores de driver podem consultar o sistema criando arquivos no sistema de arquivos proc, usando os métodos ioctl do driver, exportando atributos por meio de sysfs e assim por diante.

1. Use o sistema de arquivos /proc

O sistema de arquivos /proc é um sistema de arquivos especial criado por software. O kernel o usa para exportar informações para o mundo externo. Cada arquivo em /proc é vinculado a uma função do kernel. Quando o usuário lê o arquivo, a função gera dinamicamente o "conteúdo" do arquivo . Vimos algumas saídas de tais arquivos, por exemplo, /proc/modules lista os módulos atualmente carregados .

/proc é muito usado em sistemas Linux. Muitas ferramentas em distribuições Linux modernas usam /proc para obter as informações de que precisam, como ps, top e uptime . Alguns drivers de dispositivo também exportam informações por meio do iproc, e nossos próprios drivers podem fazer o mesmo. Como o sistema de arquivos /proc é dinâmico, os módulos de driver podem adicionar ou remover entradas dele a qualquer momento .

①. Implementar arquivos em /proc

Todos os módulos que usam /proc devem incluir <linux/proc_fs.h> e definir as funções corretas por meio desse arquivo de cabeçalho.

Quando um processo lê o arquivo /proc, o kernel aloca uma página de memória (ou seja, um bloco de memória de bytes PAGE_SIZE) e o driver pode retornar os dados ao espaço do usuário por meio dessa página de memória. Este buffer é passado para a função que definimos, que é chamada de método read_proc:

int (*read_proc)(char *page, char **start, off_t offset, int count, int *eof, void *data);
  • página: aponta para o buffer usado para gravar dados
  • start: retorna onde os dados reais são gravados na página de memória
  • offset, count: igual ao método read
  • eof: aponta para um inteiro, quando não há dados para retornar, o driver deve definir este parâmetro
  • dados: Um ponteiro de dados dedicado fornecido ao driver, que pode ser usado para gravação interna

Quando nosso método read_proc é chamado, o valor inicial de start é NULL. Se *start for deixado em branco, o kernel assumirá que os dados estão armazenados no deslocamento 0 da página de memória ; ou seja, o kernel fará as seguintes suposições simples sobre read_proc: Esta função coloca todo o conteúdo do arquivo virtual no página de memória e, ao mesmo tempo, o parâmetro offset é ignorado. Pelo contrário, se definirmos start com um valor não nulo, o kernel considerará os dados apontados por *start como sendo os dados no deslocamento especificado por offset e poderá retornar diretamente ao usuário.

Há outro grande problema com arquivos /proc, e este é o que o start pretende resolver. Às vezes, entre chamadas de leitura consecutivas, a representação ASCII da estrutura de dados do kernel muda, de modo que o processo de leitura descobre que os dados obtidos pelas duas chamadas são inconsistentes .

Observe que há uma maneira melhor de implementar arquivos /proc chamados seg_file, que abordaremos posteriormente. Agora vamos ver um exemplo, o seguinte é uma implementação simples da função read_proc do dispositivo scull:

int scull_read_procmem(char *buf, char **start, off_t offset, int count, int *eof, void *data)
{
    
    
	int i, j, len = 0;
	int limit = count - 80; /* Don't print more than this */
	for (i = 0; i < scull_nr_devs && len <= limit; i++) {
    
    
		struct scull_dev *d = &scull_devices[i];
		struct scull_qset *qs = d->data;
		if (down_interruptible(&d->sem))
			return -ERESTARTSYS;
		len += sprintf(buf+len,"\nDevice %i: qset %i, q %i, sz %li\n", i, d->qset, d->quantum, d->size);
		for (; qs && len <= limit; qs = qs->next) {
    
     /* scan the list */
			len += sprintf(buf + len, " item at %p, qset at %p\n", qs, qs->data);
			if (qs->data && !qs->next) /* dump only the last item */
				for (j = 0; j < d->qset; j++) {
    
    
					if (qs->data[j])
						len += sprintf(buf + len, " % 4i: %8p\n", j, qs->data[j]);
				}
		}
		up(&scull_devices[i].sem);
	}
	*eof = 1;
	return len;
}

②, Crie seu próprio arquivo /proc

Uma vez que uma função read_proc tenha sido definida, ela precisa ser vinculada a uma entrada /proc. Isso é obtido chamando create_proc_read_entry:

struct proc_dir_entry *create_proc_read_entry(const char *name, mode_t mode, struct proc_dir_entry *base, read_proc_t *read_proc, void *data);
  • name: o nome do arquivo a ser criado
  • modo: a máscara de proteção do arquivo (0 pode ser passado para indicar o valor padrão do sistema)
  • base: o diretório onde o arquivo está localizado (se base for NULL, o arquivo será criado no diretório raiz de /proc)
  • read_proc: implementa a função read_proc do arquivo
  • data: o kernel ignora o parâmetro data, mas passa para read_proc

Aqui está o código para o scull chamar esta função para criar o arquivo /proc:

create_proc_read_entry("scullmem", 0 /* default mode */, NULL /* parent dir */, scull_read_procmem, NULL /* client data */);

O código acima cria um arquivo chamado scullem no diretório /proc com permissões de leitura universal definidas por padrão.

Obviamente, quando o módulo é descarregado, as entradas em /proc também devem ser removidas. remove_proc_entry é a função usada para desfazer o trabalho feito por create_proc_read_entry:

remove_proc_entry("scullmem", NULL /* parent dir */);

Ao usar arquivos /proc, o leitor deve ter em mente várias deficiências dessa implementação, portanto, o uso de arquivos /proc é desencorajado.

  • A questão mais importante tem a ver com a remoção da entrada /proc. Chamadas de exclusão podem ocorrer enquanto os arquivos estão sendo usados, pois as entradas /proc não possuem um proprietário associado, portanto, o uso desses arquivos não contribui para a contagem de referência do módulo . Esse problema pode ser acionado executando o comando sleep 100 < /proc/myfile ao remover o módulo.
  • Outra dúvida é sobre cadastrar duas entradas populacionais com o mesmo nome. O kernel confia no driver para não verificar se um nome já está registrado, portanto, se você não tomar cuidado, poderá acabar com duas ou mais entradas com o mesmo nome .

③, interface seq_file

Para facilitar o trabalho de desenvolvimento do kernel, a interface seq_file é adicionada organizando o código /proc. Essa interface fornece um conjunto simples de funções para grandes arquivos virtuais do kernel.

A interface seq_file assume que o arquivo virtual que estamos criando percorre sequencialmente uma sequência de itens que devem ser retornados ao espaço do usuário. Para usar seq_file, devemos criar um objeto "iterador" simples que represente uma posição na sequência de itens e imprima um item na sequência para cada passo adiante . Abaixo usaremos este método para criar um arquivo /proc para o driver scull.

Obviamente, o primeiro passo é incluir o arquivo de cabeçalho <linux/seg_file.h>, e então quatro objetos iteradores devem ser criados, a saber: start, next, stop e show.

O método start é sempre chamado primeiro, e o protótipo desta função é o seguinte:

void *start(struct seq_file *sfile, loff_t *pos);
  • sfile: ignorado na maioria dos casos
  • pos: A posição a ser lida, porque a implementação de seq_file geralmente percorre uma sequência de itens, então a posição geralmente é interpretada como um cursor apontando para o próximo item na sequência. O driver scull trata cada dispositivo como um item na sequência, então a pos passada é simplesmente um índice no array scull_devices.

Portanto, o método start de scull pode ser escrito da seguinte forma:

static void *scull_seq_start(struct seq_file *s, loff_t *pos)
{
    
    
if (*pos >= scull_nr_devs)
	return NULL; /* No more to read */
return scull_devices + *pos;
}

Se o valor de retorno não for NULL, as implementações do iterador podem usá-lo como um valor privado.

A próxima função deve mover o transmissor para a próxima posição e retornar NULL se não houver outros itens na sequência. O protótipo do método é:

void *next(struct seq_file *sfile, void *v, loff_t *pos);
  • v: o seletor retornado por uma chamada anterior para iniciar ou próximo
  • pos: a posição atual do arquivo

O próximo método deve incrementar o valor apontado por pos, dependendo de como o iterador funciona, em alguns casos podemos querer incrementar pos em um valor maior que 1. O próximo método de scull é implementado da seguinte forma:

static void *scull_seq_next(struct seq_file *s, void *v, loff_t *pos)
{
    
    
	(*pos)++;
	if (*pos >= scull_nr_devs)
		return NULL;
	return scull_devices + *pos;
}

Quando o kernel usar o iterador, ele chamará o método stop para nos notificar para limpar:

void stop(struct seq_file *sfile, void *v);

A implementação de scull não precisa concluir o trabalho de exclusão, portanto, seu método stop está vazio.
值得注意的是,在设计上,seq_file 的代码不会在 start 和 stop 的调用之间执行其他的非原子操作。我们可以确信,start 被调用之后马上就会有对 stop 的调用。因此,在 start 方法中获取信号量或者自旋锁是安全的。只要其他 seq_file 方法是原子的,则整个调用过程也是原子的

Entre as chamadas acima, o kernel chamará o método show para enviar os dados reais para o espaço do usuário. O protótipo do método é o seguinte:

int show(struct seq_file *sfile, void *v);

Este método deve construir a saída para o item apontado pelo iterador. No entanto, ele não pode usar a função printk, mas um conjunto especial de funções para a saída seq_file:

int seq_printf(struct seq_file *sfile, const char *fmt, ...);

Esta é a função equivalente a printf implementada por seq_file; ela usa a string de formato usual mais um argumento de valor adicional. Ao mesmo tempo, também precisamos passar a estrutura seq_file passada pela função show para esta função . Se seq_printf retornar um valor diferente de zero, isso significa que o buffer estava cheio e a saída foi descartada . A maioria das implementações ignora esse valor de retorno.

int seq_putc(struct seq_file *sfile, char c);
int seq_puts(struct seq_file *sfile, const char *s);

Essas duas funções são equivalentes às funções putc e puts comumente usadas no espaço do usuário.

int seq_escape(struct seq_file *m, const char *s, const char *esc);

Esta função é equivalente a seq_puts, exceto que se um caractere em s também existir em esc, o caractere será impresso na forma octal . Um valor comum passado para o parâmetro esc é "\t\n\ ", que evita a saída de caracteres de espaço em branco que atravancam a tela ou confundem os scripts shell1.

int seq_path(struct seq_file *sfile, struct vfsmount *m, struct dentry *dentry, char *esc);

Esta função pode ser usada para gerar o nome do arquivo associado a uma entrada de diretório. É de pouco valor para um driver de dispositivo e está incluído aqui para fins de integridade.

No nosso caso, o código do método show usado no scull é o seguinte:

static int scull_seq_show(struct seq_file *s, void *v)
{
    
    
	struct scull_dev *dev = (struct scull_dev *) v;
	struct scull_qset *d;
	int i;
	if (down_interruptible (&dev->sem))
		return -ERESTARTSYS;
	seq_printf(s, "\nDevice %i: qset %i, q %i, sz %li\n", (int)(dev - scull_devices), dev->qset,
	dev->quantum, dev->size);
	for (d = dev->data; d; d = d->next) {
    
     /* scan the list */
		seq_printf(s, " item at %p, qset at %p\n", d, d->data);
		if (d->data && !d->next) /* dump only the last item */
		for (i = 0; i < dev->qset; i++) {
    
    
			if (d->data[i])
			seq_printf(s, " % 4i: %8p\n",
			i, d->data[i]);
		}
	}
	up(&dev->sem);
	return 0;
}

Aqui, finalmente interpretamos nosso próprio valor "iterador", que na verdade é um ponteiro para uma estrutura scull_dev.

Agora que definimos as funções completas de manipulação do iterador, o scull deve empacotar e concatenar essas funções com um arquivo em /proc. Primeiro preencha uma estrutura seq_operations:

static struct seq_operations scull_seq_ops = {
    
    
	.start = scull_seq_start,
	.next = scull_seq_next,
	.stop = scull_seq_stop,
	.show = scull_seq_show
};

Com essa estrutura instalada, temos que criar uma implementação de arquivo que o kernel entenda. Ao usar seq_file, em vez de usar o método read_proc descrito anteriormente, é melhor conectar-se a /proc em um nível ligeiramente inferior. Ou seja, criaremos uma estrutura file_operations (a mesma estrutura usada para o driver de caracteres), que implementará todas as operações que o kernel precisa ler e localizar neste arquivo /proc. Felizmente, o processo é bastante simples. Primeiro crie um método aberto que concatene o arquivo para a operação seq_file:

static int scull_proc_open(struct inode *inode, struct file *file)
{
    
    
	return seq_open(file, &scull_seq_ops);
}

A chamada para seq_open concatena a estrutura do arquivo com a operação de sequência que definimos acima. open é a única operação de arquivo que deve ser implementada por nós, portanto, nossa estrutura file_operations pode ser definida da seguinte maneira.

static struct file_operations scull_proc_ops = {
    
    
	.owner = THIS_MODULE,
	.open = scull_proc_open,
	.read = seq_read,
	.llseek = seq_lseek,
	.release = seq_release
};

Aqui, especificamos nosso próprio método aberto, mas para outros membros file_operations, usamos os métodos seq_read, seq lseek e seq_release já definidos.

Por fim, criamos o arquivo /proc real:

entry = create_proc_entry("scullseq", 0, NULL);
if (entry)
	entry->proc_fops = &scull_proc_ops;

Desta vez, em vez de usar a função create_proc_read_entry, usamos o create_proc_entry de baixo nível, cujo protótipo é definido da seguinte forma:

struct proc_dir_entry *create_proc_entry(const char *name,mode_t mode,struct proc_dir_entry *parent);

Os parâmetros desta função são equivalentes à entrada create_procread, que são o nome do arquivo (name), a máscara de proteção de acesso (modo) e o diretório pai.

Com o código acima, o scull agora possui um arquivo em /proc semelhante à versão anterior. Mas, obviamente, esse arquivo é mais flexível, não importa o tamanho da saída, ele pode lidar com o posicionamento do arquivo corretamente e o código relacionado é mais fácil de ler e manter . Se o arquivo /proc do leitor contiver um grande número de linhas de saída, recomendamos usar a interface seq_file para implementar o arquivo.

2. método ioctl

iocil é uma chamada de sistema que opera em descritores de arquivo. ioctl recebe um número de "comando" e outro parâmetro (opcional), o número do comando é usado para identificar o comando a ser executado, e o parâmetro opcional geralmente é um ponteiro . Como alternativa ao sistema de arquivos /proc, podemos criar vários comandos ioctl especificamente para depuração. Esses comandos copiam os dados relevantes do driver para o espaço do usuário, onde os dados podem ser verificados.

Quarto, por meio de monitoramento e depuração

Existem muitas maneiras de monitorar as condições de trabalho dos programas de espaço do usuário, como usar um depurador para rastrear suas funções passo a passo, inserir instruções de impressão ou executar programas no estado strace e assim por diante. A última técnica merece mais atenção ao examinar o código do kernel.

O comando strace é uma ferramenta muito poderosa que exibe todas as chamadas de sistema feitas por programas de espaço do usuário . Ele pode exibir não apenas chamadas, mas também parâmetros de chamada e valores de retorno em forma simbólica. Quando a chamada do sistema falha, o valor simbólico incorreto (como ENOMEM) e a string correspondente (como "estouro de memória sem memória") podem ser exibidos. strace tem muitas opções de linha de comando, as mais úteis são as seguintes:

  • -t, esta opção é usada para exibir a hora em que ocorreu a chamada;
  • -T, exibe o tempo gasto na chamada;
  • -e, limitar o tipo pragmático a ser rastreado;
  • -0, redireciona a saída para um arquivo

Por padrão, strace imprime informações de rastreamento para stderr.

Aqui estão as últimas linhas de saída do comando strace ls /dev > /dev/scull0:

open("/dev", O_RDONLY|O_NONBLOCK|O_LARGEFILE|O_DIRECTORY) = 3
fstat64(3, {
    
    st_mode=S_IFDIR|0755, st_size=24576, ...}) = 0
fcntl64(3, F_SETFD, FD_CLOEXEC) = 0
getdents64(3, /* 141 entries */, 4096) = 4088
[...]
getdents64(3, /* 0 entries */, 4096) = 0
close(3) = 0
[...]
fstat64(1, {
    
    st_mode=S_IFCHR|0664, st_rdev=makedev(254, 0), ...}) = 0
write(1, "MAKEDEV\nadmmidi0\nadmmidi1\nadmmid"..., 4096) = 4000
write(1, "b\nptywc\nptywd\nptywe\nptywf\nptyx0\n"..., 96) = 96
write(1, "b\nptyxc\nptyxd\nptyxe\nptyxf\nptyy0\n"..., 4096) = 3904
write(1, "s17\nvcs18\nvcs19\nvcs2\nvcs20\nvcs21"..., 192) = 192
write(1, "\nvcs47\nvcs48\nvcs49\nvcs5\nvcs50\nvc"..., 673) = 673
close(1) = 0
exit_group(0) = ?

Aparentemente, quando ls termina de recuperar o diretório de destino, ele tenta gravar 4 KB de dados em sua primeira chamada para gravação. Estranhamente (para ls), apenas 4000 bytes são realmente gravados e, em seguida, ele repete a operação. No entanto, sabemos que a implementação de gravação do scull grava no máximo um quantum por vez (o tamanho do quantum definido no scull é de 4.000 bytes) , portanto, o que esperamos é a gravação parcial descrita acima. Depois de algumas etapas, tudo passa sem problemas e o programa sai normalmente.

Aqui está outro exemplo, vamos ler do dispositivo scull (usando o comando wc)

[...]
open("/dev/scull0", O_RDONLY|O_LARGEFILE) = 3
fstat64(3, {
    
    st_mode=S_IFCHR|0664, st_rdev=makedev(254, 0), ...}) = 0
read(3, "MAKEDEV\nadmmidi0\nadmmidi1\nadmmid"..., 16384) = 4000
read(3, "b\nptywc\nptywd\nptywe\nptywf\nptyx0\n"..., 16384) = 4000
read(3, "s17\nvcs18\nvcs19\nvcs2\nvcs20\nvcs21"..., 16384) = 865
read(3, "", 16384) = 0
fstat64(1, {
    
    st_mode=S_IFCHR|0620, st_rdev=makedev(136, 1), ...}) = 0
write(1, "8865 /dev/scull0\n", 17) = 17
close(3) = 0
exit_group(0) = ?

Como esperávamos, read pode ler apenas 4.000 bytes por vez, mas a quantidade total de dados é a mesma que a quantidade total gravada no exemplo anterior.

strace é mais útil para encontrar bugs sutis no tempo de execução das chamadas do sistema. Normalmente, as informações de chamada perror em um aplicativo ou programa de demonstração não são detalhadas o suficiente para depuração, mas o strace pode ser muito útil para depuração, sendo capaz de identificar exatamente qual parâmetro da chamada do sistema causou o erro .

5. Depurar falha do sistema

Mesmo com todas essas técnicas de monitoramento e depuração, às vezes há bugs no driver e esse driver causará uma falha no sistema quando executado.

Observe que "falha" não significa "pânico". O código do Linux é muito robusto e responde bem à maioria dos erros: uma falha geralmente trava o processo atual, enquanto o sistema continua funcionando . O sistema só pode entrar em pânico se ocorrer uma falha fora do contexto do processo ou se uma parte crítica do sistema for comprometida. Mas se o problema estiver no driver, geralmente apenas faz com que o próprio processo que está usando o driver seja encerrado abruptamente. A única perda irrecuperável é que alguma memória alocada para o contexto do processo pode ser perdida quando o processo é finalizado , por exemplo, a lista encadeada dinâmica alocada pelo driver via kmalloc pode ser perdida. No entanto, como o kernel chamará close no dispositivo aberto quando o processo terminar, o driver ainda poderá liberar os recursos alocados pelo método open.

1. ops mensagem

A maioria dos erros é causada pela desvalorização de ponteiros NULL ou pelo uso de outros valores de ponteiro incorretos. Esses erros geralmente resultam em uma mensagem de oops.

由处理器使用的地址几乎都是虚拟地址,这些地址(除了内存管理子系统本身所使用的物理内存之外)通过一个复杂的被称为“页表”的结构被映射为物理地址。当引用一个非法指针时,分页机制无法将该地址映射到物理地址,此时处理器就会向操作系统发出一个“页面失效 (page fault)”的信号。如果地址非法,内核就无法“换入 (page in)缺失页面;这时,如果处理器恰好处于超级用户模式,系统就会产生一个 oops。

oops exibe o estado do processador no momento do erro, como o conteúdo dos registradores da CPU e outras informações aparentemente incompreensíveis. Essas mensagens são geradas pelas instruções printk nos manipuladores de falhas (arch/*/kernel/traps.c) e são tratadas conforme descrito acima na seção "printk".

Vamos ver um exemplo de mensagens oops. Quando usamos um ponteiro NULL em um PC executando o kernel 2.6, isso fará com que as seguintes informações sejam exibidas. A informação mais relevante aqui é o ponteiro de instrução (EIP), o endereço da instrução com falha .

Unable to handle kernel NULL pointer dereference at virtual address 00000000
printing eip:
d083a064
Oops: 0002 [#1]
SMP
CPU: 0
EIP: 0060:[<d083a064>] Not tainted
EFLAGS: 00010246 (2.6.6)
EIP is at faulty_write+0x4/0x10 [faulty]
eax: 00000000 ebx: 00000000 ecx: 00000000 edx: 00000000
esi: cf8b2460 edi: cf8b2480 ebp: 00000005 esp: c31c5f74
ds: 007b es: 007b ss: 0068
Process bash (pid: 2086, threadinfo=c31c4000 task=cfa0a6c0)
Stack: c0150558 cf8b2460 080e9408 00000005 cf8b2480 00000000 cf8b2460 cf8b2460
fffffff7 080e9408 c31c4000 c0150682 cf8b2460 080e9408 00000005 cf8b2480
00000000 00000001 00000005 c0103f8f 00000001 080e9408 00000005 00000005
Call Trace:
[<c0150558>] vfs_write+0xb8/0x130
[<c0150682>] sys_write+0x42/0x70
[<c0103f8f>] syscall_call+0x7/0xb
Code: 89 15 00 00 00 00 c3 90 8d 74 26 00 83 ec 0c b8 00 a6 83 d0

Esta mensagem foi gerada por uma gravação em um dispositivo no módulo defeituoso, que foi gravado para demonstrar erros. A implementação do método write em failed.c é simples:

ssize_t faulty_write (struct file *filp, const char __user *buf, size_t count, loff_t *pos)
{
    
    
	/* make a simple fault by dereferencing a NULL pointer */
	*(int *)0 = 0;
	return 0;
}

Um ponteiro NULL é referenciado aqui. Como 0 nunca é um valor de ponteiro válido, ocorre um erro e o kernel entra no estado de mensagem oops acima. O processo de chamada é então encerrado.

Na implementação de leitura do módulo defeituoso, o módulo também exibe estados de erro mais interessantes:

ssize_t faulty_read(struct file *filp, char __user *buf, size_t count, loff_t *pos)
{
    
    
	int ret;
	char stack_buf[4];
	/* Let's try a buffer overflow */
	memset(stack_buf, 0xff, 20);
	if (count > 4)
		count = 4; /* copy 4 bytes to the user */
	ret = copy_to_user(buf, stack_buf, count);
	if (!ret)
		return count;
	return ret;
}

Este método copia uma string para uma variável local, mas, infelizmente, a string é maior que a matriz de destino. Isso causará um oops devido a um estouro de buffer quando a função retornar. No entanto, como a instrução de retorno leva o ponteiro de instrução para um local inesperado, esse tipo de erro é difícil de rastrear e a única informação que pode ser obtida é a seguinte:

EIP: 0010:[<00000000>]
Unable to handle kernel paging request at virtual address ffffffff
printing eip:
ffffffff
Oops: 0000 [#5]
SMP
CPU: 0
EIP: 0060:[<ffffffff>] Not tainted
EFLAGS: 00010296 (2.6.6)
EIP is at 0xffffffff
eax: 0000000c ebx: ffffffff ecx: 00000000 edx: bfffda7c
esi: cf434f00 edi: ffffffff ebp: 00002000 esp: c27fff78
ds: 007b es: 007b ss: 0068
Process head (pid: 2331, threadinfo=c27fe000 task=c3226150)
Stack: ffffffff bfffda70 00002000 cf434f20 00000001 00000286 cf434f00 fffffff7
bfffda70 c27fe000 c0150612 cf434f00 bfffda70 00002000 cf434f20 00000000
00000003 00002000 c0103f8f 00000003 bfffda70 00002000 00002000 bfffda70
Call Trace: [<c0150612>] sys_read+0x42/0x70 [<c0103f8f>] syscall_call+0x7/0xb
Code: Bad EIP value.

Nesse caso, podemos ver apenas parte da chamada (vfs_read e failed_read não podem ser vistos) e o kernel reclama que um "valor de EIP inválido" foi encontrado. Esta reclamação, juntamente com o endereço aparentemente errado (ffffffff) listado no início, indica que a pilha do kernel foi corrompida.

Normalmente, quando nos deparamos com um oops, a primeira coisa a observar é a localização do problema, que geralmente pode ser obtida através das informações da pilha de chamadas . Nos primeiros oops dados acima, as informações relevantes são:

EIP is at faulty_write+0x4/0x10 [faulty]

A partir daqui podemos ver que a função com falha é faulty_write, que está localizada no módulo com falha (listado entre colchetes). Os dados hexadecimais mostram que o ponteiro de instrução tem 4 bytes na função, enquanto a própria função tem 10 (hexadecimais) bytes de comprimento . Normalmente, essas informações são suficientes para nos permitir ver qual é realmente o problema.

Se forem necessárias mais informações, a pilha de chamadas pode nos dizer como o sistema chegou ao ponto de falha. A própria pilha é impressa em hexadecimal e, com um pouco de trabalho, podemos determinar os valores das variáveis ​​locais e parâmetros de função da listagem da pilha. Desenvolvedores de kernel experientes podem efetivamente detectar problemas com tais padrões. Por exemplo, se observarmos a listagem da pilha dos oops gerados por failed_read:

Stack: ffffffff bfffda70 00002000 cf434f20 00000001 00000286 cf434f00 fffffff7
	   bfffda70 c27fe000 c0150612 cf434f00 bfffda70 00002000 cf434f20 00000000
	   00000003 00002000 c0103f8f 00000003 bfffda70 00002000 00002000 bfffda70

O ffffffff no topo da pilha faz parte da string que causou a falha. Na arquitetura x86, a pilha no espaço do usuário é padronizada de 0xc0000000 para baixo. Portanto, é fácil pensar que 0xbfffda70 pode ser o endereço da pilha do espaço do usuário, ou seja, o endereço do buffer passado para a chamada do sistema read, e esse endereço será passado repetidamente na cadeia de chamadas do kernel. Na arquitetura x86 (ainda o padrão), o espaço do kernel começa em 0xc0000000, então valores maiores que 0xc0000000 são quase certamente endereços no espaço do kernel , e assim por diante.
insira a descrição da imagem aqui
Finalmente, ao olhar para a lista de oops, lembre-se também de olhar para o valor "veneno da laje" discutido anteriormente neste capítulo. Por exemplo, se obtivermos oops do kernel contendo endereços como 0xa5a5a5a5, quase certamente esquecemos de inicializar a memória alocada dinamicamente em algum lugar.
需要注意的是,只有在构造内核时打开了 CONFIG_KALLSYMS 选项,我们才能看到符号化的调用栈(就像上面列出的那样);否则,我们只能看到裸的、十六进制的清单,因而只有通过其他途径解开这些数字的含义,才能弄清楚真正的调用栈。

2. O sistema trava

Embora a maioria dos bugs no código do kernel resulte em nada mais do que uma mensagem de oops, às vezes eles travam o sistema completamente.Se o sistema travar, nenhuma mensagem pode ser impressa .

Os loops infinitos podem ser evitados inserindo chamadas de agendamento em pontos-chave . A função de agendamento (como o leitor adivinhou) invoca o agendador e, portanto, permite que outros processos "roubem" o tempo de CPU do processo atual. Se o processo estiver preso em um loop infinito no espaço do kernel devido a um erro de driver, você pode encerrar o processo com a ajuda de uma chamada de agendamento após rastrear essa situação.

Às vezes parece que o sistema está travado, mas não está. Isso pode acontecer, por exemplo, se o teclado estiver bloqueado por algum motivo estranho. Neste ponto, execute um programa projetado para detectar isso e observe sua saída para identificar esse travamento falso. Um relógio no display ou um medidor de carga do sistema são bons monitores de status, desde que esses programas sejam mantidos atualizados, o agendador ainda está funcionando.

Para os cenários acima, uma ferramenta indispensável é a "chave mágica SysRq (chave mágica SysRg)", que pode ser usada na maioria das arquiteturas . A magia SysRq pode ser ativada através da combinação de ALT e SysRq (à direita da tecla F12) no teclado do PC e através de outras teclas especiais em outras plataformas (ver Documentação/sysrq.txt para detalhes). o console serial. Dependendo de qual terceira tecla é pressionada junto com essas duas teclas, o kernel executa uma das muitas ações úteis, como segue:

  • r: Desligue o modo raw do teclado. Quando um aplicativo travado (como um servidor X) deixa o teclado em um estado estranho, use esta tecla para desativar o modo bruto.
  • k: ativa a função "chave de atenção segura (SAK)". O SAK matará todos os processos atualmente em execução no console, deixando um terminal limpo.
  • s: Execute a sincronização urgente de todos os discos.
  • u: Tenta remontar todos os discos no modo somente leitura. Essa operação geralmente é chamada imediatamente após a ação s e pode economizar muito tempo verificando o sistema de arquivos quando o sistema está em um estado de falha grave.
  • b: Reinicie o sistema imediatamente. Observe que você deve executar uma sincronização e remontar o disco primeiro.
  • p: Imprime as informações do registro do processador atual.
  • t: Imprima a lista de tarefas atual.
  • m: Imprimir informações de memória.

A funcionalidade SysRq deve ser habilitada explicitamente na configuração do kernel, mas para um sistema usado para desenvolvimento de driver, o problema de recompilar um novo kernel para habilitar a funcionalidade SysRq vale a pena. Quando o sistema está rodando, a função SysRq pode ser habilitada pelo seguinte comando:

echo 0 > /proc/sys/kernel/sysrq

Como as funções do SysRq são tão úteis, elas também estão abertas a administradores de sistema que não têm acesso ao console. /proc/sysrq-trigger é um ponto de entrada /proc somente para gravação. Grave os caracteres correspondentes neste arquivo para acionar a ação SysRq correspondente . Este ponto de entrada para SysRq está sempre disponível, mesmo se SysRq estiver desabilitado no console.

Outra precaução a ser tomada ao reproduzir uma falha de travamento do sistema é montar todos os discos somente leitura no sistema (ou simplesmente desmontá-los) . Se o disco for somente leitura ou desmontado, não há risco de corromper o sistema de arquivos ou deixá-lo em um estado inconsistente. Outro método viável é instalar todos os sistemas de arquivos por meio do NFS (sistema de arquivos de rede). Este método requer que o kernel tenha "capacidade NFS-Root, e alguns parâmetros específicos precisam ser passados ​​durante a inicialização .

6. Depurador e ferramentas relacionadas

1. Use gdb

O kernel deve ser visto como um aplicativo ao iniciar o depurador. Além de especificar o nome do arquivo de imagem do kernel descompactado, o nome do "arquivo principal" também deve ser fornecido na linha de comando . Para um kernel em execução, o chamado arquivo principal é a imagem principal do kernel na memória, ou seja, /proc/kcore. Uma chamada gdb típica se parece com isto:

gdb /usr/src/linux/vmlinux /proc/kcore

O primeiro argumento é o nome do executável ELF do kernel descompactado , não zlmage ou bzlmage ou qualquer outra imagem de kernel especial criada para um ambiente de inicialização específico.

**O segundo parâmetro da linha de comando gdb é o nome do arquivo principal. Como outros arquivos em /proc, /proc/kcore é criado quando é lido. ** Quando a chamada do sistema read é executada no sistema de arquivos /proc, ela é mapeada para uma função de geração de dados em vez de leitura de dados; No uso do gdb, você pode visualizar as variáveis ​​do kernel por meio de comandos padrão do gdb. Por exemplo, o comando p jiffies imprime o número de tiques do relógio desde a inicialização do sistema até o momento atual.

Ao imprimir dados do gdb, o kernel ainda está em execução e os valores de diferentes itens de dados serão alterados em momentos diferentes; no entanto, o gdb armazenará em cache os dados lidos para otimizar o acesso ao arquivo principal. Se você observar a variável jiffies novamente, ainda obterá o mesmo valor da última vez . Para arquivos principais comuns, é correto armazenar em cache os valores das variáveis ​​para evitar o acesso extra ao disco. **Mas é inconveniente para arquivos principais "dinâmicos". A solução é executar o comando core-file /proc/kcore sempre que o cache gdb precisar ser liberado; o depurador usará o novo arquivo principal e descartará todas as informações antigas. **No entanto, nem sempre é necessário executar o comando core-file ao ler novos dados, porque o gdb lê o arquivo principal em pequenos blocos de dados de alguns KB de tamanho e apenas alguns pequenos blocos que foram referenciados são armazenados em cache .

Muitos recursos comuns do gdb não estão disponíveis durante a depuração do kernel. Por exemplo, o gdb não pode modificar os dados do kernel porque o gdb espera que o programa seja depurado para executar sob seu controle antes de processar sua imagem de memória. Da mesma forma, não podemos definir breakpoints ou watchpoints, ou passo único através de funções do kernel.
注意,为了让 gdb 使用内核的符号信息,我们必须在打开 CONFIG_DEBUG_INFO 选项的情况下编译内核。其结果将产生一个非常大的内核映像,但若没有符号信息,观察内核变量的目的基本上无法完成。

Um módulo carregável do Linux é uma imagem executável no formato ELF e o módulo é dividido em vários segmentos de código . Um módulo típico pode conter uma dúzia ou mais de segmentos de código, mas para uma sessão de depuração, os únicos segmentos de código relevantes são os três seguintes:

  • .texto
    • Esta seção de código contém o código executável do módulo. O depurador deve saber a localização do segmento de código para fornecer informações de rastreamento ou definir pontos de interrupção (quando executamos o depurador em /proc/kcore, nenhuma dessas operações pode ser realizada, mas se usarmos o kgdb mencionado abaixo, essas duas operações serão muito útil).
  • .bss
  • .dados
    • Esses dois trechos de código contêm as variáveis ​​do módulo. Quaisquer variáveis ​​não inicializadas em tempo de compilação são mantidas na seção .bss, enquanto outras variáveis ​​inicializadas são mantidas na seção .data.

Para que o gdb seja capaz de processar um módulo carregável, o depurador deve ser informado exatamente onde a seção de código do módulo carregado está localizada. Essas informações estão disponíveis em /sysfs/module em sysfs . Por exemplo, após o módulo scull ser carregado, o diretório /sys/module/scull/sections conterá arquivos com nomes como .text, e o conteúdo desses arquivos são os endereços base das seções de código correspondentes.

Agora é possível informar ao depurador sobre o módulo com um único comando gdb. Este comando é add-symbol-file, que requer o nome do arquivo de objeto do módulo, o endereço base da seção .text e outras opções que descrevem outras informações necessárias da seção de código como parâmetros . Após obter os dados do segmento de código do módulo através do sysfs, podemos construir este comando da seguinte forma:

(gdb) add-symbol-file .../scull.ko 0xd0832000 \
-s .bss 0xd0837100 \
-s .data 0xd0836be0

Depois disso, o gdb pode ser usado para inspecionar as variáveis ​​no módulo carregável. Aqui está um exemplo de uma sessão de depuração scull:

(gdb) add-symbol-file scull.ko 0xd0832000 \
-s .bss 0xd0837100 \
-s .data 0xd0836be0
add symbol table from file "scull.ko" at
.text_addr = 0xd0832000
.bss_addr = 0xd0837100
.data_addr = 0xd0836be0
(y or n) y
Reading symbols from scull.ko...done.
(gdb) p scull_devices[0]
$1 = {
    
    data = 0xcfd66c50,
quantum = 4000,
qset = 1000,
size = 20881,
access_key = 0,
...}

Pode ser visto no exemplo acima que o primeiro dispositivo scull contém atualmente 20881 bytes de dados. Também podemos seguir o link de dados, se quisermos, ou consultar qualquer outro dado de interesse no módulo. Outra habilidade que vale a pena dominar é:

(gdb)print *(address)

Aqui, um endereço hexadecimal apontado por address é preenchido, e a saída é o arquivo e o número da linha do código correspondente a esse endereço.Esta técnica pode ser útil, por exemplo, para descobrir para onde realmente aponta um ponteiro de função.

2. depurador de kernel kdb

Linus não confia em depuradores interativos. Ele estava preocupado que esses depuradores levassem a algumas modificações indesejadas, então ele não suportava ter um depurador embutido no kernel. No entanto, outros desenvolvedores de kernel ocasionalmente usam ferramentas de depuração interativas. Um desses depuradores de kernel embutidos é o kdb, que está disponível como um patch não oficial em oss.sgi.com.

Assim que o kernel que suporta o kdb estiver em execução, você pode usar os seguintes métodos para entrar no estado de depuração do kdb. Pressionar a tecla Pause (ou Break) no console iniciará a depuração. Quando ocorre oops no kernel ou quando um ponto de interrupção é atingido, o kdb também é iniciado. Em ambos os casos, você verá uma mensagem como esta:

Entering kdb (0xc0347b80) on processor 0 due to Keyboard Entry
[0]kdb>

Observe que tudo o que o kernel faz é interrompido enquanto o kdb está em execução.

Como exemplo, considere o seguinte procedimento rápido de depuração de scull. Supondo que o driver já esteja carregado, você pode instruir o kdb a definir um ponto de interrupção na função de leitura do scull como este:

[0]kdb> bp scull_read
Instruction(i) BP #0 at 0xcd087c5dc (scull_read)
is enabled globally adjust 1
[0]kdb> go

O comando bp instrui o kdb a parar de executar na próxima vez que o kernel inserir scull_read. Então entramos em go para continuar a execução. Depois de colocar algo em um dos dispositivos do scull, podemos executar o comando cat no shell de outro terminal para tentar ler o dispositivo, o que resultaria no seguinte estado:

Instruction(i) breakpoint #0 at 0xd087c5dc (adjusted)
0xd087c5dc scull_read: int3
Entering kdb (current=0xcf09f890, pid 1575) on processor 0 due to
Breakpoint @ 0xd087c5dc
[0]kdb>

Estamos agora no início do scull_read. Para descobrir como chegamos lá, podemos olhar para o rastreamento de pilha:

[0]kdb> bt
ESP EIP Function (args)
0xcdbddf74 0xd087c5dc [scull]scull_read
0xcdbddf78 0xc0150718 vfs_read+0xb8
0xcdbddfa4 0xc01509c2 sys_read+0x42
0xcdbddfc4 0xc0103fcf syscall_call+0x7
[0]kdb>

kdb tenta imprimir a lista de argumentos para cada função registrada pelo rastreamento de chamada. No entanto, ele tende a se confundir com os truques de otimização usados ​​pelo compilador. Portanto, ele não pode imprimir os argumentos de scull_read corretamente.

Vamos dar uma olhada em como consultar dados. O comando mds é usado para processar dados ; podemos usar o seguinte comando para consultar o valor do ponteiro scull_devices:

[0]kdb> mds scull_devices 1
0xd0880de8 cf36ac00 ....

Aqui, estamos olhando para um tamanho de palavra (4 bytes) de dados começando no local do ponteiro scull_devices; o resultado desse comando nos diz que a matriz do dispositivo começa em 0xd0880de8 e a própria estrutura do primeiro dispositivo está em 0xcf36ac00 . Para ver os dados na estrutura do dispositivo, precisamos usar este endereço:

[0]kdb> mds cf36ac00
0xcf36ac00 ce137dbc ....
0xcf36ac04 00000fa0 ....
0xcf36ac08 000003e8 ....
0xcf36ac0c 0000009b ....
0xcf36ac10 00000000 ....
0xcf36ac14 00000001 ....
0xcf36ac18 00000000 ....
0xcf36ac1c 00000001 ....

As 8 linhas de dados acima correspondem aos dados iniciais na estrutura scull_dev, respectivamente. Dessa forma, por meio desses dados, podemos saber que a memória do primeiro dispositivo é alocada de 0xce137dbc, o tamanho quântico é de 4000 (fa0 em formato hexadecimal) bytes e o tamanho do conjunto quântico é de 1000 (3e8 em formato hexadecimal). Este dispositivo contém 155 (9b em hexadecimal) bytes de dados, etc.

kdb também pode modificar dados. Digamos que queremos cortar alguns dados do dispositivo:

[0]kdb> mm cf26ac0c 0x50
0xcf26ac0c = 0x50

As operações cat subseqüentes no dispositivo retornarão menos dados do que a anterior.
kdb tem muitos outros recursos, incluindo instruções passo a passo (instruções, não linhas de código-fonte C), definição de pontos de interrupção em acessos a dados, desmontagem de código, rastreamento de listas vinculadas, acesso a dados de registro e muito mais. As páginas man completas relacionadas ao kdb podem ser encontradas no diretório Documentation/kdb da árvore fonte do kernel após a aplicação do patch kdb.


Meu qq: 2442391036, bem-vindo ao comunicar!


Acho que você gosta

Origin blog.csdn.net/qq_41839588/article/details/131615583
Recomendado
Clasificación