Shared library что это

Jenkins Pipeline Shared Libraries

Всем привет. В данной статье хочу поделиться знаниями, полученными в процессе автоматизации развертывания наших сервисов на различные серверы в разных дата-центрах.

Задача была следующей: есть определенный набор скриптов для развертывания сервисов, которые нужно запускать на каждом сервере каждого дата-центра. Скрипты выполняют серию операций: проверка статуса, вывод из-под load balancer’а, выпуск версии, развертывание, проверка статуса, отправка уведомлений через email и Slack и т.д. Это просто и удобно, но с ростом числа дата-центров и сервисов процесс выкатки новой версии может занять целый день. Кроме того, за некоторые действия отвечают отдельные команды, например, настройка load balancer’а. Также хотелось, чтобы управляющий процессом код хранился в общедоступном репозитории, дабы каждый член команды мог его поддерживать.

Решить задачу удалось с помощью Jenkins Pipeline Shared Libraries: этапы процесса разделились визуально на логические части, код хранится в репозитории, а осуществить доставку на 20 серверов стало возможно в один клик. Ниже приведен пример подобного тестового проекта:

Shared library что это. image loader. Shared library что это фото. Shared library что это-image loader. картинка Shared library что это. картинка image loader

Сейчас я расскажу и покажу примеры как этого достичь. Надеюсь эта статья поможет сохранить время другим разработчикам, а также буду рад дельным комментариям.

Создание библиотеки

Первое что необходимо сделать, создать собственную библиотеку, в которой будут храниться наши функции.
Создаем новый репозиторий, который должен иметь следующую структуру:

Shared library что это. image loader. Shared library что это фото. Shared library что это-image loader. картинка Shared library что это. картинка image loader
Директория src используется для Groovy классов, которые добавляются в classpath при выполнении Pipeline.

Директория vars используется в скриптах, которые определяют глобальные переменные, доступные из Pipeline.

Ниже приведен пример Groovy класса

В классах можно использовать любые возможности языка: создавать потоки, соединяться по FTP и т.д.

Ниже пример скрипта:

В скриптах можно использовать любые возможности языка и также получить переменные сборки и параметры Jenkins.

Вот пример репозитория с другими примерами.

Подключение репозитория

Следующий шаг — добавить наш репозиторий как глобальную библиотеку Pipeline.
Для этого в Jenkins нужно перейти: Manage Jenkins → Configure System (Настроить Jenkins → Конфигурирование системы). В блоке Global Pipeline Libraries, добавить наш репозиторий, как на картинке ниже:

Shared library что это. image loader. Shared library что это фото. Shared library что это-image loader. картинка Shared library что это. картинка image loader

Создание Pipeline

Последний шаг — создать Pipeline.

Наш Pipeline будет выглядеть следующим образом:

Наш Pipeline готов.

В следующий раз я покажу, как я настраивал параметризованную сборку, чтобы элегантно получить зависимые параметры из REST сервиса.

Источник

Разделяемые библиотеки (shared libraries) (lib share gcc ldd elf)

Ключевые слова: lib, share, gcc, ldd, elf, (найти похожие документы)

From: Павел Колодин
Newsgroups: email
Date: Mon, 31 Jul 2007 14:31:37 +0000 (UTC)
Subject: Разделяемые библиотеки (shared libraries)

* совершенствовать библиотеки, продолжая поддерживать программы,
использующие более старые версии этих библиотек, не совместимые с
новыми.

* заменять какие-то конкретные библиотеки или даже конкретные
функции в библиотеке во время выполнения какой-то конкретной
программы.

* проделывать всё это «на горячую», когда программа запущена и
какие-то библиотеки уже использует.

Соглашения.

Чтобы разделяемые библиотеки всё это умели, они должны соответствовать
определённым нормам. Нужно разбираться в том, что называется soname и
real name и как эти понятия связаны, также в том, где в файловой
системе разделяемые библиотеки должны лежать.

Далее. У библиотеки существует ещё один тип имени, который
используется системой, когда библиотеку запрашивают. Называется оно
linker name, которое на самом деле равно soname, но без указания
версии.

Способ содержания разделяемых библиотек в хозяйстве состоит в
различении их по именам. Каждое приложение должно помнить о
разделяемой библиотеке, которая ему нужна, помня только soname этой
библиотеки. Наоборот, когда вы разделяемую библиотеку создаёте, вы
имеете дело только с именем файла, содержащего более детальную
информацию о версии. Когда вашу библиотеку будут устанавливать, её
запишут в один из специально предназначенных для этого каталогов в
файловой системе и запустят программу ldconfig. Эта хитрая программа
смотрит в упомянутые специальные каталоги и создаёт soname-имена как
символические ссылки к реальным именам, публикуя новости с фронтов в
файле /etc/ld.so.cache (речь об нём позже).

ldconfig не создаёт linker-имена. Обычно это делается при
инициализации библиотеки и linter-имена создаются просто как
символические ссылки к последним актуальным soname-именам или к
последним актуальным real-именам. Я бы рекомендовал делать объектом
ссылки soname-имена, ибо меняются они реже, чем real-имена. А причина,
по которой ldconfig автоматически не простраивает linker-имена в том,
чтобы дать юзверям возможность запустить код, который выбирает
lintker-имя библиотеки на своё усмотрение.

Если вы хотите заменить только некоторые функции в библиотеке, оставив
какую-то часть библиотеки в строю, вам нужно записать имена библиотек
(файлы «*.o») с заменающими функциями в файл /etc/ld.so.preload. Так
вы поднимаете приоритет заменяющих библиотек над стандартными. Этот
предзагрузочный файл обычно используется для срочных заплаток.
Дистрибутивораспространители обычно плюют на него.

Чтобы как-то разгрузить процесс старта бинарей ELF от рытья в
каталогах, используется кеширование. Софтина ldconfig по дефолту
читает /etc/ld.so.conf, создаёт все нужные символические ссылки и
создаёт файл /etc/ld.so.cache, который далее используется другими
программами. Эта замануха охрененно ускоряет доступ к либам. Фишка в
том, что ldconfig запускается при добавлении DLL, удалении DLL или
когда список каталогов с DLL-ами поменялся. Запуск ldconfig-а часто
является одним из шагов, выполняемых пакетными менеджерами при
установке библиотеки. Далее при запусках софтин динамический загрузчик
юзает уже /etc/ld.so.cache, загружая либы по мере надобности.

Переменные окружения.

LD_LIBRARY_PATH

export LD_DEBUG=files
ls /

На что здесь следует обратить внимание:

* В некоторых случаях при создании объектного файла понадобится
включить опцию gcc «-Wl,-export-dynamic». Обычно таблица
динамических символов содержит только те символы, которые
используются динамическим объектом. Эта опция (при создании файла
ELF) добавляет все символы в таблицу динамических символов (зырь
ld(1) for more information). Эта опция хороша для создания
библиотек с «обратной зависимостью». Это когда динамически
загружаемая библиотека содержит неудовлетворённые символы, которые
по соглашению должны быть определены в самих приложениях, которые
эти библиотеки загружают. Для жизнеспособности идеи «обратной
зависимости», символы приложения, использующего такие библиотеки,
должны быть выполнены динамически доступными. Кстати, вы могли бы
дать опцию «-rdynamic» вместо «-Wl,export-dynamic», если
компиляция делается для Linux. Но согласно документации на ELF,
флаг «-rdynamic» не всегда работает для gcc в других системах.

Обычно, когда человек разрабатывает разделяемую библиотеку, он
тестирует её на узком круге каких-то приложений. Ему не нужно, чтобы
оставшееся большое количество приложений в системе видело её. Поэтому
при сборке программы, с которой тестируется библиотека, этой программе
можно показать, где библиотеку искать опцией линкера «rpath». Для gcc:

Если вы используете эту опцию при создании программы, с которой вы
библиотеку тестируете, то с переменной окружения LD_LIBRARY_PATH иметь
дело не придётся.

Сначала вы должны создать разделяемую библиотеку где-либо. Затем вам
понадобится создать необходимые символические ссылки, в частности
ссылку soname, показывающую на реальное имя (также ссылку soname, на
которой нет версии, то есть, soname оканчивающееся на «.so» для
пользователей, которым версия вокруг табуретки). Простейший способ:

Если своей библиотекой вы хотите заменить только часть функций, можете
сделать это так: создать замещающий объектный файл и установить
LD_PRELOAD. Функции из этого объектного файла заместят только те же
самые функции (не касаясь остальных).

Обычно вы можете заменять библиотеки без специальных предупреждений.
Если новая библиотека реализует изменения в API, тогда автор
библиотеки меняет soname. Таким образом, в одной системе могут
сосуществовать несколько библиотек, но каждая программа в системе
находит нужные ей. Если же программа сломалась при замене библиотеки,
при которой сохранилось старое её soname, вы можете заставить
программу использовать старую версию библиотеки. Для этого скопируйте
старую библиотеку куда-нибудь, затем переименуйте программу (например
прибавьте к старому имени «.orig»), после чего создайте небольшой
«обёрточный» скрипт. Он будет устанавливать переменные окружения,
сообщающие загрузчику альтернативный путь для поиска библиотек и затем
запускать переименованную копию программы. Вот такой:

#!/bin/sh
export LD_LIBRARY_PATH=/usr/local/my_lib:$LD_LIBRARY_PATH
exec /usr/bin/my_program.orig &

Пожалуйста, не вооружайтесь этим способом запуска программ, когда
пишете свои собственные программы. Попытайтесь гарантировать, что ваши
новые библиотеки совместимы со старыми версиями или не забывайте
увеличивать номер версии в soname-имени при каждом изменении,
нарушающем совместимость. Описанный метод рассматривайте как помощь в
исключительных ситуациях.

Просмотреть список разделяемых библиотек, используемых программой
возможно с помощью ldd(1). Например так:

В основном вы увидите список soname-имён в парах с полными именами
файлов. Практически все ваши программы будут зависеть от:

* /lib/ld-linux.so.N (где N больше единицы, но обычно 2). Эта
библиотека загружает все другие библиотеки.

* libc.so.N (где N больше или равно шести). Это стандартная
библиотека языка C. Даже другие языки программирования часто
используют libc (как минимум для реализации своих библиотек).

Но не запускайте ldd для тех программ, которым не доверяете. Как ясно
изложено в руководстве по ldd(1), она выполняет свои функции,
устанавливая специальные переменные окружения (для ELF объектов
LD_TRACE_LOADED_OBJECTS), после чего запускает программу. Таким
образом ненадёжной программе может быть предоставлена возможность
запуска произвольного кода, от имени пользователя, выполняющего ldd.

Несовместиомость библиотек.

Когда новая версия библиотеки бинарно несовместима со старой, тогда
нужно менять soname. Для языка C есть четыре основных причины, по
которым библиотека может оказаться бинарно несовместимой.
1. Так меняется поведение функции, что она больше не соответствует
своей спецификации.
2. Меняется формат выдачи результатов. Исключение: добавление новых
полей в конец структуры или что-то такое.
3. Функция исчезла.
4. Меняется формат входных данных для функции.

Избегайте этих явлений, чтобы добиться бинарной совместимости. Другими
словами, вы можете сохранять Application Binary Interface (ABI)
библиотеки неизменным, избегая перечисленные случаи. Например,
добавляйте новые функции без удаления старых. Добавляйте поля в
структуры данных так, чтобы этого не чувствовали существущие программы
— например, в конец. А ещё можно делать новые поля структуры
опционально включаемыми.

Для С++ и других языков, поддерживающих шаблоны или виртуальные
функции (полиморфизм во время выполнения) всё куда хитрее. Всё хитро
так, как описано выше, но возведённое ещё в некоторую степень (-;
Строго говоря, ничего нового, просто код, порождённый компилятором C++
может вызывать функции очень удивительным образом. Вот список того,
чего нельзя делать в С++, надеясь на сохранение бинарной
совместимости:

Источник

C: создание и применение shared library в Linux

Библиотека – это файл, содержащий скопилированный код из нескольких объектных файлов в один файл библиотеки, который может содержать функции используемые другими программами.

Библиотеки могут быть статичными (static) и динамическими или разделяемыми (dynamic, shared).

Ниже – краткий пример создания и применения shared library на C в Linux.

Доступ к общей библиотеке может осуществляться по нескольким именам:

Версия для общей бибилиотеки меняется в случае, когда изменения в коде этой бибилиотеки делают её несовместимой с предыдущими версиями, например – если из библиотеки была убрана какая-то функция ( libpthread.so.1 )

Минорная версия меняется, если изменения не затронули совметимость библиотеки, например – какой-то фикс в одной из функций. В таком случае версия останется прежней, а изменится только минорная часть ( libpthread.so.1.1 ).

Такое соглашение об именах версий библиотек позволяет существование разных версий одной библиотеки в одной системе.

Программа, которая будет линковаться с этой бибилиотекой, не будет привязана к определённому файлу с последней версией библиотеки. Вместо этого, после установки последней версии – все связанные программы будут использовать её.

Создание библиотеки

Создадим простой файл libhello.c с одной функцией:

Создаём заголовочный файл библиотеки libhello.h с прототипом функции:

Приступаем к сборке библиотеки.

Источник

Position-independent code (PIC) в разделяемых библиотеках

Shared library что это. image loader. Shared library что это фото. Shared library что это-image loader. картинка Shared library что это. картинка image loader

Привет. Меня зовут Марко, и я системный программист в Badoo. Я очень люблю досконально разбираться в том, как работают те или иные вещи, и тонкости работы разделяемых библиотек в Linux не исключение. Я представляю вам перевод именно такого разбора. Приятного чтения.

Я уже описывал необходимость специальной обработки разделяемых библиотек во время загрузки их в адресное пространство процесса. Если кратко, то, когда линкер создает разделяемую библиотеку, он заранее не знает, в каком месте в памяти она будет загружена. Из-за этого делать ссылки на данные и код внутри библиотеки проблематично: непонятно, как создавать ссылку, чтобы она указывала в правильное место после того, как библиотека будет загружена.

В Linux и в ELF существует два главных способа решить эту проблему:

Релокацию во время загрузки мы уже рассмотрели. А сейчас рассмотрим второй подход – PIC.

Изначально я планировал рассказывать и о x86, и о x64 (также известной как x86-64), но статья всё росла и росла, и я решил, что нужно быть более практичным. Так что в этой статье я расскажу только о x86, а о x64 речь пойдёт в другой (я надеюсь, гораздо более короткой). Я взял более старую архитектуру x86, так как в отличие от x64 она разрабатывалась без учета PIC, и реализация PIC в ней чуть более сложная.

Проблемы релокации во время загрузки

Как мы увидели в предыдущей статье, релокация во время загрузки – очень простой и прямолинейный метод. И он работает. Но PIC гораздо более популярен на данный момент и является рекомендуемым способом создания разделяемых библиотек. Почему, спросите вы?

У релокации есть несколько проблем: она занимает время и секция text (содержащая машинный код) уже не подходит для разделения между процессами.

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

Ну, и несколько слов про проблему невозможности расшарить text-секцию. Она несколько серьёзнее. Одна из главных задач существования разделяемых библиотек – сэкономить на памяти. Некоторые библиотеки используются несколькими приложениями одновременно. Если text-секция (где находится машинный код) может быть загружена в память только один раз (и затем добавлена в другие процессы с помощью mmap), то можно сэкономить довольно большое количество оперативной памяти. Но это невозможно при использовании релокации, так как text-секция должна быть изменена во время загрузки, чтобы подставить правильные указатели для конкретного процесса. Получается, для каждого процесса, использующего библиотеку, приходится держать полную копию этой библиотеки в памяти [1]. Никакого разделения не происходит.

Более того, держать text-секцию с правами на запись (а она должна быть с правами на запись, чтобы загрузчик мог подкорректировать ссылки) – плохо с точки зрения безопасности. Сделать эксплоит в этом случае гораздо легче.

Как мы увидим далее, PIC практически полностью решает эти проблемы.

Введение

Идея, которая стоит за PIC, очень проста – добавление в код промежуточного слоя для всех ссылок на глобальные объекты и функции. Если по-умному использовать некоторые артефакты процессов линковки и загрузки, можно сделать раздел text действительно не зависящим от адреса, куда его положат; мы сможем отобразить сегмент с помощью mmap на самые разные адреса в адресном пространстве процесса, и нам не понадобится изменять в нём ни один бит. В следующих нескольких разделах я покажу, как можно этого достичь.

Ключевая идея №1. Смещение между секциями text и data

Одна из ключевых идей, на которых основывается PIC, – смещение между секциями text и data, размер которого известен линкеру во время линковки. Когда линкер объединяет несколько объектных файлов, он собирает их секции вместе (к примеру, все секции text объединяются в одну большую секцию text). Таким образом, линкеру известны и размеры секций, и их относительное расположение.

Например, сразу за секцией text может следовать секция data, и в этом случае смещение от любой инструкции из секции text до начала секции data будет равняться размеру секции text минус смещение до данной инструкции от начала секции text. И все эти размеры и смещения известны линкеру.

Shared library что это. image loader. Shared library что это фото. Shared library что это-image loader. картинка Shared library что это. картинка image loader

На диаграмме выше секция code была загружена по некоторому адресу (неизвестному нам на момент линковки) 0xXXXX0000 (иксы буквально означают «всё равно, что там»), а секция data – сразу после нее по адресу 0xXXXXF000. В этом случае, если какая-то инструкция по смещению 0x80 в секции code захочет указать на что-то в секции data, линкер знает относительное смещение (0xEF80 в данном случае) и может добавить его в инструкцию.

Заметьте, что ничего не изменится, если другая секция будет замаплена между секциями code и data или если секция data будет расположена до секции code. Поскольку линкер знает размеры всех секций и решает, куда их положить, идея остаётся неизменной.

Ключевая идея №2. Делаем так, чтобы смещение относительно IP работало на x86

Всё, о чём было рассказано выше, работает, если мы вообще можем воспользоваться относительными смещениями. Ведь ссылки на данные (например, как в инструкции MOV) на x86 требуют абсолютные адреса. Так что же нам делать?

Если у нас есть относительный адрес, а нужен абсолютный, нам не хватает значения указателя команд, или счётчика команд (instruction pointer – IP). Ведь по определению относительный адрес относителен по отношению к IP. На x86 не существует инструкции для получения IP, но мы можем воспользоваться простой хитростью. Вот небольшой ассемблерный псевдокод, который её демонстрирует:

Что здесь происходит:

Глобальная таблица смещений (GOT)

Теперь у нас есть всё, чтобы, наконец, рассказать о том, как реализована не зависящая от позиции адресация на x86. А реализована она с помощью глобальной таблицы смещений (global offset table или GOT).

GOT – это просто таблица с адресами, которая находится в секции data. Предположим, что какая-то инструкция в секции code хочет обратиться к переменной. Вместо того, чтобы обратится к ней через абсолютный адрес (который потребует релокации), она обращается к записи в GOT. Поскольку GOT имеет строго определённое место в секции data, и линкер знает о нём, это обращение тоже является относительным. А запись в GOT уже содержит абсолютный адрес переменной:

Shared library что это. image loader. Shared library что это фото. Shared library что это-image loader. картинка Shared library что это. картинка image loader

В псевдоассемблере это будет выглядеть как замена абсолютной адресации.

на адресацию через регистр и небольшую прокладку:

Каким-то образом найдём адрес GOT и положим его в ebx:

lea ebx, ADDR_OF_GOT

Предположим, адрес переменной (ADDR_OF_VAR) находится по смещению 0x10 в GOT. В этом случае следующая инструкция положит ADDR_OF_VAR в edx:

mov edx, DWORD PTR [ebx + 0x10]

Наконец, обратимся к переменной и положим её значение в edx:

mov edx, DWORD PTR [edx]

Таким образом мы избавились от релокации в секции code путём перенаправления обращений через GOT. Но мы также создали релокацию в секции data. Почему? Потому что GOT в любом случае должна содержать абсолютный адрес переменной, чтобы вышеописанная схема работала. Так где же профит?

А профита, оказывается, много. Релокация в data-секции сопряжена с гораздо меньшим количеством проблем, чем релокация в секции code. Этому есть две причины, соответствующие двум проблемам, возникающим при релокации во время загрузки.

PIC с обращениями через GOT (пример)

Сейчас я покажу полноценный пример, который демонстрирует механику PIC:

Давайте посмотрим, что сгенерировал компилятор, фокусируясь на функции ml_func:

Я буду указывать на адрес инструкций (самое левое число в выводе). Этот адрес – это смещение от того адреса, на который была замаплена библиотека.

Давайте достанем калькулятор и проверим компилятор. Ищем myglob. Как я уже упоминал выше, вызов __i686.get_pc_thunk.cx кладёт адрес следующей инструкции в ecx. Это 0x444 [2]. Следующая инструкция прибавляет к нему 0x1bb0 – и в результате в ecx мы получим 0x1ff4. Наконец, чтобы получить элемент GOT, который содержит адрес myglob, делаем [ecx — 0x10]. Элемент, таким образом, имеет адрес 0x1fe4, и это первый элемент в GOT, согласно заголовку секции.

Но есть одна вещь, которой нам пока не хватает. Как именно адрес myglob оказывается в элементе GOT по адресу 0x1fe4? Вспомните, что я упоминал релокацию, так что давайте её найдём:

Вот она, релокация для myglob, указывающая на адрес 0x1fe4, как мы и ожидали. Релокация имеет тип R_386_GLOB_DAT, который просто говорит загрузчику: «Положи реальное значение симпола (то есть его адрес) по данному смещению». Теперь всё понятно. Осталось только посмотреть как, это всё выглядит при загрузке библиотеки. Мы можем это сделать, создав простой бинарник (driver), который линкуется к libmlpic_dataonly.so и вызывает ml_func, и запустив его через gdb.

Дебаггер вошёл в ml_func и остановился на IP 0x0013144a [4]. Мы видим, что ecx имеет значение 0x132ff4 (адрес инструкции плюс 0x1bb0). Заметьте, что в данный момент, во время работы, это всё абсолютные адреса – библиотека уже загружена в адресное пространство процесса.

Так, элемент GOT с myglob должен быть на [ecx — 0x10]. Давайте проверим:

То есть мы ожидаем что 0x0013300c – это адрес myglob. Проверяем:

Вызов функций в PIC

Итак, мы увидели, как работает PIC для адресов на данные. Но что насчёт функций? Теоретически тот же самый способ будет работать и для функций. Вместо того, чтобы call содержал адрес функции, пусть он содержит адрес элемента из GOT, а элемент уже будет заполнен при загрузке.

Но вызов функций в PIC работает не так, в реальности всё несколько сложнее. Прежде чем я объясню, как именно, в двух словах расскажу о мотивации выбора такого механизма.

Оптимизация: «ленивый» байндинг

Когда разделяемая библиотека использует какую-либо функцию, реальный адрес этой функции ещё не известен. Определение реального адреса называется байндинг (binding), и это то, что загрузчик делает, когда загружает разделяемую библиотеку в адресное пространство процесса. Байндинг не тривиален, так как загрузчику нужно искать символы функций в специальных таблицах [5].

Таким образом, определение реального адреса каждой функции занимает какое-то время (не так много времени, но так как вызовов функций может быть значительно больше, чем данных, длительность этого процесса увеличивается). Более того, в большинстве случаев это делается зря, так как при обычном запуске программы будет вызвана лишь малая часть функций (подумайте, как много вызовов требуются только при возникновении ошибок или каких-то специальных условий).

Чтобы ускорить этот процесс, и была придумана хитрая схема «ленивого» байндинга. «Ленивая» — это общий термин оптимизаций в IT, когда какая-либо работа откладывается до самого последнего момента. Смысл этой оптимизации в том, чтобы не делать лишнюю работу, которая может быть и не нужна. Примерами такой «ленивой» оптимизации являются механизм copy-on-write и «ленивые» вычисления.

«Ленивая» схема реализована путём добавления ещё одного уровня адресации – PLT.

Procedure Linkage Table (PLT)

PLT – это часть секции text в бинарнике, состоящая из набора элементов (один элемент на одну внешнюю функцию, которую вызывает библиотека). Каждый элемент в PLT – это небольшой кусок выполняемого машинного кода. Вместо вызова функции напрямую вызывается кусок кода из PLT, который уже сам вызывает функцию. Такой подход часто называют «трамплином». Каждый элемент из PLT имеет собственный элемент в GOT, который содержит реальное смещение для функции. После того как загрузчик определит её, конечно.

На первый взгляд всё довольно запутанно, но я надеюсь, что скоро всё станет более понятно – в следующих разделах я расскажу о деталях с диаграммами.

Как я уже упоминал, PLT позволяет делать «ленивое» определение адресов функций. В тот момент, когда разделяемая библиотека впервые загружена, реальные адреса функций ещё не определены:

Shared library что это. image loader. Shared library что это фото. Shared library что это-image loader. картинка Shared library что это. картинка image loader

Что происходит после того, как func вызвана первый раз:

После первого раза диаграмма выглядит немного по-другому:

Shared library что это. image loader. Shared library что это фото. Shared library что это-image loader. картинка Shared library что это. картинка image loader

Заметьте, что GOT[n] теперь указывает на реальную func [7] вместо того чтобы указывать обратно в PLT. Так что когда функция вызывается повторно, происходит следующее:

Другими словами, func теперь попросту вызывается без использования метода «определения» и без лишнего прыжка. Этот механизм позволяет делать «ленивое» определение адресов функций и не делать никакого определения для тех функций, которые не вызываются.

Обратите внимание, библиотека при этом абсолютно не зависит от адреса, по которому она будет загружена, ведь единственное место, где используется абсолютный адрес, – это GOT, а она находится в секции data и будет релоцирована во время загрузки загрузчиком. Даже PLT не зависит от адреса загрузки, так что она может находиться в секции text, доступной только для чтения.

Я не углубляюсь в детали работы метода «определения», но это и не так важно. Метод – это просто кусок низкоуровневого кода в загрузчике, который делает своё дело. Аргументы, которые готовятся перед вызовом метода, дают ему знать, адрес какой функции необходимо определить и куда следует поместить результат.

PIC с вызовом функции через PLT и GOT (пример)

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

Вот код разделяемой библиотеки:

Этот код будет скомпилирован в libmlpic.so, и мы сфокусируемся на вызове ml_util_func из ml_func. Дизассемблируем ml_func:

Вспомните, что каждый элемент PLT состоит из трёх частей:

Метод «определения» (элемент 0 в PLT) находится по адресу 0x370, но он нас сейчас не интересует. Гораздо интересно посмотреть, что содержит GOT. Для этого нам снова понадобится калькулятор.

Трюк для получения текущего IP в ml_func был сделан по адресу 0x483, и к нему мы прибавили 0x1b71. Так что GOT находится по адресу 0x1ff4. Мы можем увидеть, что там, с помощью readelf [8]:

Запись в GOT для ml_util_func@plt, похоже, находится по смещению +0x14, или 0x2008. Судя по выводу выше, слово по этому адресу имеет значение 0x3a6, а это адрес push-инструкции в ml_util_func@plt.

Чтобы помочь загрузчику сделать своё дело, в GOT добавлена запись с адресом места в GOT, куда нужно записать адрес ml_util_func:

Последняя строчка означает, что загрузчику нужно положить адрес символа ml_util_func в 0x2008 (а это, в свою очередь, элемент GOT для данной функции).

Было бы классно увидеть, как происходит эта модификация в GOT. Для этого воспользуемся GDB ещё раз.

Мы сейчас находимся перед первым вызовом ml_util_func. Вспомните, что адрес GOT находится в ebx. Посмотрим, что там:

Смещение для нужного нам элемента находится по адресу [ebx+0x14]:

Да, заканчивается на 0x3a6. Выглядит правильно. Теперь давайте шагнём до вызова ml_util_func и посмотрим ещё раз:

Значение по адресу 0x133008 поменялось. Получается, что 0x0013146c – реальный адрес ml_util_func, который был положен туда загрузчиком:

Управляем определением адреса загрузчиком

Сейчас самое время упомянуть о том, что процесс «ленивого» определения адреса, который осуществляется загрузчиком, может быть настроен несколькими переменными окружения (а также соответствующими аргументами для линкера ld). Иногда эти настройки могут быть полезны для дебаггинга или каких-то специальных требований к производительности.

Переменная LD_BIND_NOW, когда она определена, говорит загрузчику определять все адреса при старте, а не «лениво». Её работу можно проверить, посмотрев вывод gdb для примера выше в том случае, когда она задана. Мы увидим, что элемент из GOT для ml_util_func содержит реальный адрес функции ещё до первого вызова функции.

Напротив, LD_BIND_NOT говорит загрузчику не обновлять GOT никогда. То есть каждый вызов функции в этом случае будет идти через метод «определения».

Загрузчик настраивается и некоторыми другими флагами. Я рекомендую изучить man ld.so. Там много интересной информации.

Стоимость PIC

Мы начали разговор с проблемы релокации во время работы и решения этой проблемы PIC. Но сам PIC, увы, тоже не без проблем. Одна из них – стоимость лишней косвенной адресации. Это лишнее обращение к памяти при каждом обращении к глобальной переменной или функции. «Масштаб бедствия» зависит от компилятора, процессорной архитектуры и собственно приложения.

Другая, менее очевидная, проблема – использование дополнительных регистров для реализации PIC. Чтобы не определять адрес GOT слишком часто, компилятору имеет смысл сгенерировать код, который будет хранить адрес в регистре (например, ebx). Но это значит, что целый регистр уходит только на GOT. Для RISC-архитектур, у которых обычно много регистров общего пользования, это не такая уж большая проблема, чего не скажешь об архитектурах типа x86, у которых мало доступных регистров. Использование PIC означает на один регистр меньше, а значит, нужно будет делать больше обращений к памяти.

Заключение

Теперь вы знаете, что такое код, не зависящий от адреса, и как он помогает создавать разделяемые библиотеки с разделяемой, доступной только для чтения, секцией text.

У PIC есть плюсы и минусы по сравнению с релокацией во время работы, и результат будет зависеть от множества факторов (в частности, от архитектуры процессора, на котором будет работать программа).

Однако, несмотря на недостатки, PIC становится всё более популярным подходом. Некоторые неIntel-архитектуры, такие как SPARC64, требуют обязательного использования PIC для разделяемых библиотек, а многие другие (например, ARM) – имеют IP-зависимую адресацию, чтобы сделать PIC более эффективным. И то, и другое верно для наследницы x86 – x64.

Мы не фокусировались на проблемах производительности и архитектурах процессора. Моя задача была в том, чтобы рассказать, как работает PIC. Если объяснение было недостаточно «прозрачным», дайте мне знать в комментариях – и я постараюсь дать больше информации.

Источник

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *