Les allocations mémoire

Un article de GuruMed.

Sommaire

Comment allouer et libérer la mémoire correctement

par Thomas Richter, traduit par StAn.

Cet article est la traduction directe d'un chapitre de la documentation de MuGuardianAngel (readme), l'utilitaire de débuggage écrit par Thomas Richter. Merci à lui pour cette doc très complète et son autorisation de traduction et publication sur Guru-Meditation.net. Certaines parties de cette documentation sont d'un niveau relativement élevé et/ou ne concernent que des usages spécifiques, donc si vous vous sentez un peu perdu, n'hésitez pas à passer à la suite de l'article !

Gestion correcte de la mémoire: éviter les hits

Si vous vous demandez comment les hits MuGuardianAngel peuvent être évités, voici les règles à suivre pour allouer et libérer la mémoire correctement.

Les règles qui suivent s'appliquent à tous les programmes qui sont supposés tourner d'une façon respectueuse du système. Je ne les ai pas inventées. Ce que vous trouverez ici est plus ou moins une copie des règles situées dans le ROM Kernel Reference Manual, la documentation développeur officielle de l'Amiga.

Enfreindre ces règles conduira à des programmes instables, que ce soit avec ou sans utilitaires mémoire additionnels. Un programme qui crée des hits lorsque MuGuardianAngel tourne, mais fonctionne correctement sinon, est quoi qu'il en soit instable et pourrait planter dans certaines situations.


Allocations mémoire

Le flag MEMF_PUBLIC

Positionnez le bit MEMF_PUBLIC (exec/memory.h). C'est généralement nécessaire !

Ne pas positionner ce bit entraine l'allocation de mémoire qui:

  1. est *privée* et réservée à votre tache, c'est-à-dire qu'elle ne peut pas être lue depuis une quelconque autre tache,
  2. ne peut pas être lue en toute sécurité à l'intérieur d'une paire Forbid()/Permit() ou Enable()/Disable().

Le système d'exploitation actuel [NDT: AmigaOS 3.9 et inférieurs] n'implémente AUCUN test pour cette règle, et MuGuardianAngel non plus. Puisque la mémoire privée n'est pas sous le contrôle direct de MuGuardianAngel, ce type de mémoire n'est jamais rendu indisponible ou touché de quelque façon que ce soit par cet outil de débuggage. Dans le futur, les gestionnaires de mémoire pourraient voir dans ce bit un indice indiquant la possibilité d'assigner de la "mémoire virtuelle" à l'allocation, c'est-à-dire de la mémoire qui peut être déplacée sur disque.

Par exemple, VMM requiert une utilisation correcte de ce bit. [NDT: Toute mémoire allouée avec le flag MEMF_PUBLIC ne sera jamais déplacée sur le disque]

Toute mémoire supposée contenir des structures du système d'exploitation doit être allouée avec le flag MEMF_PUBLIC, de même que toute mémoire devant être lue par d'autres taches, interruptions, exceptions, tampons d'entrées/sorties.

Les seules exceptions sont les structures privées qui ne sont lues ou écrites que par votre tache, qui ne sont jamais passées ni lues ou écrites par d'autres processus ou fonctions du système d'exploitation, et ne sont pas lues lorsque le multitache est désactivé.


Vidages de la mémoire

Soyez prêts à ce qu'une allocation mémoire puisse vider les bibliothèques, polices de caractères et devices inutilisés de la mémoire. En particulier, n'utilisez pas de ressources fermées. Utiliser "FindName()" sur une liste de ressources d'exec n'est pas suffisant pour utiliser une ressource. Si vous ne voulez PAS que des ressources soient vidées de la mémoire, positionnez le flag MEMF_NO_EXPUNGE lors de l'allocation mémoire. Cf exec/memory.h.

Voici un moyen sûr de vider la mémoire: AllocMem(0x7ffffff0,MEMF_PUBLIC); (c'est comme ceci que "Avail flush" vide la mémoire).

MuGuardianAngel ne signalera jamais un vidage de mémoire comme étant une allocation mémoire échouée, contrairement à MungWall.


La mémoire et les coprocesseurs de l'Amiga

La mémoire devant être lue par les coprocesseurs doit être allouée avec le flag MEMF_CHIP, ou les coprocesseurs ne pourront pas y accéder. Ceci est valable pour:

  • les zones d'affichage (bitmaps natifs [NDT: ne concerne donc pas les cartes graphiques])
  • les copper lists hardware (mais pas leurs abstractions gfx pour
  • la famille CMove(),CWait() etc...)
  • les bitmaps d'images (struct IntuiImage->ImageData)
  • les tampons du lecteur de disquettes (mais depuis la V37 ce n'est plus nécessaire pour les tampons d'entrées/sorties du trackdisk.device)
  • les tampons du coprocesseur audio
  • les sprites hardware et les données des images des "Bobs"
  • tout ce à quoi les coprocesseurs natifs pourraient accéder.

L'ordre des blocs de mémoire

Ne faites aucune supposition quand à l'ordre dans lequel vous obtenez de la mémoire. La deuxième allocation n'est pas forcément située à une adresse plus élevée !


Le bit MEMF_FAST

N'utilisez pas le bit MEMF_FAST inutilement si de la mémoire CHIP peut être OK pour vous également. Le système d'exploitation est suffisament intelligent pour allouer de la mémoire FAST s'il y en a de disponible. Il se repliera sur la mémoire CHIP s'il n'y a plus de mémoire FAST. Il n'y a en général aucune raison de demander explicitement de la mémoire FAST.


Alignement

Toute mémoire allouée avec AllocMem() est forcément alignée sur un multiple de deux mots longs [NDT: 8 octets], c'est-à-dire que les bits 0 à 2 de l'adresse seront toujours à zéro. Si vous avez besoin d'un alignement plus important, voyez l'astuce ci-dessous.


La taille des tampons

Faites bien attention à toujours allouer assez de mémoire pour le pire cas possible. En langage C, une chaine nécessite n+1 octets de mémoire pour contenir une chaine de longueur n. Certaines fonctions de l'OS nécessitent, à cause de bugs, un tampon légèrement plus grand que ce qu'il pourrait vous sembler; controlez la section "BUGS" des Autodocs (ce bug affecte souvent les fonctions DOS, mais également certaines fonction d'intuition.)

MuGuardianAngel est capable de détecter *certains* accès hors limites à la mémoire, mais en général seulement lors de la libération de cette mémoire.


Attributs mémoire

Ne positionnez aucun bit non documenté pour les attributs mémoire d'AllocMem(). Ils *pourraient* être ignorés sous cette version de l'OS, mais ne le seront probablement pas dès la prochaine version. Regardez le fichier exec/memory.h pour y trouver les flags valides. Dans la version actuelle (V40), les flags suivants sont définis:


	
#define MEMF_ANY      (0L)     /* N'importe-quel type de mémoire fera l'affaire */
#define MEMF_PUBLIC   (1L<<0)  /* Très important, cf les avertissements au dessus ! */
#define MEMF_CHIP     (1L<<1)  /* pour les coprocesseurs natifs ("custom chips") */
#define MEMF_FAST     (1L<<2)  /* mémoire FAST, cf avertissements ! */
#define MEMF_LOCAL    (1L<<8)  /* Mémoire qui ne disparait pas au RESET */
#define MEMF_24BITDMA (1L<<9)  /* Mémoire utilisable par le DMA dont l'adresse tient en 24 bits */
#define MEMF_KICK     (1L<<10) /* Mémoire utilisable pour les KickTags */
#define MEMF_CLEAR   (1L<<16)  /* AllocMem: rempli la zone de zéro (NULL) */
#define MEMF_LARGEST (1L<<17)  /* AvailMem: retourne la taille du plus grand bloc */
#define MEMF_REVERSE (1L<<18)  /* AllocMem: alloue du haut vers le bas */
#define MEMF_TOTAL   (1L<<19)  /* AvailMem: retourne la taille totale de la mémoire */


Contenu de la mémoire

N'émettez aucune hypothèse quand au contenu d'une zone de mémoire à moins que vous n'ayez spécifié l'attribut MEMF_CLEAR pour l'effacer. Ne pas positionner ce bit rend l'allocation un peu plus rapide, mais dans ce cas le bloc de mémoire peut contenir tout et n'importe-quoi.

MuGuardianAngel tente d'être un peu méchant avec les programmes qui n'allouent pas la mémoire avec MEMF_CLEAR: au lieu de laisser la mémoire telle-quelle, il la remplit avec un motif spécial non NULL.


Code auto-modifiant

L'usage de code auto-modifiant n'est pas conseillé. Si vous devez absolument jouer avec ça et ne pouvez pas faire autrement, utilisez l'appel système suivant pour vider les caches CPU une fois que votre code est placé en mémoire et doit s'exécuter:

ClearCacheU()

Ne vous attendez pas à ce qu'il soit en mémoire avant que vous appeliez cette routine. C'est encore plus important pour les routines comme les interruptions qui sont appelées de manière asynchrone.


Échecs

Soyez prêts à ce que votre requête de mémoire échoue. Un test explicite est requis après un appel à AllocMem() [NDT: ainsi qu'après toutes les autres fonctions d'allocation mémoire, bien sûr]. Se contenter de laisser le Guru apparaitre dans le cas d'une allocation échouée n'est pas suffisant. Affichez un message d'alerte, quittez votre programme proprement, vérifiez votre code !

Pour ceux qui programment en assembleur: non, il n'est pas documenté qu'AllocMem() positionne le bit zero si l'allocation a échoué. Vous devez faire le test vous-même.

Si vous l'appelez depuis un Process, les versions 37 et supérieures de l'OS garantissent un code de retour de IoErr() égal à ERROR_NO_FREE_STORE (=103L).

MuGuardianAngel montrera les allocations mémoire échouées si l'option SHOWFAIL est activée. De plus, il essaye d'être méchant avec les programmes et change volontairement les registres d1/a0-a1 ou d0-d1/a0-a1 (aussi appelés scratch registers) avant de quitter les routines de gestion de mémoire. Il positionnera aussi le flag Z pour tromper les programmes assembleur qui ne testent pas d0 correctement.


AllocMem() et les changements de contexte

Ni AllocMem() ni FreeMem() ne "cassent" un état "Forbid" [NDT: c'est-à-dire que si le multitache est coupé, ces fonctions ne le restaureront pas]. C'est important car c'est la seule façon d'"afficher" une liste dont l'accès est protégé par Forbid() à l'aide de la dos.library et d'autres fonctions.

La séquence suivante permet de faire ceci légalement:

  • appeler Forbid()
  • faire une copie de la liste élément par élément, en utilisant AllocMem()
  • appeler Permit()
  • afficher la copie de la liste
  • libérer la mémoire

Exécuter un Wait(), par exemple en utilisant un sémaphore pour protéger l'accès à la memory list, serait ici fatal.


AllocMem(), FreeMem(), AllocAbs() et les interruptions

Aucune de ces fonctions ne peut être appelée depuis une interruption ou en mode superviseur.

Souvenez-vous, cependant, que les input handlers de l'input.device ne tournent pas sous interruptions mais dans le contexte de la tache de l'input.device, bien qu'ils soient construits par dessus une structure d'interruption. Par conséquent, appeler AllocMem() dans ce cas pour faire une copie d'un input event est légal.


Libération de mémoire

Taille des désallocations

La taille d'une désallocation doit correspondre exactement à la taille de l'allocation. Il n'est pas autorisé de:

arrondir la taille, car l'algorithme d'arrondi de l'OS pourrait changer dans le futur pour supporter certains matériels (par exemple les lignes de cache du PowerPC qui font 32 octets).

Et aussi une autre règle qui n'a pas été mentionnée dans les RKRM:

libérer un morceau de bloc de mémoire, c'est-à-dire une partie d'un tableau. CECI EST ABSOLUMENT ILLEGAL, pas d'exceptions, pas d'excuses. Libérez tout ou rien.

Libérer un morceau d'un bloc de mémoire requiert la connaissance des règles d'alignement de l'OS et pourrait rendre le programme instable et inefficace si ces règles changeaient dans de futures versions.

Je recommenderais donc fortement de ne pas utiliser cette technique.

MuGuardianAngel va "hitter" si un programme essaye de libérer un bloc de mémoire d'une taille différente de celle avec laquelle il a été alloué. De plus, il hittera si un programme essaye de libérer de la mémoire "non alignée", c'est-à-dire un bloc de mémoire dont l'adresse n'est pas multiple de 8 octets.

Cependant, les versions 37 et inférieures de la layers.library ne respectent pas cette règle (beurk!); cette bibliothèque est donc explicitement exclue de la gestion mémoire de MuGuardianAngel. Elle libère, malheureusement, des blocs de mémoire partiels.


Accès à la mémoire désallouée

Ne touchez pas à la mémoire désallouée. Si elle a été libérée, elle a été libérée et vous n'avez plus le droit de l'utiliser, d'y faire référence, de la lire ni d'y écrire. Une autre tache pourrait la vouloir.

MuGuardianAngel marquera la mémoire libérée comme indisponible dès que possible. Si vous essayez d'y accéder, un hit se produira.

Une exception qui n'est pas formulée dans les RKRM mais est malheureusement largement utilisée:

La désallocation de mémoire à l'intérieur d'une paire Forbid()/Permit(). La mémoire, sauf les 8 premiers octets qui sont utilisés pour l'administration, reste non modifiée et prête à être utilisée tant que le multitache est désactivé. Exécuter un Wait(), directement ou indirectement, annulera l'état "Forbid()" et rendra donc la mémoire inutilisable.

Soyez prévenus ! Bien que cet accès soit en quelque sorte légal, et donc toléré par MuGuardianAngel, ceci est tout de même "laid" selon moi et par conséquent fortement déconseillé. Une des très rares exceptions où ce comportement pourrait être utile se trouve dans le bout de code suivant, qui "décharge" le segment d'un programme "load and stay resident" [NDT: Non, moi non plus je ne sais pas ce que ça veut dire]:


	
move.l  SysBase(a4),a6
jsr     _LVOForbid(a6)
move.l  DOSBase(a4),a6
move.l  Segment(a4),d1
jsr     _LVOUnloadSeg(a6)   ;"Décharge" son propre code
move.l  a6,a1               ;CE CODE RESTE LEGAL grâce
move.l  SysBase(a4),a6      ;au Forbid()
jsr     CloseLibrary(a6)    ;ferme la dos.library
moveq   #0,d0
rts                         ;quitte.

Notez que vous devez absolument être certain que le segment n'est pas un segment "overlay" car UnloadSeg() annulera l'état "Forbid()" dans ce cas. Cependant, ceci ne fonctionne pas pour les programmes "load and stay resident" de toute façon.

MuGuardianAngel garanti actuellement que la mémoire n'est pas rendue indisponible si elle est libérée lorsque le multitache est coupé (état "Forbid()"). Elle est rendue indisponible dès que le multitache est réinstauré. Contrairement à MungWall, MuGuardianAngel modifie cette mémoire et controle son intégrité dès qu'il en a le droit. Il testera même la mémoire deux fois: dès que FreeMem() est appelé, et une autre fois lorsque le multitache est restauré.


Mémoire Chip et accès par le Blitter

La logique du blitter utilise le DMA (Direct Memory Access = Accès direct à la mémoire) et accède à la mémoire CHIP indépendemment du processeur. Si vous utilisez un tampon temporaire pour le blitter, prenez garde à ce que le blitter n'y accède plus avant de le désallouer. Pour en être sûr, appelez WaitBlit() avant de libérer de la mémoire qui a été utilisée comme tampon pour le blitter.

MuGuardianAngel ne peut malheureusement pas détecter les accès illégaux à la mémoire CHIP par le Blitter, car ceci est hors de contrôle de la MMU.


La mémoire et les accès DMA matériels

Les contrôleurs de disque dur modernes peuvent accéder à la mémoire par DMA, parallèlement au processeur. Si vous avez l'intention d'utiliser ce DMA matériel directement parce que vous écrivez un pilote de périphérique pour ce matériel, préparez-vous à vider les caches du processeur correctement. En particulier, appelez CachePreDMA(...)avant l'opération DMA et CachePostDMA(...) ensuite.

Consultez les AutoDocs pour les détails de ces fonctions et leurs paramètres. Lisez-les, puis réfléchissez-y, puis lisez-les encore. C'est important que vous les compreniez correctement !


Valeur de retour

FreeMem() ne retourne aucune valeur utile, ni ne positionne de code de condition.

Comme toujours, MuGuardianAngel essayera d'être méchant avec les programmes et modifiera les registres "perdus" et les codes de condition.


AllocAbs() et autres curiosités

AllocAbs est destiné à une utilisation spéciale consistant à allouer de la mémoire à un emplacement prédéfini. Ne l'utilisez pas sans de bonnes raisons.


Portée de la mémoire allouée

AllocAbs() arrondi les adresses. Préparez-vous à ce que le bloc de mémoire que vous obtenez ne soit pas identique à celui que vous avez demandé. Cependant, si l'allocation est réussie, il est garanti que le bloc de mémoire demandé se trouve dans le bloc de mémoire retourné.

Attendez-vous à ce que votre requête ne puisse pas être satisfaite parce que la mémoire requise est déjà utilisée par une autre tache.

Dans ce cas, AllocAbs() retourne NULL. Vous devez tester cela explicitement ! Aucun code de condition n'est positionné.

AllocAbs() ne positionnera pas le code ERROR_NO_FREE_STORE de IoErr().

MuGuardianAngel, comme toujours, remplira les registres "perdus" (scratch registers) avec des valeurs insignifiantes et positionnera le flag Z pour amener les programmes mal écrits à une condition d'échec.


Contenu de la mémoire allouée

N'émettez aucune hypothèse quand au contenu du bloc de mémoire alloué. L'OS utilise des parties des blocs de mémoiore libres pour des raisons d'administration et pourrait avoir écrit dans des parties de blocs de mémoire.

Ceci signifie plus particulièrement pour les programmes résistant au reset, dont la mémoire est allouée de cette façon par le mécanisme de KickMemPtr d'exec, que les huit premiers octets seront corrompus. Pensez-y !

MuGuardianAngel corrompra la mémoire allouée volontairement, sauf celle allouée par AllocAbs().


Libération de la mémoire allouée avec AllocAbs()

Pour être sûr que la mémoire allouée est réellement libérée complètement, appelez FreeMem() avec l'adresse et la taille de la mémoire que vous avez demandée, pas avec la valeur de retour d'AllocAbs(). Ceci peut paraitre étrange, mais la logique de FreeMem() applique les même arrondis de taille et d'adresse qu'AllocAbs(). Si vous passez néanmoins une adresse différente, comme la valeur de retour au lieu de l'adresse demandée, il n'est pas garanti que toute la mémoire sera libérée.

Un petit exemple pourrait être utile (en utilisant l'algorithme d'arrondi actuel):

AllocAbs(0x07,0x300007);

alloue 16 octets et retourne 0x300000. Appeler ensuite

FreeMem(0x300000,0x07);

ne libèrera que huit octets en partant de 0x300000 au lieu de 16. Par contre,

FreeMem(0x300007,0x07);

marchera comme souhaité.

Cependant, MuGuardianAngel détectera ce dernier FreeMem() comme mal aligné. Il faut ignorer le hit dans ce cas.


Utiliser AllocAbs() pour allouer de la mémoire alignée

La routine suivante est une astuce pour allouer de la mémoire alignée:


	
void *AllocAligned(ULONG bytesize,ULONG attributes,ULONG alignment)
{
    UBYTE *mem,*res;

    alignment--;
    if (mem=AllocMem(bytesize+alignment,attributes & (~MEMF_CLEAR))) {
    Forbid();
    FreeMem(mem,bytesize+alignment);
    mem = (mem + alignment)&(~alignment);
    res = AllocAbs(bytesize,mem);
    Permit();
    if (res) {
        if (attributes & MEMF_CLEAR)
            memset(mem,0,bytesize);
        } else mem = NULL;
    }
    return mem;
}

Appelez cette routine avec "alignment" valant 16 pour aligner la mémoire sur une frontière de 16 octets. Il ne faut pas fournir une valeur qui ne soit pas une puissance de 2.

Notez que la mémoire est mise à zéro "à la main" si MEMF_CLEAR fait partie des attributs. Ceci doit être fait car AllocAbs() ne garanti pas le contenu de la mémoire, même si l'AllocMem() précédent a déjà initialisé la mémoire.



Tout programme se conformant à ces règles ne causera aucun problème avec MuGuardianAngel !


Outils de débuggage

Si vous écrivez un débugger et que que devez absolument lire de la mémoire non allouée, allez en mode superviseur pour la lire. MuGuardianAngel ne détectera que les accès à la mémoire "libre" en mode utilisateur.


Les outils de débuggage suivants sont recommandés:

MuForce Détecte les accès à la base de vecteurs (VBR) et aux zones mémoire non mappées.

MuGuardianAngel (-: Détecte les accès à la mémoire "libre", les accès illégaux et hors de portée, ainsi que les problèmes mentionnés plus haut.

SegTracker Garde les noms des programmes associés à leurs segments chargés en mémoire afin d'identifier aisément le code. Utilisé par les programmes ci-dessus.

PatchWork Détecte les paramètres invalides lors des appels systèmes.

Sashimi Redirige les hits MuGuardianAngel vers une fenêtre "console".

SaferPatches Détecte les patchs de fonctions illégaux. Si celui-ci plante en faisant un Guru, il y a un problème.

Même un programme fonctionnant sans problème avec ces outils n'est pas obligatoirement exempt de bugs !


D'autres particularités pour les amateurs éclairés. (-:

Ce qui suit est une liste de caractéristiques de l'OS dont vous devriez être conscient si vous avez l'intention d'écrire votre propre pool mémoire. Je les ai trouvés en écrivant PoolMem, donc les voici pour votre information. Cependant, n'utilisez pas ces techniques dans votre propre code.

Bien que les règles ci dessus aient été mises en place pour les développeurs, ça ne veut pas dire que l'OS les respecte ("Quod licet Iovi non licet bovi.").


J'ai trouvé les caractéristiques de l'OS suivantes:


FFS (toutes les versions entre la V37 et la V43) s'attend à ce que FreeMem() retourne la valeur "-1". Ceci a été corrigé dans la version 43.20.

MuGuardianAngel provoque donc un code de retour étrange pour la fonction Close() pour les versions de FFS inférieures à la V43.20.


La version 37 de la layers.library alloue la mémoire par grands blocs, mais la libère en une suite de petites désallocations. En d'autres termes, elle découpe de grands blocs de mémoire en plus petits blocs.

La version actuelle de MuGuardianAngel contient un test spécial pour permettre ceci exclusivement pour la layers.library. Cependant, ceci a toujours été illégal, est illégal et continuera a être illégal. J'espère que ce fouillis a été nettoyé dans la V39.


Certains programmes s'attendent à ce que le flag Z (zéro) du processeur soit positionné lors d'un échec après un appel à AllocMem(), et ne le soit pas si l'allocation a réussi. Ceci n'est pas documenté.

MuGuardianAngel fera échouer ces programmes volontairement.