Escrevendo um bootloader BIOS para modo de 64 bits
(thasso.xyz)- 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,objcopye 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 0x13da 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.PGe 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
- A BIOS procura um disco cujo primeiro setor termine com o número mágico
- 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
- Para acompanhar, são necessários o Intel 64 and IA-32 Architectures Software Developer’s Manual, um assembler e o QEMU
- O assembler do exemplo é o nasm, e o texto assume familiaridade com assembly x86 e a sintaxe do nasm
- Mesmo sem uma CPU x86_64, é possível executar tudo emulando uma CPU x86 no QEMU
- Como material básico até o modo de 32 bits, o texto recomenda Writing a Simple Operating System — from Scratch
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 comhlte um loop - Para imprimir a string, usa a chamada de video services da BIOS
int 0x10comah = 0x0e - O
Makefilecria o objeto com nasm, faz o link com um linker script e gera uma imagem de boot raw comobjcopy -O binary make bootexecuta a imagem no QEMUqemu-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_sectoremMEMORYé0x7c00, com tamanho512 - A seção
.bootsignadiciona0x55,0xaana posição0x7c00 + 510
- A origem
- 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 0x13ah = 0x42é o recurso de leitura estendida da BIOSdl = 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 64para 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_stringdo 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
clidesativa as interrupçõeslgdt [gdt32_pseudo_descriptor]carrega no GDTR o endereço e o tamanho da GDT- Define
cr0.PE, isto é, o bit 0 decr0, para ativar o protected mode - Um far jump limpa o pipeline de instruções e atualiza
cspara o novo code segment
- Depois de entrar em protected mode, os valores antigos de segmento deixam de fazer sentido, então
ds,ss,es,fsegssã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_string32grava bytes de caractere e cor no endereço0xb8000- O valor de cor é
0xf - Cada célula de caractere usa 2 bytes
- O valor de cor é
- 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_tablecria 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
- O tamanho de página é
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
cr3o endereço da tabela PML4 - Define o bit 5 de
cr4para ativar PAE - Lê o MSR
0xc0000080e defineEFER.LME, isto é, o bit 8 - Define o flag
PG, o bit 31 decr0, 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
- Cria uma tabela de páginas de 4 níveis no endereço
- 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.climpa o buffer VGA0xb8000e imprime"Hello from C"- No lado do assembly,
start_long_modedeclaraextern _start_kerneldepois 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, tamanho512stage2:0x7e00, tamanho512kernel:0x8000, tamanho0x10000
- As seções
.text,.data,.rodatae.bsssão posicionadas na região do kernel - O
Makefilepassa 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 compilador C é o
- O código completo do exemplo é fornecido em link para download
1 comentários
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
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
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
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
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 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
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
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
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