À propos de la construction de JDK 8 sur Ubuntu, la qualité du code Hotspot et pourquoi tout échoue en C ++

Je voulais dormir aujourd'hui, mais j'ai encore échoué. Un message est apparu dans le Telegram que quelqu'un n'allait pas à Java ... et nous ne nous sommes réveillés qu'après quelques heures, fatigués et heureux.




Qui peut utiliser ce message? Oui, probablement, à n'importe qui, sauf à ceux qui collectionnent également JDK8 ou qui aiment tout simplement lire les horreurs des cauchemars. En général, je vous ai prévenu, fermez l'article d'urgence.

Trois problèmes:


  • Ne va pas ( niveau un )
    Une partie très ennuyeuse à sauter. Seulement nécessaire pour ceux qui veulent restaurer complètement l'histoire des événements;
  • Ne va pas ( niveau deux )
    C'est plus intéressant car il y a quelques erreurs typiques, la nécromancie, la nécrophilie, que BSD est meilleur que GNU / Linux et pourquoi cela vaut la peine de passer à de nouvelles versions de JDK.
  • Même si ça va tomber
    Plus intéressant. Yahuuu, la JVM est tombée dans la croûte, allons-y!

Sous le chat montre une solution détaillée aux problèmes, avec différentes pensées secondaires sur la vie.


Il y aura beaucoup de C ++, il n'y aura pas de code Java du tout. Tout javist à la fin commence à écrire uniquement en C ++ ...


Ne va pas


Celui qui a construit Java au moins une fois sait que cela ressemble à ceci:


hg clone http://hg.openjdk.java.net/jdk8u/jdk8u cd jdk8u sh ./get_source.sh sh ./configure \ --with-debug-level=fastdebug \ --with-target-bits=64 \ --with-native-debug-symbols=internal \ --with-boot-jdk=/home/me/opt/jdk1.8.0_161 make images 

(Tous mes utilisateurs sont simplement appelés «moi», afin que vous puissiez donner la machine virtuelle à n'importe qui à tout moment et ne pas créer de refus en utilisant votre propre nom d'utilisateur)


Le problème, bien sûr, est que cela ne fonctionne pas. Et d'une manière plutôt cynique.



Premier niveau de plongée


Essayons d'exécuter:


 /home/me/git/jdk8u/hotspot/src/os/linux/vm/os_linux.inline.hpp:127:18: warning: 'int readdir_r(DIR*, dirent*, dirent**)' is deprecated [-Wdeprecated-declarations] if((status = ::readdir_r(dirp, dbuf, &p)) != 0) { ^~~~~~~~~ 

Tout d'abord, pour que vous compreniez, j'ai installé ceci:


 $ g++ --version g++ (Ubuntu 7.3.0-16ubuntu3) 7.3.0 Copyright (C) 2017 Free Software Foundation, Inc. 

Le compilateur n'est pas la première fraîcheur, pas la 8.2, mais celle-ci devrait également fonctionner.


Les développeurs C ++ aiment tester les logiciels uniquement sur la version du compilateur qu'ils ont installée. Habituellement, le désir de tester sur différentes plates-formes se termine quelque part dans la région de la différence entre gcc et clang dans un sens général. Par conséquent, il est tout à fait normal d'exécuter d'abord -Werror ("traiter les avertissements comme des erreurs"), puis d'écrire du code qui, dans toutes les autres versions, sera considéré comme des vorings.


Il s'agit d'un problème connu, et il est clair comment le résoudre. Vous devez définir votre variable d'environnement CXX_FLAGS, dans laquelle définir le niveau d'erreur correct.


 export CXX_FLAGS=-Wno-error=deprecated-declarations -Wno-error-deprecated-declarations 

Et puis nous voyons le merveilleux:


 Ignoring CXXFLAGS found in environment. Use --with-extra-cxxflags 

Ok, construisez le système, tout ce que vous voulez! Nous remplaçons configure par celui-ci:


 hg clone http://hg.openjdk.java.net/jdk8u/jdk8u cd jdk8u sh ./configure \ --with-extra-cflags='-Wno-cpp -Wno-error=deprecated-declarations' \ --with-extra-cxxflags='-Wno-cpp -Wno-error=deprecated-declarations' \ --with-debug-level=fastdebug \ --with-target-bits=64 \ --with-native-debug-symbols=internal \ --with-boot-jdk=/home/me/opt/jdk1.8.0_161 make images 

Et l'erreur reste la même!
On passe à l'artillerie lourde: le code source.


 grep -rl "Werror" . 

Une énorme quantité de chapeau généré automatiquement tombe, parmi lesquels il y a des aperçus de fichiers significatifs:


 ./common/autoconf/flags.m4 ./hotspot/make/bsd/makefiles/gcc.make ./hotspot/make/solaris/makefiles/gcc.make ./hotspot/make/aix/makefiles/xlc.make 

Dans flags.m4 nous trouvons facilement le message précédent sur «Ignorer CXXFLAGS» et le drapeau codé en dur plus invétéré CCXX_FLGS (oui, deux lettres C), qui agit immédiatement au lieu de CFLAGS et au lieu de XX_FLAGS . Idéalement! Deux faits sont intéressants:


  • Cet indicateur n'est pas transmis via les paramètres de configuration;
  • Dans la valeur par défaut sont significatifs et étrangement similaires à ces paramètres:

  # Setup compiler/platform specific flags to CFLAGS_JDK, # CXXFLAGS_JDK and CCXXFLAGS_JDK (common to C and CXX?) if test "x$TOOLCHAIN_TYPE" = xgcc; then # these options are used for both C and C++ compiles CCXXFLAGS_JDK="$CCXXFLAGS $CCXXFLAGS_JDK -Wall -Wno-parentheses -Wextra -Wno-unused -Wno-unused-parameter -Wformat=2 \ -pipe -D_GNU_SOURCE -D_REENTRANT -D_LARGEFILE64_SOURCE" 

Cette question semble très agréable dans les commentaires - mais quoi, les drapeaux sont-ils communs? Non?


Nous ne jouerons pas la démocratie et nous y -w autoritaire ("ne montrez aucune erreur"):


  CCXXFLAGS_JDK="$CCXXFLAGS $CCXXFLAGS_JDK -w -ffreestanding -fno-builtin -Wno-parentheses -Wno-unused -Wno-unused-parameter -Wformat=2 \ 

Et - bravo! - la première erreur que nous avons traversée. Elle ne fait plus de rapport, et en général tout va bien. Il semblerait.



Deuxième niveau de plongée


Mais maintenant, il tombe dans une pile d'autres nouveaux endroits!


Il s'avère que notre -w fonctionne, mais n'est pas transmis à toutes les parties de l'assemblage. Nous lisons attentivement les makefiles et ne comprenons pas du tout comment ce paramètre peut être transmis. Vous l'avez vraiment oublié?


Connaissant la bonne question de Google («pourquoi cxx n'atteint-il pas la version?!»), Nous arrivons rapidement à la page de bogue avec le dicton «configurer --with-extra-cxxflags n'affecte pas le hotspot» ( JDK-8156967 ).


Qui promet d'être corrigé dans JDK 12. Peut-être. Magnifique - le paramètre de construction le plus important n'est pas utilisé dans l'assemblage!


La première idée est, eh bien, remontons nos manches et corrigeons les erreurs!


Erreur 1.xn [12]


 dependencies.cpp: In function 'static void Dependencies::write_dependency_to(xmlStream*, Dependencies::DepType, GrowableArray<Dependencies::DepArgument>*, Klass*)': dependencies.cpp:498:6: error: '%d' directive writing between 1 and 10 bytes into a region of size 9 [-Werror=format-overflow=] void Dependencies::write_dependency_to(xmlStream* xtty, ^~~~~~~~~~~~ dependencies.cpp:498:6: note: directive argument in the range [0, 2147483647] 

Eh bien, nous devons probablement agrandir la région. Cent livres, quelqu'un a calculé le tampon en cliquant sur le "I'm Lucky!" dans google.


Mais comment comprendriez-vous combien vous avez besoin? Il y a un autre type de raffinement ci-dessous:


 stdio2.h:34:43: note: '__builtin___sprintf_chk' output between 3 and 12 bytes into a destination of size 10 __bos (__s), __fmt, __va_arg_pack ()); 

La position 12 ressemble à quelque chose de valable, avec laquelle vous pouvez maintenant pénétrer dans la source avec les pieds sales.


Nous montons dans dependencies.cpp et observons l'image suivante:


 DepArgument arg = args->at(j); if (j == 1) { if (arg.is_oop()) { xtty->object("x", arg.oop_value()); } else { xtty->object("x", arg.metadata_value()); } } else { char xn[10]; sprintf(xn, "x%d", j); if (arg.is_oop()) { xtty->object(xn, arg.oop_value()); } else { xtty->object(xn, arg.metadata_value()); } } 

Faites attention à la ligne problématique:


 char xn[10]; sprintf(xn, "x%d", j); 

Nous changeons 10 à 12, remontons et ... le montage est parti!


Mais suis-je le seul à être aussi intelligent et à corriger le bug de tous les temps? Pas de doute, nous conduisons à nouveau notre mégapatch dans Google: char xn[12];


Et nous voyons ... oui, c'est vrai. Le bogue JDK-8184309 , interdit par Vladimir Ivanov, contient exactement le même correctif.


Mais l'essentiel est qu'il n'est corrigé que dans JDK 10 et que nifiga n'est pas rétroporté sur jdk8u. C'est la question de savoir pourquoi de nouvelles versions de Java sont nécessaires.


Erreur 2. strcmp


 fprofiler.cpp: In member function 'void ThreadProfiler::vm_update(TickPosition)': /home/me/git/jdk8ut/hotspot/src/share/vm/runtime/fprofiler.cpp:638:56: error: argument 1 null where non-null expected [-Werror=nonnull] bool vm_match(const char* name) const { return strcmp(name, _name) == 0; } 

Enseigné par une expérience amère précédente, nous allons immédiatement voir ce qui se trouve dans cet endroit dans JDK 11. Et ... ce fichier n'est pas là. La structure du répertoire a également subi une refactorisation.


Mais vous ne pouvez pas simplement vous éloigner de nous!


Tout javiste est un petit nécromancien dans son âme, et peut-être même un nécrophile. Par conséquent, il y aura maintenant NECROMANCE EN ACTION!


Vous devez d'abord faire appel à l'âme des morts et savoir quand il est mort:


 $ hg log --template "File(s) deleted in rev {rev}: {file_dels % '\n {file}'}\n\n" -r 'removes("**/fprofiler.cpp")' File(s) deleted in rev 47106: hotspot/src/share/vm/runtime/fprofiler.cpp hotspot/src/share/vm/runtime/fprofiler.hpp hotspot/test/runtime/MinimalVM/Xprof.java 

Vous devez maintenant découvrir la cause de sa mort:


 hg log -r 47106 changeset: 47106:bed18a111b90 parent: 47104:6bdc0c9c44af user: gziemski date: Thu Aug 31 20:26:53 2017 -0500 summary: 8173715: Remove FlatProfiler 

Donc, nous avons un tueur: gziemski . Voyons pourquoi il a cloué ce dossier malheureux.


Pour ce faire, accédez à fat dans le ticket spécifié dans le résumé du commit. C'est JDK-8173715 :


Supprimer FlatProfiler:
Nous supposons que cette technologie n'est plus utilisée et est une source d'analyse racine pour le GC.


Pour shih bis. En fait, nous sommes maintenant invités à réparer le cadavre juste pour que la construction continue. Ce qui s'est tellement décomposé que même nos collègues nécromanciens d'OpenJDK l'ont abandonné.


Ressuscitons le mort et essayons de lui demander ce dont il s'est souvenu en dernier. Il était déjà mort dans la révision 47106, ce qui signifie qu'il y en a une de moins dans la révision - c'est «une seconde avant»:


 hg cat "~/git/jdk11/hotspot/src/share/vm/runtime/fprofiler.cpp" -r 47105 > ~/tmp/fprofiler_new.cpp cp ~/git/jdk8u/hotspot/src/share/vm/runtime/fprofiler.cpp ~/tmp/fprofiler_old.cpp cd ~/tmp diff fprofiler_old.cpp fprofiler_new.cpp 

Malheureusement, rien du tout concernant return strcmp(name, _name) == 0; dans diff no. Le patient est décédé des suites d'un coup avec un objet tranchant émoussé (utilité rm), mais au moment de sa mort, il était déjà en phase terminale.


Explorons l'essence de l'erreur.


Voici ce que l'auteur du code aimerait nous dire:


  const char *name() const { return _name; } bool is_compiled() const { return true; } bool vm_match(const char* name) const { return strcmp(name, _name) == 0; } 

Maintenant un peu de philosophie.


La norme C11 dans la clause 7.1.4, «Utilisation des fonctions de bibliothèque», dit explicitement:


Chacune des instructions suivantes s'applique, sauf indication contraire explicite dans les descriptions détaillées qui suivent: Si un argument d'une fonction a une valeur non valide (comme [...] un pointeur nul [...]) [...], le comportement n'est pas défini.

C'est-à-dire que maintenant toute la question est de savoir s'il y en a «explicitement indiqué autrement» . Rien de tel n'est écrit dans la description de strcmp dans la section 7.24.4, et je n'ai pas d'autres sections pour vous.


Autrement dit, nous avons un comportement indéfini ici.


Bien sûr, vous pouvez prendre et réécrire ce morceau de code en l'entourant de vérification. Mais je ne suis absolument pas sûr de bien comprendre la logique des gens qui utilisent UB là où il ne devrait pas être. Par exemple, certains systèmes génèrent SIGSERV pour zéro déréférencement, et un amateur de hack peut en profiter, mais ce comportement n'est pas obligatoire et peut démarrer sur une autre plate-forme.


Oui, bien sûr, quelqu'un dira que vous êtes un imbécile pour vous-même, que vous utilisez GCC 7.3, mais dans GCC 4 tout se serait rassemblé. Mais comportement indéfini! = Non spécifié! = Implémentation définie. Cela pour les deux derniers peut être défini pour fonctionner dans l'ancien compilateur. Et UB dans la sixième version était UB.


En bref, j'ai été complètement attristé par cette question philosophique complexe (devrais-je entrer dans le code avec mes hypothèses) quand j'ai soudainement réalisé qu'elle pouvait être différente.


Il y a une autre façon


Comme vous le savez, les bons héros font toujours le tour.


Même si nous ignorons notre philosophie de l'UB, il y a là une quantité incroyable de problèmes. Pas le fait qu'ils peuvent être réparés jusqu'au matin. Pas le fait que je ne peux pas le faire avec mes mains tordues. Encore moins est le fait que cela sera accepté en amont: le dernier patch de jdk8u était il y a 6 semaines, et c'était la fusion globale de la nouvelle balise.


Imaginez simplement que le code ci-dessus est réellement écrit correctement. Tout ce qui se trouve entre nous et son exécution est un avertissement, qui a été perçu comme une erreur en raison d'un bogue dans le système de construction. Mais nous pouvons construire un système de construction.


Le sorceleur Geralt de Rivia a dit un jour:


«Le mal est mal, Stregobor», dit gravement le sorceleur en se levant. - Plus petit, plus grand, moyen - tout est pareil, les proportions sont arbitraires et les frontières sont floues. Je ne suis pas un saint ermite, non seulement j'ai fait un bien dans ma vie. Mais si vous devez choisir entre un mal et un autre, je préfère ne pas choisir du tout.

- Zło à zło, Stregoborze - rzekł poważnie wiedźmin wstając. - Mniejsze, większe, średnie, wszystko jedno, proporcje są umowne a granice zatarte. Nie jestem świątobliwym pustelnikiem, nie samo dobro czyniłem w życiu. Ale jeżeli mam wybierać pomiędzy jednym złem a drugim, à wolę nie wybierać wcale.

Ceci est une citation de The Last Wish, une histoire appelée Lesser Evil. Nous savons tous que Geralt n'a presque jamais pu jouer le rôle d'un personnage vraiment neutre, et est même mort en raison d'un autre comportement de type chaotique classique.


Faisons donc une démonstration rapide du moindre mal. Passons au système de construction.


Au tout début, nous avons déjà vu cet échappement:


 grep -rl "Werror" . ./common/autoconf/flags.m4 ./hotspot/make/linux/makefiles/gcc.make ./hotspot/make/bsd/makefiles/gcc.make ./hotspot/make/solaris/makefiles/gcc.make ./hotspot/make/aix/makefiles/xlc.make 

En comparant ces deux fichiers, j'ai rompu tout le visage avec le facespalm et réalisé la différence de culture des deux plateformes:


BSD est une histoire de liberté et de choix:


 # Compiler warnings are treated as errors ifneq ($(COMPILER_WARNINGS_FATAL),false) WARNINGS_ARE_ERRORS = -Werror endif 

GNU / Linux est un régime puriste autoritaire:


 # Compiler warnings are treated as errors WARNINGS_ARE_ERRORS = -Werror 

Eh bien, elle serait transmise à linux via XX_FLAGS , cette variable n'est pas prise en compte lors du calcul de WARNINGS_ARE_ERRORS ! Dans la version pour GNU / Linux, nous n'avons tout simplement pas d'autre choix que de suivre les valeurs par défaut qui ont été lancées d'en haut.


Eh bien, ou vous pouvez le rendre plus facile et changer la valeur de WARNINGS_ARE_ERRORS en un -w court, mais non moins puissant. Ça vous plaît, Elon Musk?


Comme vous l'avez peut-être deviné, cela résout complètement ce problème de construction.


Lorsque le code est assemblé, vous voyez un tas de problèmes étranges et terriblement volants. Parfois, cela arrivait si effrayant que je voulais vraiment appuyer sur ctrl + C et essayer de le comprendre. Mais non, vous ne pouvez pas, vous ne pouvez pas ...


Il semble que tout s'est rassemblé et n'a pas posé de problèmes supplémentaires. Bien sûr, je n'ai pas osé commencer les tests. Pourtant, la nuit, mes yeux commencent à se coller et, d'une manière ou d'une autre, je ne veux pas aller au dernier recours - quatre canettes d'énergie du réfrigérateur.



Tombe dans la croûte


L'assemblage est passé, les exécutables ont été générés, nous sommes super.


Et donc nous sommes arrivés à la ligne d'arrivée. Ou n'est pas venu?


Notre montage se déroule de la manière suivante:


 export JAVA_HOME=~/git/jdk8u/build/linux-x86_64-normal-server-fastdebug/jdk export PATH=$JAVA_HOME/bin:$PATH 

Lorsque vous essayez d'exécuter l'exécutable java , il se bloque instantanément. Pour ceux qui ne sont pas familiers - cela ressemble à ceci:




En même temps, Alex a Debian 9.5 et j'ai Ubuntu. Deux versions différentes de GCC, deux croûtes d'aspect différent. J'ai des farces innocentes avec le patch manuel strcmp et quelques autres endroits, Alex ne le fait pas. Quel est le problème?


Cette histoire est digne d'une histoire distincte, mais ici, allons directement aux conclusions sèches, sinon je n'ajouterai jamais ce post.


Le problème est que nos pogromistes C ++ préférés ont de nouveau utilisé un comportement non défini.


(De plus, là où cela dépend d'une manière inconnue de l'implémentation du compilateur. Cependant, nous devons nous rappeler que UB est toujours UB, même sur une version connue du compilateur, il est impossible de s'y allonger)


Dans un endroit, nous nous tournons vers le champ d'une classe sous-conçue là-bas, et tout se casse. Ne demandez pas comment c'est arrivé, tout est compliqué.


Il est très difficile pour un javista d'imaginer comment on peut se tourner vers une classe sous-construite, sauf en émettant un lien vers celle-ci directement depuis le constructeur. Heureusement, le merveilleux langage C ++ peut tout faire ou presque. Je vais écrire un exemple avec un certain pseudocode:


 class A { A() { _b.Show(); } private: static B _b; }; A a; BA::_b; int main() { } 

Bon débogage!


Si vous regardez C ++ 98 [class.cdtor]:


Pour un objet de type de classe non-POD ... avant que le constructeur commence l'exécution ... se référer à un membre non statique ou à une classe de base de l'objet entraîne un comportement indéfini

À partir de GCC d'une certaine version (et j'en ai 7.3), une optimisation de «l'élimination du magasin mort à vie» est apparue, qui croit que nous ne faisons référence à un objet que pendant sa durée de vie, et tout crache en dehors de la durée de vie.


La solution consiste à désactiver les nouvelles optimisations et à revenir telles qu'elles étaient dans l'ancien GCC:


 CFLAGS += -fno-strict-aliasing -fno-lifetime-dse -fno-delete-null-pointer-checks 

Il y a une discussion à ce sujet ici .
Pour une raison quelconque, les participants à la discussion ont décidé que cela ne serait pas inclus en amont. Mais vous devez toujours essayer de l'envoyer.


Ajoutez ces options à notre ./hotspot/make/linux/makefiles/gcc.make , réassemblez tout et voyez les lignes chères:


 t$ ~/git/jdk8u/build/linux-x86_64-normal-server-fastdebug/jdk/bin/java -version openjdk version "1.8.0-internal-fastdebug" OpenJDK Runtime Environment (build 1.8.0-internal-fastdebug-me_2018_09_10_08_14-b00) OpenJDK 64-Bit Server VM (build 25.71-b00-fastdebug, mixed mode) 

Conclusion


Vous pensiez probablement que la conclusion serait: "Java est une sorte d'enfer, il y a des ordures dans le code, il n'y a pas de support, tout est mauvais."


Ce n'est pas le cas! Au contraire, les exemples ci-dessus montrent de quel terrible mal nos amis, les nécromanciens d'OpenJDK, nous empêchent.


Et malgré le fait qu'ils doivent vivre et utiliser C ++, trembler avec chaque UB et changer la version du compilateur et apprendre les subtilités des plates-formes, le code utilisateur final en Java est incroyablement stable, et sur les versions publiées sur les sites Web officiels de sociétés telles que Azul, Red Hat et Oracle peuvent difficilement se heurter à la croûte dans un cas simple.


La seule chose triste est, très probablement, que les erreurs trouvées sont peu susceptibles d'être acceptées dans jdk8u. Nous avons pris JDK 8 simplement parce qu'il est plus facile pour nous de le patcher ici et maintenant, et nous devrons faire face à JDK 11. Néanmoins, utiliser JDK 8 en 2018 est à mon humble avis, c'est une très mauvaise pratique, et nous ne le faisons pas d'une bonne vie. Peut-être que notre vie s'améliorera à l'avenir, et vous lirez de nombreuses autres histoires incroyables du monde de JDK 11 et JDK 12.


Merci pour l'attention portée à un texte aussi ennuyeux sans images :-)


Minute de publicité. La conférence Joker 2018 se tiendra très prochainement, où il y aura de nombreux spécialistes éminents de Java et de JVM. Consultez la liste complète des orateurs et les rapports sur le site officiel . J'y serai aussi, il sera possible de se rencontrer et de broyer pour la vie et OpenJDK.

Source: https://habr.com/ru/post/fr422861/


All Articles