L'ASM efficace pour lutins pressés

PART II

Dans ce second opus nous allons mettre la barre légèrement plus haut et tenter de faire des choses un peu plus utiles.

N'hésitez pas à vous servir du tuto précédent si certains éléments vous échappent.

I - Le code mystérieux et magique

Le programme suivant se contente de prendre les arguments qu'on lui passe et de les afficher sur la sortie standard. La suite du turoriel permettra de le déchiffrer.

1      section .text
2         global _start
3 
4      section .rodata
5         newline: db 0x0a
6 
7      _start:
8         add esp, 4
9     
10     encore:
11        add esp, 4
12        cmp dword [esp], 0
13        jz fin_encore
14        call affiche_string
15        jmp encore
16
17     fin_encore:
18        mov eax, 1
19        xor ebx, ebx
20        int 80h
21
22     affiche_string:
23        push dword [esp + 4]
24        call asm_strlen
25        add esp, 4
26        mov edx, eax
27        mov ecx, [esp + 4]
28        mov eax, 4
29        mov ebx, 1
30        int 80h
31	
32        mov eax, 4
33        mov ebx, 1
34        mov ecx, newline
35        mov edx, 1
36        int 80h
37
38        ret
39
40     asm_strlen:
41        mov eax, [esp + 4]
42        xor ecx, ecx
43     
44     count_chars:
45        inc ecx
46        cmp byte [eax+ecx-1], 0
47        jnz count_chars
48        dec ecx
49        mov eax, ecx
50        ret

Aaahhh rien de tel qu'un bon petit code source incompréhensible…

II - LigneParLignajation... Terriblement efficace...

Première ligne, bon là je l'ai déjà expliqué.

Ligne 5, on déclare une constante, newline. Chaque fois que nasm rencontrera dans notre code le mot “newline” il le remplacera par 0x0A qui est la valeur ascii du fameux “\n”.

Nous voilà lancé, et déjà première question: ça veut dire quoi “add esp, 4” en ligne 8? Et bien c'est très simple, vous savez tous ce que signifient argc et argv dans la ligne “int main(int argc, char **argv)” ?

En fait lorsqu'on lance un programme, tout en haut de la stack se trouve argc, puis vient ensuite argv[0], puis argv[1], etc… soit :

     ESP = argc
     ESP+4 = argv[0]
     ESP+8 = argv[1]
     ...
     ESP+4+4*argc = argv[argc] = NULL

Attention ceci n'est pas toujours vrai, nous verrons l'autre configuration possible dans le prochain tutoriel. Cette configuration est valable uniquement lorsqu'on utilise “_start” et donc qu'on link avec ld.

Que fait donc notre “add esp, 4” ? Il se contente “d'éliminer” en quelque sorte argc de notre “champ de vision” ou, pour parler en termes techniques , de notre stack frame (délimitée par esp et ebp).

Après cette commande notre stack qui était structurée comme ci dessus deviens :

     ESP = argv[0]
     ESP+4 = argv[1]
     etc...

Ligne 10, notre premier label nommé “encore”. Un label permet aux instructions de saut (jmp, jnz, etc…) d'accéder à une partie du code.

Ligne 11, si vous avez bien compris ce que j'ai tenté de vous expliquer plus haut, add esp, 4 aura pour effet d'enlever argv[0] de la stack frame. Pourquoi ? Car argv[0] c'est le nom du programme et on s'en fout.

Ligne 12, cette ligne permet de voir si esp pointe pas sur NULL, ce qui signifierais qu'il n'y a pas ou plus d'arguments passés au programme. NULL = 0, d'où la comparaison avec 0.

Ligne 13, jz signifie Jump If equal Zero. Et donc si ZF est à 0, ce qui signifie que le résultat de CMP n'est pas 0, sinon cela signifierai qu'il n'y a plus d'arguments passés au programme et qu'il peut se terminer, d'où le label “fin_encore”, qui, devinez quoi, appelle exit.

Ligne 14, vous découvrez l'instruction call, qui appelle la “procedure” affiche string. Nous y reviendrons.

Ligne 15, une fois la procedure affiche_string terminée, le cpu retourne dans la partie “encore”, et l'instruction jmp se contente de boucler à nouveau, pour vérifier s'il y a encore des arguments à afficher.

Ligne 22, “déclaration” de la procédure. C'est un label tout simplement mais vous remarquerez l'instruction “ret” qui indique donc qu'il s'agit d'une procédure ou fonction, et non pas d'un simple label.

Ligne 23, Push ! c'est quoi ce machin bidule?

PUSH ELEMENT
Cette instruction permet d'ajouter un élément sur la pile.

POP REGISTRE
Cette instruction permet d'enlever un élément de la pile et de le placer dans REGISTRE ou une variable au préalable allouée.

Lorsqu'on utilise l'instruction CALL, celle-ci va PUSHer sur la stack l'adresse de retour. En gros, le CPU a besoin de savoir où il va devoir reprendre une fois la fonction terminée, du coup il met l'adresse sur la pile.

Ceci implique qu'il ne faut jamais écraser cette valeur! (enfin si vous utilisez ret, sinon on s'en fout, mais c'est pas propre). Il faut impérativement la conserver, sinon vous allez avoir droit a un comportement totalement indéfini. Le processeur va prendre le premier truc sur la pile et exécuter ce qu'il trouve à cette adresse, si ça n'a aucun sens, la plupart du temps ce sera un SIGSEGV.

Revenons à notre ligne 23. Le mot clé dword indique simplement que ce que nous allons PUSHer est un double mot, sur 32 bits donc. Lorsqu'on utilise [ et ] en syntaxe intel cela signifie que l'on fait référence à la mémoire.

Traduction de “push dword [esp + 4]” :
Mettre en haut de la stack ce qui se trouve à l'adresse ESP+4, de taille 32 bits. Bon, la traduction est pas super, mais vous aurez compris quoi.

En fait on ne fait que copier argv[1] en haut de la stack :

     ESP= argv[1]
     ESP+4= return_addr
     ESP+8= argv[1]
     etc...

Pourquoi est-ce qu'on fait ça ? Car avant d'utiliser CALL, ce qu'on PUSHe sur la stack ce sont les arguments que l'on passe à la fonction.

Ligne 24, ici on appelle la fonction asm_strlen, qui comme vous vous en doutez est une implémentation d'strlen en assembleur. Si vous vous souvenez bien, strlen est définie comme ça : “size_t strlen(const char *s);”

Et bien notre implementation sera identique et prendra en argument un pointeur sur un char, et retournera un size_t, un entier quoi.

NOTE: Nous allons le voir plus bas, mais sachez qu'une fonction met sa valeur de retour dans le registre EAX. Et donc, pour le formuler autrement, après un CALL, eax contiendra la valeur de retour de la fonction appellée.

Ligne 25, on enlève l'élément PUSHé sur la stack en ligne 23. Il faut TOUJOURS enlever les arguments que vous avez mis sur la pile. Soit par un add, soit pas un pop.

Ligne 26, on met la valeur de retour, et donc la lenght de notre cher argv[1] dans edx. Oui puisque le but du programme et d'afficher les arguments qu'on lui a passé, on va forcément faire appel à write et donc comme on l'a vu dans la PART I, pour appeller write on a besoin de 4 registres. EAX qui contiendra le chiffre de l'apel systeme, ici 4, ebx qui contiendra le descripteur de fichier sur lequel écrire, ici 1 pour stdout, ecx qui contiendra un char *, et edx qui contiendra le nombre de caractères à écrire, et donc la len de notre string calculée par asm_strlen, dont la valeur se trouve dans eax après le call et que l'on met bien évidemment dans edx.

Si vous me dites que vous avez besoin que j'explique encore une fois je vous met mon poing sur la figure en toute amicalité aucune.

Ligne 27, notre fameux argv[1] est mis dans ecx.

Ligne 28, 4 pour write dans eax.

Ligne 29, 1 pour stdout dans ebx.

Ligne 30, appel du kernel, allez hop, bosses un peu fénéant.

Les lignes qui suivent son facilement déchifrables suite aux explication précédentes. Mais au cas où certains se poseraient des questions, on appelle write pour afficher un “\n”…

Ligne 41, ici on met le paramètre passé à notre fonction dans eax. Ici cet argument cera notre argv[1]… il persiste lui…

XOR
Cette instruction effectue un OU exclusif des deux opérandes et sotcke le résultat dans celle de gauche.

Ligne 42, lorsqu'on a ce genre de lignes “xor eax, eax”, et donc que les deux opérandes sont le même registre, cela signifie qu'on le met à 0. Ici donc ecx est mis à 0.

Ligne 44, un petit label qui va nous permettre de compter le nombres de caractères de la string passée en paramètre.

Ligne 45, on incrémente ecx, qui va contenir le nombres de caractères de la string.

J'ai choisi ECX, mais j'aurais pu en choisir un autre hein..

Ligne 46, ici on vérifie si string[ecx-1] != \0, le fameux \0 de fin de chaine. EAX contient l'adresse du premier caractère de la string, ECX est un peu comme un i en C.. vous savez les i++ qui trainent dans tous les codes sources.. Ben ici c'est pareil, sauf que i c'est ECX. Le ++ se situe en ligne 45 avec l'instruction inc…

Ligne 47, si donc ZF est à 0, cela signifie qu'il y a encore des caractères et qu'on peut continuer à incrémenter. Du coup loop sur count_chars.

Ligne 48, on décrément ECX, que nous avions incrémenté injustement en ligne 45, pour avoir le nombre exact de caractères.

On met ce fameux résultat dans EAX, oui car comme je l'ai dis plus haut, une fonction met la valeur de retour dans EAX.

Puis on utilise RET pour que le CPU retourne au contexte d'appel. C'est à dire qu'on sort de la fonction asm_strlen.

Bon, c'était pas si dur ? Si vous avez des zones d'ombres, relisez tout bien, et essayez de comprendre le code par vous même. Vous pouvez aussi essayer de rajouter quelques fonctionalités.

Dans la troisième et sans doute dernière partie nous étudierons la conception d'un serveur socket en Assembleur, ce qui j'espère vous excite un peu quand même :)

Tous les liens sympas que j'ai sur l'assembleur je les mettrai en fin de la troisième partie.

jfg 2007/09/21 00:29

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