Core Dump Pattern?

지난 주에 현장에 있던 시스템에서 core dump가 발생했습니다. :-(

제가 담당하는 부분은 아니어서 쳐다보지도 않고 있다가 오늘 뭔가 의심스러운게 있는지 확인하기 위해 불려갔지요. 사실 core dump를 열어 보지도 않아서 그냥 듣고만 있었는데 듣다 보니 원인이 뭔지는 짐작이 가더군요.

소멸자에서 core가 났는데 소멸자 코드안에는 아무 것도 없습니다. 그리고 call stack의 마지막 부분은 std::string에 있는 lock 관련 함수에서 났어요.


해당 pstack은 담당자의 말대로 다음과 같았습니다. 4~6 라인을 보시면 됩니다. ((c++filt를 적용한 pstack입니다.)) ((어제 밤에 CodeHighlighter에 line number 출력 기능을 넣었어요. :-) ))


----------------- lwp# 153 / thread# 73 --------------------
...
--- called from signal handler with signal 11 (SIGSEGV) ---
fe42b1d8 _mutex_lwp_lock (ef1860, fe44e000, 2ef1800, de55cc, ef1860, 40000013) + 20
00de55cc void std::basic_string,std::allocator >::__unLink() (9e0070c, 1ec9fdb8, 1ec9fdb8, f7901774, 1daa6770, 1e1cbc80) + 4c
00dc3f64 BRISession::~BRISession() (9e003a0, 2df4c44, 88f2e0, 1, 0, 1e3799b8) + 124
00de75e4 __SLIP.DELETER__G (9e003a0, 1, f7901860, f790185c, 44, 4122640) + 4
...


아마 core dump를 많이 보셨다면 이미 위의 말만으로도 원인을 짐작하실 수 있었을겁니다.

원인은 바로 double delete죠. 한번 지워진 object에 대해 다시 delete가 불리면 해당 object의 소멸자가 다시 호출되게 됩니다. 만약 이 소멸자에서 직접적으로 this 포인터를 사용하지 않았다면 문제가 발생하지 않았을 수도 있습니다. ((물론 double delete의 결과는 undefined behavior이기 때문에 실제 어떤 문제가 발생할지는 알 수 없습니다.))

하지만 위의 경우처럼 해당 클래스가 소멸자를 가진 member를 가지고 있고 그 소멸자에서 this가 가리키는 영역을 사용한다면 문제가 발생하게 됩니다. 이 경우에는 std::string을 member로 가지고 있었지요.

core의 내용에 대해선 이 정도로 마치고요, 중요한 사실은 제가 담당자의 말만 듣고도 해당 원인을 알 수 있었다는 사실입니다. 왜 알 수 있었을까를 생각해보니 이미 이와 유사한 core dump가 여러번 반복되었기 때문이었죠. 즉, 이번 core와 정확히 똑같지는 않으나 뭔가 유사한 core의 경험이 여러번 있었다라는 것이죠.

그럼 이쯤에서 Christopher AlexanderA Pattern Language라는 책에 쓰셨다는 말을 다시 기억해 보죠.

Each pattern describes a problem which occurs over and over again in our environment, and then describes the core of the solution to that problem, in such a way that you can use this solution a million times over, without ever doing it the same way twice.


각각의 패턴은 우리 주위에서 계속해서 발생하는 문제들과 그 문제에 대한 해결책을 설명한다고 되어 있습니다.

Core dump의 경우도 마찬가지로 뭔가 반복해서 나타나는 유사한 경험들을 모아서 pattern 형식으로 정리를 해놓으면 debugging시 도움이 되지 않을까하는 생각을 하게 되었지요. Design pattern이 design시에, 혹은 refactoring catalog가 refactoring시에 도움을 주는 것처럼 말이죠.

그래서 위의 문제를 가지고 만들어 본 Core Dump Pattern의 예제입니다. ;-)


  1. Name
    • Core on Empty Destructor


  2. Problem
    • pstack에서 소멸자가 보이며 call stack의 마지막 부분은 해당 클래스에 있는 멤버 변수의 소멸자이다.

      --- called from signal handler with signal 11 (SIGSEGV) ---
      00de55cc void std::basic_string::__unLink() (9e0070c, 1ec9fdb8, 1ec9fdb8, f7901774, 1daa6770, 1e1cbc80) + 4c
      00dc3f64 Foo::~Foo() (9e003a0, 2df4c44, 88f2e0, 1, 0, 1e3799b8) + 124


  3. Cause
    • 원인은 다음과 같이 double delete일 가능성이 매우 높다.

      class Foo
      {
      std::string s_;
      };

      Foo* f1 = new Foo;
      Foo* f2 = f1;

      delete f1;
      ...
      delete f2;


  4. Solution

    • Code review나 inspection을 통해 double delete에러를 찾는다.

    • Purify와 같은 runtime memory analysis 툴을 사용하여 디버깅한다.

    • Prevent와 같은 static code analysis 툴을 사용하여 code를 검사한다.



  5. Other Possible Causes
    • double delete외에 다음과 같이 memcpy에 의해서도 발생할 수 있다.

      struct POD
      {
      std::string s;
      };

      POD s, t;
      memcpy(&t, &s, sizeof(POD));




다른 부분은 그럭저럭 괜찮은데 Solution 부분이 마음에 들지 않네요. 물론 문제가 발생한 소스 코드도 없이 어디다라고 콕 찍어줄 순 없지만 너무 당연한 해결책들만 들어가 있는건 아닌지 모르겠네요. Solution 부분은 아예 없애고 Core Dump Pattern은 원인만 알려주는데 만족해야 하는 걸까요? :-|

혹시 비슷한 생각을 하신 분들이 계신지 궁금하네요. 다음 core dump가 발생하면 그때도 pattern을 찾을 수 있을지 확인해 봐야 겠습니다.

Comments

  1. 흥미있는 글이였습니다.

    최초에 생긴문제는 스마트 포인터로 해결이 가능하네요. boost::shared_ptr같은걸 사용하면 대부분의 경우에 방지가 가능하다고 생각합니다.

    5.Other Possible Causes 읽고 제가 사용하고 있는 매크로를 다음과 같이 고칠까 하다가 보류했습니다.

    // NO copy, No assign.
    #define SET_NO_COPYING(type) \
    type(const type&); \
    type& operator= (const type&);

    이런 매크로를 사용하고 있거든요.

    #define SET_NO_COPYING(type) \
    type(const type&); \
    type& operator= (const type&); \
    type* operator& ( void );

    이렇게 한줄을 추가하려고 했죠.

    사용은

    class Foo
    {
    private:
    SET_NO_COPYING( Foo );
    };

    와 같이 사용합니다. 근데 매크로에 한줄 추가로 이름이 적절치도 않고, 클래스의 포인터를 얻는게 원천적으로 금지되는터라 보류 상태입니다. 포인터를 얻어야 하면 타이핑을 따로 해야 하는 일이 생긴다는 귀찬음을 이기지 못할것 같습니다-_-;

    ReplyDelete
  2. 저희는 애시당초 pointer의 사용을 금지하다 시피한... ( '')
    무조건 shared_ptr을 이용하도록 강요한다는... ('' )

    delete가 사용되는 경우는 shared_ptr에 사용하는 삭제자를 제외하곤 거의 없답니다. :)

    ... 자랑인가요? -_-

    ReplyDelete
  3. @mkseo: :eek:


    @neux님: 매크로를 만들어서 사용하고 계시군요. 그런데 operator &를 private으로 만든다고 해도 다음과 같은 경우에는 compile-time 에러가 발생하지 않기 때문에 별로 도움이 안될듯 합니다.

    Foo *f1, *f2;
    memcpy(f1, f2, sizeof(Foo));

    그리고 아시겠지만 boost::noncopyable이 사용하고 계신 매크로와 같은 작업을 하죠.

    class Foo : boost::noncopyable { ... };

    memcpy를 좀 더 효과적으로 막을 방법이 있을지 좀 더 생각해봐야겠네요. :-)


    @까막: 크~ 자랑 맞네요. shared_ptr 좋죠. 저희는 공식적으로 외부 라이브러리를 가져다 사용하기가 어렵습니다. 그래서 제가 몰래 :roll: 파일 몇개만 체크인해서 사용하고 있는데 그 두개가 바로 shared_ptr와 BOOST_STATIC_ASSERT죠.

    개인적으로 하는 프로젝트들에는 모두 shared_ptr를 다음과 같은 naming convention을 써서 사용하고 있답니다.

    class Foo { ... };

    typedef boost::shared_ptr<Foo> FooSP;

    ReplyDelete

Post a Comment

Popular Posts