web-dev-qa-db-fra.com

Comment passer de l'assemblage au code machine (génération de code)

Existe-t-il un moyen simple de visualiser l'étape entre l'assemblage du code et le code machine?

Par exemple, si vous ouvrez un fichier binaire dans le bloc-notes, vous voyez une représentation au format texte du code machine. Je suppose que chaque octet (symbole) que vous voyez est le caractère ascii correspondant pour sa valeur binaire?

Mais comment passer de l'assemblage au binaire, que se passe-t-il en coulisses ??

16
user12979

Regardez la documentation du jeu d'instructions, et vous trouverez des entrées comme celle-ci dans n microcontrôleur pic pour chaque instruction:

example addlw instruction

La ligne "encoding" indique à quoi ressemble cette instruction en binaire. Dans ce cas, il commence toujours par 5 uns, puis un bit indifférent (qui peut être soit un, soit zéro), puis le "k" représente le littéral que vous ajoutez.

Les premiers bits sont appelés "opcode", sont uniques pour chaque instruction. Le CPU regarde fondamentalement l'opcode pour voir de quelle instruction il s'agit, puis il sait décoder les "k" comme un nombre à ajouter.

C'est fastidieux, mais pas si difficile à encoder et à décoder. J'avais un cours de premier cycle où nous devions le faire à la main lors des examens.

Pour réellement créer un fichier exécutable complet, vous devez également faire des choses comme allouer de la mémoire, calculer des décalages de branche et le mettre dans un format comme ELF , selon votre système d'exploitation.

28
Karl Bielefeldt

Les opcodes d'assemblage ont, pour la plupart, une correspondance biunivoque avec les instructions machine sous-jacentes. Il vous suffit donc d'identifier chaque code opération dans le langage d'assemblage, de le mapper à l'instruction machine correspondante et d'écrire l'instruction machine dans un fichier, avec ses paramètres correspondants (le cas échéant). Vous répétez ensuite le processus pour chaque opcode supplémentaire dans le fichier source.

Bien sûr, il faut plus que cela pour créer un fichier exécutable qui se chargera et s'exécutera correctement sur un système d'exploitation, et la plupart des assembleurs décents ont des capacités supplémentaires au-delà du simple mappage d'opcodes en instructions machine (telles que des macros, par exemple).

10
Robert Harvey

La première chose dont vous avez besoin est quelque chose comme ce fichier . Il s'agit de la base de données d'instructions pour les processeurs x86 utilisée par l'assembleur NASM (que j'ai aidé à écrire, mais pas les parties qui traduisent réellement les instructions). Permet de choisir une ligne arbitraire dans la base de données:

ADD   rm32,imm8    [mi:    hle o32 83 /0 ib,s]      386,LOCK

Cela signifie qu'il décrit l'instruction ADD. Il existe plusieurs variantes de cette instruction, et celle qui est décrite ici est la variante qui prend soit un registre 32 bits soit une adresse mémoire et ajoute une valeur immédiate 8 bits (c'est-à-dire une constante directement incluse dans l'instruction). Un exemple d'instructions d'assemblage qui utiliserait cette version est le suivant:

add eax, 42

Maintenant, vous devez prendre votre texte et l'analyser en instructions et opérandes individuels. Pour l'instruction ci-dessus, cela entraînerait probablement une structure qui contient l'instruction, ADD, et un tableau d'opérandes (une référence au registre EAX et à la valeur 42). Une fois que vous avez cette structure, vous parcourez la base de données d'instructions et trouvez la ligne qui correspond à la fois au nom de l'instruction et aux types d'opérandes. Si vous ne trouvez pas de correspondance, c'est une erreur qui doit être présentée à l'utilisateur ("combinaison illégale d'opcode et d'opérandes" ou similaire est le texte habituel).

Une fois que nous avons obtenu la ligne de la base de données, nous regardons la troisième colonne, qui pour cette instruction est:

[mi:    hle o32 83 /0 ib,s] 

Il s'agit d'un ensemble d'instructions qui décrivent comment générer l'instruction de code machine requise:

  • Le mi est une description des opérandes: un a modr/m (registre ou mémoire) opérande (ce qui signifie que nous devons ajouter un modr/m octet à la fin de l'instruction, à laquelle nous reviendrons plus tard) et un une instruction immédiate (qui sera utilisée dans la description de l'instruction).
  • Vient ensuite hle. Ceci identifie la façon dont nous gérons le préfixe "lock". Nous n'avons pas utilisé "lock", nous l'ignorons donc.
  • Le suivant est o32. Cela nous indique que si nous assemblons du code pour un format de sortie 16 bits, l'instruction a besoin d'un préfixe de remplacement de taille d'opérande. Si nous produisions une sortie 16 bits, nous produirions le préfixe maintenant (0x66), mais je suppose que nous ne le sommes pas et je continue.
  • Le suivant est 83. Il s'agit d'un octet littéral en hexadécimal. Nous le sortons.
  • Le suivant est /0. Cela spécifie quelques bits supplémentaires dont nous aurons besoin dans le sous-élément modr/m, et nous amène à le générer. Le modr/m octet est utilisé pour coder des registres ou des références de mémoire indirectes. Nous avons un seul tel opérande, un registre. Le registre a un numéro, qui est spécifié dans n autre fichier de données :

    eax     REG_EAX         reg32           0
    
  • Nous vérifions que reg32 est d'accord avec la taille requise de l'instruction de la base de données d'origine (c'est le cas). Le 0 est le numéro du registre. UNE modr/m octet est une structure de données spécifiée par le processeur, qui ressemble à ceci:

     (most significant bit)
     2 bits       mod    - 00 => indirect, e.g. [eax]
                           01 => indirect plus byte offset
                           10 => indirect plus Word offset
                           11 => register
     3 bits       reg    - identifies register
     3 bits       rm     - identifies second register or additional data
     (least significant bit)
    
  • Parce que nous travaillons avec un registre, le champ mod est 0b11.

  • Le champ reg est le numéro du registre que nous utilisons, 0b000
  • Comme il n'y a qu'un seul registre dans cette instruction, nous devons remplir le champ rm avec quelque chose. C'est ce que les données supplémentaires spécifiées dans /0 était pour, nous avons donc mis cela dans le champ rm, 0b000.
  • Le modr/m l'octet est donc 0b11000000 ou 0xC0. Nous sortons cela.
  • Le suivant est ib,s. Ceci spécifie un octet immédiat signé. Nous regardons les opérandes et notons que nous avons une valeur immédiate disponible. Nous le convertissons en octet signé et le sortons (42 => 0x2A).

L'instruction complète assemblée est donc: 0x83 0xC0 0x2A. Envoyez-le à votre module de sortie, avec une note qu'aucun des octets ne constitue des références de mémoire (le module de sortie peut avoir besoin de savoir s'ils le font).

Répétez pour chaque instruction. Gardez une trace des étiquettes pour savoir quoi insérer lorsqu'elles sont référencées. Ajoutez des fonctionnalités pour les macros et les directives qui sont transmises à vos modules de sortie de fichier objet. Et c'est essentiellement comment fonctionne un assembleur.

7
Jules

En pratique, un assembleur ne produit généralement pas directement un binaire exécutable , mais un certain - fichier objet (à alimenter plus tard au éditeur de liens ). Cependant, il existe des exceptions (vous pouvez utiliser certains assembleurs pour produire directement un exécutable binaire; ils sont rares).

Tout d'abord, notez que de nombreux assembleurs sont aujourd'hui des programmes logiciels libres . Alors téléchargez et compilez sur votre ordinateur le code source de GNU as (une partie de binutils ) et de nasm . Ensuite, étudiez leur code source. BTW, je recommande d'utiliser Linux à cette fin (c'est un système d'exploitation très convivial pour les développeurs et les logiciels libres).

Le fichier objet produit par un assembleur contient notamment des instructions segment de code et relocation . Il est organisé dans un format de fichier bien documenté, qui dépend du système d'exploitation. Sous Linux, ce format (utilisé pour les fichiers objets, les bibliothèques partagées, les vidages mémoire et les exécutables) est ELF . Ce fichier objet est ensuite entré dans le linker (qui produit finalement un exécutable). Les délocalisations sont spécifiées par ABI (par exemple x86-64 ABI ). Lisez le livre de Levine Linkers and Loaders for more.

Le segment de code dans un tel fichier objet contient du code machine avec des trous (à remplir, à l'aide des informations de relocalisation, par l'éditeur de liens). Le code machine (déplaçable) généré par un assembleur est évidemment spécifique à une architecture de jeu d'instructions . Les ISA x86 ou x86-64 (utilisés dans la plupart des processeurs pour ordinateurs portables ou de bureau) sont terriblement complexes dans leurs détails. Mais un sous-ensemble simplifié, appelé y86 ou y86-64, a été inventé à des fins d'enseignement. Lisez diapositives dessus. D'autres réponses à cette question expliquent également un peu cela. Vous voudrez peut-être lire un bon livre sur l'architecture informatique .

La plupart des assembleurs travaillent dans deux passes , la seconde émettant une relocalisation ou corrigeant une partie de la sortie de la première passe. Ils utilisent maintenant les techniques habituelles analyse (alors lisez peut-être The Dragon Book ).

Comment un exécutable est démarré par le système d'exploitation noya (par exemple, comment l'appel système execve fonctionne sous Linux) est une question différente (et complexe). Il configure généralement un espace d'adressage virtuel (dans le processus faisant cela execve (2) ...) puis réinitialise l'état interne du processus ( y compris mode utilisateur registres). Un éditeur de liens dynamique - tel que ld-linux.so (8) sous Linux- pourrait être impliqué lors de l'exécution. Lisez un bon livre, comme Système d'exploitation: trois pièces faciles . Le OSDEV wiki fournit également des informations utiles.

PS. Votre question est si large que vous devez lire plusieurs livres à ce sujet. J'ai donné quelques références (très incomplètes). Vous devriez en trouver plus.

2