Les tableaux 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 tableaux en assembleur x86-64

Post by Hydraxx »

Les tableaux en assembleur x86-64

Les tableaux sont des zones de mémoire contiguës contenant plusieurs éléments de même type.

En assembleur, il n'existe pas directement une abstraction de haut niveau appelée "tableau" comme en C ou C++. On manipule simplement :

Code: Select all

une adresse de base
un index
une taille d'élément
La formule essentielle à retenir est :

Code: Select all

adresse_element = base + index * taille_element
Cette formule apparaît constamment dans le code compilé et en reverse engineering.

1. Structure d'un tableau en mémoire

Prenons :

Code: Select all

int tableau[4] = {10, 20, 30, 40};
Un `int` fait généralement 4 octets.

En mémoire :

Code: Select all

adresse base + 0   -> 10
adresse base + 4   -> 20
adresse base + 8   -> 30
adresse base + 12  -> 40
Donc pour accéder à :

Code: Select all

tableau[i]
on calcule :

Code: Select all

base + i * 4
2. Indexation à partir de zéro

Comme en C/C++, le premier élément est généralement :

Code: Select all

index 0
Donc :

Code: Select all

tableau[0]
tableau[1]
tableau[2]
...
Pour un tableau d'éléments de 4 octets :

Code: Select all

tableau[0] -> base + 0
tableau[1] -> base + 4
tableau[2] -> base + 8
tableau[3] -> base + 12
3. Forme générale des références mémoire

L'adressage x86-64 permet une forme très puissante :

Code: Select all

[base + index*scale + displacement]
Les quatre composants sont :

Code: Select all

base         = adresse de départ
index        = numéro d'élément
scale        = taille multiplicative
displacement = constante ajoutée
Exemple :

Code: Select all

[rbx + rcx*4 + 8]
signifie :

Code: Select all

adresse = RBX + RCX*4 + 8
4. Valeurs possibles de SCALE

Le facteur d'échelle matériel peut être :

Code: Select all

1
2
4
8
Cela correspond parfaitement aux tailles fréquentes :

Code: Select all

1 octet  -> byte
2 octets -> word
4 octets -> dword
8 octets -> qword
Exemples :

Code: Select all

[rax + rcx]     ; éléments de 1 octet
[rax + rcx*2]   ; éléments de 2 octets
[rax + rcx*4]   ; éléments de 4 octets
[rax + rcx*8]   ; éléments de 8 octets
5. Tableau de bytes

Exemple :

Code: Select all

char buffer[100];
Chaque élément fait 1 octet.

Un accès peut ressembler à :

Code: Select all

mov al, [rbx + rcx]
Ici :

Code: Select all

RBX = base du tableau
RCX = index
Comme la taille d'un élément vaut 1, aucun `*2`, `*4` ou `*8` n'est nécessaire.

6. Tableau de words

Pour un tableau de valeurs de 2 octets :

Code: Select all

mov ax, [rbx + rcx*2]
Le facteur d'échelle indique immédiatement :

Code: Select all

taille_element = 2
7. Tableau de dwords

Pour un tableau de `int` 32 bits :

Code: Select all

mov eax, [rbx + rcx*4]
C'est un motif extrêmement fréquent.

On peut le lire directement comme :

Code: Select all

EAX = tableau[RCX]
si `RBX` contient l'adresse de base.

8. Tableau de qwords

Pour des éléments de 8 octets :

Code: Select all

mov rax, [rbx + rcx*8]
Cela peut correspondre à :
  • un tableau de `uint64_t` ;
  • un tableau de `long long` ;
  • un tableau de pointeurs.
En reverse, un `*8` est donc souvent un indice qu'on travaille avec des valeurs 64 bits ou des pointeurs.

9. Exemple concret de calcul d'adresse

Supposons :

Code: Select all

base  = 1000h
index = 3
taille_element = 4
Alors :

Code: Select all

adresse = 1000h + 3*4
        = 1000h + 0Ch
        = 100Ch
Donc :

Code: Select all

tableau[3]
se trouve à :

Code: Select all

100Ch
10. Ajouter un déplacement

On peut aussi rencontrer :

Code: Select all

[rbx + rcx*4 + 10h]
Cela signifie :

Code: Select all

base + index*4 + 0x10
Le déplacement peut correspondre à :
  • un champ situé dans une structure ;
  • une sous-zone ;
  • un offset fixe avant le tableau ;
  • un membre de structure contenant un tableau.
11. LEA pour calculer une adresse

`lea` est très utile pour calculer des adresses.

Exemple :

Code: Select all

lea rax, [rbx + rcx*4]
Ici, RAX reçoit :

Code: Select all

RBX + RCX*4
mais la mémoire n'est pas lue.

Différence :

Code: Select all

lea rax, [rbx + rcx*4]
-> calcule l'adresse

Code: Select all

mov eax, [rbx + rcx*4]
-> lit la valeur située à cette adresse

Cette différence est fondamentale en reverse.

12. Parcourir un tableau

Un parcours classique ressemble à :

Code: Select all

xor ecx, ecx

loop:
mov eax, [rbx + rcx*4]

inc rcx
cmp rcx, 10
jl loop
On peut reconstruire :

Code: Select all

for (int i = 0; i < 10; i++)
{
    eax = tableau[i];
}
13. Modifier chaque élément

Exemple :

Code: Select all

xor ecx, ecx

loop:
mov eax, [rbx + rcx*4]
add eax, 1
mov [rbx + rcx*4], eax

inc rcx
cmp rcx, 10
jl loop
Pseudo-C :

Code: Select all

for (int i = 0; i < 10; i++)
{
    tableau[i]++;
}
14. Copier un tableau

Supposons :

Code: Select all

RBX = source
RDX = destination
RCX = index
Pour des éléments de 4 octets :

Code: Select all

mov eax, [rbx + rcx*4]
mov [rdx + rcx*4], eax
Cela effectue :

Code: Select all

destination[i] = source[i]
Il suffit ensuite d'incrémenter l'index dans une boucle.

15. Tableau passé à une fonction

En assembleur, une fonction reçoit généralement l'adresse du premier élément du tableau.

Exemple C :

Code: Select all

void process(int* array, size_t count);
Sous Windows x64 :

Code: Select all

RCX = adresse du tableau
RDX = nombre d'éléments
La fonction peut ensuite parcourir :

Code: Select all

[rcx + index*4]
si les éléments font 4 octets.

16. Tableau et pointeur

Au niveau assembleur, un tableau est essentiellement manipulé via une adresse.

Donc :

Code: Select all

int array[10]
et :

Code: Select all

int* ptr
finissent souvent par être manipulés de manière très proche :

Code: Select all

base + index * taille
C'est pourquoi en reverse, il faut souvent déduire depuis le contexte s'il s'agit d'un tableau, d'un buffer ou d'une structure répétée.

17. Allocation dynamique d'un tableau

Pour allouer dynamiquement un tableau :

Code: Select all

nombre_elements * taille_element
Exemple :

Code: Select all

100 int
```

nécessitent :

[code]
100 * 4 = 400 octets
En C :

Code: Select all

int* array = malloc(100 * sizeof(int));
En assembleur, on prépare la taille puis on appelle `malloc`.

Après l'appel :

Code: Select all

RAX = pointeur retourné
si l'allocation réussit.

18. Vérifier le retour de malloc

Après :

Code: Select all

call malloc
il est fréquent de voir :

Code: Select all

test rax, rax
je allocation_failed
Cela signifie :

Code: Select all

if (rax == NULL)
    allocation_failed;
En reverse, c'est un motif très classique.

19. Libération avec free

Une mémoire allouée dynamiquement doit être libérée lorsqu'elle n'est plus nécessaire.

En C :

Code: Select all

free(array);
Sous Windows x64, pour une fonction C respectant la convention Microsoft x64 :

Code: Select all

RCX = pointeur
call free
Cela concerne la mémoire dynamique, pas les tableaux statiques ni la stack.

20. Remplir un tableau

Exemple :

Code: Select all

xor ecx, ecx

fill_loop:
mov eax, ecx
mov [rbx + rcx*4], eax

inc rcx
cmp rcx, 10
jl fill_loop
Cela produit conceptuellement :

Code: Select all

array[0] = 0
array[1] = 1
array[2] = 2
...
array[9] = 9
21. Remplissage avec nombres aléatoires

Une fonction peut appeler un générateur aléatoire puis stocker le résultat dans chaque élément.

Conceptuellement :

Code: Select all

for (i = 0; i < count; i++)
{
    array[i] = rand();
}
En assembleur, on verra :

Code: Select all

call rand
mov [base + index*4], eax
puis l'index avancera.

22. Afficher les éléments

Pour afficher un tableau, une boucle lit un élément puis appelle une fonction comme `printf`.

Conceptuellement :

Code: Select all

for (i = 0; i < count; i++)
{
    printf("%d", array[i]);
}
En reverse, il faut alors suivre :

Code: Select all

adresse du format
valeur courante
appel à printf
index
23. Recherche du minimum

Un algorithme classique :

Code: Select all

min = array[0];

for (i = 1; i < count; i++)
{
    if (array[i] < min)
        min = array[i];
}
En assembleur, on peut voir :

Code: Select all

mov eax, [rbx]         ; min = array[0]
mov ecx, 1

loop:
mov edx, [rbx + rcx*4]
cmp edx, eax
jge no_change

mov eax, edx

no_change:
inc rcx
...
24. Recherche du maximum

Même logique :

Code: Select all

max = array[0];

for (...)
{
    if (array[i] > max)
        max = array[i];
}
En reverse, un registre peut servir à conserver la meilleure valeur trouvée jusqu'ici.

25. Alignement des tableaux

Les données peuvent être alignées sur certaines frontières.

Exemples :

Code: Select all

align 4
align 8
align 16
L'alignement peut améliorer les performances et être requis dans certains contextes SIMD.

Il peut aussi introduire du padding entre certaines zones de données.

Pour un simple reverse de code user mode, retiens surtout que les adresses peuvent ne pas être immédiatement consécutives entre plusieurs objets distincts à cause de l'alignement.

26. argv est un tableau de pointeurs

En C :

Code: Select all

int main(int argc, char** argv)
`argv` est un tableau de pointeurs vers des chaînes.

Conceptuellement :

Code: Select all

argv[0] -> pointeur vers le nom du programme
argv[1] -> pointeur vers le premier argument
argv[2] -> pointeur vers le deuxième argument
...
Sur x64, un pointeur fait 8 octets.

Donc un accès à :

Code: Select all

argv[i]
peut ressembler à :

Code: Select all

mov rax, [rbx + rcx*8]
Le `*8` est ici très logique : on parcourt un tableau de pointeurs.

27. Reconnaître un tableau en reverse

Une instruction comme :

Code: Select all

mov eax, [rbx + rcx*4]
donne déjà plusieurs indices :

Code: Select all

RBX = base probable
RCX = index probable
4   = taille probable d'un élément
Cela peut correspondre à un tableau de `int` 32 bits.

De même :

Code: Select all

mov rax, [rbx + rcx*8]
peut correspondre à un tableau de pointeurs ou de valeurs 64 bits.

28. Attention : scale ne prouve pas le type exact

Voir :

Code: Select all

*4
ne prouve pas forcément qu'il s'agit d'un `int`.

Cela signifie seulement que l'index est multiplié par 4.

Le type réel peut être :
  • un entier 32 bits ;
  • un float ;
  • une structure de 4 octets ;
  • une partie d'un calcul d'adresse.
Il faut utiliser le contexte.

29. Tableau de structures

Une structure peut faire une taille différente de 1, 2, 4 ou 8 octets.

Exemple :

Code: Select all

struct Item
{
    int a;
    int b;
    int c;
};
Taille probable :

Code: Select all

12 octets
Comme le scale matériel ne peut pas être 12, le compilateur peut calculer l'index autrement :

Code: Select all

imul rax, rcx, 12
lea rdx, [rbx + rax]
ou utiliser plusieurs additions / `lea`.

En reverse, tous les tableaux ne se reconnaissent donc pas uniquement à `*4` ou `*8`.

30. Exemple typique de reverse

Supposons :

Code: Select all

xor ecx, ecx

loop:
mov eax, [rbx + rcx*4]
add eax, 1
mov [rbx + rcx*4], eax

inc rcx
cmp rcx, 10
jl loop
On peut reconstruire :

Code: Select all

for (int i = 0; i < 10; i++)
{
    array[i] += 1;
}
Indices :

Code: Select all

RCX démarre à 0
RCX est incrémenté
comparaison avec 10
accès [RBX + RCX*4]
31. Ce qu'il faut retenir pour le reverse
  • un tableau est une suite d'éléments contigus ;
  • la formule clé est `base + index * taille_element` ;
  • la forme x86-64 classique est `[base + index*scale + displacement]` ;
  • scale vaut 1, 2, 4 ou 8 ;
  • `[rax+rcx*4]` est un motif typique d'accès à un élément de 4 octets ;
  • `[rax+rcx*8]` est fréquent pour des pointeurs ou valeurs 64 bits ;
  • `lea` calcule une adresse sans lire la mémoire ;
  • un tableau passé à une fonction est généralement représenté par un pointeur ;
  • le nombre d'éléments est souvent passé séparément ;
  • `malloc` retourne généralement le pointeur dans RAX ;
  • il faut tester le retour de malloc ;
  • `free` libère une allocation dynamique ;
  • `argv` est un tableau de pointeurs ;
  • le scale permet de deviner une taille probable, pas forcément le type exact ;
  • une boucle avec index + `cmp` + accès mémoire est un motif très fréquent de parcours de tableau.
32. Exercices

Exercice 1

Un tableau commence à :

Code: Select all

1000h
Chaque élément fait 4 octets.

Quelle est l'adresse de :

Code: Select all

array[5]
Exercice 2

Que signifie :

Code: Select all

mov eax, [rbx + rcx*4]
si RBX est l'adresse d'un tableau ?

Exercice 3

Quelle taille probable ont les éléments ici ?

Code: Select all

mov rax, [rbx + rcx*8]
Exercice 4

Explique la différence entre :

Code: Select all

lea rax, [rbx + rcx*4]
mov eax, [rbx + rcx*4]
Exercice 5

Reconstitue le pseudo-C :

Code: Select all

xor ecx, ecx

loop:
mov eax, [rbx + rcx*4]
add eax, 2
mov [rbx + rcx*4], eax

inc rcx
cmp rcx, 5
jl loop
Exercice 6

Si un tableau contient 100 éléments de 8 octets, combien d'octets faut-il demander à `malloc` ?

Exercice 7

Que signifie probablement :

Code: Select all

test rax, rax
je failed
juste après :

Code: Select all

call malloc
Exercice 8

Pourquoi :

Code: Select all

[rbx + rcx*8]
est-il un motif fréquent pour `argv` ?

Exercice 9

Dans x64dbg, trouve une boucle qui accède à une adresse sous la forme :

Code: Select all

[base + index*scale]
Puis identifie :

Code: Select all

base
index
scale
nombre d'éléments probable
Exercice 10

Explique pourquoi un tableau de structures de 12 octets peut nécessiter un `imul` ou plusieurs additions au lieu d'un simple `*12` directement dans l'opérande mémoire.

Who is online

Users browsing this forum: No registered users and 0 guests