2011. 7. 4. 22:44

♧ 같은 이름을 갖는 심볼의 충돌

C 또는 C++프로그램에서 같은 이름을 갖는 전역 심볼이 둘 이상 존재하면 어떻게 될까?

   

[ .o 파일을 모아서 링크할 경우 ]

▷ 우선 a.c와 b.c가 동일한 이름의 func()함수 정의

/* a.c */

#include <stdio.h>

void func() {

printf ("func() in a.c\n");

}

/* b.c */

#include <stdio.h>

void func() {

printf ("func() in b.c\n");

}

▷ main.c에서 func() 호출.

void func();

int main() {

func();

return 0;

}

   

▶ 위 세 파일을 각각 컴파일해서 실행.

☞ 정적으로 링크하려 하면 func() 함수가 여러 개 존재하므로 링커에서 에러를 출력.

이 에러 덕분에 예상외의 func() 함수가 호출되는 문제를 사전에 피해갈 수 있다.

  

   

   

[ 라이브러리를 생성해서 링크할 경우 ]

♧ 앞에서와 동일한 소스코드 a.c와 b.c에서 ar을 사용해

정적 라이브러리를 생성하면 링크할 때 에러가 발생하지 않는다.

그 이유는 링커와 달리 오브젝트 파일로 아카이브를 생성하는

ar 명령은 심볼 충돌을 검사하지 않기 때문.

% gcc -c a.c b.c main.c

% ar cr libfoo.a a.o b.o

% gcc main.o libfoo.a

   

▶ 생성된 실행 파일 a.out을 실행

☞ 실행해 보면 a.c의 func()를 호출함을 알 수 있다.

이는 링크할 때 libfoo.a에 있는 두 func() 중에 a.c의 함수를 b.c의 함수보다 먼저 찾기 때문.

   

▶ 정적 라이브러리 libfoo.a를 ar 명령으로 생성할 때

인자의 순서를 a.o b.o에서 b.o a.o로 변경하면 b.o의 func()가 호출.

   

▶ 또한 a.c와 b.c를 이용해 각각 정적 라이브러리를 만든 경우에도 링크할 때는

에러가 발생하지 않는다. 이때 gcc에 넘긴 liba.a와 libb.a의 순서를 반대로 하면

b.c의 func()를 호출하게 된다.

   

▶ 마찬가지로 a.c와 b.c로 각각 동적 공유 오브젝트 생성해서 링크하면

에러는 발생하지 않는다. 아래 예에서는 a.so의 func()가 호출.

   

링크할 때 명령행 인자의 a.so와 b.so의 순서를 바꾸면 b.so의 func() 함수를 먼저 찾게 된다.

동적 링크할 때는 기본적으로 먼저 발견한 심볼 정의가 사용된다.

   

▷ GNU C Library의 주요 개발 구성원인 울리히 드레퍼가 작성한

How to write Shared Libraries 에는 다음과 같이 기술 되어 있다.

"scope 내에 동일한 심볼 정의가 둘 이상 포함되어 있어도 상관 없다.

심볼을 찾는 알고리즘은 단순히 최초에 발견한 심볼 정의를 사용할 뿐이다.

....이 개념은 상당히 강력해서 .... 그 일례는 LD_PRELOAD의 기능을 이용하는 것이다."

  

   

   

[ C++와 같은 이름의 클래스 ]

C++에서 동일 심볼 문제는 예상치 않은 멤버 함수를 호출하는 문제를 야기한다.

   

소비세를 계산하는 s.{h, cpp}라는 소스코드와 이를 이용한 main.cpp가 있다고 하자.

▷ a.h 작성

/* a.h */

class Tax {

public:

int tax (int price);

Tax();

private:

double consumption_tax_;

};

   

▷ a.cpp 작성

/* a.cpp */

#include "a.h"

Tax::Tax() : consumption_tax_(1.05) {}

   

int Tax::tax(int price) {

return price *consumption_tax_;

}

   

▷ main.cpp 작성

/* main.cpp */

#include <iostream>

#include "a.h"

int main() {

Tax tax;

int apple_price =100;

std::cout << "apple: " << tax.tax(apple_price) << std::endl;

return 0;

}

▶ 위 소스코드 컴파일, 링크해서 실행하면 apple: 105라는 메시지 출력.

   

여기서 별도로 개발된 b.cpp가 존재하고 같은 이름의 클래스 Tax가 구현되어 있다고 하자.

b.cpp내의 Tax 클래스의 용도는 a.cpp와는 전혀 다르다.

▷ b.cpp 작성

/* b.cpp */

class Tax {

public:

int deduct (int income);

Tax();

private:

double deduction_rate_;

};

   

Tax::Tax() : deduction_rate_(0.1) {}

   

int Tax::deduct (int income) {

return int(income * (1.0 - deduction_rate_));

}

▶ 그리고 b.cpp를 b.so로 컴파일하고 불행히도 b.so, a.so, main.so 순으로

링크된다면 성가신 일이 발생한다.

이번 실행 결과는 apple: 10이다.

이는 Tax 객체의 생성자로 b.so 내의 Tax::Tax()가 호출되고, Tax 객체의 멤버함수로서

a.so 내의 Tax::tax()가 호출되었기 때문이다.

즉, 예상외의 생성자가 호출되어 버그의 원인이 되었다.

   

☞ 프로그램이 거대해질수록 발생가능성이 커진다.

따라서 namespace를 사용해서 충돌을 피해갈 필요가 있다.

심볼을 하나의 파일 내에 모두 넣는다면 이름 없는 namespace를 사용해도 될 것이다.

예를 들면 b.cpp 전체를 snamespace { … }로 둘러싸면 위와 같은 문제는 발생하지 않는다.

  

   

[ weak 심볼 ]

♧ 첫머리에서 .o 파일을 모아서 링크할 때는 동일한 이름의 심볼 충돌이 확인 했다.

그러나 weak 심볼이 존재하면 애기는 달라진다.

   

다음과 같은 프로그램 main.cpp를 생각해보자.

/* main.cpp */

#include <iostream>

class Foo {

public:

Foo (int x) : x_(x) {}

void func() {

std::cout << x_ << std::endl;

}

private:

int x_;

};

   

int main() {

Foo foo(256);

foo.func();

return 0;

}

▶ 컴파일 실행하면 256 출력.

   

여기에 main.cpp와는 전혀 별개로 개발된 다음과 같은 a.cpp가 있다.

a.cpp 내에도 클래스 Foo가 구현되어 있다.

/* a.cpp */

#include <iostream>

class Foo {

public:

Foo(int x);

private:

int x_;

};

Foo::Foo(int x) : x_(x * x) {}

▶ 컴파일한 a.o가 불행히 main에 링크된다면 매우 성가신 일이 발생한다.

   

☞ 이번 실행 결과는 256이 아니라 제곱수인 65536이 출력 되었다.

이때는 링크 순서가 main.o, a.o 또는 a.o, main.o 둘 다 상관 없이 결과는 변하지 않는다.

main.cpp 내에 정의된 생성자 Foo::Foo(int)가 아니라 a.cpp 내의 Foo::Foo(int)가

사용된 것은 분명하지만, 이런 문제가 발생하는 이유는 무엇일까?

   

▶ 〔weak 심볼〕이란 무엇인가?

원인은 main.o에 포함된 Foo::Foo(int)는 weak 심볼이고

a.o에 포함된 Foo::Foo(int)는 weak 심볼이 아니기 때문이다.

▷ 아래의 nm 출력 결과에서 W로 표시된 것이 weak 심볼을 나타내는 표시이다.

   

   

☞ weak 심볼에 관해서는 『Linkers & Loaders』(john R. Levine 저, Morgan Kaufmaun)

의 6장에 다음과 같이 설명되어 있다.

"ELF에서는 weak 참조 외에 weak 정의라고 하는 또 하나의 weak 심볼을

추가하고 있다. weak 정의는 일반적인 정의가 존재하지 않는 경우에 대역 심볼을

정의한다. 일번적인 정의가 존재하는 경우에는 weak 정의가 무시된다."

즉, a.o에 일반적인(비weak 인) 정의가 존재하므로 main.o에 있는

Foo::Foo(int)의 weak 정의는 무시되었다는 것이다.

일단 이 문제를 피해가려면 이름 없는 namespace를 사용해 링키지 링크를 막는 것이

효과적이다. 구체적으로 main.cppㅇ듸 클래스 Foo { … }주위에 namespace { … }로 둘러싼다.

   

▶ weak 정의와 중복 코드 제거

그런데 왜 main.o Foo::Foo(int) 및 Foo::Func()는 weak 정의가 되는 것일까?

그 이유는 g++는 인라인 함수를 weak 정의로서 컴파일 하기 때문이다.

(클래스 정의 내의 함수정의는 표준 C++ 규격에 따라 인라인 함수로 간주)

따라서 weak 정의는 .o파일 내의 .gnu.linkonce.t.*라는 이름의 섹션에 배치된다.

.gnu.linkonce.t.*는 링크할 때 중복 코드 제거에 사용되는 섹션이다.

클래스 정의는 대개의 경우 .h 파일 내에 기술되고,

.h 파일은 여러 개의 .cpp 파일에 #include 된다. 이 때문에 각각의 .o 파일에

포함된 weak 정의를 링크할 때 하나로 모을 필요가 있다.

인라인 함수가 weak 정의로 되는 것은 이 때문이다.

  

 

Posted by devanix