Tutorial de Assembler de
Adam Hyde 1.0 PARTE 9 |
Versão : 1.2
Data : 19-12-1996 / online by Renato 03-11-1998
Contato : blackcat@vale.faroc.com.au
http://www.faroc.com.au/~blackcat
;Renato :
rnbastos@ig.com.br
http://www.geocities.com/SiliconValley/Park/3174
E/S de Arquivos | Chamando o ASM no C++ | Introdução às Macros | O Demo
Nota: O Tutorial de Assembler de Adam é PROTEGIDO POR DIREITOS AUTORAIS, e todos os direitos são reservadas pelo autor. Você pode redistribuir só o arquivo ORIGINAL livremente, e os tutoriais não deveriam ser editados de qualquer forma.
(by Renato - antes que alguém me pergunte, desde o início, antes de traduzir a
1ª palavra do Tutorial 1, eu já havia pedido ao Adam a permissão para fazer isso,
OK? Suponho que eu possa traduzir, já que ele deixou - embora, como ele mesmo disse,
ele não entenda nada de Português, ou Espanhol -- tinha alguém traduzindo para
Espanhol.)
Bem, este tutorial realmente chega atrasado e eu me sinto particularmente culpado sobre isto. Todavia, eu ainda incluí outro programa demonstrativo com esta edição de forma que isto compensa um pouco.
Apenas uma palavrinha sobre o último programa de demonstração - FIRE!. Depois de
obter uma versão mais recente do TASM, eu descobri aquele FIRE! não gosta de compilar
tudo direitinho, assim tenha certeza de que você tem a versão
mais recente do Tutorial Oito (V 1.4) com o bugfix (by Renato - hmmmm, não sei que erro
é esse, e não tenho essa versão 1.4 - o cara sumiu - se alguém souber de algum erro me
diz que vou ver se sei consertar). Você sempre pode obter diretamente todas as revisões
mais novas dos tutoriais de ftp.faroc.com.au no diretório
/pub/blackcat/programming/tutorials, e isso é altamente recomendado.
De qualquer maneira, vamos lá com:
Cedo ou tarde, você vai querer mexer com arquivos. Tudo que você tem que ter em mente aqui é que tudo é BASEADO EM HANDLES. Aqueles de vocês que usaram ou experimentaram com XMS perceberão o que eu quero dizer com handles exatamente, mas se você não, então aqui vai um resumo rápido:
* Você abre/cria um arquivo.
* Você recebe um inteiro de 16 bits sem sinal para referenciá-lo.
Qual a dificuldade nisso?
Nota: Antigamente, antes do DOS 2, você tinha que usar Blocos de Controle de Arquivo (FCB) para referenciar seus arquivos. (Você provavelmente já viu FCBS=xxxx em arquivos de CONFIG.SYS, e isso é para ajudar programas que foram projetados para o XT.) Nós podemos esquecer agora tudo sobre FCBs, já eles estão quase obsoletos.
Abrindo UM Arquivo: (Interrupção 21H)
AH = 3DH
AL = tipo de operação:
0 = operação só
de leitura;
1 = operação só de
escrita;
2 = operação de
leitura/escrita.
DS:DX = nome do arquivo
Retorna:
Se foi realizada com sucesso, o flag de carry é zerado, e o handle de
arquivo é returnado em AX. Porém, se algo saiu errado, o flag de carry é setado em um,
e o código de erro volta em AX. Para uma lista de todo os códigos de erro,
veja a seguinte tabela mais abaixo.
Agora, depois de tudo isso, um exemplo:
.MODEL SMALL
.STACK 200H
.DATA
FileName DB "EXAMPLE.TXT$"
Error DB "Uh oh$"
.CODE
START:
MOV AX, @DATA
;
Aponta AX para o segmento de dados
MOV DS, AX
; AX --> DX
MOV DX, OFFSET FileName
; Põe o offset do arquivo a abrir em DX
MOV AH, 3DH
; Abre
MOV AL, 00H
; só para leitura
INT 21H
JC Problem ; Aconteceu algo de errado?
; Aqui você deveria ter o handle AX, e fazer alguma coisa
JMP Done ; Nada
Problem:
MOV DX, OFFSET Error
; Uh oh
MOV AH, 09H
INT 21H
Done:
MOV AX, 4C00H
;
Pula de volta pro DOS - fechando qualquer
INT 21H
; arquivo aberto. Relaxado, mas nós ainda não sabemos
; como fechar arquivos.
END START
OK... simples bastante, espero. Agora, suponha que queiramos criar um arquivo novo? É
apenas uma outra subfunção simples da interrupção 21H. É assim que se faz:
Criando UM Arquivo Novo: (Interrupção 21H)
AH = 3CH
CX = tipo de arquivo:
0 = arquivo normal;
1 = só de leitura;
2 = arquivo escondido;
4 = arquivo de sistema;
DS:DX = nome do arquivo
Retorna:
Como antes, se realizar com sucesso, o flag de carry é zerado, e o handle do arquivo é retornado em AX. Note que você deve tomar cuidado com arquivos existentes antes de criar um arquivo novo com mesmo nome. O DOS não conferirá se um arquivo do mesmo nome já existe, e escreverá por cima do velho.
Antes de criar um arquivo novo - tente abrir o arquivo primeiro. Se
você obtiver o código de erro 2 em AX, (arquivo não existe), então
prossiga e
crie o arquivo novo. Se você não conseguiu o erro 2, você estará
escrevendo
por cima de um arquivo já existente!
Como você deveria saber de experiências com linguagens de alto-nível, você tem que
fechar seus arquivos antes de terminar seu programa. (Na verdade, a função 4CH fecha
todos os arquivos abertos de qualquer maneira, mas isso é um modo relaxado de fazer
as coisas.) Para fechar um arquivo aberto, você deveria fazer isto:
Fechando UM Arquivo: (Interrupção 21H)
AH = 3EH
BX = handle de arquivo
Retorna:
De novo, qualquer erro é refletido no flag de carry e AX.
Finalmente, códigos de erro. Apenas checando o CF para ver se qualquer coisa saiu
errado, nos deixará saber certamente se algo está faltando, mas nós realmente gostamos
de mais detalhes. Examinar o AX depois de um erro ser descoberto é o caminho a seguir, e
AX poderia conter qualquer um dos códigos seguintes:
Código | Explicação |
00H | erro Desconhecido |
01H | número de função inválido |
02H | Arquivo não achado |
03H | Caminho não achado |
04H | muitos arquivos abertos |
05H | Acesso negado |
06H | handle inválido |
07H | Blocos de controle destruídos |
08H | Falta de memória |
09H | endereço de bloco de controle Ruim |
0AH | ambiente inválido |
0BH | formato inválido |
0CH | código de acesso inválido |
0DH | dados inválidos |
0EH | erro desconhecido |
0FH | drive inválido |
10H | não pode remover diretório atual |
11H | Dispositivo não é o mesmo |
12H | mais nenhum arquivo disponível |
13H | Disco protegido contra escrita |
14H | unidade Ruim |
15H | Drive não pronto |
16H | comando Desconhecido |
17H | erro de CRC |
18H | tamanho de estrutura ruim |
19H | erro de procura |
1AH | mídia inválida |
1BH | Setor não achado |
1CH | Impressora desligada ** |
1DH | erro de escrita |
1EH | erro de leitura |
1FH | falha geral |
** Eu sei, eu não cheguei lá ainda. Eu acho que isso está lá porque o DOS trata tudo como arquivo.
Ufa! Tudo cortesia da boa e velha referência técnica do DOS. Algum deles lá encima são bem obscuros - na verdade só há alguns que você precisa de se lembrar. Algum de meu *favoritos * é: Setor não achado, Erro de procura e Erro de CRC no meio de uma pilha de disquetes relaxados arjeados. É o tipo de porcaria que traz recordações. :)
Certo, assim nós vimos como criar, abrir e fechar arquivos. Agora vamos fazer algo com
eles. Para ler alguns bytes de um arquivo, você tem que usar função 3FH. Assumindo que
você já abriu o arquivo de onde você quer ler, você pode usar um pouco de código como
o abaixo:
MOV AH, 3FH
; Lê byte(s)
MOV BX, Handle
; arquivo a trabalhar
MOV CX, BytesToRead
; quanto a ler
MOV DX, OFFSET WhereToPutThem ; um array ou
variável
INT 21H
JC DidSomethingGoWrong
; Checa erros
Se você está tendo problemas em sacar algo disso - não se preocupe muito. Apenas volte aos exemplos acima e veja como pode fazer sentido. Próximo tutorial nós continuaremos com sprites - (e como carrregá-los do disco) - assim você verá um bom exemplo.
Bem... agora, escrevendo em um arquivo. Muito semelhante a ler, nós usamos a função
40H. Um código para escrever um byte se pareceria com isso:
MOV AH, 40H
; Escreve byte(s)
MOV BX, Handle
; arquivo para se escrever nele
MOV CX, BytesToWrite
; quanto escrever
MOV DX, OFFSET WhereToWriteFrom ; de onde os dados estão
vindo
INT 21H
JC DidSomethingGoWrong
; algum erro?
Bem, aquilo quase conclui E/S de arquivos para este tutorial. Embora não seja um componente principal da peogramação da linguuagem Assembly, E/S de arquivos é todavia, um conceito importante para se pegar.
Eu suponho que já passou da hora de falar
sobre como linkar o Assembler no C. Pessoalmente, eu prefiro codificar VGA numa
combinação de Assembler/Pascal. Porém, C tem seu lugar, e linkar com C é um
assunto importante que nós
deveríamos cobrir.
Você deve ter percebido que você pode entrar código Assembly em seu programa de C
desse jeito:
/ * Seu código em C vai aqui * /
asm {
/ * * /
/ * Seu código em Assembler vai aqui * /
/ * * /
}
/ * Seu código em C continua daqui * /
Agora, considerando que nós podemos inserir o Assembly diretamente no código em C, por que nos preocuparíamos em escrever código externo? A resposta é bastante simples. Usando rotinas externas, temos código que é mais rápido de se executar, mais rápido de compilar, que pode usar algumas das características especiais de Turbo Assembler - como modo ideal, e pode ser até mesmo portável a outras linguagens.
Escrever código externo para C é bem simples, e é gratificantemente mais fácil que
escrever código externo para Pascal. (Veja o Tutorial Sete). Como você pôde observar no
Tutorial Sete, nós tínhamos que declarar o segmento de código e o de dados usando o a
meio confusa diretiva SEGMENT. Isto é devido ao modo como o Pascal gosta de organizar a
memória, e só há um modo de contornar problema - nós podemos usar o modelo TPASCAL.
Infelizmente, TPASCAL é um modo antiquado de fazer as coisas, assim nós temos que pôr
um pouco de trabalho nisso. Eu não vou falar novamente em TPASCAL, assim nós podemos nos
esquecer seguramente de detalhes chatos.
Note que nada disto aplica a nós em C - nós podemos usar felizmente nossos simples e
agradáveis esqueletos de Assembler. Há algumas restrições colocadas, entretanto, a
nós pela maioria dos compiladores:
Além desses pequenos detalhes, há pouco que nós precisamos ter em mente.Vamos lá!
OK... agora nós vamos escrever uma pequena rotina externa e linkar isto ao C. Vamos
dar uma olhada num esqueleto básico que apenas põe algum texto na tela.
============================ LIBRARY.ASM
=============================
.MODEL SMALL
.DATA
Message DB "Well looky here - we got ourselves some text$"
.CODE
PUBLIC _sample
; ---------------------------------------------------------------------------
;
; void sample();
;
_sample PROC NEAR ; Declara uma procedure near
MOV AH, 00H
; Acerta o modo de video
MOV AL, 03H
; Modo 03H
INT 10H
MOV AH, 09H
; Imprime uma string
MOV DX, OFFSET Message ; DS:DX <--
Mensagem
INT 21H
RET ; Fora daqui!
_sample ENDP
END
Bem.... não há nada muito engenhoso lá. Agora, e o código C que vai junto com isto?
============================= EXAMPLE.C
==============================
extern void sample();
int main()
{
sample();
return 0;
}
E para compilar o lote, a linha abaixo fará o trabalho.
C:\> TCC EXAMPLE.C LIBRARY.ASM
Claro que, se você está usando então que outro "sabor" de C, substitua TCC
com qualquer outro interpretador de linha de comando que você tiver. Também é possível
fazer o C reconhecer variáveis declaradas em Assembler, e o seguinte esqueleto explica
como isso é feito:
============================ LIBRARY.ASM
=============================
.MODEL SMALL
.DATA
PUBLIC _YourVariable ; Declara uma variável externa
_YourVariable DW 9999 ; Faz a variável ser uma word valendo 9999
.CODE
END
============================= EXAMPLE.C
==============================
extern int YourVariable;
int main()
{
printf("The Assembler external variable is:
%d", YourVariable);
return(0);
}
Novamente, compile isto com: TCC EXAMPLE.C LIBRARY.ASM
Mas que tal passar parâmetros para suas rotinas? Nós poderíamos fazer isso do modo difícil, como nós fizemos com Pascal, ou alternativamente, poderíamos usar a diretiva ARG.
ARG é brilhante, porque simplifica grandemente as coisas -- mas tem algumas negligências. Isto é, em toda rotina você precisa de umas três instruções adicionais. Se você quer velocidade e não se incomoda com um pouco de trabalho duro, trabalhe diretamente com a pilha como nós fizemos no Tutorial Sete.
Aqui está como se usa ARG:
============================ LIBRARY.ASM
=============================
.MODEL SMALL
.DATA
.CODE
PUBLIC _putpixel ; Declara a procedure externa
; ---------------------------------------------------------------------------
;
; void putpixel(int x, int y, char color, int location);
;
_putpixel PROC NEAR
ARG X : Word, Y : Word, Color : Byte, Location : Word
PUSH BP
; Salva BP
MOV BP, SP
; BP *deve ser* igual a
SP para ARG funcionar
MOV AX, [Location] ; Parâmetros podem ser
acessados facilmente agora
MOV ES, AX
MOV BX, [X]
MOV DX, [Y]
MOV DI, BX
MOV BX, DX
SHL DX, 8
SHL BX, 6
ADD DX, BX
ADD DI, DX
MOV AL, [Color]
MOV ES:[DI], AL
POP BP ; BP precisa ser restaurado!
RET
_putpixel ENDP
END
============================= EXAMPLE.C
==============================
extern void putpixel(int x, int y, char color, int location);
int main()
{
asm {
mov ax, 0x13
int 0x10
}
putpixel(100, 100, 12, 0xa000);
sleep(2);
asm {
mov ax, 0x03
int 0x10
}
return(0);
}
Não é tão macetoso, hein? Porém, se você escolher escrever rotinas externas porque você quer a velocidade que o Assembler pode lhe dar, então acesse a pilha do modo difícil. Esses extras push's e pop's realmente podem crescer se sua rotina de putpixel for chamada 320x200 vezes!
Macros são uma das características mais poderosas que você tem à sua disposição quando está trabalhando com o Assembler. Freqüentemente você se achará repetindo as mesmas poucas linhas de código inúmeras vezes quando estiver escrevendo programas maiores. Você não quer fazer aquela dificuldade de criar um procedimento -- que reduziria a velocidade do código, mas você não quer continuar se repetindo.
A resposta.... MACROS.
Uma macro é só um conjunto de instruções que recebe um nome pelo qual ela será
referenciada no código. Você pode definir um macro assim:
MyMacroName MACRO
;
; Suas instruções vão aqui
;
ENDM
MyMacroName
E dali em diante, sempre que você puser MyMacroName em seu código, serão colocadas
as instruções contidas dentro da macro no lugar do nome da macro.
OBS.: É provavelmente melhor declarar qualquer macro antes de declarar o segmento de dados. Para ficar mais claro, coloque todas suas macros em outro arquivo de texto e então use INCLUDE<nomedoarquivo> para incluir as macros.
Macros também podem ter parâmetros e podendo ser muito úteis. Por exemplo, eu usei muito a função DOS 09H para pôr uma string na tela. Eu poderia fazer os programas que eu escrevo mais fáceis de ler à primeira vista criando a seguinte macro:
PutText MACRO TextParam
MOV AH, 09H
; TextParam é o parâmetro--NÃO
MOV DX, OFFSET TextParam
; uma variável. Substitua TextParam com
INT 21H
; qualquer nome que você escolher.
ENDM PutText
Então, assumindo no segmento de dados que eu tenha declarado uma string assim:
AString DB "This is a string$"
Eu poderia exibir aquela string escrevendo:
PutText AString
OBS.: Quando você está trabalhando com macros, tenha cuidado em observar
que registradores elas mudam. Se estiver em dúvida, dê um push e um pop em
quaisquer registradores que você sente que possam ser afetados.
Embora aquela macro simples realmente não fosse nada de especial, macros têm muitas outras utilidades. Eu não vou dizer mais nada sobre macros agora, mas eu as usarei de vez em quando em programas de demonstração no futuro, e você aprenderá outras técnicas que você pode pôr em bom uso.
De qualquer maneira, vamos ao que eu queria fazer:
No princípio eu ia lançar este tutorial sem um programa demo, mas vendo como eu fui um pouco preguiçoso esse tempo todo, (e também porque um amigo meu há pouco tempo fez uma demonstração como essa), eu decidi incluir um demo de plasma.
Plasmas podem ser um pouco engenhosas em umas partes -- me levou um bom tempo para
fazer a coisa funcionar direito por causa de um problema que eu tive com minha tabela de
lookup. Mas se você seguir o algoritmo abaixo, você não deve ter nenhum problema.
*** Antes de começar, você precisará de QUATRO variáveis temporárias em
***
seu código. Em Assembler isto pode se pôr um pouco trabalhoso porque você
se achará freqüentemente com falta de registradores. Você poderia declarar
alguns bytes no segmento de dados, mas é mais rápido usar registradores.
Estas quatro variáveis temporárias armazenarão só números entre 0 e 255,
assim elas só precisam ser BYTES.
No algoritmo, eu me refiro a
estas variáveis temporárias como Temp1,
*** Temp2, Temp3 e Temp4.
***
O algoritmo se parece com isso:
Isto é basicamente só uma senóide longa. Você pode
experimentar
usar uma onda de co-seno, ou alterar a amplitude da função que você
está usando. Eu criei minha tabela de lookup usando a seguinte expressão:
For W := 1 To 512 Do
SinTable[W] := Round(Sin(W / 255 * Pi * 2) * 128);
(SinTable é um array de 512 BYTES)
Eu pessoalmente gosto de fazer minhas palettes depois de
ver o demonstrativo
rodando com a palette padrão. Desse modo, fazendo certas cores escuras e outras
muito claras, o resultado é exatamente do jeito que eu quero.
Eu descobri que o melhor modo de fazer isto é capturar a tela quando a demonstração
está rodando, com um programa como Screen Thief, então carregar aquela tela
em um programa de pintura que deixe alterar a palette.
Depois de conseguir a palette do jeito que você quer,
salve-a para o disco como
um arquivo COL (se possível) e então escreve um pequeno programa para ler no
arquivo COL e escrever um arquivo tipo o PLASMA.DAT.
Se lembre, Screen Thief é shareware, assim se você for
usá-lo, envie para
o autor algum dinheiro, hein?
Loop (1): Antes de começar a plotar a primeira linha, você deve:
* Zerar Temp4;
* Decrementar Temp3 de dois;
Você pode
fazer experiências com Temp3 -- quanto maior for o número
que você subtrair, mais rápido o plasma vai mover.
Você agora vai para o Loop (2).
Loop (2): Ao início de cada linha você deve:
* Incrementar Temp4 de um;
* Fazer Temp1 =
Sintable[Linha corrente + Temp3];
* Fazer Temp2 =
SinTable[Temp4];
Você agora vai para o Loop (3).
Loop (3): Para todo pixel na linha atual você deve:
* Calcular a cor daquele pixel a ser plotado;
O valor de cor daquele pixel é simplesmente definido por:
SinTable[Temp1 + Temp2] + SinTable[Linha corrente + Temp2]
Infelizmente,
isto é um pouco mais difícil de calcular no
Assembler e acaba levando muitas linhas de código!!
* Incrementar Temp1 de um;
* Incrementar Temp2 de um;
Depois de fazer uma linha inteira, você então volta atrás ao Loop (2).
Uma vez feitas todas as linhas (200), você pode então voltar
ao Loop (1).
Claro que, você também vai querer pôr algo para checar o retrace, e seria uma boa
idéia também se alguém apertou alguma tecla!!
NOTA: Para quem não sabe, o VGA tem um registrador de estado que vale a pena
prestar atenção em que ele serve. É registrador 03DAH, e conferindo seus
vários bits, podemos ver o que está acontecendo com o VGA.
(Para aqueles que querem saber para quê são
exatamente todos os bits,
ache que deveriam obter uma cópia da Ralf Brown's Interrupt List. Isto
está disponível na minha homepage e ela contém uma lista completa de todas
as interrupções, registradores e muito mais.)
De qualquer modo, nós estamos só interessados no
quarto bit do 03DAH que nos
deixa saber se um retrace está acontecendo. Se pudermos acessar o VGA enquanto
o canhão de elétrons do monitor está voltando (retrace) ao topo da tela -- nós
podemos obter um rápido acesso, livre de flicks (tremidas..., sacou? - by Renato)
O que é demais, já que o retrace acontece a cada 1/80 de segundo EM TODOS OS
COMPUTADORES, é agora nós temos um método de fazer que nosso demo rode numa
velocidade específica em todas as máquinas.
Para conferir se o retrace está acontecendo, nós
examinamos simplesmente
o bit quatro. Se o bit quatro está setado, um retrace está em execução.
Porém, nós não sabemos o quanto de um retrace já aconteceu, e não sabemos
quanto tempo de acesso live de flicks nós temos. A solução é checar de novo
por um retrace, de modo que podemos estar seguros de que estamos no
COMEÇO de um.
Eu usei retrace no código para ter certeza de que
o demo ia
rodar à mesma velocidade em todas as máquinas. (Mais ou menos).
Também note que meu plasma é mais uma variante
de plasma. Você pode,
e é encorajado a alterar o código -- (tente incrementar os valores de temp
em vez de decrementar e mudar a quanto o valor de temp é decrementado ou
mudar o modo como o valor das corer é achado. Também tente mudar a palette,
porque isto pode fazer o plasma parecer completamente diferente).
É possível criar todos os tipos de efeitos só
fazendo mudanças simples,
assim, experimente... seja criativo!
Bem, isso conclui as coisas para este tutorial. Eu não estou precisamente seguro sobre o que eu porei no Tutorial Dez (by Renato - que Tutorial 10? tá sonhando... cadê? bem que eu queria que eles continuassem. Se alguém souber onde o Adam foi parar, me avise.) -- serão (bye Renato - seriam) cobertos sprites, mas eu posso ver que os tutoriais talvez tendam a efeitos de demonstração e como você pode os codificar em Assembler puro.
Até a próxima vez, (by Renato - hahaha. Engraçadinho!)
-- Adam.
(by Renato - cadê a finalização de Sempre? Sobre pegar na próxima semana?
Parece que o cara já sabia que esse era o último.
E agora, as minhas despedidas:
Bom galera, agradeço a todos vocês que me ajudaram a continuar essa tradução,
incentivando-me a terminar logo isso, sempre me escrevendo, cobrando.
O interesse demonstrado por vocês foi fundamental para que isso acabasse.
Finalmente, a tradução desses tutoriais de Assembler terminaram.
Se houver erros na tradução, ou alguma frase que não tá dando pra entender,
me avisem, que eu traduzo ela de novo!, de modo a ficar inteligível.
Valeu!
Agora, vou ver se eu começo a traduzir os tutoriais de VGA do Denthor.
Alguém sabe onde ele foi parar também?
bye,
Renato Nunes Bastos, aka Krull
Rio de Janeiro, 3 de Novembro de 1998)