Comment x86-64 retourne les valeurs selon l'ABI System V
Le compilateur x86-64 sous l'ABI System V utilise les registres rax et rdx pour renvoyer les deux premières valeurs entières. Pour les structures, des règles de classification s'appliquent : INTEGER pour les petites tailles, MEMORY pour les plus grandes. Cela détermine si la valeur est retournée directement ou via un pointeur caché passé dans rdi.
Examinons le cas de base du retour d'un int :
one_plus_one():
mov DWORD PTR [rbp-4], 2
mov eax, DWORD PTR [rbp-4]
ret
La valeur est chargée dans eax (les 32 bits inférieurs de rax). De même, pour les types 64 bits, l'ensemble du registre rax est utilisé.
Structures petites jusqu'à 16 octets : retour par registres
Les petites structures sont renvoyées directement dans rax et rdx. Prenons une structure de 16 octets :
struct nums {
std::int64_t first{};
std::int64_t second{};
};
nums construct() {
nums ret{10, 120};
return ret;
}
Assemblage généré :
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
Les champs sont disposés séquentiellement : first va dans rax, second dans rdx. La disposition mémoire suit l'ordre little-endian, avec la pile qui grandit vers le bas.
- Classification INTEGER : jusqu’à deux champs de 64 bits tiennent dans les registres.
- Alignement : les champs sont alignés sur des limites de 8 octets.
- Limite : la taille totale doit être ≤16 octets pour un retour direct.
Structures grandes : argument caché dans rdi
Les structures de plus de 16 octets sont classées comme MEMORY. L'appelant alloue la mémoire et passe son adresse dans rdi comme premier argument caché.
Exemple : une structure de 32 octets :
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;
}
Dans construct_scary :
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
L'instruction mov rax, [rbp-8] répétée est un artefact de -O0. Le code optimisé utilise directement rdi.
Dans main :
main:
sub rsp, 32
lea rax, [rbp-32]
mov rdi, rax
call construct_scary
ret
- Allocation mémoire : effectuée via
sub rsp, sizeou sur le tas/la pile. - Passage d’adresse : l’adresse est passée dans
rdiavant l’appel. - Classification MEMORY : utilisée par l’ABI System V pour les types >16 octets ou non-INTEGER.
Règles de classification des valeurs de retour
L’ABI System V définit trois catégories :
- INTEGER : jusqu’à 16 octets, champs scalaires ou agrégés ≤2×64 bits → retournés dans
rax/rdx. - MEMORY : >16 octets, types SSE >2×128 bits, ou non-INTEGER → argument caché
rdi. - SSE : valeurs flottantes jusqu’à 128 bits → retournées dans
xmm0(non abordé ici).
Les compilateurs comme GCC et Clang génèrent du code selon ces règles. Utilisez -O0 en débogage pour voir clairement les étapes intermédiaires.
Points clés à retenir
- Utilisez
rax/rdxpour les retours entiers jusqu’à 16 octets. - Pour les types classifiés
MEMORY, l’appelant fournit la mémoire viardi. - L’ABI System V garantit un comportement cohérent sous Linux et macOS.
- Les optimisations (
-O2+) éliminent les instructionsmovinutiles. - Vérifiez le code généré avec
objdump -dou Godbolt Compiler Explorer.
— Editorial Team
Aucun commentaire pour le moment.