Journal Compilateur trop intelligent

Posté par  (site web personnel) . Licence CC By‑SA.
Étiquettes :
30
1
nov.
2017

Une belle illustration des decisions radicales que peut prendre l'optimiseur de clang quand il rencontre des comportements indéfinis :

exemple en c++ sur https://gcc.godbolt.org

L'explication se trouve ici , je ne spoile pas pour ceux qui veulent trouver tout seuls comment le compilateur a choisi d'effacer le disque dur.

  • # Existe-t-il des compilateurs C/C++ qui donnent une sémantique à tous les programmes ?

    Posté par  (site web personnel) . Évalué à 2.

    Je veux dire, est-ce qu'il y a eu des initiatives pour faire un compilateur qui donne une sémantique à tous ces comportements indéfinis, quitte à sacrifier un peu en performance, avec crash assuré quand il ne peut pas faire mieux ? Parce que même CompCert (prouvé en Coq) pour C ne donne pas de garanties s'il y a des comportements indéfinis pour lesquels la sémantique bloque (même si le compilo essaie en pratique d'éviter d'être trop intelligent, j'ai l'impression, mais le théorème ne dit rien sur ça).

    • [^] # Re: Existe-t-il des compilateurs C/C++ qui donnent une sémantique à tous les programmes ?

      Posté par  (site web personnel) . Évalué à 2.

      avec crash assuré quand il ne peut pas faire mieux ?

      tu ne chercherais pas GCC Undefined Behavior Sanitizer – ubsan?

      • [^] # Re: Existe-t-il des compilateurs C/C++ qui donnent une sémantique à tous les programmes ?

        Posté par  . Évalué à 4.

        La notion de il peut pas faire mieux est pas vraiment celle là.

        Par exemple,

        uint32_t rol_attempt(uint32_t a, int x)
        {
            return (a << x) | (a >> (32-x));
        }

        au plan du matos il n'y a pas d'archi, probablement pas du tout, en tout cas aucune en dehors d'un truc vraiment exotique, pour laquelle le code précédant aurait des raisons de ne pas produire un rol pour x entre 0 et 32 inclus.

        Pourtant, je crois bien que les normes du C/C++ rendent ce code undefined pour x == 0 ou x == 32.
        Avec un sanitizer d'UB il n'y a pas d'autre choix que de crasher. Alors que ce que le programmeur voulait clairement, c'est un rol 0 => return a;

        Et malheureusement vu la politique irréfléchie de certains dev de compilo, ça pourrait bien être le "bon" choix dorénavant car je crois pas que y en ait beaucoup qui garantissent encore qu'ils vont produire un rol à partir de ce code.

        Bref, on a même plus de moyen d'exprimer un rol d'une manière pas trop convolué avec toutes ces conneries. L'argument de "l'exploitation des UB" pour optimiser les perfs des programmes tombe à l'eau—cette approche, poussée à son extrême comme ça a malheureusement été fait, c'est de la fumisterie. De la fumisterie dangereuse.

        • [^] # Re: Existe-t-il des compilateurs C/C++ qui donnent une sémantique à tous les programmes ?

          Posté par  (site web personnel) . Évalué à 5. Dernière modification le 01 novembre 2017 à 13:38.

          Pourtant, je crois bien que les normes du C/C++ rendent ce code undefined pour x == 0 ou x == 32.

          "If the value of the right operand is negative or is greater than or equal to the width of the promoted left operand, the behavior is undefined." si j'ai bien googlé.

          Donc avec x == 0 ça marche.
          Mais en effet, un peu plus le bordel avec x == 32.

          L'argument de "l'exploitation des UB" pour optimiser les perfs des programmes tombe à l'eau

          Dans ton exemple, de ce que j'ai suivi quand je suis tombé dessus c'est que c'est au niveau des CPU Intel (j'ai essayé de faire un un "uint32_t x=1; x << 32" et j'ai bien "1" en résultat) qui optimise en regardant que les premiers bits. Mais si je joue avec les shift SSE/AVX, de tête ça donne "0" donc si on fait du C pour SSE/AVX et que C définit la règle pour >=32, il me semble, si je ne dis pas trop de conneries, que ça ralentira l'un ou l'autre suivant la décision.

          Ca se défend donc de laisser indéfini (le C/C++ est la pour ne pas pourrir la perf si le CPU ne fait pas pareil, ça se défend, si un langage dit que c'est OK et que ça décale quand même donc 0, on se retrouve à devoir tout le temps ralentir le shift en testant la valeur du shift avant de faire la commande au CPU), et je préfère que tu gères de ton côté le problème que d'imposer un ralentissement à tous.

          (encore une fois : si j'ai bien tout compris)

        • [^] # Re: Existe-t-il des compilateurs C/C++ qui donnent une sémantique à tous les programmes ?

          Posté par  (site web personnel) . Évalué à 3.

          Pour un rol portable et efficace, il y a : https://blog.regehr.org/archives/1063 !

          cf https://godbolt.org/g/TQchFT

      • [^] # Re: Existe-t-il des compilateurs C/C++ qui donnent une sémantique à tous les programmes ?

        Posté par  (site web personnel) . Évalué à 3.

        Intéressant, merci pour le lien !

      • [^] # Re: Existe-t-il des compilateurs C/C++ qui donnent une sémantique à tous les programmes ?

        Posté par  . Évalué à 2.

    • [^] # Re: Existe-t-il des compilateurs C/C++ qui donnent une sémantique à tous les programmes ?

      Posté par  . Évalué à 3.

      Je suis même pas sûr que c'est possible de tout définir, mais probablement une partie non négligeable ça devrait l'être. Ceci étant dit vu que l'existence d'un tel compilateur moins punitif est hypothétique, il faut basculer vers d'autres mitigation immédiatement disponibles: en détecter le plus possible à la compile (certaines fois des flags -W existent déjà pour ça), définir une sémantique quand ça semble pertinent et que le compilo fourni un flag (malheureusement -fno-delete-null-pointer-checks inhibe Wnull-dereference, et ce dernier ne détecte évidemment pas tout lorsqu'il fonctionne), et enfin faute de mieux pour les cas que tout cela ne couvre pas: instrumenter au runtime et tester.

      Ou alors passer à des langages moins débiles.

      • [^] # [HS] Pourquoi ce besoin d'insulter?

        Posté par  (site web personnel) . Évalué à -4. Dernière modification le 01 novembre 2017 à 13:03.

        débiles

        Sans vouloir préjuger de ton niveau d'intelligence à dire que c'est un langage débile, je pense que si c'est indéfini, ce n'est pas pour le plaisir de faire chier les gens, mais qu'il y a une bonne raison derrière (les personnes qui pondent les specs du langages étant loin d'être des débiles).
        On peut te sortir 36 trucs "débiles" pour chaque langage que tu choisiras, tu vas tourner en boucle à passer à des "langages moins débiles" à chaque fois qu'un langage ne fait pas ce que tu attends de lui.

        • [^] # Re: [HS] Pourquoi ce besoin d'insulter?

          Posté par  . Évalué à 5. Dernière modification le 01 novembre 2017 à 13:13.

          Je parle de ce qu'est devenu le langage en pratique sur certains aspects, pas des gens qui le normalise.

          Je pourrais par contre parler de certains de ceux qui l'implémente, en n'utilisant pas le même mot (sauf si je dérape lorsque je suis trop énervé), mais disons en doutant de leur capacité a ne pas mettre en danger le public de par leur approche inconsidérée sur de l'infrastructure aussi critique en utilisant des approches aussi dangereuses. Et sans doutes que les qualificatifs retenus seront au final plus durs que "débile", car on pourrait arguer que ce terme les aurait un peu déresponsabilisés.

          Si encore ils étaient capable de pondre des bases de code irréprochables selon leurs propres exigences, je dirais pas, mais lance un compilo buildé avec les sanitizeurs activés, et tu comprendra la folie dans laquelle ils vivent.

          Et de par cela le danger que l'on court en 2017 en continuant à utiliser des langages qui sont devenus en pratique, sur certains aspects critiques, débiles.

          • [^] # Re: [HS] Pourquoi ce besoin d'insulter?

            Posté par  (site web personnel) . Évalué à 4.

            des langages qui sont devenus en pratique, sur certains aspects critiques, débiles.

            Mais est-ce le langage en soi, ou l'implémentation ? Pour prendre un exemple, les dépassements d'entiers signés, comportement indéfini, à l'origine c'était parce qu'il y avait, suivant les architectures, quelque chose comme trois façons d'implémenter ça. Aujourd'hui il me semble que tous font la même chose, donc le comportement serait facile à définir, mais même en supposant qu'il y a trois ou plus possibilités, entre « tout peut arriver » et « un comportement parmi trois possibles peut arriver » il y aurait une différence. Mais, en pratique, les compilateurs se permettent d'interpréter ce comportement indéfini pour déduire des choses comme que x + 1 > x est vrai, qui sont plutôt des dérives d'implémentation optimisante que des choses auxquelles auraient pensé comme normales les gens qui écrivaient le standard à l'époque, j'ai l'impression.

        • [^] # Re: [HS] Pourquoi ce besoin d'insulter?

          Posté par  (site web personnel) . Évalué à 10.

          Guillaume Knispel:
          Ou alors passer à des langages moins débiles.

          Zenitram:
          les personnes qui pondent les specs du langages étant loin d'être des débiles

          Tu fais chier Zenitram, je peux pas lire tranquilou un fil de discussion sur un sujet passionnant concernant les compilateurs sans me farcir des horreurs comme ça. Nan mais ho, ce glissement de langage à personne, il est quand même vachement grossier là, non ? Et puis, pourquoi être susceptible à la place des autres et en faire une affaire personnelle pour les autres ?

          Oh et puis cet homme de paille sans aucune dignité :

          ce n'est pas pour le plaisir de faire chier les gens

          Et cet homme de paille il est au service de quel argumentaire ?

          je pense que si c'est indéfini

          Ah ok, donc en fait t’est juste en train de mettre des conneries dans la bouche de Guillaume Knispel pour te faire passer pour quelqu’un qui pense. Ouah, c’est beaucoup d’irrespect et d’indécence pour pas grand chose !

          Ah au fait, « débile » signifie « faible ».

          Ton argumentaire est débile.

          ce commentaire est sous licence cc by 4 et précédentes

      • [^] # Re: Existe-t-il des compilateurs C/C++ qui donnent une sémantique à tous les programmes ?

        Posté par  (site web personnel) . Évalué à 4.

        Ou alors passer à des langages moins débiles.

        Étant donné que pour une raison ou une autre (historique ou non), il y a beaucoup de programmes écrits en C/C++ qui n'ont pas besoin de performances extrêmes, mais qui par contre bénéficierait d'un comportement plus sûr, un compilateur qui élimine les comportements indéfinis, quitte à être moins performant, c'est intéressant : réécrire tous ces logiciels (si tant est que ce soit toujours possible) est peut-être plus laborieux qu'adapter un compilateur pour ajouter des checks au runtime et éviter les optimisations qui utilisent les comportements indéfinis.

        • [^] # Re: Existe-t-il des compilateurs C/C++ qui donnent une sémantique à tous les programmes ?

          Posté par  (site web personnel, Mastodon) . Évalué à 5.

          Il y a déjà eu quelques corrections dans C99, par exemple, sur le comportement de l'opérateur % avec des opérandes négatifs (c'est juste un exemple, il y en a probablement d'autres dans C99 et C11).

          Mais le C reste un langage bas niveau et qui tient compte des possibilités de très nombreuses architectures, notament des trucs franchement euh… "créatifs" pour certains systèmes embarqués: bytes qui ne font pas 8 bits (avec CHAR_BITS), mémoire adressée par segment:offset comme sur les 386, architectures ou l'espace mémoire est découpé en une partie pour le code et l'autre pour les données (AVR8, par exemple). Le tout en préférant toujours l'approche qui donnera les meilleures performances quitte à avoir un comportement indéfini, ou au mieux, défini par l'implémentation (là au moins on sait ce qu'il se passe pour un compilateur donné).

          Je ne pense pas que le langage se prête bien à l'écriture de code sur et sans comportements indéfinis. Donc oui, à mon avis, il serait mieux de changer de langage, même si cela demandera beaucoup d'efforts.

    • [^] # Re: Existe-t-il des compilateurs C/C++ qui donnent une sémantique à tous les programmes ?

      Posté par  . Évalué à 5.

      Le problème c'est que tu ne sacrifie pas "un peu" mais "énormément" en perf. Par exemple, un simple

      for (int i = 0; i < my_array.size(); i++) {
          something(my_array[i]);
      }

      Deviendrait:

      for (int i = 0; i < my_array.size(); i++) {
          assert(i > 0 && i < my_array.size());
          something(my_array[i]);
      }

      Ce qui est totalement inutile et redondant. La raison pour laquelle on utilise systématiquement operator[] en C++ et pas la méthode at(), c'est justement pour profiter du gain de vitesse d'un UB. Bien sur, dans cet exemple, un analyseur statique pourrais arriver à garantir que l'utilisation de operator[] ne peux pas lever de UB, mais dans un cas plus complexe, ce n'est pas forcément garantit.

      bépo powered

      • [^] # Re: Existe-t-il des compilateurs C/C++ qui donnent une sémantique à tous les programmes ?

        Posté par  (site web personnel) . Évalué à 2.

        Le problème c'est que tu ne sacrifie pas "un peu" mais "énormément" en perf.

        C'est peut-être le cas parfois, mais je doute que l'exemple que tu donnes (les bound-checks pour les tableaux), on soit dans l'« énormément » pour la plupart des logiciels : dans les trente dernières années les prédicteurs de branchement ont quand même fait beaucoup de progrès dans les CPU, et là c'est un cas particulièrement bien traité normalement. Et même sans ça, c'est significatif seulement si le something se fait très rapidement, donc calcul scientifique ou analogue probablement. Donc je demande un bench :) Idéalement il faudrait un « on a rajouté des bound-checks dans tel programme C, et maintenant c'est XXX% fois plus lent/gourmand en espace », avec la condition que le programme soit pas du calcul scientifique ou quelque chose de similaire.

        Par contre, les bounds-check risquent de compliquer l'analyse du programme et de gêner significativement d'autres optimisations.

  • # Y'a bien mieux (sans perte de données)

    Posté par  (site web personnel) . Évalué à 4.

    C'est rien comme optim comparé à d'autres choses que les compilateurs font:

    https://youtu.be/bSkpMdDe4g4

    Spoiler: remplacer une boucle sommant des entiers consécutifs par la formule adequat, détecter une multiplication (même déguisée) et l'optimiser, remplacer une division par une multiplication …

  • # Utilisateur trop bête

    Posté par  (Mastodon) . Évalué à 4.

    Ce n'est pas le compilateur qui est trop intelligent, c'est l'utilisateur qui est trop bête. Le compilateur ne fait que ce qu'on lui dit de faire et il exploite notamment tous les comportements indéfinis. Ici, il prend la seule décision rationnelle en fonction de ce qu'il sait. Le fait que la fonction en question efface le disque est un choix de l'utilisateur (ce n'est pas le compilateur qui a écrit cette fonction).

    • [^] # Re: Utilisateur trop bête

      Posté par  . Évalué à 10.

      Ce n'est pas le compilateur qui est trop intelligent, c'est l'utilisateur qui est trop bête.

      Ou bien tout simplement c'est la spécification du langage qui laisse une place trop importante aux comportements indéfinis.

      Ici, il prend la seule décision rationnelle en fonction de ce qu'il sait

      Ça veut dire quoi décision rationnelle ? Par construction un comportement indéfini c'est … Indéfini. Si le compilateur avait décidé de remplacer l'appel par un bout de code qui affiche "Bonjour" ça aurait été tout autant valide vis-à-vis de la spécification.

      ce n'est pas le compilateur qui a écrit cette fonction

      Mais c'est le compilateur qui décide de ré-écrire le code source. Il se trouve qu'il ré-écrit un code qui n'a pas de sémantique en un code qui en a une, et c'est bien ici que se trouve la "surprise". On peut dire que c'est la faute de la personne qui programme, mais si on évite tous les comportements indéfinis en C/C++ … on est mal parti. Le reproche est bien sur le langage: à tout vouloir déléguer à la discrétion du compilateur, un programme qui devrait (selon moi) ne pas compiler se retrouve à avoir une sémantique éloignée de celle imaginée par l'utilisateur.

      Pour conclure, il est assez facile de se retrouver dans cette situation: il suffit qu'au cours d'une modification on supprime sans y faire attention une définition préalable de Do…. Normalement c'est pour ce genre d'erreurs qu'un système de type et un compilateur en général sont utiles.

    • [^] # Re: Utilisateur trop bête

      Posté par  . Évalué à 4.

      Attention, bientôt tu vas exiger un permis de programmer comme d’autres veulent un permis d’utiliser un ordinateur. Le soucis c’est qu’on risquerait de se retrouver facilement avec une pénurie de programmeur avec ce niveau d’exigence.

      • [^] # Re: Utilisateur trop bête

        Posté par  . Évalué à 4.

        Vaut-il mieux une pénurie de bons programmeurs ou une pénurie de programmeurs?

        • [^] # Re: Utilisateur trop bête

          Posté par  . Évalué à 1.

          Pour que les comportements indéfinis du C/C++ ne soient pas un problèmes, il ne faut pas apprendre la spec par cœur. Suffit de programmer en utilisant d’autres langages.

          Et figure toi que ça n’implique pas d’âtre un mauvais programmeur ;)

    • [^] # Re: Utilisateur trop bête

      Posté par  (site web personnel) . Évalué à 10. Dernière modification le 01 novembre 2017 à 18:56.

      Non la décision n’est pas rationnelle, la fonction Do n’est pas définie, elle est seulement déclarée. Si le compilateur ne sait pas voir ça, l’utilisateur devrait au moins s’attendre à une erreur d’exécution. Le fait que la fonction en exemple efface le disque n’est qu’une aspect contingent du problème. Puisqu’il fallait montrer qu’on pouvait exécuter du code a priori mort, l’utilisateur a choisi cet exemple mais aurait pu choisir d’imprimer “hello world” et l’exemple aurait été aussi efficace mais moins effrayant.

      J’imagine que ce type de comportement peut être employé pour rebrancher l’exécution de manière discrète vers d’autres parties du code dans le but d’introduire des fonctionnalité cachées ou d’en faire ignorer d’autres. Ce n’est pas le compilateur qui a écrit cette fonction mais c’est le compilateur qui fait en sorte qu’on puisse exécuter du code mort. Peu importe ce code mort, c’est un exemple.

      Le compilateur ne fait que ce qu'on lui dit de faire et il exploite notamment tous les comportements indéfinis.

      Si c’est indéfini il n’y a rien à exploiter. Le code est inaccessible.

      On pourrait imaginer qu’une fonction définie ait pour adresse nullptr tant qu’elle n’est pas déclarée, ce qui permettrait de tuer le programme à l’exécution au moment où nullptr serait appelé comme une fonction.

      ce commentaire est sous licence cc by 4 et précédentes

      • [^] # Re: Utilisateur trop bête

        Posté par  (site web personnel) . Évalué à 3.

        la fonction Do n’est pas définie, elle est seulement déclarée.

        Attention, ce n'est pas une fonction déclarée mais indéfinie, c'est un pointeur de fonction initialisé à null (implicitement, car statique). Si la fonction était déclarée et non-définie, on aurait une erreur au link.

    • [^] # Re: Utilisateur trop bête

      Posté par  (site web personnel) . Évalué à 10.

      Le “code qui efface le disque” est seulement un marqueur, parce qu’on sait par avance qu’une chaîne de ce type sera transmise sans modification jusqu’à la fin du processus de compilation et qu’on peut donc la retrouver dans le code assembleur ou le binaire final pour vérifier qu’elle est toujours là, même sans tout maîtriser. En fait l’exemple est réalisé de telle manière que même quelqu’un qui n’a jamais lu d’assembleur comprenne ce qui se passe. L’auteur aurait pu écrie simplement “je suis encore là”, c’est un marqueur.

      lune, sage, doigt, toussa.

      ce commentaire est sous licence cc by 4 et précédentes

  • # Comportement attendu

    Posté par  . Évalué à 2.

    Clang a tout à fait raison de produire ce code, comme expliqué ici.

    Do est noté static, donc elle ne peut pas être modifiée directement par une autre unité de compilation.
    La seule sémantique correcte pour ce programme est qu'une autre unité de compilation va appeler NeverCalled avant main, c'est donc ce que suppose Clang.

    Le problème est dans le langage, pas dans l'implémentation.

    À la limite, vous pouvez blâmer Clang de supposer que votre programme est correct..

    • [^] # Re: Comportement attendu

      Posté par  . Évalué à 5.

      À la limite, vous pouvez blâmer Clang de supposer que votre programme est correct..

      Oui, c'est tout à fait ma réaction fasse à ce comportement. Ce programme est incorrect, d'ailleurs Clang a dû le modifier pour qu'il fonctionne.

      Une autre manière plus simple (et à mon avis moins risquée) de résoudre la situation aurait été d'initialiser le pointeur "Do" à une valeur invalide, genre zéro, afin de faire explicitement crasher le programme. Mais le mieux reste d'échouer à la compilation, à mon avis.

      • [^] # Re: Comportement attendu

        Posté par  . Évalué à 4.

        Do étant une variable globale, elle est déjà (implicitement) initialisée à zéro. Testez

        static Function Do = nullptr;

        Vous verrez que le comportement est le même.

        D'après moi, il faudrait:
        - Soit indiquer dans son type que ce pointeur ne dois jamais être null, ce qui forcerait l'utilisateur à donner une valeur par défaut. Mais ce n'est pas possible en C++..
        - Soit utiliser un type optionnel (du genre std::optional) qui force l'utilisateur à traiter le cas où le pointeur est vide explicitement

        Sinon, il faudrait déconseiller l'utilisation de pointeurs de fonction en C++. Au lieu de ça, on peut utiliser std::function.

        • [^] # Re: Comportement attendu

          Posté par  . Évalué à 8.

          Je n'avais pas remarqué que Clang faisant la même chose même dans le cas où on initialise "Do" à nullptr ou 0.

          Dans ce dernier cas, je suis encore plus choqué: ce n'est pas un cas indéfini! On demande à appeler une adresse précise, de quel droit Clang modifie-t-il notre code ?

          Si j'écris Do = (Function)0 --> paf, remplacé par EraseAll
          Si j'écris Do = (Function)1 --> hop, il prend la valeur

          Je ne vois aucune justification valable pour ce genre "d'optimisation".

          • [^] # Re: Comportement attendu

            Posté par  . Évalué à 2.

            clang refuse d'appeler l'adresse 0 même avec

            int main() {
              return reinterpret_cast<Function>(0)();
            }
            

            Il y a quelque chose dans le standard qui interdit l'adresse 0 ? Ou c'est la plateforme x86_64 pour laquelle c'est toujours invalide qui permet l'optimisation ?

          • [^] # Re: Comportement attendu

            Posté par  (Mastodon) . Évalué à 3.

            Un pointeur nul a une signification particulière dans les langages C et C++, et en particulier, le fait de le déréférencer est un comportement indéfini, et donc le compilateur n'a absolument aucune obligation de transformer ça en un accès à l'adresse 0, il peut en faire ce qu'il veut.

            • [^] # Re: Comportement attendu

              Posté par  . Évalué à 1.

              Déréférencer un pointeur nul est un comportement indéfini ? Et moi qui croyais que ça voulait simplement dire "accéder à l'adresse zéro"…

              Tu aurais une source ? Parce que j'ai l'impression qu'on est en train de confondre "valeur de pointeur probablement incorrecte" et "comportement indéfini" là…

              • [^] # Re: Comportement attendu

                Posté par  (Mastodon) . Évalué à 3.

                Je n'ai pas de source officielle, mais si tu fais une recherche "C++ null pointer undefined behavior", tu vas trouver des tas de références.

                Maintenant, en pratique, ça va effectivement dans la plupart des cas résulter en un accès à l'adresse 0, et la plupart des OS vont faire en sorte que ça donne une erreur (en ne mappant aucun page à cette adresse), ce qui va donner le SEGFAULT bien connu.

                Mais c'est juste une possibilité, si le compilateur voit une opportunité d'optimisation, il a tout-à-fait le droit de faire ce qu'il veut.

                • [^] # Re: Comportement attendu

                  Posté par  . Évalué à -2.

                  si le compilateur voit une opportunité d'optimisation, il a tout-à-fait le droit de faire ce qu'il veut.

                  Ok, je veux bien qu’il optimise, mais de là à faire « ce qu’il veut. », je ne suis pas d’accord… un compilateur traduit l’intention du programmeur. Quand il optimise, il n’est pas sensé modifié le comportement du programme.

                  Sinon, je te fait un compilateur qui optimise n’importe quoi à mort. En remplaçant le code par :

                  int main()
                  {
                      return 0;
                  }

                  Comme ça, en plus ton programme retourne toujours que tout va bien ;-)

                  • [^] # Re: Comportement attendu

                    Posté par  (site web personnel) . Évalué à 10.

                    Dans les manuels de GCC, c'est spécifié de souvenir qu'à partir de -O2 (depuis peu, avant c'était -O3), ils se réservent le droit de changer la sémantique du code. Ce sont des optimisations destructrices. Sinon à quoi servirait les optimisations non destructrices, suffiraient de les appliquer en permanence, non ?

                    Ok, je veux bien qu’il optimise, mais de là à faire « ce qu’il veut. », je ne suis pas d’accord… un compilateur traduit l’intention du programmeur. Quand il optimise, il n’est pas sensé modifié le comportement du programme.

                    Sauf qu'il est spécifié dans la norme qu'un pointeur nul déférencé était forcément invalide. Donc quelle est la motivation du codeur dans ce cas de figure ? Comment le compilateur peut considérer un cas défini comme invalide comme une intention valide ?

                    Il fait donc ce qui lui paraît valide à partir des éléments à sa disposition.

                    • [^] # Re: Comportement attendu

                      Posté par  . Évalué à 3.

                      Ce sont des optimisations destructrices. Sinon à quoi servirait les optimisations non destructrices, suffiraient de les appliquer en permanence, non ?

                      On peut imaginer que c'est pour le temps de compilation qui deviendrait significativement plus long en effectuant toutes les passes d'optimisation… Enfin, le principe même d'une optimisation c'est transformer un code qui fait quelque chose en un code qui fait la même chose en étant plus rapide. C'est précisément pour cela que des comportements sont laissés indéfinis en C: afin de laisser plus de marge aux compilateurs pour l'optimisation (puisqu'on transforme un truc qui n'a au départ pas de sens toute ré-écriture est valide…). Donc le coup des optimisations "destructrices" je suis surpris … cela peut-il vraiment changer la sémantique d'un code qui n'utilise pas d'UB ?

                      • [^] # Re: Comportement attendu

                        Posté par  (site web personnel) . Évalué à 3.

                        Je découvre ça moi aussi o_O Pour moi, les -O0, -O2, -O3, etc. correspondaient juste à des usages et compromis différents : debug, compilation plus ou moins rapide, taille du binaire vs performances, optimisations plus ou moins risquées (parce que plus l'optimisation est complexe, plus il y a des risques de bug dans le compilo), mais dans tous les cas il s'agissait de respecter le standard.

                        • [^] # Re: Comportement attendu

                          Posté par  (site web personnel) . Évalué à 5.

                          Tu as des options pour l'optimisation mémoire ou de débogage : -Os ou -Og.

                          Notons que dans l'ensemble, à part les ajouts de GCC/LLVM au langage C de base, les compilateurs respectent la norme du langage C. Mais oui, à des fins d'optimisations ils exploitent largement les comportements indéfinis ou invalides ce qui est normal : ces cas étant déclaré non valides, le compilateur est libre d'en faire ce qu'il veut pour aboutir à un programme qui finit sur quelque chose qui a du sens. C'est standard. Ces cas indéfinis sont prévus pour cela aussi.

                          C'est pourquoi, en C ou C++, si on veut de la vraie portabilité, il faut s'assurer que son programme fonctionne sur des OS, architectures matérielles et généré avec des compilateurs différents pour s'assurer que le bon fonctionnement ne dépend pas de l'interprétation d'un comportement indéfini.

                          Et pour ceux qui reprochent au C et C++ ce manque de définition, il ne faut pas oublier qu'à l'époque l'univers informatique était bien plus diversifiée que cela (et donc il fallait s'assurer que le C puisse tourner partout facilement) et que cela simplifie grandement le port ou l'écriture d'un compilateur. Le C n'a pas d'équivalent en terme de diversité d'environnements d'exécution, en partie grâce à cela.

                          • [^] # Re: Comportement attendu

                            Posté par  (site web personnel, Mastodon) . Évalué à 4.

                            Même aujourd'hui, il y a toujours une "guerre" de performances entre C et Fortran qui pousse à garder ces possibilités d'optimisation. C'est comme ça que C99 a ajouté le "strict aliasing", les "restrict" sur les pointeurs, et quelques autres trucs qui permettaient à Fortran d'aller plus vite que le C à l'époque.

                        • [^] # Re: Comportement attendu

                          Posté par  . Évalué à 3.

                          Tu as des options de gcc, comme Ofast, qui sont explicitement "dangereuses" (modifications éventuelles de la sémantique d'un code valide).

                          • [^] # Re: Comportement attendu

                            Posté par  (site web personnel) . Évalué à 4.

                            Ok, cet -Ofast fait un peu peur, mais le message précédent de Renault laissait sous-entendre ceci pour -O2 et -O3, alors que j'aurais pensé que ceux-ci, contrairement à -Ofast, profitent uniquement des comportements indéfinis (de manière plus ou moins agressive et risquée), mais respectent la sémantique du moment qu'elle est définie au sens du standard (le message de Renault ci-dessus me laisse penser que c'est ce qu'il voulait dire). Quand elle n'est pas définie, la notion de ne pas la respecter n'est pas vraiment définie, si je puis me permettre :)

                        • [^] # Re: Comportement attendu

                          Posté par  . Évalué à 1.

                          Mais le standard est respecté, justement, c'est juste que les optimisations les plus agressives exploitent les trous laissés parfois volontairement par les standards pour essayer (résultat non garantit) d'aller au plus vite pour un même résultat conforme au standard.

                          Enfin, c'est sûr, si on compare à des langages qui sont basés sur des implémentations de référence, forcément ça peut surprendre. Mais je ne crois pas que ces langages soient ou souhaitent être réputés pour leurs performances.

                          • [^] # Re: Comportement attendu

                            Posté par  . Évalué à 2.

                            trous laissés parfois volontairement par les standards pour essayer (résultat non garantit) d'aller au plus vite pour un même résultat conforme au standard.

                            Si le programme n'a pas de sens … quelles garanties voudrais-tu avoir ? L'incompréhension venait du fait que le message de Renault pouvait être compris comme : « à partir de -O2 le compilateur se réservait le droit de modifier la sémantique d'un programme [qui possède une sémantique] ». En particulier cela se serait appliqué à tout programme sans UB. Il n'est pas encore clair au vu des réponses si oui ou non -O2 fait une telle chose. D'après un autre commentaire (d'Arnaudus) -Ofast le fait très clairement.

                            Partons donc du principe que -Ofast s'arroge le droit de modifier la sémantique d'un code valide sans comportement indéfini. Un commentaire d'Anthony Jaguenaud qui s'est fait moinsser assez fortement explique clairement que selon cette spécification le compilateur qui construit juste un int main (int argc, char** argv) { return 0; } est valide et produit le code le plus rapide possible. Un tel compilateur serait la version « ultime » de -Ofast. Conclusion: si on ne met pas d'autres restrictions cela n'a pas beaucoup de sens de parler « d'optimisations destructrices ».

                            Ainsi: soit on ne peut qu'utiliser les comportements indéfinis pour optimiser, soit on modifie le sens de code valide et à ce moment là il faut bien un jour définir proprement quelles garanties on apporte …

                            optimisations les plus agressives exploitent les trous laissés parfois volontairement par les standards

                            Mais ce n'était pas (je pense) ce qui faisait réagir Anasteo et moi-même, on est bien tous d'accord qu'un programme sans sémantique n'a de toute manière … pas de sémantique, donc pas besoin de creuser plus loin. Encore une fois ce qui était a fait réagir Anasteo (« [je pensais que] dans tous les cas il s'agissait de respecter le standard ») c'est bien le fait que malgré tous les trous du standard gcc décide de ne pas en respecter une partie.

                            • [^] # Re: Comportement attendu

                              Posté par  . Évalué à 4. Dernière modification le 04 novembre 2017 à 08:46.

                              L'incompréhension venait du fait que le message de Renault pouvait être compris comme : « à partir de -O2 le compilateur se réservait le droit de modifier la sémantique d'un programme [qui possède une sémantique] ».

                              Tu n'es pas très joueur ! Moi, quand je m'ennuie, je joue à la roulette russe; ou en plus fun, je fais comme cap'tain sports extrêmes : je fais du saut à l'élastique mais sans élastique, avec un élastique on a aucune sensas' ! :-P

                              Pour prendre le manuel d'un compilateur qui ne s'amuse pas à changer la sémantique du code (« Traduttore, traditore » ou « Traducteur, traître » dit son manuel) :

                              There are three noteworthy points in the statement of semantic preservation above:

                              • […]
                              • Second, the compiler is allowed to select one of the possible behaviors of the source program. The C language has some nondeterminism in expression evaluation order; different orders can result in several different observable behaviors. By choosing an evaluation order of its liking, the compiler implements one of these valid observable behaviors.
                              • Third, the compiler is allowed to improve the behavior of the source program. Here, to improve means to convert a run-time error (such as crashing on an integer division by zero) into a more defined behavior. This can happen if the run-time error (e.g. division by zero) was optimized away (e.g. removed because the result of the division is unused). […]

                              et à la question « qu'est-ce qu'un comportement observable ? », il précise :

                              More precisely, we follow the ISO C standards in considering that we can observe:

                              • Whether the program terminates or diverges (runs forever), and if it terminates, whether it terminates normally (by returning from the main function) or on an error (by running into an undefined behavior such as integer division by zero).
                              • All calls to standard library functions that perform input/output, such as printf() or getchar().
                              • All read and write accesses to global variables of volatile types. These variables can correspond to memory-mapped hardware devices, hence any read or write over such a variable is treated as an input/output operation.

                              Je ne connais pas bien la sémantique du C mais, pour l'exemple du journal, il me semble bien que l'optimisation de clang ne respecte pas ces principes.

                              Sapere aude ! Aie le courage de te servir de ton propre entendement. Voilà la devise des Lumières.

                              • [^] # Re: Comportement attendu

                                Posté par  (site web personnel) . Évalué à 2.

                                Je ne connais pas bien la sémantique du C mais, pour l'exemple du journal, il me semble bien que l'optimisation de clang ne respecte pas ces principes.

                                Si l'exemple du journal correspond vraiment à un comportement indéfini, alors le théorème de CompCert ne dit rien à son sujet, a priori :

                                Theorem transf_c_program_preservation:
                                  forall p tp beh,
                                  transf_c_program p = OK tp ->
                                  program_behaves (Asm.semantics tp) beh ->
                                  exists beh', program_behaves (Csem.semantics p) beh' /\ behavior_improves beh' beh.

                                En traduction, ça veut dire que si le code compilé a un certain comportement, alors un des comportements possibles du source correspond à une « amélioration » de ce comportement (ce qui fait écho à ton commentaire). Par contre, s'il y a un comportement indéfini du source, donc sémantique non définie, je pense pas que ça dise quoi que ce soit d'utile (on obtient juste un comportement « go wrong » accompagné d'une trace dès que la sémantique bloque, donc au mieux ça dirait peut-être que jusqu'au moment où ça bloque la sémantique est préservée).

                                • [^] # Re: Comportement attendu

                                  Posté par  . Évalué à 2.

                                  Par contre, s'il y a un comportement indéfini du source, donc sémantique non définie, je pense pas que ça dise quoi que ce soit d'utile (on obtient juste un comportement « go wrong » accompagné d'une trace dès que la sémantique bloque, donc au mieux ça dirait peut-être que jusqu'au moment où ça bloque la sémantique est préservée).

                                  C'est sur ce point que je ne sais pas ce qui est permis par le standard du C. Il y a des exemples de undefined behavior dans le manuel mais pas celle de l'exemple du journal : les exemples sont les overflow sur l'arithmétique signée (qui sont définies dans CompCert par les règles de l'arithmétique modulaire) et les accès hors bornes pour les tableaux qui renvoient une erreur.

                                  D'après le deuxième principe que je cite du manuel « Second, the compiler is allowed to select one of the possible behaviors of the source program », le compilateur peut choisir n'importe quel comportement possible en accord avec la norme en cas de non déterminisme; c'est sur ce point que je n'ai pas bien compris si le choix de clang est acceptable (certains disent que oui, d'autres disent non) et pour moi cela reste encore bien obscur.

                                  Sapere aude ! Aie le courage de te servir de ton propre entendement. Voilà la devise des Lumières.

                                  • [^] # Re: Comportement attendu

                                    Posté par  (site web personnel) . Évalué à 2.

                                    le compilateur peut choisir n'importe quel comportement possible en accord avec la norme en cas de non déterminisme; c'est sur ce point que je n'ai pas bien compris si le choix de clang est acceptable

                                    Je ne suis pas sûr de te suivre : le non déterminisme (plusieurs choix) est une chose différente des comportements indéfinis (sémantique bloquante). Ici je sais pas si c'est vraiment un comportement indéfini ou non, par contre. Mais une des premières choses que fait CompCert, c'est de faire un choix pour tous les non-déterminismes de C, afin de faire en sorte que tous les langages intermédiaires suivants soient déterministes, ce qui simplifie pas mal certaines choses.

                                    Je m'aperçois par contre que j'ai tourné cette phrase à l'envers :

                                    alors un des comportements possibles du source correspond à une « amélioration » de ce comportement

                                    C'est le code compilé qui est une « amélioration » est pas l'inverse, mais j'imagine que tu avais bien compris :)

                  • [^] # Re: Comportement attendu

                    Posté par  (Mastodon) . Évalué à 1.

                    Je parle uniquement en cas de comportement indéfini. Quand c'est défini, le compilateur doit convertir le code de façon à ce résultat corresponde à ce qui est défini, c'est évident, sinon il ne respecte pas le standard.

                    Mais dans le cas dont on parle (déréférencement d'un pointeur nul), le comportement est bien indéfini, et ça laisse donc la liberté au compilateur d'en faire ce qu'il veut. Il aurait effectivement tout aussi bien pu remplacer le tout par le code que tu donnes, ça aurait été autant valable que le choix qui a été fait.

              • [^] # Re: Comportement attendu

                Posté par  . Évalué à 7.

                Oui c’est bien un comportement indéfini.
                Pour la source, il suffit d’aller voir le standard.

                Si on va voir la norme du C99 (AFAIK ça n’a pas changé en C11), ISO/IEC 9899:TC3 section 6.5.3.2/4:

                If an invalid value has been assigned to the pointer, the behavior of the unary * operator is undefined(87).

                La note de bas de page 87 nous dit

                Among the invalid values for dereferencing a pointer by the unary * operator are a null pointer, an address inappropriately aligned for the type of object pointed to, and the address of an object after the end of its lifetime

                Donc déréférencer un pointeur NULL c’est un UB.

                Ça va même plus loin en fait, tu peux faire très peu de choses avec un pointeur NULL (avec les pointeurs invalides de manière générale en fait) : toute opération arithmétique avec un pointeur NULL débouche sur un UB.
                Oui, même faire NULL - NULL c’est un comportement indéfini en C :D

                Point intéressant à noter, le C++ diffère là dessus : il définit le résultat de certaines opérations arithmétiques sur NULL (voir cet article pour les explications du « pourquoi »).

            • [^] # Re: Comportement attendu

              Posté par  . Évalué à 2.

              Mais nullptr et adresse 0 à ne sont ils pas différents ? L'implémentation peut utiliser la valeur qu'elle veut pour nullptr. Si je fais de la programmation bas niveau et que j'ai besoin d’accéder à des adresses particulières, je dois m'assurer que celle-ci n'est pas la valeur choisie pour nullptr ? Dans mes souvenirs de programmation sur ma calculatrice, on trouvait le vecteur d'interruption à cette adresse, et on pouvait le réécrire vu qu'il n'y avait pas de protection mémoire.

              • [^] # Re: Comportement attendu

                Posté par  . Évalué à 3.

                En fait « 0 » est une valeur spéciale du point de vue du compilateur. En effet, « 0 » peut initialiser un nombre mais aussi un pointeur :

                int foo = 0;
                int *bar = 0;
                int (*baz)() = 0;

                Certaines machines ont une valeur pour les pointeurs nuls qui n'est pas forcément 0. Dans un tel cas le compilateur utilisera cette valeur en mémoire pour initialiser le pointeur. Le programmeur quand à lui ne ce préocuppe pas de cette valeur spéciale, tout le travail de conversion de 0 à la valeur réelle utilisée en mémoire par la machine est fait par le compilateur.

                Notons que pour initialiser un pointeur il est aussi possible d'utiliser (void*)0. NULL en pratique peut avoir deux définitions possible :

                #define NULL 0
                #define NULL (void*)0

                Mais la seconde définition est préférable, en effet, le code suivant est invalide si NULL vaut 0, car l'opérateur de conversion %p doit être utilisé avec un pointeur et non un entier :

                printf("%p", NULL); // Erreur si NULL = 0

                0 et (void*)0 sont des valeurs spéciales qui peuvent être utilisées pour initialiser tout types de pointeurs, y compris les pointeurs de fonctions :

                int (*foo)() = 0;
                int (*bar)() = (void*)0;
                int (*baz)() = (void*)(void*)0; // Erreur
    • [^] # Re: Comportement attendu

      Posté par  (site web personnel) . Évalué à 4.

      À la limite, vous pouvez blâmer Clang de supposer que votre programme est correct.

      Mais c'est exactement ça dont il s'agit : le compilateur peut faire comme si il n'y avait aucun comportement indéfini dans le programme, et donc pouvoir optimiser plus facilement au risque d'avoir des mauvaises surprises, ou il peut choisir de faire n'importe quoi d'autre en cas de comportement indéfini et, en particulier, choisir le comportement le plus sûr, le moins suprenant et le plus proche de ce que voulait probablement le programmeur.

      Un certain nombre de comportements indéfinis du C/C++ sont des reliques historiques liées à des problèmes de portabilité vers des architectures qui n'existent plus, ou aussi liées au fait que la technologie des compilateurs en était à ses débuts à l'époque, que les ordis étaient beaucoup plus lents et que donc rendre faciles les optimisations était quelque chose de beaucoup plus important qu'aujourd'hui.

      On peut donc interpréter « indéfini » par : on ne peut pas faire la même chose sur toutes les archis pour ça, donc on a besoin d'un peu de flexibilité, ou bien, on ne sait pas encore écrire telle optimisation de façon efficace avec la contrainte que tel comportement indéfini doit être traité de façon sûre, donc on prend le risque.

  • # Erreur?

    Posté par  (site web personnel) . Évalué à 5.

    Je ne comprends pas pourquoi les développeurs de clang ne lèvent pas tout simplement une erreur pour ces cas là…

    Ça me semble infiniment plus simple à implémenter.

    Le post ci-dessus est une grosse connerie, ne le lisez pas sérieusement.

    • [^] # Re: Erreur?

      Posté par  . Évalué à 4.

      Le problème de la plupart des UB c'est que si on met un warning à chaque fois qu'on en vois un, ton compilateur va inonder ton terminal, rendant tout diagnostic absolument impossible.

      Est ce que tu aimerais que ton compilateur te mette un warning à chaque fois que tu fais une addition entre deux entiers, par ce qu'il peut y avoir un overflow? À chaque fois que tu accède à un élément dans un tableau avec operator[], …

      bépo powered

    • [^] # Re: Erreur?

      Posté par  (Mastodon) . Évalué à 3.

      Je vois plusieurs explications qui peuvent expliquer qu'il n'y ait pas d'avertissement :

      • c'est compliqué à implémenter dans le cas général
      • ce n'est pas vraiment utile en général
      • ça risque d'inonder l'utilisateur de faux positifs dans le cas d'un programme typique

      Le point commun de tout ça : l'exemple donné ici est construit exprès pour démontrer ce comportement, ça ne représente pas un cas général. Il ne faut donc pas jeter trop vite la pierre aux devs de Clang, ils cherchent avant tout à optimiser des programmes réels, infiniment plus complexes que la démo qu'on a ici. Et je suppose que le problème doit être d'une complexité toute autre quand on a des centaines de milliers de lignes de code réparties dans plusieurs dizaines de fichiers.

    • [^] # Re: Erreur?

      Posté par  (site web personnel, Mastodon) . Évalué à 1.

      Je demande à voir quelle serait l'erreur levée. Comme c'est indiqué dans le premier commentaire sur reddit, ce code a un cas d'exécution valide, si NeverCalled est appelé (ce qui est possible, puisque cette fonction n'est pas statique) avant main() (ce qui est possible, par exemple en C++ en initialisant un objet statc/global).

      Le compilateur optimise ce cas valide, et a le droit d'ignorer l'autre cas (celui ou le pointeur est NULL et donc le comportement indéfini). Lui faire faire la même chose que l'autre cas, c'est un choix raisonable.

      Ceux qui trouvent ça inacceptable, il faut changer de langage comme ça a été dit plein de fois au-dessus. Et accepter d'avoir du code un peu moins performant mais beaucoup plus sûr.

      • [^] # Re: Erreur?

        Posté par  (site web personnel) . Évalué à 7.

        Je demande à voir quelle serait l'erreur levée.

        À la compilation, aucune, admettons, puisque tu peux toujours lier par la suite l'unité compilée à une autre qui initialisera normalement la fonction. Mais (en l'absence de lien qui fait ça) choisir "tiens je vais appeler telle fonction possible" est parfaitement ridicule. Ça donne quoi d'ailleurs si je le compile en mode "bibliothèque", puis je le lie à un autre code qui initialise la fonction à une autre implèm, comme le compilo a déjà choisi (suivant des règles invoquées depuis le Vide) bah mon implem je me la carre ?

        Maintenant le compilateur décide de prouver des choses en les invoquant depuis le néant ? Cool, donc si je mets le bon UB en prémisse, je peux lui faire prouver P = NP ? On va se la donner grave.

        Remarque ça ne m'étonne pas tellement de voir ici globalement approuvée cette hérésie. Par contre quand c'est systemd qui a un comportement par défaut parfaitement défini qui fait que lorsqu'un service sans directive User valide est lancé, il l'est en root, là c'est Lennart qui veut la mort de Linux. Mais un compilateur qui décide quelque chose en dépit de quelque théorie saine que ce soit, c'est du C, c'est le langage de Dieu, c'est forcément bon.

        • [^] # Re: Erreur?

          Posté par  . Évalué à 2.

          Ça donne quoi d'ailleurs si je le compile en mode "bibliothèque", puis je le lie à un autre code qui initialise la fonction à une autre implèm, comme le compilo a déjà choisi (suivant des règles invoquées depuis le Vide) bah mon implem je me la carre ?

          Dans ton autre unité de compilation, tu ne peux pas modifier Do, vu qu'elle est marquée static. Sans toucher ce code, Do ne peut valoir que NULL ou EraseAll.

  • # Comportement indéfini ou incorrect ?

    Posté par  . Évalué à 5.

    Je ne vois pas de comportement indéfini dans le code. La variable Do est initialisé à 0 par défaut. Donc lors de l’appel dans le main il y a deux possibilités (par rapport à ce que je connais de la norme) :

    • La plus courante avec un OS, la MMU déclenche une exception et sort avec une erreur (segmentation fault)
    • En barre métal(sans OS) ou en mode noyau, il peut être légitime d’accéder à l’adresse 0, et dans ce cas pas de problème.

    Par contre, l’optimisation de clang me semble cavalière… en O0, ça fait ce qui est attendu. Par contre, dès -O1, il remplace par un jmp à EraseAll. en O[234s] le compilateur inline la fonction et appelle directement system depuis main.

    J’avoue que j’aurais compris une optimisation de se genre dès O3 ou O4, mais avant ça me semble trop.

    Si quelqu’un peut m’indiquer ce qui est indéfini dans la norme qui induit ce comportement, je suis preneur.

    • [^] # Re: Comportement indéfini ou incorrect ?

      Posté par  . Évalué à 2.

      La variable Do est initialisé à 0 par défaut.

      À quel endroit? Je ne vois aucune affectation à Do autre que dans EraseAll moi. Le standard spécifie qu'une variable est toujours initialisée à 0? Je n'en suis pas si sûr… d'ailleurs, je surpris qu'il n'y ait pas le classique warning "value not initialised" ou un truc dans ce goût la.

    • [^] # Re: Comportement indéfini ou incorrect ?

      Posté par  (Mastodon) . Évalué à 2.

      Je ne vois pas de comportement indéfini dans le code.

      J'ai l'impression que tu considères qu'un pointeur nul est un pointeur vers l'adresse 0. Ce n'est pas ce que disent les différents standard C/C++, un pointeur nul est une valeur à part, ça représente une adresse invalide. Quand on essaie de le déréférencer, on a un comportement indéfini.

      En pratique, le compilateur ne va en général pas s'embêter et va effectivement générer un accès à l'adresse 0, mais il n'a aucune obligation de le faire.

      • [^] # Re: Comportement indéfini ou incorrect ?

        Posté par  . Évalué à 2. Dernière modification le 02 novembre 2017 à 14:40.

        Je n’ai pas la norme sous la main, mais dans mon souvenir de la norme C99, il est écrit qui NULL est défini comme (void*)0.

        Les variables non initialisé sont initialisées à zéro. Donc c’est comme si on avait :

        static void (*Do)() = 0;

        Quand à l’adresse 0, on y trouve plein de chose… les vecteurs d’interruption en mode réel 8086 (ça date) ;-) de la flash sur mon projet actuel, un BAR PCI sur un ancien projet…

        Je voudrais juste me coucher moins bête et comprendre, où dans la norme, il est indiqué qu’il y a un comportement indéfini dans ce cas là.

        • [^] # Re: Comportement indéfini ou incorrect ?

          Posté par  (Mastodon) . Évalué à 1.

          Cet article explique assez bien le problème et donne des références vers les sections du standard C99 https://www.viva64.com/en/b/0306/

        • [^] # Re: Comportement indéfini ou incorrect ?

          Posté par  (site web personnel) . Évalué à 2.

          Que je sache, de manière générale, déréférencer le pointeur null est indéfini. C'est que qui permet, par exemple, dans:

          int main(void) {
              int *p;
              *p = 3;
              return 0;
          }

          au compilateur de justifier d'enlever p vu qu'il n'est pas utilisé, ce qui fait qu'au lieu d'avoir un crash ou une écriture à l'adresse nulle (les deux comportements intuitifs que tu mentionnes), il termine et renvoie 0.

          • [^] # Re: Comportement indéfini ou incorrect ?

            Posté par  (site web personnel) . Évalué à 2.

            En plus marrant, il y a :

            int main(void) {
                int *p;
                *p = 3;
                if (*p == 3) {
                   return 0;
                }
                return 1;
            }

            Qui renvoie 0 avec -O2 et crashe avec -O0.

            • [^] # Re: Comportement indéfini ou incorrect ?

              Posté par  . Évalué à 2.

              Oui, mais dans ce cas, tu ne fais pas l’affectation dans une fonction externe comme dans l’exemple du journal… là, le compilateur il sait que le contenue de p est 3… même s’il ne se préoccupe pas de l’endroit en mémoire où doit être stocké ce trois.

              D’autant qu’ici, tu dois avoir un warning il me semble.

              • [^] # Re: Comportement indéfini ou incorrect ?

                Posté par  (site web personnel) . Évalué à 3.

                Oui, il y a un warning (mais du coup ça c'est le compilo, pas le standard), mais je me dit surtout que c'est pas vraiment un pointeur nul, mais plutôt un pointeur non initialisé, en effet.

                • [^] # Re: Comportement indéfini ou incorrect ?

                  Posté par  . Évalué à 3.

                  Effectivement, le pointeur n'est pas initialisé, enfin… pas toujours

                  Pour le programme:

                  #include <iostream>
                  
                  int main(void) {
                      int *p;
                      std::cout << p << "\n" ;
                      return 0;
                  }

                  j'obtiens sans optimisation:

                  g++ -O0 p.cc && ./a.out
                  0x41d7b1

                  et avec optimisation:

                  g++ -O2 p.cc && ./a.out
                  0
                  

                  Et j'ai un mauvais souvenir d'un autre compilo qui faisait l'inverse, initialisation implicite a zero en mode debug, et initialisation "aléatoire" en mode release… c'était surprenant.

                  • [^] # Re: Comportement indéfini ou incorrect ?

                    Posté par  (site web personnel) . Évalué à 2.

                    Et j'ai un mauvais souvenir d'un autre compilo qui faisait l'inverse, initialisation implicite a zero en mode debug, et initialisation "aléatoire" en mode release… c'était surprenant.

                    Bah c'est plutôt habituel comme comportement ça. Les assignations à la valeur nulle par défaut ont un coût donc pour des raisons de perfs le compilateur par défaut va laisser la valeur en mode ça prend la valeur de la case mémoire où il est placé (et tant pis si c'est n'importe quoi). Mais pour le débogue, pour faciliter le travail, tout est initialisé par défaut.Car en mode débogue tout le monde s'en fout de la course à la performance.

                    Cela explique pourquoi certains bogues disparaissent en mode débogue.

                  • [^] # Re: Comportement indéfini ou incorrect ?

                    Posté par  (site web personnel) . Évalué à 3. Dernière modification le 02 novembre 2017 à 21:08.

                    Effectivement, le pointeur n'est pas initialisé, enfin… pas toujours

                    En fait, le pointeur n'est probablement pas initialisé, quand il existe.
                    L'optimisation a probablement réduit l'instruction à:

                    #include <iostream>
                    
                    int main(void) {
                    
                        std::cout << 0 << "\n" ;
                        return 0;
                    }

                    Par non défini, il faut comprendre «non défini par la norme». Le compilateur, lui, va suivre ses propres règles, qui peuvent varier d'une cible à l'autre.

                    On en a déjà causé dans cet autre journal.

      • [^] # Re: Comportement indéfini ou incorrect ?

        Posté par  . Évalué à 2.

        mais il n'a aucune obligation de le faire.

        anéfé. pour la culture http://c-faq.com/null/machexamp.html

Suivre le flux des commentaires

Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.