sábado, 10 de dezembro de 2011

Programa "Olá Mundo" em Assembly Z80 (MSX)

Olá amantes do MSX.

Este artigo contém um trecho de outro artigo chamado "Desmistificando a Linguagem Assembly", onde eu apresento um exemplo de programação Assembly Z80 no MSX. Aqui você confere um programa em Assembly que quando compilado usando o cross-assembler ASMSX gera um arquivo .ROM executável num emulador de MSX, como o BlueMSX, por exemplo. Trata-se do famoso "Olá Mundo" ou "Hello World", que você vai aprender a programar aqui usando Assembly do Z80. para MSX. Pois vamos ao que interessa!

OBS: Para compilar o código Assembly a seguir você precisa do cross-assembler ASMSX. Se não possui, dê uma olhada no meu artigo onde há um link para baixá-lo: ASMSX.

Imagine um programa que escreve uma mensagem, como um "Hello World" na tela de um computador MSX. O que precisamos para fazer isso em Assembly (do Z80, no MSX) da forma mais simples possível? Precisamos de uma região de memória para armazenar a frase "Hello World", e de algumas rotinas da BIOS do MSX, uma que inicializa a tela e outra que transfere dados da memória diretamente para a tela. Confira o código completo e a seguir a explicação detalhada:


;--------------------------------------------
; PROGRAMA HELLO WORLD, PARA O MSX
;--------------------------------------------

.org 8000h
.start 8000h
.rom

INITXT equ 050eh
LDIRVM equ 0744h

INICIO:

call INITXT
ld BC, 12
ld DE, 0
ld HL, TEXTO
call LDIRVM

FIM:

jp FIM

TEXTO:

db "Hello World!"


Para compilar este código, foi necessário usar o programa assembler ASMSX, que traduz código Assembly Z80 para código nativo do Z80 nos computadores MSX. Agora a explicação do passo-a-passo:

O programa inicia com um bloco de comentário, onde cada linha de comentário começa por um ponto-e-vírgula (;). Comentários são ignorados pelo assembler e só servem para nós humanos lerem.

Em seguida, vem três palavras que iniciam por um ponto (.), estas são diretivas do assembler. Diretivas não são compiladas para código de máquina. Elas servem só para passar algumas informações para o assembler. A diretiva .org serve para indicar a partir de qual endereço de memória do computador MSX o programa vai ser colocado, neste caso, o programa será colocado a partir do endereço 8000h (o "h" no final indica que a notação é hexadecimal). A diretiva .start indica que o programa vai começar a executar a partir do endereço 8000h, ou seja, desde o início. Existem situações onde o segmento de dados vêm no início do programa e o código executável vem depois, e para isso existe essa diretiva, pois se os dados viessem logo no início do programa, o programa em si deveria executar a partir de outro endereço e não do endereço inicial. Como no caso deste programa o segmento de dados vem lá no final, então o endereço inicial da execução do programa coincide com o endereço inicial do código em si. Por fim, a diretiva .rom apenas indica para o assembler ASMSX que ele deve gerar um arquivo ROM que contém um cabeçalho típico das ROMs do MSX seguido do código binário do Z80.

Logo a seguir, vem duas diretivas EQU. Estas diretivas servem para criar constantes e contém do lado esquerdo um identificador (isto é, um nome), e do lado direito um valor, que neste caso é um endereço de memória. Basicamente, estamos dizendo neste caso que INITXT é um nome que será substituído no código pelo endereço de memória 050e (em hexadecimal). Este endereço pertence ao BIOS específico das máquinas MSX e serve para inicializar a tela no modo texto de 40x25 caracteres. A outra EQU diz que o nome LDIVRM refere-se ao endereço 0744 (em hexadecimal). Este endereço também pertence ao BIOS específico dos computadores MSX e serve para transferir um bloco de dados da memória RAM para a VRAM (que é a memória de vídeo nos MSX).

Após definir as constantes necessárias, vem a palavra "INICIO:". Qualquer palavra seguida de dois-pontos (:) indica para o assembler ASMSX que tal palavra trata-se de um rótulo. Um rótulo é um nome para um endereço de memória qualquer. Neste caso esta linha indica para o assembler que INICIO é o nome do endereço de memória (não importa exatamente qual endereço) a partir do qual o código a seguir se encontra. A palavra pode ser qualquer uma, não necessariamente "INICIO". Apenas coloquei este nome porque ela indica onde eu comecei a escrever o código do programa em si.

Abaixo do rótulo INICIO, que neste caso indica onde é o início do programa propriamente dito, vem o código Assembly. A instrução CALL invoca a subrotina (um trecho de código Assembly) que se encontra a partir do endereço de memória especificado. Neste caso, o endereço foi especificado através de uma constante, a INITXT. Na hora da compilação, o assembler substitui esta palavra pelo endereço real (que defini como 050e logo no início do programa, lembra?). Ao ser invocada, esta subrotina da BIOS do MSX inicializa a tela no modo texto de 40x24 caracteres, para que possamos escrever na tela em seguida.

A seguir, vem três instruções LD. As instruções LD do Z80 servem para transferir dados de um lugar a outro. Neste caso, elas estão transferindo bytes e endereços de memória para os registradores BC, DE e HL:

  • LD BC, 12 = insere o byte 12 (decimal) no registrador BC.
  • LD DE, 0 = insere o byte 0 no registrador DE.
  • LD HL, TEXTO = insere o endereço do rótulo TEXTO no registrador HL.

Mas qual é a moral disso tudo? Bom, para escrever na tela de um computador MSX usando a função LDIRVM da BIOS, é necessário informar alguns parâmetros. Para passar parâmetros para subrotinas ou funções, é necessário usar os registradores. No caso da subrotina LDIRVM, ela requer que o número de caracteres do texto esteja armazenado no registrador BC, e o texto "Hello World!" tem 12 caracteres. Ela também requer que a posição do texto esteja armazenada no registrador DE, e quero que o texto esteja logo no topo da tela então armazenei 0. Além disso, a função LDIRVM requer que o texto a ser escrito na tela esteja armazenado a partir da posição de memória armazenada no registrador HL, por isso inseri o endereço do rótulo TEXTO, pois é a partir deste rótulo que defini o texto "Hello World!".

Depois de usar os registradores BC, DE e HL para armazenar os parâmetros a serrem passados para a função da LDIVRM da BIOS do MSX, precisamos só invocar a função. Isso se consegue através da instrução CALL seguida do endereço de memória da função, por isso escrevi a linha "call LDIVRM". Pronto, isso trata de escrever "Hello World!" na tela de um computador MSX. Simples assim.

Quer ver o resultado na tela do MSX? Dá uma olhada no screenshot, é isso que aparece na tela de um computador MSX ao executar o código binário gerado a partir desse programa Assembly:


O código binário gerado por esse programa Assembly faz isso no MSX:




O rótulo FIM só serve para que eu saiba onde termina o programa. A única instrução deste rótulo é a JP, que serve para "pular" para o endereço de memória especificado e executar o código a partir de lá. Neste caso, a instrução JP "pula" para o próprio rótulo onde ela está! Efetivamente, isto leva a um loop infinito onde o processador fica executando sempre a mesma instrução e "trava". Mas no caso dos computadores MSX, isso não é problema, pois em geral programas em ROMs fazem isso mesmo. Em um computador MSX real, caso quizesse encerrar o programa, bastaria "retirar" o cartucho ROM e o computador reiniciaria normalmente. Em um emulador, bastaria iniciar a emulação sem abrir o arquivo ROM contendo a imagem binária do cartucho (que é o arquivo gerado pelo assembler ASMSX no meu caso).

O que aparece no final do programa, após o rótulo TEXTO, é mais uma diretiva do ASMSX, a diretiva DB, que serve para definir bytes. Quando escrevemos DB "Hello World!" estamos solicitando para o assembler colocar exatamente estas letras entre aspas dentro do arquivo ROM, e são exatamente estas letras que o programa escreve na tela, porque foi o rótulo TEXTO que passei como parâmetro para a função LDIVRM, através do registrador HL, lembra?

Quer ver o arquivo ROM gerado pelo ASMSX a partir do código Assembly? Dá só uma olhada (clique na imagem para ampliar. É o screenshot tirado do programa HxD, que permite analizar o código binário dentro de um arquivo qualquer). Cada byte nesse arquivo corresponde ou a uma instrução em código de máquina do Z80, ou a dados do programa, ou ao cabeçalho gerado pelo ASMSX (os primeiros bytes do arquivo contém o cabeçalho, o AB que aparece logo no início é o número mágico ou assinatura dos arquivos ROM do MSX, e em seguida dois bytes contém o endereço inicial do programa (00 80, aparece invertido mas na verdade o endereço inicial é 8000, lembra?).


Arquivo ROM criado pelo ASMSX contendo o código binário gerado a partir desse programa:




Enfim, um simples "Hello World" é assim em Assembly, da forma mais simples possível, num computador MSX. Este código só roda em computadores MSX e em nenhum outro. Daí uma das maiores desvantagens da programação Assembly: código gerado para um determinado processador (no caso deste programa, para um Z80), não funciona em nenhum outro processador, a não ser que o outro processador possua uma arquitetura equivalente. No caso deste programa, uma vez que utilizei chamadas a funções da BIOS do MSX, ele não funciona em nenhum outro computador, mesmo que o outro computador utilize o mesmo processador. Isto porque as rotinas da BIOS do MSX são específicas deste tipo de máquina.