Когда вы запускаете программу, она почти никогда не несет в себе весь код целиком. Функции вроде printf, открытия сокета или работы с UTF-8 лежат в общих библиотеках, которые подгружаются в момент запуска. Этот урок про то, как именно бинарник находит свои .so-файлы, кто ему в этом помогает, и почему сломанный кэш или неосторожно выставленная переменная окружения превращают рабочую систему в набор программ с ошибкой "cannot open shared object file". Администратор сталкивается с этим при установке софта мимо пакетного менеджера, при сборке из исходников и при разборе чужих инцидентов.

Как это работает
Библиотеки бывают статические (.a) и разделяемые (.so, shared object). Статическая вшивается в бинарник на этапе линковки - программа становится толще, зато самодостаточна. Разделяемая существует одним файлом на диске, и десятки процессов отображают ее в память совместно. Экономится и диск, и оперативка, и - главное - обновление библиотеки (например, закрытие уязвимости в libssl) чинит сразу все программы, а не каждую по отдельности.
За загрузку отвечает динамический компоновщик ld.so (точнее, ld-linux-x86-64.so.2 на 64-битных системах). Это не обычная программа, а интерпретатор, прописанный прямо в заголовке ELF-бинарника. Ядро при exec видит, что файлу нужен интерпретатор, и передает управление ему. Компоновщик читает список нужных библиотек из секции бинарника, находит каждую на диске, проецирует в память и связывает символы.
Чтобы не сканировать весь диск при каждом запуске, ld.so использует заранее построенный кэш - файл /etc/ld.so.cache. В нем лежит готовая таблица "имя библиотеки -> полный путь". Строит этот кэш утилита ldconfig. Она обходит стандартные каталоги (/lib, /usr/lib и их 64-битные варианты) плюс пути из конфигурации и записывает результат в кэш. Пока вы не выполнили ldconfig, свежеустановленная в нестандартное место библиотека компоновщику не видна.
Ключевое понятие версионирования - SONAME. Внутри .so-файла зашито "официальное" имя, например libssl.so.3. Программа на этапе сборки запоминает не имя файла, а именно SONAME, и при запуске ищет файл, чье SONAME совпадает. На диске обычно стоит цепочка символических ссылок: libssl.so -> libssl.so.3 -> libssl.so.3.0.14. Ссылка без версии (libssl.so) нужна только линковщику при сборке (флаг -lssl) и часто лежит в -dev/-devel пакете. Версия в SONAME (тройка) меняется только при несовместимых изменениях API - это и есть гарантия, что libssl.so.3 не подсунут программе несовместимую libssl.so.4.
Команды и примеры
Посмотреть, от каких библиотек зависит бинарник и нашлись ли они:
Код: Выделить всё
ldd /usr/bin/ssh
linux-vdso.so.1 (0x00007ffd...)
libssl.so.3 => /lib/x86_64-linux-gnu/libssl.so.3 (0x00007f...)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f...)
/lib64/ld-linux-x86-64.so.2 (0x00007f...)Узнать SONAME конкретного файла и посмотреть его зависимости без запуска - через readelf или objdump:
Код: Выделить всё
readelf -d /lib/x86_64-linux-gnu/libssl.so.3 | grep -E 'SONAME|NEEDED'
0x000000000000000e (SONAME) Library soname: [libssl.so.3]
0x0000000000000001 (NEEDED) Shared library: [libcrypto.so.3]Код: Выделить всё
sudo ldconfig # перестроить /etc/ld.so.cache
sudo ldconfig -v # то же, с подробным выводом обрабатываемых каталогов
ldconfig -p | grep libssl # показать, что компоновщик знает о libsslКод: Выделить всё
echo "/opt/myapp/lib" | sudo tee /etc/ld.so.conf.d/myapp.conf
sudo ldconfig
ldconfig -p | grep myappВ Debian/Ubuntu принадлежность файла пакету и поиск -dev пакета:
Код: Выделить всё
dpkg -S /lib/x86_64-linux-gnu/libssl.so.3 # какой пакет дал файл
apt-get install libssl-dev # заголовки и .so-ссылка для сборкиКод: Выделить всё
rpm -qf /usr/lib64/libssl.so.3 # какой пакет владеет файлом
dnf install openssl-devel # devel-пакет с заголовками и ссылкойПеременная LD_LIBRARY_PATH добавляет каталоги в поиск для одного запуска, минуя кэш:
Код: Выделить всё
LD_LIBRARY_PATH=/opt/test/lib ./mybinaryЧастые грабли
- Поставили библиотеку (скопировали .so в /usr/local/lib) и забыли ldconfig - программа падает с "cannot open shared object file", хотя файл на месте. Кэш просто про него не знает.
- LD_LIBRARY_PATH прописали глобально в /etc/environment или в профиль "чтобы заработало". Теперь эта переменная наследуется всеми процессами, перекрывает системные библиотеки и ломает совсем другой софт. Она еще и опасна с setuid-бинарниками (для них ld.so ее игнорирует, и люди удивляются, почему "не работает").
- Скопировали один файл libfoo.so.1.2.3 без символической ссылки libfoo.so.1. Компоновщик ищет по SONAME (libfoo.so.1), а ссылки нет - "not found". ldconfig сам создает versioned-ссылку по SONAME, но только в каталогах, которые он обходит.
- Перепутали роли ссылок: думают, что для запуска нужна libfoo.so (без версии). Нет - она для сборки. Для запуска нужна именно versioned (libfoo.so.N).
- Запустили ldd на скачанном непонятно откуда бинарнике. Поскольку ldd может выполнять код через компоновщик, это потенциальный вектор атаки - для недоверенных файлов берите readelf -d или objdump -p.
- Подняли мажорную версию (libssl.so.1.1 -> libssl.so.3) и ждут, что старый софт подхватит новую. Не подхватит: разные SONAME - это намеренно несовместимые ABI, нужна пересборка или старый пакет совместимости.
- Выберите любой системный бинарник (например, /usr/bin/curl) и выполните ldd на нем. Запишите, сколько библиотек он тянет и где они лежат.
- Через readelf -d на одной из этих библиотек найдите ее SONAME и сравните с именем файла на диске.
- Создайте каталог /opt/lab/lib и скопируйте туда любую системную .so (например libz). Запустите ldconfig -p | grep libz - убедитесь, что нового пути там нет.
- Создайте файл /etc/ld.so.conf.d/lab.conf с путем /opt/lab/lib, выполните ldconfig и снова проверьте ldconfig -p.
- Сравните результат запуска программы с переменной LD_LIBRARY_PATH=/opt/lab/lib и без нее, посмотрите через ldd, какой путь выбирается.
- Уберите файл lab.conf, выполните ldconfig и проверьте, что кэш вернулся к исходному состоянию.
- Чем отличается статическая библиотека от разделяемой и в чем выигрыш разделяемой при выпуске security-обновления?
- Какой файл является кэшем компоновщика и какая команда его перестраивает?
- Что такое SONAME и почему программа ищет библиотеку по нему, а не по имени файла?
- Где правильно прописать дополнительный каталог поиска библиотек постоянно и какие два шага для этого нужны?
- Почему глобальное использование LD_LIBRARY_PATH считается плохой практикой и как она ведет себя с setuid-программами?
- Чем безопаснее заменить ldd при анализе недоверенного бинарника?