Comprendre les modèles de code d'architecture x64

"Quel modèle de code dois-je utiliser?" - une question fréquemment posée, mais rarement discutée lors de l'écriture de code pour l'architecture x64. Néanmoins, c'est un problème assez intéressant, et il est utile d'avoir une idée des modèles de code pour comprendre le code machine x64 généré par les compilateurs. De plus, pour ceux qui se soucient des performances jusqu'aux plus petites commandes, le choix du modèle de code affecte également l'optimisation.



Les informations à ce sujet sur le Web ou ailleurs sont rares. La plus importante des ressources disponibles est l'ABI officielle x64, vous pouvez la télécharger ici (ci-après, elle sera appelée «ABI»). Certaines informations peuvent également être trouvées sur les manpagesgcc. Le but de cet article est de fournir des recommandations accessibles sur un sujet, de discuter des problèmes connexes et de démontrer certains concepts grâce à un bon code utilisé dans le travail.



Remarque importante: cet article n'est pas un tutoriel pour les débutants. Avant de se familiariser, il est recommandé d'avoir une bonne maîtrise du C et de l'assembleur, ainsi qu'une connaissance de base de l'architecture x64.






Voir également notre article précédent sur un sujet similaire: Comment x86_x64 traite la mémoire






Modèles de code. Partie de motivation



Dans l'architecture x64, le code et les données sont envoyés via des modèles d'adressage relatifs à la commande (ou, en utilisant le jargon x64, relatifs à RIP). Dans ces commandes, le décalage de RIP est limité à 32 bits, cependant, il peut y avoir des cas où l'équipe, lorsqu'elle tente d'adresser une partie de la mémoire ou des données, n'a tout simplement pas de décalage de 32 bits, par exemple, lorsqu'elle travaille avec des programmes de plus de deux gigaoctets.



Une façon de résoudre ce problème consiste à abandonner complètement le mode d'adressage relatif au RIP au profit d'un décalage complet de 64 bits pour toutes les données et références de code. Cependant, cette étape sera très coûteuse: pour couvrir le cas (plutôt rare) de programmes et de bibliothèques incroyablement volumineux, même les opérations les plus simples dans tout le code nécessiteront plus que le nombre habituel de commandes.



Ainsi, les modèles de code deviennent un compromis. [1] Un modèle de code est un accord formel entre le programmeur et le compilateur dans lequel le programmeur spécifie ses intentions sur la taille du ou des programmes attendus qui contiendront le module objet en cours de compilation. [2] Les modèles de code sont nécessaires pour que le programmeur puisse dire au compilateur: "Ne vous inquiétez pas, ce module objet n'ira que dans de petits programmes, vous pouvez donc utiliser des modes d'adressage relatifs au RIP rapides." D'un autre côté, il peut dire au compilateur ce qui suit: «nous allons compiler ce module dans de grands programmes, alors veuillez utiliser des modes d'adressage absolus tranquilles et sûrs avec un décalage complet de 64 bits.»



Ce que cet article racontera



Nous parlerons des deux scénarios décrits ci-dessus, un petit modèle de code et un grand modèle de code: le premier modèle indique au compilateur qu'un décalage relatif de 32 bits devrait être suffisant pour toutes les références au code et aux données dans le module objet; le second insiste pour que le compilateur utilise des modes d'adressage absolus 64 bits. En outre, il existe également une version intermédiaire, le modèle dit de code intermédiaire .



Chacun de ces modèles de code est présenté dans des variantes PIC et non PIC indépendantes, et nous parlerons de chacun des six.



Exemple C original



Pour illustrer les concepts abordés dans cet article, j'utiliserai le programme C suivant et le compilerai avec divers modèles de code. Comme vous pouvez le voir, la fonction maina accès à quatre tableaux globaux différents et à une fonction globale. Les tableaux diffèrent par deux paramètres: la taille et la visibilité. La taille est importante pour expliquer le modèle de code moyen et ne sera pas nécessaire lorsque vous travaillez avec des modèles petits et grands. La visibilité est importante pour le fonctionnement des modèles de code PIC et est soit statique (visible uniquement dans le fichier source) soit globale (visibilité de tous les objets liés au programme).



int global_arr[100] = {2, 3};
static int static_arr[100] = {9, 7};
int global_arr_big[50000] = {5, 6};
static int static_arr_big[50000] = {10, 20};

int global_func(int param)
{
    return param * 10;
}

int main(int argc, const char* argv[])
{
    int t = global_func(argc);
    t += global_arr[7];
    t += static_arr[7];
    t += global_arr_big[7];
    t += static_arr_big[7];
    return t;
}


gccutilise le modèle de code comme valeur d'option -mcmodel. De plus, une -fpiccompilation PIC peut être définie avec un indicateur .



Un exemple de compilation dans un module objet via un grand modèle de code utilisant PIC:



> gcc -g -O0 -c codemodel1.c -fpic -mcmodel=large -o codemodel1_large_pic.o


Petit modèle de code



Traduction d'une citation de man gcc sur le modèle petit code:



-mcmodel = small

Génération de code pour un petit modèle: le programme et ses symboles doivent être liés dans les deux derniers gigaoctets de l'espace d'adressage. La taille des pointeurs est de 64 bits. Les programmes peuvent être construits à la fois de manière statique et dynamique. C'est le modèle de code de base.




En d'autres termes, le compilateur peut supposer en toute sécurité que le code et les données sont accessibles via un décalage relatif RIP 32 bits à partir de n'importe quelle commande du code. Jetons un coup d'œil à un exemple démonté d'un programme C que nous avons compilé via un modèle de petit code non PIC:



> objdump -dS codemodel1_small.o
[...]
int main(int argc, const char* argv[])
{
  15: 55                      push   %rbp
  16: 48 89 e5                mov    %rsp,%rbp
  19: 48 83 ec 20             sub    $0x20,%rsp
  1d: 89 7d ec                mov    %edi,-0x14(%rbp)
  20: 48 89 75 e0             mov    %rsi,-0x20(%rbp)
    int t = global_func(argc);
  24: 8b 45 ec                mov    -0x14(%rbp),%eax
  27: 89 c7                   mov    %eax,%edi
  29: b8 00 00 00 00          mov    $0x0,%eax
  2e: e8 00 00 00 00          callq  33 <main+0x1e>
  33: 89 45 fc                mov    %eax,-0x4(%rbp)
    t += global_arr[7];
  36: 8b 05 00 00 00 00       mov    0x0(%rip),%eax
  3c: 01 45 fc                add    %eax,-0x4(%rbp)
    t += static_arr[7];
  3f: 8b 05 00 00 00 00       mov    0x0(%rip),%eax
  45: 01 45 fc                add    %eax,-0x4(%rbp)
    t += global_arr_big[7];
  48: 8b 05 00 00 00 00       mov    0x0(%rip),%eax
  4e: 01 45 fc                add    %eax,-0x4(%rbp)
    t += static_arr_big[7];
  51: 8b 05 00 00 00 00       mov    0x0(%rip),%eax
  57: 01 45 fc                add    %eax,-0x4(%rbp)
    return t;
  5a: 8b 45 fc                mov    -0x4(%rbp),%eax
}
  5d: c9                      leaveq
  5e: c3                      retq


Comme vous pouvez le voir, l'accès à tous les tableaux est organisé de la même manière - en utilisant le décalage relatif au RIP. Cependant, dans le code, le décalage est de 0, car le compilateur ne sait pas où le segment de données sera placé, donc pour chacun de ces accès, il crée une relocalisation:



> readelf -r codemodel1_small.o

Relocation section '.rela.text' at offset 0x62bd8 contains 5 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
00000000002f  001500000002 R_X86_64_PC32     0000000000000000 global_func - 4
000000000038  001100000002 R_X86_64_PC32     0000000000000000 global_arr + 18
000000000041  000300000002 R_X86_64_PC32     0000000000000000 .data + 1b8
00000000004a  001200000002 R_X86_64_PC32     0000000000000340 global_arr_big + 18
000000000053  000300000002 R_X86_64_PC32     0000000000000000 .data + 31098


Décodons complètement l'accès à global_arr. Le segment démonté qui nous intéresse:



  t += global_arr[7];
36:       8b 05 00 00 00 00       mov    0x0(%rip),%eax
3c:       01 45 fc                add    %eax,-0x4(%rbp)


L'adressage relatif au RIP est relatif à la commande suivante, donc le décalage doit être corrigé dans la commande movafin qu'il corresponde à 0x3s. Nous nous intéressons à la deuxième relocalisation, R_X86_64_PC32elle pointe vers l'opérande movà l'adresse 0x38et signifie ce qui suit: nous prenons la valeur du symbole, ajoutons le terme et soustrayons le décalage indiqué par la relocalisation. Si vous avez tout calculé correctement, vous verrez comment le résultat place un décalage relatif entre l'équipe suivante et global_arr, plus 01. Puisqu'il 01signifie «le septième entier dans le tableau» (dans l'architecture x64, la taille de chacun intest de 4 octets), nous avons besoin de ce décalage relatif. Ainsi, en utilisant l'adressage relatif au RIP, la commande référence correctement global_arr[7].



Il est également intéressant de noter ce qui suit: bien que les commandes d'accès static_arrici soient similaires, sa redirection utilise un symbole différent, pointant ainsi vers une section au lieu d'un symbole spécifique .data. Cela est dû aux actions de l'éditeur de liens, il place le tableau statique à un emplacement connu dans la section, et donc le tableau ne peut pas être partagé avec d'autres bibliothèques partagées. En conséquence, l'éditeur de liens résoudra la situation avec ce déplacement. En revanche, puisqu'il global_arrpeut être utilisé (ou écrasé) par une autre bibliothèque partagée, le chargeur déjà dynamique devra gérer le lien vers global_arr. [3]



Enfin, regardons le lien vers global_func:



  int t = global_func(argc);
24:       8b 45 ec                mov    -0x14(%rbp),%eax
27:       89 c7                   mov    %eax,%edi
29:       b8 00 00 00 00          mov    $0x0,%eax
2e:       e8 00 00 00 00          callq  33 <main+0x1e>
33:       89 45 fc                mov    %eax,-0x4(%rbp)


Étant donné que l'opérande est callqégalement relatif au RIP, la relocalisation R_X86_64_PC32fonctionne ici de la même manière que le placement de l'offset relatif réel à global_func dans l'opérande.



En conclusion, nous notons qu'en raison du petit modèle de code, le compilateur perçoit toutes les données et le code du futur programme comme accessibles via un décalage de 32 bits, et crée ainsi un code simple et efficace pour accéder à toutes sortes d'objets.



Grand modèle de code



Traduction de citations man gccsur le thème d'un grand modèle de code:



-mcmodel = large

Génération de code pour un grand modèle: ce modèle ne fait aucune hypothèse sur les adresses et les tailles de section.


Un exemple de code désassemblé maincompilé avec un grand modèle non-PIC:



int main(int argc, const char* argv[])
{
  15: 55                      push   %rbp
  16: 48 89 e5                mov    %rsp,%rbp
  19: 48 83 ec 20             sub    $0x20,%rsp
  1d: 89 7d ec                mov    %edi,-0x14(%rbp)
  20: 48 89 75 e0             mov    %rsi,-0x20(%rbp)
    int t = global_func(argc);
  24: 8b 45 ec                mov    -0x14(%rbp),%eax
  27: 89 c7                   mov    %eax,%edi
  29: b8 00 00 00 00          mov    $0x0,%eax
  2e: 48 ba 00 00 00 00 00    movabs $0x0,%rdx
  35: 00 00 00
  38: ff d2                   callq  *%rdx
  3a: 89 45 fc                mov    %eax,-0x4(%rbp)
    t += global_arr[7];
  3d: 48 b8 00 00 00 00 00    movabs $0x0,%rax
  44: 00 00 00
  47: 8b 40 1c                mov    0x1c(%rax),%eax
  4a: 01 45 fc                add    %eax,-0x4(%rbp)
    t += static_arr[7];
  4d: 48 b8 00 00 00 00 00    movabs $0x0,%rax
  54: 00 00 00
  57: 8b 40 1c                mov    0x1c(%rax),%eax
  5a: 01 45 fc                add    %eax,-0x4(%rbp)
    t += global_arr_big[7];
  5d: 48 b8 00 00 00 00 00    movabs $0x0,%rax
  64: 00 00 00
  67: 8b 40 1c                mov    0x1c(%rax),%eax
  6a: 01 45 fc                add    %eax,-0x4(%rbp)
    t += static_arr_big[7];
  6d: 48 b8 00 00 00 00 00    movabs $0x0,%rax
  74: 00 00 00
  77: 8b 40 1c                mov    0x1c(%rax),%eax
  7a: 01 45 fc                add    %eax,-0x4(%rbp)
    return t;
  7d: 8b 45 fc                mov    -0x4(%rbp),%eax
}
  80: c9                      leaveq
  81: c3                      retq


Encore une fois, il est utile d'examiner les délocalisations:



Relocation section '.rela.text' at offset 0x62c18 contains 5 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000000030  001500000001 R_X86_64_64       0000000000000000 global_func + 0
00000000003f  001100000001 R_X86_64_64       0000000000000000 global_arr + 0
00000000004f  000300000001 R_X86_64_64       0000000000000000 .data + 1a0
00000000005f  001200000001 R_X86_64_64       0000000000000340 global_arr_big + 0
00000000006f  000300000001 R_X86_64_64       0000000000000000 .data + 31080


Puisqu'il n'est pas nécessaire de faire des hypothèses sur la taille des sections de code et des données, le grand modèle de code est assez unifié et identifie l'accès à toutes les données de la même manière. Jetons un autre regard sur global_arr:



  t += global_arr[7];
3d:       48 b8 00 00 00 00 00    movabs $0x0,%rax
44:       00 00 00
47:       8b 40 1c                mov    0x1c(%rax),%eax
4a:       01 45 fc                add    %eax,-0x4(%rbp)


Deux commandes doivent obtenir la valeur souhaitée du tableau. La première commande place l'adresse 64 bits absolue dans rax, qui, comme nous le verrons bientôt, sera l'adresse global_arr, tandis que la deuxième commande charge le mot de (rax) + 01into eax.



Concentrons-nous donc sur l'équipe à 0x3d, movabsversion 64 bits absolue movdans l'architecture x64. Il peut lancer la constante 64 bits complète directement dans le registre, et comme dans notre code désassemblé la valeur de cette constante est égale à zéro, nous devrons nous tourner vers la table de relocalisation pour une réponse. Nous y trouverons la relocalisation absolue R_X86_64_64de l'opérande à l'adresse 0x3f, avec la valeur suivante: placement de la valeur du symbole plus le terme de retour au décalage. En d'autres termes,raxcontiendra une adresse absolue global_arr.



Qu'en est-il de la fonction d'appel?



  int t = global_func(argc);
24:       8b 45 ec                mov    -0x14(%rbp),%eax
27:       89 c7                   mov    %eax,%edi
29:       b8 00 00 00 00          mov    $0x0,%eax
2e:       48 ba 00 00 00 00 00    movabs $0x0,%rdx
35:       00 00 00
38:       ff d2                   callq  *%rdx
3a:       89 45 fc                mov    %eax,-0x4(%rbp)


Celui que nous connaissons déjà est movabssuivi d'une commande callqui appelle une fonction à l'adresse dans rdx. Il suffit de regarder la délocalisation correspondante pour comprendre à quel point elle est similaire à l'accès aux données.



Comme vous pouvez le voir, le modèle de code volumineux ne fait aucune hypothèse sur la taille des sections de code et de données, ainsi que sur l'emplacement final des caractères, il fait simplement référence aux caractères via des étapes absolues de 64 bits, une sorte de «chemin sûr». Cependant, notez comment, par rapport à un petit modèle de code, un grand modèle est obligé d'utiliser une commande supplémentaire lors de l'accès à chaque caractère. C'est le prix de la sécurité.



Nous avons donc rencontré deux modèles complètement opposés: alors que le petit modèle du code suppose que tout rentre dans les deux gigaoctets inférieurs de mémoire, le grand modèle suppose que rien n'est impossible et que n'importe quel caractère peut être n'importe où en entier 64- espace d'adressage de bits. Le compromis entre les deux modèles est le modèle de code du milieu.



Modèle de code moyen



Comme précédemment, regardons la traduction de la citation de man gcc:



-mcmodel=medium

: . . , -mlarge-data-threshold, bss . , .


Semblable au petit modèle de code, le modèle du milieu suppose que le code entier est organisé en deux gigaoctets inférieurs. Cependant, les données sont divisées en «petites données» censées être disposées dans les deux gigaoctets inférieurs et illimitées en mémoire «big data». Les données sont classées comme volumineuses lorsqu'elles dépassent la limite, qui est par définition de 64 kilo-octets.



Il est également important de noter que lorsque vous travaillez avec un modèle de code moyen pour le Big Data, par analogie avec les sections .dataet .bss, des sections spéciales sont créées: .ldataet .lbss. Ce n'est pas si important dans le prisme du sujet de l'article actuel, mais je vais m'en écarter un peu. Plus de détails sur ce problème peuvent être trouvés dans l'ABI.



Maintenant, il devient clair pourquoi ces tableaux sont apparus dans l'exemple_big: elles sont nécessaires au modèle moyen pour interpréter les "big data" qu'elles sont, à 200 kilo-octets chacune. Ci-dessous, vous pouvez voir le résultat du démontage:



int main(int argc, const char* argv[])
{
  15: 55                      push   %rbp
  16: 48 89 e5                mov    %rsp,%rbp
  19: 48 83 ec 20             sub    $0x20,%rsp
  1d: 89 7d ec                mov    %edi,-0x14(%rbp)
  20: 48 89 75 e0             mov    %rsi,-0x20(%rbp)
    int t = global_func(argc);
  24: 8b 45 ec                mov    -0x14(%rbp),%eax
  27: 89 c7                   mov    %eax,%edi
  29: b8 00 00 00 00          mov    $0x0,%eax
  2e: e8 00 00 00 00          callq  33 <main+0x1e>
  33: 89 45 fc                mov    %eax,-0x4(%rbp)
    t += global_arr[7];
  36: 8b 05 00 00 00 00       mov    0x0(%rip),%eax
  3c: 01 45 fc                add    %eax,-0x4(%rbp)
    t += static_arr[7];
  3f: 8b 05 00 00 00 00       mov    0x0(%rip),%eax
  45: 01 45 fc                add    %eax,-0x4(%rbp)
    t += global_arr_big[7];
  48: 48 b8 00 00 00 00 00    movabs $0x0,%rax
  4f: 00 00 00
  52: 8b 40 1c                mov    0x1c(%rax),%eax
  55: 01 45 fc                add    %eax,-0x4(%rbp)
    t += static_arr_big[7];
  58: 48 b8 00 00 00 00 00    movabs $0x0,%rax
  5f: 00 00 00
  62: 8b 40 1c                mov    0x1c(%rax),%eax
  65: 01 45 fc                add    %eax,-0x4(%rbp)
    return t;
  68: 8b 45 fc                mov    -0x4(%rbp),%eax
}
  6b: c9                      leaveq
  6c: c3                      retq


Faites attention à la façon dont les tableaux sont accessibles: les tableaux _bigsont accessibles via les méthodes du grand modèle de code, tandis que le reste des tableaux est accessible via les méthodes du petit modèle. La fonction est également appelée à l'aide de la méthode du petit modèle de code, et les délocalisations sont tellement similaires aux exemples précédents que je ne vais même pas les démontrer.



Le modèle de code moyen est un compromis judicieux entre les grands et les petits modèles. Il est peu probable que le code du programme se révèle trop volumineux [4], donc seuls de gros morceaux de données liés statiquement peuvent le déplacer au-delà de la limite de deux gigaoctets, peut-être dans le cadre d'une recherche de table volumineuse. Étant donné que le modèle intermédiaire du code filtre ces gros morceaux de données et les traite de manière spéciale, les appels par le code de fonctions et les petits symboles seront aussi efficaces que dans le petit modèle de code. Seuls les accès aux grands symboles, par analogie avec le grand modèle, nécessiteront que le code utilise la méthode 64 bits complète du grand modèle.



Petit modèle de code PIC



Examinons maintenant les variantes PIC des modèles de code et, comme auparavant, nous commençons par le petit modèle. [5] Ci-dessous, vous pouvez voir un exemple du code compilé à travers le petit modèle PIC:



int main(int argc, const char* argv[])
{
  15:   55                      push   %rbp
  16:   48 89 e5                mov    %rsp,%rbp
  19:   48 83 ec 20             sub    $0x20,%rsp
  1d:   89 7d ec                mov    %edi,-0x14(%rbp)
  20:   48 89 75 e0             mov    %rsi,-0x20(%rbp)
    int t = global_func(argc);
  24:   8b 45 ec                mov    -0x14(%rbp),%eax
  27:   89 c7                   mov    %eax,%edi
  29:   b8 00 00 00 00          mov    $0x0,%eax
  2e:   e8 00 00 00 00          callq  33 <main+0x1e>
  33:   89 45 fc                mov    %eax,-0x4(%rbp)
    t += global_arr[7];
  36:   48 8b 05 00 00 00 00    mov    0x0(%rip),%rax
  3d:   8b 40 1c                mov    0x1c(%rax),%eax
  40:   01 45 fc                add    %eax,-0x4(%rbp)
    t += static_arr[7];
  43:   8b 05 00 00 00 00       mov    0x0(%rip),%eax
  49:   01 45 fc                add    %eax,-0x4(%rbp)
    t += global_arr_big[7];
  4c:   48 8b 05 00 00 00 00    mov    0x0(%rip),%rax
  53:   8b 40 1c                mov    0x1c(%rax),%eax
  56:   01 45 fc                add    %eax,-0x4(%rbp)
    t += static_arr_big[7];
  59:   8b 05 00 00 00 00       mov    0x0(%rip),%eax
  5f:   01 45 fc                add    %eax,-0x4(%rbp)
    return t;
  62:   8b 45 fc                mov    -0x4(%rbp),%eax
}
  65:   c9                      leaveq
  66:   c3                      retq


Déménagements:



Relocation section '.rela.text' at offset 0x62ce8 contains 5 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
00000000002f  001600000004 R_X86_64_PLT32    0000000000000000 global_func - 4
000000000039  001100000009 R_X86_64_GOTPCREL 0000000000000000 global_arr - 4
000000000045  000300000002 R_X86_64_PC32     0000000000000000 .data + 1b8
00000000004f  001200000009 R_X86_64_GOTPCREL 0000000000000340 global_arr_big - 4
00000000005b  000300000002 R_X86_64_PC32     0000000000000000 .data + 31098


Étant donné que les différences entre les grandes et les petites données ne jouent aucun rôle dans un petit modèle de code, nous nous concentrerons sur les points importants lors de la génération de code via PIC: les différences entre les caractères locaux (statiques) et globaux.



Comme vous pouvez le voir, il n'y a aucune différence entre le code généré pour les tableaux statiques et le code dans le cas non-PIC. C'est l'un des avantages de l'architecture x64: grâce à l'accès IP aux données, nous obtenons un PIC en bonus, au moins jusqu'à ce qu'un accès externe aux caractères soit requis. Toutes les commandes et relocalisations restent les mêmes, il n'est donc pas nécessaire de les traiter à nouveau.



Il est intéressant de prêter attention aux tableaux globaux: il convient de rappeler qu'en PIC, les données globales doivent passer par le GOT, car à un moment donné, elles peuvent être stockées ou utilisées par des bibliothèques partagées [6]. Ci-dessous vous pouvez voir le code d'accès global_arr:



  t += global_arr[7];
36:   48 8b 05 00 00 00 00    mov    0x0(%rip),%rax
3d:   8b 40 1c                mov    0x1c(%rax),%eax
40:   01 45 fc                add    %eax,-0x4(%rbp)


La délocalisation qui nous intéresse est R_X86_64_GOTPCREL: la position de l'entrée du symbole dans le GOT plus le terme, moins le décalage pour appliquer la délocalisation. En d'autres termes, le décalage relatif entre le RIP (instruction suivante) et l' global_arremplacement réservé pour dans le GOT est corrigé dans la commande . Ainsi, l' adresse réelle est placée raxdans la commande à 0x36l'adresse global_arr. Après cette étape, vous réinitialisez le lien vers l'adresse, global_arrplus un décalage vers son septième élément dans eax.



Jetons maintenant un coup d'œil à l'appel de fonction:



  int t = global_func(argc);
24:   8b 45 ec                mov    -0x14(%rbp),%eax
27:   89 c7                   mov    %eax,%edi
29:   b8 00 00 00 00          mov    $0x0,%eax
2e:   e8 00 00 00 00          callq  33 <main+0x1e>
33:   89 45 fc                mov    %eax,-0x4(%rbp)


Il a un déplacement de l'opérande callqadresse 0x2e, R_X86_64_PLT32: adresse d'entrée de PLT pour le déplacement négatif symbole plus terme pour l'application de la délocalisation. En d'autres termes, il callqdevrait appeler correctement le tremplin PLT global_func.



Notez les hypothèses implicites du compilateur: que le GOT et le PLT sont accessibles via l'adressage relatif au RIP. Cela sera important lors de la comparaison de ce modèle avec d'autres variantes PIC de modèles de code.



Grand modèle de code PIC



Démontage:



int main(int argc, const char* argv[])
{
  15: 55                      push   %rbp
  16: 48 89 e5                mov    %rsp,%rbp
  19: 53                      push   %rbx
  1a: 48 83 ec 28             sub    $0x28,%rsp
  1e: 48 8d 1d f9 ff ff ff    lea    -0x7(%rip),%rbx
  25: 49 bb 00 00 00 00 00    movabs $0x0,%r11
  2c: 00 00 00
  2f: 4c 01 db                add    %r11,%rbx
  32: 89 7d dc                mov    %edi,-0x24(%rbp)
  35: 48 89 75 d0             mov    %rsi,-0x30(%rbp)
    int t = global_func(argc);
  39: 8b 45 dc                mov    -0x24(%rbp),%eax
  3c: 89 c7                   mov    %eax,%edi
  3e: b8 00 00 00 00          mov    $0x0,%eax
  43: 48 ba 00 00 00 00 00    movabs $0x0,%rdx
  4a: 00 00 00
  4d: 48 01 da                add    %rbx,%rdx
  50: ff d2                   callq  *%rdx
  52: 89 45 ec                mov    %eax,-0x14(%rbp)
    t += global_arr[7];
  55: 48 b8 00 00 00 00 00    movabs $0x0,%rax
  5c: 00 00 00
  5f: 48 8b 04 03             mov    (%rbx,%rax,1),%rax
  63: 8b 40 1c                mov    0x1c(%rax),%eax
  66: 01 45 ec                add    %eax,-0x14(%rbp)
    t += static_arr[7];
  69: 48 b8 00 00 00 00 00    movabs $0x0,%rax
  70: 00 00 00
  73: 8b 44 03 1c             mov    0x1c(%rbx,%rax,1),%eax
  77: 01 45 ec                add    %eax,-0x14(%rbp)
    t += global_arr_big[7];
  7a: 48 b8 00 00 00 00 00    movabs $0x0,%rax
  81: 00 00 00
  84: 48 8b 04 03             mov    (%rbx,%rax,1),%rax
  88: 8b 40 1c                mov    0x1c(%rax),%eax
  8b: 01 45 ec                add    %eax,-0x14(%rbp)
    t += static_arr_big[7];
  8e: 48 b8 00 00 00 00 00    movabs $0x0,%rax
  95: 00 00 00
  98: 8b 44 03 1c             mov    0x1c(%rbx,%rax,1),%eax
  9c: 01 45 ec                add    %eax,-0x14(%rbp)
    return t;
  9f: 8b 45 ec                mov    -0x14(%rbp),%eax
}
  a2: 48 83 c4 28             add    $0x28,%rsp
  a6: 5b                      pop    %rbx
  a7: c9                      leaveq
  a8: c3                      retq


Délocalisations: cette fois- ci, les différences entre les grandes et les petites données n'ont toujours pas d'importance, nous allons donc nous concentrer sur et . Mais d'abord, vous devez faire attention au prologue de ce code, nous n'avons jamais rencontré quelque chose comme ça:



Relocation section '.rela.text' at offset 0x62c70 contains 6 entries:

Offset Info Type Sym. Value Sym. Name + Addend

000000000027 00150000001d R_X86_64_GOTPC64 0000000000000000 _GLOBAL_OFFSET_TABLE_ + 9

000000000045 00160000001f R_X86_64_PLTOFF64 0000000000000000 global_func + 0

000000000057 00110000001b R_X86_64_GOT64 0000000000000000 global_arr + 0

00000000006b 000800000019 R_X86_64_GOTOFF64 00000000000001a0 static_arr + 0

00000000007c 00120000001b R_X86_64_GOT64 0000000000000340 global_arr_big + 0

000000000090 000900000019 R_X86_64_GOTOFF64 0000000000031080 static_arr_big + 0


static_arrglobal_arr



1e: 48 8d 1d f9 ff ff ff    lea    -0x7(%rip),%rbx
25: 49 bb 00 00 00 00 00    movabs $0x0,%r11
2c: 00 00 00
2f: 4c 01 db                add    %r11,%rbx


Ci-dessous, vous pouvez lire la traduction de la citation associée de l'ABI:



( GOT) AMD64 IP- . GOT . GOT , AMD64 ISA 32 .


Voyons comment le prologue décrit ci-dessus calcule l'adresse GOT. Tout d'abord, l'équipe d'adresse 0x1echarge sa propre adresse dans rbx. Ensuite, avec la relocalisation, R_X86_64_GOTPC64une étape absolue de 64 bits est effectuée r11. Cette relocalisation signifie ce qui suit: prenez l'adresse GOT, soustrayez le quart de travail décalé et ajoutez le terme. Enfin, la commande à l'adresse 0x2fajoute les deux résultats ensemble. Le résultat est l'adresse absolue du GOT rbx. [7]



Pourquoi se soucier de calculer l'adresse GOT? Premièrement, comme indiqué dans la citation, dans un modèle de code volumineux, nous ne pouvons pas supposer qu'un décalage relatif RIP 32 bits sera suffisant pour l'adressage GOT, c'est pourquoi nous avons besoin d'une adresse 64 bits complète. Deuxièmement, nous voulons toujours travailler avec la variation PIC, donc nous ne pouvons pas simplement mettre l'adresse absolue dans un registre. L'adresse elle-même doit plutôt être calculée par rapport au RIP. Pour cela, nous avons besoin d'un prologue: il effectue un calcul relatif RIP 64 bits.



Dans tous les cas, puisque nous avons rbxmaintenant une adresse GOT, voyons comment y accéder static_arr:



  t += static_arr[7];
69:       48 b8 00 00 00 00 00    movabs $0x0,%rax
70:       00 00 00
73:       8b 44 03 1c             mov    0x1c(%rbx,%rax,1),%eax
77:       01 45 ec                add    %eax,-0x14(%rbp)


Le déplacement de la première commande est R_X86_64_GOTOFF64: le symbole plus le terme moins GOT. Dans notre cas, il s'agit du décalage relatif entre l'adresse static_arret l'adresse GOT. L'instruction suivante ajoute le résultat à rbx(adresse GOT absolue) et réinitialise le décalage par référence 0x1c. Pour faciliter la visualisation d'un tel calcul, un exemple de pseudo-C peut être trouvé ci-dessous:



// char* static_arr
// char* GOT
rax = static_arr + 0 - GOT;  // rax now contains an offset
eax = *(rbx + rax + 0x1c);   // rbx == GOT, so eax now contains
                             // *(GOT + static_arr - GOT + 0x1c) or
                             // *(static_arr + 0x1c)


Notez un point intéressant: l'adresse GOT est utilisée comme liaison avec static_arr. Habituellement, un GOT ne contient pas d'adresse de symbole, et comme il static_arrne s'agit pas d'un symbole externe, il n'y a aucune raison de le stocker dans un GOT. Cependant, dans ce cas, le GOT est utilisé comme liaison à l'adresse de symbole relative de la section de données. Cette adresse, qui, entre autres, est indépendante de l'emplacement, peut être trouvée avec un décalage complet de 64 bits. L'éditeur de liens est capable de gérer cette réinstallation, il n'est donc pas nécessaire de modifier la section de code au moment du chargement.



Mais qu'en est-il global_arr?



  t += global_arr[7];
55:       48 b8 00 00 00 00 00    movabs $0x0,%rax
5c:       00 00 00
5f:       48 8b 04 03             mov    (%rbx,%rax,1),%rax
63:       8b 40 1c                mov    0x1c(%rax),%eax
66:       01 45 ec                add    %eax,-0x14(%rbp)


Ce code est un peu plus long et la relocalisation est différente de celle habituelle. En fait, le GOT est utilisé ici d'une manière plus traditionnelle: la relocalisation R_X86_64_GOT64de movabsindique simplement à la fonction de placer le décalage dans le GOT où raxse trouve l' adresse global_arr. La commande à l'adresse 0x5fprend l'adresse global_arrdu GOT et la place rax. La commande suivante réinitialise la référence à global_arr[7]et place la valeur dans eax.



Jetons maintenant un coup d'œil au lien de code pour global_func. Rappelez-vous que dans un grand modèle de code, nous ne pouvions pas faire d'hypothèses sur la taille des sections de code, nous devons donc supposer que même pour accéder à PLT, nous avons besoin d'une adresse 64 bits absolue:



  int t = global_func(argc);
39: 8b 45 dc                mov    -0x24(%rbp),%eax
3c: 89 c7                   mov    %eax,%edi
3e: b8 00 00 00 00          mov    $0x0,%eax
43: 48 ba 00 00 00 00 00    movabs $0x0,%rdx
4a: 00 00 00
4d: 48 01 da                add    %rbx,%rdx
50: ff d2                   callq  *%rdx
52: 89 45 ec                mov    %eax,-0x14(%rbp)


La relocalisation qui nous intéresse est R_X86_64_PLTOFF64: l' global_funcadresse d' entrée PLT pour moins l'adresse GOT. Le résultat est placé rdxlà où il est ensuite placé rbx(adresse GOT absolue). En conséquence, nous obtenons l'adresse PLT d'entrée pour global_funcà rdx.



Notez que le GOT est à nouveau utilisé comme ancre, cette fois pour fournir une référence indépendante de l'adresse au décalage de l'entrée PLT.



Modèle de code PIC moyen



Enfin, nous analyserons le code généré pour le modèle PIC moyen:



int main(int argc, const char* argv[])
{
  15:   55                      push   %rbp
  16:   48 89 e5                mov    %rsp,%rbp
  19:   53                      push   %rbx
  1a:   48 83 ec 28             sub    $0x28,%rsp
  1e:   48 8d 1d 00 00 00 00    lea    0x0(%rip),%rbx
  25:   89 7d dc                mov    %edi,-0x24(%rbp)
  28:   48 89 75 d0             mov    %rsi,-0x30(%rbp)
    int t = global_func(argc);
  2c:   8b 45 dc                mov    -0x24(%rbp),%eax
  2f:   89 c7                   mov    %eax,%edi
  31:   b8 00 00 00 00          mov    $0x0,%eax
  36:   e8 00 00 00 00          callq  3b <main+0x26>
  3b:   89 45 ec                mov    %eax,-0x14(%rbp)
    t += global_arr[7];
  3e:   48 8b 05 00 00 00 00    mov    0x0(%rip),%rax
  45:   8b 40 1c                mov    0x1c(%rax),%eax
  48:   01 45 ec                add    %eax,-0x14(%rbp)
    t += static_arr[7];
  4b:   8b 05 00 00 00 00       mov    0x0(%rip),%eax
  51:   01 45 ec                add    %eax,-0x14(%rbp)
    t += global_arr_big[7];
  54:   48 8b 05 00 00 00 00    mov    0x0(%rip),%rax
  5b:   8b 40 1c                mov    0x1c(%rax),%eax
  5e:   01 45 ec                add    %eax,-0x14(%rbp)
    t += static_arr_big[7];
  61:   48 b8 00 00 00 00 00    movabs $0x0,%rax
  68:   00 00 00
  6b:   8b 44 03 1c             mov    0x1c(%rbx,%rax,1),%eax
  6f:   01 45 ec                add    %eax,-0x14(%rbp)
    return t;
  72:   8b 45 ec                mov    -0x14(%rbp),%eax
}
  75:   48 83 c4 28             add    $0x28,%rsp
  79:   5b                      pop    %rbx
  7a:   c9                      leaveq
  7b:   c3                      retq


Déménagements:



Relocation section '.rela.text' at offset 0x62d60 contains 6 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000000021  00160000001a R_X86_64_GOTPC32  0000000000000000 _GLOBAL_OFFSET_TABLE_ - 4
000000000037  001700000004 R_X86_64_PLT32    0000000000000000 global_func - 4
000000000041  001200000009 R_X86_64_GOTPCREL 0000000000000000 global_arr - 4
00000000004d  000300000002 R_X86_64_PC32     0000000000000000 .data + 1b8
000000000057  001300000009 R_X86_64_GOTPCREL 0000000000000000 global_arr_big - 4
000000000063  000a00000019 R_X86_64_GOTOFF64 0000000000030d40 static_arr_big + 0


Tout d'abord, supprimons l'appel de fonction. De manière similaire au petit modèle, dans le modèle intermédiaire, nous supposons que les références de code ne dépassent pas les limites du décalage RIP 32 bits, par conséquent, le code à appeler est global_funccomplètement similaire au même code dans le petit modèle PIC, ainsi que pour les cas de petits tableaux de données static_arret global_arr. Par conséquent, nous nous concentrerons sur les tableaux de Big Data, mais parlons d'abord du prologue: ici, il diffère du prologue du modèle de Big Data.



1e:   48 8d 1d 00 00 00 00    lea    0x0(%rip),%rbx


C'est tout le prologue: pour utiliser la relocalisation pour R_X86_64_GOTPC32mettre l'adresse GOT rbx, il n'a fallu qu'une seule équipe (contre trois dans le grand modèle). Quelle est la différence? Le fait est que, dans le modèle intermédiaire, le GOT ne fait pas partie des "partitions de données volumineuses", nous supposons sa disponibilité dans un décalage de 32 bits. Dans le grand modèle, nous ne pouvions pas faire de telles hypothèses et avons dû utiliser un décalage complet de 64 bits.



Il est intéressant de noter que le code d'accès global_arr_bigest similaire au même code dans un petit modèle PIC. C'est pour la même raison que le prologue du modèle du milieu est plus court que le prologue du grand modèle: nous supposons que le GOT est disponible dans l'adressage relatif RIP 32 bits. En effet, au plusglobal_arr_bigvous ne pouvez pas obtenir un tel accès, mais ce cas couvre toujours le GOT, car en fait global_arr_bigil y est situé, de plus, sous la forme d'une adresse 64 bits complète.



La situation est cependant différente pour static_arr_big:



  t += static_arr_big[7];
61:   48 b8 00 00 00 00 00    movabs $0x0,%rax
68:   00 00 00
6b:   8b 44 03 1c             mov    0x1c(%rbx,%rax,1),%eax
6f:   01 45 ec                add    %eax,-0x14(%rbp)


Ce cas est similaire au grand modèle PIC du code, car ici nous obtenons toujours l'adresse absolue du caractère, qui n'est pas dans le GOT lui-même. Puisqu'il s'agit d'un grand caractère, qui ne peut pas être supposé être dans les deux giga-octets inférieurs, nous, comme dans le grand modèle, avons besoin d'un décalage PIC de 64 bits.



Remarques:



[1] Ne confondez pas les modèles de code avec les modèles de données 64 bits et les modèles de mémoire Intel , ce sont tous des sujets différents.



[2] Il est important de se rappeler: les commandes réelles sont créées par le compilateur, et les modes d'adressage sont fixés exactement à cette étape. Le compilateur ne peut pas savoir dans quels programmes ou bibliothèques partagées le module objet tombera, certains peuvent être petits et d'autres grands. L'éditeur de liens connaît la taille du programme final, mais il est trop tard: l'éditeur de liens ne peut patcher que le décalage des commandes avec relocalisation, et ne pas modifier les commandes elles-mêmes. Ainsi, la "convention" du modèle de code doit être "signée" par le programmeur au moment de la compilation.



[3] Si quelque chose n'est pas clair, consultez l' article suivant .



[4] Cependant, les volumes augmentent progressivement. La dernière fois que j'ai vérifié la version Clang de Debug + Asserts, elle a presque atteint un gigaoctet, dont beaucoup grâce au code généré automatiquement.



[5] Si vous ne savez toujours pas comment fonctionne le PIC (à la fois en général et en particulier pour l'architecture x64), il est temps de lire les articles suivants sur le sujet: un et deux .



[6] Ainsi, l'éditeur de liens ne peut pas résoudre les liens par lui-même et doit déplacer le traitement GOT vers le chargeur dynamique.



[7] 0x25 - 0x7 + GOT - 0x27 + 0x9 = GOT









All Articles