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.
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.
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.
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
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
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 :)