En lisant à propos de l'assembleur, je rencontre souvent des gens qui écrivent qu'ils Push un certain registre du processeur et pop plus tard pour restaurer son état précédent.
pousser une valeur (pas nécessairement stockée dans un registre) signifie l'écrire dans la pile.
popping signifie restaurer tout ce qui se trouve en haut de la pile dans un registre. Ce sont des instructions de base:
Push 0xdeadbeef ; Push a value to the stack
pop eax ; eax is now 0xdeadbeef
; swap contents of registers
Push eax
mov eax, ebx
pop ebx
Voici comment vous poussez un registre. Je suppose que nous parlons de x86.
Push ebx
Push eax
Il est poussé sur la pile. La valeur de ESP
register est décrémentée à la taille de la valeur transmise à mesure que la pile croît vers le bas dans les systèmes x86.
Il est nécessaire de préserver les valeurs. L'usage général est
Push eax ; preserve the value of eax
call some_method ; some method is called which will put return value in eax
mov edx, eax ; move the return value to edx
pop eax ; restore original eax
Un Push
est une instruction unique en x86, qui fait deux choses en interne.
ESP
register.ESP
à la taille de la valeur transmise.Où est-il poussé?
esp - 4
. Plus précisément:
esp
se soustrait de 4esp
pop
l'inverse.
L’ABI System V dit à Linux de faire en sorte que rsp
pointe sur un emplacement de pile judicieux lorsque le programme commence à s’exécuter: Quel est l’état du registre par défaut lors du lancement du programme (asm, linux)? qui est ce que vous devriez habituellement utiliser.
Comment pouvez-vous pousser un registre?
Minimal GNU Exemple GAS:
.data
/* .long takes 4 bytes each. */
val1:
/* Store bytes 0x 01 00 00 00 here. */
.long 1
val2:
/* 0x 02 00 00 00 */
.long 2
.text
/* Make esp point to the address of val2.
* Unusual, but totally possible. */
mov $val2, %esp
/* eax = 3 */
mov $3, %ea
Push %eax
/*
Outcome:
- esp == val1
- val1 == 3
esp was changed to point to val1,
and then val1 was modified.
*/
pop %ebx
/*
Outcome:
- esp == &val2
- ebx == 3
Inverses Push: ebx gets the value of val1 (first)
and then esp is increased back to point to val2.
*/
Le ci-dessus sur GitHub avec des assertions exécutables .
Pourquoi est-ce nécessaire?
Il est vrai que ces instructions pourraient être facilement implémentées via mov
, add
et sub
.
Ils ont raison d’exister, c’est que ces combinaisons d’instructions sont si fréquentes que Intel a décidé de nous les fournir.
La raison pour laquelle ces combinaisons sont si fréquentes est qu’elles facilitent la sauvegarde et la restauration temporaires des valeurs des registres afin qu’elles ne soient pas écrasées.
Pour comprendre le problème, essayez de compiler du code C à la main.
Une difficulté majeure consiste à décider où chaque variable sera stockée.
Idéalement, toutes les variables devraient tenir dans des registres, ce qui représente la mémoire la plus rapide à accéder (actuellement environ 100x plus rapide que la RAM).
Mais bien sûr, nous pouvons facilement avoir plus de variables que de registres, spécialement pour les arguments de fonctions imbriquées, la seule solution est donc d’écrire en mémoire.
Nous pourrions écrire dans n’importe quelle adresse mémoire, mais puisque les variables locales et les arguments des appels de fonction et les retours s’inscrivent dans un modèle de pile Nice, ce qui empêche mémoire fragmentée , c’est le meilleur moyen de Faites avec. Comparez cela avec la folie d'écrire un allocateur de tas.
Ensuite, nous laissons les compilateurs optimiser l’allocation des registres pour nous, car c’est NP complet et l’une des parties les plus difficiles de l’écriture d’un compilateur. Ce problème s’appelle register allocation , et il est isomorphe à graph coloring .
Lorsque l'allocateur du compilateur est obligé de stocker des éléments en mémoire au lieu de simplement des registres, on parle de spill.
Est-ce que cela se résume à une seule instruction de processeur ou est-ce plus complexe?
Tout ce que nous savons, c’est que Intel documente une instruction Push
et une instruction pop
, de sorte qu’elles ne constituent qu’une instruction dans ce sens.
En interne, il pourrait être étendu à plusieurs microcodes, l’un pour modifier esp
et l’autre pour effectuer l’IO mémoire, et prendre plusieurs cycles.
Mais il est également possible qu'un seul Push
soit plus rapide qu'une combinaison équivalente d'autres instructions, car il est plus spécifique.
Ceci est principalement non documenté:
Push
et pop
effectuent une micro-opération.Les registres Pushing et Popping sont dans les coulisses équivalents à ceci:
Push reg <= same as => sub $8,%rsp # subtract 8 from rsp
mov reg,(%rsp) # store, using rsp as the address
pop reg <= same as=> mov (%rsp),reg # load, using rsp as the address
add $8,%rsp # add 8 to the rsp
Notez qu'il s'agit de la syntaxe x86-64 At & t.
Utilisé par paire, cela vous permet de sauvegarder un registre sur la pile et de le restaurer plus tard. Il y a aussi d'autres utilisations.
Presque tous les processeurs utilisent la pile. La pile de programmes est la technique LIFO avec le matériel pris en charge, géré.
La pile est la quantité de mémoire programme (RAM) normalement allouée au sommet du segment de mémoire de la CPU et qui grossit (dans l'instruction Push, le pointeur de pile est diminué) dans le sens opposé. Un terme standard pour insérer dans la pile est . Appuyez sur et pour retirer de la pile est [~ # ~] pop [~ # ~] .
La pile est gérée via le registre de processeur prévu pour la pile, également appelé pointeur de pile. Ainsi, lorsque le processeur exécute [~ # ~] pop [~ # ~] ou Poussez le pointeur de pile chargera/enregistrera un registre ou une constante dans la mémoire de pile et le pointeur de pile sera automatiquement réduit ou augmenté en fonction du nombre de mots insérés ou insérés ( de) pile.
Via les instructions de l'assembleur, nous pouvons stocker pour empiler: