Избегайте ошибки компоновщика множественных определений, когда не используются переопределенные символы - PullRequest
1 голос
/ 07 мая 2020

Я пытаюсь создать исполняемый файл, который ссылается на различные общие и c библиотеки. Оказывается, две библиотеки stati c определяют один и тот же символ, что приводит к ошибке компоновщика нескольких определений. Мой исполняемый файл не использует этот символ, так что это не проблема.

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

Есть ли способ сказать компоновщику «жаловаться на несколько определений, только если используется символ»? Или, как вариант, скажите: «из библиотеки AB C игнорировать символ XYZ». Я разрабатываю с g ++ на linux.

1 Ответ

1 голос
/ 13 мая 2020

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

Вы должны быть знакомы с природой библиотек stati c и тем, как они используются при связывании, как указано здесь

Вариант символов Superflous Globals

Вот пара исходных файлов и файл заголовка:

один. cpp

#include <onetwo.h>

int clash = 1;

int get_one()
{
    return clash;
}

два. cpp

#include <onetwo.h>

int get_two()
{
    return 2;
}

onetwo.h

#pragma once

extern int get_one();
extern int get_two();

Они были встроены в статическую c библиотеку libonetwo.a

$ g++ -Wall -Wextra -pedantic -I. -c one.cpp two.cpp
$ ar rcs libonetwo.a one.o two.o

, предполагаемый API которой определен в onetwo.h

Аналогично, были созданы некоторые другие исходные файлы и заголовок. в c библиотеку libfourfive.a, предполагаемый API которой определен в fourfive.h

четыре. cpp

#include <fourfive.h>

int clash = 4;

int get_four()
{
    return clash;
}

пять. cpp

#include <fourfive.h>

int get_five()
{
    return 5;
}

fourfive.h

#pragma once

extern int get_four();
extern int get_five();

А вот исходный код программы, которая зависит от обеих библиотек:

прогр. cpp

* 1 057 *

, который мы пытаемся построить следующим образом:

$ g++ -Wall -Wextra -pedantic -I. -c prog.cpp
$ g++ -o prog prog.o -L. -lonetwo -lfourfive
/usr/bin/ld: ./libfourfive.a(four.o):(.data+0x0): multiple definition of `clash'; ./libonetwo.a(one.o):(.data+0x0): first defined here
collect2: error: ld returned 1 exit status

сталкивается с конфликтом имен для символа clash, потому что он глобально определен в двух объектных файлах, которые требуются для связывания, one.o и four.o:

$ readelf -s libonetwo.a libfourfive.a | egrep '(File|Symbol|OBJECT|FUNC)'
File: libonetwo.a(one.o)
Symbol table '.symtab' contains 11 entries:
     9: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    3 clash
    10: 0000000000000000    16 FUNC    GLOBAL DEFAULT    1 _Z7get_onev
File: libonetwo.a(two.o)
Symbol table '.symtab' contains 10 entries:
     9: 0000000000000000    15 FUNC    GLOBAL DEFAULT    1 _Z7get_twov
File: libfourfive.a(four.o)
Symbol table '.symtab' contains 11 entries:
     9: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    3 clash
    10: 0000000000000000    16 FUNC    GLOBAL DEFAULT    1 _Z8get_fourv
File: libfourfive.a(five.o)
Symbol table '.symtab' contains 10 entries:
     9: 0000000000000000    15 FUNC    GLOBAL DEFAULT    1 _Z8get_fivev

Символ проблемы clash не упоминается в нашем собственном коде prog.(cpp|o). Вы задались вопросом:

Есть ли способ сказать компоновщику «жаловаться на несколько определений, только если используется символ»?

Нет, нет, но это несущественный. one.o не был бы извлечен из libonetwo.a и связан с программой, если бы компоновщик не нуждался в этом для разрешения некоторого символа. Это нужно для разрешения get_one. Точно так же он связал только four.o, потому что это необходимо для разрешения get_four. Итак, противоречащие определения clash находятся в связи. И хотя prog.o не использует clash, он использует get_one, который использует clash и который намеревается использовать определение clash в one.o. Точно так же prog.o использует get_four, который использует clash и намеревается использовать другое определение в four.o.

Даже если clash был также не использовался каждой библиотекой как и программа, тот факт, что он определен в нескольких объектных файлах, которые должны быть связаны с программой, означает, что программа будет содержать несколько его определений, и только --allow-multiple-definitions позволит это.

В этом свете вы также увидите, что:

Или, альтернативно, [есть ли способ] сказать это: «из библиотеки AB C игнорировать символ XYZ».

в генерал не летает. Если бы мы могли сказать компоновщику игнорировать (скажем) определение clash в four.o и разрешать символ везде до определения в one.o (единственный другой кандидат), тогда get_four() вернет 1 вместо 4 в наша программа. Фактически это эффект --allow-multiple-definitions, поскольку он приводит к использованию первого определения в привязке.

Изучив исходный код libonetwo.a (или libfourfive.a), мы можем довольно уверенно определите root причину проблемы. Символ clash оставлен с внешней связью, где требуется только внутренняя связь, поскольку он не объявлен в связанном файле заголовка и нигде не упоминается в библиотеке, кроме файла, в котором он определен. В исходных файлах с нарушением правил должно быть написано:

one_good. cpp

#include <onetwo.h>

namespace {
    int clash = 1;
}

int get_one()
{
    return clash;
}

four_good. cpp

#include <fourfive.h>

namespace {
    int clash = 4;
}

int get_four()
{
    return clash;
}

и все было бы хорошо:

$ g++ -Wall -Wextra -pedantic -I. -c one_good.cpp four_good.cpp
$ readelf -s one_good.o four_good.o | egrep '(File|Symbol|OBJECT|FUNC)'
File: one_good.o
Symbol table '.symtab' contains 11 entries:
     5: 0000000000000000     4 OBJECT  LOCAL  DEFAULT    3 _ZN12_GLOBAL__N_15clashE
    10: 0000000000000000    16 FUNC    GLOBAL DEFAULT    1 _Z7get_onev
File: four_good.o
Symbol table '.symtab' contains 11 entries:
     5: 0000000000000000     4 OBJECT  LOCAL  DEFAULT    3 _ZN12_GLOBAL__N_15clashE
    10: 0000000000000000    16 FUNC    GLOBAL DEFAULT    1 _Z8get_fourv

$ g++ -o prog prog.o one_good.o four_good.o
$./prog; echo $?
5

Так как переписывать исходный код подобным образом нельзя, мы должны изменить объектные файлы с тем же эффектом. Инструмент для этого: objcopy.

$ objcopy --localize-symbol=clash libonetwo.a libonetwo_good.a

Эта команда имеет тот же эффект, что и выполнение:

$ objcopy --localize-symbol=clash orig.o fixed.o

для каждого из объектных файлов libonetwo(orig.o) для вывода фиксированного объектного файла fixed.o и архивации всех fixed.o файлов в новую c библиотеку libonetwo_good.a. И Эффект --localize-symbol=clash для каждого объектного файла заключается в изменении связи символа clash, если он определен, с внешнего (GLOBAL) на внутреннее (LOCAL):

$ readelf -s libonetwo_good.a | egrep '(File|Symbol|OBJECT|FUNC)'
File: libonetwo_good.a(one.o)
Symbol table '.symtab' contains 11 entries:
     9: 0000000000000000     4 OBJECT  LOCAL  DEFAULT    3 clash
    10: 0000000000000000    16 FUNC    GLOBAL DEFAULT    1 _Z7get_onev
File: libonetwo_good.a(two.o)
Symbol table '.symtab' contains 10 entries:

Теперь компоновщик не видит LOCAL определение clash в libonetwo_good.a(one.o).

Этого достаточно, чтобы предотвратить ошибку множественного определения, но поскольку libfourfive.a имеет тот же дефект, мы исправим и его :

$ objcopy --localize-symbol=clash libfourfive.a libfourfive_good.a

И затем мы можем повторно связать prog, используя фиксированные библиотеки.

$ g++ -o prog prog.o -L. -lonetwo_good -lfourfive_good
$ ./prog; echo $?
5

Вариант тупика глобальных символов

В этом сценарии Источники и заголовки для libonetwo.a:

один. cpp

#include <onetwo.h>
#include "priv_onetwo.h"

int inc_one()
{
    return inc(clash);
}

два. cpp

#include <onetwo.h>
#include "priv_onetwo.h"

int inc_two()
{
    return inc(clash + 1);
}

priv_onetwo. cpp

#include "priv_onetwo.h"

int clash = 1;

int inc(int i)
{
    return i + 1;
}

priv_onetwo.h

#pragma once

extern int clash;
extern int inc(int);

onetwo.h

#pragma once

extern int inc_one();
extern int inc_two();

А для libfourfive.a это:

четыре. cpp

#include <fourfive.h>
#include "priv_fourfive.h"

int dec_four()
{
    return dec(clash);
}

пять. cpp

#include <fourfive.h>
#include "priv_fourfive.h"

int dec_five()
{
    return dec(clash + 1);
}

priv_fourfive. cpp

#include "priv_fourfive.h"

int clash = 4;

int dec(int i)
{
    return i - 1;
}

priv_fourfive. h

#pragma once

extern int clash;
extern int dec(int);

fourfive.h

#pragma once

extern int dec_four();
extern int dec_five();

Каждая из этих библиотек построена с некоторыми общими внутренними компонентами, определенными в исходном файле - (priv_onetwo.cpp | priv_fourfive.cpp) - и эти внутренние компоненты глобально объявлены для построения библиотеки через частный заголовок - (priv_onetwo.h | priv_fourfive.h) - который не распространяется с библиотекой. Это недокументированные символы, но, тем не менее, они доступны компоновщику.

Теперь в каждой библиотеке есть два файла, которые делают неопределенные (UND) ссылки на глобальный символ clash, который определен в другом файле:

$ readelf -s libonetwo.a libfourfive.a | egrep '(File|Symbol|OBJECT|FUNC|clash)'
File: libonetwo.a(one.o)
Symbol table '.symtab' contains 13 entries:
     9: 0000000000000000    23 FUNC    GLOBAL DEFAULT    1 _Z7inc_onev
    10: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND clash
File: libonetwo.a(two.o)
Symbol table '.symtab' contains 13 entries:
     9: 0000000000000000    26 FUNC    GLOBAL DEFAULT    1 _Z7inc_twov
    10: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND clash
File: libonetwo.a(priv_onetwo.o)
Symbol table '.symtab' contains 11 entries:
     9: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    2 clash
    10: 0000000000000000    19 FUNC    GLOBAL DEFAULT    1 _Z3inci
File: libfourfive.a(four.o)
Symbol table '.symtab' contains 13 entries:
     9: 0000000000000000    23 FUNC    GLOBAL DEFAULT    1 _Z8dec_fourv
    10: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND clash
File: libfourfive.a(five.o)
Symbol table '.symtab' contains 13 entries:
     9: 0000000000000000    26 FUNC    GLOBAL DEFAULT    1 _Z8dec_fivev
    10: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND clash
File: libfourfive.a(priv_fourfive.o)
Symbol table '.symtab' contains 11 entries:
     9: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    2 clash
    10: 0000000000000000    19 FUNC    GLOBAL DEFAULT    1 _Z3deci

Наш источник программы на этот раз:

prog. cpp

#include <onetwo.h>
#include <fourfive.h>

int main()
{
    return inc_one() + dec_four();
}

и:

$ g++ -Wall -Wextra -pedantic -I. -c prog.cpp
$ g++ -o prog prog.o -L. -lonetwo -lfourfive
/usr/bin/ld: ./libfourfive.a(priv_fourfive.o):(.data+0x0): multiple definition of `clash'; ./libonetwo.a(priv_onetwo.o):(.data+0x0): first defined here
collect2: error: ld returned 1 exit status

еще раз clash многократно определено. Чтобы разрешить inc_one в main, компоновщику требуется one.o, что обязывает его разрешить inc, что делает его необходимым priv_onetwo.o, который содержит первое определение clash. Чтобы разрешить dec_four в main, компоновщику требуется four.o, что обязывает его разрешить dec, что требует priv_fourfive.o, которое содержит конкурирующее определение clash.

В этом случае это не ошибка кодирования ни в одной из библиотек, в которых clash есть внешняя связь. Он должен иметь внешнюю связь. Локализация определения clash с objcopy в любом из libonetwo.a(priv_onetwo.o) или libfourfive.a(priv_fourfive.o) не будет работать. Если мы сделаем это, компоновка будет успешной, но выведет программу с ошибками, потому что компоновщик разрешит clash одно сохранившееся GLOBAL определение из другого объектного файла: тогда dec_four() вернет 0 вместо 3 в программе, dec_five() вернет 1, а не 4; иначе inc_one() вернет 5, а inc_two() вернет 6. И если мы локализуем оба определения, тогда определение clash не будет найдено в связи prog, чтобы удовлетворить ссылки в one.o или four.o, и он не будет работать для неопределенной ссылки на clash

На этот раз objcopy снова приходит на помощь, но с другим вариантом 1 :

$ objcopy --redefine-sym clash=clash_onetwo libonetwo.a libonetwo_good.a

Результатом этой команды является создание новой c библиотеки libonetwo_good.a, содержащей новые объектные файлы, которые попарно идентичны файлам в libonetwo.a, за исключением того, что символ clash был везде заменен на clash_onetwo:

$ readelf -s libonetwo_good.a | egrep '(File|Symbol|clash)'
File: libonetwo_good.a(one.o)
Symbol table '.symtab' contains 13 entries:
    10: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND clash_onetwo
File: libonetwo_good.a(two.o)
Symbol table '.symtab' contains 13 entries:
    10: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND clash_onetwo
File: libonetwo_good.a(priv_onetwo.o)
Symbol table '.symtab' contains 11 entries:
     9: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    2 clash_onetwo

Мы сделаем то же самое с libfourfive.a:

$ objcopy --redefine-sym clash=clash_fourfive libfourfive.a libfourfive_good.a

Теперь нам хорошо go еще раз :

$ g++ -o prog prog.o -L. -lonetwo_good -lfourfive_good
$ ./prog; echo $?
5

Из двух решений используйте исправление для Вариант символов Superflous Globals , если у вас есть избыточные глобалы, хотя исправление для The Global Символы Вариант тупика также будет работать. Никогда не желательно вмешиваться в объектные файлы между компиляцией и компоновкой; это может быть только неизбежным или меньшим из зол. Но если вы собираетесь вмешиваться в них, локализация глобального символа, который никогда не должен был быть глобальным, будет более очевидным вмешательством, чем изменение имени символа на имя, не имеющее происхождения в исходном коде.


[1] Не забывайте, что если вы хотите использовать objcopy с любым аргументом опции, который является символом в объектном файле C ++, вы должны использовать искаженное имя идентификатора C ++. чем соответствует символу. В этом демонстрационном коде случается, что искаженное имя идентификатора C ++ clash также clash. Но если, например, полный идентификатор был onetwo::clash, его искаженное имя было бы _ZN6onetwo5clashE, как сообщает nm или readelf. И наоборот, если вы хотите использовать objcopy для замены _ZN6onetwo5clashE в объектном файле на символ, который будет выглядеть как onetwo::klash, тогда этот символ будет _ZN6onetwo5klashE.

...