[c] 정적으로 링크 된 라이브러리 간의 기호 충돌을 처리하는 방법은 무엇입니까?

라이브러리를 작성할 때 가장 중요한 규칙과 모범 사례 중 하나는 라이브러리의 모든 심볼을 라이브러리 별 네임 스페이스에 넣는 것입니다. C ++는 namespace키워드 덕분에 이것을 쉽게 만듭니다 . C에서 일반적인 접근 방식은 식별자 앞에 라이브러리 특정 접두사를 붙이는 것입니다.

C 표준의 규칙 (안전 컴파일)들에 대한 몇 가지 제약 넣어 : AC 컴파일러는 식별자의 처음 8 자만 볼 수 있으므로 foobar2k_eggsfoobar2k_spam유효 같은 식별자로 해석 될 수있다 – 그러나 모든 현대 컴파일러는 임의의 긴 식별자 가능 , 그래서 우리 시대 (21 세기)에 우리는 이것에 대해 신경 쓸 필요가 없습니다.

그러나 심볼 이름 / ID를 변경할 수없는 일부 라이브러리에 직면하면 어떻게됩니까? 정적 바이너리와 헤더 만 얻었거나 원하지 않거나 스스로 조정하고 재 컴파일 할 수 없습니다.



답변

적어도 정적 라이브러리 의 경우 매우 편리하게 해결할 수 있습니다.

라이브러리 foobar의 헤더를 고려하십시오 . 이 튜토리얼을 위해 소스 파일도 제공합니다.

예 /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라이브러리에서 내 보낸 모든 기호의 접두사를 지정 하는 여러 전 처리기 지시문이 포함됩니다 . 나중에 우리는 멋지고 아늑한 wrapper-header로 이것을 수행하지만, 무슨 일이 일어나고 있는지 보여주기 위해 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

objcopy는 임시 파일에서 작동하므로 제자리에서 작동하는 것처럼 사용할 수 있습니다. –prefix-symbols 라는 하나의 옵션 / 작업이 있으며 그 작업 을 3 가지 추측 할 수 있습니다.

따라서이 친구를 완고한 라이브러리에 던져 보겠습니다.

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

nm 는 이것이 작동하는 것처럼 보임을 보여줍니다.

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 표준의 규칙은 이것들 (안전한 컴파일을 위해)에 몇 가지 제약을 둡니다. AC 컴파일러는 식별자의 처음 8 자만 볼 수 있으므로 foobar2k_eggs 및 foobar2k_spam은 동일한 식별자로 유효하게 해석 될 수 있습니다. 그러나 모든 최신 컴파일러는 임의의 그래서 우리 시대 (21 세기)에 우리는 이것에 대해 신경 쓸 필요가 없습니다.

이것은 단지 최신 컴파일러의 확장이 아닙니다. 현재 C 표준 에서는 컴파일러가 합리적으로 긴 외부 이름을 지원 해야 합니다. 정확한 길이를 잊어 버렸 습니다만, 제대로 기억한다면 지금은 31 자 정도입니다.

그러나 심볼 이름 / ID를 변경할 수없는 일부 라이브러리에 직면하면 어떻게됩니까? 정적 바이너리와 헤더 만 얻었거나 원하지 않거나 스스로 조정하고 재 컴파일 할 수 없습니다.

그렇다면 당신은 갇혀 있습니다. 도서관의 저자에게 불평하십시오. 한 번은 내 응용 프로그램 사용자가 데비안의 libSDL링크 로 인해 데비안에서 빌드 할 수없는 버그가 발생했습니다.이 버그는 libsoundfile(적어도 당시에는) 전역 이름 공간을 dsp(농담이 아닙니다!) 와 같은 변수로 끔찍하게 오염 시켰습니다 . 나는 데비안에 불만을 토로했고 그들은 패키지를 고치고 수정 사항을 업스트림으로 보냈는데, 내가 그 문제에 대해 다시는 들어 본 적이 없기 때문에 적용되었다고 가정했습니다.

이것이 모든 사람 의 문제 해결하기 때문에 이것이 최선의 방법이라고 생각합니다 . 모든 로컬 해킹은 다음 불행한 사용자가 다시 만나서 싸울 수 있도록 문제를 라이브러리에 남겨 둘 것입니다.

정말 빠른 수정이 필요하고 소스가 -Dfoo=crappylib_foo -Dbar=crappylib_bar있다면 makefile에 여러 가지 등을 추가 하여 수정할 수 있습니다. 그렇지 않은 경우 objcopy찾은 솔루션을 사용하십시오 .


답변

GCC를 사용하는 경우 –allow-multiple-definition 링커 스위치는 편리한 디버깅 도구입니다. 이것은 링커가 첫 번째 정의를 사용하도록 유도합니다 (그리고 그것에 대해 징징 대지 않음). 여기 에 대해 자세히 알아 보십시오 .

이것은 공급 업체에서 제공하는 라이브러리의 소스를 사용할 수 있고 어떤 이유로 든 라이브러리 기능을 추적해야 할 때 개발 중에 도움이되었습니다. 이 스위치를 사용하면 소스 파일의 로컬 복사본에서 컴파일 및 링크 할 수 있으며 수정되지 않은 정적 공급 업체 라이브러리에 계속 연결할 수 있습니다. 발견의 여정이 완료되면 make 기호에서 스위치를 다시 잡아 당기는 것을 잊지 마십시오. 의도적 인 네임 스페이스 충돌이있는 배송 릴리스 코드는 의도 하지 않은 네임 스페이스 충돌을 포함하여 함정에 취약합니다 .


답변