Atualizado: 18/10/2025

Este conteúdo é original e não foi gerado por inteligência artificial.

Chamada de Funções C/C++ no Linux em Assembly NASM

O NASM permite chamar funções escritas em C ou C++ a partir de um programa em Assembly. Isso facilita a criação de aplicações, pois torna possível reutilizar funções já implementadas em C, sem precisar reescrevê-las manualmente. Para realizar essas chamadas, seguem-se convenções conhecidas como ABI (Application Binary Interface ou interface binária de aplicação).


Convenções de Chamada em Linux

No Linux, aplica-se o padrão System V ABI. De acordo com ele, os seis primeiros parâmetros de uma função são enviados por registradores, na seguinte ordem:

  1. rdi
  2. rsi
  3. rdx
  4. rcx
  5. r8
  6. r9

Se houver apenas um parâmetro, ele é enviado por rdi. Se houver dois, o primeiro é enviado por rdi e o segundo por rsi. Quando a função recebe mais de seis parâmetros, os adicionais são colocados na pilha como valores de 8 bytes (geralmente com push), sendo o último parâmetro o primeiro a ser inserido.

O valor de retorno é colocado em rax. Se houver dois valores de retorno, o segundo é colocado em rdx, formando o par rdx:rax. Quando há múltiplos retornos, normalmente rax contém um ponteiro para uma estrutura ou conjunto de valores, ou os parâmetros de entrada incluem ponteiros para as áreas de memória onde esses valores serão armazenados.

Quando os parâmetros são números de ponto flutuante, eles são enviados por registradores xmm0 a xmm7. Se a função receber tanto números inteiros quanto de ponto flutuante, os inteiros seguem o método normal (rdi, rsi, etc.), enquanto os de ponto flutuante utilizam os registradores XMM.

Para funções com número variável de argumentos (como printf ou scanf), o número de registradores XMM utilizados é informado em rax, permitindo que a função chamada saiba quantos registradores deve preservar.

Quando o valor de retorno é de ponto flutuante, ele é colocado em xmm0. Se houver dois valores de ponto flutuante, são usados xmm1:xmm0.

A função chamada deve preservar o conteúdo dos registradores RSP, RBP, RBX, R12, R13, R14 e R15, conhecidos como não voláteis (non-volatile ou callee-saved). Os demais — RAX, RCX, RDX, RSI, RDI, R8, R9, R10 e R11 — são voláteis (volatile) e podem ser modificados; a responsabilidade de salvá-los cabe à função que faz a chamada.


Exemplo de Assinatura de Função

double myfunc(int a, double b, int c, double d)

Nesse caso:

  • ardi
  • bxmm0
  • crsi
  • dxmm1 O valor de retorno (double) é colocado em xmm0.

Se fosse uma função com número variável de argumentos, rax seria definido como 2, indicando o uso de dois registradores XMM.

O System V ABI também determina que a pilha deve estar alinhada a múltiplos de 16 bytes imediatamente antes de uma chamada de função. Isso significa que o endereço em rsp precisa ser divisível por 16. A ausência desse alinhamento pode causar falhas em algumas funções. Como o endereço de retorno ocupa 8 bytes, o alinhamento costuma ser ajustado no início da função, muitas vezes junto com o salvamento de rbp, resultando em 16 bytes de diferença — o que simplifica o gerenciamento da pilha.

Para funções variádicas (com número indefinido de argumentos), é recomendado definir rax como zero caso não existam valores de ponto flutuante a serem passados.


Ponto de Entrada em Programas Assembly

Ao usar funções C/C++ em Assembly, o ponto de entrada do programa é a função main, e não o rótulo _start:

global main     ; função main é o ponto de entrada
section .text
main:
    ; instruções da função main
    ret

A finalização do programa ocorre com a instrução ret, como em uma função comum. O código associado a _start é incluído por uma biblioteca auxiliar, responsável por inicializar o ambiente C e, então, chamar main. Após sua execução, main retorna o controle a esse código inicial.


Exemplo: Chamando printf em Linux

O exemplo a seguir imprime uma mensagem no console chamando a função printf:

global main     ; função main - ponto de entrada

extern printf   ; referência externa para printf

section .data
message db "Hello www.programicio.com", 10   ; string para exibição

section .text
main:
    sub rsp, 8             ; garante alinhamento de 16 bytes
    mov rdi, message       ; primeiro parâmetro: endereço da string
    call printf
    add rsp, 8             ; restaura o valor original da pilha
    ret

A diretiva extern informa ao compilador que printf está definido fora do programa Assembly:

extern printf

Para manter o alinhamento da pilha, subtrai-se 8 bytes de rsp:

sub rsp, 8

Caso o alinhamento seja incerto, pode-se forçar o ajuste:

and rsp, -16

A compilação e linkagem são feitas assim:

nasm -f elf64 hello.asm -o hello.o
gcc hello.o -static -o hello

O uso de -static faz com que as funções da biblioteca C sejam incorporadas diretamente ao executável, resultando em um arquivo maior (cerca de 880 KB no exemplo). Bibliotecas adicionais podem ser incluídas com o parâmetro -l.

Execução:

./hello
Hello www.programicio.com

Compilação Dinâmica

Em vez da vinculação estática, é possível compilar dinamicamente, o que reduz o tamanho do executável:

gcc hello.o -o hello -no-pie

O parâmetro -no-pie indica que o código não é independente de posição (Position Independent Executable). O executável gerado é menor (aproximadamente 16 KB), mas esse método é menos seguro, pois torna o código mais suscetível a vulnerabilidades.

Ao omitir -no-pie, o compilador exibe erros de relocação devido à natureza independente de posição do binário.

Para corrigir isso, o código pode ser modificado da seguinte forma:

global main

extern printf

section .data
message db "Hello www.programicio.com/2",10

section .text
main:
    sub rsp, 8
    lea rdi, [rel message] ; carrega o endereço relativo
    call printf WRT ..plt
    add rsp, 8
    ret

O uso de lea rdi, [rel message] aplica endereçamento relativo ao ponteiro de instrução (RIP), necessário em código compatível com PIE. As referências a variáveis, como números em .data, também seguem essa forma:

mov rsi, [rel num]

O sufixo WRT ..plt após call printf indica que o endereço real da função deve ser buscado na Procedure Linkage Table (PLT), que contém os endereços das funções dinâmicas carregadas em tempo de execução.

Compilação e execução:

nasm -f elf64 hello.asm -o hello.o
gcc hello.o -o hello
./hello
Hello www.programicio.com

Envio de Parâmetros para Funções C/C++

O exemplo a seguir mostra o envio de múltiplos parâmetros para printf:

global main

extern printf

section .data
message db "Name: %s  Age: %u  Company: %s  Salary: %u",10,0
name db "Tom",0
age dq 39
company db "www.programicio.com",0
salary dq 1150

section .text
main:
    sub rsp, 8
    mov rdi, message
    mov rsi, name
    mov rdx, [age]
    mov rcx, company
    mov r8, [salary]
    mov rax, 0
    call printf
    add rsp, 8
    ret

Nesse caso, printf recebe cinco parâmetros:

  • o formato da string (rdi)
  • e quatro valores subsequentes (rsi, rdx, rcx, r8).

Compilação e execução:

nasm -f elf64 hello.asm -o hello.o
gcc hello.o -static -o hello
./hello
Name: Tom  Age: 39  Company: www.programicio.com  Salary: 1150

Resumo

  • O NASM segue o padrão System V ABI para chamadas de função em Linux.
  • Os seis primeiros parâmetros são enviados por registradores; os demais, pela pilha.
  • Valores de ponto flutuante usam os registradores xmm0–xmm7.
  • A pilha deve estar alinhada a 16 bytes antes de qualquer chamada.
  • extern declara funções externas, como printf.
  • É possível escolher entre compilação estática (executável maior) e dinâmica (executável menor).
  • Para compatibilidade com PIE, deve-se usar endereçamento relativo (rel) e chamadas via PLT.
Política de Privacidade

Copyright © www.programicio.com Todos os direitos reservados

É proibida a reprodução do conteúdo desta página sem autorização prévia do autor.

Contato: programicio@gmail.com