Continuando a especificação do Projeto 1 (veja o post anterior aqui!), em breve estaremos lendo carregando o kernel para a memória (e ele ainda nem existe no nosso código). Contudo, temos um problema: nos é pedido que o kernel seja carregado no endereço 0x1000. Se o kernel ultrapassar 27KB, ele vai alcançar a região de memória 0x7c00, que foi onde a BIOS colocou o nosso bootloader. Se a BIOS ainda não tiver terminado seu trabalho (e ainda há muito o que fazer antes de passar o controle ao kernel), o código do Kernel sobrescreverá o bootloader, e coisas ruins podem acontecer.
Temos duas informações importantes:
Your bootloader can safely modify memory in the range [0x0a00, 0xa0000) without having to worry about overwriting video memory, the interrupt vector table, or BIOS.
Notice that a loaded larger kernel may overlap with the bootloader, so you will have to relocate the bootloader to a higher address that can't be overwritten before loading the kernel (see extra credit).
Portanto, se a gente mover o bootloader para um endereço alto, próximo da fronteira 0xa0000, não deveremos ter problemas. E como esse não é um sistema de produção, decidi mover o bootloader para o endereço 0x80000. Assim, o kernel tem espaço de sobra para ocupar entre os endereços 0x1000 e 0x80000.
Minha ideia é portanto fazer como a imagem abaixo.
Os passos que descrevem a imagem acima são:
Nossa imagem é um bloco contíguo concatenado do bootloader (512 bytes) e o kernel, que ainda não sabemos o tamanho. Se o bootloader exceder os 512 bytes, é necessário considerar isso ao ler o disco e mover o bootloader. Mas não cuidaremos disso até que seja necessário. Portanto, assumimos que nosso bootloader tem 512 bytes.
A BIOS se encarrega de colocar nosso bootloader no endereço 0x7c00 (1 - BIOS LOADING, na imagem acima). Já tratamos desse procedimento em artigos anteriores.
O bootloader corrige os segmentos, e ajusta a pilha para o endereço 0x9ffff.
O bootloader então copia o conteúdo de 512 bytes do endereço 0x7c00 para o endereço 0x80000 (2 - BOOTLOADER MOVING). Isso significa que há uma cópia do bootloader nesse novo endereço.
Fazemos um far jump para a próxima instrução no segmento 0x8000. Isso implica que estamos continuando o código de nosso bootloader, mas em outro ponto da memória!
A partir dai, o bootloader pode fazer o que for necessário, por exemplo, ler o kernel do disco e posicioná-lo em 0x1000, sem se preocupar com sobreposições. Não temos isso pronto ainda, e vamos continuar usando nossas funções
print_char
eprint_string
para testes.
O que temos de novo no nosso código? Vamos descobrir!
Criei mais três constantes. NEW_BOOTLOADER_SEGMENT
guarda o segmento da nova posição do bootloader, 0x8000. Se considerarmos que o segmento do bootloader velho é 0x07c0, então podemos usar o mesmo deslocamento para os dois, partindo de BOOTLOADER_INIT,
0x0000. Isso resulta nos endereços absolutos 0x7c00 e 0x80000 para o velho e novo bootloader, respectivamente. Por fim, defini a constante de quantos bytes eu quero mover, e como nosso bootloader tem o tamanho de 1 setor do disco, definimos a constante SECTOR_SIZE
= 512.
Como podemos copiar os bytes do bootloader de 0x7c00 para 0x80000 de forma eficiente? Podemos utilizar a instrução rep movsb
. Essa instrução copia a quantidade de bytes que está no registrador CX
do endereço DS:SI
para o endereço ES:DI
. Para isso ele incrementa automaticamente os registradores SI
e DI
a cada byte copiado (caso a Flag DF - Direction Flag tenha o valor 0), ou decrementa (caso a Flag DF - Direction Flag tenha o valor 1). Podemos alterar o valor dessa flag com as instruções CLD
- Clear Direction Flag e STD
- Set Direction Flag). Como você deve ter notado, precisamos do valor 0 nessa flag, então a instrução que devemos usar é CLD
.
Então agora ficou fácil. Precisamos definir os valores dos registradores DS
(segmento de origem - 0x07c0), ES
(segmento de destino - 0x8000), endereços de origem e destino nos segmentos, respectivamente, SI
e DI
(começaremos do 0), CX
indica quantos bytes serão copiados (512), e zeramos o direction flag (CLD
). E ai podemos executar a instrução rep movsb
. Essa parte do código ficou assim:
load_kernel:
mov ax, STACK_SEG_BOOTLOADER
mov ss, ax
mov sp, STACK_POINTER_BOOTLOADER
mov ax, SEGMENTS_BOOTLOADER
mov ds, ax
mov ax, NEW_BOOTLOADER_SEGMENT
mov es, ax
mov si, BOOTLOADER_INIT
mov di, BOOTLOADER_INIT
mov cx, SECTOR_SIZE
cld
rep movsb
before_jump:
jmp word NEW_BOOTLOADER_SEGMENT:new_boot_region
new_boot_region:
mov ax, NEW_BOOTLOADER_SEGMENT
mov ds, ax
Depois que a cópia terminou, podemos “pular” para o novo segmento para o rótulo new_boot_region,
ou seja, justamente a próxima instrução. A pilha já está no endereço certo (SS = 0x9000), os registradores CS e ES já apontam para o novo segmento 0x8000 (CS foi atualizado na instrução jmp
). Portanto, o único registrador de segmento errado ainda é o DS, e é isso que corrigimos logo após o jmp
ser efetuado no código acima.
Dai pra frente, o código é o mesmo do artigo anterior. Deve aparecer na tela a mensagem “pHello, world”. Se você executar esse código, tudo funcionará conforme imaginado.
Duas observações para esse novo código:
Primeiro, a instrução lodsb
, usada na função print_string
, também usa o Direction Flag para fazer a cópia dos bytes. Quem sabe faz ao vivo! No nosso caso, estava funcionando sem querer, mas para garantir que funcionará *sempre*, precisamos executar a instrução cld
antes da primeira execução da instrução lodsb
. Isso foi corrigido nessa versão.
Segundo, depois da cópia do bootloader, os endereços 0x7c00 e 0x80000 possuem o mesmo código certo? Portanto, para facilitar a depuração com o gdb
, é útil adicionarmos os símbolos no novo endereço também no arquivo debug_BoringOS.sh:
-ex "add-symbol-file $CURRDIR/build/bootloader.o 0x80000"
Contudo, fazer breakpoints usando os símbolos agora não funciona mais, pelo menos não da forma como se é esperado. Afinal, temos os mesmos símbolos em dois endereços diferentes. Se eu fizer um hbreak *before_jump,
onde ele colocará o breakpoint? Na minha execução aqui, ele colocou aqui:
Hum… ele colocou no endereço do novo bootloader (0x8004f
). Acredito que ele sobrescreveu o endereço do símbolo before_jump
quando o segundo add-symbol-file
foi executado. E o engraçado é que se eu pedir para continuar a execução agora, esse breakpoint nunca será executado, já que ele está antes do jmp
no segmento 0x8000, mas o jmp
pula para o rótulo new_boot_region, que fica depois. Enfim, isso não é nenhum problema, e é o comportamento esperado.
Para evitar essa situação, se precisar pular para algum símbolo do arquivo bootloader.o, o melhor a ser feito é carregar os símbolos em apenas um dos dois endereços. Ou então use breakpoints com endereços absolutos. Se não precisar de breakpoints com símbolos, carregar o mesmo símbolo nos dois endereços é uma boa ideia na visualização da execução no gdb
.