C for Experts
Acesso à Memoria e Assembly
Você está em: MarMSX >> Cursos >> C
Linguagem Assembly exclusiva do MSX.
Acesso à Memória
O acesso à memória em C é feito utilizando-se os ponteiros. Veja o exemplo a seguir.
#include <stdio.h>
main()
{
unsigned char *p;
/* Faz p apontar para &HF000 */
p = (unsigned char*) 0xF000;
/* Insere o valor 4 em &HF000 */
*p = 4
printf("Valor de p: %d\n", *p);
printf("Endereço de p: %x\n", (int) p);
}
Saída:
Valor de p: 4
Endereço de p: F000
Após executar o programa, vá ao Basic e digite o comando a seguir.
PRINT PEEK(&HF000)
E o valor 4 está lá.
Acesso à porta do MSX
As funções inp(porta) e outp(porta,valor) da biblioteca "sys.h", lê e escreve, respectivamente, em uma determinada porta do MSX.
Exemplo:
#include <stdio.h>
#include <sys.h>
main()
{
printf("Valor da porta A8: %d\n", inp(0xa8));
}
Assembly do MSX
Podemos inserir códigos em Assembly dentro do programa C. Para isso, devemos utilizar as directivas "#asm" e "#endasm" para delimitar o código em Assembly, que diferente do Pascal, deve ser escrito com mnemônicos.
Veja o exemplo a seguir, onde um programa em Assembly carrega o valor 25 na posição de memória &HF000.
#include <stdio.h>
main()
{
unsigned char *p = (usigned char*) 0xF000;
#asm
ld a,25
ld (0F000h),a
#endasm
printf("Valor em F000: %d\n", *p);
}
Saída:
Valor em F000: 25
Quando não é definido o endereço inicial do programa em Assembly (ORG end), o compilador C se encarrega de colocá-lo em uma determinada posição de memória.
Obs: o valor em hexadecimal deverá sempre começar por um número. No caso de F000, inserimos o valor 0 antes de F.
Pseudo-ops
DEFB, DEFF, DEFW, DEFS, DEFM
Define um byte, um float, um word (2 bytes), reserva um espaço em memória e define uma string. Exs:
DEFB 15,'m',0ABh ; Insere os bytes 15, 'm' e &HAB na memória
DEFF 1.44 ; Insere o ponto flutuante 1.44 na memória
DEFW 0FCC1h ; Insere o valor &HFCC1, que ocupa 2 bytes
DEFS 25 ; Reserva 25 bytes na memória (para uso posterior)
DEFM 'Ola mundo' ; Define a string "Ola mundo"
Veja ao final do capítulo 7 do curso de Assembly um exemplo do uso dessas pseudo-ops.
EQU
Associa um nome a uma expressão. Ex:
cls equ 00C3h
...
call cls ; Em vez de call 00C3h
ORG
Define o endereço inicial do programa. Ex:
ORG 0C000h
MACRO
Define uma espécie de "sub-rotina" do Assembly. É delimitado por "MACRO" e "ENDM", possui um nome associado a ele e pode receber parâmetros.
Sintaxe:
MACRO nome,param1,param2,...
; codigo
EDNM
ou
nome MACRO param1,param2,...
; codigo
EDNM
Exemplo:
MACRO rotina,param
ld a,param
ld (0F000h),a
ENDM
; Chama a macro
rotina 15
Programa em C:
#include <stdio.h>
main()
{
unsigned char *p = (unsigned char*) 0xF000;
#asm
MACRO rotina,param
ld a,param
ld (0F000h),a
ENDM
rotina 15
#endasm
printf("Valor em F000: %d\n",*p);
}
Saída:
Valor em F000: 15
PSECT
Serve para especificar o tipo de área de um programa. Não é necessário o seu uso. Ex [1]:
print MACRO string
psect data ; Define área de dados
999: db string,'$'
psect text ; Define área de código
ld de,999b
ld c,9
call 5
ENDM
Entretanto, quando há trechos separados de códigos e de dados, o "linker" irá fazer com que os trechos de código fiquem em posição contígua. Ex:
psect text
; Código
psect data
; Dados
psect text
; Mais código
Labels
É possível criar labels (etiquetas) para referenciar trechos de código. Porém, quando os labels forem alfanuméricos, devemos declará-los como sendo LOCAL ou GLOBAL.
Um label LOCAL é somente utilizado pelo módulo em questão, enquanto que um label GLOBAL pode ser enxergado por outros módulos do programa.
Exemplo:
LOCAL salto1
ld a,(hl)
cp b
jr nz,salto1
inc hl
ret
salto1: add hl,hl
ret
Código Assembly em função e passagem de parâmetros
Vamos passar o nosso código em Assembly para dentro de uma função, e fazer com que esse código receba o valor de um parâmetro passado para essa função. Veja o programa a seguir.
#include <stdio.h>
void teste(unsigned char c)
{
#asm
ld a,(ix+6)
ld (0F000h),a
#endasm
}
main()
{
unsigned char *p = (usigned char*) 0xF000;
teste(87);
printf("Valor em F000: %d\n", *p);
}
Saída:
Valor em F000: 87
O registrador IX contém os endereços dos parâmetros passados para a função teste. O índice IX+6 contém o endereço do primeiro parâmetro. Como um endereço ocupa 2 bytes, o segundo parâmetro está em IX+8, o terceiro em IX+10 e assim por diante.
┌────┐ ┌────┐
IX+6 │ F3 │ ┌─> D5F3 │ 87 │
├────┼─┘ ├────┤
│ D5 │ D5F4 │ xx │
├────┤ ├────┤
IX+8 │ xx │ D5F5 │ xx │
├────┤ ├────┤
│ xx │ D5F6 │ xx │
└────┘ └────┘
Exemplo com dois parâmetros:
#include <stdio.h>
void teste(unsigned char a, unsigned char b)
{
#asm
ld a,(ix+6) ; Recebe "a"
ld (0F000h),a
ld a,(ix+8) ; Recebe "b"
ld (0F001h),a
#endasm
}
main()
{
unsigned char *p = (usigned char*) 0xF000;
teste(28,99);
printf("Valor em F000: %d\n", *p++);
printf("Valor em F001: %d\n", *p);
}
Saída:
Valor em F000: 28
Valor em F001: 99
┌────┐ ┌────┐
IX+6 │ F3 │ ┌─> D5F3 │ 28 │
├────┼─┘ ├────┤
│ D5 │ ┌─> D5F4 │ 99 │
├────┤ │ ├────┤
IX+8 │ F4 │ │ D5F5 │ xx │
├────┼─┘ ├────┤
│ D5 │ D5F6 │ xx │
└────┘ └────┘
O registrador HL irá armazenar o valor de retorno de uma função. Veja o exemplo a seguir.
#include <stdio.h>
int secreto()
{
#asm
ld hl,61
#endasm
}
main()
{
printf("Meu número secreto é: %d\n", secreto());
}
Saída:
Meu número secreto é: 61
Outro exemplo:
#include <stdio.h>
int soma(int a)
{
#asm
ld a,(ix+6) ; Obtém o valor de "a"
ld hl,(0F000h)
ld b,0
ld c,a
add hl,bc ; Soma o valor de "a" com o valor de &HF000
; e deixa como retorno em HL
#endasm
}
main()
{
int *p = (int *) 0xF000;
*p = 12;
printf("A soma de 12 com 21 dá: %d\n", soma(21));
}
Saída:
A soma de 12 com 21 dá: 33
Configuração da Memória no DOS
Quando criamos um programa em Assembly para rodar no ambiente do Basic, temos os 64 KB de memória configurados da seguinte maneira:
0000H ┌─────┐
│ ROM │ Página 0 - BIOS
4000H ├─────┤
│ ROM │ Página 1 - Interpretador Basic
8000H ├─────┤
│ RAM │ Página 2 - Livre
C000H ├─────┤
│ RAM │ Página 3 - Livre
FFFFH └─────┘
Observamos na configuração apresentada, que tanto a BIOS como o interpretador Basic já estão ativos nas páginas 0 e 1, respectivamente. Assim, qualquer chamada à BIOS ou ao interpretador Basic é feita diretamente ao endereço da rotina.
Por exemplo, para chamar a rotina de limpeza de tela (CLS) da BIOS, desviamos o programa para o endereço &H00C3.
10 CLS: EQU &HC3
CD C3 00 20 CALL CLS
C9 30 RET
No ambiente MSX-DOS, a memória é configurada da seguinte maneira:
0000H ┌─────┐
│ RAM │ Página 0 - DOS / Livre
4000H ├─────┤
│ RAM │ Página 1 - Livre
8000H ├─────┤
│ RAM │ Página 2 - Livre
C000H ├─────┤
│ RAM │ Página 3 - Livre
FFFFH └─────┘
Agora, as quatro páginas de memória estão configuradas para RAM. O acesso à BIOS ou ao interpretador Basic só poderá ser feito, se alterarmos as páginas 0 e/ou 1 para o modo ROM.
Esse processo pode ser feito de duas formas:
- Através de uma rotina de troca de slots da BIOS, que também está disponível para o DOS.
- Trocando manualmente os slots.
A primeira opção realiza a troca de slots automaticamente para o usuário, chama a rotina e depois retorna à configuração do DOS.
As rotinas para troca de slots da BIOS, onde algumas também estão disponíveis para o DOS são as seguintes [2]:
Rotina |
Endereço |
Descrição |
RDSLT |
000CH |
Lê um byte em qualquer slot |
WRSLT |
0014H |
Escreve um byte em qualquer slot |
CALSLT |
001CH |
Chama uma rotina em qualquer slot |
ENASLT |
0024H |
Troca páginas e slots |
CALLF |
0030H |
Chama uma rotina em qualquer slot |
RSLREG |
0138H |
Lê um registrador em slot primário |
WSLREG |
013BH |
Escreve em um registrador de slot primário |
SUBROM |
015CH |
Chama uma rotina na SUB-ROM |
EXTROM |
015FH |
Chama uma rotina na SUB-ROM |
A rotina de troca de slots mais utilizada é a rotina CALSLT (&H1C), que será abordada na próxima seção.
A outra opção é trocar manualmente os slots, através da porta &HA8. Quando isso é feito, o programa em execução DEVERÁ estar em uma página diferente da página da ROM requisitada. Por exemplo, quando chamamos uma rotina da BIOS, devemos estar em uma página diferente da 0.
Essa opção será abordada na seção "Rodando programas em Assembly em qualquer região de memória".
Observação importante: se o seu programa em Assembly NÃO faz qualquer acesso à BIOS ou ao interpretador Basic, NÃO é necessário trocar os slots.
Podemos obter os slots da ROM e SUB-ROM acessando as variáveis de sistema, localizadas na página 3 da RAM. Esta página está disponível tanto para o Basic, como para o DOS.
EXPTBL (&HFCC1) - Slot da ROM
EXBRSA (&HFAF8) - Slot da SUB-ROM
Chamada à BIOS do MSX
O programa a seguir ilustra como chamar uma função da BIOS do MSX. Ele chama a função CHGET, que espera uma tecla ser pressionada.
#include <stdio.h>
main()
{
#asm
chget equ 009fh
exptbl equ 0FCC1h
calslt equ 001Ch
di
push ix
push iy
ld iy,(exptbl-1)
ld ix,chget
call calslt
pop iy
pop ix
ei
#endasm
}
No caso do C, temos que salvar o conteúdo dos registradores IX e IY antes de chamar o CALSLT, e depois recuperá-los.
No exemplo a seguir, faremos uma chamada à SUB-ROM do MSX 2, trocando a cor da paleta para o índice 15 para verde.
#include <stdio.h>
main()
{
#asm
setplt equ 014Dh
exbrsa equ 0FAF8h
calslt equ 001Ch
di
push ix
push iy
ld d,15 ; Indice
ld a,0 ; RB
ld e,7 ; G
ld iy,(exbrsa-1)
ld ix,setplt
call calslt
pop iy
pop ix
ei
#endasm
}
Resumo das chamadas à BIOS:
; Chamda à BIOS (ROM):
PUSH IX
PUSH IY
LD IX,rotina
LD IY,(EXPTBL-1)
CALL CALSLT
POP IY
POP IX
; Chamada à SUB-ROM:
PUSH IX
PUSH IY
LD IX,rotina
LD IY,(EXBRSA-1)
CALL CALSLT
POP IY
POP IX
Para maiores informações sobre esses dois métodos de acesso à BIOS do MSX, consulte o capítulo Criando Rotinas em Assembly do curso de Pascal.
Rodando programas em Assembly em qualquer região de memória
Uma das soluções para determinar o endereço inicial do programa é uso da pseudo-op "ORG". Entretanto, será apresentada uma solução semelhante à do Pascal, para copiar o código em hexadecimal de um vetor para uma determinada região de memória e depois executá-lo.
O exemplo a seguir cria um vetor contendo um código de máquina, onde uma cópia é feita para a posição de memória &HE100 e depois executado pelo programa contido entre #asm e #endasm.
/* Código de máquina */
unsigned char code[14] = { 0xdb, 0xa8, 0xf5, 0xe6, 0xf0, 0xd3, 0xa8,
0xcd, 0x9f, 0x00, 0xf1, 0xd3, 0xa8, 0xc9 };
int i;
main()
{
/* Aponta p para &HE100 */
unsigned char *p = (unsigned char *) 0xe100;
/* Copia o programa para &HE100 */
for (i=0; i<14; i++)
*(p+i) = code[i];
/* Chama o prgrama */
#asm
di
push ix
push iy
call 0e100h
pop iy
pop ix
ei
#endasm
}
O objetivo desse programa é chamar a rotina CHGET na ROM, que espera por uma tecla ser pressionada.
Código mnemônico do programa:
DB A8 IN A,(0A8h) ; Lê porta A8
F5 PUSH AF ; Salva resultado
E6 F0 AND 0F0h ; Seta ROM e BASIC
D3 A8 OUT (0A8h),A
CD F9 00 CALL 09FH ; Chama CHGET
F1 POP AF
D3 A8 OUT (0A8h),A ; Restaura conf.
C9 RET
Podemos também chamar a rotina através do CALSLT:
FD 2A C0 FC LD IY,(EXPTBL-1)
DD 21 9F 00 LD IX,CHGET
CD 1C 00 CALL CALSLT
C9 RET
Código em C:
unsigned char code[12] = { 0xfd, 0x2a, 0xc0, 0xfc, 0xdd, 0x21, 0x9f, 0x00,
0xcd, 0x1c, 0x00, 0xc9 };
Referências:
[1]- Hi-Tech C user's manual, 1989.
[2]- Rotinas do Pierre Gielen, 1993.