Exemple de programme x86-64
J'ai écrit le programme x86 suivant pour m'assurer que je suis les bonnes pratiques pour appeler une fonction, puis pour quitter le système d'exploitation:
.globl _start
_start:
# Calculate 2*3 + 7*9 = 6 + 63 = 69
# The multiplication will be done with a separate function call
# Parameters passed in System V ABI
# The first 6 integer/pointer arguments are passed in:
# %rdi, %rsi, %rdx, %rcx, %r8, and %r9
# The return value is passed in %rax
# multiply(2, 3)
# Part 1 --> Call the parameters
mov $2, %rdi mov $3, %rsi
# Part 2 --> Call the function (`push` return address onto stack and `jmp` to function label)
call multiply
# Part 3 --> Handle the return value from %rax (here we'll just push it to the stack as a test)
push %rax
# multiply(7, 9)
mov $7, %rdi mov $9, %rsi
call multiply
# Add the two together
# Restore from stack onto rdi for the first function
pop %rdi
# The previous value from multiply(7,9) is already in rax, so just add to rbx
add %rax, %rdi
# for the 64-bit calling convention, do syscall instead of int 0x80
# use %rdi instead of %rbx for the exit arg
# use $60 instead of 1 for the exit code movq $60, %rax # use the `_exit` [fast] syscall
# rdi contains out exit code
syscall # make syscall
multiply:
mov %rdi, %rax
imul %rsi, %rax
ret
Ce qui précède suit-il correctement les conventions x86-64? Je sais que c'est probablement aussi basique que possible, mais qu'est-ce qui peut être amélioré ici?
Réponses
Pour élaborer sur certains commentaires que vous avez reçus sur la version SO de cette question, la principale chose qui vous manque est l' alignement de la pile , une exigence des conventions d'appel SysV ABI qui est souvent négligée par les débutants.
L'exigence est (ABI 3.2.2):
La fin de la zone d'argument d'entrée doit être alignée sur une limite de 16 octets (32 ou 64, si
__m256ou__m512est passé sur la pile).
Cela signifie que, à l'instant avant d'exécuter une callinstruction, le pointeur de pile %rspdoit être un multiple de 16. Dans votre cas, vous avez un pushde 8 octets sans un popentre vos deux appels à multiply, donc ils ne peuvent pas tous les deux avoir alignement correct.
Certaines rides sont introduites ici par le fait que votre fonction parent est à la _startplace de mainou une autre fonction appelée par le code C:
Les conditions d'entrée à
_startsont décrites en 3.4 de l'ABI. En particulier, la pile est alignée sur 16 octets au moment où elle_startprend le contrôle. De plus, comme vous ne pouvez pas revenir de_start(il n'y a pas d'adresse de retour sur la pile), vous devez quitter avec un appel système comme vous le faites, et il n'est donc pas nécessaire de sauvegarder des registres pour l'appelant.Pour
mainou pour toute autre fonction, la pile aurait été alignée sur 16 octets avant que votre fonction ne soit appelée, donc les 8 octets supplémentaires pour l'adresse de retour signifient qu'à l'entrée de votre fonction, la pile est maintenant "désalignée", c'est-à-dire la valeur derspest 8 plus ou moins qu'un multiple de 16. (Comme on ne manipulerait normalement la pile que par incréments de 8 octets, ce n'est vraiment que jamais dans deux états possibles, que j'appellerai «aligné» et «désaligné».) Aussi , dans de telles fonctions, vous devez conserver le contenu des registres enregistrés par l'appelé%rbx, %rbp, %r12-r15.
Dans l'état actuel des choses, votre premier appel à multiplya un alignement correct de la pile, mais pas le second. Bien sûr, cela n'a qu'un intérêt académique dans ce cas, car multiplyne fait rien qui nécessite un alignement de pile (il n'utilise même pas du tout la pile), mais c'est une bonne pratique de le faire correctement.
Une façon de résoudre ce problème serait de soustraire 8 octets sub $8, %rspsupplémentaires du pointeur de pile avant le deuxième appel, avec ou (plus efficacement) en utilisant simplement pushn'importe quel registre aléatoire de 64 bits. Mais pourquoi devrions-nous prendre la peine d'utiliser la pile pour enregistrer cette valeur? Nous pourrions simplement le mettre dans l'un des registres enregistrés, disons %rbx, que nous savons multiplydevoir préserver. Normalement, cela nous obligerait à sauvegarder et restaurer le contenu de ce registre, mais puisque nous sommes dans le cas particulier de l'être _start, nous n'avons pas à le faire.
Un commentaire séparé est que vous avez beaucoup d'instructions comme mov $7, %rdioù vous opérez sur des registres 64 bits. Ce serait mieux d'écrire comme mov $7, %edi. Rappelez-vous que chaque écriture dans un registre 32 bits remettra à zéro la moitié supérieure du registre 64 bits correspondant , donc l'effet est le même tant que votre constante est 32 bits non signée et que l'encodage de mov $7, %ediest un octet plus court qu'il ne le fait pas besoin d'un préfixe REX.
Je réviserais donc votre code comme
.globl _start
_start:
# Calculate 2*3 + 7*9 = 6 + 63 = 69
# The multiplication will be done with a separate function call
# Parameters passed in System V ABI
# The first 6 integer/pointer arguments are passed in:
# %rdi, %rsi, %rdx, %rcx, %r8, and %r9
# The return value is passed in %rax
# multiply(2, 3)
# Part 1 --> Load the parameters
mov $2, %edi mov $3, %esi
# Part 2 --> Call the function (`push` return address onto stack and `jmp` to function label)
call multiply
# Part 3 --> Save the return value
mov %rax, %rbx # could also do mov %ebx, %eax if you know the result fits in 32 bits
# multiply(7, 9)
mov $7, %edi mov $9, %esi
call multiply
# Add the two together
add %rbx, %rax
mov %rax, %rdi
# for the 64-bit calling convention, do syscall instead of int 0x80
# use %rdi instead of %rbx for the exit arg
# use $60 instead of 1 for the exit code mov $60, %eax # use the `_exit` [fast] syscall
# rdi contains out exit code
syscall # make syscall
multiply:
mov %rdi, %rax
imul %rsi, %rax
ret
Si vous voulez vous fier au résultat de l' multiplyajustement en 32 bits, vous pouvez remplacer mov %rax, %rbxpar mov %eax, %ebxpour économiser un octet. Et de même, "Ajouter les deux ensemble" pourrait utiliser des instructions 32 bits à la place pour enregistrer deux octets supplémentaires.
Enfin, il y a un point stylistique sur l'opportunité d'utiliser les suffixes de taille d'opérande de syntaxe AT & T, comme addqversus add. Ils sont facultatifs lorsqu'un opérande est un registre, puisque la taille de l'opérande peut être déduite de la taille de ce registre (par exemple 32 bits pour %eax, 64 bits pour %rax, etc.). Ma préférence personnelle est de toujours les utiliser, comme une petite vérification supplémentaire que vous écrivez vraiment ce que vous voulez dire, mais les omettre comme vous l'avez fait (principalement) est également courant et très bien; soyez juste cohérent. Vous avez eu une instance movq $60, %raxoù ce n'était pas nécessaire, donc par souci de cohérence, j'ai omis le suffixe. (Je l'ai également changé %eaxpour les raisons indiquées ci-dessus.)