Powrót do strony głównej

Zwrot struktur x86-64: rejestry i MEMORY ABI

Artykuł analizuje mechanizmy zwrotu typów fundamentalnych i struktur z funkcji w x86-64 według System V ABI. Opisano reguły klasyfikacji INTEGER i MEMORY, użycie rejestrów rax/rdx i ukrytego argumentu rdi. Podano przykłady asemblera bez optymalizacji.

x86-64: jak zwracać struktury przez rax, rdx i rdi
Advertisement 728x90

Zwracanie wartości z funkcji w x86-64 według System V ABI

Kompilator x86-64 zgodny z System V ABI wykorzystuje rejestry rax i rdx do zwracania pierwszych dwóch wartości typu integer. Dla struktur stosowane są zasady klasyfikacji: INTEGER dla małych rozmiarów, MEMORY dla dużych. Oznacza to, czy wartość jest przekazywana bezpośrednio czy przez ukryty wskaźnik w rejestrze rdi.

Rozważmy podstawowy przypadek zwracania int:

one_plus_one():
  mov DWORD PTR [rbp-4], 2
  mov eax, DWORD PTR [rbp-4]
  ret

Wartość jest ładowana do eax (dolnych 32 bitów rax). Podobnie dla typów 64-bitowych używany jest pełen rax.

Google AdInline article slot

Struktury do 16 bajtów: zwracanie przez rejestry

Małe struktury umieszczane są w rax i rdx. Przykład struktury o rozmiarze 16 bajtów:

struct nums {
    std::int64_t first{};
    std::int64_t second{};
};
nums construct() {
    nums ret{10, 120};
    return ret;
}

Wygenerowany kod:

construct():
  mov QWORD PTR [rbp-16], 10
  mov QWORD PTR [rbp-8], 120
  mov rax, QWORD PTR [rbp-16]
  mov rdx, QWORD PTR [rbp-8]
  ret

Pola są rozprowadzane kolejno: first w rax, second w rdx. Kolejność w pamięci odpowiada little-endian, a stos rośnie w dół.

Google AdInline article slot
  • Klasyfikacja INTEGER: Do dwóch pól 64-bitowych można umieścić dane w rejestrach.
  • Wyrównanie: Pola są wyrównane do 8 bajtów.
  • Ograniczenie: Łączny rozmiar ≤ 16 bajtów dla bezpośredniego zwracania.

Duże struktury: ukryty argument w rdi

Struktury >16 bajtów klasyfikowane są jako MEMORY. Kod wywołujący alokuje pamięć i przekazuje adres w rdi jako pierwszy (ukryty) argument.

Przykład struktury 32 bajtów:

struct many_nums {
    std::int64_t first{};
    std::int64_t second{};
    std::int64_t third{};
    std::int64_t fourth{};
};
many_nums construct_scary() {
    many_nums temp{10, 20, 30, 40};
    return temp;
}

W construct_scary:

Google AdInline article slot
construct_scary():
  mov QWORD PTR [rbp-8], rdi
  mov rax, QWORD PTR [rbp-8]
  mov QWORD PTR [rax], 10
  mov QWORD PTR [rax+8], 20
  mov QWORD PTR [rax+16], 30
  mov QWORD PTR [rax+24], 40
  ret

Powtarzające się mov rax, [rbp-8] — artefakt -O0. W kodzie zoptymalizowanym używa się rdi bezpośrednio.

W main:

main:
  sub rsp, 32
  lea rax, [rbp-32]
  mov rdi, rax
  call construct_scary
  ret
  • Alokacja pamięci: sub rsp, size lub na stosie/heapie.
  • Przekazanie adresu: W rdi przed wywołaniem.
  • Klasyfikacja MEMORY: Według System V ABI dla typów >16 bajtów lub nie-INTEGER.

Zasady klasyfikacji zwracanych wartości

System V ABI definiuje klasy:

  • INTEGER: Do 16 bajtów, skalarne lub agregowane pola ≤2×64 bitów → rax/rdx.
  • MEMORY: >16 bajtów, zbiory SSE >2×128 bitów, nie-INTEGER → ukryty rdi.
  • SSE: Liczby zmiennoprzecinkowe do 128 bitów → xmm0 (nie omawiane tutaj).

Kompilator (GCC/Clang) generuje kod zgodnie z tymi zasadami. Do debugowania użyj -O0, aby zobaczyć kroki pośrednie.

Co ważne

  • Rejestry rax/rdx do zwracania wartości integer ≤16 bajtów.
  • Ukryty argument rdi dla klasy MEMORY: wywołujący alokuje pamięć.
  • System V ABI jednolity dla Linuxa i macOS.
  • Optymalizacje (-O2+) minimalizują zbędne mov.
  • Sprawdzaj za pomocą objdump -d lub Godbolt do weryfikacji.

— Editorial Team

Advertisement 728x90

Czytaj dalej