Ответ 1
Кратко:
ld
не знаю, где находятся ваши проектные библиотеки. Вы должны поместить его в известные каталоги ld или указать полный путь к вашей библиотеке с -L
параметра -L
для компоновщика.
Чтобы создать свою программу, вы должны иметь свою библиотеку в путях поиска /bin/ld
и своего коллегу тоже. Зачем? Смотрите подробный ответ.
Подробно:
Во-первых, мы должны понять, что инструменты делают, что:
- Компилятор создает простые
object files
с неразрешенными символами (он не заботится о символах во время выполнения). - Компоновщик объединяет несколько
object
иarchive files
, перемещает их данные и связывает ссылки на символы в один файл: исполняемый файл или библиотеку.
Давайте начнем с некоторого примера. Например, у вас есть проект, который состоит из 3 файлов: main.c
, func.h
и func.c
main.c
#include "func.h"
int main() {
func();
return 0;
}
func.h
void func();
func.c
#include "func.h"
void func() { }
Таким образом, когда вы компилируете свой исходный код (main.c
) в объектный файл (main.o
), он еще не может быть запущен, потому что он содержит неразрешенные символы. Давайте начнем с начала producing an executable
рабочего процесса (без подробностей):
Препроцессор после своей работы выдает следующий main.c.preprocessed
:
void func();
int main() {
func();
return 0;
}
и следующий func.c.preprocessed
:
void func();
void func() { }
Как вы можете видеть в main.c.preprocessed
, нет никаких соединений с вашим файлом func.c
и реализацией void func()
, компилятор просто не знает об этом, он компилирует все исходные файлы отдельно. Итак, чтобы иметь возможность скомпилировать этот проект, вы должны скомпилировать оба исходных файла, используя что-то вроде cc -c main.c -o main.o
и cc -c func.c -o func.o
, это будет произвести 2 объектных файла, main.o
и func.o
func.o
все символы, потому что у него есть только одна функция, тело которой написано прямо внутри func.c
но main.o
еще не разрешил символ func
потому что он не знает, где он реализован.
Давайте посмотрим, что внутри func.o
:
$ nm func.o
0000000000000000 T func
Просто он содержит символ, который находится в разделе текстового кода, так что это наша функция func
.
И давайте заглянем внутрь main.o
:
$ nm main.o
U func
0000000000000000 T main
Наш main.o
имеет реализованную и разрешенную статическую функцию main
и мы можем увидеть ее в объектном файле. Но мы также видим символ func
который помечен как неразрешенный U
, и, следовательно, мы не можем видеть его смещение адреса.
Для решения этой проблемы мы должны использовать компоновщик. Он возьмет все объектные файлы и разрешит все эти символы (void func();
в нашем примере). Если компоновщик почему-то не может этого сделать, он выдает ошибку, например, unresolved external symbol
: void func()
. Это может произойти, если вы не передадите объектный файл func.o
компоновщику. Итак, давайте передадим все имеющиеся у нас объектные файлы компоновщику:
ld main.o func.o -o test
Компоновщик пройдет через main.o
, затем через func.o
, попытается разрешить символы и, если все пойдет нормально, вывести его в test
файл. Если мы посмотрим на полученный вывод, то увидим, что все символы разрешены:
$ nm test
0000000000601000 R __bss_start
0000000000601000 R _edata
0000000000601000 R _end
00000000004000b0 T func
00000000004000b7 T main
Здесь наша работа выполнена. Давайте посмотрим на ситуацию с динамическими (общими) библиотеками. Давайте func.c
общую библиотеку из нашего исходного файла func.c
:
gcc -c func.c -o func.o
gcc -shared -fPIC -Wl,-soname,libfunc.so.1 -o libfunc.so.1.5.0 func.o
Вуаля, у нас это есть. Теперь давайте поместим его в известный путь библиотеки динамических компоновщиков, /usr/lib/
:
sudo mv libfunc.so.1.5.0 /usr/lib/ # to make program be able to run
sudo ln -s libfunc.so.1.5.0 /usr/lib/libfunc.so.1 #creating symlink for the program to run
sudo ln -s libfunc.so.1 /usr/lib/libfunc.so # to make compilation possible
И давайте сделаем наш проект зависимым от этой разделяемой библиотеки, оставив символ func()
неразрешенным после компиляции и статического процесса компоновки, создав исполняемый файл и связав его (динамически) с нашей разделяемой библиотекой (libfunc
):
cc main.c -lfunc
Теперь, если мы ищем символ в его таблице символов, наш символ все еще остается неразрешенным:
$ nm a.out | grep fun
U func
Но это больше не проблема, потому что символ func
будет решаться динамическим загрузчиком перед каждым запуском программы. Хорошо, теперь вернемся к теории.
На самом деле библиотеки - это просто объектные файлы, которые помещаются в один архив с помощью инструмента ar
с таблицей символов, созданной с ranlib
инструмента ranlib
.
Компилятор при компиляции объектных файлов не разрешает symbols
. Эти символы будут заменены на адреса компоновщиком. Таким образом, разрешение символов может быть сделано двумя вещами: the linker
и dynamic loader
:
-
Компоновщик:
ld
, выполняет 2 задания:a) Для статических библиотек или простых объектных файлов этот компоновщик заменяет внешние символы в объектных файлах на адреса реальных объектов. Например, если мы используем C++, компоновщик искажения имени изменит
_ZNK3MapI10StringName3RefI8GDScriptE10ComparatorIS0_E16DefaultAllocatorE3hasERKS0_
на0x07f4123f0
.b) Для динамических библиотек он только проверяет, могут ли символы быть разрешены (вы пытаетесь связать их с правильной библиотекой), но не заменяет символы по адресу. Если символы не могут быть разрешены (например, они не реализованы в разделяемой библиотеке, на которую вы
undefined reference to
) - выдаетundefined reference to
ошибку и нарушает процесс сборки, потому что вы пытаетесь использовать эти символы, но компоновщик не может найти такие Символ в нем объектных файлов, которые он обрабатывает в данный момент. В противном случае этот компоновщик добавляет некоторую информацию в исполняемый файлELF
:я.
.interp
section - запрос на.interp
interpreter
- динамического загрузчика перед выполнением, поэтому этот раздел просто содержит путь к динамическому загрузчику. Если вы посмотрите на свой исполняемый файл, который, например, зависит от разделяемой библиотеки (libfunc
), вы увидите раздел$ readelf -L a.out
:INTERP 0x0000000000000238 0x0000000000400238 0x0000000000400238 0x000000000000001c 0x000000000000001c R 1 [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
II. Раздел
.dynamic
- список общих библиотек, которыеinterpreter
будет искать перед выполнением. Вы можете увидеть их поldd
илиreadelf
:$ ldd a.out linux-vdso.so.1 => (0x00007ffd577dc000) libfunc.so.1 => /usr/lib/libfunc.so.1 (0x00007fc629eca000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fefe148a000) /lib64/ld-linux-x86-64.so.2 (0x000055747925e000) $ readelf -d a.out Dynamic section at offset 0xe18 contains 25 entries: Tag Type Name/Value 0x0000000000000001 (NEEDED) Shared library: [libfunc.so.1] 0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
Обратите внимание, что
ldd
также находит все библиотеки в вашей файловой системе, тогда как readelf показывает только, какие библиотеки нужны вашей программе. Итак, все эти библиотеки будут найдены динамическим загрузчиком (следующий абзац). Компоновщик работает во время сборки. -
Динамический загрузчик:
ld.so
илиld-Linux
. Он находит и загружает все общие библиотеки, необходимые программе (если они не были загружены ранее), разрешает символы, заменяя их на реальные адреса непосредственно перед запуском программы, подготавливает программу к запуску и затем запускает ее. Работает после сборки и до запуска программы. Говоря менее, динамическое связывание означает разрешение символов в вашем исполняемом файле перед каждым запуском программы.
На самом деле, когда вы запускаете исполняемый файл ELF
с разделом .interp
(для этого нужно загрузить несколько общих библиотек), ОС (Linux) сначала запускает интерпретатор, но не вашу программу. В противном случае у вас неопределенное поведение - в вашей программе есть символы, но они не определяются по адресам, что обычно означает, что программа не сможет работать должным образом.
Вы также можете запустить динамический загрузчик самостоятельно, но это не обязательно (двоичный файл /lib/ld-Linux.so.2
для 32-разрядной архитектуры elf и /lib64/ld-Linux-x86-64.so.2
для эльфа 64-битной архитектуры).
Почему компоновщик утверждает, что /usr/bin/ld: cannot find -Lblpapi3_64
в вашем случае? Потому что он пытается найти все библиотеки в нем известных путей. Почему он ищет библиотеку, если она будет загружена во время выполнения? Потому что он должен проверить, могут ли все необходимые символы быть разрешены этой библиотекой, и поместить его имя в раздел .dynamic
для динамического загрузчика. На самом деле, секция .interp
существует почти в каждом c/C++ elf, поскольку libc
и libstdC++
являются общими, и компилятор по умолчанию связывает с ними любой проект динамически. Вы также можете связать их статически, но это увеличит общий размер исполняемого файла. Таким образом, если общая библиотека не может быть найдена ваши символы останутся нерешенными, и вы не сможете запустить приложение, таким образом, он не может производить исполняемый файл. Вы можете получить список каталогов, в которых библиотеки обычно ищут:
- Передача команды компоновщику в аргументах компилятора.
- Разбор
ld --verbose
вывода. -
ldconfig
выводldconfig
.
Некоторые из этих методов описаны здесь.
Динамический загрузчик пытается найти все библиотеки, используя:
-
DT_RPATH
динамический раздел файла ELF. -
DT_RUNPATH
раздел исполняемого файла. - Переменная среды
LD_LIBRARY_PATH
. -
/etc/ld.so.cache
- собственный файл кэша, который содержит скомпилированный список библиотек-кандидатов, ранее найденных в пути расширенной библиотеки. - Пути по умолчанию: в пути по умолчанию /lib, а затем /usr/lib. Если двоичный файл был связан с
-z nodeflib
компоновщика-z nodeflib
, этот шаг пропускается.
Также обратите внимание, что если мы говорим об общих библиотеках, они называются не .so
а в формате .so.version
. При сборке приложения компоновщик будет искать файл .so
(который обычно является символической .so.version
на .so.version
), но при запуске приложения динамический загрузчик ищет файл .so.version
. Например, допустим, у нас есть библиотечный test
версией 1.1.1
соответствии с semver. В файловой системе это будет выглядеть так:
/usr/lib/libtest.so -> /usr/lib/libtest.so.1.1.1
/usr/lib/libtest.so.1 -> /usr/lib/libtest.so.1.1.1
/usr/lib/libtest.so.1.1 -> /usr/lib/libtest.so.1.1.1
/usr/lib/libtest.so.1.1.1
Таким образом, чтобы иметь возможность компилировать, вы должны иметь все версионные файлы (libtest.so.1
, libtest.so.1.1
и libtest.so.1.1.1
) и файл libtest.so
но для запуска вашего приложения вы должны иметь только 3 версионных файла библиотеки перечислены первыми. Это также объясняет, почему пакеты Debian или rpm имеют раздельно -packages devel
: обычный (который состоит только из файлов, необходимых уже скомпилированным приложениям для их запуска), который имеет 3 версионных библиотечных файла и пакет devel, который имеет только файл символической ссылки для делая возможным компиляцию проекта.
Продолжить
После всего этого:
- Вы, ваш коллега и КАЖДЫЙ пользователь кода вашего приложения должны иметь все библиотеки в своих системных путях компоновки, чтобы иметь возможность компилировать (создавать ваше приложение). В противном случае они должны изменить Makefile (или команду компиляции), чтобы добавить каталог расположения общей библиотеки, добавив
-L<somePathToTheSharedLibrary>
качестве аргумента. - После успешной сборки вам снова понадобится ваша библиотека, чтобы иметь возможность запустить программу. Вашу библиотеку будет искать динамический загрузчик (
ld-Linux
), поэтому она должна находиться в ее путях (см. Выше) или в путях системного компоновщика. В большинстве дистрибутивов Linux-программ, например, в играх Steam, есть shell-скрипт, который устанавливаетLD_LIBRARY_PATH
которая указывает на все разделяемые библиотеки, необходимые для игры.