Artigo :: Usando syscalls do Linux em Assembly

Introdução

Este tutorial prático ensina como escrever programas assembly que invocam chamadas de sistema do Linux diretamente, ignorando a biblioteca C e interagindo com o kernel no nível mais baixo.

Visão Geral

Quando um programa precisa interagir com o sistema operacional; para escrever em um arquivo, alocar memória ou finalizar graciosamente; ele faz uma chamada de sistema (syscall). A maioria dos programadores usa essas chamadas indiretamente através de wrappers da biblioteca C, mas entender como invocar syscalls diretamente do assembly te dá uma visão profunda de como programas realmente funcionam.

Neste tutorial, vamos construir um programa Linux x64 simples que imprime “Hack The Planet” na tela usando syscalls puras, sem nenhuma dependência da biblioteca C.

Pré-requisitos

Antes de começar, você deve ter:

Objetivo

Vamos construir um binário Linux ELF64 que usa syscalls básicas para imprimir uma mensagem na tela. No caminho, vamos aprender:

Encontrando Números de Syscalls

Cada syscall no Linux tem um número único. Para x64, estes são definidos em um arquivo header. Vamos encontrar a syscall write:

1
2
3
4
5
6
7
cat /usr/include/x86_64-linux-gnu/asm/unistd_64.h | grep write
#define __NR_write 1
#define __NR_pwrite64 18
#define __NR_writev 20
#define __NR_pwritev 296
#define __NR_process_vm_writev 311
#define __NR_pwritev2 328

A syscall básica write é o número 1. Agora vamos consultar o manual para entender seus argumentos:

1
man 2 write
Página do manual da syscall write

Como podemos ver, a syscall write tem três argumentos:

*ssize_t write(int fd, const void buf, size_t count);

  1. fd - Descritor de arquivo (0=stdin, 1=stdout, 2=stderr)
  2. buf - Ponteiro para os dados a escrever
  3. count - Número de bytes a escrever

Convenções de Registradores para Syscalls

Lendo o manual de syscalls, aprendemos como configurar os registradores x64 para invocar syscalls:

Configuração de registradores para syscalls

Mapeamento de registradores para syscalls x64:

  1. RAX - Número da syscall
  2. RDI - Primeiro argumento
  3. RSI - Segundo argumento
  4. RDX - Terceiro argumento
  5. R10 - Quarto argumento
  6. R8 - Quinto argumento
  7. R9 - Sexto argumento

Escrevendo o Código Assembly

Para nosso programa, vamos:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
global _start

section .text

_start:
;
; Configurando os registradores para imprimir a mensagem
; usando a syscall "write"
;
  mov rax, 1          ; número da syscall para write
    mov rdi, 1          ; descritor de arquivo 1 = stdout
    mov rsi, msg        ; ponteiro para mensagem
    mov rdx, length     ; comprimento da mensagem
    syscall             ; invocar kernel

section .data
    msg: db 'Hack The Planet',0xa    ; mensagem com newline
    length: equ $-msg                 ; calcular comprimento

Compilando o Código

Vamos compilar e linkar este código assembly:

1
2
nasm -felf64 syscall-001.nasm -o syscall-001.o
ld syscall-001.o -o syscall-001.bin

Flags explicadas:

Quando rodamos nosso programa com ./syscall-001.bin, vemos que nossa mensagem “Hack The Planet” é impressa, mas recebemos um erro Segmentation Fault:

Execução do programa com segfault

Adicionando a Syscall Exit

Vamos encontrar o número da syscall exit:

1
2
3
cat /usr/include/x86_64-linux-gnu/asm/unistd_64.h | grep exit
#define __NR_exit 60
#define __NR_exit_group 231

A syscall exit é o número 60. Vamos consultar sua página de manual:

1
man 2 exit
Página do manual da syscall exit

Existe apenas um argumento: int status (o código de saída, como 0 para sucesso ou diferente de zero para erros).

1
void _exit(int status);

Vamos adicionar a syscall exit ao nosso programa. Usaremos status de saída 1 para teste:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
global _start

section .text

_start:
;
; Configurando os registradores para imprimir a mensagem
; usando a syscall "write"
;
  mov rax, 1          ; número da syscall para write
    mov rdi, 1          ; descritor de arquivo 1 = stdout
    mov rsi, msg        ; ponteiro para mensagem
    mov rdx, length     ; comprimento da mensagem
    syscall             ; invocar kernel

;
; Configurando os registradores para chamar a syscall "exit"
; e sair normalmente com retorno 1
; Nota: se comentarmos isso o processo finalizará com
; erro Segmentation Fault
;
  mov rax, 60         ; número da syscall para exit
    mov rdi, 1          ; código de status de saída
    syscall             ; invocar kernel

section .data
    msg: db 'Hack The Planet',0xa
    length: equ $-msg

Agora compile novamente:

1
2
nasm -felf64 syscall-001.nasm -o syscall-001.o
ld syscall-001.o -o syscall-001.bin

Após executar ./syscall-001.bin, podemos verificar que o status de saída é 1 como projetado:

Execução do programa com status de saída 1

Perfeito! Sem segfault. O programa sai limpo.

Analisando com strace

Se usarmos strace para analisar este binário ELF64, veremos detalhes interessantes. Quando executamos o binário, o shell usa execve com nosso binário como argumento junto com variáveis de ambiente:

Saída do strace mostrando execve
Detalhes da syscall execve

Fluxo de Execução de Syscalls

Veja como as syscalls funcionam nos bastidores:

Diagrama de fluxo de execução de syscalls
Detalhes da execução de syscalls

O processo:

  1. Programa do usuário configura registradores (RAX para número da syscall, RDI/RSI/RDX para argumentos)
  2. Instrução syscall dispara uma troca de contexto para modo kernel
  3. Kernel valida argumentos e executa a operação solicitada
  4. Kernel retorna controle ao userland com resultado em RAX
  5. Programa continua a execução

Pontos-Chave

Exercícios Práticos

Leitura Adicional