Cpp: usr/bin/ld: не удается найти -l <nameOfTheLibrary>

Я создал проект cpp, который использовал файл lib с именем: libblpapi3_64.so Этот файл происходит из библиотеки, которую я загружаю из Интернета.

Мой проект запускается без ошибок. Поэтому я обновляю его до битбакет. Затем мой коллега загружает его и запускает на своем компьютере. Но он получает сообщение об ошибке:

usr/bin/ld: cannot find -lblpapi3_64.

Фактически, я скопировал его в свой репозиторий проектов. Я имею в виду, что я создал файл под именем lib в моем проекте, и все файлы lib, которые я использовал, находятся в нем.

Существуют также другие файлы lib, такие как liblog4cpp.a, но все они хороши. Ошибка только libblpapi3_64.so.

Это потому, что файл .so не является .a? Или есть другая причина?
Btw, имя файла libblpapi3_64.so равно green, а другие файлы (.a) - white. Я думаю, что это не файл ссылки, это оригинальный файл.

Ответ 1

Кратко:

ld не знаю, где находятся ваши проектные библиотеки. Вы должны поместить его в известные каталоги ld или указать полный путь к вашей библиотеке с -L параметра -L для компоновщика.

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

Подробно:

Во-первых, мы должны понять, что инструменты делают, что:

  1. Компилятор создает простые object files с неразрешенными символами (он не заботится о символах во время выполнения).
  2. Компоновщик объединяет несколько 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:

  1. Компоновщик: 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 показывает только, какие библиотеки нужны вашей программе. Итак, все эти библиотеки будут найдены динамическим загрузчиком (следующий абзац). Компоновщик работает во время сборки.

  2. Динамический загрузчик: 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++ являются общими, и компилятор по умолчанию связывает с ними любой проект динамически. Вы также можете связать их статически, но это увеличит общий размер исполняемого файла. Таким образом, если общая библиотека не может быть найдена ваши символы останутся нерешенными, и вы не сможете запустить приложение, таким образом, он не может производить исполняемый файл. Вы можете получить список каталогов, в которых библиотеки обычно ищут:

  1. Передача команды компоновщику в аргументах компилятора.
  2. Разбор ld --verbose вывода.
  3. ldconfig вывод ldconfig.

Некоторые из этих методов описаны здесь.

Динамический загрузчик пытается найти все библиотеки, используя:

  1. DT_RPATH динамический раздел файла ELF.
  2. DT_RUNPATH раздел исполняемого файла.
  3. Переменная среды LD_LIBRARY_PATH.
  4. /etc/ld.so.cache - собственный файл кэша, который содержит скомпилированный список библиотек-кандидатов, ранее найденных в пути расширенной библиотеки.
  5. Пути по умолчанию: в пути по умолчанию /lib, а затем /usr/lib. Если двоичный файл был связан с -z nodeflib компоновщика -z nodeflib, этот шаг пропускается.

алгоритм поиска inux ld -L

Также обратите внимание, что если мы говорим об общих библиотеках, они называются не .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, который имеет только файл символической ссылки для делая возможным компиляцию проекта.

Продолжить

После всего этого:

  1. Вы, ваш коллега и КАЖДЫЙ пользователь кода вашего приложения должны иметь все библиотеки в своих системных путях компоновки, чтобы иметь возможность компилировать (создавать ваше приложение). В противном случае они должны изменить Makefile (или команду компиляции), чтобы добавить каталог расположения общей библиотеки, добавив -L<somePathToTheSharedLibrary> качестве аргумента.
  2. После успешной сборки вам снова понадобится ваша библиотека, чтобы иметь возможность запустить программу. Вашу библиотеку будет искать динамический загрузчик (ld-Linux), поэтому она должна находиться в ее путях (см. Выше) или в путях системного компоновщика. В большинстве дистрибутивов Linux-программ, например, в играх Steam, есть shell-скрипт, который устанавливает LD_LIBRARY_PATH которая указывает на все разделяемые библиотеки, необходимые для игры.

Ответ 2

Вы можете посмотреть наш пакет Rblapi, который также использует эту самую библиотеку.

В вашем основном вопросе "как сделать видимую библиотеку" действительно есть два ответа:

  • Используйте ld.so. Самый простой способ - скопировать blpapi3_64.so в /usr/local/lib. Если вы затем вызываете ldconfig для обновления кеша, вы должны быть настроены. Вы можете проверить это через ldconfig -p | grep blpapi, который должен показать его.

  • Используйте инструкцию rpath при создании приложения; это в основном кодирует путь и делает вас независимым от ld.so.