Você está na página 1de 154

10/04/2021 Noção geral da arquitetura - Assembly x86

Noção geral da arquitetura


Noção geral da arquitetura x86

Antes de ver a linguagem Assembly em si é importante ter conhecimento sobre a arquitetura do


Assembly que vamos estudar, até porque estão intrinsecamente ligados.
É claro que não dá para explicar todas as características da arquitetura x86 aqui, só para ter
uma noção o manual para desenvolvedores da Intel tem mais de 4.900 páginas. (um dia eu vou
ler tudo... Um dia)
Mas por enquanto vamos ter uma noção geral da arquitetura x86.

O que é a arquitetura x86?

Esta arquitetura nasceu no 8086, que foi um microprocessador da Intel que fez grande sucesso.
Daí em diante a Intel lançou outros processadores baseados na arquitetura do 8086 ganhando
nomes como: 80186, 80286, 80386 etc.
Daí surgiu a nomenclatura x86, já que o nome dos processadores era um número qualquer (x)
terminando com 86.

A arquitetura evoluiu com o tempo e foi ganhando adições de tecnologias, porém sempre
mantendo compatibilidade com os processadores anteriores.
O processador que você tem aí pode rodar código programado para o 8086 sem problema
algum.

Mais para frente a AMD criou a arquitetura x86-64, que é um superconjunto da arquitetura x86
da Intel e adiciona o modo de 64 bit.
Nos dias atuais a Intel e a AMD fazem um trabalho em conjunto para a evolução da arquitetura,
por isso os processadores das duas fabricantes são compatíveis.

Ou seja, x86 é um nome genérico para se referir a uma família de arquiteturas de processadores.
Por motivos de simplicidade eu vou me referir as arquiteturas apenas como x86, mas na prática
estamos abordando três arquiteturas neste livro:

Nome oficial Nome alternativo Bit

IA-16 16

https://silva97.gitbook.io/assembly-x86/a-base/arquitetura-x86 1/4
10/04/2021 Noção geral da arquitetura - Assembly x86

IA-32 i386 32

x86-64 i686 64

AMD64 e Intel64 são os nomes das implementações da AMD e da Intel para a arquitetura x86-
64. Podemos dizer aqui que são sinônimos já que as implementações são compatíveis.

Um software compilado para x86 consegue tanto rodar em um processador Intel como também
AMD. Só fazendo diferença é claro em detalhes de otimização que são específicos para
determinados processadores.
Bem como também algumas tecnologias exclusivas de cada uma das fabricantes.

Comumente um compilador não irá gerar código usando tecnologia exclusiva, afim
de aumentar a portabilidade.
Alguns compiladores aceitam que você passe uma flag na linha de comando para
que eles otimizem o código usando tecnologias exclusivas, como o GCC por
exemplo.

Endianness

A arquitetura x86 é little-endian, o que significa que a ordem dos bytes de valores numéricos
segue do menos significativo ao mais significativo.
Por exemplo, o seguinte valor numérico em hexadecimal 0x1a2b3c4d  ficaria disposto na
memória RAM na seguinte ordem:

4d 3c 2b 1a

Instruções

A arquitetura x86 é uma arquitetura CISC que, resumindo, é uma arquitetura com um conjunto
complexo de instruções.
Falando de maneira leviana isso significa que há várias instruções e cada uma delas tem um
nível de complexidade completamente variada. Boa parte das instruções são complexas na
arquitetura x86. Uma instrução "complexa" é uma instrução que faz várias operações.

https://silva97.gitbook.io/assembly-x86/a-base/arquitetura-x86 2/4
10/04/2021 Noção geral da arquitetura - Assembly x86

Cada instrução do código de máquina tem um tamanho que pode variar de 1 até 15 bytes. E
cada instrução consome um número de ciclos diferente. (devido a sua complexidade variada)

Modelo

A arquitetura x86 segue o modelo de Von Neumann onde este, mais uma vez resumindo,
trabalha principalmente usando uma unidade central de processamento (CPU) e uma memória
principal.

Diagrama da arquitetura de Von Neumann

As instruções podem trabalhar manipulando/lendo dados em registradores, que são pequenas


áreas de memória internas a CPU, e na memória principal que no caso é a memória RAM.
Bem como também usar o sistema de entrada e saída de dados, feito pelas portas físicas.

Portas físicas

Uma porta física é um socket do processador usado para se comunicar com o restante do
hardware. Por exemplo para poder usar a memória secundária, o HD, usamos uma porta física
para enviar e receber dados do dispositivo.
O gerenciamento desta comunicação é feito pelo chipset da placa-mãe.

https://silva97.gitbook.io/assembly-x86/a-base/arquitetura-x86 3/4
10/04/2021 Noção geral da arquitetura - Assembly x86

Do nosso ponto de vista uma porta física é só um número especificado na instrução, muito
parecido com uma porta lógica usada para comunicação em rede.

FPU

Na época do 8086 a Intel também lançou o chamado 8087, que é um co-processador de ponto
flutuante que trabalhava em conjunto com o 8086.
Os processadores seguintes também ganharam co-processadores que receberam o nome
genérico de x87.
A partir do 80486 a FPU é interna a CPU e não mais um co-processador, porém por motivos
históricos ainda chamamos a unidade de ponto flutuante da arquitetura x86 de x87.

FPU nada mais é que a unidade de processamento responsável por fazer cálculos de ponto
flutuante, os famosos números float.

Outras tecnologias

Quem dera um processador fosse tão simples assim, já mencionei que o manual da Intel tem
mais de 4.900 páginas?
Deixei de abordar muita coisa aqui mas que fique claro que os processadores da arquitetura x86
tem várias outras tecnologias, como o 3DNow! da AMD e o SSE da Intel.

Os processadores da AMD também implementam o SSE, já o 3DNow! é exclusivo dos


processadores da AMD.

https://silva97.gitbook.io/assembly-x86/a-base/arquitetura-x86 4/4
10/04/2021 Modos de processamento - Assembly x86

Modos de processamento
Real ou não real? Eis a questão.

Como já explicado a arquitetura x86 foi uma evolução ao longo dos anos, mas sempre
mantendo compatibilidade com os processadores anteriores.
Mas código de 16, 32 e 64 bit são demasiadamente diferentes e boa parte das instruções não
são equivalentes. O que teoricamente faria com que, por exemplo, código de 32 bit fosse
impossível de rodar em um processador x86-64.
Mas é aí que entra os modos de processamento.

Um processador x86-64 consegue executar código de versões anteriores simplesmente


trocando o modo de processamento.
Cada modo faz com que o processador funcione de maneira um tanto quanto diferente, fazendo
com que as instruções executadas também tenham resultados diferentes.

Ou seja, lá no 8086 só existia o modo de 16 bit. Com a chegada dos processadores de 32 bit, na
verdade simplesmente foi adicionado um modo de processamento novo aos processadores,
que seria o modo de 32 bit.
E o mesmo aconteceu com a chegada dos processadores x86-64, que basicamente adiciona um
modo de processamento de 64 bit.
É claro que além dos modos de processamento novos também surgem novas tecnologias e
novas instruções, mas o modo de processamento anterior fica intacto e por isso se tem
compatibilidade com os processadores anteriores.

Podemos dizer que existem três modos de processamento principais.

Modo de processamento Largura do barramento interno

Real mode / Modo real 16 bit

Protected mode / Modo protegido 32 bit

64-bit submode / Submodo de 64-bit 64 bit

Barramento interno

https://silva97.gitbook.io/assembly-x86/a-base/modos-de-processamento 1/3
10/04/2021 Modos de processamento - Assembly x86

Então os tais "bit" que é muito conhecido mas pouco entendido, na verdade é simplesmente
uma referência a largura do barramento interno do processador quando ele está em
determinado modo de processamento.
A largura do barramento interno do processador nada mais é que o tamanho dos dados que ele
pode processar de uma única vez.

Imagine uma enorme via com 16 faixas e no final dela um pedágio, isso significa que 16 carros
serão atendidos por vez no pedágio.
Se é necessário atender 32 carros, então será necessário duas vezes para atender todos os
carros já que apenas 16 podem ser atendidos em uma única vez.
A largura de um barramento nada mais é que uma "via de bits", quanto mais largo mais
informação pode ser enviada de uma única vez. O que teoricamente aumenta a eficiência.

No caso do barramento interno do processador seria a "via de bits" que o processador usa em
todo o seu sistema interno, desconsiderando a comunicação com o hardware externo que é
feito pelo barramento externo e não necessariamente tem o mesmo tamanho do barramento
interno.

Também existe o barramento de endereço, mas não vamos abordar isto agora.

Ainda tem mais

Ok, pelo que nós vimos acima então na verdade um "sistema operacional de 64 bit" nada mais é
que um sistema operacional que executa em submodo de 64-bit.
Ah, mas aí fica a pergunta:

Se está rodando em 64 bit como é possível executar código de 32 bit?

É muito simples, porque existem mais modos de processamento do que os que eu já citei.
Reparou que eu disse "submodo" de 64-bit? É porque na verdade o 64-bit não é um modo
principal mas sim um submodo.
A hierarquia completa de modos de processamento de um processador Intel64 ficaria da
seguinte forma:

Real mode (16 bit)


Protected mode (32 bit)
SMM (não vamos falar deste modo, mas ele existe)
IA-32e
https://silva97.gitbook.io/assembly-x86/a-base/modos-de-processamento 2/3
10/04/2021 Modos de processamento - Assembly x86

64-bit (64 bit)


Compatibility mode (32 bit)

O modo IA-32e é uma adição dos processadores x86-64.


Repare que ele tem outro submodo chamado "compatibility mode", ou em português, "modo de
compatibilidade".

Não confundir com o modo de compatibilidade do Windows, ali é uma coisa diferente
que leva o mesmo nome.

O modo de compatibilidade serve para obter compatibilidade com a arquitetura IA-32.


Um sistema operacional pode setar para que código de apenas determinado segmento na
memória rode neste modo, permitindo assim que ele execute código de 32 e 64 bit
paralelamente. (supondo que o processador esteja em modo IA-32e)
Por isso que seu Debian de 64 bit consegue rodar softwares de 32 bit, assim como o seu
Windows 10 de 64 bit também consegue.

Virtual-8086

Lembra que o antigo Windows XP de 32 bit era capaz de rodar programas de 16 bit do MS-DOS?
Isto era possível devido ao modo Virtual-8086, que de maneira parecida com o compatibility
mode, permite executar código de 16 bit enquanto o processador está em protected mode.
Nos processadores atuais o Virtual-8086 não é mais um submodo de processamento do
protected mode, mas sim um atributo que pode ser setado enquanto o processador está
executando neste modo.
Mas nos manuais da Intel este recurso ainda é chamado de "modo Virtual-8086", mesmo não
sendo mais um modo de processamento.

Repare que rodando em compatibility mode não é possível usar o modo Virtual-8086.
É por isso que o Windows XP de 32 bit conseguia rodar programas do MS-DOS mas o
XP de 64 bit não.

https://silva97.gitbook.io/assembly-x86/a-base/modos-de-processamento 3/3
10/04/2021 Sintaxe - Assembly x86

Sintaxe
Entendendo a sintaxe da linguagem Assembly no nasm

O Assembly da arquitetura x86 tem duas versões diferentes de sintaxe: A sintaxe Intel e a
sintaxe AT&T.
A sintaxe Intel é a que iremos usar neste livro já que, ao meu ver, ela é mais intuitiva e legível.
Também é a sintaxe que o nasm usa, já o Gas suporta as duas.
É importante saber ler código das duas sintaxes, mas por enquanto vamos aprender apenas a
sintaxe do nasm.

Case Insensitive

As instruções da linguagem Assembly, bem como também as instruções particulares do nasm,


são case-insensitive.
O que significa que não faz diferença se eu escrevo em caixa-alta, baixa ou mesclando os dois.
Veja que cada linha abaixo o nasm irá montar como a mesma instrução:

1 mov eax, 777


2 Mov Eax, 777
3 MOV EAX, 777
4 mov EAX, 777
5 MoV EaX, 777

Comentários

No nasm se pode usar o ponto-vírgula ;  para comentários que terminam no final da linha.
Equivalente ao //  em C.
Comentários de múltiplas linhas podem ser feitos usando a diretiva pré-processada %comment 
para iniciar o comentário e %endcomment  para finalizá-lo.
Exemplo:

1 ; Um exemplo
2 mov eax, 777 ; Outro exemplo

https://silva97.gitbook.io/assembly-x86/a-base/sintaxe 1/8
10/04/2021 Sintaxe - Assembly x86

3
4 %comment
5 Mais
6 um
7 exemplo
8 %endcomment

Números

Números podem ser escritos na base decimal, hexadecimal, octal e binário.


Também é possível escrever constantes numéricas de ponto flutuante no nasm, conforme
exemplos:

Exemplo Formato

0b0111 Binário

0o10 Octal

9 Decimal

0x0a Hexadecimal

11.0 Ponto flutuante

Strings simples

Strings podem ser escritas no nasm de três formas diferentes:

Representação Explicação

"String" String normal

'String' String normal, mesmo que usar "

`String\n` String que aceita caracteres de escape no estilo da linguagem C.

Os dois primeiros são equivalentes e não tem nenhuma diferença para o nasm.
O último aceita caracteres de escape no mesmo estilo da linguagem C.

Formato das instruções


https://silva97.gitbook.io/assembly-x86/a-base/sintaxe 2/8
10/04/2021 Sintaxe - Assembly x86

As instruções em Assembly seguem a premissa de especificar uma operação e seus operandos.


Na arquitetura x86 uma instrução pode não ter operando algum e chegar até três operandos.

operação operando1, operando2, operando3

Algumas instruções alteram o valor de um ou mais operandos, que pode ser um endereçamento
na memória ou um registrador.
Nas instruções que alteram o valor de apenas um operando, ele sempre será o operando mais a
esquerda. Um exemplo prático é a instrução mov:

mov eax, 777

O mov especifica a operação enquanto o eax  e o 777 são os operandos.


Essa instrução altera o valor do operando destino eax  para 777 
Exemplo de pseudo-código:

eax = 777;

Da mesma forma que não é possível fazer 777 = eax; em linguagens de alto nível,
também não dá para passar um valor numérico como operando destino para mov .
Ou seja, isto está errado:
mov 777, eax 

Endereçamento

O endereçamento em Assembly x86 é basicamente um cálculo para acessar determinado valor


na memória. O resultado deste cálculo é o endereço na memória que o processador irá acessar,
seja para ler ou escrever dados no mesmo.
Usá-se os colchetes []  para denotar um endereçamento. Ao usar colchetes como operando
você está basicamente acessando um valor na memória. Por exemplo poderíamos alterar o
valor no endereço 0x100  usando a instrução mov para o valor contido no registrador eax 

https://silva97.gitbook.io/assembly-x86/a-base/sintaxe 3/8
10/04/2021 Sintaxe - Assembly x86

mov [0x100], eax

Como eu já mencionei o valor contido dentro dos colchetes é um cálculo matemático. Vamos
aprender mais a respeito quando eu for falar de endereçamento na memória.

Você só pode usar um operando na memória por instrução. Então não é possível
fazer algo como:
mov [0x100], [0x200] 

Tamanho do operando

Quando um dos operandos é um endereçamento na memória você precisa especificar o seu


tamanho.
Ao fazer isso você define o número de bytes que serão lidos ou escritos na memória. A maioria
das instruções exigem que o operando destino tenha o mesmo tamanho do operando que irá
definir o seu valor, salvo algumas exceções.
No nasm existem palavras que você pode posicionar logo antes do operando para determinar o
seu tamanho.

Nome Nome estendido Tamanho do operando (em bytes)

byte 1

word 2

dword double word 4

qword quad word 8

tword ten word 10

oword 16

yword 32

zword 64

https://silva97.gitbook.io/assembly-x86/a-base/sintaxe 4/8
10/04/2021 Sintaxe - Assembly x86

Exemplo:

mov dword [0x100], 777

Como o nasm é inteligente se você usar um dos operandos como um registrador ele irá
automaticamente assumir o tamanho do operando como o mesmo tamanho do registrador.
Este é o único caso onde você não é obrigado a especificar o tamanho. Porém em algumas
instruções o nasm não consegue assumir este tamanho.

Pseudo-instruções

No nasm existem o que são chamadas de "pseudo-instruções", são instruções que não são de
fato instruções da arquitetura x86 mas sim instruções que serão interpretadas pelo nasm.
Elas são úteis para deixar o código em Assembly mais versátil, mas deixando claro que elas não
são instruções que serão executadas pelo processador.
Exemplo básico é a pseudo-instrução db  que serve para despejar bytes no correspondente
local no arquivo binário de saída do nasm.
Observe:

db 0x41, 0x42, 0x43, 0x44, "String", 0

Dá para especificar o byte como um número ou então uma sequência de bytes em formato de
string usando aspas. Esta pseudo-instrução não tem limite de valores separados por vírgula.
Veja a saída do exemplo acima no hexdump, um visualizador hexadecimal:

https://silva97.gitbook.io/assembly-x86/a-base/sintaxe 5/8
10/04/2021 Sintaxe - Assembly x86

Rótulos

Os rótulos, ou em inglês label, são definições de símbolos usados para identificar determinadas
áreas da memória no código fonte em Assembly. Podem ser usados de maneira bastante
parecida com os rótulos em C.
O nome do rótulo serve para pegar o endereço na memória do byte seguinte a posição do rótulo,
que pode ser uma instrução ou um byte qualquer produzido por uma pseudo-instrução.
Para escrever um rótulo basta digitar seu nome seguido de dois-pontos : 

meu_rotulo: instrução/pseudo-instrução

Você pode inserir instruções/pseudo-instruções imediatamente após o rótulo ou então em


qualquer linha seguinte, não faz diferença no resultado final.
Também é possível adicionar um rótulo no final do arquivo, o fazendo apontar para o byte
seguinte ao conteúdo do arquivo na memória.

Já vimos um exemplo prático de uso de rótulo na nossa PoC:

https://silva97.gitbook.io/assembly-x86/a-base/sintaxe 6/8
10/04/2021 Sintaxe - Assembly x86

1 bits 64
2
3 global assembly
4 assembly:
5 mov eax, 777
6 ret

Repare o rótulo assembly  na linha 4. Neste caso o rótulo está sendo usado para denotar o
símbolo que aponta para a primeira instrução da nossa função de mesmo nome.

Rótulos locais

Um rótulo local, em inglês local label, é basicamente um rótulo que hierarquicamente pertence a
outro rótulo.
Para definir um rótulo local podemos simplesmente adicionar um ponto .  como primeiro
caractere do nosso rótulo.
Veja o exemplo:

1 meu_rotulo:
2 mov eax, 777
3 .subrotulo:
4 mov ebx, 555

Desta forma o nome completo de .subrotulo  é na verdade meu_rotulo.subrotulo 


As instruções que estejam hierarquicamente dentro do rótulo "pai" podem acessar o subrótulo
usando de sua nomenclatura com .  ao invés de citar o nome completo.

Não se preocupe se não entendeu direito, isso aqui é apenas para ver a sintaxe.
Vamos aprender mais sobre os rótulos e símbolos depois.

Diretivas

Parecido com as pseudo-instruções, o nasm também oferece as chamadas diretivas. A


diferença é que as pseudo-instruções apresentam uma saída de código exatamente onde elas
são utilizadas, já as diretivas são como comandos para modificar o comportamento do
montador.
https://silva97.gitbook.io/assembly-x86/a-base/sintaxe 7/8
10/04/2021 Sintaxe - Assembly x86

Por exemplo a diretiva bits  que serve para especificar se as instruções seguintes são de 64,
32 ou 16 bits. Podemos observar o uso desta diretiva na nossa PoC.
Por padrão o nasm monta as instruções como se fossem de 16 bits.

https://silva97.gitbook.io/assembly-x86/a-base/sintaxe 8/8
10/04/2021 Registradores gerais - Assembly x86

Registradores gerais
Que fique registrado que eles são rápidos

Seguindo o modelo da arquitetura de Von Neumann, interno a CPU existem pequenos espaços
de memória chamados de registers, ou em português, registradores.

Estes espaços são pequenos, apenas o suficiente para armazenar um valor numérico de N bits
de tamanho.
Ler e escrever dados em um registrador é muito mais rápido do que a tarefa equivalente na
memória principal. (a memória RAM)
Do ponto de vista do programador é interessante usar registradores para manipular valores
enquanto está trabalhando com eles, e depois armazená-lo de volta na memória se for o caso.
Seguindo um fluxo como:

1 Registrador = Memória
2 Operações com o valor no registrador
3 Memória = Registrador

Mapeamento dos registradores gerais

Afim de aumentar a versatilidade no uso de registradores, para poder manipular dados de


tamanhos variados no mesmo espaço de memória do registrador, alguns registradores são
subdivido em registradores menores.
Isto seria o "mapeamento" dos registradores que faz com que vários registradores de tamanhos
diferentes compartilhem o mesmo espaço.
Se você entende como funciona uma union em C, já deve ter entendido a lógica aqui.

Lá nos primórdios da arquitetura x86 os registradores tinham o tamanho de 16 bits (2 bytes).


Os processadores IA-32 aumentaram o tamanho desses registradores para acompanhar a
largura do barramento interno de 32 bits (4 bytes).
A referência para o registrador completo ganha um prefixo 'E' que seria a primeira letra de
"Extended". (estendido)
Processadores x86-64 aumentaram mais uma vez o tamanho desses registradores para 64 bits

https://silva97.gitbook.io/assembly-x86/a-base/registradores-gerais 1/8
10/04/2021 Registradores gerais - Assembly x86

(8 bytes), dessa vez dando um prefixo 'R' que seria de "Re-extended". (re-estendido)
Só que também trazendo alguns novos registradores gerais.

Registradores gerais (IA-16)

Os chamados "registradores gerais" são registradores que são, como o nome sugere, de uso
geral pelas instruções.
Na arquitetura IA-16 nós temos os registradores de 16 bits que são mapeados em subdivisões
como explicado acima.

Determinadas instruções da arquitetura usam alguns desses registradores para tarefas


específicas, mas eles não são limitados somente para este uso.
Você pode usá-los da maneira que quiser porém recomendo seguir o padrão para melhorar a
legibilidade do código.
O "apelido" na tabela abaixo é o nome dado aos registradores em inglês, serve para fins de
memorização.

Registrador Apelido Uso

Usado em instruções de operações aritméticas para receber o


AX Accumulator
resultado de um cálculo

Usado geralmente em endereçamento de memória para se


BX Base
referir ao endereço inicial, isto é, o endereço base.

Usado em instruções de repetição de código, loops, para


CX Counter
controlar o número de repetições limite.

Usado em operações de entrada e saída por portas físicas para


DX Data
armazenar o dado enviado/recebido.

Stack
SP Usado como ponteiro para o topo da stack.
Pointer

BP Base Pointer Usado como ponteiro para o endereço inicial do stack frame.

Em operações com blocos de dados, ou strings, esse registrador


Source
SI é usado para apontar para o endereço de origem de onde os
Index
dados serão lidos.

Destination Trabalhando em conjunto com o registrador acima, este aponta


DI
Index para o endereço destino onde os dados serão gravados.

https://silva97.gitbook.io/assembly-x86/a-base/registradores-gerais 2/8
10/04/2021 Registradores gerais - Assembly x86

Os registradores AX, BX, CX e DX são subdivididos em 2 registradores cada um. Um dos


registradores é mapeado no seu byte mais significativo (Higher byte) e o outro no byte menos
significativo. (Lower byte)
Reparou que os registradores são uma sequência de letras seguido do X?
Para simplificar podemos dizer que os registradores são A, B, C e D.
E o sufixo X serve para mapear todo o registrador, enquanto o sufixo H mapeia o Higher byte e o
sufixo L mapeia o Lower byte.

Ou seja, se alteramos o valor de AL na verdade estamos alterando o byte menos significativo de


AX. E se alteramos AH, então é o byte mais significativo de AX.
Como no exemplo abaixo:

reg-ex1.asm

1 mov ah, 0xaa


2 mov al, 0xbb
3 ; Aqui o valor de AX é 0xaabb

Esse mesmo mapeamento ocorre também nos registradores BX, CX e DX.


Como podemos ver na tabela abaixo:

Do processador 80386 em diante, em real mode é possível usar as versões


estendidas dos registradores existentes em IA-32.
Porém os registradores estendidos de x86-64 só podem ser acessados em submodo
de 64-bit.

Registradores gerais (IA-32)

Como já explicado, no IA-32 os registradores são estendidos para 32 bits de tamanho e ganham
o prefixo 'E', ficando assim a lista:
EAX, EBX, ECX, EDX, ESP, EBP, ESI, EDI 

https://silva97.gitbook.io/assembly-x86/a-base/registradores-gerais 3/8
10/04/2021 Registradores gerais - Assembly x86

Todos os outros registradores gerais existentes em IA-16 não deixam de existir em IA-32. Eles
são mapeados nos 2 bytes menos significativos dos registradores estendidos. Por exemplo o
registrador EAX fica mapeado da seguinte forma:

Já vimos o registrador "EAX" sendo manipulado na nossa PoC, como o prefixo 'E' indica ele é de
32 bits (4 bytes) de tamanho.
Poderíamos simular este registrador com uma union  em C da seguinte forma:

reg.c

1 #include <stdio.h>
2 #include <stdint.h>
3
4 union reg {
5 uint32_t eax;
6
7 struct {
8 uint8_t al;
9 uint8_t ah;
10 } ax;
11 };
12
13 int main(void)
14 {
15 union reg x = { .eax = 0x11223344 };
16
17 printf("AH: %02x\n"
18 "AL: %02x\n"
19 "AX: %04x\n"
20 "EAX: %08x\n",
21 x.ax.ah,
22 x.ax.al,
23 x.ax,
24 x.eax);
25
26 return 0;
27 }

O que deveria gerar a seguinte saída:

https://silva97.gitbook.io/assembly-x86/a-base/registradores-gerais 4/8
10/04/2021 Registradores gerais - Assembly x86

Podemos testar o mapeamento de EAX com nossa PoC:

assembly.asm

1 ; Repare que também adicionei o arquivo main.c


2 ; Veja a aba logo acima
3
4 bits 64
5
6 global assembly
7 assembly:
8 mov eax, 0x11223344
9 mov ax, 0xaabb
10 ret

main.c

1 #include <stdio.h>
2
3 int assembly(void);
4
5 int main(void)
6 {
7 printf("Resultado: %08x\n", assembly());
8 return 0;
9 }

https://silva97.gitbook.io/assembly-x86/a-base/registradores-gerais 5/8
10/04/2021 Registradores gerais - Assembly x86

Na linha 8 alteramos o valor de EAX para 0x11223344  e logo em seguida, na linha 9, alteramos
AX para 0xaabb 
Isto deveria resultar em EAX = 0x1122aabb 

Teste o código e tente alterar AH e/ou AL ao invés de AX diretamente.

Caso ainda não tenha reparado o retorno da nossa função assembly()  é guardado
no registrador EAX. Isto será explicado mais para frente.

Registradores gerais (x86-64)

Os registradores gerais em x86-64 são estendidos para 64 bits e ganham o prefixo 'R', ficando a
lista:
RAX, RBX, RCX, RDX, RSP, RBP, RSI, RDI 

Todos os registradores gerais em IA-32 são mapeados nos 4 bytes menos significativos dos
registradores re-estendidos. Seguindo o mesmo padrão de mapeamento anterior.

Mas estes na verdade são nomes alternativos para o novo padrão de mapeamento do x86-64,
que recebe mais registradores gerais.
Os nomes dos registradores são uma letra 'R' seguido de um número de 0 a 15.
Os primeiros 8 registradores, R0 a R7, são nomes diferentes para os mesmos registradores
gerais listados mais acima.
Como demonstra tabela abaixo:

Nome Alias

R0 RAX

R1 RCX

R2 RDX

R3 RBX

R4 RSP

R5 RBP

https://silva97.gitbook.io/assembly-x86/a-base/registradores-gerais 6/8
10/04/2021 Registradores gerais - Assembly x86

R6 RSI

R7 RDI

Quando se usa o nome no padrão numérico R0  o mapeamento é feito de maneira diferente.
Podemos usar o sufixo 'B' para acessar o byte menos significativo, o sufixo 'W' para acessar a
word (2 bytes) menos significativa e 'D' para acessar a double word (4 bytes) menos
significativa.
Usando RAX/R0 como exemplo, podemos montar a tabela abaixo com o novo mapeamento e
seu alias:

Nome Alias

R0B AL

R0W AX

R0D EAX

O Higher byte (AH por exemplo) de AX, BX, CX e DX não deixa de ser mapeado, ele
apenas não tem um nome no novo padrão que o x86-64 implementa.

Repare que agora é possível acessar o byte menos significativo dos registradores SP, BP, SI e
DI... O que não é possível em IA-32.
Eles também ganham um alias, conforme tabela abaixo:

Nome Alias

R4B SPL

R5B BPL

R6B SIL

R7B DIL

https://silva97.gitbook.io/assembly-x86/a-base/registradores-gerais 7/8
10/04/2021 Registradores gerais - Assembly x86

Os registradores de R8 até R15 não tem nenhum alias.


Esses registradores podem ser usados da maneira que você quiser, assim como os
outros registradores gerais.

Escrita nos registradores gerais (x86-64)

A escrita de dados nos 4 bytes menos significativos de um registrador em x86-64 funciona de


maneira um pouco diferente da que nós estamos acostumados.
Observe o exemplo:

1 mov rax, 0x11223344aabbccdd


2 mov eax, 0x1234

A instrução na linha 2 mudaria o valor de RAX para 0x0000000000001234 


Isto acontece porque o valor é "zero-extended", ou seja, ele é estendido de forma que os 4 bytes
mais significativos são zerados.

O mesmo vale para todos os registradores gerais, incluindo os registradores R8-R15


caso você escreva algum valor em R8D-R15D

https://silva97.gitbook.io/assembly-x86/a-base/registradores-gerais 8/8
10/04/2021 Endereçamento - Assembly x86

Endereçamento
Acessando a memória RAM

O acesso a operandos na memória principal é feito definindo alguns fatores que, após serem
calculados pelo processador, resultam no endereço físico que será utilizado a partir do
barramento de endereço para acessar aquela região da memória.
Do ponto de vista do programador são apenas algumas somas e multiplicações.

O endereçamento de um operando também pode ser chamado de endereço efetivo,


ou em inglês, effective address.

Não tente ler ou modificar a memória com nossa PoC ainda.


No final do tópico eu falo sobre a instrução LEA que pode ser usada para testar o
endereçamento.

Endereçamento em IA-16

No código de máquina da arquitetura IA-16 existe um byte chamado ModR/M que serve para
especificar algumas informações relacionadas ao acesso de (R)egistradores e/ou (M)emória.
O endereçamento em IA-16 é totalmente especificado neste byte, e ele nos permite fazer um
cálculo no seguinte formato:
REG + REG + DESLOCAMENTO 

Onde REG  seria o nome de um registrador e DESLOCAMENTO  um valor numérico também


somado ao endereço.
Os registradores BX, BP, SI e DI  podem ser utilizados. Enquanto o deslocamento é um valor
de 8 ou 16 bits.

Neste cálculo um dos registradores é usado como base, o endereço inicial, e o outro é usado
como índice, um valor numérico a ser somado assim como o deslocamento.
BX e BP  são usados para base enquanto SI e DI  são usados para índice. Perceba que não
https://silva97.gitbook.io/assembly-x86/a-base/enderecamento 1/5
10/04/2021 Endereçamento - Assembly x86

é possível somar base+base  e nem índice+índice 


Alguns exemplos para facilitar o entendimento:

1 mov [bx], ax ; Correto!


2 mov [bx+si], ax ; Correto!
3 mov [bp+di], ax ; Correto!
4 mov [bp+si], ax ; Correto!
5 mov [bx+di + 0xa1], ax ; Correto!
6 mov [si], ax ; Correto!
7 mov [0x1a], ax ; Correto!
8
9 mov [dx], ax ; ERRADO!
10 mov [bx+bp], ax ; ERRADO!
11 mov [si+di], ax ; ERRADO!

Endereçamento em IA-32

Em IA-32 o código de máquina tem também o byte SIB, que é um novo modo de endereçamento.
Enquanto em IA-16 nós temos apenas uma base e um índice, em IA-32 nós ganhamos também
um fator de escala.

O fator de escala é basicamente um número que irá multiplicar o valor de índice.

A escala pode ser 1, 2, 4 ou 8.


O registrador de índice pode ser qualquer um dos registradores gerais exceto ESP.
O registrador de base pode ser qualquer registrador geral.
O deslocamento pode ser de 8 ou 32 bits.

Exemplos:

1 mov [edx], eax ; Correto!


2 mov [ebx+ebp], eax ; Correto!
3 mov [esi+edi], eax ; Correto!
4 mov [esp+ecx], eax ; Correto!
5 mov [ebx*4 + 0x1a], eax ; Correto!
6 mov [ebx + ebp*8 + 0xab12cd34], eax ; Correto!
7 mov [esp + ebx*2], eax ; Correto!
8 mov [0xffffaaaa], eax ; Correto!
9
10 mov [esp*2], eax ; ERRADO!

https://silva97.gitbook.io/assembly-x86/a-base/enderecamento 2/5
10/04/2021 Endereçamento - Assembly x86

SIB é sigla para Scale, Index and Base. Que são os três valores usados para calcular
o endereço efetivo.

Endereçamento em x86-64

Em x86-64 segue a mesma premissa de IA-32, com alguns adendos:

É possível usar registradores de 32 ou 64 bit.


Os registradores de R8 a R15 ou R8D a R15D podem ser usados como base ou índice.
Não é possível mesclar 32 e 64 bits.

Exemplos:

1 mov [rbx], rax ; Correto!


2 mov [ebx], rax ; Correto!
3 mov [r15 + r10*4], rax ; Correto!
4 mov [r15d + r10d*4], rax ; Correto!
5
6 mov [r10 + r15d], rax ; ERRADO!
7 mov [rsp*2], rax ; ERRADO!

Truque do nasm

Cuidado para não se confundir em relação ao fator de escala. Veja por exemplo esta instrução
64-bit:

mov [rbx*3], rax

Apesar de 3 não ser um valor válido de escala o nasm irá montar o código sem apresentar erros.
Isto acontece porque ele converteu a instrução para a seguinte:

mov [rbx + rbx*2], rax

https://silva97.gitbook.io/assembly-x86/a-base/enderecamento 3/5
10/04/2021 Endereçamento - Assembly x86

Ele usa RBX tanto como base como também índice, e usa o fator de escala 2.
Resultando no mesmo valor que se multiplicasse RBX por 3.
Este é um truque do nasm que pode levar ao erro, por exemplo:

mov [rsi + rbx*3], rax

Desta vez acusaria erro já que a base foi explicitada.


Lembre-se que os fatores de escala válidos são 1, 2, 4 ou 8.

Instrução LEA

lea registrador, [endereço]

A instrução LEA, sigla para Load Effective Address, calcula o endereço efetivo do segundo
operando e armazena o resultado do cálculo em um registrador.
Esta instrução pode ser útil para testar o cálculo do effective address e ver os resultados
usando nossa PoC, conforme exemplo abaixo:

assembly.asm

1 bits 64
2
3 global assembly
4 assembly:
5 mov rbx, 5
6 mov rcx, 10
7 lea eax, [rcx + rbx*2 + 5]
8 ret

https://silva97.gitbook.io/assembly-x86/a-base/enderecamento 4/5
10/04/2021 Endereçamento - Assembly x86

main.c

1 #include <stdio.h>
2
3 int assembly(void);
4
5 int main(void)
6 {
7 printf("Resultado: %d\n", assembly());
8 return 0;
9 }

https://silva97.gitbook.io/assembly-x86/a-base/enderecamento 5/5
10/04/2021 Pilha - Assembly x86

Pilha
Pilhas são muito úteis

Uma pilha, em inglês stack, é uma estrutura de dados LIFO -- Last In First Out -- o que significa
que o último dado a entrar é o primeiro a sair.
Imagine uma pilha de livros onde você vai colocando um livro sobre o outro e, após empilhar
tudo, você resolve retirar um de cada vez.
Ao retirar os livros você vai retirando desde o topo até a base, ou seja, os livros saem na ordem
inversa em que foram colocados. O que significa que o último livro que você colocou na pilha vai
ser o primeiro a ser retirado, isto é LIFO.

Hardware Stack

Processadores da arquitetura x86 tem implementações nativas de uma pilha que é representada
na memória RAM, onde esta pode ser manipulada por instruções específicas da arquitetura ou
diretamente como qualquer outra região da memória.
Esta pilha normalmente é chamada de hardware stack.

O registrador SP/ESP/RSP, Stack Pointer, serve como ponteiro para o topo da pilha podendo ser
usado como referência inicial para manipulação de valores na mesma.

A manipulação básica da pilha é empilhar(push) e desempilhar (pop) valores na mesma. Veja


exemplo na nossa PoC:

https://silva97.gitbook.io/assembly-x86/a-base/pilha 1/3
10/04/2021 Pilha - Assembly x86

assembly.asm

1 bits 64
2
3 global assembly
4 assembly:
5 mov rax, 12345
6 push rax
7
8 mov rax, 112233
9 pop rax
10 ret

main.c

1 #include <stdio.h>
2
3 int assembly(void);
4
5 int main(void)
6 {
7 printf("Resultado: %d\n", assembly());
8 return 0;
9 }

Na linha 6  empilhamos o valor de RAX na pilha, alteramos o valor na linha 8  mas logo em
seguida desempilhamos o valor e jogamos devolta em RAX.
O resultado disso seria o valor 12345  sendo retornado pela função.

No caso a instrução pop  recebe como operando um registrador ou endereçamento de


memória onde ele deve armazenar o valor desempilhado.

A instrução push  recebe como operando o valor a ser empilhado. O tamanho de cada valor na
pilha também acompanha o barramento interno. (64 bits em 64-bit, 32 bits em protected mode e
16 bits em real mode)
Pode-se passar como operando um valor na memória, registrador ou valor imediato.

https://silva97.gitbook.io/assembly-x86/a-base/pilha 2/3
10/04/2021 Pilha - Assembly x86

A pilha "cresce" para baixo, o que significa que toda vez que um valor é inserido nela
o valor de ESP é subtraído pelo tamanho em bytes do valor. E na mesma lógica um
pop  incrementa o valor de ESP.

https://silva97.gitbook.io/assembly-x86/a-base/pilha 3/3
10/04/2021 Saltos - Assembly x86

Saltos
Desviando o fluxo do código

Provavelmente você já sabe o que é um desvio de fluxo do código em uma linguagem de alto
nível. Algo como uma instrução if  que condicionalmente executa um determinado bloco de
código, ou um for  que executa várias vezes o mesmo bloco de código.
Tudo isto é possível devido ao desvio do fluxo de código. Vamos a um pseudo-exemplo de um
if :

1 1. Compare o valor de X com Y


2 2. Se o valor de X for maior, pule para 4.
3 3. Adicione 2 ao valor de X
4 4.

Repare que se a comparação no passo 1 der que o valor de X é maior, a instrução no passo 2 faz
um desvio para o passo 4... Deste jeito o passo 3 nunca será executado.
Porém caso a condição no passo 2 for falsa, isto é, o valor de X não é maior do que o valor de Y,
então o desvio não irá acontecer e o passo 3 será executado.

Ou seja, o passo 3 só será executado sob uma determinada condição. Isto é um código
condicional, isto é um if .
Repare que o resultado da comparação no passo 1 precisa ficar armazenado em algum lugar, e
este "lugar" é o registrador FLAGS.

Salto não condicional

Antes de vermos um desvio de fluxo condicional, vamos entender como é o próprio desvio de
fluxo em si.
Na verdade existem muito mais registradores do que os que eu já citei. E um deles é o
registrador IP, sigla para Instruction Pointer. (ponteiro de instrução)
Este registrador também acompanha o tamanho do barramento interno, assim como os
registradores gerais:

https://silva97.gitbook.io/assembly-x86/a-base/saltos 1/6
10/04/2021 Saltos - Assembly x86

IP EIP RIP

16 bits 32 bits 64 bits

Assim como o nome sugere, o Instruction Pointer serve como um ponteiro para a próxima
instrução a ser executada pelo processador.
Desse jeito é possível mudar o fluxo do código simplesmente alterando o valor de IP, porém não
é possível fazer isso diretamente com uma instrução como a mov .

Na arquitetura x86 existem as instruções de jump, salto em inglês, que alteram o valor de IP
permitindo assim que o fluxo seja alterado.
A instrução de jump não condicional, intuitivamente, se chama JMP.
Na prática uma instrução goto  em C nada mais é que uma instrução JMP, após o código ser
compilado.
Seu uso é:

jmp endereço

Onde o operando você pode passar um rótulo que o assembler irá converter para o endereço
corretamente.
Veja o exemplo na nossa PoC:

assembly.asm

1 bits 64
2
3 global assembly
4 assembly:
5 mov eax, 555
6 jmp .end
7
8 mov eax, 333
9
10 .end:
11 ret

https://silva97.gitbook.io/assembly-x86/a-base/saltos 2/6
10/04/2021 Saltos - Assembly x86

main.c

1 #include <stdio.h>
2
3 int assembly(void);
4
5 int main(void)
6 {
7 printf("Resultado: %d\n", assembly());
8 return 0;
9 }

A instrução na linha 8 nunca será executada devido ao JMP na linha 6.

Repare que na linha 10 estamos usando um rótulo local, que foi explicado no tópico
sobre a sintaxe do nasm.

Registrador FLAGS

O registrador FLAGS também é estendido junto ao tamanho do barramento interno. Então


temos:

FLAGS EFLAGS RFLAGS

16 bits 32 bits 64 bits

Este registrador, diferente dos registradores gerais, não pode ser acessado diretamente por uma
instrução.
O valor de cada bit do registrador é testado por determinadas instruções, e são ligados e
desligados por outras instruções.
É testando o valor dos bits do registrador FLAGS que as instruções condicionais funcionam.

Salto condicional

Os jumps condicionais, normalmente referido como Jcc, são instruções que condicionalmente
fazem o desvio de fluxo do código.
https://silva97.gitbook.io/assembly-x86/a-base/saltos 3/6
10/04/2021 Saltos - Assembly x86

Elas verificam os valores dos bits do registrador FLAGS e, com base nos valores, será decidido
se o salto será tomado ou não.
Assim como no caso do JMP, as instruções Jcc também recebem como operando o endereço
para onde devem tomar o salto caso a condição seja atendida.
Se ela não for, então o fluxo de código continuará normalmente.
Eis a lista dos saltos condicionais mais comuns:

Instrução Nome estendido Condição

JE Jump if Equal Pula se for igual

JNE Jump if Not Equal Pula se não for igual

JL Jump if Less than Pula se for menor que

JG Jump if Greater than Pula se for maior que

JLE Jump if Less or Equal Pula se for menor ou igual

JGE Jump if Greater or Equal Pula se for maior ou igual

O nome Jcc para se referir aos saltos condicionais vem do prefixo 'J' seguido de 'cc'
para indicar uma condição, que é o formato da nomenclatura das instruções.
Exemplo: JLE -- 'J' prefixo, 'LE' condição (Less or Equal)
Essa mesma nomenclatura também é usada para as outras instruções condicionais,
como por exemplo CMOVcc.

A maneira mais comum usada para setar as flags para um salto condicional é a instrução CMP.
Ela recebe dois operandos e compara o valor dos dois, com base no resultado da comparação
ela seta as flags corretamente.
Agora um exemplo na nossa PoC:

https://silva97.gitbook.io/assembly-x86/a-base/saltos 4/6
10/04/2021 Saltos - Assembly x86

assembly.asm

1 bits 64
2
3 global assembly
4 assembly:
5 mov eax, 0
6
7 mov rbx, 7
8 mov rcx, 5
9 cmp rbx, rcx
10 jle .end
11
12 mov eax, 1
13 .end:
14 ret

main.c

1 #include <stdio.h>
2
3 int assembly(void);
4
5 int main(void)
6 {
7 printf("Resultado: %d\n", assembly());
8 return 0;
9 }

Na linha 10 temos um Jump if Less or Equal para o rótulo local .end , e logo na linha anterior
uma comparação entre RBX e RCX.
Se o valor de RBX for menor ou igual a RCX, então o salto será tomado e a instrução na linha 12
não será executada.
Desta forma, temos algo muito parecido com um if  no pseudocódigo abaixo:

1 eax = 0;
2 rbx = 7;
3 rcx = 5;
4 if(rbx > rcx){

https://silva97.gitbook.io/assembly-x86/a-base/saltos 5/6
10/04/2021 Saltos - Assembly x86

5 eax = 1;
6 }
7 return;

Repare que a condição para o código ser executado é exatamente o oposto da condição para o
salto ser tomado.
Afinal de contas, a lógica é que caso o salto seja tomado o código não será executado.

Experimente modificar os valores de RBX e RCX, e também teste usando outros Jcc.

https://silva97.gitbook.io/assembly-x86/a-base/saltos 6/6
10/04/2021 Procedimentos - Assembly x86

Procedimentos
Não existem funções em Assembly?

O conceito de um procedimento nada mais é que um pedaço de código que, em determinado


momento, é convocado para ser executado e, logo em seguida, o processador volta a executar
as instruções em sequência.
Isto nada mais é que uma combinação de dois desvios de fluxo de código, um para a execução
do procedimento e outro no fim dele para voltar o fluxo de código para a instrução seguinte a
convocação do procedimento.
Veja o exemplo:

1 * Define A para 3
2 * Chama o procedimento setarA
3 * Compara A e 5
4 * Finaliza o código
5
6 setarA:
7 * Define A para 5
8 * Retorna

Seguindo o fluxo de execução do código, a sequência de instruções fica assim:

1 * Definimos o valor de A para 3


2 * Definimos o valor de A para 5
3 * Comparamos o valor de A com 5
4 * Finalizamos o código

Deste jeito se nota que a comparação na linha 3 vai dar positiva porque o valor de A foi setado
para 5 dentro do procedimento setarA .

Em Assembly x86 temos duas instruções principais para o uso de procedimentos:

Instrução Operando Ação

https://silva97.gitbook.io/assembly-x86/a-base/procedimentos 1/4
10/04/2021 Procedimentos - Assembly x86

CALL endereço Chama um procedimento no endereço especificado

RET ??? Retorna de um procedimento

A esta altura você já deve ter reparado que nossa função assembly  na nossa PoC nada mais é
que um procedimento chamado por uma instrução CALL, por isso no final dela temos uma
instrução RET.

Na prática o que uma instrução CALL faz é empilhar o endereço da instrução seguinte na stack
e, logo em seguida, faz o desvio de fluxo para o endereço especificado assim como um JMP.
E a instrução RET basicamente desempilha este endereço e faz o desvio de fluxo para o
mesmo.
Um exemplo na nossa PoC:

assembly.asm

1 bits 64
2
3 global assembly
4 assembly:
5 mov eax, 3
6 call setarA
7
8 ret
9
10 setarA:
11 mov eax, 5
12 ret

https://silva97.gitbook.io/assembly-x86/a-base/procedimentos 2/4
10/04/2021 Procedimentos - Assembly x86

main.c

1 #include <stdio.h>
2
3 int assembly(void);
4
5 int main(void)
6 {
7 printf("Resultado: %d\n", assembly());
8 return 0;
9 }

Na linha 6 damos um call  no procedimento setarA  na linha 10, este por sua vez altera o
valor de EAX antes de retornar.

O que são convenções de chamadas?

É seguindo esta lógica que "milagrosamente" o nosso código em C sabe que o valor em EAX é o
valor de retorno da nossa função assembly .
Linguagens de alto nível, como C por exemplo, usam um conjunto de regras para definir como
uma função deve ser chamada e como ela retorna um valor.
Estas regras são a convenção de chamada, em inglês, calling convention.

Na nossa PoC a função assembly  retorna uma variável do tipo int , que na arquitetura x86
tem o tamanho de 4 bytes e é retornado no registrador EAX.
A maioria dos valores serão retornados em alguma parte mapeada de RAX, que coincida com o
mesmo tamanho do tipo. Exemplos:

Tipo Tamanho em x86-64 Registrador

char 1 byte AL

short int 2 bytes AX

int 4 bytes EAX

char * 8 bytes RAX

https://silva97.gitbook.io/assembly-x86/a-base/procedimentos 3/4
10/04/2021 Procedimentos - Assembly x86

Por enquanto não vamos ver a convenção de chamada que a linguagem C usa, só estou
adiantando isso para que possamos entender melhor como nossa função assembly  funciona.

Em um código em C não tente adivinhar o tamanho em bytes de um tipo. Para cada


arquitetura diferente que você compilar o código, o tipo pode ter um tamanho
diferente.
Sempre que precisar do tamanho de um tipo use o operador sizeof .

https://silva97.gitbook.io/assembly-x86/a-base/procedimentos 4/4
10/04/2021 Seções e símbolos - Assembly x86

Seções e símbolos
Entendendo um pouco do arquivo objeto

A esta altura você já deve ter reparado que nossa função assembly  está em um arquivo
separado da função main , mas de alguma maneira mágica a função pode ser executada e seu
retorno capturado.
Isto acontece graças a uma ferramenta chamada linker.

O arquivo objeto

Um arquivo objeto é um formato de arquivo especial que permite organizar código e várias
informações relacionadas a ele.
Os arquivos .o que geramos com a compilação da nossa PoC são arquivos objetos, eles
organizam informações que serão usadas pelo linker na hora de gerar o executável.
Dentre estas informações, além do código em si, tem duas principais que são as seções e os
símbolos.

As seções

Uma seção no arquivo objeto nada mais é que uma maneira de agrupar dados no arquivo. É
como criar um grupo novo e dar um sentido para ele.
Três exemplos principais de seções são:

A seção de código, onde o código que é executado pelo processador fica.


Seção de dados, onde variáveis são alocadas.
Seção de dados não inicializada, onde a memória será alocada dinamicamente ao carregar
o executável na memória. Geralmente usada para variáveis não inicializadas, isto é, que não
tiveram um valor inicial definido.

Na prática se pode definir quantas seções quiser e para quais propósitos quiser também.
Podemos até mesmo ter mais de uma seção de código, mais de uma seção de dados etc.

O código em C é organizado pelo compilador, no nosso caso o GCC, e por isso nós não fizemos
este tipo de organização manualmente.
Existem quatro seções principais que podemos usar no nosso código e o linker irá resolvê-las
https://silva97.gitbook.io/assembly-x86/a-base/secoes-e-simbolos 1/4
10/04/2021 Seções e símbolos - Assembly x86

corretamente sem que nós precisamos dizer a ele como fazer seu trabalho. O nasm também
reconhece essas seções como "padrão" e já configura os atributos delas corretamente.

.text  -- Usada para armazenar o código executável do nosso programa.

.data  -- Usada para armazenar dados inicializados do programa, por exemplo uma

variável global.
.bss  -- Usada para reservar espaço para dados não-inicializados, por exemplo uma

variável global que foi declarada mas não teve um valor inicial definido.
.rodata  -- Usada para armazenar dados que sejam somente leitura (readonly), por

exemplo uma constante que nunca deve ter seu valor alterado.

Estes nomes de seções são padronizados e códigos em C geralmente usa estas


seções com estes mesmos nomes.

Seções tem flags que definem atributos para a seção, as duas flags principais e que nos importa
saber é:

write  -- Dá permissão de escrita para a seção, assim o código executado pode escrever

dados nela.
exec  -- Dá permissão de executar os dados contidos na seção como código.

Os símbolos

Uma das informações salvas no arquivo objeto é a tabela de símbolos que é, como o nome
sugere, uma tabela que define nomes e endereços para determinados símbolos usados no
arquivo objeto.
Um símbolo nada mais é que um nome para se referir a determinado endereço... Parece
familiar? Pois é, símbolos e rótulos são essencialmente a mesma coisa.
A única diferença prática é que o rótulo apenas existe como conceito no arquivo fonte, e o
símbolo existe como um valor no arquivo objeto.
Quando definimos um rótulo em Assembly podemos "exportá-lo" como um símbolo para que
outros arquivos objetos possam acessar aquele determinado endereço.

Já vimos isto ser feito na nossa PoC, a diretiva global  do nasm serve justamente para definir
que aquele rótulo é global... Ou seja, que deve ser possível acessá-lo a partir de outros arquivos
objetos.

https://silva97.gitbook.io/assembly-x86/a-base/secoes-e-simbolos 2/4
10/04/2021 Seções e símbolos - Assembly x86

O linker

O linker é o software encarregado de processar os arquivos objetos para que eles possam
"conversar" entre sí. Por exemplo, um símbolo definido no arquivo objeto assembly.o para que
possa ser acessado no arquivo main.o o linker precisa intermediar, porque os arquivos não vão
trocar informação por mágica.

Na nossa PoC o arquivo objeto main.o avisa para o linker que ele está acessando um símbolo
externo (que está em outro arquivo objeto) chamado assembly .
O linker então se encarrega de procurar por esse símbolo, e ele acaba o achando no assembly.o.
Ao achar o linker calcula o endereço para aquele símbolo e seja lá aonde ele foi utilizado em
main.o o linker irá colocar o endereço correto.

Todas essas informações (os locais onde foi utilizado, o endereço do símbolo, os símbolos
externos acessados, os símbolos exportados etc.) ficam na tabela de símbolos.
Com a maravilhosa ferramenta objdump do GCC podemos ver a tal da tabela de símbolos nos
nossos arquivos objetos. Basta rodar o comando:

$ objdump -t arquivo_objeto

Se usarmos essa tool nos nossos arquivos objetos podemos ver que, dentre vários símbolos lá
encontrados, um deles é o assembly .

https://silva97.gitbook.io/assembly-x86/a-base/secoes-e-simbolos 3/4
10/04/2021 Seções e símbolos - Assembly x86

O executável

Depois do linker fazer o trabalho dele, ele gera o arquivo final que nós normalmente chamamos
de executável.
O executável de um sistema operacional nada mais é que um arquivo objeto que pode ser
executado.
A diferença deste arquivo objeto final para o arquivo objeto anterior, é que este está organizado
de acordo com as "exigências" do sistema operacional e pronto para ser rodado.
Enquanto o outro só tem informação referente àquele arquivo fonte, sem dar as informações
necessárias para o sistema operacional poder rodá-lo como código.
Até porque este código ainda não está pronto para ser executado, ainda há símbolos e outras
dependências para serem resolvidas pelo linker.

https://silva97.gitbook.io/assembly-x86/a-base/secoes-e-simbolos 4/4
10/04/2021 Instruções - Assembly x86

Instruções
Algumas instruções do ASM x86

Já expliquei os conceitos principais por trás da linguagem Assembly da arquitetura x86, agora
que já entendemos como a base funciona precisamos nos munir de algumas instruções para
poder fazer códigos mais complexos.
Pensando nisso vou listar aqui algumas instruções e uma explicação bem básica de como
utilizá-la.

Formato de uma instrução

Já expliquei a sintaxe de uma instrução, mas não expliquei o formato em si da instrução no


código de máquina. Para simplificar aqui uma instrução pode ter os seguintes operandos:

Um operando registrador
Um operando registrador OU operando na memória
Um valor imediato, que é um operando digitado diretamente

Basicamente são três tipos de operandos: Um registrador, valor na memória e um valor


imediato. Um exemplo de cada um para ilustrar sendo mostrado como o segundo operando de
MOV:

1 mov eax, ebx ; EBX = Registrador


2 mov eax, [ebx] ; [EBX] = Memória
3 mov eax, 65 ; 65 = Valor imediato
4 mov eax, "A" ; "A" = Valor imediato, mesmo que 65

Como demonstrado na linha 4, strings podem ser passadas como um operando


imediato. O assembler irá converter a string em sua respectiva representação em
bytes, só que é necessário ter atenção em relação ao tamanho da string que não
pode ser maior do que o operando destino.

https://silva97.gitbook.io/assembly-x86/a-base/instrucoes 1/15
10/04/2021 Instruções - Assembly x86

São três operandos diferentes e cada um deles é opcional, isto é, pode ou não ser utilizado pela
instrução. (opcional para a instrução e não para nós)
Repare que somente um dos operandos pode ser um valor na memória ou registrador, enquanto
o outro é especificamente um registrador.
É devido a isto que nasce a limitação que haver apenas um operando na memória, enquanto que
o uso de dois registradores é permitido.

Notação

Irei utilizar uma explicação simplificada aqui que irá deixar muita informação
importante de fora. Depois irei especificar as instruções com mais detalhes.

As seguintes nomenclaturas serão utilizadas:

Nomenclatura Significado

reg Um operando registrador

r/m Um operando registrador ou na memória

imm Um operando imediato

Denota um endereço, geralmente se usa um rótulo. Na prática é um valor


addr
imediato assim como o operando imediato.

Em alguns casos eu posso colocar um número junto a esta nomenclatura para especificar o
tamanho do operando em bits. Por exemplo r/m16  indica um operando registrador/memória
de 16 bits.

Em cada instrução irei apresentar a notação demonstrando cada combinação diferente de


operandos que é possível utilizar.
Lembrando que o operando destino é o mais a esquerda, enquanto que o operando fonte é o
operando mais a direita.

Cada nome de uma instrução em Assembly é um mnemônico, que é basicamente


uma abreviatura feita para fácil memorização.

https://silva97.gitbook.io/assembly-x86/a-base/instrucoes 2/15
10/04/2021 Instruções - Assembly x86

Pensando nisto, leia cada instrução com seu nome extenso equivalente para lembrar
o que ela faz.
No título de cada instrução irei deixar após um "|" o nome extenso da instrução para
facilitar nesta tarefa.

MOV | Move

1 mov reg, r/m


2 mov reg, imm
3 mov r/m, reg
4 mov r/m, imm

Copia o valor do operando fonte para o operando destino.

pseudo.c

1 destiny = source;

ADD

1 add reg, r/m


2 add reg, imm
3 add r/m, reg
4 add r/m, imm

Soma o valor do operando destino com o valor do operando fonte.

pseudo.c

1 destiny = destiny + source;

SUB | Subtract

https://silva97.gitbook.io/assembly-x86/a-base/instrucoes 3/15
10/04/2021 Instruções - Assembly x86

1 sub reg, r/m


2 sub reg, imm
3 sub r/m, reg
4 sub r/m, imm

Subtrai o valor do operando destino com o valor do operando fonte.

pseudo.c

1 destiny = destiny - source;

INC | Increment

inc r/m

Incrementa o valor do operando destino em 1.

pseudo.c

1 destiny++;

DEC | Decrement

dec r/m

Decrementa o valor do operando destino em 1.

pseudo.c

1 destiny--;

https://silva97.gitbook.io/assembly-x86/a-base/instrucoes 4/15
10/04/2021 Instruções - Assembly x86

MUL | Multiplicate

mul r/m

Multiplica uma parte do mapeamento de RAX pelo operando passado.


Com base no tamanho do operando uma parte diferente de RAX será multiplicada e o resultado
armazenado em um registrador diferente.

Operando 1 Operando 2 Destino

AL r/m8 AX

AX r/m16 DX:AX

EAX r/m32 EDX:EAX

RAX r/m64 RDX:RAX

No caso por exemplo de DX:AX, os registradores de 16 bits são usados em conjunto para
representar o valor de 32 bits. Onde DX armazena os 2 bytes mais significativos do valor, e AX
os 2 bytes menos significativos.

pseudo.c

1 // Se operando de 8 bits
2 AX = AL * operand;
3
4 // Se operando de 16 bits
5 aux = AX * operand;
6 DX = (aux & 0xffff0000) >> 16;
7 AX = aux & 0x0000ffff;

DIV | Divide

div r/m

https://silva97.gitbook.io/assembly-x86/a-base/instrucoes 5/15
10/04/2021 Instruções - Assembly x86

Seguindo uma premissa inversa de MUL, faz a divisão de um valor pelo operando passado e
armazena o quociente e a sobra desta divisão.

Operando 1 Operando 2 Destino quociente Destino sobra

AX r/m8 AL AH

DX:AX r/m16 AX DX

EDX:EAX r/m32 EAX EDX

RDX:RAX r/m64 RAX RDX

pseudo.c

1 // Se operando de 8 bits
2 AL = AX / operand;
3 AH = AX % operand;

LEA | Load Effective Address

lea reg, mem

Calcula o endereço efetivo do operando fonte e armazena o resultado no endereço destino.

pseudo.c

1 destiny = address_of(source);

AND

1 and reg, r/m


2 and reg, imm
3 and r/m, reg
4 and r/m, imm

https://silva97.gitbook.io/assembly-x86/a-base/instrucoes 6/15
10/04/2021 Instruções - Assembly x86

Faz uma operação E bit a bit nos operandos e armazena o resultado no operando destino.

pseudo.c

1 destiny = destiny & source;

OR

1 or reg, r/m
2 or reg, imm
3 or r/m, reg
4 or r/m, imm

Faz uma operação OU bit a bit nos operandos e armazena o resultado no operando destino.

pseudo.c

1 destiny = destiny | source;

XOR | Exclusive OR

1 xor reg, r/m


2 xor reg, imm
3 xor r/m, reg
4 xor r/m, imm

Faz uma operação OU Exclusivo bit a bit nos operandos e armazena o resultado no operando
destino.

pseudo.c

1 destiny = destiny ^ source;

https://silva97.gitbook.io/assembly-x86/a-base/instrucoes 7/15
10/04/2021 Instruções - Assembly x86

XCHG | Exchange

1 xchg reg, r/m


2 xchg r/m, reg

O operando destino recebe o valor do operando fonte, e o operando fonte recebe o valor anterior
do operando destino.
Fazendo assim uma troca nos valores dos mesmos.

1 auxiliary = destiny;
2 destiny = source;
3 source = auxiliary;

XADD | Exchange and Add

xadd r/m, reg

O operando fonte recebe o valor do operando destino e, em seguida, o operando destino é


somado com o valor anterior do operando fonte.
Basicamente preserva o valor do destino ao mesmo tempo que faz um ADD nele.

pseudo.c

1 auxiliary = source;
2 source = destiny;
3 destiny = destiny + auxiliary;

SHL | Shift Left

1 shl r/m
2 shl r/m, imm
3 shl r/m, CL

https://silva97.gitbook.io/assembly-x86/a-base/instrucoes 8/15
10/04/2021 Instruções - Assembly x86

Faz o deslocamento de bits do operando destino para a esquerda com base no número
especificado no operando fonte.
Se o operando fonte não é especificado, então faz o shift left apenas 1 vez.

pseudo.c

1 destiny = destiny << 1; // Se: shl r/m


2 destiny = destiny << source; // Nos outros casos

SHR | Shift Right

1 shr r/m
2 shr r/m, imm
3 shr r/m, CL

Mesmo caso que SHL, porém faz o deslocamento de bits para a direita.

pseudo.c

1 destiny = destiny >> 1; // Se: shr r/m


2 destiny = destiny >> source; // Nos outros casos

CMP | Compare

1 cmp r/m, imm


2 cmp r/m, reg
3 cmp reg, r/m

Compara o valor dos dois operandos e define RFLAGS de acordo.

pseudo.c

1 RFLAGS = compare(operand1, operand2);

https://silva97.gitbook.io/assembly-x86/a-base/instrucoes 9/15
10/04/2021 Instruções - Assembly x86

SETcc | Set byte if Condition

SETcc r/m8

Define o valor do operando de 8 bits para 1 ou 0 dependendo se a condição for atendida (1) ou
não (0).
Assim como no caso dos jumps condicionais, o 'cc' aqui denota uma sigla para uma condição.
Cuja elas podem ser as mesmas utilizadas nos jumps. Exemplo:

1 sete al
2 ; Se RFLAGS indica um valor igual, AL = 1. Se não AL = 0

pseudo.c

1 if(verify_rflags(condition) == true) destiny = 1;


2 else destiny = 0;

CMOVcc | Conditional Move

CMOVcc reg, r/m

Basicamente uma instrução MOV condicional, só irá definir o valor do operando destino caso a
condição seja atendida.

pseudo.c

1 if(verify_rflags(condition) == true) destiny = source;

NEG | Negate

https://silva97.gitbook.io/assembly-x86/a-base/instrucoes 10/15
10/04/2021 Instruções - Assembly x86

neg r/m

Inverte o sinal do valor numérico do operando.

pseudo.c

1 destiny = -destiny;

NOT

not r/m

Faz uma operação NÃO bit a bit no operando.

pseudo.c

1 destiny = ~destiny;

MOVSB/MOVSW/MOVSD/MOVSQ | Move String

1 movsb ; byte (1 byte)


2 movsw ; word (2 bytes)
3 movsd ; double word (4 bytes)
4 movsq ; quad word (8 bytes)

Copia um valor do tamanho de um byte, word, double word ou quad word a partir do endereço
apontado por RSI (Source Index) para o endereço apontado por RDI. (Destiny Index).
Depois disso incrementa o valor dos dois registradores com base no tamanho do dado que foi
movido.

pseudo.c
https://silva97.gitbook.io/assembly-x86/a-base/instrucoes 11/15
10/04/2021 Instruções - Assembly x86

1 // Se MOVSW
2 word [RDI] = word [RSI];
3 RDI = RDI + 2;
4 RSI = RSI + 2;

CMPSB/CMPSW/CMPSD/CMPSQ | Compare String

1 cmpsb ; byte (1 byte)


2 cmpsw ; word (2 bytes)
3 cmpsd ; double word (4 bytes)
4 cmpsq ; quad word (8 bytes)

Compara os valores na memória apontados por RDI e RSI, e depois incrementa os registradores
com base no tamanho do dado.

pseudo.c

1 // CMPSW
2 RFLAGS = compare(word [RDI], word [RSI]);
3 RDI = RDI + 2;
4 RSI = RSI + 2;

LODSB/LODSW/LODSD/LODSQ | Load String

1 lodsb ; byte (1 byte)


2 lodsw ; word (2 bytes)
3 lodsd ; double word (4 bytes)
4 lodsq ; quad word (8 bytes)

Copia o valor na memória apontado por RSI para uma parte do mapeamento de RAX equivalente
ao tamanho do dado, e depois incrementa RSI de acordo.

pseudo.c

1 // LODSW
2 AX = word [RSI];
3 RSI = RSI + 2;
https://silva97.gitbook.io/assembly-x86/a-base/instrucoes 12/15
10/04/2021 Instruções - Assembly x86

SCASB/SCASW/SCASD/SCASQ | Scan String

1 scasb ; byte (1 byte)


2 scasw ; word (2 bytes)
3 scasd ; double word (4 bytes)
4 scasq ; quad word (8 bytes)

Compara o valor em uma parte mapeada de RAX com o valor na memória apontado por RDI, e
depois incrementa RDI de acordo.

pseudo.c

1 // SCASW
2 RFLAGS = compare(AX, word [RDI]);
3 RDI = RDI + 2;

STOSB/STOSW/STOSD/STODQ | Store String

1 stosb ; byte (1 byte)


2 stosw ; word (2 bytes)
3 stosd ; double word (4 bytes)
4 stosq ; quad word (8 bytes)

Copia o valor de uma parte mapeada de RAX e armazena na memória apontada por RDI, depois
incrementa RDI de acordo.

pseudo.c

1 // STOSW
2 word [RDI] = AX;
3 RDI = RDI + 2;

LOOP/LOOPE/LOOPNE
https://silva97.gitbook.io/assembly-x86/a-base/instrucoes 13/15
10/04/2021 Instruções - Assembly x86

pseudo.c

1 loop addr8
2 loope addr8
3 loopne addr8

Essas instruções são utilizadas para gerar procedimentos de laço (loop) usando o registrador
RCX como contador.
Elas primeiro decrementam o valor de RCX e comparam o mesmo com o valor zero. Se RCX for
diferente de zero, a instrução faz um salto para o endereço passado com operando, senão o
fluxo de código continua normalmente.
No caso de loope  e loopne , os sufixos indicam a condição de igual e não igual
respectivamente.
Ou seja, além da comparação do valor de RCX elas também verificam o valor de RFLAGS como
uma condição extra.

pseudo.c

1 // loop
2 RCX = RCX - 1;
3 if(RCX != 0) goto operand;
4
5 // loope
6 RCX = RCX - 1;
7 if(RCX != 0 && verify_rflags(EQUAL) == true) goto operand;
8
9 // loopne
10 RCX = RCX - 1;
11 if(RCX != 0 && verify_rflags(EQUAL) == false) goto operand;

NOP | No Operation

nop

Não faz nenhuma operação... Sério, não faz nada.

pseudo.c

https://silva97.gitbook.io/assembly-x86/a-base/instrucoes 14/15
10/04/2021 Instruções - Assembly x86

1 EAX = EAX;

https://silva97.gitbook.io/assembly-x86/a-base/instrucoes 15/15
10/04/2021 Instruções do nasm - Assembly x86

Instruções do nasm
Um pouco sobre o uso do nasm

Programar em Assembly é lidar diretamente com as instruções do arquivo binário final. Neste
processo o assembler é o responsável por organizar o formato da saída e organizar o código.
Vamos aprender aqui as diretivas e pseudo-instruções básicas do nasm para poder trabalhar
melhor com nosso código.

Seções

Ok, antes de mais nada vamos aprender a dividir nosso código em seções. Não adianta de nada
usarmos um linker se não trabalharmos com ele, não é mesmo?
A sintaxe para definir uma seção é tão simples quanto possível, basta usar a diretiva section 
seguido do nome que você quer dar para a seção e os atributos que você quer definir para ela.
As seções .text , .data , .rodata  e .bss  já tem seus atributos padrões definidos e por
isso não precisamos setá-los.
Por padrão o nasm joga todo o conteúdo do arquivo fonte na seção .text  e por isso nós não a
definimos na nossa PoC. Mas poderíamos reescrever nossa PoC desta vez especificando a
seção:

assembly.asm

1 bits 64
2
3 section .text
4
5 global assembly
6 assembly:
7 mov eax, 777
8 ret

https://silva97.gitbook.io/assembly-x86/a-base/instrucoes-do-nasm 1/9
10/04/2021 Instruções do nasm - Assembly x86

main.c

1 #include <stdio.h>
2
3 int assembly(void);
4
5 int main(void)
6 {
7 printf("Resultado: %d\n", assembly());
8 return 0;
9 }

A partir da diretiva na linha 3, todo o código é organizado no arquivo objeto dentro da seção
.text , que é destinada ao código executável do programa e por padrão tem o atributo de

execução (exec) habilitado pelo nasm.

Símbolos

Como já vimos na nossa PoC os símbolos internos podem ser exportados para poderem serem
acessados a partir de outros arquivos objetos usando a diretiva global . Podemos exportar
mais de um símbolo de uma vez separando cada nome de rótulo por vírgula, exemplo:

global assembly, anotherFunction, gVariable

Deste jeito um endereço especificado por um rótulo no nosso código fonte em Assembly pode
ser acessado por código fonte compilado em outro arquivo objeto, tudo graças ao linker.

Mas as vezes também teremos a necessidade de acessar um símbolo externo, isto é,


pertencente a outro arquivo objeto.
Para podermos fazer isso existe a diretiva extern  que serve para declarar no arquivo objeto
que estamos acessando um símbolo que está em outro arquivo objeto.
Já vimos que no arquivo objeto main.o havia na symbol table a declaração do uso do símbolo
assembly  que estava em um arquivo externo. A diretiva extern  serve para inserir esta

informação na tabela de símbolos do arquivo objeto de saída. A diretiva extern  segue a


mesma sintaxe de global :

https://silva97.gitbook.io/assembly-x86/a-base/instrucoes-do-nasm 2/9
10/04/2021 Instruções do nasm - Assembly x86

extern symbol1, symbol2, symbol3

Veja um exemplo de uso com nossa PoC:

main.c

1 #include <stdio.h>
2
3 int assembly(void);
4
5 int main(void)
6 {
7 printf("Resultado: %d\n", assembly());
8 return 0;
9 }
10
11 int number(void)
12 {
13 return 777;
14 }

assembly.asm

1 bits 64
2 extern number
3
4 section .text
5
6 global assembly
7 assembly:
8 call number
9 add eax, 111
10 ret

Declaramos na linha 11 do arquivo main.c a função number , e no arquivo assembly.asm


usamos a diretiva extern  na linha 2 para declarar o acesso ao símbolo number , que usamos
na linha 8.

https://silva97.gitbook.io/assembly-x86/a-base/instrucoes-do-nasm 3/9
10/04/2021 Instruções do nasm - Assembly x86

Para o nasm não faz diferença alguma aonde você coloca as diretivas extern  e
global , porém por questões de legibilidade do código eu recomendo que use

extern  logo no começo do arquivo fonte e global  logo antes da declaração do

rótulo.
Isto irá facilitar a leitura do seu código, já que ao ver o rótulo imediatamente se sabe
que ele foi exportado...
E ao abrir o arquivo fonte imediatamente, logo nas primeiras linhas, já se sabe quais
símbolos externos estão sendo acessados.

Variáveis?

Em Assembly não existe a declaração de uma variável, porém assim como funções existem
como conceitos e podem ser implementados em Assembly, variáveis também são desta forma.

Em um código em C variáveis globais ficam na seção .data  ou .bss .


A seção .data  do executável nada mais é que uma cópia dos dados contidos na seção
.data  do arquivo binário.

Ou seja, o que despejarmos de dados em .data  será copiado para a memória RAM e será
acessível em tempo de execução e com permissão de escrita.
Para despejar dados no arquivo binário existe a pseudo-instrução db  e semelhantes, cada uma
despejando um tamanho diferente de dados.
Mas todas tendo a mesma sintaxe de separar cada valor numérico por vírgula.
Veja a tabela:

Pseudo-instrução Tamanho dos dados Bytes

db byte 1

dw word 2

dd double word 4

dq quad word 8

dt ten word 10

do 16

dy 32

https://silva97.gitbook.io/assembly-x86/a-base/instrucoes-do-nasm 4/9
10/04/2021 Instruções do nasm - Assembly x86

dz 64

As quatro últimas dt, do, dy e dz  não suportam que seja passado uma string
como valor.

Podemos por exemplo guardar uma variável global na seção .data e acessar ela a partir do
código fonte em C, bem como também no próprio código em Assembly.
Exemplo:

assembly.asm

1 bits 64
2
3 global myVar
4 section .data
5 myVar: dd 777
6
7 section .text
8
9 global assembly
10 assembly:
11 add dword [myVar], 3
12 ret

https://silva97.gitbook.io/assembly-x86/a-base/instrucoes-do-nasm 5/9
10/04/2021 Instruções do nasm - Assembly x86

main.c

1 #include <stdio.h>
2
3 int assembly(void);
4 extern int myVar;
5
6 int main(void)
7 {
8 printf("Valor: %d\n", myVar);
9 assembly();
10 printf("Valor: %d\n", myVar);
11 return 0;
12 }

Repare que em C usamos a keyword extern  para especificar que a variável global
myVar  estaria em outro arquivo objeto, comportamento muito parecido com a

diretiva extern  do nasm.

Variáveis não-inicializadas

A seção .bss  é usada para armazenar variáveis não-inicializadas, isto é, que não tem um valor
inicial definido.
Basicamente esta seção no arquivo objeto tem um tamanho definido para ser alocada pelo
sistema operacional em memória mas não um conteúdo explícito copiado do arquivo binário.
Existem pseudo-instruções do nasm que permitem alocar espaço na seção sem de fato
despejar nada ali.
É a resb  e suas semelhantes que seguem a mesma premissa de db .
Os tamanhos disponíveis de dados são os mesmos de db , por isso não vou repetir a tabela
aqui. Só ressaltando que a última letra da pseudo-instrução indica o tamanho do dado.
A sintaxe da pseudo-instrução é:

resb número_de_dados

https://silva97.gitbook.io/assembly-x86/a-base/instrucoes-do-nasm 6/9
10/04/2021 Instruções do nasm - Assembly x86

Onde como operando ela recebe o número de dados do tamanho que a pseudo-instrução
especifica serão alocados. Por exemplo:

resd 6 ; Aloca o espaço de 6 double-words, ao todo 24 bytes.

A ideia de usar esta pseudo-instrução é poder declarar um rótulo/símbolo que irá apontar para o
endereço dos dados alocados em memória.
Veja mais um exemplo na nossa PoC:

assembly.asm

1 bits 64
2
3 global myVar
4 section .bss
5 myVar: resd 1
6
7 section .text
8
9 global assembly
10 assembly:
11 mov dword [myVar], 777
12 ret

main.c

1 #include <stdio.h>
2
3 int assembly(void);
4 extern int myVar;
5
6 int main(void)
7 {
8 assembly();
9 printf("Valor: %d\n", myVar);
10 return 0;
11 }

https://silva97.gitbook.io/assembly-x86/a-base/instrucoes-do-nasm 7/9
10/04/2021 Instruções do nasm - Assembly x86

Constantes

Uma constante nada mais é que um apelido para representar um valor em código afim de
facilitar a modificação daquele valor posteriormente ou então evitar um magic number.
Podemos declarar uma constante usando a pseudo-instrução equ :

NOME_DA_CONSTANTE equ expressão

Por convenção é interessante usar nomes de constantes totalmente em letras maiúsculas para
facilitar a sua identificação no código fonte em contraste com o nome de um rótulo.
Seja lá aonde a constante for usada no código fonte, ela irá expandir para o seu valor definido no
código binário resultante. Exemplo:

1 EXAMPLE equ 34
2 mov eax, EXAMPLE

A instrução na linha 2 alteraria o valor de EAX para 34.

Constantes em memória

Constantes em memória nada mais são do que valores despejados na seção .rodata , que é
muito parecida com .data  com a diferença de não ter permissão de escrita. Exemplo:

1 section .rodata
2 const_value: dd 777

Expressões

O nasm aceita que você escreva expressões matemáticas seguindo a mesma sintaxe da
linguagem C e seus operadores. Essas expressões serão calculadas pelo próprio nasm e não
em tempo de execução, por isso é necessário adicionar a expressão somente rótulos,
constantes ou qualquer outro valor que exista em tempo de compilação e não em tempo de
execução.

https://silva97.gitbook.io/assembly-x86/a-base/instrucoes-do-nasm 8/9
10/04/2021 Instruções do nasm - Assembly x86

Podemos usar expressão matemática em qualquer pseudo-instrução ou instrução que aceita


um valor numérico como operando. Exemplos:

1 CONST equ (5 + 2*5) / 3 ; Correto!


2 mov eax, 4 << 2 ; Correto!
3 mov eax, [(2341 >> 6) % 10] ; Correto!
4 mov eax, CONST + 4 ; Correto!
5
6 mov eax, ebx + 2 ; ERRADO!

O nasm também permite o uso de dois símbolos especiais que expandem para endereços
relacionados a posição da instrução atual:

Símbolo Valor

$ Endereço da instrução atual

$$ Endereço do início da seção atual

https://silva97.gitbook.io/assembly-x86/a-base/instrucoes-do-nasm 9/9
10/04/2021 Pré-processador do nasm - Assembly x86

Pré-processador do nasm
Um pouco sobre o pré-processador

O nasm tem um pré-processador de código baseado no pré-processador da linguagem C. O que


ele faz basicamente é interpretar instruções específicas do pré-processador para gerar o código
fonte final, que será de fato montado pelo nasm para o código de máquina.
É por isso que tem o nome de pré-processador, já que ele processa o código fonte antes do
nasm montar o código.

As diretivas interpretadas pelo pré-processador são prefixadas pelo símbolo %  e dão um poder
absurdo para a programação diretamente em Assembly no nasm.
Abaixo irei listar as mais básicas e o seu uso.

%define

1 %define nome "valor"


2 %define nome(arg1, arg2) arg1 + arg2

Assim como a diretiva #define  do C, esta diretiva é usada para definir macros. Ela no caso
tem o propósito de definir um macro de uma única linha. Seja lá aonde o nome do macro for
citado no código fonte, ele expandirá para exatamente o conteúdo que você definiu para ele...
Como se você estivesse fazendo uma cópia.
E assim como no C, é possível passar argumentos para um macro usando de uma sintaxe muito
parecida com uma função.
Exemplo de uso básico:

1 %define teste mov eax, 31


2 teste
3 teste

https://silva97.gitbook.io/assembly-x86/a-base/pre-processador-do-nasm 1/8
10/04/2021 Pré-processador do nasm - Assembly x86

As linhas 2 e 3 iriam expandir para a instrução mov eax, 31  como se tivesse feito uma cópia
do valor definido para o macro.
Podemos também é claro escrever um macro como parte de uma instrução, por exemplo:

1 %define addr [ebx*2 + 4]


2 mov eax, addr

Isto iria expandir a instrução na linha 2 para mov eax, [ebx*2 + 4] 

A diferença entre definir um macro desta forma e definir uma constante, é que a constante
recebe uma expressão matemática e expande para o valor do resultado. Enquanto o macro
expande para qualquer coisa que você definir para ele.

O outro uso do macro, que é mais poderoso, é passando argumentos para ele assim como se é
possível fazer em C.
Para isso basta a gente definir o nome do macro seguido dos parênteses e, dentro dos
parênteses, os nomes dos argumentos que queremos receber separados por vírgula.
No valor definido para o macro, os nomes desses argumentos irão expandir para qualquer
conteúdo que você passe como argumento na hora que chamar um macro.
Veja por exemplo o mesmo macro acima, porém desta vez dando a possibilidade de escolher o
registrador:

1 %define addr(reg) [reg*2 + 4]


2 mov eax, addr(ebx)
3 mov eax, addr(esi)

A linha 2 irá expandir para mov eax, [ebx*2 + 4] .


E a linha 3 irá expandir para mov eax, [esi*2 + 4] .

%undef

%undef nome_do_macro

Simplesmente apaga um macro anteriormente declarado por %define .

https://silva97.gitbook.io/assembly-x86/a-base/pre-processador-do-nasm 2/8
10/04/2021 Pré-processador do nasm - Assembly x86

%macro

1 %macro nome NÚMERO_DE_ARGUMENTOS


2 ; Código aqui
3 %endmacro

Além dos macros uma única linha, existem também os macros de múltiplas linhas oferecido
pelo nasm.
Após a especificação do nome que queremos dar ao macro, podemos especificar o número de
argumentos passados para ele. Caso não queira receber argumentos no macro, basta definir
este valor para zero.
Exemplo:

1 %macro sum5 0
2 mov ebx, 5
3 add eax, ebx
4 %endmacro
5
6 sum5
7 sum5

O %endmacro  sinaliza o final do macro, e todas as instruções inseridas entre as diretivas serão
expandidas quando o macro for citado.

Para usar argumentos com um macro de múltiplas linhas difere de um macro definido com
%define , ao invés do uso de parênteses o macro recebe argumentos seguindo a mesma

sintaxe de uma instrução e separando cada um por vírgula.


Para usar o argumento dentro do macro basta usar %n , onde n seria o número do argumento
que começa contando em 1.

1 %macro sum 2
2 mov ebx, %2
3 add %1, ebx
4 %endmacro
5
6 sum esi, edi
7 sum ebp, eax

https://silva97.gitbook.io/assembly-x86/a-base/pre-processador-do-nasm 3/8
10/04/2021 Pré-processador do nasm - Assembly x86

Também é possível fazer com que o último argumento do macro expanda para todo o conteúdo
passado, mesmo que contenha vírgula.
Para isso basta adicionar um +  ao número de argumentos. Por exemplo:

1 %macro example 2+
2 inc %1
3 mov %2
4 %endmacro
5
6 example eax, ebx, ecx
7 example ebx, esi, edi, edx

Linha 6 iria expandir para as instruções:

1 inc eax
2 mov ebx, ecx

Enquanto a linha 7 iria acusar erro, já que na linha 3 do macro a instrução expandiu para
mov esi, edi, edx  o que está errado.

Rótulos em um macro

Usar um rótulo dentro de um macro é problemático porque, se o macro for usado mais de uma
vez, estaremos redefinindo o mesmo rótulo já que seu nome nunca muda.
Para não ter este problema existem os rótulos locais de um macro que será expandido para um
nome diferente, definido pelo nasm, a cada uso do macro.
A sintaxe é simples, basta prefixar o nome do rótulo com %% . Exemplo:

1 ; Repare como o código abaixo ficaria mais simples usando SETcc


2 ; ou até mesmo CMOVcc
3
4 %macro compare 2
5 cmp %1, %2
6 je %%is_equal
7 mov eax, 0
8 jmp %%end
9 %%is_equal:
10 mov eax, 1
11 %%end:
12 %endmacro
https://silva97.gitbook.io/assembly-x86/a-base/pre-processador-do-nasm 4/8
10/04/2021 Pré-processador do nasm - Assembly x86

13
14 compare eax, edx

%unmacro

%unmacro nome NÚMERO_DE_ARGUMENTOS

Apaga um macro anteriormente definido com %macro , o número de argumentos especificado


deve ser o mesmo utilizado na hora de declarar o macro.

Montagem condicional

Assim como o pré-processador do C, o nasm também suporta diretivas de código condicional. A


sintaxe básica é:

1 %if<condição>
2 ; Código 1
3 %elif<condição>
4 ; Código 2
5 %else
6 ; Código 3
7 %endif

Onde o código dentro da diretiva %if  só é montado se a condição for atendida. Caso não seja
é possível usar a diretiva %elif  para fazer o teste de uma nova condição. Enquanto o código
na diretiva %else  é expandido caso nenhuma das condições anteriormente testadas sejam
atendidas.
Por fim é usado a diretiva %endif  para indicar o fim da diretiva %if .

É possível passar para %if  e %elif  uma expressão matemática afim de testar o resultado de
um cálculo com uma constante ou algo semelhante.
Se o valor for diferente de zero, a expressão será considerada verdadeira e bloco de código
expandido.

Também é possível inverter a lógica das instruções adicionando um 'n', fazendo com que o
bloco seja expandido caso a condição não seja atendida.

https://silva97.gitbook.io/assembly-x86/a-base/pre-processador-do-nasm 5/8
10/04/2021 Pré-processador do nasm - Assembly x86

Exemplo:

1 CONST equ 5
2
3 %ifn CONST * 2 > 7
4 call is_smallest
5 %else
6 call is_bigger
7 %endif

Além do %if  básico também podemos usar variantes que verificam por uma condição
específica ao invés de receber uma expressão e testar seu resultado.

%ifdef e %elifdef

1 %ifdef nome_do_macro
2 %elifdef nome_do_macro

Estas diretivas verifica se um macro de linha única foi declarado por um %define 
anteriormente. É possível também usar estas diretivas em forma de negação adicionando o 'n'
após o 'if'. Ficando: %ifndef  e %elifndef , respectivamente.

%ifmacro e %elifmacro

1 %ifmacro nome_do_macro
2 %elifmacro nome_do_macro

Mesmo que %ifdef  porém para macros de múltiplas linhas declarados por %macro .
E também da mesma forma tem suas versões em negação:
%ifnmacro  e %elifnmacro .

%error e %warning

1 %error "Mensagem de erro"


2 %warning "Mensagem de alerta"
https://silva97.gitbook.io/assembly-x86/a-base/pre-processador-do-nasm 6/8
10/04/2021 Pré-processador do nasm - Assembly x86

Usando diretivas condicionais as vezes queremos acusar um erro ou emitir um alerta no


console para indicar alguma mensagem no processo de montagem de algum projeto.
%error  imprime a mensagem como um erro e finaliza o processo de montagem.

Enquanto %warning  emite a mensagem como um alerta e o processo de montagem continua


normalmente.
Podemos por exemplo acusar um erro caso um determinado macro necessário para o código
não esteja definindo:

1 %ifndef macro_importante
2 %ifdef macro_substituto
3 %warning "Macro importante não foi definido"
4 %else
5 %error "Macro importante e substituto não foram definidos"
6 %endif
7 %endif

%include

%include "nome do arquivo.ext"

Esta diretiva tem o uso parecido com a diretiva #include  da linguagem C e ela faz exatamente
a mesma coisa: Copia o conteúdo do arquivo passado como argumento para o exato local
aonde ela foi utilizada no arquivo fonte.
Seria como você manualmente abrir o arquivo, copiar todo o conteúdo dele e depois colar no
código fonte.

Assim como fazemos em um header file incluído por #include  na linguagem C, é importante
usar as diretivas condicionais para evitar a inclusão duplicada de um mesmo arquivo. Por
exemplo:

arquivo.asm

1 %ifndef _ARQUIVO_ASM
2 %define _ARQUIVO_ASM
3
4 ; Código aqui
https://silva97.gitbook.io/assembly-x86/a-base/pre-processador-do-nasm 7/8
10/04/2021 Pré-processador do nasm - Assembly x86

5
6 %endif

Desta forma, quando incluirmos o arquivo pela primeira vez o macro _ARQUIVO_ASM  será
declarado.
Se ele for incluído mais uma vez, o macro já estará declarado e o %ifndef  da linha 1 terá uma
condição falsa e portanto não expandirá o conteúdo dentro de sua diretiva.

É importante fazer isso para evitar a redeclaração de macros, constantes ou rótulos. Bem como
também evita que o mesmo código fique duplicado.

https://silva97.gitbook.io/assembly-x86/a-base/pre-processador-do-nasm 8/8
10/04/2021 Syscall no Linux - Assembly x86

Syscall no Linux
Chamada de sistema no Linux

Uma chamada de sistema, ou syscall (abreviação para system call), é algo muito parecido com
uma call  mas com a diferença nada sutil de que é o kernel do sistema operacional quem irá
executar o código.
O kernel, caso não saiba, é a parte principal de um sistema operacional. Ele é a base de todo o
restante do sistema que roda sobre controle do kernel.
O Linux na verdade é um kernel, um sistema operacional "Linux" na verdade é um sistema
operacional que usa o kernel Linux.

Em x86-64 existe uma instrução que foi feita especificamente para fazer chamadas de sistema
e o nome dela é, intuitivamente, syscall .
Ela não recebe nenhum operando e a especificação de qual código ela irá executar e com quais
argumentos é definido por uma convenção de chamada assim como no caso das funções.

Convenção de syscall x86-64

A convenção para efetuar uma chamada de sistema em Linux x86-64 é bem simples, basta
definir RAX para o número da syscall que você quer executar e outros 6 registradores são
usados para passar argumentos. Veja a tabela:

Registrador Uso

RAX Número da syscall / Valor de retorno

RDI Primeiro argumento

RSI Segundo argumento

RDX Terceiro argumento

R10 Quarto argumento

R8 Quinto argumento

R9 Sexto argumento

https://silva97.gitbook.io/assembly-x86/a-base/syscall-no-linux 1/3
10/04/2021 Syscall no Linux - Assembly x86

Em syscalls que recebem menos do que 6 argumentos, não é necessário definir o valor dos
registradores restantes porque não serão utilizados.
E por fim, o retorno da syscall também fica em RAX assim como na convenção de chamada da
linguagem C.

exit

Nome RAX RDI

exit 60 int status_de_saída

Vou ensinar aqui a usar a syscall mais simples que é a exit , ela basicamente finaliza a
execução do programa.
Ela recebe um só argumento que é o status de saída do programa. Esse número nada mais é do
que um valor definido para o sistema operacional que indica as condições da finalização do
programa.
Por convenção geralmente o número zero indica que o programa finalizou sem problemas, e
qualquer valor diferente deste indica que houve algum erro.
Um exemplo na nossa PoC:

assembly.asm

1 bits 64
2
3 section .text
4
5 global assembly
6 assembly:
7 mov rax, 60
8 mov rdi, 0
9 syscall
10 ret

https://silva97.gitbook.io/assembly-x86/a-base/syscall-no-linux 2/3
10/04/2021 Syscall no Linux - Assembly x86

main.c

1 #include <stdio.h>
2
3 int assembly(void);
4
5 int main(void)
6 {
7 assembly();
8 puts("Hello World!");
9 return 0;
10 }

A instrução ret  na linha 10 nunca será executada porque a syscall disparada pela instrução
syscall  na linha 9 não retorna. No momento em que for chamada o programa será finalizado

com o valor de RDI como status de saída.


No Linux se quiser ver o status de saída de um programa, a variável especial $? expande para o
status de saída do último programa executado.
Então você pode executar nossa PoC da seguinte forma:

1 $ ./test
2 $ echo $?

O echo  teoricamente iria imprimir 0 que é o status de saída que nós definimos. Experimente
mudar o valor de RDI e ver se reflete na mudança do valor de $? corretamente.

Outras syscalls

Se quiser ver uma lista completa de syscalls x86-64 do Linux, pode ver no link abaixo:

Linux System Call Table for x86 64

https://silva97.gitbook.io/assembly-x86/a-base/syscall-no-linux 3/3
10/04/2021 Olá mundo no Linux - Assembly x86

Olá mundo no Linux


Finalmente o Hello World

Geralmente o "Hello World" é a primeira coisa que vemos quando estamos aprendendo uma
linguagem de programação.
Neste caso eu deixei por último pois acredito que seria de extrema importância entender todos
os conceitos antes de vê-lo, isso evitaria a intuição de ver um código em Assembly como um
"código em C mais difícil de ler".
Acredito que esta comparação mental involuntária é muito ruim e prejudicaria o aprendizado.
Por isso optei por explicar tudo antes mesmo de apresentar o famoso "Hello World".

Hello World

Desta vez vamos escrever um código em Assembly sem misturar com C, será um executável do
Linux (formato ELF64) fazendo chamadas de sistema diretamente.
Vamos vê-lo logo:

hello.asm

1 bits 64
2
3 section .rodata
4 msg: db `Hello World!\n`
5 MSG_SIZE equ $-msg
6
7 section .text
8
9 global _start
10 _start:
11 mov rax, 1
12 mov rdi, 1
13 mov rsi, msg
14 mov rdx, MSG_SIZE
15 syscall ; write
16
17 mov rax, 60
18 xor rdi, rdi
19 syscall ; exit

https://silva97.gitbook.io/assembly-x86/a-base/ola-mundo-no-linux 1/4
10/04/2021 Olá mundo no Linux - Assembly x86

Para compilar este código basta usar o nasm especificando o format elf64 e depois, desta vez,
iremos usar o linker do pacote GCC diretamente. O nome do executável é ld e o uso básico é
bem simples, basta especificar o nome do arquivo de saída com -o. Ficando assim:

1 $ nasm hello.asm -felf64


2 $ ld hello.o -o hello
3 $ ./hello

Na linha 5 definimos uma constante usando o símbolo $ para pegar o endereço da instrução
atual e subtraímos pelo endereço do rótulo msg .
Isto resulta no tamanho do texto porque msg aponta para o início da string e, como está logo
em seguida, $ seria o endereço do final da string.
final - início = tamanho 

write

Nome RAX RDI RSI RDX

write 1 file_descriptor endereço tamanho (em bytes)

Como deve ter reparado usamos mais uma syscall, que foi a syscall write .
Esta syscall basicamente escreve dados em um arquivo, o primeiro argumento é um número
que serve identificar o arquivo para o qual queremos escrever os dados.
No Linux a saída e entrada de um programa nada mais é que dados sendo escritos e lidos em
arquivos, por isso a usamos para escrever o texto na tela.
E isto é feito por três arquivos que estão sempre abertos em um programa e tem sempre o
mesmo file descriptor, são eles:

File
Nome Descrição
descriptor

stdin 0 Entrada de dados (o que é digitado pelo usuário)

stdout 1 Saída padrão (o que é impresso no terminal)

Saída de erro (também impresso no terminal, porém destinado a


stderr 2
mensagens de erro)

https://silva97.gitbook.io/assembly-x86/a-base/ola-mundo-no-linux 2/4
10/04/2021 Olá mundo no Linux - Assembly x86

Se quiser ver o código de implementação desta syscall no Linux, pode ver aqui.

Entry point

Reparou que nosso programa tem um símbolo _start  e que magicamente este é o código que
o sistema operacional está executando primeiro?
Isto acontece porque o linker definiu o endereço daquele símbolo como o entry point (ponto de
entrada) do nosso programa.

O entry point nada mais é o que o próprio nome sugere, o endereço inicial de execução do
programa.
Eu sei o que você está pensando:

Então a função main de um programa em C é o entry point?

A resposta é não! Um programa em C usando a libc tem uma série de códigos que são
executados antes da main. E o primeiro deles, pasme, é uma função chamada _start definida
pela própria libc.

Na verdade qualquer símbolo pode ser definido como o entry point para o executável, não faz
diferença qual nome você dá para ele.
Só que _start  é o símbolo padrão que o ld define como entry point. Se você quiser usar um
símbolo diferente é só especificar com a opção -e.
Por exemplo, podemos reescrever nosso Hello World assim:

1 bits 64
2
3 section .rodata
4 msg: db `Hello World!\n`
5 MSG_SIZE equ $-msg
6
7 section .text
8
9 global _eu_que_mando_no_meu_exec
10 _eu_que_mando_no_meu_exec:
11 mov rax, 1
12 mov rdi, 1
13 mov rsi, msg
14 mov rdx, MSG_SIZE
15 syscall
16

https://silva97.gitbook.io/assembly-x86/a-base/ola-mundo-no-linux 3/4
10/04/2021 Olá mundo no Linux - Assembly x86

17 mov rax, 60
18 xor rdi, rdi
19 syscall

E compilar assim:

1 $ nasm hello.asm -felf64


2 $ ld hello.o -o hello -e _eu_que_mando_no_meu_exec
3 $ ./hello

Fácil fazer um Hello World, né?


Ei, o que acha de fazer uns macros para melhorar o uso dessas syscalls aí?
Seria interessante também salvar os macros em um arquivo separado e incluir ele com a
diretiva %include .

https://silva97.gitbook.io/assembly-x86/a-base/ola-mundo-no-linux 4/4
10/04/2021 Revisão - Assembly x86

Revisão
Entenda tudo o que viu aqui

As instruções de Assembly por si só na verdade é bem simples, como já vimos antes a sintaxe
de uma instrução é bem fácil e entender o que ela faz também não é o maior segredo do
mundo.
Porém como também já vimos, para de fato ter conhecimento adequado da linguagem é
necessário aprender muita coisa e talvez esse conhecimento variado tenha ficado disperso na
sua mente. (e olha que só aprendemos o básico...)
A ideia deste tópico é juntar tudo e mostrar como e porque está relacionado a Assembly.

O que estamos fazendo?

Programar em uma linguagem de baixo nível como Assembly não é a mesma coisa de
programar em uma linguagem de alto nível como C.
Ao programar em Assembly estamos escrevendo diretamente as instruções que serão
executadas pelo processador.
Não apenas isto, como também estamos organizando todo o formato do arquivo de acordo com
o formato final que queremos executar.
Então é importante entender duas coisas, antes de mais nada: A arquitetura para a qual
estamos programando e o formato de arquivo que queremos escrever.

A arquitetura é a x86, como já sabemos. E um código que irá trabalhar com a linguagem C é
montado para um arquivo objeto.
Por isso estudamos os conceitos básicos da arquitetura x86 propriamente dita, e também
estudamos um pouco do arquivo objeto.
Sem saber o que são seções, o que á symbol table etc. não dá para entender o que se está
fazendo.
Por que o código em C consegue acessar um rótulo no meu código em Assembly? Por que
dados despejados em .data  são chamados de variáveis e os em .text  são chamados de
código?
Por que dados em .rodata  não podem ser modificados e são chamados de constantes?
Por que isso é considerado uma função e isso uma variável? Os dois não são símbolos?

https://silva97.gitbook.io/assembly-x86/a-base/revisao 1/3
10/04/2021 Revisão - Assembly x86

Ao programar em Assembly nós não estamos apenas escrevendo as instruções que o


processador irá executar, estamos também construindo todo o arquivo binário final
manualmente.
Felizmente o nasm facilita nossa vida ao formatar o arquivo binário para o formato desejado, é a
tal da opção -f elf64 ou -f win64 que passamos na linha de comando.
Mas mesmo assim temos que dar informações para o nasm sobre o que fica aonde.

Por que estamos fazendo?

Em uma linguagem de alto nível todos esse conceitos relacionados ao formato do arquivo
binário e da arquitetura do processador, são abstraídos.
Já em uma linguagem de baixo nível, esses conceitos tem muito pouca abstração e precisamos
lidar com eles manualmente.
Isto é necessário porque estamos escrevendo diretamente as instruções que o hardware, o
processador, irá executar.
E para poder se comunicar com o processador precisamos entender o que ele está fazendo.

Imagine tentar instruir um funcionário de uma empresa de entregas exatamente como ele deve
organizar a carga e como ele deve entregá-la...
Porém você não sabe o que é a carga e nem para quem ela deve ser entregue...
Impossível, né?

Isto é útil por quê?

Estudar Assembly não é só decorar instruções e o que elas fazem, isto é fácil até demais.
Estudar Assembly é estudar a arquitetura, o formato do executável, como o executável funciona,
convenções de chamadas, características do sistema operacional, características do hardware
etc... Ah, e estudar as instruções também.
Junte tudo isso e você terá um belo conhecimento para entender como um software funciona
na prática.

Já citei antes porque estudar Assembly é útil na introdução do livro.


Mas só decorar as instruções não é útil por si só, a questão é todo o resto que você irá aprender
ao estudar Assembly.

O que é um código fonte em Assembly?

Como já vimos, o assembler é bem mais complexo do que simplesmente converter as


instruções que você escreve em código de máquina.

https://silva97.gitbook.io/assembly-x86/a-base/revisao 2/3
10/04/2021 Revisão - Assembly x86

Ele tem diretivas, pseudo-instruções e pré-processamento de código para formatar o código em


Assembly e lhe dar mais poder na programação.
Ele também formata o código de saída para um formato especificado e nos permite escolher o
modo de montagem das instruções de 64, 32 ou 16 bits.

Ou seja, um código fonte em Assembly não é apenas instruções mas também diretivas para a
formatação do arquivo binário que será feita pelo assembler.
Que é muito diferente de uma linguagem de alto nível como C, que contém apenas instruções e
todo o resto fica abstraído como se nem existisse.

https://silva97.gitbook.io/assembly-x86/a-base/revisao 3/3
10/04/2021 Aprofundando em Assembly - Assembly x86

Aprofundando em Assembly
Aprendendo mais um pouco

Esta parte do livro ainda está sendo escrita. Tópicos podem estar fora de ordem ou
com informações incompletas.

Agora temos conhecimento o bastante para entender como um código em Assembly funciona e
porque é importante estudar diversos assuntos relacionados ao sistema operacional, formato
do binário e a arquitetura em si para poder programar em Assembly.

Mas vimos tudo isso com código rodando sobre um sistema operacional em submodo 64-bit.
A ideia desta parte do livro é focar menos nas características do sistema operacional e mais
nas características da própria arquitetura.
Para isso vamos testar código de 64, 32 e 16 bit.

Ferramentas

Se certifique de ter o Dosbox instalado no seu sistema ou qualquer outro emulador do MS-DOS
que você saiba utilizar.
Sistemas compatíveis com o MS-DOS, como o FreeDOS por exemplo também podem ser
utilizados.

Também é importante que o seu GCC possa compilar código para 64 e 32 bit.
Em um Linux x86-64 ao instalar o gcc você já pode compilar código de 64 bit. Para compilar
para 32 bit basta instalar o pacote gcc-multilib. No Debian você pode fazer:

$ sudo apt install gcc-multilib

No Windows basta instalar o Mingw-w64 como já mencionei.

https://silva97.gitbook.io/assembly-x86/aprofundando-em-assembly 1/2
10/04/2021 Aprofundando em Assembly - Assembly x86

Para testar se está funcionando adequadamente, você pode passar para o GCC a opção -m32
para compilar para 32 bit.
Tente compilar um "Hello World" em C e veja se funciona:

$ gcc hello.c -o hello -m32

Neste capítulo usaremos também uma ferramenta que vem junto com o nasm, o ndisasm. Ele é
um disassembler, um software que converte código de máquina em código Assembly. Se você
tem o nasm instalado, também tem o ndisasm disponível.
O uso básico é só especificar se as instruções devem ser desmontadas como instruções de 16,
32 ou 64 bits. Por padrão ele desmonta as instruções como de 16 bits. Para mudar isso, basta
usar a opção -b e especificar os bits. Exemplo:

$ ndisasm -b32 binary

Qualquer dúvida pode entrar no grupo e fazer uma pergunta:

FreeDev - Grupo no Facebook

https://silva97.gitbook.io/assembly-x86/aprofundando-em-assembly 2/2
10/04/2021 Registradores de segmento - Assembly x86

Registradores de segmento
Segmentação da memória RAM

Na arquitetura x86 o acesso a memória RAM é comumente dividido em segmentos.


Um segmento de memória nada mais é que um pedaço da memória RAM que o programador
usa dando algum sentido a ele.
Por exemplo, podemos usar um segmento só para armazenar variáveis. E usar outro para
armazenar o código executado pelo processador.

Rodando sob um sistema operacional a segmentação da memória é totalmente


controlada pelo kernel. Ou seja, não tente fazer o que você não tem permissão.

Barramento de endereço

O barramento de endereço é um socket do processador que serve para se comunicar com a


memória principal. (memória RAM)
Basicamente a largura deste barramento indica quanta memória o processador consegue
endereçar. Já que ele indica o endereço físico da memória que se deseja acessar.

Em IA-16 o barramento tem o tamanho padrão de 20 bits.


Calculando 220 temos o número de bytes endereçáveis.
Que são exatamente 1 MiB de memória endereçavel.
É da largura do barramento de endereço que surge a limitação de acesso a memória RAM.

Em IA-32 e x86-64 o barramento de endereço tem a largura de 32 e 64 bits respectivamente.

Segmentação em IA-16

Em IA-16 a segmentação é bem simplista e o código trabalha basicamente com 4 segmentos


simultaneamente.
Esses segmentos são definidos simplesmente alterando o registrador de segmento equivalente,
cujo eles são:

https://silva97.gitbook.io/assembly-x86/aprofundando-em-assembly/registradores-de-segmento 1/3
10/04/2021 Registradores de segmento - Assembly x86

Registrador Nome

CS Code Segment / Segmento de código

DS Data Segment / Segmento de dado

ES Extra Segment / Segmento extra

SS Stack Segment / Segmento da pilha

Cada um desses registradores tem 16 bits de tamanho.

Quando acessamos um endereço na memória estamos usando um endereço lógico que é a


junção de um segmento e um offset (deslocamento), seguindo o formato:
segmento:offset 

O tamanho do valor de offset pode variar dependendo da instrução, mas por padrão
acompanha o tamanho do barramento interno do processador.
Ou seja, em IA-16 é 16 bits de tamanho.

Veja por exemplo a instrução:

mov [0x100], ax

O endereçamento definido pelos colchetes é na verdade o offset, que juntamente com o


segmento DS, se obtém o endereço físico.
Dependendo da instrução um registrador de segmento é implicitamente utilizado, no caso do
mov  o DS é o segmento padrão. Ou seja o endereço lógico é DS:0x100 

Podemos especificar um segmento diferente com a seguinte sintaxe do nasm:

1 ; O nome deste recurso é "segment override"


2 ; Ou em PT-BR: Substituição do segmento
3
4 mov [es:0x100], ax

https://silva97.gitbook.io/assembly-x86/aprofundando-em-assembly/registradores-de-segmento 2/3
10/04/2021 Registradores de segmento - Assembly x86

A conversão de endereço lógico para endereço físico é feita pelo processador com um cálculo
simples:

1 endereço físico = (segmento << 4) + offset


2
3 O operador << denota um deslocamento de bits para a esquerda,
4 uma operação shift left.

Segmentação em IA-32

Além dos registradores de segmento do IA-16, em IA-32 se ganha mais dois registradores de
segmento: FS e GS
Diferente dos registradores gerais, os registradores de segmento não são expandidos.
Permanecem com o tamanho de 16 bits.

Em protected mode os registradores de segmento não são usados para gerar um endereço
lógico junto com o offset, ao invés disso, serve de seletor identificando o segmento por um
índice em uma tabela que lista os segmentos.

https://silva97.gitbook.io/assembly-x86/aprofundando-em-assembly/registradores-de-segmento 3/3
10/04/2021 Call e Ret - Assembly x86

Call e Ret
Ainda não vimos tudo

Quando se trata de chamadas de procedimentos existem dois conceitos relacionados ao


endereço deste procedimento. Existem chamadas "próximas" (near) e "distantes" (far).
Enquanto no near call  nós apenas especificamos o offset do endereço, no far call  nós
também especificamos o segmento.

O outro conceito é de endereço "relativo" (relative) e "absoluto" (absolute).

Tamanho do offset

O tamanho que o offset do endereço deve ter acompanha a largura do barramento interno.
Então se estamos em real mode (16 bit), por padrão o offset deve ser de 16 bit.
Ou seja, basicamente o mesmo tamanho do Instruction Pointer.

Near Relative Call

call rel16/rel32

Esta é a call  que já usamos, não tem segredo. Ela basicamente recebe um número negativo
ou positivo indicando o número de bytes que devem ser desviados. Veja da seguinte forma:

Instruction_Pointer = Instruction_Pointer + operand

A matemática básica nos diz que "mais com menos é menos", ou seja, se o operando for
negativo essa soma resultará em uma subtração.

Onde está RIP?

https://silva97.gitbook.io/assembly-x86/aprofundando-em-assembly/call-e-ret 1/5
10/04/2021 Call e Ret - Assembly x86

Existe um detalhe bem simples porém importante para conseguir lidar com endereços relativos
corretamente. Quando o processador for executar a instrução, o Instruction Pointer já estará
apontando para a instrução seguinte.
Isso faz toda a diferença porque desvios de fluxo para trás precisam contar os bytes da própria
instrução em si.
Claro que este cálculo não é feito por nós e sim pelo assembler, mas é importante saber.
Ah, e lembra do símbolo $ que eu falei que o nasm expande para o endereço da instrução atual?
Veja que ele não coincide com o valor de RIP, cujo o mesmo já está apontando para a instrução
seguinte.
Por exemplo, poderíamos fazer uma chamada na própria instrução gerando um loop "infinito"
usando a sintaxe:

call $

Near Absolute Call

call r/m

Diferente da chamada relativa que indica um número de bytes a serem somados com RIP, numa
chamada absoluta você passa o endereço exato de onde você quer fazer a chamada.
Você pode experimentar fazer uma chamada assim:

1 mov rax, rotulo


2 call rax

Se você passar rotulo  para a call  diretamente, você estará fazendo uma chamada relativa
porque deste jeito você estará passando um valor imediato. E a única call  que recebe valor
imediato é a de endereço relativo, por isso o nasm passa o endereço relativo daquele rótulo.
Mas ao definir o endereço do rótulo para um registrador ou memória, o assembler irá passar o
endereço absoluto dele.

É importante entender que tipo de operando cada instrução recebe para evitar se confundir
sobre como o assembler irá montar a instrução.
E sim, saber como a instrução é montada em código de máquina é muitas vezes importante.
https://silva97.gitbook.io/assembly-x86/aprofundando-em-assembly/call-e-ret 2/5
10/04/2021 Call e Ret - Assembly x86

Far Call

1 call seg16:off16 ; Em 16-bit


2 call seg16:off32 ; Em 32-bit
3
4 call mem16:16 ; Em 16-bit
5 call mem16:32 ; Em 32-bit
6 call mem16:64 ; Em 64-bit

As chamadas far são todas absolutas e recebem no operando um valor seguindo o formato de
especificar um offset seguido do segmento de 16-bit.
No nasm, um valor imediato pode ser passado da seguinte forma:

call 0x1234:0xabcdef99

Onde o valor a esquerda especifica o segmento e o da direita o offset. Detalhe que esta
instrução não é suportada em 64-bit.

O segundo tipo de far call , suportado em 64-bit, é o que recebe como operando um valor na
memória. Mas perceba que temos um near call  que recebe o mesmo tipo de argumento, não
é mesmo?
Por padrão o nasm irá montar as instruções como near e não far. Mas você pode evitar essa
ambiguidade explicitando com keywords do nasm, que são bem intuitivas. Veja:

1 call [rbx] ; Próximo e absoluto


2 call near [rbx] ; Próximo e absoluto
3 call far [rbx] ; Distante

O near espera o endereço do offset em memória, não tem segredo. Mas o far espera o offset
seguido do segmento.
Em um sistema de 32-bit vamos supor que nosso procedimento está no segmento 0xaaaa e no
offset 0xbbbb1111. Em memória o valor precisa estar assim:

11 11 bb bb aa aa

https://silva97.gitbook.io/assembly-x86/aprofundando-em-assembly/call-e-ret 3/5
10/04/2021 Call e Ret - Assembly x86

Lembrando que x86 é little-endian.

Basicamente o far call  modifica o valor de CS e IP ao mesmo tempo, enquanto o near call 
apenas modifica o valor de IP.

No código de máquina, a diferença entre o far e o near call  que usam o operando
em memória está no campo REG do byte ModR/M. O near tem o valor 2 e o far tem o
valor 3. O opcode é FF .
Se você não entendeu isso aqui, não se preocupa com isso...

Ret

1 ret(f/n)
2 ret(f/n) imm16

Como talvez você já tenha reparado intuitivamente, a chamada far também preserva o valor de
CS na stack e não apenas o valor de IP. (lembrando que IP já estaria apontando para a instrução
seguinte...)
Por isso a instrução ret  também precisa ser diferente. Ao invés de apenas ler o offset na
stack ela precisa ler o segmento também, assim modificando CS e IP do mesmo jeito que o
call .

Repetindo que o nasm por padrão irá montar as instruções como near então precisamos
especificar para o nasm, em um procedimento que deve ser chamado como far, que queremos
usar um ret  far.
Para isso podemos simplesmente adicionar um sufixo 'n' para especificar como near, que já é o
padrão, ou o sufixo 'f' para especificar como far.
Ficando:

retf ; Usado em procedimentos que devem ser chamados com far call

https://silva97.gitbook.io/assembly-x86/aprofundando-em-assembly/call-e-ret 4/5
10/04/2021 Call e Ret - Assembly x86

Existe também uma outra opção de instrução ret  que recebe como operando um valor
imediato de 16-bit que especifica um número de bytes a serem desempilhados da stack.
Basicamente o que ele faz é somar o valor de SP com esse número, porque como sabemos a
pilha cresce "para baixo".
Ou seja, se subtraímos valor em SP estamos fazendo a pilha crescer. Se somamos, estamos
fazendo ela diminuir.
Por exemplo, podemos escrever em pseudo-código a instrução retf 12  da seguinte forma:

pseudo.c

1 RIP = pop();
2 CS = pop();
3 RSP = RSP + 12;

https://silva97.gitbook.io/assembly-x86/aprofundando-em-assembly/call-e-ret 5/5
10/04/2021 Atributos - Assembly x86

Atributos
Nem tudo fica explícito

Você já deve ter reparado que as instruções tem mais informações do que nós explicitamos
nelas. Por exemplo, a instrução mov  implicitamente acessa a memória a partir do segmento
DS, além de que magicamente a instrução tem um tamanho específico de operando sem que a
gente diga a ela.

Todas essas informações implícitas da instrução são especificadas a partir de atributos que
tem determinados valores padrões que podem ser modificados.
Os três atributos mais importantes para a gente entender é o operand-size, address-size e
segment.

O opcode é um byte do código de máquina que especifica a operação a ser


executada pelo processador. Em algumas instruções, mais alguns bits de outro byte
da instrução em código de máquina é utilizado para especificar operações diferentes,
que é o campo REG do byte ModR/M.
Como o já citado far call  por exemplo.

Operand-size

Em protected mode nós podemos acessar operandos de 32, 16 ou 8 bits... Beleza, mas como o
processador sabe disso se nós não dizemos para ele?
É aí que entra o atributo operand-size.

Instruções que lidam com operandos de 8 bits tem opcodes próprios só para eles. Mas as
instruções que lidam com operandos de 16 e 32 são as mesmas instruções, mudando somente
o atributo operand-size.

Vamos fazer um experimento com o código abaixo:

tst.asm

https://silva97.gitbook.io/assembly-x86/aprofundando-em-assembly/atributos 1/8
10/04/2021 Atributos - Assembly x86

1 bits 32
2
3 mov ah, bh
4 mov eax, ebx

Monte este código sem especificar qualquer formatação para o nasm, assim ele irá apenas
colocar na saída as instruções que escrevemos:

$ nasm tst.asm -o tst

Depois disso use o ndisasm especificando para desmontar instruções como de 32 bits, e
depois, como de 16 bits. A saída ficará como no print abaixo:

Repare que tanto em 32 quanto 16 bits, a instrução mov ah, bh  não muda...
Porém a instrução mov eax, ebx  e mov ax, bx  são a mesma instrução.

Só o que muda de um para outro é o operand-size. Enquanto em 32-bit por padrão o operand-
size é de 32 bits, em 16-bit ele é de 16-bit.

https://silva97.gitbook.io/assembly-x86/aprofundando-em-assembly/atributos 2/8
10/04/2021 Atributos - Assembly x86

Por isso que se dizemos para o disassembler que as instruções são de 16-bit, ele desmonta a
instrução como mov ax, bx . Porque é de fato esta operação que o processador em modo de
16-bit iria executar, não é um erro do disassembler.

E isto não vale só para registradores mas também para operandos imediatos e operandos em
memória. Vamos fazer outro experimento:

tst.asm

1 bits 32
2
3 mov eax, 0x11223344

As instruções ficam:

1 $ nasm tst.asm -o tst


2 $ ndisasm -b32 tst
3 $ ndisasm -b16 tst

A saída fica assim:

https://silva97.gitbook.io/assembly-x86/aprofundando-em-assembly/atributos 3/8
10/04/2021 Atributos - Assembly x86

Entendendo melhor a saída do ndisasm:

A esquerda fica o raw address da instrução em hexadecimal, que é um nome bonitinho para
o índice do byte dentro do arquivo. (contando a partir do 0)
No centro fica o código de máquina em hexadecimal. Os bytes são mostrados na mesma
ordem em que estão no arquivo binário.
Por fim a direita, o disassembly das instruções em código de máquina.

Repare que quando dizemos para o ndisasm que as instruções são de 32-bit, ele faz o
disassembly correto e mostra mov eax, 0x11223344 .
Porém quando dizemos que é de 16-bit, ele desmonta mov ax, 0x3344  seguido de uma
instrução que não tem nada a ver com o que a gente escreveu.

Se você prestar atenção no código de máquina, vai notar que nosso operando imediato
0x11223344 está bem ali em little-endian, logo após o byte B8. (o opcode)
Porque é assim que operandos imediatos são dispostos no código de máquina... Agora no
segundo caso, quando dizemos que são instruções de 16-bit...
A instrução não espera um operando de 4 bytes mas sim 2 bytes...
Por isso o disassembler considera isto aqui como a instrução:

https://silva97.gitbook.io/assembly-x86/aprofundando-em-assembly/atributos 4/8
10/04/2021 Atributos - Assembly x86

B8 44 33

Os bytes 22 11  ficam sobrando e acabam sendo desmontados como se fossem uma
instrução diferente.

Em 64-bit o operando também tem 32 bits de tamanho por padrão.

Address-size

O atributo de address-size define o modo de endereçamento. O tamanho padrão do offset


acompanha a largura do barramento interno do processador.

Quando o processador está em modo de 16-bit, pode-se usar endereçamento de 16 ou 32 bits. O


mesmo vale para modo de 32-bit, onde se usa por padrão 32 bits de endereçamento mas dá
para usar modo de endereçamento de 16 bits.

Já em 64-bit o endereçamento pode ser feito com registradores de 64 bits por padrão, mas
também com registradores de 32 bits.

Mas o atributo não muda somente o tamanho do offset mas todo ele, devido ao fato de haver
diferenças entre o modo de endereçamento de 16-bit e de 32-bit.
Observe o disassembly no print:

https://silva97.gitbook.io/assembly-x86/aprofundando-em-assembly/atributos 5/8
10/04/2021 Atributos - Assembly x86

A instrução mov byte [bx], 42  montada para 16-bit não altera apenas o tamanho do
registrador, quando está em 32-bit, mas também o registrador em si.
Isto acontece devido as diferenças de endereçamento já explicadas neste livro em "A base".

Agora observe a instrução mov byte [ebx], 42  montada para 32-bit:

https://silva97.gitbook.io/assembly-x86/aprofundando-em-assembly/atributos 6/8
10/04/2021 Atributos - Assembly x86

Desta vez, a diferença entre 32-bit e 64-bit foi unicamente relacionado ao tamanho.
Mas agora um último experimento: mov byte [r12], 42 
Desta vez com um registrador que não existe uma versão menor em 32-bit.

https://silva97.gitbook.io/assembly-x86/aprofundando-em-assembly/atributos 7/8
10/04/2021 Atributos - Assembly x86

Existem duas diferenças: O registrador mudou para ESP e um byte 41 ficou sobrando antes da
instrução.
Dando um pouco de spoiler do próximo tópico do livro, o byte que sobrou ali é o prefixo REX que
não existe em 32-bit e por isso foi interpretado como outra instrução.

Segment

Como explicado no tópico que fala sobre registradores de segmentos, algumas instruções
fazem o endereçamento em determinados segmentos.
O atributo de segmento padrão é definido de acordo com qual registrador é acessado pela
instrução.

RIP -- Segmento CS.


RSP ou RBP -- Segmento SS.
Qualquer outro registrador -- Segmento DS.

Determinadas instruções usam segmentos específicos, como no caso dos far call  ou movsb 
. Onde este último acessa DS:RSI e ES:RDI.

https://silva97.gitbook.io/assembly-x86/aprofundando-em-assembly/atributos 8/8
10/04/2021 Prefixos - Assembly x86

Prefixos
Modificando a operação

O código de máquina pode receber alguns bytes que antecedem o opcode que são chamados
de prefixos. Eles basicamente servem para modificar a operação que será executada pelo
processador.
Reparou que no tópico anterior eu falei constantemente sobre atributos "padrão"? É que existem
alguns prefixos que servem para sobrescrever os atributos da instrução.

Abaixo vou falar de alguns prefixos e explicar o que eles fazem.

Operand-size override

Este prefixo, cujo o byte é 66, serve para sobrescrever o atributo de operand-size. Ele
basicamente alterna o atributo para o seu valor não-padrão.
Se o operand-size padrão é de 32 bits ao usar esse prefixo ele alterna para 16 bits, e vice-versa.
Observe abaixo:

https://silva97.gitbook.io/assembly-x86/aprofundando-em-assembly/prefixos 1/7
10/04/2021 Prefixos - Assembly x86

No primeiro disassembly se a gente prestar atenção no código de máquina irá notar que a única
diferença entre as duas instruções, além do tamanho do operando imediato, é a presença do
byte 66 logo antes do opcode B8.

O nasm se encarrega de usar os prefixos adequados quando se mostram necessários. Porém


podemos usar as diretivas o16 , o32  e o64  no nasm para "forçar" o tamanho do operand-
size para 16, 32 ou 64 bits, respectivamente. Deste jeito o nasm usaria os prefixos corretos se
fossem necessários.
É importante entender o que a instrução faz e o que cada atributo representa nela para poder
fazer o uso correto destas diretivas.

Se você quiser forçar o uso de um prefixo em uma determinada instrução, basta fazer
o dump do byte logo antes da mesma. Exemplo:
db 0x66
mov eax, ebx 

Obs.: Isso é gambiarra. Só mostrei como curiosidade.

https://silva97.gitbook.io/assembly-x86/aprofundando-em-assembly/prefixos 2/7
10/04/2021 Prefixos - Assembly x86

Address-size override

Este prefixo de byte 67 segue a mesma lógica do anterior, só que desta vez alternando o
tamanho do atributo de address-size.
O nasm tem as diretivas a16 , a32  e a64  para explicitar um address-size para a instrução.

Um exemplo interessante de uso é com a instrução LOOP/LOOPcc . Acontece que o que


determina se esta instrução irá usar RCX, ECX ou CX é o address-size.
Vamos supor o código de 16-bit:

1 bits 16
2
3 mov ecx, 99999
4 .lp:
5 ; Faça alguma coisa
6 a32 loop .lp

Ao adicionar o prefixo 67 à instrução loop  eu sobrescrevo o address-size para 32 bits e faço a


instrução usar o registrador ECX ao invés de CX... Me permitindo assim efetuar loops mais
longos do que supostamente sou limitado.
E se por acaso eu monto esta instrução como de 32-bit, então o prefixo não será adicionado
pelo nasm e ECX ainda será usado de qualquer forma.

Cuidado ao usar a64  ou o64 . Esta diretiva demanda o uso do prefixo REX, que só
existe em submodo de 64-bit.

Segment override

Este não é um mas sim 6 prefixos diferentes usados para fazer a sobrescrita do segmento para
CS, SS, DS, ES, FS ou GS.
No tópico de registradores de segmento nós já vimos uma forma de usar o prefixo de
sobrescrita de segmento, porém também é possível usá-lo simplesmente adicionando o nome
do registrador de segmento antes da instrução.
Veja que as duas instruções abaixo são equivalentes:

https://silva97.gitbook.io/assembly-x86/aprofundando-em-assembly/prefixos 3/7
10/04/2021 Prefixos - Assembly x86

1 bits 32
2
3 mov byte [es:ebx], 32
4 es mov byte [ebx], 32

Por que você não tenta usar cada um destes prefixos para ver qual byte eles adicionam no
código de máquina?

REX

Você já deve ter notado que dá para brincar entre 32 e 16 bits... Mas e os 64 bits?
Bom, acontece que para tornar o x86-64 possível foi feita muita gambiarra adaptação no
machine code da arquitetura.

Veja este código:

1 bits 32
2
3 inc ecx
4 db 0xFF, 0xC1

Agora veja o que o disasembler nos diz sobre isso aí:

https://silva97.gitbook.io/assembly-x86/aprofundando-em-assembly/prefixos 4/7
10/04/2021 Prefixos - Assembly x86

Pois é, os bytes que eu fiz o dump manualmente resultam na mesma operação. Só que o nasm
sempre usa a primeira versão porque é menor, só tem 1 byte de tamanho em contraste com os 2
bytes da outra.

Estas duas instruções equivalentes basicamente são:

1 inc reg
2 inc r/m

Se eu escrevesse inc dword [ebx]  aí sim o nasm usaria a segunda instrução, porém para
incrementar um operando em memória.

Em 64-bit as instruções inc reg  e dec reg  simplesmente não existem. Elas foram
assassinadas para dar lugar para um novo prefixo, o REX. ( inc r/m  e dec r/m  são usadas
em seu lugar)
O REX tem um campo de 4 bits que serve para trabalhar com operações em versão de 64 bits.
Todas as alternâncias em relação a 32/64 bits é feita em um dos bits do prefixo REX.
Basicamente o REX, incluindo todas as variações de combinações de cada bit, são todos os

https://silva97.gitbook.io/assembly-x86/aprofundando-em-assembly/prefixos 5/7
10/04/2021 Prefixos - Assembly x86

bytes de 40 até 4F. (só em 64-bit, é claro)


Vejamos o exemplo:

Veja que para fazer o incremento de RCX o prefixo REX 48 foi utilizado. Em 32-bit este byte foi
interpretado como dec eax .

REP/REPE/REPNE

Instruções relacionadas a operações com blocos de dados, as famosas strings, podem ser
acompanhadas por um prefixo para fazer com que a instrução seja repetida várias vezes.
O uso deste prefixo é basicamente seguindo a mesma lógica das instruções
LOOP/LOOPE/LOOPNE , que usa uma parte do mapeamento de RCX como contador e é possível

usar uma condição extra para só repetir se a comparação der igual ou não igual.

Também é possível sobrescrever address-size para mudar o registrador usado como contador.
Observe um exemplo de reimplementação de strlen()  usando este prefixo e a instrução
scasb , tente entender o código:

https://silva97.gitbook.io/assembly-x86/aprofundando-em-assembly/prefixos 6/7
10/04/2021 Prefixos - Assembly x86

assembly.asm

1 bits 64
2
3 section .text
4
5 global my_strlen
6 my_strlen:
7 mov ecx, -1
8 xor eax, eax
9
10 repne scasb
11
12 mov eax, -2
13 sub eax, ecx
14 ret

main.c

1 #include <stdio.h>
2
3 int my_strlen(char *);
4
5 int main(void)
6 {
7 printf("Resultado: %d\n", my_strlen("Hello World!"));
8 return 0;
9 }

REP/REPE são nomes diferentes para o mesmo prefixo. Sua lógica muda
dependendo de em qual instrução foi utilizada, se em uma que faz comparação ou
não.

https://silva97.gitbook.io/assembly-x86/aprofundando-em-assembly/prefixos 7/7
10/04/2021 Flags do processador - Assembly x86

Flags do processador
Registrador EFLAGS e FLAGS

O registrador EFLAGS contém flags que servem para indicar três tipos de informações
diferentes:

Status -- Indicam o resultado de uma operação aritmética.


Control -- Controlam alguma característica de execução do processador.
System -- Servem para configurar ou indicar alguma característica do hardware relacionado
a execução do código ou do sistema.

Enquanto o RFLAGS de 64 bits contém todas as mesmas flags de EFLAGS sem nenhuma nova.
Todos os 32 bits mais significativos do RFLAGS estão reservados e sem nenhum uso
atualmente.
Observe a figura abaixo retirada do Intel Developer's Manual Vol. 1, mostrando uma visão geral
do bits de EFLAGS:

https://silva97.gitbook.io/assembly-x86/aprofundando-em-assembly/flags-do-processador 1/5
10/04/2021 Flags do processador - Assembly x86

Status Flags

Instruções que fazem operações aritméticas modificam estas flags conforme o valor do
resultado da operação. São instruções como ADD , SUB , MUL  e DIV  por exemplo.
Porém um detalhe que é interessante saber é que existem duas instruções que normalmente
são utilizadas para definir essas flags para serem usadas junto com uma instrução condicional.
Elas são: CMP  e TEST .
A instrução cmp  nada mais é do que uma instrução que faz a mesma operação que sub 
porém sem modificar o valor dos operandos. Só que é uma operação aritmética de subtração da
mesma forma.

Enquanto test  faz uma operação bitwise AND (E bit a bit) também sem modificar os
operandos. Ou seja, o mesmo que a instrução and .
Veja a tabela abaixo com todas as status flags:

Bit Nome Sigla Descrição

https://silva97.gitbook.io/assembly-x86/aprofundando-em-assembly/flags-do-processador 2/5
10/04/2021 Flags do processador - Assembly x86

Setado se uma condição de Carry ou Borrow acontecer no bit mais


Carry
0 CF significativo do resultado. Basicamente indica o overflow de um valor
Flag
não-sinalizado.

Parity Setado se o byte menos significativo do resultado conter um número


2 PF
Flag par de bits 1.

Auxiliary
Setado se uma condição de Carry ou Borrow acontecer no bit 3 do
4 Carry AF
resultado.
Flag

Zero
6 ZF Setado se o resultado for zero.
Flag

Sign Setado para o mesmo valor do bit mais significativo do resultado.


7 SF
Flag Onde 0 indica um valor positivo e 1 indica um valor negativo.

Overflow Setado se o resultado não tiver o sinal esperado da operação


11 OF
Flag aritmética. Basicamente indica o overflow de um número sinalizado.

Carry, ou carrinho/transporte, é o que a gente conhece no Brasil como "vai um" em


uma operação aritmética de soma.
Borrow é o mesmo princípio porém em aritmética de subtração, em linguagem
coloquial chamado de "pegar emprestado".

Dentre essas flags somente CF pode ser modificada diretamente, e isto é feito com as seguintes
instruções:

1 stc ; (Set CF) Seta o valor da Carry Flag


2 clc ; (Clear CF) Zera o valor da Carry Flag
3 cmc ; (coMplement CF) Inverte o valor da Carry Flag

Control Flags

Bit Nome Sigla Descrição

https://silva97.gitbook.io/assembly-x86/aprofundando-em-assembly/flags-do-processador 3/5
10/04/2021 Flags do processador - Assembly x86

Direction Controla a direção para onde as instruções de string ( MOVS , SCAS ,


10 DF
Flag STOS , CMPS  e LODS ) irão decorrer a memória.

Se DF estiver setada, as instruções de string irão decrementar o valor do(s) registrador(es). Se


estiver zerada ela irá incrementar, que é o valor padrão para esta flag.

1 std ; (Set DF) Seta o valor da Direction Flag


2 cld ; (Clear DF) Zera o valor da Direction Flag

Caso sete o valor desta flag é importante que a zere novamente em seguida. Código
compilado normalmente espera que por padrão esta flag esteja zerada.
Comportamentos imprevistos podem acontecer caso você não a zere depois de usar.

System Flags

Estas flags podem ser lidas por qualquer programa porém somente o sistema operacional pode
modificar seus valores. (exceto ID)
Abaixo irei falar somente das flags que nos interessam saber por agora.

Bit Nome Sigla Descrição

Se setada o processador irá executar as instruções do programa


passo a passo. Neste modo o processador dispara uma
8 Trap Flag TF
exception para cada instrução executada. É normalmente usada
para depuração de código.

Interrupt Controla a resposta do processador para interrupções que podem


9 IF
enable Flag ser ignoradas. (interrupções mascaráveis)

12- I/O Privilege Indica o nível de acesso para a comunicação direta com o
IOPL
13 Level field hardware (operações de I/O) do programa atual.

Nested Task Se setada indica que a tarefa atual está vinculada com uma tarefa
14 NT
flag anterior. Esta flag controla o comportamento da instrução IRET .

https://silva97.gitbook.io/assembly-x86/aprofundando-em-assembly/flags-do-processador 4/5
10/04/2021 Flags do processador - Assembly x86

Se setada as exceptions disparadas pelo processador são


16 Resume Flag RF temporariamente desabilitadas na instrução seguinte. Geralmente
usada por depuradores.

Virtual-8086 Em protected mode, se esta flag for setada o processador entra


17 VM
Mode flag em modo Virtual-8086.

Identification Se um processo conseguir setar ou zerar esta flag, isto indica que
21 ID
flag o processador suporta a instrução CPUID .

IOPL na verdade não é uma flag mas sim um campo de 2 bits que indica o nível de
privilégio de acesso para operações de I/O a partir da porta física do processador.

As instruções abaixo podem ser utilizadas para modificar o valor de IF:

1 sti ; (Set IF) Seta o valor da Interrupt Flag


2 cli ; (Clear IF) Zera o valor da Interrupt Flag

FLAGS (16-bit)

Em real mode dentre as system flags somente TF e IF existem. E elas são consideradas control
flags junto com DF, ou seja, não dependem de qualquer tipo de privilégio para serem
modificadas.

https://silva97.gitbook.io/assembly-x86/aprofundando-em-assembly/flags-do-processador 5/5
10/04/2021 Instruções condicionais - Assembly x86

Instruções condicionais
Usando as status flags

As instruções condicionais basicamente avaliam as status flags para executar uma operação
apenas se a condição for atendida. Existem condições que testam o valor de mais de uma flag
em combinação para casos diferentes.

A nomenclatura de escrita de uma instrução condicional é o seu nome seguido de um 'cc' que é
sigla para conditional code.
Abaixo uma tabela de códigos condicionais válidos para as instruções CMOVcc , SETcc  e Jcc 
:

Os termos "abaixo" e "acima" usados na descrição se referem a verificação de um


valor numérico não-sinalizado.
Enquanto "maior" e "menor" é usado para se referir a um valor numérico sinalizado.

cc Descrição Condição

A if Above/se acima CF=0 e ZF=0

AE if Above or Equal/se acima ou igual CF=0

B if Below/se abaixo CF=1

BE if Below or Equal/se acima ou igual CF=1 ou ZF=1

C if Carry/se carry flag estiver setada CF=1

E if Equal/se igual ZF=1

G if Greater/se maior ZF=0 e SF=OF

GE if Greater or Equal/se maior ou igual SF=OF

L if Less/se menor SF!=OF

https://silva97.gitbook.io/assembly-x86/aprofundando-em-assembly/instrucoes-condicionais 1/3
10/04/2021 Instruções condicionais - Assembly x86

LE if Less or Equal/se menor ou igual ZF=1 ou SF!=OF

NA if Not Above/se não acima CF=1 ou ZF=1

NAE if Not Above or Equal/se não acima ou igual CF=1

NB if Not Below/se não abaixo CF=0

NBE if Not Below or Equal/se não abaixou ou igual CF=0 e ZF=0

NC if Not Carry/se carry flag não estiver setada CF=0

NE if Not Equal/se não igual ZF=0

NG if Not Greater/se não maior ZF=1 ou SF!=OF

NGE if Not Greater or Equal/se não maior ou igual SF!=OF

NL if Not Less/se não menor SF=OF

NLE if Not Less or Equal/se não menor ou igual ZF=0 e SF=OF

NO if Not Overflow/se não setado overflow flag OF=0

NP if Not Parity/se não setado parity flag PF=0

NS if Not Sign/se não setado sign flag SF=0

NZ if Not Zero/se não setado zero flag ZF=0

O if Overflow/se setado overflow flag OF=1

P if Parity/se setado parity flag PF=1

PE if Parity Even/se parity indica par PF=1

PO if Parity Odd/se parity indicar ímpar PF=0

S if Sign/se setado sign flag SF=1

Z if Zero/se setado zero flag ZF=1

JCXZ e JECXZ

Além das condições acima, existem mais três Jcc  que testam o valor de CX, ECX e RCX
respectivamente.
https://silva97.gitbook.io/assembly-x86/aprofundando-em-assembly/instrucoes-condicionais 2/3
10/04/2021 Instruções condicionais - Assembly x86

Jcc Descrição Condição

JCXZ Jump if CX is zero/pula se CX for igual a zero CX=0

JECXZ Jump if ECX is zero/pula se ECX for igual a zero ECX=0

JRCXZ Jump if RCX is zero/pula se RCX for igual a zero RCX=0

A última instrução, obviamente, somente existe em submodo de 64-bit. Enquanto JCXZ  não
existe em 64-bit.

No código de máquina o opcode desta instrução é E3 e a alternância entre o tamanho do


registrador é feita de acordo com o atributo address-size, sendo modificado pelo prefixo 67.

https://silva97.gitbook.io/assembly-x86/aprofundando-em-assembly/instrucoes-condicionais 3/3
10/04/2021 Programando no MS-DOS - Assembly x86

Programando no MS-DOS
Conhecendo o ambiente

O clássico MS-DOS, antigo sistema operacional de 16 bits da Microsoft, foi muito utilizado e até
hoje existem projetos relacionados a esse sistema.
Existe por exemplo o FreeDOS que é um sistema operacional de código aberto e que é
compatível com o MS-DOS.

A famosa "telinha preta" do Windows, o prompt de comando, muitas vezes é erroneamente


chamado de MS-DOS devido aos dois usarem o mesmo shellscript chamado de Batch. Isso
fazia com que comandos rodados no MS-DOS fossem quase totalmente compatíveis na linha de
comando do Windows.
Mas o prompt de comando do Windows não é o MS-DOS. Este é apenas o Terminal do sistema
operacional Windows e que usa uma versão mais avançada do mesmo shellscript que rodava
no MS-DOS.

Real mode

O MS-DOS era um sistema operacional que rodava em modo de processamento real mode, o
famoso modo de 16-bit que é compatível com o 8086 original.

Text mode

Existem modos diferentes de se usar a saída de vídeo, isto é, o monitor do computador. Dentre
os vários modos que o monitor suporta, existe a divisão entre modo de texto (text mode) e
modo de vídeo (video mode).

O modo de vídeo é este modo que o seu sistema operacional está rodando agora. Nele o
software define informações de cor para cada pixel da tela, formando assim imagens desde
mais simples (formas opacas) até as mais complexas (imagens renderizadas
tridimensionalmente).
Todas essas imagens que você vê são geradas pixel a pixel para poderem serem apresentadas
pelo monitor.

https://silva97.gitbook.io/assembly-x86/aprofundando-em-assembly/programando-no-ms-dos 1/3
10/04/2021 Programando no MS-DOS - Assembly x86

O MS-DOS rodava em modo de texto, cujo este modo é bem mais simples.
Ao invés de você definir cada pixel que o monitor apresenta, você define unicamente
informações de caracteres.
Imagine por exemplo que seu monitor seja dividido em grade formando 80x25 quadrados na
tela. Ou seja, 80 colunas e 25 linhas.
Ao invés de definir cada pixel, você apenas definia qual caractere seria apresentado naquele
quadrado e um atributo para este caractere.

Executáveis .COM

O formato de executável mais básico que o MS-DOS suportava era os de extensão .com que era
um raw binary. Este termo é usado para se referir a um "binário puro", isto é, um arquivo binário
que não tem qualquer tipo de formatação especial.
Uma comparação com arquivos de texto seria você comparar um código fonte em C com um
arquivo de texto "normal".
O código fonte em C também é um arquivo de texto, porém ele tem formatações especiais que
seguem a sintaxe da linguagem de programação.
Enquanto o arquivo de texto "normal" é apenas texto, sem seguir qualquer regra de formatação.

No caso do raw binary é a mesma coisa, informação binária sem qualquer regra de formatação
especial.
Este executável do MS-DOS tinha como "entry point" logo o primeiro byte do arquivo. Como eu já
disse, não tinha qualquer regra especial nele então você poderia organizá-lo da maneira que
quisesse manualmente.

Execução do .COM

O processo que o MS-DOS fazia para executar este tipo de executável era tão simples quanto
possível. Seguindo o fluxo:

Recebe um comando na linha de comando.


Coloca o tamanho em bytes dos argumentos passados pela linha de comando no offset
0x80 do segmento do executável.
Coloca os argumentos da linha de comando no offset 0x81 como texto puro, sem qualquer
formatação.
Carrega todo o .COM no offset 0x100
Define os registradores DS, SS e ES para o segmento onde o executável foi carregado.
Faz um call  no endereço onde o executável foi carregado.

https://silva97.gitbook.io/assembly-x86/aprofundando-em-assembly/programando-no-ms-dos 2/3
10/04/2021 Programando no MS-DOS - Assembly x86

Perceba que a chamada do executável nada mais é que um call , por isso esses executáveis
finalizavam simplesmente executando um ret .
Mais simples impossível, né?

ORG | Origin

org endereço_inicial

A esta altura você já deve ter reparado que o nasm calcula o endereço dos rótulos sozinho, sem
precisar da nossa ajuda, né?
Então, mas ele faz isso considerando que o primeiro byte do nosso arquivo binário esteja
especificamente em 0. Ou seja, ele começa a contar do zero em diante.
No caso de um executável .COM ele é carregado no offset 0x100 e não em 0, então o cálculo vai
dar errado.

Mas os desenvolvedores do nasm não são amadores e sabem que existe esse tipo de
necessidade, e é para isso que serve a diretiva org .
Com esta diretiva podemos dizer para o nasm a partir de qual endereço ele deve calcular o
endereço dos rótulos, ou seja, o endereço de origem do nosso binário.
Veja o exemplo:

1 bits 16
2 org 0x100
3
4 msg: db "abc"
5
6 codigo:
7 mov ax, 77
8 ret

O rótulo codigo  ao invés de ter o endereço calculado como 0x0003 como normalmente teria,
terá o endereço 0x0103 devido ao uso da diretiva org  na segunda linha.

https://silva97.gitbook.io/assembly-x86/aprofundando-em-assembly/programando-no-ms-dos 3/3
10/04/2021 Interrupções de software e exceções - Assembly x86

Interrupções de software e exceções


Interrupções e exceções sendo entendidas na prática.

Uma interrupção é um sinal enviado para o processador solicitando a atenção dele para a
execução de outro código. Então ele para o que está executando agora, executa este
determinado código da interrupção e depois volta a executar o código que estava executando
antes.
Este sinal é geralmente enviado por um hardware externo para a CPU, cujo o mesmo é chamado
de IRQ — Interrupt Request — que significa "pedido de interrupção".

Enquanto a interrupção de software é executada de maneira muito semelhante a uma chamada


de procedimento por far call . Ela é basicamente uma interrupção que é executada pelo
software rodando na CPU, daí o nome.

Interrupt Descriptor Table

O código que é executado quando uma interrupção é disparada se chama handler, e o endereço
do mesmo é definido na IDT — Interrupt Descriptor Table —.
Esta tabela nada mais é que uma sequência de valores indicando o offset e segmento do código
à ser executado. É basicamente uma array onde cada elemento contém estas duas
informações. Poderíamos representar em C da seguinte forma:

1 // Em 16-bit
2
3 struct elem {
4 uint16_t offset;
5 uint16_t segment;
6 }
7
8 struct elem idt[256];

Ou seja, o número que identifica a interrupção nada mais é que o índice a ser lido no vetor.

Exception

https://silva97.gitbook.io/assembly-x86/aprofundando-em-assembly/interrupcoes-de-software 1/8
10/04/2021 Interrupções de software e exceções - Assembly x86

Provavelmente você já ouviu falar em exception. A exception é tratada pelo processador da


mesma forma que uma interrupção, inclusive o handler delas fica na mesma tabela. Por
exemplo quando você comete um erro clássico de tentar acessar uma região de memória
inválida ou sem permissões adequadas em C, você compila o código e recebe a clássica
mensagem segmentation fault.

Neste caso a exceção que foi disparada pelo processador se chama General Protection e pode
ser referida pelo mnemônico #GP, seu índice na tabela é 13.

SDM volume 1, capítulo 6

Essa exceção é disparada quando há um problema na referência de memória ou qualquer


proteção à memória que foi violada. Como por exemplo ao tentar escrever em uma seção de
memória que não se tem permissão para isso.

Um sistema operacional configura uma exceção da mesma forma que configura uma
interrupção, modificando a IDT para apontar para o código que ele quer que execute. Neste caso
o índice 13 precisaria ser modificado.

No Linux basicamente o que o sistema faz é criar um handler que trata a exceção e
manda um sinal para o processo. Este sinal o processo pode configurar como ele

https://silva97.gitbook.io/assembly-x86/aprofundando-em-assembly/interrupcoes-de-software 2/8
10/04/2021 Interrupções de software e exceções - Assembly x86

quer tratar, mas por padrão o processo escreve uma mensagem no terminal e
finaliza.

IDT em Real Mode

A instrução int  é usada para disparar interrupções de software ou exceções.


Bastando simplesmente passar o índice da interrupção como operando.

Vamos ver na prática a configuração de uma interrupção em 16-bit. Para isso vamos usar o MS-
DOS para que fique mais simples.

A IDT está localizada no endereço 0, por isso podemos configurar para acessar o segmento zero
e assim o offset seria o índice de cada elemento da IDT. O que precisamos fazer é acessar o
índice que queremos modificar na IDT, depois é só jogar o offset e segmento do procedimento
que queremos que seja executado. Em 16-bit isso acontece de uma maneira muito mais simples
do que em protected mode, por isso é ideal para entender na prática.

Eis o código:

int.asm

1 bits 16
2 org 0x100
3
4 VADDR equ 0xb800
5
6 ; ID, segmento, offset
7 %macro setint 3
8 mov bx, (%1) * 4
9 mov word [es:bx], %3
10 add bx, 2
11 mov word [es:bx], %2
12 %endmacro
13
14
15 ; -- Main -- ;
16 mov ax, 0
17 mov es, ax
18
19 setint 0x66, cs, int_putchar
20

https://silva97.gitbook.io/assembly-x86/aprofundando-em-assembly/interrupcoes-de-software 3/8
10/04/2021 Interrupções de software e exceções - Assembly x86

21 mov al, 'A'


22 mov ah, 0x0B
23 int 0x66
24
25 mov ah, 0x0C
26 int 0x66
27
28 ret
29
30 ; -- Interrupção -- ;
31 int_cursor: dw 0
32
33 ; Argumentos:
34 ; AL Caractere
35 ; AH Atributo
36 int_putchar:
37 push es
38 mov bx, VADDR
39 mov es, bx
40
41 mov di, [int_cursor]
42 mov word [es:di], ax
43
44 add word [int_cursor], 2
45 pop es
46 iret

Para montar e testar usando o Dosbox:

1 $ nasm int.asm -o int.com


2 $ dosbox int.com

https://silva97.gitbook.io/assembly-x86/aprofundando-em-assembly/interrupcoes-de-software 4/8
10/04/2021 Interrupções de software e exceções - Assembly x86

A interrupção simplesmente escreve os caracteres na parte superior esquerda da tela.

Note que a interrupção retorna usando a instrução iret  ao invés de ret . Em 16-bit a única
diferença nesta instrução é que ela também desempilha o registrador de flags, que é empilhado
pelo processador ao disparar a interrupção/exceção.

Perceba que é unicamente um código de exemplo. Esta não é uma maneira segura
de se configurar uma interrupção tendo em vista que seu handler está na memória do
.com que, após finalizar sua execução, a memória será sobrescrita por outro
programa executado posteriormente.

Vamos dessa vez configurar uma exceção. A exceção que vamos configurar é a #BP de índice 3.
Se você já usou um depurador, ou pelo menos tem uma noção à respeito, sabe que "breakpoint"
é um ponto no código onde o depurador faz uma parada e te permite analisar o programa
enquanto ele fica em pausa.

Os depuradores modificam a instrução original colocando a instrução que dispara a


exceção de breakpoint, depois tratam o sinal enviado para o processo, restauram a
instrução original e continuam seu trabalho.
https://silva97.gitbook.io/assembly-x86/aprofundando-em-assembly/interrupcoes-de-software 5/8
10/04/2021 Interrupções de software e exceções - Assembly x86

O breakpoint nada mais é que uma exceção que é disparada por uma instrução. Podemos usar
int 0x03 para fazer isso, porém esta instrução tem 2 bytes de tamanho e não é muito

apropriada para um depurador usar. Por isto existe a instrução int3  que dispara #BP
explicitamente e tem somente 1 byte de tamanho. (opcode 0xCC)

int.asm

1 bits 16
2 org 0x100
3
4 ; ID, segmento, offset
5 %macro setint 3
6 mov bx, (%1) * 4
7 mov word [es:bx], %3
8 add bx, 2
9 mov word [es:bx], %2
10 %endmacro
11
12
13 ; -- Main -- ;
14
15 xor ax, ax
16 mov es, ax
17
18 setint 0x03, cs, break
19
20 int3
21 int3
22
23 ret
24
25 ; -- Breakpoint -- ;
26
27 break:
28 mov ah, 0x0E
29 mov al, 'X'
30 int 0x10
31 iret

https://silva97.gitbook.io/assembly-x86/aprofundando-em-assembly/interrupcoes-de-software 6/8
10/04/2021 Interrupções de software e exceções - Assembly x86

Repare que cada execução de int3 executou o código do nosso procedimento break. Este por
sua vez imprimiu o caractere 'X' na tela do nosso MS-DOS usando a interrupção 0x10 que será
explicada no próximo capítulo.

Sinais

Só para deixar mais claro o que falei sobre os sinais que são enviados para o processo quando
uma exception é disparada, aqui um código em C de exemplo:

1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <string.h>
4 #include <signal.h>
5
6 void segfault(int signum)
7 {
8 fputs("Tá pegando fogo bixo!\n", stderr);
9 exit(signum);
10 }
11
12 // Esse código também funciona no Windows.
13 int main(void)
14 {
15 char *desastre = NULL;
16 signal(SIGSEGV, segfault);

https://silva97.gitbook.io/assembly-x86/aprofundando-em-assembly/interrupcoes-de-software 7/8
10/04/2021 Interrupções de software e exceções - Assembly x86

17
18 strcpy(desastre, "Eita!");
19
20 puts("Tchau mundo!");
21 return 0;
22 }
23

https://silva97.gitbook.io/assembly-x86/aprofundando-em-assembly/interrupcoes-de-software 8/8
10/04/2021 Procedimentos do BIOS - Assembly x86

Procedimentos do BIOS
Existem algumas interrupções que são criadas pelo próprio BIOS do sistema. Vamos ver
algumas delas aqui.

BIOS — Basic Input/Output System — é o firmware da placa-mãe responsável pela inicialização


do hardware. Ele quem começa o processo de boot do sistema, além de anteriormente fazer um
teste rápido (POST — Power-On Self Test) para verificar se o hardware está funcionando
apropriadamente.

Mas além de fazer essa tarefa de inicialização do PC, ele também define algumas interrupções
que podem ser usadas pelo software em 16-bit para tarefas básicas. E é daí que vem seu nome,
já que estas tarefas são operações básicas de entrada e saída de dados para o hardware.

Cada interrupção não faz um procedimento único, mas sim vários procedimentos relacionados
à um determinado hardware. Qual procedimento especificamente será executado é, na maioria
das vezes, definido no registrador AH ou AX .

INT 0x10

Esta interrupção tem procedimentos relacionados ao vídeo, como a escrita de caracteres na tela
ou até mesmo alterar o modo de vídeo.

AH 0x0E

O procedimento INT 0x10 / AH 0x0E simplesmente escreve um caractere na tela em modo


teletype, que é um nome chique para dizer que o caractere é impresso na posição atual do
cursor e atualiza a posição do mesmo. É algo bem semelhante ao que a gente vê sobre um
sistema operacional usando uma função como putchar() em C.

Esse procedimento recebe como argumento no registrador AL o caractere a ser impresso, e em
BH o número da página.

O número da página varia entre 0 e 7. São 8 páginas diferentes que podem ser apresentadas
para o monitor como o conteúdo da tela. Por padrão é usada a página 0, mas você pode alternar

https://silva97.gitbook.io/assembly-x86/aprofundando-em-assembly/procedimentos-do-bios 1/9
10/04/2021 Procedimentos do BIOS - Assembly x86

entre as páginas fazendo com que conteúdo diferente seja apresentado na tela sem perder o
conteúdo da outra página.

Se você já usou o MS-DOS deve ter visto programas, como editores de código, que imprimiam
uma interface de texto (TUI), mas depois que finalizava o conteúdo do prompt voltava para a
tela... Esses programas basicamente alternavam de página.

exemplo.asm

1 mov ah, 0x0E


2 mov al, 'H'
3 int 0x10
4
5 mov al, 'i'
6 int 0x10
7
8 ret

No exemplo acima usamos a interrupção duas vezes para imprimir dois caracteres diferentes,
fazendo assim um "Hello World" de míseros 11 bytes.

Poderíamos fazer um procedimento para escrever uma string inteira usando um loop. Ficaria
assim:

hello.asm

1 bits 16
2 org 0x100
3
4 mov si, string
5 call echo
6
7 ret
8
9 string: db "Hello World!", 0
10
11 ; SI = ASCIIZ string
12 ; BH = Página
13 echo:
14 mov ah, 0x0E
15
16 .loop:
17 lodsb
18 test al, al
19 jz .stop

https://silva97.gitbook.io/assembly-x86/aprofundando-em-assembly/procedimentos-do-bios 2/9
10/04/2021 Procedimentos do BIOS - Assembly x86

20
21 int 0x10
22 jmp .loop
23
24
25 .stop:
26 ret

AH 0x02

AH BH DH DL

0x02 Página Linha Coluna

Esse procedimento seta a posição do cursor em uma determinada página.

AH 0x03

AH BH

0x03 Página

Pega a posição atual do cursor na página especificada. Retornando:

CH CL DH DL

Scanline inicial Scanline final Linha Coluna

AH 0x05

AH AL

0x05 Página

Alterna para a página especificada por AL, que deve ser um número entre 0 e 7.

AH 0x09

AH AL BH BL CX

https://silva97.gitbook.io/assembly-x86/aprofundando-em-assembly/procedimentos-do-bios 3/9
10/04/2021 Procedimentos do BIOS - Assembly x86

0x09 Caractere Página Atributo Vezes para imprimir o caractere

Imprime o caractere AL na posição atual do cursor CX vezes, sem atualizar o cursor. BL é o


atributo do caractere, que será explicado mais embaixo.

AH 0x0A

AH AL BH CX

0x0A Caractere Página Vezes para imprimir

Mesma coisa que o procedimento anterior, mudando somente que não é especificado um
atributo para o caractere.

AH 0x13

Registrador Parâmetro

AL Modo de escrita

BH Página

BL Atributo

CX Tamanho da string

DH Linha

DL Coluna

ES:BP Endereço da string

Este procedimento imprime uma string na tela podendo ser especificado um atributo. O modo
de escrita pode variar entre 0 e 3, se trata de 2 bits especificando duas informações diferentes:

Bit Informação

0 Se ligado, atualiza a posição do cursor.

1 Se desligado, BL é usado para definir o atributo. Se ligado, o atributo é lido da string.

https://silva97.gitbook.io/assembly-x86/aprofundando-em-assembly/procedimentos-do-bios 4/9
10/04/2021 Procedimentos do BIOS - Assembly x86

No caso do segundo bit, se estiver ligado então o procedimento irá ler a string considerando que
se trata de uma sequência de caractere e atributo. Assim cada caractere pode ter um atributo
diferente. Conforme exemplo abaixo:

str: db 'A', 0x05, 'B', 0x0C, 'C', 0x0A

Caracteres de Ação

Os procedimentos 0x0E e 0x13 interpretam caracteres especiais como determinadas ações que
devem ser executadas ao invés de imprimir o caractere na tela. Cada caractere faz uma ação
diferente, conforme tabela abaixo:

Seq. de
Caractere Nome Ação
escape

0x07 Bell \a Emite um beep.

0x08 Backspace \b Retorna o cursor uma posição.

Horizontal
0x09 \t Avança o cursor 4 posições.
TAB

Move o cursor verticalmente para a próxima


0x0A Line feed \n
linha.

Carriage
0x0D \r Move o cursor para o início da linha.
return

Você pode combinar 0x0D e 0x0A para fazer uma quebra de linha.

INT 0x16

Os procedimentos definidos nesta interrupção são todos relacionados à entrada do teclado.


Toda vez que o usuário pressiona uma tecla, esta é lida e armazenada no buffer do teclado. Se
você tentar ler do buffer sem haver dados lá, então o sistema irá ficar esperando o usuário
inserir uma entrada.

https://silva97.gitbook.io/assembly-x86/aprofundando-em-assembly/procedimentos-do-bios 5/9
10/04/2021 Procedimentos do BIOS - Assembly x86

AH 0x00

Lê um caractere do buffer do teclado e o remove de lá. Retorna os seguintes valores:

Registrador Valor

AL Código ASCII do caractere

AH Scancode da tecla. *

Scancode é um número que identifica a tecla e não especificamente o caractere inserido.

AH 0x01

Verifica se há um caractere disponível no buffer sem removê-lo do mesmo. Se houver caractere


disponível, retorna:

Registrador Valor

AL Código ASCII

AH Scancode

O procedimento também modifica a Zero Flag para especificar se há ou não caractere


disponível. A define para 0 se houver, caso contrário para 1.

Você pode usar em seguida o AH 0x00 para remover o caractere do buffer, se assim
desejar. Desse jeito é possível pegar um caractere sem fazer uma pausa.

AH 0x02

Pega status relacionados ao teclado. É retornado em AL 8 flags diferentes, cada uma


especificando informações diferentes sobre o estado atual do teclado. Conforme tabela:

Bit Flag

0 Tecla shift direita está pressionada.

https://silva97.gitbook.io/assembly-x86/aprofundando-em-assembly/procedimentos-do-bios 6/9
10/04/2021 Procedimentos do BIOS - Assembly x86

1 Tecla shift esquerda está pressiona.

2 Tecla ctrl está pressionada.

3 Tecla alt está pressionada.

4 Scroll lock está ligado.

5 Num lock está ligado.

6 Caps lock está ligado.

7 Modo Insert está ligado.

Memória de Vídeo em Text Mode

Quando o sistema está em modo texto, a memória onde se armazena os caracteres começa no
endereço 0xb800:0x0000 e ela é estruturada da seguinte forma:

1 // Em modo de texto 80x25, padrão do MS-DOS


2
3 struct character {
4 uint8_t ascii;
5 uint8_t attribute;
6 };
7
8 struct character vmem[8][25][80];

Ou seja, começando em 0xb800:0x0000 as páginas estão uma atrás da outra na memória como
uma grande array.

Atributo

O caractere nada mais é que o código ASCII do mesmo, já o atributo é um valor usado para
especificar informações de cor e blink do caractere. Podemos representar o valor em
hexadecimal e desta forma o digito hexadecimal mais a direita seria referente ao atributo do
texto, e o mais a esquerda referente ao atributo do fundo.

Os 4 bits (nibble) mais significativo indicam o atributo do fundo e os 4 bits menos significativos
o atributo do texto, gerando uma cor na escala RGB. Caso não conheça, esta é a escala de cor
da luz onde as cores primárias Red (vermelo), Green (verde) e Blue (azul) são usadas em

https://silva97.gitbook.io/assembly-x86/aprofundando-em-assembly/procedimentos-do-bios 7/9
10/04/2021 Procedimentos do BIOS - Assembly x86

conjunto para formar qualquer outra cor. Conforme figura abaixo, podemos ver qual bit significa
o quê:

Bits de um atributo e seus significados

O atributo intensidade no atributo de texto, caso ligado, faz com que a cor do texto fique mais
viva. Desligado as cores são mais escuras.
Já o atributo blink especifica se o texto deve permanecer piscando. Caso ativo, o texto irá ficar
aparecendo e desaparecendo da tela constantemente.

Olá Mundo

Um exemplo de "Hello World" usando alguns conceitos apresentados aqui.

1 bits 16
2 org 0x100
3
4 %macro puts 2
5 mov bx, %1
6 mov bp, %2
7 call puts
8 %endmacro
9
10
11 puts 0x000A, str1
12 puts 0x000C, str2
13
14 ret
15
16 str1: db `Hello World!\r\n`, 0
17 str2: db "Second message.", 0
18
19 ; BL = Atributo
20 ; BH = Página

https://silva97.gitbook.io/assembly-x86/aprofundando-em-assembly/procedimentos-do-bios 8/9
10/04/2021 Procedimentos do BIOS - Assembly x86

21 ; BP = ASCIIZ String
22 puts:
23 mov ah, 0x03
24 int 0x10
25
26 mov di, bp
27 call strlen
28 mov cx, ax
29
30 mov al, 0b01
31 mov ah, 0x13
32 int 0x10
33
34 ret
35
36
37 ; DI = ASCIIZ String
38 ; Retorna o tamanho da string
39 strlen:
40 mov cx, -1
41 xor ax, ax
42
43 repnz scasb
44
45 mov ax, -2
46 sub ax, cx
47 ret

Para uma lista completa de todas as interrupções definidas pelo BIOS, sugiro a
leitura:

http://vitaly_filatov.tripod.com/ng/asm/asm_001.html

https://silva97.gitbook.io/assembly-x86/aprofundando-em-assembly/procedimentos-do-bios 9/9
10/04/2021 Usando instruções da FPU - Assembly x86

Usando instruções da FPU


Aprendendo a usar o x87 para fazer cálculos.

Podemos usar a FPU para fazer cálculos com valores de ponto flutuante. Mas também é
possível usar valores inteiros nos cálculos com a FPU.

A arquitetura x86 segue a padronização IEEE-754 para a representação de valores de ponto


flutuante.

Apenas algumas instruções da FPU serão ensinadas aqui, não sendo uma lista
completa.

Registradores

As instruções da FPU trabalham com os registradores de st0 até st7, são 8 registradores de 80
bits de tamanho cada. Juntos eles formam uma stack (pilha) onde você pode empilhar valores
para trabalhar com eles ou desempilhar para armazenar o resultado das operações em algum
lugar.

O empilhamento de valores funciona colocando o novo valor em st0 e, todos os outros valores
anteriores, são "empurrados" para os registradores posteriores. Um exemplo bem leviano desta
operação:

1 st0 = 10
2 st1 = 20
3 st2 = 30
4
5 * é feito um push do valor 40
6
7 st0 = 40
8 st1 = 10
9 st2 = 20
10 st3 = 30
11
12 * é feito um pop, o valor 40 é pego.

https://silva97.gitbook.io/assembly-x86/aprofundando-em-assembly/usando-instrucoes-da-fpu 1/14
10/04/2021 Usando instruções da FPU - Assembly x86

13
14 st0 = 10
15 st1 = 20
16 st2 = 30

Um adendo que convencionalmente compiladores de C não trabalham com valores


de ponto flutuante desta maneira, porque a arquitetura x86 hoje em dia tem maneiras
mais eficientes de fazer estes cálculos.

Detalhe que só é possível usar esses registradores em instruções da FPU, algo como este
código está errado:

mov eax, st1

Formato das instruções

As instruções da FPU todas começam com um prefixo F, e as que operam com valores inteiros
também tem uma letra I após a letra F. Por fim, instruções que fazem o pop de um valor da pilha,
isto é, remove o valor de lá, terminam com um sufixo P. Entendendo isto fica muito mais fácil
identificar o que cada mnemônico significa e assim você não perde tempo tentando decorar
uma sopa de letrinhas, se estas letras existem é porque tem um motivo.

Caso tenha vindo de uma arquitetura RISC, geralmente o termo load é usado para a
operação em que você carrega um valor da memória para um registrador. Já store é
usado para se referir a operação contrária, do registrador para a memória.

Neste caso as operações podem ser feita entre registradores da FPU também,
conforme será explicado.

Fazer load de um valor é basicamente carregar um valor da memória para a pilha em st0, é
como um push quando estamos falando da pilha convencional. A diferença aqui é a maneira
como o valor é colocado na pilha, como já foi explicado anteriormente.

https://silva97.gitbook.io/assembly-x86/aprofundando-em-assembly/usando-instrucoes-da-fpu 2/14
10/04/2021 Usando instruções da FPU - Assembly x86

Já o store é pegar o valor da pilha, mais especificamente em st0, e armazenar em algum lugar
da memória. Algumas instruções store permite armazenar o valor em outro registrador da FPU.

Aqui eu vou ensinar a usar a FPU mas sem diretamente trabalhar com a linguagem C e os tipos
float ou double, pois como já foi mencionado, não é assim que o compilador trabalha com
cálculos de ponto flutuante.

Vou usar a notação memXXfp e memXXint  para especificar valores na memória que sejam float
ou inteiro, respectivamente. Onde XX seria o tamanho do valor em bits. Já a notação st(i) 
será usada para se referir a qualquer registrador de st0 até st7. O st(0) seria o registrador st0
especificamente.

FINIT | Initialization

finit

Normalmente vamos usar esta instrução antes de começar a usar a FPU, pois ela reseta a FPU
para o estado inicial. Deste jeito quaisquer operações anteriores com a FPU são descartadas e
podemos começar tudo do zero. Assim não é necessário, por exemplo, a gente limpar a pilha da
FPU toda vez que terminar as operações com ela. Basta rodar esta instrução antes de usá-la.

FLD, FILD | (Integer) Load

1 fld mem32fp
2 fld mem64fp
3 fld mem80fp
4 fld st(i)
5
6 fild mem16int
7 fild mem32int
8 fild mem64int

A instrução fld carrega um valor float de 32, 64 ou 80 bits para st0. Repare como é possível
dar load em um dos registradores da pilha, o que torna possível retrabalhar com valores
anteriormente carregados. Se você rodar fld st0 estará basicamente duplicando o último
valor carregado.

https://silva97.gitbook.io/assembly-x86/aprofundando-em-assembly/usando-instrucoes-da-fpu 3/14
10/04/2021 Usando instruções da FPU - Assembly x86

Já fild carrega um valor inteiro sinalizado de 16, 32 ou 64 bits, o convertendo para float de 64


bits.

Load Constant

Existem várias instruções para dar push de valores constantes na pilha da FPU, e elas são:

Instrução Valor

FLD1 +1.0

FLDZ +0.0

FLDL2T log2(10)

FLDL2E log2(e)

FLDPI Valor de PI. (3.14 blabla)

FLDLG2 log10(2)

FLDLN2 logE(2)

FST, FSTP | Store (and Pop)

1 fst mem32fp
2 fst mem64fp
3 fst st(i)
4
5 fstp mem32fp
6 fstp mem64fp
7 fstp mem80fp
8 fstp st(i)

Pega o valor float de st0 e copia para o operando destino. A versão com o sufixo P também faz
o pop do valor da stack, sendo possível dar store em um float de 80 bits somente com esta
instrução.

FIST, FISTP | Integer Store (and Pop)

https://silva97.gitbook.io/assembly-x86/aprofundando-em-assembly/usando-instrucoes-da-fpu 4/14
10/04/2021 Usando instruções da FPU - Assembly x86

1 fist mem16int
2 fist mem32int
3
4 fistp mem16int
5 fistp mem32int
6 fistp mem64int

Pega o valor em st0, converte para inteiro sinalizado e armazena no operando destino. Só é
possível dar store em um inteiro de 64 bits na versão da instrução que faz o pop.

Só com essas instruções já podemos converter um float para inteiro e vice-versa. Conforme
exemplo:

assembly.asm

1 bits 64
2
3 section .data
4 num: dq 23.87
5
6 section .bss
7 result: resd 1
8
9 section .text
10 global assembly
11 assembly:
12 finit
13 fld qword [num]
14 fistp dword [result]
15
16 mov eax, [result]
17 ret

https://silva97.gitbook.io/assembly-x86/aprofundando-em-assembly/usando-instrucoes-da-fpu 5/14
10/04/2021 Usando instruções da FPU - Assembly x86

main.c

1 #include <stdio.h>
2
3 int assembly(void);
4
5 int main(void)
6 {
7 printf("Resultado: %d\n", assembly());
8 return 0;
9 }

Se você rodar esse teste irá notar que o valor foi convertido para 24, já que houve um
arredondamento.

FADD, FADDP, FIADD | (Integer) Add (and Pop)

1 fadd mem32fp
2 fadd mem64fp
3 fadd st(0), st(i)
4 fadd st(i), st(0)
5
6 faddp st(i), st(0)
7 faddp
8
9 fiadd mem16int
10 fiadd mem32int

As versões de fadd com operando na memória faz a soma do operando com st0 e armazena o
resultado da soma no próprio st0. Já fiadd com operando em memória faz a mesma coisa,
porém convertendo o valor inteiro para float 64 bits antes.

As instruções com registradores fazem a soma e armazenam o resultado no operando mais a


esquerda, o operando destino. Enquanto a faddp  sem operandos soma st0 com st1, armazena
o resultado em st1 e depois faz o pop.

Exemplo de soma simples:

https://silva97.gitbook.io/assembly-x86/aprofundando-em-assembly/usando-instrucoes-da-fpu 6/14
10/04/2021 Usando instruções da FPU - Assembly x86

assembly.asm

1 bits 64
2
3 section .data
4 num1: dq 24.3
5 num2: dq 0.7
6
7 section .bss
8 result: resd 1
9
10 section .text
11 global assembly
12 assembly:
13 finit
14 fld qword [num1]
15 fadd qword [num2]
16
17 fist dword [result]
18 mov eax, [result]
19 ret

main.c

1 #include <stdio.h>
2
3 int assembly(void);
4
5 int main(void)
6 {
7 printf("Resultado: %d\n", assembly());
8 return 0;
9 }

FSUB, FSUBP, FISUB | (Integer) Subtract (and Pop)

1 fsub mem32fp
2 fsub mem64fp
3 fsub st(0), st(i)
4 fsub st(i), st(0)
5

https://silva97.gitbook.io/assembly-x86/aprofundando-em-assembly/usando-instrucoes-da-fpu 7/14
10/04/2021 Usando instruções da FPU - Assembly x86

6 fsubp st(i), st(0)


7 fsubp
8
9 fisub mem16int
10 fisub mem32int

Mesma coisa que as instruções acima, só que fazendo uma operação de subtração.

FDIV, FDIVP, FIDIV | (integer) Division (and Pop)

1 fdiv mem32fp
2 fdiv mem64fp
3 fdiv st(0), st(i)
4 fdiv st(i), st(0)
5
6 fdivp st(i), st(0)
7 fdivp
8
9 fidiv mem16int
10 fidiv mem32int

Mesma coisa que FADD etc. porém faz uma operação de divisão.

FMUL, FMULP, FIMUL | (Integer) Multiply (and Pop)

1 fmul mem32fp
2 fmul mem64fp
3 fmul st(0), st(i)
4 fmul st(i), st(0)
5
6 fmulp st(i), st(0)
7 fmulp
8
9 fimul mem16int
10 fimul mem32int

Cansei de repetir, já sabe né? Operação de multiplicação.

FSUBR, FSUBRP, FISUBR | (Integer) Subtract Reverse (and Pop)

https://silva97.gitbook.io/assembly-x86/aprofundando-em-assembly/usando-instrucoes-da-fpu 8/14
10/04/2021 Usando instruções da FPU - Assembly x86

Faz a mesma coisa que a família FSUB, só que ao contrário. Conforme ilustração:

1 a = a - b // fsub etc.
2 a = b - a // fsubr etc.

Ou seja faz a subtração na ordem inversa dos operandos, porém onde o resultado é armazenado
continua sendo o mesmo.

FDIVR, FDIVRP, FIDIVRP | (Integer) Division Reverse (and Pop)

Mesma lógica que as instruções acima, porém faz a divisão na ordem inversa dos operandos.

FXCH | Exchange

1 fxch st(i)
2 fxch

Seguindo a mesma lógica da instrução xchg , troca o valor de st0 com st(i). A versão da
instrução sem operando especificado faz a troca entre st0 e st1.

FSQRT | Square root

fsqrt

Calcula a raíz quadrada de st0 e armazena o resultado no próprio st0.

FABS | Absolute

fabs

https://silva97.gitbook.io/assembly-x86/aprofundando-em-assembly/usando-instrucoes-da-fpu 9/14
10/04/2021 Usando instruções da FPU - Assembly x86

Calcula o valor absoluto de st0 e armazena em st0. Basicamente zera o bit de sinalização do
valor.

FCHS | Change Sign

fchs

Inverte o sinal de st0, se era negativo passa a ser positivo e vice-versa.

FCOS | Cosine

fcos

Calcula o cosseno de st0, que deve ser um valor radiano, e armazena o resultado nele próprio.

FSIN | Sine

fsin

Calcula o seno de st0, que deve estar em radianos.

FSINCOS | Sine and Cosine

fsincos

Calcula o seno e o cosseno de st0. O seno é armazenado em st0 enquanto o cosseno é dado
push na pilha. Detalhe que após o push, o valor do cosseno vai estar em st0 enquanto o valor do
seno estará em st1.

FPTAN | Partial Tangent


https://silva97.gitbook.io/assembly-x86/aprofundando-em-assembly/usando-instrucoes-da-fpu 10/14
10/04/2021 Usando instruções da FPU - Assembly x86

fptan

Calcula a tangente de st0 e armazena o resultado no próprio registrador, logo após faz o push
do valor 1.0 na pilha. O valor em st0 para ser calculado deve estar em radianos.

FPATAN | Partial Arctangent

fpatan

Calcula o arco-tangente de st1 dividido por st0, armazena o resultado em st1 e depois faz o pop.
O resultado tem o mesmo sinal que o operando que estava em st1.

st1 = arctan(st1 ÷ st0)

F2XM1 | 2^x - 1

f2xm1

Faz o cálculo de 2 elevado a st0 menos 1, e armazena o resultado em st0.

st0 = 2st0 − 1

FYL2X | y * log2(x)

fyl2x

https://silva97.gitbook.io/assembly-x86/aprofundando-em-assembly/usando-instrucoes-da-fpu 11/14
10/04/2021 Usando instruções da FPU - Assembly x86

Faz esse cálculo aí, com logaritmo de base 2:

st1 = st1 ⋅ log2 (st0)

Após o cálculo é feito um pop.

FYL2XP1 | y * log2(x + 1)

fyl2xp1

Mesma coisa que a instrução anterior, porém somando 1.

st1 = st1 ⋅ log2 (st0 + 1)

FRNDINT | Round to Integer

frndint

Arredonda st0 para a parte inteira mais próxima e armazena o resultado em st0.

FPREM, FPREM1 | Partial Reminder

1 fprem
2 fprem1

As duas instruções armazenam a sobra da divisão entre st0 e st1 no registrador st0. Com a
diferença que fprem1 segue a padronização IEEE-754.

https://silva97.gitbook.io/assembly-x86/aprofundando-em-assembly/usando-instrucoes-da-fpu 12/14
10/04/2021 Usando instruções da FPU - Assembly x86

FCOMI, FCOMIP, FUCOMI, FUCOMIP | Compare

1 fcomi st(0), st(i)


2 fcomip st(0), st(i)
3
4 fucomi st(0), st(i)
5 fucomip st(0), st(i)

Faz a comparação entre st0 e st(i) setando as status flags de acordo. A diferença de fucomi e
fucomip é que estas duas verificam se os valores nos registradores não são NaN, sendo o caso

a instrução irá disparar uma exception #IA.

FCMOVcc | Conditional Move

1 fcmovb st(0), st(i)


2 fcmove st(0), st(i)
3 fcmovbe st(0), st(i)
4 fcmovu st(0), st(i)
5
6 fcmovnb st(0), st(i)
7 fcmovne st(0), st(i)
8 fcmovnbe st(0), st(i)
9 fcmovnu st(0), st(i)

Faz uma operação move condicional levando em consideração as status flags.

Vendo os resultados

Não vou explicar sobre a SSE ou seus registradores neste tópico, mas saiba que um valor float
na convenção de chamada do C é retornado no registrador xmm0. Podemos ver o resultado de
nossos testes da seguinte forma, usando a instrução movss :

https://silva97.gitbook.io/assembly-x86/aprofundando-em-assembly/usando-instrucoes-da-fpu 13/14
10/04/2021 Usando instruções da FPU - Assembly x86

assembly.asm

1 bits 64
2
3 section .data
4 num1: dq 3.0
5 num2: dq 3.0
6
7 section .bss
8 result: resd 1
9
10 section .text
11 global assembly
12 assembly:
13 finit
14 fld qword [num1]
15 fmul qword [num2]
16
17 fst dword [result]
18 movss xmm0, [result]
19 ret

main.c

1 #include <stdio.h>
2
3 float assembly(void);
4
5 int main(void)
6 {
7 printf("Resultado: %f\n", assembly());
8 return 0;
9 }

https://silva97.gitbook.io/assembly-x86/aprofundando-em-assembly/usando-instrucoes-da-fpu 14/14

Você também pode gostar