web-dev-qa-db-fra.com

Liste définitive des raisons courantes des erreurs de segmentation

REMARQUE: Nous avons beaucoup de questions de défaut de segmentation, avec en grande partie les mêmes réponses, donc j'essaie de les réduire en une question canonique comme nous l'avons pour référence non définie .

Bien que nous ayons une question couvrant ce qu'est une erreur de segmentation , elle couvre le quoi , mais ne donne pas de nombreuses raisons. La première réponse indique "il existe de nombreuses raisons" et n'en répertorie qu'une, et la plupart des autres réponses ne répertorient aucune raison.

Dans l'ensemble, je pense que nous avons besoin d'un wiki communautaire bien organisé sur ce sujet, qui répertorie toutes les causes courantes (puis certaines) pour obtenir des erreurs de segmentation . Le but est d'aider au débogage, comme mentionné dans l'avertissement de la réponse.

Je sais ce qu'est une erreur de segmentation, mais il peut être difficile de repérer le code sans savoir à quoi il ressemble souvent. Bien qu'il y ait, sans aucun doute, beaucoup trop pour les énumérer de manière exhaustive, quelles sont les causes les plus courantes des défauts de segmentation en C et C++?

54
CodeMouse92

AVERTISSEMENT!

Les éléments suivants sont potentiel raisons d'un défaut de segmentation. Il est pratiquement impossible d'énumérer toutes les raisons . Le but de cette liste est d'aider à diagnostiquer un défaut de segmentation existant.

La relation entre les défauts de segmentation et le comportement indéfini ne peut pas être assez souligné! Toutes les situations ci-dessous qui peuvent créer une erreur de segmentation sont un comportement techniquement indéfini. Cela signifie qu'ils peuvent faire n'importe quoi, pas seulement une erreur de segmentation - en tant que personne une fois dit sur USENET, " il est légal pour le compilateur de faire sortir les démons de votre nez. ". Ne comptez pas sur une erreur de segmentation lorsque vous avez un comportement indéfini. Vous devez savoir quels comportements non définis existent en C et/ou C++, et éviter d'écrire du code qui les contient!

Plus d'informations sur le comportement indéfini:


Qu'est-ce qu'un Segfault?

En bref, une erreur de segmentation est provoquée lorsque le code tente d'accéder à la mémoire à laquelle il n'a pas l'autorisation d'accéder . Chaque programme dispose d'un morceau de mémoire (RAM) avec lequel travailler, et pour des raisons de sécurité, il est uniquement autorisé à accéder à la mémoire de ce bloc.

Pour une explication technique plus approfondie de ce qu'est un défaut de segmentation c'est, voir Qu'est-ce qu'un défaut de segmentation? .

Voici les raisons les plus courantes d'une erreur de segmentation. Encore une fois, ceux-ci doivent être utilisés pour diagnostiquer un défaut de segmentation existant . Pour savoir comment les éviter, apprenez les comportements indéfinis de votre langue.

Cette liste n'est également pas un remplacement pour faire votre propre travail de débogage . (Voir cette section au bas de la réponse.) Ce sont des choses que vous pouvez rechercher, mais vos outils de débogage sont le seul moyen fiable de résoudre le problème.


Accès à un pointeur NULL ou non initialisé

Si vous avez un pointeur NULL (ptr=0) ou qui n'est pas complètement initialisé (il n'a encore rien défini), la tentative d'accès ou de modification à l'aide de ce pointeur a un comportement indéfini.

int* ptr = 0;
*ptr += 5;

Puisqu'une allocation échouée (comme avec malloc ou new) retournera un pointeur nul, vous devez toujours vérifier que votre pointeur n'est pas NULL avant de travailler avec.

Notez également que même lecture valeurs (sans déréférencement) des pointeurs non initialisés (et des variables en général) est un comportement non défini.

Parfois, cet accès à un pointeur non défini peut être assez subtil, comme pour essayer d'interpréter un tel pointeur comme une chaîne dans une instruction d'impression C.

char* ptr;
sprintf(id, "%s", ptr);

Voir également:


Accéder à un pointeur suspendu

Si vous utilisez malloc ou new pour allouer de la mémoire, puis plus tard free ou delete cette mémoire via un pointeur, ce pointeur est désormais considéré comme un pointeur pendant . Le déréférencer (ainsi que simplement lire sa valeur - étant donné que vous ne lui avez pas assigné de nouvelle valeur telle que NULL) est un comportement non défini et peut entraîner un défaut de segmentation.

Something* ptr = new Something(123, 456);
delete ptr;
std::cout << ptr->foo << std::endl;

Voir également:


Débordement de pile

[Non, pas le site sur lequel vous vous trouvez actuellement, ce qui était nommé pour.] Trop simplifié, la "pile" est comme cette pointe sur laquelle vous collez votre papier de commande dans certains convives. Ce problème peut se produire lorsque vous passez trop de commandes sur ce pic, pour ainsi dire. Dans l'ordinateur, toute variable qui est pas allouée dynamiquement et toute commande qui n'a pas encore été traitée par le CPU, va sur la pile.

Une des causes pourrait être une récursion profonde ou infinie, comme lorsqu'une fonction s'appelle sans aucun moyen de s'arrêter. Parce que cette pile a débordé, les ordres de travail commencent à "tomber" et à occuper d'autres espaces qui ne leur sont pas destinés. Ainsi, nous pouvons obtenir un défaut de segmentation. Une autre cause pourrait être la tentative d'initialisation d'un très grand tableau: il ne s'agit que d'une seule commande, mais déjà suffisamment grande par elle-même.

int stupidFunction(int n)
{
   return stupidFunction(n);
}

Une autre cause d'un débordement de pile serait d'avoir trop de variables (allouées de manière non dynamique) à la fois.

int stupidArray[600851475143];

Un cas de débordement de pile dans la nature provient d'une simple omission d'une instruction return dans un conditionnel destiné à empêcher une récursion infinie dans une fonction. La morale de cette histoire, assurez-vous toujours que vos vérifications d'erreur fonctionnent!

Voir également:


Pointeurs sauvages

Créer un pointeur vers un emplacement aléatoire dans la mémoire, c'est comme jouer à la roulette russe avec votre code - vous pourriez facilement manquer et créer un pointeur vers un emplacement auquel vous n'avez pas les droits d'accès.

int n = 123;
int* ptr = (&n + 0xDEADBEEF); //This is just stupid, people.

En règle générale, ne créez pas de pointeurs vers des emplacements de mémoire littérale. Même s'ils travaillent une fois, la prochaine fois, ils pourraient ne pas. Vous ne pouvez pas prédire où sera la mémoire de votre programme à une exécution donnée.

Voir également:


Tentative de lecture après la fin d'un tableau

Un tableau est une région contiguë de mémoire, où chaque élément successif est situé à l'adresse suivante en mémoire. Cependant, la plupart des tableaux n'ont pas une idée innée de leur taille ou du dernier élément. Ainsi, il est facile de dépasser la fin du tableau et de ne jamais le savoir, surtout si vous utilisez l'arithmétique des pointeurs.

Si vous lisez après la fin du tableau, vous risquez de vous retrouver dans une mémoire non initialisée ou appartenant à autre chose. Il s'agit techniquement d'un comportement indéfini . Une faute de segmentation n'est qu'un de ces nombreux comportements potentiels non définis. [Franchement, si vous obtenez une erreur de segmentation ici, vous avez de la chance. D'autres sont plus difficiles à diagnostiquer.]

// like most UB, this code is a total crapshoot.
int arr[3] {5, 151, 478};
int i = 0;
while(arr[i] != 16)
{
   std::cout << arr[i] << std::endl;
   i++;
}

Ou celui que vous voyez fréquemment en utilisant for avec <= au lieu de < (lit 1 octet de trop):

char arr[10];
for (int i = 0; i<=10; i++)
{
   std::cout << arr[i] << std::endl;
}

Ou même une faute de frappe qui compile bien (vu ici ) et alloue seulement 1 élément initialisé avec dim au lieu de dim éléments.

int* my_array = new int(dim);

De plus, il convient de noter que vous n'êtes même pas autorisé à créer (sans parler de déréférencement) un pointeur qui pointe en dehors du tableau (vous ne pouvez créer un tel pointeur que s'il pointe vers un élément du tableau, ou après la fin). Sinon, vous déclenchez un comportement non défini.

Voir également:


Oublier un terminateur NUL sur une chaîne C.

Les chaînes C sont, elles-mêmes, des tableaux avec quelques comportements supplémentaires. Ils doivent être terminés par null, ce qui signifie qu'ils ont un \0 à la fin, pour être utilisé de manière fiable comme chaînes. Cela se fait automatiquement dans certains cas et pas dans d'autres.

Si cela est oublié, certaines fonctions qui gèrent les chaînes C ne savent jamais quand s'arrêter, et vous pouvez obtenir les mêmes problèmes qu'avec la lecture après la fin d'un tableau.

char str[3] = {'f', 'o', 'o'};
int i = 0;
while(str[i] != '\0')
{
   std::cout << str[i] << std::endl;
   i++;
}

Avec les chaînes en C, c'est vraiment un hasard si \0 fera toute différence. Vous devez supposer que cela évitera un comportement indéfini: il vaut donc mieux écrire char str[4] = {'f', 'o', 'o', '\0'};


Tentative de modification d'un littéral de chaîne

Si vous affectez un littéral de chaîne à un caractère *, il ne peut pas être modifié. Par exemple...

char* foo = "Hello, world!"
foo[7] = 'W';

... déclenche un comportement indéfini , et une erreur de segmentation est un résultat possible.

Voir également:


Méthodes d'allocation et de désallocation incompatibles

Vous devez utiliser malloc et free ensemble, new et delete ensemble, et new[] et delete[] ensemble. Si vous les mélangez, vous pouvez obtenir des erreurs de segmentation et d'autres comportements étranges.

Voir également:


Erreurs dans la chaîne d'outils.

Un bogue dans le backend du code machine d'un compilateur est tout à fait capable de transformer un code valide en un exécutable qui segfaults. Un bug dans l'éditeur de liens peut certainement le faire aussi.

Particulièrement effrayant en ce que ce n'est pas UB invoqué par votre propre code.

Cela dit, vous devez toujours supposer que le problème est vous jusqu'à preuve du contraire.


Autres causes

Les causes possibles des défauts de segmentation sont à peu près aussi nombreuses que le nombre de comportements non définis, et il y en a beaucoup trop pour que même la documentation standard soit répertoriée.

Quelques causes moins courantes à vérifier:


DÉBOGAGE

Les outils de débogage sont essentiels pour diagnostiquer les causes d'une erreur de segmentation. Compilez votre programme avec l'indicateur de débogage (-g), puis exécutez-le avec votre débogueur pour trouver où le segfault se produit probablement.

Les compilateurs récents prennent en charge la construction avec -fsanitize=address, ce qui entraîne généralement un programme qui s'exécute environ 2 fois plus lentement mais qui peut détecter les erreurs d'adresse plus précisément. Cependant, d'autres erreurs (telles que la lecture de la mémoire non initialisée ou la fuite de ressources non-mémoire telles que les descripteurs de fichiers) ne sont pas prises en charge par cette méthode, et il est impossible d'utiliser de nombreux outils de débogage et ASan en même temps temps.

Certains débogueurs de mémoire

  • GDB | Mac, Linux
  • valgrind (memcheck) | Linux
  • Dr. Memory | les fenêtres

De plus, il est recommandé d'utiliser des outils d'analyse statique pour détecter un comportement indéfini - mais encore une fois, ils ne sont qu'un outil pour vous aider à trouver un comportement indéfini, et ils ne garantissent pas de trouver toutes les occurrences de comportement indéfini.

Cependant, si vous n'avez vraiment pas de chance, l'utilisation d'un débogueur (ou, plus rarement, une simple recompilation avec les informations de débogage) peut suffisamment influencer le code et la mémoire du programme pour que la panne ne se produise plus, un phénomène connu sous le nom de heisenbug .

67
CodeMouse92