♧ 같은 이름을 갖는 심볼의 충돌
C 또는 C++프로그램에서 같은 이름을 갖는 전역 심볼이 둘 이상 존재하면 어떻게 될까?
[ .o 파일을 모아서 링크할 경우 ] |
|||
▷ 우선 a.c와 b.c가 동일한 이름의 func()함수 정의
▷ main.c에서 func() 호출.
▶ 위 세 파일을 각각 컴파일해서 실행.
☞ 정적으로 링크하려 하면 func() 함수가 여러 개 존재하므로 링커에서 에러를 출력. 이 에러 덕분에 예상외의 func() 함수가 호출되는 문제를 사전에 피해갈 수 있다.
|
[ 라이브러리를 생성해서 링크할 경우 ] |
||
♧ 앞에서와 동일한 소스코드 a.c와 b.c에서 ar을 사용해 정적 라이브러리를 생성하면 링크할 때 에러가 발생하지 않는다. 그 이유는 링커와 달리 오브젝트 파일로 아카이브를 생성하는 ar 명령은 심볼 충돌을 검사하지 않기 때문.
▶ 생성된 실행 파일 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』 에는 다음과 같이 기술 되어 있다.
|
[ C++와 같은 이름의 클래스 ] |
||||
C++에서 동일 심볼 문제는 예상치 않은 멤버 함수를 호출하는 문제를 야기한다.
소비세를 계산하는 s.{h, cpp}라는 소스코드와 이를 이용한 main.cpp가 있다고 하자. ▷ a.h 작성
▷ a.cpp 작성
▷ main.cpp 작성
▶ 위 소스코드 컴파일, 링크해서 실행하면 apple: 105라는 메시지 출력.
여기서 별도로 개발된 b.cpp가 존재하고 같은 이름의 클래스 Tax가 구현되어 있다고 하자. b.cpp내의 Tax 클래스의 용도는 a.cpp와는 전혀 다르다. ▷ b.cpp 작성
▶ 그리고 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를 생각해보자.
▶ 컴파일 실행하면 256 출력.
여기에 main.cpp와는 전혀 별개로 개발된 다음과 같은 a.cpp가 있다. a.cpp 내에도 클래스 Foo가 구현되어 있다.
▶ 컴파일한 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장에 다음과 같이 설명되어 있다.
즉, 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 정의로 되는 것은 이 때문이다.
|
'컴퓨터 서적 정리 > Binary Hacks' 카테고리의 다른 글
[Hack #22] GCC 확장기능 (빌트인, 애트리뷰트, 레이블) (0) | 2011.07.05 |
---|---|
[Hack #20] 공유 라이브러리에 PIC를 사용하는 이유 (1) | 2011.07.05 |
[Hack #18] C와 C++ 프로그램 링크 방법 (0) | 2011.07.04 |
[Hack #17] ar - 정적 라이브러리 다루기 (0) | 2011.07.04 |
[Hack #16] strip - 오브젝트 파일에서 심볼 삭제 (0) | 2011.07.04 |