Почему g ++ обнаруживает неопределенную ссылку при динамическом соединении - PullRequest
0 голосов
/ 06 июля 2018

Возможно, я ошибаюсь из-за того, как работает динамическое связывание, потому что не могу понять это. Как я понял, когда библиотека динамически связана, ее символы разрешаются во время выполнения. От этот ответ:

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

[...]

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

Затем во время выполнения загрузчик операционной системы выполняет позднюю привязку основная программа с библиотекой времени выполнения C (динамическая библиотека или общая библиотека или другая номенклатура).

Я не совсем понимаю, почему g++, похоже, ожидает, что общий объект будет присутствовать при динамическом связывании с ним. Конечно, я ожидаю, что имя библиотеки будет необходимо, чтобы ее можно было загрузить во время выполнения, но почему это .so необходимо на этом этапе? Кроме того, g++ жалуется на неопределенные ссылки при ссылках на библиотеку.

Мои вопросы:

  1. Почему g++, по-видимому, требует совместно используемого объекта при динамическом связывании с ним, если загрузка библиотеки происходит только во время выполнения? Я понимаю, как может потребоваться флаг -l для указания имени общего объекта, чтобы его можно было загружать во время выполнения, но я не вижу смысла указывать путь к .so во время ссылки (-L) или сам .so.
  2. Почему g++ пытается разрешить символы при динамическом связывании? Ничто не мешает мне получить полный .so во время соединения, но затем предоставить другое (неполное) .so во время выполнения, что вызывает сбой программы при попытке использовать неопределенный символ.

Я сделал воспроизводимый пример:

Структура каталогов:

.
├── main.cpp
└── test
    ├── usertest.cpp
    └── usertest.h

Содержимое файла:

тест / usertest.h

#ifndef USERTEST_H_4AD3C656_8109_11E8_BED5_5BE6E678B346
#define USERTEST_H_4AD3C656_8109_11E8_BED5_5BE6E678B346

namespace usertest
{
    void helloWorld();

    // This method is not defined anywhere
    void byeWorld();
};

#endif /* USERTEST_H_4AD3C656_8109_11E8_BED5_5BE6E678B346 */

тест / usertest.cpp

#include "usertest.h"
#include <iostream>

void usertest::helloWorld()
{
    std::cout << "Hello, world\n";
}

main.cpp

#include "test/usertest.h"

int main()
{
    usertest::helloWorld();
    usertest::byeWorld();
}

Использование

$ cd test
$ g++ -c -fPIC usertest.cpp
$ g++ usertest.o -shared -o libusertest.so
$ cd ..
$ g++ main.cpp -L test/ -lusertest
$ LD_LIBRARY_PATH="test" ./a.out

Ожидаемое поведение

Я бы ожидал, что все будет зависать при попытке запустить a.out, потому что он не может найти необходимые символы в libusertest.so.

Фактическое поведение

Сборка a.out завершается неудачно во время соединения, потому что не может найти byeWorld():

/tmp/ccVNcRRY.o: In function `main':
main.cpp:(.text+0xa): undefined reference to `usertest::byeWorld()'
collect2: error: ld returned 1 exit status

Ответы [ 2 ]

0 голосов
/ 09 июля 2018

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

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

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

Итак, в этом очень упрощенном объяснении ваш вызов usertest::helloWorld(); транслируется во что-то вроде dynamic_resolve("usertest::helloWorld", "libusertest.so")();, при этом dynamic_resolve получает имя символа и имя библиотеки и возвращает указатель на функцию. Внутренне то, что делает dynamic_resolve (составное имя), загружает библиотеку "libusertest.so", извлекает адрес функции из библиотеки, кэширует ее в хеш-таблице и затем возвращает указатель на функцию. Вероятно, используются эти системные вызовы. После первого вызова, поскольку результат кэшируется в хеш-таблице и библиотека уже загружена, все последующие вызовы значительно дешевле.

0 голосов
/ 06 июля 2018

В формате ELF действительно нет необходимости знать, какие символы принадлежат какой библиотеке, поскольку фактическое разрешение символов происходит при выполнении программы. По соглашению, хотя ld по-прежнему разрешает символы при создании двоичного файла. Это для вашего удобства, так что вы получаете немедленную обратную связь, когда у вас отсутствуют символы, поскольку в этом случае велика вероятность, что ваша программа не будет работать.

Используя флаг --warn-unresolved-symbols, вы можете изменить поведение ld в этом случае с ошибки на предупреждение:

$ g++ -Wl,--warn-unresolved-symbols main.cpp -lusertest

Должен выдать предупреждение, но все равно создать исполняемый файл. Обратите внимание, что вам все равно нужно указать имя библиотеки, иначе ld не будет знать, где искать нужные символы.

В Windows компоновщик должен точно знать, какой символ принадлежит какой библиотеке, чтобы создать необходимые таблицы импорта. Поэтому невозможно построить двоичный файл PE с неразрешенными символами.

...