2
votes

Comportement étrange dans la destruction de struct

J'essaye d'écrire une structure dans un fichier et de la relire. Le code pour le faire est ici:

(gdb) bt
#0  0x00007f035330595c in ?? ()
#1  0x00000000004014d8 in info::~info() () at binio.cc:7
#2  0x00000000004013c9 in main () at binio.cc:21

Cependant, j'obtiens un étrange défaut de segmentation à la fin.

Le résultat est:

ID =50 Name = adam
Segmentation fault (core dumped)


3 commentaires

Double possible de Comment écrire std :: string dans un fichier?


@AlanBirtles L'autre question lit et écrit dans des processus séparés. Je sais que l'objet string stockera un pointeur vers son contenu dans le tas et lorsqu'il sera lu dans un processus séparé, il plantera. Cependant, dans ma question, la lecture et l'écriture se déroulent dans le même processus.


Son comportement toujours non défini, vous créez 2 objets pointant vers les mêmes données et la solution est la même


3 Réponses :


3
votes

Vous ne pouvez pas sérialiser / désérialiser comme ça. Sur cette ligne ici:

std::cout << std::is_trivially_copyable<std::string>::value << '\n';

Vous écrivez simplement 1: 1 sur une instance de info , qui inclut un std :: string code >. Ce ne sont pas seulement des tableaux de caractères - ils allouent dynamiquement leur stockage sur le tas et le gèrent à l'aide de pointeurs. Ainsi, la chaîne devient invalide si vous l'écrasez comme ça, c'est un comportement indéfini car ses pointeurs ne pointent plus vers un endroit valide.

Au lieu de cela, vous devez enregistrer les caractères réels, pas l'objet chaîne, et créer une nouvelle chaîne avec ce contenu lors du chargement.


En général, vous pouvez faire une copie comme celle-là avec des objets triviaux. Vous pouvez le tester comme ceci:

file2.read((char*)&student, sizeof(student));


9 commentaires

Fait intéressant, cela fonctionne avec clang 7.0 et libc ++ , probablement car le contenu de la chaîne s'insère dans leur tampon SSO.


Vous ne pouvez pas faire cela en général, c'est vrai, mais vous pouvez parfois, en particulier lorsque la struct is_trivially_copyable .


Bonne suggestion, j'ai ajouté un peu à la réponse.


@Blaze Puisque les deux objets partagent le même espace mémoire, l'écriture 1: 1 ne devrait-elle pas fonctionner? student et adam auront le même contenu de mémoire. Ceci est souligné par le fait que nous obtenons les valeurs de contenu correctes. Je ne suis pas en mesure d'obtenir la raison correcte de l'erreur de segmentation.


@eager Vous avez deux chaînes, toutes deux avec leurs propres pointeurs vers le tas, et maintenant vous définissez l'une d'elles pour qu'elle pointe vers la mémoire du tas de l'autre. À la fin du programme, les deux essaient de libérer la même mémoire, ce qui cause le problème. Au moins, c'est une façon dont le comportement non défini pourrait se manifester, cela dépend vraiment de la façon dont le compilateur / configuration spécifique l'a implémenté sous le capot, la taille de la chaîne, et peut-être plus.


@Blaze En fait, j'ai essayé de déterminer l'objet exact où se produit le défaut. Je soupçonnais le problème de la double libération, mais à ma grande surprise, la destruction de l'objet étudiant provoque l'erreur alors que adam n'a pas encore été détruit.


@lubgr "fonctionne" est le pire symptôme possible d'un comportement indéfini, en particulier avec des choses comme les petites chaînes


@eager Essayer de raisonner sur du code dont le comportement n'est pas défini n'est généralement pas significatif. Le compilateur suppose que votre code adhère à la norme lorsqu'il effectue ses transformations, et ce qu'il produit lorsque vous enfreignez ces hypothèses n'a pas besoin d'avoir un rapport avec ce que vous (ou la norme) attendez. Si vous connaissez les composants internes du compilateur (ou regardez uniquement l'assembly), vous pouvez trouver «l'erreur», mais le débogueur peut simplement vous donner des informations incorrectes.


@lubgr Le problème avec gcc est en fait aussi en SSO (non pas que cela éviterait un double free sans SSO non plus). gcc effectue simplement son suivi SSO différemment. Voyez ma réponse.



0
votes

En bref et en pensant conceptuellement, lorsque adam.name = "adam"; est terminé, la mémoire appropriée est allouée en interne pour adam.name .

Lorsque file2.read ((char *) & student, sizeof (student)); est terminé, vous écrivez à l'emplacement de mémoire, c'est-à-dire à l'adresse & student qui n'est pas encore alloué correctement pour accueillir les données en cours de lecture. student.adam n'a pas assez de mémoire valide qui lui est allouée. Faire une telle lecture dans l'emplacement de l'objet étudiant provoque en fait une corruption de la mémoire.


0 commentaires

3
votes

Pour ajouter à la réponse acceptée, car le demandeur est encore confus sur "pourquoi se bloque-t-il lors de la suppression du premier objet?":

Regardons le diassembly, car il ne peut pas mentir, même en face d'un programme incorrect qui présente UB (contrairement au débogueur).

https: // godbolt. org / z / pstZu5

(Notez que rsp - notre pointeur de pile - n'est jamais changé en dehors de l'ajustement au début et à la fin de main ).

Voici l'initialisation de adam:

    mov     rdi, QWORD PTR [rsp+56]
    lea     rax, [rsp+72]
    cmp     rdi, rax
    je      .L90
    call    operator delete(void*)

Il semble que [rsp + 16] et [rsp + 24] contiennent la taille et la capacité de la chaîne, tandis que [rsp + 8] contient le pointeur vers le tampon interne. Ce pointeur est configuré pour pointer vers l'objet chaîne lui-même.

Ensuite, adam.name est remplacé par "adam" :

XXX

En raison de l'optimisation de la petite chaîne, le pointeur du tampon à [rsp + 8] pointe probablement toujours vers le même endroit ( rsp + 24 ) pour indiquer à la chaîne que nous avons une petite mémoire tampon et aucune allocation de mémoire (c'est à mon avis clair).

Plus tard, nous initialisons student de la même manière:

    lea     rax, [rsp+72]
    // ...
    mov     QWORD PTR [rsp+64], 0
    // ...
    mov     QWORD PTR [rsp+56], rax
    mov     BYTE PTR [rsp+72], 0

Notez comment le pointeur de tampon de étudiant pointe vers étudiant pour indiquer un petit tampon .

Maintenant, vous remplacez brutalement les éléments internes de student par ceux de adam . Et soudain, le pointeur de tampon de student ne pointe plus vers l'endroit attendu. Est-ce un problème?

   call    std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_M_replace(unsigned long, unsigned long, char const*, unsigned long)

Ouais! Si le tampon interne de étudiant pointe ailleurs que l'endroit où nous l'avons initialement défini ( rsp + 72 ), il supprimera ce pointeur. À ce stade, nous ne savons pas exactement où pointe le pointeur de tampon de adam (que vous avez copié dans student ), mais ce n'est certainement pas le bon endroit. Comme expliqué ci-dessus, "adam" est probablement toujours couvert par l'optimisation de petites chaînes, donc le pointeur de tampon de adam était probablement exactement au même endroit qu'avant: rsp +24 . Comme nous l'avons copié dans student et que c'est différent de rsp + 72 , nous appelons delete (rsp + 24) - qui est au milieu de notre propre pile. L'environnement ne trouve pas cela très drôle et vous obtenez une erreur de segmentation juste là, dans la première désallocation (la seconde ne supprimer rien parce que le monde irait toujours bien là-bas - adam n'a pas été blessé par vous).


Conclusion: n'essayez pas de surclasser le compilateur ("il ne peut pas segfault car il sera sur le même tas! "). Tu vas perdre. Suivez les règles de la langue et personne ne sera blessé. ;)

Note latérale: Cette conception dans gcc pourrait même être intentionnelle. Je pense qu'ils pourraient tout aussi facilement stocker un nullptr au lieu de pointer vers l'objet string pour désigner un petit tampon de chaîne. Mais dans ce cas, vous ne vous sépareriez pas de cette faute professionnelle.


1 commentaires

Cela répond très bien à ma question. Merci beaucoup :)