J'ai lu récemment que le dépassement des nombres entiers signés en C et C++ entraîne un comportement indéfini :
Si, lors de l'évaluation d'une expression, le résultat n'est pas défini mathématiquement ou ne se trouve pas dans la plage des valeurs représentables pour son type, le comportement est indéfini.
J'essaie actuellement de comprendre la raison de ce comportement indéfini. Je pense que le comportement indéfini se produit ici parce que l'entier commence à manipuler la mémoire autour de lui quand il devient trop grand pour s'adapter au type sous-jacent.
J'ai donc décidé d'écrire un petit programme de test dans Visual Studio 2015 pour vérifier cette théorie avec le code suivant :
#include <stdio.h>
#include <limits.h>
struct TestStruct
{
char pad1[50];
int testVal;
char pad2[50];
};
int main()
{
TestStruct test;
memset(&test, 0, sizeof(test));
for (test.testVal = 0; ; test.testVal++)
{
if (test.testVal == INT_MAX)
printf("Overflowing\r\n");
}
return 0;
}
J'ai utilisé une structure ici pour éviter toute protection de Visual Studio en mode débogage, comme le remplissage temporaire des variables de la pile et ainsi de suite. La boucle sans fin devrait provoquer plusieurs débordements de test.testVal
et c'est effectivement le cas, mais sans aucune conséquence autre que le débordement lui-même.
J'ai jeté un coup d'œil au vidage de la mémoire lors de l'exécution des tests de débordement et j'ai obtenu le résultat suivant ( test.testVal
avait une adresse mémoire de 0x001CFAFC
):
0x001CFAE5 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x001CFAFC 94 53 ca d8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Comme vous le voyez, la mémoire autour de l'int qui déborde continuellement est restée "intacte". J'ai testé ceci plusieurs fois avec un résultat similaire. Jamais la mémoire autour de l'int qui déborde n'a été endommagée.
Que se passe-t-il ici ? Pourquoi la mémoire autour de la variable n'est-elle pas endommagée ? test.testVal
? Comment cela peut-il provoquer un comportement indéfini ?
J'essaie de comprendre mon erreur et pourquoi il n'y a pas de corruption de mémoire lors d'un débordement d'entier.
37 votes
Vous vous attendez à obtenir une définition du comportement qui est "indéfini" ? ! On vous dit explicitement qu'il n'y a pas d'attentes raisonnables que vous pouvez avoir, donc le comportement ne peut pas différer de ce que vous êtes autorisé à attendre.
8 votes
Le dépassement des nombres entiers n'affecte pas la mémoire adjacente.
0 votes
De quelle autre manière devrait-elle devenir indéfinie ? La valeur n'a pas la taille définie, oui c'est vrai, mais UB ne signifie-t-il pas que la mémoire est corrompue et que des choses indéfinies se produisent ? Je veux dire qu'un int qui devient quelque chose comme -2147483648 après avoir débordé est en quelque sorte défini, n'est-ce pas ?
9 votes
@NathanOliver, il n'y a aucun mal à raisonner un comportement non défini. Je trouve personnellement cet exercice très utile.
2 votes
Essayer de définir comportement indéfini . Cherchez le mot dans un dictionnaire. Et ne postez pas d'images de texte !
7 votes
@Olaf UB a une raison, et j'essaie de la faire comprendre. L'image ne contient pas une partie cruciale de la question mais est plutôt là pour l'illustration graphique de mes résultats de test. Tout dans l'image, y compris le code utilisé, a été posté en texte clair.
20 votes
Le downvoting de cette question est tout à fait incorrect à mon avis. Le PO montre en fait un désir très sain de comprendre, plutôt que de suivre aveuglément.
0 votes
@SergeyA : Il pourrait, mais n'a pas montré d'effort de recherche. Par exemple : ne pas informer si C a des entiers de largeur arbitraire. Une simple recherche aurait répondu à cette question. Semblable pour ne pas vérifier le code assembleur. comment l'incrémentation est effectuée sur son plate-forme. Sans parler des images de position du texte.
3 votes
@Olaf, la question aurait pu être mieux posée (je ne suis pas non plus fan de cette image). Mais je continue de penser qu'elle a plus de qualité que les downvotes ne le suggèrent.
3 votes
Si vous reformulez cette question comme une question sur le matériel (x86) au lieu d'une question sur le C++, ce serait beaucoup mieux. Sur x86, en ajoutant 1 à 0x7fffffff (valeur positive maximale), on obtient 0x80000000 (valeur négative minimale). Mais d'autres machines donneront des résultats différents.
0 votes
Je suis désolé pour tous les malentendus qui ont entouré mon message. Je l'ai édité et j'espère qu'il est maintenant clair que j'ai essayé de comprendre pourquoi ma théorie, que le débordement d'entier cause la corruption de mémoire, est fausse :) Merci à tous pour vos réponses et commentaires !
0 votes
@brianbeuning merci, j'ai ajouté une balise x86 à la question. Vous avez tout à fait raison, c'est basé sur l'architecture.
1 votes
@brianbeuning : Ce n'est pas garanti. Un compilateur peut (et certains le feront parfois) détecter les opérations de débordement et générer un code qui ne se comporte pas comme prévu - absolument conforme à la norme.
0 votes
Malheureusement, il n'y a pas que votre nez d'où les démons peuvent sortir :(
4 votes
@Olaf La question semble être basée sur le fait que non tout à fait comprendre la signification de "comportement non défini" dans ce contexte. Je doute qu'une recherche sur Google puisse y remédier, car, d'après mon expérience, les didacticiels partent généralement du principe que le lecteur est capable de déduire la signification correcte. Je pense que c'est une très bonne question.
2 votes
Izkata : Au risque de paraître dur : C'est une compétence linguistique de base. Si c'est un problème de traduction (ce dont je doute d'après le texte), il peut être facilement traduit. Sinon, voici la vérité (potentiellement dure) : nous pouvons très bien attendre des recherches de la part du PO. Et attendre un comportement défini de quelque chose d'indéfini par sa nature explicite est un oxymore par exellence.
2 votes
@Olaf Par "contexte", je ne veux pas dire le langage humain. Je veux dire un contexte technique par rapport à un contexte non-technique. "Undefined behavior" peut signifier n'importe quoi en anglais, mais en programmation, il se réfère à un sous-ensemble de ce que l'expression signifie en anglais courant. J'ai l'impression que le PO n'a pas tout à fait compris cela, et comme je l'ai dit, la différence est rarement expliquée quelque part.
1 votes
@Izkata : 1) Il s'agit d'un site consacré aux questions de programmation, donc l'ensemble sémantique est évident. 2) Vous êtes le bienvenu pour fournir un exemple dans n'importe quel contexte où " un défini" signifie "quelque peu défini". 3) Si cela ne devient pas clair après quelques recherches mineures, peut-être que la programmation (ou tout ce qui implique de l'ingénierie) n'est pas l'objectif à atteindre (j'ai rencontré beaucoup de personnes de ce genre et elles sont peut-être bonnes dans d'autres domaines où je suis très mauvais). 4) Si vous tombez d'un haut bâtiment, il ne sert pas à grand chose de négocier avec la gravité.
2 votes
@brianbeuning : La norme a fait du débordement d'entier un comportement non défini dans le but de permettre des implémentations C efficaces sur du matériel où il pourrait avoir de méchants effets éventuellement imprévisibles, mais la philosophie du compilateur hyper-moderne dicte que parce que le manque d'exigences de la norme en ce qui concerne le débordement est plus important que la prévisibilité de la réponse du matériel.
1 votes
@Olaf : La norme a été écrite en partant du principe que si certaines implémentations peuvent définir à peu de frais un comportement dans une certaine situation et d'autres non, la norme devrait laisser la tâche de définir le comportement dans ces situations aux implémentations qui peuvent les supporter. Dans de nombreux cas, les implémentations sont conçues de manière à offrir naturellement certaines garanties comportementales, mais comme la norme ne dispose pas de la terminologie nécessaire pour décrire les comportements non déterministes, de nombreuses implémentations n'ont pas pris la peine de les documenter. C'est regrettable car il existe de nombreuses situations où...
0 votes
...une fonction va, dans le cadre de ses fonctions, calculer une valeur qui peut ou non être significative, pour un code qui peut ou non s'en soucier. Si l'on peut garantir que les calculs de nombres entiers n'ont pas d'effets secondaires (une garantie peu coûteuse sur la plupart des plates-formes), effectuer de tels calculs sans condition d'une manière qui donnerait des résultats arbitraires en cas de débordement peut être moins coûteux que d'avoir à déterminer si les résultats sont nécessaires ou sûrs. Puisque peu d'auteurs de compilateurs prennent la peine de documenter "Ce calcul peut donner des résultats arbitraires mais n'a pas d'effets secondaires", même si la conception de leurs compilateurs...
0 votes
...l'aurait naturellement garanti, et puisque sur de nombreuses plates-formes il n'y a vraiment aucune raison pour que les calculs d'entiers aient des effets secondaires (y a-t-il des contre-exemples pour les compilateurs de micro-ordinateurs d'usage général avant 2000 ?), l'interprétation normale de "Comportement indéfini" signifiait "Donne un résultat qui pourrait ne pas se comporter de manière cohérente comme une valeur du type indiqué, mais n'a pas d'effets secondaires".
0 votes
@supercat : Vous pourriez être surpris de voir à quel point un bon compilateur peut optimiser le code s'il détecte des UB. gcc est assez célèbre pour exploiter de telles faiblesses dans le code. Et je le soutiens très bien. Les choses peuvent être (et sont) différentes sur des plateformes spéciales où un comportement spécifique est souhaité (par exemple la saturation), éventuellement avec des pargmas ou des intrinsèques. Cela permettrait toujours d'utiliser un niveau d'abstraction plus élevé que l'Assembleur pour le code, tout en fournissant un contrôle fin. C'est l'une des raisons pour lesquelles le C est devenu si populaire.
0 votes
@sashoalm "Le dépassement d'un nombre entier n'affecte pas la mémoire adjacente" : Sur ma plateforme, c'est le cas. (Oui, je plaisante. Mais il peut affecter ce qu'il veut. C'est tout le problème avec UB).
2 votes
@supercat Je suppose qu'Olaf fait allusion à des choses comme stackoverflow.com/q/7682477 ou le célèbre bogue gcc 30475 . Undefined est indéfini. Le développeur peut penser : "Hé, je suis sur une plateforme x86 32 bits avec des nombres à 2 compléments, je ". connaître ce qui se passe en cas de débordement", mais à cause de l'UB, même un code apparemment raisonnable peut produire des déchets complets.
0 votes
@Olaf
You are very welcome to provide an example in any context where "undefined" means "somewhat defined".
L'exemple de l'OP en est un. Il fait la même chose sur son système à chaque exécution, donc c'est cohérent. Un "comportement indéfini" anglais donnerait des résultats différents à chaque exécution.0 votes
@Olaf : Je ne suis pas surpris de voir jusqu'où les compilateurs hyper-modernes iront pour exploiter la décision de la norme d'ignorer des comportements qui étaient 100% cohérents sur les compilateurs pour micro-ordinateurs. Et je ne suis pas d'accord avec votre évaluation de la raison pour laquelle le C est devenu populaire. Le C est devenu populaire parce que dans les cas où les exigences de cas limite d'un programme étaient vaguement définies et pouvaient être satisfaites par le comportement naturel de la plate-forme sans code de cas spécial, il n'y avait souvent aucun besoin pour le programmeur ou le compilateur de générer des vérifications de limites. Si une fonction était censée, entre autres tâches, ...
2 votes
@Izkata : Le terme "non défini" ne nécessite pas au hasard comportement. Pire, cela inclut très bien d'obtenir les mêmes résultats (et même ceux attendus). Le pire avec le BU est qu'il passe inaperçu (ce qui ne veut pas dire qu'il ne fait pas de mal).
0 votes
...effectuer un calcul qui peut ou non être significatif, pour un code qui peut ou non utiliser le résultat, être capable d'ignorer le débordement dans les cas où le résultat finirait par être jeté de toute façon permet au code d'être plus efficace. Je dirais qu'un bon optimiseur ne devrait jamais rendre nécessaire la génération d'un code significativement plus lent que celui qui pourrait être généré par un programmeur avec un compilateur plus simpliste. Bien que je ne sois pas surpris par le comportement des compilateurs hypermodernes, je ne suis pas d'accord avec votre utilisation du terme "bon".
0 votes
@supercat : C'est exactement ce que je dis. Il laisse ces vérifications au programmeur et se concentre sur les meilleures performances. Un peu comme le fait l'assembleur. En revanche, vous pouvez utiliser des structures de données (ce qui est le véritable avantage du C par rapport à l'Assembleur) pour des niveaux d'abstraction plus élevés.
2 votes
@Olaf Comme je l'ai dit dans mon premier commentaire, "comportement indéfini" a une nuance différente dans un contexte technique (ce qui est ce que vous défendez et Je ne suis pas en désaccord ), et en anglais simple. Cette différence est rarement expliquée explicitement aux nouveaux programmeurs. Vous semblez coder depuis si longtemps que le sens technique est le sens par défaut dans votre tête.
0 votes
@Olaf : Les compilateurs C laissaient aux programmeurs la liberté de décider si le code de cas limite devait être inclus. Les compilateurs hyper-modernes forcent les programmeurs à inclure du code de cas limite, que dans de nombreux cas les compilateurs ne peuvent pas optimiser, même dans les cas où le code machine avec le code de traitement des cas limites omis aurait très bien fonctionné. .
1 votes
@supercat : Je ne suis pas du tout d'accord. Vous deviez toujours vous assurer de ne pas provoquer un comportement indéfini. Par exemple, un débordement signé : Envelopper 2s complément est toujours un problème, donc vous devez vous assurer que votre code est sûr de toute façon. Se plaindre des compilateurs "hypermodernes" (peu importe ce que cela signifie) est inutile. Si vous voulez exploiter un comportement au niveau machine, allez au niveau machine ; c'est la raison pour laquelle l'assembleur en ligne existe. Sinon, écrivez un code correct, ce n'est pas si compliqué.
0 votes
@Izkata : La signification, même dans des contextes techniques, a changé. Au moment de la rédaction de la norme C, les auteurs ont noté que la majorité des implémentations avaient défini un comportement de débordement tel que le comportement de
uint1 = int1 * (signed)uchar1;
yuint1 = int1 * (unsigned)uchar1
seraient équivalentes pour les résultats entreINT_MAX+1u
yUINT_MAX
à moins que le résultat ne soit utilisé de manière spécifique. Bien que la norme n'exige pas que les implémentations se comportent de cette manière, les auteurs de la justification ont noté que la plupart des implémentations actuelles le faisaient, et ils s'attendaient probablement à ce que cela reste vrai pour toujours.0 votes
@Olaf : Supposons que l'on doive calculer
long l2=i1*i2+l1;
sur un système doté d'un multiplicateur rapide dans les cas où i1 et i2 sont tous deux compris entre -1000 et 1000, ou fixer l2 à n'importe quelle valeur sinon. Quel est le moyen le plus efficace d'y parvenir ? En l'écrivantlong l2=(int)((unsigned)i1*i2)+l1;
générerait sur de nombreuses plates-formes un code moins efficace que l'original, mais le code machine original répondrait aux exigences dans tous les cas où le compilateur n'essaierait pas de tirer des déductions inappropriées sur i1 et i2.0 votes
Cela invoquerait déjà l'UB sur les plateformes I16. Et pourquoi la distribution à
unsigned
? Comme vous l'avez écrit, la plage inclut des valeurs négatives. Juste(long)i1*i2)+l1
est suffisante. Un bon compilateur peut très bien utiliser par exemple la multiplication 32*32->32 pour les plateformes I32L64 ou 16*16->32 sur I16, ou tout ce qui est optimal. Je ne sais pas quel est votre problème ici.0 votes
@Olaf : Pour les plateformes I16, utilisez -100 à +100 comme plage de valeurs, et pour les plateformes 32 bits, utilisez "long long". Les instructions de multiplication de certaines plateformes peuvent calculer un résultat de double longueur plus rapidement qu'un résultat de simple longueur avec extension de signe, mais pour d'autres (par exemple Cortex-M0) ce n'est pas le cas. Une multiplication 32x32->64 sera plus de quatre fois plus lente que 32x32->32 sur cette plateforme. Si le code ne se soucie pas de la valeur calculée lorsque i1 et i2 sont en dehors de la plage donnée, pourquoi passer du temps supplémentaire sur les calculs pour ce cas ?
0 votes
@Olaf : De plus, avez-vous lu la justification de la décision de C89 de faire en sorte que les types unsigned courts soient promus en int signés ? La norme n'exige pas que les implémentations définissent un comportement de débordement, mais les auteurs de la norme considéraient que la majorité des implémentations de l'époque le faisaient.
0 votes
@supercat : Non, et je ne me soucie pas de C89. La seule norme C est C11 et cela laisse la conversion à l'implémentation. Ce qui signifie que l'implémentation doit la documenter. De toute façon, vous ne discutez pas, mais vous vous contentez de me balancer vos déclarations. Je m'en vais.
2 votes
@Olaf Vous n'aidez personne en les réprimandant parce qu'ils ne comprennent pas un concept et en les accusant de ne pas faire de recherches superficielles. Pensez-vous que quelqu'un puisse honnêtement penser qu'une recherche sur Google est plus difficile que d'écrire une longue question sur stackoverflow comme celle-ci ? Il est évident que le PO a une idée fausse du concept de comportement indéfini de la norme et de la raison pour laquelle il utilise un tel concept. Un dictionnaire ne clarifiera pas son incompréhension et vous le savez.