Programação assembly… “like a boss”

like a boss

Estava fazendo uns testes de código em MSX-DOS e experimentando como chamar a BIOS a partir do ambiente e… opa! Melhor explicar uma coisa importante antes.

No MSX, quando estamos no BASIC a memória tem o seguinte leiaute:

0x0000 - 0x7FFF : BIOS e MSX-BASIC
0x8000 - 0xFFFF : RAM (32Kib)

Ou seja uns 32KiB de ROM estão lá ocupando 50% do espaço de endereçamento que o Z80 entende e chama de “mundo”.

Mas quando estamos no MSX-DOS a memória fica assim:

0x0000-0xFFFF : RAM (64Kib)

Mas como se faz para acessar a BIOS já que ela “desapareceu” da vista do Z80?

Antes de continuar é bom lembrarque as variáveis de sistema nos dois casos ainda estão lá, organizadas a partir de 0xFFFF e seguindo rumo a 0x0000 — o que é bom pois tem muita coisa que precisa daquilo ali pra continuar funcionando — e também que o MSX-DOS (tal qual acontece com o CP/M) os primeiros 256 bytes (ou seja, de 0 até 255) contém o “sistema operacional” e ali não é para inventar de mexer!

Aliás este é o motivo dos programas começarem sempre no endereço 256 (ou 0x0100).

Chamando a BIOS

Existe uma rotina, a CALSLT (0x001C), que serve para executar um trecho de código que esteja localizado em qualquer slot — ela é irmã das RDSLT e WRSLT que, respectivamente, lê e escreve um byte em qualquer slot. Assim, basta informar no registrador IY o slot onde está rotina, em IX o endereço a executar e chamá-la. Assim:

        ld iy,(EXPTBL-1)
        ld ix,endereço da rotina
        call CALSLT

Pelo que entendi a localização da ROM está armazenado no endereço anterior ao da variável de ambiente EXPTBL (0xFCC1), ou seja, está em 0xFCC0 mas como parece que todos evitam fazer referência direta a ela, também farei o mesmo.

Para fazer um programa em MSX-DOS usando a rotina rotina BEEP (0x00C0) da BIOS para emitir um bipe (dahhh….) o programa ficaria assim:

;
;  BEEP.COM - emite um beep
;
BEEP:   equ 0x00C0
CALSLT: equ 0x001C
EXPTBL: equ 0xFCC1
        org 0x0100
        ld iy,(EXPTBL-1)
        ld ix,BEEP
        call CALSLT
        ret

Lindo não? E pode montar que funciona! Mas se duvida…

$ pasmo -v -d beep1.asm beep1.com
Loading file: beep1.asm in 0
Finished loading file: beep1.asm in 11
Entering pass 1
Pass 1 finished
Entering pass 2
                ORG 0100
BEEP            EQU 00C0
CALSLT          EQU 001C
EXPTBL          EQU FCC1
0100:FD2AC0FC   LD IY, (FCC0)
0104:DD21C000   LD IX, 00C0
0108:CD1C00     CALL 001C
010B:C9         RET
Pass 2 finished
Emiting raw binary from 0100 to 010B

Automatizando um pouco

É óbvio que ao escrever a terceira chamada à BIOS num programa você já estará de saco cheio de escrever “LD IY,…” e “LD IX,…” e as chances de inverter a ordem ou esquecer algo começarão a aumentar. Mas como todo bom programador você poderia criar uma casca (um “wrapper”) para encapsular as chamadas mas aí você estaria encapsulando algo que já foi encapsulado.

É aí que entram as facilidades dos assemblers (lembre-se: montadores!) , basta criar uma macro que cuidará de automatizar a tarefa e deixando até mesmo! Ou seja, dizer ao montador que onde estiver X você quer que ele escreva A, B e C.

Eu criei uma assim:

;
;  BEEP2.COM - emite um beep ("like a boss" edition)
;
BEEP:   equ 0x00C0
CALSLT: equ 0x001C
EXPTBL: equ 0xFCC1
        org 0x0100
MACRO __call,BIOS
        ld iy,(EXPTBL-1)
        ld ix,BIOS
        call CALSLT
ENDM
        __call BEEP
        ret

A sintaxe com os dois sublinhados na frente são invenção minha para ajudar a diferenciar o mnemônico “call” da macro “__call” e, sim, é meio que uma inspiração direta de Python.

O fonte ficou com uma aparência mais bonita e até elegante porém um pouco mais longo ao saltar de 11 para 15 linhas, porém repare que se eu resolver tocar três bipes os dois códigos acabarão com o mesmo tamanho e a partir do quarto a versão que não usa a macro já será maior (além de mais confusa).

Na montagem acontece o seguinte:

$ pasmo -v -d beep2.asm beep2.com
Loading file: beep2.asm in 0
Finished loading file: beep2.asm in 15
Entering pass 1
Pass 1 finished
Entering pass 2
                ORG 0100
BEEP            EQU 00C0
CALSLT          EQU 001C
EXPTBL          EQU FCC1
Defining MACRO __call
Params: BIOS
Expanding MACRO __call
BIOS= BEEP 
LD IY , ( EXPTBL - 0001 ) 
0100:FD2AC0FC   LD IY, (FCC0)
LD IX , BIOS 
0104:DD21C000   LD IX, 00C0
CALL CALSLT 
0108:CD1C00     CALL 001C
ENDM 
                ENDM
End of MACRO __call
010B:C9         RET
Pass 2 finished
Emiting raw binary from 0100 to 010B

O bom observador perceberá que o programa gerado aqui é idêntico ao do primeiro exemplo já que não existe código novo, apenas a substituição da ocorrência da macro pelas instruções correspondentes, ou seja, no final fica igual.

Para os incrédulos:

$ md5sum beep?.com
faa9a3b426121de5839e8295bca2d1c6  beep1.com
faa9a3b426121de5839e8295bca2d1c6  beep2.com

Automatizando mais ainda!

A vantagem desta abordagem é permitir a criação de código facilmente adaptável a determinadas situações, por exemplo um código que se ajusta ao formato destino dele sem a necessidade de fazer (muitas) alterações no código:

;
; BEEP3.COM - emite um beep ("like a final boss" edition)
;
BEEP:   equ 0x00C0
CALSLT: equ 0x001C
EXPTBL: equ 0xFCC1

if TARGET=1
    ;
    ; monta para MSX-DOS
    ;
    MACRO __call,BIOS
        ld iy,(EXPTBL-1)
        ld ix,BIOS
        call CALSLT
    ENDM
        org 0x0100
else
    ;
    ; monta para MSX-BASIC (BLOAD)
    ;
    MACRO __call,BIOS
        call BIOS
    ENDM
        org 0x8000-7
        db 0xfe
        dw START
        dw STOP
        dw EXEC
endif

START:
EXEC:
        __call BEEP
        ret
STOP:

Aqui há uma definição, a TARGET que igual a um produzirá um programa de MSX-DOS ou com outro valor resultará em um arquivo binário para o MSX-BASIC para carregar com o comando BLOAD. Basta colocar antes do “if” a definição “TARGET: equ 0” ou “TARGET: equ 1” dependendo da necessidade.

Já que a complexidade do código aumentou, vamos aumentar o processo de montagem também, este cara cuida de gerar as duas versões sem interferência humana com um pequeno script em Bash:

#!/bin/bash
TEMP=/tmp/beep3_tmp.asm
I=0
for J in bin com
do
    echo "TARGET: equ ${I}" > ${TEMP}
    cat beep3.asm >> ${TEMP}
    pasmo -d -v ${TEMP} beep3.${J} > ${J}.log 2>${J}.err
    I=$((I+1))
done
rm ${TEMP}
exit 0

E mesmo assim o código gerado continua é o mesmo:

$ md5sum beep?.com
faa9a3b426121de5839e8295bca2d1c6  beep1.com
faa9a3b426121de5839e8295bca2d1c6  beep2.com
faa9a3b426121de5839e8295bca2d1c6  beep3.com

Provocação final

Agora como provocação da minha parte vou deixar um exercício: Que tal acrescentar a possibilidade de produzir também um arquivo ROM? No código do Flappybird para MSX tem a dica de como produzir uma imagem ROM, inclusive com preenchimento do que falta com zeros (o padding). E boa diversão!

Sobre Giovanni Nunes

Giovanni Nunes (anteriormente conhecido como “O Quinto Elemento”) é uma das mentes em baixa resolução que compõem o Governo de Retrópolis, responsável pela identidade visual de todas as facetas do nosso Império Midiático.

0 pensou em “Programação assembly… “like a boss”

  1. Muito bom artigo!

    Fiquei com uma dúvida. As variáveis do sistema começam em 0xFFFF e continuam em 0x0000? 0xFFFF não é o final da memória?

    1. Começam em 0xFFFF e vão “subindo” pra 0x0000, vou corrigir minha interpretação da realidade.

  2. Excelente post. Sugiro montar uma seção, ou um blog, com dicas e descobertas.