MyToyOS: Acessando o disco sem usar a BIOS!

Uma das coisas mais simples (e tem um monte de exemplos por ai) para fazer é o código do MBR (Master Boot Record). Geralmente, alguém faz um pequeno código que mostra uma string na tela e entra em modo halt, assim:

;=======================
; BOOT.ASM
;=======================
bits 16
org 0
  ; Geralmente queremos um jump aqui para acomodar
  ; variáveis logo abaixo. Por exemplo, a string que
  ; mostraremos!
  jmp 0x7c0:_start

;------------
; Vars & Consts
;------------
msg:  db  "Hello!",13,10,0

;------------
; Início do código.
;------------
_start:
  cld             ; Certifica-se que instruções que
                  ; lidam com strings incrementarão
                  ; os ponteiros.

  mov   ax,cs     ; Ajusta DS e SS
  mov   ds,ax
  cli             ; Desabilita maskable ints antes de
                  ; ajustar a pilha.
  mov   ax,0x9000 ; Aponta para o final da RAM baixa.
  mov   ss,ax
  mov   sp,0xfffc
  sti
  
  call  setup_screen

  mov   si,msg    ; Mostra a string...
  call  puts

.halt_loop:
  hlt              ; Coloca o processador em estado de
  jmp   .halt_loop ; halt. Se houver uma int, coloca de
                   ; novo...

;------------
; Rotina que ajusta o modo de vídeo.
;------------
setup_screen:
  mov   ax,2
  int   0x10
  ret

;------------
; Rotina que imprime string na tela
; usando serviço de TTY da BIOS.
; Entrada: DS:SI = ptr.
;------------
puts:
  lodsb
  test  al,al
  jz    .puts_exit
  mov   ah,0x0e
  int   0x10
  jmp   puts
.puts_exit:
  ret

;============
; Assinatura EXIGIDA no final do setor
;============
  times 510-($-$$) db 0
  db    0x55,0xaa

Adivinha só o que acontecerá se você compilar e executar isso num ambiente virtualizado!

$ nasm -f bin -o boot.bin boot.asm
$ dd if=/dev/zero of=disk.img bs=1024 count=720
$ dd if=boot.bin of=disk.img conv=notrunc
$ qemu-system-i386 disk.img
Surpresa!
Surpresa!

Aqui cabe uma pequena explicação: Com o comando dd estou emulando um disco de 720 KiB. Não é preciso! Eu poderia, muito bem, executar o setor de boot diretamente no qemu assim: qemu-system-i386 boot.bin. O primeiro dd cria um disco vazio. O segundo, copia o setor de boot para o seu lugar… Prefiro fazer isso porque, no futuro, os dois estágios iniciais de boot podem ser copiados independentemente para seus devidos lugares na mesma imagem, usando o dd.

O programinha acima não é nada surpreendente… Mas isto não é um bootloader porque não carrega coisa alguma. No caso de um setor de boot real, provavelmente você terá que fazer uso do serviço de I/O de disco da BIOS para ler os demais setores do disco. Neste ponto, também não há nada supreendente, mas devemos lembrar que o nosso OS, eventualmente, mudará o modo de operação do processador para protegido, onde as rotinas da BIOS são inúteis. Ficamos, então, com um grande problema: Como ler/escrever setores sem usar a BIOS?

Modos de endereçamento de setores:

A BIOS usa um esquema geométrico para localizar um setor específico no disco. Três coordenadas são fornecidas: Cilindro, Cabeça e Setor. Esse esquema é chamado de CHS (‘H’ de Head) e, na INT 0x13, AH=2 essas coordenadas são passadas nos registradores DH e CX, assim:

Serviço AH=2 (Read Disk Sector)
Entrada: DL:    : disco (bit 7 setado se HD).
         AL     : número de setores para ler.
         ES:BX  : buffer onde os setores serão colocados.
         CH     : lsb do cilindro.
         CL[7:6]: 2 bits do msb do cilindro.
         CL[5:0]: setor inicial.
         DH:    : cabeça (head, de 0 a 15, no máximo!).

Saída:   CF=0 (sucesso).
         CF=1 (erro), AH=código de erro,
                      AL=setores transferidos.

Note a limitação de 10 bits para o número do cilindros e de 6 bits para o setor inicial. A controladora também limita o número de cabeças em 16. Acontece que o setor inicial de cada cilindro não começa em 0 (zero), mas em 1. Assim, a BIOS limita o número total de stores endereçáveis em 1024*63*16, ou seja, 1032192 setores (ou 504 MiB). Vale lembrar que esse serviço foi concebido numa época em que HDs de 40 MB eram enormes!

Felizmente a BIOS nos fornece serviços que permitem ultrapassar essas limitações. É o caso do serviço 0x42:

Serviço AH=0x42 (Extended Read Disk Sector)
Entrada: DL     : disco (bit 7 setado se HD).
         DS:SI  : Ponteiro para a DAP (Disk Address Packet).

Saída:   CF=0 (sucesso).
         CF=1 (erro), AH=código de erro,

Tabela DAP:
         Offset  Tamanho  Descrição
      ---------  -------  -----------------
              0    WORD   Tamanho da DAP (sempre 16).
              2    WORD   número de setores a ler.
              4    DWORD  segmento:offset do buffer.
              8    QWORD  endereço LBA.

Esse serviço tem a vantagem de usar o método de endereçamento chamado de LBA (Logical Block Addressing). Por causa das limitações do modelo CHS o modo LBA foi criado para resolver dois problemas:

  • Precisamos de mais setores que o modelo CHS pode fornecer;
  • Podemos, finalmente, nos livrar da noção de “geometria” do disco.

O LBA enxerga o disco como um grande array de setores começando no setor 0 (zero)! E, como existem dois sabores de LBA (de 28 e 48 bits, chamados respectivamente de LBA28 e LBA48), podemos endereçar até 2^{28} setores, ou seja, 128 GiB usando LBA28, chegando até ao máximo de 2^{48} setores ou 128 PiB, usando LBA48! Não é difícil conceber que a maioria das implementações de hardware atuais suporta LBA48.

O endereçamento LBA é, na verdade, uma maneira linear de agrupar os parâmetros CHS com base nos valores máximos suportados pelo disco (não é uma relação de campos binários simplesmente!). Com base nos parâmetros CHS e nos máximos, podemos calcular o LBA com a seguinte equação:

\displaystyle A_{lba}(C,H,S)=(C\cdot n_{heads}+H)\cdot n_{sectors}+(S-1)

Pelo fato do serviço 0x42 da BIOS suportar um LBA de 64 bits na tabela DAP, devo supor que a rotina seja inteligente o suficiente para usar LBA28 ou LBA48 de acordo com os bits contidos no endereço… LBA28 é mais simples de lidar e um cadinho mais rápido.

Se você precisar lidar com CHS e tem um endereço LBA, as equações também são simples:

\displaystyle \left\{\begin{matrix}  C=\frac{A_{lba}}{n_{heads}\cdot n_{sectors}}\\  \\  H=(\frac{A_{lba}}{n_{sectors}})\mod\,n_{heads}\\  \\  S=(A_{lba}\mod\,n_{sectors})+1  \end{matrix}\right.

É claro, os valores A_{lba}, n_{sectors} e n_{heads} devem estar dentro das faixas permissíveis pelo modelo CHS, citado acima.

Mas, não estamos interessados na BIOS, estamos?

Os serviços da BIOS são simples atalhos para o padrão chamado ATA. HDs que não sejam SCSI, seguem o padrão ATA (de AT Attachment) e ele dita as regras de características elétricas e de acesso via registradores, uso de interrupções e DMA. A documentação oficial do padrão ATA-6 pode ser obtido aqui. Existem documentos mais recentes, mas a maioria dos dispositivos atuais segue este padrão. Para mais documentos, você pode acessar o comitê T13 do ANSI (aqui).

Outra documentação interessante é as do Intel I/O Controller Hub (ICH) e do PIIX 3. Lá vocẽ verá que as portas de I/O para duas controladoras de HD estão mapeadas diretamente ao PCI Local Bus. Cada controladora suporta dois discos (um mestre e um escravo, no caso dos IDEs – lembra do strap chamado de Cable Select, na traseira do disco?). A primeira controladora é acessível através das portas de I/O no intervalo de 0x1f0 até 0x1f7. A segunda, de 0x170 até 0x177. Ambos os blocos são idênticos, exceto por serem de controladoras diferentes.

Os chipsets modernos aceitam até 6 HDs SATA (pode ver o manual da sua placa mãe!). Isso significa que eles possuem 3 controladoras de HD (SATA) e, cada uma suporta dois HDs. Mas, como acessar a terceira controladora? Infelizmente ela não está mapeada em I/O do mesmo jeito que as duas primeiras. Para usá-la você precisará obter os endereços de I/O mapeados em memória para o dispositivo, através do espaço de configuração para o device correspondente, no PCI Local Bus. Abaixo, mostro os 8 registradores dessas controladoras:

Offset Registrador
0  Dados
1  Erro
2  Contador de setores
3  S ou LBA[7:0]
4  C[7:0] ou LBA [15:8]
5  C[15:0] ou LBA [23:16]
6 drive[4]/H[3:0] ou drive[4]/LBA[27:24]
bit 6=0, use CHS; bit 6=1, LBA28.
7 Status (Read)/Comando (Write)

Note que podemos tanto usar o esquema de endereçamento CHS quanto o LBA28. A correspondência de CHS e LBA28, como pode ficar parecendo acima, é puramente incidental. Lembre-se que o cálculo do valor LBA depende dos limites impostos pelo dispositivo.

Para ler/escrever um ou mais setores no disco, tudo o que temos que fazer é ajustar os registradores nos offsets de 2 até 6 e escrever um comando no registrador no offset 7. Feito isso, se nao estivermos lidando com DMA e IRQs, podemos verificar o estado do HD até que ele não esteja mais “ocupado” e, se houver um erro, verificar o registrador no offset 1, caso contrário, ler/escrever os dados no registrador 0.

É simples assim… você ajusta parâmetros e envia um comando. Basta apenas seguir o protocolo do comando…

E se eu tiver um disco com mais de 128 GiB?

LBA48 parece não ser possível com a distribuição dos registradores acima. No entanto, a especificação ATA-6 nos diz que basta escrevermos duas vezes em cada um dos registradores do offset 2 até 6 para informarmos o LBA48… Note que do offset 3 até 5 temos 24 bits dos 28. Ao escrever duas vezes temos 48! A primeira escrita fornece os bits superiores em cada faixa. Na segunda escrita, os 24 bits inferiores, na ordem abaixo:

Offset 2ª escrita 1ª escrita
2 Setores [7:0] Setores [15:8]
3 LBA[7:0] LBA[31:24]
4 LBA [15:8] LBA [39:32]
5 LBA [23:16] LBA [47:40]
6 Bit 6=1 (LBA)
drive[4]
não escreve

Isso funciona com comandos de leitura/escrita extendidos.

Todos os registradores são de 8 bits, exceto o de dados

O registrador no offset 0 é sempre de 16 bits. Podemos ler/escrever 8 bits lá, mas os 8 bits superiores serão zerados. Da mesma forma, podemos ler/escrever 32 bits, mas duas escritas/leituras de 16 bits serão feitas.

Soft reset

Algumas vezes a controladora entra num estado “indefinido” ou “errado”. Para corrigir isso, ou, de outra forma, para ter um ponto de partida estável, é prudente resetá-la! No entanto, existem dois tipos de reset. Através de um comando você pode realizar um DEVICE RESET, que é mais drástico! Outra maneira é efetuar um soft reset na controladora inteira.

Isso só pode ser feito pela porta do Device Controller Register (0x3f6 para as controladoras ligadas no barramento principal do seu PC). Basta ligar o bit 2 e a controladora é colocada num estado inicial estável… E é sempre bom fazer isso, se formos lidar com transferências de dados via PIO.

Exemplo de leitura de N setores, usando LBA28, usando PIO

PIO é o modo de acesso ao dispositivo que realiza leituras/escritas sem o recurso de DMA ou IRQ. O processo deve fazer pooling do registro de status para verificar se o a controladora está ocupada ou não, antes de enviar qualquer comando ou ler/escrever dados. O exemplo abaixo lê N setores à partir do setor S do disco. Usarei algo parecido no segundo estágio do bootloader para ler o kernel e colocá-lo na memória alta. Será feito assim porque o estágio 1 (começa em 0, com o MBR) salta para o modo protegido e não poderei usar a BIOS…

; Lê N setores a partir do setor S do disco para
; a memória.
; Entrada: CL=N
;          EBX=S (LBA28)
;          ES:EDI=ptr do buffer.
;          DX=endereço base de I/O da controladora.
; Saída:   CF=0, ok
;          CF=1, erro.
; Atenção!
;   O valor de N não pode ser grande o suficiente para que o cache do
;   HD sofra overflow. É preciso verificar o máximo de setores que
;   podem ser lidos de uma única vez antes de chamar essa rotina.
;
read_lba28:
  ; OBS: Esse "soft reset" não é realmente necessário.
  ;      Está aqui para termos um ponto de partida estável.
  push dx
  mov  dx,0x3f6        ; Device Controller Register
  in   al,dx
  or   al,0x04         ; Seta apenas o bit SRST.
  out  dx,al
  pop  dx

  add  dx,2            ; aponta para sector count.
  mov  al,cl           ; Ajusta contagem de setores.
  out  dx,al
  
  inc  dx              ; Ajusta LBA28
  mov  eax,ebx
  out  dx,al
  shr  eax,8
  inc  dx
  out  dx,al
  shr  eax,8
  inc  dx
  out  dx,al
  inc  dx
  shr  eax,8
  and  al,0x0f         ; bit 4=0 (drive 0) e lba [27:24]
  or   al,0x40         ; seta bit 6 (LBA).
  out  dx,al           

  inc  dx              ; Envia comando READ_SECTORS
  mov  al,0x20
  out  dx,al

  ; Espera até a controladora terminar...
  ; OBS: Aqui temos um macete... A especificação nos
  ;      garante que cada leitura do registrador de
  ;      status toma 100ns. 4 leituras em sequência
  ;      tomarão 400ns, que é um tempo razoável para
  ;      verificar o status depois de enviar um
  ;      comando...
  times 3 in al,dx
.loop:
  in    al,dx
  mov   bl,al
  and   al,0xc0         ; BSY=0 e RDY=1?
  cmp   al,0x40
  jne   .loop

  ; Podemos testar erro aqui (bit 0)...
  test  bl,1
  jz    .continue
  stc                  ; seta CF em caso de erro.
  ret

.continue:
  ; Cada setor tem 512 bytes ou 256 words
  sub   dx,7           ; Aponta para o registro de dados.
  movzx ecx,cl
  shl   ecx,8          ; ECX=256*CL
  cld
  rep   insw           ; Lê 256*N words no buffer.

  clc                  ; zera CF indicando sucesso.
  ret

Obtendo informações sobre um drive

É claro que, antes de fazermos qualquer coisa com um disco, devemos obter informações essenciais sobre ele: Quantos setores ele têm? Ele suporta LBA? Senão suporta, qual é a geometria (CHS)? Felizmente o padrão ATA fornece um comando chamado IDENTIFY_DEVICE (comando 0xec), onde uma série de informações podem ser obtidas. Este comando funciona mais ou menos da mesma forma que a leitura de um setor, como acima, exceto que o endereço de um setor não é fornecido… O comando 0xec faz com que a controladora disponibilize 1 setor inteiro contendo informações úteis… mas, não é um setor real, vindo do disco.

Outro detalhe é que a quantidade de WORDs disponibilizadas pode estar incompleta (podemos ter menos que 256 WORDs). Por exemplo, se o disco estiver em estado de hibernação e certos dados não puderem ser obtidos até que o disco seja “acordado”. Consulte a especificação para ficar atento a esse detalhe. Abaixo um código simples, escrito usando o Borland C++ 3.1 para MS-DOS, onde obtenho esse “setor” de informação:

#include <stdio.h>
#include <dos.h>

int main(void)
{
  FILE *f;
  unsigned int data;

  // Escreve em outro disco!
  if (!(f = fopen("a:\\hdainfo.bin", "wb")))
  {
    fputs("Erro ao escrever 'a:\\hdainfo.bin'.\n", stderr);
    return 1;
  }

  /* Soft Reset */
  outp(0x3f6, inp(0x3f6) | 4);

  outp(0x1f6, 0);     // disk 0.
  outp(0x1f7, 0xec);  // IDENTIFY_DEVICE.

  // Espera por !BUSY && RDY.
  inp(0x1f7);
  inp(0x1f7);
  inp(0x1f7);
  while (((inp(0x1f7) & 0xc0) != 0x40);

  // Lê as 256 words e escreve no arquivo.
  for (i = 0; i < 256; i++)
  {
    data = inpw(0x1f0);
    fwrite(&data, sizeof(unsigned int), 1, f);
  } 

  fclose(f);

  return 0;
}

Abaixo, uma captura de tela do arquivo obtido. Compare esse dump com a estrutura retornada por IDENTIFY_DEVICE, na especificação…

virtualbox_ms-dos-6_06_01_2017_14_58_26

Dando uma olhada na tabela da especificação ATAPI poderíamos ver que este dispositivo suporta a transferência de blocos de até 128 setores, no máximo e que os comandos READ MULTIPLE e WRITE MULTIPLE são suportados. Ainda, que suporta apenas LBA28, já que o número máximo de setores disponíveis é de 4194304 (disco de 2 GB de tamanho). A controladora suporta DMA e IORDY (interrupção). A especificação suportada pelo dispositivo é ATAPI-6, dentre outras informações, como número serial (“BVf9cc8df6c-027dc7”), por exemplo.

Usando DMA e IRQ

Vou deixar isso para outro artigo. No momento, o modo PIO é suficiente para ler/escrever registros durante os estágios do bootloader. Especialmente se lidarmos com LBA…

Anúncios