Um bom programador de Assembly precisa conhecer um pouco mais do que o conjunto de instruções da linguagem - entender a arquitetura do computador também é necessário. A não ser que se conheça a operação interna das instruções e como estas operações internas interagem, não é possível escrever programas rápidos e eficientes. Da mesma forma, se não se souber como a máquina codifica instruções, não será possível avaliar se uma determinada sequência de instruções é mais curta ou mais longa que uma outra.
Neste laboratório usaremos um debugger rudimentar para criar, testar e executar pequenos programas em linguagem de máquina. Também exploraremos os métodos de otimização de contagem de ciclos e contagem de bytes. Estes métodos são importantes quando se trabalha com programas complexos em linguagem Assembly.
Um debugger é um programa que permite mostrar e modificar diferentes posições de memória, executar instruções, mostrar registradores da máquina e realizar outras operações comuns necessárias para se testar e corrigir programas em Assembly. O uso correto de um debugger reduz dramaticamente o tempo de desenvolvimento. Neste laboratório usaremos um debugger muito simples para os processadores x86. É uma introdução para o debugger CodeView que será descrito no capítulo 4.
Antes de descrever o debugger é preciso esclarecer o que é o simulador x86. Como os processadores x86 são hipotéticos, não existe qualquer tipo de hardware no qual os programas x86 possam ser executados. O simulador x86 (SIMx86) é um software que simula um processador x86. Este programa busca os opcodes x86 na memória (a do 80x86) e depois executa uma sequência de instruções que emulam o comportamento destas instruções.
O programa SIMx86 usa variáveis inteiras para guardar os valores dos registradores e os endereços de memória. Não é possível encontrar diferenças entre o programa SIMx86 e o processador 886 (se existisse um). Aliás, a técnica de simulação não se limita apenas a processadores hipotéticos. SoftPC e outros programas usam exatamente esta técnica para emular processadores 80x86 no Macintosh, NeXT e outros sistemas.
O programa SIMx86 é da autoria de Randall Hyde. Originalmente escrito em Object Pascal (Delphi), foi traduzido e recompilado por mim. Você pode fazer o download do executável, dos códigos fonte e de exemplos de programas aqui na Aldeia.
O programa SIMx86 simula o espaço de endereços de 64 K do processador 886 usando um array de 64K no espaço de memória do 80x86. Tudo o que normalmente apareceria na localização zero da memória do processador 886 é colocado no índice zero deste array. Como em qualquer outro debugger decente, o programa SIMx86 mostra e permite a entrada de valores na memória. Quando o programa é iniciado, todo o espaço da memória é preenchido com zeros. Para verificar o conteúdo da memória, clique na aba "memória" e veja inicialmente as posições de 0000h a 0038h. Para verificar outras posições basta dar entrada de um novo endereço inicial (na notação hexadecimal) no campo situado no canto superior esquerdo que o dump é automaticamente realizado, mostrando os novos valores encontrados.
Este dump é muito útil para mostrar os valores de variáveis, arrays ou até de código de máquina (a representação binária de instruções). Entretanto é preciso ter em mente um aspecto muito importante - o dump mostra os valores na sequência endereço mais baixo/endereço mais alto. Até aí, tudo normal. Acontece que, por este motivo, os valores word, que são armazenados em dois bytes consecutivos com o byte menos significativo no endereço mais baixo e o byte mais significativo no endereço mais alto, aparecem com os "bytes trocados". Se o word no endereço 1000h contém 1234h, o que será visto no dump será:
1000: 34 12 xx xx xx xx xx xx
Você precisa se lembrar de reposicionar mentalmente os bytes para obter os valores corretos. Um erro muito comum feito pelos iniciantes é esquecer de trocar as posições dos bytes mostrados pelo debugger.
Para alterar um valor na memória basta ativar a posição desejada e digitar o novo valor (tudo, sempre, em hexadecimal). Não se esqueça da "inversão" de bytes também na entrada de novos valores!
1. Escolher a aba "Memória". 2. Digitar 18A0 no campo do endereço inicial.
1. Escolher a aba "Memória". 2. Ativar a posição 8000. 3. Digitar 37. 4. Ativar a posição 8001. 5. Digitar 98.
Para poder criar pequenos programas para o x86 é preciso conhecer seu conjunto de instruções. Este assunto foi amplamente discutido no tópico O conjunto de instruções do x86. Apenas para refrescar a memória, veja abaixo as formas das instruções possíveis:
mov reg, reg/mem/const mov mem, reg add reg, reg/mem/const sub reg, reg/mem/const cmp reg, reg/mem/const and reg, reg/mem/const or reg, reg/mem/const not reg/mem ja dest -- Desvia se maior jae dest -- Desvia se maior ou igual jb dest -- Desvia se menor jbe dest -- Desvia se menor ou igual je dest -- Desvia se igual jne dest -- Desvia se não igual jmp dest -- Desvio incondicional iret -- Retorna de uma interrupção get -- Espera entrada do usuário em ax put -- Mostra o valor de ax halt -- Termina o programa brk -- Suspende a execução
Os opcodes são os códigos que o processador usa para identificar as instruções. Para reavivar a memória, os opcodes das instruções básicas podem ser vistos abaixo:
![]() Fig.1 - Codificação de opcodes no x86 |
Observe que, para montar o opcode de uma instrução, basta compor o primeiro byte de acordo com o padrão adotado. Assim, por exemplo, o opcode da instrução and ax, bx é montado da seguinte forma:
Coloque o valor hexadecimal 41 na posição de memória 0000 e clique na aba "Emulador". Verifique que a posição 0000 mostra exatamente a instrução referente ao opcode 41, ou seja, and ax, bx.

mov ax, ... = 1100 0111 = C 7 7BA2 = A2 7B mov ax, 7BA2 = C7 A2 7B

mov bx, [....] = 1100 1110 = C E 8000 = 00 80 mov bx, [8000] = CE 00 80
mov bx, [8000] = CE 00 80 = 3 bytes
Move o conteúdo do endereço de memória 8000 para o registrador BX.
mov ax, [1000] mov [8000], ax
![add cx, [bx]](grafs/CH03labQ8.gif)
1011 0100 = B4
add cx, [bx] = B4 = 1 byte
Adiciona o valor do conteúdo do endereço de memória indicado pelo registrador BX ao valor do registrador CX e armazena o resultado no registrador CX.
![sub dx, [xxxx+bx]](grafs/CH03labQ11.gif)
sub dx, [xxxx+bx] = 3 bytes
1001 1101 = 9D 2002 = 02 20 sub dx, [2002+bx] = 9D 02 20
Busca o valor das posições de memória 2002+BX e 2003+BX e o subtrai do registrador DX, deixando o resuldo no registrador DX, ou seja, DX = DX - (2002+BX 2003+BX).
Os opcodes das instruções de desvios de execução, também chamados de saltos, podem ser vistos na Fig.2.
![]() Fig.2 - Opcodes de saltos |
Se os bits de número 3 a 7 (numerados de 0 a 7, da direita para a esquerda) forem 10000 (que são vistos na Fig.2 como 00001), o 886 sabe que o opcode se refere a uma instrução de salto. Estas instruções sempre exigem, além do byte do opcode, mais dois bytes (ou 16 bits) que contenham o endereço alvo do salto. Portanto, estas instruções sempre têm o comprimento de 3 bytes.
Não custa lembrar que estas instruções, com exceção da instução jmp, só têm sentido se usadas após uma instrução de comparação (cmp), a qual prepara as flags que serão utilizadas pelo processador para decidir se o salto condicional deve ou não ser efetuado. Portanto, quando se pretende um salto condicional, além dos 3 bytes do próprio salto precisamos contar com 1 byte adicional da operação de comparação (total de 4 bytes em duas instruções).

cmp dx, bx = 0111 1001 = 79h

ja 8098 = 0000 1100 = 0C 98 80
O valor do registrador DX será comparado com o valor do registrador BX (cmp dx, bx). A seguir, se o valor de DX for maior que o do registrador BX, a execução é desviada para o endereço de memória 8098. Caso contrário, a execução continua com a próxima instrução.
Agora que sabemos como os opcodes são montados, como inserí-los em posições de memória e como visualizá-los no simulador, chegou a hora de criar alguns programas. Logicamente, o SIMx86 também permite executá-los (passo a passo ou de forma corrida) e testá-los. Além disso podemos alterar valores nos registradores e analisar comparações. Mas antes, uma palavrinha sobre o editor do SIMx86.
O simulador SIMx86 possui um editor para facilitar a nossa vida. Não será preciso ficar calculando cada um dos opcodes das instruções que quisermos utilizar - o editor faz este trabalho para nós - basta indicar a sequência de instruções que nos interessa. E mais! Caso haja algum erro de sintaxe ou de "ortografia", ele gentilmente nos informa. Estes programas podem ser salvos (como podem ser abertos) para serem usados em outras ocasiões. NÃO SE ESQUEÇA: estes programas só rodam no simulador pois baseiam-se no funcionamento de um processador hipotético!
Antes de começar com um programa "de verdade" que será rodado no processador 886 "de mentira", é melhor fazer uma faxina: clique na aba "Memória" e no botão "Limpar Memória" para zerar todas as posições. A seguir, clique na aba "Editor" e ponha no "Endereço Inicial" o valor 0000 (se é que já não está assim). A seguir, digite o seguinte programa exemplo que pede ao usuário que entre com cinco números e no final apresenta a soma dos mesmos. NÃO digite as observações, estas são citadas apenas para explicar o funcionamento do programa:
mov dx, 5 ; Repetir o loop 5 vezes mov cx, 0 ; Contador do loop mov bx, 0 ; Acumular o resultado aqui a: get ; Ler o valor do usuário add bx, ax ; Soma o valor em bx add cx, 1 ; Aumenta o contador em 1 cmp cx, dx ; Compara o contador com 5 jb a ; Se CX for menor, desvia para o marcador a: mov ax, bx ; Põe a soma em AX put ; Apresenta o resultado de AX halt ; Tudo em riba, terminamos
Observe que usamos um marcador para o salto condicional (o marcador e o salto estão destacados em negrito). Se a condição for preenchida, ou seja, se o valor do contador CX for menor do que o valor de DX=5, então a execução deve ser desviada para o endereço do marcador "a:". Os marcadores são sempre letras únicas seguidas por dois pontos. A vantagem do uso de marcadores é que não precisamos calcular o endereço do salto - o editor faz isto para nós.
Depois de digitar o código do programa, clique no botão "Transferir". Se o código estiver correto, não aparecem mensagens de erro. Caso alguma seja mostrada, corrija o erro e clique novamente no botão. Só por curiosidade, clique na aba "Memória", certifique-se de que o endereço inicial é 0000 e dê uma olhada nos opcodes. Observe que, com apenas 21 bytes, até que fizemos um programinha legal. Agora só falta testá-lo.
Clique na aba "Emulador". Você deve encontrar o seguinte:

Observe o programa já posicionado na memória com os opcodes e as respectivas instruções. Se o ponteiro de instruções não estiver em 0000 ou se algum registrador estiver com um valor diferente de 0000, clique apenas no botão "Reset" para "limpar" o emulador. A seguir, clique no botão "Rodar". O programa deve pedir 5 valores hexadecimais numa janela própria. Digite o valor desejado e depois no botão "Ok". Cada vez que você repetir esta operação, o painel "Entrada" indicará os valores escolhidos.
Depois do quinto número, o programa indica a soma dos valores no painel "Saída" e avisa que encontrou uma instrução halt na linha 0014. Os registradores mostram os valores finais. Apesar de ter funcionado bem, é muito mais interessante rodar este programa passo a passo. Clique no botão "Reset" e observe: tudo é zerado e o Ponteiro de Instrução aponta novamente para o início do programa.
Agora clique no botão "Passo". A primeira instrução é executada e o registrador DX mostra o valor 0005. Continue clicando no botão "Passo" e, logo após a execução da instrução de comparação (cmp cx,dx) observe que a checkbox identificada com "Menor" está checada - ela indica o resultado da comparação que acabou de ser feita. Continue no passo a passo e acompanhe a atualização dos valores dos registradores de acordo com o andamento do programa.
Parabéns! Você acaba de programar em Assembly para o processador 886! E não pense que programar para outros processadores seja uma coisa muito diferente!
A posição que o programa ocupa na memória não influencia o seu funcionamento contanto que os endereços dos saltos estejam ajustados.
De acordo com a tabela de tempos mostrada no tópico O processador 886 do capítulo 3, os tempos medidos em ciclos de clock são os seguintes:
Instrução mov add, sub, cmp, not jmp jxx and, or -------------|----------|------------------|--------|--------|--------| reg,reg 5 7 reg,xxxx 6-7 8-9 reg,[bx] 7-8 9-10 reg,[xxxx] 8-10 10-12 reg,[xxxx+bx] 10-12 12-14 [bx],reg 7-8 [xxxx],reg 8-10 [xxxx+bx],reg 10-12 reg 6 [bx] 9-11 [xxxx] 10-13 [xxxx+bx] 12-15 [xxxx] 6-7 6-8
Vamos analisar quantos ciclos nosso programa consome para ser executado. Para isto, criaremos uma tabela com as instruções e seus respectivos consumos de ciclos de clock:
mov dx, 5 = 6-7 ciclos mov cx, 0 = 6-7 ciclos mov bx, 0 = 6-7 ciclos a: get = 1 ciclo add bx, ax = 7 ciclos add cx, 1 = 8-9 ciclos cmp cx, dx = 7 ciclos jb a = 6-8 ciclos mov ax, bx = 5 ciclos put = 1 ciclo halt = 0 ciclos
É preciso lembrar que o loop passa pelo código 5 vezes portanto, o total de ciclos dentro do loop precisa ser multiplicado por 5: no mínimo (1 + 7 + 8 + 7 + 6) * 5 = 145 e no máximo (1 + 7 + 9 + 7 + 8) * 5 = 160. Os ciclos consumidos fora do loop são no mínimo 6 + 6 + 6 + 5 + 1 = 24 e no máximo 7 + 7 + 7 + 5 + 1 = 27. Portanto, para rodar nosso programa precisamos de no mínimo 145 + 24 = 169 ciclos e no máximo de 160 + 27 = 187 ciclos. Afinal de contas, quantos ciclos são realmente necessários para executar o programa?
Basta lembrar da história do número de bytes de uma instrução e dos endereços de memória pares e ímpares. Tomemos como exemplo a primeira instrução, mov cx, 0. O código operacional tem 1 byte e a constante é de dois bytes (porque nosso 886 é um processador de 16 bits). Se a constante cair num endereço par, o processador é capaz de buscá-la num único ciclo de clock; se a constante cair num endereço ímpar, o processador vai precisar de dois ciclos de clock para obtê-la. Como o endereço inicial do nosso programa é 0000, basta verificar as posições das constantes das primeiras três instruções:
0000: opcode mov 0001: 05 00 <= endereço ímpar = 7 ciclos 0003: opcode mov 0004: 00 00 <= endereço par = 6 ciclos 0006: opcode mov 0007: 00 00 <= endereço ímpar = 7 ciclos
As outras duas instruções cujos ciclos podem variar são add cx,1 e mov ax,bx. Analisando suas posições verificamos que:
000B: opcode add 000C: 01 00 <= endereço par = 8 ciclos ... 000F: opcode ja 0010: 09 00 <= endereço par sem cálculo = 6 ciclos
Agora temos o número exato de ciclos do nosso programa: 7 + 6 + 7 + ((1 + 7 + 8 + 7 + 6) * 5) + 5 + 1 + 0 = 20 + 145 + 6 = 171.
0001: opcode mov 0002: 05 00 <= endereço par = 6 ciclos 0004: opcode mov 0005: 00 00 <= endereço ímpar = 7 ciclos 0007: opcode mov 0008: 00 00 <= endereço par = 6 ciclos 000A: get 1 * 5 = 5 ciclos 000B: add bx,ax 7 * 5 = 35 ciclos 000C: opcode add 000D: 01 00 <= endereço ímpar = 9 * 5 = 45 ciclos 000F: cmp dx,cx 7 * 5 = 35 ciclos 0010: opcode jb 0011: 00 0A <= endereço ímpar sem cálculo = 7 * 5 = 35 ciclos 0013: mov ax,bx 5 ciclos 0014: put 1 ciclo 0015: halt 0 ciclos TOTAL = 6 + 7 + 6 + 5 + 35 + 45 + 35 + 35 + 5 + 1 = 180 ciclos O deslocamento de endereço tornou o programa mais lento. Ao invés de 171 ciclos, irá precisar de 180.
Como vimos, o SIMx86 é uma "poderosa" ferramenta de programação. O conjunto de instruções do 886 é pequeno mas, sabendo usá-lo, podemos programar coisas muito interessantes e o emulador permite que os programas sejam executados. Use a imaginação e mãos à obra! A seguir, algumas idéias:
mov bx, 1000 a: get mov [bx], ax add bx, 2 cmp ax, 0 jne a mov cx, bx mov bx,1000 mov ax, 0 b: add ax, [bx] add bx, 2 cmp bx, cx jb b put halt
Além das instruções GET e PUT, os processadores x86 permitem E/S mapeada para a memória. Sistemas com E/S mapeada para a memória usam posições de memória como interface para dispositivos externos. o SIMx86 permite até 8 dispositivos externos: quatro LEDs e quatro interruptores. As localizações de memória de 0FFF0h a 0FFF6h correspondem aos interruptores e as posições de memória de 0FFF8h a 0FFFEh correspondem aos quatro LEDs.
Lembre-se de que o x86 é de 16 bits. Portanto, 0FFF0h/0FFF1h corresponde ao primeiro interruptor, 0FFF2h/0FFF3 ao segundo e assim por diante. Se, por exemplo, a posição de memória 0FFF2h contiver 0 (zero), é porque o interruptor está desligado; se contiver 1, é porque está ligado. O mesmo ocorre com os LEDs. Se a posição de memória correspondente a um LED contiver 0 (zero), o LED está desligado, se contiver 1, o LED está ligado.
mov ax, 1 ; Valor para aceso mov bx, FFF8 ; Endereço do primeiro LED mov [bx], ax ; Acende add bx, 2 ; Vai para o endereço seguinte (FFFA) mov [bx], ax ; Acende halt
mov ax, 0 ; Valor para desligado mov bx, FFEE ; Endereço inicial - 2 mov cx, FFFE ; Último endereço a: add bx, 2 ; Ajusta o endereço para o próximo dispositivo mov [bx], ax ; Desliga cmp bx, cx ; É o último endereço? jb a ; Não, então repete o loop halt ; Sim, termina o programa
Agora, para fechar com chave de ouro, tente criar um programa que responde acendendo ou apagando os três primeiros LEDs de acordo com a posição dos três primeiros interruptores. Reserve o interruptor mais da direita para terminar o programa.
mov ax, 0 ; Vamos desligar tudo (veja acima) mov bx, FFEE mov cx, FFFE a: add bx, 2 mov [bx], ax cmp bx, cx jb a ; Todos os dispositivos zerados mov dx, ax ; Guarda zero em DX que é o último interruptor desligado b: mov bx, FFF0 ; Endereço do primeiro interruptor mov cx, FFF6 ; Endereço do último interruptor c: mov ax, [bx] ; Pega status do interruptor mov [8+bx], ax ; Passa para o LED correspondente add bx, 2 ; Avança endereço cmp bx, cx ; Passou do último interruptor? jbe c ; Não, vai para o próximo e repete o loop cmp ax, dx ; Sim, então verifica se o último interruptor foi ligado je b ; Se continua zerado, rastreia novamente halt ; Se foi ligado, termina o programa
mov ax, 0 ; Desligar todos os dispositivos mov bx, FFEE mov cx, FFFE a: add bx, 2 mov [bx], ax cmp bx, cx jb a ; Tudo desligado get ; Pede o primeiro valor mov bx, ax ; Transfere para BX get ; Pede o segundo valor and ax, bx ; AND dos dois valores put ; Põe resultado na saída mov bx, FFF8 ; Endereço do primeiro LED mov [bx], ax ; Aciona o LED com o resultado halt
mov ax, 0 ; Desligar todos os dispositivos mov bx, FFEE mov cx, FFFE a: add bx, 2 mov [bx], ax cmp bx, cx jb a ; Tudo desligado get ; Pede o primeiro valor mov bx, ax ; Transfere para BX get ; Pede o segundo valor or ax, bx ; OR dos dois valores put ; Põe resultado na saída mov bx, FFFA ; Endereço do segundo LED mov [bx], ax ; Aciona o LED com o resultado halt
mov ax, 0 ; Desligar todos os dispositivos mov bx, FFEE mov cx, FFFE a: add bx, 2 mov [bx], ax cmp bx, cx jb a ; Tudo desligado get ; Pede o primeiro valor mov bx, ax ; Transfere para BX get ; Pede o segundo valor and ax, bx ; AND dos dois valores not ax ; NOT do resultado put ; Põe resultado na saída mov bx, FFFC ; Endereço do terceiro LED mov [bx], ax ; Aciona o LED com o resultado halt
O conjunto de instruções do x86 é bastante restrito. Não possui, por exemplo, instruções de multiplicação e de divisão. Apesar disso, esta falha aparente não prejudica a nossa programação.
mov ax, 0 ; Desligar todos os dispositivos mov bx, FFEE mov cx, FFFE a: add bx, 2 mov [bx], ax cmp bx, cx jb a ; Tudo desligado get ; Pede o primeiro valor mov cx, ax ; Guarda em CX get ; Pede o segundo valor mov dx, ax ; Guarda em DX mov ax, 0 ; Zera o acumulador AX mov bx, 0 ; Zera BX para comparar com contador cmp cx, ax ; Compara o primeiro valor com zero je c ; Se for zero, põe resultado zero b: add ax, dx ; Acumula o segundo valor sub cx, 1 ; Decrementa o contador cmp cx, bx ; Compara o contador com zero ja b ; Se for maior, acumula novamente c: put ; Mostra o resultado de AX and ax, 1 ; Isola o último bit de AX ; (0 = par e 1 = ímpar) cmp ax, bx ; Compara com zero je d ; Se zero (par), salta para d: mov bx, FFFA ; Põe endereço do segundo LED em BX mov [bx], ax ; Liga o segundo LED (AX = 1) halt ; Termina o programa d: mov bx, FFF8 ; Põe endereço do primeiro LED em BX mov ax, 1 ; Põe 1 em AX mov [bx], ax ; Liga o primeiro LED halt ; Termina o programa
mov ax, 0 ; Desligar todos os dispositivos mov bx, FFEE mov cx, FFFE a: add bx, 2 mov [bx], ax cmp bx, cx jb a ; Tudo desligado get ; Pede o valor mov cx, 0 ; Zera o contador cmp ax, cx ; Compara o valor com zero je c ; Se for zero, apresenta o resultado mov dx, 3 ; Põe o divisor 3 em DX b: cmp ax, dx ; Compara o valor com 3 jb c ; Se for menor, apresenta o resultado sub ax, dx ; Se for maior, faz a subtração add cx, 1 ; e incrementa o contador jmp b ; Repete a operação c: mov dx, ax ; Passa o resto para DX mov ax, cx ; Põe contador em AX put ; Apresenta o resultado da divisão mov ax, dx ; Põe o resto em AX put ; Apresenta o resto mov ax, 1 ; Põe 1 em AX para ligar o LED mov bx, 0 ; Zera BX para comparar com o resto cmp dx, bx ; Compara o resto com zero ja d ; Se o resto for maior do que zero ; acende o último LED mov bx, FFF8 ; Se resto = 0, põe o endereço do ; primeiro LED em BX mov [bx], ax ; Liga o primeiro LED halt ; Termina o programa d: mov bx, FFFE ; Põe o endereço do último LED em BX mov [bx], ax ; Liga o último LED halt ; Termina o programa
Deu para se divertir? Espero que sim. Já deu para perceber que a linguagem Assembly não é um bicho de sete cabeças. Pelo menos quando se trata do nosso processador hipotético, o 886 de 16 bits e quando podemos contar com o SIMx86. Brinque até não poder mais. Quanto mais você programar e testar nesta fase, mais preparado vai estar para os próximos assuntos.