Primeiros passos com assembly
Toda quinta-feira, um grupo de programadores e eu nos reunimos para conversar sobre Golang no Grupo de Estudos de Go. Devemos mudar o nome para Grupo de Estudos de Tecnologia porque discutimos todos os assuntos relacionados.
Um tema recorrente é programação em assembly. Várias vezes, discutimos iniciar demonstrações e brincadeiras carregadas via setor de boot, conhecidas como “boot sector games”.
Além de ser divertido, é uma boa forma de ensinar como o computador funciona internamente. Conceitos como velocidade de acesso, cache Lx e outros são abstratos em linguagens de alto nível.
Os exemplos não podem ser muito grandes, pois temos apenas 510 bytes para trabalhar. Nosso programa precisa caber nesse espaço, o que é desafiador.
O limite é de 510 bytes porque precisamos usar 2 bytes para a assinatura do setor de boot dos 512 bytes disponíveis.
Preparando o ambiente
Compilador nasm
O nasm é meu compilador favorito para assembly. Ele é multiplataforma, possui uma comunidade ativa e utiliza a sintaxe Intel.
Instalando o nasm
No Linux, a maioria das distribuições possui um pacote para o nasm. Geralmente, a versão é um pouco desatualizada, mas nunca tive problemas com a versão padrão do Debian. Atualmente, instalo diretamente das fontes por preferência pessoal.
No macOS, o nasm vem junto com o Xcode. Prefiro usar a versão fornecida pela Apple.
No Windows, há um instalador muito conveniente.
Instalar via código fonte
Baixe o código fonte no site oficial em https://www.nasm.us. Prefiro usar a versão estável.
Descompacte em um diretório e siga as instruções do arquivo INSTALL.
Basicamente, execute os seguintes comandos:
./configure
make
sudo make install
Verifique se a instalação foi bem-sucedida com nasm --version
.
Instalando nasm no Windows
Para instalar no Windows, use o instalador disponível em https://www.nasm.us/pub/nasm/releasebuilds/. Escolha a versão desejada, por exemplo, win64, e baixe o installer-x64.exe.
Após a instalação, adicione o diretório do nasm à variável de ambiente PATH manualmente.
Configurando nasm no Neovim
Para informar o Neovim que você está usando o nasm, adicione o seguinte comando no init.vim:
"" nasm
autocmd BufNewFile,BufRead *.asm set filetype=nasm
Instalando qemu
O qemu é um emulador versátil. Além de emular várias plataformas, ele possui recursos interessantes como o modo ncurses, que redireciona as saídas de tela em modo texto para o terminal. Isso permite visualizar telas emuladas via SSH, desde que a tela esteja em modo texto.
Para instalar, baixe a versão adequada para seu sistema em https://www.qemu.org/download/.
No Linux Debian, instale com apt install qemu
. No Windows, use o instalador para a versão de 64 bits em https://qemu.weilnetz.de/w64/.
No Windows, adicione o diretório do qemu à variável de ambiente PATH manualmente.
Testando o ambiente
Com o ambiente de desenvolvimento configurado, vamos testar com um famoso “hello world”.
Olá mundo
Crie um arquivo chamado boot.asm e insira o seguinte conteúdo:
org 0x7c00 ; define o endereço inicial
bits 16
; limpar a tela
mov ax, 0x3 ; define modo de vídeo ah=0 al=3
int 0x10 ; chamada de BIOS
; imprimir mensagem
xor ax, ax ; ax=0
mov es, ax ; es=ax
xor dx, dx ; dx=0
mov bp, msg ; define o endereço da mensagem
mov ax, 0x1301 ; ah=13h imprimir string, al=1 mover cursor
mov bx, 0x000A ; bh=0 página de vídeo 0, bl=0 atributos
mov cx, [len] ; comprimento da string
int 0x10 ; chamada de BIOS
hlt ; parar
msg db 'Ola mundo!'
len dw $-msg
times 510 - ($-$$) db 0
dw 0xAA55 ; número mágico do setor de boot
Este é um exemplo básico de setor de boot. Na primeira linha, definimos o endereço 0x7c00 onde o binário será carregado na memória.
Em seguida, definimos o tamanho da palavra básica da máquina para o compilador. Como este é o primeiro código que será executado no boot, a máquina ainda está em 16 bits, modo real.
Usamos um comando para limpar a tela ajustando o modo de vídeo, obtendo o efeito de limpar a tela.
; limpar a tela
mov ax, 0x3 ; define modo de vídeo ah=0 al=3
int 0x10 ; chamada de BIOS
O próximo passo é imprimir a string “Ola mundo!”. A impressão envolve uma chamada mais complexa, que discutiremos em detalhes futuramente.
; imprimir mensagem
xor ax, ax ; ax=0
mov es, ax ; es=ax
xor dx, dx ; dx=0
mov bp, msg ; define o endereço da mensagem
mov ax, 0x1301 ; ah=13h imprimir string, al=1 mover cursor
mov bx, 0x000A ; bh=0 página de vídeo 0, bl=0 atributos
mov cx, [len] ; comprimento da string
int 0x10 ; chamada de BIOS
Após imprimir a mensagem, queremos que a máquina pare. Para isso, usamos a instrução hlt.
Definimos duas variáveis: msg contém a mensagem e len contém o tamanho da mensagem, calculado com uma macro do nasm.
msg db 'Ola mundo!'
len dw $-msg
A assinatura do setor de boot é 0xAA55. Esses dois últimos bytes devem estar no final do setor de boot. Para posicioná-los corretamente, usamos a macro times 510 - ($-$$), que preenche com zeros até o byte 510, garantindo que dw 0xAA55 esteja nas posições finais.
Executando o exemplo
Para compilar o boot.asm com o nasm, use o seguinte comando:
nasm -f bin boot.asm -o boot.bin
Para inspecionar o executável, use o utilitário hexdump, que mostra detalhes como o número mágico da assinatura do setor de boot no final do arquivo.
hexdump boot.bin
0000000 03b8 cd00 3110 8ec0 31c0 bdd2 7c1b 01b8
0000010 bb13 000a 0e8b 7c25 10cd 4ff4 616c 6d20
0000020 6e75 6f64 0a21 0000 0000 0000 0000 0000
0000030 0000 0000 0000 0000 0000 0000 0000 0000
*
00001f0 0000 0000 0000 0000 0000 0000 0000 aa55
Para executar o boot no emulador qemu, use o seguinte comando:
qemu-system-i386 -fda boot.bin
Se você estiver usando uma máquina remota via SSH, adicione o parâmetro -curses:
qemu-system-i386 -curses -fda boot.bin
Próximos passos
Arrumar tempo para escrever exemplos é mais complicado que programar em assembly limitado a 510 bytes.
Nem tudo será tão limitado. Quando surgir a oportunidade, pretendo mostrar outras coisas interessantes.
Espero que você se divirta tanto com essa brincadeira quanto eu.