Comment passer de l'assemblage au code machine (génération de code)
16
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 coulisse ??
Regardez la documentation du jeu d'instructions, et vous trouverez des entrées comme celle-ci d' un microcontrôleur pic pour chaque 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 , en fonction de votre système d'exploitation.
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).
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'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 aboutirait probablement à une structure qui contient l'instruction ADD, et un tableau d'opérandes (une référence au registre EAXet à 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 miest une description des opérandes: un opérande modr/m(registre ou mémoire) (ce qui signifie que nous devrons ajouter un modr/moctet à la fin de l'instruction, que nous reviendrons plus tard) et un une instruction immédiate (qui être utilisé dans la description de l'instruction).
Le suivant est 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 now ( 0x66), mais je suppose que nous ne le sommes pas et continuons.
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. L' modr/moctet 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 un autre fichier de données :
eax REG_EAX reg32 0
Nous vérifions que cela reg32correspond à la taille requise de l'instruction de la base de données d'origine (c'est le cas). C'est 0le numéro du registre. Un modr/moctet 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 modchamp est 0b11.
Le regchamp est le numéro du registre que nous utilisons,0b000
Parce qu'il n'y a qu'un seul registre dans cette instruction, nous devons remplir le rmchamp avec quelque chose. C'est ce que les données supplémentaires spécifiées dans /0était pour, nous avons donc mis que dans le rmdomaine, 0b000.
L' modr/moctet est donc 0b11000000ou 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 assemblé complète 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.
Je vous remercie. Grande explication mais ne devrait-elle pas être "0x83 0xC0 0x2A" plutôt que "0x83 0xB0 0x2A" car 0b11000000 = 0xC0
Kamran
@Kamran - $ cat > test.asm bits 32 add eax,42 $ nasm -f bin test.asm -o test.bin $ od -t x1 test.bin 0000000 83 c0 2a 0000003... ouais, tu as tout à fait raison. :)
Jules
2
Dans la pratique, un assembleur ne produit généralement pas directement un exécutable binaire , mais un fichier objet (à envoyer ultérieurement à l' é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 logiciels libres . Alors téléchargez et compilez sur votre ordinateur le code source de GNU as (une partie de binutils ) et de nasm . Etudiez ensuite 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 un segment de code et des instructions de relocalisation . 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 l' éditeur de liens (qui produit finalement un exécutable). Les délocalisations sont spécifiées par l' ABI (par exemple, ABI x86-64 ). Lisez le livre de Levine Linkers and Loaders pour en savoir plus.
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élocalisable) 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 les 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 en deux passes , la seconde émettant une relocalisation ou corrigeant une partie de la sortie de la première passe. Ils utilisent maintenant des techniques d' analyse habituelles (alors lisez peut-être The Dragon Book ).
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.
En ce qui concerne les formats de fichiers objets, pour un débutant, je recommanderais de regarder le format RDOFF produit par NASM. Cela a été intentionnellement conçu pour être aussi simple que possible et fonctionner toujours dans une variété de situations. La source NASM comprend un éditeur de liens et un chargeur pour le format. (Divulgation complète - J'ai conçu et écrit tout cela)
$ cat > test.asm bits 32 add eax,42 $ nasm -f bin test.asm -o test.bin $ od -t x1 test.bin 0000000 83 c0 2a 0000003
... ouais, tu as tout à fait raison. :)Dans la pratique, un assembleur ne produit généralement pas directement un exécutable binaire , mais un fichier objet (à envoyer ultérieurement à l' é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 logiciels libres . Alors téléchargez et compilez sur votre ordinateur le code source de GNU as (une partie de binutils ) et de nasm . Etudiez ensuite 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 un segment de code et des instructions de relocalisation . 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 l' éditeur de liens (qui produit finalement un exécutable). Les délocalisations sont spécifiées par l' ABI (par exemple, ABI x86-64 ). Lisez le livre de Levine Linkers and Loaders pour en savoir plus.
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élocalisable) 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 les 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 en deux passes , la seconde émettant une relocalisation ou corrigeant une partie de la sortie de la première passe. Ils utilisent maintenant des techniques d' analyse habituelles (alors lisez peut-être The Dragon Book ).
La façon dont un exécutable est démarré par le noyau du système d' exploitation (par exemple, comment l'
execve
appel système fonctionne sous Linux) est une question différente (et complexe). Il configure généralement un espace d'adressage virtuel (dans le processus faisant que execve (2) ...) puis réinitialise l'état interne du processus (y compris les registres en mode utilisateur ). Un éditeur de liens dynamique, tel que ld-linux.so (8) sous Linux, peut être impliqué lors de l'exécution. Lisez un bon livre, comme Operating System: Three Easy Pieces . Le wiki OSDEV 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.
la source