Как бороться с символьными коллизиями между статически связанными библиотеками?

Одним из наиболее важных правил и рекомендаций при написании библиотеки является помещение всех символов библиотеки в пространство имен библиотеки. C++ делает это легко благодаря namespace ключевое слово. В Си обычный подход состоит в том, чтобы добавлять префиксы к определенному префиксу библиотеки.

Правила стандарта C накладывают на них некоторые ограничения (для безопасной компиляции): компилятор A C может просматривать только первые 8 символов идентификатора, поэтому foobar2k_eggs а также foobar2k_spam может корректно интерпретироваться как одни и те же идентификаторы - однако каждый современный компилятор допускает произвольные длинные идентификаторы, поэтому в наше время (21 век) нам не нужно об этом беспокоиться.

Но что, если вы столкнулись с некоторыми библиотеками, из которых вы не можете изменить имена / идентификаторы символов? Может быть, у вас есть только статический двоичный файл и заголовки, или вы не хотите, или вам не разрешено настраивать и перекомпилировать себя.

3 ответа

Решение

По крайней мере, в случае статических библиотек вы можете обойти это довольно удобно.

Рассмотрим эти заголовки библиотек foo и bar. Ради этого урока я также дам вам исходные файлы.

примеры / ex01 / foo.h

int spam(void);
double eggs(void);

examples / ex01 / foo.c (может быть непрозрачным / недоступным)

int the_spams;
double the_eggs;

int spam()
{
    return the_spams++;
}

double eggs()
{
    return the_eggs--;
}

Пример / ex01 / bar.h

int spam(int new_spams);
double eggs(double new_eggs);

examples / ex01 / bar.c (может быть непрозрачным / недоступным)

int the_spams;
double the_eggs;

int spam(int new_spams)
{
    int old_spams = the_spams;
    the_spams = new_spams;
    return old_spams;
}

double eggs(double new_eggs)
{
    double old_eggs = the_eggs;
    the_eggs = new_eggs;
    return old_eggs;
}

Мы хотим использовать их в программе foobar

Пример /ex01/foobar.c

#include <stdio.h>

#include "foo.h"
#include "bar.h"

int main()
{
    const int    new_bar_spam = 3;
    const double new_bar_eggs = 5.0f;

    printf("foo: spam = %d, eggs = %f\n", spam(), eggs() );
    printf("bar: old spam = %d, new spam = %d ; old eggs = %f, new eggs = %f\n", 
            spam(new_bar_spam), new_bar_spam, 
            eggs(new_bar_eggs), new_bar_eggs );

    return 0;
}

Одна проблема сразу становится очевидной: C не знает перегрузки. Таким образом, у нас есть два раза две функции с одинаковым именем, но с другой подписью. Поэтому нам нужен какой-то способ их различить. В любом случае, давайте посмотрим, что по этому поводу должен сказать компилятор:

example/ex01/ $ make
cc    -c -o foobar.o foobar.c
In file included from foobar.c:4:
bar.h:1: error: conflicting types for ‘spam’
foo.h:1: note: previous declaration of ‘spam’ was here
bar.h:2: error: conflicting types for ‘eggs’
foo.h:2: note: previous declaration of ‘eggs’ was here
foobar.c: In function ‘main’:
foobar.c:11: error: too few arguments to function ‘spam’
foobar.c:11: error: too few arguments to function ‘eggs’
make: *** [foobar.o] Error 1

Ладно, это не было неожиданностью, это просто сказало нам, что мы уже знали или, по крайней мере, подозревали.

Так можем ли мы как-то разрешить конфликт идентификаторов без изменения исходного кода или заголовков исходных библиотек? На самом деле мы можем.

Сначала давайте решим проблемы времени компиляции. Для этого мы окружаем заголовок включает в себя кучу препроцессора #define директивы, которые ставят префикс перед всеми символами, экспортируемыми библиотекой. Позже мы сделаем это с помощью удобного удобного заголовка-обертки, но просто для демонстрации того, что происходит, мы делали это дословно в исходном файле foobar.c:

Пример /ex02/foobar.c

#include <stdio.h>

#define spam foo_spam
#define eggs foo_eggs
#  include "foo.h"
#undef spam
#undef eggs

#define spam bar_spam
#define eggs bar_eggs
#  include "bar.h"
#undef spam
#undef eggs

int main()
{
    const int    new_bar_spam = 3;
    const double new_bar_eggs = 5.0f;

    printf("foo: spam = %d, eggs = %f\n", foo_spam(), foo_eggs() );
    printf("bar: old spam = %d, new spam = %d ; old eggs = %f, new eggs = %f\n", 
           bar_spam(new_bar_spam), new_bar_spam, 
           bar_eggs(new_bar_eggs), new_bar_eggs );

    return 0;
}

Теперь, если мы скомпилируем это...

example/ex02/ $ make
cc    -c -o foobar.o foobar.c
cc   foobar.o foo.o bar.o   -o foobar
bar.o: In function `spam':
bar.c:(.text+0x0): multiple definition of `spam'
foo.o:foo.c:(.text+0x0): first defined here
bar.o: In function `eggs':
bar.c:(.text+0x1e): multiple definition of `eggs'
foo.o:foo.c:(.text+0x19): first defined here
foobar.o: In function `main':
foobar.c:(.text+0x1e): undefined reference to `foo_eggs'
foobar.c:(.text+0x28): undefined reference to `foo_spam'
foobar.c:(.text+0x4d): undefined reference to `bar_eggs'
foobar.c:(.text+0x5c): undefined reference to `bar_spam'
collect2: ld returned 1 exit status
make: *** [foobar] Error 1

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

Давайте посмотрим на таблицы символов с помощью утилиты nm:

example/ex02/ $ nm foo.o
0000000000000019 T eggs
0000000000000000 T spam
0000000000000008 C the_eggs
0000000000000004 C the_spams

example/ex02/ $ nm bar.o
0000000000000019 T eggs
0000000000000000 T spam
0000000000000008 C the_eggs
0000000000000004 C the_spams

Итак, теперь перед нами стоит задача поставить префикс этих символов в некоторый непрозрачный двоичный файл. Да, я знаю, что в ходе этого примера у нас есть источники, и мы могли бы изменить это там. Но пока предположим, что у вас есть только эти файлы .o, или .a (что на самом деле просто куча .o).

цель спасения

Есть один инструмент, особенно интересный для нас: objcopy

objcopy работает с временными файлами, поэтому мы можем использовать его так, как если бы он работал на месте. Есть одна опция / операция, которая называется --prefix-symbols, и у вас есть 3 предположения, что она делает.

Итак, давайте бросим этого парня в наши упрямые библиотеки:

example/ex03/ $ objcopy --prefix-symbols=foo_ foo.o
example/ex03/ $ objcopy --prefix-symbols=bar_ bar.o

НМ показывает нам, что это, кажется, работает:

example/ex03/ $ nm foo.o
0000000000000019 T foo_eggs
0000000000000000 T foo_spam
0000000000000008 C foo_the_eggs
0000000000000004 C foo_the_spams

example/ex03/ $ nm bar.o
000000000000001e T bar_eggs
0000000000000000 T bar_spam
0000000000000008 C bar_the_eggs
0000000000000004 C bar_the_spams

Давайте попробуем связать все это:

example/ex03/ $ make
cc   foobar.o foo.o bar.o   -o foobar

И действительно, это сработало:

example/ex03/ $ ./foobar 
foo: spam = 0, eggs = 0.000000
bar: old spam = 0, new spam = 3 ; old eggs = 0.000000, new eggs = 5.000000

Теперь я оставляю это в качестве упражнения для читателя, чтобы реализовать инструмент / скрипт, который автоматически извлекает символы библиотеки, используя nm, записывает заголовочный файл оболочки структуры

/* wrapper header wrapper_foo.h for foo.h */
#define spam foo_spam
#define eggs foo_eggs
/* ... */
#include <foo.h>
#undef spam
#undef eggs
/* ... */

и применяет префикс символа к объектным файлам статической библиотеки, используя objcopy.

Как насчет общих библиотек?

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

Вы не сможете обойтись написанием батутной обертки. Хуже того, вы не можете ссылаться на разделяемую библиотеку на уровне объектного файла, но вынуждены выполнять динамическую загрузку. Но это заслуживает отдельной статьи.

Оставайтесь с нами, и счастливого кодирования.

Правила стандарта C накладывают на них некоторые ограничения (для безопасной компиляции): компилятор A C может просматривать только первые 8 символов идентификатора, поэтому foobar2k_eggs и foobar2k_spam могут корректно интерпретироваться как одни и те же идентификаторы - однако каждый современный компилятор допускает произвольные длинные идентификаторы, поэтому в наше время (21 век) нам не нужно об этом беспокоиться.

Это не просто расширение современных компиляторов; текущий стандарт C также требует, чтобы компилятор поддерживал достаточно длинные внешние имена. Я забыл точную длину, но это примерно 31 символ, если я правильно помню.

Но что, если вы столкнулись с некоторыми библиотеками, из которых вы не можете изменить имена / идентификаторы символов? Может быть, у вас есть только статический двоичный файл и заголовки, или вы не хотите, или вам не разрешено настраивать и перекомпилировать себя.

Тогда ты застрял. Пожаловаться автору библиотеки. Однажды я столкнулся с такой ошибкой, когда пользователи моего приложения не смогли собрать его на Debian из-за Debian. libSDL соединение libsoundfileкоторый (по крайней мере в то время) загрязнил глобальное пространство имен ужасными переменными, такими как dsp (Я не шучу!). Я пожаловался в Debian, и они исправили свои пакеты и отправили исправление в апстрим, где, как я полагаю, оно было применено, поскольку я больше никогда не слышал о проблеме.

Я действительно считаю, что это лучший подход, потому что он решает проблему для всех. Любой локальный хак, который вы сделаете, оставит проблему в библиотеке для следующего несчастного пользователя, с которым придется столкнуться и снова бороться.

Если вам действительно нужно быстрое исправление, и у вас есть источник, вы можете добавить кучу -Dfoo=crappylib_foo -Dbar=crappylib_bar и т.д. в make-файл, чтобы исправить это. Если нет, используйте objcopy Решение, которое вы нашли.

Если вы используете GCC, переключатель компоновщика --allow-множественное определение является удобным инструментом отладки. Это заставляет компоновщика использовать первое определение (и не ныть об этом). Подробнее об этом здесь.

Это помогло мне во время разработки, когда у меня есть источник доступной библиотеки, поставляемой поставщиком, и по той или иной причине мне нужно найти функцию библиотеки. Переключатель позволяет вам скомпилировать и связать локальную копию исходного файла и по-прежнему ссылаться на неизмененную статическую библиотеку поставщика. Не забудьте вытащить переключатель из символов make после завершения путешествия. Отправка кода релиза с преднамеренными коллизиями пространства имен подвержена ошибкам, включая непреднамеренные коллизии пространства имен.

Другие вопросы по тегам