Различия между статическими и динамическими библиотеками без учета того, как они используются компоновщиком/загрузчиком.

Я понимаю, как компоновщик/загрузчик использует статические/динамические библиотеки.

  1. Однако почему бы не иметь единый тип файла библиотеки, сопровождаемый флагами компилятора, которые указывают, как должна быть связана библиотека (статическая или динамическая)?
  2. По тому простому факту, что у нас есть статические и динамические библиотеки, я предполагаю, что эти файлы имеют определенное содержимое, которое обеспечивает статическую и динамическую компоновку соответственно. Может ли кто-нибудь пролить свет на различия между содержимым статических файлов и файлов общей библиотеки?

person rahul    schedule 04.11.2017    source источник


Ответы (4)


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

Может ли кто-нибудь пролить свет на различия между содержимым статических файлов и файлов общей библиотеки?

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

Что такое ar архив

Я сделаю архив ar, начиная с этих трех файлов:

foo.c

#include <stdio.h>

void foo(void)
{
    puts("foo");
}

bar.c

#include <stdio.h>

void bar(void)
{
    puts("bar");
}

лимерик.txt

There once was a young lady named bright
Whose speed was much faster than light
She set out one day
In a relative way
And returned on the previous night.

Я скомпилирую эти два исходных кода C в объектные файлы, независимые от позиции:

$ gcc -c -Wall -fPIC foo.c
$ gcc -c -Wall -fPIC bar.c

Нет необходимости, чтобы объектные файлы, предназначенные для архива ar, компилировались с помощью -fPIC. Я просто хочу, чтобы они были скомпилированы таким образом.

Затем я создам архив ar с именем libsundry.a, содержащий объектные файлы foo.o и bar.o, а также limerick.txt:

$ ar rcs libsundry.a foo.o bar.o limerick.txt

Архив ar создается, разумеется, с помощью ar, универсального архиватора GNU. Таким образом, он не создается компоновщиком. Никакой привязки не происходит. Вот как ar сообщает о содержимом архива:

$ ar -t libsundry.a 
foo.o
bar.o
limerick.txt

Вот как выглядит лимерик в архиве:

$ rm limerick.txt
$ ar x libsundry.a limerick.txt; cat limerick.txt
There once was a young lady named bright
Whose speed was much faster than light
She set out one day
In a relative way
And returned on the previous night.

Вопрос. Какой смысл помещать два объектных файла и лимерик ASCII в один и тот же архив ar?

А. Чтобы показать, что я могу. Чтобы показать, что архив ar — это просто набор файлов.

Давайте посмотрим, что file делает из libsundry.a.

$ file libsundry.a 
libsundry.a: current ar archive

Теперь я напишу пару программ, которые используют libsundry.a в своей компоновке.

fooprog.c

extern void foo(void);

int main(void)
{
    foo();
    return 0;
}

Скомпилируйте, свяжите и запустите это:

$ gcc -c -Wall fooprog.c
$ gcc -o fooprog fooprog.o -L. -lsundry
$ ./fooprog 
foo

Это работяга Дори. Компоновщика явно не беспокоило наличие лимерика ASCII в libsundry.a.

Причина этого в том, что компоновщик даже не пытался связать limerick.txt с программой. Давайте снова сделаем привязку, на этот раз с диагностической опцией, которая точно покажет нам, какие входные файлы связаны:

$ gcc -o fooprog fooprog.o -L. -lsundry -Wl,-trace
/usr/bin/ld: mode elf_x86_64
/usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu/crt1.o
/usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu/crti.o
/usr/lib/gcc/x86_64-linux-gnu/5/crtbegin.o
fooprog.o
(./libsundry.a)foo.o
-lgcc_s (/usr/lib/gcc/x86_64-linux-gnu/5/libgcc_s.so)
/lib/x86_64-linux-gnu/libc.so.6
(/usr/lib/x86_64-linux-gnu/libc_nonshared.a)elf-init.oS
/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
-lgcc_s (/usr/lib/gcc/x86_64-linux-gnu/5/libgcc_s.so)
/usr/lib/gcc/x86_64-linux-gnu/5/crtend.o
/usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu/crtn.o

Там много библиотек по умолчанию и объектных файлов, но единственные объектные файлы, которые мы создали и которые использует компоновщик, это:

fooprog.o
(./libsundry.a)foo.o

Все, что компоновщик сделал с ./libsundry.a, это достал foo.o из пакета и скомпоновал в программе. После привязки fooprog.o к программе нужно было найти определение для foo. Он заглянул в сумку. Он нашел определение в foo.o, поэтому взял foo.o из пакета и связал его в программе. Связывая fooprog,

gcc -o fooprog fooprog.o -L. -lsundry

точно такая же связь, как:

$ gcc -o fooprog fooprog.o foo.o

Что file говорит о fooprog?

$ file fooprog
fooprog: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), \
dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, \
for GNU/Linux 2.6.32, BuildID[sha1]=32525dce7adf18604b2eb5af7065091c9111c16e,
not stripped

Вот моя вторая программа:

foobarprog.c

extern void foo(void);
extern void bar(void);

int main(void)
{
    foo();
    bar();
    return 0;
}

Скомпилируйте, свяжите и запустите:

$ gcc -c -Wall foobarprog.c
$ gcc -o foobarprog foobarprog.o -L. -lsundry
$ ./foobarprog 
foo
bar

И вот снова связь с -trace:

$ gcc -o foobarprog foobarprog.o -L. -lsundry -Wl,-trace
/usr/bin/ld: mode elf_x86_64
/usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu/crt1.o
/usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu/crti.o
/usr/lib/gcc/x86_64-linux-gnu/5/crtbegin.o
foobarprog.o
(./libsundry.a)foo.o
(./libsundry.a)bar.o
-lgcc_s (/usr/lib/gcc/x86_64-linux-gnu/5/libgcc_s.so)
/lib/x86_64-linux-gnu/libc.so.6
(/usr/lib/x86_64-linux-gnu/libc_nonshared.a)elf-init.oS
/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
-lgcc_s (/usr/lib/gcc/x86_64-linux-gnu/5/libgcc_s.so)
/usr/lib/gcc/x86_64-linux-gnu/5/crtend.o
/usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu/crtn.o

Итак, на этот раз наши объектные файлы, используемые компоновщиком, были:

foobarprog.o
(./libsundry.a)foo.o
(./libsundry.a)bar.o

После привязки foobarprog.o к программе нужно было найти определения для foo и bar. Он заглянул в сумку. Он нашел определения соответственно в foo.o и bar.o, поэтому взял их из сумки и связал в программе. Связывая foobarprog,

gcc -o foobarprog foobarprog.o -L. -lsundry

точно такая же связь, как:

$ gcc -o foobarprog foobarprog.o foo.o bar.o

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

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

Что такое DSO

Давайте сделаем один.

foobar.c

extern void foo(void);
extern void bar(void);

void foobar(void)
{
    foo();
    bar();
}

Мы скомпилируем этот новый исходный файл:

$ gcc -c -Wall -fPIC foobar.c

а затем сделать DSO, используя foobar.o и повторно используя libsundry.a

$ gcc -shared -o libfoobar.so foobar.o -L. -lsundry -Wl,-trace
/usr/bin/ld: mode elf_x86_64
/usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu/crti.o
/usr/lib/gcc/x86_64-linux-gnu/5/crtbeginS.o
foobar.o
(./libsundry.a)foo.o
(./libsundry.a)bar.o
-lgcc_s (/usr/lib/gcc/x86_64-linux-gnu/5/libgcc_s.so)
/lib/x86_64-linux-gnu/libc.so.6
/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
-lgcc_s (/usr/lib/gcc/x86_64-linux-gnu/5/libgcc_s.so)
/usr/lib/gcc/x86_64-linux-gnu/5/crtendS.o
/usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu/crtn.o

Это сделало DSO libfoobar.so. Примечание: DSO создается компоновщиком. Он связан так же, как связана программа. Связь libfoopar.so очень похожа на связь foobarprog, но добавление опции -shared указывает компоновщику создавать DSO, а не программу. Здесь мы видим, что наши объектные файлы, используемые компоновкой:

foobar.o
(./libsundry.a)foo.o
(./libsundry.a)bar.o

ar вообще не понимает DSO:

$ ar -t libfoobar.so 
ar: libfoobar.so: File format not recognised

Но file делает:

$ file libfoobar.so 
libfoobar.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), \
dynamically linked, BuildID[sha1]=16747713db620e5ef14753334fea52e71fb3c5c8, \
not stripped

Теперь, если мы перелинкуем foobarprog, используя libfoobar.so вместо libsundry.a:

$ gcc -o foobarprog foobarprog.o -L. -lfoobar -Wl,-trace,--rpath=$(pwd)
/usr/bin/ld: mode elf_x86_64
/usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu/crt1.o
/usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu/crti.o
/usr/lib/gcc/x86_64-linux-gnu/5/crtbegin.o
foobarprog.o
-lfoobar (./libfoobar.so)
-lgcc_s (/usr/lib/gcc/x86_64-linux-gnu/5/libgcc_s.so)
/lib/x86_64-linux-gnu/libc.so.6
(/usr/lib/x86_64-linux-gnu/libc_nonshared.a)elf-init.oS
/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
-lgcc_s (/usr/lib/gcc/x86_64-linux-gnu/5/libgcc_s.so)
/usr/lib/gcc/x86_64-linux-gnu/5/crtend.o
/usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu/crtn.o

мы видим

foobarprog.o
-lfoobar (./libfoobar.so)

что ./libfoobar.so сам был связан. Не какие-то объектные файлы "внутри него". Внутри него нет никаких объектных файлов. И как это повлияло на компоновку, можно увидеть в динамических зависимостях программы:

$ ldd foobarprog
    linux-vdso.so.1 =>  (0x00007ffca47fb000)
    libfoobar.so => /home/imk/develop/so/scrap/libfoobar.so (0x00007fb050eeb000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fb050afd000)
    /lib64/ld-linux-x86-64.so.2 (0x000055d8119f0000)

Программа вышла с зависимостью времени выполнения от libfoobar.so. Вот что делает привязка DSO. Мы видим, что эта зависимость времени выполнения удовлетворена. Итак, программа будет работать:

$ ./foobarprog
foo
bar

точно так же, как прежде.

Тот факт, что DSO и программа, в отличие от архива ar, являются продуктами компоновщика, предполагает, что DSO и программа являются вариантами одного и того же типа. Выходные данные file также предполагали это. DSO и программа являются двоичными файлами ELF, которые загрузчик ОС может отображать в адресное пространство процесса. Не просто пакет файлов. Архив ar ни в коем случае не является двоичным файлом ELF.

Разница между файлом ELF программного типа и файлом ELF непрограммного типа заключается в разных значениях, которые компоновщик записывает в структуру заголовка ELF и структуру заголовков программ формата файла ELF. Эти различия предписывают загрузчику ОС инициировать новый процесс, когда он загружает ELF-файл программного типа, и дополнять процесс, который находится в стадии разработки, когда он загружает непрограммный ELF-файл. Таким образом, непрограммный DSO отображается в процесс своей родительской программы. Тот факт, что программа инициирует новый процесс, требует, чтобы программа имела единственную точку входа по умолчанию, которой ОС будет передавать управление: эта точка входа является обязательной функцией main в программе C или C++. С другой стороны, непрограммный DSO не нуждается в единой обязательной точке входа. В него можно войти через любую из глобальных функций, которые он экспортирует вызовами функций из родительской программы.

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

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

$ gcc -pie -o foobarprog foobarprog.o -L. -lfoobar -Wl,--rpath=$(pwd)

Вот что здесь делает -pie (Position Independent Executable). А потом:

$ file foobarprog
foobarprog: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), ....

file скажет, что foobarprog является DSO, хотя это тоже программа:

$ ./foobarprog
foo
bar

И исполняемые файлы PIE завоевывают популярность. В Debian 9 и производных дистрибутивах (Ubuntu 17.04...) набор инструментов GCC создает программы PIE по умолчанию.

Если вам нужны подробные сведения о форматах файлов ar и ELF, вот подробности ar, а здесь детали формата ELF.

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

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

person Mike Kinghan    schedule 05.11.2017
comment
Какой эпичный ответ!! Салют! - person Zack Zhu; 12.03.2020

Потому что это совершенно разные вещи. Статические библиотеки — это просто наборы объектного кода, сгенерированного компилятором. Динамические библиотеки подключены.

person user207421    schedule 04.11.2017

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

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

person jwdonahue    schedule 04.11.2017

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

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

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

Этот документ Ульриха Дреппера (бывший сопровождающий библиотеки GNU libc) содержит более подробные сведения. чем вы когда-либо хотели бы знать об общих библиотеках.

person Lawrence D'Oliveiro    schedule 04.11.2017