Post

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

  • 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 movabs ou syscall.

đŸ€™ 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 :

  1. empiler les arguments, et donc écrire en mémoire
  2. 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

ArchitectureConvention d’appelStockage des argumentsQui rĂ©tablit la pile ?
x86cdeclPileLa fonction appelante
x86fastcallecx, edx puis la pileLa fonction appelée
x86stdcallPileLa fonction appelée
x86_64cdeclrdi, rsi, rdx, rcx, r8, r9 puis la pileLa fonction appelante

Windows

ArchitectureConvention d’appelStockage des argumentsQui rĂ©tablit la pile ?
x86cdeclPileLa fonction appelante
x86stdcallPileLa fonction appelée
x86fastcallecx, edx puis la pileLa fonction appelée
x86thiscall (C++)ecx (pour this) puis la pileLa fonction appelée
x86_64stdcall, thiscall, cdecl, et fastcallrcx, rdx, r8, r9 puis la pileLa 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_64 sous 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 :

  1. 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.
  2. 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 😎.

This post is licensed under CC BY-NC 4.0 by the author.