알라딘MGG와이드바


C++ : One Definition Rule (ODR) 개발 이야기

팀원분들과 함께 프로젝트 셋팅을 이것저것 살펴보다가 다음과 같은 얘기가 나왔다.
만약 코드가 이렇게 되어 있다면 어떻게 될까?


   1: Test.h
   2:  
   3: class Test {
   4: public:
   5: #ifndef HIDE_VARIABLE
   6:     int m_Test[10];
   7: #endif
   8: };
   9:  
  10: Aho.h
  11:  
  12: class Test;
  13: int GetSizeA();
  14: Test* GetNewTestA();
  15:  
  16: Aho.cpp
  17:  
  18: //#define HIDE_VARIABLE
  19: #include "Test.h"
  20: #include "Aho.h"
  21:  
  22: int GetSizeA() { return sizeof(Test); }
  23: Test* GetNewTestA() { return new Test(); }
  24:  
  25: Bho.h
  26:  
  27: class Test;
  28: int GetSizeB();
  29: Test* GetNewTestB();
  30:  
  31: Bho.cpp
  32:  
  33: #define HIDE_VARIABLE   // important!
  34: #include "Test.h"
  35: #include "Bho.h"
  36:  
  37: int GetSizeB() { return sizeof(Test); }
  38: Test* GetNewTestB() { return new Test(); }
  39:  
  40: TestPrj.cpp
  41:  
  42: #include "Aho.h"
  43: #include "Bho.h"
  44: #include "Test.h"
  45:  
  46: int _tmain(int argc, _TCHAR* argv[]) {
  47:     int a = GetSizeA();
  48:     int b = GetSizeB();
  49:  
  50:     Test* pA = GetNewTestA();
  51:     Test* pB = GetNewTestB();
  52:  
  53:     pA->m_Test[0] = 1;
  54:     pB->m_Test[0] = 1;
  55:  
  56:     std::cout << a << '\t' << b << std::endl;
  57:     return 0;
  58: }

결과는
40    1
이 나온다. 같은 Test 클래스임에도 불구하고 sizeof 가 다르게 나오는 것이다. 그렇다면 pB 포인터가 받은 클래스에는 m_Test 가 없기 때문에 pB->m_Test[0] 은 에러가 나야 할 거 같은데 또 컴파일은 잘 된다. 이게 뭔가 하고 찾아봤더니 One Definition Rule (ODR) 이라는 게 있더라.
The C++ Programming Language p.203 에도 나와 있는데, 위키피디아의 설명을 간단히 정리해 보면

1. 컴파일 단위(translation unit) 에서, 어떤 template, type, function, object 의 정의(definition)는 유일해야 한다.
2. 전체 프로그램에서 객체나 비-인라인 함수에 대한 정의는 유일해야 한다.
3. type 이나 template, extern 인라인 함수같은 일부 것들은 하나 이상의 컴파일 단위에서 정의될 수 있다. 이 때 각 정의는 같아야 한다. 다른 컴파일 단위에 있는 비-extern 객체나 함수는 이름과 타입이 같아도 다른 엔티티로 본다.
일부 ODR 위반은 컴파일러가 잡아줘야 하지만, 진단하지 않아도 되는 ODR 위반도 있다.

그러니까 저 위의 코드는 ODR 위반이지만 컴파일러가 진단하지 않는 경우인듯 하다. pA, pB 둘 다 watch 창에서 보면 멀쩡해 보이거든. 하지만,위 코드를 이렇게 바뀌면 결과가 달라진다.


   1: int _tmain(int argc, _TCHAR* argv[]) {
   2:     int a = GetSizeA();
   3:     int b = GetSizeB();
   4:  
   5:     Test* pA = GetNewTestA();
   6:     Test* pB = GetNewTestB();
   7:  
   8:     pA->m_Test[0] = 0;
   9:     pB->m_Test[0] = 0;
  10:     pA->m_Test[9] = 9;
  11:     pB->m_Test[9] = 9;
  12:  
  13:     std::cout << a << '\t' << b << std::endl;
  14:  
  15:     return 0;
  16: }


아마도 Test* pB 는 실제로는 m_Test 멤버변수가 없는 듯 하다. Test 라는 클래스 이름은 같고, watch 에 넣어봐도 같아 보이지만 실제로는 다른 클래스를 가리키고 있는 것이다. 그래서 Test* pB 에는 없는 m_Test[9] 를 접근하다보니 로컬 스택이 깨져서 힙을 통해서 다른 메모리를 침범하기 때문에 std::cout 호출하는 순간에 크래시가 나 버리는 듯 하다. (혹시 제가 잘못 알고 있으면 도와주시라)

One Definition Rule (ODR) 라.. 재미있는 걸 하나 알게 되었다.

덧글

  • summerligh 2010/11/05 01:09 # 삭제 답글

    호환성 때문에 청산하지 못한 C의 유산이죠. (사실 오브젝트 파일에 기반하는 컴파일 모델로써는 어쩔 수 없기도 하지만...) ODR을 악용하는 재미있는(?) 방법 중 하나로, 원 클래스의 정의만 알고 있으면 private 필드들을 public으로 노출시킨 정의를 사용함으로써 캡슐화를 쉽게 깨뜨릴 수 있습니다.

    ODR과 직접적인 연관이 있는 문제는 아니지만 유사한 문제로는 멤버 함수 포인터 문제도 있습니다. 다중 상속이 개입된 멤버 함수 포인터는 그 크기가 sizeof(void*)와 다를 수도 있습니다. 이를 판단하려면 객체의 정의가 필요한데, 이 때 객체 정의 없이 그냥 전방 선언 해버리면 서로 다른 컴파일 유닛에서 다른 크기의 포인터를 쓰다가 프로그램이 깨져버린다더군요.

    차기 표준은 어렵겠지만 차차기 표준에서는 이런 문제 신경 안 써도 되도록 컴파일 모델에 대해 다시 한번 재고했으면 하는 바램 ;
  • 지나가다 2010/11/19 00:08 # 삭제

    그래서 저희 팀에서는 모조리 public 로 정의합니다.

    물론 그게 좋아서 그러는건 아니고(...;;;)
  • 김민장 2010/11/05 01:29 # 답글

    재미있는 문제이네요. 직접 테스트는 해보지 않고 제 생각을 남깁니다.

    1) m_Test[0], m_Test[9]에 접근하는 코드 자체는 컴파일은 되어야 합니다. 왜냐면 main 함수 내에서는 HIDE_VARIABLE이 없으므로 늘 m_Test가 있는 클래스의 정의만 보고 컴파일을 했기 때문입니다.

    2) 분명, pB는 m_Test[]가 없는 형태로 동적 할당이 되었습니다. 그런데 비록 sizeof는 1바이트가 나오더라도, malloc/new(1)을 하면 최소 4바이트나 8바이트 정렬에 맞춰 생성이 됩니다. 고로 m_Test[0]은 안전하게 처리될 가능성이 매우 높습니다. 그러나 m_Test[9]는 4/8바이트를 훨씬 벗어나는 영역이므로 로컬 스택은 아니고 힙 메모리를 망가뜨려(GetNewTestX는 new를 사용하네요) 엉뚱한 곳을 가리키고 죽거나 아니면 디버그 버전의 힙 관리자가 assertion을 띄울 것입니다.
  • kernel0 2010/11/05 01:48 # 삭제 답글

    vs2010 으로 해보니 pB->m_Test[0] 은 pb 가 힙으로부터 받는 1바이트에 쓰고 나머지(pb->m_Test[9])는 no man's land 에 덮어쓰지. 로컬 스택은 깨는 일이 없으니까 로컬 스택 이 깨진다는 얘기는 정정해야 할 듯. . win7 vs2010 release win32 에서는 m_Test 를 int array 대신 char array 로 56 bytes 쯤 되니 nt.dll 에 쓰려고 하면서 죽더군. 9정도에는 안죽더라;; 오랫만에 memory values 를 보게되었음. 땡큐.

    ABABABABUsed by Microsoft's HeapAlloc() to mark "no man's land" guard bytes after allocated heap memory
    FDFDFDFDUsed by Microsoft's C++ debugging heap to mark "no man's land" guard bytes before and after allocated heap memory
    FEEEFEEEUsed by Microsoft's HeapFree() to mark freed heap memory
  • 박PD 2010/11/05 07:47 # 답글

    summerligh : ODR을 악용하는 재미있는(?) 방법이 제가 단위테스트 강연할 때마다 알려주는 방법인데 그게 ODR 위반인 줄은 몰랐네요. 좋은 정보 감사합니다.
    김민장 : 네. m_Test[0] 만 할 때는 문제가 없어서 신기해 하고 있었어요. m_Test[9] 안 해 봤더라면 잘못된 정보를 알고 있었을지도.
    kernel0 : pA, pB 둘 다 new 로 힙에 할당했는데 왜 로컬스택이라고 생각했을까나.. 라이브 떠나 있었더니 이런 쪽에 감이 많이 떨어졌나봐. vs2010 테스트 땡큐
  • Lohengrin 2010/11/05 10:49 # 답글

    재미있는 문제네요 ^^; 실제로 헤더상에 있어서 포인터로 접근은 가능하고 메모리 자체는 할당이 안 되는가 보네요;
  • 박PD 2010/11/05 11:01 #

    pA 와 pB 가 가리키는 Test 클래스가 사실은 다른건데, 컴파일러가 TestPrj.cpp 를 컴파일할 때에는 pB 가 가리키는 Test 가 pA 가 가리키는 Test 와 같다고 착각을 해서 엄한 메모리에 값을 쓰면서 문제가 생기는 듯 합니다.
  • U.Seung 2010/11/05 13:40 # 삭제 답글

    가끔씩 빌드 할때.. 버전이 맞지 않는 라이브러리가 운좋게(?) 링크에 성공할때가 있는데..
    링크는 성공 했지만.. 정작 실행하면 정말 이상한데서 죽고 그럴 수 있습니다.
    그때 문제의 원인이 되는 것 중에 하나라고 볼 수 있을 것 같습니다.

    class에 저런 define이 걸려있다면... 관련 class를 쓰는 애들을 변경 시에 싸그리 리빌드 해야 됩니다.
    그렇지 않으면 불행의 시작이 될지도.... ^^
  • 박PD 2010/11/05 14:31 #

    header 쪽에 define 걸 때는 주의해야 겠더군요. 좋은 의견 감사합니다.
  • BombFox 2010/11/05 17:17 # 답글

    안녕하세요..^^
    궁금한게 있습니다.
    실례가 안되겠다면 위에 코드 하이라이팅 하신거 어떻게 하신건지 가르쳐주세요.
    이글루스에서 어떻게 해야하는지 모르겠습니다.
  • 박PD 2010/11/05 22:45 #

    http://parkpd.egloos.com/2524085 이거를 읽어보세요 :)
  • 미루엘 2010/11/06 11:53 # 답글

    ODR은 Link상의 문제입니다. ODR에서의 Definition은 '정의'라고 읽지만, 의미상으로는 '구현'입니다. 어떤 함수를 호출했는데 구현이 여러개 일 때 어느 구현으로 점프(호출)하도록 링크할 것인가?의 문제죠.

    본문의 케이스는 ODR 보다는, CPP (NOT C++, BUT C Pre-Processor)와 관련한 문제입니다.
    CPP의 동작은 #if 조건, #define, #include에 근거하여 사용자의 코드를 "물리적으로 제거, 또는 교체, 삽입"한 하나의 거대한 파일(Translation unit)로 변환하여 컴파일러에게 전달하고, 컴파일러는 이 파일 "하나만을 컴파일" 하죠.
    따라서, 본문의 경우 Bho.cpp 파일과 다른 파일들이 컴파일되는 시점에 바라보는 Test의 코드가 (당연하게도) "물리적"으로 다르지요. gcc는 "gcc -E', 비주얼 C의 경우는 http://weseetips.com/2008/06/12/how-to-get-the-preprocessed-c-cpp-source-files/ 를 참조하시면 CPP의 처리 결과를 확인할 수 있습니다.

    최근의 언어들에서 사용되지 않는 Pre-Processor가 야기하는 혼란중의 하나지요.
  • 오린간 2010/11/18 03:00 # 답글

    재미있는 내용이네요!!
  • 지나가다 2010/11/18 11:52 # 삭제 답글

    위 sizeof() 와는 다르지만, 어찌보면 비슷한 문제라고 생각되는데...

    분명 같은 클래스인데도, 어셈코드에서는 다른 오프셋을 참조하는 경우를 몇번 겪고, 그것이 헤더에 선언한 #define 때문(...-.-a) 이라는걸 깨달은 뒤로는, 인터페이스 클래스(또는 헤더) 수정시에는 거의 무조건적으로 #define 를 넣지 않게 되었습니다.

    걍 구현 클래스에서 깡통 함수 만드는게 속편하드라구용...ㅎㅎ
  • 박PD 2010/11/18 12:26 #

    네, define 이 참 위험한 물건이더군요 :)
  • highseek 2010/11/24 01:44 # 답글

    저희 회사에서는, define들을 정의하는 별도의 헤더파일을 하나 만들어 공통으로 씁니다.
댓글 입력 영역


Yes24위대한게임의탄생3

위대한 게임의 탄생 3
예스24 | 애드온2