Я искал некоторые ошибки компоновщика в довольно сложном продукте, и это довольно безумно, до чего я дошел. Ниже приведен очень минимальный пример, демонстрирующий беспокоящее меня поведение. Я пытаюсь понять это поведение. Если изменение поведения является преднамеренным, можно ли отследить его до журнала изменений gcc, чтобы я мог положиться на него в будущем?
На самом деле это проблема порядка ссылок, и оказывается, что компоновщики разных версий ведут себя по-разному неожиданным образом.
Первая общая библиотека (libtest1.so):
void test2();
void test1()
{
test2();
}
Вторая общая библиотека (libtest2.so):
#include <iostream>
void test2()
{
std::cout << “calling test 2” << std::endl;
}
Основная программа:
#include “test1.hpp”
int main()
{
test1();
}
Когда я создаю общие библиотеки, я не заставляю одну библиотеку зависеть от другой. Я просто оставлю неразрешенные символы в test1, а потом проставлю и test1, и test2 в настройках ссылки при компиляции main. Не беспокойтесь о таких вещах, как rpath и LD_LIBRARY_PATH; они не имеют значения в обсуждении.
Использование gcc 7.5.0
g++ -std=c++11 main.cpp -o main -L. -ltest1 -ltest2 //compiles OK
g++ -std=c++11 main.cpp -o main -L. -ltest2 -ltest1 //link fails; symbol test2 is unresolved
// This makes 100% good sense to me. The linker is a single pass linker and order matters.
// We know and understand this.
Использование gcc 8.3.0 или gcc 9.2.0
g++ -std=c++11 main.cpp -o main -L. -ltest1 -ltest2 //compiles OK
g++ -std=c++11 main.cpp -o main -L. -ltest2 -ltest1 //compiles OK
// This totally breaks my brain!!! This makes it seem like the linker in the newer gcc toolchains
// is a multi pass linker which is doing a lot more work to find the symbols that are needed to
// correctly link. This seems like a HUGE change in the way the linker works. However, I have
// scoured the change logs for the compilers and I haven’t been able to find any mention that the
// way the linker operates has changed in any significant way. This scares me and makes me worry
// that we can’t depend on this behavior consistently.
Так что же здесь за история? Я не понимаю, как более поздние версии компоновщика могут успешно выполнять несколько проходов объектных файлов. Компоновщик gcc стал умнее? Если да, то насколько умнее и останется ли он умнее в будущем? Любая помощь, которую вы можете предоставить, будет принята с благодарностью.
Оба компилятора, вероятно, используют один и тот же компоновщик, поэтому компоновщик не становится умнее. Все дело в опциях компоновщика, которые компилятор передает компоновщику по умолчанию. -v
твой друг.
Эти команды работают одинаково в любой версии компилятора.
g++ -Wl,-as-needed -std=c++11 main.cpp -o main -L. -ltest2 -ltest1 # fails
g++ -Wl,-no-as-needed -std=c++11 main.cpp -o main -L. -ltest2 -ltest1 #succeeds
Это была именно проблема. Даже немного покопавшись, я обнаружил, что то, что компилятор передает компоновщику по умолчанию, варьируется от дистрибутива к дистрибутиву (от сборки к сборке) одних и тех же версий компилятора. Опция
-v
была полезна. Также используетсяstrace
, чтобы увидеть, как вызывается collect/ld. Спасибо!