Хабрахабр

О сборке JDK 8 на Ubuntu, качестве кода Hotspot и почему всё валят на C++

В Телеграме появилось сообщение, что у кого-то не собирается Java… и мы очнулись только через пару часов, уставшие и довольные. Хотел сегодня поспать, но опять не удалось.

Да, наверное, никому, кроме тех, кто тоже собирает JDK8 или просто любит почитать кошмарные ужасы. Кому этот пост может быть полезен? В общем, я вас предупредил, закрывайте статью срочно.

Проблемы три:

  • Не собирается (уровень первый)
    Очень скучная часть, которую можно пропустить. Нужна только для тех, кто хочет полностью восстановить историю событий;
  • Не собирается (уровень второй)
    Интересней, потому что там есть пара типичных ошибок, некромантия, некрофилия, в чём BSD лучше GNU/Linux и почему стоит переходить на новые версии JDK.
  • Даже если собирается, падает в корку
    Более интересно. Йахууу, JVM упала в корку, давайте пинать её ногами!

Под катом показан подробный ход решения проблем, с разными побочными мыслями о жизни.

Любой джавист в конце концов начинает писать только на C++… Будет много C++, кода на Java не будет вообще.

Кто хоть раз собирал Java, знает, что это выглядит как-то так:

hg clone http://hg.openjdk.java.net/jdk8u/jdk8u
cd jdk8u 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

(У меня все пользователи называются просто «me», чтобы виртуалку можно было в любой момент отдать любому человеку и не создать отторжения от пользования не своим юзернеймом)

Причём довольно циничным образом. Проблема, конечно, в том, что это не работает.

Первый уровень погружения

Давайте попробуем запустить:

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

Вначале, чтобы вы понимали, у меня установлено вот такое:

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

0, но и этот должен бы подойти. Компилятор не первой свежести, не 8.

Обычно желание тестировать на разных платформах заканчивается где-то в районе разницы между gcc и clang в общем смысле. C++ разработчики любят тестировать софт только на той версии компилятора, что установлена у них. Поэтому вполне нормально вначале впендюрить -Werror («считать предупреждения ошибками») и потом писать такой код, который во всех остальных версиях будет считаться ворнингами.

Нужно установить свою переменную окружения CXX_FLAGS, в которой прописать верный уровень ерроров. Проблема это известная, и ясно, как её решать.

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

И тут же видим чудесное:

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

Подменяем configure на вот такой: Хорошо, система сборки, всё, что хочешь!

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

И ошибка остаётся той же самой!
Переходим к тяжелой артиллерии: грепу исходников.

grep -rl "Werror" .

Вываливается огромное количество всякой автосгенерированной шляпы, среди которой есть проблески осмысленных файлов:

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

Удобно! Во flags.m4 легко находим предыдущее сообщение про «Ignoring CXXFLAGS» и более матёрый захардкоженый флаг CCXX_FLGS (да, две буквы C), который сразу действует и вместо CFLAGS, и вместо СXX_FLAGS. Интересно два факта:

  • Этот флаг никак не передаётся через параметры configure;
  • В дефолтном значении находятся осмысленные и подозрительно похожие на настоящие параметры:

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

Правда? Очень мило смотрится в комментариях этот вопрос — а что, флаги общие?

Не будем играть в демократию и авторитарно захардкодим туда -w («не показывать никаких ошибок»):

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

— первую ошибку мы прошли. И — ура! Казалось бы. Она больше не репортится, и вообще всё отлично.

Второй уровень погружения

Но теперь оно падает в куче других новых мест!

Аккуратно вычитываем мейкфайлы и не понимаем, как именно этот параметр вообще может проброситься. Получается, что наш -w работает, но пробрасывается не во все части сборки. Неужто о нём забыли?

Зная верный вопрос к гуглу («почему cxx не доходит до сборки?!»), быстренько попадаем на страницу бага с говорящим названием «configure --with-extra-cxxflags doesn't affect hotspot» (JDK-8156967).

Может быть. Который обещают пофиксить в JDK 12. Чудесно — самый важный параметр сборки не используется в сборке!

Первая идея — ну что ж, давайте засучим рукава и поправим ошибки!

Ошибка 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]

Сто пудов кто-то вычислил буфер нажатием кнопки «Мне Повезёт!» в гугле. Хорошо, нам, наверное, нужно увеличить регион.

Ниже есть уточнение другого рода: Но как бы понять, сколько надо?

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

Позиция 12 выглядит как что-то стоящее, с чем теперь можно вломиться грязными ногами в исходник.

Лезем в dependencies.cpp и наблюдаем следующую картину:

DepArgument arg = args->at(j);
if (j == 1) 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()); }
}

Обратите внимание на проблемную строчку:

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

Меняем 10 на 12, пересобираем и… сборка пошла!

Не вопрос, опять вбиваем в гугл наш мегапатч: char xn[12]; Но неужели я один такой умный и починил багу всех времён и народов?

Баг JDK-8184309, заревьюенный Владимиром Ивановым, содержит точно такое же исправление. И видим… да, всё верно.

Это к вопросу о том, зачем нужны новые версии джавы. Но суть в том, что он поправлен только в JDK 10 и нифига не бэкпортирован в jdk8u.

Ошибка 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; }

И… этого файла там нет. Наученные предыдущим горьким опытом, сразу же идём смотреть, что в этом месте находится в JDK 11. Структура каталогов тоже подверглась некоторому рефакторингу.

Но от нас так просто не уйдёшь!

Поэтому сейчас будет НЕКРОМАНТИЯ В ДЕЙСТВИИ! Любой джавист — в душе немного некромант, а может даже и некрофил.

Вначале нужно воззвать к душе мёртвого и узнать, когда он умер:

$ 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

Теперь нужно выяснить причину его гибели:

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

Давайте выясним, зачем он прибил этот несчастный файл. Итак, у нас есть убийца: gziemski.

Это JDK-8173715: Для этого надо пройти в жиру в тикет, указанный в summary коммита.

Remove FlatProfiler:
We assume that this technology is no longer in use and is a source of root scanning for the GC.

По сути, сейчас нам предлагается починить труп просто для того, чтобы билд собрался. За-ши-бись. Который разложился настолько, что даже наши коллеги-некроманты из OpenJDK забросили.

Он был уже мёртв в ревизии 47106, значит в ревизии на единичку меньше — это «за секунду до»: Давайте воскресим мертвеца и попробуем расспросить его, что он запомнил последним.

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

Поциент умер от удара тупым острым предметом (утилитой rm), но на момент смерти уже был неизлечимо болен. К сожалению, совершенно ничего, касающегося return strcmp(name, _name) == 0; в диффе нет.

Давайте копнем в суть ошибки.

Вот что как бы хотел сказать нам автор кода:

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

Теперь немного философии.

1. Стандарт C11 в пункте 7. 4, «Use of library functions», явным образом говорит:

Each of the following statements applies unless explicitly stated otherwise in the detailed descriptions that follow: If an argument to a function has an invalid value (such as [...] a null pointer [...]) [...], the behavior is undefined.

В описании strcmp в разделе 7. То есть теперь весь вопрос в том, есть ли некое «explicitly stated otherwise». 4 ничего подобного не написано, а других разделов у меня для вас нет. 24.

То есть мы здесь имеем undefined behavior.

Но я совершенно не уверен, что правильно понимаю логику людей, которые используют UB там, где его быть не должно. Конечно, можно взять и переписать этот кусок кода, окружив его проверкой. Например, некоторые системы генерят на дереференсинг нуля SIGSERV, и любитель хаков может на это заложиться, но такое поведение не является обязательным и может бахнуть на другой платформе.

3, а вот в GCC 4 точно бы все собралось. Да, конечно, кто-то скажет, что это ты сам себе дурак, что используешь GCC 7. Это для последних двух можно закладываться на работу в старом компиляторе. Но ведь undefined behavior != unspecified != implementation defined. А UB и в шестой версии был UB.

Короче, я совсем взгрустнул над этим сложным философским вопросом (лезть ли со своими предположениями в код), когда внезапно осознал — можно и по-другому.

Как известно, хорошие герои всегда идут в обход.

Не факт, что их можно починить до утра. Даже если отвлечься от нашей философии про UB, проблем там невероятное количество. Еще менее факт, что это примут в апстрим: последний патч в jdk8u был 6 недель назад, и это был глобальный мердж нового тэга. Не факт, что я своими кривыми руками не накосячу.

Всё, что стоит между нами и его выполнением, — некий warning, который был воспринят как error по причине бага в системе сборки. Просто представим, что код выше на самом деле написан правильно. Но ведь мы можем похачить систему сборки.

Ведьмак Геральт из Ривии говорил когда-то:

— Меньшее, бо́льшее, среднее — всё едино, пропорции условны, а границы размыты. — Зло — это зло, Стрегобор, — серьёзно сказал ведьмак, вставая. Но если приходится выбирать между одним злом и другим, я предпочитаю не выбирать вообще. Я не святой отшельник, не только одно добро творил в жизни.

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

Мы-то знаем, что Геральт почти никогда не мог до конца сыграть роль истинно-нейтрального персонажа, и даже умер по причине очередного классического хаотически-доброго поведения. Это цитата из книги «Последнее желание», рассказ «Меньшее зло».

Похачим систему сборки. Поэтому давайте-ка зашкваримся об меньшее зло.

В самом начале мы уже видели вот такой выхлоп:

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

Сравнивая два этих файла, я разбил всё лицо фейспалмом и осознал разницу в культуре двух платформ:

BSD — это история о свободе и возможностях выбора:

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

GNU/Linux — это авторитарный режим пуристов:

# Compiler warnings are treated as errors
WARNINGS_ARE_ERRORS = -Werror

В билде для GNU/Linux у нас просто нет выбора, кроме как следовать спущенным свыше дефолтам. Ну ещё бы оно пробрасывалось в linux через ССXX_FLAGS, эта переменная при вычислении WARNINGS_ARE_ERRORS близко не учитывается!

Как тебе такое, Илон Маск? Ну или можно сделать проще и поменять значение WARNINGS_ARE_ERRORS на краткое, но не менее мощное -w.

Как вы могли догадаться, это полностью решает данную проблему сборки.

Иногда бывало так страшно, что очень хотелось нажать ctrl+C и попробовать разобраться. Когда код собирается, вы видите пролетающую мимо кучу странных, жутко выглядящих проблем. Но нет, нельзя, нельзя…

Хотя я, конечно, не решился начать заниматься тестированием. Вроде бы всё собралось и не принесло никаких дополнительных проблем. Все-таки ночь, глаза начинают слипаться, а переходить к последнему средству — четырем банкам энергетика из холодильника — как-то пока не хочется.

Сборка прошла, сгенерированы экзешники, мы молодцы.

Или не пришли? И вот мы пришли к финишу.

Наша сборка лежит по следующему пути:

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

Для тех, кто не знаком — это выглядит примерно так: При попытке запустить исполняемый файл java он мгновенно падает в корку.

5, а у меня — Ubuntu. При этом у Алекса — Debian 9. У меня есть невинные шалости с ручным патчем strcmp и еще нескольких мест, у Алекса — нет. Две разных версии GCC, две по-разному выглядящие корки. В чём же проблема?

Эта история достойна отдельного рассказа, но тут давайте сразу перейдём к сухим выводам, иначе я этот пост не допишу никогда.

Проблема в том, что наши любимые C++-погромисты опять использовали undefined behavior.

Впрочем, надо помнить, что UB — всегда UB, даже на известной версии компилятора закладываться на него нельзя) (Причем там, где оно неизвестным способом зависит от реализации компилятора.

Не спрашивайте, как так вышло, всё сложно. В одном месте мы там обращаются к полю недосконструированного класса, и всё ломается.

К счастью, чудесный язык C++ может всё или практически всё. Для джависта очень сложно представить, как можно обратиться к недосконструированному классу, кроме как выпустив ссылку на него прямо из конструктора. Запишу пример неким псевдокодом:

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

Have a nice debug!

Если поглядеть на C++98 [class.cdtor]:

For an object of non-POD class type… before the constructor begins execution… referring to any non-static member or base class of the object results in undefined behavior

3) появилась оптимизация «lifetime dead store elimination», которая считает, что к объекту мы обращаемся только в ходе его лайфтайма, а вне лайфтайма всё выкашивает. Начиная с GCC какой-то версии (а у меня 7.

Решение — отключить новые оптимизации и вернуть как было в старых GCC:

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

Но всё равно надо попробовать заслать. Про это здесь есть обсуждение.
По какой-то причине участники дискуссии решили, что в апстрим это не включат.

Добавляем эти опции в наш ./hotspot/make/linux/makefiles/gcc.make, всё пересобираем ещё раз и видим заветные строчки:

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)

Вы, наверное, подумали, что вывод будет следующий: «Java — это какой-то ад, в коде мусор, поддержки нет, всё плохо».

Напротив, примеры выше показывают, от какого страшного зла хранят нас наши друзья, некроманты из OpenJDK. Это не так!

И несмотря на то, что им приходится жить и пользоваться C++, дрожать от каждого UB и изменения версии компилятора и изучать тонкости платформ, финальный пользовательский код на языке Java — безумно стабильный, а на билдах, выложенных на официальных сайтах компаний, таких как Azul, Red Hat и Oracle, вряд ли можно напороться на корку в простом случае.

Мы взяли JDK 8 просто потому, что нам проще его запатчить прямо здесь и сейчас, а с JDK 11 придется разбираться. Единственная печальная штука — скорей всего, найденные ошибки вряд ли примут в jdk8u. Возможно, в будущем наша жизнь улучшится, и вы прочитаете ещё множество невероятных историй из мира JDK 11 и JDK 12. Тем не менее, использовать в 2018 году JDK 8 — имхо, это очень плохая практика, и мы делаем это не от хорошей жизни.

Спасибо за внимание, уделённое столь занудному тексту без картинок 🙂

Совсем скоро пройдёт конференция Joker 2018, на которой будет множество видных специалистов по Java и JVM. Минутка рекламы. Я там тоже буду, можно будет встретиться и перетереть за жизнь и OpenJDK. Посмотреть полный список спикеров и докладов можно на официальном сайте.

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть