3 pontos por GN⁺ 2025-07-14 | 1 comentários | Compartilhar no WhatsApp
  • Apresentação do primeiro artigo de uma série introdutória sobre assembly x86-64
  • Explica a instalação das ferramentas e a estrutura básica com foco em sistemas modernos de 64 bits
  • Orienta o uso de Flat Assembler (FASM) e WinDbg como principais ferramentas de desenvolvimento e depuração
  • Inclui um resumo dos conhecimentos essenciais para uso prático, como formato PE, importação de DLL e convenção de chamada do Windows
  • Explicação centrada na experiência prática de escrever um programa simples de encerramento e executar o procedimento de depuração

Introdução e importância

  • Ao ter o primeiro contato com assembly x86, era comum nas universidades aprender em ambientes antigos, baseados em 16 bits, DOS e memória segmentada
  • Como hoje os processadores de 64 bits são o padrão, esta série trata apenas do ambiente x86-64 realmente usado na prática, excluindo por completo os elementos legados
  • Este tutorial se concentra no desenvolvimento de programas de 64 bits que rodam no ambiente do sistema operacional Windows
  • Começa com código mínimo que acessa o sistema operacional diretamente, sem usar bibliotecas
  • O texto é voltado para desenvolvedores que querem aprender assembly pela primeira vez e assume conhecimento básico de C/C++

Preparando as ferramentas de desenvolvimento

Assembler

  • A CPU só consegue interpretar código de máquina, que é difícil para humanos entenderem; a linguagem assembly é sua representação em um formato legível por pessoas
  • O programa que converte linguagem assembly em código de máquina é o assembler
  • Não existe um padrão único para assembly x86-64, e cada assembler tem diferenças de sintaxe e comportamento
  • Nesta série é usado o Flat Assembler (FASM), por ser pequeno, fácil de usar e oferecer um sistema de macros poderoso e um editor

Depurador

  • Para analisar o código assembly escrito e observar o fluxo de execução, o depurador é uma ferramenta indispensável
  • A recomendação é o WinDbg, que permite verificar e manipular separadamente registradores, memória e código assembly
  • Ele pode ser instalado selecionando apenas os componentes necessários no Windows 10 SDK
  • Com o depurador, é possível observar diretamente o estado interno do programa, a estrutura da memória e as mudanças nos registradores

A perspectiva da programação em assembly

Estrutura da CPU e conjunto de instruções

  • A CPU só pode executar um conjunto limitado de operações de acordo com um conjunto de instruções específico
  • Uma instrução é a unidade básica de trabalho que a CPU consegue executar
  • Cada instrução opera de forma muito simples com seus parâmetros, como armazenar valores ou fazer operações aritméticas
  • Em programação de baixo nível e depuração, o ponto central é entender que essa estrutura é a base de todos os conceitos de alto nível

Registradores

  • Registradores são áreas de memória dedicadas e extremamente rápidas embutidas na CPU
  • No x86-64, existem 16 registradores de propósito geral, todos com 64 bits
  • Cada registrador pode ser acessado parcialmente em unidades de byte, word e doubleword
Registrador Byte inferior Word inferior Doubleword inferior
rax al ax eax
rbx bl bx ebx
rcx cl cx ecx
rdx dl dx edx
rsp spl sp esp
rsi sil si esi
rdi dil di edi
rbp bpl bp ebp
r8~r15 r8b~r15b r8w~r15w r8d~r15d
  • rsp funciona como ponteiro da pilha, e rsi/rdi atuam como índices no processamento de strings, então alguns registradores têm finalidades especiais
  • rip é o ponteiro de instrução, e rflags é um registrador especial que guarda flags de estado do resultado de operações

Memória e endereços

  • A memória funciona como um arranjo contínuo de bytes a partir do índice 0
  • Em arquiteturas x86 antigas, o esquema segmento-offset era obrigatório, mas no x86-64 toda a memória é tratada como um espaço de endereçamento flat
  • Na prática, o sistema operacional e o hardware mapeiam dinamicamente o espaço de endereços virtuais de cada processo para a memória física
  • Ou seja, mesmo que o endereço virtual seja o mesmo, em processos diferentes ele pode corresponder a memórias físicas diferentes
  • Instruções e dados coexistem na mesma memória (arquitetura de von Neumann), em contraste com arquiteturas Harvard como a AVR usada em Arduino, que armazena os dados separadamente

Escrevendo o primeiro programa em assembly

  • Depois de instalar o FASM, o exercício é escrever e compilar o programa simples abaixo
format PE64 NX GUI 6.0
entry start

section '.text' code readable executable
start:
        int3
        ret

Explicação do código

  • format PE64 NX GUI 6.0 : especifica o formato do executável que o FASM vai gerar; aqui é um PE (Portable Executable) GUI de 64 bits
  • entry start : define o entry point do programa; a execução começa na posição do rótulo start
  • section '.text' code readable executable : indica que esta é a seção de código do PE, em uma área executável
  • start: : dá nome ao ponto de entrada definido anteriormente
  • int3 : breakpoint para depuração, usado para pausar o programa e inspecionar seu estado
  • ret : instrução que retira um endereço da pilha e transfere o controle para ele; neste programa, responde com encerramento imediato

Exercício de depuração

  • No WinDbg, abra o executável (.exe) do programa acima e prepare várias janelas, como desassembly e registradores

  • Pressione F5 para fazer o programa chegar ao breakpoint e, a cada F8, execute uma instrução por vez, passo a passo

  • É possível observar em tempo real as mudanças nos registradores, como rip

  • Depois da execução de ret, o controle é devolvido ao sistema operacional, que em seguida chama RtlExitUserThread, encerrando a thread e o processo

  • Atenção: ao encerrar apenas com ret, o processo pode permanecer ativo dependendo da existência de outras execuções em segundo plano além da thread, portanto em um encerramento normal, o ideal é sempre chamar ExitProcess

Formato PE e importação de DLL

Visão geral da estrutura de importação de funções de DLL

  • Funções WinAPI como ExitProcess ficam em KERNEL32.DLL
  • Para usar essas funções externas, é necessário montar a tabela de importação do executável (seção .idata)
  • A Import Directory Table (IDT) da seção idata contém informações como o nome da DLL e os endereços (RVA) de nome da função, IAT e ILT
  • A IAT (Import Address Table) é sobrescrita em tempo de execução pelo loader do sistema operacional com o endereço real da função
  • A Hint/Name Table é composta pelo nome de cada função e suas informações de hint

Exemplo de definição da seção .idata no FASM

section '.idata' import readable writeable
idt: 
    dd rva kernel32_iat
    dd 0
    dd 0
    dd rva kernel32_name
    dd rva kernel32_iat
    dd 5 dup(0)
name_table: 
    _ExitProcess_Name dw 0
                      db "ExitProcess", 0, 0
kernel32_name: db "KERNEL32.DLL", 0
kernel32_iat: 
    ExitProcess dq rva _ExitProcess_Name
    dq 0 
  • db/dw/dd/dq : inserem valores em unidades de byte/word/doubleword/quadword (8 bytes)
  • rva : calcula o endereço virtual relativo (Relative Virtual Address) de um símbolo
  • É possível referenciar funções de DLL montando manualmente a IAT e a Name Table

Convenção de chamada do Windows em 64 bits (MS x64 Calling Convention)

  • É a convenção padrão que define como passar argumentos e usar a pilha em chamadas de função
  • No Windows de 64 bits, usa-se a Microsoft x64 Calling Convention
  • Principais características:
    • O ponteiro da pilha deve estar sempre alinhado em 16 bytes
    • Os 4 primeiros argumentos inteiros/ponteiros usam os registradores rcx, rdx, r8, r9
    • Os 4 primeiros argumentos de ponto flutuante vão em xmm0~xmm3
    • Argumentos adicionais usam a pilha
    • Independentemente da quantidade de argumentos, é preciso reservar 32 bytes de shadow space na pilha
    • A limpeza da pilha é responsabilidade de quem chama

Exemplo de chamada de ExitProcess

format PE64 NX GUI 6.0
entry start

section '.text' code readable executable
start:
    int3
    sub rsp, 8 * 5
    xor rcx, rcx
    call [ExitProcess]

section '.idata' import readable writeable
idt: 
    dd rva kernel32_iat
    dd 0
    dd 0
    dd rva kernel32_name
    dd rva kernel32_iat
    dd 5 dup(0)
name_table: 
    _ExitProcess_Name dw 0
                      db "ExitProcess", 0, 0
kernel32_name db "KERNEL32.DLL", 0
kernel32_iat: 
    ExitProcess dq rva _ExitProcess_Name
    dq 0 

Análise do novo código

  • sub rsp, 8 * 5 : ajusta o ponteiro da pilha (reserva 40 bytes), tratando de uma vez o alinhamento em 16 bytes e a reserva do shadow space

  • xor rcx, rcx : atribui 0 ao registrador rcx, que é o primeiro argumento (usado como código de saída)

  • call [ExitProcess] : salta para o endereço da função ExitProcess efetivamente registrado na import table

  • Ao executar passo a passo no WinDbg, é possível verificar diretamente as mudanças no ponteiro da pilha (rsp), no registrador rcx e no fluxo de encerramento do processo

Encerramento

  • Este artigo apresenta, com foco prático, o fluxo geral de assembly x86-64, desde a configuração das ferramentas básicas até formato PE, importação de DLL, convenção de chamada x64, criação do primeiro programa e depuração
  • A próxima parte vai tratar da implementação de funções mais variadas e de código real

1 comentários

 
GN⁺ 2025-07-14
Comentários no Hacker News
  • Queria compartilhar um projeto que venho desenvolvendo há alguns anos
    https://asm-editor.specy.app
    É uma IDE online interativa que oferece suporte a várias linguagens assembly, como M68K, MIPS, RISC-V e x86
    Tem muitos recursos voltados ao ensino de programação em assembly
    Também pode ser incorporada em outros sites

  • Eu não sabia que existia acesso direto ao byte de baixo endereço em registradores de indexação de ponteiro, por exemplo, em 16/32 bits si/esi podendo ser acessados como sil
    É um conceito parecido com acessar al a partir de ax/eax
    Fiquei curioso se realmente existem opcodes novos adicionados no x86_64
    Acho que vou precisar conferir novamente a especificação da plataforma
    Pergunto isso por pura curiosidade

  • Compartilho um material introdutório sobre assembly que escrevi
    https://www.nayuki.io/page/a-fundamental-introduction-to-x86-assembly-programming

  • Fiquei curioso para saber se conseguiria deixar o dispatch do meu emulador de CPU mais rápido em assembly do que em C++, então tentei otimizar em assembly
    Rodei um programa de Fibonacci, mas o resultado nem chegou perto
    No fim, só fiz o merge com a opção desativada por padrão
    Mesmo assim, acredito que certamente exista uma forma de deixá-lo mais rápido
    https://github.com/libriscv/libriscv/blob/master/lib/libriscv/amd64/inaccurate_dispatch.nasm
    Aprendendo maneiras de acessar memória, consegui melhorar um pouco o desempenho
    Reduzi a jump table de 64 bits para 32 bits e a coloquei na seção .text, para usar acesso relativo a RIP
    O programa de Fibonacci não precisava de muitos bytecodes
    Eu realmente gostaria de ouvir dicas sobre o que mais pode ser melhorado

    • Fico curioso se você comparou diretamente o código que escreveu com o código gerado pelo compilador de C++
      Não conheço bem o contexto, mas acho possível que a diferença não venha do mecanismo de dispatch em si, como a forma de fazer fetch das instruções, e sim da diferença na implementação real das instruções
      Como estratégia de otimização, dá para mapear os registradores emulados diretamente para registradores reais do x86-64 e evitar totalmente gravá-los na memória
      Assim, operações como add podem ser feitas diretamente, sem buscar os valores na memória
      Só que essa abordagem torna a escrita do emulador bem mais trabalhosa
  • É um material introdutório a assembly x86 com prática no navegador
    Dá para executar os exemplos imediatamente, sem precisar de configuração local
    https://shikaan.github.io/assembly/x86/guide/2024/09/08/x86-64-introduction-hello.html
    Aliás, fui eu que escrevi esse material

    • Fiquei curioso se há validação de entrada
      Parece ser um esquema que monta diretamente com NASM e depois executa o binário, então isso me fez pensar na questão de segurança
  • Só pela foto de perfil, achei que fosse o junferno

  • Mesmo só mexer um pouco com assembly já aprofunda bastante a compreensão geral, então sempre acaba sendo uma boa experiência
    Não precisa chegar ao ponto de fazer um grande projeto; recomendo criar coragem e tentar fazer um pouco por conta própria

  • Compartilho o link da discussão no HN da época (2020)
    https://news.ycombinator.com/item?id=24195627

  • Ainda bem que usa a sintaxe de assembly no estilo Intel

    • Fiquei curioso sobre que outras sintaxes de assembly existem
  • Tenho vontade de fazer alguma coisa em assembly, mas não me vem nenhuma ideia específica

    • Recomendo o jogo TIS-100
      É um jogo de resolver puzzles com uma espécie de pseudo-assembly
      Acho que esse tipo de jogo pode ajudar a matar a vontade de brincar com assembly