Les Structures Data

Nouveaux concepts :

Stockage des données
Variables
Pointeurs
Indirection et déréférencement
Tableaux, Structures et Unions

 


Avant d'aller plus loin, il est nécessaire de comprendre les structures de données, car elles peuvent sembler impressionnantes mais sont importantes dans la programmation et reverse engeneering. Par exemple, les données qui composent les Headers des fichiers exécutables portables sont contenus dans diverses structures, donc pour coder une application qui manipule des fichiers PE, vous devez savoir comment les structures de données sont gérées en masm. Le SEH (Structured Exception Handling),qui est présent dans certains systèmes de protection, utilise également des structures. Lors du dernier tutoriel, nous nous sommes familiarisés avec les structures "startupinfo" et "process_information". Dans le prochain, nous utiliserons la structure "openfilename" et travaillerons avec les PE Headers (en-têtes).

Stockage des données en mémoire

Bien qu'une unité de mémoire dans les ordinateurs modernes contienne un byte (octet), les unités de données utilisées dans un programme sont souvent beaucoup plus grandes. Avant que des données puissent être stockées en mémoire, vous devez indiquer à l'ordinateur combien d'espace réserver en utilisant un type de données abstrait. Cela spécifie la quantité de mémoire nécessaire pour stocker les données et le type de données qui sera stocké à cet emplacement mémoire. Il existe 2 types de types de données abstraits : les types de données primitifs et les types de données définis par l'utilisateur.

Les types de données primitifs sont intégrés dans le langage de programmation. Par exemple, MASM utilise les types de variables entières (nombres entiers) suivants :

BYTE                          8 bits     nombres non signés de 0 à 255
WORD     (2 octets) 16 bits     nombres non signés de 0 à 65 535 (64 Ko)
DWORD   (4 octets) 32 bits     nombres non signés de 0 à 4 294 967 295 (4 Mo)
SWORD   (mot signé)          nombres signés de -32 768 à +32 767
SDWORD (mot double signé) nombres signés de -2 Mo à +2 Mo

Les types de données définis par l'utilisateur sont des groupes de types de données primitifs définis par le programmeur et comprennent des chaînes de caractères, des tableaux, des structures et des unions dans lesquelles vous pouvez accéder aux données en tant qu'unité ou en tant qu'éléments individuels qui composent l'unité.

Alors que les variables peuvent être placées n'importe où en mémoire, les éléments des types de données définis par l'utilisateur se trouvent les uns à côté des autres en mémoire. Il est plus efficace de pointer vers les éléments d'un tableau que vers des variables, car vous pouvez pointer vers l'élément suivant du tableau en vous déplaçant simplement vers la prochaine adresse mémoire. Nous aborderons les tableaux et les structures plus en détail dans un instant.

Indirection et déréférencement

Il est important de comprendre la distinction entre le CONTENU d'une variable (sa valeur) et l'ADRESSE d'une variable (son emplacement en mémoire).

Lorsque vous faites référence à une variable, vous effectuez une référence DIRECTE, mais lorsque vous faites référence à l'ADRESSE d'une variable, vous effectuez une référence INDIRECTE.

Avoir l'ADRESSE d'une variable établit un niveau de INDIRECTION "un". Lorsque vous travaillez avec des tableaux, vous pouvez avoir un tableau d'adresses. Si vous utilisez l'ADRESSE de ce tableau pour accéder à la VALEUR d'une variable à partir de son adresse dans le tableau, vous avez un niveau supplémentaire d'indirection.

La raison pour laquelle les niveaux d'indirection sont si utiles est qu'une seule adresse DWORD ou POINTEUR peut accéder à un nombre beaucoup plus élevé de variables très rapidement.

REMARQUE : Un pointeur est simplement une variable qui contient une adresse en tant que valeur.

Il est possible de supprimer un niveau d'indirection à partir d'une adresse en la plaçant entre crochets. Cette technique s'appelle le DÉRÉFÉRENCEMENT. Dans l'exemple suivant, eax est utilisé comme un pointeur vers la variable MyVar et est déréférencé dans ebx pour donner la valeur de la variable :

.data
MyVar      dd           77h code
mov eax,offset MyVar      ;  eax contient maintenant l'adresse de MyVar
mov ebx,[eax]                ;  ebx contient maintenant 77h

Voir ici pour des notes sur la syntaxe [] (crochets).

ARRAYS (TABLEAUX)


Comme mentionné précédemment, un tableau est une collection séquentielle de variables, toutes de même taille et de même type, appelées éléments. Vous pouvez accéder à tous les éléments d'un tableau par rapport au premier élément. Alors qu'un tableau est identifié par son nom, chaque élément d'un tableau est référencé par un numéro d'index, en commençant par zéro. L'indice du tableau apparaît entre crochets après le nom du tableau :

array[9]


En assembleur, l'indice d'un élément fait référence au nombre de octets entre l'élément et le début du tableau. Cette distinction peut être ignorée pour les tableaux d'éléments de taille d'un octet, puisque le numéro de position d'un élément correspond à son indice. Par exemple, en définissant un tableau d'octets appelé "prime":


prime BYTE 1, 3, 5, 7, 11, 13, 17


on attribue la valeur 1 à prime[0], la valeur 3 à prime[1], et ainsi de suite. Cependant, pour les tableaux dont les éléments sont plus grands que 1 octet, les numéros d'index (à l'exception de zéro) ne correspondent pas à la position d'un élément. Vous devez multiplier la position d'un élément par sa taille pour déterminer l'index de l'élément. Ainsi, pour le tableau


wprime WORD 1, 3, 5, 7, 11, 13, 17


wprime[4] représente le troisième élément (5), qui se trouve à 4 octets du début du tableau. De même, l'expression wprime[6] représente le quatrième élément (7) et wprime[10] représente le sixième élément (13).

Le décalage nécessaire pour accéder à un élément du tableau est calculé avec la formule suivante : n-ième élément du tableau = tableau[(n-1) * taille de l'élément]

Une chaîne de caractères est également un tableau de caractères, chaque caractère occupant 1 octet en mémoire.


STRUCTURES


Les structures sont simplement des modèles pour les données à stocker en mémoire de manière organisée. Elles ne sont pas des données en elles-mêmes.

Une structure est un groupe de types de données abstraits qui peuvent être accédés en tant qu'unité ou par l'un de ses champs composants. Les champs au sein de la structure peuvent être de différents types de données et de différentes tailles. Ils sont placés séquentiellement en mémoire lorsqu'une instance de la structure est définie dans un programme.

Vous pouvez utiliser l'ensemble de la variable de structure ou uniquement les champs individuels en tant qu'opérandes dans les instructions d'assemblage. Lors de l'utilisation de structures, le type de structure doit être déclaré en premier. Lorsque vous déclarez un type de structure ou d'union, vous créez un modèle de données. Le modèle indique les tailles et, éventuellement, les valeurs initiales dans la structure, mais n'alloue aucune mémoire.

Une fois que vous avez déclaré un type de structure, vous pouvez définir autant de variables de ce type que vous le souhaitez. Pour chaque variable définie, de la mémoire est allouée. Les champs individuels peuvent ensuite être référencés directement ou indirectement. Comme d'autres variables, les variables de structure peuvent être accessibles par leur nom. Vous pouvez accéder directement aux champs des variables de structure avec cette syntaxe : variable.champ

1. Déclarez d'abord le modèle de structure. Cela se fait généralement avec les déclarations de prototypes après les instructions include et includelib :

DATE STRUCT
day     BYTE   ?
month BYTE   ?
year    WORD ?
DATE ENDS


2. Ensuite, définissez la variable (une instance de la structure DATE appelée "yesterday") soit avec des données non initialisées :

.data?
yesterday DATE <>

ou avec des données initialisées :

.data
yesterday DATE <19, 06, 1993>

3. Enfin, vous pouvez accéder à la structure et à ses champs directement par leur nom, comme ceci :

mov al, yesterday.month

Ou indirectement via un pointeur, comme ceci :

mov ebx, offset yesterday       ;charge l'adresse de la variable yesterday dans ebx
mov al, [ebx].DATE.month       ;copie le champ month de ebx dans al

L'utilisation d'un pointeur DWORD vers une structure accélère les appels de fonction, car de grandes quantités de données dans une structure peuvent être accessibles par la fonction, mais seul le pointeur doit être passé sur la pile.

Les structures peuvent également être imbriquées. Les éléments des structures imbriquées sont accessibles de la même manière que décrit ci-dessus. Un exemple du format PE est le PE header:

1

Celui-ci contient 3 éléments, le premier est un DWORD non initialisé contenant la signature exécutable (pour PE, NE, OS/2, etc.), puis 2 structures imbriquées supplémentaires contenant le header de yesterday et le header optionnel. Le header de yesterday est le suivant :

1

Ainsi, les champs Signature et NumberOfSections peuvent être indirectement accessibles assez simplement. Supposons que nous ayons un pointeur vers le début du PE header dans edi, vous pouvez superposer le modèle de structure sur ce bloc de mémoire afin d'adresser les différents champs par leur nom, comme ceci :

mov eax,[edi].IMAGE_NT_HEADERS.Signature
mov bx,[edi].IMAGE_NT_HEADERS.FileHeader.NumberOfSections

Masm comprendra également cela si les crochets englobent l'opérande entier :

mov eax,[edi.IMAGE_NT_HEADERS.Signature]
mov bx,[edi.IMAGE_NT_HEADERS.FileHeader.NumberOfSections]

Nous en verrons plus à ce sujet dans les tutoriels suivants. Il existe également une autre façon d'utiliser les structures de manière indirecte avec la directive "assume". Cela indique à l'assembleur de substituer le registre de votre choix par une expression contenant le pointeur de votre structure. Par exemple :

assume edi:ptr IMAGE_NT_HEADERS
mov eax,[edi].Signature
mov bx,[edi].FileHeader.NumberOfSections
assume edi:nothing

Cependant, bien que cela semble légèrement plus rapide, le fait d'assumer un registre puis d'oublier de le "désassumer" produit des résultats imprévisibles. De plus, la directive "assume" peut être si éloignée de la ligne qui en a besoin que sa signification reste floue.

Il existe une quatrième syntaxe pour faire cela, mais elle est maladroite et ne fonctionnera pas dans certaines circonstances (par exemple, après une directive ".if"), il est donc conseillé d'utiliser la première méthode. Elle utilise l'opérateur "ptr" et je l'ai incluse ici pour des raisons de complétude :

mov eax,(IMAGE_NT_HEADERS ptr [edi]).Signature
mov bx,(IMAGE_NT_HEADERS ptr [eax]).FileHeader.NumberOfSections

Cette section deviendra particulièrement importante lorsque nous aborderons plus tard la programmation pour les fichiers PE.

UNIONS

Les unions apparaissent également dans les PE header. Elles sont identiques aux structures, à la différence que les champs d'une union se chevauchent en mémoire, ce qui vous permet de définir différents formats de données pour le même espace mémoire. Les unions peuvent stocker différents types de données en fonction de la situation. Elles peuvent également stocker des données en tant qu'un type de données et les récupérer en tant qu'un autre type de données.

Alors que chaque champ dans une structure a un décalage par rapport au premier octet de la structure, tous les champs dans une union commencent au même décalage. La taille d'une structure est la somme de ses composants ; la taille d'une union est la longueur du champ le plus long. Un exemple d'union dans le format PE est le premier membre de la structure IMAGE_IMPORT_DESCRIPTOR :

1

Le premier membre peut contenir soit les données Characteristics, soit les données OriginalFirstThunk. Dans ce cas, Characteristics a pu être à un moment donné un ensemble de flags, mais cela a été interrompu par M$ et elle représente maintenant toujours les données OriginalFirstThunk.

Au passage, sachez que la plupart des structures nécessaires pour la programmation sous Windows (y compris les éléments PE) sont déjà déclarées pour nous dans le fichier windows.inc.

 


Copyright (C)- xtx Team (2021)

XHTML valide 1.1 CSS Valide !