web-dev-qa-db-fra.com

Quels registres sont conservés via un appel de fonction Linux x86-64

Je crois que je comprends comment le linux x86-64 ABI utilise des registres et une pile pour passer des paramètres à une fonction (cf. discussion ABI précédente ). Ce qui me dérange, c'est si/quels registres devraient être conservés dans un appel de fonction. Autrement dit, quels registres sont garantis pour ne pas être encombrés?

37
boneheadgeek

Voici le tableau complet des registres et leur utilisation dans la documentation [ PDF Link ]:

table from docs

r12, r13, r14, r15, rbx, rsp, rbp sont les registres sauvegardés par l'appelé - ils ont un "Oui" dans la colonne "Préservé entre les appels de fonction".

64
Carl Norum

L'ABI spécifie ce qu'un logiciel conforme aux normes est autorisé à attendre. Il est écrit principalement pour les auteurs de compilateurs, de linkers et d'autres logiciels de traitement de langage. Ces auteurs souhaitent que leur compilateur produise du code qui fonctionnera correctement avec du code compilé par le même (ou un autre) compilateur. Ils doivent tous accepter un ensemble de règles: comment les arguments formels des fonctions sont-ils passés de l'appelant à l'appelé, comment les valeurs de retour de fonction sont-elles renvoyées de l'appelé à l'appelant, quels registres sont conservés/rayés/non définis à travers la limite de l'appel, et ainsi de suite sur.

Par exemple, une règle stipule que le code d'assembly généré pour une fonction doit enregistrer la valeur d'un registre conservé avant de modifier la valeur et que le code doit restaurer la valeur enregistrée avant de revenir à son appelant. Pour un registre de travail, le code généré n'est pas requis pour enregistrer et restaurer la valeur du registre; il peut le faire s'il le souhaite, mais le logiciel conforme aux normes n'est pas autorisé à dépendre de ce comportement (s'il le fait, il ne s'agit pas d'un logiciel conforme aux normes).

Si vous écrivez du code Assembly, vous êtes responsable de jouer selon ces mêmes règles (vous jouez le rôle du compilateur). Autrement dit, si votre code modifie un registre préservé de l'appelé, vous êtes responsable de l'insertion d'instructions qui enregistrent et restaurent la valeur de registre d'origine. Si votre code Assembly appelle une fonction externe, votre code doit passer des arguments de la manière conforme aux normes, et cela peut dépendre du fait que, lorsque l'appelé revient, les valeurs de registre préservées sont en fait préservées.

Les règles définissent comment les logiciels conformes aux normes peuvent s'entendre. Cependant, il est parfaitement légal d'écrire (ou de générer) du code qui ne pas respecte ces règles! Les compilateurs le font tout le temps, car ils savent que les règles n'ont pas besoin d'être suivies dans certaines circonstances.

Par exemple, considérons une fonction C nommée foo qui est déclarée comme suit et n'a jamais pris son adresse:

static foo(int x);

Au moment de la compilation, le compilateur est certain à 100% que cette fonction ne peut être appelée que par un autre code du ou des fichiers en cours de compilation. La fonction foo ne peut jamais être appelée par autre chose, étant donné la définition de ce que signifie être statique. Parce que le compilateur connaît tous les appelants de foo au moment de la compilation, le compilateur est libre d'utiliser la séquence d'appel qu'il veut (jusqu'à et y compris ne pas faire d'appel du tout, c'est-à-dire en insérant le code pour foo dans les appelants de foo.

En tant qu'auteur du code Assembly, vous pouvez également le faire. C'est-à-dire que vous pouvez implémenter un "accord privé" entre deux routines ou plus, tant que cet accord n'interfère pas ou ne viole pas les attentes des logiciels conformes aux normes.

4
Jeff N

Approche expérimentale: démonter le code GCC

Surtout pour le plaisir, mais aussi pour vérifier rapidement que vous avez bien compris l'ABI.

Essayons d'encombrer tous les registres avec l'assemblage en ligne pour forcer GCC à les enregistrer et les restaurer:

principal c

#include <inttypes.h>

uint64_t inc(uint64_t i) {
    __asm__ __volatile__(
        ""
        : "+m" (i)
        :
        : "rax",
          "rbx",
          "rcx",
          "rdx",
          "rsi",
          "rdi",
          "rbp",
          "rsp",
          "r8",
          "r9",
          "r10",
          "r11",
          "r12",
          "r13",
          "r14",
          "r15",
          "ymm0",
          "ymm1",
          "ymm2",
          "ymm3",
          "ymm4",
          "ymm5",
          "ymm6",
          "ymm7",
          "ymm8",
          "ymm9",
          "ymm10",
          "ymm11",
          "ymm12",
          "ymm13",
          "ymm14",
          "ymm15"
    );
    return i + 1;
}

int main(int argc, char **argv) {
    (void)argv;
    return inc(argc);
}

GitHub en amont .

Compilez et démontez:

 gcc -std=gnu99 -O3 -ggdb3 -Wall -Wextra -pedantic -o main.out main.c
 objdump -d main.out

Le démontage contient:

00000000000011a0 <inc>:
    11a0:       55                      Push   %rbp
    11a1:       48 89 e5                mov    %rsp,%rbp
    11a4:       41 57                   Push   %r15
    11a6:       41 56                   Push   %r14
    11a8:       41 55                   Push   %r13
    11aa:       41 54                   Push   %r12
    11ac:       53                      Push   %rbx
    11ad:       48 83 ec 08             sub    $0x8,%rsp
    11b1:       48 89 7d d0             mov    %rdi,-0x30(%rbp)
    11b5:       48 8b 45 d0             mov    -0x30(%rbp),%rax
    11b9:       48 8d 65 d8             lea    -0x28(%rbp),%rsp
    11bd:       5b                      pop    %rbx
    11be:       41 5c                   pop    %r12
    11c0:       48 83 c0 01             add    $0x1,%rax
    11c4:       41 5d                   pop    %r13
    11c6:       41 5e                   pop    %r14
    11c8:       41 5f                   pop    %r15
    11ca:       5d                      pop    %rbp
    11cb:       c3                      retq   
    11cc:       0f 1f 40 00             nopl   0x0(%rax)

et nous voyons donc clairement que les éléments suivants sont poussés et sautés:

rbx
r12
r13
r14
r15
rbp

Le seul manquant dans la spécification est rsp, mais nous nous attendons à ce que la pile soit restaurée bien sûr. Une lecture attentive de l'Assemblée confirme qu'elle est maintenue dans ce cas:

  • sub $0x8, %rsp: Alloue 8 octets sur la pile pour enregistrer %rdi Dans %rdi, -0x30(%rbp), ce qui est fait pour la contrainte d'assemblage en ligne +m
  • lea -0x28(%rbp), %rsp restaure %rsp avant le sub, c'est-à-dire 5 sauts après mov %rsp, %rbp
  • il y a 6 pushs et 6 pops correspondants
  • aucune autre instruction ne touche %rsp

Testé dans Ubuntu 18.10, GCC 8.2.0.