1 pontos por GN⁺ 2024-07-16 | 1 comentários | Compartilhar no WhatsApp
  • Começa em um setor de boot de 512 bytes e monta passo a passo o fluxo mínimo de bootloader que leva uma CPU x86_64 do real mode de 16 bits ao long mode de 64 bits
  • Primeiro verifica a saída usando interrupções da BIOS e depois conecta nasm, ld, objcopy e QEMU para validar se a imagem de boot realmente é executada
  • Por causa do limite de tamanho do setor de boot, divide em stage 1/stage 2 e, antes de entrar em protected mode, lê o próximo código do disco com int 0x13 da BIOS
  • A partir do protected mode de 32 bits, não é mais possível usar rotinas da BIOS, então são necessárias inicializações de hardware como GDT, flat segmentation e saída direta no buffer VGA
  • Para entrar no long mode de 64 bits, é preciso alinhar tabelas de páginas, PAE, EFER.LME, cr0.PG e a GDT para 64 bits; depois disso, dá para chamar código C freestanding como se fosse um kernel

Ambiente de 16 bits iniciado pela BIOS

  • Após o reset, a CPU x86 está em real mode, e o tamanho padrão dos operandos é de 16 bits
  • O real mode usa segmentation para criar um espaço de endereçamento de 20 bits e pode lidar com até 1 MB de memória
  • O primeiro código executado pela BIOS fica no setor de boot do disco
    • A BIOS procura um disco cujo primeiro setor termine com o número mágico 0xaa55
    • Ela carrega esse setor no endereço de memória 0x7c00
  • O espaço entregue pela BIOS tem apenas 512 bytes, então esse código se concentra em atuar como bootstrap para carregar o restante do bootloader
  • As rotinas da BIOS só podem ser usadas enquanto o sistema permanecer em real mode

Pré-requisitos e ambiente de build

Criando o setor de boot e verificando a execução

  • O primeiro setor de boot imprime "Hello, world!" com uma rotina da BIOS e depois para com hlt e um loop
  • Para imprimir a string, usa a chamada de video services da BIOS int 0x10 com ah = 0x0e
  • O Makefile cria o objeto com nasm, faz o link com um linker script e gera uma imagem de boot raw com objcopy -O binary
  • make boot executa a imagem no QEMU
    • qemu-system-x86_64 -no-reboot -drive file=$<,format=raw,index=0,media=disk
  • O linker script configura o setor de boot para ser posicionado com base em 0x7c00
    • A origem boot_sector em MEMORY é 0x7c00, com tamanho 512
    • A seção .bootsign adiciona 0x55, 0xaa na posição 0x7c00 + 510
  • Também seria possível tratar diretamente o offset e o número mágico dentro do assembly do setor de boot, mas aqui o linker script assume esse papel

Como o stage 1 lê o stage 2 do disco

  • O stage 1 é o código do setor de boot carregado pela BIOS, e seu objetivo é trazer o stage 2 para a memória
  • O stage 2 contém o código que faz a transição do real mode de 16 bits para o protected mode de 32 bits
  • Como não é mais possível usar rotinas da BIOS depois de entrar em protected mode, a leitura dos setores do disco precisa terminar antes da transição
  • Para acesso ao disco, usa os BIOS disk services com int 0x13
    • ah = 0x42 é o recurso de leitura estendida da BIOS
    • dl = 0x80 é o número do drive
    • O disk address packet contém a quantidade de setores a ler, o endereço de destino e o setor inicial
  • O exemplo usa READ_SECTORS_NUM equ 64 para ler 64 setores
    • Como o setor de boot é o setor 0, o stage 2 é lido a partir do setor 1
    • O endereço de destino é BOOT_LOAD_ADDR + SECTOR_SIZE, ou seja, 0x7c00 + 512
  • O código ainda mantém um tratamento temporário que aceita casos em que menos setores do que o solicitado são lidos
  • No começo, o stage 2 copia a print_string do real mode para imprimir "Hello from stage 2" e confirmar que o salto a partir do stage 1 funcionou corretamente

Mudando para o protected mode de 32 bits

  • Para entrar em protected mode, primeiro é preciso definir a Global Descriptor Table (GDT)
  • No protected mode, a segmentation é usada por padrão para proteção de memória
  • No long mode de 64 bits, paging é obrigatório, mas para chegar ao protected mode essa configuração de segmentation vem primeiro
  • A GDT do exemplo segue o flat model do manual da Intel
    • Há um code segment e um data segment
    • Os dois segmentos mapeiam todo o espaço de endereçamento linear
    • Como é uma etapa intermediária rumo ao long mode, é usado o modelo mais simples
  • A GDT é uma estrutura contínua na memória
    • No início existe um null descriptor para capturar traduções inválidas
    • Depois vêm o descriptor do code segment e o descriptor do data segment
  • A transição segue esta sequência
    • cli desativa as interrupções
    • lgdt [gdt32_pseudo_descriptor] carrega no GDTR o endereço e o tamanho da GDT
    • Define cr0.PE, isto é, o bit 0 de cr0, para ativar o protected mode
    • Um far jump limpa o pipeline de instruções e atualiza cs para o novo code segment
  • Depois de entrar em protected mode, os valores antigos de segmento deixam de fazer sentido, então ds, ss, es, fs e gs são configurados com o novo seletor de data segment
  • Para reativar interrupções depois de toda a configuração, são necessários passos adicionais

Imprimindo na tela sem a BIOS

  • No protected mode, não é mais possível chamar rotinas da BIOS
  • A impressão de strings passa a ser feita escrevendo diretamente no buffer VGA
  • print_string32 grava bytes de caractere e cor no endereço 0xb8000
    • O valor de cor é 0xf
    • Cada célula de caractere usa 2 bytes
  • Essa função de saída é bem simples, então a mensagem sempre aparece no canto superior esquerdo da tela

Tabelas de páginas para entrar em long mode

  • O IA-32e mode da documentação da Intel corresponde ao long mode no manual AMD64
  • Para mudar para long mode, a CPU precisa estar em protected mode e o paging também precisa estar habilitado
  • Para o conceito de paging, o texto recomenda Introduction to Paging e OSTEP
  • No long mode com PAE ativado, são usadas tabelas de páginas em 4 níveis
  • build_page_table cria uma tabela de páginas de 4 níveis no endereço especificado
    • O tamanho de página é 0x1000
    • Cada page table tem tamanho 0x1000
    • O número de entradas é 512
    • Primeiro, quatro tabelas são inicializadas com 0 para deixar todas as entradas como not present
    • Depois, liga as primeiras entradas de PML4 → PDP → PD → page table
    • Na camada mais baixa da page table, define 512 entradas

GDT para 64 bits e sequência de transição para long mode

  • Embora o paging cuide do espaço de endereçamento virtual e das permissões, a GDT ainda é necessária no long mode
  • A GDT para 64 bits também segue o flat model e é quase igual à usada no protected mode
  • A diferença está nos bits relacionados ao long mode
    • É definido o flag de code segment de 64 bits
    • Quando esse flag está definido, o bit de tamanho padrão de operação deve ser 0
  • A transição para long mode segue este fluxo
    • Cria uma tabela de páginas de 4 níveis no endereço 0x1000
    • Coloca em cr3 o endereço da tabela PML4
    • Define o bit 5 de cr4 para ativar PAE
    • Lê o MSR 0xc0000080 e define EFER.LME, isto é, o bit 8
    • Define o flag PG, o bit 31 de cr0, para ativar o paging
    • Carrega a GDT de 64 bits com lgdt
    • Faz um far jump para o code segment de 64 bits e entra no modo de 64 bits
  • Logo após ativar o paging, o sistema ainda está em IA-32e compatibility mode; a mudança efetiva para 64 bits acontece ao saltar usando uma GDT com o flag de segmento de 64 bits
  • A mensagem de confirmação de sucesso é exibida no canto superior esquerdo da tela via buffer VGA

Chamando código C freestanding

  • Depois de entrar no long mode de 64 bits, já é possível chamar código C freestanding
  • kernel.c limpa o buffer VGA 0xb8000 e imprime "Hello from C"
  • No lado do assembly, start_long_mode declara extern _start_kernel depois de imprimir a string em 64 bits e chama _start_kernel
  • O linker script divide a região de memória em três partes
    • boot_sector: 0x7c00, tamanho 512
    • stage2: 0x7e00, tamanho 512
    • kernel: 0x8000, tamanho 0x10000
  • As seções .text, .data, .rodata e .bss são posicionadas na região do kernel
  • O Makefile passa a compilar tanto o assembly quanto o C
    • O compilador C é o gcc
    • As principais CFLAGS são -std=c99 -ffreestanding -m64 -mno-red-zone -fno-builtin -nostdinc -Wall -Wextra
  • O código completo do exemplo é fornecido em link para download

1 comentários

 
GN⁺ 2024-07-16
Comentários do Hacker News
  • É possível entrar direto no long mode com muito menos código, sem passar pelo modo protegido: https://wiki.osdev.org/Entering_Long_Mode_Directly
    Havia um bootloader para um pequeno kernel de 64 bits feito dessa forma e, mesmo incluindo o código para ler o kernel do disco e configurar o modo VESA, ele cabia com folga no setor de boot. Nem era necessário um loader de segundo estágio

    • Fico curioso para saber como colocar tudo isso em 512 bytes. Não deve haver um sistema de arquivos real que permita deixar o kernel em qualquer lugar do disco como um arquivo comum; só lidar com fragmentação de arquivos já passaria fácil de 512 bytes
    • Basta usar https://limine-bootloader.org/, que fica muito mais simples. Não é preciso mexer com modo real, o mesmo vale para SMP, ele carrega automaticamente o kernel com mapeamento higher-half e também funciona em aarch64 e riscv64
    • É verdade, mas se você quiser incluir uma tabela de partições e dar suporte a controladores AHCI e SATA modernos, o espaço do bootloader fica ainda menor e passa a exigir otimização. Nesse caso, não dá para usar todos os 510 bytes para o loader; fica bem menos, e, para preencher também entradas de partição válidas, nem dá para usar os bytes dentro da tabela, o que torna tudo mais complicado
      Para usar um disco rígido moderno de verdade, é melhor consultar GPT em vez de MBR. Assim dá para lidar com discos grandes, acima de 2 TB, sem esbarrar nas limitações da tabela de partições. UEFI elimina esse tipo de problema e permite usar um layout de disco adequado sem grandes dificuldades
      Para entrar em modo de 64 bits, o modo protegido não é necessário. Dito isso, é melhor não usar BIOS. É sujo e só torna o trabalho mais trabalhoso
      Usar UEFI com EDK2 ou GnuEFI é uma abordagem melhor, e ambos são razoavelmente fáceis e confortáveis de implementar. Leva um tempo para se acostumar aos conceitos iniciais de UEFI, mas olhando projetos de exemplo no GitHub dá para entender a estrutura facilmente. O EDK tem alguns pontos meio ruins, como arquivos .dec e .inf, e no GnuEFI é preciso ler os arquivos de cabeçalho para descobrir as funcionalidades, mas ainda assim é muito melhor do que uma interface de BIOS com especificação pouco clara. Em hardware real, nem dá para presumir que int 0x10, int 0x15 etc. existam corretamente
      Em sistemas UEFI, dá para assumir uma base mínima estável, e também enumerar recursos de hardware e da plataforma de forma sane. Além disso, como a UEFI já deixa a plataforma bastante configurada, não é preciso fazer muita inicialização no componente de loader do OS; basta carregar diretamente o OS e os componentes adequados ao projeto dos drivers e do kernel. É só obter o mapa de memória, acessar o sistema de arquivos EFI e ler o que for necessário
    • Eu não sabia que isso era possível. Se o objetivo é simplesmente chegar ao long mode, fico me perguntando por que seria preciso passar pelo modo protegido em primeiro lugar
  • O 80286 tem o Machine Status Word (MSW), um registrador de 16 bits, e o 80386 o expandiu para o registrador de 32 bits CR0. Depois, o long mode de 64 bits adicionou o MSR EFER e expandiu o CR0 para 64 bits, mas, até hoje, só 11 bits do CR0 são usados, e o EFER também tem apenas 8 bits ativos
    Fico curioso por que Intel/AMD fizeram uma nova escolha duas vezes, em vez de simplesmente usar os bits sobrando nos registradores existentes: https://wiki.osdev.org/CPU_Registers_x86-64#CR0

    • Provavelmente o motivo é tornar a compatibilidade retroativa mais robusta. Software pode assumir valores para bits reservados ou escrever valores neles. A alocação de bits nesses registradores de hardware é bastante arbitrária, e também não há muito custo em usar bits mais altos
    • Em uma palavra, a resposta provavelmente é burocracia. Organizações grandes, em geral, não são especialmente boas em tomar boas decisões, e muitas escolhas sem sentido acabam surgindo
      O motivo de CR1 e CR5~CR7 ainda estarem reservados enquanto CR8 existe parece parecido
  • O que parece mais desnecessariamente complicado neste texto são o Makefile e o script do linker. O NASM oferece saída binária plana, mas aparentemente consideraram isso “hacky” demais

    • Pessoalmente, acho scripts de linker muito mais legíveis e fáceis de raciocinar do que NASM plano. Especialmente quando há vários arquivos-fonte
    • Completamente certo. Mais tarde, Makefiles e scripts de linker viram dores de cabeça importantes, mas, se você vai criar um binário plano, então simplesmente crie um binário plano. Não há necessidade de inflar isso
      Antigamente, no meu OS, eu tinha um arquivo chamado make.sh para tirar sarro disso. Hoje uso coisas chiques como “formatos de arquivo”, então -fbin e --oformat=binary viraram lembranças passageiras. Passei muito tempo tentando separar arquivos C de dados e arquivos C de código, despejá-los como binário e montar um monstro a partir disso, mas linkar e carregar ficou difícil demais. É melhor simplesmente usar ELF ou PE; na prática, parece que esses formatos fazem esse trabalho
  • Parece legal e um bom exercício, mas não sei se é útil. Fico me perguntando se existe ao menos uma UX estilo brinquedo Fisher-Price para verificar ou alterar configurações durante a execução
    Bootar é o processo de sair do modo minime, modo monousuário e modo de recuperação para um estado de decolagem
    Uso Unix junto com produtos da Microsoft desde os tempos do Xenix/DOS, acho que há uns 40 anos. Fico em dúvida sobre quanto progresso houve nesse tempo
    Também usei Linux desde a versão sueca, ou seja, desde o primeiro lançamento, e experimentei o GNU 0.1
    Peço desculpas por ter chamado Xenix de Unix. O Xenix era uma bagunça me-too que queria virar coisa do passado desde logo depois do lançamento até seu declínio
    A Microsoft parece uma empresa que não lança produtos, mas esvazia uma caixa de areia de gato em cima dos clientes. Exemplos recentes são Copilot e 22H2
    Quando vejo como carros de F1, lápis e calculadoras de bolso evoluíram, fico me perguntando o quão perto estamos de um ideal utilizável
    Também me pergunto por que o bootloader não é um modo de kernel estático. Antigamente era assim, e alguém recentemente propôs que deveria voltar a ser; eu concordei

  • https://wiki.osdev.org/A20_Line

  • É surpreendente ver que todas as etapas necessárias para colocar a CPU no modo correto parecem desnecessárias. A maioria parece ser procedimentos exigidos por compatibilidade retroativa
    Fico me perguntando se a Intel não poderia ter fornecido desde o início uma flag ou instrução para iniciar no modo correto, ou se não poderia ter removido a compatibilidade retroativa
    Lembro que o ARM64 também tinha alguns problemas parecidos. Fico curioso se existe alguma CPU que tenha sido projetada como 64 bits desde o começo, sem precisar de compatibilidade retroativa, e que entre por padrão no estado desejado. Talvez esse fosse o objetivo ou o projeto do Itanium

    • O X86S proposto pela Intel tem esse propósito

      X86S is a legacy-reduced-OS ISA that removes outdated execution modes and operating system ISA.
      The presence of the X86S ISA is enumerated by a single, main CPUID feature LEGACY_REDUCED_ISA in CPUID 7.1.ECX[2] which implies all the ISA removals described in this document. A new, 64-bit “start-up” interprocessor interrupt (SIPI) has a separate CPUID feature flag.
      [0] https://cdrdv2.intel.com/v1/dl/getContent/776648 [atenção: PDF]

    • A Intel tentou isso com o 80376, mas não deu muito certo: https://en.wikipedia.org/wiki/Intel_80376
      O mesmo vale para o Itanium, ou melhor, Itanic
      A compatibilidade retroativa é justamente o principal motivo para escolher x86 em vez de ARM, MIPS, RISC-V etc. Infelizmente, parece que algumas pessoas na Intel e na AMD não entendem bem isso
    • UEFI já existe. Se você colocar um binário parecido com o do Windows em uma pasta da partição, ele será executado em um ambiente hospedado em modo de 64 bits. Claro, também há inúmeros bootloaders que cuidam disso por você
    • Não sei qual é o problema no arm64
  • Excelente projeto. Se os defensores de UEFI aqui estão se perguntando por que alguém criaria de propósito uma nova forma de bootloader, acho que estão deixando passar o motivo pelo qual as pessoas fazem esse tipo de trabalho
    Como o autor escreveu no fim, “se você chegou até aqui, isso é legal”; é literalmente muito legal

  • Fico pensando há quanto tempo o UEFI já existe. Teria sido bom se tivessem aposentado o BIOS junto com o long mode; é uma pena

    • O BIOS já está previsto para obsolescência. Nas placas-mãe novas, essa funcionalidade é basicamente emulada por meio do UEFI e não está sendo expandida
      Dizer que está previsto para obsolescência não significa que foi removido, mas que não é mais atualizado nem desenvolvido, com a intenção de ser eliminado
  • Fico curioso se esse procedimento de boot também funciona em EFI/UEFI. Se funcionar, também queria saber se o supervisor UEFI emula as transições entre modo real, modo protegido e long mode, ou se isso é executado no hardware real

    • Não. O firmware UEFI fornece ao bootloader UEFI um ambiente totalmente diferente do ambiente de BIOS legado, isto é, do modo de endereço real. O firmware UEFI em sistemas modernos entra diretamente no long mode de 64 bits e também configura uma GDT com modelo de memória plano e paginação com mapeamento de identidade
      Escrevi aqui o processo de criar um bootloader UEFI para um SO de hobby: https://0xc0ffee.netlify.app/osdev/05-bootloader-p1.html
  • Fico me perguntando se isso é mais simples em ARM

    • É simples no sentido de que cada fabricante de placa faz as coisas do jeito que quiser. É simples para os fabricantes de placas, mas fica terrivelmente complicado para todo o resto
    • Sim. O bootloader continua sendo complexo, mas há menos configuração legada necessária. Por outro lado, se você mirar UEFI em vez de BIOS, também fica muito mais simples no x86
    • Não tenho certeza e não espero que seja. Estou me aprofundando em RISC-V agora, e ali parece haver esperança