Версионная метка VER_2 также указывает на то, что к ней будет привязана новая функция pqr(), которая экспортируется библиотекой. Если не объявить функцию pqr() таким способом, то ключевое слово local, наследованное меткой VER_2 от VER_1, сделает ее невидимой за пределами библиотеки. Стоит также отметить: если бы мы просто опустили ключевое слово local, то символы xyz_old() и xyz_new() тоже были бы экспортированы (обычно это нежелательно).
Теперь соберем новую версию нашей библиотеки уже привычным способом:
$ gcc — g — c — fPIC — Wall sv_lib_v2.c
$ gcc — g — shared — o libsv.so sv_lib_v2.o — Wl, — version-script,sv_v2.map
Теперь можно создать новую программу, p2, которая использует новое определение функции xyz(); при этом программа p1 будет применять ее старую версию.
$ gcc — g — o p2 sv_prog.c libsv.so
$ LD_LIBRARY_PATH=. /p2
v2 xyz
$ LD_LIBRARY_PATH=. /p1
v1 xyz
Зависимости версионной метки записываются в исполняемый файл на этапе статической компоновки. Если вывести с помощью команды objdump — t таблицы символов для обеих программ, то получим разные зависимости:
$ objdump — t p1 | grep xyz
08048380 F *UND* 0000002e xyz@@VER_1
$ objdump — t p2 | grep xyz
080483a0 F *UND* 0000002e xyz@@VER_2
Похожую информацию можно также получить, задействуя команду readelf — s.
Дополнительные сведения о версионировании символов можно найти на https://www.akkadia.org/drepper/symbol-versioning или воспользовавшись командой info ld scripts.
Можно определить одну или несколько функций, которые будут автоматически вызываться при загрузке и выгрузке разделяемой библиотеки. Это позволит выполнять действия по инициализации и финализации во время работы с библиотеками. Функции инициализации и финализации вызываются вне зависимости от того, загружена библиотека автоматически или вручную, используя интерфейс dlopen (см. раздел 42.1).
Функции инициализации и финализации определяются с помощью атрибутов constructor и destructor компилятора gcc. Любую функцию, которую нужно выполнить при загрузке библиотеки, следует определить таким образом:
void __attribute__ ((constructor)) some_name_load(void)
{
/* Код инициализации */
}
Функции выгрузки имеют похожее определение:
void __attribute__ ((destructor)) some_name_unload(void)
{
/* Код финализации */
}
Вместо some_name_load() и some_name_unload() можно использовать любые другие имена на ваш выбор.
Атрибуты constructor и destructor компилятора gcc можно также применять для создания функций инициализации и финализации в главной программе.
Существует и более старый способ инициализации и финализации разделяемых библиотек. Он заключается в создании внутри библиотеки двух функций, _init() и _fini(). Функция void _init(void) содержит код, который выполняется, когда библиотека впервые загружается процессом. Код функции void _fini(void) выполняется при выгрузке библиотеки.
Создав функции _init() и _fini(), нужно указать во время компиляции разделяемой библиотеки параметр gcc — nostartfiles, чтобы не дать компоновщику включить их стандартные версии (при желании можно выбрать для них другие имена, воспользовавшись параметрами — Wl, — init и — Wl, — fini).
Применение функций _init() и _fini() считается устаревшей практикой. Им на смену пришли атрибуты constructor и destructor компилятора gcc, которые среди прочих преимуществ позволяют определить несколько функций инициализации и финализации.
Во время тестирования иногда может понадобиться переопределить функции (и другие символы), которые в обычных условиях были бы найдены динамическим компоновщиком на основе правил, описанных в разделе 41.11. Для этого переменной среды LD_PRELOAD можно присвоить строку с именами разделяемых библиотек, которые следует загрузить раньше других (имена разделяются двоеточиями). Поскольку данные библиотеки загружаются в первую очередь, их функции, запрашиваемые программой, будут использоваться автоматически, переопределяя любые одноименные символы, которые в противном случае пришлось бы искать динамическому компоновщику. Представьте, к примеру, что наша программа вызывает функции x1() и x2(), определенные в библиотеке libdemo. Запустив эту программу, мы увидим следующий вывод:
$ ./prog
Called mod1-x1 DEMO
Called mod2-x2 DEMO
В данном примере мы исходим из того, что разделяемая библиотека находится в одной из стандартных каталогов, поэтому не нужно использовать переменную среды LD_LIBRARY_PATH.