54 votes

En quoi le C et l'Assembleur compilent-ils réellement ?

J'ai découvert que les programmes C(++) ne sont pas compilés en "binaire" pur (j'ai peut-être mal compris certaines choses, dans ce cas je suis désolé :D) mais en une série de choses (table de symboles, trucs liés aux os,...) mais...

  • Est-ce que l'assembleur "compile" en binaire pur ? Cela signifie qu'il n'y a pas d'éléments supplémentaires en dehors des ressources comme les chaînes prédéfinies, etc.

  • Si le C se compile en autre chose qu'un simple binaire, comment ce petit chargeur de démarrage assembleur peut-il simplement copier les instructions du disque dur vers la mémoire et les exécuter ? Je veux dire que si le noyau du système d'exploitation, qui est probablement écrit en C, compile en quelque chose d'autre qu'un simple binaire, comment le chargeur de démarrage le gère-t-il ?

edit : Je sais que l'assembleur ne "compile" pas parce qu'il n'a que le jeu d'instructions de votre machine - je n'ai pas trouvé de bon mot pour décrire ce que l'assembleur "assemble". Si vous en avez un, laissez-le ici en commentaire et je le changerai.

2 votes

Le chargeur de démarrage n'est que du code machine sans les en-têtes binaires et autres éléments que le système d'exploitation utilise lorsqu'il charge un binaire en mémoire. L'assembleur, le C et le C++ sont tous compilés (la plupart du temps) en binaires. En d'autres termes, ils peuvent être "emballés" différemment.

0 votes

@lamas, où avez-vous trouvé ça ? Mon livre C : The Complete Reference, 4e édition, par Herbert Schildt, que je viens d'acheter, dit qu'il compile en binaire. La norme ANSI pour C devrait clarifier la question. Malheureusement, je n'en ai pas d'exemplaire.

9 votes

Le livre de @Geoffey Schildt a la réputation d'être le pire livre technique jamais écrit - il est truffé d'erreurs et de contre-vérités.

54voto

Norman Ramsey Points 115730

Le C se compile généralement en assembleur, simplement parce que cela rend la vie facile au pauvre auteur du compilateur.

Le code assembleur s'assemble toujours (et non pas "compile") en code objet déplaçable . Vous pouvez considérer cela comme du code machine binaire et des données binaires, mais avec beaucoup de décoration et de métadonnées. Les éléments clés sont :

  • Le code et les données apparaissent dans des "sections" nommées.

  • Les fichiers d'objets déplaçables peuvent inclure des définitions de étiquettes qui font référence à des emplacements dans les sections.

  • Les fichiers d'objets déplaçables peuvent inclure des "trous" qui doivent être remplis avec les valeurs d'étiquettes définies ailleurs. Le nom officiel d'un tel trou est un entrée de délocalisation .

Par exemple, si vous compilez et assemblez (mais ne liez pas) ce programme

int main () { printf("Hello, world\n"); }

vous risquez de vous retrouver avec un fichier d'objets relocalisables avec

  • A text section contenant le code machine pour main

  • Une définition d'étiquette pour main qui pointe vers le début de la section de texte

  • A rodata (données en lecture seule) section contenant les octets de la chaîne littérale "Hello, world\n"

  • Une entrée de relocalisation qui dépend de printf et qui indique un "trou" dans une instruction d'appel au milieu d'une section de texte.

Si vous êtes sur un système Unix, un fichier d'objets relocalisables est généralement appelé un fichier .o, comme dans hello.o et vous pouvez explorer les définitions et utilisations des étiquettes avec un outil simple appelé nm et vous pouvez obtenir des informations plus détaillées grâce à un outil un peu plus compliqué appelé objdump .

J'enseigne une classe qui couvre ces sujets, et je demande aux étudiants d'écrire un assembleur et un linker, ce qui prend quelques semaines, mais quand ils l'ont fait, la plupart d'entre eux ont une assez bonne maîtrise du code objet relocalisable. Ce n'est pas une chose si facile.

2 votes

La plupart des compilateurs C compilent directement en code machine relocalisable. Il est plus rapide de sauter l'étape textuelle lente. Certains (comme les compilateurs 16 bits capables de traiter les fichiers .COM) peuvent générer directement du code non relocalisable. On pourrait cependant argumenter que dans les compilateurs générant directement du code machine, l'assembleur est une partie relativement indépendante.

7 votes

Le code déplaçable n'est pas une exigence du C, et de nombreuses plateformes ne l'utilisent pas.

0 votes

Y a-t-il un script pour votre cours disponible en ligne ?

43voto

Paul Nathan Points 22910

Prenons un programme en C.

Lorsque vous exécutez gcc , clang ou 'cl' sur le programme c, il passera par ces étapes :

  1. Préprocesseur (#include, #ifdef, analyse des trigraphes, traductions d'encodage, gestion des commentaires, macros...) incluant la lexie dans les tokens du préprocesseur et résultant éventuellement en un texte plat pour l'entrée dans le compilateur proprement dit.
  2. Analyse lexicale (production de tokens et d'erreurs lexicales).
  3. Analyse syntaxique (production d'un arbre d'analyse et d'erreurs syntaxiques).
  4. Analyse sémantique (production d'une table de symboles, d'informations sur le scoping et les erreurs de scoping/typage) Également flux de données, transformation de la logique du programme en une "représentation intermédiaire" avec laquelle l'optimiseur peut travailler. (Souvent un SSA ). clang/LLVM utilise LLVM-IR, gcc utilise GIMPLE puis RTL.
  5. L'optimisation de la logique du programme, y compris la propagation des constantes, l'inlining, l'extraction des invariants des boucles, l'auto-vectorisation, et bien d'autres choses encore. (La majeure partie du code d'un compilateur moderne largement utilisé est constituée de passes d'optimisation.) La transformation par le biais de représentations intermédiaires fait simplement partie du fonctionnement de certains compilateurs, ce qui fait qu'il n'est pas nécessaire de les transformer. impossible / inutile de "désactiver toutes les optimisations".
  6. Sortie en source d'assemblage (ou un autre format intermédiaire tel que .NET IL bytecode )
  7. Assemblage de l'assemblage dans un format d'objet binaire.
  8. L'assemblage de l'ensemble dans les bibliothèques statiques nécessaires, ainsi que sa relocalisation si nécessaire.
  9. Sortie de l'exécutable final en elf, PE/coff, MachO64, ou tout autre format.

En pratique, certaines de ces étapes peuvent être effectuées en même temps, mais c'est l'ordre logique. La plupart des compilateurs ont des options pour s'arrêter après une étape donnée (par exemple preprocess ou asm), y compris le vidage de la représentation interne entre les passes d'optimisation pour les compilateurs open-source comme GCC. ( -ftree-dump-... )

Notez qu'il y a un "conteneur" de format elf ou coff autour du binaire exécutable, sauf s'il s'agit d'un DOS. .com exécutable

Vous trouverez qu'un livre sur les compilateurs (je vous recommande le Dragon livre, l'ouvrage d'introduction standard dans le domaine) aura tous les informations dont vous avez besoin et plus encore.

Comme l'a fait remarquer Marco, la liaison et le chargement constituent un vaste domaine et le livre du Dragon s'arrête plus ou moins à la sortie du binaire exécutable. Passer de là à l'exécution sur un système d'exploitation est un processus relativement complexe, que Levine a décrit dans son livre. Linkers et chargeurs couvertures.

J'ai mis cette réponse sur wiki pour permettre aux gens de corriger les erreurs ou d'ajouter des informations.

5 votes

Hmm, le livre Dragon traite principalement de l'analyse syntaxique. Je vous recommande "Linkers and Loaders" de Levine, iecc.com/linker qui est également disponible sur le web.

0 votes

Linkers and loaders est également un bon livre.

1 votes

En fait, dans l'ordre "logique", l'analyse lexicale intervient avant le prétraitement, car le préprocesseur opère sur un flux de tokens. C'est ainsi qu'il est défini dans le standard C, et c'est également ainsi que cela se passe dans les versions modernes de gcc (lorsque le préprocesseur a été réécrit et transformé en bibliothèque de lexie).

19voto

Thomas Matthews Points 19838

La traduction de C++ en un exécutable binaire comporte différentes phases. La spécification du langage n'indique pas explicitement les phases de traduction. Cependant, je vais décrire les phases de traduction les plus courantes.

Source C++ vers langage Assembleur ou Itermédiaire

Certains compilateurs traduisent en fait le code C++ en langage d'assemblage ou en langage intermédiaire. Il ne s'agit pas d'une phase obligatoire, mais elle est utile pour le débogage et les optimisations.

De l'assemblage au code objet

L'étape suivante consiste à traduire le langage d'assemblage en un code objet. Le code objet contient du code assembleur avec des adresses relatives et des références ouvertes à des sous-routines externes (méthodes ou fonctions). En général, le traducteur introduit autant d'informations que possible dans un fichier objet, tout le reste étant du ressort de l'utilisateur. non résolu .

Code(s) objet(s) de liaison

La phase de liaison combine un ou plusieurs codes objet, résout les références et élimine les sous-programmes en double. Le résultat final est un exécutable fichier. Ce fichier contient des informations sur le système d'exploitation et relatif adresses.

Exécuter Binaire Fichiers

Le système d'exploitation charge le fichier exécutable, généralement à partir d'un disque dur, et le place en mémoire. Le système d'exploitation peut convertir les adresses relatives en emplacements physiques. Le système d'exploitation peut également préparer les ressources (telles que les DLL et les widgets de l'interface graphique) requises par l'exécutable (ce qui peut être indiqué dans le fichier exécutable).

Compilation directe en binaire Certains compilateurs, tels que ceux utilisés dans les systèmes embarqués, ont la capacité de compiler du C++ directement vers un code binaire exécutable. Ce code aura des adresses physiques au lieu d'adresses relatives et ne nécessitera pas le chargement d'un système d'exploitation.

Avantages

L'un des avantages de ces phases est que les programmes C++ peuvent être décomposés en morceaux, compilés individuellement et liés ultérieurement. Ils peuvent même être liés avec des morceaux provenant d'autres développeurs (c'est-à-dire des bibliothèques). Cela permet aux développeurs de ne compiler que les morceaux en cours de développement et de lier les morceaux déjà validés. En général, la traduction de C++ en objet est la partie la plus longue du processus. De plus, une personne ne veut pas attendre la fin de toutes les phases lorsqu'il y a une erreur dans le code source.

Gardez l'esprit ouvert et attendez-vous toujours à ce que Troisième option (Option) .

0 votes

Ce qui était vraiment intéressant lorsque nous avions une mémoire de 100 mots, mais est-ce encore un avantage aujourd'hui ou plutôt un artefact ? Une granularité de compilation qui utiliserait mieux la mémoire disponible (par exemple pour éviter les réparations répétées d'en-têtes, les E/S disque relativement lentes ou même simplement le temps de démarrage du binaire) serait plus conforme aux exigences modernes ?

5voto

t0mm13b Points 21031

Pour répondre à vos questions, veuillez noter que c'est subjectif car il existe différents processeurs, différentes plateformes, différents assembleurs et compilateurs C, dans ce cas, je parlerai de la plateforme Intel x86.

  1. Les assembleurs n'assemblent généralement pas en binaire pur / plat (code machine brut), mais plutôt dans un fichier défini par des segments tels que des données, du texte et des bss, pour n'en citer que quelques-uns ; c'est ce qu'on appelle un fichier objet. Le linker intervient et ajuste les segments pour rendre le fichier exécutable, c'est-à-dire prêt à être exécuté. Par ailleurs, la sortie par défaut lorsque vous assemblez avec GNU as foo.s est a.out qui est un raccourci pour Assembler Output. (Mais le même nom de fichier est le nom par défaut de gcc pour le fichier linker la sortie de l'assembleur n'étant que temporaire).
  2. Les chargeurs de démarrage ont une directive spéciale définie, à l'époque du DOS, il était courant de trouver une directive telle que .Org 100h qui définit le code assembleur comme étant de l'ancienne variété .COM avant que .EXE ne devienne populaire. De plus, il n'était pas nécessaire d'avoir un assembleur pour produire un fichier .COM, l'utilisation du vieux debug.exe fourni avec MSDOS faisait l'affaire pour les petits programmes simples, les fichiers .COM n'avaient pas besoin d'un linker et étaient directement prêts à fonctionner au format binaire. Voici une session simple utilisant DEBUG.

    1:*a 0100 2:* mov AH,07 3:* int 21 4:* cmp AL,00 5:* jnz 010c 6:* mov AH,07 7:* int 21 8:* mov AH,4C 9:* int 21 10:* 11:*r CX 12:*10 13:*n respond.com 14:*w 15:*q

On obtient ainsi un programme .COM prêt à être exécuté, appelé 'respond.com', qui attend la frappe d'une touche et ne l'affiche pas à l'écran. Remarquez, au début, l'utilisation de 'a 100h' qui montre que le pointeur d'instruction commence à 100h ce qui est la caractéristique d'un .COM. Cet ancien script était principalement utilisé dans des fichiers batch attendant une réponse et ne la répercutant pas. L'original du script se trouve à l'adresse suivante ici .

Encore une fois, dans le cas des chargeurs de démarrage, ils sont convertis en un format binaire, il y avait un programme qui venait avec le DOS, appelé EXE2BIN . C'était le travail de conversion du code objet brut dans un format qui peut être copié sur un disque bootable pour le démarrage. Rappelez-vous qu'aucun éditeur de liens n'est exécuté contre le code assemblé, car l'éditeur de liens est destiné à l'environnement d'exécution et configure le code pour le rendre exécutable.

Le BIOS, lorsqu'il démarre, s'attend à ce que le code soit au segment : offset, 0x7c00, si ma mémoire est correcte, le code (après avoir été EXE2BINé), commencera à s'exécuter, puis le chargeur de démarrage se relocalisera plus bas dans la mémoire et continuera à charger en émettant int 0x13 pour lire depuis le disque, activer la porte A20, activer le DMA, passer en mode protégé car le BIOS est en mode 16bit, puis les données lues depuis le disque sont chargées en mémoire, puis le chargeur de démarrage émet un saut lointain dans le code de données (probablement écrit en C). C'est en substance la façon dont le système démarre.

Ok, le paragraphe précédent semble abstrait et simple, j'ai peut-être oublié quelque chose, mais c'est comme ça en résumé.

0 votes

Debug.exe est un assembleur. (Un mauvais assembleur selon les normes modernes, par exemple, pas d'étiquettes, ce qui vous oblige à calculer les adresses des cibles de branchement à la main). De plus, le code machine brut est pas un fichier objet ; s'il était littéralement brut (comme nasm -f bin par exemple, un fichier .com), il n'y a pas de métadonnées de section, ni aucune autre métadonnée. J'ai fait une modification à ce paragraphe.

0 votes

@PeterCordes C'est vrai, mais il serait injuste de le comparer aux standards modernes car cela faisait partie de la base d'installation de MSDOS dans les années 80 et 90, c'était bien avant que Linux / open source, n'apparaisse sur la scène qui a ouvert les couloirs de la conscience générale des standards. :)

0 votes

C'est 100% équitable si les gens proposent de l'utiliser encore aujourd'hui ! Apparemment, certains pauvres malheureux ont des devoirs qui leur demandent d'écrire du code DOS x86 16 bits pour debug.exe, ce qui conduit à des questions sur SO à ce sujet. C'est ce que je voulais dire par "selon les normes modernes". De plus, c'était mieux que rien à l'époque, mais même alors, je suppose que vous auriez voulu TASM, MASM ou AS86 si vous pouviez les avoir, pour tout ce qui n'est pas un petit jouet.

1voto

Steven Sudit Points 13793

Ils compilent un fichier dans un format spécifique (COFF pour Windows, etc.), composé d'en-têtes et de segments, dont certains ont des codes d'opération "binaires simples". Les assembleurs et les compilateurs (comme le C) créent le même type de sortie. Certains formats, comme les anciens fichiers *.COM, n'avaient pas d'en-têtes, mais comportaient tout de même certaines hypothèses (comme l'endroit de la mémoire où ils seraient chargés ou leur taille).

Sur les machines Windows, le chargeur du système d'exploitation se trouve dans un secteur du disque chargé par le BIOS, où ces deux éléments sont "ordinaires". Une fois que le SE a chargé son chargeur, il peut lire les fichiers qui ont des en-têtes et des segments.

Est-ce que ça aide ?

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X