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
man
pagesgcc
. 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
main
a 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;
}
gcc
utilise le modèle de code comme valeur d'option -mcmodel
. De plus, une -fpic
compilation 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
mov
afin qu'il corresponde à 0x3s. Nous nous intéressons à la deuxième relocalisation, R_X86_64_PC32
elle pointe vers l'opérande mov
à l'adresse 0x38
et 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 01
signifie «le septième entier dans le tableau» (dans l'architecture x64, la taille de chacun int
est 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_arr
ici 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_arr
peut ê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_PC32
fonctionne 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
gcc
sur 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é
main
compilé 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) + 01
into eax
.
Concentrons-nous donc sur l'équipe à
0x3d
, movabs
version 64 bits absolue mov
dans 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_64
de 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,rax
contiendra 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
movabs
suivi d'une commande call
qui 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
.data
et .bss
, des sections spéciales sont créées: .ldata
et .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
_big
sont 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_arr
emplacement réservé pour dans le GOT est corrigé dans la commande . Ainsi, l' adresse réelle est placée rax
dans la commande à 0x36
l'adresse global_arr
. Après cette étape, vous réinitialisez le lien vers l'adresse, global_arr
plus 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
callq
adresse 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 callq
devrait 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_arr
global_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
0x1e
charge sa propre adresse dans rbx
. Ensuite, avec la relocalisation, R_X86_64_GOTPC64
une é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 0x2f
ajoute 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
rbx
maintenant 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_arr
et 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_arr
ne 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_GOT64
de movabs
indique simplement à la fonction de placer le décalage dans le GOT où rax
se trouve l' adresse global_arr
. La commande à l'adresse 0x5f
prend l'adresse global_arr
du 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_func
adresse d' entrée PLT pour moins l'adresse GOT. Le résultat est placé rdx
là 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_func
complètement similaire au même code dans le petit modèle PIC, ainsi que pour les cas de petits tableaux de données static_arr
et 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_GOTPC32
mettre 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_big
est 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_big
vous ne pouvez pas obtenir un tel accès, mais ce cas couvre toujours le GOT, car en fait global_arr_big
il 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