Всем привет! С момента написания этой статьи в моей трудовой деятельности ничего не изменилось. Я так же все еще разрабатываю программы для операционной системы 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 в следующем порядке:

Список объектов *BNDDIR
Список объектов *BNDDIR

Далее была написана программа, которую скомпилировали в модуль MAIN:

#include "boo"
int main() { boo(); }           

Программа из скомпилированного модуля была собрана следующей командой:

Команда запуска линкера
Команда запуска линкера

Ожидание vs Реальность

После создания программы я как наивный чукотский разработчик полагал, что в ней будет 2 модуля: MAIN и BOO$. Согласитесь, ведь это очевидно на первый взгляд. Однако в программу помимо этих двух модулей попал и модуль FOO$:

Камон! Зачем тут модуль FOO$???
Камон! Зачем тут модуль 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)


  1. Rampage
    16.12.2022 15:51

    Серёжа, спасибо!
    Обратитесь в поддержку IBM :))


    1. d7d1cd Автор
      16.12.2022 17:05

      Им и при хороших временах было пофигу. А сейчас и подавно.

      Мне интересно мнение читателя по поводу вопроса.


      1. tzlom
        18.12.2022 00:52

        По сути тут реализован приоритет символов. С ld можно такое же поведение получить


  1. sshmakov
    18.12.2022 01:28

    Как inline функция оказалась в экспорте? Может оптимизация отключена настолько, что inline не раскрываются?


    1. d7d1cd Автор
      18.12.2022 09:51

      Не проводил широких экспериментов с оптимизацией. Пример с inline функцией был выбран для упрощения. Если вместо нее в foo и boo создавать объект типа std::string, то тоже будет наблюдаться включение "ненужного" модуля. Линкер в первом модуле *BNDDIR увидит инстанцированный конструктор std::basic_string и включит этот модуль в программу, хотя это инстанцирование уже есть в модуле BOO$.


      1. sshmakov
        18.12.2022 12:20

        Это важно. Если оптимизация отключена, то для облегчения отладки компилятор не раскрывает inline - тогда можно встать дебаггером внутрь inline функции. Поскольку inline определен в хедере, то компилятор фактически создаёт обычную, не inline, функцию внутри модуля, в котором используется эта функция. Внутри какого модуля - зависит от реализации компилятора и порядка обхода модулей во время компиляции.

        Если бы inline раскрылся, то символа вообще не создалось бы, ни в импорте, ни в экспорте.


        1. d7d1cd Автор
          18.12.2022 13:02

          Внутри какого модуля - зависит от реализации компилятора и порядка обхода модулей во время компиляции.

          В моем примере модули скомпилированы отдельно друг от друга, а не в одной команде. Поэтому функция zoo есть в каждом, это нормально.


          1. sshmakov
            18.12.2022 13:05

            По любому, не имеет большого смысла предъявлять претензии к недостаточной оптимизации сборки, если оптимизация отключена.


            1. mayorovp
              18.12.2022 13:28

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


              1. sshmakov
                18.12.2022 13:33
                -1

                Проверьте


    1. mayorovp
      18.12.2022 10:05
      +1

      Да фиг с ним с экспортом, как она оказалась в импорте?


      1. d7d1cd Автор
        18.12.2022 10:21

        Вот да, поддерживаю вопрос.