Assembly é uma linguagem de baixo nível que serve como ponte entre o código de máquina e as linguagens de alto nível, como [[C]] ou [[Python]]. Escrever em Assembly significa falar diretamente com o processador, controlando cada instrução executada, cada registrador utilizado, cada byte de memória manipulado. É aqui que o programador tem controle total — e também total responsabilidade. Cada arquitetura de processador (como x86, ARM ou MIPS) possui seu próprio conjunto de instruções (ISA – Instruction Set Architecture), o que significa que o Assembly é intrinsecamente dependente da arquitetura para a qual se está programando. Por isso, o estudo de Assembly envolve não apenas aprender a sintaxe da linguagem, mas também compreender o funcionamento interno da CPU, registradores, pilha, alocação de memória e o ciclo de instrução. Apesar de sua complexidade e verbosidade, Assembly é fundamental para áreas como engenharia reversa, segurança da informação, desenvolvimento de sistemas embarcados e otimização de desempenho. Conhecê-lo é desvendar os bastidores da computação — é enxergar como cada linha de código se transforma, passo a passo, em operações executadas pelo hardware. ## Seções do código Os programas em assembly podem ter três seções: - `section .data`: Área dos dados inicializados. - `section .bss`: Área de dados não inicializados. - `section .text`: O código do programa. Obs.: A ordem das seções é indiferente. ### Estrutura de uma linha de código Cada linha de código do NASM (a menos que seja uma macro, uma diretiva de pré-processamento ou uma diretiva de compilação) é composta pela combinação de quatro campos: ```shell +---------+-----------+-----------+--------------+ | rótulo: | instrução | operandos | ; comentário | +---------+-----------+-----------+--------------+ ``` OBS: A presença ou ausência de qualquer um desses campos pode variar ou até ser opcional, mas este é o layout geral. ### O seu primeiro programa A execução de programas em assembly é feita de cima para baixo e deve sempre conter um rótulo (_label_) para indicar o ponto de início do programa: seu _ponto de entrada_, o que nós definimos desta forma na seção `.text`. Na seção `.data` nós definimos os identificadores de _dados inicializados_, algo análogo às variáveis e constantes inicializadas de linguagens de alto nível. É "algo análogo" porque, em linguagens de baixo nível, o conceito de "variáveis" é uma mera analogia com o que temos em linguagens de alto nível: na prática, esses identificadores de dados são rótulos (_labels_) e apenas nomeiam endereços na memória. ```shell section .data section .text global _start ; A diretiva 'global' torna o rótulo '_start' ; visível de qualquer parte do programa. _start: ; Aqui está o início do programa. ``` #### Diretivas No contexto de Assembly, o termo **"diretiva"** é usado porque `global` não é uma instrução executável do processador, mas sim um **comando para o assembler** (o programa que converte código Assembly em código de máquina). As diretivas são instruções que controlam como o assembler deve processar o código, não o que o processador deve executar. Elas servem para: - **Configurar símbolos e rótulos** (`global`, `extern`) - **Definir seções de memória** (`section .data`, `section .text`) - **Reservar espaço** (`db`, `dw`, `dd`) - **Incluir arquivos** (`include`) A diretiva `global _start` informa ao assembler que o símbolo `_start` deve ser exportado, permitindo que o linker o encontre como ponto de entrada do programa. **Diferença importante:** - **Instruções**: `mov`, `add`, `call` → executadas pelo processador - **Diretivas**: `global`, `section`, `db` → processadas pelo assembler #### Rótulos Os rótulos (_labels_) são identificadores de endereços e servem para que possamos fazer saltos (_jumps_) para diferentes partes do programa ou localizar dados na memória. Nós podemos criá-los livremente, mas o rótulo `_start:` normalmente é utilizado para definir o _ponto de entrada_ do programa. ### Escrevendo uma mensagem ```asm section .data msg db "Hello!",10 ; 'msg' - rótulo dos dados definidos ; 'db' - os dados são definidos como ; uma cadeia de bytes. section .text global _start ; A diretiva 'global' torna o rótulo '_start' ; visível de qualquer parte do código. _start: ; Aqui está o início do programa. ``` #### As pseudos intruções A pseudo-instrução `db` (_define bytes_) é utilizada para definir que os dados devem ser interpretados como uma cadeia de bytes. Contudo, nós ainda podemos definir dados como cadeias de palavras (`dw`), cadeias de palavras duplas (`dd`) ou cadeias de palavras quádruplas (`dq`), onde, em processadores x86_64, as palavras terão, 2, 4 ou 8 bytes de comprimento, respectivamente. Isso afeta diretamente o espaço ocupado pelos dados na memória: | Diretiva | Caracteres | Caracteres em hexa na memória | Tamanho | | -------- | ---------- | ----------------------------------------- | ------------------------------ | | `db` | `'ABCDE'` | `0x41 0x42 0x43 0x44 0x45` | 5 bytes | | `dw` | `'ABCDE'` | `0x41 0x42 0x43 0x44 0x45 0x00` | 6 bytes, 3 palavras de 2 bytes | | `dd` | `'ABCDE'` | `0x41 0x42 0x43 0x44 0x45 0x00 0x00 0x00` | 8 bytes, 2 palavras de 4 bytes | | `dq` | `'ABCDE'` | `0x41 0x42 0x43 0x44 0x45 0x00 0x00 0x00` | 8 bytes, 1 palavra de 8 bytes | Existem outros múltiplos de palavras após `dq`: - `dt`: palavras de 10 bytes (_ten-word_: 80 bits). - `do`: palavras de 16 bytes (_octo-word_: 128 bits). - `dy`: palavras de 32 bytes (256 bits). - `dz`: palavras de 64 bytes (512 bits). > Nenhum deles permite a passagem de constantes numéricas inteiras como valor. ``` ### Tabela de Registradores | 64 bits | 32 bits | 16 bits | 8 bits Low | 8 bits High | | :-----: | :-----: | :-----: | :--------: | :---------: | | RAX | EAX | AX | AL | AH | | RBX | EBX | BX | BL | BH | | RCX | ECX | CX | CL | CH | | RDX | EDX | DX | DL | DH | | RSI | ESI | SI | — | — | | RDI | EDI | DI | — | — | | RSP | [[ESP]] | SP | — | — | | RBP | [[EBP]] | BP | — | — | | RIP | [[EIP]] | IP | — | — | | R8–R15 | — | — | — | — | - RBP/EBP/BP: é o **Base Pointer** (também chamado de _frame pointer_). Ele serve como **referência estável** para acessar variáveis locais e parâmetros da função durante sua execução. Em resumo, durante a execução da função, o **RBP guarda o valor da RSP (stack pointer)** no exato momento em que a função começou. Ele serve como âncora fixa. ESP -> Aponta para a topo da pilha. EBP -> Aponta para a base da pilha. EIP -> Aponta para o próximo endereço a ser executado. #### O que é um **Stack Frame**? Um **stack frame** (ou "quadro de pilha") é um **bloco de memória** alocado na pilha toda vez que uma função é chamada. Ele guarda **tudo o que a função precisa para funcionar isoladamente**: - **Parâmetros da função** (caso não estejam em registradores) - **Variáveis locais** - **Valor antigo do RBP** (base pointer da função chamadora) - **RIP - Endereço de retorno** (para onde a função deve voltar ao terminar) ```shell Pilha de um binário de 64 bits +-------------------------+ | Parâmetros adicionais | ← Caso sejam mais que RAX,RBX,etc. +-------------------------+ | Variáveis locais | ← RSP aponta para esse local. +-------------------------+ | Saved RBP | ← Base do Stack Frame. +-------------------------+ | RIP | ← Para onde return vai pular. +-------------------------+ ``` ### Estrutura de Memória de um Processo | **Seção** | **Descrição** | | ----------------- | ------------------------------------------------------------------------------------------------------------------------------ | | **Text** | Código do programa (instruções executáveis). | | **Code** | Dados inicializados (variáveis com valor definido). | | **BSS** | Dados não inicializados (variáveis globais sem valor inicial). | | **Heap** | Alocação dinâmica (o programa pode requisitar mais espaço em tempo de execução). | | **Unused Memory** | Espaço de memória ainda não utilizado pelo processo. | | **Stack** | Pilha do programa. Funciona como LIFO (_Last In, First Out_), utilizada para armazenar funções, parâmetros e variáveis locais. | ### Assembler O **Assembler** é um programa (ou ferramenta) que **converte código escrito em linguagem Assembly** para **código de máquina** (binário) que o processador pode entender e executar diretamente. **Assembly → Assembler → Código de máquina (opcodes - arquivos .o)** Os principais Assemblers do mercado são: - **NASM** (Netwide Assembler) – muito usado em Linux. - **MASM** (Microsoft Assembler) – comum em ambientes Windows. - **GAS** (GNU Assembler) – usado com GCC no Linux. - **FASM** (Flat Assembler) – leve, usado em projetos independentes. Exemplo de código NASM: ``` section .text global _start _start: mov eax, 1 ; syscall: exit mov ebx, 0 ; exit code int 0x80 ; chamada de sistema ``` Depois de compilado ele fica dessa forma: ``` B8 01 00 00 00 BB 00 00 00 00 CD 80 ``` ### Linker O **Linker** (ligador) é uma ferramenta que **combina um ou mais arquivos objeto (.o ou .obj)** gerados pelo Assembler e **resolve todas as referências entre eles**, produzindo o **arquivo executável final** (como um `.exe` no Windows ou um binário ELF no Linux). **Assembly → Assembler → Linker > Binário Executável** Funções principais do Linker - **Resolve símbolos**: Exemplo: Se você chama a função `printf`, o Linker encontra onde ela está (ex: libc) e conecta seu código com ela. - **Combina arquivos objeto**: Permite modularidade. Você pode ter múltiplos `.o` e juntar tudo num `.exe`. - **Aloca endereços**: Decide onde cada função/variável vai ficar na memória no binário final. - **Inclui bibliotecas externas**: Está chamando funções do sistema? O Linker conecta seu programa com essas bibliotecas. ### Funções Assembly ```markdown MOV - Define um valor para um registrador. JMP - Pula para o local deixando de executar o que está abaixo do JUMPER. CALL - Chama uma função em seguida, quando a função termina volta para o função chamadora. LEA (Load Effective Address) - Seria uma chamada para ler a posição de memória do que é passado. RET - Retorna para a posição antiga (Usada dentro de uma função) PUSH -> O `PUSH` coloca um valor no topo da pilha (stack). A pilha é uma estrutura LIFO (último a entrar, primeiro a sair), usada principalmente para: - Passar argumentos para funções; - Salvar valores temporários (como registradores); - Controlar o fluxo da execução (retornos, etc.). ``` ### Partes de um código Assembly | Seção | Uso | | ------- | ------------------------------------------------------- | | `.data` | Variáveis **inicializadas** (strings, números, etc.) | | `.bss` | Variáveis **não inicializadas** (buffers, arrays, etc.) | | `.text` | Código executável (instruções) | ### Exemplos baseados nas funções: Manipulação de registradores: ``` MOV EAX, 0x1337 ; Move valor 0x1337 para EAX ADD EAX, 0x1 ; Soma 1 ao valor em EAX (incrementa) SUB EBX, EAX ; Subtrai EAX de EBX XOR EAX, EAX ; Zera EAX (técnica comum pra zerar registrador) ``` Stack (pilha): ``` PUSH EAX ; Empilha valor de EAX POP EBX ; Desempilha para EBX PUSH 0x41414141 ; Empilha valor AAAAAAAA (útil em fuzzing/shellcode) ``` Controle de fluxo: ``` CMP EAX, EBX ; Compara EAX com EBX JNE not_equal ; Salta para "not_equal" se os valores forem diferentes JMP exit ; Salta incondicionalmente para "exit" CALL my_function ; Chama uma função ``` Interrupções e no-ops: ``` NOP ; No Operation (usado em NOP sleds) INT3 ; Breakpoint para debugging (\xCC) ``` ### Exemplos práticos para Windows Gerando um alerta no Windows com uma caixa de mensagem: ```c default rel extern MessageBoxA global main section .data texto db "www.ooclaar.com.br", 0 titulo db "Keep Learning", 0 section .text main: sub rsp, 40 ; espaço para alinhamento da pilha mov rcx, 0 ; hWnd = NULL mov rdx, texto ; lpText mov r8, titulo ; lpCaption mov r9d, 0x00000001 ; uType = MB_OKCANCEL call MessageBoxA add rsp, 40 ; limpa stack ret ``` Compilando o código assembly em um arquivo .obj com NASM. ```markdown # Para compilar em 32 bits nasm.exe -f win32 -o $file_name.o $file_name.asm # Para compilar em 64 bits. nasm.exe -f win64 -o $file_name.o $file_name.asm ``` Realizando a ligações com LD ou GCC. ```markdown # Compilando 32bits usando LD. ld.exe -m i386pe -o .\exemplo1.exe .\exemplo1.obj -e _main # Compilando 64bits usando LD. ld -o exemplo.exe exemplo.obj -e main -luser32 -lkernel32 # Compitando para 32 bits usando GCC gcc -m32 exemplo.obj -o exemplo.exe -luser32 # Compilando para 64bits usando GCC gcc .\exemplo1.obj -o ./exemplo1.exe -luser32 ``` *OBS: O parâmetro -luser32 incluir a **DLL do Windows chamada `user32.dll`**, que contém funções da interface gráfica, como: `MessageBoxA`, `CreateWindowEx`, etc.* *OBS2: O código assembly pode mudar um pouco de acordo com a versão do sistema que irá compilar.* *Referência a chamada de API:* *https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-messageboxa* Criando um código malicioso para funcionar de forma oculta: ``` default rel extern ShellExecuteA global main section .data tipo db "open", 0 cmd db "cmd", 0 param db "/c mkdir ola", 0 section .text main: sub rsp, 32 ; alinha a pilha (shadow space) xor rcx, rcx ; HWND = 0 mov rdx, tipo ; lpOperation = "open" mov r8, cmd ; lpFile = "cmd" mov r9, param ; lpParameters = "/c mkdir ola" mov qword [rsp + 32], 0 ; lpDirectory = NULL mov qword [rsp + 40], 1 ; nShowCmd = 1 (SW_SHOWNORMAL) call ShellExecuteA add rsp, 32 ; restaura pilha ret ``` *Referência a chamada de API:* *https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-shellexecutea *Referência aos conjuntos de SysCall Linux:* *https://syscalls.w3challs.com/?arch=x86 *Localização dos arquivos syscall do Linux:* */usr/include/x86_64-linux-gnu/asm/\* ```markdown # Analisando uma função de syscall usando man man 2 white ``` ## Exemplos práticos para Linux (i386) ``` global _main section .data curso: db 'Texto do ooclaar', 0xa section .text _main: mov eax, 4 mov ebx, 1 mov ecx, curso mov edx, 15 int 0x80 mov eax, 1 mov ebx, 0 int 0x80 ``` ```markdown # NASM de Output: # - elf64: Linux 64 bits # - elf32: Linux 32 bits # - win64: Windows 64 bits # - win32: Windows 32 bits # - macho64: macOS 64 bits # Compitando para 32 bits usando GCC nasm -f elf32 arquivo.asm -o arquivo.o # Compilando para 64bits usando GCC nasm -f elf64 arquivo.asm -o arquivo.o # Compilando para 32bits (Windows) usando LD. x86_64-w64-mingw32-ld arquivo.o -o exemplo.exe # Compilando para 64bits (Windows) usando LD. i686-w64-mingw32-ld arquivo.o -o exemple.exe # Compilando para 32bits (MacOS) usando LD. ld -macosx_version_min 10.7.0 -lSystem arquivo.o -o executavel # Compilando 32bits usando LD. ld --entry _main -m elf_i386 ./exemplo.o -o exemplo # Compilando 64bits usando LD. ld --entry main -m elf_x86_64 ./exemplo.o -o exemplo ```