Panics de Kernel e Stack Backtraces
Um kernel panic é o que um kernel faz quando decide, deliberadamente, que não há forma segura de continuar: uma asserção que nunca deveria falhar falhou, uma estrutura de dados da qual o kernel depende está comprovadamente corrompida, ou uma exceção chegou sem recuperação sã alguma (uma falha de página dentro do próprio tratador de falha de página, por exemplo). Vetores de exceção reservados explica o que dispara muitas das falhas subjacentes; este artigo cobre o que acontece uma vez que o kernel decidiu que uma delas, ou uma checagem interna, justifica parar por completo em vez de tentar seguir em frente.
O que uma rotina de panic reporta
Seção intitulada “O que uma rotina de panic reporta”Os registradores de propósito geral no momento da falha, capturados do frame de interrupção se o panic se origina de um tratador de exceção ou do contexto chamador de outra forma, são a primeira coisa que vale a pena preservar, já que o que quer que tenha corrompido um ponteiro ou calculado mal um índice frequentemente ainda está visível no estado de registrador momentos depois do fato mesmo quando já se foi da memória no momento em que alguém olha. Uma mensagem curta e específica identificando o que deu errado (qual asserção falhou, qual exceção disparou e em qual endereço) importa mais do que pode parecer: uma rotina de panic que só imprime “kernel panic” e nada mais transforma todo crash futuro numa investigação nova do zero, onde uma que nomeia a condição que falhou frequentemente aponta direto para o código responsável. O terceiro elemento, um backtrace, é o que o resto deste artigo cobre em detalhe, já que reconstruí-lo corretamente é a parte menos óbvia dos três.
Reconstruindo um backtrace a partir de frame pointers
Seção intitulada “Reconstruindo um backtrace a partir de frame pointers”Um backtrace lista a cadeia de chamadas de função ativas no momento da falha, e a forma mais barata de reconstruir uma não precisa de informação de depuração alguma embutida no binário: ela percorre uma lista ligada de frame pointers salvos já presente na stack, deixada ali pelo prólogo padrão de entrada de função que toda função não-folha executa.
; prólogo padrão de função x86-64push rbpmov rbp, rspComo esse prólogo empilha o RBP do chamador antes de estabelecer o próprio, RBP em qualquer ponto durante a execução de uma função não-folha aponta para um local na stack contendo o RBP do frame anterior, formando uma lista ligada simples entrelaçada para trás através de toda chamada atualmente ativa, um link por frame de stack, até o exato primeiro frame.
void print_backtrace(uint64_t rbp) { while (rbp) { uint64_t return_addr = *(uint64_t *)(rbp + 8); // endereço de retorno fica logo acima do RBP salvo printf(" at %#lx\n", return_addr); rbp = *(uint64_t *)rbp; // segue a cadeia até o frame do chamador }}Todo frame também tem o endereço de retorno da função chamadora sentado imediatamente acima do RBP salvo na stack (empilhado ali pela própria instrução call, antes do prólogo da função chamada rodar), então percorrer a cadeia e ler esse offset fixo em cada link produz uma lista completa de endereços, um por chamada ativa, que um kernel pode imprimir crua ou resolver contra sua própria tabela de símbolos se uma estiver embutida no binário. Essa técnica inteira depende do compilador de fato manter a cadeia de RBP: -fomit-frame-pointer, um padrão em níveis de otimização mais altos em algumas toolchains, reaproveita RBP como um registrador de propósito geral comum em vez disso, quebrando silenciosamente a travessia, motivo pelo qual código de kernel que pretende suportar esse tipo de backtrace geralmente compila com -fno-omit-frame-pointer explicitamente em vez de confiar no padrão do próprio compilador.
Travando toda CPU, não só a que causou o panic
Seção intitulada “Travando toda CPU, não só a que causou o panic”Num sistema de núcleo único, travar a execução depois de reportar é direto: desabilitar interrupções e entrar num loop infinito, ou executar hlt num loop, já que nada mais na máquina poderia ainda estar rodando de qualquer forma. Num sistema multiprocessador isso não é suficiente: toda outra CPU continua executando código de kernel, potencialmente o exato caminho de código que produziu a corrupção que o panic está reportando, exatamente durante e depois do momento em que uma CPU decide parar, o que tanto torna o estado reportado desatualizado em instantes quanto arrisca um segundo panic, aparentemente não relacionado, em outro núcleo disparado pela mesma corrupção subjacente. Uma rotina de panic em SMP portanto precisa enviar uma interrupção entre processadores para todo outro núcleo especificamente para forçá-lo a travar também, antes que o sistema como um todo esteja num estado de fato seguro para inspecionar (via serial ou sob um depurador): só depois que todo núcleo parou de tocar memória o snapshot que um panic reporta de fato descreve um sistema que parou de mudar por baixo de quem está lendo.
Notas de implementação
Seção intitulada “Notas de implementação”Uma rotina de panic está, quase por definição, rodando no estado menos confiável em que um kernel algum dia está, o que impõe limites reais sobre o que ela pode fazer com segurança: alocar memória, adquirir um lock que outra CPU já poderia estar segurando (a própria corrupção sendo reportada pode ser exatamente o que está impedindo esse lock de algum dia ser liberado), ou chamar de volta caminhos de código comuns do kernel todos arriscam transformar um panic diagnosticável num segundo panic, menos informativo, antes mesmo do relatório do primeiro terminar de ser escrito. As implementações de panic mais seguras escrevem diretamente num framebuffer ou porta serial usando o caminho de código mais pequeno e autocontido disponível, evitando deliberadamente as próprias rotinas de formatação ou gerenciamento de memória de propósito geral do kernel, precisamente porque essas rotinas são exatamente o que já pode estar quebrado.
Referências
Seção intitulada “Referências”- ^ Documentação do kernel Linux, Kernel Oops: descreve o formato de relatório análogo e não fatal ao qual o mecanismo de panic coberto neste artigo é intimamente relacionado.
Ver também
Seção intitulada “Ver também”- A IDT: os vetores de exceção cujos tratadores mais comumente decidem causar um panic.
- Emulating & Debugging: a ferramenta de fato usada para inspecionar o estado que um panic reporta, particularmente uma vez que o GDB está conectado.