Найти тему
CodeDream

Немного о линковки статических библиотек

Оглавление

Базовые понятия, кратко

Совершенно неожиданное открытие сделал недавно. Оказывается порядок появления файлов на входе линковщика важен, если мы говорим о линковки статических библиотек. Для того что бы понять почему так происходит, нужно понимать принцип работы линковщика. Объектные файлы как предоставляют(экспортируют) символы(имена функций, переменных), так и ожидают(импортируют) их. Рассмотрим небольшой пример:

int imported(int);

static int internal(int x) {
return x * 2;
}

int exported(int x) {
return imported(x) * internal(x);
}

Теперь скомпилируем этот файл:

gcc -c test.c
nm test.o


000000000000000e T exported
U imported
0000000000000000 t internal

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

Процесс линковки

Вызовите ли вы его или же линкер будет вызван компилятором, так или иначе на вход линковщику будут поданы объектные файлы и/или библиотеки. Порядок появления файлов на входе линковщика и есть порядок линковки. Вот что делает линковщик:

  • Линковщик поддерживает таблицу символов. Эта таблица помимо всего содержит два списка
  • Список символов экспортируемых всеми объектными файлами и библиотеками, обработанными на данный момент.
  • Список не определенных символов, которые обработанные на данный момент объектные файлы и библиотеки запросили. Имеются ввиду те символы что еще не были найдены.
  • Когда линковщик обрабатывает новый объектный файл, он смотрит на:
  • Экспортируемые символы - они добавляются в список экспортируемых символов. Если какой-то из них находился в списке не определенных символов, то он оттуда убирается. Если какой-то из символов уже находится в списке экспортируемых символов, то мы получаем ошибку: множественное объявление(multiple definition).
  • Импортируемые символы - эти добавляются в список не определенных символов, если их не удалось найти в списке экспортируемых.
  • Когда линковщик обрабатывает новую библиотеку, то все становиться несколько интересней. Линковщик проходится по всем объектным файлам внутри библиотеки и для каждой он смотрит в первую очередь экспортируемые символы:
  • Если какой-то из экспортируемых символов находится в списке не определенных, тогда объектный файл добавляется к линкуемым. В ином случае следующий шаг пропускается.
  • Если объектный файл был добавлен, то он обрабатывается как обычный - экспортируемые и импортируемые символы добавляются в соотвествующие таблицы.
  • Наконец,  если какой-либо  из объектных файлов библиотеки был добавлен, то библиотека сканируется заново - возможно что какие-то из импортируемых символов могут быть найдены в других объектных файлах в этой библиотеки.

Когда линковщик заканчивает, он смотрит таблицы символов. Если таблица не определенных символов не пуста линковщик выкинет исключение "undefined reference".

Стоит заметить что после того как библиотека будет просканирована, линковщик к ней уже возвращаться не будет. Даже если она экспортирует символы которые понадобятся библиотекам которые будут просканированы  позже нее. Единственный случай при котором линковщик пересканирует библиотеку был описан выше - когда какой-либо из объектных файлов этой библиотеки был слинкован, тогда библиотека сканируется заново. Поведение линковщика можно изменить флагами линковки(см. --start-group --end-group в руководстве ld).

Так же стоит заметить что линковщик добавляет не все объектные файлы библиотеки, а только те что содержат необходимые экспортируемые символы(те что есть в списке неопределенных символов). Это очень важная особенность линковки статических библиотек, которая часто применяется, допустим библиотека C(libc) сформирована так что объектный файл содержит только одну функцию. Например, если ваша программа использует только strlen, то только этот объектный файл будет слинкован с вашим приложением/библиотекой(а так же те объектные файлы что потребуются strlen), что позволит уменьшить конечный размер приложения/библиотеки.

Давайте разберем на простом примере, есть файл simplefunc.c:

int func(int i) {
return i + 21;
}

и файл simplemain.c:

int func(int);

int main(int argc, const char* argv[])
{
return func(argc);
}

Скомпилируем эти 2 файла в приложение:

$ gcc -c simplefunc.c
$
gcc -c simplemain.c
$
gcc simplefunc.o simplemain.o
$ .
/a.out ; echo $?

Все работает как и задумывалось.

Но что если simplefunc мы преобразуем в статическую библиотеку?! И попробуем собрать:

$ ar r libsimplefunc.a simplefunc.o
$
ranlib libsimplefunc.a
$
gcc simplemain.o -L. -lsimplefunc
$
./a.out ; echo $?
22

Все работает, но что будет если изменить порядок линковки:

$ gcc -L. -lsimplefunc simplemain.o
simplemain.o: In function 'main':
simplemain.c:(.text+0x15): undefined reference to 'func'
collect2: ld returned 1 exit status

Вот все и сломалось. Понимание алгоритмов линковки помогает понять почему этот пример не линкуется. На момент обработки libsimplefunc.a линковщик еще не "видел" simplemain.o и потому func еще не в списке неопределенных символов. Когда линковщик обрабатывает библиотеку, он видит экспортируемый символ func, но он не "нужен" и потому объектный файл не линкуется. Когда же он доходит до simplemain.o, то он находит импортируемый символ func, который добавляется в список неопределенных символов, правда на вход линковщика больше не поступают файлы и потому func остается не определенным.

А в предыдущем случае, где библиотека и объектный файл поступали в другом порядке этого не произошло. Отсюда следует что если объектный файл или библиотека AAA нуждается в символе из библиотеки BBB, то AAA должна появится перед BBB на входе линковщика. Все это относиться к линковки статических библиотек.
Статья является частичным переводом статьи 
Eli Bendersky.