Passagem de Estruturas entre Assembly e C/C++
As estruturas (structs) são uma forma prática de organizar dados e são amplamente utilizadas como parâmetros ou valores de retorno de funções em C/C++. A seguir, veremos como é possível transferir estruturas entre código C e Assembly — tanto enviando uma estrutura de C para Assembly quanto retornando uma estrutura de Assembly para C.
Recebendo Estruturas em Assembly a partir de uma Função C
Considere o seguinte arquivo person.c:
struct person { char* name; int age; };
struct person create_person()
{
struct person tom = { .name = "Tom", .age = 39 };
return tom;
}Aqui, definimos a estrutura person com dois campos — name (nome) e age (idade).
A função create_person() cria uma instância dessa estrutura e a retorna.
Compilação do arquivo para objeto:
gcc -c person.c -o person.o
Agora, vamos analisar o código Assembly gerado por meio de um disassembly:
objdump -d -M intel person.o
Saída simplificada:
0000000000000000 <create_person>: 0: f3 0f 1e fa endbr64 4: 55 push rbp 5: 48 89 e5 mov rbp,rsp 8: 48 8d 05 00 00 00 00 lea rax,[rip+0x0] f: 48 89 45 f0 mov QWORD PTR [rbp-0x10],rax 13: c7 45 f8 27 00 00 00 mov DWORD PTR [rbp-0x8],0x27 1a: 48 8b 45 f0 mov rax,QWORD PTR [rbp-0x10] 1e: 48 8b 55 f8 mov rdx,QWORD PTR [rbp-0x8] 22: 5d pop rbp 23: c3 ret
A função create_person retorna os campos da estrutura por meio dos registradores RAX e RDX:
RAX→ camponameRDX→ campoage
Esse comportamento segue a convenção do System V ABI para funções que retornam dois valores.
Agora, podemos recuperar esses valores em Assembly. Crie o arquivo app.asm:
global main
extern printf
extern create_person
section .data
name db "Name: %s", 10, 0
age db "Age: %d", 10, 0
section .text
main:
sub rsp, 24 ; 16 bytes para variáveis + 8 para alinhamento
call create_person ; retorna person em RAX (name) e RDX (age)
mov [rsp+16], rax ; salva name na pilha
mov [rsp+8], rdx ; salva age na pilha
mov rdi, name
mov rsi, [rsp+16]
call printf
mov rdi, age
mov rsi, [rsp+8]
call printf
add rsp, 24
retApós a chamada de create_person, os valores retornados são armazenados temporariamente na pilha e, em seguida, usados para exibição com printf.
Compilação e execução:
nasm -f elf64 app.asm -o app.o gcc person.c app.o -static -o app ./app
Saída:
Name: Tom Age: 39
Estruturas Mais Complexas
Quando uma estrutura contém mais de dois campos simples, seus dados passam a ser retornados via pilha, e não apenas por registradores.
Modifique o arquivo person.c:
struct person { int id; char* name; int age; };
struct person create_person()
{
struct person tom = { .id = 112, .name = "Tom", .age = 39 };
return tom;
}Após compilar e inspecionar com objdump, obtemos algo como:
0000000000000000 <create_person>: 8: 48 89 7d d8 mov QWORD PTR [rbp-0x28],rdi c: c7 45 e0 70 00 00 00 mov DWORD PTR [rbp-0x20],0x70 13: 48 8d 05 00 00 00 00 lea rax,[rip+0x0] 1a: 48 89 45 e8 mov QWORD PTR [rbp-0x18],rax 1e: c7 45 f0 27 00 00 00 mov DWORD PTR [rbp-0x10],0x27 25: 48 8b 4d d8 mov rcx,QWORD PTR [rbp-0x28] 29: 48 8b 45 e0 mov rax,QWORD PTR [rbp-0x20] 2d: 48 8b 55 e8 mov rdx,QWORD PTR [rbp-0x18] 31: 48 89 01 mov QWORD PTR [rcx],rax 34: 48 89 51 08 mov QWORD PTR [rcx+8],rdx 38: 48 8b 45 f0 mov rax,QWORD PTR [rbp-0x10] 3c: 48 89 41 10 mov QWORD PTR [rcx+16],rax
Nesse caso:
- O ponteiro de destino da estrutura é passado em RDI.
- A função preenche os campos no endereço recebido, na ordem dos campos definidos no
struct.
Para receber a estrutura em Assembly, podemos escrever app.asm assim:
global main
extern printf
extern create_person
section .data
name db "Name: %s", 10, 0
age db "Age: %d", 10, 0
id db "Id: %d", 10, 0
section .text
main:
sub rsp, 24 ; 24 bytes para os campos
mov rdi, rsp ; endereço de destino
call create_person ; função preenche a estrutura
mov rdi, id
mov rsi, [rsp]
call printf
mov rdi, name
mov rsi, [rsp+8]
call printf
mov rdi, age
mov rsi, [rsp+16]
call printf
add rsp, 24
retOs três campos (id, name, age) são armazenados sequencialmente na pilha.
A função create_person grava os dados diretamente nesse endereço.
Compilação e execução:
nasm -f elf64 hello.asm -o hello.o gcc person.c hello.o -static -o hello ./hello
Saída:
Id: 112 Name: Tom Age: 39
Enviando Estruturas de Assembly para C
Para enviar uma estrutura do Assembly para uma função em C, os campos são normalmente passados pela pilha.
Exemplo de person.c:
#include <stdio.h>
struct person { int id; char* name; int age; };
void print_person(struct person bob)
{
printf("Id: %d\n", bob.id);
printf("Name: %s\n", bob.name);
printf("Age: %d\n", bob.age);
}E o arquivo app.asm correspondente:
global main
extern print_person
section .data
name db "Bob", 0
age dq 43
id dq 111
section .text
main:
sub rsp, 24
mov rax, [id]
mov [rsp], rax ; id
lea rax, [name]
mov [rsp+8], rax ; name
mov rax, [age]
mov [rsp+16], rax ; age
call print_person
add rsp, 24
retOs três campos são empilhados na mesma ordem em que aparecem na estrutura.
A função C os interpreta corretamente ao receber o parâmetro struct person bob.
Compilação e execução:
nasm -f elf64 hello.asm -o hello.o gcc person.c hello.o -static -o hello ./hello
Saída:
Id: 111 Name: Bob Age: 43
Passagem de Ponteiro para Estrutura
Também é possível passar à função C um ponteiro para uma estrutura, em vez da estrutura completa. Nesse caso, a função recebe o endereço da estrutura como argumento.
Exemplo person.c:
#include <stdio.h>
struct person { int id; char* name; int age; };
void print_person(struct person* bob)
{
printf("Id: %d\n", bob->id);
printf("Name: %s\n", bob->name);
printf("Age: %d\n", bob->age);
}Código Assembly correspondente:
global main
extern print_person
section .data
name db "Sam", 0
age dq 28
id dq 110
section .text
main:
sub rsp, 24
mov rax, [id]
mov [rsp], rax
lea rax, [name]
mov [rsp+8], rax
mov rax, [age]
mov [rsp+16], rax
mov rdi, rsp ; endereço da estrutura
call print_person
add rsp, 24
retNesse caso, o endereço da estrutura (armazenada temporariamente na pilha) é passado em RDI, o registrador usado para o primeiro parâmetro de funções no Linux (System V ABI).
Estrutura em Memória Estática
Também é possível armazenar os campos da estrutura diretamente em memória estática (na seção .data), evitando a alocação na pilha.
global main
extern print_person
section .data
name db "Sam", 0
id dq 110
namePtr dq name
age dq 28
section .text
main:
sub rsp, 8
mov rdi, id ; endereço do primeiro campo (início da estrutura)
call print_person
add rsp, 8
retNesse exemplo, RDI recebe o endereço de id, que é seguido pelos demais campos (namePtr e age) em sequência de 8 bytes cada.
Essa abordagem economiza instruções e evita movimentação desnecessária de dados para a pilha, embora exija cuidado com alinhamento e tamanho dos campos.
Resumo
- Estruturas pequenas (até dois campos simples) podem ser retornadas por registradores (
RAX,RDX). - Estruturas maiores são retornadas por meio de um ponteiro, cujo endereço é passado em RDI.
- Ao enviar estruturas de Assembly para C, os campos são empilhados na mesma ordem definida na estrutura.
- Também é possível passar apenas o endereço da estrutura (ponteiro).
- Códigos que utilizam
.datapodem economizar instruções e memória de pilha, desde que respeitem o alinhamento.