Je me suis toujours demandé. Je sais que les compilateurs convertissent le code que vous écrivez en binaires, mais que font les éditeurs de liens? Ils ont toujours été un mystère pour moi.
Je comprends à peu près ce qu'est le "lien". C'est lorsque des références aux bibliothèques et aux frameworks sont ajoutées au binaire. Je ne comprends rien de plus que ça. Pour moi, cela "fonctionne". Je comprends également les bases de la liaison dynamique, mais rien de trop profond.
Quelqu'un pourrait-il expliquer les termes?
Pour comprendre les éditeurs de liens, il est utile de comprendre d'abord ce qui se passe "sous le capot" lorsque vous convertissez un fichier source (tel qu'un fichier C ou C++) en un fichier exécutable (un fichier exécutable est un fichier qui peut être exécuté sur votre machine ou la machine de quelqu'un d'autre exécutant la même architecture de machine).
Sous le capot, lorsqu'un programme est compilé, le compilateur convertit le fichier source en code octet objet. Ce code d'octet (parfois appelé code objet) est une instruction mnémonique que seule l'architecture de votre ordinateur comprend. Traditionnellement, ces fichiers ont une extension .OBJ.
Une fois le fichier objet créé, l'éditeur de liens entre en jeu. Plus souvent qu'autrement, un vrai programme qui fait quelque chose d'utile devra référencer d'autres fichiers. En C, par exemple, un programme simple pour imprimer votre nom à l'écran serait composé de:
printf("Hello Kristina!\n");
Lorsque le compilateur a compilé votre programme dans un fichier obj, il met simplement une référence à la fonction printf
. L'éditeur de liens résout cette référence. La plupart des langages de programmation ont une bibliothèque standard de routines pour couvrir les éléments de base attendus de ce langage. L'éditeur de liens relie votre fichier OBJ à cette bibliothèque standard. L'éditeur de liens peut également lier votre fichier OBJ à d'autres fichiers OBJ. Vous pouvez créer d'autres fichiers OBJ qui ont des fonctions qui peuvent être appelées par un autre fichier OBJ. L'éditeur de liens fonctionne presque comme un copier-coller d'un traitement de texte. Il "copie" toutes les fonctions nécessaires que votre programme référence et crée un seul exécutable. Parfois, d'autres bibliothèques copiées dépendent d'autres fichiers OBJ ou de bibliothèque. Parfois, un éditeur de liens doit être assez récursif pour faire son travail.
Notez que tous les systèmes d'exploitation ne créent pas un seul exécutable. Windows, par exemple, utilise des DLL qui conservent toutes ces fonctions dans un seul fichier. Cela réduit la taille de votre exécutable, mais rend votre exécutable dépendant de ces DLL spécifiques. DOS utilisait autrefois des choses appelées superpositions (fichiers .OVL). Cela avait de nombreux objectifs, mais l'un consistait à regrouper les fonctions couramment utilisées dans un seul fichier (un autre objectif qu'il servait, au cas où vous vous poseriez la question, était de pouvoir adapter de gros programmes en mémoire. DOS a une limitation en mémoire et les superpositions pouvaient être "déchargé" de la mémoire et d'autres superpositions pourraient être "chargées" au-dessus de cette mémoire, d'où le nom, "superpositions"). Linux a des bibliothèques partagées, ce qui est essentiellement la même idée que les DLL (les gars de noyau dur que je connais me diraient qu'il y a BEAUCOUP de différences).
J'espère que cela vous aide à comprendre!
La relocalisation des adresses est l'une des fonctions cruciales de la liaison.
Voyons donc comment cela fonctionne avec un exemple minimal.
Résumé: la relocalisation modifie la section .text
Des fichiers objets à traduire:
Cela doit être fait par l'éditeur de liens car le compilateur ne voit qu'un seul fichier d'entrée à la fois, mais nous devons connaître tous les fichiers objets à la fois pour décider comment:
.text
et .data
de plusieurs fichiers objetsPrérequis: compréhension minimale de:
La liaison n'a rien à voir avec C ou C++ en particulier: les compilateurs génèrent simplement les fichiers objets. L'éditeur de liens les prend ensuite en entrée sans jamais savoir quelle langue les a compilés. Cela pourrait aussi bien être Fortran.
Donc, pour réduire la croûte, étudions un monde bonjour NASF x86-64 ELF Linux:
section .data
hello_world db "Hello world!", 10
section .text
global _start
_start:
; sys_write
mov rax, 1
mov rdi, 1
mov rsi, hello_world
mov rdx, 13
syscall
; sys_exit
mov rax, 60
mov rdi, 0
syscall
compilé et assemblé avec:
nasm -o hello_world.o hello_world.asm
ld -o hello_world.out hello_world.o
avec NASM 2.10.09.
Nous décompilons d'abord la section .text
Du fichier objet:
objdump -d hello_world.o
qui donne:
0000000000000000 <_start>:
0: b8 01 00 00 00 mov $0x1,%eax
5: bf 01 00 00 00 mov $0x1,%edi
a: 48 be 00 00 00 00 00 movabs $0x0,%rsi
11: 00 00 00
14: ba 0d 00 00 00 mov $0xd,%edx
19: 0f 05 syscall
1b: b8 3c 00 00 00 mov $0x3c,%eax
20: bf 00 00 00 00 mov $0x0,%edi
25: 0f 05 syscall
les lignes cruciales sont:
a: 48 be 00 00 00 00 00 movabs $0x0,%rsi
11: 00 00 00
qui doit déplacer l'adresse de la chaîne hello world dans le registre rsi
, qui est passé à l'appel système d'écriture.
Mais attendez! Comment le compilateur peut-il savoir où "Hello world!"
Se retrouvera en mémoire lorsque le programme sera chargé?
Eh bien, cela ne peut pas, surtout après avoir lié un tas de fichiers .o
Avec plusieurs sections .data
.
Seul l'éditeur de liens peut le faire car seul il aura tous ces fichiers objets.
Donc, le compilateur juste:
0x0
sur la sortie compiléeCes "informations supplémentaires" sont contenues dans la section .rela.text
Du fichier objet
.rela.text
Signifie "relocalisation de la section .text".
La relocalisation de Word est utilisée car l'éditeur de liens devra déplacer l'adresse de l'objet dans l'exécutable.
Nous pouvons démonter la section .rela.text
Avec:
readelf -r hello_world.o
qui contient;
Relocation section '.rela.text' at offset 0x340 contains 1 entries:
Offset Info Type Sym. Value Sym. Name + Addend
00000000000c 000200000001 R_X86_64_64 0000000000000000 .data + 0
Le format de cette section est corrigé et documenté à: http://www.sco.com/developers/gabi/2003-12-17/ch4.reloc.html
Chaque entrée indique à l'éditeur de liens une adresse qui doit être déplacée, ici nous n'en avons qu'une pour la chaîne.
Simplifiant un peu, pour cette ligne particulière, nous avons les informations suivantes:
Offset = C
: Quel est le premier octet du .text
Que cette entrée change.
Si nous regardons le texte décompilé, il se trouve exactement à l'intérieur du movabs $0x0,%rsi
Critique, et ceux qui connaissent le codage des instructions x86-64 remarqueront que cela code la partie d'adresse 64 bits de l'instruction.
Name = .data
: L'adresse pointe vers la section .data
Type = R_X86_64_64
, Qui spécifie exactement quel calcul doit être fait pour traduire l'adresse.
Ce champ est en fait dépendant du processeur, et donc documenté sur le extension AMD64 System V ABI section 4.4 "Relocalisation".
Ce document indique que R_X86_64_64
:
Field = Word64
: 8 octets, donc le 00 00 00 00 00 00 00 00
À l'adresse 0xC
Calculation = S + A
S
est la valeur à l'adresse à déplacer, donc 00 00 00 00 00 00 00 00
A
est le complément qui est 0
ici. Il s'agit d'un champ de l'entrée de relocalisation.Donc S + A == 0
Et nous serons relocalisés à la toute première adresse de la section .data
.
Voyons maintenant la zone de texte de l'exécutable ld
généré pour nous:
objdump -d hello_world.out
donne:
00000000004000b0 <_start>:
4000b0: b8 01 00 00 00 mov $0x1,%eax
4000b5: bf 01 00 00 00 mov $0x1,%edi
4000ba: 48 be d8 00 60 00 00 movabs $0x6000d8,%rsi
4000c1: 00 00 00
4000c4: ba 0d 00 00 00 mov $0xd,%edx
4000c9: 0f 05 syscall
4000cb: b8 3c 00 00 00 mov $0x3c,%eax
4000d0: bf 00 00 00 00 mov $0x0,%edi
4000d5: 0f 05 syscall
Donc, la seule chose qui a changé depuis le fichier objet sont les lignes critiques:
4000ba: 48 be d8 00 60 00 00 movabs $0x6000d8,%rsi
4000c1: 00 00 00
qui pointe désormais vers l'adresse 0x6000d8
(d8 00 60 00 00 00 00 00
en petit-boutien) au lieu de 0x0
.
Est-ce le bon emplacement pour la chaîne hello_world
?
Pour décider, nous devons vérifier les en-têtes du programme, qui indiquent à Linux où charger chaque section.
Nous les démontons avec:
readelf -l hello_world.out
qui donne:
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
0x00000000000000d7 0x00000000000000d7 R E 200000
LOAD 0x00000000000000d8 0x00000000006000d8 0x00000000006000d8
0x000000000000000d 0x000000000000000d RW 200000
Section to Segment mapping:
Segment Sections...
00 .text
01 .data
Cela nous indique que la section .data
, Qui est la deuxième, commence à VirtAddr
= 0x06000d8
.
Et la seule chose dans la section des données est notre chaîne Hello World.
Dans des langages comme "C", les modules individuels de code sont traditionnellement compilés séparément en blobs de code objet, qui est prêt à être exécuté à tous égards, sauf que toutes les références que ce module fait en dehors de lui-même (c'est-à-dire aux bibliothèques ou à d'autres modules) ont pas encore résolus (c'est-à-dire qu'ils sont vides, en attendant que quelqu'un arrive et fasse toutes les connexions).
Ce que fait l'éditeur de liens, c'est de regarder tous les modules ensemble, de voir ce dont chaque module a besoin pour se connecter à l'extérieur de lui-même et de regarder tout ce qu'il exporte. Il corrige ensuite tout cela et produit un exécutable final, qui peut ensuite être exécuté.
Là où la liaison dynamique est également en cours, la sortie de l'éditeur de liens est toujours ne peut pas être exécutée - il y a encore des références à des bibliothèques externes non encore résolues, et elles sont résolues par le système d'exploitation à l'époque il charge l'application (ou peut-être même plus tard pendant l'exécution).
Lorsque le compilateur produit un fichier objet, il inclut des entrées pour les symboles définis dans ce fichier objet et des références aux symboles qui ne sont pas définis dans ce fichier objet. L'éditeur de liens prend ceux-ci et les rassemble afin que (quand tout fonctionne bien) toutes les références externes de chaque fichier soient satisfaites par des symboles définis dans d'autres fichiers objets.
Il combine ensuite tous ces fichiers objets ensemble et attribue des adresses à chacun des symboles, et lorsqu'un fichier objet a une référence externe à un autre fichier objet, il remplit l'adresse de chaque symbole partout où il est utilisé par un autre objet. Dans un cas typique, il construira également une table de toutes les adresses absolues utilisées, de sorte que le chargeur peut/corrigera les adresses lorsque le fichier est chargé (c'est-à-dire qu'il ajoutera l'adresse de chargement de base à chacune de ces adresses). adresses afin qu'elles se réfèrent toutes à la bonne adresse mémoire).
De nombreux éditeurs de liens modernes peuvent également exécuter (dans certains cas, lot) d'autres "trucs", tels que l'optimisation du code d'une manière qui n'est possible qu'une fois que tous les modules sont visibles ( par exemple, supprimer les fonctions qui étaient incluses parce qu'il était possible qu'un autre module pourrait les appeler, mais une fois que tous les modules sont assemblés, il est évident que rien ne les appelle jamais).