Hooks


Você está em: MarMSX >> Cursos >> Assembly Z80   Um hook é um local da memória onde alguns programas contidos na BIOS ou no interpretador Basic fazem referência, de forma a permitir que um outro programa seja disparado antes dessa sub-rotina rodar. Esse mecanismo é similar aos triggers de um sistema gerenciador de banco de dados (SGBD).
  O trecho de memória da área de trabalho do sistema de &HFD9A até &HFFC9 contém 112 hooks, cada um reservado a uma rotina da BIOS em especial. Na inicialização do sistema, cada trecho de memória dos hooks é preenchido com 5 bytes, todos com a instrução RET (&HC9) do Z80. Assim, os hooks em seu estado inicial retornam o controle à rotina chamadora, sem realizar qualquer outra ação.
  As figuras abaixo ilustram como funcionam os hooks do MSX.

Hook no estado normal Hook desviando para um programa

  Quando uma sub-rotina da BIOS que acessa um hook é chamada, ela desvia sua execução para o endereço inicial de seu respectivo hook (figura da esquerda). Sem qualquer modificação nesse hook, a instrução RET é imediatamente encontrada e o controle é retornado para a sub-rotina chamadora do hook, sem realizar qualquer outra ação.
  Entretanto, quando modificamos um hook de forma a desviar a execução da sub-rotina para um novo endereço de memória, que contém um determinado programa (figura da direita), o MSX irá executar esse programa antes de prosseguir com a sub-rotina principal que chamou o hook.

  A ilustração a seguir mostra como estão estruturados os hooks na memória de trabalho na inicialização do MSX.
+--------------++--------------++--------------++--------------+
|    Hook 1    ||    Hook 2    ||      ...     ||   Hook 112   |
+--+--+--+--+--++--+--+--+--++--+--+--+--+--+--++--+--+--+--+--+
|C9|C9|C9|C9|C9||C9|C9|C9|C9|C9||..|..|..|..|..||C9|C9|C9|C9|C9|
+--+--+--+--+--++--+--+--+--+--++--+--+--+--+--++--+--+--+--+--+

  Como cada hook tem apenas 5 bytes, temos somente espaço para colocar uma instrução de desvio para o nosso programa (gasta 3 bytes). Se a instrução de desvio em Assembly for o CALL (&hCD), não podemos esquecer de deixar um comando RET para que a execução volte para a sub-rotina chamadora do hook. Assim, necessitamos de 4 bytes ao todo.
+--------------+
|     Hook     |
+--+--+--+--+--+
|CD| End.|C9|xx|
+--+--+--+--+--+
  Onde xx é um valor qualquer (podemos manter o valor inicial de &hC9).

  Entretanto, se a instrução de desvio for um salto longo JP (&hC3), não há necessidade de se utilizar uma instrução de retorno, pois a instrução RET do programa do usuário irá retornar diretamente para a sub-rotina chamadora.
+--------------+
|     Hook     |
+--+--+--+--+--+
|C3| End.|xx|xx|
+--+--+--+--+--+

  Isto acontece porque a instrução CALL do Assembly coloca na pilha o endereço da próxima instrução antes de desviar para um novo endereço. A instrução RET, por sua vez, desempilha o endereço contido no topo da pilha e desvia a execução do programa para lá. Como a sub-rotina da BIOS chama o hook através de uma instrução CALL, ao utilizarmos outra instrução CALL no hook, o endereço do topo da pilha passa a ser o quarto byte do hook. Já no caso da instrução JP, o topo da pilha ainda é a próxima instrução da sub-rotina chamadora do hook.


  Simulação dos Hooks em Basic

  O programa a seguir mostra algumas rotinas da BIOS e o hook no estado inicial. Até a linha 100 fica o programa principal do usuário.
10 REM
100 END
1000 '
1010 ' Rotinas da BIOS
1020 '
1030 GOSUB 2030 : PRINT "LIST" : END
1040 GOSUB 2040 : PRINT "CLS" : END
1050 GOSUB 2050 : PRINT "NEW" : END
2000 '
2010 ' Hooks
2020 '
2030 RETURN
2040 RETURN 
2050 RETURN

  Quando chamamos uma rotina qualquer no nosso programa, ela visita sua área de hook correspondente e retorna imediatamente para executar o restante das instruções. por exemplo, o comando "LIST".
10 GOSUB 1030
100 END
1000 '
1010 ' Rotinas da BIOS
1020 '
1030 GOSUB 2030 : PRINT "LIST" : END
1040 GOSUB 2040 : PRINT "CLS" : END
1050 GOSUB 2050 : PRINT "NEW" : END
2000 '
2010 ' Hooks
2020 '
2030 RETURN
2040 RETURN 
2050 RETURN
  Saída:
  LIST

  Conforme mencionado na seção anterior, não há espaço suficiente na seção de hooks para inserir um programa. Dessa forma, desviamos a execução no hook para um programa do usuário. Veja o exemplo a seguir.
10 GOSUB 1030
100 END
500 '
510 ' Programa do usuario
520 '
530 PRINT "Acao anterior" : RETURN
1000 '
1010 ' Rotinas da BIOS
1020 '
1030 GOSUB 2030 : PRINT "LIST" : END
1040 GOSUB 2040 : PRINT "CLS" : END
1050 GOSUB 2050 : PRINT "NEW" : END
2000 '
2010 ' Hooks
2020 '
2030 GOTO 530 : RETURN
2040 RETURN 
2050 RETURN
  Saída:
  Acao anterior
  LIST

  Os exemplos acima são meramente ilustrativos. Não podemos desviar o hook para uma linha de um programa do Basic. Dessa forma, o programa para o qual o hook desvia deverá ser em linguagem de máquina.


  A lista completa de hooks [1]

  A seguir, a lista completa das funções do MSX que possuem desvio para os hooks. A primeira coluna contém o endereço do hook. A segunda e terceira possuem a função que chama o hook e seu endereço na ROM. Por fim, a última coluna contém a descrição dessa função.

Hook    Rotina    Descrição
---------------------------------
FD9AH HKEYI 0C4AH Controle de interrupção
FD9FH HTIMI 0C53H Controle de interrupção
FDA4H HCHPU 08C0H Rotina padrão CHPUT
FDA9H HDSPC 09E6H Cursor da tela
FDAEH HERAC 0A33H Cursor de apagar
FDB3H HDSPF 0B2BH Rotina padrão DSPFNK
FDB8H HERAF 0B15H Rotina padrão ERAFNK
FDBDH HTOTE 0842H Rotina padrão TOTEXT
FDC2H HCHGE 10CEH Rotina padrão CHGET
FDC7H HINIP 071EH Copy character set to VDP
FDCCH HKEYC 1025H Keyboard decoder
FDD1H HKYEA 0F10H Keyboard decoder
FDD6H HNMI  1398H NMI Rotina padrão
FDDBH HPINL 23BFH Rotina padrão PINLIN
FDE0H HQINL 23CCH Rotina padrão QINLIN
FDE5H HINLI 23D5H Rotina padrão INLIN
FDEAH HONGO 7810H "ON DEVICE GOSUB"
FDEFH HDSKO 7C16H "DSKO$"
FDF4H HSETS 7C1BH "SET"
FDF9H HNAME 7C20H "NAME"
FDFEH HKILL 7C25H "KILL"
FE03H HIPL  7C2AH "IPL"
FE08H HCOPY 7C2FH "COPY"
FE0DH HCMD  7C34H "CMD"
FE12H HDSKF 7C39H "DSKF"
FE17H HDSKI 7C3EH "DSKI$"
FE1CH HATTR 7C43H "ATTR$"
FE21H HLSET 7C48H "LSET"
FE26H HRSET 7C4DH "RSET"
FE2BH HFIEL 7C52H "FIELD"
FE30H HMKI$ 7C57H "MKI$"
FE35H HMKS$ 7C5CH "MKS$"
FE3AH HMKD$ 7C61H "MKD$"
FE3FH HCVI  7C66H "CVI"
FE44H HCVS  7C6BH "CVS"
FE49H HCVD  7C70H "CVD"
FE4EH HGETP 6A93H Locate FCB
FE53H HSETF 6AB3H Locate FCB
FE58H HNOFO 6AF6H "OPEN"
FE5DH HNULO 6B0FH "OPEN"
FE62H HNTFL 6B3BH Close I/O buffer 0
FE67H HMERG 6B63H "MERGE/LOAD"
FE6CH HSAVE 6BA6H "SAVE"
FE71H HBINS 6BCEH "SAVE"
FE76H HBINL 6BD4H "MERGE/LOAD"
FE7BH HFILE 6C2FH "FILES"
FE80H HDGET 6C3BH "GET/PUT"
FE85H HFILO 6C51H Saída sequencial
FE8AH HINDS 6C79H Saída sequencial
FE8FH HRSLF 6CD8H "INPUT$"
FE94H HSAVD 6D03H "LOC"
            6D14H "LOF"
            6D25H "EOF"
            6D39H "FPOS"
FE99H HLOC 6D0FH "LOC"
FE9EH HLOF 6D20H "LOF"
FEA3H HEOF 6D33H "EOF"
FEA8H HFPOS 6D43H "FPOS"
Hook    Rotina    Descrição
---------------------------------
FEADH HBAKU 6E36H "LINE INPUT#"
FEB2H HPARD 6F15H Parse device name
FEB7H HNODE 6F33H Parse device name
FEBCH HPOSD 6F37H Parse device name
FEC1H HDEVN       Esse hook não é usado
FEC6H HGEND 6F8FH I/O function dispatcher
FECBH HRUNC 629AH Run-clear
FED0H HCLEA 62A1H Run-clear
FED5H HLOPD 62AFH Run-clear
FEDAH HSTKE 62F0H Resseta a pilha
FEDFH HISFL 145FH Rotina padrão ISFLIO
FEE4H HOUTD 1B46H Rotina padrão OUTDO
FEE9H HCRDO 7328H CR,LF to OUTDO
FEEEH HDSKC 7374H Mainloop line input
FEF3H HDOGR 593CH Line draw
FEF8H HPRGE 4039H Fim do programa
FEFDH HERRP 40DCH Controle do erro
FF02H HERRF 40FDH Controle do erro
FF07H HREAD 4128H Mainloop "OK"
FF0CH HMAIN 4134H Mainloop
FF11H HDIRD 41A8H Mainloop direct statement
FF16H HFINI 4237H Mainloop finished
FF1BH HFINE 4247H Mainloop finished
FF20H HCRUN 42B9H Tokenize
FF25H HCRUS 4353H Tokenize
FF2AH HISRE 437CH Tokenize
FF2FH HNTFN 43A4H Tokenize
FF34H HNOTR 44EBH Tokenize
FF39H HSNGF 45D1H "FOR"
FF3EH HNEWS 4601H Runloop new statement
FF43H HGONE 4646H Runloop execute
FF48H HCHRG 4666H Rotina padrão CHRGTR
FF4DH HRETU 4821H "RETURN"
FF52H HPRTF 4A5EH "PRINT"
FF57H HCOMP 4A54H "PRINT"
FF5CH HFINP 4AFFH "PRINT"
FF61H HTRMN 4B4DH "READ/INPUT" error
FF66H HFRME 4C6DH Expression Evaluator
FF6BH HNTPL 4CA6H Expression Evaluator
FF70H HEVAL 4DD9H Factor Evaluator
FF75H HOKNO 4F2CH Factor Evaluator
FF7AH HFING 4F3EH Factor Evaluator
FF7FH HISMI 51C3H Runloop execute
FF84H HWIDT 51CCH "WIDTH"
FF89H HLIST 522EH "LIST"
FF8EH HBUFL 532DH Detokenize
FF93H HFRQI 543FH Converte para inteiro
FF98H HSCNE 5514H Ponteiro para a linha
FF9DH HFRET 67EEH Descritor livre
FFA2H HPTRG 5EA9H Busca variável
FFA7H HPHYD 148AH Rotina padrão PHYDIO
FFACH HFORM 148EH Rotina padrão FORMAT
FFB1H HERRO 406FH Controle de erro
FFB6H HLPTO 085DH Rotina padrão LPTOUT
FFBBH HLPTS 0884H Rotina padrão LPTSTT
FFC0H HSCRE 79CCH "SCREEN"
FFC5H HPLAY 73E5H Sentença "PLAY"



  Utilizando os hooks

  A seguir, será mostrado como utilizar o mecanismo de hooks.

  O comando do Basic LIST possui um hook localizado em &HFF89, tendo os bytes de &HFF89 a &HFF8D reservados para ele.
  Podemos modificar esse hook, de forma a desviar a execução da sub-rotina do LIST para a sub-rotina de limpeza de tela da BIOS (&H00C3). Assim, antes de listar um programa em Basic, o MSX irá limpar a tela.
  O programa em Assembly é listado a seguir.
CD C3 00	CALL &HC3
C9		RET

  A partir desse programa, podemos criar um programa em Basic e alterar o Hook.
10 POKE &HFF89,&HCD
20 POKE &HFF8A,&HC3
30 POKE &HFF8B,0

  A memória correspondente a esse hook é alterada para:
+--------------+
|    "LIST"    |
+--+--+--+--+--+
|CD|C3|00|C9|C9|
+--+--+--+--+--+
  Uma vez que a instrução RET já está no hook, não precisamos inserí-la ao final do programa em Basic.

  Experimente agora dar o comando LIST. A tela será limpa antes do MSX listar o programa.

  Para interromper uma ação do hook, basta alterar o primeiro byte dele de volta para &HC9. Assim ao darmos:
POKE &HFF89,&HC9
  O comando LIST pára de apagar a tela. Isto porque o hook do LIST agora não desvia mais para a rotina de apagar a tela.

  Em vez de utilizar uma instrução CALL, podemos também uma instrução de desvio longo (JP - &HC3). É o que vamos ver no exemplo a seguir.

  No livro 50 dicas para MSX [2], um dos autores sugere utilizar o hook do teclado para emitir um BEEP (sub-rotina &H00C0 da ROM). Para isso, utilizamos o hook correspondente ao comando CHGET (&HFDC2), que espera uma tecla ser pressionada. Nesse caso, sempre que uma tecla for pressionada, o hook correspondente ao CHGET é chamado e a rotina do BEEP é executada. Vamos ao programa em Basic.
10 POKE &HFDC2,&HC3 : ' JP &HC0
20 POKE &HFDC2,&HC0
30 POKE &HFDC2,&H00
  O comando "POKE &HFDC2,&HC9" pára o BEEP.

  Nos dois exemplos apresentados, o hook somente é acionado quando a respectiva função é utilizada. Assim, no primeiro caso, a tela é apagada somente quando utilizamos o comando LIST, e no segundo, somente quando pressionamos uma tecla, o BEEP é executado pelo hook da função CHGET.

  No próximo exemplo, iremos utilizar um hook de uma função que é acionada constantemente pelo sistema, ou seja, uma interrupção. Dessa forma, podemos criar uma animação em um sprite, fazendo com que suas coordenadas sejam atualizadas por um programa que é chamado através do hook dessa interrupção.

  O programa a seguir for adaptado de [3] e faz com que uma bolinha passeie pela tela na screen 1, enquanto estamos livres para mover o cursor. No programa original, um sprite realiza movimentos em diagonal na screen 2 a espera do usuário teclar algo e deasabilitar o hook. Entretanto, achamos mais conveniente demonstrar o funcionamento do hook em segundo plano na screen 1, enquanto podemos realizar outras tarefas.

  A listagem em Assembly do movimento em Y é apresentada a seguir. Para X, troque o endereço C100 para C101.
			10	ORG &HC000
E880	21 00 1B	20	LD HL,&H1B00	; Endereço VRAM de Y
E883	CD 4A 00	30	CALL &H4A	; Lê VRAM
E886	47		40	LD B,A		; Salva em B
E887	3A 00 C1	50	LD A,(&HC100)	; Carrega variável de incremento
E88A	80		60	ADD A,B		; Realiza soma
E88B	4F		70	LD C,A		; Salva em C
E88C	FE 00		80	CP 0		; Verifica se igual a 0
E88E	28 04		90	JR Z,INV	; Se sim, inverte variável de incremento
E890	FE FF		100	CP &HB8		; Verifica se igual a B8
E892	20 08		110	JR NZ,NXT	; Senão continua
E894	3A 00 C1	120 INV:LD A,(&HC100)	; Carrega valor da variável atual
E897	ED 44		130	NEG		; Inverte sinal
E899	32 00 C1	140	LD (&HC100),A	; Salva na memória
E89C	79		150 NXT:LD A,C		; Retoma valor salvo em C
E89D	CD 4D 00	160	CALL &H4D	; Atualiza na VRAM
E8A0	C9		170	RET		; Retona

  Listagem em Basic do programa final.
10 ' Hook bolinha
20 SCREEN 1,0
30 FOR A=&HC000 TO &HC044
40 READ A$
50 POKE A,VAL("&H"+A$)
60 NEXT A
70 ' Movimento em Y
80 DATA 21,00,1B,CD,4A,00,47,3A,00,C1,80
90 DATA 4F,FE,00,28,04,FE,B8,20,08,3A,00
100 DATA C1,ED,44,32,00,C1,79,CD,4D,00
110 ' X-axis movement
120 DATA 21,01,1B,CD,4A,00,47,3A,01,C1,80
130 DATA 4F,FE,00,28,04,FE,F8,20,08,3A,01
140 DATA C1,ED,44,32,01,C1,79,CD,4D,00
145 DATA F7,00,00,00,C9
150 POKE &HC100,2:POKE &HC101,2
155 POKE &HC041,PEEK(&HFDA0):POKE &HC042,PEEK(&HFDA1):POKE &HC043,PEEK(&HFDA2)
160 POKE &HFD9F,&HC3 : '
170 POKE &HFDA0,&H00 : ' Modifica o hook para chamar o programa
180 POKE &HFDA1,&HC0 : '
190 SPRITE$(0)=CHR$(&H3C)+CHR$(&H7E)+CHR$(&HFF)+CHR$(&HFF)+CHR$(&HFF)+CHR$(&HFF)+CHR$(&H7E)+CHR$(&H3C)
200 PUT SPRITE 0,(128,100)
210 END
220 POKE &HFDA0,PEEK(&HC041)
230 POKE &HFDA1,PEEK(&HC042)
240 POKE &HFDA2,PEEK(&HC043)
250 POKE &HFDA3,&HC9
260 POKE &HFD9F,&HF7

  Quando quiser parar o programa da bolinha, digite:
RUN 220 

  Observe que no programa anterior, tivemos que copiar o conteúdo do hook (linhas 145 e 155) para o final do programa do usuário, e depois recuperar esses valores em 220-260. Veremos nas seções a seguir o porquê disso.


  Chamando o programa do usuário de forma segura

  Algumas funções podem apresentar problemas de contexto dos registradores, uma vez que dependem de alguns valores de entrada fornecidos por outra rotina para executar. Se o programa do usuário ou uma rotina da BIOS modificar um desses registradores, poderá haver problemas quando a função chamadora receber o controle de volta e executar.
  Um bom exemplo disso é quando modificamos o hook da função WIDTH.
10 POKE &HFF84,&HC3 ' JP &HC0
20 POKE &HFF85,&HC0
30 POKE &HFF85,0

  Ao darmos o comando WIDTH 40 no Basic, o BEEP é ouvido e surge a seguinte mensagem:
WIDTH 40 
Illegal function call

  Dessa forma, devemos salvar o contexto dos registradores antes de chamar o programa do usuário e depois recuperar o contexto salvo antes de retornar à função chamadora. É exatamente que faz o programa apresentado a seguir.
			10  ORG &HC000
C000	F3		20  DI		; Desabilita interrupções
C001	F5		30  PUSH AF	;
C002	C5		40  PUSH BC	; Salva contexto dos registradores
C003	D5		50  PUSH DE	;
C004	E5		60  PUSH HL	;
C005	CD xx xx	70  CALL end	; Chama programa do usuário em "end" 
C008	E1		80  POP HL	;
C009	D1		90  POP DE	; Recupera contexto
C00A	C1		100 POP BC	;
C00B	F1		110 POP AF	;
C00C	FB		120 EI		; Habilita interrupções
C00D	C9		130 RET		; Retorna
  Onde xx xx é o endereço do programa do usuário. Para a função BEEP (&HC3), por exemplo, temos os valores C0 e 00.

  Agora, em vez de desviar o hook diretamente para o programa do usuário, desviamos para este programa. Assim, modificamos a seguir o hook correspondente ao WIDTH.
10 POKE &HFF84,&HC3 ' JP &HC000
20 POKE &HFF85,0
30 POKE &HFF85,&HC0
  E damos novamente o comando WIDTH na tela do Basic.
WIDTH 40 
Ok
  Dessa vez, tudo funcionou normalmente.



  Quando o hook já possui um desvio - o caso do FILES

  O hook do comando FILES possui o seu estado inicial já modificado para rodar uma determinada rotina antes de apresentar os arquivos em tela. Essa é uma proposta do hook, a de permitir extensões para algumas rotinas da BIOS.
  Podemos sim utilizar hooks desse tipo. Entretanto, devemos copiar o conteúdo original do hook para o final do programa do usuário, de forma a não impedir a rotina chamadora de rodar sua extensão.
  Os hooks modificados podem apresentar diversos valores nos diferentes modelos de MSX. Por exemplo, o hook inicial do FILES no MSX Expert 1.1 é apresentado a seguir. É uma chamada inter-slot [1].
FE7B	F7	RST &H30
FE7C	1	"ID do SLOT"
FE7D	88 6E	"ENDERECO MEMORIA"
FE7F	C9	RET
  Já no MSX 2+ modelo Sanyo PHC-70FD2, temos:
FE7B	F7	RST &H30
FE7C	8B	"ID do SLOT"
FE7D	88 6E	"ENDERECO MEMORIA"
FE7F	C9	RET

  Criamos em seguida um programa no endereço &HC000 para dar um BEEP antes do comando FILES, acrescentando o conteúdo do hook ao final.
10 POKE &HC000,&HCD '
20 POKE &HC001,&HC0 ' CALL &HC0
30 POKE &HC002,0    '
40 POKE &HC003,&HF7 ' RST &H30
50 POKE &HC004,PEEK(&HFE7C)
60 POKE &HC005,PEEK(&HFE7D)
70 POKE &HC006,PEEK(&HFE7E)
80 POKE &HC007,&HC9 ' RET
  As linhas 50-70 copiam os valores diretamente do hook dos files, garantindo que o programa irá rodar em qualquer modelo de MSX.

  Obviamente, devemos desviar o hook do FILES para o nosso programa em &HC000.
100 POKE &HFE7B,&HC3
110 POKE &HFE7C,0
120 POKE &HFE7D,&HC0
130 END
  Nota importante: rode o programa apenas uma vez, pois ele modifica o hook. Se rodado novamente, ele irá ler informações alteradas do hook em vez das informações originais.

  Para desabilitar o BEEP no comando FILES, acrescente as seguintes linhas:
200 POKE &HFE7B,&HF7
210 POKE &HFE7C,PEEK(&HC004)
220 POKE &HFE7D,PEEK(&HC005)
  E dê o comando:
RUN 200 



  Referências:

  1- O Livro Vermelho do MSX, Avalon Software. Editora McGraw Hill.
  2- 50 dicas para MSX, Pierluigi Piazzi et al. Editora Aleph, 1989.
  3- Revista Load MSX #3, artigo sobre hooks. Argentina, 1986.