Aller au contenu

De l'assembleur au langage machine

Programme

Notions Compétences Remarques
Modèle d’architecture séquentielle (von Neumann) Dérouler l’exécution d’une séquence d’instructions simples du type langage machine. La présentation se limite aux concepts généraux.
Les circuits combinatoires réalisent des fonctions booléennes.
Le langage machine

Comme nous l'avons vu, au [chapitre précédent{--architecture-materielle}], des instructions machines sont exécutées par l'Unité de Commande. Ainsi les programmes exécutés par les ordinateurs sont constitués de suites d'instructions, elles même réalisables au moyen de combinaisons de portes logiques.

Toutefois, le CPU est incapable d'exécuter directement des programmes écrits, par exemple, en Python. En effet, comme tous les autres constituants d'un ordinateur, le CPU gère uniquement 2 états (toujours symbolisés par 1 et 0), les instructions exécutées au niveau du CPU doivent donc préalablement être codées en binaire.

DĂ©finition du langage machine

L'ensemble des instructions binaires, exécutables directement par le microprocesseur, constitue ce que l'on appelle le langage machine.

Une instruction machine est donc une chaîne binaire composée principalement de 2 parties :

  • le champ code opĂ©ration qui indique au processeur le type de traitement Ă  rĂ©aliser. Par exemple le code "00100110" donne l'ordre au CPU d'effectuer une multiplication.

  • le champ opĂ©randes indique la nature des donnĂ©es sur lesquelles l'opĂ©ration dĂ©signĂ©e par le code opĂ©ration doit ĂŞtre effectuĂ©e.

Champ code opération Champ opérandes
00100110 0110110110110101

Exemple d'instruction machine

Les codes opération

Les instructions machines sont relativement basiques (on parle d'instructions de bas niveau), voici quelques exemples :

  • les instructions arithmĂ©tiques (addition, soustraction, multiplication...).

    Exemple : additionne la valeur contenue dans le registre R1 et le nombre 789 et range le résultat dans le registre R0.

  • les instructions de transfert de donnĂ©es qui permettent de transfĂ©rer une donnĂ©e d'un registre du CPU vers la mĂ©moire vive et vice versa.

    Exemple : prendre la valeur située à l'adresse mémoire 487 et la placer dans la registre R2

  • les instructions de rupture de sĂ©quence : Au cours de l'exĂ©cution d'un programme, le CPU passe d'une instruction Ă  une autre dans l'ordre avec lequel elles sont inscrite dans la mĂ©moire vive. Les instructions de rupture de sĂ©quence d'exĂ©cution permettent d'interrompre l'ordre initial sous certaines conditions.

    Exemple : si la valeur contenue dans le registre R1 est strictement supérieure à 0 alors exécuter l'instruction située à l'adresse mémoire 4521

Les opérandes

Les opérandes sont les données sur lesquelles le code opération de l'instruction doit être réalisée. Un opérande peut être de 3 natures différentes :

1- l'opérande est une valeur immédiate : l'opération est effectuée directement sur la valeur donnée dans l'opérande.

2- l'opérande est un registre du CPU : l'opération est effectuée sur la valeur située dans le registre indiqué dans l'opérande (R0,R1, R2,...).

3- l'opérande est une donnée située en mémoire vive : l'opération est effectuée sur la valeur située en mémoire vive à l'adresse XXXXX indiquée dans l'opérande.

Le langage Assembleur

Un programme en langage machine est donc une suite très très longue de 1 et de 0, ce qui est très peu intuitif à programmer : une seule erreur et votre programme ne fonctionne pas. Il faut donc traduire en langage-machine (binaire) la phrase additionne le nombre 125 et la valeur située dans le registre R2 , range le résultat dans le registre R1 pour qu'elle soit interprétable par le CPU.

DĂ©finition du langage assembleur

Le programme de bas niveau réalisé dans le langage appelé assembleur (ou ASM) assure le passage de symboles mnémoniques de type addl %eax, %ebx au langague-machine 11100010100000100001000001111101.

L'écriture de programme en assembleur plutôt qu'en langage de plus haut niveau (python par exemple) permet d'être au plus proche de l'architecture matérielle et d'en optimiser l'allocation des ressources.

Exemples de commandes en assembleur Y86

Remarque

Il n'est pas nécessaire d'apprendre à coder en ASM, mais il faut en connaître les principes et savoir en dérouler un code.

Il existe différents types de langages assembleur, qui varient selon le type de processeur utilisé (x86-64, ARM...).

y86 est un langage assembleur simplifié qui est dédié à l'apprentissage. Il est basé sur l'architecture des processeurs x86 (IA-32), les mémoires sont donc alignés sur 32 bits.

y86 propose un jeu d'instructions réduit dont les principales sont :

  • instructions arithmĂ©tiques :

    addl <rA>, <rB> ; Addition : rB = rB + rA
    subl <rA>, <rB> ; Soustraction : rB = rB - rA 
    andl <rA>, <rB> ; ET bit Ă  bit : rB = rB and rA
    xorl <rA>, <rB> ; Ou exclusif : rB = rB xor rA
    

  • instructions de transfert de donnĂ©es :

    rrmov1 <rA>, <rB> ; Copie d'une valeur d'un registre Ă  un autre
    mrmovl <D>, <rA> ; Copie d'une valeur de l'adresse D de la mémoire au registre rA
    rmmovl <rA>, <D> ; Copie d'une valeur du registre rA à l'adresse D de la mémoire
    irmovl <V>, <rA> ; Copie d'une valeur immediate V à l'adresse  <rA> de la mémoire
    

  • instructions de rupture de sĂ©quence (conditionnels ou pas). Évalue le rĂ©sultat de la prĂ©cĂ©dente opĂ©ration arithmĂ©tique, que l'on appellera r.

    jmp label ; Se rend au point nommé label (Jump)
    je label ; Jump si r = 0 
    jne label ; Jump si r != 0
    jg label ; Jump si r > 0
    jge label ; Jump si r >= 0
    jl label ; Jump si r < 0
    jle label ; Jump si r <= 0
    

  • gestion de la pile :

    pushl %eax    ; Empile la valeur du registre %eax sur la pile
    popl %ebx    ; DĂ©pile la valeur du dessus de la pile et la stocke dans le registre %ebx
    

  • appel/retour de fonction :

    call label ; Execute la fonction située au label puis reviens ou il s'était arrêté
    ret ; force le retour au point d'où une fonction à été appelée
    

  • divers :

    halt             ; Arrête l'exécution du programme
    nop              ; Aucune opération (instruction nulle)
    

  • Les registres s'Ă©crivent sous la forme : %eax, %ebx...

Activité 1 - Simulation d'un programme en assembleur

Étudions le programme python ci-dessous :

a = 3
b = 5
c = a + b

Le processeur ne comprend pas directement python : les instructions doivent lui être passées en langage-machine. C'est le rôle des interpréteurs (pour le Python, par exemple) ou des compilateurs (pour le C, par exemple) que de faire le lien entre le langage pratiqué par les humains (Python, C...) et le langage-machine, qui n'est qu'une succession de chiffres binaires.

Question : Comment déterminer le langage-machine correspondant à ce code ?

En assembleur Y86, notre programme s'Ă©crirait :

#initialisation de 
#la séquence d'instruction
.pos 0
    # affecte 
    # la valeur a (3)
    # au registre %eax
    mrmovl a, %eax
    # affecte
    # la valeur b (5) 
    # au registre %ebx 
    mrmovl b, %ebx
    # affecte 
    # la somme %eax + %ebx
    # Ă  %ebx 
    addl %eax, %ebx 
    # enregistre 
    # dans la mémoire de c
    # la valeur de %ebx
    rmmovl %ebx, c 
    #  fin
    halt
# définition des variables
.align 4
a:  .long 3
b:  .long 5
c:  .long 0    

Copier ce code dans le simulateur ci-dessous, cliquer sur assembler, puis sur pas à pas, en observant l'évolution de la valeur en mémoire.

Sur la partie droite du simulateur, la zone Mémoire contient, après assemblage, la traduction de notre code en langage-machine au format hexadécimal :

500f1800
0000503f
1c000000
6003403f
20000000
00000000
03000000
05000000

Ce qui correspond correspond au langage-machine binaire suivant (voir chapitre suivant pour conversions hexa-> bin) :

01010000 00001111 00011000 00000000
00000000 00000000 01010000 00111111
00011100 00000000 00000000 00000000
01100000 00000011 01000000 00111111
00100000 00000000 00000000 00000000
00000000 00000000 00000000 00000000
00000011 00000000 00000000 00000000
00000101 00000000 00000000 00000000

Simulation d'un programme en assembleur

Activité 2 - Réaliser un code en assembleur

Sur la base de l'activité précédente, coder en assembleur la séquence d'instruction suivante :

w = 10
x = 3
y = 5
z = w - (x + y)

Astuce

Vous aurez pour cela besoin de l'instruction subl rA rB qui effectue l'opération rB-rA et la stocke dans rB. (rA et rB sont les noms des registres).

.pos 0
mrmovl x, %eax
mrmovl y, %ebx
mrmovl w, %ecx
addl %eax, %ebx
subl %ebx, %ecx
rmmovl %ecx, z
halt

.align 4
w:  .long 10
x:  .long 3
y:  .long 5
z:  .long 0
Activité 3 - Cracker un programme par désassemblage

On considère le programme en langage C suivant :

crackme.c
#include "stdio.h"
#include "stdlib.h"
#include "string.h"

int main()
{

char saisie[20];
printf("Accès restreint : saisissez votre mot de passe \n");
while (strcmp(saisie,"LYCEEXP")!=0)
{
printf("Mot de passe ? \n");
scanf("%19s",saisie);
}

printf("Accès autorisé \n");

return 0;
} 

Questions :

  • Que fait ce programme ?

  • Quel est le mot de passe ?

  • Dans le terminal ci-dessous, rend toi dans le dossier crackme : cd crackme puis tape l'instruction gcc crackme.c -o crackme pour compiler le programme, c'est Ă  dire le convertir en langage machine : le fichier binaire crackme.
  • Tape ./crackme pour lancer le programme et joue avec.

Avec la commande hexedit crackme, il est possible d'observer la valeur des octets directement dans le fichier binaire crackme. Écrit en langage-machine, il est incompréhensible. hexedit nous aide en affichant le code hexadécimal, mais aussi la conversion chaînes de caractères (dans la partie droite).

  • Parcours le code (flèche vers le bas) et retrouve notre mot de passe.

Dans notre code C l'instruction while (strcmp(saisie,"LYCEEXP")!=0) est le cœur de la vérification du mot de passe. En assembleur, elle va donner naissance à une instruction JNE (pour Jump if Not Equal). Cette instruction est codée en hexadécimal par l'opcode 75 BC. Nous allons rechercher ces octets et les remplacer par 90 90, 90 étant l'opcode pour NOP (ne rien faire).

  • Lance hexedit crackme

  • Recherche 75BC (CTR+S).

  • Remplace par 90 90.

  • Sauvegarde le fichier (CTR+O).

  • ExĂ©cute ce code ./crackme et constate les changements !

Cracker des programme grâce au language-machine

Le désassemblage d'un programme est une opération très complexe et les opérations et chaînes de caractères qui apparaissent sont souvent incompréhensibles (parfois volontairement, dans le cas d'obfuscation de code).
NĂ©anmoins, il est parfois possible d'agir au niveau le plus bas (le langage-machine) pour modifier un code, comme nous venons de le faire.

Le compilateur gcc, avec le paramètre -S, permet de générer le langage ASM correspondant. Ce langage, adapté au processeur de votre machine est un peu plus complexe que le langage Y86 étudié plus haut, mais le principe reste le même.

  • Executer gcc crackme.c -S -o crackme.asm

  • Visualiser le fichier gĂ©nĂ©rĂ© avec nano crackme.asm (CTR+X pour en sortir)

Utilise le terminal Linux pour cracker ce logiciel :

Terminal Linux