Panic, diagnóstico serial e kernel log¶
Sobre este capítulo Fronteiras de confiança e entrada de exceções
Neste capítulo
- Panic, diagnóstico serial e kernel log
- Escopo
- Serial no início do boot
- Inicialização da serial e estado de disponibilidade
- panic
- panic_exception
- Identidade
- Por que build identity importa
- Armazenamento da identidade de build e contrato de formatação
- Ring klog
- Escrita
- Leitura
- Invariantes do ring e custo das operações
- Persistência no boot
- Fan-out real entre serial e klog
- Dependência de locks no panic path
- Fault user não é panic global
- O que permanece fatal
- Concorrência
- Grafo de dependência do fatal path
- Reprodutibilidade
- Segurança
- Desempenho
- Validação
- Limitações atuais
- Mapa de fonte
Escopo¶
Logging em kernel opera justamente quando dependências comuns podem estar quebradas. Heap pode estar corrompido, filesystem ausente, gráficos falhando, interrupções inseguras e CPU em exception context. Por isso o ChrisOS mantém o fatal path pequeno e usa serial como canal de último recurso. Separadamente, mantém um ring log fixo em memória, sem alocação, que pode ser persistido em ChrisFS depois que storage fica disponível.
A arquitetura também distingue exception fatal em ring 0 de fault em processo user. A primeira paralisa a máquina; a segunda pode ser registrada e contida.
Serial no início do boot¶
kstart inicializa serial antes da maioria dos subsistemas. Se serial_init falha, executa cli e hlt indefinidamente.
Assim serial é raiz do diagnóstico. Bootinfo, self-tests, device status e panic podem aparecer sem heap, filesystem ou desktop.
Quanto menos dependências o caminho de emergência tiver, menor a chance de a própria observabilidade falhar recursivamente.
Inicialização da serial e estado de disponibilidade¶
serial_init começa inicializando o klog, depois programa a COM1 e executa um teste de loopback. A rotina grava o byte 0xAE com o modem-control register em modo de loopback e verifica se a leitura do data register retorna o mesmo valor. Em caso de falha, define serial_available = false e retorna false; no sucesso, restaura o controle normal, marca a serial como disponível e inicializa g_serial_lock.
O boot trata falha da serial como fatal antes da maior parte da inicialização do kernel: kstart executa cli e entra diretamente em loop de hlt, sem chamar o formatador normal de panic. É um failure path distinto porque o transporte usado pelo panic é justamente o subsistema cuja inicialização falhou.
Depois do sucesso, a ordem de locks por caractere é fixa:
O código serial atual não adquire esses locks na ordem inversa, evitando uma inversão AB/BA interna ao subsistema. Ainda assim, ambos são não reentrantes; entrar novamente no logging enquanto a mesma CPU já possui um deles continua inseguro.
Uma falha no probe da COM1 não impede tecnicamente serial_putc de registrar bytes no klog, porque o byte é gravado no ring antes do teste de serial_available. No fluxo normal de boot esse fallback tem utilidade limitada, pois kstart para imediatamente quando serial_init retorna false, mas essa diferença é importante para descrever corretamente o contrato da API.
panic¶
panic(message) é _Noreturn.
Primeiro executa cli. Depois imprime “PANIC: ”, mensagem e identidade. Por fim entra em loop de hlt.
Não há branch de recuperação. Violação fatal é tratada como fail-stop para evitar continuar com invariantes desconhecidos já quebrados.
panic_exception¶
Também é non-return. Desliga interrupções, lê CR2 e registra: - vector; - error; - RIP; - CR2.
Depois inclui identidade e halta.
CR2 é mais relevante no #PF, mas estar presente no registro padroniza diagnóstico.
Identidade¶
panic_identity lê:
- índice da CPU;
- CR3;
- RSP;
- build ID;
- revisão Git;
- SHA-256 do kernel.
Esses dados tornam o crash revision-bound. Analisar um RIP contra source de outra compilação pode produzir diagnóstico completamente errado.
CR3 ajuda a identificar address space ativo. RSP ajuda a inferir stack/contexto.
Por que build identity importa¶
Kernel layout muda com pequenas alterações. Timing e offsets também.
Um crash report sem revisão pode ser impossível de reproduzir. Ao imprimir Git/hash, o sistema conecta a falha ao binário e à documentação da mesma revisão.
Armazenamento da identidade de build e contrato de formatação¶
A imagem do kernel contém uma string fixa iniciada por CHRISOSHASH: seguida de 64 caracteres zero. A ferramenta de stamping localiza esse marcador depois do link e substitui o campo zerado pelo SHA-256 da imagem vinculada enquanto o placeholder ainda está na forma conhecida. build_kernel_sha256() devolve um ponteiro diretamente para a parte do hash dentro dessa string estática, após o prefixo de 12 caracteres.
Revisão Git, build ID e data chegam por macros definidas no build, enquanto a identidade do compilador usa __VERSION__. build_info_format concatena esses campos em um buffer fornecido pelo caller sem alocar heap. Se a capacidade não comportar todo o texto e o terminador, retorna -1 em vez de produzir saída parcial tratada como válida.
Assim a identidade da compilação fica disponível tanto no log normal de boot quanto no panic sem depender de metadata de filesystem. Ainda é necessário separar identidade de verificação de integridade: imprimir um hash informa qual valor foi incorporado à imagem em execução; validação independente exige comparar esse valor com um artefato confiável ou com resultado de build reproduzível.
Ring klog¶
klog.c usa array estático de 8.192 bytes, posição, length, spinlock e ready flag.
klog_init inicializa uma vez. Não aloca e não usa filesystem.
Esse desenho permite log antes do allocator e evita que crescimento de log consuma memória sem limite.
Escrita¶
klog_putc faz lazy init, pega lock, escreve em g_pos, avança modulo capacidade e aumenta length até 8192.
Depois de cheio, bytes novos sobrescrevem os mais antigos.
klog_puts chama putc por caractere.
Como lock é adquirido em cada caractere, dois klog_puts concorrentes podem intercalar mensagens por caractere. O lock protege estrutura do ring, não atomicidade de mensagens completas.
Leitura¶
klog_copy(dst, cap) valida destino/cap/ready.
Sob lock: - n = min(length, cap); - start = (pos + capacity - n) % capacity; - copia n bytes em ordem cronológica.
Isso recompõe corretamente a ordem depois do wrap.
A função retorna count e não insere NUL. Consumer deve tratar como bytes.
Invariantes do ring e custo das operações¶
O ring mantém dois estados diferentes: g_pos aponta para o próximo slot físico a ser sobrescrito e g_len representa quantos bytes válidos existem, limitado por KLOG_CAP. Os invariantes são:
Cada escrita custa O(1), desconsiderando contenção do spinlock. klog_copy custa O(n) para os bytes efetivamente copiados. Nenhuma dessas operações usa heap, de modo que a memória consumida pelo log permanece limitada estaticamente a 8.192 bytes.
Quando o caller fornece capacidade menor que o conteúdo retido, a expressão start = (g_pos + KLOG_CAP - n) % KLOG_CAP seleciona os n bytes mais recentes, não o prefixo mais antigo. Por isso o buffer de 4.096 bytes usado para persistir o boot log captura a metade mais recente de um ring completamente cheio.
O desenho é orientado a bytes e não possui limites de record. Depois de wrap ou truncamento, uma cópia pode começar no meio de uma linha. Além disso, chamadas concorrentes de klog_puts podem intercalar caracteres. Consumers devem interpretar o ring como stream cronológico de bytes, não como conjunto de mensagens atômicas.
Persistência no boot¶
Depois de storage/fs/install, kstart verifica backend ChrisFS. Usa buffer estático de 4096 bytes e chama klog_copy. Se houver dados, escreve SYS/BOOT.LOG.
Portanto: - logging pode começar antes de disco; - depois um snapshot pode ser persistido.
O snapshot é de até 4 KiB, embora ring comporte 8 KiB.
Fan-out real entre serial e klog¶
A implementação atual acopla a saída serial ao ring em memória. serial_putc executa klog_putc(value) antes mesmo de verificar se a COM1 está disponível. Portanto todo caractere enviado pela API serial é registrado primeiro no klog, inclusive quando a saída física ou virtual da serial está indisponível.
serial_puts também converte cada newline em carriage return seguido de newline chamando serial_putc('\r') e depois serial_putc('\n'). O ring recebe, portanto, o mesmo par CRLF gerado para o transporte serial.
O caminho efetivo é:
serial_puts
-> serial_putc
-> klog_putc
-> adquire g_lock
-> atualiza ring de 8 KiB
-> libera g_lock
-> se COM1 indisponível: retorna
-> adquire g_serial_lock
-> espera transmitter-ready
-> outb(COM1, byte)
-> libera g_serial_lock
Esse fan-out oferece uma propriedade útil: depois que serial_init inicializa o klog, diagnósticos continuam preservados em memória mesmo se a COM1 não puder receber o byte. Porém isso também significa que o fatal path não é independente dos locks de logging.
Dependência de locks no panic path¶
panic e panic_exception executam cli e depois chamam serial_puts. Como serial_putc passa primeiro por klog_putc, o panic pode adquirir g_lock; se serial estiver disponível, também pode adquirir g_serial_lock.
O spinlock usado pelo kernel é um loop CAS não reentrante. Não há owner, bypass de panic nem try-lock. Assim, se uma exception fatal ocorrer enquanto a própria CPU já possui um desses locks, a tentativa de imprimir o panic pode girar indefinidamente antes de alcançar o loop final de hlt. Desabilitar interrupções não libera lock já adquirido e não interrompe outra CPU que esteja segurando o recurso.
Essa é uma limitação real da revisão atual. Um caminho fatal mais robusto poderia ter saída serial de emergência sem locks, try-lock com fallback forçado ou um modo de panic que ignore o fan-out comum. O código analisado ainda não implementa essa proteção.
Fault user não é panic global¶
panic_user_fault registra fault, destrói processo, loga RIP/CR2/error/CPU/build, define -11 e reescreve o frame para voltar ao kernel.
Não chama panic.
Assim uma aplicação inválida não derruba necessariamente todo o S.O.
O que permanece fatal¶
Depois de demand paging e tratamento de origem user, exceptions abaixo de 32 chegam a panic_exception.
Fault não recuperável de kernel, invalid opcode privilegiado e protection fault em ring 0 param o sistema.
Em kernel experimental, continuar depois de invariantes desconhecidos pode ser mais perigoso que parar.
Concorrência¶
Klog tem spinlock. Panic desliga interrupções localmente, mas não para automaticamente todos os outros CPUs.
Mais de um CPU pode tentar imprimir serial ao mesmo tempo se o driver não serializar. O número da CPU ajuda a atribuir linhas.
Kernels maduros podem eleger uma panic CPU e parar as outras. ChrisOS já tem NMI para TLB fencing, mas panic.c não implementa nem reivindica um protocolo completo de SMP panic stop.
Grafo de dependência do fatal path¶
Não depende de heap, filesystem, graphics, scheduler recovery ou userspace.
Klog normal pode ser persistido depois, mas não faz parte da cadeia mínima obrigatória do panic.
Reprodutibilidade¶
Para conectar crash ao estado do sistema, os campos principais são revisão, CPU, CR3, RIP, vector/error, CR2 e RSP.
O código atual fornece esse núcleo.
Backtrace simbolizado seria útil, porém unwind confiável exige metadata e validação de stack. Não deve ser inventado a partir de um único RIP.
Segurança¶
Logs de desenvolvimento revelam endereços e build identity. Em sistema multiusuário hardened, exposição desses dados teria de ser controlada.
Serial aqui é canal privilegiado de debug, não API sanitizada para aplicação.
O ring limitado evita que erro repetitivo cresça memória indefinidamente.
Desempenho¶
Lock por caractere e serial character-oriented podem alterar bastante timing sob log intenso.
Para throughput alto, buffers per-CPU e records estruturados seriam alternativas, ao custo de merge/order/timestamps mais complexos.
Validação¶
Testes controlados devem: - chamar panic e confirmar halt; - provocar exception kernel conhecida e verificar campos; - preencher/wrapar klog; - copiar com cap menor; - persistir boot log em ChrisFS; - provocar fault user e confirmar que kernel segue; - executar writers concorrentes e checar integridade do ring; - validar build/hash contra binário.
Limitações atuais¶
Não há severity/facility estruturada, timestamp no ring, atomicidade de mensagem inteira, coordenador global de panic SMP, stack unwinder nem crash dump persistente produzido diretamente no fatal path.
A base existente ainda é sólida: serial cedo, identidade revisionada, fatal fail-stop, ring bounded sem alocação e persistência posterior.
Mapa de fonte¶
Panic: kernel/metal/panic.c/panic.h. Ring: klog.c/klog.h. Serial: serial.c/serial.h. Build metadata: buildid.c/buildid.h. Fault user: syscall.c. Persistência: start.c. O Source Atlas publica tudo integralmente na revisão e05a17fd76333114a3fb5c2452f38ca747d4ac56.