web-dev-qa-db-fra.com

Quelle est la fonction des instructions Push / pop utilisées sur les registres de l'assemblage x86?

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.

  • Comment pouvez-vous pousser un registre? Où est-il poussé? Pourquoi est-ce nécessaire?
  • Cela se résume-t-il à une seule instruction du processeur ou est-ce plus complexe?
72
Ars emble

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
118
Linus Kleen

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.

  1. Stocke la valeur transmise à l'adresse actuelle de ESP register.
  2. Décrémentez le registre ESP à la taille de la valeur transmise.
37
Madhur Ahuja

Où est-il poussé?

esp - 4. Plus précisément:

  • esp se soustrait de 4
  • la valeur est poussée à esp

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é:

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.

16
gowrath

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:

  1. Les registres de la CPU et aussi des constantes.
  2. Adresses de retour pour fonctions ou procédures
  3. Fonctions/procédures variables in/out
  4. Fonctions/procédures variables locales.
11
GJ.