Exécution du programme (6/11)

Il existe deux types d'exécution de programme : l'une est basée sur l'environnement du système d'exploitation et l'autre consiste à exécuter des programmes sans système d'exploitation dans un environnement sans système d'exploitation. Dans l'environnement Linux, le fichier exécutable est au format ELF (en plus du segment de code de base, du segment de données, de l'en-tête du fichier, de la table des symboles et d'autres informations utilisées pour aider l'exécution du programme), le programme exécuté dans l'environnement nu est généralement Au format BIN/HEX, ce sont de purs fichiers d'instructions.

Bien que les environnements d'exécution des deux programmes soient différents et que les formats de fichiers soient également différents, les principes sont les mêmes : les instructions doivent être chargées à l'emplacement spécifié de la mémoire, et cet emplacement spécifié est lié à l'adresse du lien lors de l'exécution du lien de fichier.

Exécution du programme sous environnement de système d'exploitation

Lorsqu'un système informatique doté d'un système d'exploitation exécute une application, il exécutera d'abord un programme appelé chargeur. Le chargeur chargera le fichier exécutable de la ROM dans la mémoire en fonction des informations sur le chemin d'installation du logiciel, puis effectuera certaines opérations. liés à l'initialisation et à la relocalisation dynamique de la bibliothèque, et enfin passer au point d'entrée du programme à exécuter. Lors de l'exécution d'une application en mode ligne de commande Linux, un programme de terminal Shell comme sh ou bash agit comme un chargeur : il charge le programme en mémoire, l'encapsule dans un processus et participe à la planification et à l'exécution du système d'exploitation.

Un fichier exécutable peut être composé de différentes sections, notamment des segments de code, des segments de données, des segments BSS, etc. Lorsque le chargeur est en cours d'exécution, le chargeur chargera ces segments de code et segments de données à différents emplacements de la mémoire. L'en-tête du fichier exécutable fournit des informations de base telles que le type de fichier, la plate-forme en cours d'exécution et l'adresse d'entrée du programme. Avant de charger le programme, le chargeur effectuera d'abord quelques jugements en fonction des informations d'en-tête du fichier. S'il s'avère que la plate-forme en cours d'exécution du programme ne correspond pas à l'environnement actuel, une erreur sera signalée.

De plus, il existe un segment dans le fichier exécutable appelé table d'en-tête de segment. La table d'en-tête de segment enregistre les informations pertinentes sur la façon de charger le fichier exécutable dans la mémoire, y compris les segments du fichier exécutable à charger dans la mémoire. adresse d'entrée et autres informations. Dans un fichier exécutable, le chargeur doit charger le programme en mémoire et s'appuie sur les informations fournies par la table d'en-tête de segment, la table d'en-tête de segment est donc nécessaire.

jiaming@jiaming-pc:~/Documents/CSDN_Project$ readelf -l a.out 

Elf file type is EXEC (Executable file)
Entry point 0x1030c
There are 9 program headers, starting at offset 52

Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  EXIDX          0x000718 0x00010718 0x00010718 0x00008 0x00008 R   0x4
  PHDR           0x000034 0x00010034 0x00010034 0x00120 0x00120 R   0x4
  INTERP         0x000154 0x00010154 0x00010154 0x00013 0x00013 R   0x1
      [Requesting program interpreter: /lib/ld-linux.so.3]
  LOAD           0x000000 0x00010000 0x00010000 0x00724 0x00724 R E 0x10000
  LOAD           0x000f10 0x00020f10 0x00020f10 0x00118 0x0011c RW  0x10000
  DYNAMIC        0x000f18 0x00020f18 0x00020f18 0x000e8 0x000e8 RW  0x4
  NOTE           0x000168 0x00010168 0x00010168 0x00044 0x00044 R   0x4
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0x10
  GNU_RELRO      0x000f10 0x00020f10 0x00020f10 0x000f0 0x000f0 R   0x1

 Section to Segment mapping:
  Segment Sections...
   00     .ARM.exidx 
   01     
   02     .interp 
   03     .interp .note.gnu.build-id .note.ABI-tag .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .text .fini .rodata .ARM.exidx .eh_frame 
   04     .init_array .fini_array .dynamic .got .data .bss 
   05     .dynamic 
   06     .note.gnu.build-id .note.ABI-tag 
   07     
   08     .init_array .fini_array .dynamic 

Les programmes exécutés dans l'environnement Linux sont généralement encapsulés dans des processus et participent à la planification et au fonctionnement unifiés du système d'exploitation. Pour exécuter un programme dans un environnement Shell, le programme du terminal Shell crée généralement un processus enfant pour créer un espace d'adressage de processus virtuel indépendant, puis appelle la fonction execve pour charger le programme à exécuter dans l'espace de processus : via l'en-tête de fichier de le fichier exécutable, rechercher L'adresse d'entrée du programme établit la relation de mappage entre l'espace d'adressage virtuel du processus et le fichier exécutable. Placez le pointeur PC sur l'adresse d'entrée du fichier exécutable pour commencer à s'exécuter. La relation correspondante entre un programme C, le fichier exécutable compilé et le processus lors de l'exécution du fichier exécutable est la suivante :

Insérer la description de l'image ici
Différents compilateurs ont des adresses de début de lien différentes. Dans l'environnement Linux, GCC commence généralement à stocker les segments de code à 0x08040000 comme adresse de départ lors de la liaison, tandis que le compilateur croisé ARM GCC utilise généralement 0x10000 comme adresse de départ de liaison. À côté du segment de code, le segment de données est stocké à partir d'une adresse alignée sur les limites de 4 Ko. À côté du segment de données se trouve le segment BSS. Le premier alignement d'adresse de 4 Ko après le segment BSS est l'espace de tas que nous appliquons pour utiliser malloc() / free() dans le programme.

Pour chaque processus en cours d'exécution, le noyau Linux utilise une structure task_struct pour le représenter, et plusieurs structures forment une liste chaînée via des pointeurs. Le système d'exploitation peut gérer, planifier et exécuter ces processus en fonction de cette liste chaînée. Les segments de code et les segments de données de différents processus sont stockés dans différentes pages physiques de la mémoire physique. Les processus sont indépendants les uns des autres. Grâce au changement de contexte, ils occupent à tour de rôle le CPU pour exécuter leurs propres instructions. Lorsque plusieurs processus s'exécutent simultanément dans l'environnement Linux, la relation correspondante entre les programmes sources C, les fichiers exécutables, les processus et la mémoire physique est la suivante :

Insérer la description de l'image ici

Programme exécuté dans un environnement nu

Sur une plate-forme nue, après la mise sous tension du système, il n'y a aucun environnement pour exécuter le programme. Il est nécessaire d'utiliser des outils tiers pour charger le programme dans la mémoire avant qu'il puisse s'exécuter normalement.

De nombreux environnements de développement intégrés, tels que ADS1.2, Keil, RVDS et autres IDE, fournissent non seulement des fonctions d'édition et de compilation de programmes, mais prennent également en charge l'exécution, le débogage et la programmation de programmes. En prenant l'environnement de développement intégré ADS1.2 comme exemple, vous pouvez communiquer avec la carte de développement via l'interface JTAG et télécharger le fichier exécutable ARM au format BIN/HEX compilé sur le PC dans la mémoire de la carte de développement pour l'exécuter. Il peut être défini en fonction de l'adresse physique RAM réelle de la carte de développement via l'option de réglage des paramètres de débogage fournie par l'environnement de développement intégré ADS1.2 lors de la compilation du programme.

Insérer la description de l'image ici

Dans un système Linux embarqué, l'exécution de l'image du noyau Linux correspond en fait à l'exécution du programme dans l'environnement nu. L'image du noyau Linux est généralement chargée depuis la partition de stockage Flash dans la mémoire et exécutée à l'aide de l'outil de chargement U-boot. u-boot joue le rôle de chargeur dans le processus de démarrage de Linux.

Analyse de la fonction main() de l'entrée du programme

Une fois que le chargeur a chargé les instructions dans la mémoire, il commence alors à exécuter le programme. La fonction main() est la fonction d'entrée de tous les programmes au sens habituel, mais l'entrée par défaut du programme est le symbole _start, et non main. Ce dernier est juste une convention. .

Avant l'exécution de la fonction principale, de nombreuses opérations d'initialisation ont été effectuées : elles effectuent principalement un travail d'initialisation avant d'exécuter la fonction principale, comme l'initialisation du pointeur de pile, l'initialisation du contenu du segment de données, parmi les variables globales initialisées, tous les types int sont initialisés à 0, les variables de type booléen sont initialisées à False et les types de pointeurs sont initialisés à NULL. Après avoir terminé l'environnement d'initialisation, cette partie du code transmettra également les paramètres transmis par l'utilisateur à main, et passera enfin à la fonction principale à exécuter.

Cette partie du code d'initialisation est automatiquement ajoutée au fichier exécutable par le compilateur lors de la phase de compilation du programme. Cette partie du code appartient au code de la bibliothèque d'exécution C (C Running Time, CRT). Lorsque le fabricant du compilateur développe le compilateur, en plus d'implémenter les fonctions standards telles que printf, fopen et fread spécifiées dans le langage C. standard, il implémentera également cette partie de l'initialisation. Le code effectue une série d'opérations d'initialisation avant d'entrer dans la fonction principale.

  • L'environnement de pile de base et l'environnement de processus pour le fonctionnement du langage C.
  • Chargement, libération, initialisation, nettoyage, etc. de bibliothèques dynamiques.
  • Transmettez les paramètres argc et argv à la fonction principale et appelez la fonction principale pour exécution.
  • Une fois la fonction principale terminée, appelez la fonction exit pour terminer le processus.

Dans le répertoire lib sous le chemin d'installation du compilateur croisé ARM, vous verrez crt1.ole fichier objet. Ce fichier est compilé et généré par le code d'initialisation de l'assembly et fait partie du CRT. Au cours du processus de liaison, l'éditeur de liens assemblera le fichier objet crt1.o et les objectifs du projet pour générer le fichier exécutable final.

Segment BSS

Pour les variables globales non initialisées et les variables locales statiques, le compilateur les place dans la section BSS. Le segment BSS n'occupe pas l'espace de stockage du fichier exécutable. Le but de la définition du segment BSS est de réduire la taille du fichier exécutable et d'économiser de l'espace disque.

Bien que le segment BSS n'occupe pas d'espace de stockage dans le fichier exécutable, lorsque le programme est chargé en mémoire et exécuté, le chargeur alloue un espace de stockage dans la mémoire pour le segment BSS. La taille du segment BSS est enregistrée dans la table des segments, et l'adresse et la taille de chaque variable sont enregistrées dans la table des symboles. Sur la base de ces informations, le chargeur allouera un espace mémoire d'une taille spécifiée derrière le segment de données et l'effacera, et allouera de l'espace de stockage à chaque variable globale non initialisée et variable statique dans cette mémoire en fonction de l'adresse de chaque variable dans la table des symboles. .

Je suppose que tu aimes

Origine blog.csdn.net/weixin_39541632/article/details/132228915
conseillé
Classement