Todo sistema operacional começa antes de ser realmente ele mesmo. A CPU inicia em um ambiente definido pela plataforma, o firmware inicializa hardware suficiente para carregar o primeiro executável, e esse executável prepara a máquina para o kernel. Só depois que essa cadeia faz seu trabalho o sistema operacional pode começar a aplicar suas próprias regras.

É fácil tratar esse caminho inicial como mera infraestrutura, mas o processo de boot faz parte do modelo de segurança. Para o EriX, o boot é o primeiro lugar em que bytes não confiáveis se tornam execução confiável, e também é o primeiro lugar em que a autoridade da máquina é traduzida em estrutura explícita: imagens verificadas, mapas de memória, descritores de módulos, endereços de entrada, metadados de framebuffer, ponteiros ACPI e, eventualmente, objetos do kernel.

Se esse caminho for descuidado, o sistema de capacidades começa a partir de uma mentira. Este post percorre o caminho de boot do EriX do firmware ao kernel: o que UEFI fornece, o que o bootloader precisa fazer, o que o handoff contém e por que o kernel é entrado como um executável de metade alta.


O boot começa com autoridade de firmware

Nos alvos atuais do EriX, o caminho de boot começa com UEFI em x86_64. UEFI é firmware e roda antes do sistema operacional. Ele fornece os serviços de boot que permitem a uma aplicação EFI ler arquivos, alocar memória, inspecionar o mapa de memória da plataforma, descobrir tabelas de configuração como ACPI, consultar saída gráfica via GOP e sair dos serviços de boot antes que o kernel assuma o controle.

O bootloader do EriX é construído como uma aplicação UEFI, então o firmware carrega primeiro o bootloader e chama seu ponto de entrada UEFI. Na implementação, esse ponto de entrada é efi_main, usando a convenção de chamada UEFI x86_64. Nesse momento o EriX ainda não controla a máquina; ele está rodando dentro de um ambiente fornecido pelo firmware.

O bootloader pode pedir ao UEFI para ler um arquivo, alocar memória, inspecionar tabelas de firmware e preparar tabelas de páginas, mas toda essa autoridade é temporária. Os serviços de boot do UEFI não são o sistema operacional. Eles são andaimes que o bootloader deve usar com cuidado, resumir em fatos explícitos e depois deixar para trás.


O bootloader é código confiável

O bootloader roda antes que o kernel possa impor qualquer coisa, o que o coloca na base de computação confiável. Se o bootloader carregar os bytes errados, saltar para o endereço errado, aceitar uma imagem adulterada, rotular memória incorretamente ou inventar autoridade que não veio de entrada verificada, o kernel começa a partir de uma fundação comprometida. Para manter esse risco auditável, o EriX mantém o trabalho do bootloader estreito e explícito:

  • localizar e carregar boot.img
  • validar a imagem antes de confiar nela
  • validar o dynamic boot store
  • realocar o kernel dinâmico e seus objetos iniciais requeridos
  • construir uma estrutura de handoff determinística
  • preparar tabelas de páginas e uma pilha de bootstrap
  • sair dos serviços de boot do UEFI
  • saltar para o kernel uma única vez

Isso deliberadamente não é um ambiente de boot de propósito geral. Não há política de menu de boot no escopo atual, não há caminho de recuperação remoto e não há tentativa de continuar depois de entrada malformada crítica para o boot. A regra é verificar antes de executar: se a imagem de boot não puder ser parseada, verificada, carregada, mapeada ou descrita de forma consistente, a tentativa de boot falha antes que o kernel receba o controle.


Carregando boot.img

O caminho UEFI atual procura este arquivo no volume de boot. O caminho é fixo para o perfil atual, o que mantém estreito o tratamento inicial da mídia e evita transformar boot em um problema de descoberta de política:

\ERIX\BOOT.IMG

O bootloader abre o volume de boot por meio de protocolos de arquivo UEFI, lê o arquivo em memória alocada pelo UEFI e faz uma primeira verificação barata do magic de contêiner ERIXBOOT antes do parsing mais profundo. Depois disso, lib-bootimg assume: o bootloader parseia a imagem como um BootImage, verifica sua assinatura e seus hashes, checa a arquitetura do manifesto e só então começa a extrair payloads.

Essa separação importa porque a mídia de boot não é confiável. O arquivo pode estar ausente, truncado, malformado ou adulterado, e o bootloader não pode tratar um descritor de seção como verdadeiro só porque ele veio do disco. No EriX, o parser e verificador de boot.img fazem parte do caminho de boot confiável, e precisam rejeitar estrutura ruim, limites ruins, hashes ruins, versões sem suporte e dados de arquitetura incompatíveis antes que qualquer byte carregado se torne executável.

Os próximos posts vão se aprofundar no formato boot.img e na verificação de imagem. Para este post, o ponto importante é a ordem:

  1. ler bytes
  2. parsear estrutura
  3. verificar integridade
  4. checar compatibilidade do alvo
  5. validar e realocar artefatos dinâmicos de boot
  6. construir metadados de handoff

A execução vem depois da validação, não antes. Essa ordem é a diferença entre tratar a imagem de boot como entrada e tratá-la como autoridade.


Carregando o kernel

Depois que boot.img é aceito, o bootloader precisa de um kernel, e no caminho de boot do EriX o kernel é carregado a partir do dynamic boot store assinado. O bootloader valida o kernel dinâmico como um objeto ELF64 ET_DYN, checa os metadados de dynamic-link do EriX, verifica nomes de dependências e hashes de objetos contra metadados assinados, carrega objetos compartilhados requeridos, aplica realocações aprovadas, impõe restrições W^X e prepara um catálogo de boot dinâmico para o kernel.

Isso soa como muita coisa porque é muita coisa, mas a forma de segurança continua direta e revisável:

  • os dados vêm de uma imagem de boot verificada
  • o parsing do formato executável é explícito
  • os intervalos de segmentos são checados
  • os efeitos colaterais de realocação são restritos
  • metadados dinâmicos ausentes ou inconsistentes falham antes da entrada no kernel

O bootloader existe para validar e realocar o kernel dinâmico, e então descrever ao kernel o grafo verificado de objetos iniciais. Ele não se torna um linker dinâmico geral para o sistema em execução; esse papel pertence a uma fase posterior, depois que o kernel e o modelo de serviços em espaço de usuário existem.


Preservando metadados de boot

O kernel dinâmico não é a única coisa na imagem de boot. O bootloader também preserva metadados de boot não executáveis requeridos pelo kernel e pelo sistema inicial em espaço de usuário, enquanto serviços executáveis pertencem ao grafo de objetos dinâmico. Esses artefatos executáveis são descritos pelo catálogo dinâmico como objetos, segmentos e arestas de dependência derivadas do store e do manifesto assinados.

Outras seções requeridas são blobs não executáveis. Exemplos incluem configuração de boot, stores de metadados de dynamic-link e dados de fonte de console. Eles são copiados para memória mapeada e descritos como módulos somente leitura sem ponto de entrada, porque um blob não é executável só porque aparece na imagem de boot.

Essa distinção é importante para o design de capacidades. Um payload de configuração de boot deve ser legível pelo sistema inicial, mas não deve ser tratado como código. Um blob de fonte pode ser necessário para continuidade do framebuffer, mas não precisa de autoridade de execução. O EriX preserva essa distinção no handoff:

  • descritores de objetos dinâmicos para artefatos executáveis
  • descritores de segmentos dinâmicos para intervalos executáveis e de dados mapeados
  • descritores de dependências dinâmicas para relações de objetos iniciais
  • SECTION_TYPE_BOOT_CONFIG para dados de política de boot
  • descritores de módulos para blobs de boot não executáveis

O handoff leva essa diferença adiante para que o kernel e rootd não precisem adivinhar se uma peça de dados de boot é autoridade executável, configuração somente leitura ou metadados comuns.


O handoff é o contrato

O bootloader não chama uma API do kernel, porque ainda não há kernel em execução. Em vez disso, o bootloader constrói um blob de handoff: um contrato binário versionado definido por lib-handoff. No perfil atual de bootloader para kernel, ele começa com o magic ERIXHK01, campos de versão maior e menor, tamanho total, identificadores de arquitetura e plataforma, e então offsets e contagens para as tabelas seguintes.

O handoff pode incluir várias classes de dados de que o kernel precisa antes de poder construir sua própria visão runtime da máquina:

  • entradas normalizadas de mapa de memória
  • descritores de módulos carregados
  • ponteiro ACPI RSDP
  • build ID e hash de imagem verificados
  • metadados de continuidade de framebuffer
  • descritores de objetos dinâmicos
  • descritores de segmentos dinâmicos
  • arestas de dependência dinâmicas

Essa é a primeira transferência estruturada de autoridade. O firmware deu ao bootloader fatos brutos e serviços temporários, e a imagem de boot verificada deu ao bootloader metadados de payload assinados. O bootloader combina tudo isso em uma descrição determinística do que carregou, onde colocou e em que o kernel pode confiar.

O handoff não é uma dica nem metadados “best effort”. Ele é a entrada a partir da qual o kernel começa a construir sua própria visão da máquina, e é por isso que carrega contagens, tamanhos de entrada, offsets, hashes, tipos, intervalos e campos de versão. Deve ser possível para o kernel rejeitá-lo.


Mapas de memória precisam de normalização

Mapas de memória de firmware não são automaticamente moldados para a política do kernel. UEFI relata regiões com tipos de memória de firmware, enquanto o bootloader também conhece a memória que alocou para o kernel dinâmico, mapeamentos de objetos iniciais, blobs de boot requeridos, pilha de bootstrap, páginas de handoff, mapeamentos de framebuffer e a imagem de boot original. Essas visões precisam ser mescladas antes que o kernel possa raciocinar sobre a memória disponível.

No EriX, o bootloader captura o mapa de memória UEFI, adiciona regiões explicitamente possuídas pelo boot e normaliza o resultado em intervalos não sobrepostos com tipos de memória do EriX como:

  • RAM utilizável
  • memória reservada
  • memória ACPI reclaimable
  • memória ACPI NVS
  • memória MMIO/dispositivo
  • memória possuída pelo bootloader
  • memória possuída pela imagem de boot

Regiões explicitamente possuídas pelo boot vencem classificações genéricas do firmware. Isso importa porque o kernel não deve tratar acidentalmente a memória que contém a imagem de boot, o blob de handoff, os mapeamentos de objetos dinâmicos, os blobs de boot requeridos ou a pilha de bootstrap como RAM livre comum. Memória é autoridade no EriX, então mesmo tão cedo o sistema toma cuidado com quem pode reutilizar quais bytes.


Deixando UEFI para trás

Os serviços de boot do UEFI são úteis, mas temporários. Antes de saltar para o kernel, o bootloader chama ExitBootServices, que na prática é uma transição de mão única. Depois de uma saída bem-sucedida, o bootloader deve tratar ponteiros para serviços de boot como inválidos; ele não pode continuar pedindo ao firmware que aloque memória ou leia arquivos depois que o sistema operacional assume o controle.

Essa transição é delicada porque UEFI exige que o bootloader saia usando uma chave atual do mapa de memória. Se o mapa mudar entre a captura e a saída, a chamada pode falhar e o loader deve tentar novamente com um mapa novo. O adaptador UEFI do EriX cuida desse loop de repetição na camada de plataforma.

O ponto de design importante é que o kernel é entrado depois que o bootloader terminou de usar serviços de firmware. O kernel não deve herdar uma dependência de firmware meio aberta; ele deve herdar dados explícitos.


Construindo as primeiras tabelas de páginas

O kernel é entrado com paginação já ativa. Para o perfil UEFI x86_64 atual, o bootloader constrói tabelas de páginas mínimas com dois tipos de mapeamentos: uma região de memória baixa mapeada por identidade para o bring-up inicial e mapeamentos específicos de metade alta para os objetos que o kernel usará imediatamente.

A implementação atual mapeia por identidade o primeiro 1 GiB usando páginas de 2 MiB e também mapeia por identidade as janelas MMIO de APIC. Em seguida, mapeia intervalos virtuais de metade alta para o kernel, objetos dinâmicos carregados, blobs de boot requeridos, blob de handoff, framebuffer e pilha de bootstrap. Isso é suficiente para que o kernel comece no espaço de endereços que espera.

As tabelas de páginas não são o sistema final de memória virtual. Elas são uma ponte que permite ao kernel executar, validar o handoff, instalar estado inicial de CPU e começar a construir o ambiente real de kernel e espaço de usuário. A ponte ainda precisa estar correta: se a página de entrada do kernel não estiver mapeada, a máquina falha imediatamente; se o blob de handoff estiver mapeado no endereço virtual errado, o kernel lê lixo; se a pilha estiver ausente, a entrada falha antes que o código Rust possa fazer muita coisa.


Por que um kernel de metade alta?

O EriX entra no kernel na metade alta do espaço de endereços virtual. Isso significa que o kernel roda em endereços virtuais altos em vez de ser linkado e executado apenas no intervalo baixo mapeado por identidade. Esse é um design comum de kernel porque dá ao kernel uma região de endereços virtuais estável independente de onde a memória física foi alocada.

O layout de metade alta também separa o espaço virtual do kernel dos intervalos comuns de espaço de usuário e permite que o kernel mantenha seus próprios mapeamentos presentes através de mudanças posteriores de espaço de endereços, enquanto mapeamentos de espaço de usuário podem variar por tarefa. O layout de endereços por si só não impõe todo o modelo de segurança, mas apoia a fronteira ao tornar a memória do kernel distinta da memória normal de processo.

No EriX, o bootloader é responsável por tornar possível a entrada inicial na metade alta. Ele carrega o kernel de acordo com endereços virtuais ELF, mapeia esses endereços virtuais para páginas físicas alocadas, cria uma pilha de bootstrap na metade alta, mapeia o blob de handoff em um endereço conhecido de metade alta, carrega cr3 e salta para o ponto de entrada do kernel.

No salto, o contrato x86_64 atual é intencionalmente pequeno e explícito. O bootloader fornece apenas o estado de que o kernel precisa para começar a validar o handoff e instalar seu próprio estado inicial de CPU:

  • ABI SysV
  • rdi contém o ponteiro de handoff
  • rsp aponta para a pilha de bootstrap com o alinhamento esperado
  • paginação está ativa
  • o controle não retorna

Essa ABI é pequena por design. Quanto menos estado implícito a entrada do kernel depende, mais fácil é auditar a fronteira.


Entrando no kernel

Do lado do kernel, a entrada começa antes que o runtime completo do kernel exista. O kernel dinâmico expõe erix_dynlink_entry, que entra no caminho inicial do kernel com o ponteiro de handoff, e o primeiro trabalho é defensivo em vez de carregado de política.

O kernel desativa interrupções, inicializa saída serial inicial, verifica que o ponteiro de handoff não é nulo, lê o tamanho do handoff a partir do cabeçalho, impõe um tamanho máximo de handoff e então valida o blob inteiro por meio de lib-handoff. Depois da validação estrutural, o kernel aplica suas próprias checagens de política:

  • a arquitetura deve ser x86_64
  • a plataforma deve ser UEFI
  • o build ID não deve estar vazio
  • as tabelas do catálogo dinâmico devem ser internamente consistentes
  • nomes, hashes, segmentos, dependências e intervalos do store de objetos dinâmicos devem ser validados antes do uso

O bootloader já construiu o handoff, mas o kernel ainda o valida. Componentes confiáveis não podem pular contratos só porque outro componente confiável produziu os dados. O objetivo de um handoff versionado é que os dois lados possam concordar exatamente sobre o que foi transferido.

Só depois disso o kernel avança mais profundamente na inicialização inicial: instalação de GDT, instalação de IDT, configuração do caminho de syscall, inicialização opcional do console inicial e, por fim, orquestração de bootstrap para a primeira tarefa root. O kernel se torna kernel gradualmente, e a primeira coisa que faz é checar o chão sob seus pés.


Boot é tradução de autoridade

É tentador descrever boot como “carregar o kernel e saltar”. Isso é tecnicamente verdade, mas perde o ponto de design de sistema operacional. Para o EriX, boot é tradução de autoridade: autoridade de firmware vira fatos de boot explícitos, bytes da imagem de boot viram seções verificadas, arquivos ELF viram intervalos executáveis mapeados, blobs viram descritores de módulos não executáveis, mapas de memória de firmware viram regiões de memória normalizadas, metadados de dynamic-link viram um grafo de objetos limitado e o estado do framebuffer vira metadados de continuidade.

Tudo isso se torna um blob de handoff, e o kernel recebe esse blob e decide se ele é aceitável. Só então pode começar a transformar recursos da máquina em objetos do kernel, capacidades, espaços de endereços, endpoints e a primeira tarefa de espaço de usuário. É por isso que o processo de boot pertence a uma discussão sobre sistemas operacionais baseados em capacidades: o modelo de capacidades não começa depois do boot como um acréscimo; ele depende de o boot não contrabandear autoridade ambiente.

O bootloader não deve simplesmente dizer que carregou algumas coisas. Deve dizer exatamente o que carregou, onde está, o que é, como foi verificado e quais fatos de plataforma observou. Essa é a diferença entre um salto e um handoff.


O que o EriX mantém fora do bootloader

O bootloader é poderoso porque roda cedo, e é exatamente por isso que deve permanecer pequeno. O EriX não quer que o bootloader decida política runtime: ele não deve decidir qual serviço possui a política de memória, como processos são supervisionados, como drivers são gerenciados ou como sistemas de arquivos são compostos.

Essas decisões pertencem ao kernel e aos serviços de sistema em espaço de usuário. O trabalho do bootloader é mais estreito: validar o artefato de boot, preparar o ambiente mínimo de execução, descrever o que fez e transferir o controle.

Essa linha importa para o tamanho da TCB. Um bootloader com mais funcionalidades não é automaticamente melhor, porque cada funcionalidade no boot inicial é código que roda antes que o kernel possa isolá-lo. Cada parser, caminho de fallback, modo interativo e exceção de política aumenta a quantidade de comportamento confiável que precisa estar correto antes que o sistema inicie.


Olhando adiante

Este post tratou boot.img principalmente como um contêiner verificado. O próximo passo é abrir esse contêiner e olhar diretamente para seu design.

O próximo post explicará o formato boot.img do EriX: por que o sistema usa uma imagem unificada, como as seções são organizadas, quais metadados são carregados e como o formato apoia artefatos de boot reproduzíveis e determinísticos.