C for Experts
Acesso à Memoria e Assembly


  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

  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:   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.


/MARMSX/CURSOS/C