Quelques rappels à propos de la mémoire |
La mémoire, une succession ordonnée d'octets
L'espace de travail
Accès aux données
Intervalle de travail
Statut de l'objet
Nettoyage
Redimensionner l'espace de travail
Libérer l'espace de travail
Pointer les données
Octet, série d'octets, bloc d'octets et série de blocs d'octets
Légende des schémas |
N | Adresse de l'octet (en notation hexadécimale) |
N |
Adresse de début de la mémoire allouée (en notation hexadécimale) |
N | Adresse intermédiaire de la mémoire allouée (en notation hexadécimale) |
N | Adresse de fin de la mémoire allouée (en notation hexadécimale) |
? | Contenu indéterminé/inconnu de l'octet |
N A |
Contenu connu de l'octet (en notation hexadécimale) + valeur ASCII correspondante (sur seconde ligne) |
N |
Adresse de début de la mémoire allouée mais ne disposant d'aucun espace de travail (en notation hexadécimale) |
N | Offset de l'octet par rapport à l'adresse de base (en notation hexadécimale) |
La mémoire, une succession ordonnée d'octets |
La mémoire est une zone constituée d'une série de cellules, appelée octets, dont le nombre total varie suivant le système sur laquelle elle est présente. Chacun de ces octets est disposé l'un à la suite de l'autre et porte un numéro d'identification incrémentiel propre et unique, on parle alors de son adresse. Chacun de ces octets est constitué de 8 bits qui peuvent contenir une information pouvant représenter un nombre s'étalant de 0 à 255 (soit du binaire 00000000 à 11111111). Le schéma suivant montre une représentation simplifiée de la mémoire :
Disparité entre les systèmes 32 bits et 64 bits C'est le système qui accède à la mémoire. Il faut savoir que le système ne sait compter mécaniquement que jusqu'à ce que son architecture le lui permet. Les systèmes 32 bits ne peuvent compter que jusqu'à 4.294.967.295 (±4 Go) car 32 bits ne permettent pas de représenter un nombre supérieur et les systèmes 64 bits ne peuvent compter que jusqu'à 18.446.744.073.709.551.615 (±18 milliards de Go) car 64 bits ne permettent pas de représenter un nombre supérieur. La librairie EclatLib est disponible en version 32 bits et 64 bits. Elle peut donc accéder à la totalité de la mémoire que se soit en 32 bits ou 64 bits. |
L'espace de travail |
L'espace de travail est la portion de la mémoire que le système nous a octroyé pour notre usage personnel. Pour disposer d'une partie de cette mémoire comme espace de travail, nous devons la réclamer au système, c'est l'allocation de mémoire. Sur un système Windows 32 bits, la taille de mémoire allouable est (théoriquement) de ±4 Go (bien que celui-ci s'en réserve au mois 1/4 pour le noyau) soit 4.294.967.295 octets. Sur un système Windows 64 bits, la taille de mémoire allouable est (théoriquement) de ±18 Go de Go (là aussi celui-ci s'en réserve une partie pour le noyau) soit 18.446.744.073.709.551.615 octets. Allocation de mémoire Le système cherche une série d'octets disponibles d'un seul tenant de la taille demandée. Une fois trouvée, il marque cette portion comme allouée pour ne pas qu'elle soit attribuée à d'autres programmes puis nous communique l'adresse du premier octet (appelée aussi adresse de base) de cette portion.
Allocation impossible En cas d'échec le système retourne l'adresse 0x00 (NULL). Certes l'adresse 0x00 correspond au premier octet de la mémoire mais, par convention, elle est réservée pour spécifier une indisponibilité. Une adresse NULL est toujours synonyme d'adresse non valide.
Pas de fragmentation Il est important de comprendre que lorsque le système alloue de la mémoire, chacun des octets qui la compose doit et est toujours disposé l'un à la suite de l'autre. Vous n'aurez jamais, contrairement aux fichiers, une mémoire allouée répartie sur plusieurs blocs de la mémoire, c'est à dire fragmentée. Si le système ne trouve pas la quantité de mémoire libre d'un seul bloc, il vous signifie que l'allocation a échoué. Hors de l'espace de travail, point de salut Il ne faut jamais effectuer de lecture ou d'écriture à l'extérieur de la zone de l'espace de travail sinon le système déclanchera une exception (erreur) qui fermera le programme sans possibilité de continuation. L'écrasante majorité des bugs provient d'un accès non autorisé à la mémoire. Particularité, allocation de 0 octet Si la taille à allouer est égale à 0, alors la fonction alloue 0 octet et retourne erOk. C'est à dire que le pointeur m_pBuffer est valide (adresse différente de NULL) mais que l'espace de travail est vide. Cela semble curieux mais c'est la règle ! C'est un peu comme créer un fichier mais de ne rien y écrire dedant. Cela n'est pas une erreur mais il est impossible d'effectuer une opération de lecture ou d'écriture sous peine d'erreur.
|
Accès aux données |
Pour lire ou écrire une information dans l'un des octets de la mémoire allouée, il suffit d'utiliser son adresse. Etant donné que les octets alloués sont TOUJOURS CONTINUS et CONTIGUS, il est simple de déterminer leur adresse. Le premier octet est situé à l'adresse de base, le second juste après, c'est à dire à l'adresse de base + 1 octet, le troisième à l'adresse de base + 2 octets, etc... Cette indexation de l'adresse par rapport à l'adresse de base est appelé déplacement mais le plus souvent offset. Adressage offsetisé Chaque octet étant rangé l'un à la suite de l'autre, on calcule l'adresse de l'octet à manipuler en ajouter à l'adresse de base sa position par rapport à cette première par la formule suivante : Adresse de l'octet désiré = adresse de base + offset de l'octet désiré depuis l'adresse de base. Ainsi pour le premier octet, l'adresse réelle est adresse de base + 0x00; pour le second octet, l'adresse réelle est adresse de base + 0x01; etc...
Limite d'accès On ne peut accéder en lecture et/ou en écriture à une adresse qui ne fait pas partie intégrante de la mémoire allouée par votre programme. Le système renverrait alors une erreur qui le fermerait. Dans l'exemple précédent, les adresses exploitables s'étendent de 0x04 à 0x07 inclus et par voie de conséquence, l'offset doit se situer entre la valeur 0x00 et 0x03. Suivant l'offset que vous utilisez, les fonctions membres de l'objet EMemory détermine son statut qui donnera lieu à l'exécution de celles-ci ou bien du renvoi prématuré d'une erreur. Vous pouvez déterminer le statut d'un offset en utilisant la fonction OffsetStatusGet. EMemory::OffsetStatus_Unknown Le statut de l'offset n'a pu être déterminé. Ce statut ne devrait jamais apparaître, mais on n'est jamais assez prudent. EMemory::OffsetStatus_In L'offset est situé dans l'espace de travail.
EMemory::OffsetStatus_Limit L'offset est situé à limite finale externe de l'espace de travail. Cette position ne fait pas partie de l'espace de travail et l'on ne peut effectuer d'opération directe à cette emplacement exepté les opérations d'insertion et de concaténation.
EMemory::OffsetStatus_Out L'offset est situé au delà de la limite finale externe de l'espace de travail et de l'espace de travail. Il est interdit d'effectuer des opération directe (même d'insertion ou de concaténation).
|
Intervalle de travail |
Avec l'objet EMemory vous avez accès à toute la mémoire que vous avez allouée, c'est l'espace de travail. Il se peut que vous désiriez travailler, non pas sur tout l'espace de travail allouée mais que sur une portion; c'est l'intervalle de travail. L'intervalle de travail n'est pas une substitution à l'espace de travail mais un filtre additionnel. Ce n'est pas une particularité du C/C++ mais une fonctionnalité qui a été programmée pour étendre et simplifier la manipulation de données au sein de l'objet EMemory. Lorsque vous utilisez un intervalle de travail, les manipulations n'ont lieu que sur l'intervalle de travail. Les données hors intervalle de travail ne le sont jamais. Utiliser un intervalle de travail Presque toutes les fonctions de l'objet EMemory supportent le travail sur un intervalle. Ces fonctions sont reconnaissables par les deux derniers arguments Fonction( ..., ulonglong ullOffset = 0, ulonglong ullOffsetSize = EMemory::SizeUpToEnd );. L'argument ulonglong ullOffset détermine le début de l'intervalle de travail, l'argument ulonglong ullOffsetSize détermine l'étendue de cet intervalle. Par exemple si ullOffset est égal à 0x02 et que ullOffsetSize est égal à 0x03, la fonction utilisée ne travaillera que de l'offset 0x02 à l'offset 0x04 (et par voie de conséquence de l'adresse 0x06 à 0x08) :
Maintenant si vous désirez définir un intervalle de travail démarrant d'un offset déterminé jusqu'à la fin de l'espace de travail, n'utilisez pas l'argument ullOffsetSize (ou plutôt laissez le à sa valeur par défaut pré-définie) ou donnez lui la valeur constante EMemory::SizeUpToEnd. Lorsque vous utilisez la constante EMemory::SizeUpToEnd (pour l'argument ullOffsetSize) la fonction calcule automatiquement l'étendue de l'intervalle de travail pour qu'il aille jusqu'à la fin de l'espace de travail. Par exemple si ullOffset est égal à 0x02 et que ullOffsetSize est égal à EMemory::SizeUpToEnd, la fonction utilisée ne travaillera que de l'offset 0x02 à l'offset 0x06 :
Ne pas utiliser un intervalle de travail Ne pas utiliser un intervalle de travail signifie travailler sur toutes l'étendue de l'espace de travail ou plutôt définir un intervalle de travail qui coïncide avec l'espace de travail. Dans ce cas, il suffit de donner la valeur 0x00 à l'argument ullOffset et la valeur constante EMemory::SizeUpToEnd à ullOffsetSize. Etant donné que ces arguments disposent de valeur par défaut pré-déterminée, il vous suffit de ne rien mettre et le compilateur affectera automatiquement la valeur 0x00 à ullOffset et EMemory::SizeUpToEnd à ullOffsetSize. Par exemple si ullOffset est égal à 0x00 et que ullOffsetSize est égal à EMemory::SizeUpToEnd, la fonction utilisée travaillera sur toutes les octets de l'espace de travail :
Statut d'un intervalle de travail Suivant l'offset et son étendue que vous utilisez, les fonctions membres de l'objet EMemory détermine son statut qui donnera lieu à l'exécution de celles-ci ou bien du renvoi prématuré d'une erreur. Vous pouvez déterminer le statut d'un intervalle en utilisant la fonction OffsetStatusSizeGet. EMemory::OffsetStatus_Unknown Le statut de l'intervallle n'a pu être déterminé. Ce statut ne devrait jamais apparaître, mais on n'est jamais assez prudent. EMemory::OffsetStatus_In L'intervalle est situé dans l'espace de travail.
EMemory::OffsetStatus_Limit L'intervalle est situé à limite finale externe de l'espace de travail. Cet intervalle ne fait pas partie de l'espace de travail et il est interdit d'effectuer des opération directes (même d'insertion ou de concaténation).
EMemory::OffsetStatus_Out L'intervalle est situé au delà de la limite finale externe de l'espace de travail et de l'espace de travail. Il est interdit d'effectuer des opération directes (même d'insertion ou de concaténation).
EMemory::OffsetStatus_Ride L'intervalle est à cheval entre l'espace de travail et l'extérieur de l'espace de travail. L'offset est à l'intérieur mais une partie de son étendue à l'extérieure.
Note : comme vous avez pu le constater les fonctions OffsetStatusGet et OffsetStatusSizeGet renvoie toutes les deux des constantes de type EMemory::OffsetStatus_.... Mais seule la fonction OffsetStatusSizeGet peut retourner la constante EMemory::OffsetStatus_Ride. |
Statut de l'objet |
Suivant l'état de l'objet, il a un statut spécifique qui autorise ou non certaines actions. L'objet se base sur ses deux variables membres protégées m_pBuffer et m_ullSize pour le déterminer. La variable m_pBuffer est un pointeur de type void contenant l'adresse de base de l'espace de travail alloué et m_ullSize contient la taille de cet espace de travail.
Vous pouvez déterminer le statut de l'objet en utilisant la fonction ObjetStatusGet. EMemory::ObjectStatus_Unknown Ce statut ne devrait jamais apparaître car les variables membres sont verrouillées. L'état des deux variables membres ne sont pas compatibles. EMemory::ObjectStatus_Free C'est le statut par défaut lorsque l'on crée un objet EMemory en appelant le constucteur par défaut. L'objet n'a aucun espace de travail.
EMemory::ObjectStatus_Empty C'est le statut lorsque l'espace de travail est alloué mais de 0 octet. L'objet a un espace de travail mais il est vide d'octets (c'est comme pour un fichier qui existe mais qui aurait une taille de 0 octet).
EMemory::ObjectStatus_Enable C'est le statut lorsque l'espace de travail est alloué avec au moins 1 octet. L'objet a un espace de travail et des données.
|
Nettoyage |
Une fois notre espace de travail alloué, nous avons à notre disposition une série d'octets. Comme c'est toujours le cas, des informations sont déjà présentes dans chacun de ces octets. Ces données sont indéterminées, il s'agit de résidu d'une allocation tierce précédente de votre programme ou d'un autre. Lorsque le système alloue ou désalloue de la mémoire, il ne nettoie pas les informations contenues dans chaque octet, elles restent telles quelles.
Comme vous pouvez le constater les données contenues dans chacun des octets alloués sont arbitraires. Le nettoyage n'est pas obligatoire mais conseillé pour eviter toutes méprises lors d'une utilisation future. La convention veut que ce nettoyage se fasse avec la 0x00 mais dans les exemples donnés dans cette documentation j'utilise des valeurs plus perceptibles pour plus de relief. Nettoyage dès l'instanciation Dès que vous instanciez un objet EMemory, vous pouvez le nettoyer dans la foulée avec une valeur.
Nettoyage a posteriori C'est à dire qu'il a lieu après que les octets aient été alloués.
Nettoyage a priori C'est à dire qu'il a lieu avant que les octets ne soient librérés. Cette approche s'inscrit dans la volonté d'effacer toutes traces des données sensibles de la mémoire après librération de la mémoire qui les contenaient.
Note : Si l'on avait pas fait de nettoyage a priori le mot de passe serait toujours en mémoire :
|
Redimensionner l'espace de travail |
Lorsque que l'on désire disposer de plus ou moins de mémoire, il est possible de redimensionner la quantité de mémoire allouée dans notre espace de travail. C'est la reallocation. Note importante : lorsque le redimensionnement de l'espace de travail échoue (parce qu'il n'y a pas assez de mémoire par exemple), l'ancien espace de travail reste intact, il n'est pas détruit contrairement à ce que l'on pourrait penser. Par exemple si l'espace de travail était de 100 kilo-octets, que le redimensionnement demandé est de 1 Mo et que cela échoue, votre espace est toujours de 100 kilo-octets (les données sont les mêmes d'avant la demande de redimensionnement). La reallocation de mémoire n'est pas la désallocation de mémoire allouée puis l'allocation d'une nouvelle mémoire dans la foulée. C'est un autre mécanisme. Le système va tenter conserver le même emplacement des octets actuellement alloués mais en modulant son étendu. Pourquoi conserver le même emplacement ? Le mécanisme de reallocation dans un autre emplacement est lourd. Une reallocation n'est qu'un redimensionnement. Cela implique que les informations contenues dans les informations contenues dans chaque octet de notre espace de travail actuel doivent subsister dans notre nouvel espace de travail. Si le système n'arrive pas à re-utiliser le même emplacement par manque de place à la queue de l'espace de travail actuel, il alloue un nouvel espace de travail, y copie les informations de l'ancien espace de travail (par contre les octets ajoutés à l'espace de travail ne sont pas nettoyés). L'ancien espace de travail est alors désalloué et l'adresse du premier octet du nouvel espace de travail est alors différente.
Tout ça pour dire quoi ? Comme vous pouvez le voir le redimensionnement à la hausse peut être lourd, ne soyez pas petit bras : essayer d'allouer un espace de travail assez grand pour ne pas à avoir à faire de multiples redimensionnements qui peuvent s'avérer lourds suivant les cas. Redimensionnement à la baisse Les derniers octets faisant partie de l'espace de travail sont retirés.
Redimensionnement à la hausse La reallocation à la hausse est différente car il faut qu'il y ait assez d'octets libres à la fin de l'espace de travail pour pouvoir conserver le même emplacement.
Note à propos du nettoyage lors d'un redimensionnement Tout d'abord il n'est obligatoire mais conseillé (à la hausse en tout cas); les fonctions de redimensionnement surchargées SizeSet( ulonglong ullNewSize, uchar ucByteToFill ) et SizeSet( ulonglong ullNewSize, void *pBlockToFill, ulonglong ullBlockToFillSize ) ont été spécialement écrites pour cela. Et vous n'aurez pas à le faire par la suite. D'autre part, si la plage mémoire est déplacée au cours d'un redimensionnement à la hausse car il n'y a pas assez d'octets libre à la queue de l'espace de travail, il est impossible de nettoyer l'ancienne plage car il est impossible de savoir si la plage mémoire va être déplacée dans un autre emplacement à moins de mettre en place une procédure très lourde (ce qui n'est pas le cas dans l'objet EMemory). |
Libérer l'espace de travail |
Une fois que l'on a plus besoin de la mémoire allouée, il convient de la libérer pour qu'elle soit de nouveau disponible pour le système. Lorsque l'objet est détruit, celui-ci libère automatiquement l'espace de travail alloué, si ce n'est déjà fait.
Note : la fonction Free libère l'espace de travail mais pas l'objet lui-même. Tant que l'objet n'a pas été détruit automatiquement ou implicitement, il reste disponible pour allouer un autre espace de travail. Si vous avez instancié un objet dynamiquement, vous devez le détruire explicitement sous peine de fuite mémoire :
|
Pointer les données |
Lorsque l'objet EMemory alloue un espace de travail, il stocke l'adresse de celui-ci dans un pointeur membre protégé m_pBuffer. Ce pointeur est de type void, c'est à dire indéterminé. Mais il est tout à fait possible de convertir (on dit aussi caster) ce type en un autre type pour pouvoir manipuler les données renfermées dans l'espace de travail (voir les fonctions de la famille Pointer...Get(...);). Unicité virtuelle Un pointeur permet de lire ou d'écrire des données ayant la même densité que son type. C'est à dire d'un pointeur de type char ne pourra manipuler que des données de type char (1 octet), qu'un pointeur de type long ne pourra manipuler que des données de type long (4 octets), etc... Donc chaque octet de l'espace de travail ne doit pas être considéré comme une entité unitaire ad vitam eternam mais comme des éléments qui peuvent être regroupés au gré de leur utilisation. Il s'agit de virtualisation. Pour un pointeur de type char, chaque octet de l'espace de travail est un entité propre car le type char permet de ne manipuler que des données ayant une densité de 1 octet. Par contre pour un pointeur de type long, ce sont des paquets de 4 octets qui sont des entités propres car le type long permet de ne manipuler que des données ayant une densité de 4 octets. Le schéma suivant montre l'unicité des données d'un espace de travail si on le virtualise à partir d'un pointeur de type char :
Le schéma suivant montre l'unicité des données d'un espace de travail si on le virtualise à partir d'un pointeur de type long :
Le premier offset (offset 0x00) est situé à l'adresse 0x04, mais le second offset (0x01) n'est pas situé à l'adresse 0x05 mais à 0x08 car le type long a une densité de 4 octets. Il englobe 4 octets dans une même entité. Lorsque l'on lit ou écrit des données à l'aide d'un pointeur long, ce n'est pas 1 octet qui est manipulé mais 4, c'est à dire le nombre d'octets correspondant à sa densité.
Risque d'eccueuil Si l'on utilise un pointeur qui a une densité supérieure à 1 octet, il faut être vigilant à ce qu'il y ait assez l'espace de travail lorsque lors de la manipulation de ce pointeur. En d'autres termes il faut que l'espace de travail soit un multiple de la densité du type de pointeur utilisé sinon il y a un gros risque de plantage. Cela est d'autant plus dangereux que certaines fois le programme ne plante pas alors qu'il le devrait, donc vous pensez que votre code est valide. Mais tôt ou tard cela plantera, c'est inévitable, et il sera très difficile de retrouver l'origine de cette erreur impromptue. Je vous le rappelle, l'écrasante majorité des plantages proviennent d'un accès illégal à une mémoire non autorisée.
|
Octet, série d'octets, bloc d'octets et série de blocs d'octets |
Les fonctions de l'objet EMemory sont subdivisées en thématiques. Au sein de chacune de ces thématiques, ses fonctions peuvent travailler sur diverses organisations de données. On peut, en gros, les subdiviser en quatre catégories : L'octet Chaque octet est considéré comme étant unitaire.
La série d'octets Il s'agit d'une série d'octets dont chaque octet est considéré comme étant unitaire. Cela permet d'effectuer une série d'opérations lors d'un seul appel de fonction.
L'exemple précédent ne cherche pas les données "ECA" mais soit la donnée "E" soit la donnée "C" soit la donnée "A". Lorsque la fonction BytesFind trouve l'une de ces valeurs, elle retourne le résultat de sa recherche. Le bloc d'octets Il s'agit d'une série d'octets dont l'ensemble des octets est considérée comme étant unitaire. Chaque octet n'est pas une entitié propre mais l'un des éléments d'un bloc.
L'exemple précédent ne cherche pas individuellement soit la donnée "B" soit la donnée "C" soit la donnée "D" mais trois octets contigus ayant respectivement les valeurs 0x42, 0x43 et 0x44 ("BCD" en ASCII). La série de blocs d'octets Il s'agit d'une série de blocs d'octets chaque bloc est considéré comme étant unitaire (pour plus d'informations voir la structure EMemory::SC_BLOCK). Cela permet d'effectuer une série d'opérations lors d'un seul appel de fonction.
|