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  :  rnbastos@ig.com.br 
                 http://www.geocities.com/SiliconValley/Park/3174


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.


Um Exemplo de Comparação

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 ecoa na tela, contudo,
                                               ; 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 com um 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.


Shifts

Um simples conceito, e um 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 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.:

FATO ENGRAÇADO: Caso você não saiba ainda, 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 '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 referenciado como 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.

   1) 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.

   2) 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.

   3) Agora temos um resto de 241.  Dividindo isto por 16 nos dá 15.0625.
       Pegamos o 15, e calculamos o resto.

   4) 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 - VOCÊ 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 achar que isso acelerará 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.


PROGRAMANDO A VGA EM ASSEMBLER  

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

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:

    1) 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.

    2) 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.

    3) você _deve_ 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.

    4) 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:

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! "