web-dev-qa-db-fra.com

Que font les éditeurs de liens?

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?

105
Kristina Brooks

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!

134
Icemanind

Exemple minimal de relocalisation d'adresse

La relocalisation des adresses est l'une des fonctions cruciales de la liaison.

Voyons donc comment cela fonctionne avec un exemple minimal.

0) Introduction

Résumé: la relocalisation modifie la section .text 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
  • ne pas heurter plusieurs sections .text et .data de plusieurs fichiers objets

Pré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.

1) .text de .o

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:

  • met une valeur d'espace réservé 0x0 sur 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 section .rela.text Du fichier objet

2) .rela.text

.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.

3) .text de .out

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.

Niveau bonus

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).

15
Will Dean

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).

10
Jerry Coffin