CURSO RELÂMPAGO DE ASSEMBLY
Um dos requisitos para poder começar a pensar em criar um sistema operacional é conhecer a linguagem de programação Assembly. Isto você já sabe (se leu o módulo anterior), pois o Assembly é a melhor forma que temos de nos comunicar com o processador. Se você não tiver a mínima noção do que se trata, sugiro que se dedique um pouco ao assunto. Na Aldeia NumaBoa você encontra o tutorial Assembly para todos os gostos. Para aqueles que não querem se aprofundar no Assembly, já têm algum conhecimento da linguagem ou apenas preguiça, basta seguir este Curso Relâmpago.
Se você conseguir acompanhar e entender o texto - tudo bem, vá em frente. Se não for este o caso, sugiro que desista deste tutorial, pelo menos, por enquanto
Ferramentas
* NASM - o compilador Assembly. As versões mais atualizadas e a documentação estão disponíveis para download na página do NASM na SourceForge. A versão 0.98.38 e a versão 0.98.38 para Windows estão disponíveis aqui na Aldeia.
* HELPPC - Onde você encontra todas as referências imagináveis. Faça o download do helppc versão 2.1 aqui na Aldeia ou procure-o pela Internet.
A linguagem Assembly
Todo processador (ou CPU) possui um conjunto de instruções próprio. É como se fosse uma "linguagem" embutida dentro da CPU. Cada instrução possui um código operacional e realiza determinada tarefa dentro do processador utilizando seus registradores. Estes códigos são identificados por números. Na linguagem Assembly estes números são transformados nos chamados mnemônicos para facilitar a programação. Assim, por exemplo, o número hexadecimal B2 (178 decimal) corresponde a MOV DL em Assembly. MOV é o mnemônico de MOVer e DL é o byte baixo do registrador DX.
Os registradores
Registradores são áreas de trabalho especiais dentro do processador (CPU) que foram projetadas para trabalharem com códigos operacionais. São uma espécie de "memória" da CPU e servem para armazenar informações temporariamente.
Os registradores mais utilizados são 8, chamados de uso geral. Destes, usaremos com frequência apenas 4: AX, BX, CX e DX. Estes registradores podem armazenar 16 bits, ou seja, 2 bytes (nas máquinas de 32 bits, assim chamadas porque os registradores da sua CPU podem armazenar 32 bits, estes mesmos registradores recebem os nomes de EAX, EBX, ECX e EDX).
Não é à toa que os registradores receberam estes nomes: AX é também o Acumulador, BX é a Base, CX é o Contador e DX vem de Dados. Os nomes derivam do uso mais frequente destes registradores.
Com Assembly podemos acessar diretamente os dois bytes (2 bytes = word) dos registradores de 16 bits ou podemos acessar o byte alto (high) ou baixo (low) diretamente. Assim, por exemplo, podemos usar instruções com AX (word), AH (byte alto de AX) ou AL (byte baixo de AX), com BX, BH ou BL, etc.
É claro que existem mais registradores, mas não precisaremos deles por enquanto. Se tiver curiosidade e quiser ampliar seus conhecimentos, leia Arquitetura Intel e O que são registradores. Estes textos acessórios serão mostrados em novas janelas - feche-as para retornar a este tutorial.
A instrução MOV
Esta instrução, uma das mais usadas, copia dados de um local para outro e o menumônico deriva de MOVe. Veja alguns exemplos de uso:
mov ax, 1 ; Move 1 para AX, ou seja, AX = 1
mov bx, ax ; Move o valor de AX para BAX, ou seja, BX = AX
mov cl, 0x12 ; CL = 12 hex (CL = o byte baixo de CX)
mov ch, 0x34 ; CH = 34 hex (CH = o byte alto de CX)
mov dx, 0x3412 ; DX=3412hex, DH=34hex, DL=12hex
A instrução INT
A instrução INT gera uma interrupção de software. O DOS e a BIOS fornecem muitas funções úteis que podem ser acessadas através desta instrução:
INT 0x10 ; chama uma interrupção 10hex de software
Um pequeno programa
Este é um programa extremamente simples:
mov ax, 0x4C00 ; AH = 0x4C, AL=0
int 0x21 ; a interrupção de software 0x21 é chamada
Este programa nada mais faz do que... terminar. Está na hora de usar o helppc para esclarecer este assunto:
1. Inicie o helppc - uma tela chamada "Main Topic Menu" será mostrada.
2. Destaque o item de menu "Interrupt Services DOS-BIOS-EMS-Mouse" e tecle Enter.
3. Destaque o item "DOS Functions" e tecle Enter.
4. Escolha INT 21,4C
O helppc nos informa que a INT 21,4C corresponde a "Terminate Process With Return Code - Termina o Processo com Código de Retorno". Também informa que AH precisa conter o valor 4C e que AL precisa conter o código de retorno (de arquivo de lote). Além disso, a instrução não tem valor de retorno.
Portanto, para usar esta instrução, basta preparar AH e AL com os parâmetros exigidos e chamar a interrupção. Com isto terminamos o programa... e com o método aprovado! Se quiser testar este programa, salve o código fonte como teste1.asm, compile-o com o NASM com nasm -o teste1.com teste1.asm para obter teste1.com
Escrevendo um caracter na tela
Do mesmo modo que no exemplo anterior, procure no helppc por uma função que dê saída para a tela. Em "DOS Functions", a primeira encontrada é a INT 21,2 e que revela: precisamos de AH=02 e DL com o caracter que deve ser mostrado. Daí são dois palitos:
mov ah,2 ; serviço 2 da interrupção 21
mov dl,'A' ; caracter que deve ser impresso
int 0x21 ; chama a interrupção e imprime na tela
mov ax,0x4C00 ; serviço 4C da interrupção 21
int 0x21 ; chama a interrupção e termina o programa
Para compilar e testar o programa, proceda como indicado no exemplo anterior.
Variáveis
As variáveis no Assembly são um pouco diferentes quando comparadas com as de outras linguagens de programação. No Assembly podemos inserir dados diretamente no nosso programa com DB, DW e DD (onde B=byte=8bits, W=word=16 bits e D=duplo word=32 bits). Além disso, podemos atribuir marcadores para os dados para poder identificá-los por nomes - estão aí as nossas variáveis!
varb db 0 ; 1 byte chamado "varb" é inserido
varw dw 0 ; 1 word chamado "varw" é inserido (1 word=2 bytes)
vard dd 0 ; 1 duplo chamado "vard" é inserido (1 double=4 bytes)
b2 db 0,1 ; 2 bytes chamados "b2" são inseridos
w3 dw 1,1,1 ; 3 words chamados "w3" são inseridos
str db 'abcd' ; uma string (4 bytes) chamada "str" é inserida
Veja alguns exemplos de como manipular os valores das variáveis acima definidas:
mov al, 15 ; Põe 15 no byte baixo de AX
mov [varb], al ; Transfere 15 para a variável varb
mov bx, 0x1234 ; Põe 1234hex no byte baixo de BX
mov [varw], bx ; O novo valor de varw agora é 0x1234
mov [b2], bx ; O valor de "b2" agora é 0x1234
mov bl, [b2] ; BL = Primeiro byte em "b2" = 0x34
mov bh, [b2+1] ; BH = Segundo byte em "b2" = 0x12
mov dl, [str] ; DL = Primeiro byte em "str" = 'a'
mov dl, [str+1] ; DL = Segundo byte em "str" = 'b'
mov dl, [str+2] ; DL = Terceiro byte em "str" = 'c'
mov dl, [str+3] ; DL = Quarto byte em "str" = 'd'
mov ax, [w3] ; AX = valor do word no endereço w3 = 1
mov ax, [w3+1] ; AX = valor do word no endereço w3+1 = 256
mov ax, [w3+2] ; AX = valor do word no endereço w3+2 = 1
mov al, [w3+6] ; AL = 'a'
Para obter o endereço de variáveis, basta omitir os colchetes:
mov dx, str ; DX = Ponteiro pata "str"
mov al, [dx] ; AL = Primeiro byte em "str" = 'a'
Escrevendo uma string na tela
Procure novamente no helppc por uma função do DOS que imprima texto na tela. Se você encontrou a INT 21,9 - parabéns! O serviço 9 da interrupção 21 pede que DSX apontem para uma string terminada em $. Por enquanto ignore a novidade do DSX, a única coisa importante no momento é saber que DX precisa conter o endereço da string:
; Este é um programa DOS => ele será carregado no endereço 0x100
; O Nasm precisa do endereço para poder calcular o valor dos ponteiros.
; Para dar um endereço base para o Nasm, use o comando ORG (de ORiGem):
[ORG 0x100]
mov ah, 9 ; Imprimir string
mov dx, msg ; (DX=Ponteiro para msg)
int 0x21
mov ax, 0x4C00 ; Termina programa
int 0x21
msg db 'NumaBoa$' ; Insere a mensagem - uma string terminada em "$"
Entrada de teclado
Ponha novamente o helppc em ação e procure pela interrupção do DOS INT 21,A. A explicação parece um pouco complicada, mas não se preocupe. DSX deve conter um ponteiro para um buffer. Este buffer contém 3 áreas: max - o número máximo de caracteres que devem ser lidos, count - o número de caracteres retornados e buffer - a área de entrada. O código comentado vai facilitar as coisas:
[ORG 0x100]
mov ah, 9 ; Imprime "Escreva alguma coisa: "
mov dx, msg1 ;
int 0x21 ;
mov ah, 0xA ; Coleciona as teclas digitadas
mov dx, buf ; armazenando-as no buffer
int 0x21 ;
mov ax, 0x4C00 ; Termina o programa
int 0x21 ;
msg1 db 'Escreva alguma coisa: $'
; Este é o buffer de entrada
buf:
max db 20
count db 0
data db 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
Como atribuímos 20 zeros à área data do buffer e indicamos 20 na área max, o buffer poderá conter no máximo 20 caracteres, ou seja, após 20 "tecladas" nenhuma mais é aceita. Para encerrar o programa, basta digitar Enter.
Saltos
Saltos, os famosos jump, desviam a linha de execução. Geralmente são condicionais, do tipo "se alguma coisa, então salte para...", mas existe também uma instrução de salto incondicional. Os mais comuns são:
JA Salte se acima (above)
JAE Salte se acima ou igual (above or equal)
JB Salte se abaixo (below)
JBE Salte se abaixo ou igual (below or equal)
JE Salte se igual (equal)
JMP Salto incondicional
JNA Salte se não acima (not above)
JNB Salte se não abaixo (not below)
JNE Salte se igual (not equal)
JNZ Salte se não zero (not zero)
JZ Salte se zero
Como a maioria dos saltos são condicionais, é claro que precisamos de uma instrução que faça comparações. É a CMP, menumônico de CoMParar (ou CoMPare). Observe no código abaixo como criar um loop com uma instrução de salto. Este programa imprime NumaBoa cinco vezes e termina:
[ORG 0x100]
mov cx, 0 ; Contador = 0
Marcador: ; marcador para dirigir o salto
mov ah, 9 ; Imprimir mensagem
mov dx, msg
int 0x21
inc cx ; Contador = Contador + 1 (inc=incrementa)
cmp cx, 5 ; Compara contador com 5
jne Marcador ; Se contador diferente de 5, salta para Marcador
mov ax, 0x4C00 ; Termina o programa
int 0x21
msg db 'NumaBoa',13,10,'$' ; A string contém nova linha (13,10)
Funções
As funções em Assembly são como os marcadores só que, ao invés de serem acessadas através de saltos, são chamadas e precisam ter uma instrução de retorno. Também é possível enviar parâmetros para as funções através de registradores ou da pilha. As funções são úteis quando há tarefas repetitivas. Observe um esqueleto de uma função:
[ORG 0x100]
call func ; Chama a função
mov ax, 0x4C00 ; Termina o programa
int 0x21 ;
func: ; Início da função
; aqui vai o código que deve ser executado ...
retn ; retornar
Melhorando o "Escreva alguma coisa"
Fazendo uso de saltos, o programa "Escreva alguma coisa" pode ficar muito mais amigável. A tecla Enter interrompe a entrada de caracteres e pede nova entrada. A tecla Esc encerra o programa. Imagino que os comentários do código sejam suficientes:
[ORG 0x100]
inicio:
mov ah, 9 ; Serviço de impressão da INT 21
mov dx, msg1 ; Imprime "Escreva alguma coisa: "
int 0x21
tecla:
mov ah, 0x1 ; Serviço de rastrear uma tecla
int 0x21
cmp al,13 ; se for Enter
je termina ; salte para termina e põe caracteres na tela
cmp al, 27 ; se foe Esc
je fecha ; termina o programa
mov [data+bx], al ; põe caracter da tecla no buffer
mov bl, [count] ; põe número já teclado em BL
inc bx ; incrementa número de caracteres
mov [count], bl ; atualiza count
cmp bl, 20 ; compara número de caracteres com 20
je termina ; se forem 20, termina
jmp tecla ; senão volta a esperar tecla
termina:
mov al, '$' ; Põe caracter terminador '$' em AL
mov bl, [count] ; Põe número de teclas digitadas em BL
mov bh, 0 ; Zera o byte alto de BX
mov [data+bx], al ; Adiciona '$' no final do buffer
mov ah, 9 ; Imprime "Você escreveu: "
mov dx, msg2 ;
int 0x21 ;
mov ah, 9 ; Imprime o conteúdo do buffer
mov dx, data
int 0x21
xor bx, bx ; Zera o registrador BX (o mesmo que mov bx,0)
mov bl, 20 ; Máximo de caracteres (20)
xor ax, ax ; Zera o registrador AX
mov [count], al ; Zera o contador
limpa:
mov [data+bx], al ; Põe 0 na posição início do buffer + BL
dec bl ; Decrementa BL
cmp bl, 0 ; Compara com 0
ja limpa ; Se for maior que 0 continua limpando o buffer
jmp inicio ; Se não, pede "Escreva alguma coisa: "
fecha:
mov ax, 0x4c00 ; Serviço para terminar programa
int 0x21
; Os caracteres 13 e 10 forçam uma nova linha
msg1 db 13,10,13,10,'Escreva alguma coisa: $'
msg2 db 13,10,'Você escreveu: $'
; Este é o buffer de entrada com espaço para 20 bytes para caracteres
; e 1 byte para '$'
buf:
count db 0
data db 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
A Pilha
A pilha é uma área especial de memória dentro do processador. Informações que se guarda na pilha com PUSH são colocadas no topo. Quando informações são tiradas da pilha com POP, estas também são tiradas do topo. Isto resulta numa regra simples: os últimos dados que entram na pilha são os primeiros a sair. A pilha é muito utilizada pelas rotinas dos códigos operacionais do computador. Por exemplo, toda instrução CALL põe o endereço da instrução de chamada na pilha para que a instrução de retorno RETN saiba para onde saltar para voltar ao ponto de chamada.
Veja um pouco de código usando PUSH e POP e observe como os valores dos registradores AX e BX foram trocados com o auxílio da pilha:
mov ax, 123
mov bx, 456
push ax ; Topo da pilha = 123
push bx ; Topo da pilha = 456
pop ax ; AX = Topo da pilha = 456
pop bx ; BX = Topo da pilha = 123
Segmentos e Deslocamentos
Este é o tipo do assunto que no início dá nó na cabeça de qualquer um mas, uma vez entendido, facilita muito a programação Assembly.
Segmentos
Quando as máquinas foram melhoradas, a memória do computador, aquelas "tripinhas" de chips que a gente adiciona à placa-mãe, passaram a armazenar vários megabytes - muitas vezes o maior número que conseguimos obter com 16 bits. Num sistema de 16 bits, o maior número que conseguimos obter é 1111 1111 1111 1111 em binário ou 65535 em decimal. Este número corresponde a 64 Kb, muito longe dos megabytes. A questão era: o que fazer para acessar posições de memória acima de 65535?
Foi daí que o pessoal resolveu "lotear" a memória em segmentos e, adivinhe, cada segmento com 65536 bytes (de 0 a 65535). Para acessar qualquer ponto da memória, passou-se a usar um duplo endereço: o número do segmento e a posição dentro deste segmento. Com os segmentos colocados um após o outro, as posições de memória ficariam da seguinte forma:
* Segmento 0 começa na posição 0 e vai até 65535
* Segmento 1 começa na posição 65536 e vai até 131071
* ...
Acontece que nem sempre os dados colocados na memória, como os programas, os dados e a pilha, ocupavam segmentos inteiros. Como resultado, a memória - chipzinhos caros pra caramba - pareciam um queijo suiço cheio de buracos. Resolveu-se então sobrepor os segmentos, usando uma distância entre eles de 16 bytes. Desta forma, as posições de memória foram loteadas assim:
Segmento 0 começa na posição 0 e vai até 65535
Segmento 1 começa na posição 16 e vai até 65551
...
Segmento 100 começa na posição 1600 e vai até 67135
...
Segmento X começa na posição X*16 e vai até X*16 + 65535
Deslocamento
O deslocamento (offset) é um endereço relativo porque depende do segmento. Nada mais é do que a posição de determinado byte dentro das 65536 possíveis. Lembre-se de que as posições são numeradas de 0 a 65535, o que dá o total de 65536 posições. Lembre-se também que o deslocamento nunca pode ser maior do que 65535 porque... não conseguimos um número maior que este com 16 bits.
O deslocamento é a segunda porção do duplo endereço de uma posição de memória que geralmente é indicada por Segmentoeslocamento. Para facilitar, vamos a um exemplo prático:
Endereço 1234:5678 => Segmento 1234 e Deslocamento 5678
Início do segmento => 1234 * 16 = 19744
Deslocamento dentro do segmento = 5678
------
Endereço da memória 25422
O processador possui registradores especiais para gerenciar os segmentos. Os principais são o CS (Code Segment), o DS (Data Segment) e o ES (Extra Segment). O CS gerencia o segmento onde se encontra o código do programa, o DS gerencia o segmentos que contém os dados do programa e o ES é um gerenciador adicional. Para o deslocamento também existe um registrador especial, o SI (Segment Index). Quando queremos acessar algum dado, é a dupla DS:SI que contém o endereço deste dado.
Se bagunçarmos estes registradores, os resultados podem ser catastróficos (nem preciso explicar porque )) Para evitar que inadvertidamente se mude os valores destes registradores, não existem instruções que façam alterações diretas - os valores só podem ser alterados indiretamente, forçando a atenção dos programadores. Veja um exemplo:
mov ax, 1000 ; AX = 1000
mov ds, ax ; Altera o número do segmento para 1000
mov ax, [ds:0] ; Põe word do endereço 1000:0000 em AX
; - o endereço é 16000
mov [1234], ax ; Põe valor de AX no segmento atual no
; deslocamento 1234
inc ax ; Incrementa AX
mov [ds:32], ax ; Põe valor de AX no endereço 1000:0032
; - o endereço é 16032
Mais sobre a memória no módulo seguinte. Aguarde-me )
Considerações finais
Este foi o curso de Assembly mais rápido que consegui dar na minha vida. Contém o mínimo necessário para você poder se aventurar nesta linguagem de baixo nível. Se você conseguiu acompanhá-lo (espero que sim), então o caminho para criar seu próprio sistema operacional (além de milhões de outras coisas) está aberto. A linguagem Assembly é rápida, eficiente e direta. O código pode parecer longo, mas os programas resultantes são espantosamente pequenos porque o código compilado é extremamente enxuto. E nada supera a deliciosa sensação de ter a CPU na mão ))
Este módulo abordou apenas a programação para sistemas de 16 bits, aliás, uma boa maneira de começar. Interessante é que sistemas de 32, ou até de 64 bits, aceitam sem problemas as instruções e os programas de 16 bits. A característica dos exemplos foi usar interrupções do DOS. Não que seja obrigatório, mesmo porque o DOS não é o único sistema operacional existente. A idéia foi apenas a de facilitar o aprendizado.
Quero agradecer o jovem Daniel Marjamäki pelo seu texto Assembly Programming, que serviu de base para este módulo. Suas idéias claras e bem colocadas me chamaram a atenção. O texto original está em Inglês.
Fonte: www.numaboa.com.br (http://www.numaboa.com.br)
;D :D ae vlw vo tentar estudarr aq
depois de quase 4 anos aq estou eu lendo esse tutorial
Eu estudo assembly lá na "aldeia numaboa"(aprendo bastante coisa).
Li esse curso relâmpago(demorou... :o).
O seu ajudou bastante no meu aprendizado.valeu pelo esforço...XD
É uma das primeiras linguagens que estou aprendendo.Achei ela bem complexa,mas estou conseguindo entender em pouco a pouco(estou me esforçando :))