Как бороться с символьными коллизиями между статически связанными библиотеками? - PullRequest
76 голосов
/ 04 августа 2011

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

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

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

Ответы [ 3 ]

123 голосов
/ 04 августа 2011

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

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

examples / 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

example / 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 :

example / 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

objcopy работает с временными файлами, поэтому мыможет использовать его так, как если бы он работал на месте.Есть одна опция / операция под названием - префикс-символы , и у вас есть 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 .

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

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

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

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

7 голосов
/ 04 августа 2011

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

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

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

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

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

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

3 голосов
/ 18 июня 2017

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

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

...