web-dev-qa-db-fra.com

Que se passe-t-il si j'écris moins de 12 octets dans un tampon de 12 octets?

Naturellement, le dépassement d'une erreur de tampon (ou crée un débordement), mais que se passe-t-il si moins de 12 octets sont utilisés dans un tampon de 12 octets? Est-il possible ou la fin vide se remplit-elle toujours de 0? Question orthogonale qui peut aider: que contient un tampon lorsqu'il est instancié mais pas encore utilisé par l'application?

J'ai regardé quelques programmes pour animaux de compagnie dans Visual Studio et il semble qu'ils soient ajoutés avec des 0 (ou des caractères nuls) mais je ne sais pas s'il s'agit d'une implémentation MS qui peut varier selon la langue/le compilateur.

28
hexadec0079

Considérez votre tampon, rempli de zéros:

[00][00][00][00][00][00][00][00][00][00][00][00]

Maintenant, écrivons-y 10 octets. Valeurs incrémentant de 1:

[01][02][03][04][05][06][07][08][09][10][00][00]

Et maintenant encore, cette fois, 4 fois 0xFF:

[FF][FF][FF][FF][05][06][07][08][09][10][00][00]

que se passe-t-il s'il y a moins de 12 octets utilisés dans un tampon de 12 octets? Est-il possible ou la fin vide se remplit-elle toujours de 0?

Vous écrivez autant que vous le souhaitez, les octets restants restent inchangés.

Question orthogonale qui peut aider: que contient un tampon lorsqu'il est instancié mais pas encore utilisé par l'application?

Non spécifié. Attendez-vous à des déchets indésirables laissés par des programmes (ou d'autres parties de votre programme) qui utilisaient cette mémoire auparavant.

J'ai regardé quelques programmes pour animaux de compagnie dans Visual Studio et il semble qu'ils soient ajoutés avec des 0 (ou des caractères nuls) mais je ne sais pas s'il s'agit d'une implémentation MS qui peut varier selon la langue/le compilateur.

C'est exactement ce que vous pensez que c'est. Quelqu'un l'avait fait pour vous cette fois, mais rien ne garantit que cela se reproduira. Il peut s'agir d'un indicateur de compilation qui attache un code de nettoyage. Certaines versions de MSVC remplissaient la mémoire fraîche avec 0xCD lors de l'exécution en débogage mais pas en version. Il peut également s'agir d'une fonction de sécurité du système qui efface la mémoire avant de la donner à votre processus (vous ne pouvez donc pas espionner d'autres applications). N'oubliez pas d'utiliser memset pour initialiser votre tampon là où cela compte. Finalement, exigez l'utilisation de certains indicateurs de compilateur dans le fichier Lisezmoi si vous comptez sur un nouveau tampon pour contenir une certaine valeur.

Mais le nettoyage n'est pas vraiment nécessaire. Vous prenez un tampon de 12 octets. Vous le remplissez avec 7 octets. Vous passez ensuite quelque part - et vous dites "voici 7 octets pour vous". La taille du tampon n'est pas pertinente lors de la lecture de celui-ci. Vous vous attendez à ce que les autres fonctions lisent autant que vous l'avez écrit, pas autant que possible. En fait, en C, il n'est généralement pas possible de dire la longueur du tampon.

Et une note latérale:

Naturellement, le dépassement d'une erreur de tampon (ou crée un débordement)

Ce n'est pas le cas, c'est le problème. C'est pourquoi c'est un énorme problème de sécurité: il n'y a pas d'erreur et le programme essaie de continuer, il exécute donc parfois le contenu malveillant qu'il n'a jamais voulu. Nous avons donc dû ajouter un tas de mécanismes au système d'exploitation, comme l'ASLR, qui augmentera la probabilité d'un plantage du programme et diminuera la probabilité qu'il continue avec une mémoire corrompue. Donc, ne dépendez jamais de ces gardes après coup et surveillez vous-même vos limites de tampon.

11
Agent_L

Prenons l'exemple suivant (dans un bloc de code, non global):

char data[12];
memcpy(data, "Selbie", 6);

Ou même cet exemple:

char* data = new char[12];
memcpy(data, "Selbie", 6);

Dans les deux cas ci-dessus, les 6 premiers octets de data sont S, e, l, b, i et e. Les 6 octets restants de data sont considérés comme "non spécifiés" (peuvent être n'importe quoi).

Est-il possible ou la fin vide se remplit-elle toujours de 0?

Pas du tout garanti. Le seul allocateur que je connaisse qui garantit un remplissage à zéro octet est calloc . Exemple:

char* data = calloc(12,1);  // will allocate an array of 12 bytes and zero-init each byte
memcpy(data, "Selbie");

que contient un tampon quand il est instancié mais pas encore utilisé par l'application?

Techniquement, selon les normes C++ les plus récentes, les octets fournis par l'allocateur sont techniquement considérés comme "non spécifiés". Vous devez supposer qu'il s'agit de données inutiles (n'importe quoi). Ne faites aucune hypothèse sur le contenu.

Les versions de débogage avec Visual Studio initialisent souvent les tampons avec avec 0xcc ou 0xcd valeurs, mais ce n'est pas le cas dans les versions. Il existe cependant des indicateurs de compilateur et des techniques d'allocation de mémoire pour Windows et Visual Studio où vous pouvez garantir des allocations de mémoire zéro-init, mais ce n'est pas portable.

17
selbie

C++ possède des classes de stockage, notamment globales, automatiques et statiques. L'initialisation dépend de la façon dont la variable est déclarée.

char global[12];  // all 0
static char s_global[12]; // all 0

void foo()
{
   static char s_local[12]; // all 0
   char local[12]; // automatic storage variables are uninitialized, accessing before initialization is undefined behavior 
}

Quelques détails intéressants ici .

11
Matthew Fisher

Le programme connaît la longueur d'une chaîne car il la termine par un terminateur nul, un caractère de valeur zéro.

C'est pourquoi, pour adapter une chaîne dans un tampon, le tampon doit avoir au moins 1 caractère de plus que le nombre de caractères de la chaîne, afin qu'il puisse également contenir la chaîne plus le terminateur nul.

Tout espace après cela dans le tampon est laissé intact. S'il y avait des données auparavant, elles sont toujours là. C'est ce que nous appelons des ordures.

Il est faux de supposer que cet espace est rempli de zéros simplement parce que vous ne l'avez pas encore utilisé, vous ne savez pas à quoi cet espace mémoire particulier a été utilisé avant que votre programme en soit arrivé là. La mémoire non initialisée doit être gérée comme si ce qu'elle contient était aléatoire et peu fiable.

4
Havenard

Toutes les réponses précédentes sont très bonnes et très détaillées, mais l'OP semble être nouveau pour la programmation C. J'ai donc pensé qu'un exemple Real World pourrait être utile.

Imaginez que vous ayez un porte-boisson en carton pouvant contenir six bouteilles. Il a été assis dans votre garage, donc au lieu de six bouteilles, il contient diverses choses peu recommandables qui s'accumulent dans les coins des garages: araignées, maisons de souris, et al.

Un tampon informatique est un peu comme ça juste après l'avoir alloué. Vous ne pouvez pas vraiment être sûr de ce qu'il contient, vous savez juste à quel point c'est grand.

Maintenant, disons que vous mettez quatre bouteilles dans votre support. Votre support n'a pas changé de taille, mais vous savez maintenant ce qu'il y a dans quatre des espaces. Les deux autres espaces, avec leur contenu douteux, sont toujours là.

Les tampons informatiques sont de la même manière. C'est pourquoi vous voyez fréquemment une variable bufferSize pour suivre la quantité de tampon utilisée. Un meilleur nom pourrait être numberOfBytesUsedInMyBuffer mais les programmeurs ont tendance à être affreusement laconiques.

3
Doug Clutter

L'écriture d'une partie d'un tampon n'affectera pas la partie non écrite du tampon; il contiendra tout ce qui s'y trouvait auparavant (ce qui dépend naturellement entièrement de la façon dont vous avez obtenu le tampon en premier lieu).

Comme les autres notes de réponse, les variables statiques et globales seront initialisées à 0, mais les variables locales ne seront pas initialisées (et contiendront à la place tout ce qui était sur la pile auparavant). Cela est conforme au principe de la surcharge zéro: l'initialisation des variables locales serait, dans certains cas, un coût d'exécution inutile et indésirable, tandis que les variables statiques et globales sont allouées au moment du chargement dans le cadre d'un segment de données.

L'initialisation du stockage en tas est au choix du gestionnaire de mémoire, mais en général, il ne sera pas non plus initialisé.

2
comingstorm

En général, il n'est pas du tout inhabituel que les tampons soient insuffisants. Il est souvent recommandé d'allouer des tampons plus grands que nécessaire. (Essayer de toujours calculer une taille de tampon exacte est une source d'erreur fréquente et souvent une perte de temps.)

Lorsqu'un tampon est plus grand qu'il ne devrait l'être, lorsque le tampon contient moins de données que sa taille allouée, il est évidemment important de garder une trace de la quantité de données is là-bas. En général, il existe deux façons de procéder: (1) avec un décompte explicite, conservé dans une variable distincte, ou (2) avec une valeur "sentinelle", comme le \0 caractère qui marque la fin d'une chaîne en C.

Mais alors il y a la question, si tout le tampon n'est pas utilisé, que contiennent les entrées inutilisées?

Une réponse est, bien sûr, que cela n'a pas d'importance. C'est ce que signifie "inutilisé". Vous vous souciez des valeurs des entrées utilisées, qui sont prises en compte par votre décompte ou votre valeur sentinelle. Vous ne vous souciez pas des valeurs inutilisées.

Il existe essentiellement quatre situations dans lesquelles vous pouvez prédire les valeurs initiales des entrées inutilisées dans un tampon:

  1. Lorsque vous allouez un tableau (y compris un tableau de caractères) avec une durée de static, toutes les entrées inutilisées sont initialisées à 0.

  2. Lorsque vous allouez un tableau et lui donnez un initialiseur explicite, toutes les entrées inutilisées sont initialisées à 0.

  3. Lorsque vous appelez calloc, la mémoire allouée est initialisée à all-bits-0.

  4. Lorsque vous appelez strncpy, la chaîne de destination est complétée à la taille n avec \0 personnages.

Dans tous les autres cas, les parties inutilisées d'un tampon sont imprévisibles et contiennent généralement tout ce qu'elles ont fait la dernière fois (quoi que cela signifie). En particulier, vous ne pouvez pas prédire le contenu d'un tableau non initialisé avec une durée automatique (c'est-à-dire celui qui est local à une fonction et qui n'est pas déclaré avec static), et vous ne pouvez pas prédire le contenu de la mémoire obtenue avec malloc. (Parfois, dans ces deux cas, la mémoire a tendance à démarrer en tant que tout-zéro la première fois, mais vous ne voulez certainement pas en dépendre.)

1
Steve Summit

Les objets déclarés de durée statique (ceux déclarés en dehors d'une fonction ou avec un qualificatif static) qui n'ont pas d'initialiseur spécifié sont initialisés à la valeur qui serait représentée par un zéro littéral [c'est-à-dire un zéro entier, un zéro à virgule flottante ou un pointeur nul, selon le cas, ou une structure ou union contenant de telles valeurs]. Si la déclaration d'un objet (y compris ceux de durée automatique) comprend un initialiseur, les parties dont les valeurs sont spécifiées par cet initialiseur seront définies comme spécifié, et le reste sera mis à zéro comme avec les objets statiques.

Pour les objets automatiques sans initialiseurs, la situation est un peu plus ambiguë. Étant donné quelque chose comme:

#include <string.h>

unsigned char static1[5], static2[5];

void test(void)
{
  unsigned char temp[5];
  strcpy(temp, "Hey");
  memcpy(static1, temp, 5);
  memcpy(static2, temp, 5);
}

le standard est clair que test n'invoquerait pas un comportement indéfini, même s'il copie des parties de temp qui n'ont pas été initialisées. Le texte de la norme, au moins à partir de C11, n'est pas clair quant à savoir si quelque chose est garanti sur les valeurs de static1[4] et static2[4], notamment s'ils peuvent conserver des valeurs différentes. Un rapport de défaut indique que la norme n'était pas destinée à interdire à un compilateur de se comporter comme si le code avait été:

unsigned char static1[5]={1,1,1,1,1}, static2[5]={2,2,2,2,2};

void test(void)
{
  unsigned char temp[4];
  strcpy(temp, "Hey");
  memcpy(static1, temp, 4);
  memcpy(static2, temp, 4);
}

qui pourrait laisser static1[4] et static2[4] contenant des valeurs différentes. La norme ne dit pas si les compilateurs de qualité destinés à diverses fins devraient se comporter dans cette fonction. La norme n'offre également aucune indication sur la façon dont la fonction doit être écrite si l'intention du programmeur exige que static1[4] et static2[4] conserve la même valeur, mais peu importe quelle est cette valeur.

1
supercat

Cela dépend du spécificateur de classe de stockage, de votre implémentation et de ses paramètres. Quelques exemples intéressants: - Les variables de pile non initialisées peuvent être définies sur 0xCCCCCCCC - Les variables de tas non initialisées peuvent être définies sur 0xCDCDCDCD - Les variables statiques ou globales non initialisées peuvent être définies sur 0x00000000 - ou ça pourrait être des ordures. Il est risqué de faire des hypothèses à ce sujet.

1
Tim Randall

Je pense que la bonne réponse est que vous devriez toujours garder une trace du nombre de caractères écrits. Comme pour les fonctions de bas niveau comme la lecture et l'écriture, il faut ou donner le nombre de caractères lus ou écrits. De la même manière std :: string garde une trace du nombre de caractères dans son implémentation

1
izulh