Notions de base sur l'éditeur de liens

 

Parfois, vous pouvez apprendre des connaissances, mais pas du temps. -Zhong Yunlong


Basique: https://blog.csdn.net/qq_35865125/article/details/105214201


Présentation

Dans le système de compilation, l'éditeur de liens joue un rôle similaire à "glue". Il colle et épissure le fichier objet déplaçable généré par l'assembleur en un fichier ELF exécutable. Cependant, l'éditeur de liens n'épissure pas mécaniquement le fichier objet. Il doit également terminer l'allocation d'adresse de segment, le calcul d'adresse de symbole et la correction du contenu des données / instructions qui ne peuvent pas être exécutées lors de l'assemblage.

Ces trois tâches principales impliquent le processus de base du travail de l'éditeur de liens: l'allocation d'espace d'adressage, la résolution des symboles et la relocalisation.


Dans chaque entrée de la table d'en-tête de section du fichier objet déplaçable, l'adresse virtuelle de la section est définie sur 0 par défaut. En effet, il est impossible de connaître l'adresse de chargement du segment lors de l'étape de traitement de l'assembleur. L'objectif principal de l'opération d'allocation d'espace d'adressage de l'éditeur de liens est de spécifier l'adresse de chargement du segment ( c'est-à-dire de déterminer où chaque section du fichier cible est placée dans le fichier exécutable ).

 

Après avoir déterminé l'adresse de chargement de la section (appelée adresse de base de la section), l'adresse du symbole dans le fichier exécutable peut être calculée en fonction de l'adresse de décalage du symbole dans le fichier cible ( appelée adresse de symbole, telle que l'adresse de la fonction définie ) . L'opération de résolution de symbole de l'éditeur de liens ne se limite pas au calcul de l'adresse du symbole. Il doit également analyser la référence de symbole entre les fichiers cibles et calculer l'adresse du symbole externe référencé dans le fichier cible.

 

Après la résolution du symbole, les adresses symboliques (par exemple: adresses dans le fichier exécutable) de tous les fichiers cibles ont été déterminées. L'éditeur de liens corrige l'adresse symbolique référencée dans le segment de code ou le segment de données via l'opération de relocalisation ( par exemple. Le segment de code a l'appel printf, et le printf doit être modifié à l'adresse de la fonction ) .

 

Enfin, l'éditeur de liens exporte les informations de fichier traitées par les opérations ci-dessus en tant que fichier ELF exécutable pour terminer le travail de liaison.

 

Collecte d'informations

Pour l'éditeur de liens, l'entrée est une série de fichiers d'objets déplaçables. Pour terminer le travail de suivi, l'éditeur de liens doit analyser les fichiers cibles un par un et extraire les informations requises pour le traitement.

L'éditeur de liens doit analyser les références des symboles dans le fichier objet. La raison de l'analyse des informations de référence du symbole est que dans un fichier objet traité par l'éditeur de liens, il existe des symboles non définis, c'est-à-dire des références à des symboles d'autres fichiers objet. Afin de faciliter le traitement de la résolution des symboles de l'éditeur de liens, deux jeux de symboles sont généralement définis: l'un est un jeu de symboles d'exportation, qui représente tous les jeux de symboles globaux définis dans tous les fichiers cible qui peuvent être référencés par d'autres cibles; l'autre est un jeu de symboles d'importation, qui représente les fichiers cible Il n'est pas défini en interne et doit faire référence au jeu de symboles d'autres fichiers objets.

 

Allocation d'espace d'adressage

Lorsque l'assembleur génère le fichier objet, car l'adresse de chargement du segment ne peut pas être déterminée, l'adresse de base du segment est enregistrée comme 0 par défaut. La première étape de l'éditeur de liens consiste à déterminer l'adresse de base de segment du segment à charger. Le processus de spécification de l'adresse de base de segment pour le segment à charger est appelé allocation d'espace d'adressage.

L'éditeur de liens spécifie l'adresse de base pour le segment, qui doit être considérée sous trois aspects.

1) Adresse de début du chargement du segment.

      Cette adresse est la position de départ de tous les segments de charge. Dans les systèmes Linux 32 bits, elle est généralement définie sur 0x08048000.

2) La séquence d'épissage des segments.

     L'éditeur de liens analyse séquentiellement les segments du même nom dans chaque fichier cible et «place» les données binaires des segments en séquence.

3) Alignement des segments.

      L'alignement de segment comprend deux niveaux: l'alignement du décalage du fichier de segment et l'alignement de l'adresse de base du segment.

Dans le fichier cible déplaçable, l'alignement de décalage de fichier du segment est généralement défini sur 4 octets, quel que soit l'alignement de l'adresse de base du segment (l'adresse de base du segment est 0, il n'y a aucune signification d'alignement).

Dans le fichier exécutable, l'alignement de décalage de fichier du segment de code ".text" est défini sur 16 octets, et l'alignement de décalage de fichier des autres segments est toujours de 4 octets par défaut. L'alignement de l'adresse de base du segment est plus compliqué. Il faut s'assurer que l'adresse linéaire du segment et le décalage de fichier correspondant du segment sont modulo égaux à la valeur d'alignement du segment (c'est-à-dire la taille de la page, qui est de 4096 octets par défaut sous Linux).

( Le champ d'alignement de segment dans la table d'en-tête du programme p_align: p_ align indique l'alignement du segment, la règle d'alignement est p_ vaddr% p_ align = 0, c'est-à-dire que l'adresse linéaire du segment doit être un multiple entier de p_ align. En général, p_ align prend la valeur 0x1000 = 4096, qui est la taille de page par défaut du système d'exploitation Linux ).

 

La figure suivante montre un exemple d'allocation d'espace d'adressage. La taille de segment de code du fichier cible ao est 0x4a octets, la taille de segment de données est 0x08 octets, la taille de segment de code bo est 0x21 octets et la taille de segment de données est 0x04 octets.

 

-Aucune table d'en-tête de section n'est requise dans le fichier exécutable, elle n'est requise que dans le fichier objet.

 

Résolution des symboles

La table des symboles du fichier cible stocke le décalage de chaque symbole défini par rapport à l'adresse de base du segment. Lorsque l'espace d'adressage du segment est alloué, l'adresse de base de chaque segment est déterminée, de sorte que l'adresse du symbole peut être calculée à l'aide de la formule suivante:

Adresse du symbole = adresse de base du segment + décalage du symbole par rapport à l'adresse de base du segment

Cependant, avant de calculer l'adresse symbolique, certains travaux de préparation sont encore nécessaires.

Tout d'abord, vous devez analyser la table des symboles dans le fichier cible pour obtenir la définition et les informations de référence des symboles, c'est-à-dire le jeu de symboles exporté et le jeu de symboles importés décrits ci-dessus.

Deuxièmement, il est nécessaire de vérifier la légalité du jeu de symboles importé et du jeu de symboles exporté . La vérification des symboles comprend deux aspects:

1) Redéfinition des symboles: c'est-à-dire qu'un symbole du même nom existe dans le jeu de symboles exporté. Lorsque le fichier cible est lié, le symbole est traité par récupération de nom et la redéfinition du symbole empêchera le fichier qui fait référence au symbole de déterminer quel symbole doit être utilisé spécifiquement.

2) Le symbole n'est pas défini: le jeu de symboles importé contient des symboles qui n'existent pas dans le jeu exporté. Lorsque le symbole externe référencé par le fichier objet ne peut pas trouver la définition correspondante dans d'autres fichiers objet, l'adresse du symbole ne peut pas être déterminée. Une fois le symbole redéfini ou indéfini, le travail du lieur ne peut pas continuer.

 

Remarque:

Il y a une grande différence entre le fichier cible et le fichier exécutable: le champ d'entrée du programme e_ entrée de l'en-tête du fichier du fichier cible est 0, et le point d'entrée du programme du fichier exécutable est une adresse linéaire. Nous devons supposer que l'adresse d'entrée du programme est enregistrée dans un symbole nommé "@start". Évidemment, ce symbole ne peut pas être le nom de symbole généré par le compilateur. Afin de s'assurer que l'éditeur de liens puisse trouver le point d'entrée du programme, la phase de vérification de référence de symbole doit être forcée d'exporter le symbole "@start" . Quant au fournisseur du symbole "@start", il peut être supposé temporairement provenir d'un fichier objet existant.

 

De manière générale, la résolution d'adresse symbolique est divisée en deux étapes:

1) Scannez les symboles locaux de tous les fichiers cibles ELF pour calculer l'adresse des symboles locaux.

2) Analysez tous les symboles de l'ensemble importé (c'est-à-dire qu'un fichier doit utiliser les symboles définis par d'autres fichiers cibles) et passez l'adresse du symbole à la table des symboles du fichier cible qui fait référence au symbole.

 

Délocalisation

 

( https://blog.csdn.net/qq_35865125/article/details/105214201

Les symboles qui doivent être déplacés sont stockés dans la table de déplacement dans chaque fichier cible, correspondant à la section nommée " .rel" au début. Les sections où les fichiers ELF doivent être déplacés correspondent généralement à une table de relocalisation. Par exemple, la section de code, c'est-à-dire la table de relocalisation de la sectioin ". Text" est stockée dans la section ". Rel. Text", et la table de relocalisation de ". Data" est stockée dans ". Rel. Data")

 

Les informations de relocalisation du fichier cible contiennent trois éléments clés:

#Relocation symbol -which symbol address is used for relocation ;-( in the relocation table in each target file)

#Relocation location - where to relocate; ( Ces informations peuvent également être obtenues à partir de la table de relocalisation du fichier cible, qui stocke le nom du symbole qui doit être déplacé, et enregistre également à quelle section du fichier cible le symbole appartient , Et le décalage dans cette section, après la liaison pour terminer l'allocation de l'espace d'adressage, l'adresse de cette section dans le fichier cible est également déterminée, de sorte que la position du symbole dans le fichier exécutable peut être localisée en fonction du décalage ).

#Relocation type - quelle méthode utiliser pour la relocalisation.

 

Premièrement, étant donné que l'opération de relocalisation repose sur l'adresse du symbole déplacé, elle ne peut pas être déplacée tant que la résolution du symbole n'est pas terminée.

 

Il existe deux types de relocalisation:

Relocalisation d'adresse absolue et relocalisation d'adresse relative. La correction des données de segment en fonction des différents types de relocalisation est au cœur de la relocalisation.

1) L'opération de relocalisation d'adresse absolue est relativement simple, où la relocalisation d'adresse absolue est généralement dérivée d'une référence directe à l'adresse du symbole. Comme l'assembleur ne peut pas déterminer l'adresse virtuelle du symbole, le symbole de référence est finalement rempli de 0 comme espace réservé Adresse. Par conséquent, l'opération de relocalisation d'adresse absolue n'a qu'à renseigner directement l'adresse virtuelle du symbole de relocalisation à la position de relocalisation.

Adresse de relocalisation absolue = adresse du symbole de relocalisation

 

2) La relocalisation d'adresse relative est un peu plus compliquée. L'endroit où une relocalisation d'adresse relative est nécessaire est généralement dérivé de l'instruction d'adresse de saut référençant l'adresse symbolique d'autres fichiers .

Bien que l'assembleur ne puisse pas déterminer l'adresse virtuelle du symbole référencé, il n'utilise pas 0 comme espace réservé pour remplir l'adresse du symbole de référence, mais utilise la "position de décalage par rapport à l'adresse de l'instruction suivante" pour remplir la position. Lorsque l'éditeur de liens effectue une opération de relocalisation d'adresse relative, il calcule le décalage de l'adresse de symbole par rapport à la position de relocalisation, puis ajoute le décalage au contenu stocké dans la position de relocalisation.

Adresse de réinstallation relative = adresse du symbole de réinstallation - emplacement de réinstallation + contenu des données de l'emplacement de réinstallation

                           = (Symbole de relocalisation adresse-position de relocalisation) + (position de relocalisation-adresse d'instruction suivante)

                           = Adresse du symbole de relocalisation - prochaine adresse d'instruction

Selon le calcul ci-dessus, il peut être clairement vu que l'adresse de relocalisation relative calculée finale est le décalage de l'adresse du symbole par rapport à l'adresse de l'instruction suivante, et elle est également conforme aux exigences de l'instruction de saut pour l'opérande . Quant à savoir pourquoi un tel calcul "lourd" de la relocalisation d'adresse relative, l'auteur estime que de cette manière, pour des instructions de différentes longueurs et structures de conception, tant que les données de la position de relocalisation sont corrigées selon la méthode de l'adresse relative, puis l'adresse de relocalisation relative La méthode de calcul est inchangée, la différence n'est que la valeur des données à la position de relocalisation. Par exemple, pour les instructions de saut Intel 32 bits, la valeur des données de position est –4, pour les instructions de saut Intel 64 bits, la valeur des données de position est –8.

 

Ce qui suit décrit le processus de réinstallation avec un exemple.

 

 

Point d'entrée du programme et bibliothèque d'exécution

Comme mentionné dans la section précédente, l'adresse du point d'entrée du programme est stockée dans un symbole spécial nommé "@start" et le fichier objet qui définit le symbole n'est pas généré par le compilateur basé sur le code source. Ensuite, deux problèmes doivent être clarifiés:

1) Pourquoi introduire de nouveaux symboles au lieu de la fonction principale comme point d'entrée du programme?

2) Comment puis-je obtenir le fichier cible qui définit le nouveau symbole?

Expliquez d'abord la première question. La forme des fragments de code assembleur générés pour la fonction principale est la suivante:

 

Essentiellement, la fonction principale n'est pas très différente des fonctions ordinaires: elle contient le code de pile de fonctions (lignes 3 ~ 5), le code de corps de fonction (ligne 6 omis) et le code de pile de fonctions (lignes 7 ~ 9) OK). En supposant que la fonction principale est utilisée comme point d'entrée du programme, c'est-à-dire que l'adresse linéaire du symbole principal est écrite dans le champ e_entry dans l'en- tête du fichier ELF , puis après le chargement et l'exécution du programme, l'instruction sera lue à partir de la position d'adresse du symbole principal pour démarrer l'exécution. Il n'y aura pas de problèmes pendant l'exécution de la fonction principale jusqu'à l'exécution de l'instruction ret. Selon la sémantique de l'instruction ret , le programme prendra les données 32 bits du haut de la pile comme adresse de retour, puis passera à cette adresse pour continuer l'exécution ! Cependant, avant que le programme n'exécute la fonction principale, les données stockées en haut de la pile sont inconnues, donc le comportement final du programme ne peut pas être prévu. La conséquence la plus courante est de déclencher le processus "SegmentFault".

Par conséquent, pour que le programme se termine correctement, un appelant de la fonction principale doit être construit pour terminer le travail de "nettoyage" après l'appel de fonction. Cela fournit également une solution au deuxième problème.

Dans l'appel système de Linux, l'appel système portant le numéro d'appel 1 est exit. L'utilisation d'exit peut entraîner la fermeture normale du processus. Le code d'assembly pour appeler exit est indiqué sur les lignes 6 à 8, où le registre eax contient le numéro d'appel système de sortie 1, ebx contient le paramètre d'appel système de sortie 0 et l'instruction int déclenche l'appel système de sortie pour quitter le processus. Le code au symbole "@start" appellera la fonction principale et utilisera l'appel système exit pour quitter le processus. Avant et après l'appel de la fonction principale, vous pouvez effectuer un travail d'initialisation (ligne 3 omis) et de nettoyage (ligne 5 omis).

Si le compilateur enregistre le code ci-dessus dans start.s, après traitement par l'assembleur, le fichier objet start.o peut être obtenu. Ensuite, utilisez l'outil readelf pour afficher la table des symboles de start.o:

 

Du point de vue du flux de travail de l'ensemble du système de compilation, le fichier start.o est le fichier cible nécessaire au fonctionnement normal du système de compilation. Quelle que soit la façon dont le code source traité par le système de compilation est défini, start.o et les autres fichiers objet doivent être liés ensemble lors de la dernière étape de liaison pour générer normalement un fichier exécutable. Pour un tel fichier objet, il existe un nom unifié - «bibliothèque d'exécution de langue» . Évidemment, start.o devrait être la bibliothèque d'exécution la plus simple, elle est uniquement responsable du guidage et de l'appel de la fonction principale, et ne fait rien d'autre.

 

  ------ Grande perspicacité -----------

Selon une méthode similaire, les fonctions de la bibliothèque d'exécution du langage de programmation peuvent être facilement étendues .

Par exemple, vous pouvez définir printf.s pour implémenter la fonction de sortie standard printf et générer le fichier objet printf.o après traitement par l'assembleur. Tant que la déclaration de code source utilise la fonction printf, liez printf.o au fichier exécutable lors de la liaison, puis la fonction de sortie standard peut être réalisée dans le langage de haut niveau. En outre, le fichier math.c peut être directement défini pour implémenter des fonctions liées aux mathématiques, et le fichier objet math.o peut être généré après traitement par le compilateur et l'assembleur, de sorte que les langages de haut niveau puissent effectuer des calculs mathématiques complexes.

Si le préprocesseur est implémenté dans le système de compilation et prend en charge les instructions d'inclusion, les instructions de déclaration de fonction telles que la fonction printf ou math.c peuvent être placées dans un fichier d'en-tête comme "stdio.h" ou "math.h" .  

Si l'éditeur de liens prend en charge les fichiers d'entrée au format de package compressé, les fichiers objets tels que printf.o et math.o peuvent être empaquetés et placés dans un package compressé (bibliothèque) comme "libc.a" . L'éditeur de liens doit uniquement se lier avant Décompressez le package compressé. Lors de l'écriture de programmes linguistiques de haut niveau, tant que les fichiers d'en-tête requis sont inclus et que les fichiers de bibliothèque correspondants sont inclus dans la phase de liaison , des fonctionnalités linguistiques plus puissantes peuvent être utilisées.

 

 

En comparaison, la bibliothèque d'exécution C de GCC (CRT) est beaucoup plus compliquée. Rappelons l'exemple du chapitre 1:

(Lorsqu'il est lié statiquement, GCC copiera cinq fichiers objets importants crt1.o, crti.o, crtbeginT.o, crtend.o, crtn.o et trois bibliothèques statiques libgcc.a dans la bibliothèque d'exécution en langage C (CRT). , Libgcc_ eh. A, libc. Un lien vers le fichier exécutable bonjour. )

 

5 fichiers cibles crt1.o, crti.o, crtbeginT.o, crtend.o, crtn.o et 3 bibliothèques statiques libgcc.a, libgcc_eh.a, libc.a impliquées dans la description du flux de travail du lien statique GCC Les fonctions de ces fichiers sont:

1) crt1.o: Définissez le point d'entrée du programme "_start", appelez le code ".init" pour exécuter l'initialisation du programme, appelez la fonction principale et appelez le code ".finit" pour effectuer le nettoyage du programme. La version antérieure était crt0.o et les sections ".init" et ".finit" n'étaient pas prises en charge.

2) crti.o: définir la fonction de la section ".init" dans le code de pile, appeler le code de construction global C ++.

3) crtn.o: définir la fonction de la section ".finit" du code de pile, appeler le code destructeur global C ++.

4) crtbeginT.o: définir le code de construction global C ++.

5) crtend.o: définir le code destructeur global C ++.

6) libc.a: Définissez le code de bibliothèque standard du langage C. --- Ce devrait être un fichier prêt à l'emploi de gcc. Apportez-le lorsque vous installez gcc. Pour utiliser les fonctions, #incluez les fichiers d'en-tête correspondants dans le code. Le but des fichiers d'en-tête est uniquement de déclarer l'existence de fonctions. Pendant la phase de liaison, l'éditeur de liens obtient ces fonctions de libc.a. Hé! http://www.delorie.com/djgpp/doc/libc/libc_1.html

7) libgcc.a: Définissez le code de la fonction auxiliaire en raison des différences de plate-forme.

8) libgcc_eh.a: définit le code lié à la plate-forme pour la gestion des exceptions C ++.

On peut voir que, pour un langage de haut niveau, en plus du compilateur, l'assembleur et l'éditeur de liens sont des parties essentielles, la bibliothèque d'exécution du langage est également une partie indispensable . La bibliothèque d'exécution riche en fonctionnalités peut rendre l'expression de langages de haut niveau plus puissante.

 

Génération de fichiers ELF

 

 


Réf:

Fan Zhidong; Zhang Qiongsheng. "Construire un système de compilation par soi-même: compilation, compilation et liaison" Presses de l'industrie des machines.

Publié 374 articles originaux · 95 éloges · 260 000+ vues

Je suppose que tu aimes

Origine blog.csdn.net/qq_35865125/article/details/105458421
conseillé
Classement