При линковке приложения с двумя статическими библиотеками, в которых определён один и тот же символ, возникает классическая и потенциально фатальная проблема — двойное определение символа. Вроде бы всё просто: multiple definition — ошибка, надо переименовать. Но не тут-то было.
Разберёмся, как устроен линковщик, почему конфликты могут не проявляться сразу, и как на проде всё может пойти не так. Ну и конечно, как эту проблему исправить, не трогая архитектуру проекта.
Исходники: два public-файла и одна общая зависимость.
// pri.h int pri_foo(int); // pri.c #include "pri.h" int pri_foo(int a) { return a * 10; } // pub_foo.h int pub_foo(int); // pub_foo.c #include "pub_foo.h" #include "pri.h" int pub_foo(int a) { return pri_foo(a + 11); } // pub_boo.h int pub_boo(int); // pub_boo.c #include "pub_boo.h" #include "pri.h" int pub_boo(int a) { return pri_foo(a + 22); }
Собираем и смотрим символы.
gcc -c pri.c -o pri.o gcc -c pub_foo.c -o pub_foo.o gcc -c pub_boo.c -o pub_boo.o nm *.o ======================================= pri.o: T pri_foo pub_foo.o: U pri_foo, T pub_foo pub_boo.o: U pri_foo, T pub_boo
Ничего необычного: pri_foo глобальный, вызывается из обоих public-файлов.
Собираем библиотеки с дублированием pri.o .
ar rcs libpub_foo.a pub_foo.o pri.o ar rcs libpub_boo.a pub_boo.o pri.o nm *.a ======================================= libpub_boo.a: pub_boo.o: U pri_foo, T pub_boo pri.o: T pri_foo libpub_foo.a: pub_foo.o: U pri_foo, T pub_foo pri.o: T pri_foo
В обеих статических библиотеках теперь содержится реализация pri_foo. Казалось бы — жди конфликтов при линковке.
// main.c #include "pub_foo.h" #include "pub_boo.h" #include <stdio.h> int main() { printf("%d\n", pub_foo(0)); printf("%d\n", pub_boo(0)); }
gcc main.c -L. -lpub_foo -lpub_boo -o test ./test ======================================= 110 220
…И это неверно. Хотя, как говорится, ответ вроде бы верный.
Почему? Потому что линковщик ld просто берёт первую встретившуюся реализацию pri_foo, удовлетворяет U, и вторую выбрасывает без предупреждений.
Порядок флагов -lpub_foo -lpub_boo определяет, какая версия pri_foo попадёт в бинарь, другая в «забвение».
А если pri_fooбудет иметь две разные реализации в каждой библиотеке?
// pri_for_foo.c #include "pri.h" int pri_foo(int a) { return a * 1000; } // pri_for_boo.c #include "pri.h" int pri_foo(int a) { return a * 100; }
Собираем по той же схеме:
gcc -c pri_for_foo.c pub_foo.c gcc -c pri_for_boo.c pub_boo.c ar rcs libpub_foo.a pub_foo.o pri_for_foo.o ar rcs libpub_boo.a pub_boo.o pri_for_boo.o nm *.a ======================================= libpub_boo.a: pub_boo.o: U pri_foo, T pub_boo pri_for_foo.o: T pri_foo libpub_foo.a: pub_foo.o: U pri_foo, T pub_foo pri_for_boo.o: T pri_foo
Ну и чего мы ждём от тестового приложения?
11 * 1000 = 11000
22 * 100 = 2200
И что же мы увидим?
gcc main.c -L. -lpub_boo -lpub_foo -o test && ./test # Вывод: 1100, 2200
Как видно, теперь ответ неверный. Могу показать фокус.
gcc main.c -L. -lpub_foo -lpub_boo -o test && ./test # Вывод: 11000, 22000
Катастрофа: результат зависит от порядка флагов линковки. Это — полноценное неопределённое поведение. В больших проектах с зависимостями это приведёт к краху, отладке в ночи и душевным травмам.
И если вам кажется что с вами этого произойти не может, то напомню что любая не static функция из .c файла попадает в список внешних символов. Или, например, ваша библиотека статически линкует универсальные модули, и если разные версии этих модулей встретятся при линковке конечного приложения, то вы в ловушке.
Можно ли найти решение в динамической линковке? Определённо, но контроль и версионность символов в них отдельная тема.
Можно ли решить проблему в нашем примере? Да, но для этого понадобится два дополнительных шага при сборке. По сути необходимо разрешить символ pri_fooна стадии сборки самой библиотеки и скрыть символ pri_foo. Следите за руками!
Берём наши существующие объектные файл и объединяем их в один .o
> ld -r pri_for_boo.o pub_boo.o -o boo_combo.o > ld -r pri_for_foo.o pub_foo.o -o foo_combo.o nm foo_combo.o boo_combo.o ======================================= boo_combo.o: T pri_foo, T pub_boo foo_combo.o: T pri_foo, T pub_foo
Внешняя зависимость U pri_fooушла, теперь с чистой совестью символ pri_fooможно делать локальным.
objcopy boo_combo.o --localize-symbol pri_foo objcopy foo_combo.o --localize-symbol pri_foo nm foo_combo.o boo_combo.o ======================================= boo_combo.o: t pri_foo, T pub_boo foo_combo.o: t pri_foo, T pub_foo
Поменялось не многое, но по сути мы сделали символ pri_foo почти static. Почему почти? Потому что он всё ещё тут и удалить его не получится. Красивая картинка, когда nm показывает ТОЛЬКО публичное API библиотеки, не выходит. И удалить символ полностью мне пока не удалось. Если есть идеи, буду рад почитать в комментариях!
Однако что же будет теперь с выводом приложения?
> ar rcs libpub_foo.a foo_combo.o; ar rcs libpub_boo.a boo_combo.o > gcc main.c -L. -lpub_foo -lpub_boo -o test > ./test 11000 2200 > gcc main.c -L. -lpub_boo -lpub_foo -o test > ./test 11000 2200
Порядок линковки не важен, всё на месте.
Мораль: разработчику Си-библиотек приходится контролировать не только публичное API библиотеки, но и следить, чтобы приватное не просачивалось в глобальный namespace. Также большим плюсом считаю, что дёрнуть приватную функцию из библиотеки, просто угадав прототип, уже не выйдет.
ссылка на оригинал статьи https://habr.com/ru/articles/902448/
Добавить комментарий