Les fonctions en assembleur x86-64

Salut ce forum est dédié à comprendre les entrailles de l'ordinateur au plus bas niveau bienvenue dans le monde du reverse et de l'assembleur

Moderator: Rick

Post Reply
Hydraxx
Site Admin
Posts: 114
Joined: Mon Jan 12, 2026 4:04 pm
Location: France
Contact:

Les fonctions en assembleur x86-64

Post by Hydraxx »

Les fonctions en assembleur x86-64

Les fonctions sont essentielles en assembleur et en reverse engineering. Pour analyser correctement un programme, il faut savoir reconnaître un appel de fonction, retrouver ses paramètres, comprendre sa stack frame et identifier sa valeur de retour.

1. La stack

La stack est utilisée notamment pour :
  • les adresses de retour ;
  • les registres sauvegardés ;
  • certaines variables locales ;
  • les paramètres supplémentaires ;
  • les zones temporaires utilisées pendant les appels.
Le registre RSP pointe vers le sommet courant de la stack.

Sur x86-64, la stack grandit généralement vers les adresses plus basses.

Code: Select all

Adresse haute

+----------------------+
| anciennes données    |
+----------------------+
| adresse de retour    |
+----------------------+
| variables locales    |
+----------------------+ <- RSP

Adresse basse
2. PUSH et POP

`push` place une valeur sur la stack.

Code: Select all

push rax
En 64 bits, on peut le visualiser ainsi :

Code: Select all

RSP = RSP - 8
[RSP] = RAX
`pop` réalise l'opération inverse :

Code: Select all

pop rax
équivaut conceptuellement à :

Code: Select all

RAX = [RSP]
RSP = RSP + 8
La stack fonctionne donc selon un principe LIFO : la dernière valeur empilée est la première récupérée.

3. CALL — appeler une fonction

L'instruction :

Code: Select all

call my_function
réalise deux opérations fondamentales :
  • elle sauvegarde sur la stack l'adresse de l'instruction suivante ;
  • elle transfère l'exécution vers la fonction appelée.
Conceptuellement :

Code: Select all

push adresse_instruction_suivante
jmp my_function
C'est la différence essentielle avec `jmp` : `call` conserve une adresse permettant de revenir à l'appelant.

4. L'adresse de retour

Supposons :

Code: Select all

401000: call test
401005: mov eax, 10
Au moment du `call`, l'adresse :

Code: Select all

401005
est placée sur la stack.

La fonction `test` peut ensuite s'exécuter, puis `ret` utilisera cette adresse pour revenir à l'instruction suivante.

5. RET — revenir à l'appelant

L'instruction :

Code: Select all

ret
récupère l'adresse de retour située au sommet de la stack et y transfère l'exécution.

Conceptuellement :

Code: Select all

RIP = [RSP]
RSP = RSP + 8
Le couple :

Code: Select all

call
ret
constitue donc le mécanisme fondamental des appels de fonctions.

6. Création d'une stack frame

Une fonction peut réserver une zone de stack qui lui est propre.

Un prologue classique ressemble à :

Code: Select all

push rbp
mov rbp, rsp
sub rsp, 20h
`push rbp` sauvegarde l'ancien RBP.

`mov rbp, rsp` crée un repère stable pour la frame courante.

`sub rsp, 20h` réserve 32 octets supplémentaires.

7. RBP et RSP

RSP représente le sommet courant de la stack.

RBP peut servir de repère fixe pour retrouver les données de la fonction.

Exemple :

Code: Select all

mov eax, [rbp-4]
mov ecx, [rbp-8]
On peut visualiser :

Code: Select all

RBP --------> repère fixe
 |
 | [RBP-4]  variable locale
 | [RBP-8]  autre variable
 |
RSP --------> sommet actuel
Important : en x64 moderne, les compilateurs ne sont pas obligés d'utiliser RBP comme frame pointer. Ils peuvent travailler directement avec RSP.

8. LEAVE

À la fin d'une fonction utilisant RBP, on peut rencontrer :

Code: Select all

leave
ret
`leave` équivaut conceptuellement à :

Code: Select all

mov rsp, rbp
pop rbp
Il restaure donc la frame précédente avant le `ret`.

9. Convention d'appel Windows x64

Sous Windows x64, les quatre premiers paramètres entiers ou pointeurs sont généralement passés dans :

Code: Select all

RCX = paramètre 1
RDX = paramètre 2
R8  = paramètre 3
R9  = paramètre 4
Les paramètres supplémentaires sont passés sur la stack.

La valeur de retour entière ou pointeur est généralement placée dans :

Code: Select all

RAX
C'est une règle essentielle à connaître en reverse Windows.

10. Exemple avec VirtualAlloc

Pour :

Code: Select all

VirtualAlloc(lpAddress, dwSize, flAllocationType, flProtect)
on peut typiquement retrouver juste avant le `call` :

Code: Select all

RCX = lpAddress
RDX = dwSize
R8  = flAllocationType
R9  = flProtect
Exemple :

Code: Select all

RCX = 0
RDX = 1000h
R8  = 3000h
R9  = 4

call VirtualAlloc
Après l'appel :

Code: Select all

RAX = adresse retournée
Cette connaissance permet de retrouver très rapidement les arguments d'une API dans x64dbg.

11. Shadow Space sous Windows x64

La convention Microsoft x64 impose à l'appelant de réserver 32 octets de stack pour la fonction appelée.

Cette zone est appelée :

Code: Select all

shadow space
home space
Elle correspond à quatre emplacements de 8 octets.

On peut voir par exemple :

Code: Select all

sub rsp, 20h
call fonction
add rsp, 20h
Les quatre premiers paramètres restent transmis dans RCX, RDX, R8 et R9, mais cette zone de stack doit être disponible pour la fonction appelée.

12. Paramètres supplémentaires

Pour :

Code: Select all

foo(a, b, c, d, e, f)
sous Windows x64 :

Code: Select all

RCX = a
RDX = b
R8  = c
R9  = d

e et f -> stack
Lorsqu'on analyse leurs offsets dans un désassemblage, il faut tenir compte de l'adresse de retour et du shadow space.

Pour retenir simplement :

Code: Select all

arguments 1 à 4 -> registres
arguments 5+    -> stack
13. Alignement de la stack

La convention Windows x64 impose également des contraintes d'alignement.

Une valeur fondamentale à retenir est :

Code: Select all

16 octets
Le compilateur doit maintenir l'alignement requis aux endroits prévus par l'ABI.

C'est une raison pour laquelle on peut rencontrer des tailles comme :

Code: Select all

sub rsp, 28h
sub rsp, 38h
Ces réservations peuvent combiner :
  • shadow space ;
  • variables locales ;
  • sauvegardes ;
  • alignement.
14. Valeurs de retour

Une fonction qui retourne un entier ou un pointeur utilise généralement :

Code: Select all

RAX
Exemple :

Code: Select all

mov eax, 5
ret
peut correspondre à :

Code: Select all

return 5;
Et :

Code: Select all

xor eax, eax
ret
peut souvent être interprété comme :

Code: Select all

return 0;
Pour les valeurs flottantes, le registre principal à connaître est :

Code: Select all

XMM0
15. Registres volatiles et non volatiles

Sous Windows x64, certains registres peuvent être librement modifiés par une fonction appelée.

Parmi les volatiles :

Code: Select all

RAX
RCX
RDX
R8
R9
R10
R11
D'autres doivent être préservés si la fonction les utilise.

Parmi les non volatiles :

Code: Select all

RBX
RBP
RSI
RDI
R12
R13
R14
R15
C'est pour cette raison qu'une fonction peut faire :

Code: Select all

push rbx
...
pop rbx
16. Convention System V / Linux x64

Sous Linux x86-64 et d'autres systèmes utilisant System V, les paramètres entiers/pointeurs sont généralement passés dans :

Code: Select all

RDI = paramètre 1
RSI = paramètre 2
RDX = paramètre 3
RCX = paramètre 4
R8  = paramètre 5
R9  = paramètre 6
Comparaison :

Code: Select all

Windows x64
1 RCX
2 RDX
3 R8
4 R9

System V
1 RDI
2 RSI
3 RDX
4 RCX
5 R8
6 R9
Cette différence est très importante lorsqu'on passe du reverse Windows au reverse Linux.

17. LEA et les adresses

L'instruction :

Code: Select all

lea rcx, [message]
charge l'adresse de `message`.

À comparer avec :

Code: Select all

mov rcx, [message]
qui charge le contenu situé à cette adresse.

On peut retenir :

Code: Select all

LEA -> adresse
MOV avec [] -> contenu mémoire
C'est une distinction fondamentale lorsqu'on analyse des paramètres pointeurs.

18. Appels de fonctions externes

L'assembleur peut appeler des fonctions provenant de bibliothèques externes.

Exemple :

Code: Select all

call printf
Il faut préparer les paramètres selon la convention d'appel utilisée.

Sous Windows x64 :

Code: Select all

lea rcx, [message]
call printf
peut passer l'adresse de `message` comme premier paramètre.

19. GOT et adressage RIP-relative sous Linux

Avec les bibliothèques partagées sous Linux, on peut rencontrer la :

Code: Select all

GOT = Global Offset Table
Les appels vers des fonctions externes peuvent utiliser des entrées permettant de résoudre leurs adresses dynamiquement.

En x86-64, l'adressage relatif à RIP est également fréquent :

Code: Select all

lea rax, [rip + offset]
Il permet notamment de produire du code plus facilement relogeable.

En reverse, `[rip+...]` désigne donc souvent une donnée ou une adresse proche du code courant.

20. Fonctions variadiques

Certaines fonctions acceptent un nombre variable d'arguments.

Exemple :

Code: Select all

printf("%d %d", a, b);
Sous Windows x64, on peut conceptuellement retrouver :

Code: Select all

RCX = adresse de la chaîne de format
RDX = a
R8  = b
Les fonctions variadiques ont des règles ABI supplémentaires, notamment pour les flottants.

Pour le reverse, le point principal est qu'un appel à `printf` peut utiliser plusieurs registres et éventuellement la stack selon le nombre de paramètres.

21. Une stack frame plus complète

Une stack frame peut contenir plusieurs catégories de données :

Code: Select all

+---------------------------+
| paramètres supplémentaires|
+---------------------------+
| adresse de retour         |
+---------------------------+
| ancien RBP                |
+---------------------------+
| variables locales         |
+---------------------------+
| registres sauvegardés     |
+---------------------------+
| zones temporaires         |
+---------------------------+
La disposition exacte dépend du compilateur, des optimisations et de la fonction.

Il ne faut donc pas apprendre un unique schéma comme s'il était toujours identique.

22. Fonction simple sans stack frame

Code C :

Code: Select all

int add(int a, int b)
{
    return a + b;
}
Sous Windows x64, une version simple peut être :

Code: Select all

mov eax, ecx
add eax, edx
ret
Interprétation :

Code: Select all

ECX = a
EDX = b

EAX = a
EAX += b

return EAX
Une fonction très simple n'a donc pas forcément besoin de créer une stack frame.

23. Exemple : fonction MAX

Code C :

Code: Select all

long max(long a, long b)
{
    if (a > b)
        return a;

    return b;
}
Version assembleur simplifiée :

Code: Select all

mov rax, rcx
cmp rcx, rdx
jg done

mov rax, rdx

done:
ret
On retrouve :
  • les paramètres dans RCX et RDX ;
  • la comparaison ;
  • la valeur de retour dans RAX.
24. Step Into et Step Over

Dans un debugger :

Step Into entre dans la fonction appelée.

Step Over exécute la fonction sans entrer dans son détail et s'arrête à l'instruction suivante.

Pour le reverse :

Code: Select all

Step Into -> fonction intéressante
Step Over -> fonction que l'on ne veut pas analyser maintenant
25. Observer CALL dans x64dbg

Supposons :

Code: Select all

00007FF6...1000 call test
00007FF6...1005 mov ebx, eax
Avant le `call`, observe :

Code: Select all

RSP
RCX
RDX
R8
R9
Après un Step Into, l'adresse de retour :

Code: Select all

00007FF6...1005
a été empilée.

Après le `ret`, RIP revient à cette adresse.

26. Lire une stack frame

Exemple :

Code: Select all

push rbp
mov rbp, rsp
sub rsp, 30h

mov [rbp-8], rcx
mov [rbp-10h], rdx
On peut comprendre :

Code: Select all

RBP = repère

[RBP-8]  = copie du paramètre 1
[RBP-10] = copie du paramètre 2
C'est très utile pour identifier les variables locales et suivre les paramètres pendant un reverse.

27. Récursion

Une fonction récursive s'appelle elle-même.

Exemple :

Code: Select all

factorial(5)
 -> factorial(4)
   -> factorial(3)
     -> factorial(2)
       -> factorial(1)
Chaque `call` ajoute une nouvelle adresse de retour et peut créer une nouvelle stack frame.

Les retours se produisent ensuite dans l'ordre inverse.

28. Exemple : factorielle récursive

Code C :

Code: Select all

long long factorial(long long n)
{
    if (n <= 1)
        return 1;

    return n * factorial(n - 1);
}
Version conceptuelle :

Code: Select all

factorial:

cmp rcx, 1
jg recurse

mov eax, 1
ret

recurse:
push rcx

dec rcx
call factorial

pop rcx
imul rax, rcx
ret
La valeur originale de `n` doit être préservée afin de pouvoir multiplier le résultat récursif par cette valeur.

29. Récursion et consommation de stack

Chaque appel récursif peut consommer de la stack pour :
  • l'adresse de retour ;
  • les variables locales ;
  • les registres sauvegardés ;
  • le shadow space ;
  • les paramètres supplémentaires.
Une récursion trop profonde peut donc provoquer un :

Code: Select all

stack overflow
30. Ce qu'il faut retenir pour le reverse
  • `call` sauvegarde l'adresse de retour et entre dans une fonction ;
  • `ret` récupère cette adresse et revient à l'appelant ;
  • `RSP` est le sommet courant de la stack ;
  • `RBP` peut être un repère stable de stack frame ;
  • `push` et `pop` modifient RSP ;
  • Windows x64 utilise RCX, RDX, R8, R9 pour les quatre premiers paramètres entiers/pointeurs ;
  • les paramètres supplémentaires sont sur la stack ;
  • RAX contient généralement le retour entier/pointeur ;
  • XMM0 est important pour les flottants ;
  • Windows x64 réserve 32 octets de shadow space ;
  • l'alignement de la stack est important ;
  • `lea` charge une adresse ;
  • une fonction simple peut ne pas avoir de stack frame ;
  • les conventions Windows et System V ne passent pas les paramètres dans les mêmes registres ;
  • chaque appel récursif ajoute un nouveau contexte sur la stack.
31. Exercices

Exercice 1

Si :

Code: Select all

RSP = 1000h
quelle sera sa valeur après :

Code: Select all

push rax
en x64 ?

Exercice 2

Que fait `call test` avant de transférer l'exécution vers `test` ?

Exercice 3

Sous Windows x64, retrouve les registres de :

Code: Select all

foo(a, b, c, d)
Exercice 4

Pour :

Code: Select all

foo(a, b, c, d, e)
où se trouve le cinquième argument ?

Exercice 5

Réécris en pseudo-C :

Code: Select all

mov eax, ecx
add eax, edx
ret
Exercice 6

Explique la différence entre :

Code: Select all

lea rcx, [message]
mov rcx, [message]
Exercice 7

Que représente généralement une réservation de 32 octets avant un appel sous Windows x64 ?

Exercice 8

Analyse :

Code: Select all

push rbp
mov rbp, rsp
sub rsp, 20h
...
leave
ret
et explique le rôle de chaque instruction.

Exercice 9

Dans x64dbg, place un breakpoint juste avant :

Code: Select all

call VirtualAlloc
Observe :

Code: Select all

RCX
RDX
R8
R9
puis exécute l'appel et observe RAX.

Exercice 10

Observe RSP avant et après un `call` et retrouve l'adresse de retour au sommet de la stack.

Exercice 11

Explique pourquoi une fonction récursive consomme progressivement de la stack.

Exercice 12

Compare la convention d'appel Windows x64 et System V x64 et explique pourquoi cette différence est importante en reverse.

Who is online

Users browsing this forum: No registered users and 0 guests