Skip to main content

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 from this blog

1의 개수 세기 - 해답

벌써 어제 말한 내일이 되었는데 답을 주신 분이 아무도 없어서 좀 뻘쭘하네요. :-P 그리고 어제 문제에 O(1)이라고 적었는데 엄밀히 얘기하자면 O(log 10 n)이라고 적었어야 했네요. 죄송합니다. ... 문제를 잠시 생각해보면 1~n까지의 수들 중 1의 개수를 얻기 위해서는 해당 숫자 n의 각 자리의 1의 개수가 모두 몇개나 될지를 구해서 더하면 된다는 사실을 알 수 있습니다. 예를 들어 13이라는 수를 생각해 보면 1~13까지의 수에서 1의 자리에는 1이 모두 몇개나 되는지와 10의 자리에는 모두 몇개나 되는지를 구해 이 값을 더하면 됩니다. 먼저 1의 자리를 생각해 보면 1, 11의 두 개가 있으며 10의 자리의 경우, 10, 11, 12, 13의 네 개가 있습니다. 따라서 2+4=6이라는 값을 구할 수 있습니다. 이번엔 234라는 수에서 10의 자리를 예로 들어 살펴 보겠습니다. 1~234라는 수들 중 10의 자리에 1이 들어가는 수는 10, 11, ..., 19, 110, 111, ... 119, 210, 211, ..., 219들로 모두 30개가 있음을 알 수 있습니다. 이 규칙들을 보면 해당 자리수의 1의 개수를 구하는 공식을 만들 수 있습니다. 234의 10의 자리에 해당하는 1의 개수는 ((234/100)+1)*10이 됩니다. 여기서 +1은 해당 자리수의 수가 0이 아닌 경우에만 더해집니다. 예를 들어 204라면 ((204/100)+0)*10으로 30개가 아닌 20개가 됩니다. 이런 방식으로 234의 각 자리수의 1의 개수를 구하면 1의 자리에 해당하는 1의 개수는 ((234/10)+1)*1=24개가 되고 100의 자리에 해당하는 개수는 ((234/1000)+1)*100=100이 됩니다. 이들 세 수를 모두 합하면 24+30+100=154개가 됩니다. 한가지 추가로 생각해야 할 점은 제일 큰 자리의 수가 1인 경우 위의 공식이 아닌 다른 공식이 필요하다는 점입니다. 예를 들어 123에서 100의 자리에 해당하는 1의 개수는 ((123/1

std::map에 insert하기

얼마전 회사 동료가 refactoring한 코드를 열심히 revert하고 있어서 물어보니 다음과 같은 문제였습니다. 원래 코드와 refactoring한 코드는 다음과 같더군요. nvp[name] = value; // original code nvp.insert(make_pair(name, value)); // refactored 아시겠지만 위의 두 라인은 전혀 다른 기능을 하죠. C++03에 보면 각각 다음과 같이 설명되어 있습니다. 23.1.2/7 Associative containers a_uniq.insert(t): pair<iterator, bool> inserts t if and only if there is no element in the container with key equivalent to the key of t. The bool component of the returned pair indicates whether the insertion takes place and the iterator component of the pair points to the element with key equivalent to the key of t. 23.3.1.2/1 map element access [lib.map.access] T& operator[](const key_type& x); Returns: (*((insert(make_pair(x, T()))).first)).second. 원래 코드는 매번 새 값으로 이전 값을 overwrite했지만 새 코드는 이전에 키가 존재하면 새값으로 overwrite하지 않습니다. 따라서 원래 기능이 제대로 동작하지 않게 된것이죠. 그래서 물어봤죠. "왜 이렇게 했어?" "insert가 성능이 더 좋다 그래서 했지." :-? 사실 Fowler 아저씨는 Refactoring 책에서 refactoring은 성능을 optimizing하기 위한 것이 아니다라

C++ of the Day #9 - Boost.Python 사용하기 #1

Python 은 가장 인기있는 interpret 언어중의 하나입니다. Python의 장점 중 하나는 C/C++ 모듈과 쉽게 연동할 수 있다는 점입니다. 물론 손으로 일일히 wrapper를 만드는 것은 손이 많이 가고 에러를 만들수 있는 작업이나 SWIG 등과 같은 도구를 사용하면 쉽게 python 모듈을 만들 수 있습니다. Boost.Python 은 이런 SWIG와 같이 python 모듈을 쉽게 만들 수 있도록 도와주는 라이브러리로 순수 C++만을 사용한다는 점이 SWIG와 다른 점입니다. 그리고 개인적으로는 Boost 라이브러리에 포함되어 있는 것들이 왠지 좀 더 믿음직스러워서... :-) 이번 글에서는 Boost.Python 문서에 나와 있는 예제 를 가지고 간단하게 python 모듈을 만드는 방법에 대해서 알아보겠습니다. Requirements 리눅스 이 글에서는 리눅스 환경에서의 사용 방법을 설명한다. Boost.Python 라이브러리 (1.33.1) Boost 라이브러리를 다운로드받아 아래와 유사한 명령으로 라이브러리를 빌드한다. bjam -sTOOLS=gcc -with-python install bjam의 --prefix 옵션으로 라이브러리가 설치될 위치를 변경할 수 있다. Python 라이브러리 (2.4.3) Python을 다운로드 받아 빌드하여 설치한다. 위의 경우와 유사하게 configure의 --prefix 옵션으로 설치될 위치를 변경할 수 있다. Write C++ Code 다음과 같이 코드를 작성한다. // greet.cpp #include <stdexcept> char const* greet(unsigned x) { static char const* const msgs[] = { "hello", "Boost.Python", "world!" }; if (x > 2) throw std::range_error("