Registradores de Depuração e Breakpoints de Hardware
DR0 a DR3, DR6, e DR7 são os registradores de depuração da CPU, e um kernel pode programá-los diretamente, sem GDB algum ou depurador externo envolvido, para implementar seus próprios watchpoints de memória: útil para capturar uma escrita inesperada numa estrutura específica na exata instrução responsável, em vez de dar single-step pelo código atrás dela. Emulating & Debugging já cobre os mesmos quatro registradores de endereço do lado oposto, os comandos hbreak e watch do GDB, apoiados pela emulação fiel do QEMU deles.
Os quatro registradores de endereço
Seção intitulada “Os quatro registradores de endereço”DR0 a DR3 cada um contém um endereço linear para observar, dando à CPU quatro slots de breakpoint independentes ativos simultaneamente. Cada endereço é comparado contra toda busca de instrução ou acesso de memória relevante conforme acontece em hardware, sem sondagem de software nem truque de page fault envolvido, o que é o que torna um watchpoint de hardware dramaticamente mais rápido do que a alternativa de marcar uma página como não presente e capturar todo acesso a ela como uma falha de página: só o endereço específico importa, não a página inteira em que ele vive, e todo outro acesso a essa mesma página prossegue em velocidade plena, intocado.
DR7: habilitando e configurando cada slot
Seção intitulada “DR7: habilitando e configurando cada slot”DR7 controla os quatro slots de uma vez, com dois bits por slot governando se ele está ativo, e um campo adicional de dois bits por slot selecionando qual condição o dispara: 00 para execução de instrução, 01 para escrita de dado, 11 para leitura ou escrita de dado, com 10 reservado a menos que CR4.DE esteja definido, caso em que significa leitura ou escrita de I/O. Um campo separado de dois bits de comprimento por slot (00, 01, 10, 11 mapeando para 1, 2, 8, e 4 bytes respectivamente, com a codificação de 8 bytes válida só em modo de 64 bits) define a quantos bytes a partir do endereço no registrador DRn correspondente a condição se aplica, permitindo que um watchpoint cubra um campo inteiro de múltiplos bytes em vez de só seu primeiro byte.
#define DR7_LOCAL(n) (1u << (2 * (n)))#define DR7_RW(n, rw) ((rw) << (16 + 4 * (n)))#define DR7_LEN(n, len) ((len) << (18 + 4 * (n)))
// arma DR0 como watchpoint de escrita de 4 bytes, ativo só na tarefa atualuint64_t dr7 = DR7_LOCAL(0) | DR7_RW(0, 0b01) | DR7_LEN(0, 0b11);__asm__ volatile("mov %0, %%dr7" :: "r"(dr7));O bit de habilitação de cada slot na verdade vem em dois sabores, um bit local que permanece definido só até a próxima troca de tarefa e um bit global que a CPU nunca limpa sozinha: um kernel implementando watchpoints por tarefa precisa reprogramar DR7 (e o DRn correspondente) em toda troca para uma tarefa que possua um, usando a forma local, já que um breakpoint global deixado ativo de outra forma dispararia contra toda tarefa compartilhando o mesmo endereço linear para algo inteiramente não relacionado ao que o watchpoint deveria capturar.
DR6: descobrindo qual disparou
Seção intitulada “DR6: descobrindo qual disparou”Uma vez que qualquer condição armada é atendida, a CPU levanta #DB (vetor 1) e define um bit em DR6 identificando qual dos quatro slots de fato disparou, B0 a B3 correspondendo a DR0 a DR3, permitindo que um único handler compartilhado de #DB distinga entre até quatro watchpoints armados independentemente em vez de precisar de um handler separado por slot. DR6 não é limpo automaticamente pela CPU na entrada da exceção, e seus bits são limpos escrevendo zero neles explicitamente, o que o handler precisa fazer ele mesmo antes de retornar; um handler que lê DR6 mas nunca o limpa encontra os mesmos bits desatualizados ainda definidos na próxima vez que qualquer watchpoint disparar, nesse ponto incapaz de distinguir um disparo genuinamente novo de um remanescente de antes.
void db_handler(void) { uint64_t dr6; __asm__ volatile("mov %%dr6, %0" : "=r"(dr6));
if (dr6 & (1 << 0)) handle_watchpoint(0); if (dr6 & (1 << 1)) handle_watchpoint(1); // ...
__asm__ volatile("mov %0, %%dr6" :: "r"(0ULL)); // limpa antes de retornar}Notas de implementação
Seção intitulada “Notas de implementação”Ler ou escrever qualquer registrador de depuração exige ring 0, sem equivalente algum ao bitmap de permissão de I/O da TSS que permite a um kernel conceder acesso de porta seletivamente ao ring 3: um depurador em user space como o GDB nunca toca esses registradores diretamente, passando em vez disso por uma interface mediada pelo kernel (ptrace no Linux) que realiza o mov real para DRn em nome do depurador depois de checar permissões. O bit DE (Debugging Extensions) de CR4 adicionalmente precisa estar definido para que a codificação 10 do campo RW signifique um breakpoint de I/O, em vez de ficar indefinida, um detalhe relevante principalmente para breakpoints de I/O em vez dos watchpoints de dados que este artigo cobre e seguro de deixar no padrão na maior parte do código de registrador de depuração de kernel. Como um breakpoint de hardware numa busca de instrução dispara antes dessa instrução executar enquanto um watchpoint de dado dispara depois do acesso se completar, um handler para os dois casos não pode compartilhar suposições idênticas sobre qual estado já mudou no momento em que roda.
Referências
Seção intitulada “Referências”- ^ Intel, Intel 64 and IA-32 Architectures Software Developer’s Manual, Volume 3B, Capítulo 18: a referência completa de registradores de depuração, incluindo o layout de campos de
DR7e as condições de entrega de#DB.
Ver também
Seção intitulada “Ver também”- Emulating & Debugging: os mesmos registradores DR0-DR3 usados do lado do
hbreak/watchdo GDB, via emulação do QEMU. - A IDT: a tabela de vetores de exceção reservados à qual
#DBpertence. - A Task State Segment: o modelo de controle de acesso de ring 3 do qual registradores de depuração notavelmente carecem de um equivalente.