Как компоновщики решают, какие части библиотек включать?

Предположим, что в библиотеке A есть функции a() и b(). Если я свяжу свою программу B с A и вызову a(), будет ли b() включена в двоичный файл? Видит ли компилятор, вызывает ли какая-либо функция в программе b() (возможно, a() вызывает b() или другая библиотека вызывает b())? Если да, то как компилятор получает эту информацию? Если нет, не является ли это большой тратой окончательного размера компиляции, если я ссылаюсь на большую библиотеку, но использую только второстепенную функцию?


person Oliver Zheng    schedule 03.04.2009    source источник


Ответы (9)


Взгляните на оптимизацию времени ссылки. Это обязательно зависит от поставщика. Это также будет зависеть от того, как вы создаете свои двоичные файлы. Компиляторы MS (по крайней мере, начиная с 2005 г.) предоставляют нечто, называемое связывание на функциональном уровне. -- это еще один способ убрать символы, которые вам не нужны. В этом сообщении объясняется, как того же можно добиться с помощью GCC (это устарело, GCC должен мы пошли дальше, но содержание имеет отношение к вашему вопросу).

Также взгляните на реализацию LLVM (и раздел примеров).

Я предлагаю вам также взглянуть на компоновщики и загрузчики Джон Левайн - отличное чтение.

person dirkgently    schedule 03.04.2009
comment
Как правило, оптимизация во время компоновки затрагивает гораздо больше, чем просто удаление объектов и функций, на которые нет ссылок. LTO обычно относится к технологии, которая может оптимизировать сгенерированный код во время компоновки, например встраивание функций или распознавание того, что указатели не являются псевдонимами объектов. - person Michael Burr; 05.04.2009
comment
@Майкл Берр: Правильно. Но это одно место для поиска. Кроме того, именно поэтому я привел так много ссылок. - person dirkgently; 05.04.2009
comment
Конечно, я не имел в виду, что с ответом что-то не так, просто указав, что LTO, как правило, намного больше, чем связывание на функциональном уровне. - person Michael Burr; 05.04.2009

Это зависит.

Если библиотека является общим объектом или DLL, то все в библиотеке загружается, но во время выполнения. Стоимость дополнительной памяти (надеюсь) компенсируется за счет совместного использования библиотеки (на самом деле, кодовых страниц) между всеми процессами в памяти, которые используют эту библиотеку. Это большая победа для чего-то вроде libc.so, в меньшей степени для myreallyobscurelibrary.so. Но на самом деле вы, вероятно, не спрашиваете об общих объектах.

Статические библиотеки — это просто набор отдельных объектных файлов, каждый из которых является результатом отдельной компиляции (или сборки) и, возможно, даже не написан на одном и том же исходном языке. Каждый объектный файл имеет ряд экспортируемых символов и почти всегда ряд импортированных символов.

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

Обычно именованные объектные модули ожидают получить символы из какой-нибудь библиотеки, например libc.a.

В вашем примере у вас есть один модуль, который вызывает функцию a(), в результате чего компоновщик ищет модуль, который экспортирует a().

Вы говорите, что библиотека с именем A (в unix, вероятно, libA.a) предлагает a() и b(), но не указываете, как именно. Вы намекнули, что a() и b() не звонят друг другу, что я и предполагаю.

Если libA.a был создан из a.o и b.o, где каждый определяет соответствующую единственную функцию, то компоновщик будет включать a.o и игнорировать b.o.

Однако, если libA.a включает ab.o, который определяет как a(), так и b(), тогда он будет включать ab.o в ссылку, удовлетворяя потребность в a() и включая неиспользуемую функцию b().

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

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

person RBerteig    schedule 03.04.2009

Это зависит от линкера.

например. Microsoft Visual C++ имеет параметр «Включить связывание на уровне функций», поэтому вы можете включить его вручную.

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

person Jimmy J    schedule 03.04.2009

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

Если вы используете разделяемые библиотеки, вы не влияете на размер компиляции, используя большее или меньшее их количество. Однако ваш размер среды выполнения будет включать их.

person lothar    schedule 03.04.2009

Эта лекция в Academic Earth дает довольно хороший обзор, о ссылках говорится во второй половине доклада. , IIRC.

person Leif    schedule 03.04.2009

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

person mmx    schedule 03.04.2009

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

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

person Community    schedule 03.04.2009

Это зависит от параметров, переданных компоновщику, но обычно компоновщик пропускает объектные файлы в библиотеке, на которые нигде нет ссылок.

$ cat foo.c
int main(){}

$ gcc -static foo.c

$ size
   text    data     bss     dec     hex filename
 452659    1928    6880  461467   70a9b a.out

# force linking of libz.a even though it isn't used
$ gcc -static foo.c -Wl,-whole-archive -lz -Wl,-no-whole-archive

$ size
   text    data     bss     dec     hex filename
 517951    2180    6844  526975   80a7f a.out
person sigjuice    schedule 03.04.2009

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

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

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

person Michael Burr    schedule 03.04.2009