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