View in Telegram
Отложенное связывание символов (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 инициализируется не сразу, а по мере необходимости).
Love Center
Love Center
Find friends or serious relationships easily