vendredi 15 août 2008

Au coeur des fonctions C, dissection d' une fonction

Qu' est-ce qui ce passe avant, pendant, et après l' apel d' une fonction C ? C' est ce que je vais décrire pas à pas.Pour pouvoir suivre cette article il est indispensable de connaitre, le fonctionnement d' une stack (pile), et avoir les bases du language assembleur, syntaxe intel, et AT&T pour les sortie avec GDB, une connaissance aussi du debugger GDB ne sera pas de trop, et biensur une bonne maitrise du langage C, bien que ce soit juste les fonctions qui nous intéressent ici.

Avant de commencer je vais quand même rappeler brièvement quelques points fondamentaux.

La mémoire d' un ordinateur est adressée par mot (word) de 4 octets (32bit) et débute à 0x0000000 et fini à 0xfffffff.
Cette espace de mémoire est divisé en deux zones :

1) L 'espace user de 0x00000000 à 0xbfffffff
2) L 'espace kernel de 0xc0000000 à 0xffffffff

Quand un processus est lancer , il est mappé en mémoire et dispose d' un espace de mémoire indépendant de tout les autres processus.(En gros je passe les détails)
Une fois charger en mémoire le processus est divisé en plusieur section:
.text la ou est placer le code du programme
.data la ou sont placer les données initialisées ou non.
.bss la ou sont placer les données globales non initialisées

En plus il y a la stack (pile) qui sert de stockage pour y placer des données temporaire, les variables local d' une fonction seront par exemple placer sur la pile.Maintenant essayons de comprendre le fonctionnement de cette stack.
La stack peut être vue comme une jeux de carte, elle grandit vers les adresses basses, pour revenir au jeux de carte c 'est comme si on ajoutais des cartes par dessous, on appelera %ebp la base de la pile donc le sommet de notre jeux de carte, et on appelera toujour %esp le pointeur vers la dernière valeur(carte) qu' on a ajouter.

adresse haute ------ %ebp ( haut du jeux de carte )
------
------
adresse basse ------ <-- %esp ( dernière carte ajouter )

l' opcode push ajoute une carte, et pop retire une carte, ce qu' il faut bien retenir surtout, c' est que cette pile marche selon le modele LIFO "last in first out" c' est à dire que la dernière donnée(carte) ajouter sera toujour le première à sortir.
Enfaite on ne verra pas ces 2 instructions ici,car gdb utilise la syntax AT&T qui est je l' avoue franchement plus chiante a lire que celle d' intel, mais bon, on a rien sans rien donc on apprend les deux et on s' écrase...

Voila c' étais bref mais le but n' est pas de faire un cour sur l' assembleur et la mémoire d' un ordinateur.

Bon pour tout le reste de cette article je me baserais uniquement sur cette exemple de programme C:

#include <stdio.h>

int addition(int parametre1, int parametre2)
{
int res = 0;
res = parametre1 + parametre2;
printf("%d", res);
}

int main()
{
addition(5, 6);
return 0;
}


Donc maintenant on compile et lance avec celui qui va nous permettre d' analyser le programme [ GDB ]:

Afin d' avoir des valeurs qui tombe juste j' ai compiler avec l' option -mpreferred-stack-boundary=2 et -g bien évidement pour le debug.

napoleon@s4t4n:~$ gdb ./exemple
GNU gdb 6.8-debian
Copyright (C) 2008 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "i486-linux-gnu"...
(gdb) disass main
Dump of assembler code for function main:
0x0804839f <main+0>: push %ebp
0x080483a0 <main+1>: mov %esp,%ebp
0x080483a2 <main+3>: sub $0x8,%esp
0x080483a5 <main+6>: movl $0x6,0x4(%esp)
0x080483ad <main+14>: movl $0x5,(%esp)
0x080483b4 <main+21>: call 0x8048374 <addition>
0x080483b9 <main+26>: mov $0x0,%eax
0x080483be <main+31>: leave
0x080483bf <main+32>: ret
End of assembler dump.
(gdb) disass addition
Dump of assembler code for function addition:
0x08048374 <addition+0>: push %ebp
0x08048375 <addition+1>: mov %esp,%ebp
0x08048377 <addition+3>: sub $0xc,%esp
0x0804837a <addition+6>: movl $0x0,-0x4(%ebp)
0x08048381 <addition+13>: mov 0xc(%ebp),%eax
0x08048384 <addition+16>: add 0x8(%ebp),%eax
0x08048387 <addition+19>: mov %eax,-0x4(%ebp)
0x0804838a <addition+22>: mov -0x4(%ebp),%eax
0x0804838d <addition+25>: mov %eax,0x4(%esp)
0x08048391 <addition+29>: movl $0x8048480,(%esp)
0x08048398 <addition+36>: call 0x80482d8 <printf@plt>
0x0804839d <addition+41>: leave
0x0804839e <addition+42>: ret
End of assembler dump.
(gdb)


Commencons par regarder ce qui ce passe en main+6 car c' est ici que débute notre fonction.Enfaite c' est pas le début mais la préparation de la fonction, a ce moment elle n' a pas encore été appelé.
movl $0x6,0x4(%esp)
Cette instruction peut être traduite par : Ajoute la valeur 0x6 (6 en décimal) a +4 de l' adresse pointé par %esp.En syntaxe Intel on pourrais faire push dword 0x6
Et oui ! %esp contient une adresse.
Et cette valeur 6 est bien le deuxième argument de notre fonction addition, enfaite il empile les valeurs en sens inverse ce qui ce vérifie encore en main+14 movl $0x5,(%esp) ou il empile le premier argument.

En main+21 on retrouve un call:
call 0x8048374 <addition>
call est un saut inconditionnel c' est à dire que lorsque qu' il arrive ici il saute sans aucune condition a l' adresse 0x8048374 qui est bien l' adresse de notre fonction addition:
0x08048374 <addition+0>: push %ebp
Pendant cette apel il va ce passer une opération implicite, il va sauvegarder l' adresse de retour de la fonction, donc %eip, puisque je rapel que %eip pointe toujour sur l' adresse de la prochaine instruction à executer.

Une fois nos argument, et l' adresse de retour sauvegarder il va ce passer ce qu' on apel le prologue de la fonction:
0x08048374 <addition+0>: push %ebp
0x08048375 <addition+1>: mov %esp,%ebp

En addition+0 il prepare le nouvel environnement pour la fonction addition,il sauvegarde %ebp sur la pile, puis en addition+1 il met %esp dans %ebp donc a ce moment la %ebp = %esp

Après il va reserver de la place pour les variables locales de la fonction.Il décremente %esp de 12(0xc), en gros il soustrait 12 a %esp:
sub $0xc,%esp
Pourquoi 12 ? Un peu de calcul, nous avons 3 variables de type (int) qui sont des entiers, un entier en général prend 4 octets, donc 3 x 4 = 12 (0xc), par contre si on aurais un char, donc 1 octets il aurais quand même reserver 0x4 puisque il réserve que des multiples de 4, si on on aurais un chaine de 6 octets il aurais réserver 0x8 (a ne pas oublier).

Ensuite passons en addition+13 et addition+14
mov 0xc(%ebp),%eax
add 0x8(%ebp),%eax
Il place la valeur qui est en %ebp+12 dans %eax, puis en addition+16 il additione la valeur qui est placer en %ebp+8 avec la valeur de %eax.

Regardond de plus près ce que contient %ebp+12, %ebp+8 à ce moment la:


(gdb) b addition
Breakpoint 1 at 0x804837a: file exemple.c, line 5.
(gdb) r
Starting program: /home/napoleon/exemple

Breakpoint 1, addition (parametre1=5, parametre2=6) at exemple.c:5
5 int res = 0;
(gdb) x/bc $ebp+12
0xbf8535d4: 6 '\006'
(gdb) x/bc $ebp+8
0xbf8535d0: 5 '\005'
(gdb)


La tout devient plus clair !!
mov 0xc(%ebp),%eax place moi la valeur 0x6(6 en déacimal) dans %eax.
add 0x8(%ebp),%eax Additionne moi la valeur placer dans %ebp+8 qui est donc 0x5 (5 en décimal) avec %eax.
ce qui donne bien l' addition qu' on lui a demander de faire 5+6 dans la fonction.

Vérifions maintenant ce que contient %eax à la ligne 7:

Breakpoint 2 at 0x804838a: file exemple.c, line 7.
(gdb) c
Continuing.

Breakpoint 2, addition (parametre1=5, parametre2=6) at exemple.c:7
7 printf("%d", res);
(gdb) p $eax
$1 = 11
(gdb)


Il est donc clair que %eax sers a manipuler des données, beaucoup d' opération passe par lui, entre autre ici il contient le résultat de notre opération soit (11).

En dernier il faut remmettre l' état de la pile comme elle était avant l' apel de la fonction, ce qui ce fait avec ces deux instructions:
leave
ret
Enfaite leave est équivalent à:
mov ebp,esp
pop ebp

Donc %ebp et %esp reviennent ou ils étaient avant l' apel de la fonction.
Puis viens le
ret
Qui a pour effet de dépiler %eip qui contient l' adresse de retour. ainsi le programme reprendra son execution la ou il l' avait interrompu.
On verra dans un autre article qu' il est possible de modifier cette adresse de retour afin que le programme saute ou on lui dit de sauter, c' est le principe des buffer overflow et d' ailleur cette article est je pense une bonne mise en jambe pour ça.
Voilà on a vue en détail avant, pendant, et après l' apel d' une fonction.

Aucun commentaire: