Esse fim de semana andei estudando um pouco de assembly. Decidi fazer um bootstrap para ver se me animava um pouco, e acabou me voltando a vontade de realmente brincar de criar o meu sistema operacional. Como tudo precisa de um começo, decidi que o bootstrap seria um belo lugar para começar.
Vejamos então como é que fazemos para dar um boot na maquina e o que é que a(o) BIOS (Basic Input Output System) espera para iniciar o sistema operacional.
Quando o computador inicia e o POST termina, a BIOS faz uma chamada para ler o primeiro setor do disco que está definido como primário no setup. Funciona assim:
A Lista de software que usei:
Quando definimos o disco no qual desejamos dar o boot, a BIOS chama uma interrupção (19h se nao me engano), para ler o primeiro setor desse disco. O primeiro setor é o que se encontra na posição CHS (Cylinder Head Sector) 0:0:1. Cada setor no disco tem 512 bytes, então como a BIOS lê o primeiro setor do disco, nosso ‘programa’ precisa ter 512 bytes ou menos. Meu objetivo aqui não é de falar muito sobre HCS, maiores informações você pode encontrar aqui.
O que identifica se temos um setor de boot válido ou não?
O setor de boot tem uma ‘assinatura’, os últimos dois bytes do setor devem ser ‘0xAA55’. Quando essa assinatura é encontrada, os 512 bytes são carregados para a memória na posição 7C00h e o programa é executado.
Como já diria o conde Drácula em ‘Castlevania: Symphony of the Night’: “Enough talk!”.
Vamos dar uma olhada então em um código bem simples, que imprime uma string na tela.
; Boot.asm
ORG 7C00h ; Posição onde estaremos quando o código for
; carregado para a memória
mymsg: db 'Olá Setor de BOOT',10,0
xor ax,ax ; Limpando ax
mov si,mymsg ; Nossa mensagem em SI
putstr:
lodsb ; Coloca o byte apontado em SI em AL, e incrementa
; o contador
or al,al ; Verificamos se encontramos o byte '0' da string
jz hang; ; Se sim, paramos de imprimir
mov ah,0x0E ; Função para escrever
mov bx,0x0007 ; Define a página e a cor onde escrevemos
int 0x10 ; Interrupção de video
jmp putstr ; Imprimir proximo caracter
hang:
jmp hang ; Após impressão, entramos em loop infinito
times 512-($-$$)-2 DB 0 ; Preenchemos o resto da memória com '0's
; até 510 bytes
DW 0xAA55 ; Assinatura do setor de boot
Acredito que o código esteja auto explicativo. Você deve precisar manjar um pouco de assembly, então se não entendeu o código, procure estudar um pouquinho de assembly.
Vamos agora compilar o nosso código, criar um disco para o QEMU, e escrever o nosso setor de boot no disco.
$ nasm boot.asm -f bin -o boot.bin
$ qemu-img create /tmp/boot.img -f qcow 1M
$ dd if=boot.bin of=/tmp/boot.img
Com isso feito, basta agora executar o QEMU e dizer que desejamos usar o arquivo /tmp/boot.img como nosso disco.
$ qemu /tmp/boot.img -m 16
O resultado é a nossa frase impressa logo após a BIOS fazer o POST.
Bom, agora que já sabemos dar o boot, está na hora de colocarmos código no disco e executar esse código. Esse é um processo um pouco mais complicado, mas vamos que vamos!
A idéia agora é fazer com que o nosso programa de boot chame um binario que esteja gravado no disco e o execute. Então precisaremos escrever dois programas distintos: Um para ser o nosso setor de boot, e um que será chamado por ele. “Show me the code”
; Boot2.asm
ORG 7C00h
mov si,msg
prntMsg: ; Imprime a mensagem em SI
lodsb
mov ah,0x0E
mov bx,0x0007
int 10h
or al,al
jnz prntMsg
mov [drv],dl ; DL contém o identificador da unidade em que
; o setor de boot foi encontrado
; Inicializar o disco. Aqui colocamos a cabeça do disco no inicio dele
.diskSetup
mov ax,0 ; Função para resetar o disco rígido
mov dl,[drv] ; O drive que vamos resetar
int 13h ; Chama a interrupção de disco
jc .diskSetup ; Se der erro, tentamos de novo
; Após resetar o disco, colocaremos a cabeça no setor que o programa se
; encontra e carregamos o programa na memória
.diskRead
mov ah,02h ; Função para ler o disco
mov al,3 ; Ler 3 setores (512 * 3 bytes)
mov ch,0 ; Apontar para o cilindro 0
mov cl,0x02 ; Ler a partir do setor 2 (2, 3 e 4)
mov dh,0 ; Cabeça 0
mov dl,[drv] ; Disco de onde queremos ler os dados
; Os dados que lemos com essa função são armazenados em ES:BX
; No nosso caso aqui entao teremos 1000h:0
mov bx,0x1000
mov es,bx
mov bx,0
int 13h ; Interrupção do disco
jc .diskRead ; Em caso de erro, tenta de novo
jmp 1000h:0; ; Aqui nós pulamos para o nosso código que acaba de ser
; carregado na memória
hang:
jmp hang ; Loop infinito
drv db 0
msg db 'Chamando programa do HD',13,10,0
times 512-($-$$)-2 db 0 ; Completa o espaço do setor que sobra com 0s
DW 0xAA55 ; Assinatura do setor de boot nos ultimos dois bytes
Esse código então, como da para perceber, lê o código do disco, coloca o código na posição de memória 1000h:0 e pula para lá para começar a execução. Bom, precisamos agora então do programa que desejamos ler do disco e executar. Esse programa, assim como o primeiro exemplo vai simplesmente imprimir uma mensagem na tela.
; Programa.asm
mov ax, 1000h ; Atualizar os registros de segmentos
mov ds, ax
mov es, ax
mov si, msg ; Mensagem em SI
putstr:
lodsb
or al,al
jz hang
mov ah,0x0E
mov bx,0x0007
int 0x10
jmp putstr
hang:
jmp hang
msg db 'Bla bla bla!',13,10,0
Nesse programa nós atualizamos os registros de segmento para o mesmo endereço onde carregamos o programa do nosso HD, e executamos a rotina basica de imprimir a mensagem na tela, e entrar em loop infinito.
Para executar esse exemplo, os paso são parecidos. Vejamos:
$ nasm boot2.asm -f bin -o boot2.bin
$ nasm programa.asm -f bin -o programa.bin
$ qemu-img create ./boot2.img -f qcow 1M
$ dd if=boot2.bin of=./boot2.img
$ dd if=programa.bin of=./boot2.img bs=512 seek=1
$ qemu ./boot.img -m 16
O comando ‘$ dd if=programa.bin of=./boot2.img bs=512 seek=1’ coloca o nosso código no segundo setor do disco, pois conforme definimos em nosso código, é la que nosso bootstrap está esperando encontra-lo.
Bom pessoal, é isso ai. Como o tpitulo disse, é um bootstrap simples, apenas para dar um gostinho de como funciona, e trazer mais animo para maiores pesquisas.
Espero que tenham gostado e que possa ser útil para alguém.
Até o próxmo :D