Que font les linkers?


127

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 les références aux bibliothèques et aux frameworks sont ajoutées au binaire. Je ne comprends rien d'autre. 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?

Réponses:


160

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 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 d'octet objet. Ce code d'octet (parfois appelé code objet) est des instructions mnémotechniques 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. Le plus souvent, 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 se composerait de:

printf("Hello Kristina!\n");

Lorsque le compilateur a compilé votre programme dans un fichier obj, il met simplement une référence à la printffonction. 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 avec 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 le copier-coller d'un traitement de texte. Il "copie" toutes les fonctions nécessaires auxquelles votre programme fait référence et crée un seul exécutable. Parfois, d'autres bibliothèques copiées dépendent encore d'autres fichiers OBJ ou de bibliothèque. Parfois, un éditeur de liens doit devenir 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 rassemblent 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 des choses appelées Overlays (fichiers .OVL). Cela avait de nombreux objectifs, mais l'un était de conserver les fonctions couramment utilisées ensemble dans un fichier (un autre objectif, au cas où vous vous poseriez la question, était de pouvoir insérer de grands programmes en mémoire. DOS a une limitation de la mémoire et les superpositions pourraient ê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 fondamentalement la même idée que les DLL (les gars de Linux du noyau dur que je connais me diraient qu'il y a BEAUCOUP DE GRANDES différences).

J'espère que cela vous aide à comprendre!


9
Très bonne réponse. De plus, la plupart des éditeurs de liens modernes supprimeront le code redondant comme les instanciations de modèles.
Edward Strange

1
Est-ce un endroit approprié pour passer en revue certaines de ces différences?
John P

2
Bonjour, Supposons que mon fichier ne fasse référence à aucun autre fichier. Supposons que je déclare et initialise simplement deux variables. Ce fichier source ira-t-il également dans l'éditeur de liens?
Mangesh Kherdekar

3
@MangeshKherdekar - Oui, cela passe toujours par un éditeur de liens. L'éditeur de liens peut ne pas lier des bibliothèques externes, mais la phase de liaison doit encore se produire pour produire un exécutable.
Icemanind

78

Exemple minimal de relocalisation d'adresse

La délocalisation d'adresses est l'une des fonctions cruciales de la liaison.

Voyons donc comment cela fonctionne avec un exemple minimal.

0) Présentation

Résumé: la relocalisation modifie la .textsection des fichiers objets à traduire:

  • adresse du fichier objet
  • dans l'adresse finale de l'exécutable

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:

  • résoudre les symboles non définis comme les fonctions non définies déclarées
  • pas de conflit entre plusieurs .textet .datasections de plusieurs fichiers objets

Prérequis: compréhension minimale de:

La liaison n'a rien à voir avec C ou C ++ spécifiquement: les compilateurs génèrent simplement les fichiers objets. L'éditeur de liens les prend alors comme entrée sans jamais savoir dans quel langage les compilait. Cela pourrait aussi bien être Fortran.

Alors pour réduire la croûte, étudions un NASM x86-64 ELF Linux bonjour le monde:

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.

1) .text de .o

Nous décompilons d'abord la .textsection 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 devrait déplacer l'adresse de la chaîne hello world dans le rsiregistre, 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 est chargé?

Eh bien, ce n'est pas possible, surtout après avoir lié un tas de .ofichiers avec plusieurs .datasections.

Seul l'éditeur de liens peut le faire car lui seul aura tous ces fichiers objets.

Donc, le compilateur vient de:

  • met une valeur d'espace réservé 0x0sur la sortie compilée
  • donne des informations supplémentaires à l'éditeur de liens sur la façon de modifier le code compilé avec les bonnes adresses

Ces "informations supplémentaires" sont contenues dans la .rela.textsection du fichier objet

2) .rela.text

.rela.text signifie "relocalisation de la section .text".

Le mot relocation est utilisé car l'éditeur de liens devra déplacer l'adresse de l'objet vers l'exécutable.

Nous pouvons démonter la .rela.textsection 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é à l' adresse : 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.

Pour simplifier un peu, pour cette ligne particulière, nous avons les informations suivantes:

  • Offset = C: quel est le premier octet de la .textmodification de cette entrée.

    Si nous regardons en arrière le texte décompilé, il est exactement à l'intérieur du critique movabs $0x0,%rsi, et ceux qui connaissent le codage des instructions x86-64 remarqueront que cela encode la partie adresse 64 bits de l'instruction.

  • Name = .data: l'adresse pointe vers la .datasection

  • Type = R_X86_64_64, qui spécifie exactement quel calcul doit être effectué pour traduire l'adresse.

    Ce champ dépend en fait du processeur et est donc documenté sur l' extension AMD64 System V ABI section 4.4 «Relocation».

    Ce document dit que R_X86_64_64:

    • Field = word64: 8 octets, donc l' 00 00 00 00 00 00 00 00adresse at0xC

    • Calculation = S + A

      • Sest la valeur à l'adresse à déplacer, donc00 00 00 00 00 00 00 00
      • Aest l'addend qui est 0ici. Il s'agit d'un champ de l'entrée de réinstallation.

      Donc S + A == 0et nous allons être relocalisés à la toute première adresse de la .datasection.

3) .text de .out

Regardons maintenant la zone de texte de l'exécutable ldgé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 pointent maintenant vers l'adresse 0x6000d8( d8 00 60 00 00 00 00 00en petit-boutiste) au lieu de 0x0.

Est-ce le bon emplacement pour la hello_worldchaîne?

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 .datasection, qui est la seconde, commence à VirtAddr= 0x06000d8.

Et la seule chose sur la section de données est notre chaîne hello world.

Niveau bonus


1
Mec, tu es génial. Le lien vers le didacticiel «Structure globale d'un fichier ELF» est rompu.
Adam Zahran du

1
@AdamZahran merci! URL de pages GitHub stupides qui ne peuvent pas gérer les barres obliques!
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

15

Dans des langages comme `` C '', les modules de code individuels sont traditionnellement compilés séparément en blobs de code objet, qui est prêt à s'exécuter à tous égards autre que toutes les références que le module fait en dehors de lui-même (c'est-à-dire à des bibliothèques ou à d'autres modules) ont pas encore été résolu (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 est de regarder tous les modules ensemble, de regarder ce dont chaque module a besoin pour se connecter à l'extérieur de lui-même, et de regarder toutes les choses 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 n'est toujours pas capable d'ê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 au moment où il charge l'application (ou peut-être même plus tard pendant la course).


Il est intéressant de noter que certains assembleurs ou compilateurs peuvent produire un fichier exécutable directement si le compilateur "voit" tout ce qui est nécessaire (généralement dans un seul fichier source plus tout ce qu'il #inclut). Quelques compilateurs, généralement pour les petits micros, ont cela comme leur seul mode de fonctionnement.
supercat du

Oui, j'ai essayé de donner une réponse intermédiaire. Bien sûr, comme dans votre cas, l'inverse est également vrai, dans la mesure où certains types de fichiers objets n'ont même pas la génération complète du code; c'est fait par l'éditeur de liens (c'est ainsi que fonctionne l'optimisation du programme entier de MSVC).
Will Dean

@WillDean et l'optimisation du temps de liaison de GCC, autant que je sache - il diffuse tout le «code» en tant que langage intermédiaire GIMPLE avec les métadonnées requises, le rend disponible pour l'éditeur de liens et l'optimise en une seule fois à la fin. (Malgré ce que la documentation obsolète implique, seul GIMPLE est maintenant diffusé par défaut, plutôt que l'ancien mode `` gros '' avec les deux représentations du code objet.)
underscore_d

10

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 à des symboles qui ne sont pas définis dans ce fichier objet. L'éditeur de liens les prend et les assemble afin que (lorsque tout fonctionne correctement) 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 / va "réparer" les adresses lorsque le fichier est chargé (c'est-à-dire qu'il ajoutera l'adresse de chargement de base à chacun de ces adresses afin qu'elles renvoient toutes à l'adresse mémoire correcte).

De nombreux éditeurs de liens modernes peuvent également effectuer certains (dans certains cas, beaucoup ) 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 des fonctions qui ont été incluses car il était possible qu'un autre module puisse les appeler, mais une fois que tous les modules sont assemblés, il est évident que rien ne les appelle jamais).

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.