Acerca de compilar JDK 8 en Ubuntu, la calidad del código de Hotspot y por qué todo falla en C ++

Quería dormir hoy, pero nuevamente fallé. En Telegram apareció un mensaje de que alguien no iba a Java ... y nos despertamos solo después de un par de horas, cansados ​​y felices.




¿Quién puede usar esta publicación? Sí, probablemente para cualquiera, excepto para aquellos que también coleccionan JDK8 o simplemente les encanta leer horrores de pesadilla. En general, te lo advertí, cierra el artículo con urgencia.

Tres problemas:


  • No voy ( nivel uno )
    Una parte muy aburrida para saltear. Solo es necesario para aquellos que desean restaurar completamente la historia de los eventos;
  • No voy ( nivel dos )
    Es más interesante, porque hay un par de errores típicos, nigromancia, necrofilia, que BSD es mejor que GNU / Linux y por qué debería actualizar a nuevas versiones de JDK.
  • Incluso si va, cae en la corteza
    Más interesante Yahuuu, la JVM cayó en la corteza, ¡pateémosla!

Under the cat muestra una solución detallada a los problemas, con diferentes reflexiones sobre la vida.


Habrá mucho C ++, no habrá código Java en absoluto. Cualquier javista al final comienza a escribir solo en C ++ ...


No va a


Quien haya creado Java al menos una vez sabe que se parece a esto:


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 

(Todos mis usuarios simplemente se llaman "yo", para que pueda entregar la máquina virtual a cualquier persona en cualquier momento y no crear un rechazo por usar su propio nombre de usuario)


El problema, por supuesto, es que esto no funciona. Y de una manera bastante cínica.



Primer nivel de inmersión


Intentemos ejecutar:


 /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) { ^~~~~~~~~ 

Primero, para que lo entiendas, he instalado esto:


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

El compilador no es la primera frescura, no 8.2, pero este también debería funcionar.


A los desarrolladores de C ++ les gusta probar el software solo en la versión del compilador que han instalado. Por lo general, el deseo de probar en diferentes plataformas termina en algún lugar en la región de la diferencia entre gcc y clang en un sentido general. Por lo tanto, es bastante normal ejecutar primero -Werror ("tratar las advertencias como errores") y luego escribir un código que en todas las demás versiones se considerará vorings.


Este es un problema conocido, y está claro cómo resolverlo. Debe establecer su variable de entorno CXX_FLAGS, en la que establecer el nivel correcto de errores.


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

Y luego vemos lo maravilloso:


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

Ok, sistema de construcción, lo que quieras! Reemplazamos configure con este:


 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 

¡Y el error sigue siendo el mismo!
Pasamos a la artillería pesada: el código fuente.


 grep -rl "Werror" . 

Se cae una gran cantidad de sombreros generados automáticamente, entre los que se vislumbran archivos significativos:


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

En flags.m4 encontramos fácilmente el mensaje anterior sobre "Ignorar CXXFLAGS" y la bandera codificada más madura CCXX_FLGS (sí, dos letras C), que actúa inmediatamente en lugar de CFLAGS y en lugar de XX_FLAGS . Convenientemente! Dos hechos son interesantes:


  • Este indicador no se pasa a través de los parámetros de configuración;
  • En el valor predeterminado son significativos y sospechosamente similares a estos parámetros:

  # 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" 

Esta pregunta se ve muy bien en los comentarios, pero ¿qué son comunes las banderas? Derecho?


No jugaremos democracia y lo codificaremos autoritariamente allí -w ("no mostrar ningún error"):


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

Y - ¡salud! - El primer error que pasamos. Ella ya no informa, y ​​en general todo está bien. Parecería.



Segundo nivel de inmersión


¡Pero ahora está cayendo en una pila de otros lugares nuevos!


Resulta que nuestro -w funciona, pero no se reenvía a todas las partes del ensamblaje. Leemos detenidamente los archivos MAKE y no entendemos cómo se puede reenviar exactamente este parámetro. ¿Realmente se olvidó de él?


Conociendo la pregunta correcta de Google ("¿por qué cxx no llega a la compilación?"), Llegamos rápidamente a la página de error con el dicho "configure --with-extra-cxxflags no afecta el punto de acceso" ( JDK-8156967 ).


Que prometen arreglarse en JDK 12. Quizás. Maravilloso: ¡el parámetro de compilación más importante no se usa en el ensamblaje!


La primera idea es, bueno, ¡arremangarnos y arreglar los errores!


Error 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] 

Bueno, probablemente necesitemos agrandar la región. Cien libras, alguien calculó el búfer haciendo clic en "¡Soy afortunado!" en google


Pero, ¿cómo entenderías cuánto necesitas? Hay otro tipo de refinamiento a continuación:


 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 posición 12 parece algo que vale la pena, con la que ahora puedes entrar en la fuente con los pies sucios.


Subimos a dependencies.cpp y observamos la siguiente imagen:


 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()); } } 

Presta atención a la línea problemática:


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

Cambiamos de 10 a 12, volvemos a armar y ... ¡la asamblea se ha ido!


¿Pero soy el único tan inteligente y solucionado el error de todos los tiempos? Sin duda, de nuevo llevamos nuestro megapatch a Google: char xn[12];


Y vemos ... sí, es cierto. El error JDK-8184309 , prohibido por Vladimir Ivanov, contiene exactamente la misma solución.


Pero la conclusión es que solo está arreglado en JDK 10 y nifiga no está soportado en jdk8u. Esta es la pregunta de por qué se necesitan nuevas versiones de Java.


Error 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; } 

Enseñado por la amarga experiencia previa, inmediatamente vamos a ver qué hay en este lugar en JDK 11. Y ... este archivo no está allí. La estructura del directorio también ha sufrido algunas refactorizaciones.


¡Pero no puedes alejarte de nosotros!


Cualquier javista es un pequeño nigromante en su alma, y ​​tal vez incluso un necrófilo. ¡Por lo tanto, ahora habrá NECROMANCE EN ACCIÓN!


Primero debes apelar al alma de los muertos y descubrir cuándo murió:


 $ 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 

Ahora necesitas descubrir la causa de su muerte:


 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 

Entonces, tenemos un asesino: gziemski . Veamos por qué clavó este desafortunado archivo.


Para hacer esto, vaya a gordo en el ticket especificado en el resumen de la confirmación. Este es JDK-8173715 :


Eliminar FlatProfiler:
Asumimos que esta tecnología ya no está en uso y es una fuente de escaneo de raíz para el GC.


Para shih bis. De hecho, ahora estamos invitados a reparar el cadáver para que la construcción continúe. Lo que se descompuso tanto que incluso nuestros colegas nigromantes de OpenJDK lo abandonaron.


Resucitemos al hombre muerto e intentemos preguntarle qué fue lo último que recordó. Ya estaba muerto en la revisión 47106, lo que significa que hay uno menos en la revisión, esto es "un segundo antes":


 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 

Desafortunadamente, nada en absoluto con respecto a return strcmp(name, _name) == 0; en diff no. El paciente murió a causa de un golpe con un objeto contundente (utilidad rm), pero al momento de la muerte ya tenía una enfermedad terminal.


Profundicemos en la esencia del error.


Esto es lo que el autor del código quisiera decirnos:


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

Ahora un poco de filosofía.


El estándar C11 en la cláusula 7.1.4, "Uso de funciones de biblioteca", dice explícitamente:


Cada una de las siguientes afirmaciones se aplica a menos que se indique explícitamente lo contrario en las descripciones detalladas que siguen: Si un argumento a una función tiene un valor no válido (como [...] un puntero nulo [...]) [...], El comportamiento es indefinido.

Es decir, ahora toda la cuestión es si existe alguna "declaración explícita de lo contrario" . Nada de eso está escrito en la descripción de strcmp en la sección 7.24.4, y no tengo otras secciones para usted.


Es decir, tenemos un comportamiento indefinido aquí.


Por supuesto, puede tomar y reescribir este fragmento de código, rodeándolo con verificación. Pero no estoy completamente seguro de entender correctamente la lógica de las personas que usan UB donde no debería estar. Por ejemplo, algunos sistemas generan SIGSERV para cero desreferenciación, y un amante de los hacks puede aprovecharlo, pero este comportamiento no es obligatorio y puede comenzar en otra plataforma.


Sí, por supuesto, alguien dirá que eres un tonto, que estás usando GCC 7.3, pero en GCC 4 todo se habría reunido. ¡Pero comportamiento indefinido! = ¡Sin especificar! = Implementación definida. Esto para los dos últimos puede establecerse para que funcione en el compilador anterior. Y UB en la sexta versión era UB.


En resumen, me entristeció por completo esta compleja pregunta filosófica (debería entrar en el código con mis suposiciones) cuando de repente me di cuenta de que podía ser diferente.


Hay otra manera


Como sabes, los buenos héroes siempre andan por ahí.


Incluso si ignoramos nuestra filosofía sobre UB, hay una cantidad increíble de problemas allí. No es el hecho de que puedan repararse hasta la mañana. No es el hecho de que no puedo hacerlo con mis manos torcidas. Aún menos es el hecho de que esto se aceptará en la fase inicial: el último parche en jdk8u fue hace 6 semanas, y esta fue la fusión global de la nueva etiqueta.


Solo imagine que el código anterior está escrito correctamente. Todo lo que se interpone entre nosotros y su ejecución es una advertencia, que se percibió como un error debido a un error en el sistema de compilación. Pero podemos construir un sistema de construcción.


El brujo Geralt de Rivia dijo una vez:


"El mal es malvado, Stregobor", dijo el brujo con gravedad, levantándose. - Más pequeño, más grande, promedio: todo es igual, las proporciones son arbitrarias y los bordes están borrosos. No soy un ermitaño sagrado, no solo hice un bien en mi vida. Pero si tienes que elegir entre un mal y otro, prefiero no elegir en absoluto.

- Zło a 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, a wolę nie wybierać wcale.

Esta es una cita de The Last Wish, una historia llamada Lesser Evil. Todos sabemos que Geralt casi nunca podría desempeñar el papel de un personaje verdaderamente neutral, e incluso murió debido a otro comportamiento caótico clásico.


Así que hagamos una demostración rápida del mal menor. Pasemos al sistema de compilación.


Al principio, ya vimos este escape:


 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 

Al comparar estos dos archivos, rompí toda la cara con la cara palmada y me di cuenta de la diferencia en la cultura de las dos plataformas:


BSD es una historia sobre libertad y elección:


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

GNU / Linux es un régimen purista autoritario:


 # Compiler warnings are treated as errors WARNINGS_ARE_ERRORS = -Werror 

Bueno, se XX_FLAGS a Linux a través de XX_FLAGS , ¡esta variable no se tiene en cuenta al calcular WARNINGS_ARE_ERRORS ! En la compilación para GNU / Linux, simplemente no tenemos más opción que seguir los valores predeterminados que se han lanzado desde arriba.


Bueno, o puede hacerlo más fácil y cambiar el valor de WARNINGS_ARE_ERRORS a un breve, pero no menos poderoso -w . ¿Qué te parece eso, Elon Musk?


Como habrás adivinado, esto resuelve completamente este problema de compilación.


Cuando se ensambla el código, ves pasar un montón de problemas extraños y terriblemente vistos. A veces sucedía tanto miedo que realmente quería presionar ctrl + C e intentar resolverlo. Pero no, no puedes, no puedes ...


Parece que todo se reunió y no trajo ningún problema adicional. Aunque, por supuesto, no me atreví a comenzar a probar. Aún así, de noche, mis ojos comienzan a fijarse, y de alguna manera no quiero ir al último recurso: cuatro latas de energía del refrigerador.



Cae en la corteza


La asamblea ha pasado, se han generado los ejecutables, somos geniales.


Y así llegamos a la línea de meta. ¿O no viniste?


Nuestro montaje se encuentra de la siguiente manera:


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

Cuando intenta ejecutar el ejecutable de java , se bloquea instantáneamente. Para aquellos que no están familiarizados, se ve así:




Al mismo tiempo, Alex tiene Debian 9.5 y yo tengo Ubuntu. Dos versiones diferentes de GCC, dos costras de aspecto diferente. Tengo bromas inocentes con el parche manual strcmp y algunos lugares más, Alex no. Cual es el problema


Esta historia es digna de otra historia, pero aquí vamos directamente a las conclusiones secas, de lo contrario nunca agregaré esta publicación.


El problema es que nuestros pogromistas favoritos de C ++ nuevamente usaron un comportamiento indefinido.


(Además, donde de alguna manera desconocida depende de la implementación del compilador. Sin embargo, debemos recordar que UB siempre es UB, incluso en una versión conocida del compilador es imposible establecerlo)


En un lugar pasamos al campo de una clase poco diseñada allí, y todo se rompe. No preguntes cómo sucedió, todo es complicado.


Es muy difícil para un javista imaginar cómo se puede recurrir a una clase subconstruida, excepto emitiendo un enlace directamente desde el constructor. Afortunadamente, el maravilloso lenguaje C ++ puede hacer todo o casi todo. Escribiré un ejemplo con cierto pseudocódigo:


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

¡Que tengas una buena depuración!


Si nos fijamos en C ++ 98 [class.cdtor]:


Para un objeto de tipo de clase no POD ... antes de que el constructor comience la ejecución ... hacer referencia a cualquier miembro no estático o clase base del objeto da como resultado un comportamiento indefinido

Comenzando con GCC de alguna versión (y tengo 7.3), apareció una optimización de la "eliminación del depósito muerto de por vida", que cree que nos referimos a un objeto solo durante su tiempo de vida, y todo se agota fuera del tiempo de vida.


La solución es deshabilitar las nuevas optimizaciones y volver como estaban en el antiguo CCG:


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

Hay una discusión sobre esto aquí .
Por alguna razón, los participantes en la discusión decidieron que esto no se incluiría en la corriente ascendente. Pero aún debes intentar enviarlo.


Agregue estas opciones a nuestro ./hotspot/make/linux/makefiles/gcc.make , ./hotspot/make/linux/makefiles/gcc.make ensamblar todo nuevamente y vea las apreciadas líneas:


 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) 

Conclusión


Probablemente pensaste que la conclusión sería: "Java es una especie de infierno, hay basura en el código, no hay soporte, todo está mal".


¡Esto no es así! Por el contrario, los ejemplos anteriores muestran el mal terrible que nuestros amigos, nigromantes de OpenJDK, nos impiden.


Y a pesar del hecho de que tienen que vivir y usar C ++, temblar con cada UB y cambiar la versión del compilador y aprender las sutilezas de las plataformas, el código de usuario final en Java es increíblemente estable y en las versiones publicadas en sitios web oficiales de compañías como Azul, Red Hat y Oracle apenas pueden toparse con la corteza en un caso simple.


Lo único triste es que lo más probable es que los errores encontrados no sean aceptados en jdk8u. Tomamos JDK 8 simplemente porque nos es más fácil parchearlo aquí y ahora, y tendremos que lidiar con JDK 11. Sin embargo, usar JDK 8 en 2018 es en mi humilde opinión, esta es una muy mala práctica, y no lo hacemos desde una buena vida. Quizás nuestra vida mejore en el futuro, y leerás muchas más historias increíbles del mundo de JDK 11 y JDK 12.


Gracias por la atención prestada a un texto tan aburrido sin imágenes :-)


Minuto de publicidad. La Conferencia Joker 2018 se llevará a cabo muy pronto, donde habrá muchos especialistas destacados en Java y JVM. Vea la lista completa de oradores e informes en el sitio web oficial . Yo también estaré allí, será posible conocer y abatir de por vida y OpenJDK.

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


All Articles