Escolar Documentos
Profissional Documentos
Cultura Documentos
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:
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.
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.
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.
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.
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.
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:
É 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:
Não confundir com o modo de compatibilidade do Windows, ali é uma coisa diferente
que leva o mesmo nome.
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
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
Exemplo Formato
0b0111 Binário
0o10 Octal
9 Decimal
0x0a Hexadecimal
Strings simples
Representação Explicação
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.
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:
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
https://silva97.gitbook.io/assembly-x86/a-base/sintaxe 3/8
10/04/2021 Sintaxe - Assembly x86
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
byte 1
word 2
oword 16
yword 32
zword 64
https://silva97.gitbook.io/assembly-x86/a-base/sintaxe 4/8
10/04/2021 Sintaxe - Assembly x86
Exemplo:
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:
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
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
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
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
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.
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.
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.
https://silva97.gitbook.io/assembly-x86/a-base/registradores-gerais 2/8
10/04/2021 Registradores gerais - Assembly x86
reg-ex1.asm
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 }
https://silva97.gitbook.io/assembly-x86/a-base/registradores-gerais 4/8
10/04/2021 Registradores gerais - Assembly x86
assembly.asm
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
Caso ainda não tenha reparado o retorno da nossa função assembly() é guardado
no registrador EAX. Isto será explicado mais para frente.
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
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.
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
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
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.
Exemplos:
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
Exemplos:
Truque do nasm
Cuidado para não se confundir em relação ao fator de escala. Veja por exemplo esta instrução
64-bit:
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:
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:
Instrução LEA
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.
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.
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 :
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.
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
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 }
Repare que na linha 10 estamos usando um rótulo local, que foi explicado no tópico
sobre a sintaxe do nasm.
Registrador FLAGS
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:
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?
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
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 .
https://silva97.gitbook.io/assembly-x86/a-base/procedimentos 1/4
10/04/2021 Procedimentos - Assembly x86
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.
É 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:
char 1 byte AL
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.
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:
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.
.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.
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.
Um operando registrador
Um operando registrador OU operando na memória
Um valor imediato, que é um operando digitado diretamente
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.
Nomenclatura Significado
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.
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
pseudo.c
1 destiny = source;
ADD
pseudo.c
SUB | Subtract
https://silva97.gitbook.io/assembly-x86/a-base/instrucoes 3/15
10/04/2021 Instruções - Assembly x86
pseudo.c
INC | Increment
inc r/m
pseudo.c
1 destiny++;
DEC | Decrement
dec r/m
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
AL r/m8 AX
AX r/m16 DX:AX
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.
AX r/m8 AL AH
DX:AX r/m16 AX DX
pseudo.c
1 // Se operando de 8 bits
2 AL = AX / operand;
3 AH = AX % operand;
pseudo.c
1 destiny = address_of(source);
AND
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
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
XOR | Exclusive OR
Faz uma operação OU Exclusivo bit a bit nos operandos e armazena o resultado no operando
destino.
pseudo.c
https://silva97.gitbook.io/assembly-x86/a-base/instrucoes 7/15
10/04/2021 Instruções - Assembly x86
XCHG | Exchange
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;
pseudo.c
1 auxiliary = source;
2 source = destiny;
3 destiny = destiny + auxiliary;
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 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
CMP | Compare
pseudo.c
https://silva97.gitbook.io/assembly-x86/a-base/instrucoes 9/15
10/04/2021 Instruções - Assembly x86
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
Basicamente uma instrução MOV condicional, só irá definir o valor do operando destino caso a
condição seja atendida.
pseudo.c
NEG | Negate
https://silva97.gitbook.io/assembly-x86/a-base/instrucoes 10/15
10/04/2021 Instruções - Assembly x86
neg r/m
pseudo.c
1 destiny = -destiny;
NOT
not r/m
pseudo.c
1 destiny = ~destiny;
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;
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;
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
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;
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
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
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:
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.
https://silva97.gitbook.io/assembly-x86/a-base/instrucoes-do-nasm 2/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 }
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
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
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.
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:
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
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:
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 :
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
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
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
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
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
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:
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:
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:
%undef
%undef nome_do_macro
https://silva97.gitbook.io/assembly-x86/a-base/pre-processador-do-nasm 2/8
10/04/2021 Pré-processador do nasm - Assembly x86
%macro
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
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
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:
13
14 compare eax, edx
%unmacro
Montagem condicional
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 %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
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.
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
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
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
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:
https://silva97.gitbook.io/assembly-x86/a-base/syscall-no-linux 3/3
10/04/2021 Olá mundo no Linux - Assembly x86
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:
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
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
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:
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:
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.
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
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é?
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.
https://silva97.gitbook.io/assembly-x86/a-base/revisao 2/3
10/04/2021 Revisão - Assembly x86
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:
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:
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:
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
Barramento de endereço
Segmentação em IA-16
https://silva97.gitbook.io/assembly-x86/aprofundando-em-assembly/registradores-de-segmento 1/3
10/04/2021 Registradores de segmento - Assembly x86
Registrador Nome
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.
mov [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:
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
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.
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:
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.
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 $
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:
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
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:
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
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.
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.
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:
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:
https://silva97.gitbook.io/assembly-x86/aprofundando-em-assembly/atributos 3/8
10/04/2021 Atributos - Assembly x86
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.
Address-size
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.
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.
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.
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
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.
1 bits 16
2
3 mov ecx, 99999
4 .lp:
5 ; Faça alguma coisa
6 a32 loop .lp
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.
1 bits 32
2
3 inc ecx
4 db 0xFF, 0xC1
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.
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
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:
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:
https://silva97.gitbook.io/assembly-x86/aprofundando-em-assembly/flags-do-processador 2/5
10/04/2021 Flags do processador - Assembly x86
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
Dentre essas flags somente CF pode ser modificada diretamente, e isto é feito com as seguintes
instruções:
Control Flags
https://silva97.gitbook.io/assembly-x86/aprofundando-em-assembly/flags-do-processador 3/5
10/04/2021 Flags do processador - Assembly x86
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.
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
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.
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
:
cc Descrição Condição
https://silva97.gitbook.io/assembly-x86/aprofundando-em-assembly/instrucoes-condicionais 1/3
10/04/2021 Instruções condicionais - Assembly x86
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
A última instrução, obviamente, somente existe em submodo de 64-bit. Enquanto JCXZ não
existe em 64-bit.
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.
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:
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
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".
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
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.
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.
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
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
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.
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.
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
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
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
AH 0x03
AH BH
0x03 Página
CH CL DH DL
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
AH 0x0A
AH AL BH CX
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
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
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:
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
Horizontal
0x09 \t Avança o cursor 4 posições.
TAB
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
https://silva97.gitbook.io/assembly-x86/aprofundando-em-assembly/procedimentos-do-bios 5/9
10/04/2021 Procedimentos do BIOS - Assembly x86
AH 0x00
Registrador Valor
AH Scancode da tecla. *
AH 0x01
Registrador Valor
AL Código ASCII
AH Scancode
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
Bit Flag
https://silva97.gitbook.io/assembly-x86/aprofundando-em-assembly/procedimentos-do-bios 6/9
10/04/2021 Procedimentos do BIOS - Assembly x86
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:
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ê:
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
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
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.
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
Detalhe que só é possível usar esses registradores em instruções da FPU, algo como este
código está errado:
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.
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
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)
FLDLG2 log10(2)
FLDLN2 logE(2)
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.
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.
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.
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 }
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
Mesma coisa que as instruções acima, só que fazendo uma operação de subtração.
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.
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
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.
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
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
FCOS | Cosine
fcos
Calcula o cosseno de st0, que deve ser um valor radiano, e armazena o resultado nele próprio.
FSIN | Sine
fsin
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
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
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.
F2XM1 | 2^x - 1
f2xm1
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
FYL2XP1 | y * log2(x + 1)
fyl2xp1
frndint
Arredonda st0 para a parte inteira mais próxima e armazena o resultado em st0.
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
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
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