J'essaie donc d'apprendre un peu l'assemblage, car j'en ai besoin pour le cours d'architecture informatique. J'ai écrit quelques programmes, comme l'impression de la séquence de Fibonacci.
J'ai reconnu que chaque fois que j'écris une fonction, j'utilise ces 3 lignes (comme je l'ai appris en comparant le code Assembly généré à partir de gcc
à son équivalent C
:)
pushq %rbp
movq %rsp, %rbp
subq $16, %rsp
J'ai 2 questions à ce sujet:
%rbp
? N'est-il pas plus simple d'utiliser %rsp
, lorsque son contenu est déplacé vers %rbp
sur la 2e ligne?%rsp
? Je veux dire que ce n'est pas toujours 16
, quand j’étais printf
comme 7 ou 8 variables, je soustrais donc 24
ou 28
.J'utilise Manjaro 64 bits sur une machine virtuelle (4 Go de RAM), processeur Intel 64 bits
rbp
est le pointeur du cadre sur x86_64. Dans votre code généré, il obtient un instantané du pointeur de la pile (rsp
). Ainsi, lorsque des ajustements sont apportés à rsp
(c’est-à-dire réservant de l’espace pour des variables locales ou des valeurs Push
ing sur à la pile), les variables locales et les paramètres de fonction sont toujours accessibles à partir d’un décalage constant de rbp
.
De nombreux compilateurs proposent l’omission du pointeur de trame comme option d’optimisation; cela créera les variables d'accès au code d'assemblage générées relatives à rsp
à la place et libérera rbp
en tant qu'un autre registre à usage général à utiliser dans les fonctions.
Dans le cas de GCC, que je suppose que vous utilisez à partir de la syntaxe de l'assembleur AT & T, ce commutateur est -fomit-frame-pointer
. Essayez de compiler votre code avec ce commutateur et voyez quel code d'assemblage vous obtenez. Vous remarquerez probablement que lors de l'accès à des valeurs relatives à rsp
au lieu de rbp
, le décalage par rapport au pointeur varie dans l'ensemble de la fonction.
Linux utilise l'architecture System V ABI pour x86-64 (AMD64); voir System V ABI sur OSDev Wiki pour plus de détails.
Cela signifie que la pile se développe ; les petites adresses sont "plus haut" dans la pile. Les fonctions C typiques sont compilées pour
pushq %rbp ; Save address of previous stack frame
movq %rsp, %rbp ; Address of current stack frame
subq $16, %rsp ; Reserve 16 bytes for local variables
; ... function ...
movq %rbp, %rsp ; \ equivalent to the
popq %rbp ; / 'leave' instruction
ret
La quantité de mémoire réservée aux variables locales est toujours un multiple de 16 octets, afin que la pile reste alignée sur 16 octets. Si aucun espace de pile n'est requis pour les variables locales, il n'y a pas de subq $16, %rsp
ou instruction similaire.
(Notez que l'adresse de retour et le précédent %rbp
poussés vers la pile ont une taille de 8 octets, soit 16 octets au total.)
Tandis que %rbp
pointe sur le cadre de pile actuel, %rsp
pointe vers le haut de la pile. Parce que le compilateur connaît la différence entre %rbp
et %rsp
_ à n'importe quel moment de la fonction, il est libre d'utiliser l'une ou l'autre comme base pour les variables locales.
Un cadre de pile est simplement le terrain de jeu de la fonction locale: la région de pile utilisée par la fonction actuelle.
Les versions actuelles de GCC désactivent le cadre de la pile chaque fois que des optimisations sont utilisées. Cela a du sens, car pour les programmes écrits en C, les cadres de pile sont les plus utiles pour le débogage, mais pas grand-chose. (Vous pouvez utiliser, par exemple, -O2 -fno-omit-frame-pointer
pour conserver les images de la pile tout en permettant les optimisations sinon.)
Bien que la même ABI s'applique à tous les fichiers binaires, quelle que soit la langue dans laquelle ils sont écrits, certaines autres langues ont besoin de cadres de pile pour être "déroulés" (par exemple, pour "lever des exceptions" à un appelant ancêtre de la fonction actuelle); c.-à-d. pour "dérouler" des trames de pile qu'une ou plusieurs fonctions peuvent être abandonnées et contrôlées par une fonction ancêtre, sans laisser de trucs inutiles sur la pile.
Lorsque les cadres de pile sont omis - -fomit-frame-pointer
pour GCC -, l’implémentation de la fonction change essentiellement en
subq $8, %rsp ; Re-align stack frame, and
; reserve memory for local variables
; ... function ...
addq $8, %rsp
ret
Parce qu'il n'y a pas de frame de pile (%rbp
est utilisé à d’autres fins, et sa valeur n’est jamais transmise à la pile), chaque appel de fonction pousse uniquement l’adresse de retour dans la pile, ce qui correspond à une quantité de 8 octets; nous devons donc soustraire 8 de %rsp
pour le conserver comme multiple de 16. (En général, la valeur soustraite et ajoutée à %rsp
est un multiple impair de 8.)
Les paramètres de fonction sont généralement passés dans des registres. Voir le lien ABI au début de cette réponse pour plus de détails, mais en bref, les types et pointeurs intégraux sont passés dans les registres %rdi
, %rsi
, %rdx
, %rcx
, %r8
, et %r9
, avec des arguments en virgule flottante dans le %xmm0
à %xmm7
registres.
Dans certains cas, vous verrez rep ret
au lieu de rep
. Ne soyez pas confus: le rep ret
signifie exactement la même chose que ret
; le préfixe rep
, bien que normalement utilisé avec des instructions de chaîne (instructions répétées), ne fait rien lorsqu'il est appliqué à l'instruction ret
. C'est simplement que certains prédicteurs de branche de processeurs AMD n'aiment pas sauter à une instruction ret
, et la solution de contournement recommandée consiste à utiliser un rep ret
il y a la place.
Enfin, j'ai omis le zone rouge au-dessus du sommet de la pile (les 128 octets aux adresses inférieures à %rsp
). En effet, cela n’est pas vraiment utile pour les fonctions classiques: dans le cas normal d’une pile have-frame-frame, vous voulez que vos données locales se trouvent dans la pile afin de permettre le débogage. Dans le cas omit-stack-frame, les exigences d’alignement de la pile signifient déjà que nous devons soustraire 8 de %rsp
, donc inclure la mémoire nécessaire aux variables locales dans cette soustraction ne coûte rien.