Хотел сегодня поспать, но опять не удалось. В Телеграме появилось сообщение, что у кого-то не собирается Java… и мы очнулись только через пару часов, уставшие и довольные.
Кому этот пост может быть полезен? Да, наверное, никому, кроме тех, кто тоже собирает JDK8 или просто любит почитать кошмарные ужасы. В общем, я вас предупредил, закрывайте статью срочно.
Проблемы три:
- Не собирается (уровень первый)
Очень скучная часть, которую можно пропустить. Нужна только для тех, кто хочет полностью восстановить историю событий; - Не собирается (уровень второй)
Интересней, потому что там есть пара типичных ошибок, некромантия, некрофилия, в чём BSD лучше GNU/Linux и почему стоит переходить на новые версии JDK. - Даже если собирается, падает в корку
Более интересно. Йахууу, JVM упала в корку, давайте пинать её ногами!
Под катом показан подробный ход решения проблем, с разными побочными мыслями о жизни.
Будет много C++, кода на Java не будет вообще. Любой джавист в конце концов начинает писать только на C++…
Не собирается
Кто хоть раз собирал Java, знает, что это выглядит как-то так:
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
(У меня все пользователи называются просто «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.
Компилятор не первой свежести, не 8.2, но и этот должен бы подойти.
C++ разработчики любят тестировать софт только на той версии компилятора, что установлена у них. Обычно желание тестировать на разных платформах заканчивается где-то в районе разницы между gcc и clang в общем смысле. Поэтому вполне нормально вначале впендюрить -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) {
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());
}
}
Обратите внимание на проблемную строчку:
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. Давайте выясним, зачем он прибил этот несчастный файл.
Для этого надо пройти в жиру в тикет, указанный в summary коммита. Это JDK-8173715:
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
К сожалению, совершенно ничего, касающегося return strcmp(name, _name) == 0;
в диффе нет. Поциент умер от удара тупым острым предметом (утилитой rm), но на момент смерти уже был неизлечимо болен.
Давайте копнем в суть ошибки.
Вот что как бы хотел сказать нам автор кода:
const char *name() const { return _name; }
bool is_compiled() const { return true; }
bool vm_match(const char* name) const { return strcmp(name, _name) == 0; }
Теперь немного философии.
Стандарт C11 в пункте 7.1.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.
То есть теперь весь вопрос в том, есть ли некое «explicitly stated otherwise». В описании strcmp
в разделе 7.24.4 ничего подобного не написано, а других разделов у меня для вас нет.
То есть мы здесь имеем undefined behavior.
Конечно, можно взять и переписать этот кусок кода, окружив его проверкой. Но я совершенно не уверен, что правильно понимаю логику людей, которые используют UB там, где его быть не должно. Например, некоторые системы генерят на дереференсинг нуля SIGSERV, и любитель хаков может на это заложиться, но такое поведение не является обязательным и может бахнуть на другой платформе.
Да, конечно, кто-то скажет, что это ты сам себе дурак, что используешь GCC 7.3, а вот в GCC 4 точно бы все собралось. Но ведь undefined behavior != unspecified != implementation defined. Это для последних двух можно закладываться на работу в старом компиляторе. А UB и в шестой версии был UB.
Короче, я совсем взгрустнул над этим сложным философским вопросом (лезть ли со своими предположениями в код), когда внезапно осознал — можно и по-другому.
Есть другой путь
Как известно, хорошие герои всегда идут в обход.
Даже если отвлечься от нашей философии про UB, проблем там невероятное количество. Не факт, что их можно починить до утра. Не факт, что я своими кривыми руками не накосячу. Еще менее факт, что это примут в апстрим: последний патч в jdk8u был 6 недель назад, и это был глобальный мердж нового тэга.
Просто представим, что код выше на самом деле написан правильно. Всё, что стоит между нами и его выполнением, — некий warning, который был воспринят как error по причине бага в системе сборки. Но ведь мы можем похачить систему сборки.
Ведьмак Геральт из Ривии говорил когда-то:
— Зло — это зло, Стрегобор, — серьёзно сказал ведьмак, вставая. — Меньшее, бо́льшее, среднее — всё едино, пропорции условны, а границы размыты. Я не святой отшельник, не только одно добро творил в жизни. Но если приходится выбирать между одним злом и другим, я предпочитаю не выбирать вообще.
— Zło to 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, to wolę nie wybierać wcale.
Это цитата из книги «Последнее желание», рассказ «Меньшее зло». Мы-то знаем, что Геральт почти никогда не мог до конца сыграть роль истинно-нейтрального персонажа, и даже умер по причине очередного классического хаотически-доброго поведения.
Поэтому давайте-ка зашкваримся об меньшее зло. Похачим систему сборки.
В самом начале мы уже видели вот такой выхлоп:
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
Ну ещё бы оно пробрасывалось в linux через ССXX_FLAGS
, эта переменная при вычислении WARNINGS_ARE_ERRORS
близко не учитывается! В билде для GNU/Linux у нас просто нет выбора, кроме как следовать спущенным свыше дефолтам.
Ну или можно сделать проще и поменять значение 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
он мгновенно падает в корку. Для тех, кто не знаком — это выглядит примерно так:
При этом у Алекса — Debian 9.5, а у меня — Ubuntu. Две разных версии GCC, две по-разному выглядящие корки. У меня есть невинные шалости с ручным патчем strcmp и еще нескольких мест, у Алекса — нет. В чём же проблема?
Эта история достойна отдельного рассказа, но тут давайте сразу перейдём к сухим выводам, иначе я этот пост не допишу никогда.
Проблема в том, что наши любимые 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
Начиная с GCC какой-то версии (а у меня 7.3) появилась оптимизация «lifetime dead store elimination», которая считает, что к объекту мы обращаемся только в ходе его лайфтайма, а вне лайфтайма всё выкашивает.
Решение — отключить новые оптимизации и вернуть как было в старых 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, вряд ли можно напороться на корку в простом случае.
Единственная печальная штука — скорей всего, найденные ошибки вряд ли примут в jdk8u. Мы взяли JDK 8 просто потому, что нам проще его запатчить прямо здесь и сейчас, а с JDK 11 придется разбираться. Тем не менее, использовать в 2018 году JDK 8 — имхо, это очень плохая практика, и мы делаем это не от хорошей жизни. Возможно, в будущем наша жизнь улучшится, и вы прочитаете ещё множество невероятных историй из мира JDK 11 и JDK 12.
Спасибо за внимание, уделённое столь занудному тексту без картинок :-)
Минутка рекламы. Совсем скоро пройдёт конференция Joker 2018, на которой будет множество видных специалистов по Java и JVM. Посмотреть полный список спикеров и докладов можно на официальном сайте. Я там тоже буду, можно будет встретиться и перетереть за жизнь и OpenJDK.