assembler

107
O que é Assembler? -------------------- Assembler é uma linguagem de baixo nível, que você pode usar em seus programas para acelerar tarefas lentas. Basicamente ela consite de sentenças que representam instruções em linguagem de máquina, e, como ela está próxima ao código de máquina, ela é rápida. Há muito tempo atrás, quando o 8086 apareceu, programar não era uma tarefa fácil. Quando os primeiros computadores foram desenvolvidos, a programação tinha que ser feita em código de máquina, que não era uma tarefa fácil, e assim o Assembler nasceu. Por que usá-lo? ----------------- Assembler é veloz. Ele também permite a você falar com a máquina a nível de hardware, e lhe dá muito maior controle e flexibilidade sobre o PC. LIÇÃO 1 - Registradores -------------------------- Quando você está trabalhando com Assembler, você tem que usar registradores. Você pode imaginá-los como sendo variveis já definidas para você. Os mais comuns estão listados abaixo: þ AX - o acumulador. Compreende AH e AL, os bytes alto e baixo de AX. Comumente usado em operações matemáticas e de E/S. þ BX - a base. Compreende BH e BL. Comumente usado como uma base ou registrador apontador. þ CX - o contador. Compreende CH e CL. Usado frequentemente em loops. þ DX - o deslocamento, similar ao registrador de base. Compreende DH e DL. Acho que você está pegando o espírito da coisa agora. Estes registradores são definidos como registradores de uso geral pois podemos realmente armazenar qualquer coisa que quisermos neles. São também registradores de 16 bits, o que significa que podemos armazenar um inteiro positivo de 0 a 65535, ou um inteiro com sinal de -32768 to 32768.

Upload: ave-fenix

Post on 24-Jul-2015

77 views

Category:

Documents


0 download

TRANSCRIPT

Page 1: Assembler

O que é Assembler? -------------------- Assembler é uma linguagem de baixo nível, que você pode usar em seus programas para acelerar tarefas lentas. Basicamente ela consite de sentenças que representam instruções em linguagem de máquina, e, como ela está próxima ao código de máquina, ela é rápida. Há muito tempo atrás, quando o 8086 apareceu, programar não era uma tarefa fácil. Quando os primeiros computadores foram desenvolvidos, a programação tinha que ser feita em código de máquina, que não era uma tarefa fácil, e assim o Assembler nasceu. Por que usá-lo? ----------------- Assembler é veloz. Ele também permite a você falar com a máquina a nível de hardware, e lhe dá muito maior controle e flexibilidade sobre o PC. LIÇÃO 1 - Registradores -------------------------- Quando você está trabalhando com Assembler, você tem que usar registradores. Você pode imaginá-los como sendo variveis já definidas para você. Os mais comuns estão listados abaixo: þ AX - o acumulador. Compreende AH e AL, os bytes alto e baixo de AX. Comumente usado em operações matemáticas e de E/S. þ BX - a base. Compreende BH e BL. Comumente usado como uma base ou registrador apontador. þ CX - o contador. Compreende CH e CL. Usado frequentemente em loops. þ DX - o deslocamento, similar ao registrador de base. Compreende DH e DL. Acho que você está pegando o espírito da coisa agora. Estes registradores são definidos como registradores de uso geral pois podemos realmente armazenar qualquer coisa que quisermos neles. São também registradores de 16 bits, o que significa que podemos armazenar um inteiro positivo de 0 a 65535, ou um inteiro com sinal de -32768 to 32768.

Page 2: Assembler

Incidentalmente, o assunto do alto e do baixo byte destes resgistradores causou muita confusão no passado, logo, tentarei dar alguma explicação aqui. AX tem um intervalo de 0 até FFFFh. Isto significa que você tem um intervalo de 0 até FFh para AH e AL. (Se sabe pouco sobre hexadecimal, não se preocupe. O próximo tutorial vai falar sobre ele.) Agora, se nós tivermos que armazenar 0A4Ch em AX, AH conterá 0Ah, e AL conterá 4Ch. Sacou? Este é um conceito muito importante, e eu falarei sobre ele mais profundamente no próximo tutorial. Os registradores de segmento: - ta da! Estes são outros registradores que nós não vamos ver nos primeiros tutorias, mas vamos vê-los em maior profundidade mais tarde. Eles são imensamente úteis, mas podem ser também perigosos. þ CS - o segmento de código. O bloco de memória onde o código é armazenado. NÃO brinque com esse, a menos que saiba o que está fazendo. þ DS - o segmento de dados. A área na memória onde os dados são armazenados. Durante operações de bloco, quando grandes blocos de dados são movidos, este é o segmento a que a CPU comumente se refere. þ ES - o segmento extra. Apenas outro segmento de dados, mas este é comumente usado quando se quer acessar o vídeo. þ SS - É o segmento de pilha, em que a CPU armazena endereços de retorno de subrotinas. Tome cuidado com ele. Alguns outros que você vai comumente usar: þ SI - o índice de fonte. Frequentemente usado para movimentações de blocos de instruções. Este é um ponteiro que, com um segmento, geralmente DS, é usado pela CPU para leitura. þ DI - o índice de destino. Novamente, você o usará muito. Um outro ponteiro que, com um segmento, geralmente ES, é usado para escrita pela CPU. þ BP - o apontador da base, usado em conjunto com o segmento de pilha. Nós não vamos usá-lo muito. þ SP - o apontador da pilha, comumente usado com o segmento de pilha. NÃO brinque com isso de jeito nenhum.

Page 3: Assembler

Por enquanto você deveria saber o que são registradores. Há outros registradores também, e coisas conhecidas como flags, mas nós não iremos a eles agora. COISAS PARA FAZER: 1) Aprender os vários registradores de cor. 2) Arrumar uma calculadora que suporte hexadecimal - ou pelo menos uma tabela ASCII. Isso cobre 0 - 255, ou, de 0h a FFh. LIÇÃO 2 - O conjunto de instruções do 8086: ---------------------------------------------- Okay, então você já aprendeu sobre registradores, mas, como usá-los, e como se codifica em Assembler? Bem, primeiro você precisa de algumas instruções. As seguintes instruções podem ser usadas em todas as CPU's do 8086 para cima. þ MOV <dest>, <valor> - MOVE. Esta instrução permite MOVER um valor para uma posição na mem¢ria. Ex.: MOV AX, 13h Isso deveria mover 13h (19 em decimal) para o registrador AX. Logo, se AX valia antes 0, ele agora seria 13h. ISSO APENAS MOVE UM VALOR PARA UM REGISTRADOR, NÃO FAZ NADA MAIS. Ex.: (Em Pascal) AX := $13; þ INT <número> - INTERRUPÇÃO. Esta instrução gera uma interupção. Você pode pensar nisso como sendo quase uma procedure. Ex.: INT 10h Geraria a interrupção 10h (16 em decimal). Agora, o que isso faria depende do conteúdo do registrador AH, entre outras coisas. Por exemplo, se AX = 13h e a interrupção 10h foi gerada, o vídeo seria

Page 4: Assembler

colocado no modo 320x200x256. Mais precisamente: AH seria igual a 00 - seleciona a subfunção do modo, e AL seria igual a 13h - modo gráfico 320x200x256. Contudo, se AH = 2h, e a interrupção 16h foi gerada, isso instruiria a CPU para checar se alguma tecla pressionada está no buffer do teclado. Se AH = 2h, e BH = 0h e a interrupção 10h foi gerada, então a CPU moveria o cursor para a posição X em DL e posição Y DH. NÃO SE PREOCUPE COM ISSO POR ENQUANTO! NÓS FALAREMOS NISSO MAIS TARDE, COM MAIS DETALHES. þ ADD <dest> <valor> - ADICIONA. Esta instrução soma um número ao valor armazenado em dest. Ex: MOV AX, 0h ; AX agora é igual a 0h ADD AX, 5h ; AX agora é igual a 5h ADD AX, 10h ; AX agora é igual a 15h þ SUB <dest> <valor> - SUBTRAI. Acho que dá pra você adivinhar o que isso faz. Ex: MOV AX, 13h ; AX agora é igual a 13h (19 dec) SUB AX, 5h ; AX agora é igual a 0Eh (14 dec) þ DEC <registrador> - DECREMENTA algo. Ex: MOV AX, 13h ; AX agora é igual a 13h DEC AX ; AX agora é igual a 12h þ INC <registrador> - INCREMENTA algo. Ex: MOV AX, 13h ; Adivinha... INC AX ; AX = AX + 1 þ JMP <posição> - PULA para uma posição.

Page 5: Assembler

EG: JMP 020Ah ; Pula para a instrução em 020Ah JMP @MyLabel ; Pula para @MyLabel. NÃO SE PREOCUPE SE ISTO É UM POUCO CONFUSO - VAI FICAR PIOR! HÁ OUTRAS 28 INSTRUÇÕES JUMP PARA APRENDER, TALVEZ MAIS. FALAREMOS NELAS MAIS TARDE. þ CALL <procedimento> - CHAMA uma subfunção. EG: Procedure MyProc; Begin { MyProc } { ... } End; { MyProc } Begin { Main } Asm CALL MyProc ; Adivinha o que isso faz! End; End. Ou: CALL F6E0h ; Chama subfunção em F6E0h þ LOOP <rótulo/label> - Faz LOOPS (repetição) durante um certo tempo. EG: MOV CX, 10h ; Isto é o porque de CX ser ; chamado de registro CONTADOR. ; 10h = 16 @MyLabel: ; alguma coisa ; mais coisa LOOP @MyLabel ; At‚ que CX = 0 ; Note: CX é decrementado ; a cada vez. NÆo decremente-o ; você mesmo (DEC CX). ; ISSO DEVERIA SE REPETIR 16 vezes - i.e., 10 em hexadecimal. þ LODSB - Carrega um byte LODSW - Carrega uma word

Page 6: Assembler

STOSB - Armazena um byte STOSW - Armazena uma word Estas instruções são usadas para pôr ou conseguir algo numa posição na memória. O registrador DS:SI, (lembra que nós falamos sobre isso antes, sobre SI ser o índice de fonte?), aponta para a localização de onde queremos obter os dados, e DS:DI aponta para onde colocaremos informações. É claro, não somos obrigados a usar DS - poderia ser ES por exemplo. Meu procedimento PutPixel colocar um byte em ES:DI. De qualquer modo, imagine que temos a seguinte configuração na mem¢ria: Posição na memória ³ 06 ³ 07 ³ 08 ³ 09 ³ 10 ³ 11 ³ 12 Valor ³ 50 ³ 32 ³ 38 ³ 03 ³ 23 ³ 01 ³ 12 Quando nós usamos LODSB ou STOSB, ele retorna ou pega um número de AL. Assim, se DS:SI apontava para 07 e executássemos uma instrução LODSB, AL seria agora igual a 32. Agora, se nós apontássemos DS:DI para 11, colocando, diria, 50 no registrador AL, e executasse STOSB, então teríamos o seguinte resultado: Posição na Memória ³ 06 ³ 07 ³ 08 ³ 09 ³ 10 ³ 11 ³ 12 Valor ³ 50 ³ 32 ³ 38 ³ 03 ³ 23 ³ 50 ³ 12 OBS.: Quando usamos LODSB/STOSB, usamos AL. Isto porque estaremos mexendo com um número de 8 bits (um byte), apenas. Podemos armazenar um número de 8 bits em AL, AH, ou AX, mas não podemos armazenar um número de 16 bits em AH ou AL porque eles são REGISTRADORES DE 8 BITS. Como resultado, quando usarmos LODSW ou STOSW, nós devemos usar AX e não AL, já que estaremos pegando/colocando um número de 16 bits. þ MOVSB - Move um byte MOVSW - Move uma word Como exemplo vamos pegar um byte de DS:SI e mandá-lo para ES:DI. Em DS:SI:

Page 7: Assembler

Posição de Memória ³ 06 ³ 07 ³ 08 ³ 09 ³ 10 ³ 11 ³ 12 Valor ³ 50 ³ 32 ³ 38 ³ 03 ³ 23 ³ 50 ³ 12 Em ES:DI: Posição de Memória ³ 06 ³ 07 ³ 08 ³ 09 ³ 10 ³ 11 ³ 12 Valor ³ 10 ³ 11 ³ 20 ³ 02 ³ 67 ³ 00 ³ 12 Se apontarmos DS:SI para a posição 07, apontarmos ES:SI para a posição 11 e executarmos MOVSB, o resultado em ES:DI pareceria com: Em ES:DI: Posição de Memória ³ 06 ³ 07 ³ 08 ³ 09 ³ 10 ³ 11 ³ 12 Valor ³ 10 ³ 11 ³ 20 ³ 02 ³ 67 ³ 32 ³ 12 ESPERO QUE VOCÊ PEGUE A IDÉIA GERAL. CONTUDO, É CLARO, NÃO É TÃO SIMPLES. POSIÇÕES DE MEMÓRIA NÃO SÃO ARRUMADOAS EM FORMA DE ARRAY, EMBORA EU DESEJASSE MUITO QUE FOSSEM. QUANDO FOR MOVER/PEGAR/COLOCAR, VOCE ESTARÁ MEXENDO COM UMA POSIÇÃO TAL COMO: 100:102H. AINDA ASSIM, VOCÊ DEVERIA PEGAR A IDÉIA. þ REP - REPETE o número de vezes especificado no registrador CX. Um REP na frente de um MOVSB/LODSB/STOSB causaria a repetição da instrução. Logo: Se CX = 5, e se ES:DI apontava para 1000:1000h, então REP STOSB armazenaria o que estava no registrador AL na posição 1000:1000h 5 vezes. COISAS A FAZER: 1) Memorizar todas as instruções acima - não é tão difícil assim e não há tantas lá. 2) Tenha certeza que você entendeu a teoria por trás delas.

Page 8: Assembler

LIÇÃO 3 - Segmentos e Offsets --------------------------------- Antes de explorarmos o grande e mau mundo dos segmentos e offsets, há umas terminologias que você precisar conhecer. þ O BIT - a menor parte de dados que podemos usar. Um bit - um oitavo de um byte pode ser ou um 1 ou um 0. Usando esses dois dígitos podemos fazer números em BINÁRIO ou BASE 2. EX. 0000 = 0 0100 = 4 1000 = 8 1100 = 12 10000 = 16 0001 = 1 0101 = 5 1001 = 9 1101 = 13 ... 0010 = 2 0110 = 6 1010 = 10 1110 = 14 0011 = 3 0111 = 7 1011 = 11 1111 = 15 þ O NIBBLE, ou quatro bits. Um nibble pode ter um valor máximo de 1111 que é 15 em decimal. É aqui que o hexadecimal entra. Hex é baseado naqueles 16 números, (0-15), e quando escrevemos em hex, usamos os 'dígitos' abaixo: 0 1 2 3 4 5 6 7 8 9 A B C D E F Hexadecimal é na verdade muito fácil de se usar, e, apenas como curiosidade, eu acho que os Babilônios - uma civilização antiga qualquer - usava um sistema de numeração em BASE 16. Tem algum historiador aí fora que queira confirmar isso? IMPORTANTE >>> Um nibble pode aguentar um valor até Fh <<< IMPORTANTE þ O BYTE - o que mais usaremos. O byte tem 8 bits de tamanho - isso é 2 nibbles, e é o único valor que você vai conseguir colocar num registrador de 8 bits. EX.: AH, AL, BH, BL, ... Um byte tem um valor máximo de 255 em decimal, 11111111 em binário, ou FFh em hexadecimal. þ A WORD - outra unidade comumente usada. Uma word é um número de 16 bits, e é capaz de armazenar um número até 65535. Isso é 1111111111111111 em binário, e FFFFh em hex. Obs.: Por causa de uma word ser quatro nibbles, é também representada por quatro dígitos hexadecimais. Obs.: Isto é um número de 16 bits, e corresponde aos registradores de 16 bits. Ou seja, AX, BX, CX, DX, DI, SI, BP, SP, DS, ES, SS

Page 9: Assembler

e IP. þ A DWORD, ou double word consiste de 2 words ou 4 bytes ou 8 nibbles ou 32 bits. Você não vai usar muito as double words nestes tutoriais, mas vamos mencioná-las mais tarde quando falarmos de PROGRAMAÇÃO EM 32 BITS. Uma DWORD pode armazenar de 0 a 4,294,967,295, que é FFFFFFFFh, ou 11111111111111111111111111111111. Espero que haja 32 um's lá atrás. A DWORD também é o tamanho dos registradores extendiddos de 32 BITS, ou seja, EAX, EBX, ECX, EDX, EDI, ESI, EBP, ESP e EIP. þ O KILOBYTE, é 1024 bytes, NÃO 1000 bytes. O kilobyte é igual a 256 double-words, 512 words, 1024 bytes, 2048 nibbles ou 8192 BITS. Eu não vou escrever todos os um's. þ O MEGABYTE, ou 1024 kilobytes. Isso é 1,048,576 bytes ou 8,388,608 bits. Agora que já cobrimos a terminologia, vamos dar uma olhada mais de perto como aqueles registradores são estruturados. Nós dissemos que AL e AH eram registradores de 8 bits, logo, eles não deveriam se parecer com algo assim? AH AL ³ 0³0³0³0³0³0³0³0 ³ ³ 0³0³0³0³0³0³0³0 ³ Neste caso, ambos AH e AL = 0, OU 00h e 00h. Como resultado, para calcular AX usamos: AX = 00h + 00h. Quando digo + eu quero dizer, 'ponha junto' não AX = AH MAIS AL. Assim, se AH era igual a 00000011 e AL era igual a 0000100, para calcular AX nós devemos fazer o seguinte. 1) Pegue os valores hexadecimais de AH e AL. 00000011 = 03h 00010000 = 10h 2) Combine-os. AX = AH + AL AX = 03h + 10h AX = 0310h E aí você consegue o resultado. Não é tão macetoso assim.

Page 10: Assembler

Okay, agora vamos ver os registradores de 16 bits: AX ³ ³ AH AL ³ 0³0³0³0³0³0³0³0 ³ ³ 0³0³0³0³0³0³0³0 ³ De onde podemos ver que AX = 00000000 e 00000000, ou 0000000000000000. Agora por último, vejamos como um registrador de 32 bits se parece: ³ EAX ³ ³ AX ³ ³ ³ 00000000 ³ 00000000 ³ 00000000 00000000 ³ ³ AH ³ AL ³ ³ Não ‚ muito difícil, espero. E se entendeu isso, você está pronto para SEGMENTOS e OFFSETS. Uma Arquitetura Segmentada ----------------------------- Há muito, muito tempo atrás, quando a IBM construiu o primeiro PC, não era costume programas terem mais de 1 megabyte - eca, os primeiros XT's tinham apenas 64K de RAM! De qualquer modo, vendo que os projetistas do XT não consideravam aplicações enormes, decidiram dividir a memória em SEGMENTOS, pequenas áreas de mem¢ria RAM que você pode colocar APENAS uma tela virtual para gráficos em modo 320x200x256. É claro, você pode acessar mais de um megabyte de RAM, mas você tem que dividi-la em segmentos para usá-la, e esse é o problema. É obvio, com programação em 32 bits dé pra acessas até 4GB de RAM sem usar segmentos, mas isso é uma outra hist¢ria. Segmentos e offsets são apenas um método de especificar uma posição na memória.

Page 11: Assembler

EG: 3CE5:502A ^^^^ ^^^^ SEG OFS Okay, aqui está a especificação: Um OFFSET = SEGMENT X 16 Um SEGMENT = OFFSET / 16 Algums registradores de segmento são: CS, DS, ES, SS e FS, GF - Obs.: Os últimos 2 são registradores que só existem em 386 ou superiores. Alguns registradores de offset são: BX, DI, SI, BP, SP, IP - Obs.: Quando em modo protegido, você pode usar qualquer registrador de uso geral como um registrador de offset - EXCETO IP. Alguns segmentos e offsets comuns são: CS:IP - Endereço do c¢digo executando no momento. SS:SP - Endereço da posição atual da pilha. OBS.: NÃO SE INTROMETA COM ELES ! Assim quando nos referirmos a segmentos e offsets, faremos dessa forma: SEGMENTO:OFFSET Um bom exemplo seria: A000:0000 - que na verdade corresponde ao topo esquerdo da tela VGA em modo colorido 320x200x256. ** FATO ENGRAÇADO ** A RAM da VGA começa em A000h :) Ufa! Isso foi muito para o segundo tutorial. Contudo, ainda não terminamos. Esse negócio de AX, AH, AL é um conceito que você pode não ter sacado ainda, então lá vamos nós:

Page 12: Assembler

MOV AX, 0 ; AX = 0 MOV AL, 0 ; AL = 0 MOV AH, 0 ; AH = 0 MOV AL, FFh ; AL = FFh ; AX = 00FFh ; AH = 00h INC AX ; AX = AX + 1 ; AX = 0100h ; AH = 01h ; AL = 00h MOV AH, ABh ; AX = AB00h ; AH = ABh ; AL = 00h COISAS A FAZER: 1) Aprender aquele negócio de BIT/NIBBLE/BYTE... de cor. 2) Voltar nos exemplos de segmento e offset. 3) Tenha certeza que você entendeu a relação entre AX, AH e AL. 4) Que tal um problemas de adição hexadecimal? A Pilha ----------- A pilha é uma característica muito útil de que podemos tirar vantagem. Pense nela como uma pilha de papéis numa bandeja de ENTRADA. Se você põe algo no topo, ela será a primeira a ser tirada. À medida que você adiciona algo à pilha, o apontador de pilha é DECREMENTADO, e quando tira, é INCREMENTADO. Para explicar isso melhor, veja o diagrama abaixo: ³ A PILHA ³ ³ ³ <<< Quando colocamos um byte na pilha, ele vai ³ þ ³ aqui - último a entrar, primeiro a sair. ³ þ ³ ³ þ ³

Page 13: Assembler

³ þ ³ ³ þ ³ ³ SP ³ <<< O ponteiro de pilha se move para baixo. E na prática: MOV AX, 03h ; AX = 03h PUSH AX ; PUSH AX na pilha (coloca no topo) MOV AX, 04Eh ; AX = 04Eh ; Faça alguma coisa... uma soma? POP AX ; AX = 03h Ou: MOV AX, 03h ; AX = 03h PUSH AX ; Adiciona AX à pilha MOV AX, 04Eh ; AX = 04Eh ; Faça alguma coisa... uma soma? POP BX ; BX = 03h Você acabou de aprender duas instruções: þ PUSH <registrador> - PUSH (coloca algo na pilha), e þ POP <registrador> - POP (retira ele de volta). É tudo o que você precisa de aprender sobre pilha - por enquanto. Por último, algumas procedures que demonstram algo disso tudo. Note que os comentários foram DELIBERADAMENTE REMOVIDOS. É seu dever tentar comentá-los. Note também, que algumas novas instruções são introduzidas.

Page 14: Assembler

Procedure ClearScreen(A : Byte; Ch : Char); Assembler; Asm { ClearScreen } mov ax, 0B800h mov es, ax xor di, di mov cx, 2000 mov ah, A mov al, &Ch rep stosw End; { ClearScreen } Procedure CursorXY(X, Y : Word); Assembler; Asm { CursorXY } mov ax, Y mov dh, al dec dh mov ax, X mov dl, al dec dl mov ah, 2 xor bh, bh int 10h End; { CursorXY } Procedure PutPixel(X, Y : Integer; C : Byte; Adr : Word); Assembler; Asm { PutPixel } mov ax, [Adr] mov es, ax mov bx, [X] mov dx, [Y] xchg dh, dl mov al, [C] mov di, dx shr di, 2 add di, dx add di, bx stosb End; { PutPixel }

Page 15: Assembler

Procedure Delay(ms : Word); Assembler; Asm { Delay } mov ax, 1000 mul ms mov cx, dx mov dx, ax mov ah, 86h int 15h End; { Delay } COISAS A FAZER: 1) Vá ao exemplo de pilha. Faça seu próprio código exemplo. 2) Comente as procedures acima do melhor modo que puder. Tente adivinhar o que as novas intruções fazem. Não é tão difícil. Durante este tutorial, você achará os livros "Peter Norton's Guide to Assembler", "Peter Norton's Guide to the VGA Card", ou qualquer um dos livros "Peter Norton's Guide to..." muito úteis. Você não pode programar em Assembler sem saber pra que são todas as interrupções e o que são todas as subfunções. Eu lhe recomendo conseguir uma cópia desses livros assim que possível. Um Programa Assembly ----------------------- Eu geralmente não escrevo código 100% em Assembly. É muito mais conveniente usar uma linguagem de alto nível como C ou Pascal, e usar Assembly para acelerar os bits lentos. Contudo, você pode querer se torturar e escrever uma aplicação completamente em Assembly, então aqui vai a configuração básica: ³ DOSSEG - diz à CPU como organizar o segmento ³ MODEL - declara o modelo que vamos usar ³ STACK - quanta pilha vamos alocar? ³ DATA - o quê vai no segmento de dados ³ CODE - o quê vai no segmento de código

Page 16: Assembler

³ START - o início do seu código ³ END START - o fim do seu código DOSSEG .MODEL SMALL .STACK 200h .DATA .CODE START: MOV AX, 4C00h ; AH = 4Ch, AL = 00h INT 21h END START Vamos ver em detalhes. Abaixo, cada uma das frases acima está explicada. þ DOSSEG - isto ordena os segmentos na ordem: Segmentos de Código; Segmentos de Dados; Segmentos de Pilha. Não se preocupe muito com isso por enquanto, apenas inclua até que você saiba o que está fazendo. þ MODEL - isso permite à CPU determinar como seu programa está estruturado. Você pode ter os seguintes MODELos: 1) TINY - tanto código quanto dados se encaixam no mesmo segmento de 64K. 2) SMALL - código e dados estão em segmentos diferentes, embora cada um tenha menos de. 3) MEDIUM - código pode ser maior que 64K, mas os dados têm que ter menos que 64K. 4) COMPACT - códido é menos de 64K, mas dados podem ter mais que 64K. 5) LARGE - código e dados podem ter mais que 64K, embora arrays não possam ser maiores que 64K.

Page 17: Assembler

6) HUGE - código, dados e arrays podem ter mais de 64K. þ STACK - isso instrui ao PC para arrumar uma pilha tão grande quanto for especificado. þ DATA - permite a você criar um segmento de dados. Por exemplo: MySegment SEGMENT PARA PUBLIC 'DATA' ; Declare alguns bytes, words, etc. MySegment ENDS Isso é similar a CONSTANTES in Pascal. þ CODE - permite a você criar um segmento de código. Ex.: MyCodeSegment SEGMENT PARA PUBLIC 'CODE' ; Declare algo MyCodeSegment ENDS þ START - Apenas um label para dizer ao compilador onde a parte principal do seu programa começa. þ MOV AX, 4C00h ; AH = 4Ch, AL = 00h Isso move 4Ch para ah, que coincidentemente nos traz de volta ao DOS. Quando a interrupção 21h é chamada e AH = 4Ch, de volta ao DOS lá vamos nós. þ INT 21h þ END START - Você não tem imaginação? Okay, Espeo que você tenha entendido tudo isso. Neste exemplo nós vamos usar a interrupção 21h, (a interrupção do DOS), para imprimir uma string. Para ser preciso, vamos usar a subfunção 9h, e ela se parece com isso: þ INTERRUPÇÃO 21h þ SUBFUNÇÃO 9h

Page 18: Assembler

Requer: þ AH = 9h þ DS:DX = ponteiro FAR para a string a ser impressa. A string deve ser terminada com um sinal $. Assim, aqui está o exemplo: DOSSEG .MODEL SMALL .STACK 200h .DATA OurString DB "Isto é uma string de caracteres" .CODE START: MOV AX, SEG OurString ; Move o segmento onde OurString está MOV DS, AX ; para AX, e agora para DS MOV DX, OFFSET OurString ; Offset de OurString -> DX MOV AH, 9h ; Subfunção de imprimir strings INT 21h ; Gera a interrupção 21h MOV AX, 4C00h ; Subfunção de saída para o DOS INT 21h ; Gera a interrupção 21h END START Se você assemblar isso com TASM - TASM SEJALADOQUEVOCECHAMOUELE.ASM então linkar com TLINK - TLINK SEJALADOQUEVOCECHAMOUELE.OBJ você vai conseguir um arquivo EXE de cerca de 652 bytes. Você pode usar estes programas no DEBUG com algumas modificações, mas eu vou deixar isso contigo. Para trabalhar com Assembler puro você _precisa_ de TASM e TLINK, embora eu ache que MASM faria o mesmo trabalho muito bem. Agora vamos ao código com um pouco mais detalhado: MOV AX, SEG OurString ; Move o segment onde OurString está MOV DS, AX ; para AX, e agora para DS MOV DX, OFFSET OurString ; Move o offset onde OurString está localizado MOV AH, 9h ; Subfunção de escrita de strings INT 21h ; Gera a interrupção 21h

Page 19: Assembler

Você vai notar que tivemos que usar AX para p“r o endereço do segmento de OurString em DS. Você vai descobrir que não dá pra referenciar um registrador de segmento diretamente em Assembler. Na procedure PutPixel do último tutorial, eu movi o endereço da VGA para AX, e então para ES. A instrução SEG é também introduzida. SEG retorna o segmento onde a string OurString está localizada, e OFFSET retorna, adivinha o quê?, o offset do início do segmento para onde a string termina. Note também que nós usamos DB. DB não é nada de especial, e significa Declare Byte, que é o que tudo o que ela faz. DW, Declare Word e DD, Declare Double Word também existem. Você poderia ter também colocado OurString segmento de código, a vantagem é que estaria CS apontando para o mesmo segmento que OurSting, de modo que você não tem que se preocupar em procurar o segmento em que OurString está. O programa acima no segmento de código seria mais ou menos assim: DOSSEG .MODEL SMALL .STACK 200h .CODE OurString DB "Abaixo o segmento de dados!$" START: MOV AX, CS MOV DS, AX MOV DX, OFFSET OurString MOV AH, 9 INT 21h MOV AX, 4C00h INT 21h END START Simples, não? Então, o que são flags? --------------------------

Page 20: Assembler

Esta parte é para meu companheiro Clive que tem me perguntado sobre flags, então lá vamos nós Clive, com FLAGS. Eu não me lembro se já introduzimos a instrução CMP ou não, CMP - (COMPARE), mas CMP compara dois números e reflete a comparação nos FLAGS. Para usá-la você faria algo desse tipo: þ CMP AX, BX então seguir com uma instrução como essas abaixo: COMPARAÇÕES SEM SINAL: ------------------------ þ JA - pula (jump) se AX foi MAIOR que BX; þ JAE - pula se AX foi MAIOR ou IGUAL a BX; þ JB - pula se AX foi MENOR que BX; þ JBE - pula se AX foi MENOR ou IGUAL a BX; þ JNA - pula se AX foi NÃO MAIOR que BX; þ JNAE - pula se AX foi NÃO MAIOR ou IGUAL a BX; þ JNB - pula se AX foi NÃO MENOR que BX; þ JNBE - pula se AX foi NÃO MENOR ou IGUAL a BX; þ JZ - pula se o flag de ZERO está setado - o mesmo que JE; þ JE - pula se AX for IGUAL a BX; þ JNZ - pula se o flag de ZERO NÃO está setado - o mesmo que JNE; þ JNE - pula se AX NÃO for IGUAL a BX; COMPARAÇÕES COM SINAL: ------------------------ þ JG - pula (jump) se AX foi MAIOR que BX; þ JGE - pula se AX foi MAIOR ou IGUAL a BX; þ JL - pula se AX foi MENOR que BX; þ JLE - pula se AX foi MENOR ou IGUAL a BX þ JNG - pula se AX foi NÃO MAIOR que BX þ JNGE - pula se AX foi NÃO MAIOR ou IGUAL a BX; þ JNL - pula se AX foi NÃO MENOR que BX; þ JNLE - pula se AX foi NÃO MENOR ou IGUAL a BX; þ JZ - pula se o flag de ZERO está setado - o mesmo que JE; þ JE - pula se AX for IGUAL a BX; þ JNZ - pula se o flag de ZERO NÃO está setado - o mesmo que JNE; þ JNE - pula se AX NÃO for IGUAL a BX;

Page 21: Assembler

NÃO TÃO COMUNS: ----------------- þ JC - pula se o flag de CARRY está setado; þ JNC - pula se o flag de CARRY NÃO está setado; þ JO - pula se o flag de OVERFLOW está setado; þ JNO - pula se o flag de OVERFLOW NÃO está setado; þ JP - pula se o flag de PARIDADE está setado; þ JNP - pula se o flag de PARIDADE NÃO está setado; þ JPE - pula se a PARIDADE for PAR - o mesmo que JP; þ JPO - pula se a PARIDADE for ÖMPAR - o mesmo que JNP; þ JS - pula se o flag de SINAL NÃO está setado; þ JNS - pula se o flag de SINAL está setado. De qualquer modo, aqui está com o que eles se parecem: ³ Flag ³ SF ³ ZF ³ -- ³ AF ³ -- ³ PF ³ -- ³ CF ³ ³ Bit ³ 07 ³ 06 ³ 05 ³ 04 ³ 03 ³ 02 ³ 01 ³ 00 ³ Legenda: ---------- SF - Flag de Sinal; ZF - Flag de Zero; AF - Flag Auxiliar; PF - Flag de Paridade. CF - Flag de Carry (vai um). Nota: HÁ MUITO MAIS FLAGS PARA APRENDER. Eles serão vistos num Tutorial mais à frente. COISAS PARA FAZER: 1) Volte ao frame da configuração básica de Assembler e memorize-o. 2) Tenter escreer um programa simples que mostre alguns comentários criativos. 3) Aprenda as instruções JUMP menos criptográficos de cor.

Page 22: Assembler

PS: - Procedure: apenas um sumário do que cada comando faz. Ex.: MOV AX, 0003h ; AX agora é igual a 03h; ADD AX, 0004h ; AX agora é igual a 07h; Então, aqui vai o conjunto completo das procedures com comentários: { Esta procedure limpa a tela em modo texto } Procedure ClearScreen(A : Byte; Ch : Char); Assembler; Asm { ClearScreen } mov ax, 0B800h { Move o endereço de vídeo para AX } mov es, ax { Aponta ES para o segmento de vídeo } xor di, di { Zera DI } mov cx, 2000 { Move 2000 (80x25) para CX } mov ah, A { Move o atributo para AH } mov al, &Ch { Move o caracter a usar para AL } rep stosw { Faz isso } End; { ClearScreen } Explicação: Nós zeramos DI, logo é igual a 0 - o canto esquerdo da tela. Isto é de onde vamos começar a encher a tela. Movemos 2000 para CX porque vamos colocar 2000 caracteres na tela. { Esta procedure move o cursor para a posição X, Y } Procedure CursorXY(X, Y : Word); Assembler; Asm { CursorXY } mov ax, Y { Move o valor Y para AX } mov dh, al { Y vai para DH } dec dh { rotina baseada em ajustar para zero } mov ax, X { Move o valor de X para AX } mov dl, al { X vai para DL } dec dl { rotina baseada em ajustar para zero }

Page 23: Assembler

mov ah, 2 { Chama a função correspondente } xor bh, bh { Zera BH } int 10h { faz isso (põe o cursor na posição) } End; { CursorXY } Explicação: A ' rotina baseada em ajustar para zero' é realizada porque a BIOS refere-se à posição (1, 1) como (0, 0), e igualmente (80, 25) como (79, 24). Procedure PutPixel(X, Y : Integer; C : Byte; Adr : Word); Assembler; Asm { PutPixel } mov ax, [Adr] { Move o endereço do VGA em AX } mov es, ax { Joga AX em ES } mov bx, [X] { Move o valor de X para BX } mov dx, [Y] { Move o valor de Y para DX } xchg dh, dl { Daqui pra frente calcula o } mov al, [C] { offset do pixel a ser plotado } mov di, dx { e põe este valor em DI. Vamos } shr di, 2 { ver isso mais tarde - próximo tutorial} add di, dx { quando falarmos sobre shifts } add di, bx { versus muls } stosb { Guarda o byte em ES:DI } End; { PutPixel } NOTA: Eu estaria muito interessado em achar uma procedure PutPixel mais rápida que essa. Eu já vi uma inline que faz isso em metade do tempo, mas mesmo assim, essa é muito quente. { Esta procedure é uma função de delay independente de CPU } Procedure Delay(ms : Word); Assembler; Asm { Delay } mov ax, 1000 { Move o número de ms em um segundo para AX } mul ms { Faz AX = número de ms a esperar } mov cx, dx { Prepara para o delay - põe número de ms } mov dx, ax { onde necessário } mov ah, 86h { Cria o delay } int 15h

Page 24: Assembler

End; { Delay } Vamos falar um pouco sobre o VGA. 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

Page 25: Assembler

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

Page 26: Assembler

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. Eu geralmente uso uma calculadora científica, mas é bom ser capaz de saber como multiplicar, somar e converter entre as várias bases. 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 º º ³ 7 ³ 6 ³ 5 ³ 4 ³ 3 ³ 2 ³ 1 ³ 0 º º Equivalente Decimal ³ 2 ³ 2 ³ 2 ³ 2 ³ 2 ³ 2 ³ 2 ³ 2 º º Equivalente Decimal ³ 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 = 28 + 64 + 0 + 0 + 8 + 4 + 0 + 1 = 205 Desculpe se isto é um pouco confuso, mas é difícil explicar sem demonstrar. Aqui vai outro exemplo:

Page 27: Assembler

º Número Binário ³ 0 ³ 1 ³ 1 ³ 1 ³ 1 ³ 1 ³ 0 ³ 0 º º ³ 7 ³ 6 ³ 5 ³ 4 ³ 3 ³ 2 ³ 1 ³ 0 º º Equivalente Decimal ³ 2 ³ 2 ³ 2 ³ 2 ³ 2 ³ 2 ³ 2 ³ 2 º º Equivalente Decimal ³ 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 serpar ou ímpar pelo primeiro bit. Ex.: No exemplo acima, o primeiro bit = 0, então o número é PAR. No primeiro exemplo, o primeiro bit é 1, então o número é ÍMPAR. 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

Page 28: Assembler

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

Page 29: Assembler

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

Page 30: Assembler

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. OK, agora que já lidamos com os cálculos horripilantes, Você já está pronto para os shifts. - 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. 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: 8 6 Offset = Y x 2 + Y x 2 + X Ou: Offset = Y x 256 + y x 64 + X

Page 31: Assembler

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

Page 32: Assembler

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

Page 33: Assembler

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, pgina 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

Page 34: Assembler

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. 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 comfortvel com comparações, porque vamos começar a testar bits em breve.

Page 35: Assembler

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, porque na próxima semana vamos entrar a fundo nisso. Note que eu incluí um demo starfield neste tutorial dessa semana. Você pode rodar STARS.EXE, ou olhar STARS.PAS que é o código fonte. É só um simples demo, mas pode ser usado para fazer alguns efeitos interessantes. Agora, esta semana vamos inicialmente listar um sumário de todas as instruções que você já deveria ter aprendido até agora, e umas novas. Então vamos ver como a VGA é arrumada, e cobrir uma rotina simples de linha. SUMÁRIO DO CONJUNTO DE INSTRUÇÕES þ ADC <DEST>, <FONTE> - Nome: Soma com Vai-Um Tipo: 8086+ Descrição: Esta instrução soma <FONTE> e <DEST> e soma o valor armazenado no flag de vai-um, que será um ou zero,

Page 36: Assembler

a <DEST> também. Basicamente, DEST = DEST + FONTE + CF EX.: ADD AX, BX þ ADD <DEST>, <FONTE> - Nome: Add Tipo: 8086+ Descrição: Esta instrução soma <FONTE> e <DEST>, armazenando o resultado em <DEST>. EX.: ADD AX, BX þ AND <DEST>, <FONTE> - Nome: E Lógico Tipo: 8086+ Descrição: Esta instrução realiza uma comparação bit a bit de <DEST> e <FONTE>, armazenando o resultado em <DEST>. EX.: AND 0, 0 = 0 AND 0, 1 = 0 AND 1, 0 = 0 AND 1, 1 = 1 þ BT <DEST>, <BIT NUMBER> - Nome: Testa Bit Tipo: 80386+ Descrição: Esta instrução testa <BIT NUMBER> de <DEST> que pode ser um registrador de 16 ou 32 bits ou posição da memória. Se <DEST> é um número de 16 bits então o <BIT NUMBER> pode ser de 0 a 15, senão, se <DEST> é um número de 32 bits, então o <BIT NUMBER> pode ter um valor de 0 a 31. O valor em <BIT NUMBER> de <DEST> é então copiado para o flag de vai-um.

Page 37: Assembler

EX.: BT AX, 3 JC EraIgualAUm þ CALL <DEST> - Nome: Chama Procedimento Tipo: 8086+ Descrição: Esta instrução simplesmente chama uma subrotina. Em termos mais técnicos, ela põe o endereço da próxima instrução, IP, na pilha, e seta IP, o registro de instruções, para o valor especificado por <DEST>. EX.: CALL MyProc þ CBW - Nome: Converte Byte para Word Tipo: 8086+ Descrição: Esta instrução extende o byte em AL para AX. EX.: MOV AL, 01h CBW ADD BX, AX ; Faz algo com AX þ CLC - Nome: Limpa Flag de Vai-Um Tipo: 8086+ Descrição: Esta instrução zera o flag de vai-um no registrador de flags. EX.: CLC þ CLD - Nome: Limpa Flag de Direção Tipo: 8086+ Descrição: Esta instrução limpa o flag de direção no registrador de flags para 0. Quando o flag de direção é 0, qualquer instrução de strings incrementa os registradores de índice SI e DI.

Page 38: Assembler

EX.: CLD þ CLI - Nome: limpa Flag de Interrupção Tipo: 8086+ Descrição: Esta instrução limpa o flag de interrupção no registrador de flags para 0, assim desabilitando interrupções de hardware. EX.: CLI þ CMC - Nome: Complementa o Flag de Vai-Um Tipo: 8086+ Descrição: Esta instrução checa o valor atual no flag de vai-um. Se for 0 - transforma em 1 e se for 1 - passa a ser 0. EX.: BT AX, 1 ; Testa o bit 1 de AX JC EraUm JMP Fim EraUm: CMC ; Retorna CF para 0 Fim: þ CMP <VALOR1>, <VALOR2> - Nome: Comparação Inteira Tipo: 8086+ Descrição: Esta instrução compara <VALOR1> e <VALOR2> e reflete a comparação nos flags. EX.: CMP AX, BX Veja também as intruções Jcc. þ CWD - Nome: Converte Word para Doubleword Tipo: 8086+

Page 39: Assembler

Descrição: Esta instrução extende a word em AX para o par DX:AX. EX.: CWD þ DEC <VALOR> - Nome: Decrementa Tipo: 8086+ Descrição: Esta instrução subtrai um do valor em <VALOR> e armazena o resultado em <VALOR>. EX.: DEC AX þ DIV <VALOR> - Nome: Divisão sem Sinal Tipo: 8086+ Descrição: Esta instrução divide <VALOR> por, ou AX para byte, DX:AX para word ou EDX:EAX para doubleword. Para byte, o quociente é retornado em AL e o resto em AH, para word o quociente é retornado em AX e o resto em DX e para DWORD, o quociente volta em EAX e o resto em EDX. EX.: MOV AX, 12 MOV BH, 5 DIV BH MOV Quociente, AL MOV Resto, AH þ IN <ACUMULADOR>, <PORTA> - Nome: Entrada de porta de E/S Tipo: 8086+ Descrição: Esta instrução lê um valor de uma das 65536 portas de hardware especificada no <ACUMULADOR>. AX e AL são comumente usados para portas

Page 40: Assembler

de entrada, e DX é comumente usado para identificar a porta. EX.: IN AX, 72h MOV DX, 3C7h IN AL, DX þ INC <VALOR> - Nome: Incrementa Tipo: 8086+ Descrição: Esta instrução soma um ao número em <VALOR>, e armazena o resultado em <VALOR>. EX.: MOV AX, 13h ; AX = 13h INC AX ; AX = 14h þ INT <INTERRUPÇÃO> - Nome: Gera uma Interrupção Tipo: 8086+ Descrição: Esta instrução salva os valores correntes dos flags e IP na pilha, e então chama a <INTERRUPÇÃO> baseada no valor de AH. EX.: MOV AH, 00h ; Seta o modo de ; vídeo MOV AL, 13h ; Modo 13h INT 10h ; Gera interrupção þ Jcc - Nome: Pula (Jump) se Condição Tipo: 8086+ Não vou repetir eu mesmo todos os 32, dê uma olhada no Tutorial Três a lista completa deles. Tenha em mente que seria uma boa idéia chamar CMP, OR, DEC ou algo semelhante antes de usar uma dessas instruções. :) EX.: DEC AX JZ AX_Chegou_A_Zero þ JMP <DEST> - Nome: Jump

Page 41: Assembler

Tipo: 8086+ Descrição: Esta instrução simplesmente carrega um novo valor, <DEST>, em IP, assim transferindo o controle para outra parte do código. EX.: JMP MyLabel þ LAHF - Nome: Carega AH com Flags Tipo: 8086+ Descrição: Esta instrução copia os bytes mais baixos do registrador de flags para AH. O conteúdo de AH vai parecer com algo semeelhante depois que a instrução for executada: Flag SF ZF AF PF CF Bit 07 06 05 04 03 02 01 00 Você pode agora testar os bits individualmente, ou realizar uma instrução similar à seguinte para pegar um flag apenas: EG: LAHF SHR AH, 6 AND AH, 1 ; AH contém o flag ZF. þ LEA <DEST>, <FONTE> - Nome: Carrega Endereço Efetivo Tipo: 8086+ Descrição: Esta instrução carrega o endreço de memória que <FONTE> significa, em <DEST>. EX.: eu uso LEA SI, Str numa das minhas procedures que pões uma string na tela bem rápido. þ LOOP <LABEL> - Nome: Decrementa CX e Repete

Page 42: Assembler

Tipo: 8086+ Descrição: Esta instrução é uma forma do loop For...Do que existe na maioria das linguagens de alto nível. Basicamente ele volta ao label, ou segmento de memória até que CX = 0. EX.: MOV CX, 12 FazAlgumaCoisa: ;... ;... ;... Isto será repetido 12 vezes LOOP FazAlgumaCoisa þ Lseg <DEST>, <FONTE> - Nome: Carega Registrador de Segmento Tipo: 8086+ Descrição: Esta instrução existe de várias formas. Todas aceitam mesma sintaxe, em que <FONTE> especifica um ponteiro de 48 bits, consistindo de um offset de 32 bits e um seletor de 16 bit. O offset de 32 bis é caregado em <DEST>, e o seletor é carregado no registrador de segmento especificado por seg. As seguintes formas existem: LDS LES LFS * 32 bits LGS * 32 bits LSS EX.: LES SI, Um_Ponteiro þ MOV <DEST>, <FONTE> - Nome: Move Dados Tipo: 8086+ Descrição: Esta instrução copia

Page 43: Assembler

<FONTE> em <DEST>. EX.: MOV AX, 3Eh MOV SI, 12h þ MUL <FONTE> - Nome: Multiplicação sem Sinal Tipo: 8086+ Descrição: Esta instrução multiplica <FONTE> pelo acumulador, que depende do tamanho de <FONTE>. Se <FONTE> é um byte então: * AL é o multiplicando; * AX é o produto. Se <FONTE> é uma word então: * AX é o multiplicando; * DX:AX é o produto. Se <FONTE> é uma doubleword então: * EAX é o multiplicando; * EDX:EAX é o produto. OBS.: Os flags são inalterados excetos para OF e CF, que são zerados se o byte alto, word ou dword do produto for 0. EX.: MOV AL, 3 MUL 10 MOV Resultado, AX þ NEG <VALOR> - Nome: Nega Tipo: 8086+ Descrição: Esta instrução subtrai <VALOR> de 0, resultando na negação do complemento a dois de <VALOR>. EX.: MOV AX, 03h

Page 44: Assembler

NEG AX ; AX = -3 þ NOT <VALOR> - Nome: Complemento Lógico Tipo: 8086+ Descrição: Esta instrução inverte o estado de cada bit no operando. EX.: NOT CX þ OR <DEST>, <FONTE> - Nome: OU Lógico Tipo: 8086+ Descrição: Esta instrução realiza uma operação de OU booleano entre cada bit de <DEST> e <FONTE>, guardando o resultado em <DEST>. EX.: OR 0, 0 = 0 OR 0, 1 = 1 OR 1, 0 = 1 OR 1, 1 = 1 þ OUT <PORTA>, <ACUMULADOR> - Nome: Saída para a Porta Tipo: 8086+ Descrição: Esta instrução manda para a saída o valor do acumulador para <PORTA>. Usando o registrador DX para pasar a porta OUT, você pode acessar 65,536 portas. EX.: MOV DX, 378h OUT DX, AX þ POP <REGISTRADOR> - Nome: Carega Registrador da pilha Tipo: 8086+ Descrição: Esta instrução pega o valor atual do topo da pilha e coloca no <REGISTRADOR>. EX.: POP AX

Page 45: Assembler

þ POPA - Nome: Pega Todos os Registradores Gerais Tipo: 80186+ Descrição: Esta instrução pega todos os registradores de uso geral de 16 bits da pilha, exceto SP. É o mesmo que: POP AX POP BX POP CX ... EX.: POPA þ POPF - Nome: Pega o valor do topo para Flags Tipo: 8086+ Descrição: Esta instrução pega o byte baixo dos flags da pilha. EX.: POPF þ PUSH <REGISTRADOR> - Nome: Põe na Pilha o Registrador Tipo: 8086+ Descrição: Esta instrução põe <REGISTRADOR> na pilha. EX.: PUSH AX þ PUSHA - Nome: Põe todos os registradores na pilha Tipo: 80186+ Descrição: Esta instrução põe todos os registradores de uso geral de 16 bits na pilha. É o mesmo que:

Page 46: Assembler

PUSH AX PUSH BX PUSH CX ... EX.: PUSHA þ PUSHF - Nome: Põe flags na pilha Tipo: 8086+ Descrição: Esta instrução põe o byte baixo dos flags na pilha. EX.: PUSHF þ REP - Nome: Sufixo de Repetição Tipo: 8086+ Descrição: Esta instrução repetirá a intrução seguinte o número de vezes especificado em CX. EX.: MOV CX, 6 REP STOSB ; Armazena 6 bytes þ RET - Nome: Retorno de Subrotina (Near/próximo) Tipo: 8086+ Descrição: Esta instrução retorna IP ao valor que ele tinha antes da última instrução CALL. RET, ou RETF para um jump distante (far), deve ser chamado quando se usa assembler puro. EX.: RET þ ROL <DEST>, <VALOR> - Nome: Roda à esquerda Type: 8086+ Descrição: Esta instrução roda <DEST> <VALOR> vezes. Uma rodada é realizada shiftando <DEST> uma vez, então

Page 47: Assembler

transfere-se o bit que saiu para a posição de mais baixa ordem de <DEST>. EX.: ROL AX, 3 þ ROR <DEST>, <VALOR> - Nome: Roda à Direita Tipo: 8086+ Descrição: Esta instrução roda <DEST> <VALOR> vezes. Uma rodada é realizada shiftando <DEST> uma vez, então transfere-se o bit que saiu para a posição de mais alta ordem de <DEST>. EX.: ROR BX, 5 þ SAHF - Nome: Armazena AH nos Flags Tipo: 8086+ Descrição: Esta instrução carrega o conteúdo do registrador AH nos bits 7, 6, 4, 2 e 0 do registrador de flags. EX.: SAHF þ SBB <DEST>, <FONTE> - Nome: Subtrai com "pede-emprestado" Tipo: 8086+ Descrição: Esta instrução subtrai <FONTE> de <DEST>, e decrementa <DEST> de uma unidade de o flag de vai-um estiver setado, armazenando o resultado em <DEST>. Basicamemte, <DEST> = <DEST> - <FONTE> - CF EX.: SBB AX, BX þ SHL <DEST>, <VALOR> - Nome: Shift à esquerda Tipo: 8086+ Descrição: Esta instrução desloca <DEST>

Page 48: Assembler

à esquerda de <VALUE> unidades. Eu não vou entrar em detalhes sobre a teoria disso de novo. Se você não tem certeza do que esta instrução faz, por favor, leia o Tutorial Quatro. EX.: SHL AX, 5 þ SHR <DEST>, <VALOR> - Name: Shift à direita Type: 8086+ Descrição: Esta instrução desloca <DEST> à direita de <VALUE> unidades. Por favor veja o Tutorial Quatro para a teoria dos shifts. EX.: SHR DX, 1 þ STC - Nome: Seta o flag de vai-um (Carry Flag) Tipo: 8086+ Descrição: Esta instrução seta o valor do carry flag para um. EX.: STC þ STD - Nome: Seta o flag de direção Tipo: 8086+ Descrição: Esta instrução seta o valor do flag de direção para um. Isto instrui a todas operações a decrementar os registradores de índice. EX.: STD REP STOSB ; DI está sendo ; decrementado þ STI - Nome: Seta Flag de Interrupção Tipo: 8086+

Page 49: Assembler

Descrição: Esta instrução seta o valor do flag de interrupção para um, assim permitindo que interrupções de hardware ocorram. EX.: CLI ; Pára interrupções ... ; Realiza uma função crucial STI ; Habilita interrupções þ STOS - Nome: Armazena String Tipo: 8086+ Descrição: Esta instrução existe nas seguintes formas: STOSB - Armazena um byte - AL STOSW - Armazena uma word - AX STOSD - Armazena uma doubleword - EAX As instruções escrevem o conteúdo atual do acumulador para a posição de memória apontada por ES:DI. Então ela incrementa ou decrementa DI de acordo com o operando usado, e o valor do flag de direção. Ex.: MOV AX, 0A000h MOV ES, AX MOV AL, 03h MOV DI, 0 STOSB ; Armazena 03 em ES:DI, ; que é o topo da tela ; em modo 13h þ SUB <DEST>, <FONTE> - Nome: Subtração Tipo: 8086+ Descrição: Esta instrução subtrai <FONTE> de <DEST>, armazenando o resultado em <DEST>. EX.: SUB ECX, 12 þ TEST <DEST>, <FONTE> - Nome: Testa Bits

Page 50: Assembler

Tipo: 8086+ Descrição: Esta instrução realiza uma operação AND bit-a-bit em <FONTE> e <DEST>. O resultado refflete nos flags, e eles são são setados como se fizéssemos um AND. EX.: TEST AL, 0Fh ; Checa se algum ; bits está setado ; no mais baixo ; nibble de AL þ XCHG <VALOR1>, <VALOR2> - Nome: Troca Tipo: 8086+ Descrição: Esta instrução troca os valores de <VALOR1> e <VALOR2>. EX.: XCHG AX, BX þ XOR <DEST>, <FONTE> - Nome: OU Exclusivo Lógico Tipo: 8086+ Descrição: Esta instrução realiza um OU exclusivo bit-a-bit em <FONTE> e <DEST>. A operação é definida como segue: XOR 0, 0 = 0 XOR 0, 1 = 1 XOR 1, 0 = 1 XOR 1, 1 = 0 EX.: XOR AX, BX Não se preocupe com isso por enquanto. Vamos continuar normalmente, e introduzir as novas instruções acima uma por uma, explicando-as quando o fizermos. Se você já as entende, isso é um bônus. Você vai notar que havia muitas instruções acima, do 8086. Há, na verdade, poucos casos em que é necessário usar uma instrução do 386 ou 486, muito menos do Pentium.

Page 51: Assembler

De qualquer modo, antes de avançar com a VGA, eu vou só listar a velocidade em que cada instrução acima é executada, assim você pode usar isso para ver como as rotinas em Assembler são rápidas. Instrução Clocks no 386 Clocks no 486 ADC 2 1 ADD 2 1 AND 2 1 BT 3 3 CALL 7+m 3 CBW 3 3 CLC 2 2 CLD 2 2 CLI 5 3 CMC 2 2 CMP 2 1 CWD 2 3 DEC 2 1 DIV - - - Byte 9-14 13-18 - Word 9-22 13-26 - DWord 9-38 13-42 IN 12/13 14 INC 2 1 INT depende depende Jcc - - - Em loop 7+m 3 - Não loop 3 1 JMP 7+m 3 LAHF 2 3 LEA 2 1 LOOP 11 6 Lseg 7 6 MOV 2 1 MUL - - - Byte 9-14 13-18 - Word 9-22 13-26 - DWord 9-38 13-42 NEG 2 1 NOT 2 1 OR 2 1

Page 52: Assembler

OUT 10/11 16 POP 4 1 POPA 24 9 POPF 5 9 PUSH 2 1 PUSHA 18 11 PUSHF 4 4 REP depende depende RET 10+m 5 ROL 3 3 ROR 3 3 SAHF 3 2 SBB 2 1 SHL 3 3 SHR 3 3 STC 2 2 STD 2 2 STI 3 5 STOS 4 5 SUB 2 1 TEST 2 1 XCHG 3 3 XOR 2 1 Obs.: m = Número de componentes na próxima instrução executada. Você provavlmente já notou que sua placa de vídeo tem mais que 256K de RAM. (se não tem, então estes tutoriais não são provavelmente para você.) Mesmo que você tenha só 256K de RAM, como meu velho 386, você ainda é capaz de entrar no modo 13h - 320x200x256. Porém, isto levanta algumas quetões. Multiplique 320 por 200 e você vai notar que você só precisa de 64,000 bytes de memória para armazenar uma tela simples. (A VGA na verdade nos dá 64K, que é 65,536 bytes para quem não sabe.) O que aconteceu com os restantes 192K? Bem, a VGA é na verdade arrumada em planos de bits, como isso: 64,000 Cada plano sendo de 64,000 bytes. Aqui está como isso funciona: Um pixel at 0, 0 é mapeado no plano 0 no offset 0; Um pixel at 1, 0 é mapeado no plano 1 no offset 0;

Page 53: Assembler

Um pixel at 2, 0 é mapeado no plano 2 no offset 0; Um pixel at 3, 0 é mapeado no plano 3 no offset 0; Um pixel at 4, 0 é mapeado no plano 0 no offset 1 ... e assim por diante... Por causa de os pixels serem encadeados através dos 4 planos, é impossível usar múltiplas páginas no modo 13h sem ter que to usar uma tela virtual, ou algo do tipo. O mapeamento automático dos pixels é feito todo pela placa de vídeo, de modo que você pode trabalhar de olhos fechados sem saber dos 4 planos de bits se você quiser. Vamos ver como você pode contornar essa situação, entrando num modo especial, conhecido como Modo X, mais tarde, mas por enquanto, vamos só ver o que podemos fazer no velho modo 13h. DESENHANDO LINHAS Passamos um pouco da tamanho do tamanho que eu tinha planejado para este tutorial, e eu pretendo falar do Algoritmo de Retas de Bresenham, mas isso vai ter que esperar a semana que vem. No entanto, vou cobrir como desenhar um linha reta horzontal simples em Assembler. Um Rotina em Assembler para Retas Horizontais: ------------------------------------------------ Primeiramente vamos precisar de apontar ES para a VGA. Isso deve resolver: MOV AX, 0A000h MOV ES, AX Agora, precisaremos de ler os valores de X1, X2 e Y nos registradores, então algo assim deveria funcionar: MOV AX, X1 ; AX é igual ao valor X1 agora MOV BX, Y ; BX é igual ao valor Y agora MOV CX, X2 ; CX é igual ao valor X2 agora Será preciso calcular o tamanho da linha, então vamos usar CX para

Page 54: Assembler

guardar isso, sendo que: i) CX já tem o valor de X2, e ii) vamos usar uma instrução REP, que usará CX como contador. SUB CX, AX ; CX = X2 - X1 Agora vamos precisar de calcular o DI para o primeiro pixel que plotaremos, então vamos fazer o que fizemos na rotina de PutPixel: MOV DI, AX ; DI = X1 MOV DX, BX ; DX = Y SHL BX, 8 ; Shift Y à esquerda 8 SHL DX, 6 ; Shift Y à esquerda 6 ADD DX, BX ; DX = Y SHL 8 + Y SHL 6 ADD DI, DX ; DI = Y x 320 + X Temos o offset do primeiro pixel agora, então tudo o que temos que fazer é colocar a cor que queremos desenhar em AL, e usar STOSB para plotar o resto da linha. MOV AL, Cor ; Move a cor a plotar em AL REP STOSB ; Plota CX pixels Note que usamos STOSB porque ele vai incrementar DI para nós, assim economizando um monte de MOV's e INC's. Agora, dependendo de que linguagem você vai usar para implementar, você vai chegar a algo assim: void Draw_Horizontal_Line(int x1, int x2, int y, unsigned char color) { _asm { mov ax, 0A000h mov es, ax ; Aponta ES para a VGA mov ax, x1 ; AX = X1 mov bx, y ; BX = Y mov cx, x2 ; CX = X2 sub cx, ax ; CX = Diferença de X2 e X1 mov di, ax ; DI = X1 mov dx, bx ; DX = Y shl bx, 8 ; Y SHL 8 shl dx, 6 ; Y SHL 6 add dx, bx ; DX = Y SHL 8 + Y SHL 6 add di, dx ; DI = Offset do primeiro pixel

Page 55: Assembler

mov al, color ; Põe a cor a plotar em AL rep stosb ; Desenha a linha } } A rotina acima não é cegamente rápida, mas não é de toda má. Só de mudar o cálculo da parte de DI como na PutPixel que eu dei no Tutorial Dois, já dobraria a velocidade desta rotina. Minha própria rotina de linha horizontal á provavelmente cerca de 4 a 5 vezes mais rápida que esta, assim, no futuro, eu vou lhe mostrar como otimizar essa rotina por completo. Semana que vem vamos ver como pegar e acertar a palette, e como podemos desenhar círculos. Sinto muito se não fiz isso nesse tutorial, mas ele cresceu um pouco demais... COISAS PARA FAZER: -------------------- 1) Escreva um rotina para linhas verticais baseada na rotina acima. Dica: Você precisa incrementar DI de 320 em algum lugar. 2) Volte à lista das instruções de Assembler, e aprenda quantas puder. 3) Dê uma olhda no Starfield, e tente corrigir os bugs dele. Veja o que você pode fazer com ele. Semana passada nós chegamos à seguinte rotina de linhas horizontais - mov ax, 0A000h mov es, ax ; Aponta ES para a VGA mov ax, x1 ; AX = X1 mov bx, y ; BX = Y mov cx, x2 ; CX = X2 sub cx, ax ; CX = Diferença de X2 e X1 mov di, ax ; DI = X1 mov dx, bx ; DX = Y shl bx, 8 ; Y SHL 8

Page 56: Assembler

shl dx, 6 ; Y SHL 6 add dx, bx ; DX = Y SHL 8 + Y SHL 6 add di, dx ; DI = Offset do primeiro pixel mov al, color ; Põe a cor a plotar em AL rep stosb ; Desenha a linha Agora, embora essa rotina seja muito mais rápida que as rotinas do BGI, (ou seja lá o que for que seu compilador tenha), ela poderia ser melhorada pra caramba. Se entrarmos na rotina, com a lista de clocks que eu dei no último tutorial, você vai ver que ela gasta bem pouco. Eu vou deixar a otimização com você por enquanto, (vamos ver isso em outro tutorial), mas se substituir STOSB por MOV ES:[DI], AL ou STOSW vai melhorar muito as coisas. Não se esqueça que se você decidir usar um loop, para jogar words na VGA, você terá que decrementar CX de uma unidade. Agora, vamos ver uma linha vertical. Teremos que calcular o offset do primeiro pixel como nós fizemos na rotina de linhas horizontais, então, algo desse tipo funcionaria: mov ax, 0A000h ; Põe o segmento VGA em AX mov es, ax ; Aponta ES para a VGA mov ax, Y1 ; Move o primeiro valor de Y para AX shl ax, 6 ; Y x 2^^6 (dois à sexta potência) mov di, ax ; Move o novo valor de Y para DI shl ax, 2 ; Agora temos Y = Y x 320 add di, ax ; Adiciona aquele valor a DI add di, X ; Soma o valor de X a DI Agora umas coisas básicas... mov cx, Y2 ; Guarda Y2 em CX mov al, Color ; Guarda a cor a plotar em AL sub cx, Y1 ; CX = tamanho da linha E agora o loop final... Plota: mov es:[di], al ; Põe um pixel no offset corrente

Page 57: Assembler

add di, 320 ; Move para a próxima linha dec cx ; Decrementa CX de um jnz Plota ; Se CX <> 0, então continua plotando Não é uma rotina fantástica, mas é muito boa. Note como foi possível realizar uma comparação depois de DEC CX. Isto é um conceito extremamente útil, logo, não se esqueça de que isso é possível. Brinque um pouco com o código, e tente fazê-lo mais rápido. Tente outros métodos de calcular o offset, ou métodos diferentes de controle de fluxo. Agora, isso foi a coisa fácil. Vamos ver agora uma rotina capaz de desenhar linhas diagonais. A seguinte rotina foi tirada de SWAG, autor desconhecido, e é uma rotina ideal para demonstrar um algoritmo de linhas. Ele está precisando muito de uma otimização, assim, essa pode ser uma tarefa para você - se você quiser. Alguns dos pontos a considerar são: 1) Seja lá quem o escreveu nunca ouviu falar de XCHG - isso economizaria alguns clocks; 2) Ele comete um dos grandes pecados do código não-otimizado - ele move um valor para AX, e então realiza uma operação envolvendo AX na próxima instrução, assim fazendo um ciclo a mais. (Vamos falar sobre isso semana que vem). 3) Ele trabalha com BYTES e não WORDS, assim, a velocidade de escrita para a VGA poderia se dobrada se usasse words. 4) E o maior pecado de todos, ele usa um MUL para achar o offset. Tente usar shifts ou um XCHG para acelerar as coisas. De qualquer modo, eu pus os comentários nele, e acho que ele é auto-explicativo, assim, eu não vou entrar em detalhes como ele funciona. Você deve ser capaz de pegar isso sozinho. Adentre a rotina, e veja como a derivada (gradiente, variação, inclinação...) da linha foi calculada. Procedure Line(X1, Y1, X2, Y2 : Word; Color : Byte); Assembler; Var DeX : Integer; DeY : Integer;

Page 58: Assembler

IncF : Integer; Asm { Line } mov ax, [X2] { Move X2 para AX } sub ax, [X1] { Pega o tamanho horizontal da linha (X2 - X1) } jnc @Dont1 { X2 - X1 é negativo? } neg ax { Sim, então faz com que seja positivo } @Dont1: mov [DeX], ax { Agora, move o tamanho horizontal da linha para DeX } mov ax, [Y2] { Move Y2 para AX } sub ax, [Y1] { Subtrai Y1 de Y2, dando o tamanho vertical } jnc @Dont2 { Foi negativo? } neg ax { Sim, então faça-o positivo } @Dont2: mov [DeY], ax { Move o tamanho vertica para DeY } cmp ax, [DeX] { Compara o tamanho vertivcal com o horizontal } jbe @OtherLine { Se o vertical foi <= horizontal então pula } mov ax, [Y1] { Move Y1 para AX } cmp ax, [Y2] { Compara Y1 a Y2 } jbe @DontSwap1 { Se Y1 <= Y2 então pula, senão... } mov bx, [Y2] { Põe Y2 em BX } mov [Y1], bx { Põe Y2 em Y1 } mov [Y2], ax { Move Y1 para Y2 } { Para que depois de tudo isso..... } { Y1 = Y2 e Y2 = Y1 } mov ax, [X1] { Põe X1 em AX } mov bx, [X2] { Põe X2 em BX } mov [X1], bx { Põe X2 em X1 } mov [X2], ax { Põe X1 em X2 } @DontSwap1: mov [IncF], 1 { Põe 1 em IncF, i.e., plota outro pixel } mov ax, [X1] { Põe X1 em AX } cmp ax, [X2] { Compara X1 com X2 } jbe @SkipNegate1 { Se X1 <= X2 então pula, senão... } neg [IncF] { Nega IncF } @SkipNegate1: mov ax, [Y1] { Move Y1 para AX } mov bx, 320 { Move 320 para BX } mul bx { Multiplica 320 por Y1 } mov di, ax { Põe o resultado em DI }

Page 59: Assembler

add di, [X1] { Soma X1 a DI, e tcham - offset em DI } mov bx, [DeY] { Põe DeY em BX } mov cx, bx { Põe DeY em CX } mov ax, 0A000h { Põe o segmento a ser plotado, em AX } mov es, ax { ES aponta para a VGA } mov dl, [Color] { Põe a cor a usar em DL } mov si, [DeX] { Aponta SI para DeX } @DrawLoop1: mov es:[di], dl { Põe a cor a plotar, DL, em ES:DI } add di, 320 { Soma 320 a DI, i.e., próxima linha abaixo } sub bx, si { Subtrai DeX de BX, DeY } jnc @GoOn1 { Ficou negativo? } add bx, [DeY] { Sim, então soma DeY a BX } add di, [IncF] { Soma a quantidade a incrementar a DI } @GoOn1: loop @DrawLoop1 { Nenhum resultado negativo, então plota outro pixel } jmp @ExitLine { Acabou, então vamos embora! } @OtherLine: mov ax, [X1] { Move X1 para AX } cmp ax, [X2] { Compara X1 a X2 } jbe @DontSwap2 { X1 <= X2 ? } mov bx, [X2] { Não, então move X2 para BX } mov [X1], bx { Move X2 para X1 } mov [X2], ax { Move X1 para X2 } mov ax, [Y1] { Move Y1 para AX } mov bx, [Y2] { Move Y2 para BX } mov [Y1], bx { Move Y2 para Y1 } mov [Y2], ax { Move Y1 para Y2 } @DontSwap2: mov [IncF], 320 { Move 320 para IncF, i.e., o próximo pixel está na } { próxima linha } mov ax, [Y1] { Move Y1 para AX } cmp ax, [Y2] { Compara Y1 a Y2 } jbe @SkipNegate2 { Y1 <= Y2 ? } neg [IncF] { Não, então nega IncF } @SkipNegate2: mov ax, [Y1] { Move Y1 para AX } mov bx, 320 { Move 320 para BX } mul bx { Multiplica AX por 320 } mov di, ax { Move o resultado para DI } add di, [X1] { Soma X1 a DI, dando o offset }

Page 60: Assembler

mov bx, [DeX] { Move DeX para BX } mov cx, bx { Move BX para CX } mov ax, 0A000h { Move o endereço da VGA para AX } mov es, ax { Aponta ES para a VGA } mov dl, [Color] { Move a cor a plotar para DL } mov si, [DeY] { Move DeY para SI } @DrawLoop2: mov es:[di], dl { Põe o byte em DL para ES:DI } inc di { Incrementa DI de um, o próximo pixel } sub bx, si { Subtrai SI de BX } jnc @GoOn2 { Ficou negativo? } add bx, [DeX] { Sim, então soma DeX a BX } add di, [IncF] { Soma IncF a DI } @GoOn2: loop @DrawLoop2 { Continua plotando } @ExitLine: { Pronto! } End; Acho que não fiz nenhum erro com os comentários, mas eu estou bem cansado, e não tenho bebido cafeína há dias - se você encontrar um erro - por favor me diga. OS INS E OUTS DE IN E OUT IN e OUT são uma parte muito importante de código em Assembler. Elas permitem você a mandar/receber diretamente dados de qualquer uma das 65,536 portas de hardware, ou registradores. A sintaxe básica é como segue: þ IN <ACUMULADOR>, <PORTA> - Nome: Entrada de porta de E/S Tipo: 8086+ Descrição: Esta instrução lê um valor de uma das 65536 portas de hardware para o acumulador especificado.

Page 61: Assembler

AX e AL são comumente usados para portas de entrada, e DX é mais usado para identificar a porta. EX.: IN AX, 72h MOV DX, 3C7h IN AL, DX þ OUT <PORTA>, <ACUMULADOR> - Nome: Saída para a Porta Tipo: 8086+ Descrição: Esta instrução põe na saída o valor no acumulador para <PORTA>. Usando o registrador DX para passar a porta para OUT, você pode acessar até 65,536 portas. EX.: MOV DX, 378h OUT DX, AX OK, isso não ajudou muito, já que não disse muito sobre como usar - muito menos para que usar. Bem, se você pretende trabalhar muito com a VGA, você terá que ser capaz de programar seus registradores internos. Semelhantes aos registradores com que você tem trabalhado até agora, você pode pensar em mudá-los como interrupções, exceto que: 1) Você passa os valores para a porta, e é isso aí; e 2) É muito perto de ser instantâneo. Como exemplo, vamos ver como setar e pegar a palette controlando diretamente o hardware da VGA. Agora, a VGA tem uma porção de registradores, mas as próximas três é bom que você conheça bem: þ 03C7h - PEL Registrador de Endereços (Leitura) Seta a palette em mode de leitura þ 03C8h - PEL Registrador de Endereços (Escrita) Seta a palette em modo de escrita þ 03C9h - PEL Registrador de Dados (Leitura/Escrita) Lê, ou escreve 3 valores RGB, a cada terceira escrita, o índice, ou cor que você está setando, é incrementado de um.

Page 62: Assembler

O que tudo isso significa é - Se nós fôssemos setar um valor RGB de uma cor RGB, nós mandaríamos o valor da cor que queríamos mudar para 03C8h, então ler os 3 valores de 03C9h. Em Assembler, faríamos isso: mov dx, 03C8h ; Põe o registrador DAC de leitura em DX mov al, [Color] ; Põe o valor da cor em AL out dx, al ; Manda AL para a porta DX inc dx ; Agora usa a porta 03C9h mov al, [R] ; Põe o novo valor VERMELHO em AL out dx, al ; Manda AL para a porta DX mov al, [G] ; Põe o novo valor VERDE em AL out dx, al ; Manda AL para a porta DX mov al, [B] ; Põe o novo valor AZUL em AL out dx, al ; Manda AL para a porta DX E aquilo deveria fazer a coisas direitinho. Para ler a palette, faríamos isso: mov dx, 03C7h ; Põe o registrador DAC de escrita em DX mov al, [Color] ; Põe o valor da cor em AL out dx, al ; Manda AL para a porta DX add dx, 2 ; Agora usa a porta 03C9h in al, dx ; Põe o valor conseguido da porta DX em AL les di, [R] ; Aponta DI para a variável R - Isso vem do Pascal stosb ; Guarda AL em R in al, dx ; Põe o valor conseguido da porta DX em AL les di, [G] ; Aponta DI para a variável G stosb ; Guarda AL em G in al, dx ; Põe o valor conseguido da porta DX em AL les di, [B] ; Aponta DI para a variável B stosb ; Guarda AL em B Note como essa rotina foi codificada diferentemente. Isso era originalmente uma rotina em Pascal, e como o Pascal não gosta que você mexa com variáveis de Pascal em Assembler, você tem que improvisar. Se você está trabalhando com Assembler puro, então você pode codificar isso muito mais eficientemente, como o primeiro exemplo. Eu deixei o código como estava para que aqueles trabalhando com uma linguagem de alto nível possam chegar a um problema particularmente irritante. Agora você já viu como IN e OUT podem ser úteis. Controlar diretamente o

Page 63: Assembler

hardware é mais rápido e mais eficiente. Nas próximas semanas, eu posso incluir uma lista das portas mais comuns, mas se você tivesse uma cópia da Ralf Brown's Interrupt List (disponível no X2FTP), você já teria uma cópia. OBS.: Você pode achar um link para a Ralf Brown's Interrupt List na minha página. Um pouco mais sobre o registrador de flags: Agora, embora tenhamos usado o registrador de flags em quase todo nosso código até esse ponto, eu não entrei profundamente nesse assunto. Você pode trabalhar felizmente sem conhecer muito sobre os flags, e comparar coisas sem saber o que está realmente acontecendo, mas se você quiser avançar no Assembler, você precisa saber algo mais. De volta ao Tutorial Três, eu dei uma visão simplista do registrador de FLAGS. Na realidade, os FLAGS, ou EFLAGS é na verdade um registrador de 32-bit, embora apenas só os bits de 0 a 18 sejam usados. Na realidade não precisamos conhecer os flags acima do 11 por enquanto, mas é bom saber que eles existem. O registrador EFLAGS na verdade se parece com isso: 18 17 16 15 14 13 12 11 10 09 08 07 06 05 04 03 02 01 00 AC VM RF -- NT IO/PL OF DF IF TF SF ZF -- AF -- PF -- CF Agora, os flags são os seguintes: þ AC - Alignment Check (80486) / Checagem de Alinhamento þ VM - Virtual 8086 Mode / Modo Virtual 8086 þ RF - Resume Flag / Flag de Continuação þ NT - Nested Task Flag / Flag de Tarefa Aninhada þ IOPL - I/O Privilege Level / Nível de Privilégio de E/S Tem um valor de 0,1,2 ou 3 logo ocupa 2 bits þ OF - Overflow Flag / Flag de Overflow Este bit é setado para UM se uma instrução aritmética gerar um resultado que é muito grande ou muito pequeno para caber no registrador destino. þ DF - Direction Flag / Flag de Direção

Page 64: Assembler

Qaundo setado para ZERO, as instruções de string, como MOVS, LODS, e STOS incrementarão o endereço de memória que elas estão trabalhando de uma unidade. Isto significa que, digamos, DI será incrementado quando você usar STOSB para colocar um pixel em ES:DI. Setando o bit para UM decrementará o endereço de memória após cada chamada. þ IF - Interrupt Enable Flag / Flag de Habilitação de Interrupções Quando este bit está setado, o processador responderá a interrupções externas do hardware. Quando o bit for resetado, interrupções de hardware são ignoradas. þ TF - Trap Flag / Flag de Trap ("armadilha") Quando este bit estiver setado, uma interrupção ocorrerá imediatamente depois que a próxima instrução executar. Isto é geralmente usado em depurações. þ SF - Sign Flag / Flag de Sinal Este bit é mudado após operações aritméticas. O bit recebe o bit de mais alta ordem do resultado, e se setado para UM, indica que o resultado da operação foi negativo. þ ZF - Zero Flag / Flag de Zero Este bit é setado quando instruções aritméticas geram um resultado zero. þ AF - Auxiliary Carry Flag / Flag de Vai-Um Auxiliar Este bit indica que um vai-um no nibble de baixa ordem de AL ocorreu na instrução aritmética. þ PF - Parity Flag / Flag de Paridade Este bit é setado para um quando uma instrução aritmética resulta num número par de bits 1. þ CF - Carry Flag / Flag de Vai-Um Este bit é setado quando o resultado de uma operação aritmética é muito grande ou muito pequena para o registrador destino ou endereço de memória. Agora, de todos esses acima, você não precisa mesmo se preocupar muito com a maioria deles. Por enquanto, só conhecer CF, PF, ZF, IF, DF e OF será suficiente. Eu não dei comentários para os primeiros já que eles são puramente técnicos, e são usados mais no modo protegido e situações complexas. Você não deveria ter que conhecê-los.

Page 65: Assembler

Você pode, se quiser, mover uma copia do flags para AH com LAHF - (Carrega AH com Flags) - e modificar ou ler bits individualmente, ou mudar o status dos bits mais facilmente com CLx e STx. Contudo se você planeja mudar os flags, lembre-se de que eles podem ser extremamente úteis em muitas situações. (Eles podem também ser muito enjoados quando tarde da noite, linhas começam a desenhar para trás, e você gasta um hora procurando o porquê - e então se lembra que você se esqueceu de limpar o flag de direção!) Acho que cobrimos muito pouca coisa importante neste tutorial. Dê uma olhada nos flags, e volte naquela rotina compridona de fazer linhas, já que ela é um ótimo exemplo de controle de fluxo. Assegure-se de que suas capacidades de controlar fluxos de intruções estão perfeitas. Quando eu comecei a brincar com Assembler eu logo vi que o Turbo Pascal, (a linguagem com que eu trabalhava até então), tinha poucas limitações - uma delas é que ela era, e ainda é, uma linguagem de 16 bits. Isso significava que se eu quisesse brincar com escritas super-rápidas em 32 bits, eu não poderia. Nem mesmo com seu próprio Assembler (bem, não facilmente). O que eu precisava fazer era escrever código separadamente 100% em Assembler e linkar ao Turbo. Isso não é uma tarefa particularmente difícil, e uma das que eu vou tentar ensinar a você hoje. A outra vantagem de escrever rotinas em Assembler puro é que você também pode linkar o código objeto resultante a outra linguagem de alto nível, como o C. ESCREVENDO CÓDIGO EXTERNO PARA SUA LINGUAGEM DE ALTO NÍVEL Antes de começarmos, você precisa de uma idéia do que são chamadas far e near. Se você já sabe, então pule essa pequena seção. Como discutimos antes, o PC tem uma arquitetura segmentada. Como você sabe, você só pode acessar um segmento de 64K de cada vez. Agora se você está trabalhando com código de menos de 64K de tamanho, ou em uma linguagem que cuida de todas as preocupações para você, você não precisa de se preocupar tanto. Contudo, trabalhando em Assembler, precisamos sim. Imagine que tenhamos o seguinte programa caregado na memória:

Page 66: Assembler

64K ROTINA DOIS ROTINA UM 64K Entradada PROGRAMA PRINCIPAL Saída Quando um JMP for executado para transferir o controle para a Rotina Um, esse será uma chamada near(perto). Nós não deixamos o segmento em que o corpo principal do programa está localizado, e assim quando o JMP ou CALL é executado, e CS:IP é mudado por JMP, só o IP precisa ser mudado, não CS. O offset muda, mas o segmento não. Agora, pular para a Rotina Dois seria diferente. Isto deixa o segmento corrente, e assim ambas as partes do par CS:IP precisarão de ser alteradas. Isto é uma chamada far (longe). O problema ocorre quando a CPU encontra um RET ou RETF no fim da chamada. Digamos que você, por acidente, colocou RET no fim da Rotina Dois, ao invés de RETF. Como a CPU viu RET, ela só tiraria IP da pilha, e assim, sua máquina travaria, provavelmente, já que CS:IP estaria apontando para um lixo. Este ponto é especialmente importante quando for linkar a uma linguagem de alto nível. Seja lá quando for que você escrever um código em Assembly e linkar, digamos, ao Pascal, lembre-se de usar a diretiva de compilação {$F+}, mesmo se não foi uma chamada FAR. Deste modo, depois de o Turbo chamar a rotina, ele tira CS e IP da pilha, e tudo vai bem. Falhas em fazer isso são problemas seus! OK, de volta ao modelo em Assembly puro do Tutorial Três. Eu não me lembro direito, mas eu acho que era alguma coisa assim: DOSSEG .MODEL SMALL .STACK 200h

Page 67: Assembler

.DATA .CODE START: END START Agora, acho que é hora de vocês pularem um grau no uso daquele esqueleto. Vejamos outros modos de arrumar uma rotina-esqueleto. DATA SEGMENT WORD PUBLIC DATA ENDS CODE SEGMENT WORD PUBLIC ASSUME CS:CODE, DS:DATA CODE ENDS END Este é, obviamente, um esqueleto diferente. Note como eu omiti o ponto antes de DATA e CODE. Dependendo de que Assembler/Linker você usar, você pode precisar do ponto ou não. TASM, o Assembler que eu uso, aceita os dois formatos, então, pegue um com que você e seu assembler estejam felizes. Note também o uso de DATA SEGMENT WORD PUBLIC. Primeiramente, WORD diz ao Assembler para alinhar o segmento em limites de word. FATO ENGRAÇADO: Você não precisa se preocupar com isso por enquanto, pois o Turbo Pascal faz isso de qualquer modo, assim, colocar BYTE ao invés de word não faria diferença nenhuma. :) PUBLIC permite ao compilador que você usar, acessar quaisquer variáveis no segmento de dados. Se você não quer que seu compilador tenha acesso a qualquer variável que você declarar, então apenas omita isso. Se você não precisar de acessar o segmento de dados, então esqueça o segmento de dados todo. Agora, o segmento de código. Geralmente, você vai precisar incluir isso em todo o código que você escrever. :) A sentença ASSUME também será um padrão em tudo que você vai trabalhar. Você também pode esperar ver CSEG e DSEG

Page 68: Assembler

ao invés de CODE e DATA. Note de novo que ele é declarado como PUBLIC. É nele que todas as nossas rotinas vão. Então, como eu declaro procedures externas? Ok, por exemplo, vamos usar umas poucas rotinas simples similares àquelas na biblioteca do modo 13H do PASCAL (disponível na minha homepage). Se você se lembrar, a procedure se parece um pouco com isso: þ Procedure PutPixel(X, Y : Integer; Color : Byte); þ Procedure InitMCGA; þ Procedure Init80x25; Ajustando isso no nosso esqueleto, temos: CODE SEGMENT WORD PUBLIC ASSUME CS:CODE DS:DATA PUBLIC PutPixel PUBLIC InitMCGA PUBLIC Init80x25 CODE ENDS END Agora, tudo o que temos a fazer é codificá-los. Mas, espere um minuto - a rotina PutPixel tem PARÂMETROS! Como usá-los em código externo?? Isto é um macete. O que fazemos é colocar os valores na pilha, simplesmente dizendo -- PutPixel(10,25,15); -- já faz isso para nós. É tirar eles de lá é que é o mais difícil. O que eu geralmente faço, e sugiro a vocês fazer, é se certificar de DECLARAR TODAS AS PROCEDURES EXTERNAS COMO FAR. Isso faz as coisas trabalharem com a pilha mais fácil.

Page 69: Assembler

FATO ENGRAÇADO: Lembre que a primeira cois a entrar na pilha é a éLTIMA A SAIR. :) Quando você chamar a Putpixel, a pilha estará mudada. Como isso é uma chamada FAR, os primeiros 4 bytes são CS:IP. Os bytes daí em diante são os nossos parâmetros. Para encurtar a história, digamos que a pilha se pareça com isso: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ... Depois de chamar -- PutPixel(10, 20, 15); -- um tempo depois, ela pode se parecer com isso: 4C EF 43 12 0F 00 14 00 0A 00 9E F4 3A 23 1E 21 ... ^^^^^^^^^^^ ^^^^^ ^^^^^ ^^^^^ ^^^^^^^^^^^^^^^^^ CS:IP Color Y X Algum lixo Agora, para complicar as coisas, a CPU armazena words na pilha com a PARTE MENOS SIGNIFICATIVA PRIMEIRO. Isso não nos atrapalha muito, mas se você ficar andando por aí com um debugger sem saber disso, você vai ficar mesmo confuso. Note também que quando o Turbo Pascal põe um tipo de dado byte na pilha, ele te chupa DOIS BYTES, NÃO UM. Você não adora o modo como o PC é organizado? ;) Agora, tudo que eu disse até agora só se aos parâmetros passados por valor - PARÂMETROS QUE VOCÊ NÃO PODE MUDAR. Quando você estiver por aí brincando com PARÂMETROS PASSADOS POR REFERÊNCIA, como -- MyProc(Var A, B, C : Word); -- cada parâmetro agora usa QUATRO BYTES de pilha, dois para o segmento e dois para o offset de onde a variável está na memória. Assim, se você pegou uma variável que está, digamos, na posição de memória 4AD8:000Eh, não interessa o valor dela, 4AD8:000Eh seria armazenado na pilha. Já que isso acontece, você ia ver 0E 00 D8 4A na pilha, lembrando que o nibble menos significativo é armazenado primeiro. FATO ENGRAÇADO: Parâmetros de Valor põe na verdade o valor na pilha, e Parâmetros de Referência armazenam o endereço. :)

Page 70: Assembler

Ok, agora que eu tenho você realmente confuso, e com razão, isso piora! Para referenciar estes parâmetros no seu código, você tem que usar o ponteiro de pilha, SP. O problema é que você não pode brincar com SP diretamente, você tem que botar BP na pilha, e mover SP para ele. Isso agora coloca mais dois bytes na pilha. Digamos que BP era igual a 0A45h. Antes de colocar BP, a pilha era assim: 4C EF 43 12 0F 00 14 00 0A 00 ^^^^^^^^^^^ ^^^^^ ^^^^^ ^^^^^ CS:IP Color Y X Depois, ela ficou assim: 45 0A 4C EF 43 12 0F 00 14 00 0A 00 ^^^^ ^^^^^^^^^^^ ^^^^^ ^^^^^ ^^^^^ BP CS:IP Color Y X Agora que nós passamos por isso tudo, podemos realmente acessar essas as porcarias! O que você faria depois de chamar -- PutPixel(10, 20, 15); -- para acessar o valor de Color é isto: PUSH BP MOV BP, SP MOV AX, [BP+6] ; Agora temos Color Podemos acessar X e Y assim: MOV BX, [BP+8] ; Agora temos Y MOV CX, [BP+10] ; Agora temos X E agora restauramos BP: POP BP Agora, retornamos de uma chamada FAR, e removemos os seis bytes de dados que pusemos na pilha:

Page 71: Assembler

RETF 6 E é só isso! Agora vamos por a PutPixel, InitMCGS e Init80x25 em código Assembler. Você obtém algo assim: CODE SEGMENT WORD PUBLIC ASSUME CS:CODE DS:DATA PUBLIC PutPixel ; Declara as procedures públicas PUBLIC InitMCGA PUBLIC Init80x25 .386 ; Vamos usar alguns registradores de 386 Procedure PutPixel(X, Y : Integer; Color : Byte) PutPixel PROC FAR ; Declara uma procedure FAR PUSH BP MOV BP, SP ; Arruma a pilha MOV BX, [BP+10] ; BX = X MOV DX, [BP+08] ; DX = Y XCHG DH, DL ; Como Y sempre terá um valor menor quee 200, MOV AL, [BP+06] ; isto é 320x200, não se esqueça, dizer XCHG DH,DL MOV DI, DX ; é um modo genial de dizer SHL DX, 8 SHR DI, 2 ADD DI, DX ADD DI, BX ; Agora temos o offset, então... MOV FS:[DI], AL ; ...plote em FS:DI POP BP RETF 6 PutPixel ENDP

Page 72: Assembler

Procedure InitMCGA InitMCGA PROC FAR MOV AX, 0A000H ; Aponta AX para a VGA MOV FS, AX ; Porque não FS? MOV AH, 00H MOV AL, 13H INT 10H RETF InitMCGA ENDP Procedure Init80x25 Init80x25 PROC FAR MOV AH, 00H MOV AL, 03H INT 10H RETF Init80x25 ENDP CODE ENDS END E é só. Desculpe-me se eu fiz a coisa toda um pouco confusa, mas essa é a graça dos computadores! :) Ah! A propósito, você pode usar o código acima em Pascal, assemblando-o com TASM ou MASM. Depois, inclua no seu código isso: {$L SEJALµDOQUEVOCÒCHAMOU.OBJ} {$F+} Procedure PutPixel(X, Y : Integer; Color : Byte); External; Procedure InitMCGA; External;

Page 73: Assembler

Procedure Init80x25; External; {$F-} Begin InitMCGA; PutPixel(100, 100, 100); ReadLn; Init80x25; End. FUNÇÕES E OTIMIZAÇÕES POSTERIORES Você pode fazer suas rotinas Assembler retornarem valores que você pode usar em sua linguagem de alto-nível, se você quiser. A tabela abaixo contém toda a informação que você precisa saber. Tipo a Retornar Registrador(es)a Usar Byte AL Word AX LongInt DX:AX Pointer DX:AX Real DX:BX:AX Agora que você já viu como escrever código externo, você provavelmente quer saber como melhorá-lo para obter a performance total que o código externo pode oferecer. Alguns pontos para você trabalhar se seguem: þ Não se pode trabalhar com SP diretamente, mas você pode usar ESP. þ Isso vai acabar com a lentidão de empilhar/desempilhar BP. þ Lembre-se de mudar [xx+6] para [xx+4] para o último (primeiro) parâmetro, já que BP não está mais na pilha. Gaste um tempo e veja o que você pode fazer com isso. É possível através de melhorias, fazer um código mais rápido que a rotina no MODE13H.ZIP versão 1 (disponível na minha homepage).

Page 74: Assembler

Nota: Eu planejo mais pra frente desenvolver a biblioteca MODE13H, adicionando fontes e outras coisas legais. Ela será eventualmente codificada só em Assembler, e poderá ser chamada do C ou Pascal. Código puro em Assembler também tem um grande aumento de velocidade. Hoje eu testei a rotina PutPixel da biblioteca MODE13H e uma pura (praticamente idêntica), e vi uma diferença espantosa. Num 486SX-25 com 4Mb de RAm e uma placa VGA de 16 bits, levou 5 centéssimos de segundo para a rotina pura desenhar 65536 pixels no meio da tela, contra 31 centésimos de segundo da outra. Grande diferença, não? OTIMIZAÇÃO Por mais rápido que o Assembler seja, você sempre pode acelerar as coisas. Eu vou falar como acelerar seu código no 80486, e no 80386. Eu não vou me preocupar muito com o Pentium por enquanto, já que os truques de uso do Pentium são realmente truques, e demoraria um pouco para explicar. Também, você deveria evitar código específico para Pentium (embora isso esteja mudando lentamente). A AGI (Address Generation Interlock): Que merda é essa?, você pergunta. Uma AGI ocorre quando uma registrador que está correntemente sendo usado como base ou índice foi o destino da última instrução. AGI's são ruins, e chupam clocks. EX.: MOV ECX, 3 MOV FS, ECX Isso pode ser evitado executando-se uma outra instrução entre os dois MOV's, pois AGI's podem ocorrer só entre instruções adjacentes (no 486). No Pentium, uma AGI pode acontecer até entre 3 instruções!

Page 75: Assembler

Use Instruções/Registradores de 32 bits: Usar registradores de 32 bits tende a ser mais rápido que usar seus equivalentes de 16 (particularmente EAX, já que muitas instruções ficam um byte menor quando ele é usado. Usar DS ao invés de ES também é mais rápido pelo mesmo motivo). Outras coisas para se tentar: þ Evite LOOP's. tente usar um DEC, ou INC seguido de um JZ ou instrução similar. Isso pode fazer uma diferença enorme. þ Quando for zerar registradores, use XOR ao invés de MOV xx, 0. acredite ou não, é mesmo mais rápido. þ Use o TEST quando for checar se um registrador é igual a zero. Em se fazer um AND dos operandos juntos não se gasta tempo com um registrador destino. TEST EAX,EAX é um bom mode de checar de EAX=0. þ USE SHIFTS! Não use multiplicação para calcular mesmo as mais simples somas. A CPU pode mover uns poucos zeros para a esquerda ou para a direita muito mais rápido que ela pode fazer uma multiplicação/divisão. þ Faça uso da LEA. Uma instrução é tudo o que leva para realizar uma multiplicação inteira e armazenar o resultado num registrador. Esta é uma alternativa útil para SHL/SHR (eu sei, eu sei... eu disse que a multiplicação era ruim. Mas uma LEA às vezes pode ser útil, já que pode economizar várias instruções.). EX.: LEA ECX, [EDX+EDX*4] ; ECX = EDX x 5 þ Evite fazer MOV's para registradores de segmento muito frequentemente. Se você vai trabalhar com um valor que não muda, tal como A000h, então carregue-o em FS, por exemplo, e use FS daí em diante. þ Acredite ou não, instruções de string (LODSx, MOVSx, STOSx))são muito mais rápidas num 386 que num 486. Se estiver trabalhando num 486 ou mais, então use outra instrução, mais simples. þ Quando for mover pedaços de 32 bits, REP STOSD é mais rápido que usar um loop para fazer a mesma coisa.

Page 76: Assembler

Bem, agora você já viu como escrever código externo, declarar procedures em Assembler e otimizar suas rotinas. ESTRUTURAS DE DADOS EM ASSEMBLER Bem, até agora você deveria saber que você pode usar o DB, (Declare Byte) e DW, (Declare Word) para criar variáveis. Porém, até agora nós os temos usado como você usaria a declaração de Const em Pascal. Quer dizer, temos usado isto para dar a um byte ou a uma word um valor. Ex.: MyByte DB 10 -- que é o mesmo que -- Const MyByte : Byte = 10; Contudo, poderíamos dizer: MyByte DB ? ...e então dizer depois: MOV MyByte, 10 De fato DB realmente é muito poderoso. Há vários tutoriais atrás, quando você estava aprendendo a escrever strings na tela, você viu algo desse tipo: MyString DB 10, 13 "This is a string$" Agora, o mais curioso de vocês provavelmente teria dito a si prórpio: "Peraí!... aquele cara do tutorial disse que DB declara um BYTE. Como é que o DB pode declarar uma string, então "? Bem, DB tem a habilidade de reservar espaço para valores de vários bytes - de 1 a tantos bytes quanto você precisa. Você também pode ter desejado saber o que os números 10 e 13 antes do texto representavam. Bem, dê uma olhada na sua tabela ASCII e veja o que são o 10 e

Page 77: Assembler

o 13. Você notará que 10 é o Line Feed e o 13 é o Carriage Return. Basicamente, é o mesmo que dizer: MyString := #10 + #13 + 'This is a string'; em Pascal. Ok, então você viu como criar variáveis corretamente. Mas, e constantes? Bem, em Assembler, constantes são conhecidas como Equates. Equates fazem a cofificação em Assembler muito mais fácil, e pode simplificar muito as coisas. Por exemplo, se eu tivesse usado o seguinte em tutoriais anteriores: LF EQU 10 CR EQU 13 DB LF, CR "Isso é uma string$" ...as pessoas teriam entendido direito aquela coisa de 10 e 13. Mas, para fazer as coisas um pouco mais complicadas, há ainda um outro modo que você pode usar para dar valores a identificadores. Você pode fazer como você faria em BASIC: Population = 4Ch Magnitude = 0 Basicamente, você pode ter em mente os seguintes pontos: þ Uma vez que você tenha usado EQU para dar um valor a um identificador, você não pode mudar isto. þ EQU pode ser usado para definir quase qualquer tipo - inclusive strings. Contudo, você não pode fazer isto quando se usa um ' = '. Um ' = ' só pode definir valores numéricos. þ Você pode usar EQU quase em qualquer lugar de seu programa. þ Valores definidos com ' = ' podem ser mudados. E agora, vamos a um dos pontos mais macetosos de codificação em Assembler - estruturas. Estruturas não são variáveis, são um TIPO - basicamente um esquema

Page 78: Assembler

de uma variável. Como um exemplo, se você tivesse o seguinte em Pascal: Type Date = Record; Day : Byte; Month : Byte; Year : Word; End; { Record } Você poderia representar isto em Assembler como segue: Date STRUC Day DB ? Month DB ? Year DW ? Date ENDS Porém, um das vantagens de Assembler é que você pode inicializar todos ou alguns dos campos da estrutura antes mesmo de você se referir à estrutura em seu segmento de código. Aquela estrutura acima poderia ser escrita facilmente como: Date STRUC Day DB ? Month DB 6 Year DW 1996 Date ENDS Alguns pontos importantes para se lembrar são os seguintes: þ Você pode declarar uma estrutura em qualquer lugar em seu código, embora que, para um bom design, você deva colocá-los no segmento de dados, a menos que eles só sejam usados por uma subrotina. þ Definir uma estrutura não reserva qualquer byte de memória para a mesma. Isso só acontece quando você declara uma variável dessa estrutura - aí a memória é alocada. REFERENCIANDO ESTRUTURAS DE DADOS EM ASSEMBLER

Page 79: Assembler

Bem, você viu como definir estruturas, mas como você se refere de verdade a elas em seu código? Tudo o que você tem a fazer, é colocar em algum lugar algumas linhas como as seguintes em seu programa - de preferência no segmento de dados. Date STRUC Day DB 19 Month DB 6 Year DW 1996 Date ENDS Date_I_Passed_Physics Date <> ; Espero! Neste momento, Date_I_Passed_Physics tem todos os seus três campos preenchidos. Dia é setado para 19, Mês para 6 e Ano para 1996. Agora, o que são esses parênteses,"<>", fazendo depois de data? - você pergunta. Os parênteses nos apresentam um outro modo de alterar os conteúdos dos campos da variável. Se eu tivesse escrito isto: Date_I_Passed_Physics Date <10,10,1900> ...então os campos teriam sido mudados para os valores nos parênteses. Alternativamente, teria sido possível fazer isto: Date_I_Passed_Physics Date <,10,> ; E só agora o campo de Mês foi mudado. Note que neste exemplo, a segunda vírgula não era necessária, pois nós não mudamos outros campos posteriores. É sua escolha, (e do compilador!), se deixar a segunda vírgula ou não. Agora tudo isso tá indo muito bem, mas como você se estes valores em seu código? Simplesmente basta dizer: MOV AX, [Date_I_Passed_Physics.Month] ; ou algo como MOV [Date_I_Passed_Physics.Day], 5 ; ou até mesmo CMP [Date_I_Passed_Physics.Year], 1996

Page 80: Assembler

Simples, né? CRIANDO ARRAYS EM ASSEMBLER Certo, arrays são bem fáceis de se implementar. Por exemplo, digamos que você tivesse a seguintes estrutura de array em Pascal: Var MyArray: Array[0 ..19] of Word; Para criar um array semelhante em Assembler, você tem que usar o operador DUP. DUP, ou DUPlique Variável, tem a seguinte sintaxe: þ <rótulo> <diretiva> <contador> DUP (expressão) Onde (expressão) é um valor opcional para inicializar o array. Basicamente, aquele array no Pascal se pareceria com isso: MyArray DW 20 DUP (?) Ou, se você quisesse inicializar cada valor para zero, então você poderia dizer isto: MyArray DW 20 DUP (0) E, como outro exemplo de como o Assembler é flexível, você poderia dizer algo desse tipo: MyArray DB 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,,, ...para criar um array de 10 bytes, com todos os dez elementos inicializados em 1, 2, 3... INDEXANDO ARRAYS EM ASSEMBLER

Page 81: Assembler

Bem, agora que você já viu como criar arrays, eu suponho que você queira saber como referenciar elementos individualmente. Bem, digamos que você tivesse o seguinte array: OutroArray DB 50 DUP (?) Se você quisesse mover o elemento 24 para, digamos, BL, então você poderia fazer isto: MOV BL, [OutroArray + 23]; Ou, seria possível dizer: MOV AX, 23, MOV BL, [OutroArray + AX] NOTA: Não esqueça que todos os arrays começam no elemento ZERO. Linguagens de alto-nível como C e Pascal fazem você esquecer isto devido ao modo que eles deixam você referenciar arrays. Agora, isso foi fácil, mas, e se OutroArray fosse de 50 WORDS, não BYTES? OutroArray DW 50 DUP (?) ; como esse. Bem, para acessar o elemento 24, você teria que multiplicar o valor de índice por dois, e então somar isso a OutroArray para conseguir o elemento desejado. MOV AX, 23 ; Acesso elemento 24 SHL AX, 1 ; Multiplique AX por dois MOV BX, [OutroArray + AX] ; Adquira elemento 24 em BX Não é tão difícil assim, né? Porém, este método fica um pouco macetoso quando você não tem cálculos fáceis para fazer quando o índice não é uma potência de dois. Digamos que você tivesse um array que tem um tamanho de elemento de 5 bytes. Se nós quiséssemos conferir o sétimo elemento, nós teríamos que fazer algo assim: MOV AX, 6 ; Pega o sétimo elemento MOV BX, 5 ; Cada elemento tem cinco bytes MUL BX ; AX = 6 x 5 MOV DX, [YetAnotherArray + AX] ; Coloca o elemento 7 em DX

Page 82: Assembler

Porém, como eu disse antes, MUL não é um modo muito eficiente de codificação, assim, substituir o MUL por um SHL 2 e um ADD seria a ordem do dia. Antes de continuarmos com mais alguma coisa, eu suponho que seja hora de falar sobre números de ponto flutuante. Agora, números de ponto flutuantes podem ser desajeitados para se manipular em Assembler, assim vê se não sai escrevendo aquele programa de planilha eletrônica que você sempre quis, em código de máquina! Porém, quando estiver trabalhando com mapeamento de textura, círculos e outras funções mais complicadas, é inevitável que você precise de algo para declarar números de ponto flutuante. Digamos que quiséssemos armazenar Pi. Para declarar Pi, nós precisamos usar a DT diretiva. Você poderia declarar Pi assim: Pi DT 3.14 DT na verdade reserva dez bytes de memória, assim seria possível declarar Pi com um número maior de casas decimais. Eu não vou entrar nas particularidades de números de ponto flutuante neste tutorial. Quando nós precisarmos deles mais tarde, eu falo sobre isso. Certo, no último tutorial disse eu que eu daria algum tipo de resumo do que nós cobrimos durante os últimos quatro meses. (Ei - isso é como se fosse um tutorial a cada duas semanas, então talvez eles não tenham saído tão irregularmente, afinal de contas!) De qualquer maneira, eu vou falar sobre a parte de pegar e setar bits individuais num registrador, porque este é um tópico importante que eu deveria ter coberto há muito tempo atrás. OPERADORES LÓGICOS Certo, de volta ao Tutorial Cinco, eu dei as três tabelas verdade para E, OU

Page 83: Assembler

e XOR. (A propósito, em uma edição de Tutorial Cinco, eu errei a tabela para XOR, amavelmente apontado por Keith Weatherby, assim se você não tem a versão mais atual, (V 1.3), então pegue agora. Por favor, embora eu tente o meu melhor para excluir qualquer erro dos Tutoriais, alguns ficam com erros, assim se você achar algum, por favor me avise. Mas tenha certeza de que você tem as edições mais recentes dos tutoriais antes de fazer isto!) Certo, chega de meus erros. Essas tabelas se pareciam com estas: AND OR XOR 0 AND 0 = 0 0 OR 0 = 0 0 XOR 0 = 0 0 AND 1 = 0 0 OR 1 = 1 0 XOR 1 = 1 1 AND 0 = 0 1 OR 0 = 1 1 XOR 0 = 1 1 AND 1 = 1 1 OR 1 = 1 1 XOR 1 = 0 Isto está tudo muito bem, mas pra quê vamos usar isso? Bem, em primeiro lugar, vamos dar uma olhada no que o AND pode fazer. Nós podemos usar o AND para mascarar bits em um registrador ou variável, e assim setar e resetar bits individuais. Como um exemplo, usaremos o AND para testar um valor de um único bit. Olhe os exemplos seguintes, e veja como você pode usar AND para seus próprios fins. Um uso bom para AND seria conferir se um caracter lido do teclado é uma maiúscula ou não. (Você pode fazer isto, porque a diferença entre uma maiúscula e sua minúscula é de um bit. Ex: 'A' = 65 = 01000001 'a' = 97 = 01100001 'S' = 83 = 01010011 's' = 115 = 01110011) Assim, da mesma forma que você pode azer um AND de números binários, você poderia usar uma aproximação semelhante para escrever uma rotina que confere se um caracter é maiúsculo ou minúsculo. Ex: 0101 0011 0111 0011

Page 84: Assembler

AND 0010 0000 AND 0010 0000 = 0000 0000 = 0010 0000 ^^^ Essa é maiúscula ^^^ ^^^ Essa é minúscula ^^^ Agora, e o OR? O OR é geralmente usado depois de um AND, mas não tem que ser. Você pode usar OR para mudar bits individuais em um registrador ou variável sem mudar quaisquer um dos outros bits. Você poderia usar OR para escrever uma rotina para mudar um caracter para maiúsculo se já não for, ou talvez para minúscula se fosse maiúscula. Ex: 0101 0011 OR 0010 0000 = 0111 0011 ^^^ S maiúsculo agora foi mudado para s minúsculo ^^^ A combinação de AND/OR é um dos truques mais frequentemente usados no mundo do Assember, assim tenha certeza de que você entendeu bem o conceito. Você me verá freqüentemente usando-os, tirando proveito da velocidade das instruções. Finalmente, e o XOR? Bem, o OU exclusivo pode ser às vezes muito útil. XOR pode ser de útil para alternar bits individuaisentre 0 e 1 sem ter que saber qual o conteúdo que cada bit tinha anteriormente. Lembre-se, como com OU, uma máscara de zero permite ao bit original continuar com seu valor. Ex: 1010 0010 XOR 1110 1011 = 0100 1001 Faça alguma tentativa para aprender estes operadores binários, e o que eles fazem. Eles são uma ferramenta inestimável quando se está trabalhando com números binários. OBS.: Para simplicidade, o Turbo Assembler lhe permite usar números binários em seu código. Por exemplo, seria possível dizer, AND AX, 0001000b em vez de AND AX, 8h para testar o bit 3 de AX. Isto pode facilitar as coisas para você quando codificar.

Page 85: Assembler

O PROGRAM DEMONSTRATIVO Certo, chega da parte chata - vamos ao programa demonstrativo que eu incluí! Eu pensei que já era sem tempo escrever outra demonstração - 100% Assembler desta vez, e vamos a uma rotina de fogo. Rotinas de fogo podem parecer bem efetivas, e são surpreendentemente fáceis de se fazer, assim, pensei, por que não... Agora, os princípios de uma rotina de fogo são bastante simples. Você basicamente faz o seguinte: þ Crie um buffer com o qual você vai trabalhar Este buffer pode ser quase de qualquer tamanho, entretanto quanto menor você o fizer, o mais rápido seu programa será, e quanto maior você o fizer, o mais bem definido o fogo será. Você precisa acertar um equilíbrio entre claridade e velocidade. Minha rotina está um pouco lenta, e isto é devido em parte à claridade do fogo. Eu escolhi 320 x 104 como tamanho do meu buffer, assim eu fiz um compromisso. A resolução horizontal é boa - 1 pixel por elemento de array, mas a resolução vertical é um pouco baixa - 2 pixels por elemento de array. Contudo, eu já vi rotinas onde um buffer de 80 x 50 é usado, significando que há 4 pixels por elemento para o eixo horizontal e vertical. É rápido, mas com baixíssima definição. þ Faça uma palette agradável Seria idéia boa para ter cor 0 como preto, (0, 0, 0) e a cor 255 como branco - (63, 63, 63). Tudo entre isso deveria ser uma mistura de amarelo-avermelhado flamejante. Eu suponho você poderia ter chamas verdes se você quisesse, mas nós vamos usar as chamas que nós conhecemos agora. :) Agora o loop principal começa. No loop você deve: þ Criar uma linha de fundo ramdômica, ou duas linhas de fundo

Page 86: Assembler

Basicamente, você tem um loop como: For X := 1 To Xmax Do Begin Temp := Random(256); Buffer[X, Ymax - 1] := Temp; Buffer[X, Ymax] := Temp; End; Codifique isso na linguagem de sua escolha, e você está no negócio. þ Suavize o array: Agora este é o único pedaço com macete. O que você tem que fazer, é como segue: * Comece da segunda linha pra baixo do buffer. * Mover para baixo, e para cada pixel: * Some os valores dos quatro pixels que cercam o pixel. * Divida o total por quatro conseguir uma média. * Tire um da média. * Ponha a média - 1 no array DIRETAMENTE ACIMA onde o pixel velho estava. (Você pode alterar isto, e digamos, pôr acima e à direita, e então parecerá que a chama está sendo soprada pelo vento.) * Faça isso até você chegar à última linha. þ Copie o array para a tela Se seu array é de 320 x 200, então você pode copiar elemento-para-pixel. Se não é, então coisas são mais difíceis. O que eu tive que fazer era copiar uma linha do array para a tela, abaixar uma linha da tela, copiar a mesma linha do array para a tela, e então entrar numa linha diferente no array e na tela. Deste modo, eu espalhei o fogo um pouco. Você vai, é, querer saber exatamente por que meu array é de 320 x 104 e não de 320 x 100. Bem, a razão para isto é bastante simples. Se eu tivesse usado 320 x 100 como minhas dimensões de array, e então copiasse isso para a tela, as últimas quatro linhas teriam parecido bem estranhas. Elas não teriam sido suavizados corretamente, e o resultado final não estaria de todo flamejante. Assim, eu apenas copiei até a linha 100 para a tela, e deixei o resto pra lá.

Page 87: Assembler

Como uma experiência, tente mudar a terceira linha abaixo no procedimento de DrawScreen para MOV BX, BufferY e mudar as dimensões para 320x100 e veja o que acontece. MOV SI, OFFSET Buffer ; Aponta SI para o início do buffer XOR DI, DI ; Começa a desenhar em 0, 0 MOV BX, BufferY - 4 ; Perde as 4 últimas linhas do ; buffer. Estas linhas não vão se parecer ; com fogo de jeito nehum. þ Volta para o início. Bem, não importa o quão bem eu expliquei isso tudo, é muito difícil de ver o que está acontecendo sem olhar o código. Então agora nós vamos dar uma olhada no programa, seguindo o que está acontecendo. Bem, em primeiro lugar, você tem o header. .MODEL SMALL ; Segmento de dados < 64K, segmento de código < 64K .STACK 200H ; Arruma 512 bytes de espaço para a pilha .386 Aqui, eu disse que o programa terá um segmento de código e de dados total de menos que 128K. Eu vou dar para o programa uma pilha de 512 bytes, e permitir instruções do 386. .DATA CR EQU 13 LF EQU 10 O segmento de dados começa, e eu dou para CR e para LF os valores de "carriage return" e "line feed" (retorno de carro e alimentação de linha, i.e, volta pro início e desce uma linha). BufferX EQU 320 ; Largura do buffer de tela BufferY EQU 104 ; Altura do buffer de tela

Page 88: Assembler

AllDone DB CR, LF, "That was:" DB CR, LF DB CR, LF, " FFFFFFFFF IIIIIII RRRRRRRRR ..." DB CR, LF, " FFF III RRR RRR ..." DB CR, LF, " FFF III RRR RRR ..." DB CR, LF, " FFF III RRRRRRRR ..." DB CR, LF, " FFFFFFF III RRRRRRRR ..." DB CR, LF, " FFF III RRR RRR ..." DB CR, LF, " FFF III RRR RRR ..." DB CR, LF, " FFF III RRR RRR ..." DB CR, LF, " FFFFF IIIIIII RRRR RRRR ..." DB CR, LF DB CR, LF DB CR, LF, " The demo program from Assembler Tutorial 8. ..." DB CR, LF, " author, Adam Hyde, at: ", CR, LF DB CR, LF, " þ [email protected]" DB CR, LF, " þ http://www.faroc.com.au/~blackcat", CR, LF, "$" Buffer DB BufferX * BufferY DUP (?) ; O buffer de tela Seed DW 3749h ; O valor de seed, e metado do meu número de ; telefone - não em hexa. :) INCLUDE PALETTE.DAT ; A palette, gerada com ; Autodesk Animator, e um programa simples em ; Pascal. Agora, no fim, eu declaro o array e declaro um VALOR DE SEED (semente) para o procedimento Random que segue. A seed é só um número que é necessário para começar o procedimento Random, e pode ser qualquer coisa que você quiser. Eu também economizei algum espaço e pus os dados para a palette em um arquivo externo que é incluído no código assembly. Dê uma olhada no arquivo. Usar INCLUDE pode economizar muito espaço e confusão. Eu pulei alguns procedimentos que são bastante auto-explicativos, e fui direto para a procedure DrawScreen. DrawScreen PROC MOV SI, OFFSET Buffer ; Aponta SI para o início do buffer XOR DI, DI ; Começa a desenhar em 0, 0

Page 89: Assembler

MOV BX, BufferY - 4 ; Perde as últimas 4 linhas do buffer ; Essas linhas não se parecem ; com fogo, de jeito nenhum Row: MOV CX, BufferX SHR 1 ; 160 WORDS REP MOVSW ; Move-as SUB SI, 320 ; Volta pro início da linha do array MOV CX, BufferX SHR 1 ; 160 WORDS REP MOVSW ; Move-as DEC BX ; Decrementa o número de linhas VGA restantes JNZ Row ; Terminamos? RET DrawScreen ENDP Isto também é fácil seguir, e tira proveito de MOVSW, usando-a para mover dados entre DS:SI e ES:DI. AveragePixels PROC MOV CX, BufferX * BufferY - BufferX * 2 ; Altera todo o buffer, ; exceto a primeira linha e a última MOV SI, OFFSET Buffer + 320 ; Começa da segunda linha Alter: XOR AX, AX ; Zera AX MOV AL, DS:[SI] ; Pega o valor do pixel atual ADD AL, DS:[SI+1] ; Pega o valor do pixel à direita ADC AH, 0 ADD AL, DS:[SI-1] ; Pega o valor do pixel à esquerda ADC AH, 0 ADD AL, DS:[SI+BufferX] ; Pega o valor do pixel abaixo ADC AH, 0 SHR AX, 2 ; Divide o total por quatro JZ NextPixel ; O resultado é zero? DEC AX ; Não, então decrementa de um NOTA: O valor de decay (queda) é UM. Se você mudar a linha acima para, por exemplo "SUB AX, 2" você vai ver que o fogo não chega tão alto. Experimente... seja criativo! :)

Page 90: Assembler

NextPixel: MOV DS:[SI-BufferX], AL ; Põe o novo valor no array INC SI ; Próximo pixel DEC CX ; Um a menos para fazer JNZ Alter ; Já fizemos todos? RET AveragePixels ENDP Agora nós vimos a procedure que faz toda a suavização. Basicamente, nós só temos um loop que soma os valores de cor dos pixels ao redor de um pixel, carregando os valores dos pixels antes. Quando ela tem o total em AX, é dividido por quatro para conseguir uma média. A média é então plotada diretamente sobre o pixel atual. Para mais informação relativo à instrução de ADC, observe isto em Tutorial 5, e olhe os programas abaixo: Var Var W : Word; W : Word; Begin Begin Asm Asm MOV AL, 255 MOV AL, 255 ADD AL, 1 ADD AL, 1 MOV AH, 0 MOV W, AX ADC AH, 0 End; MOV W, AX End; Write(W); End; Write(W); End; ^^^ Este programa returna 256 ^^^ Este programa returna 0 Lembre-se de que ADC é usado para ter certeza que quando um registrador ou variável não é grande bastante para armazenar um resultado, o resultado não será perdido. OK, depois de pular algumas procedures um pouco mais irrelevantes, chegamos ao corpo principal do programa, que é algo desse tipo: Start:

Page 91: Assembler

MOV AX, @DATA MOV DS, AX ; DS agora aponta para o segmento de dados. Nós apontamos DS primeiramente para o segmento de dados, de modo que possamos ter acesso a todas nossas variáveis. CALL InitializeMCGA CALL SetUpPalette MainLoop: CALL AveragePixels MOV SI, OFFSET Buffer + BufferX * BufferY - BufferX SHL 1 ; SI agora aponta para o início da segunda última linha (?????? - by Krull) MOV CX, BufferX SHL 1 ; Prepara para pegar BufferX x 2 números randômicos BottomLine: CALL Random ; Pega um número randômico MOV DS:[SI], DL ; Usa apenas o byte baixo de DX, i.e., INC SI ; o número vai ser de 0 --> 255 DEC CX ; Um pixel a menos para fazer JNZ BottomLine ; Já acabamos? Aqui, uma nova linha do fundo é calculada. O procedimento Random - muitas graças ao autor desconhecido da USENET - retorna um valor muito alto em DX:AX. Porém, nós só requeremos um número de 0 a 255, assim, usando só DL, nós temos tal número. CALL DrawScreen ; Copia o buffer para a VGA MOV AH, 01H ; Checa se foi pressionada alguma tecla INT 16H ; Há alguma tecla esperando no buffer? JZ MainLoop ; Não, segue em frente MOV AH, 00H ; Sim, então pega a tecla INT 16H CALL TextMode MOV AH, 4CH MOV AL, 00H

Page 92: Assembler

INT 21H ; Volta ao DOS END Start E eu acho que essa última parte também é bem fácil de entender. Eu tentei comentar o fonte o tanto quanto eu pude, talvez um pouco mais fortemente em algumas partes, mas eu espero que agora todo mundo tenha uma idéia de como uma rotina de fogo funciona. De qualquer maneira, vamos lá com: E/S DE ARQUIVOS 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;

Page 93: Assembler

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

Page 94: Assembler

Done: MOV AX, 4C00H ; Pula de volta pro DOS - fechando qualquer INT 21H ; arquivo aberto. Relaxado, 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

Page 95: Assembler

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

Page 96: Assembler

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

Page 97: Assembler

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. CHAMANDO O ASSEMBLER NO C/C++ 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 * /

Page 98: Assembler

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: þ O compilador usa SI e DI para armazenar variáveis registradoras. Se você usou variáveis registradoras em seu código, lembre-se de dar um push e pop em SI e DI em seu código externo. þ O compilador provavelmente não vai dar push e pop em CS, DS, SS e BP, então tenha certeza de ter cuidado se for alterar algum desses registradores. 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

Page 99: Assembler

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;

Page 100: Assembler

} 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

Page 101: Assembler

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]

Page 102: Assembler

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! UMA INTRODUÇÃO ÀS MACROS

Page 103: Assembler

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.

Page 104: Assembler

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: O PROGRAMA DEMONSTRATIVO 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ê

Page 105: Assembler

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: þ Crie uma tabela de lookup 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) þ Inicialize a palette 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;

Page 106: Assembler

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.

Page 107: Assembler

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