Разделяемые библиотеки в Linux

Создание разделяемых библиотек

Последнее обновление: 30.01.2024

Разделение кода приложения на несколько файлов упрощает работу над файлами. Например, определим файл print.asm с функцией, которая выводит на консоль некоторый текст:

global print

; Функция print выводит текст на консоль
; Параметры
; RDI - количество символов
; RSI - ссылка на строку
section .text
print:
    mov rdx, rdi
    mov rdi, 1
    mov rax, 1
    syscall
    ret

Эта функция принимает через регистр RSI ссылку на строку и через регистр RDI количество символов строки и с помощью системного вызова номер 1 (системный вызов write) выводит строку на консоль.

И пусть в файле app.asm расположен основной код программы, который использует функцию print:

global _start      
extern print
 
section .data 
message: db "Hello World!",10, 0
count equ $ - message

section .text
_start:
    mov rdi, count
    lea rsi, [message]
    call print

    mov rax, 60    
    syscall 

Здесь вызывается функция print, в которую передается строка из секции .data.

С помощью ассемблера NASM мы можем по отдельности по каждому файлу кода создать объектный файл и затем с помощью компоновщика мы сможем объединить их в один исполняемый бинарный файл

root@Eugene:~/asm# nasm -felf64 print.asm -o print.o
root@Eugene:~/asm# nasm -felf64 app.asm -o app.o
root@Eugene:~/asm# ld app.o print.o -o app
root@Eugene:~/asm# ./app
Hello World!
root@Eugene:~/asm#

В итоге создаются объектные файлы "print.o" и "app.o", и затем они объединяются в файл "app". Мы можем запустить файл app и насладиться выводом строки "Hello World!". Однако данный подход в определенных ситуациях может иметь недостатки. Прежде всего код обоих объектных файлов объединяется в один исполняемый файл, что естественно увеличивает объем финального файла программы, если файлов очень много.

Другой аспект - мы могли бы использовать функцию print в других программах, чтобы аналогичным образом выводить строку на консоль. И было бы не плохо, если бы мы могли один раз скомпилировать эту функцию и многократно подключать в произвольное количество программ.

И Linux также позволяет использовать другой подход - dynamic linking (динамическое связывание или динамическая компоновка). При динамическом связывании код библиотек не включается в приложение, и библиотеки остаются отдельными файлами, а код приложения просто ссылается на них. Они объединяются только во время (или иногда после) запуска программы. Это более гибкий подход, поскольку если код находится в библиотеках, библиотеки можно обновлять отдельно от приложений. Поэтому, если в библиотеке возникла проблема с безопасностью, единственное, что нужно изменить, — это саму библиотеку. Это также экономит дисковое пространство, поскольку отдельные функции не копируются в каждую программу, а существуют только в одном месте файловой системы. Эти библиотеки еще называются shared libraries (общие или разделяемые библиотеки).

В системе Linux общие библиотеки также называются shared objectsы (общими объектами) и имеют расширение .so. Стоит отметить, что аналогичные библиотеки есть в других системах: на Windows это динамические библиотеки с расширением .dll, на Mac - библиотеки с расширением .dylib (хотя также используется расширение .so). Рассмотрим как создать свою разделяемую библиотеку.

Создание разделяемой библиотеки

Возьмем ранее рассмотренный код файла print.asm

global print:function   ; формат объекта - function

; Функция print выводит текст на консоль
; Параметры
; RDI - количество символов
; RSI - ссылка на строку
section .text
print:
    mov rdx, rdi
    mov rdi, 1
    mov rax, 1
    syscall
    ret

Обратите внимание, что в начале файла при экспорте функции print указывается формат объекта - function. Без подобного указания к функции print нельзя будет обращаться динамически.

Скомпилируем его в объектный файл с помощью ассемблера NASM:

nasm -felf64 print.asm -o print.o

Далее из объектного файла получим разделяемую библиотеку. Для этого компоновщику передается параметр -shared:

ld -shared print.o -o print.so

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

Простестируем библиотеку. Для этого определим следующий файл app.asm:

global _start      
extern print
 
section .data 
message: db "Hello World!",10, 0
count equ $ - message

section .text
_start:
    mov rdi, count
    lea rsi, [message]
    call print

    mov rax, 60    
    syscall 

Здесь также вызывается функция print, в которую передаются адрес строки и количество символов.

С помощью ассемблера NASM скомпилируем объектный файл:

nasm -felf64 app.asm -o app.o

Далее скомпонуем его с динамической библиотекой:

ld --dynamic-linker=/lib64/ld-linux-x86-64.so.2 app.o print.so -o app

В данном случае компоновщику ld передается с помощью параметра --dynamic-linker динамический загрузчик/компоновщик, который применяется для загрузки/компоновки приложения с разделяемой библиотекой. На архитектуре х86-64 это файл /lib64/ld-linux-x86-64.so.2. И далее передаются файлы приложения и самой библиотеки.

Запустим скомпилированный файл app. Но несмотря на успешную компиляцию при выполнении программы мы столкнемся с ошибкой следующего вида::

root@Eugene:~/asm# ./app
./app: error while loading shared libraries: print.so: cannot open shared object file: No such file or directory

Как видно мы сталкиваемся с ошибкой - загрузчик не может найти библиотеку "print.so". Дело в том, что по умолчанию поиск библиотек выполняется в некоторых стандартных каталогах операционной системы, в частности, в каталоге /lib, либо в каталогах, которые установлены в переменной окружения LD_LIBRARY_PATH. Но наша библиотека располагается не в этих каталогах, а в текущем каталоге, где также находится исполняемый файл app. Поэтому передадим переменной LD_LIBRARY_PATH путь к текущему каталогу и повторно запустим приложение:

root@Eugene:~/asm# export LD_LIBRARY_PATH=.
root@Eugene:~/asm# ./app
Hello World!
root@Eugene:~/asm# 

Обратите внимание, что поиск начинается с каталогов, определенных в LD_LIBRARY_PATH, и продолжается до стандартных каталогов.

Полный консольный вывод:

root@Eugene:~/asm# nasm -felf64 print.asm -o print.o
root@Eugene:~/asm# ld -shared print.o -o print.so
root@Eugene:~/asm# nasm -felf64 app.asm -o app.o
root@Eugene:~/asm# ld --dynamic-linker=/lib64/ld-linux-x86-64.so.2 app.o print.so -o app
root@Eugene:~/asm# ./app
./app: error while loading shared libraries: print.so: cannot open shared object file: No such file or directory
root@Eugene:~/asm# export LD_LIBRARY_PATH=.
root@Eugene:~/asm# ./app
Hello World!
root@Eugene:~/asm# 

Роль загрузчика

При компоновке в файл ELF добавляется путь к динамическому загрузчику. В примере выше при компоновке приложения указывался встроенный динамический загрузчик /lib64/ld-linux-x86-64.so. Для этого применялась команда:

ld --dynamic-linker=/lib64/ld-linux-x86-64.so.2 app.o print.so -o app

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

root@Eugene:~/asm# ld app.o print.so -o app
root@Eugene:~/asm# ./app
bash: ./app: cannot execute: required file not found

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

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

root@Eugene:~/asm# ldd app
	linux-vdso.so.1 (0x00007ffc455f3000)
	print.so => ./print.so (0x00007efc8c478000)
root@Eugene:~/asm#

Здесь первая представляет библиотеку "linux-vdso.so.1". Это специальная библиотека, называемая библиотекой vDSO и предоставляемая самим ядром Linux. Эта библиотека позволяет быстро выполнять определенные функции ядра, такие как функции времени, для доступа к которым не требуется какой-либо особый уровень привилегий. Вызов этих функций позволяет получить общедоступную системную информацию без фактического вызова системного вызова.

Вторая запись представляет подгружаемую библиотеку "print.so"

Когда компилируется код приложения, некоторые символы/идентификаторы отсутствуют в самом основном коде (например, в коде выше это функция print). Затем компилятор/компоновщик проверяет список библиотек, которые надо скомпилировать вместе с основным кодом приложения, а также проверяет, что все символы/идентифкаторы, которых нет в основном коде приложения, находятся в одной из общих библиотек. Затем компоновщик заносит все общие библиотеки, на которые ссылается приложение, в исполняемый файл. При этом компоновщики не записывает в исполняемый файл загрузчик или библиотеку vDSO, поскольку они предоставляются/вызываются самим ядром.

Каждый найденный символ функции добавляется как в таблицу Procedure Linkage Table или PLT (таблица связей процедур), так и в таблицу Global Offset Table или GOT (глобальную таблицу смещений). Таблица PLT содержит инструкцию косвенного перехода к месту, указанному в GOT. Причем таблица GOT не содержит непосредственное местоположение функции. Вместо этого GOT содержит код-загрушку, который сообщает загрузчику найти функцию и заменить в GOT ее на фактическое значение функции (это называется отложенной загрузкой). Эта позволяет быстро загружать исполняемый файл, а по мере того, как программа продолжает работать, код-загрушка заменяется на реальное значение функции. Это позволяет не ждать замены кода-заглушки всех идентифкаторов, которые, возможно, никогда не будут использованы.

Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850