web-dev-qa-db-fra.com

MIPS: utilisation pertinente pour un pointeur de pile ($ sp) et la pile

Actuellement, j'étudie pour mon organisation informatique à mi-parcours et j'essaie de bien comprendre le pointeur de pile et la pile. Je connais les faits suivants qui entourent le concept:

  • Il suit le principe du premier entré, dernier sorti
  • Et ajouter quelque chose à la pile prend un processus en deux étapes:

    addi $sp, $sp, -4
    sw $s0, 0($sp)
    

Ce qui, selon moi, m'empêche de bien comprendre, c'est que je ne peux pas trouver une situation pertinente et apparente où j'aurais besoin et/ou souhaite garder une trace des données avec un pointeur de pile.

Quelqu'un pourrait-il élaborer sur le concept dans son ensemble et me donner quelques exemples de code utiles?

12
Connor Black

Une utilisation importante de la pile consiste à imbriquer des appels de sous-programme.

Chaque sous-programme peut avoir un ensemble de variables locales à ce sous-programme. Ces variables peuvent être commodément stockées sur une pile dans un cadre de pile. Certaines conventions d'appel passent également des arguments sur la pile.

L'utilisation de sous-programmes signifie également que vous devez garder une trace de l'appelant, c'est-à-dire l'adresse de retour. Certaines architectures ont une pile dédiée à cet effet, tandis que d'autres utilisent implicitement la pile "normale". Par défaut, MIPS utilise uniquement un registre, mais dans les fonctions non-feuilles (c'est-à-dire les fonctions qui appellent d'autres fonctions), l'adresse de retour est remplacée. Par conséquent, vous devez enregistrer la valeur d'origine, généralement sur la pile parmi vos variables locales. Les conventions d'appel peuvent également déclarer que certaines valeurs de registre doivent être préservées entre les appels de fonction, vous pouvez également les enregistrer et les restaurer à l'aide de la pile.

Supposons que vous ayez ce fragment C:

extern void foo();
extern int bar();
int baz()
{
    int x = bar();
    foo();
    return x;
}

L'assemblage MIPS peut alors ressembler à:

addiu $sp, $sp, -8  # allocate 2 words on the stack
sw $ra, 4($sp)      # save $ra in the upper one
jal bar             # this overwrites $ra
sw $v0, ($sp)       # save returned value (x)
jal foo             # this overwrites $ra and possibly $v0
lw $v0, ($sp)       # reload x so we can return it
lw $ra, 4($sp)      # reload $ra so we can return to caller
addiu $sp, $sp, 8   # restore $sp, freeing the allocated space
jr $ra              # return
27
Jester

La convention d'appel MIPS requiert que les quatre premiers paramètres de fonction soient dans les registres a0 à travers a3 et le reste, s'il y en a plus, sur la pile. De plus, cela nécessite également que l'appelant de fonction alloue quatre emplacements sur la pile pour les quatre premiers paramètres, malgré ceux qui sont passés dans les registres.

Donc, si vous souhaitez accéder au paramètre cinq (et à d'autres paramètres), vous devez utiliser sp. Si la fonction appelle à son tour d'autres fonctions et utilise ses paramètres après les appels, elle devra stocker a0 à travers a3 dans ces quatre emplacements de la pile pour éviter qu'ils ne soient perdus/remplacés. Encore une fois, vous utilisez sp pour écrire ces registres dans la pile.

Si votre fonction a des variables locales et ne peut pas toutes les garder dans les registres (comme quand elle ne peut pas garder a0 à travers a3 lorsqu'il appelle d'autres fonctions), il devra utiliser l'espace sur la pile pour ces variables locales, ce qui nécessite à nouveau l'utilisation de sp.

Par exemple, si vous aviez ceci:

int tst5(int x1, int x2, int x3, int x4, int x5)
{
  return x1 + x2 + x3 + x4 + x5;
}

son démontage serait quelque chose comme:

tst5:
        lw      $2,16($sp) # r2 = x5; 4 slots are skipped
        addu    $4,$4,$5   # x1 += x2
        addu    $4,$4,$6   # x1 += x3
        addu    $4,$4,$7   # x1 += x4
        j       $31        # return
        addu    $2,$4,$2   # r2 += x1

Voir, sp est utilisé pour accéder à x5.

Et puis si vous avez du code quelque chose comme ceci:

int binary(int a, int b)
{
  return a + b;
}

void stk(void)
{
  binary(binary(binary(1, 2), binary(3, 4)), binary(binary(5, 6), binary(7, 8)));
}

voici à quoi cela ressemble lors du démontage après compilation:

binary:
        j       $31                     # return
        addu    $2,$4,$5                # r2 = a + b

stk:
        subu    $sp,$sp,32              # allocate space for local vars & 4 slots
        li      $4,0x00000001           # 1
        li      $5,0x00000002           # 2
        sw      $31,24($sp)             # store return address on stack
        sw      $17,20($sp)             # preserve r17 on stack
        jal     binary                  # call binary(1,2)
        sw      $16,16($sp)             # preserve r16 on stack

        li      $4,0x00000003           # 3
        li      $5,0x00000004           # 4
        jal     binary                  # call binary(3,4)
        move    $16,$2                  # r16 = binary(1,2)

        move    $4,$16                  # r4 = binary(1,2)
        jal     binary                  # call binary(binary(1,2), binary(3,4))
        move    $5,$2                   # r5 = binary(3,4)

        li      $4,0x00000005           # 5
        li      $5,0x00000006           # 6
        jal     binary                  # call binary(5,6)
        move    $17,$2                  # r17 = binary(binary(1,2), binary(3,4))

        li      $4,0x00000007           # 7
        li      $5,0x00000008           # 8
        jal     binary                  # call binary(7,8)
        move    $16,$2                  # r16 = binary(5,6)

        move    $4,$16                  # r4 = binary(5,6)
        jal     binary                  # call binary(binary(5,6), binary(7,8))
        move    $5,$2                   # r5 = binary(7,8)

        move    $4,$17                  # r4 = binary(binary(1,2), binary(3,4))
        jal     binary                  # call binary(binary(binary(1,2), binary(3,4)), binary(binary(5,6), binary(7,8)))
        move    $5,$2                   # r5 = binary(binary(5,6), binary(7,8))

        lw      $31,24($sp)             # restore return address from stack
        lw      $17,20($sp)             # restore r17 from stack
        lw      $16,16($sp)             # restore r16 from stack
        addu    $sp,$sp,32              # remove local vars and 4 slots
        j       $31                     # return
        nop

J'espère avoir annoté le code sans faire d'erreur.

Notez donc que le compilateur choisit d'utiliser r16 et r17 dans la fonction mais les conserve dans la pile. Puisque la fonction en appelle une autre, elle doit également conserver son adresse de retour sur la pile au lieu de simplement la conserver dans r31.

PS N'oubliez pas que toutes les instructions de branchement/saut sur MIPS exécutent effectivement l'instruction immédiatement suivante avant de transférer réellement le contrôle vers un nouvel emplacement. Cela pourrait prêter à confusion.

7
Alexey Frunze