




























































































Estude fácil! Tem muito documento disponível na Docsity
Ganhe pontos ajudando outros esrudantes ou compre um plano Premium
Prepare-se para as provas
Estude fácil! Tem muito documento disponível na Docsity
Prepare-se para as provas com trabalhos de outros alunos como você, aqui na Docsity
Encontra documentos específicos para os exames da tua universidade
Prepare-se com as videoaulas e exercícios resolvidos criados a partir da grade da sua Universidade
Responda perguntas de provas passadas e avalie sua preparação.
Ganhe pontos para baixar
Ganhe pontos ajudando outros esrudantes ou compre um plano Premium
Cursinho de Assembly fdfdfdfdfdsf
Tipologia: Traduções
1 / 132
Esta página não é visível na pré-visualização
Não perca as partes importantes!





























































































Frederico Lamberi Pissarra Atualização dos textos originais para UTF-8. Publicado originalmente no ano de 1994, na Rede Brasileira de Telemática (RBT) Revisado em 28 de julho de 2016
┃ RBT │ Curso de Assembly │ Aula Nº 01 ┃ ┗━━━━━━━┷━━━━━━━━━━━━━━━━━━━━━━━┷━━━━━━━━━━━━━━┛ Por: Frederico Pissarra A linguagem ASSEMBLY (e não assemblER!) dá medo em muita gente! Só não sei porque! As liguagens ditas de "alto nível" são MUITO mais complexas que o assembly! O programador assembly tem que saber, antes de mais nada, como está organizada a memória da máquina em que trabalha, a disponibilidade de rotinas pré-definidas na ROM do micro (que facilita muito a vida de vez em quando!) e os demais recursos que a máquina oferece. Uma grande desvantagem do assembly com relação as outras linguagens é que não existe tipagem de dados como, por exemplo, ponto-flutuante... O programador terá que desenvolver as suas próprias rotinas ou lançar mao do co-processador matemático (o TURBO ASSEMBLER, da Borland, fornece uma maneira de emular o co-processador). Não existem funções de entrada-saída como PRINT do BASIC ou o Write() do PASCAL... Não existem rotinas que imprimam dados numéricos ou strings na tela... Enfim... não existe nada de útil! (Será?! hehehe) Pra que serve o assembly então? A resposta é: Para que você possa desenvolver as suas próprias rotinas, sem ter que topar com bugs ou limitações de rotinas já existentes na ROM-BIOS ou no seu compilador "C", "PASCAL" ou qualquer outro... Cabe aqui uma consideração interessante: É muito mais produtivo usarmos uma liguagem de alto nível juntamente com nossas rotinas em assembly... Evita-se a "reinvenção da roda" e não temos que desenvolver TODAS as rotinas necessárias para os nossos programas. Em particular, o assembly é muito útil quando queremos criar rotinas que não existem na liguagem de alto-nível nativa! Uma rotina ASM bem desenvolvida pode nos dar a vantagem da velocidade ou do tamanho mais reduzido em nossos programas. O primeiro passo para começar a entender alguma coisa de assembly é entender como a CPU organiza a memória. Como no nosso caso a idéia é entender os microprocessadores da família 80x86 da Intel (presentes em qualquer PC-Compatível), vamos dar uma olhadela no modelamento de memória usado pelos PCs, funcionando sob o MS-DOS (Windows, OS/2, UNIX, etc... usam outro tipo de modelamento... MUITO MAIS COMPLICADO!). ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ Modelamento REAL da memória - A segmentação ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ A memória de qualquer PC é dividida em segmentos. Cada segmento tem 64k bytes de tamanho (65536 bytes) e por mais estranho que pareça os segmentos não são organizados de forma sequencial (o segmento seguinte não começa logo após o anterior!). Existe uma sobreposiçao. De uma olhada:
64k (tamanho do segmento 0) ┌─────────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────┬─────── │ ┊ ┊ ┊ ┊ │ ┊ ┊ ┊ ┊ │ ┊ ┊ ┊ ┊ └─────────────────────────────────────────────────────┴─────── 0 1 2 ← Numero dos segmentos └─────────────┘ 16 16 bytes bytes O segundo segmento começa exatamente 16 bytes depois do primeiro. Deu pra perceber que o inicio do segundo segmento está DENTRO do primeiro, já que os segmentos tem 64k de tamanho! Este esquema biruta confunde bastante os programadores menos experientes e, até hoje, ninguém sabe porque a Intel resolveu utilizar essa coisa esquisita. Mas, paciência, é assim que a coisa funciona! Para encontrarmos um determinado byte dentro de um segmento precisamos fornecer o OFFSET (deslocamento, em inglês) deste byte relativo ao inicio do segmento. Assim, se queremos localizar o décimo-quinto byte do segmento 0, basta especificar 0:15, ou seja, segmento 0 e offset 15. Esta notação é usada no restante deste e de outros artigos. Na realidade a CPU faz o seguinte cálculo para encontrar o "endereço físico" ou "endereço efetivo" na memória: ┌─────────────────────────────────────────────────────────────────┐ │ ENDEREÇO-EFETIVO = (SEGMENTO ∙ 16) + OFFSET │ └─────────────────────────────────────────────────────────────────┘ Ilustrando a complexidade deste esquema de endereçamento, podemos provar que existem diversas formas de especificarmos um único "endereço efetivo" da memória... Por exemplo, o endereço 0:13Ah pode ser também escrito como: 0001h:012Ah 0002h:011Ah 0003h:010Ah 0004h:00FAh 0005h:00EAh 0006h:00DAh 0007h:00CAh 0008h:00BAh 0009h:00AAh 000Ah:009Ah 000Bh:008Ah 000Ch:007Ah 000Dh:006Ah 000Eh:005Ah 000Fh:004Ah 0010h:003Ah 0011h:002Ah 0012h:001Ah 0013h:000Ah Basta fazer as contas que você verá que todas estas formas darão o mesmo resultado: o endereço-efetivo 0013Ah. Generalizando, existem, no máximo, 16 formas de especificarmos o mesmo endereço efetivo! As únicas faixas de endereços que não tem equivalentes e só podem ser especificados de uma única forma são os desesseis primeiros bytes do segmento 0 e os últimos desesseis bytes do segmento 0FFFFh. Normalmente o programador não tem que se preocupar com esse tipo de coisa. O compilador toma conta da melhor forma de endereçamento. Mas, como a toda regra existe uma excessão, a informação acima pode
Acrescente um 'h' no fim do número para sabermos que se trata da base 16, do contrário, se olharmos um número "7CA" poderiamos associa-lo a qualquer outra base numérica (base octadecimal por exemplo!)... O processo inverso, hexa → decimal, é mais simples... basta escrever o númer, multiplicando cada digito pela potência correta, levando-se em conta a equivalencia das letras com a base decimal: ┌────────────────────────────────────────────────────────────────┐ │ 7CAh = (7 ∙ 16²) + (C ∙ 16¹) + (A ∙ 16 ) =⁰ │ │ (7 ∙ 16²) + (12 ∙ 16¹) + (10 ∙ 16 ) =⁰ │ │ 1792 + 192 + 10 = 1994 │ └────────────────────────────────────────────────────────────────┘ As mesmas regras podem ser aplicadas para a base binária (que tem apenas dois digitos: 0 e 1). Por exemplo, o número 12 em binário fica: ┌────────────────────────────────────────────────────────────────┐ │ 12 ÷ 2 ⇒ Quociente = 6, Resto = 0 │ │ 6 ÷ 2 ⇒ Quociente = 3, Resto = 0 │ │ 3 ÷ 2 ⇒ Quociente = 1, Resto = 1 │ │ 1 ÷ 2 ⇒ Quociente = 0, Resto = 1 │ │ │ │ 12 = 1100b │ └────────────────────────────────────────────────────────────────┘ Cada digito na base binária é conhecido como BIT (Binary digIT - ou digito binário, em inglês). Note o 'b' no fim do número convertido... Faça o processo inverso... Converta 10100110b para decimal. A vantagem de usarmos um número em base hexadecimal é que cada digito hexadecimal equivale a exatamente quatro digitos binários! Faça as contas: Quatro bits podem conter apenas 16 números (de 0 a 15), que é exatamente a quantidade de digitos na base hexadecimal.
┃ RBT │ Curso de Assembly │ Aula Nº 02 ┃ ┗━━━━━━━┷━━━━━━━━━━━━━━━━━━━━━━━┷━━━━━━━━━━━━━━┛ Por: Frederico Pissarra Mais alguns conceitos são necessários para que o pretenso programador ASSEMBLY saiba o que está fazendo. Em eletrônica digital estuda-se a algebra booleana e aritimética com números binários. Aqui esses conceitos também são importantes... Vamos começar pela aritimética binária: A primeira operação básica - a soma - não tem muitos mistérios... basta recorrer ao equivalente decimal. Quando somamos dois números decimais, efetuamos a soma de cada algarismo em separado, prestando atenção aos "vai um" que ocorrem entre um algarismo e outro. Em binário fazemos o mesmo: ┌───────────────────────────────────────────────────────────────┐ │ 1010b + 0110b =? │ │ │ │ 111 ← "Vai uns" │ │ 1010b │ │ + 0110b │ │ ───────── │ │ 10000b │ └───────────────────────────────────────────────────────────────┘ Ora, na base decimal, quando se soma - por exemplo - 9 e 2, fica 1 e "vai um"... Tomemos o exemplo do odômetro (aquele indicador de quilometragem do carro): 09 → 10 → 11 Enquanto na base decimal existem 10 algarismos (0 até 9), na base binária temos 2 (0 e 1). O odômetro ficaria assim: 00b → 01b → 10b → 11b Portanto, 1b + 1b = 10b ou, ainda, 0b e "vai um". A subtração é mais complicada de entender... Na base decimal existem os números negativos... em binário não! (Veremos depois como "representar" um número negativo em binário!). Assim, 1b - 1b = 0b (lógico), 1b - 0b = 1b (outra vez, evidente!), 0b - 0b = 0b (hehe... você deve estar achando que eu estou te sacaneando, né?), mas e 0b - 1b = ????? A solução é a seguinte: Na base decimal quando subtraimos um algarismo menor de outro maior costumamos "tomar um emprestado" para que a conta fique correta. Em binário a coisa funciona do mesmo jeito, mas se não tivermos de onde "tomar um emprestado" devemos indicar que foi tomado um de qualquer forma:
Não basta "esquecermos" o bit 7 e lermos o restante do byte. O procedimento correto para sabermos que número está sendo representado negativamente no segundo exemplo é: ➠ Inverte-se todos os bits e, ➠ Soma-se 1 ao resultado ┌───────────────────────────────────────────────────────────────┐ │ 10001010b → 01110101b + 00000001b → 01110110b │ │ 01110110b = 118 │ │ Logo: │ │ 10001010b = -118 │ └───────────────────────────────────────────────────────────────┘ Com isso podemos explicar a diferença entre os extremos da faixa de um "signed char": ➠ Os números positivos contam de 00000000b até 01111111b, isto é, de 0 até 127. ➠ Os números negativos contam de 10000000b até 11111111b, isto é, de -128 até -1. Em "C" (ou PASCAL), a mesma lógica pode ser aplicada aos "int" e "long" (ou INTEGER e LONGINT), só que a quantidade de bits será maior ("int" tem 32 ou 16 bits de tamanho, de acordo com a arquitetura, e "long" tem 32). Não se preocupe MUITO com a representação de números negativos em binário... A CPU toma conta de tudo isso sozinha... mas, as vezes, você tem que saber que resultado poderá ser obtido de uma operação aritimética em seus programas, ok? As outras duas operações matemáticas básicas (multiplicação e divisão) tanbém estão presentes nos processadores 80x86... Mas, não necessitamos ver como o processo é feito a nível binário. Confie na CPU! :)
AX (16 bits) BX (16 bits) ┌─────────────────┐ ┌─────────────────┐ ┌────────┬────────┐ ┌────────┬────────┐ │ AH │ AL │ │ BH │ BL │ └────────┴────────┘ └────────┴────────┘ 15 8 7 0 15 8 7 0 CX (16 bits) DX (16 bits) ┌─────────────────┐ ┌─────────────────┐ ┌────────┬────────┐ ┌────────┬────────┐ │ CH │ CL │ │ DH │ DL │ └────────┴────────┘ └────────┴────────┘ 15 8 7 0 15 8 7 0 AH é o byte mais significativo do registrador AX, enquanto que AL é o menos significativo. Se alterarmos o conteúdo de AL, estaremos alterando o byte menos significativo de AX ao mesmo tempo... Não existem registradores de oito bits em separado... tudo é uma coisa só. Portanto, ao manipularmos AH, estaremos manipulando AX ao mesmo tempo! O nome de cada registrador tem o seu sentido de ser... "A" de AX quer dizer que este registrador é um "acumulador" (usado por default em algumas operações matematicas!), por exemplo... AX → Acumulador BX → Base CX → Contador DX → Dados O "X" de AX significa "eXtended". "H" de AH significa "High byte". Embora estes registradores possam ser usados sem restrições, é interessante atribuir uma função para cada um deles nos nossos programas sempre que possível... Isto facilita a leitura do código e nos educa a seguirmos uma linha de raciocínio mais concisa... Mas, se for de sua preferência não seguir qualquer padrão no uso desses registradores, não se preocupe... não haverá qualquer desvantagem nisso (Well... depende do código, as vezes somos obrigados a usar determinado registrador!). Alguns pontos importantes quanto a esses nomes serão observados no decorrer do curso... Por exemplo, certas instruções usam AX (ou AL, ou AH) e somente ele, não permitindo o uso de nenhum outro registrador... Outras, usam CX para contar, etc... essas instruções específicas serão vistas em outra oportunidade. Os registradores SI e DI são usados como índices para tabelas. Em particular, SI é usado para leitura de uma tabela e DI para escrita (fonte e destino... lembra algum procedimento de cópia, nao?). No entanto, esses registradores podem ser usados com outras finalidades... Podemos incluí-los no grupo de "registradores de uso geral", mas assim como alguns registradores de uso geral, eles têm aplicação exclusiva em algumas instruções, SI e DI são usados especificamente como índices em instruções que manipulam blocos (também veremos isso mais tarde!). Os registradores CS, DS, ES e SS armazenam os segmentos onde
estão o código (programa sendo executado), os dados, os dados extras, e a pilha, respectivamente. Lembre-se que a memória é segmentada em blocos de 64kbytes (dê uma olhada na primeira mensagem dessa série). Quando nos referimos, através de alguma instrução, a um endereço de memória, estaremos nos referindo ao OFFSET dentro de um segmento. O registrador de segmento usado para localizar o dado no offset especificado vai depender da própria instrução... Um exemplo em assembly: ┌────────────────────────────────────────────────────────────────┐ │ MOV AL,[1D4Ah] │ └────────────────────────────────────────────────────────────────┘ O número hexadecimal entre os colchetes é a indicação de um offset em um segmento... Por default, a maioria das instruções usa o segmento de dados (valor em DS). A instrução acima é equivalente a: ┌────────────────────────────────────────────────────────────────┐ │ AL = DS:[1D4Ah] │ └────────────────────────────────────────────────────────────────┘ Isto é, em AL será colocado o byte que está armazenado no offset 1D4Ah do segmento de dados (valor em DS). Veremos mais sobre os segmentos e as instruções mais tarde :) Se quisessemos localizar o byte desejado em outro segmento (mas no mesmo offset) devemos especificar o registrador de segmento na instrução: ┌────────────────────────────────────────────────────────────────┐ │ MOV AL,ES:[1D4Ah] │ └────────────────────────────────────────────────────────────────┘ Aqui o valor de ES será usado. O registrador IP (Instruction Pointer) é o offset do segmento de código que contém a próxima instrução a ser execuatda. Este registrador não é acessível por qualquer instrução (pelo menos não pelas documentadas pela Intel)... é de uso interno do microprocessador. No entanto existem alguns macetes para conseguirmos obter o seu conteúdo (o que na maioria das aplicações não é necessario... Para que conhecer o endereço da próxima instrução se ela var ser executada de qualquer jeito?). O registrador SP é o offset do segmento SS (segmento de pilha) onde o próximo dado vai ser empilhado. A pilha serve para armazenar dados que posteriormente podem ser recuperados sem que tenhamos que usar um dos registradores para esse fim. Também é usada para armazenar o endereço de retorno das sub-rotinas. A pilha "cresce" de cima para baixo, isto é, SP é decrementado cada vez que um novo dado é colocado na pilha. Note também que existe um registrador de segmento exclusivo para a pilha... SP sempre está relacionado a esse segmento (SS), como foi dito antes. Para ilustrar o funcionamento da pilha, no gráfico abaixo simularemos o empilhamento do conteúdo do registrador AX através da
todos os bits forem deslocados em uma posição para a direita, o que acontece com o bit 0?... Resposta: Vai para o carry!) ➠ Parity: Depois de uma instrução aritimética ou lógica este bit informa se o resultado tem um número par de "1"s ou não. ➠ Auxiliar Carry: Igual ao carry, mas indica o "vai um" no meio de um dado (no caso de um byte, se houve "vai um" do bit 3 para o bit 4!). ➠ Zero: Depois de uma operação aritimética ou lógica, esse flag indica se o resultado é zero ou não. ➠ Signal: Depois de uma instrução aritimética ou lógica, este flag é uma cópia do bit de mais alta ordem do resultado, isto é, seu sinal (dê uma olhada na "representação de números negativos em binário" no texto anterior!). ➠ Trap: Quando setado (1) executa instruções passo-a-passo... Não nos interessa estudar esse bit por causa das diferenças de implementação deste flag em toda a família 80x86. ➠ Interrupt Enable Flag Habilita/Desabilita o reconhecimento de interrupções mascaráveis pela CPU. Sobre interrupções, veremos mais tarde! ➠ Direction: Quando usamos instruções de manipulação de blocos, precisamos especificar a direção que usaremos (do inicio para o fim ou do fim para o inicio). Quando D=0 a direção é a do início para o fim... D=1, então a direção é contrária! ➠ OverFlow: Depois de uma instrução aritimética ou lógica, este bit indica se houve mudança no bit mais significativo, ou seja, no sinal. Por exemplo, se somarmos FFFFh + 0001h obteremos 00h. O bit mais significativo variou de 1 para 0 (o counteúdo inicial de um registrador era FFFFh e depois da soma foi para 0000h), indicando que o resultado saiu da faixa (overflow) - ora, FFFFh
Quando aos demais bits, não se pode prever seus estados lógicos (1 ou 0). Na próxima "aula" começaremos a ver algumas instruções do microprocessador 8086. Ainda não escreveremos nenhum programa, a intenção é familiarizá-lo com a arquitetura do microprocessador antes de começarmos a colocar a mão na massa... tenha um pouco de paciência! :)
│ MOV AL,[0FFFFh] │ └────────────────────────────────────────────────────────────────┘ A instrução acima, pega o byte armazenado no endereço DS:FFFFh e coloca-o em AL. Sabemos que um byte vai ser lido do offset especificado porque AL tem 8 bits de tamanho. Ao invés de usarmos um offset imediato podemos usar um registrador: ┌────────────────────────────────────────────────────────────────┐ │ MOV BX,0FFFFh │ │ MOV CH,[BX] │ └────────────────────────────────────────────────────────────────┘ Neste caso, BX contém o offset e o byte no endereço DS:BX é armazenado em CH. Note que o registrador usado como indice obrigatoriamente deve ser de 16 bits. Uma observação quanto a essa modalidade: Dependendo do registrador usado como offset, o segmento default poderá ser DS ou SS. Se ao invés de BX usassemos BP, o segmento default seria SS e não DS - de uma olhada no diagrama de distribuição dos registradores no texto anterior. BP foi colocado no mesmo bloco de SP, indicando que ambos estão relacionados com SS (Segmento de pilha) - Eis uma tabela das modalidades e dos segmentos default que podem ser usados como offset: ┌─────────────────────────────┬─────────────────────────────────┐ │ Offset usando registros │ Segmento default │ ├─────────────────────────────┼─────────────────────────────────┤ │ [SI + deslocamento] │ DS │ │ [DI + deslocamento] │ DS │ │ [BP + deslocamento] │ SS │ │ [BX + deslocamento] │ DS │ │ [BX + SI + deslocamento] │ DS │ │ [BX + DI + deslocamento] │ DS │ │ [BP + SI + deslocamento] │ SS │ │ [BP + DI + deslocamento] │ SS │ └─────────────────────────────┴─────────────────────────────────┘ O "deslocamento" pode ser suprimido se for 0. Você pode evitar o segmento default explicitando um registrador de segmento na instrução: ┌────────────────────────────────────────────────────────────────┐ │ MOV DH,ES:[BX] ;Usa ES ao invés de DS │ │ MOV AL,CS:[SI + 4] ;Usa CS ao invés de DS │ └────────────────────────────────────────────────────────────────┘ Repare que tenho usado os registradores de 8 bits para armazenar os dados... Pode-se usar os de 16 bits também: ┌────────────────────────────────────────────────────────────────┐ │ MOV ES:[BX],AX ; Poe o valor de AX para ES:BX │ └────────────────────────────────────────────────────────────────┘
Só que neste caso serão armazenados 2 bytes no endereço ES:BX. O primeiro byte é o menos significativo e o segundo o mais signigicativo. Essa instrução equivale-se a: ┌────────────────────────────────────────────────────────────────┐ │ MOV ES:[BX],AL ; Instruçõess que fazem a mesma │ │ MOV ES:[BX + 1],AH ;coisa que MOV ES:[BX],AX │ └────────────────────────────────────────────────────────────────┘ Repare também que não é possível mover o conteúdo de uma posição da memória para outra, diretamente, usando MOV. Existe outra instrução que faz isso: MOVSB ou MOVSW. Veremos essas instruções mais tarde. Regra geral: Um dos operandos TEM que ser um registrador! Salvo no caso da movimentação de um imediato para uma posição de memória: ┌───────────────────────────────────────────────────────────────┐ │ MOV [DI],[SI] ; ERRO! │ │ MOV [BX],0 ; OK! │ └───────────────────────────────────────────────────────────────┘ Para ilustrar o uso da instrução MOV, eis um pedaço do código usado pela ROM-BIOS do IBM PS/2 Modelo 50Z para verificar a integridade dos registradores da CPU: ┌────────────────────────────────────────────────────────────────┐ │ ... │ │ MOV AX,0FFFFh ;Poe 0FFFFh em AX │ │ MOV DS,AX │ │ MOV BX,DS │ │ MOV ES,BX │ │ MOV CX,ES │ │ MOV SS,CX │ │ MOV DX,SS │ │ MOV SI,DX │ │ MOV DI,SI │ │ MOV BP,DI │ │ MOV SP,BP │ │ ... │ └────────────────────────────────────────────────────────────────┘ Se o conteúdo de BP não for 0FFFFh então a CPU está com algum problema e o computador não pode funcionar! Os flags são testados de uma outra forma... :) ┏━━━━━━┓ ┃ XCHG ┃ ┗━━━━━━┛ Esta instrução serve para trocarmos o conteúdo de um registrador pelo outro. Por exemplo: ┌────────────────────────────────────────────────────────────────┐ │ XCHG AH,AL │ └────────────────────────────────────────────────────────────────┘ Se AH=1Ah e AL=6Dh, após esta instrução AH=6Dh e AL=1Ah por