Programmes binaires et Compilateurs

Tags :
  • MON
  • 2022-2023
  • temps 2
  • Programmes Binaires
  • Compilateur
  • Yacc et Lex
  • C
Auteurs :
  • Jean-Baptiste Durand

Comment fonctionne le language machine - binaire - et comment créer un compilateur de language informatique.

Table des matières

Language Machine

Qu'est ce que le language machine ?

Le language machine est un language informatique reconnu par l'ordinateur et qu'il peut executer directement sans l'aide d'un intermédiaire (pas besoin d'un interpréteur de language). Par exemple, le C est un language informatique compilé, et Python est un language interprété.

Arbre de calcul
Avantage Inconvénient
Language Interprété Est cross-platform, fonctionne pour n'importe quel appareil Toute personne voulant executer le code doit avoir l'interpréteur sur sa machine
Language Compilé Nécessite d'utiliser le compilateur une unique fois Un fichier compilé est spécifique à une distribution, un fichier Linux est différent d'un Windows

Un language compilé peut être lu, pour comprendre ce que fait un programme avant de l'executer, ce qui peut être une bonne idée si le programme compilé est un virus.

Sur Linux, il est possible de récupérer le code machine d'un fichier compilé grâce à la commande objdump (pas disponible par défaut)

objdump -d fichierCompilé > languageMachine.txt

Il existe aussi un logiciel permettant de décompiler des fichiers : Ghidra. Ce logiciel est disponible sur Linux, Windows et MacOS et a beaucoup plus d'outil permettant de comprendre comment fonctionne le fichier compilé.

Voilà à quoi peut ressembler une partie de code désassemblée. (Ici seulement la partie principal du code)

0000000000001149 <main>:
    1149:       f3 0f 1e fa             endbr64 
    114d:       55                      push   %rbp
    114e:       48 89 e5                mov    %rsp,%rbp
    1151:       48 83 ec 10             sub    $0x10,%rsp
    1155:       c7 45 f4 02 00 00 00    movl   $0x2,-0xc(%rbp)
    115c:       c7 45 f8 05 00 00 00    movl   $0x5,-0x8(%rbp)
    1163:       8b 55 f4                mov    -0xc(%rbp),%edx
    1166:       8b 45 f8                mov    -0x8(%rbp),%eax
    1169:       01 d0                   add    %edx,%eax
    116b:       89 45 fc                mov    %eax,-0x4(%rbp)
    116e:       8b 45 fc                mov    -0x4(%rbp),%eax
    1171:       89 c6                   mov    %eax,%esi
    1173:       48 8d 05 8a 0e 00 00    lea    0xe8a(%rip),%rax        # 2004 <_IO_stdin_used+0x4>
    117a:       48 89 c7                mov    %rax,%rdi
    117d:       b8 00 00 00 00          mov    $0x0,%eax
    1182:       e8 c9 fe ff ff          call   1050 <printf@plt>
    1187:       b8 00 00 00 00          mov    $0x0,%eax
    118c:       c9                      leave  
    118d:       c3                      ret

Lire un fichier désassemblé

L'exemple précédent est le language machine associé à cette fonction écrite en C.

int main()
{
    int a=2;
    int b=5;
    int c;

    c= a + b;
    printf("%d\n",c);
    return 0;
}

Voici quelques fonction qui permettent de comprendre le fonctionnement global du code.

Permettent de stocker en mémoire une valeur.

mov quand cette valeur était déjà stockée ailleurs

movl quand c'est une nouvelle valeur.

Permet d'ajouter 2 valeurs

Permet d'appeler la fonction située à l'emplacement mémoire associé

Registre de mémoire

Il y a 4 registres de travail : eax, ebx, ecx et edx, ayant 32 bits chacun.

À ces 4 registres, s'ajoute 4 autres registres :

Dans l'exemple précédent, on peut voir notamment rax et rbp, ils fonctionnent de la même façon que respectivement eax et ebp, je ne sais pas quel est la différence entre les 2.

Compilateur de language

L'objectif est de créer sont propre language informatique et de créer le compilateur associé.

J'ai utilisé les 2 technologies yacc et lex, qui permettent de créer de manière basique, un compilateur, en C.

Lex

Lex est un générateur d’analyseur lexical, c'est à dire qu'il reconnaît les expressions régulières et peut leurs associer des actions.

exemple :

[0-9]+          {yylval.num = atoi(yytext); return NUMBER;}

[0-9] est une expression régulière permettant de reconnaître les caractères '0', '1', '2', '3', '4', '5', '6', '7', '8' et '9' + permet de trouver toutes expression respectant au moins une fois ou plus l'expression juste avant. [0-9]+ reconnaît tous les nombres, ayant 1 ou plusieurs chiffres.

Donc l'exemple va reconnaître les nombres et stocker leurs valeurs

Yacc

Yacc est un générateur d’analyseur syntaxique, c'est à dire qu'il va reconstruire une syntaxe à partir de plusieurs éléments de cette syntaxe.

exemple :

Expression:Expression'*'Expression      {$$=$1*$3;}
    |Expression'/'Expression            {$$=$1/$3;}
    |Expression'+'Expression            {$$=$1+$3;}
    |Expression'-'Expression            {$$=$1-$3;}
    |Expression'%'Expression            {$$=$1%$3;}
    |'('Expression')'                   {$$=$2;}
    |'-' Expression %prec UMINUS        {$$=-$2;}
    | NUMBER                            {$$=$1;}
;

On définit qu'un calcul peut être l'addition de 2 calculs, la différence entre 2 calculs ou un nombre. Ce qui permet de reconstruire tous les calculs possibles entre des nombres et en faisant des additions et des soustractions.

L'objectif va être de construire un arbre pour faire les opérations. Par exemple pour le calcul suivant 12+4(7+2)*, l'arbre suivant sera généré :

Arbre de calcul

Cet arbre est ensuite parcouru en Postfixe, ce qui donne :

12 4 7 2 + * +

Cette écriture est très pratique pour calculer, si on utilise une pile LIFO (ce qui est le cas dans le language machine).

Pour lire ce genre de calcul, on lit le calcul de la gauche vers la droite, si on a un nombre, on empile ce nombre, si on a un opérateur, on dépile les 2 dernière valeurs, on effectue le calcul, puis on empile le résultat.

Ici tous les opérateurs se sont retrouvés à la fin, mais on peut tout à fait avoir ce genre d'expression 12 5 - 4 7 2 + * +

Mes difficultés pour faire de l'algorithmie avancée

J'avais d'abord envie de faire les fonctions basiques de programmation (if, for, while)

Le principal problème avec YACC, c'est que les feuilles de l'arbre générées sont exécutées avant les racines (c'est lié à la lecture Postfixe de l'arbre).

Voulant créer une fonction if, j'ai naïvement écrit dans mon fichier yacc:

ifFunction : IF Condition action ELSE action    {if($2){printf("Condition passée \n");}else{printf("Possibilité par défaut \n");};}
| IF Condition action                           {if($2){printf("Condition passée \n");};}

et en executant une fonction if :

if 1>0 {print(True);} else print(False);

J'obtiens comme résultat :

Booleen : 1
Booleen : 0
Condition passée

Les deux actions - dans le cas où la condition est vraie ou fausse - sont exécutées.

Il n'est pas possible de bloquer l'execution d'une action, ce qui est embêtant pour une fonction if.

Ensuite, il n'est pas possible d'aller à un endroit précis du code, rendant les boucles for et while impossible.

Dans le language machine, une boucle while s'execute de la manière suivante :

0   condition
1   jmp 4
2   code de la boucle
3   jmp 0
4   code après la boucle

Il est donc primordial de pouvoir sauter d'une ligne d'exécution à une autre pour faire des boucles.

Solution à ces problèmes

Il n'est pas impossible de contourner ces problèmes, pour cela il faut manuellement reconstruire les lignes d'executions grace au parseur de yacc et ensuite executer le code. Voici un exemple d'une personne ayant réalisé cela : compiler if and while

Liens Utiles

Language machine

Cours architecture des ordinateurs

Cours d'assembleur

Désassembleur

Ghidra

Documentation objdump

Yacc et Lex

Cours 1

Cours 2

Header Yacc

Complément Header Yacc

Exemple avec boucle if et while

Mon projet

GitHub