web-dev-qa-db-fra.com

Malloc () alloue-t-il un bloc de mémoire contigu?

J'ai un morceau de code écrit par un très vieux programmeur d'école :-). Ca fait plutot comme ca

typedef struct ts_request
{ 
  ts_request_buffer_header_def header; 
  char                         package[1]; 
} ts_request_def; 

ts_request_def* request_buffer = 
malloc(sizeof(ts_request_def) + (2 * 1024 * 1024));

le programmeur travaille essentiellement sur un concept de dépassement de tampon. Je sais que le code a l'air louche. donc mes questions sont:

  1. Malloc alloue-t-il toujours des blocs de mémoire contigus? car dans ce code si les blocs ne sont pas contigus, le code échouera gros

  2. Faire free (request_buffer), libérera-t-il tous les octets alloués par malloc dans size.of (ts_request_def) + (2 * 1024 * 1024), ou seulement les octets de la taille de la structure sizeof (ts_request_def)

  3. Voyez-vous des problèmes évidents avec cette approche, je dois en discuter avec mon patron et je voudrais signaler toute échappatoire avec cette approche

34
user66854

Pour répondre à vos points numérotés.

  1. Oui.
  2. Tous les octets. Malloc/free ne connaît ni le type de l'objet, ni sa taille.
  3. C'est à proprement parler un comportement indéfini, mais une astuce courante supportée par de nombreuses implémentations. Voir ci-dessous pour d'autres alternatives.

La dernière norme C, ISO/IEC 9899: 1999 (officieusement C99), autorise les éléments de matrice flexibles .

Un exemple de ceci serait:

int main(void)
{       
    struct { size_t x; char a[]; } *p;
    p = malloc(sizeof *p + 100);
    if (p)
    {
        /* You can now access up to p->a[99] safely */
    }
}

Cette fonctionnalité désormais normalisée vous permet d'éviter d'utiliser l'extension d'implémentation commune, mais non standard, que vous décrivez dans votre question. À proprement parler, utiliser un membre de groupe non flexible et accéder au-delà de ses limites constitue un comportement indéfini, mais de nombreuses implémentations la documentent et l'encouragent.

De plus, gcc autorise les tableaux de longueur nulle en tant qu'extension. Les tableaux de longueur nulle sont illégaux en C standard, mais gcc a introduit cette fonctionnalité avant que C99 ne nous fournisse des membres de tableau flexibles.

En réponse à un commentaire, je vais expliquer pourquoi l'extrait de code ci-dessous est un comportement techniquement indéfini. Les numéros de section que je cite se réfèrent à C99 (ISO/IEC 9899: 1999)

struct {
    char arr[1];
} *x;
x = malloc(sizeof *x + 1024);
x->arr[23] = 42;

Premièrement, 6.5.2.1 # 2 montre que [i] est identique à (* ((a) + (i))), donc x-> arr [23] est équivalent à (* ((x-> arr)) + ( 23))). Maintenant, 6.5.6 # 8 (sur l'ajout d'un pointeur et d'un entier) dit:

"Si l'opérande de pointeur et le résultat pointent tous deux sur des éléments du même objet de tableau, ou d'un élément après le dernier élément de l'objet de tableau, l'évaluation ne produira pas de débordement; sinon, le comportement est indéfini . "

Pour cette raison, étant donné que x-> arr [23] ne fait pas partie du tableau, le comportement est indéfini. Vous pouvez toujours penser que c'est bon parce que malloc () implique que le tableau a maintenant été étendu, mais ce n'est pas strictement le cas. L'annexe informative J.2 (qui répertorie des exemples de comportement non défini) fournit des éclaircissements supplémentaires avec un exemple:

Un indice de tableau est en dehors des limites, même si un objet est apparemment accessible avec l'indice (Comme dans l'expression lvalue a [1] [7] étant donné la déclaration int A [4] [5]) (6.5.6).

47
Chris

3 - C’est une astuce assez commune pour attribuer un tableau dynamique à la fin d’une structure. L'alternative serait de placer un pointeur dans la structure, puis d'allouer le tableau séparément, sans oublier de le libérer également. Que la taille soit fixée à 2 Mo semble un peu inhabituel cependant.

10
TrayMan

1) Oui, ou malloc échouera s'il n'y a pas un bloc contigu suffisamment grand disponible. (Un échec avec malloc retournera un pointeur NULL)

2) Oui ça va. L'allocation de mémoire interne garde la trace de la quantité de mémoire allouée avec cette valeur de pointeur et la libère entièrement.

3) C'est un peu un hack de langage et un peu douteux quant à son utilisation. Il reste également sujet à des débordements de mémoire tampon, mais il faudra peut-être un peu plus de temps aux attaquants pour trouver la charge utile qui le provoquera. Le coût de la «protection» est également très lourd (avez-vous vraiment besoin de plus de 2 Mo par tampon de requête?). C'est aussi très moche, même si votre patron n'appréciera peut-être pas cet argument :)

7
workmad3

C'est une astuce standard en C, et n'est pas plus dangereux qu'un autre tampon.

Si vous essayez de montrer à votre patron que vous êtes plus intelligent que "très vieux programmeur d'école", ce code ne vous concerne pas. La vieille école n'est pas nécessairement mauvaise. On dirait que le gars de la vieille école en sait assez sur la gestion de la mémoire

7
qrdl

Je ne pense pas que les réponses existantes vont tout à fait dans le sens de ce problème. Vous dites que le programmeur de la vieille école fait quelque chose comme ça;

typedef struct ts_request
{ 
  ts_request_buffer_header_def header; 
  char                         package[1]; 
} ts_request_def;

ts_request_buffer_def* request_buffer = 
malloc(sizeof(ts_request_def) + (2 * 1024 * 1024));

Je pense qu'il est peu probable qu'il le fasse exactement, car si c'est ce qu'il voulait faire, il pourrait le faire avec un code équivalent simplifié qui ne nécessite aucune astuce;

typedef struct ts_request
{ 
  ts_request_buffer_header_def header; 
  char                         package[2*1024*1024 + 1]; 
} ts_request_def;

ts_request_buffer_def* request_buffer = 
malloc(sizeof(ts_request_def));

Je parie que ce qu'il fait vraiment est quelque chose comme ça;

typedef struct ts_request
{ 
  ts_request_buffer_header_def header; 
  char                         package[1]; // effectively package[x]
} ts_request_def;

ts_request_buffer_def* request_buffer = 
malloc( sizeof(ts_request_def) + x );

Ce qu'il veut, c'est allouer une requête avec une taille de paquet variable x. Il est bien entendu illégal de déclarer la taille du tableau avec une variable, alors il contourne cela avec un truc. On dirait qu’il sait ce qu’il fait pour moi, le truc va bien vers la fin respectable et pratique de la ruse en C.

5
Bill Forster

Oui. malloc ne renvoie qu'un seul pointeur. Comment peut-il indiquer à un demandeur qu'il a attribué plusieurs blocs non contigus pour répondre à une demande?

3
Andrew Medico

Quant à # 3, sans plus de code, il est difficile de répondre. Je ne vois rien de mal à cela, à moins que cela n'arrive souvent. Je veux dire, vous ne voulez pas allouer des morceaux de mémoire de 2 Mo tout le temps. Vous ne voulez pas non plus le faire inutilement, par exemple si vous n'utilisez que 2k.

Le fait que vous n'aimiez pas cela pour une raison quelconque ne suffit pas pour vous y opposer ou pour justifier une réécriture complète. J'examinerais de près l'utilisation, essayerais de comprendre ce que pensait le programmeur d'origine, de près les débordements de mémoire tampon (comme l'a souligné workmad3) dans le code qui utilise cette mémoire.

Vous pouvez trouver de nombreuses erreurs courantes. Par exemple, le code vérifie-t-il que malloc () a réussi?

3
i_am_jorf

L'exploit (question 3) est vraiment à la hauteur de l'interface vers cette structure. Dans le contexte, cette attribution peut avoir un sens, et sans informations supplémentaires, il est impossible de dire si elle est sécurisée ou non.
Mais si vous parlez de problèmes pour allouer de la mémoire plus grande que la structure, ceci n’est en aucun cas une mauvaise conception en C (je ne dirais même pas que c’est la vieille école ...;))
Juste une dernière remarque ici - le point avec un caractère [1] est que la valeur NULL de fin sera toujours dans la structure déclarée, ce qui signifie qu'il peut y avoir 2 * 1024 * 1024 caractères dans le tampon, et vous Vous n'avez pas à rendre compte de la valeur NULL par un "+1". Cela pourrait ressembler à un petit exploit, mais je voulais simplement le souligner.

3
E Dominique

J'ai vu et utilisé ce modèle fréquemment. 

Son avantage est de simplifier la gestion de la mémoire et d'éviter ainsi le risque de fuite de mémoire. Il suffit de libérer le bloc Malloc'ed. Avec un tampon secondaire, vous aurez besoin de deux gratuits. Cependant, il convient de définir et d'utiliser une fonction de destructeur pour encapsuler cette opération afin que vous puissiez toujours modifier son comportement, comme pour passer à la mémoire tampon secondaire ou ajouter des opérations supplémentaires à effectuer lors de la suppression de la structure. 

L'accès aux éléments du tableau est également légèrement plus efficace, mais il est de moins en moins important avec les ordinateurs modernes. 

Le code fonctionnera également correctement si l'alignement de la mémoire change dans la structure avec différents compilateurs car il est assez fréquent.

Le seul problème potentiel que je vois est si le compilateur permute l'ordre de stockage des variables membres, car cette astuce nécessite que le champ de package reste le dernier dans le stockage. Je ne sais pas si la norme C interdit la permutation.

Notez également que la taille de la mémoire tampon allouée sera probablement plus grande que nécessaire, au moins d'un octet avec les octets de remplissage supplémentaires éventuels. 

3
chmike

J'aimerais ajouter que ce n'est pas courant, mais je pourrais aussi l'appeler une pratique standard parce que l'API Windows est pleine d'une telle utilisation.

Vérifiez la structure très commune des en-têtes BITMAP, par exemple.

http://msdn.Microsoft.com/en-us/library/aa921550.aspx

Le dernier quad RBG est un tableau de 1 taille, qui dépend exactement de cette technique.

2
CDR

En réponse à votre troisième question.

free libère toujours toute la mémoire allouée d’un seul coup.

int* i = (int*) malloc(1024*2);

free(i+1024); // gives error because the pointer 'i' is offset

free(i); // releases all the 2KB memory
2
Prasenjit
2
X-Istence

La réponse aux questions 1 et 2 est oui

À propos de la laideur (question 3), qu'est-ce que le programmeur essaie de faire avec cette mémoire allouée?

1
hhafez

la chose à réaliser ici est que malloc ne voit pas le calcul être fait dans ce

malloc(sizeof(ts_request_def) + (2 * 1024 * 1024));

C'est la même chose que

  int sz = sizeof(ts_request_def) + (2 * 1024 * 1024);
   malloc(sz);

Vous pensez peut-être qu’il alloue 2 blocs de mémoire et, dans l’esprit, il s’agit de «la structure», de «certains tampons». Mais malloc ne voit pas ça du tout.

0
pm100