sábado, 10 de dezembro de 2011

Desmistificando a linguagem Assembly

Olá programadores de linguagens alto-nível e altíssimo-nível como Java, C++, Visual Basic, Delphi e outras.

Este artigo é dedicado a vocês que pensam que Assembly é coisa de nerd, doido, que só os fodões programam em Assembly e que essa linguagem é um bicho de sete cabeças que dá até medo só de pensar. Algumas pessoas quando se fala em Assembly já imaginam algo místico, algo misterioso, outras começam a imaginar bits e bytes sendo escovados à torto e à direito. Mas a verdade é que poucas pessoas sabem exatamente o que é isto que chamamos de "linguagem Assembly" e tampouco como funciona essa bagaça.

Pois bem, Fernando Aires Castello está aqui pra botar tudo em pratos limpos e desmistificar de uma vez por todas a linguagem Assembly, e provar que programar em Assembly é algo que qualquer um pode fazer, contanto que entenda como funciona o processo.

Neste artigo eu vou apresentar os conceitos das linguagens Assembly em geral, explicar o processo de desenvolvimento de um pequeno programa em Assembly, etc. Mas então chega de papo furado e vamos ao que interessa!


Observação:

A explicação sobre Assembly neste artigo é apenas superficial e simplística. Para ter um conhecimento mais aprofundado, leia outras fontes. A idéia aqui é justamente apresentar o Assembly da forma mais simples possível, para que você perca o medo e vá atrás de informações mais detalhadas!






O que é Assembly?


Assembly não é uma única linguagem de programação, é uma família de linguagens de programação relacionadas entre si. Linguagem Assembly é como chamamos a forma de representar as operações de um determinado microprocessador, que se dá através de mnemônicos. Mnemônicos são palavras que ajudam o programador a lembrar o que exatamente as operações fazem. Chamamos de instruções o conjunto dos mnemônicos usados para representar o conjunto de operações de um determinado processador. Por exemplo, existe o conjunto de instruções do processador Pentium, do Z80, do ARM, etc. e cada conjunto de instruções de cada processador é diferente um do outro. Por exemplo, a instrução do Z80 chamada LD (load) é equivalente à instrução do x86 (muitos processadores da Intel são x86) chamada MOV (move), sendo que ambas fazem a mesma coisa mas cada uma é específica ao processador para o qual elas foram definidas.

Quando falamos em programação Assembly, é importante definir de qual processador estamos falando. Assembly é algo genérico porque existem milhares ou milhões de modelos de microprocessadores por aí, e algumas linguagens Assembly são completamente diferentes de outras. Neste artigo, eu vou falar mais das linguagens Assembly do processador Z80 (da empresa Zilog), porque é com este processador que eu tenho mais familiaridade.

As instruções Assembly são palavras que representam operações elementares dos processadores, isto é, geralmente cada instrução Assembly faz uma única operação como somar um número com outro, executar alguma subrotina, armazenar um valor em um registrador, etc. Mas estas palavras apenas substituem a pura linguagem de máquina. A linguagem de máquina nada mais é do que um conjunto de bits que o processador "entende" diretamente. Cada conjunto de bits manda o processador executar alguma operação. Esse conjunto de bits (que pode ser 1011, 0111, 1010, 1100, etc) é difícil de ser lembrado, e por isso mesmo existe a linguagem Assembly, que mapeia cada conjunto de bits a uma palavra que seja de fácil aprendizado pelos programadores.

Por exemplo, no processador Z80 existe uma instrução Assembly chamada HALT, que na verdade representa a instrução de máquina 1110110 (76 em hexadecimal). O que é mais fácil, lembrar a instrução HALT ou o byte 1110110? Não há dúvidas quanto a isso.

Importante frisar que um computador não executa comandos Assembly diretamente. O processador Z80 não entende uma instrução HALT por exemplo. É necessário que um programa especial chamado Assembler traduza o código Assembly em código de máquina. Ou seja, um Assembler precisa traduzir o HALT para 1110110, porque é só isso que o processador consegue "entender" e executar. Falaremos mais sobre Assemblers nas próximas seções.


Por que programar em Assembly?


Porque não existe nenhuma lei no mundo que proíba você de escrever programas em Assembly. Mas falando sério, antigamente era só isso que existia. Para desenvolver um software, seja lá qual fosse, era preciso meter a mão em bits e bytes porque ainda não existia BASIC, nem C, nem C++ e nem Java.

Sim, houve uma época, nos primórdios da computação, em que era preciso usar Assembly para programar uma máquina qualquer, e não apenas computadores. Hoje em dia, com tantas linguagens de programação de alto-nível, tantos frameworks, tanta coisa já vem pronta, que você deve imaginar "mas quem no mundo vai usar Assembly, se tem tanta coisa pronta em outras linguagens que só preciso juntar com o meu programa e criar um sistema completo?!". Pois bem, dependendo do tipo de aplicação, e para a grande maioria das aplicações, Assembly é totalmente dispensável em favor de C, C++, Java, Visual Basic, Delphi, entre outras linguagens de alto-nível. Mas se você pensa que o Assembly é uma linguagem "morta", saiba que o ela ainda reina em um nicho do mundo da programação. E sabe qual é o lugar de Assembly hoje em dia, e onde ela ainda reina triunfante? A resposta é Sistemas Embarcados.

Um sistema embarcado é um sistema completo que roda dentro de um dispositivo e é especializado em uma determinada função ou grupo de funções intimamente relacionadas. Uma forma de mostrar a diferença entre um sistema embarcado e um não embarcado é que por exemplo, o seu Windows não é considerado um sistema embarcado porque ele serve pra tudo quanto é tarefa: escutar música, assistir vídeos, programar, desenhar, ler, escrever, gravar CDs, navegar na Internet, vender, fazer compras, estudar, namorar, conversar, fazer amizades, etc. Um controle remoto tem um sistema embarcado. Ele só serve pra controlar a televisão, DVD ou seja lá qual dispositivo ele controla. Um controle remoto de um DVD tem funções de ligar, desligar, abrir o drive, mostrar o menu, aumentar o volume, etc. ou seja, funções intimamente relacionadas com a operação do aparelho de DVD. Dispositivos como controles remoto e celulares precisam de sistemas que funcionem 24 horas por dia 7 dias por semana, sem erros e sem precisar de manutenção freqüente, sem falar que precisam gastar o mínimo de energia possível. Nestes casos, a linguagem Assembly fornece um controle muito maior do hardware do dispositivo em questão, permitindo o desenvolvimento completo do sistema com todas as funcionalidades que o dispositivo precisa.

Geralmente quem gosta de programar gosta de videogames. Pois saiba que muitos programas desenvolvidos em Assembly incluem praticamente TODOS os videogames como Atari, Nintendo, Master System, Super Nintendo, Mega Drive, e praticamente todos os outros videogames de 8 e 16 bits. Se você souber programar em Assembly dos respectivos processadores e conhecer as respectivas arquiteturas, você pode programar jogos.

Em Assembly você tem o controle total da máquina, seja lá qual for a máquina em questão. Assembly é tão baixo-nível que você pode ver e mexer nas entranhas da máquina, você pode colocar um bit aqui e outro ali, e ver o que acontece. Você pode enviar um byte para a porta tal e ler outro byte da porta tal e executar funções elementares do dispositivo. É o controle total da máquina nas suas mãos, sem as limitações de qualquer linguagem alto-nível.

Com Assembly, a máquina é toda sua, e faz com ela o que você bem entender. Ta aí. Esse é o motivo que você precisa para querer aprender a programar em Assembly. Só por isso.


O que é um Assembler?


Um processador não entende a linguagem Assembly. Ele só entende código binário, que são os "zeros e uns". Para transformar o seu programa Assembly em código binário, existem programas especiais chamados Assembler. Cada processador possui um Assembler diferente pois cada linguagem Assembly de cada processador precisa ser traduzida para o código binário nativo do processador. Por isso um Assembler que gera código binário para o processador Z80 não serve para gerar código binário para um processador ARM ou Pentium, por exemplo.

Por esses motivos, quando você pensar em progamar em Assembly, é necessário obter um programa Assembler que possa gerar o código binário nativo do processador usado pelo computador no qual você deseja executar o programa.

Importante comentar que quando você programa em certas linguagens de alto-nível como C ou C++, no final das contas o compilador do C e C++ vai traduzir o seu código alto-nível em código de máquina do seu computador. E em essência, qualquer linguagem de programação precisa ter um compilador ou interpretador que traduza a linguagem para o código binário, não importa se é C++, Java, Visual Basic ou C#. O compilador de Java por exemplo traduz o código Java para um código intermediário, que é o Bytecode. Isso não é código de máquina propriamente dito. Isso é um código que a JVM (máquina virtual de Java) entende. A JVM não é o processador do computador. Mas no final das contas quem executa o seu programa seja lá em qual linguagem ele foi escrito, é o processador real, e este só entende 0 e 1.


O que exatamente são as instruções Assembly?


As instruções Assembly como citei anteriormente são mnemônicos, palavras que ajudam o programador a lembrar as operações que um processador pode executar.

Instruções Assembly são em geral pequenas, contendo geralmente 2, 3 ou 4 letras no nome (como tipicamente no caso do Z80), mas podem ser bem maiores dependendo do processador em questão. Estas palavras são geralmente abreviações de verbos em inglês, e são suficientes para lembrar o que elas significam. Veja alguns exemplos do Z80:

  • LD = Load (carregar), "carrega" um valor de um lugar a outro.
  • CALL = Call (chamar), chama uma subrotina a partir de algum endereço de memória.
  • JP = Jump (pular), "pula" para executar código a partir de algum endereço de memória.

Algumas instruções Assembly precisam ter um ou mais operandos para poderem funcionar. Outras funcionam por si próprias, ou porque elas não precisam de nenhum operando, ou porque o operando é implícito. Falaremos mais sobre esse assunto nas próximas seções.

Algumas instruções Assembly operam sobre endereços de memória, armazenando ou lendo valores (bits, bytes, palavras, etc), ou sobre registradores, ou ambos. Falaremos mais sobre endereços de memória, bits, bytes e registradores nas próximas seções.


O que são os bits e bytes?


Um bit é a menor unidade de armazenamento de um computador. Você pode pensar num computador e tudo o que ele faz como uma movimentação de bits e bytes pra tudo quanto é lado, porque em essência, todo e qualquer programa de computador se resume a isso: movimentação de dados, e dados, independente de qualquer formato, são compostos por bits, que é só o que o processador (cérebro do computador) consegue "entender".

No mundo da computação um bit é representado por 0 ou 1, sendo que 0 significa que o bit está "apagado" ou desativado, e 1 significa que o bit está "aceso" ou ativado. Ou seja, um bit só pode ter dois valores ou assumir dois estados distintos: ativado ou "aceso" e desativado ou "apagado". Em essência, os processadores entendem os bits da seguinte forma: o bit "apagado" ou 0 nada mais é do que a ausência de pulso elétrico, e o bit "aceso" ou 1 nada mais é do que a presença de pulso elétrico. É mais ou menos assim que funciona. Para maiores informações sobre isso, estude Eletrônica.

Se pensarmos em uma cadeia arbitrária de bits, alguns acesos e outros apagados, eles podem ser interpretados de alguma forma pelo computador. Imagine a seqüencia de bits 101110100100001011110. Para nós isso não significa absolutamente nada. Agora pense em um processador "lendo" e interpretando essa mesma seqüencia de bits. Dependendo de qual processador estamos considerando, ele vai executar alguma(s) operação(ões) que corresponde(m) a esta seqüência de bits. Eventualmente essa cadeia de bits pode ter um nome que explique o que o processador faz quando interpreta ela. Daí surgem as instruções Assembly para traduzir os "zeros e uns", para que a gente entenda o que eles fazem.

Um byte nada mais é do que uma seqüencia de 8 bits. Ou seja, 00011100 é um byte. 11101010 é outro byte. E 11110111 é outro. Sempre que você vir um conjunto de 8 bits, pode interpretar como sendo 1 byte. Em geral, programadores Assembly preferem utilizar a notação hexadecimal ou decimal para se referir a bytes. Ao invés de 11111111, é mais fácil escrever e falar FF (que é 11111111 em hexadecimal) ou 255 (em decimal). Ao invés de 10101010, é mais fácil AA (hex) ou 170 (dec). Em vez de 11010010, é mais simples D2 (hex) ou 210 (dec), e assim por diante. De agora em diante, quando eu for falar de bytes, vou me referir a eles usando a notação hexadecimal ou decimal. Importante lembrar que um byte só pode conter um valor entre 0 e 255 (decimal), entre 00 e FF (hexadecimal), ou entre 00000000 e 11111111 (binário).


O que são os endereços de memória?


Os programadores precisam de algum lugar para armazenar dados. Quando se programa em linguagens de alto-nível como C++ ou Java, dizemos que armazena-se dados em variáveis e objetos. Mas em essência, as variáveis e objetos nada mais são do que endereços na memória do computador, aos quais damos um nome, que é o nome da variável ou do objeto. Os valores que colocamos nas variáveis e objetos nada mais são do que seqüencias de bits e bytes localizados a partir do endereço de memória ao qual o nome da variável ou objeto se refere. Considere o exemplo a seguir:

Cria-se uma variável X. Esse X na verdade é um nome que damos para um endereço de memória. Esse endereço de memória quem escolhe é o gerenciador de memória do sistema operacional. Ele vai procurar o bloco de memória que estiver livre (que não estiver sendo ocupado com dados de outro processo) e vai fazer com que o endereço de memória que está logo no início deste bloco tenha o nome de X, assim você pode fazer X = 10, e o valor 10 vai parar lá naquele bloco de memória que foi alocado (isto é, reservado pelo sistema). O tamanho do bloco de memória corresponde ao tamanho do tipo de dado que foi definido. Se você definir que X é um inteiro, em geral inteiros ocupam 4 bytes. Então o sistema acaba alocando um bloco livre na memória que contenha 4 bytes.

Nos computadores atuais, geralmente endereços de memória são representados por dois bytes. Quando programadores se referem a endereços de memória, utiliza-se a notação hexadecimal. Ou seja, D000 é um endereço de memória. F3B1 é outro endereço. ABCDE é outro endereço.

Para armazenar dados quando se programa em Assembly, é importante saber onde armazená-los na memória. Atualmente, a maioria dos sistemas operacionais modernos possuem um mecanismo chamado de proteção de memória. O objetivo deste mecanismo é prevenir um processo (que pode ser o seu programa Assembly) de acessar memória que foi alocada (reservada pelo sistema) para algum outro processo que estiver executando no momento (que pode ser outro programa qualquer, inclusive o próprio sistema!). Imagine se você pudesse meter o dedo em memória que não te pertence? Pois bem, milhares de bugs poderiam ocorrer, uma vez que se você vai armazenar dados em uma região da memória que estiver sendo usada por outro programa, esses dados que você armazenar iriam sobrescrever o que o outro programa havia armazenado, e daí você pode imaginar que a briga seria feia. Muito feia. Existem diversas formas empregadas pelos sistemas operacionais para proteger a memória. Algums formas comuns são segmentação de memória, virtualização de memória, chaves de proteção, paginação, etc. Mas enfim, esse tópico está além do escopo deste artigo. Para mais informações pesquise sobre proteção de memória no Google.

Por motivos de simplicidade, para facilitar a discussão sobre programação Assembly em geral, vou falar sobre o processo aplicado aos antigos computadores MSX que rodam com o processador Z80. No MSX, um endereço de memória qualquer só pode ter 2 bytes e ir de 0 (0 em hexadecimal) até 65535 (FFFF em hexadecimal).

As linguagens Assembly permitem manipular endereços de memória para armazenar e transferir dados do programa. Por exemplo, no Z80 dos computadores MSX, existe a instrução LD que serve para armazenar dados em registradores, em endereços de memória, ou tranferir dados entre registradores e endereços de memória, como na seguinte linha de código Assembly Z80, que armazena no endereço D000 o valor que estiver no registrador A: LD (D000), A


O que são registradores?


Registradores são unidades de armazenamento de um processador. Quando você programa em linguagens de alto-nível, geralmente não é possível manipular os registradores do processador. Mas em Assembly, essa limitação simplesmente desaparece! Em Assembly você não só pode como precisa utilizar os registradores para programar. Dependendo do processador em questão, pode existir 1, 2, 3, 4, 5, ou muito mais registradores, cada um com seu nome.

Por exemplo, o Z80 contém vários registradores de 8 e 16 bits: A, B, C, D, E, F, H, L, A', B', C', D', E', H', L', I, R, IX, IY, SP e PC. Alguns podem ser acessados diretamente pelo programador, outros só através de determinadas operações, outros são somente para leitura, outros são "flags" (indicam o resultado de alguma operação), e alguns registradores de 8 bits podem ser combinados para formar registradores de 16 bits, como BC (B e C), DE (D e E) e HL (H e L). Em geral, registradores de 8 bits (1 byte) são usados para armazenar valores que serão usados em cálculos. Os registradores de 16 bits (2 bytes) são geralmente usados para armazenar endereços de memória.

As linguagens Assembly utilizam os registradores para armazenar e transferir dados usados pelo programa. Confira diversos exemplos de uso dos registradores do processador Z80 nas linhas a seguir (não se preocupe com a sintaxe, porque a sintaxe da linguagem Assembly é específica para cada processador diferente!):


Armazena no registrador A o valor FF (255 em hexadecimal):


LD A, FF


Armazena no endereço D000 o valor que estiver no registrador A:


LD (D000), A


Armazena no registrador A o valor que estiver no registrador R:


LD A, R


Armazena no registrador A o valor que estiver no endereço de memória armazenado no registrador BC (isto é, se o registrador BC contiver o endereço D000 e o endereço D000 contiver o valor FF, então A vai receber o valor FF):


LD A, (BC)


Armazena no registrador HL o endereço D000 (como HL é a combinação dos registradores H e L, neste caso H contém D0 e L contém 00):


LD HL, D000


Armazena no registrador HL o endereço que estiver no endereço D000 (como HL é a combinação dos registradores H e L, então neste caso H contém o byte armazenado no endereço D001 e L contém o byte armazenado em D000. Quem recebe o primeiro byte é sempre o segundo registrador do par, ou seja, neste caso, L):


LD HL, (D000)



Hello World em Assembly do Z80 (computadores MSX)


Ainda tá difícil de entender a moral da história? Vou tentar explicar com um exemplo. Mas atenção, infelizmente o meu conhecimento sobre programação Assembly se resume a programação do processador Z80 em máquinas da linha MSX. O MSX é uma antiga linha de computadores pessoais de 8 bit da década de 70 e 80, foi nele que eu comecei a programar.

Imagine um programa que escreve uma mensagem, como um "Hello World" na tela de um computador MSX. O que precisamos para fazer isso em Assembly (do Z80, no MSX) da forma mais simples possível? Precisamos de uma região de memória para armazenar a frase "Hello World", e de algumas rotinas da BIOS do MSX, uma que inicializa a tela e outra que transfere dados da memória diretamente para a tela. Confira o código completo e a seguir a explicação detalhada:


;--------------------------------------------
; PROGRAMA HELLO WORLD, PARA O MSX
;--------------------------------------------

.org 8000h
.start 8000h
.rom

INITXT equ 050eh
LDIRVM equ 0744h

INICIO:

call INITXT
ld BC, 12
ld DE, 0
ld HL, TEXTO
call LDIRVM

FIM:

jp FIM

TEXTO:

db "Hello World!"


Para compilar este código, foi necessário usar o programa assembler ASMSX, que traduz código Assembly Z80 para código nativo do Z80 nos computadores MSX. Agora a explicação do passo-a-passo:

O programa inicia com um bloco de comentário, onde cada linha de comentário começa por um ponto-e-vírgula (;). Comentários são ignorados pelo assembler e só servem para nós humanos lerem.

Em seguida, vem três palavras que iniciam por um ponto (.), estas são diretivas do assembler. Diretivas não são compiladas para código de máquina. Elas servem só para passar algumas informações para o assembler. A diretiva .org serve para indicar a partir de qual endereço de memória do computador MSX o programa vai ser colocado, neste caso, o programa será colocado a partir do endereço 8000h (o "h" no final indica que a notação é hexadecimal). A diretiva .start indica que o programa vai começar a executar a partir do endereço 8000h, ou seja, desde o início. Existem situações onde o segmento de dados vêm no início do programa e o código executável vem depois, e para isso existe essa diretiva, pois se os dados viessem logo no início do programa, o programa em si deveria executar a partir de outro endereço e não do endereço inicial. Como no caso deste programa o segmento de dados vem lá no final, então o endereço inicial da execução do programa coincide com o endereço inicial do código em si. Por fim, a diretiva .rom apenas indica para o assembler ASMSX que ele deve gerar um arquivo ROM que contém um cabeçalho típico das ROMs do MSX seguido do código binário do Z80.

Logo a seguir, vem duas diretivas EQU. Estas diretivas servem para criar constantes e contém do lado esquerdo um identificador (isto é, um nome), e do lado direito um valor, que neste caso é um endereço de memória. Basicamente, estamos dizendo neste caso que INITXT é um nome que será substituído no código pelo endereço de memória 050e (em hexadecimal). Este endereço pertence ao BIOS específico das máquinas MSX e serve para inicializar a tela no modo texto de 40x25 caracteres. A outra EQU diz que o nome LDIVRM refere-se ao endereço 0744 (em hexadecimal). Este endereço também pertence ao BIOS específico dos computadores MSX e serve para transferir um bloco de dados da memória RAM para a VRAM (que é a memória de vídeo nos MSX).

Após definir as constantes necessárias, vem a palavra "INICIO:". Qualquer palavra seguida de dois-pontos (:) indica para o assembler ASMSX que tal palavra trata-se de um rótulo. Um rótulo é um nome para um endereço de memória qualquer. Neste caso esta linha indica para o assembler que INICIO é o nome do endereço de memória (não importa exatamente qual endereço) a partir do qual o código a seguir se encontra. A palavra pode ser qualquer uma, não necessariamente "INICIO". Apenas coloquei este nome porque ela indica onde eu comecei a escrever o código do programa em si.

Abaixo do rótulo INICIO, que neste caso indica onde é o início do programa propriamente dito, vem o código Assembly. A instrução CALL invoca a subrotina (um trecho de código Assembly) que se encontra a partir do endereço de memória especificado. Neste caso, o endereço foi especificado através de uma constante, a INITXT. Na hora da compilação, o assembler substitui esta palavra pelo endereço real (que defini como 050e logo no início do programa, lembra?). Ao ser invocada, esta subrotina da BIOS do MSX inicializa a tela no modo texto de 40x24 caracteres, para que possamos escrever na tela em seguida.

A seguir, vem três instruções LD. As instruções LD do Z80 servem para transferir dados de um lugar a outro. Neste caso, elas estão transferindo bytes e endereços de memória para os registradores BC, DE e HL:

  • LD BC, 12 = insere o byte 12 (decimal) no registrador BC.
  • LD DE, 0 = insere o byte 0 no registrador DE.
  • LD HL, TEXTO = insere o endereço do rótulo TEXTO no registrador HL.

Mas qual é a moral disso tudo? Bom, para escrever na tela de um computador MSX usando a função LDIRVM da BIOS, é necessário informar alguns parâmetros. Para passar parâmetros para subrotinas ou funções, é necessário usar os registradores. No caso da subrotina LDIRVM, ela requer que o número de caracteres do texto esteja armazenado no registrador BC, e o texto "Hello World!" tem 12 caracteres. Ela também requer que a posição do texto esteja armazenada no registrador DE, e quero que o texto esteja logo no topo da tela então armazenei 0. Além disso, a função LDIRVM requer que o texto a ser escrito na tela esteja armazenado a partir da posição de memória armazenada no registrador HL, por isso inseri o endereço do rótulo TEXTO, pois é a partir deste rótulo que defini o texto "Hello World!".

Depois de usar os registradores BC, DE e HL para armazenar os parâmetros a serrem passados para a função da LDIVRM da BIOS do MSX, precisamos só invocar a função. Isso se consegue através da instrução CALL seguida do endereço de memória da função, por isso escrevi a linha "call LDIVRM". Pronto, isso trata de escrever "Hello World!" na tela de um computador MSX. Simples assim.

Quer ver o resultado na tela do MSX? Dá uma olhada no screenshot, é isso que aparece na tela de um computador MSX ao executar o código binário gerado a partir desse programa Assembly:


O código binário gerado por esse programa Assembly faz isso no MSX:




O rótulo FIM só serve para que eu saiba onde termina o programa. A única instrução deste rótulo é a JP, que serve para "pular" para o endereço de memória especificado e executar o código a partir de lá. Neste caso, a instrução JP "pula" para o próprio rótulo onde ela está! Efetivamente, isto leva a um loop infinito onde o processador fica executando sempre a mesma instrução e "trava". Mas no caso dos computadores MSX, isso não é problema, pois em geral programas em ROMs fazem isso mesmo. Em um computador MSX real, caso quizesse encerrar o programa, bastaria "retirar" o cartucho ROM e o computador reiniciaria normalmente. Em um emulador, bastaria iniciar a emulação sem abrir o arquivo ROM contendo a imagem binária do cartucho (que é o arquivo gerado pelo assembler ASMSX no meu caso).

O que aparece no final do programa, após o rótulo TEXTO, é mais uma diretiva do ASMSX, a diretiva DB, que serve para definir bytes. Quando escrevemos DB "Hello World!" estamos solicitando para o assembler colocar exatamente estas letras entre aspas dentro do arquivo ROM, e são exatamente estas letras que o programa escreve na tela, porque foi o rótulo TEXTO que passei como parâmetro para a função LDIVRM, através do registrador HL, lembra?

Quer ver o arquivo ROM gerado pelo ASMSX a partir do código Assembly? Dá só uma olhada (clique na imagem para ampliar. É o screenshot tirado do programa HxD, que permite analizar o código binário dentro de um arquivo qualquer). Cada byte nesse arquivo corresponde ou a uma instrução em código de máquina do Z80, ou a dados do programa, ou ao cabeçalho gerado pelo ASMSX (os primeiros bytes do arquivo contém o cabeçalho, o AB que aparece logo no início é o número mágico ou assinatura dos arquivos ROM do MSX, e em seguida dois bytes contém o endereço inicial do programa (00 80, aparece invertido mas na verdade o endereço inicial é 8000, lembra?).


Arquivo ROM criado pelo ASMSX contendo o código binário gerado a partir desse programa:




Enfim, um simples "Hello World" é assim em Assembly, da forma mais simples possível, num computador MSX. Este código só roda em computadores MSX e em nenhum outro. Daí uma das maiores desvantagens da programação Assembly: código gerado para um determinado processador (no caso deste programa, para um Z80), não funciona em nenhum outro processador, a não ser que o outro processador possua uma arquitetura equivalente. No caso deste programa, uma vez que utilizei chamadas a funções da BIOS do MSX, ele não funciona em nenhum outro computador, mesmo que o outro computador utilize o mesmo processador. Isto porque as rotinas da BIOS do MSX são específicas deste tipo de máquina.


Assembly em outros computadores


É complicado falar sobre programação Assembly de forma geral, uma vez que cada linguagem Assembly é específica a um determinado processador como o Z80 ou a um grupo de processadores intimamente relacionados como os da linha x86 (como o Celeron ou o Pentium). Falar de programação Assembly em geral é falar sobre uma determinada arquitetura, de um determinado processador, de um determinado computador ou família de computadores.

Usei o processador Z80 e o computador MSX nas explicações porque é com esse ambiente que eu tenho mais intimidade. Mas alguns conceitos (que expliquei antes do exemplo do "Hello World") se aplicam à maioria das linguagens Assembly.

Alguns conceitos sobre programação Assembly precisam ser estudados junto com a arquitetura do processador e do computador que se deseja programar. Tópicos como proteção de memória, modos de endereçamento, rotinas da BIOS, entre outros, são específicos de cada máquina e influenciam profundamente a linguagem Assembly correspondente. Caso você queira aprender a programar em Assembly para a maioria dos computadores modernos, recomendo que comece a estudar Assembly x86. O que chamamos de x86 é uma arquitetura, um conjunto de instruções Assembly que vários processadores modernos possuem em comum. Muito provavelmente, o seu PC tem um processador da linha x86 dentro dele. Você pode controlá-lo completamente se souber programar em Assembly x86.

Alguns conjuntos de instruções que a maioria das linguagens Assembly de diversos processadores possuem em comum são os relacionados a:

- Transferência de dados entre memória e registradores (ex: armazenar valor em um registrador, armazenar o valor de um registrador em outro registrador, armazenar na memória o valor de um registrador, armazenar um endereço de memória em um registrador, trocar os valores entre dois registradores, etc.)

- Entrada e saída de dados para dispositivos e periféricos (ex: enviar um byte para um modem, receber um byte do teclado, enviar um byte para a tela, para a placa de vídeo, para a placa de som, etc.)

- Fluxo de execução (ex: transferir a execução para uma subrotina, retornar de uma subrotina, transferir a execução para outro endereço de memória dependendo de alguma condição, repetir uma instrução por um determinado número de vezes, ou dependendo de alguma condição, etc.)

- Manipulação de flags (ex: verificar se a última operação aritmética resultou em zero, etc.)

- Comparações entre valores (ex: verificar se o valor em um determinado registrador é igual, maior, menor, ou diferente de algum valor indicado, etc.)

- Cálculos aritméticos (ex: somar, subtrair, multiplicar, dividir, incrementar ou decrementar valores em registradores, ou na memória, etc.)

- Funções inerentes ao próprio processador (ex: parar de executar, alterar o modo de operação, etc.)

Enfim, dê uma pesquisada no Google caso queira se aprofundar no assunto de programação Assembly. E caso haja dúvidas ou sugestões sobre este artigo, basta me enviar um e-mail ou deixar o seu recado.

Espero que tenham aproveitado o artigo!