pbn/curso/aula-00.org
2025-05-28 10:03:55 -03:00

1352 lines
47 KiB
Org Mode

#+title: 0 -- Introdução à linguagem Assembly (NASM)
#+author: Blau Araujo
#+email: cursos@blauaraujo.com
#+options: toc:3
* Objetivos
- Apresentar as características e os elementos básicos da linguagem Assembly.
- Diferenciar conceitos da programação em alto e baixo nível.
- Conhecer as características da implementação NASM para Linux 64 bits.
- Aprender as instruções e diretivas essenciais para começar a programar.
- Criar executar um primeiro programa em Assembly.
* O que é Assembly
Assembly é uma linguagem de programação que oferece formas de representar, em
texto legível, os códigos numéricos das operações que o hardware da máquina é
capaz de processar. A rigor, qualquer linguagem faz isso, mas um programa em
Assembly se diferencia por expressar instruções que correspondem, cada uma, a
apenas um passo daquilo que o processador (a CPU) deve fazer -- sem abstrações
e numa relação praticamente direta com o código de máquina.
** Linguagem dependente da arquitetura
Como representa diretamente as instruções executadas por um processador,
a linguagem Assembly depende necessariamente das características da
arquitetura desse processador. Em especial, cada CPU é projetada com seu
próprio conjunto de instruções (ISA, do inglês /Instruction Set Architecture/).
A ISA é, por definição, a própria interface entre o hardware e o software
de um sistema computacional. É ela que define, entre outras coisas:
- Quais operações a CPU pode executar;
- Quais registradores estão disponíveis;
- Como as instruções são codificadas em binário;
- Como os operandos são manipulados;
- Como a memória pode ser utilizada.
Sendo assim, não há apenas uma linguagem Assembly, a não ser como um termo
genérico que engloba diversas linguagens, todas baseadas na representação
simbólica das operações de arquiteturas específicas.
No contexto dos nossos estudos, a linguagem Assembly que utilizaremos é
aquela voltada à arquitetura Intel x86_64, tal como implementada no /montador/
NASM (/Netwide Assembler/), e é a ela que estaremos nos referindo todas as
vezes que dissermos /"Assembly"/.
** Linguagem de montagem
A palavra /"assembly"/ vem do inglês e significa /"montagem"/. Portanto, o nome
deriva da primeira etapa do processo que transforma os programas escritos na
linguagem (os /códigos-fonte/) nos códigos binários executáveis pela máquina
(/código de máquina/). Esse processo se chama /"montagem"/ e é feita por um
programa chamado /"montador"/ (ou /"assembler"/, em inglês).
#+begin_quote
*Importante!* A linguagem se chama /"Assembly"/, não /"assembler"/, que é como
chamamos o programa que implementa a linguagem e faz a montagem do código
de máquina.
#+end_quote
** Classificações da linguagem
O Assembly pode ser classificado como uma linguagem...
*** De baixo nível de abstração (segunda geração)
Trabalha diretamente com os recursos da arquitetura da máquina, como
registradores, endereços de memória e instruções específicas da CPU. Não
há camadas intermediárias entre o código e o hardware.
*** Imperativa
As instruções são dadas em uma sequência explícita, descrevendo passo a
passo o que deve ser feito. O controle do fluxo é manual, baseado em saltos
e comparações, sem abstrações de alto nível.
*** De tipagem inexistente
Não há distinção formal entre tipos de dados. Tudo é interpretado como
sequências de bytes, cabendo a quem programa decidir como os dados devem
ser lidos ou manipulados.
*** Com gerenciamento manual de memória
Não há alocação automática ou coleta de lixo. O programador deve lidar com
endereços de memória diretamente, reservando, acessando e liberando espaços
manualmente, de acordo com a necessidade.
*** Compilada via montagem
O código Assembly não é interpretado nem compilado. Em vez disso, ele é
traduzido diretamente para código de máquina pelo montador (/assembler/),
que gera um binário executável diretamente ou um arquivo binário do tipo
/objeto/. Para que tenhamos um executável de fato, ainda pode ser necessário
realizar uma etapa de /ligação/, em que utilizamos um /editor de ligações/ para
adequar o binário montado às especificações do formato de executáveis do
sistema operacional.
*** Paradigmas
Também pode ser considerada uma linguagem procedural, mas com ressalvas, já
que é uma linguagem que só oferece instruções e, portanto, não impõe nenhum
paradigma. Isso quer dizer que nós podemos escrever programas de forma
procedural, mas também de forma totalmente linear e até orientada a macros.
*** De uso específico
Apesar de ser uma linguagem completa, o Assembly é geralmente usado para
finalidades bem específicas, como a implementação de trechos críticos de
desempenho, rotinas de acesso direto ao hardware, criação de módulos de
sistemas operacionais ou interação com o sistema em níveis muito mais
baixos. Raramente é usado para o desenvolvimento de aplicações inteiras,
principalmente devido à sua complexidade, verbosidade e baixa portabilidade.
** Comparação com outras linguagens
Por ser uma linguagem de segunda geração, o Assembly opera em um nível muito
mais próximo do hardware do que linguagens de alto nível. Isso traz uma série
de diferenças fundamentais em relação à forma como os programas são escritos,
estruturados e compreendidos.
Aqui estão algumas dessas diferenças...
*** Não há estruturas de controle
Em Assembly nós não temos estruturas de controle abstratas, como =if=, =case=,
=for= ou =while=. Em vez disso, todo o controle de fluxo do programa tem que
ser implementado com instruções de comparação e saltos para outras partes
do programa.
*** Não há o conceito de variáveis
Os dados são associados diretamente a registradores da CPU ou a endereços
de memória identificados por /rótulos/. Toda a manipulação de dados é feita
por meio de instruções de cópia, como resultado de operações lógicas e
aritméticas ou em definições que serão processadas durante a montagem
do arquivo binário.
*** Ponteiros, só por analogia
Como não há variáveis, também não existe exatamente o conceito de /ponteiro/,
a não ser por analogia. Os rótulos, por exemplo, representam endereços de
memória, e nós até podemos pensar neles como os ponteiros e vetores da
linguagem C, mas isso pode causar algumas confusões conceituais e deve
ser evitado.
*** Em vez de tipos, quantidades de bytes
Novamente: todos os dados são escritos diretamente em registradores ou na
memória. Se usarmos registradores, as suas capacidades determinarão quantos
bytes poderão (ou deverão) ser escritos. A escrita direta na memória, por
outro lado, não tem um limite fixo, mas depende da quantidade de memória
endereçável e da maneira como o programa a utiliza.
#+begin_quote
*Nota:* uma das principais finalidades do conceito de /tipos/ é a delimitação
do acesso aos dados. Por exemplo, declarar uma variável como do tipo =int=
limita o acesso a apenas 4 bytes (em arquiteturas x86_64), mas não existe
essa forma de restrição no Assembly. Nós somos livres para ler ou escrever
quantos bytes quisermos, em qualquer endereço, desde que tenhamos permissão
do sistema.
#+end_quote
** Por que aprender Assembly?
*** Entendimento do funcionamento de computadores
Aprender Assembly proporciona uma visão detalhada do que realmente acontece
durante a execução de um programa, como o ciclo de execução de instruções,
o uso de registradores, o papel da memória RAM, o funcionamento da pilha e
tantos outros conhecimentos fundamentais para a compreensão da arquitetura
e da organização de sistemas computacionais modernos.
*** Depuração e análise
Ferramentas de depuração, análise, segurança e engenharia reversa são muito
mais eficazes nas mãos de quem entende Assembly, especialmente quando tudo
que temos são o código de máquina ou a /desmontagem/ do binário de um programa.
*** Integração com C
É comum utilizar Assembly para otimizações pontuais em programas escritos
em C, como em trechos críticos de desempenho ou quando se deseja acesso
direto a instruções específicas da arquitetura. Também é necessário para
a escrita de rotinas de sistema, drivers ou na manipulação direta de
hardware, especialmente em ambientes com recursos limitados ou sem suporte
a bibliotecas padrão.
*** Aprendizado
Assembly é uma tremenda ferramenta didática em disciplinas como arquitetura
e organização de computadores, sistemas operacionais, design de linguagens
e construção de compiladores. Com ele, nós podemos demonstrar e observar
diretamente conceitos que, de outra forma, seriam ocultados por várias
camadas de abstração.
** Principais sintaxes
Embora as instruções de máquina sejam definidas pela arquitetura da CPU, a
maneira como essas instruções são escritas em Assembly pode variar bastante,
dependendo da sintaxe adotada pelo montador. Para as arquiteturas x86
e x86_64, destacam-se duas sintaxes principais: a sintaxe /Intel/ e a sintaxe
/AT&T/.
*** Sintaxe AT&T
A sintaxe AT&T foi popularizada no contexto dos sistemas Unix e do projeto
GNU. Ela é usada por padrão pelo montador GAS (/GNU Assembler/) e é a sintaxe
adotada implicitamente pelo GCC ao gerar código Assembly. Os operandos
seguem a ordem /origem → destino/, com prefixos =%= obrigatórios para
registradores e =$= para valores (ex.: =movl $1, %rax=).
*"Salve, simpatia!" em sintaxe AT&T (GAS):*
Arquivo: [[exemplos/00/salve-att.s][salve-att.s]]
#+begin_src asm :tangle exemplos/00/salve-att.s
# salve-att.s
# Montar com: as salve-att.s -o salve-att.o
# Linkar com: ld salve-att.o -o salve-att
.section .data
msg:
.ascii "Salve, simpatia!\n"
len = . - msg
.section .text
.global _start
_start:
# write(1, msg, len)
mov $1, %rax # syscall: write
mov $1, %rdi # stdout
lea msg(%rip), %rsi # endereço da mensagem
mov $len, %rdx # tamanho da mensagem
syscall
# exit(0)
mov $60, %rax # syscall: exit
xor %rdi, %rdi # status 0
syscall
#+end_src
*** Sintaxe Intel
Mais comum em livros didáticos, manuais da Intel e tutoriais voltados ao
desenvolvimento em baixo nível, especialmente em ambientes /bare-metal/. Ela
apresenta os operandos na ordem /destino ← origem/ (ex.: =mov rax, 1=), não usa
prefixos nos registradores e é adotada por montadores como o NASM, que
utilizaremos nos nossos estudos.
*"Salve, simpatia!" em sintaxe Intel (NASM):*
Arquivo: [[exemplos/00/salve-intel.asm][salve-intel.asm]]
#+begin_src asm :tangle exemplos/00/salve-intel.asm
; salve-intel.asm
; Montar com: nasm -f elf64 salve-intel.asm
; Linkar com: ld salve-intel.o -o salve-intel
section .data
msg db "Salve, simpatia!", 10 ; 10 = '\n'
len equ $ - msg
section .text
global _start
_start:
; write(1, msg, len)
mov rax, 1 ; syscall número 1: write
mov rdi, 1 ; stdout
mov rsi, msg ; endereço da mensagem
mov rdx, len ; tamanho da mensagem
syscall
; exit(0)
mov rax, 60 ; syscall número 60: exit
xor rdi, rdi ; status 0
syscall
#+end_src
*** Compatibilidade
Embora o GAS (executável =as=) use a sintaxe AT&T por padrão, ele também
aceita código em sintaxe Intel com a diretiva =.intel_syntax= no início do
código-fonte, geralmente acompanhada de =noprefix=. Alguns compiladores
e depuradores, como o GCC e o GDB, também oferecem suporte a ambas as
sintaxes -- por exemplo, com a opção =-masm=intel=, no GCC, e o comando
=set disassembly-flavor=, no GDB.
* O Netwide Assembler (NASM)
O /Netwide Assembler/ (NASM) é um montador livre muito utilizado na programação
em Assembly, especialmente para arquiteturas x86 e x86_64. Ele é conhecido por
sua sintaxe clara e direta e por oferecer suporte a formatos de saída para
várias plataforma.
| Característica | Descrição |
|--------------------------+---------------------------------------------------------------------------|
| *Sintaxe* | Intel (não suporta AT&T) |
| *Arquiteturas suportadas* | x86 (16/32 bits), x86_64 (64 bits) |
| *Formatos de saída* | ELF, COFF, Mach-O, bin, RDOFF |
| *Processo de montagem* | Gera binários brutos (/raw/) e arquivos objeto (exige link-edição separada) |
| *Controle de código* | Total controle sobre instruções, endereços e seções |
| *Pré-processamento* | Diretivas simples; sem pré-processador complexo como no C |
| *Suporte a macros* | Sim, com argumentos e controle condicional |
| *Integração com o sistema* | Compatível com chamadas de sistema Linux, Windows e Unix em geral |
| *Otimizações* | Não realiza otimizações nem análises semânticas |
| *Documentação* | Completa e com muitos exemplos |
** Sintaxe de instruções
A sintaxe geral de uma instrução em NASM segue uma forma bastante regular
e compreensível:
#+begin_example
[rótulo:] instrução operando1, operando2 ; comentário
#+end_example
Onde...
| Elemento | Obrigatório | Exemplos | Descrição |
|------------+-------------+-------------------+----------------------------------------------------------------------------------------------|
| *Rótulo* | Opcional | =loop_start:= | Nome associado a um endereço. Pode ser usado como destino de saltos. |
| *Instrução* | Sim | =mov=, =add=, =jmp= | A operação a ser executada. |
| *Operando 1* | Depende | =rax=, =[msg]= | Primeiro operando: geralmente o destino (registrador ou memória). |
| *Operando 2* | Depende | =rbx=, =1=, =[valor]= | Segundo operando: geralmente a origem dos dados (registrador, memória ou um valor imediato). |
| *Comentário* | Opcional | =; isso é um salto= | Começa com =;= e vai até o fim da linha. Ignorado pelo montador. |
*** Diretivas para definições de dados
Além das instruções, mas ainda seguindo a estrutura geral da sintaxe,
o NASM oferece várias diretivas para definir dados ou reservar espaços
na memória, como:
| Diretiva | Uso | Ação | Equivalente aproximado em C |
|----------+---------------+------------------------------------------+------------------------------------|
| =db= | =db 0x48= | Define um byte (define byte) | =char x = 0x48;= |
| =dw= | =dw 1234h= | Define uma *word* (2 bytes) | =short x = 0x1234;= |
| =dd= | =dd 1.0= | Define uma *double word* (4 bytes) | =int x = 1;= / =float x = 1.0f;= |
| =dq= | =dq 3.14159= | Define uma *quad word* (8 bytes) | =double x = 3.14159;= |
| =dt= | =dt 1.23e400= | Define uma *ten-byte* (80 bits, float ext) | =long double x = …= (FPU específico) |
| =resb= | =resb 64= | Reserva 64 bytes (sem inicializar) | =char buffer[64];= (sem valor) |
| =resw= | =resw 8= | Reserva 8 words (16 bytes) | =short arr[8];= |
| =resd= | =resd 4= | Reserva 4 dwords (16 bytes) | =int arr[4];= |
| =resq= | =resq 2= | Reserva 2 qwords (16 bytes) | =long arr[2];= |
Essas diretivas podem ou não ser antecedidas de rótulos para indicar onde
esses dados serão inseridos na memória, por exemplo:
#+begin_src asm
msg: db "Salve, simpatia!", 0 ; 'msg' é um rótulo para o endereço do primeiro byte
buffer: resb 64 ; 'buffer' é um rótulo para a área reservada
#+end_src
É permitido definir e reservar espaços sem rótulos, mas os dados só poderão
ser acessados com base em cálculos de posições relativas.
** Sintaxe de metaprogramação da montagem
A sintaxe de metaprogramação da montagem refere-se ao conjunto de diretivas,
macros e comandos especiais fornecidos pelo montador para escrever código
que gera ou manipula outro código durante a montagem. Essa sintaxe vai além
das instruções padrão da CPU e possibilita:
- Definir constantes simbólicas e macros reutilizáveis.
- Realizar substituições textuais antes da montagem real.
- Implementar estruturas condicionais e laços para controle do fluxo de montagem.
- Gerar código de forma dinâmica e programável, facilitando a automação e a abstração.
Na tabela, nós temos alguns exemplos de diretivas e comandos usados na
sintaxe de metaprogramação de montagem do NASM:
| Diretiva / Comando | Categoria | Descrição breve | Exemplo simples |
|--------------------+-----------------------+----------------------------------------------------+----------------------|
| =equ= | Definição simbólica | Define uma constante simbólica imutável | =MAXLEN equ 32= |
| ~=~ | Definição simbólica | Define símbolo numérico, pode ser redefinido | ~counter = 0~ |
| =%define= | Macros simbólicas | Define uma macro simples (substituição textual) | =%define MSG "Olá!"= |
| =%assign= | Macros simbólicas | Define símbolo numérico que pode ser alterado | =%assign i 10= |
| =%macro= | Macros com parâmetros | Define macros com parâmetros e corpo expansível | =%macro PRINT_MSG 0= |
| =%ifdef= / =%ifndef= | Controle condicional | Compila bloco se símbolo estiver (ou não) definido | =%ifdef DEBUG= |
| =%include= | Inclusão de arquivos | Insere outro arquivo fonte no ponto da montagem | =%include "utils.inc"= |
| =%rep= / =%endrep= | Laços e repetição | Repete um bloco de código um número fixo de vezes | =%rep 4= |
** Operadores e símbolos
No NASM, a montagem de código também envolve o uso de operadores e símbolos
especiais que permitem calcular endereços, manipular valores e controlar a
geração do programa, tudo em tempo de montagem.
Aqui estão alguns exemplos:
| Símbolo | Descrição | Uso / Exemplo |
|-----------------+---------------------------------------------------+-----------------------------------------------|
| =$= | Endereço da instrução atual | =jmp $+5= — salta 5 bytes à frente |
| =$$= | Endereço do início do segmento ou bloco atual | =jmp $$-$= — salto relativo ao início do bloco |
| =$+n= / =$-n= | Endereço relativo deslocado =n= bytes | =jmp $-10= — volta 10 bytes |
| =%= | Indica que um símbolo é um valor numérico (macro) | =%define X 10= |
| =:= | Define ou qualifica rótulos | =loop_start:= |
| =*= | Multiplicação em expressões | =len equ 4 * 10= |
| =+=, =-=, =/=, =<<=, =>>= | Operadores aritméticos e bitwise | =size equ 1 << 4= |
| =[...]= | Acesso indireto à memória (endereços) | =mov eax, [ebx]= |
| =?= | Operador ternário em macros | =%if ?(cond) ... %endif= |
** Seções no NASM
Quando escrevemos um programa em Assembly, é preciso organizar o código e os
dados de forma que o sistema operacional possa carregá-lo corretamente na
memória. Essa organização é feita por meio das /seções/ (ou /segmentos/), que
dividem o programa em partes distintas com propósitos diferentes. Mas isso
não é apenas uma convenção de organização: a separação das seções é exigida
pelo formato do executável (como ELF no Linux ou PE no Windows) e é
fundamental para que o /carregador/ do sistema (/loader/) saiba onde colocar
cada parte do programa na memória.
No NASM, nós usamos a diretiva =section= para declarar as divisões do código,
que podem ser:
- =section .text=: armazena o código executável do programa (as instruções);
- =section .data=: contém dados inicializados (variáveis com valores definidos);
- =section .rodata=: usada para dados constantes somente leitura (como strings fixas);
- =section .bss=: reserva espaço para dados não inicializados (variáveis com valor padrão, geralmente zero).
** Importação e exportação de símbolos
Símbolos são nomes associados a endereços e valores em um programa. Eles
podem representar rótulos de rotinas, posições de dados na memória ou
constantes definidas em tempo de montagem. Em Assembly, esses símbolos
servem como pontos de referência tanto para o código quanto para o montador
e o editor de ligações (/link-editor/).
Quando escrevemos programas em NASM que serão ligados com outros módulos,
como bibliotecas externas ou arquivos objeto, nós precisamos informar ao
editor de ligações quais símbolos devem ser /exportados/ para esses módulos
e quais devem ser /importados/ de outros lugares. No NASM, isso é feito com
as diretivas =global= (importação) e =extern= (exportação).
No GNU/Linux, especialmente quando não usamos uma linguagem de mais alto
nível (como C), nós precisamos especificar manualmente o ponto de entrada
do programa — ou seja, a função onde o sistema operacional deve começar
a execução. Por padrão, o Linux espera que o ponto de entrada seja chamado
de =_start=, que deve ser exportado com a diretiva:
#+begin_src asm
global _start
#+end_src
Essa instrução diz ao NASM que o símbolo =_start= deve ser incluído na tabela
de símbolos do arquivo objeto montado. Assim, o editor de ligações (=ld=)
poderá encontrá-lo. Nó código, =_start= geralmente é escrito no início da
seção =.text=:
#+begin_example
global _start
section .text
_start:
; O código de início do programa vem aqui...
#+end_example
** Tipos de operandos em instruções
Toda instrução Assembly opera sobre um ou mais valores que nós chamamos de
operandos. Eles indicam a origem e o destino dos dados e podem representar:
- Registradores;
- Endereços de memória;
- Valores imediatos (constantes escritas diretamente no código).
A combinação entre esses tipos de operandos determina a validade e o
comportamento das instruções. Por exemplo, uma instrução =mov= pode copiar...
- Dados de um registrador para outro registrador;
- Um valor imediato para um registrador;
- Dados em um endereço de memória para um registrador;
- Dados de um registrador para um endereço de memória.
Mas não pode, por exemplo, copiar dados de um registrador, ou de um endereço
de memória, para um valor imediato, e o NASM exige que essas combinações
estejam de acordo com a sintaxe e a semântica definida pela arquitetura.
* Instruções essenciais
A arquitetura x86_64 tem centenas de instruções, mas nós só precisamos
conhecer algumas delas para começar a acompanhar os exemplos dos próximos
tópicos -- com o tempo, nós acrescentaremos organicamente algumas outras
ao nosso arsenal da linguagem Assembly.
** Instrução =mov=
A instrução =mov= é usada para copiar valores de um operando para outro. É
uma das instruções mais utilizadas em Assembly, pois quase todo código precisa
manipular dados em registradores, na memória ou com valores imediatos.
*Forma geral:*
#+begin_example
mov destino, origem
#+end_example
O valor de =origem= é copiado para =destino=.
#+begin_quote
*Importante!* Isso não é uma troca de dados entre operandos, é uma cópia
de um operando para outro.
#+end_quote
*Combinações válidas:*
| Origem | Destino | Exemplo | Observações |
|-------------+-------------+----------------+-------------------------------------------|
| Imediato | Registrador | =mov rax, 42= | Valor literal (imediato) para registrador |
| Registrador | Registrador | =mov rbx, rax= | Cópia entre registradores |
| Registrador | Memória | =mov [var], rax= | Salva conteúdo em memória |
| Memória | Registrador | =mov rax, [var]= | Carrega conteúdo da memória |
| Imediato | Memória | =mov [var], 10= | Atribuição direta a endereço |
O NASM não permite movimentos diretos de memória para memória.
*Exemplo de uso:*
#+begin_src asm
section .data
msg db 42 ; variável na memória
section .text
global _start
_start:
mov rax, [msg] ; carrega o valor de msg em rax
mov rbx, rax ; copia rax para rbx
mov [msg], 99 ; sobrescreve msg com novo valor
#+end_src
** Instrução =lea=
Carrega o endereço efetivo de um operando de memória (não o valor na memória).
#+begin_quote
O endereço efetivo é o resultado da avaliação de uma expressão que resulta
no endereço que será usado para acessar a memória.
#+end_quote
*Forma geral:*
#+begin_example
lea destino, [base + índice*escala + deslocamento]
#+end_example
*Combinações válidas:*
| Destino | Origem (memória) | Exemplo |
|-------------+-------------------+----------------------|
| Registrador | Memória simbólica | =lea rax, [rbx+4]= |
| Registrador | Endereços | =lea rsi, [rip+msg]= |
*Exemplo de uso:*
#+begin_src asm
section .rodata
msg: db "Olá, mundo!", 10 ; string com quebra de linha
section .text
global _start
_start:
lea rsi, [rel msg] ; carrega o endereço de msg em rsi
mov rax, 1 ; syscall: write
mov rdi, 1 ; descritor: stdout
mov rdx, 13 ; tamanho da mensagem
syscall
; ...
#+end_src
** Instruções aritméticas básicas (=inc=, =dec=, =add= e =sub=)
Estas instruções realizam operações aritméticas simples diretamente em
registradores ou na memória.
*** Instrução =inc=
Incrementa (soma 1) o valor de um operando.
*Forma geral:*
#+begin_example
inc destino
#+end_example
*Operandos válidos:* registrador ou endereço de memória.
*Exemplo:*
#+begin_src asm
mov rcx, 5
inc rcx ; rcx passa a valer 6
#+end_src
*** Instrução =dec=
Decrementa (subtrai 1) o valor de um operando.
*Forma geral:*
#+begin_example
dec destino
#+end_example
*Operandos válidos:* registrador ou endereço de memória.
*Exemplo:*
#+begin_src asm
mov rcx, 5
dec rcx ; rcx passa a valer 4
#+end_src
*** Instrução =add=
Soma um operando ao destino e armazena o resultado no destino.
*Forma geral:*
#+begin_example
add destino, origem
#+end_example
*Combinações válidas:*
| Destino | Origem | Exemplo |
|-------------+-----------------+-------------------|
| Registrador | Registrador | add rax, rbx |
| Registrador | Valor imediato | add rax, 10 |
| Registrador | Memória | add rax, [var] |
| Memória | Registrador | add [var], rax |
| Memória | Valor imediato | add [var], 1 |
*Exemplo:*
#+begin_src asm
mov rax, 3
add rax, 7 ; rax passa a valer 10
#+end_src
*** Instrução =sub=
Subtrai um operando do destino e armazena o resultado no destino.
*Forma geral:*
#+begin_example
sub destino, origem
#+end_example
*Combinações válidas:*
| Destino | Origem | Exemplo |
|-------------+-----------------+-------------------|
| Registrador | Registrador | sub rax, rbx |
| Registrador | Valor imediato | sub rax, 10 |
| Registrador | Memória | sub rax, [var] |
| Memória | Registrador | sub [var], rax |
| Memória | Valor imediato | sub [var], 1 |
*Exemplo:*
#+begin_src asm
mov rax, 10
sub rax, 3 ; rax passa a valer 7
#+end_src
** Instruções de multiplicação e divisão (=mul=, =imul=, =div= e =idiv=)
Estas instruções realizam operações aritméticas de multiplicação e divisão.
*** Instrução =mul= (multiplicação sem sinal)
Realiza multiplicação sem sinal entre o valor no registrador acumulador e
um operando, armazenando o resultado em registradores específicos.
*Funcionamento:*
| Tamanho dos operandos | Multiplicação | Resultado |
|-----------------------+------------------+-----------|
| 8 bits | =AL= por operando | =AX= |
| 16 bits | =AX= por operando | =DX:AX= |
| 32 bits | =EAX= por operando | =EDX:EAX= |
| 64 bits | =RAX= por operando | =RDX:RAX= |
*Forma geral:*
#+begin_example
mul operando
#+end_example
*Operando:* registrador ou memória, sem sinal.
*Exemplo:*
#+begin_src asm
mov rax, 5
mul qword [valor] ; rax * [valor], resultado em RDX:RAX
#+end_src
*** Instrução =imul= (multiplicação com sinal)
Mais flexível que =mul=, pode usar 1, 2 ou 3 operandos.
*Forma geral unária (1 operando):*
#+begin_example
imul operando
#+end_example
| Tamanho dos operandos | Multiplicação | Resultado |
|-----------------------+------------------+-----------|
| 8 bits | =AL= por operando | =AX= |
| 16 bits | =AX= por operando | =DX:AX= |
| 32 bits | =EAX= por operando | =EDX:EAX= |
| 64 bits | =RAX= por operando | =RDX:RAX= |
*Forma geral binária (2 operandos):*
#+begin_example
imul destino, origem (resultado em destino)
#+end_example
- Multiplica =destino= por =origem= e armazena o resultado em =destino=.
- Se o resultado exceder a capacidade de =destino=, apenas os bytes mais
baixos serão armazenados (não há extensão de registradores).
- Afeta as flags =CF= (/carry/) e =OF= (/overflow/) no caso de estouro da
capacidade de =destino=.
*Forma geral ternária (3 operandos):*
#+begin_example
imul destino, origem, imediato (resultado em destino)
#+end_example
- Multiplica =origem= pelo valor de =imediato= e armazena o resultado em =origem=.
- Se o resultado exceder a capacidade de =destino=, apenas os bytes mais
baixos serão armazenados (não há extensão de registradores).
- Afeta as flags =CF= e =OF= no caso de estouro da capacidade de =destino=.
*Operandos válidos para =imul=:*
| Forma | Operando 1 (destino) | Operando 2 (origem) | Operando 3 (imediato) |
|-------------+--------------------------------+---------------------------------+-----------------------|
| 1 operando | =RAX=, =EAX=, =AX= ou =AL= (implícito) | Qualquer registrador ou memória | (Não há) |
| 2 operandos | Registrador | Registrador ou memória | (Não há) |
| 3 operandos | Registrador | Registrador ou memória | Valor imediato |
*Exemplo unário:*
#+begin_src asm
mov rax, 10
mov rbx, -4
imul rbx ; rdx:rax = rax * rbx (com sinal)
#+end_src
*Exemplo binário:*
#+begin_src asm
mov rax, 5
mov rbx, -3
imul rax, rbx ; rax = rax * rbx (com sinal)
#+end_src
*Exemplo ternário:*
#+begin_src asm
mov rbx, 7
imul rax, rbx, -2 ; rax = rbx * -2 (com sinal)
#+end_src
*** Instrução =div= (divisão sem sinal)
Realiza divisão sem sinal entre o valor no registrador acumulador estendido
e um operando.
*Funcionamento:*
| Tamanho dos operandos | Divisão | Quociente | Resto |
|-----------------------+----------------------+-----------+-------|
| 8 bits | =AX= por operando | =AL= | =AH= |
| 16 bits | =DX:AX= por operando | =AX= | =DX= |
| 32 bits | =EDX:EAX= por operando | =EAX= | =EDX= |
| 64 bits | =RDX:RAX= por operando | =RAX= | =RDX= |
*Forma geral:*
#+begin_example
div operando
#+end_example
*Operando:* registrador ou memória sem sinal.
*Exemplo:*
#+begin_src asm
mov rax, 20
xor rdx, rdx ; limpa (zera) RDX antes da divisão
div qword [divisor] ; divide RDX:RAX por [divisor]
#+end_src
*** Instrução =idiv= (divisão com sinal)
Realiza divisão com sinal entre o valor no registrador acumulador estendido
e um operando, considerando números negativos.
*Funcionamento:*
| Tamanho dos operandos | Divisão | Quociente | Resto |
|-----------------------+----------------------+-----------+-------|
| 8 bits | =AX= por operando | =AL= | =AH= |
| 16 bits | =DX:AX= por operando | =AX= | =DX= |
| 32 bits | =EDX:EAX= por operando | =EAX= | =EDX= |
| 64 bits | =RDX:RAX= por operando | =RAX= | =RDX= |
*Forma geral:*
#+begin_example
idiv operando
#+end_example
*Operando:* registrador ou memória com sinal.
*Exemplo:*
#+begin_src asm
mov rax, -50
cqto ; extende o sinal de RAX para RDX:RAX (antes de idiv)
idiv qword [divisor] ; divide RDX:RAX por [divisor]
#+end_src
#+begin_quote
*Nota:* Antes de usar =idiv=, é necessário preparar o registrador de extensão
(RDX) com o valor correto usando =cqto= (/Convert Quadword to Octaword/),
que estende o sinal de RAX para RDX:RAX. Para operandos menores, são
utilizados =cwd= (16 bits) ou =cdq= (32 bits).
#+end_quote
** Instrução =jmp= (salto incondicional)
Realiza um salto incondicional para o endereço ou rótulo especificado. A
execução do programa continua a partir da posição de destino, sem avaliar
qualquer condição.
*Forma geral:*
#+begin_example
jmp destino
#+end_example
*Combinações válidas:*
| Destino | Tipo de operando | Exemplo |
|-------------+------------------------+-----------|
| Rótulo | Endereço relativo | =jmp fim= |
| Registrador | Endereço absoluto | =jmp rax= |
| Memória | Ponteiro para endereço | =jmp [rax]= |
| Imediato | Offset relativo | =jmp $+5= |
*Exemplo de uso:*
#+begin_src asm
section .text
global _start
_start:
mov rax, 1 ; syscall: write
mov rdi, 1 ; descritor: stdout
mov rsi, msg ; ponteiro para a string
mov rdx, 16 ; tamanho da string
syscall
jmp fim ; salta incondicionalmente para o final
meio:
; código que nunca será executado
mov rax, 60
mov rdi, 1
syscall
fim:
mov rax, 60 ; syscall: exit
xor rdi, rdi ; Estado de término 0
syscall
section .rodata
msg: db "Salve, simpatia!", 10
#+end_src
** Instruções de salto condicional
As instruções de salto condicional (ou desvio condicional) alteram o fluxo
de execução com base no estado das flags da CPU, definidas por instruções
de comparação como =cmp= ou =test=.
*Forma geral:*
#+begin_example
jXX destino
#+end_example
Onde =jXX= representa uma das variantes condicionais e =destino= é um rótulo
no código.
*Flags envolvidas:*
- =ZF= (/Zero Flag/): Ativa (=1=) se o resultado de uma operação for zero.
- =SF= (/Sign Flag/): Reflete o bit de sinal do resultado (=0= para positivo, =1= para negativo).
- =OF= (/Overflow Flag/): Ativa (=1=) se ocorreu /overflow/ em operação aritmética com sinal.
- =CF= (/Carry Flag/): Ativa se houve transporte (/carry/) ou empréstimo (/borrow/) em
operações aritméticas com números sem sinal.
*Tabela de saltos comuns (em inteiros com sinal):*
| Instrução | Sinônimo | Condição | Descrição |
|-----------+----------+--------------------+-------------------------------|
| =je= | =jz= | ~ZF = 1~ | Salta se igual (zero) |
| =jne= | =jnz= | ~ZF = 0~ | Salta se diferente (não zero) |
| =jg= | =jnle= | ~ZF = 0~ e ~SF = OF~ | Salta se maior |
| =jge= | =jnl= | ~SF = OF~ | Salta se maior ou igual |
| =jl= | =jnge= | ~SF != OF~ | Salta se menor |
| =jle= | =jng= | ~ZF = 1~ ou ~SF != OF~ | Salta se menor ou igual |
*Outras variantes comuns (sem sinal):*
| Instrução | Condição | Descrição |
|-----------+------------------+--------------------------|
| =ja= | ~CF = 0~ e ~ZF = 0~ | Salta se acima |
| =jae= | ~CF = 0~ | Salta se acima ou igual |
| =jb= | ~CF = 1~ | Salta se abaixo |
| =jbe= | ~CF = 1~ ou ~ZF = 1~ | Salta se abaixo ou igual |
*Exemplo de uso:*
#+begin_src asm
section .text
global _start
_start:
mov rax, 5
mov rbx, 3
cmp rax, rbx ; compara rax com rbx
jg maior ; salta se rax > rbx
jl menor ; salta se rax < rbx
je igual ; salta se rax == rbx
maior:
; código para o caso de rax > rbx
jmp fim
menor:
; código para o caso de rax < rbx
jmp fim
igual:
; código para o caso de rax == rbx
fim:
mov rax, 60 ; syscall: exit
xor rdi, rdi ; Resula em 0
syscall
#+end_src
** Instrução =cmp=
Compara dois operandos realizando uma subtração entre eles, mas sem armazenar
o resultado. O objetivo da instrução é atualizar as flags da CPU, que podem
ser usadas posteriormente em instruções de salto condicional.
*Forma geral:*
#+begin_example
cmp operando1, operando2
#+end_example
Internamente, equivale a:
#+begin_example
sub operando1 operando2
#+end_example
Mas sem alterar o conteúdo de =operando1=.
*Combinações válidas:*
| Operando 1 | Operando 2 | Exemplo |
|-------------+-------------+------------------|
| Registrador | Registrador | =cmp rax, rbx= |
| Registrador | Imediato | =cmp rsi, 10= |
| Registrador | Memória | =cmp rcx, [array]= |
| Memória | Registrador | =cmp [rdi], rax= |
| Memória | Imediato | =cmp [rsi], 1= |
*Exemplo de uso:*
#+begin_src asm
section .data
valor: dq 42
section .text
global _start
_start:
mov rax, [valor] ; carrega o valor da memória
cmp rax, 42 ; compara com 42
je igual ; salta se for igual
mov rdi, 1 ; Termina com erro: não é igual
jmp fim
igual:
mov rdi, 0 ; Termina com sucesso: é igual
fim:
mov rax, 60 ; syscall: exit
syscall
#+end_src
** Instrução =test=
Realiza uma operação lógica =AND= entre dois operandos e atualiza as flags de
acordo com o resultado. A instrução é usada principalmente para testar, de
forma não destrutiva, se bits estão ligados ou se valores são zero.
#+begin_quote
A instrução =cmp= testa diferença (via subtração), enquanto =test= testa presença
de bits em comum (=AND= bit a bit) entre dois valores.
#+end_quote
*Forma geral:*
#+begin_example
test operando1, operando2
#+end_example
O resultado da operação =operando1 AND operando2= não é armazenado, apenas as
flags da CPU (zero, sinal, paridade, etc.) são atualizadas.
*Combinações válidas:*
| Operando 1 | Operando 2 | Exemplo |
|-------------+--------------+-----------------------|
| Registrador | Registrador | =test rax, rax= |
| Registrador | Imediato | =test rbx, 1= |
| Memória | Registrador | =test [rdi], rax= |
| Memória | Imediato | =test [rsi], 0xFF= |
*Exemplo de uso:*
#+begin_src asm
section .text
global _start
_start:
mov rax, 0 ; valor a testar
test rax, rax ; testa se é zero
jz deu_zero ; salta se resultado for zero
; não deu zero
mov rdi, 1
jmp sair
deu_zero:
; era zero
mov rdi, 0
sair:
; Termina com o valor recebido em rdi...
mov rax, 60 ; syscall: exit
syscall
#+end_src
** Instruções =call= e =ret=
As instruções =call= e =ret= são utilizadas para implementar chamadas e retornos
de sub-rotinas em Assembly, o que possibilita a estruturação do código em
blocos reutilizáveis. A instrução =call= salva o endereço da próxima instrução
(retorno) na pilha e salta para o endereço especificado. A instrução =ret=,
por sua vez, recupera o endereço de retorno na pilha e desvia a execução
de volta para o ponto após a chamada.
*Forma geral:*
#+begin_example
call destino
...
destino:
...
ret
#+end_example
*Combinações válidas:*
| Instrução | Operando | Exemplo |
|-----------+---------------------+--------------|
| =call= | Rótulo | =call uma_sub= |
| =call= | Registrador | =call rax= |
| =call= | Ponteiro de memória | =call [rax]= |
| =ret= | (sem operando) | =ret= |
*Observações:*
- As instruções =call= e =ret= afetam o registrador de instrução (=RIP=),
controlando o fluxo de execução.
- A instrução =call= também é utilizada para chamar funções importadas
com =extern=.
- Em x86_64, os argumentos de função geralmente são passados por
registradores, conforme a convenção de chamada de funções do sistema.
*Exemplo de uso:*
#+begin_src asm
section .text
global _start
_start:
call salve ; chama a sub-rotina
mov rax, 60 ; syscall: exit
xor rdi, rdi ; estado de término 0
syscall
salve:
mov rax, 1 ; syscall: write
mov rdi, 1 ; stdout
mov rsi, msg
mov rdx, msglen
syscall
ret
section .rodata
msg: db "Salve, simpatia!", 10
msglen equ $ - msg
#+end_src
** Instrução =syscall=
A instrução =syscall= realiza uma chamada ao kernel do sistema operacional
em 64 bits (em 32 bits, seria a instrução =int 80=), transferindo para ele
o controle temporário da execução do programa. É o principal meio de acesso
a serviços como leitura e escrita em arquivos, alocação de memória, término
do programa, etc.
#+begin_quote
Ao contrário das funções da biblioteca padrão (como =printf= ou =exit=), que são
abstrações em C, a =syscall= faz chamadas diretas ao sistema.
#+end_quote
*Forma geral:*
#+begin_example
mov rax, numero_da_syscall
mov rdi, argumento1
mov rsi, argumento2
mov rdx, argumento3
mov r10, argumento4
mov r8, argumento5
mov r9, argumento6
syscall
#+end_example
*Registradores de argumento:*
| Posição | Registrador | Descrição |
|---------+-------------+--------------------|
| 1º | =rdi= | Primeiro argumento |
| 2º | =rsi= | Segundo argumento |
| 3º | =rdx= | Terceiro argumento |
| 4º | =r10= | Quarto argumento |
| 5º | =r8= | Quinto argumento |
| 6º | =r9= | Sexto argumento |
*Resultado:*
- O valor de retorno da /syscall/ é carregado em =rax=.
- Erros são indicados com valores negativos em =rax=.
*Exemplo de uso (escrevendo na saída padrão):*
#+begin_src asm
section .rodata
msg: db "Salve, simpatia!", 10
len: equ $ - msg
section .text
global _start
_start:
mov rax, 1 ; syscall: write
mov rdi, 1 ; descritor de saída: stdout
mov rsi, msg ; ponteiro para os dados
mov rdx, len ; tamanho da mensagem
syscall
mov rax, 60 ; syscall: exit
xor rdi, rdi ; código de saída: 0
syscall
#+end_src
#+begin_quote
*Nota:* A lista de números das chamadas varia conforme o sistema operacional.
#+end_quote
** Padrões frequentemente utilizados
Aqui estão alguns padrões de código muito utilizados em Assembly, seja por
performance, clareza para o processador, ou por simples abreviação da
escrita do programa.
Por exemplo, em vez de usar:
#+begin_src asm
mov reg, 0
#+end_src
É comum escrever:
#+begin_src asm
xor reg, reg ; operação XOR bit a bit
#+end_src
Motivação:
- É mais rápido em alguns processadores.
- Utiliza menos bytes de código.
- Não depende de carregar um imediato =0=.
Nessa mesma linha, existem outros padrões bastante encontrados, como:
| Padrão | Descrição |
|---------+--------------------------------------------------------------|
| =inc reg= | Incrementa o registrador em 1 em vez de usar =add=. |
| =dec reg= | Decrementa o registrador em 1 em vez de usar =sub=. |
| =neg reg= | Inverte o sinal do registrador em vez de multiplicar por =-1=. |
* Montagem e execução de programas em Assembly NASM
** Requisitos
Para garantir que você tenha todas as ferramentas necessárias para acompanhar
nossos exemplos e demonstrações, verifique se os programas abaixo estão
instalados no seu sistema:
- Um editor de código da sua preferência (Vim, Emacs, Geany, etc)
- =git=
- =nasm=
- =gcc=
- =as=
- =ld=
- =gdb=
- =readelf=
- =objdump=
- =nm=
- =ldd=
- =hexedit=
- =xxd= (geralmente instalado com o editor Vim)
No Debian, a maioria deles é instalada com os comandos:
#+begin_example
sudo apt update
sudo apt install build-essential git nasm gdb binutils hexedit
#+end_example
** Exemplo: "Salve, simpatia!"
Tomando como exemplo o programa [[exemplos/00/salve-intel.asm][salve-intel.asm]], escrito com a sintaxe Intel
implementada pelo NASM:
#+begin_src asm
; salve-intel.asm
; Montar com: nasm -f elf64 salve-intel.asm
; Linkar com: ld salve-intel.o -o salve-intel
section .data
msg db "Salve, simpatia!", 10 ; 10 = '\n'
len equ $ - msg
section .text
global _start
_start:
; write(1, msg, len)
mov rax, 1 ; syscall número 1: write
mov rdi, 1 ; stdout
mov rsi, msg ; endereço da mensagem
mov rdx, len ; tamanho da mensagem
syscall
; exit(0)
mov rax, 60 ; syscall número 60: exit
xor rdi, rdi ; status 0
syscall
#+end_src
O processo de montagem envolve a execução do montador =nasm= informando o
formato de saída esperado (no caso, formato ELF de 64 bits):
#+begin_example
:~$ nasm -f elf64 salve-intel.asm
#+end_example
O =nasm= gera o binário montado em um arquivo com mesmo nome do código-fonte,
mas com a extensão =.o=, de /objeto/:
#+begin_example
ls
salve-intel.asm salve-intel.o
#+end_example
Esse arquivo objeto até poderia ser vinculado a outros binários para compor
um programa (/ligação estática/), mas ele ainda não pode ser executado como
está, pois ainda não contém todas as informações que o sistema operacional
requer para carregá-lo na memória e executá-lo. Para isso, o arquivo objeto
precisa ser processado por um editor de ligações (/link-editor/ ou simplesmente
/ligador/):
#+begin_example
:~$ ld salve-intel.o -o salve-intel
#+end_example
Como resultado, nós teremos um arquivo binário executável (inclusive com as
devidas permissões) de nome =salve-intel=:
#+begin_example
:~$ ls
salve-intel salve-intel.asm salve-intel.o
#+end_example
#+begin_quote
Se não tivéssemos definido um nome de saída (opção =-o=), o executável seria
chamado, por padrão, de =a.out=.
#+end_quote
Para testar, basta executar:
#+begin_example
:~$ ./salve-intel
Salve, simpatia!
#+end_example
* Links e referências úteis
- [[https://www.nasm.us/xdoc/2.16.02/html/][Manual do NASM (2.16.02)]]
- [[https://namazso.github.io/x86/][Guia de referência das instruções Intel 64]]
- [[https://blog.rchapman.org/posts/Linux_System_Call_Table_for_x86_64/][Tabela de chamadas de sistema Linux x86_64]]