Отложенное связывание символов (Lazy Binding)
С этого поста мы начинаем глубже уходить в процесс разрешения динамических зависимостей. Разобравшись с тем, как исполняемый файл получает доступ к внешним символам, мы сможем точнее понять проблему разделяемых библиотек и аккуратно подойти к возможным оптимизациям, которые к ним применимы.
Как мы
знаем, в динамической секции ELF файла содержатся названия всех разделяемых библиотек, от которых наше приложение зависит:
$ readelf -d prog
Dynamic section at offset 0xd78 contains 29 entries:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [libdemo.so]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
При старте программы, загрузчик проходится по всем известным ему путям, проверяет кэш
ld.so.cache и определяет, доступна ли указанная библиотека для дальнейшей работы. Если зависимость не найдена, приложение упадет и выведется следующий лог:
./prog: error in loading shared libraries: libdemo.so:
cannot open shared object file: No such file or directory
На данном этапе никакого связывания не происходит, сейчас загрузчику нужно просто определить библиотеку для текущего процесса: выгрузить ее в виртуальное адресное пространство программы и, при первом использовании, в физическую память (RAM).
Для связывания в ELF существует специальная секция
.dynsym, в которой ключевым словом UND (undefined) помечены названия всех символов, которые необходимо определить:
$ readelf --dyn-syms ./prog
Symbol table '.dynsym' contains 13 entries:
Num: Value Size Type Bind Vis Ndx Name
6: 0000000000000000 0 FUNC GLOBAL DEFAULT UND calculate_smth
Как и когда происходит разрешение имен?
Для того, чтобы ответить на данный вопрос, рассмотрим две дополнительные сущности ELF файла:
PLT и
GOT таблицы. Эти таблицы расположены в адресном пространстве процесса и отвечают за определение адресов динамически подгружаемых символов. На каждый символ в этих таблицах существует точка входа:
$ readelf -SW ./prog
Section Headers:
[Nr] Name Type Address
[12] .plt PROGBITS 00000000000006a0
[21] .got PROGBITS 0000000000010f78
При статическом связывании программы, все вызовы динамически подгружаемых символов записываются компоновщиком следующим образом "
<name>@plt". Это говорит о том, что в рантайме будет выполнена не сама функция, а определенная заглушка, которая, по таблице GOT выяснит, найден ли соответствующий адрес. Вот так выглядит команда на уровне ассемблера:
call 0x401060 <puts@plt>
Если адрес неизвестен, код внутри заглушки попросит загрузчик его определить и записать в соответствующее поле таблицы GOT, после чего будет выполнена команда jump на адрес и начнется выполнению кода. При последующих вызовах функции заглушка проверит адрес в таблице GOT и так как он будет определен, пропустит вызов компоновщика и сразу перейдет на jump до функции:
jump 0x404018 <puts@got.plt>
Так вот, что же такое Lazy Binding?
Это процесс определения адресов на этапе выполнения. Такой подход призван ускорить старт программы, так как не требуется при загрузке ее в память полностью заполнять все точки входа таблицы GOT: проходиться по всем библиотекам из секции
.dynamic и определять адреса для каждого символа секции
.dynsym.
Это не значит, что библиотеки, от которых зависит исполняемый файл, не будут загружены в память. Отложенная загрузка библиотек - это чуть другая история (Lazy Loading). В Linux, к сожалению, данный функционал отсутствует и реализован только сторонними утилитами, допустим,
imlib.so.
Lazy binding обладает как плюсами, так и минусами. С одной стороны, ускоряется старт программы, с другой, приложение может неожиданно упасть, если на этапе выполнения не будет найден какой-то символ (как мы помним, это может произойти из-за того, что таблица GOT инициализируется не сразу, а по мере необходимости).