Как стать автором
Обновить

Когда линковщик предаёт: как одинаковые символы из разных библиотек ломают ваше приложение

Уровень сложностиСложный
Время на прочтение4 мин
Количество просмотров946

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

Теги:
Хабы:
+10
Комментарии2

Публикации

Истории

Работа

Программист С
39 вакансий

Ближайшие события

19 марта – 28 апреля
Экспедиция «Рэйдикс»
Нижний НовгородЕкатеринбургНовосибирскВладивостокИжевскКазаньТюменьУфаИркутскЧелябинскСамараХабаровскКрасноярскОмск
22 апреля
VK Видео Meetup 2025
МоскваОнлайн
23 апреля
Meetup DevOps 43Tech
Санкт-ПетербургОнлайн
24 апреля
VK Go Meetup 2025
Санкт-ПетербургОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань
14 мая
LinkMeetup
Москва
5 июня
Конференция TechRec AI&HR 2025
МоскваОнлайн
20 – 22 июня
Летняя айти-тусовка Summer Merge
Ульяновская область