Senhas

Acho que você, provavelmente, já topou com esse tipo de regra para criação de senhas: Mínimo de 6 caracteres, contendo letras, números e caracteres especiais. E troque a senha de tempos em tempos… A maioria das empresas que conheço usam essas regra simples. Sabe de onde elas vieram?

Tem um monte de gente que acha que isso é uma daquelas coisas de “senso comum”, mas a regra veio de uma recomendação do NIST (National Institute of Stantards and Technology), um instituto governamental norte-americano que dita normas usadas pelo governo e que acabam sendo acatadas pela indústria em geral… Pois bem, este padrão foi criado em 2009 e aposentado em 2016 (veja aqui) porque este tipo de metodologia não ajuda muita coisa, ao contrário do que pensam alguns “especialistas” em segurança! Na realidade, só atrapalham o maior interessado: o usuário!

Hoje, com GPUs mais poderosas, é possível “adivinhar” uma senha de 6 caracteres em poucos segundos, usando força bruta! Alguns algoritmos conseguem testar bilhões de senhas possíveis por segundo. Uma senha montada de acordo com a regra citada anteriormente, mesmo considerando que as letras possam ser diferenciadas entre maiúsculas e minúsculas, pode ser quebrada em menos de 1 dia num PC (alguns minutos num supercomputador ou num cluster de GPUs).

Recomendo que você dê uma olhada no site GPC’s Password Haystacks para avaliar a sua senha…

Um outro esquema usado é a transmissão e armazenamento de hashs (MD5 ainda é usado em aplicações, hoje em dia!) e, ainda por cima, com um “salzinho” para tornar a coisa mais difícil… Acontece que o argumento acima continua valendo: Embora uma senha com hash SHA256 seja mais difícil de “quebrar”, processadores e GPUs modernas são capazes de testarem, literalmente, bilhões de possibilidades por segundo… Se você usar um cluster de GPUs, através de OpenCL, literalmente trilhões de testes por segundo podem ser feitos… Eis um deles:

Computador do meio com 7 placas GTX1080 em SLi, só para cracking!</small)
Computador do meio com 7 placas GTX1080 em SLi, só para cracking!

O software usado? Está disponível gratuitamente e é FOSS. Por exemplo, o ocl-hashcat e o rainbowcrack.

Senhas de 6 caracteres e, hoje em dia, inferiores a 12 (até 2016, pelo menos!), são facilmente quebráveis… Mas, e quanto a trocar a senha frequentemente?

Existe o fator psicológico aqui… Forçar o usuário a trocar a senha frequentemente faz com que ele tenta a usar a mesma senha, com algumas pequenas modificações… Por exemplo, se a senha for “secreta” (exemplo de senha!), das próximas vezes ele mudará para “secreta2”, “secreta3”, “secreta!”, “secreta?!” etc. É claro que o usuário tende a fazer isso porque não precisará se lembrar das novas senhas, mas apenas da pequena modificação.

O que fazer?

A primeira coisa é evitar usar esquemas de autorização de acesso que peçam login/senha. Por exemplo, uma grande quantidade de sites vêm usando a estrutura de autenticação do Google e do Facebook para permitirem acesso de usuários… O Google e o Facebook tiveram um trabalhão para criarem um esquema de login que seja seguro o suficiente e, como eles fornecem isso como serviço, tendem a manter o esquema cada vez mais seguro… Se seu sistema é online, usar a autenticação do Google pode ser uma boa idéia, especialmente por causa do Android…

Mas, se sua aplicação não for online, ao invés de usar login e senha, vale a pena investir um pouco num “security token”… Esses “tokens” podem ser usados na forma de smartcards, cartões RFI ou pendrives. Em essência, você tem um certificado do usuário, assinado pelo proprietário do software, que lhe permitirá tanto verificar a autenticidade da identidade do usuário (assumindo que só ele tem o token) quanto a autorização para o acesso… NENHUMA SENHA É NECESSÁRIA!

Mas, mesmo que login/senha seja algo que você não quer abrir mão, forçar um conjunto de regras pode ser uma boa ideia desde que matematicamente eficientes (“senso comum” não vale de nada!)… Por exemplo: Forçar o mínimo de 6 caracteres com letras, números e especiais é boboca porque uma senha como “aaaa@1” é válida e, provavelmente, uma das primeiras que vai ser testada num método de força bruta… A regra mais interessante é garantir que a entropia da senha seja maior que um certo valor… Infelizmente, calcular entropia de informação é algo um tanto quanto complicado (veja aqui) e, quando se aplica a senhas, meio subjetivo…

Não tema! Um dos meios mais seguros, do ponto de vista do usuário, atualmente, é o uso de passphrases e pode ser exemplificado por essas tirinhas do xkcd:

password_strength

Como qualquer pessoa pode perceber, bolar uma frase sem sentido, engraçada e que lembre alguma coisa apenas para o usuário em questão, além de aumentar a quantidade de bits de entropia (calculado como n\lg\,c, onde c é a quantidade de caracteres que podem ser usados numa única posição e n é o tamanho da string), torna a senha muito mais fácil de lembrar e difícil de ser “adivinhada”.

Durante algum tempo usei a senha (cunhada por um conhecido) “chuta que dá, certo?”…

MyToyOS: Usandao a lib gnu-efi

Podemos, é claro, codificar um bootloader EFI manualmente, mas também podemos usar a libgnuefi e aproveitarmos todos os recursos que o padrão e a BIOS podem nos oferecer. Para tanto, basta instalar:

$ sudo apt-get install gnu-efi

Eis um “hello, world”:

#include <efi/efi.h>
#include <efi/efilib.h>
 
EFI_STATUS EFIAPI efi_main(
    EFI_HANDLE ImageHandle, 
    EFI_SYSTEM_TABLE *SystemTable)
{
  InitializeLib(ImageHandle, SystemTable);
  Print(L"Hello, world!\n");
  return EFI_SUCCESS;
}

O detalhe é como compilar e montar o arquivo UEFI. Eis um Makefile:

#
# HELLO.EFI makefile
#

hello.efi: hello.so
    objcopy -j .text \
      -j .sdata \
      -j .data \
      -j .dynamic \
      -j .dynsym \
      -j .rel \
      -j .rela \
      -j .reloc \
      --target=efi-app-x86_64 \
      $^ $@

hello.so: hello.o
    ld $^ \
      /usr/lib/crt0-efi-x86_64.o \
      -nostdlib \
      -znocombreloc \
      -T /usr/lib/elf_x86_64_efi.lds \
      -shared \
      -Bsymbolic \
      -L /usr/lib \
      -l:libgnuefi.a \
      -l:libefi.a \
      -o $@

hello.o: hello.c
    gcc -c \
      -fno-stack-protector \
      -fpic \
      -fshort-wchar \
      -mno-red-zone \
      -I /usr/include/efi \
      -I /usr/include/efi/x86_64 \
      -DEFI_FUNCTION_WRAPPER \
      -o $@ $<

Eis a explicação para os comandos. O compilador, obviamente, irá criar um objeto que não contém referências à libc e código independente de posição (Position Independent Code, ou PIC). É necessário informar o diretório dos headers da gnu-efi… Logo depois criamos um shared object “linkando” o objeto com as libs estáticas libgnuefi.a e libefi.a, também com o objeto de inicialização crt0-efi-x86_64.o. A opção -Bsymbolic é importante para manter todos símbolos auto contidos no shared object.

É claro que UEFI não suporta o formato ELF e, por isso, usa-se o utilitário objcopy para copiar as sessões dela para o novo arquivo EFI. As opções -j dizem quais sessões devem ser copiadas (se existirem) e --target informa o formato do arquivo de saída: efi-app-x86_64 para aplicações EFI, efi-bsd-x86_64 para “bootstrap driver” (ou bootloader) e efi-rtd-x86_64 para “runtime driver”.

Pode ser uma boa idéia acrescentar as opções -ffreestanding e -nostdlib na linha de comando do GCC… Se não retirarmos o stack-protector e as red-zones (esse código ai é para x86-64!), o executável não funcionará!

Criando uma partição GPT de teste

Para criarmos um disco “virtual” de 100 MiB (195312 setores) com uma partição GPT e colocarmos o programinha acima na partição EFI, podemos fazer:

# dd if=/dev/zero of=disk.img bs=512 count=195312
# gdisk disk.img
GPT fdisk (gdisk) version 1.0.1

Partition table scan:
  MBR: not present
  BSD: not present
  APM: not present
  GPT: not present

Creating new GPT entries.

Command (? for help): o
This option deletes all partitions and creates a new protective MBR.
Proceed? (Y/N): Y

Command (? for help): n
Partition number (1-128, default 1): 1
First sector (34-63966, default = 2048) or {+-}size{KMGTP}: 
Last sector (2048-63966, default = 195279) or {+-}size{KMGTP}: 
Current type is 'Linux filesystem'
Hex code or GUID (L to show codes, Enter = 8300): ef00
Changed type of partition to 'EFI System'

Command (? for help): w

Final checks complete. About to write GPT data. THIS WILL OVERWRITE EXISTING
PARTITIONS!!

Do you want to proceed? (Y/N): Y
OK; writing new GUID partition table (GPT) to disk.img.
Warning: The kernel is still using the old partition table.
The new table will be used at the next reboot or after you
run partprobe(8) or kpartx(8)
The operation has completed successfully.

# losetup --offset 1048576 --sizelimit 98934272 /dev/loop0 disk.img
# mkdosfs -F32 /dev/loop0
mkfs.fat 3.0.28 (2015-05-16)
Loop device does not match a floppy size, using default hd params
# mount /dev/loop0 /mnt
# cp hello.efi /mnt/
# umount /dev/loop0
# losetup -d /dev/loop0

Os valores passados para losetup merecem uma explicação. Como criamos a partição EFI no setor 2048 (como sugerido pelo gdisk), o offset inicial é 2048\cdot512. O tamanho dessa partição é de (195279-2048)\cdot512=98934272 bytes, onde 195279 é o último setor da partição. Usar losetup permite mapear um pedaço de um arquivo num dispositivo de loopback e ai podemos formatá-lo e montá-lo…

Se você pretende criar uma imagem de um disco mais “realista”, coloque uma partição pequena para EFI (FAT32) — digamos, uns 32 MiB — e outra partição vazia que será formatada depois (com o sistema de arquivos do MyToyOS).

Testando o novo “HD” no QEMU

Temos que, antes, instalar um pacote chamado ovmf. Ele contém uma BIOS que suporta UEFI 2.6 e, para iniciar a emulação basta fazer:

$ qemu-system-x86_64 -bios /usr/share/ovmf/OVMF.fd \
  -drive file=disk.img,if=ide

Repare que não temos um bootstrap, mas uma aplicação EFI. Podemos executá-la do shell, bastando selecionar o drive FS0: e digitar hello.efi:

efiapp
Grab da tela do qemu-system-x86_64

Vantagens e desvantagens:

As óbvias vantagens são as de que este programinha executa no modo protegido x86-64! A lib gnu-efi toma conta disso pra você… A outra é que todas as funções padronizadas EFI estão a sua disposição…

A desvantagem, também óbvia, é que o executável fica grande. Note que para imprimir uma simples string de 14 caracteres (incluindo o salto de linha final) criou um arquivão de quase 45 KiB!

MyToyOS: UEFI, a assombração reaparece!

Quanto mais eu penso que me livrei dos padrões da Microsoft, mais eles me perseguem…

Recentemente, no grupo “C & Assembly para x86-64”, no Facebook, eu disse que migraria o esboço do bootloader do MyToyOS para o padrão UEFI. O motivo é simples: O particionamento tradicional não permite discos muito grandes e UEFI tem a vantagem de não precisamos lidar com vários estágios de carga do bootloader.

Como UEFI funciona?

Nada mais simples: Trata-se de um simples arquivo executável (.EXE, mas com a extensão .EFI) no formato Portable Executable da Microsoft! Eis a assombração! O formato é esse:

Teremos que lidar com essa "engronha"...
Teremos que lidar com essa “engronha”…

O significado de cada campo pode ser obtido na especificação do padrão UEFI (aqui) e na especificação do formato PE (aqui).

Mas é interessante notar: O formato usado no UEFI é simplificado com relação a esse ai. Algumas sessões não existem nos arquivo .efi e a diferenciação sobre o que o executável faz é determinada pelo subsistema… Existem, pelo menos, 3 tipos de executáveis: Aplicações, bootloaders e firmware drivers.

Em essência, tudo o que temos que fazer é um arquivo em assembly contendo o header do formato cima, preenchido, e todas as demais rotinas podem ser feitas com o GCC (4.9 ou superior, porque eles permitem geração de código em 16 bits!).

Sim… o código é em 16 bits, como um executável para MS-DOS…

O que a ROM BIOS de sistemas que suportam UEFI faz é obter o arquivo da partição EFI e executá-lo… O arquivo pode ter, em teoria, até 2 GiB de tamanho. Na prática, é prudente mantê-lo com um tamanho inferior a 128 KiB (dois segmentos – não sei onde esse arquivo é carregado!), mas isso não é um limite rígido… A vantagem, é claro, é que não estamos mais restritos a um único setor de boot carregado pela BIOS! De fato, o código do MBR (Master Boot Record) desaparece completamente. Em uma de minhas máquinas de teste, onde tenho somente o UBUNTU 16.04 instalado. Obtendo o setor da MBR (LBA 0):

# dd if=/dev/sda of=disk.img bs=512 count=1

Obtenho isso:

part
Cadê a MBR? Não existe! Ou seja, temos apenas uma única entrada da tabela de partição tradicional (a partir do offset 0x1be, usada pela especificação da GUID Partition Table) e, mesmo assim, trata-se de uma entrada desabilitada, pois começa com 0x00, ao invés de 0x80. Provavelmente essa entrada aponta para a partição EFI.

A partição EFI, por sua vez, é uma partição FAT32:

# fdisk -l /dev/sda
Disk /dev/sda: 465,8 GiB, 500107862016 bytes, 976773168 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 4096 bytes
I/O size (minimum/optimal): 4096 bytes / 4096 bytes
Disklabel type: gpt
Disk identifier: 55DC090F-5E38-4521-A4E6-7B669BBB9ED6

Device         Start       End   Sectors   Size Type
/dev/sda1       2048   1050623   1048576   512M EFI System
/dev/sda2    1050624 960253951 959203328 457,4G Linux filesystem
/dev/sda3  960253952 976771071  16517120   7,9G Linux swap

# fdisk -l /dev/sda1
Disk /dev/sda1: 512 MiB, 536870912 bytes, 1048576 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 4096 bytes
I/O size (minimum/optimal): 4096 bytes / 4096 bytes
Disklabel type: dos
Disk identifier: 0x00000000

O que tem dentro dessa partição? Os arquivos executáveis e, eventualmente, configurações:

# df /dev/sda1
Filesystem     1K-blocks  Used Available Use% Mounted on
/dev/sda1         523248  3684    519564   1% /boot/efi

# tree /boot/efi/
/boot/efi/
└── EFI
    └── ubuntu
        ├── fw/
        ├── fwupx64.efi
        ├── grub.cfg
        ├── grubx64.efi
        ├── MokManager.efi
        └── shimx64.efi

O grub colocou esses arquivos todos ai… Cada um deles, se estiver no formato correto, é executado pela BIOS quanto o sistema inicia. Simples assim. No caso, grubx64.efi lê o arquivo grub.cfg para “bootar” o sistema na partição correta. O executável fwupx64.efi, provavelmente, é usado pelo grub para o caso de termos firmwares fora do padrão, onde drivers são colocados no diretório fw/. Não me pergunte o que seria o MokManager e o shimx64… pergunte à documentação do grub 2.

Por que a especificação é tão grande?

O documento da especificação UEFI mostra um conjunto de facilidades disponíveis quando você usa o UEFI SDK (Software Development Kit). Por exemplo, o executável pode pedir para a BIOS iniciar o modo protegido pra você, tanto em 32 bits quanto em 64… Mas, prepare-se para adotar um esquema bem diferente de convenções de chamadas de funções e obter um executável gigantesco no processo…

Não recomendo o uso do SDK por esse motivo e por outro mais prático: Uma das reclamações da adoção do padrão EFI é justamente o lobby que empresas como a Microsoft têm feito para criar bootloaders “seguros” que exijam o uso de criptografia. Na prática, eles querem controlar quem tenta instalar seus sistemas (somente comprando uma chave vocẽ poderia “bootar” o Windows, por exemplo)… Isso só é possível ao usar o UEFI SDK.

E quanto ao MyToyOS?

Bem… sem a limitação do tamanho dos estágios 0 e 1 podemos criar um bootloader mais complexo, até mesmo em modo gráfico (320×200 de 256 cores, VGA, por exemplo, ou usando um dos modos VESA). Além do header MZ e PE, o código pode ser quase totalmente feito em C e terá que achar onde se encontra a partição na GPT, pular para o modo protegido, carregar o kernel na memória alta e pular para ele… Vale a pena dar uma olhada no código de boot do Linux (no diretório arch/x86/boot/ dos fontes mais recentes). O arquivo header.S é o início de tudo…

Em teoria, tudo o que temos que fazer é colocar o executável num diretório EFI/mytoyos/ na partição EFI… Bem como criar a partição com o kernel… Dá até vontade de fazer uma brincadeira como o logo do AmigaDOS:

amigados

Hehehe…

Softwares de virtualização aceitam EFI?

Aceitam… No VirtualBox basta habilitar a opção:

vbox

No caso do QEMU, provavelmente você terá que instalar o pacote ovmf.

Ao que parece, no VMWare, existe um tweaker que habilita EFI também (não tenho certeza, não uso VMWare!).

A arquitetura “maluca” dos PCs

Uma das grandes dificuldades de desenvolver hardware e software de baixo nível (sistemas operacionais) para o PC-AT é a arquitetura dessa “Advanced Technology” (o AT, depois do PC). Hoje os PCs tinham que ser conhecidos como PC-PT (Patched Tecnology ou “tecnologia remendada”, não confundir com o partido, senão eu estaria usando um Mac aqui!) .Os PCs são monstrengos que carregam características legadas desde seu surgimento, em 1981.

Eis alguns exemplos…

Acesso à memória:

Até o início dos anos 80 os microcomputadores que reinavam eram os de 8 bits, baseados em processadores da Zilog (Z-80) ou MOS Technology (6502). Em 1981 a IBM lançou o PC original, totalmente “open source”, digamos assim, como o primeiro computador pessoal (Personal Computer, ou PC) com arquitetura de 16 bits, baseado no processador da Intel 8088… Isso foi novidade porque:

  • Era um computador da IBM, ora bolas;
  • Tinha duas vezes mais bits!!!
  • Processadores Intel não eram considerados tão bons assim;
  • Finalmente existia um padrão a ser seguido…

A terceira afirmação vem do fato de que, diz a lenda, a Zilog foi uma empresa formada de engenheiros dissidentes da Intel — os mesmos do projeto do 8080. Eles melhoraram um bocado os barramentos (nada daquela história de multiplexar dados com endereços!), algumas instruções de manipulação de 16 bits foram adicionadas e o conjunto de instruções ficou mais interessante: Pelo menos no contexto dos mnemônicos, nada da diferença entre MVI e MOV, por exemplo.

Também diz a lenda que a escolha pelo processador da Intel para os PCs deveu-se ao aconselhamento da Microsoft (maldição!), que tinha sido contratada para desenvolver o PC-DOS (mais tarde renomeado de MS-DOS)… A ideia original da IBM era adotar o CP/M como padrão, mas a Digital Research, dona do sistema, não conseguiu entregar no prazo o CP/M-86… Não é à toa que o MS-DOS lembre muito o CP/M… Suspeito, mas não tenho como confirmar, que a IBM, antes de adotar os processadores Intel, tinha a intenção de criar o PC com base nos MC68000 — o que seria uma jogada de mestre, já que esse processador já era usado em alguns mainframes!

Outra grande vantagem é que, mesmo sendo um microcomputador com arquitetura de 16 bits, o barramento de endereços permitia o uso de 20 bits, ou 1 MiB de memória acessível. Na época, os computadores de 8 bits tinham, no máximo, 64 KiB de memória (alguns usavam o esquema de chaveamento de bancos de memória para usar mais, mas era raro!)… Eu mesmo já tive um Apple ][+ com 48 KiB de RAM!

Acontece que para conseguir um endereço de 20 bits usando registradores de 16 bits foi necessário adotar um esquema estranho, típico das gambiarras da Intel… A memória era dividida em blocos de 64 KiB, mas, cada bloco começa num endereço físico múltiplo de 16. Ou seja, existem “segmentos” de memória com 64 KiB de tamanho… O endereço físico é, então, calculado com base em dois registradores: Um “seletor” de segmento e um offset:

\displaystyle Addr_{fisico}=(segmento\,shl\,4)+offset

Tanto “segmento” quando “offset” têm 16 bits de tamanho. Isso cria um problema: Se ambos forem 0xffff, o endereço físico resultante terá 21 bits de tamanho: (0xffff\,shl\,4)+0xffff=0x1ffef. Repare no ‘1’, mais à esquerda!

A intel resolveu incorporar esse “problema” na nova geração de processadores, o 80286 e o “bug” virou “feature”… Surge também o PC-AT que, para manter compatibilidade com o velho PC, incorporou um sinal chamado “Gate A20“. O “gate” aqui é uma porta lógica AND que mascara o bit A20 do barramento de endereços. Ou seja, se o sinal do “Gate A20” estiver desabilitado, o bit A20 será automaticamente zerado, senão a porta deixará passar o A20 fornecido pelo processador.

Numa época onde temos capacidade de memória acima de 8 GiB o Gate A20 perdeu sua serventia, não é? Infelizmente não! Ele ainda existe!

Outro problema com o esquema segmento:offset é que é possível codificar o mesmo endereço físico de memória de várias formas possíveis. Tomemos o exemplo do endereço físico de 20 bits 0x00123… Ele pode ser escrito como 0x0012:0x0003, 0x0011:0x0013, 0x0010:0x0023, … 0x0000:0x0123. Isso, para quem está aprendendo assembly é uma confusão dos diabos!

No modo protegido isso foi corrigido mudando-se a semântica do que seria um “segmento”. A parte “segmetno” virou um “seletor” de uma tabela que descreve o bloco de memória acessível pelo offset (mais uma gambiarra da Intel).

Diversos “modos” de operação diferentes:

Seu processador pode funcionar em 4 modos diferentes:

  • Modo real: Igualzinho ao velho PC com o 8088, mas usando os registradores e instruções estendidas (EAX, EBX, …). Ainda é possível acessar apenas 1 MiB de memória física (mais 64 KiB se o gate A20 estiver habilitado);
  • Modo protegido (32 bits): O esquema segmento:offset é completamente diferente. Pode-se acessar até 4 GiB de memória física (ou 64 GiB de memória virtual). Existem privilégios e isolamento de código e dados entre processos. Suporta multithreading com auxílio do processador;
  • Modo protegido (32 bits com extensões de 64 bits): Mesma coisa que o modo de 32 bits acima, mas permite acessar mais memória (até 256 TiB de memória física, ou 4 PiB de memória virtual). Estende os registradores para 64 bits (RAX, RBX, …). Adiciona mais registradores… No entanto, continua sendo um modo de 32 bits, chamado pela Intel de IA-32e;
  • Modo Virtual 8086: Emula o modo real dentro do modo protegido;
  • Modo SMM (System Management Mode): Usado para diagnósticos e é um modo completamente diferente dos acima.

Nos modos protegidos os registradores seletores de segmento indicam uma entrada numa tabela que contém a descrição do bloco de memória que pode ser endereçado… É como se você dissesse: “use o segmento nº 1” e, na primeira entrada da tabela, temos: “este segmento começa em 0x00000000, tem 4 GiB de tamanho, só pode ser lido e pode conter instruções executáveis, e só pode ser acessado por processos com privilégio 0″… Se um processo com privilégio 3 (o menor possível) tentar acessar um dado deste segmento, acontece um erro. Se qualquer processo tentar escrever nesse segmento, acontece um erro. Se usarmos um offset que não esteja dentro da faixa de endereços permitidos para esse segmento, acontece um erro…

É uma ideia interessante… mas, porque diabos continuar suportando todos os modos de operação legados? Em modo protegido as instruções continuam funcionando como antes, somente com restrições sobre o seletor, como mostrei acima, e estendendo o tamanho dos segmentos… Dessa forma, o modo real é completamente supérfluo, mas os PCs continuam a usá-lo e os processadores continuam entrando nesse modo, por default, durante o power up… Para que isso funcione a Intel teve que fazer mais uma gambiarra. Dê uma olhada abaixo:

; No modo real:
B8 00 00          MOV AX,0
66 B8 00 00 00 00 MOV EAX,0

; No modo protegido:
66 B8 00 00       MOV AX,0
B8 00 00 00 00    MOV EAX,0

Reparou que o prefixo 0x66 tem significados diferentes, de acordo com o modo de operação? No modo real ele diz ao processador que o registrador é EAX, ao invés de AX, para o micro-código 0xB8 (MOV AX,imm). Já no modo protegido é o contrário… Isso torna os códigos em assembly incompatíveis para os dois modos principais do mesmo processador! E isso, meus amigos, é o que eu chamo de gambiarra!

Para mim, os processadores modernos deveriam entrar no modo protegido, ajustando apenas 2 segmentos no ring 0: código e dados. Toda a BIOS poderia ser em modo protegido e não precisaríamos fazer a transição para esse modo… Se precisássemos executar código legado em 16 bits, existe o modo Virtual 8086!!! Assim, apenas 2 modos seriam necessários: O modo protegido de 32 bits, com as extensões de 64 bits, e o Virtual 8086.

Dispositivos:

A necessidade de manter hardware compatível com padrões é comendável, mas o PC leva isso ao extremo… Os antigos controladores de temporização (PIT), interrupção (PIC) e DMA (DMAC) continuam acessíveis, mesmo que de forma emulada… De fato, pode-se dizer que o barramento ISA (que surgiu no PC padrão) continua existindo até os dias de hoje… Isso só faz sentido por causa do modo real..

Mesmo assim, o PIT legado continua tendo uma base de tempo muito inferior (e com um valor maluco) do que o clock do sistema (1.19187 MHz, para ser exato!). O PIC continua suportando apenas 14 requisições de interrupção e o DMAC continua podendo realizar transferências de blocos com, no máximo, 64 KiB de tamanho… Quanto ao DMAC, embora o circuito integrado aceite transferências de memória para memória, isso não é implementado no PC!

O DMAC é um exemplo da busca eterna por compatibilidade reversa levada ao extremo. Mesmo que hoje tenhamos PCHs (Platform Controller Hubs) que fazem o trabalho de refresh das DRAMs, de forma transparente, o canal 0 (zero) do DMAC ainda é reservado para esse motivo…

Claro que existem versões mais modernas do DMAC, implementadas nos PCHs, mas elas são acessíveis apenas no modo protegido porque o acesso a esses recursos é feito em I/O mapeada no topo da memória, dentro dos primeiros 4 GiB. Lembre-se que o modo real só acessa 1 MiB + 64 KiB.

Cada um faz como quer

Ok… mesmo com essas tranqueiras, existem padrões, certo?

Acontece que fabricantes diferentes costumam fazer as coisas de formas diferentes… Um exemplo é o modo de habilitar o sinal Gate A20: Alguns exigem o uso do bit A20M da porta de controle A (PS/2 em diante), outros exigem que o controlador de teclado (heim!? o que o teclado tem a ver com memória?! gambiarra!!!) habilite esse sinal. Outros, nem isso… alguns escondem como A20M possa ser habilitado e exigem que seja feito pela BIOS! Outras estranhezas: Alguns fabricantes criam um buraco de 15 MiB acima do primeiro MiB (IBM Thinkpad e alguns Compaq, por exemplo). A maioria não faz isso…

Existem diversos pequenos quirks de fabricante para fabricante e para manter o sistema operacional funcionando em todos é necessário uma série de verificações… O código, obviamente, tende a ficar maior que o necessário!

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…

MyToyOS: Acesso direto à memória

Continuando a série sobre controladores, além das requisições de interrupções alguns dispositivos podem pedir para o processador que este libere os seus barramentos de endereço e dados para que o próprio dispositivo lide com blocos de dados contidos na memória… É o caso de HDs, por exemplo, quando pedimos para que alguns setores sejam lidos o circuito do HD pode colocar esses dados diretamente na memória, sem a interferência do processador…

Da mesma forma que ocorre com as interrupções, o processador tem apenas um sinal de pedido de liberação do barramento (DRQ, de DMA Request, ou HREQ, de Hold [Bus] Request). No entanto, podemos ter n dispositivos que podem pedir que o processador entregue o controle. Como fazer para lidar com todos eles? Novamente entra a figura de um controlador, o DMAC (Direct Memory Access Controller).

Vou esboçar aqui o Intel 8237A (datasheet pode ser baixado aqui), que é o controlador de DMA legado da arquitetura dos PCs. Esse chip tem sérias limitações porque acompanha os PCs desde sua primeira concepção, na época em que usávamos o processador 8086, de 16 bits (a mesma coisa acontece com o PIC, descrito nos artigos anterior)… Por exemplo: Esse circuito integrado só permite transferências de blocos de dados de, no máximo, 64 KiB e o endereço base também está limitado em 16 bits. Isso significa que, se usarmos o DMAC legado (que ainda está presente de forma emulada) nos sistemas atuais, o buffer onde os dados serão transferidos só podem ser localizados na memória baixa.

Em outro artigo descreverei o novo DMAC, que permite transferências com tamanhos de 32 bits… Este “novo” DMAC está presente em chipsets especializados e depende da arquitetura, por isso o DMAC tradicional, diferente do que acontece com o PIC e o APIC, ainda é o mais usado…

Como o DMA funciona?

Desconsiderando o DMAC, um dispositivo coloca, no sinal HREQ, um sinal de nível 1 pedindo ao processador que este libere o barramento (coloque os barramentos de dados, endereço e controle, em estado de alta impedância). O processador, quando resolver atender o pedido, coloca nível 0 no sinal HACK# (Hold Acknowledge) e abre os circuitos dos barramentos… Neste ponto o dispositivo tem total controle do acesso à memória. Quando o sinal HREQ for zerado, o processador assume, de volta, o controle do barramento.

E como o DMAC ajuda?

O 8237A possui 4 entradas de requisição de DMA, nomeadas de DRQ0 até DRQ3. Da mesma forma como ocorre com o PIC, esses DMA são priorizados (se dois ocorrerem, um deles será atendido antes do outro). Em essência, o DMAC pedirá ou enviará dados de ou para o dispositivo, bem como de ou para a memória e, quando acabar, informa o fim do processo (sinal EOP# ou End Of Process) e, por sua vez, libera os barramentos de volta para o processador.

O DMAC também substitui, do ponto de vista do dispositivo, os sinais HREQ e HACK#, do processador, pelos sinais DRQn e DACKn#, onde n corresponde ao canal de DMA usado pelo dispositivo. É fácil perceber que o 8237A funciona, então, como um “agente”, do ponto de vista dos dispositivos.

De acordo com a especificação, a transferências da memória e para a própria memória também é possível. Mas, ao que parece, isso não foi implementado nos PCs… Não dá para pedir pro DMAC copiar, por demanda de software, um bloco de memória de um lugar para o outro. Em teoria isso seria uma boa ideia para aliviar alguma pressão em loops de cópias de dados, por parte do processador. Na prática, o 8237A é bem lento, permitindo transferências com uma taxa máxima de 2,5 MiB/s (existem versões com taxas de 5 MiB/s, mas, hoje em dia, isso não é lá muito rápido!).

Outro detalhe interessante são os modos de operação do DMAC… Alguns dispositivos são bem lentos e, por isso, aceitam apenas transferências simples, uma palavra (byte?) por vez… Outros, são rápidos, e permitem a transferência de um bloco de dados inteiro, mas ainda existem outros que precisam de algumas requisições para completar a transferência (transferem por demanda). Esses diferentes modos de operação o 8237A aceita e o PC está preparado para elas.

PCs adicionam um detalhe ao endereço base de transferência:

Note que, até aqui, o endereço base de transferência tem apenas 16 bits de tamanho. Isso limitaria o uso do DMAC a um único segmento da memória. Não é assim que funciona nos PCs… O padrão ISA adiciona o conceito de “página de DMA”, estendendo o endereço físico em 8 bits… Assim, o endereço base tem 24 bits de tamanho.

Isso não significa que podemos transferir, via DMA, blocos de 16 MiB! Os blocos continuam limitados a 64 KiB, e a página é fixa, mesmo quando há um overflow (ou underflow) no endereço. Por exemplo, se a página for 0xB8 (como é o caso com os antigos circuitos de vídeo VGA, no modo texto), se o DMAC transferir um byte do endereço físico 0xB8FFFF, o próximo endereço não será 0xB90000, mas 0xB80000. A página não muda.

O que é preciso para programar o DMAC?

O DMAC tem 12 tipos diferentes de registradores. Neles podemos ajustar o endereço base da transferência, a quantidade de bytes a serem copiados e o modo como esses dados serão transferidos (em bloco? um byte por vez?). Existem, ainda, registradores de status e máscara de requisições (do mesmo jeito que ocorre no PIC, exceto que apenas 3 bits são usados – os dois bits inferiores selecionam o canal e o bit 2 ajusta a máscara).

Para programar um canal do DMAC precisamos informar o endereço base (veja abaixo), a quantidade de bytes a serem transferidos, o modo de transferência e desmascarar o canal… O dispositivo requisitando a transferência vai indicar a transferência via sinal DRQn (uma vez que o DMAC responda com o DACKn).

É costumeiro que o dispositivo, depois de terminar a transferência envie uma interrupção ao processador para que o tratador saiba que existem dados colocados no buffer, pelo DMAC… É o caso de transferências feitas por HDs, sem o uso do modo PIO, por exemplo (por isso as IRQs 14 e 15 são alocadas para as duas controladoras possíveis de HD!)… É claro, as IRQs podem ocorrer por outro motivo e, neste caso, o tratador poderá ler o status do respectivo canal do DMAC para determinar o término da transferência…

As portas de I/O:

Existem dois DMACs no seu PC. As portas de 0x00 até 0x0F são usadas para o DMAC1, já as portas 0xC0 até 0xCF para o DMAC2… O primeiro DMAC lida com os canais de 0 até 3, o segundo, com os canais de 4 até 5, onde o canal 4 não é usável (o DMAC2 é escravo do DMAC1 via canal 4). Eis uma listagem para o DMAC1:

Porta Tamanho Registrador
0x00 word Start Address (Channel 0)
0x01 word Count (Channel 0)
0x02 word Start Address (Channel 1)
0x03 word Count (Channel 1)
0x04 word Start Address (Channel 2)
0x05 word Count (Channel 2)
0x06 word Start Address (Channel 3)
0x07 word Count (Channel 3)
0x08 byte Status(R)/Command(W)
0x09 byte Request
0x0a byte Single Channel Mask
0x0b byte Mode
0x0c byte Flip-Flop Reset
0x0d byte Intermediate(R)/Master Reset(W)
0x0e byte Mask Reset
0x0f byte Multi Channel Mask

Um cuidado ao lidar com o 8237A é que os registradores de 16 bits devem ser acessados 8 bits de cada vez, enviando (ou lendo) o LSB, primeiro e depois o MSB. Mas é importante que antes escrevamos qualquer valor no registrador Flip-flop reset. Isso deve ser feito porque o 8237A não muda o estado do flip-flop interno e, se não o fizermos “manualmente”, a próxima escrita num registrador de 16 bits ficará escrevendo apenas o MSB. Ao escrever em Flip-flop reset este flip-flop interno é resetado.

A porta Master Reset coloca o DMAC num estado inicial, como se tivéssemos feito um Power on reset. Não é interessante mexer nele. A porta Mask reset zera as máscaras de requisição de todos os canais do DMAC. Isso quer dizer que todos os canais estarão “desmascarados”…

As portas Single Channel Mask e Multi Channel Mask ligam ou desligam a máscara de requisições. A primeira o faz com apenas um canal, a última com todos os quatro, ao mesmo tempo. A porta Single Channel Mask recebe, nos dois primeiros bits (bits 0 e 1), o número do canal (de 0 a 3), tanto para o DMAC1, quanto para o DMAC2. e o bit 2 indica se a requisição para esse canal deve ser mascarada ou não. No caso de Multi Channel Mask, os bits de 0 a 3 são as máscaras de cada canal correspondnte.

Os registradores Request e Command são inúteis… O registrador Status devolve, nos 4 primeiros bits o estado da contagem… Para cada byte transferido a contagem decrementa. Se ela chega a zero, o bit correspondente ao canal estará setado, indicando um “término de contagem”. Já os quatro bits superiores indicam se existe alguma requisição de DMA pendente para o canal correspondente (bit 4 para o canal 0 até o bit 7, para o canal 3)… Ao lermos esse registrador, os bits TCn (terminal count) que estiverem setados serão automaticamente zerados.

As portas com as páginas de DMA, para o DMAC1, são as listadas abaixo e todas têm apenas 1 byte de tamanho. Note que a perfeita sequência que existe, acima, não existe aqui:

Porta Registrador
0x87 Page (Channel 0)
0x82 Page (Channel 1)
0x81 Page (Channel 2)
0x82 Page (Channel 3)

No caso do DMAC2 os endereços de I/O desses registradores assumem os valores de 0x8f (não usável), 0x8b, 0x89 e 0x8a (o mesmo que acima, mas com o bit 3 setado).

Resta-nos entender o registrador de modo… Para isso, é prudente que você consulte a documentação do 8237A. No contexto desse artigo, basta que eu diga que, geralmente, o modo usado por dispositivos são demand ou block. A não ser que o dispositivo seja bem velho, como antigos floppy drives, talvez o modo single seja útil… Ainda, nos PCs, o modo de autoinicialização não é usado por dispositivo algum e, geralmente, o endereço de transferência cresce (ao inves de ser decrementado)… No registrador de modo temos que dizer a direção que o DMAC vai trafegar os dados (do dispositivo para memória, da memória para o dispositivo ou da memória para a própria memória). As transferências Memória⇒memória geralmente não são suportadas e devem ser evitadas… Assim como no registrador Single Channel Mask, os 2 bits inferiores indicam para qual canal o modo se aplica.

ATENÇÃO: Novamente o leitor e amigo atento, Axey, vem em meu auxílio e indica que há uma diferença essencial nos datasheets da AMD (do link que forneci) e o da Intel (link aqui). No datasheet da Intel o “Terminal Count” é sinalizado quado o contador corrente vai de zero a 0xffff, ou seja, quando ha um underflow. Para o datasheet da Intel isso parece estranho, porque ao atingir zero nenhuma transferência adicional deveria ser feita, bem como decremento do contador… Considere a transferência de um bloco de 1 único byte: O DMAC faz algo assim:

...
while (current_count > 0)
{
  transfer_data(current_address++);
  current_count--;
}

No entanto, se esse for o caso, como poderíamos transferir 65536 bytes, ou exatamente 64 KiB? Segundo o datasheet da AMD seria impossível, já que o contador base tem 16 bits de tamnho. O contador máximo seria 65535… No caso da Intel, o contador base contém o valor da contagem requerida mais um byte. O que faz todo sentido… Para transferir 1 único byte teríamos que colocar 0 neste contador!

De fato, isso é descrito em ambos os datasheets, mas no da AMD eles esquecem de dizer que o terminal count é sinalizado no underflow. Afinal, não tem muito sentido programar o DMAC para transferir 0 bytes, tem?

Valeu, de novo, Axey!

MyToyOS: Machine Specific Registers

Os processadores Intel possuem mais registradores, além dos de uso geral, controle, debugger, SIMD e testes. Existe um “porrilhão” (grandeza técnica usada para indicar grandes quantidades!) de registradores usados para configurar o processador. Esses registradores são chamados de Machine Specific Registers porque muitos deles são específicos para a arquitetura do processador onde se aplicam.

Um exemplo: O MSR chamado de IA32_APIC_BASE contém, entre outras informações, o endereço básico do APIC local do processador lógico onde este registrador é consultado… O nome IA32_APIC_BASE é apenas um mnemônico, o registrador verdadeiro é identificado por um número que é passado pelo registrador ECX antes da chamada à instrução RDMSR (leitura) ou WRMSR (escrita). Cada um desses MSRs têm 64 bits de tamanho e, portanto, RDMSR retorna o valor no par de registradores EDX:EAX.

As instruções mencionadas só funcionam no ring 0. Caso contrário ela gerará um General Protection Fault, que ocorrerá também se o MSR consultado não existir… Isso quer dizer que você não tem como usar essas instruções num ambiente como Linux ou Windows, no userspace

Como o MS-DOS funciona no modo real e, por definição, executa no ring 0, eis um programinha usando o NASM e o Borland C++ 3.1 para obter o endereço base do Local APIC do processador atual:

bits 16

segment _TEXT public class=CODE align=16 use16

struc rdmsrsrk
.oldbp:   resw 1
.retaddr: resw 1
.msr:     resd 1
.ptr:     resw 1
endstruc

  global _rdmsr
  align 4
_rdmsr:
  push bp
  mov  bp,sp
  mov  ecx,[bp+rdmsrstk.msr]
  rdmsr
  mov  di,[bp+rdmsrstk.ptr]
  mov  [di],eax
  mov  [di+4],edx
  leave
  ret

O programinha em C que usa a rotina acima pode ser assim:

#include <stdio.h>

#define IA32_APIC_BASE 0x1b

struct rdmsr_s { unsigned long a, d; };

extern void rdmsr(unsigned long, struct rdmsr_s *);

void main(void)
{
  struct rdmsr_s r;

  rdmsr(IA32_APIC_BASE, &r);

  // Mostra os bits 31..12 do registrador.
  printf("LAPIC Base Addr = %lx\n",
    (r.a & 0xfffff000UL));
}

Compilando, linkando e executando, temos:

C:\WORK> nasm -f obj -o rdmsr.obj rdmsr.asm
rdmsr.asm:3: warning: segment attributes specified on redeclaration of segment: ignoring

C:\WORK> bcc -3 -ms -otest.obj -c test.c
C:\WORK> tlink \bcc\lib\c0s.obj+test.obj+rdmsr.obj,test.exe,nul,\bcc\lib\cs.lib

C:\WORK> test
LAPIC Base Addr = fee00000

O aviso do NASM indiva que o que é ignorado é o aviso, não a redeclaração do segmento…

A lista completa dos MSRs pode ser obtida no capítulo 35 do volume 3 dos manuais de desenvolvimento de software para as arquiteturas Intel 64 e IA-32 da Intel (link aqui).