L'ASM efficace pour lutins pressés

PART I

Ce petit tutoriel a pour but de vous apprendre a programmer rapidement en assembleur. Il s'adresse à des personnes ayant plus que des notions en C ou tout autre langage plus haut niveau que l'ASM, hors langages de balisage tels que le HTML qui ne vous seront d'aucune utilité ici.

Le ton sera imprécis et circoncis, je ne m'attarderais pas sur les points que je juge trop théoriques. Si par moments vous vous dites : “Mais pourquoi il a mis ça là? Et pis ça veut dire quoi d'abord?”, alors je vous invite à vous référer aux liens cités en fin de tutoriel.

I - Débroussaillage.. TtZzzRzzRrrRRzz Prrrt PrrRrtt..

Comme vous le savez tous, nos chers ordinateurs sont composés de composants, dont le CPU fait partie. Ce fameux CPU pour faire pleins de calculs contient des variables nommées Registres. Sur l'architecture X86 il en existe tout un tas et je ne vais pas m'attarder là dessus.

Nous nous contenteront pour commencer de citer les plus connus et ceux qui nous seront utiles : EAX, EBX, ECX, EDX, ESP, EFLAGS.

EAX, EBX, ECX et EDX, sont des registres généraux qui vont nous permettre de stocker des nombres, des strings, des adresses mais aussi des fringues des voitures et plein d'autres trucs cools (éléphants, mouettes, loutres, jambons de bayonne, etc…)

ESP est un registre qui contiendra toujours l'adresse de l'élément le plus haut sur la pile (ou stack en anglais).

EFLAGS est un registre qui contient plein de drapeaux (ou flags en anglais) qui vont nous permettre de connaitre le résultat de divers tests.

La stack késséssai ? C'est une pile de données. Une pile késséssai ? Bah c'est une pile. Genre t'as plein de livres et tu les empiles, ça te donne une pile. Pour se déplacer dans la pile et accéder aux valeurs on utilisera toujours ESP. Pour se déplacer dans la pile on va toujours de 4 en 4. Genre le premier élément se trouve avec ESP, et le second avec ESP+4, et le troisième avec ESP+8, ainsi de suite.

II - Compréhensage.. GnnnnnnGnnn iiiGniiiaaaaiiii.. aaaaRgr..

Pour utiliser ces variables, les registres, nous allons utiliser des instructions. Il en existe tout un tas et on ne vas se servir que des plus simples, mais quand mêmes puissantes.

MOV REGISTRE, DONNEE
Cette instruction permet de mettre une donnée dans un registre ou une variable. Ex: mov eax, 4 Va mettre 4 dans le registre eax.

ADD REGISTRE, VALEUR
Cettre instruction va ajouter VALEUR à la valeur que contient déjà le registre. Ex: si ebx vaut 8 alors après un “add ebx, 4” ebx vaudra 12. Jusque là ça va ?

SUB REGISTRE, VALEUR
La même chose que l'instruction mais au lieu d'additionner elle permet de soustraire une valeur.

INC REGISTRE
Permet d'incrémenter (ajouter 1) à un registre ou une variable. Equivaut à un “add REGISTRE, 1”.

DEC REGISTRE
Permet de décrémenter (enlever 1) à un registre ou une variable. Equivaut à un “sub REGISTRE, 1”.

JMP LABEL
Cette instruction effectue un saut jusqu'au LABEL puis exécute les instructions qui suivent le label. Nous verrons plus tard comment on déclare un label.

CMP REGISTRE, VALEUR
Cette instruction permet de comparer deux valeurs. Elle permet, avec des instructions conditionelles de saut, de reproduire l'équivalent d'un branchement if/else. Cette instruction modifie certains bits du registre EFLAGS. En effet elle va soustraire VALEUR au REGISTRE et modifiers certains flags en fonction du résultat.

JNZ LABEL
Jump if Not equal Zero. Après une instruction CMP certains bits de EFLAGS vont changer d'état. C'est à dire passer de 1 à 0 ou l'inverse. Les instructions de saut conditionelles telles que JNZ, vont vérifier l'état d'un Flag dans le registre EFLAGS et sauter au label indiqué si la condition est vraie. Dans EFLAGS il y a un drapeau nommé zeroflag (ZF), qui change d'état si un résultat est nul (il se met à 1).

Si donc la soustraction CMP REGISTRE, VALEUR vaut 0, ZF sera à 1. JNZ signifie qu'il faut sauter si ZF n'est pas à 0.

CALL FUNCTION
Similaire à jmp, sauf que la partie de code vers laquelle on jmp devra se terminer par l'instruction RET. De toute façon pour le moment on s'en fout alors ne paniquez pas.

Il existe plein d'autres commandes mais vous les apprendrez sur le tas.

III - Passage à l'acte... Sarah Connor ? Non c'est à côté...

NON, vous n'y échapperez pas. “Hello world!”…

En C:

  #include <stdio.h>

  int main(int argc, char **argv) {
      write(1, "Hello World!\n", 13);
      exit(0);
  }

On compile avec:

gcc hello.c -o hello

En ASM:

  section .data
      strhello: db "Hello World!\n"
      lenhello: equ $-strhello

  section .text
      global _start

  _start:
      mov eax, 4
      mov ebx, 1
      mov ecx, strhello
      mov edx, lenhello

      int 80h

      mov eax, 1
      mov ebx, 0

      int 80h

On assemble avec:

nasm -f elf hello.asm

et on link avec

ld hello.o -o hello

IV - Décortiquationage... (bruitages en grève)

Bon, déjà c'est quoi ces trucs là “section machin”… En assembleur, et je ne vous dirais pas pourquoi c'est comme ça, mais il y a des sections qui doivent se trouver en début de code. Ces sections diffèrent en fonction du format de fichier de votre OS (elf, olf, …)

.data
Dans cette section on déclare des variables en leur affectant une valeur.

.rodata
Dans cette section on déclare des constantes. Ou si vous préférez des variables dont on ne pourra pas modifier le contenu.

.bss
Dans cette section on ne fait que déclarer des variables en réservant simplement de l'espace mémoire. (on y reviendra)

.text
Dans cette section on déclarera les fonctions externes et la fonction principale.

On peut certainement faire d'autres trucs avec les sections, et il en existe sûrement d'autres, mais on s'en fiche on a pas besoin d'en savoir d'avantage pour le moment.

Bon ensuite, ça veut dire quoi tous ces chiffres qu'on met dans des registres et puis int 80h, c'est quoi ?

Les chiffres qu'on met dans les registres c'est dans un premier temps le numéro de l'appel système que l'on souhaite exécuter puis ses arguments.

Pour connaitre le numéro de chaque appel système il faut regarder le fichier /usr/include/asm-i386/unistd.h Dans ce fichier on a la ligne : #define __NR_write 4 Qui signifie que pour le noyau, 4 = write. On pourra remarque également que exit = 1.

Si on fait un “man 2 write” et qu'on regarde la déclaration de cet appel système on apprend qu'il prend en premier argument le descripteur de fichier sur leque écrire, en second argument la string qu'il doit écrire et en dernier argument la longeur de cette chaine de caractères.

Vous êtes censés savoir que stdin = 0, stdout = 1, et stderr = 2.

D'où le code suivant :

   mov eax, 4          ; 4 signifie ici "write"
   mov ebx, 1          ; 1 pour stdout
   mov ecx, strhello   ; La string que write requiert
   mov edx, lenhello   ; Et la lenght de cette dernière

Comme vous le devinez sans doute, int 80h, dit au kernel “exécute les instructions placées dans les registres.” Le kernel va donc regarder la valeur d'eax et comprendre (il est futé) qu'il faut appeller write, puis il va aller chercher son premier argument dans ebx, le second dans ecx et le troisième dans edx, et si tout vas bien vous devririez voir un joli “Hello World!” dans votre shell.

Bien évidemment c'est une explication simpliste, mais c'est pour que vous compreniez, si vous voulez approfondir sur les interruptions, il y plein de doc chouettes qui existent. (oui car int signifie interrupt ou en français interruption).

Quant au equ $-lenhello, c'est une bidouille nasmique qui permet de récupérer la longeur de la chaine strhello, cf manuel de nasm. Par contre l'instruction EQU permet de déclarer une constante, comme nous le verrons plus tard.

En guise de conclusion, je vous laisse deviner ce que font les trois dernières lignes de notre programme.. :)

Dans le prochain tuto nous aborderons le passage d'arguments à un programme ainsi que les sauts et les fonctions.

jfg 2007/09/21 00:41

Linux/BSD Compatibility

Le monde étant ce qu'il est, les abeilles ne pollenisant plus les fleurs à cause d'un virus et le soleil se réveillant en septembre, il s'avère que BSD et Linux ont décidé de faire les choses différemments.

Si sous Linux, le passage des arguments se fait par les registres ebx, ecx, edx et autres, sous BSD, il en va tout autrement: Les syscall prennent leur paramètres comme toute fonction normale, c'est à dire par la pile.

Plutot qu'on long exemple, je vous indiquerais plutot ce lien qui explique comment rendre son code assembleur portable de bsd vers linux grace à une macro :
FreeBSD Developers Handbook

( CF Part II & III du tuto :)

codaz/asm/l_assembleur_pour_lutins_presses.txt · Last modified: 2010/01/12 13:29 (external edit)