Partie 17 - Les programmes 64 bits
Les programmes 64 bits
Bon, câest vrai que ça fait dĂ©jĂ pas mal de chapitres que lâon a faits ensemble, il est enfin temps dâen toucher quelques mots.
Les principales différences
PremiÚrement, voyons sont les principales différences entre un programme 32 bits (x86) et un programme 64 bits (dit x86_64, amd64 ou x64) :
- Il y a plus de registres :
- x86 :
eax,ebx,ecx,edx,esp,ebp,esi,edi,eip⊠- x86_64 :
rax,rbx,rcx,rdx,rsp,rbp,rsi,rdi,rip,r8,r9,r10,r11,r12,r13,r14,r15âŠ
- x86 :
- La taille des registres en 64 bits est de ⊠64 bits (merci Sherlock đ”ïžââïž). Les registres peuvent donc dĂ©sormais contenir un
qword. - La taille des registres ayant augmentĂ©, il est possible dâaccĂ©der Ă un espace mĂ©moire bien plus Ă©levĂ© :
- x86 : 4Go (~ 4âč octets)
- x86_64 : 16Eo (~ 18Âčâž octets đ„”)
- Les conventions dâappel sont diffĂ©rentes vu quâen x86_64 il y a plus de registres disponibles :
- x86 : les arguments sont principalement passés via la pile
- x86_64 : les arguments sont principalement passés par les registres
- En raison de tailles de registres plus importantes, les programmes 64 bits sont souvent plus rapides que les programmes 32 bits.
- De nouvelles instructions ont été introduites en x86_64 telles que
movabsousyscall.
đ€ Les conventions dâappel
Les conventions de quoi đł ?
Les conventions dâappel sont les rĂšgles qui rĂ©gissent les appels et retour de fonction. Elles stipulent notamment la maniĂšre dont les arguments sont passĂ©s (ex : par la pile).
Elle permet Ă©galement de stipuler qui est en charge de âviderâ la pile lorsquâune fonction a fini son exĂ©cution : est-ce Ă la fonction appelante ou appelĂ©e de faire cela ?
â€Žïž La valeur de retour
Commençons par le plus simple : oĂč est stockĂ©e la valeur de retour ? Câest plutĂŽt clair :
- x86 : la valeur de retour est stockée dans
eax - x86_64 : la valeur de retour est stockée dans
rax
VoilĂ đ€ !
Le passage des arguments
En ce qui concerne le passage des arguments, cela sâopĂšre diffĂ©remment. En effet, le fait dâutiliser la pile sâest avĂ©rĂ© utile car la logique derriĂšre nâĂ©tait pas trĂšs compliquĂ©e : on empile les arguments un Ă un et la fonction appelĂ©e sait exactement oĂč les trouver (pour rappel : en dessous de lâadresse de retour).
NĂ©anmoins le soucis dâutiliser le pile est que ⊠celle-ci se trouve en mĂ©moire. Cela signifie quâĂ chaque fois que lâon souhaite appeler une fonction il faut :
- empiler les arguments, et donc écrire en mémoire
- récupérer les arguments, et donc lire en mémoire
Or, comme vous le savez, les accĂšs mĂ©moire pour le processeur sont bien plus lents que les accĂšs aux registres situĂ©s dans le processeur. Ainsi, les nouvelles conventions dâappel 64 bits sont venues proposer une maniĂšre plus efficace de passer les arguments, tout simplement : utiliser les registres.
Cependant, il nâexiste pas une seule maniĂšre de transmettre des arguments via des registres : cela dĂ©pend de lâarchitecture et du niveau de privilĂšge (user land / kernel land).
Pour faire simple, la mĂ©moire virtuelle dâun ordinateur est sĂ©parĂ©e en deux parties : le user land et le kernel land.
Le user land contient tous les processus âbasiquesâ que lâon utilise tous les jours : le navigateur, vos programmes compilĂ©s, votre Ă©diteur de texte âŠ
Le kernel land, quant à lui, contient tous les processus nécessitant une exécution avec un niveau de privilÚge élevé. Cela inclut donc tous les programmes réalisant des actions sensibles comme la gestion de la mémoire, la lecture et écriture dans votre disque dur / ssd etc. De tels programmes sont appelés pilotes, drivers (Windows) ou modules kernel (Linux).
Evidemment, le kernel land contient Ă©galement le noyau (kernel) de votre OS Ă©tant donnĂ© le niveau de privilĂšge Ă©levĂ© requis dâun grand nombre dâactions rĂ©alisĂ©es par ce dernier.
Au sein dâune mĂȘme architecture, il peut y avoir plusieurs conventions dâappel, câest pour cela quâelles ont un nom : cdecl, stdcall, fastcall. On comprend enfin ce que signifient ces mots clĂ©s dans IDA :
Les noms de ces conventions dâappel nous permettent de savoir, sans regarder lâassembleur, comment les arguments sont passĂ©s.
Listons ci-dessous les principales conventions dâappel.
Les registres sont affichĂ©s dans lâordre des arguments : le premier registre correspond au premier paramĂštre et ainsi de suite.
Linux
| Architecture | Convention dâappel | Stockage des arguments | Qui rĂ©tablit la pile ? |
|---|---|---|---|
| x86 | cdecl | Pile | La fonction appelante |
| x86 | fastcall | ecx, edx puis la pile | La fonction appelée |
| x86 | stdcall | Pile | La fonction appelée |
| x86_64 | cdecl | rdi, rsi, rdx, rcx, r8, r9 puis la pile | La fonction appelante |
Windows
| Architecture | Convention dâappel | Stockage des arguments | Qui rĂ©tablit la pile ? |
|---|---|---|---|
| x86 | cdecl | Pile | La fonction appelante |
| x86 | stdcall | Pile | La fonction appelée |
| x86 | fastcall | ecx, edx puis la pile | La fonction appelée |
| x86 | thiscall (C++) | ecx (pour this) puis la pile | La fonction appelée |
| x86_64 | stdcall, thiscall, cdecl, et fastcall | rcx, rdx, r8, r9 puis la pile | La fonction appelante |
En mode 64 bits, que ce soit pour Linux ou Windows, la convention dâappel utilisĂ©e pour le user land est la mĂȘme que celle utilisĂ©e en kernel land.
Comme vous pouvez le constater, sous Windows, plusieurs noms de conventions dâappel aboutissent au mĂȘme rĂ©sultat. En fait, en 64 bits, le compilateur ignore tout simplement ces mots-clĂ©s.
GĂ©nĂ©ralement sous Linux, il nâ y a pas trop de soucis entre les conventions dâappel car il nây en a pas tant que ça qui sont utilisĂ©es Ă part cdecl. Par contre, sous Windows en 32 bits, il faut rester vigilant sur la convention dâappel utilisĂ©e.
Pour rappel, IDA indique la convention dâappel utilisĂ©e dans la signature de la fonction.
Apprendre le tableau par cĆur nâest pas indispensable mais il convient de se rappeler quâen x86, câest principalement la pile qui est utilisĂ©e contrairement aux programmes 64 bits.
ConnaĂźtre les conventions dâappel
x86_64sous Linux et Windows peut ĂȘtre utile car on a tendance Ă sâemmĂȘler les pinceaux dâune convention Ă lâautre.
Comparaison x86 et x86_64
Je vous propose dâutiliser le programme suivant pour analyser la maniĂšre dont les arguments sont transmis :
1
2
3
4
5
6
7
8
9
10
int fun(int a, int b, int c, int d)
{
return (a+b) - (c*d);
}
int main()
{
fun(0xa,0xb,0xc,0xd);
return 1;
}
Pour la compilation :
- en 32 bits :
gcc -m32 main.c -o exe_32 - en 64 bits :
gcc main.c -o exe_64
En ouvrant les deux programmes dans IDA, on obtient ceci :
Nous constatons deux différences :
- Evidemment, la transmission des arguments nâest pas effectuĂ©e de la mĂȘme maniĂšre. En 32 bits, tout est en envoyĂ© sur la pile. En 64 bits, comme nous sommes sous Linux, les registres utilisĂ©s sont :
edi,esi,edx,ecx. - Lorsque la pile a Ă©tĂ© utilisĂ©e, câest la fonction appelante, ici
main, qui gÚre le rétablissement de la pile.
La différence de performances
Pour vous convaincre du gain de performance dâun programme 64 bits par rapport Ă un programme 32 bits, je vous propose de compiler et exĂ©cuter ce programme dans les deux versions :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <stdio.h>
#include <time.h>
unsigned long long operation(unsigned long long a, unsigned long long b, unsigned long long c, unsigned long long d)
{
unsigned long long result = 0;
result += (a * b) + (c - d);
return result;
}
int main() {
clock_t start, end;
double cpu_time_used;
start = clock();
unsigned long long res = 0;
for (int i = 0; i < 1000000000; ++i)
{
res += operation(i, 10*i, 150*i+10, 2000*i+3);
}
end = clock();
cpu_time_used = ((double)(end - start)) / CLOCKS_PER_SEC;
printf("Temps CPU : %f secondes\n", cpu_time_used);
return 0;
}
Le temps dâexĂ©cution est de lâordre dâune dizaine de secondes normalement.
En les compilant puis en les exĂ©cutant, on constate que le programme 64 bits a Ă©tĂ© deux fois plus rapide que le programme 32 bits đ.


