Библиотека сайта rus-linux.net
Компоновщики и загрузчики. Часть 2
Оригинал: Linkers and Loaders
Автор: Sandeep Grover
Дата: 26 ноября 2002
Перевод: Александр Тарасов aka oioki
Дата перевода: 4 декабря 2008
Это продолжение. Первую часть статьи можно прочесть здесь.
Связывание со статическими библиотеками
Статическая библиотека - это набор нескольких объектных файлов одного и того же типа. Библиотеки хранятся на диске в виде архива. Помимо самих объектных файлов этот архив содержит индексную информацию, по которой впоследствии будет легче найти определенные символы. Каждый ELF-архив начинается с магической последовательности из 8 символов: !<arch>\n
, здесь \n
- символ новой строки.
Статические библиотеки можно передавать компоновщику в виде аргументов командной строки, при этом из библиотек будут взяты лишь требуемые объектные модули, а лишние проигнорированы. В UNIX-системах библиотека libc.a содержит все стандартные функции языка C, включая printf и fopen, используемые большинством программ.
gcc foo.o bar.o /usr/lib/libc.a /usr/lib/libm.a
Библиотека libm.a - это стандартная математическая библиотека UNIX-систем. Она содержит объектные модули для вычисления квадратного корня, тригонометрических функций и т.д.
При использовании статических библиотек, в процессе разрешения символа компоновщик просматривает перемещаемые объектные файлы и архивы слева направо, как указано в командной строке. При этом компоновщик модифицирует три множества: множество O, перемещаемые объектные файлы, которые попадут в исполняемый файл; множество U, неразрешенные символы; и множество D, состоящее из символов, определенных в одном из обработанных модулей. В самом начале все три множества пусты.
- Для каждого входного аргумента командной строки, компоновщик определяет, является ли он объектным файлом или архивом. Если вход - это перемещаемый объектный файл, тогда компоновщик добавляет его во множество O, обновляет множества U и D и переходит к следующему входному файлу.
- Если на входе архив, компоновщик просматривает его содержимое на предмет наличия определений для неразрешенных до сих пор символов (из множества U). Если в каком-либо модуле библиотеки встречается ранее неразрешенный символ, то он добавляется во множество O, а множества U и D обновляются символами, найденными в этом модуле. Этот процесс выполняется для всех модулей библиотеки.
- После обработки всех входных файлов по упомянутым двум шагам, если множество U содержит какой-либо элемент (т.е. есть неразрешенные символы), компоновщик выдает ошибку и завершает свою работу. Если же U пусто, тогда он объединяет и переразмещает объектные файлы в множестве O и собирает итоговый исполняемый файл.
Это объясняет, почему статические библиотеки указываются в конце списка аргументов. Особое внимание нужно уделять случаям, когда зависимости между библиотеками приобретают циклический характер. Входные библиотеки должны быть упорядочены таким образом, чтобы каждый упоминаемый символ присутствовал в последующих библиотеках. Также если неразрешенный символ определен в более чем одной статической библиотеке, тогда определение берется из первой.
Релокация
Итак, теперь компоновщик разрешил все символы, и каждый упоминаемый символ имеет точно одно определение. На данном этапе компоновщик начинает процесс релокации (переразмещения), включающий в себя следующие два шага:
-
Релокация секций и определений символов. Компоновщик соединяет все секции одного типа в одну новую секцию. К примеру, компоновщик соединяет все секции
.data
всех входных перемещаемых объектных файлов в одну секцию.data
итогового исполняемого файла. То же самое проделывается для секции.code
. Далее компоновщик назначает адреса памяти для новых объединенных секций, для каждой секции, указанной во входном модуле и для каждого отдельного символа. После этого каждая инструкция и глобальная переменная в программе имеет свой уникальный адрес. - Релокация ссылок на символы внутри секций. На этом этапе компоновщик модифицирует каждую ссылку на символ, содержащуюся в секциях кода и данных так, чтобы они указывали на верные адреса.
Когда ассемблер встречает неразрешенный символ, он создает для него запись релокации и помещает ее в секции .relo.text
/.relo.data
. Запись релокации содержит информацию о том, как разрешить ссылку. Типичная запись релокации ELF содержит следующие элементы:
- Смещение (Offset) - смещение релоцируемой ссылки внутри секции. Для перемещаемого объектного файла это означает байтовое смещение от начала секции до единицы хранения, которая подвергается релокации.
- Символ - символ, на который должна будет указывать изменяемая ссылка. Это индекс производимой релокации в таблице символов.
-
Тип - тип релокации, обычно это
R_386_PC32
, что означает относительную PC-адресацию. ЗначениеR_386_32
означает абсолютную адресацию.
Компоновщик проходит по всем записям релокации, присутствующим в модулях и переразмещает неразрешенные символы, в зависимости от их типа. К примеру, для типа R_386_PC32 адрес релокации вычисляется как S+A-P; для типа R_386_32 адрес вычисляется как S+A. Здесь S означает значение символа из записи релокации, P - смещение секции либо адрес единицы хранения, которую переразмещаем (он вычисляется по значению смещения из записи релокации), а A - это адрес, который требуется для вычисления значения переразмещаемого поля.
Динамическое связывание: разделяемые библиотеки
У статических библиотек есть один существенный недостаток. Рассмотрим, к примеру, стандартные функции, такие как printf и scanf. Они используются во многих программах. Теперь представьте, что у вас работает 50-100 процессов, и каждый процесс хранит в памяти свою копию исполняемого кода для printf и scanf. Таким образом оперативная память расходуется по сути впустую. Здесь нам и пригодятся разделяемые библиотеки, устраняющие этот недостаток статических библиотек. Разделяемая библиотека - это объектный модуль, загружаемый по произвольному адресу памяти при запуске программы. Разделяемые библиотеки иногда называют разделяемыми объектами. В большинстве UNIX-систем они имеют расширение .so; в системе HP-UX используется расширение .sl, а в ОС от фирмы Microsoft они называются DLL (dynamic link libraries - библиотеки динамического связывания).
Чтобы получить разделяемый объект, компилятор должен быть вызван со специальным ключом:
gcc -shared -fPIC -o libfoo.so a.o b.o
Эта команда указывает компилятору, что нужно создать разделяемую библиотеку, libfoo.so
, состоящую из объектных модулей a.o
и b.o
. Ключ -fPIC
указывает компилятору на то, что нужно создать код, независимый от адреса (position independent code, PIC).
Теперь представьте, что объектный модуль с функцией main называется bar.o
, и он зависит от объектных файлов a.o
и b.o
. В этом случае компоновщик нужно вызывать с помощью команды:
gcc bar.o ./libfoo.so
Будет создан исполняемый файл a.out
, причем таким образом, что связывание с библиотекой libfoo.so
будет происходить при запуске программы. Иными словами, a.out
не будет содержать в себе кода объектных модулей a.o
и b.o
, в отличие от предыдущего случая, когда мы компилировали их в статическую библиотеку, а не в разделяемую. Исполняемый файл содержит информацию о релокации и таблицу символов, что позволяет программе разрешить ссылки на код и данные библиотеки libfoo.so
во время ее запуска. Поэтому a.out
можно назвать частично исполняемым файлом, ведь у него есть зависимость от libfoo.so
. Также в файле хранится секция .interp
, содержащая имя динамического компоновщика, который в Linux-системах сам по себе является разделяемым объектом (ld-linux.so
). При загрузке программы в память загрузчик передает управление динамическому компоновщику. Динамический компоновщик содержит стартовый код, отображающий разделяемые библиотеки на адресное пространство программ. Далее выполняются следующие шаги:
-
переразмещает текст и данные библиотеки
libfoo.so
в сегмент памяти; -
переразмещает все ссылки в
a.out
на символы, определенные вlibfoo.so
.
Наконец, динамический компоновщик передает управление приложению. С этого момента размещение разделяемого объекта в памяти уже жестко зафиксировано.
Загрузка разделяемых библиотек из приложений
Разделяемые библиотеки могут быть загружены из приложения не сразу, а в процессе ее выполнения. Приложение посылает запрос динамическому компоновщику на загрузку и связывание разделяемой библиотеки. При этом приложение даже может не содержать в себе вкомпилированных ссылок на эти разделяемые библиотеки. В Linux, Solaris и в других системах есть ряд функций, позволяющих таким образом загружать разделяемые объекты. В Linux существуют системные вызовы dlopen, dlsym и dlclose, позволяющие соответственно загрузить разделяемый объект, получить указатель на какой-либо содержащийся в нем символ, закрыть объект. В системах Windows есть функции LoadLibrary и GetProcAddress - это соответствующие замены вызовам dlopen и dlsym.
Программы для работы с объектными файлами
Ниже приведен список программ Linux, с помощью которых можно исследовать объектные/исполняемые файлы.
- ar: создает статические библиотеки.
- objdump: самый важный инструмент; предназначен для вывода всей инфорамции об объектном бинарном файле.
- strings: выводит все печатаемые строки, хранящиеся в бинарном файле.
- nm: выводит список символов, определенных в таблице символов объекта.
- ldd: выводит список разделяемых библиотек, от которых зависит объект.
- strip: удаляет таблицу символов.
Что еще можно почитать
- Linkers and Loaders, автор John Levine.
- Linkers and Libraries Guide от компании Sun