Tutorial de Assembler de Adam Hyde 1.0 PARTE 4 Traduzido por Renato Nunes Bastos |
Versão: 1.3
Data: 01-03-1996 /online by Renato 01-11-1998
Contato: blackcat@vale.faroc.com.au
http://www.faroc.com.au/~blackcat
;Renato: Contato
http://www.geocities.com/SiliconValley/Park/3174 (meu site antigo, agora está fora do ar)
http://www.krull.com.br
Exemplo de Comparação | Shifts
/ Aritmética Hexadecima e Binária | Programado a VGA em Assembler
Bem-vindos mais uma vez, florescentes programadores Assembler. Os tutoriais parecem estar ficando populares agora, e eu tenho recebido e-mails pedindo-me para falar sobre o VGA, então eu vou dar olhada. Isso é basicamente para onde eu tenho conduzido no meu modo desconjuntado, de qualquer modo, já que programação gráfica não é só recompensante, é divertido também! Bem, eu acho que é. 🙂
Primeiramente porém, devemos terminar aquela coisa de CMP/JMP, e falar de shifts. Quando se está programando em Assembler, a gente acha que comparações, shifts e testar bits são operações muito comuns.
Eu não vou perder tempo explicando minuciosamente o seguinte exemplo – ele é muito
fácil de entender e você deve pegar a idéia basica seja lá como for.
DOSSEG .MODEL SMALL .STACK 200h .DATA FirstString DB 13, 10, "Este é um grande tutorial ou o quê? :) - $" SecondString DB 13, 10, "NÃO? NÃO? O que você quer dizer, NÃO?$" ThirdString DB 13, 10, "Excelente, vamos ouvir você dizer isso de novo.$" FourthString DB 13, 10, "Apenas um Y ou N já basta.$" ExitString DB 13, 10, "Bem, deixa pra lá!$" .CODE START: MOV AX, @DATA ; Novo modo de dizer: MOV DS, AX ; DS -> SEG segmento de dados KeepOnGoing: MOV AH, 9 MOV DX, OFFSET FirstString ; DX -> OFFSET FirstString INT 21h ; Escreve a primeira mensagem MOV AH, 0 ; Pega uma tecla - armazena-a em AX INT 16h ; AL - código ASCII, AH - "scan code" ; Ela não sai (ecoa) na tela, ou seja, ; nós mesmos temos que fazer isso. PUSH AX ; Aqui nós mostramos na tela o caracter MOV DL, AL ; note que nós salvamos AX. Obviamente, MOV AH, 2 ; usando-se AH para imprimir uma string INT 21h ; destrói-se AX POP AX CMP AL, "Y" ; Checa se foi teclado 'Y' JNE HatesTute ; Se foi, continua MOV AH, 9 ; Mostra a mensagem "Excelente..." MOV DX, OFFSET ThirdString INT 21h JMP KeepOnGoing ; Volta ao início e começa de novo HatesTute: CMP AL, "N" ; Certifica que foi teclado 'N' JE DontLikeYou ; Infelizmente, sim. MOV DX, OFFSET FourthString ; Pede ao usuário para tentar de novo MOV AH, 9 INT 21h JMP KeepOnGoing ; Deixa ele tentar DontLikeYou: MOV DX, OFFSET SecondString ; Mostra a string "NÃO? NÃO? O que..." MOV AH, 9 INT 21h MOV DX, OFFSET ExitString ; Mostra a string "Bem, deixa pra lá!" MOV AH, 9 INT 21h MOV AX, 4C00h ; Volta para o DOS INT 21h END START
Você deveria entender este exemplo, brincar um pouco com ele e escrever algo melhor.
Aqueles que tiverem o livro do Peter Norton ou algo semelhante, experimentem as subfunções do teclado, e veja quais outras combinações de GetKey existem, ou melhor ainda, brinque com a interrupção 10h e entre em algum modo de vídeo sobrenatural – um que seu PC suporte! – e use algumas cores.
Esse é um conceito smples, e que eu já devia ter discutido antes, mas como eu disse – eu tenho minha própria maneira “desconjuntada” de fazer as coisas.
Primeiro você vai precisar de entender um pouco de aritmética hexadecimal e binária – um assunto que eu _deveria_ ter coberto antes. Eu geralmente uso uma calculadora científica – ei, eu sempre uso uma calculadora, eu não sou estúpido! – mas é bom ser capaz de saber como multiplicar, somar e converter entre as várias bases. Você também não pode usar uma calculadora em provas de Computação, não na Austrália.
CONVERTENDO DE BINÁRIO PARA DECIMAL:
De Volta ao Tutorial Um, nós vimos como é que números binários se parecem, então imagine que eu tenha um número binário de oito dígitos, como:
11001101
O que é isso em decimal??? Há várias formas de converter tal número, e eu uso a
seguinte, que acredito se provavelmente a mais fácil:
Número Binário | 1 | 1 | 0 | 0 | 1 | 1 | 0 | 1 | |
Equivalente Decimal | 27 | 26 | 25 | 24 | 23 | 22 | 21 | 20 | |
128 | 64 | 32 | 16 | 8 | 4 | 2 | 1 | ||
Valor Decimal |
128+64+0+0+8+4+0+1= |
205 |
Pegou a idéia? Note que para a última linha, seria mais preciso escrever:
1 x 128 + 1 x 64 + 0 x 32 + 0 x 16 + 1 x 8 + 1 x 4 + 0 x 2 + 1 x 1
= 128 + 64 + 0 + 0 + 8 + 4 + 0 + 1
= 205
Desculpe se isto é um pouco confuso, mas é difícil explicar sem demonstrar.
Aqui vai outro exemplo:
Número Binário | 0 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | |
Equivalente Decimal | 27 | 26 | 25 | 24 | 23 | 22 | 21 | 20 | |
128 | 64 | 32 | 16 | 8 | 4 | 2 | 1 | ||
Valor Decimal |
0+64+32+16+8+4+0+0= |
124 |
Obs.:
- Você pode usar esta técnica com palavras de 16 ou 32 bits também, apenas faça do
jeito certo. Ex: Depois de 128, você escreveria 256, depois 512, 1024 e assim por diante. - Você pode dizer se o equivalente decimal ser par ou ímpar pelo primeiro bit.
Ex.: No exemplo acima, o primeirobit = 0, então o número é PAR. No primeiro exemplo, o primeiro bit é 1, então o
número é ÍMPAR.
FATO ENGRAÇADO: Caso você não saiba ainda, a palavra “bit” vem de Binary digIT. 🙂
CONVERTENDO DE DECIMAL PARA BINÁRIO:
Isso é provavelmente mais fácil que da base-2 para base-10. Para calcular o que 321 seria em binário, você faria o seguinte:
321 = 256 X 1 321 - 256 = 65 = 128 X 0 65 = 64 X 1 65 - 64 = 1 = 32 X 0 1 = 16 X 0 1 = 8 X 0 1 = 4 X 0 1 = 2 X 0 1 = 1 X 1
E você obteria o número binário – 101000001. Fácil, né? Vamos tentar outro para ter certeza que sabemos fazer:
198 = 128 X 1 198 - 128 = 70 = 64 X 1 70 - 64 = 6 = 32 X 0 6 = 16 X 0 6 = 8 X 0 6 = 4 X 1 6 - 4 = 2 = 2 X 1 2 - 2 = 0 = 1 X 0
E isto nos dá – 11000110. Note como você pode checar o primeiro dígito para ver se você conseguiu sua conversão certa. Quando eu escrevi o primeiro exemplo, eu notei que eu fiz um erro quando eu chequei o primeiro bit. No primeiro exemplo, eu consegui 0 – não muito bom para um número ímpar. Eu entendi o erro e corrigi o exemplo.
CONVERTENDO DE HEXADECIMAL PARA DECIMAL:
Antes de começar, você deveria saber que o sistema numérico hexadecimal usa os
seguintes ‘dígitos’:
0 = 0 (decimal) = 0 (binário) 1 = 1 (decimal) = 1 (binário) 2 = 2 (decimal) = 10 (binário) 3 = 3 (decimal) = 11 (binário) 4 = 4 (decimal) = 100 (binário) 5 = 5 (decimal) = 101 (binário) 6 = 6 (decimal) = 110 (binário) 7 = 7 (decimal) = 111 (binário) 8 = 8 (decimal) = 1000 (binário) 9 = 9 (decimal) = 1001 (binário) A = 10 (decimal) = 1010 (binário) B = 11 (decimal) = 1011 (binário) C = 12 (decimal) = 1100 (binário) D = 13 (decimal) = 1101 (binário) E = 14 (decimal) = 1110 (binário) F = 15 (decimal) = 1111 (binário)
Você vai comumente ouvir “hexadecimal” sendo chamado de “hex”, ou “base-16”, e ela é comumente denotada por um ‘h’ – ex.: 4C00h, ou um ‘$’, ex.: – $B800.
Trabalhar com hexadecimal não é tão difícil como pode parecer, e converter pra lá ou pra cá é bem fácil. Como exemplo, vamos converter B800h para decimal:
FATO ENGRAÇADO: B800h é o endereço inicial do vídeo em modo texto para CGA e placas superiores. 🙂
B = 4096 x B = 4096 x 11 = 45056 8 = 256 x 8 = 256 x 8 = 2048 0 = 16 x 0 = 16 x 0 = 0 0 = 1 x 0 = 1 x 0 = 0
Logo B800h = 45056 + 2048 + 0 +0 = 47104
Obs.: Para números em hexadecimal maiores que FFFFh (65535 em decimal), você somente segue o mesmo procedimento como para binário, logo, para o quinto dígito hexadecimal, você multiplicaria por 65535.
Tecle 16 X X na sua calculadora, e fique apertando =. Você verá os números que precisaria usar. O mesmo aplica-se para binário. Ex.: 2 X X e = lhe daria 1, 2, 4, 8, 16... etc.
OK, isso pareceu bem fácil. Eu acho que nem precisamos de um segundo exemplo.
Vamos dar uma olhada em:
CONVERTENDO DE DECIMAL PARA HEXADECIMAL:
Mais uma vez, o mesmo tipo de procedimento como usamos para binário. Logo, para converter 32753 para hexadecimal, você faria assim:
32753 / 4096 = 7 (decimal) = 7h 32753 - (4096 x 7) = 4081 4081 / 256 = 15 (decimal) = Fh 4081 - (256 x 15) = 241 241 / 16 = 15 (decimal) = Fh 241 - (16 x 15) = 1 1 / 1 = 1 (decimal) = 1h
Assim, eventualmente temos 7FF1h como resposta. Este não é particularmente um bom
processo e requer alguma explicação.
- Quando você divide 32753 por 4096 você consegue 7.9963379… Não estamos interessados no lixo .9963379, só pegamos o 7, já que 7 é o maior número inteiro que podemos usar.
- O resto da operação acima é 4081. Devemos agora realizar a mesma operação nisso, mas com 256. Dividindo 4081 por 256 nos dá 15.941406… Novamente, pegamos só o 15.
- Agora temos um resto de 241. Dividindo isto por 16 nos dá 15.0625. Pegamos o 15, e calculamos o resto.
- Nosso último resto acontece que é um. Dividindo isso por um chegamos a, você advinhou – um. VOCÊ NÃO DEVERIA CONSEGUIR UMA RESPOSTA COM MUITAS CASAS DECIMAIS AQUI. SE VOCÊ TEM, CALCULOU ERRADO!!!
É um processo muito imundo, mas funciona. Eu não uso isso, exceto quando eu tenho que usar – eu não sou maluco. Eu uso uma calculadora científica, ou a calculadora do Windows <brrrrr> se eu precisar.
OK, agora que já lidamos com os cálculos horripilantes, você já está pronto para
os shifts. Há geralmente 2 formas da instrução shift – SHL (shift left/esquerda) e SHR
(shift right/direita). Basicamente, tudo o que essas instruções fazem é deslocar uma expressão para a esquerda ou direita um certo número de bits. Sua principal vantagem é a habilidade de lhe deixar substituir multiplicações lentas com shifts mais rápidos. Você vai descobrir que isso acelera pra caramba os algoritmos de pixel/linhas/círculo.
Os PC’s estão ficando cada vez mais rápidos a cada dia – um pouco rápido demais pro meu gosto. De volta aos dias do XT – a multiplicação era _realmente_ lenta – talvez levando atá 4 segundos para certas operações. Hoje em dia isso não acontece assim, mas é uma boa idéia otimizar seu código.
Quando nós plotamos um pixel na tela, temos que encontar o offset do pixel a plotar. Basicamente, o que fazemos é multiplicar a posição Y por 320, somar a posição X, e somar isso ao endereço A000h.
Assim basicamente, temos: A000:Yx320+X
Agora, seja lá quão rápido seu maravilhoso 486 ou Pentium é, isso poderia se feito um pouco mais rápido. Vamos reescrever aquela equação acima, assim, vamos usar alguns números diferentes:
Offset = Y x 28 + Y x 26 + X
Ou:
Offset = Y x 256 + y x 64 + X
Reconhece esses números? Eles parecem terrivelmente com aqueles que nós vimos naquela tabela de conversão binário-decimal. Contudo, nós ainda estamos usando multiplicação. Como podemos incorporar shifts?
Que tal:
Offset = Y SHL 8 + Y SHL 6 + X
Agora, isso é muito mais rápido, já que tudo o que o computador tem que fazer é um shift à esquerda com o número – muito melhor. Note que o shift à esquerda AUMENTA o número, e o shift à direita DIMINUI o número.
Aqui está um exemplo que pode te ajudar se você ainda está em dúvida no que está acontecendo. Digamos que estamos trabalhando em base-10 – decimal. Agora peguemos o número 36 como exemplo. “Shiftando” este número à esquerda de 1, temos:
36 + 36 = 72
Agora SHL 2:
36 + 36 + 36 + 36 = 144
E SHL 3:
36 + 36 + 36 + 36 + 36 + 36 + 36 + 36 = 288
Notou os núeros que se formaram? Havia 2 36’s com SHL 1, 4 36’s com SHL 2 e 8 36’s com SHL 3. Seguindo este padrão, seria justo assumir que 36 SHL 4 equivalerá a 36 x 16.
Note porém, o que está realmente acontecendo. Se você fosse trabalhar com o valor binário de 36, que é mais ou menos isso: 100100, e então shiftasse 36 à esquerda de 2, você teria 144, ou 10010000. Tudo o que a CPU faz na verdade é colocar alguns 1’s e 0’s extras na posição de memória.
Como outro exemplo, pegue o número binário 1000101. Se fizermos um shift à esquerda de 3, terminaríamos com:
1 0 0 0 1 0 1 <---------- SHL 3 1 0 0 0 1 0 1 0 0 0
Agora vamos deslocar o número 45 à DIREITA de 2 unidades. Em binário isso é 101101. De onde:
1 0 1 1 0 1 SHR 2 ----> 1 0 1 1
Notou o que ocorreu? É muito mais fácil para a CPU apenas mover alguns bits
(aproximadamente 2 unidades de clock), do que multiplicar um número. (Pode demorar até 133 unidades de clock).
Nós vamos usar bastante shifts quando estivermos programando a VGA, assim, tenha
certeza de que você entendeu os conceitos por trás disso.
Eu tenho recebido um monte de mails me pedindo para cobrir a VGA. Então, para todos os que pediram, nós estaremos gastando a maior parte do nosso tempo, mas não todo, em programar a VGA. Além do mais, não querem todos programar com gráficos?
Quando nós falamos sobre programar a VGA, nós estamos geralmente falando do modo 13h, ou um de seus parentes. Em VGA padrão este é o _único_ modo de usar 256 cores, e é provavelmente um dos modos mais fáceis também. Se você já tentou experiências com a SVGA, você vai entender o pesadelo que é para o programador dar suporte a todas as diferentes placas SVGA que existem – exceto se você usar VESA que é o que discutiremos outra hora. A grande vantagem do modo padrão 13h é que você sabe que todas as placas VGA que existem vão suportá-lo. As pessoas hoje frequentemente ignoram o modo 13h, achando a resolução muito granulada para os padrões de hoje, mas não se esqueça que Duke Nukem, DOOM, DOOM II, Halloween Harry e a maioria dos jogos da Apogee usam este modo para realizar alguns grandes efeitos.
A grande coisa sobre o modo 13h – isto é 320x200x256 caso você desconheça, é que acessar a VGA RAM é incrivelmente fácil. Como 320 x 200 é igual a 64,000, é possível encaixar a tela inteira em um segmento de 64K.
As más notícias são que o modo padrão 13h realmente só te dá uma página para usar, seriamente embaraçante para scroll e page-flipping. nós vamos cobrir mais tarde estes assuntos, como entrar em seus próprios modos – e modo X que evitará esses problemas.
Então, como entrar no modo padrão 13h?
A resposta é simples. Usamos a interrupção 10h – interrupção de vídeo, e chamamos a subfunção 00h – seleciona o modo. Em Pascal, você poderia declarar uma procedure como esta:
Procedure Init300x200; Assembler; Asm { Init300x200 } mov ah, 00h { Acerta o modo de vídeo } mov al, 13h { Usa o modo 13h } int 10h { Faz isso } End; { Init300x200 }
você também pode ver:
mov ax, 13h int 10h
Isso é perfeitamente correto, e provavelmente economiza um tempo de clock por não colocar 00h em AH e então 13h em AL, mas é mais correto usar o primeiro exemplo.
OK, então estamos no modo 13h, mas o que podemos realmente fazer nele, além de olhar para uma tela em branco? Poderíamos voltar ao modo texto usando:
mov ah, 00h mov al, 03h int 10h
Mas isso é um pouco idiota. Porque não pintar um pixel?
Há inúmeros modos de colocar um pixel na tela. O modo mais fácil em Assembler é usar interrupções. Você faria mais ou menos assim em Pascal:
Procedure PutPixel(X, Y : Integer; Color : Byte); Assembler; Asm { PutPixel } mov ah, 0Ch { subfunção de desenhar pixel } mov al, [Color] { Move a cor a plotar para AL } mov cx, [X] { Move o valor X para CX } mov dx, [Y] { Move o valor Y para DX } mov bx, 1h { BX = 1, p gina 1 } int 10h { Plota } End; { PutPixel }
Contudo, mesmo isso sendo em Assembler, não é particularmente rápido. “Por quê?”, você pergunta. Porque isso usa interrupção. Interrupções são ótimas para entar e sair de modos de vídeo, ligar e desligar o cursor, etc… mas não para gráficos.
Você pode imaginar interrupções como uma secretária eletrônica. “A CPU está ocupada neste momento, mas se você deixar sua subfunção após o sinal – nós entraremos em contato.”
Isso não é bom.
Vamos usar a técnica que discutimos anteriormente durante shifts. O que queremos fazer é botar o valor da cor que desejamor plotar na VGA diretamente. Para fazer isso, precisamos mover o endereço da VGA para ES, e calcular o offset do pixel que queremos plotar. Um exemplo disso é mostrado abaixo:
Procedure PutPixel(X, Y : Integer; Color : Byte); Assembler; Asm { PutPixel } mov ax, 0A000h { Move o segmento da VGA para AX, } mov es, ax { e agora para ES } mov bx, [X] { Move o valor X para BX } mov dx, [Y] { Move o valor Y para DX } mov di, bx { Move X para DI } mov bx, dx { Move Y para BX } shl dx, 8 { Nesta parte usamos shifts para multiplicar } shl bx, 6 { Y por 320 } add dx, bx { Agora somamos X ao valor acima calculado, } add di, dx { dando DI = Y x 320 + X } mov al, [Color] { Põe a cor a plotar em AL } stosb { Põe o byte, AL, em ES:DI } End; { PutPixel }
Esta procedure é rápida o suficiente para começar, embora eu tenha dado uma muito mais rápida uns tutoriais atrás que usa uma técnica genial para pegar DI.
OK, acho que é o suficiente para essa semana. Brinque com as rotinas de PutPixel e veja o que você pode fazer com elas. Para aqueles com um livro do Peter Norton, veja que outros procedimentos você pode fazer usando interrupções.
COISAS PARA FAZER:
- Cobrimos muita coisa nesse tutorial, e alguns conceitos importantes estão nele. Certifique-se de estar comfortável com comparações, porque vamos começar a testar bits em breve.
- Tenha certeza que entendeu aquela coisa de binário ->decimal, decimal -> binário, decimal -> hex e hex -> decimal. Faça você mesmo alguns exemplos de soma e teste suas respostas com a calculadora do Windows.
- Você tem que entender shifts. Se você ainda tem problemas, faça algumas expressões num papel e teste suas respostas num programa como:
Begin { Main } WriteLn(45 SHL 6); ReadLn; End. { Main }
e/ou a calculadora do Windows.
- Dê uma olhada na parte de VGA, e certifique-se de ter pego a teoria por trás disso, porque na próxima semana vamos entrar a fundo nisso.
Semana que vem vou tentar colocar alguns exemplos em C/C++ além de Pascal para vocês programadores de C aí fora.
No próximo tutorial vamos ver:
- Como a VGA é arrumada
- Como podemos desenhar linas e círculos
- Pegando e acertando a palette em Assembler
- Fades
- Alguns exemplos em C/C++
Se você deseja ver um tópico discutido num tutorial no futuro, escreva-me, e eu vou
ver o que eu posso fazer.
Não perca!!! Baixe o tutorial da próxima semana na minha homepage
Vejo vocês na próxima semana!
– Adam.
– Renato Nunes Bastos
“Eu _nunca_ escrevo código com bugs, eu apenas coloco algumas características a mais sem querer!”