Premiers pas avec le langage d'assemblage

Introduction

Le langage d’assemblage est une forme lisible par l’homme du langage machine ou du code machine qui est la séquence réelle de bits et d’octets sur laquelle la logique du processeur fonctionne. Il est généralement plus facile pour les humains de lire et de programmer en mnémoniques que binaire, octal ou hexadécimal, de sorte que les humains écrivent généralement du code en langage d’assemblage, puis utilisent un ou plusieurs programmes pour le convertir dans le format de langage machine compris par le processeur.

EXEMPLE:

mov eax, 4
cmp eax, 5
je point

Un assembleur est un programme qui lit le programme en langage assembleur, l’analyse et produit le langage machine correspondant. Il est important de comprendre que contrairement à un langage comme C++ qui est un langage unique défini dans un document standard, il existe de nombreux langages d’assemblage différents. Chaque architecture de processeur, ARM, MIPS, x86, etc. a un code machine différent et donc un langage d’assemblage différent. De plus, il existe parfois plusieurs langages d’assemblage différents pour la même architecture de processeur. En particulier, la famille de processeurs x86 a deux formats populaires qui sont souvent appelés syntaxe gas (gas est le nom de l’exécutable pour l’assembleur GNU) et syntaxe Intel (du nom de l’initiateur de la famille de processeurs x86). Ils sont différents mais équivalents en ce sens que l’on peut généralement écrire n’importe quel programme donné dans l’une ou l’autre syntaxe.

Généralement, l’inventeur du processeur documente le processeur et son code machine et crée un langage d’assemblage. Il est courant que ce langage d’assemblage particulier soit le seul utilisé, mais contrairement aux auteurs de compilateurs qui tentent de se conformer à une norme de langage, le langage d’assemblage défini par l’inventeur du processeur est généralement, mais pas toujours, la version utilisée par les personnes qui écrivent des assembleurs. .

Il existe deux types généraux de processeurs :

  • CISC (Complex Instruction Set Computer): ont de nombreuses instructions en langage machine différentes et souvent complexes

  • RISC (Reduced Instruction set Computers) : en revanche, a des instructions moins nombreuses et plus simples

Pour un programmeur en langage assembleur, la différence est qu’un processeur CISC peut avoir un grand nombre d’instructions à apprendre, mais il existe souvent des instructions adaptées à une tâche particulière, tandis que les processeurs RISC ont des instructions moins nombreuses et plus simples, mais toute opération donnée peut nécessiter le programmeur en langage assembleur. pour écrire plus d’instructions pour obtenir la même chose.

D’autres compilateurs de langages de programmation produisent parfois d’abord un assembleur, qui est ensuite compilé en code machine en appelant un assembleur. Par exemple, gcc utilisant son propre assembleur gas au stade final de la compilation. Le code machine produit est souvent stocké dans des fichiers object, qui peuvent être liés en exécutable par le programme de liaison.

Une “chaîne d’outils” complète se compose souvent d’un compilateur, d’un assembleur et d’un éditeur de liens. On peut alors utiliser cet assembleur et cet éditeur de liens directement pour écrire des programmes en langage d’assemblage. Dans le monde GNU, le paquet binutils contient l’assembleur, l’éditeur de liens et les outils associés ; ceux qui s’intéressent uniquement à la programmation en langage assembleur n’ont pas besoin de gcc ou d’autres packages de compilation.

Les petits microcontrôleurs sont souvent programmés uniquement en langage d’assemblage ou dans une combinaison de langage d’assemblage et d’un ou plusieurs langages de niveau supérieur tels que C ou C++. Cela est dû au fait que l’on peut souvent utiliser les aspects particuliers de l’architecture du jeu d’instructions pour que de tels dispositifs écrivent un code plus compact et efficace que ce qui serait possible dans un langage de niveau supérieur et que ces dispositifs disposent souvent d’une mémoire et de registres limités. De nombreux microprocesseurs sont utilisés dans les systèmes intégrés qui sont des appareils autres que les ordinateurs à usage général qui contiennent un microprocesseur. Des exemples de tels systèmes embarqués sont les téléviseurs, les fours à micro-ondes et l’unité de commande du moteur d’une automobile moderne. Beaucoup de ces appareils n’ont ni clavier ni écran, donc un programmeur écrit généralement le programme sur un ordinateur à usage général, exécute un ** assembleur croisé ** (ainsi appelé parce que ce type d’assembleur produit du code pour un type de processeur différent de celui sur lequel il s’exécute) et/ou un compilateur croisé et un lien croisé pour produire du code machine.

Il existe de nombreux fournisseurs de tels outils, qui sont aussi variés que les processeurs pour lesquels ils produisent du code. De nombreux processeurs, mais pas tous, ont également une solution open source comme GNU, sdcc, llvm ou autre.

Bonjour tout le monde pour Linux x86_64 (Intel 64 bits)

section .data
    msg db "Hello world!",10      ; 10 is the ASCII code for a new line (LF)

section .text
    global _start

_start:
    mov rax, 1
    mov rdi, 1
    mov rsi, msg
    mov rdx, 13
    syscall
    
    mov rax, 60
    mov rdi, 0
    syscall

Si vous voulez exécuter ce programme, vous avez d’abord besoin du Netwide Assembler, nasm, car ce code utilise sa syntaxe. Utilisez ensuite les commandes suivantes (en supposant que le code se trouve dans le fichier helloworld.asm). Ils sont nécessaires pour l’assemblage, la liaison et l’exécution, respectivement.

  • nasm -felf64 helloworld.asm
  • ld helloworld.o -o helloworld
  • ./helloworld

Le code utilise l’appel système sys_write de Linux. Ici vous pouvez voir une liste de tous les appels système pour le x86_64 architecture. Lorsque vous prenez également les pages de manuel de write et exit en compte, vous pouvez traduire le programme ci-dessus en un programme C qui fait la même chose et est beaucoup plus lisible :

#include <unistd.h>

#define STDOUT 1

int main()
{
    write(STDOUT, "Hello world!\n", 13);
    _exit(0);
}

Seules deux commandes sont nécessaires ici pour la compilation et la liaison (la première) et l’exécution :

  • gcc helloworld_c.c -o helloworld_c.
  • ./helloworld_c

Langage machine

Le code machine est un terme désignant les données en particulier au format machine natif, qui sont directement traitées par la machine - généralement par le processeur appelé CPU (Central Processing Unit).

L’architecture informatique commune (architecture de von Neumann) se compose d’un processeur à usage général (CPU), d’une mémoire à usage général - stockant à la fois le programme (ROM/RAM) et les données traitées et périphériques d’entrée et de sortie (périphériques d’E/S).

L’avantage majeur de cette architecture est la simplicité relative et l’universalité de chacun des composants - par rapport aux machines informatiques d’avant (avec un programme câblé dans la construction de la machine), ou des architectures concurrentes (par exemple l’architecture Harvard séparant la mémoire du programme de la mémoire de Les données). L’inconvénient est une performance générale un peu moins bonne. À long terme, l’universalité a permis une utilisation flexible, qui l’emportait généralement sur le coût des performances.

Quel est le rapport avec le code machine ?

Le programme et les données sont stockés dans ces ordinateurs sous forme de nombres, dans la mémoire. Il n’existe aucun moyen véritable de distinguer le code des données, de sorte que les systèmes d’exploitation et les opérateurs de la machine donnent au processeur des indications sur le point d’entrée de la mémoire qui démarre le programme, après avoir chargé tous les nombres en mémoire. Le CPU lit ensuite l’instruction (numéro) stockée au point d’entrée et la traite rigoureusement, en lisant séquentiellement les numéros suivants comme instructions supplémentaires, à moins que le programme lui-même ne dise au CPU de poursuivre l’exécution ailleurs.

Par exemple, deux nombres de 8 bits (8 bits regroupés sont égaux à 1 octet, c’est un nombre entier non signé dans la plage 0-255): 60 201, lorsqu’il est exécuté en tant que code sur le processeur Zilog Z80 sera traité comme deux instructions : INC a (incrémentation de la valeur dans le registre a de un) et RET (retour d’un sous-programme, pointant le CPU pour exécuter des instructions à partir d’une partie différente de la mémoire).

Pour définir ce programme, un humain peut saisir ces nombres par un éditeur de mémoire/fichier, par exemple dans un éditeur hexadécimal sous la forme de deux octets : “3C C9” (nombres décimaux 60 et 201 écrits en codage en base 16). Ce serait de la programmation en code machine.

Pour rendre la tâche de programmation CPU plus facile pour les humains, un programme Assembleur a été créé, capable de lire un fichier texte contenant quelque chose comme :

subroutineIncrementA:
    INC   a
    RET

dataValueDefinedInAssemblerSource:
    DB    60          ; define byte with value 60 right after the ret

sortie de la séquence de nombres hexadécimaux d’octets “3C C9 3C”, entourée de numéros supplémentaires optionnels spécifiques à la plate-forme cible : marquer quelle partie de ce binaire est un code exécutable, où est le point d’entrée du programme (la première instruction de celui-ci), quelles parties sont des données encodées (non exécutables), etc.

Notez comment le programmeur a spécifié le dernier octet avec la valeur 60 comme “données”, mais du point de vue du processeur, il ne diffère en rien de l’octet ‘INC a’. Il appartient au programme en cours d’exécution de naviguer correctement dans le processeur sur les octets préparés en tant qu’instructions et de traiter les octets de données uniquement en tant que données pour les instructions.

Une telle sortie est généralement stockée dans un fichier sur un périphérique de stockage, chargé plus tard par le système d’exploitation (Système d’exploitation - un code machine déjà en cours d’exécution sur l’ordinateur, aidant à manipuler avec l’ordinateur) dans la mémoire avant de l’exécuter, et enfin pointant le CPU sur le point d’entrée du programme.

Le processeur ne peut traiter et exécuter que du code machine - mais tout contenu de mémoire, même aléatoire, peut être traité comme tel, bien que le résultat puisse être aléatoire, allant de “* crash *” détecté et géré par le système d’exploitation jusqu’à l’effacement accidentel des données de Périphériques d’E/S, ou endommagement des équipements sensibles connectés à l’ordinateur (ce qui n’est pas un cas courant pour les ordinateurs personnels :) ).

Le processus similaire est suivi par de nombreux autres langages de programmation de haut niveau, compilant la source (forme textuelle lisible par l’homme du programme) en nombres, représentant soit le code machine (instructions natives du processeur), soit en cas d’interprétation/hybride. langages en un code de machine virtuelle spécifique au langage général, qui est ensuite décodé en code machine natif lors de l’exécution par l’interpréteur ou la machine virtuelle.

Certains compilateurs utilisent l’assembleur comme étape intermédiaire de compilation, traduisant d’abord la source sous forme d’assembleur, puis exécutant l’outil assembleur pour en extraire le code machine final (exemple GCC : exécutez gcc -S helloworld.c pour obtenir une version assembleur du programme C helloworld.c).

Hello World pour OS X (x86_64, gaz de syntaxe Intel)

.intel_syntax noprefix

.data

.align 16
hello_msg:
    .asciz "Hello, World!"

.text

.global _main
_main:
    push rbp
    mov rbp, rsp

    lea rdi, [rip+hello_msg]
    call _puts

    xor rax, rax
    leave
    ret

Assembler:

clang main.s -o hello
./hello

Remarques:

  • L’utilisation d’appels système est déconseillée car l’API d’appel système sous OS X n’est pas considérée comme stable. Utilisez plutôt la bibliothèque C. ([Référence à une question Stack Overflow][1])
  • Intel recommande que les structures plus grandes qu’un mot commencent sur une limite de 16 octets. ([Référence à la documentation Intel][2])
  • Les données de commande sont transmises aux fonctions via les registres : rdi, rsi, rdx, rcx, r8 et r9. ([Référence au Système V ABI][3])

[1] : http://stackoverflow.com/a/357113/6557303 [2] : https://software.intel.com/en-us/articles/data-alignment-when-migrating-to-64-bit-intel-architecture/ [3] : http://people.freebsd.org/~obrien/amd64-elf-abi.pdf

Exécution de l’assemblage x86 dans Visual Studio 2015

Étape 1 : Créez un projet vide via Fichier -> Nouveau projet.

[![entrez la description de l’image ici][1]][1]

Étape 2 : Faites un clic droit sur la solution du projet et sélectionnez Build Dependencies->Build Customizations.

Étape 3 : Cochez la case ".masm".

[![entrez la description de l’image ici][2]][2]

Étape 4 : Appuyez sur le bouton “ok”.

Étape 5 : Créez votre fichier d’assemblage et saisissez ceci :

.386
    .model small
        .code

            public main
                main proc

                    ; Ends program normally

                    ret
                main endp
            end main

Étape 6 : Compilez !

[![entrez la description de l’image ici][3]][3]

[1] : https://i.stack.imgur.com/LE2AI.png [2] : https://i.stack.imgur.com/Fkbpm.png [3] : https://i.stack.imgur.com/18zg4.png