Всем привет! С момента написания этой статьи в моей трудовой деятельности ничего не изменилось. Я так же все еще разрабатываю программы для операционной системы IBM i. Для облегчения работы с рутиной у меня давно появилась идея написания библиотеки (об этом тоже планируется статья, но пока она только в планах). По определенным причинам библиотека делается статической и именно с этим аспектом связана тема настоящего поста. Ну и по классике Хабра: кому интересно, прошу под кат.
Основные определения
Приведу здесь некий глоссарий, чтобы не знакомым с IBM i был понятен дальнейший текст, а знатоки IBM i понимали что я имею ввиду.
Модуль - объект, который является результатом работы компилятора. То есть, это объектный файл в "обычной" терминологии.
Линкер - программа, запускаемая командой CRTPGM. Эта команда собирает программу из отдельных модулей.
Хедер - заголовочный файл с прототипами функций, классов и т. д.
*BNDDIR - объект системы IBM i. Хранит в себе список модулей и/или сервисных программ (динамические библиотеки). Используется для автоматической линковки модулей при сборке программы командой CRTPGM.
Сказ о разработчике
Жил-был разработчик программ банковских под платформу заморскую. И как-то устал он от рутины программистской и решил облегчить труд свой ратный создав библиотеку кода себе на радость и другим на потеху. Библиотеку было решено делать статической простоты поддержки ради, а также по другим причинам.
Написал разработчик первый модуль, создал для него хедер и положил все это добро в место общедоступное. Облегчилась работа немного: когда требовалось, разработчик включал в код программы хедер и вовсю вызывал функции свои, библиотечные. При сборке же указывал он расположение модуля готового.
Скоро сказка сказывается, да не скоро библиотеки пишутся. Понял разработчик, что мало ему функций написанных. Еще надо написать. Но задумался он крепко о том, где их размещать. С одной стороны - можно разместить в существующем модуле. Но в этом случае, даже когда потребуется всего одна функция, то в программе они будут все, ибо все они в одном модуле (заметка про LTO - link time optimization будет ниже). С другой стороны, если сделать еще один модуль (а потом еще 100500), то возникает сложность с добавлением их при сборке программы. Ведь не известно какие из них нужны, особенно, если один модуль библиотеки зависит от другого. Пригорюнился разработчик, но тут на подмогу ему пришла сама система заморская со своим списком *BNDDIR...
Обрадовался разработчик: теперь можно делать маленькие модули, пути к которым будут добавлены в список *BNDDIR, а при сборке программы можно указать лишь этот список. После этого линкер сам определит какие модули требуются в программе. И все именно так и стало и даже больше...
Эксперимент
Сказ о разработчике заканчивается словами "и даже больше". Далее я покажу что имелось ввиду. Для демонстрации я написал 2 библиотечных модуля (FOO$, BOO$), 2 хедера для них (foo.h, boo.h) и 1 независимый хедер (zoo.h). Далее показан код, но не всего перечисленного выше, а только того, что требуется для объяснения эксперимента. Кроме этого в хедерах опущен code guard.
// zoo.h
inline void zoo() {}
// boo.h
void boo();
// foo.cpp
#include "zoo"
void foo() { zoo(); }
// boo.cpp
#include "zoo"
void boo() { zoo(); }
Модули были были добавлены в *BNDDIR в следующем порядке:
Далее была написана программа, которую скомпилировали в модуль MAIN:
#include "boo"
int main() { boo(); }
Программа из скомпилированного модуля была собрана следующей командой:
Ожидание vs Реальность
После создания программы я как наивный чукотский разработчик полагал, что в ней будет 2 модуля: MAIN и BOO$. Согласитесь, ведь это очевидно на первый взгляд. Однако в программу помимо этих двух модулей попал и модуль FOO$:
Анализ факапа ожидаемости
Гуляя после проведенных экспериментов темным вечером и размышляя о результате, я пришел к следующему объяснению происходящего.
Получив приказ на сборку, линкер, первым делом, проверил, есть ли в единственном, переданном ему в команде, модуле точка входа (entry point). Убедившись, что она там есть, он прочитал список импорта этого модуля:
Список импорта модуля MAIN
Symbol Symbol
Name Type ARGOPT
_C_exception_router PROCEDURE *UNKNOWN
boo__Fv PROCEDURE *NO
__Set__vd__FPvRCQ2_3std9nothrow_t PROCEDURE *NO
__Set__vn__FUiRCQ2_3std9nothrow_t PROCEDURE *NO
__Set__dl__FPv PROCEDURE *NO
__Set__dl__FPvRCQ2_3std9nothrow_t PROCEDURE *NO
__Set__vd__FPv PROCEDURE *NO
__Set__nw__FUi PROCEDURE *NO
__Set__nw__FUiRCQ2_3std9nothrow_t PROCEDURE *NO
__Set__vn__FUi PROCEDURE *NO
__set_new_throws_exception__3stdFb PROCEDURE *NO
_C_main_420 PROCEDURE *NO
Q LE leDefaultEh2 PROCEDURE *UNKNOWN
Q LE leBdyCh2 PROCEDURE *UNKNOWN
Q LE setActGrpUserRC PROCEDURE *NO
Q LE leBdyEpilog2 PROCEDURE *NO
_C_SIGABRT_ctl_action DATA
_C_SIGFPE_ctl_action DATA
_C_SIGILL_ctl_action DATA
_C_SIGINT_ctl_action DATA
_C_SIGSEGV_ctl_action DATA
_C_SIGTERM_ctl_action DATA
_C_SIGUSR1_ctl_action DATA
_C_SIGUSR2_ctl_action DATA
_C_SIGIO_ctl_action DATA
_C_SIGALL_ctl_action DATA
_C_SIGOTHER_ctl_action DATA
В этом списке помимо системных функций фигурирует boo__Fv - манглированное имя функции boo. Видя, что других явно переданных модулей кроме модуля MAIN нет, линкер пошел смотреть в *BNDDIR. "Открыв" *BNDDIR, линкер увидел первым модуль FOO$ и посмотрел в его экспорт:
Список экспорта модуля FOO$
Symbol Symbol
Name Type ARGOPT
foo__Fv PROCEDURE *NO
zoo__Fv PROCEDURE *NO
Среди имен он не нашел boo__Fv и пошел смотреть экспорт следующего модуля BOO$ в списке *BNDDIR:
Список экспорта модуля BOO$
Symbol Symbol
Name Type ARGOPT
boo__Fv PROCEDURE *NO
zoo__Fv PROCEDURE *NO
Тут он увидел заветное имя boo__Fv и включил модуль BOO$ в создаваемую программу.
А дальше, как я предполагаю, произошло следующее. Так как в программу добавился новый модуль, то у него, конечно же, тоже есть список импорта. Он тоже может хотеть заполучить какие-то имена. Линкер посмотрел в список импорта нового модуля и увидел там имя zoo__Fv. Но вместо того, чтобы еще раз посмотреть в экспорт BOO$ и увидеть там zoo__Fv, линкер пошел снова смотреть список *BNDDIR с начала (не досмотрев этот список до конца, надо заметить, то есть пошла рекурсия). Конечно же, в экспорте FOO$ он увидел zoo__Fv и добавил этот модуль в программу.
Примечания
Если явно передать модуль BOO$ при сборке (и при этом оставить *BNDDIR), то в программу модуль FOO$ не добавится.
Если перед сборкой удалить модуль FOO$, то он, конечно же, не будет включен в программу. Но при этом программа соберется (ибо zoo__Fv есть в модуле BOO$).
Если в *BNDDIR после BOO$ будет еще один модуль, в котором экспортируется zoo__Fv, то он не будет добавлен в программу. Все так же добавится FOO$. Это подтверждает предположение о том, что линкер просматривает *BNDDIR рекурсивно.
Заметка про LTO
Возможность оптимизации времени сборки есть в IBM i. Для этого модули необходимо компилировать с параметром MODCRTOPT(*KEEPILDTA), а при сборке линкеру давать опцию IPA(*YES). В этом случае линкер "перелопатит" все модули (поданные явно и определенные по *BNDDIR), выкинет ненужное и сформирует новый модуль (модули). Вот программу из выше показанного эксперимента я собрал с LTO. В ней вместо 3-х модулей остался один, в котором вообще нет функций boo, foo или zoo:
Использование данного метода требует, чтобы модули имели оптимизацию не менее 20 (базовая оптимизация). Кроме этого, независимо от того с каким уровнем отладочной информации были собраны модули, итоговые модули отладки не будет иметь вообще. Как мне кажется, это основной фактор не использования LTO на IBM i. По крайней мере у нас. Сейчас многие, наверное, подумали: "Ну так в ПРОД то так и надо собирать - без отладки и с максимальной оптимизацией". И я тоже так думаю, но так не делают почему-то...
Вопрос вместо заключения
Дочитав до этого места, как вы ответите на вопрос этой статьи? Описанное поведение линкера при работе с *BNDDIR - это баг или здесь заложена некая возможность, о которой я не догадываюсь? Прошу оставить ваше мнение в комментарии.
Лично мое мнение - это баг.
Засим разрешите откланяться. Спасибо, что уделили время и прочитали!
Комментарии (12)
sshmakov
18.12.2022 01:28Как inline функция оказалась в экспорте? Может оптимизация отключена настолько, что inline не раскрываются?
d7d1cd Автор
18.12.2022 09:51Не проводил широких экспериментов с оптимизацией. Пример с inline функцией был выбран для упрощения. Если вместо нее в foo и boo создавать объект типа std::string, то тоже будет наблюдаться включение "ненужного" модуля. Линкер в первом модуле *BNDDIR увидит инстанцированный конструктор std::basic_string и включит этот модуль в программу, хотя это инстанцирование уже есть в модуле BOO$.
sshmakov
18.12.2022 12:20Это важно. Если оптимизация отключена, то для облегчения отладки компилятор не раскрывает inline - тогда можно встать дебаггером внутрь inline функции. Поскольку inline определен в хедере, то компилятор фактически создаёт обычную, не inline, функцию внутри модуля, в котором используется эта функция. Внутри какого модуля - зависит от реализации компилятора и порядка обхода модулей во время компиляции.
Если бы inline раскрылся, то символа вообще не создалось бы, ни в импорте, ни в экспорте.
d7d1cd Автор
18.12.2022 13:02Внутри какого модуля - зависит от реализации компилятора и порядка обхода модулей во время компиляции.
В моем примере модули скомпилированы отдельно друг от друга, а не в одной команде. Поэтому функция zoo есть в каждом, это нормально.
sshmakov
18.12.2022 13:05По любому, не имеет большого смысла предъявлять претензии к недостаточной оптимизации сборки, если оптимизация отключена.
Rampage
Серёжа, спасибо!
Обратитесь в поддержку IBM :))
d7d1cd Автор
Им и при хороших временах было пофигу. А сейчас и подавно.
Мне интересно мнение читателя по поводу вопроса.
tzlom
По сути тут реализован приоритет символов. С ld можно такое же поведение получить