사고뭉치를 위한 디버깅 방법 02

지난 포스팅에서도 이야기 했다시피 일반적으로 버그에 대한 보고는 자신이 겪은 증상에 대한 보고 입니다.

아주 행복한 시나리오는 보고 받은 대로 시도하면 100% 재현 되는 버그입니다. 이런 종류의 버그는 너무나 해결하기 쉬워 “훗~ 어떻게 고쳐줄까?” 라고 고민하는 것만 집중하면 되지요.

하지만 대다수의 잡기 힘들었던 버그는

  1. “그냥 뜬금 없이 프로그램이 종료”
  2. “같은 방법으로 재현하려 해도 매번 다른 동작을 보여주는 버그”
  3. “보고 받은 대로 해도 재현 되지 않는 버그”

위와 같은 버그들입니다.

이번 포스팅에서는 이런 버그들을 잡는 법에 대해 알아보겠습니다.

기본적으로 제가 생각하는 디버깅은 추리입니다.

이를테면 이 녀석이랑 우리는 비슷한 일은 한다는 거죠~

명탐정코난

버그를 잡는 과정은 다음과 같습니다.

  1. A라는 오류가 생겼다.
  2. 그로 인한 원인은 B이다. (로그 혹은 재현)
  3. 해결책은 C다.

여기서 가장 중요한 것은 B를 증명하는 과정입니다. B를 증명하지 못한다면 그저 우연에 맡기는 프로그래밍(실용주의 프로그래머에서 인용)으로 서비스를 하는 것일테죠.

명확히 로그로 A라는 오류가 생겼다는 로그가 발견됐다면? 별거 아닐겁니다. 아마 그럴겁니다.

물론 실수는 누구나 할 수 있기에 단순 실수였다니 다행이군~ 하고 넘어갑시다.

하지만 대부분의 버그는 부주의에서 나오고, 그런 부주의의 원인이 무엇인지를 알려주는 로그 따위는 기대하지 맙시다.

대부분의 디버깅 상황은 명확한 증상과 원인을 알려주는 로그 따위는 없는 경우가 많습니다. 주로 한정된 로그와 로직의 흐름을 통해서 이루어지는 것이 일반적입니다.

그래서 제한된 정보를 바탕으로 재앙의 원인을 찾아내는 과정을 거치게 되는데 이 과정을 추리라고 보시면 됩니다.

명탐정 코난 보셨죠? 아주 작은 실마리 하나를 놓치지 않고 범인을 찾아내는 명석한 두뇌!! 아, 물론 우리는 그 정도로 똑똑할 필요는 없습니다.

하지만 종종 그에 못지않은 추리와 상상력이 필요할 때가 있습니다.

대부분의 경우 힌트는 조각나있습니다. 조각난 힌트들을 퍼즐 맞추듯 하나 하나 끼워 맞추는 과정이 우리에겐 필요합니다.

예를 들어 볼까요?

게임하다 갑자기 클라이언트가 종료됐다는 보고가 왔습니다. 그런데 전체 유저도 아니고 소수 유저라고 하는군요. (10명 내외)

아! 덤프 서버!! 덤프 분석 페이지를 가 봅시다. 어라? 덤프가 안남았다니…최악이군요.

여기서 보통 2가지 반응을 보입니다.

  1. “어? 뭐야? 보고가 잘못된거 아냐??”
  2. “헐….뭐지 덤프 안남고 죽는 상황들에는 뭐가 있더라…뭐 여하튼 힌트가 될만한건?”

물론, 우리는 2번 반응을 보여야 합니다. 유저의 보고가 잘못됐다는 것 마저도 우리는 증명해야 합니다. 우리는 엔지니어니까요. ^-^

기술자(技術者, technician)는 어떤 분야에 공학적인 일에 숙련된 사람을 말한다. 반면 공학자(工學者, engineer)는 공학의 일에 자연과학적인 지식과 기술적인 지식을 가지고 과학자와 기술자 사이에 매개체가 되는 사람을 가리킨다. 공학자는 기술, 수학, 과학 지식을 사용하여 실용적인 문제를 해결한다. 공학자로 일하는 사람들은 보통 공학 분야에서 학위를 가지고 있다. 공학자는 자연과학적 지식에 기초하고 있기 때문에 기술자와 구분된다. 15-16세기에는 엔지니어란 군사 분야의 기술을 맡거나, 건축가, 수력학자, 조각가, 화가로서 재능을 빌려주는 사람이었다. [1]

자~ 그럼 뭐라도 힌트를 얻어볼까요? 에러 로그 먼저 살펴보죠.

에러 로그를 봐도 별다른게 없습니다. 쳇…쉽지 않네요.

그렇다면…추가적인 보고되는 상황도 뭔가 조금씩 다릅니다. 대부분 유저의 보고는 감정적으로 이루어지는 경우가 많아, 고의적이 아니더라도 현상을 다르게 보고하는 경향이 있습니다. (상대적으로 이성적으로 판단해주시는 QA 분들 마저 현상을 착각하는 경우도 종종 있습니다.)

이런 경우 공통점을 찾기 시작해야 합니다. 대부분 로그에서 결정적인 힌트는 없더라도 비슷한 점이 발견되는 경우가 종종 있습니다.

문 제가 발생한 모든 유저의 로그에서, 유저가 크래시 발생전에 했던 행동중 우편함을 열어보았다는 로그를 발견했습니다. (저의 경우에는 Disconnect 시점에서 최근 10개 패킷 번호를 로그로 남기는 구현을 했던 적이 있는데, 이 로그가 디버깅에 아주 큰 도움이 되곤했습니다.)

우편함을 뒤져볼까요?

아…뭔가 조금 이상하군요. C++ 구조체에 정의된 최대 크기는 64바이트, DB에서 기록 가능한 최대 메시지가 128바이트였군요.

DB에서 실제 사용된 메시지 크기를 기준으로 검색해봅시다.

아…53명이군요. 53명중 10명만 오류에 대한 보고를 한 것이고요.

아마 버퍼 오버플로우가 났었겠지요?

어라? 어쨰서 유저들의 보고에서는 우편함을 클릭했었다 라는 보고가 없었던 걸까요?

그리고 어째서 서버는 크래시 되지 않았던걸까요?

서버 코드는 다음과 같습니다.

typedef DWORD MAIL_IDX;

typedef DWORD USER_IDX;

class Mail : public ISyncroized
{
public:
   Mail (const MAIL_IDX Idx, const char* szMessage, const USER_IDX UserIdx)
   {
       m_Idx = Idx;
       m_szMessage = szMessage;
       m_UserIdx = UserIdx;
   }

   virtual bool Encode()
   {
       // 인코딩 작업 구현
   }

private:
   MAIL_IDX m_Idx;
   USER_IDX m_UserIdx;
   std::string m_strMessage;
};

Mail mail(Idx, szMessage, UserIdx);
pSession->Send(mail); // Send 함수 내부에서 ISyncroized형 참조자를 통해 Encode된 정보를 유저에게 전달함.

자 그렇다면 클라이언트 코드를 볼까요?

const int MAX_MAIL_MESSAGE = 64;

typedef DWORD MAIL_IDX;
typedef DWORD USER_IDX;

class Mail
{
public:
   Mail(const MAIL_IDX Idx, std::string strMessage, const USER_IDX UserIdx)
   {
       m_Idx = Idx;
       strcpy(m_szMessage, strMessage.c_str());
       m_UserIdx = UserIdx;
   }

private:
   MAIL_IDX m_Idx;

   USER_IDX m_UserIdx;

   char m_szMessage[MAX_MAIL_MESSAGE];

private:
   IUIForm* m_pForm;

private:
   MailBox* m_pMailBox;
};

MAIL_IDX Idx = packet.Decode();

std::string strMessage = packet.Decode();

USER_IDX UserIdx = packet.Decode();

Mail mail(Idx, strMessage, UserIdx);

변수 배치상 우편함을 열자마자 오류가 생긴 것이 아니고, 그로 인한 2차 감염으로 문제가 생겼네요.

자! 찾았습니다. 신난다~~ 신난다~~~

그렇지만 좋아만 할때가 아니죠?

자. 어떠한 실수가 이런 문제를 만든 걸까요?

  1. DB와 C++ 코드와 텍스트 길이 오차
    • 64 < 128
  2. non-safe 한 c 표준 함수 사용. (strcpy)
    • 버퍼 길이를 지정하는 함수로 변경. (strncpy)

직접적인 원인은 위 두가지겠지만, 실제론 더 있다고 볼 수 있습니다.

  1. 패킷에서 사용하기로한 자료형은 std::string에 Decode되는 자료 구조형과 달리 NULL-Terminated 문자열 사용.
    • 패킷을 데이터로 변환하는 함수에서 std::string을 지원한다면 굳이 NULL-terminated형을 사용할 필요가 없었음.
  2. UI에서 제대로 표기가능한 문자열은 실제로 128byte였음.
    • 무언가 기획서의 반영이 안된것인가 찾아보았더니, 최초 UI폼은 64byte만큼 표시가 가능했으나, 이후 UI폼에선 128byte만큼 표기 가능하게 변경됨. C++ 코드에서의 UI변화 코드 수정 누락으로 인한 문제 발생.

이렇듯, 문제가 왜 발생했는가의 경위를 알아내는 것이 매우 중요합니다. 이런 실수들은 꽤나 큰 문제를 일으키곤하는데요, 방금전 버퍼 오버 플로우 관려련 코드가 로그인 시에 매번 수행되는 코드에서 사용되었다면 유저 대부분이 떨어져 나가버려 서비스는 엉망이 될 것이고, 덤프도 안남는 상황에서 (혹은 엉터리 같은 덤프가 남는 상황에서) 그 많은 유저 사이에서의 공통점을 찾기란 서울에서 김서방 찾기만큼 힘들었을겁니다.

“찾았다~~ 문제생긴 코드만 후딱 수정해서 패치하면 되지 뭐~.”

이렇게 대처해서는 안됩니다.

생각보다 많은 팀에서 버그나 크래시가 발생했을 때 이를 너무 크게다뤄 지나치게 혼을 낸다거나, “뭐 이런일도 있는거지” 하며 쉽게 넘어가죠.

실제로 필요한 대처는 버그가 발생한 코드 조각이 아니라, 진짜 단순 실수였는가, 개발자의 무지에서 온 것은 아닌가, 시스템으로 커버 불가능한 오류였는가, 개발 과정에서 캐치 할 순 없었는가 등을 다 고민해보아야 합니다.

이런 과정은 “좋은 서비스”를 하고 있는 개발 팀일 수록 체계화 되어있으며, 개개인의 실수가 팀 차원에서 커버되는 경우가 많습니다.

여하튼 버그를 찾았다면 그런 버그가 “다시는” 나오지 않게 하기 위한 여러가지 고민을 할 차례입니다.

이런 고민에 대한 이야기를 다음 글에서 이어서 하겠습니다.

사고뭉치를 위한 디버깅 방법 01

안녕하세요. 엘키라고 합니다.

이 블로그를 보시는 분들 다들 뛰어나고 다양한 경험을 많이 갖춘 분들이시겠지만, 제 경험담을 바탕으로 디버깅에 대해 알아보는 시간을 가져보고자 이렇게 나섰습니다. ^^

우리는 모두 사고 뭉치입니다. 사람이란 실수할 수도 있다는 것을 전제로 이 글을 작성 하고자 마음 먹었죠. (정확성이 떨어지는 발로 하는 스포츠인 축구계의 명언에서 인용했습니다.)

그래서~ 언제나 실수할 여지를 갖고 있는 우리 모두를 위해, 실수를 했을 때 이를 빨리 수습하기 위해 해야 되는 작업이 디버깅이라는 의미로, 사고뭉치들을 위한 디버깅 방법이라 이름을 지었습니다.

흔히 디버깅이라하면, VS Debugger, Windbg, Ollydbg 등의 디버거를 이용하는 것에만 집중 하시는데요, 이 글에서는 디버깅 툴에 의존하지 않는 범용적인 디버깅 과정에 대해 썰을 풀어볼 계획입니다.

이런 창을 보여주지 않는 코딩에 대한 글은 절대 아닙니다. 이런 상황의 원인을 찾고 해결하기 위한 글입니다.

프로그래머가 디버깅을 하게 되는 상황은 주로 다음과 같습니다.

  1. 누군가에서 버그 상황을 전달 받는다. (혹은 자동화된 시스템을 통해)
  2. 직접 겪는다.

그렇다면, 디버깅 해야 될 상황은 어떤게 있을까요?

  1. 프로그램이 비정상 종료 내지는 멈춘다.
  2. 사용자가 기대하지 않은 상황과 맞이한다.

비정상 종료 내지는 멈췄을 땐 어떻게 해결 해야 될까요?

  1. 덤프가 남았는지 확인한다.
  2. 로그를 확인한다.

덤프가 안남았을땐, 무한 루프에 빠지거나 dead-lock 에 빠지지 않았는지 확인해야 합니다.

dead-lock : 코어 점유는 없고, 데드락이 걸린 스레드 몇개만 잠긴다. 무한 루프 : 코어 1개를 점유 하고 있음.

그래서 dead-lock 을 회피하기 위해 모든 스레드를 잠깐씩 거치는 falling-thread를 놓고, 해당 thread가 잠겼는지 확인하는 방식으로 구현하기도 합니다.

falling-thread가 멈추면 어떠한 특정 thread 인해 dead-lock이 일어났다고 판단할 수 있기 때문입니다.

물론 dead-lock 상황은 userdump와 같은 프로그램을 통해 덤프를 생성함으로써 확인 가능하지만 이보다 빠른 대처와 판단이 가능해지는 장점이 있지요.

한가지 팁을 드리자면, 모든 thread는 생성시 thread의 역할과 thread-id 를 로그로 기록하는 것이, 디버깅시 여러모로 유용합니다.

windbg 등의 툴이나 로그등에서 확인할 때 이 정보는 매우 유용해 지죠. (스레드별 로깅을 하는 방식도 나름 괜찮음)또한 크리티컬섹션(CRITICAL_SECTION)객체에는 Ownering Thread 정보가 존재하기에 이를 바탕으로 한 잠긴 Thread를 확인할수 있기 때문이죠.

어쨋거나 dead-lock, 무한루프 모두 dump를 생성하거나 debugger로 attach해서 원인 파악이 가능합니다.

만약 덤프가 남았고 크래시 내지는 예외 발생 후 스택 되감기(Stack Unwinding)를 통해 정상 동작하고 있을땐 어떻게 해야 될까요?

덤프가 남았지만 정상 동작하는 상황

  • 스택 오버 플로우가 났을 때 (새로운 Thread를 만들어서, 기존 익셉션 정보를 처리해야함. ExceptionProcess를 위해 스택을 구성하는 과정에서 오류가 생기기 때문)
  • 잘못된 함수 포인터 콜
  • 스택 메모리 덮어썼을때 (스택 깨먹었다고도 하죠)
  • Divide Zero
  • NULL 포인터 접근
  • 같은 메모리에 delete 두번

크래시가 나는 상황 (절대적이진 않음)

  • 스택 되감기 도중 예외 발생.
  • 잘못된 포인터 캐스팅을 통한 가상 함수 콜
  • 잘못된 함수 포인터 콜
  • 변수 값 덮어 썼을때 (주로 가상함수 포인터 테이블을 덮어썼을때 크래시가 남.)
  • 잘못된 포인터 접근

덤프가 남았을 경우는 대부분이 단순 실수가 많습니다. NULL 포인터 참조 오류와 같은 경우나 잘못된 포인터 참조는 꽤나 자주 겪게 되는 단순 실수에 해당 되기 때문에, 발견하게 되면 바로 고칠 수 있는 단순 버그에 속합니다.

당연한 얘기겠지만, 스택 되감기란 아주 단순한 동작이 아니기 때문에 이로 인한 무한 루프에 빠지는 경우도 종종 생깁니다.

예외 처리를 위해 감싸는 로직의 단위를 잘 판단할 필요가 있습니다.

애매모호한 동작을 보이지만 잘못된 포인터 캐스팅은 상대적으로 찾기 쉬운 편입니다. 해당 포인터를 강제 캐스팅 해야 될 만큼 잘못된 설계가 되어있는 경우가 대다수지만, 피치 못할 사정 상 그렇게 구현 했다고 해도, 캐스팅을 위한 검증 값 체크만 잘하면 적어도 크래시가 나지 않게 동작 시키는 것은 무리가 아니지요.

덤프가 남았는데, 콜스택이 영~ 이상하다. 이럴 때 참 난감합니다.

이러한 상황을 만드는 데에는 변수 값 덮어쓰기가 주범이 되곤 합니다.

덮어 써진 메모리가 유효한 위치 (프로그램 내에서 할당된 영역)이라면 예외가 발생하지 않습니다.

이런 상황을 감염이라 부르는데, 문제가 생긴 곳에서 바로 증상이 생기지 않고, 영~ 쌩뚱 맞은 곳에서 오류가 생기기 때문입니다. (버그로 발현되기도 하고, 크래시가 되기도 함)

그래서 흔히 감염이 최악의 버그라고 평하곤 하지요.

가장 주의해야 될 버그이고, 가장 찾기 힘든 버그입니다. 그래서, 좋은 프로그래밍 습관을 필요로 하지요.

흔히 c스타일의 코딩(memcpy, strcpy, 포인터 직접 조작 등)에서 자주 발생하는 문제입니다. 코어 레벨의 코딩에서는 여러가지 상황으로 인해 포인터 조작이나 memcpy를 할 수 밖에 없다고 하더라도, 컨텐츠 단에서는 반드시 safe한 함수를 사용할 것을 권장합니다.

이외에도 예외를 발생 시키지 않는 프로그래밍 습관에 대한 이야기도 해보고 싶은데 이 글의 주제를 벗어나기 때문에 기회 되면 따로 글을 작성해보고 싶네요.

자~ 여기까지 내용이 서론이었습니다. 진짜 하고 싶은 이야기인 잡기 힘든 버그를 해결하는 과정에 대한 썰을 다음 글에서 풀어보고자 합니다.

다음 시간에 만나요~ 제발~~~

애플 신드롬과 MS에 대한 사설

“애플보다 MS가 세상을 바꾼 능력자”… 그 이유는?

나 위 기사와 역시 같은 생각이다.

내 지인들이라면 알겠지만 나는 MS가 만들어준 환경이 고마움을 절실히 느끼는 사람이다.

실제로 Visual Studio, Windows, Office 등 MS 제품군의 완성도면에서 굉장히 만족하는 편이고.

내가 늘 주장하는 바는 이렇다.

‘과연 다른 회사가 MS의 위치까지 도달했었다고 했을때, 지금의 MS만큼 잘해낼 수 있었겠는가?’

나는 그렇지 않다고 본다.

Art of Unix Programming 에서 비난받고, 여러 애플 추종자들에게 공격받는 MS지만, 실제로 이렇게 철저히 오픈된 개발 환경에서 이만큼 이룩한 것은 대단한 것이다.

윈도우가 어떤 정책을 갖고, 어떤 기준을 갖고 만들어져 왔는지를 알고 싶다면, 윈도우 개발 282 스토리를 추천한다.

MS에 대한 큰 오해중 하나는 절대로 기득권 세력이라 MS가 잘 나가고 있는것이 아니란 점이다. (물론 MS가 인수 합병을 통해 성장한 분야도 있다는 것은 부인할 수 없는 사실) 실제로 MS보다 먼저 DOS를 개발하고 추후 동시 개발한 곳은 IBM이고, GUI 기반의 OS 시장을 개척한 것은 애플(매킨토시)이다. (실제 개발의 최초는 제록스지만)

여기서 중요한 것은 MS가 더 나은 기업이냐 애플이 더 나은 기업이냐가 아니고, MS가 악의 축은 절대 아니란 점이다.

도대체 MS가 뭘 그렇게 잘못했는지 나는 잘 모르겠다.

적어도 내가 써오고 봐오기로 MS의 운영체제는 대 부분 완성도 있었다. 시기적으로 경쟁사 제품들보다 완성도 있고 현명한 판단을 많이 했다고 느껴지는데, 많은 사용자의 원망을 들어온 탓인지 어느새 죄인이 되어있는게 너무 안타깝다.

실제로 그들중 대다수는 윈도우와 맥이나 리눅스를 병행해서 쓰면서 말이다.

개인적으로 내가 개인 사용자라 했을때 리눅스는 여전히 어렵고, 맥은 기능이 제한적이다.

과거로 거슬러올라가도 매킨토시와 DOS, 윈도우와의 안정성 비교에서 도토리 키재기였다고 본다. (윈미는…3년간 써본 나로써도 완성도가 극도로 떨어졌다고 생각하니…인정하고 넘어가겠다. 윈도 2000이 아닌 윈미를 선택한 내 잘못이겠지…-_-)

MS 오피스나 윈도우 UI가 불편하다고 하는데…나에게 있어선 맥도 만만치 않게 어려웠다. 우분투 리눅스도 직관적이지 않긴 마찬가지고. 결국 어느 OS나 학습 비용과 커스터마이즈 비용은 어느정도 들기 마련인데 어째서 윈도우만, MS만 비난받는지 잘 이해하기 어렵다.

우리 모두 곰곰히 생각해보자.

정말 애플만이 세상을 바꿨는지

윈도우 환경에서의 C++ 프로그램 예외처리

윈도우 예외 처리에 대한 정리 개요 윈도우에서 사용가능한 예외 처리로는 C++ 예외 처리와, SEH (Structured Exception Handling) 이 있습니다.

일반적으로 SEH(Structured Exception Handling)이라고 말하면 Windows 자체적으로 지원하는 구조적 예외 처리를 의미합니다. (관련 키워드 : __try, __except, __finally, __leave)

그리고 C++ Exception Handling (이하 C++ EH) 라 하면 C++ 에서 정의하고 있는 구조적 예외 처리를 의미합니다. (관련 키워드 : try, catch, throw) 두 예외 처리 방식에 대해 간단히 설명드리겠습니다.

__try
{
    int a = 500;
    int b = a / 0; // 0으로 나누기
}
__except(GetExceptionCode() == EXCEPTION_INT_DIVIDE_BY_ZERO ?
            EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH)
{
    // 예외 처리
}

이 것이 기본적인 SEH 사용법입니다.

__finally 키워드의 경우 예외가 발생하던 발생하지 않던 수행해야 되는 구문에서 쓰입니다.

DWORD FilterFunction()
{
   printf("1 ");                     // printed first
   return EXCEPTION_EXECUTE_HANDLER;
}
 
void main(VOID)
{
   __try
   {
       __try
       {
           RaiseException(
               1,                    // exception code
               0,                    // continuable exception
               0, NULL);             // no arguments
       }
       __finally
       {
           printf("2 ");             // this is printed second
       }
   }
   __except ( FilterFunction() )
   {
       printf("3\n");                // this is printed last
   }
}

위 코드 수행 후 1 2 3이 출력됩니다.

즉, 예외 발생했음에도 __finaly안의 구문은 수행됐음을 의미하죠.

DWORD FilterFunction()
{
    printf("1 ");                     // printed first
    return EXCEPTION_EXECUTE_HANDLER;
}
 
void main(void)
{
    __try
    {
           __try
           {
                // none         
           }
           __finally
           {
                   printf("2 ");             // this is printed second
           }
    }
    __except ( FilterFunction() )
    {
           printf("3\n");                // this is printed last
    }
}

위 코드 수행은 2만 출력됩니다.

예외 발생 유무에 상관 없이 __finaly 안의 구문은 수행되는 것이죠. __leave 구문은 __try 구문안에서 빠져나가고자 할 때 사용됩니다.

DWORD FilterFunction()
{
    printf("1 ");                     // printed first
    return EXCEPTION_EXECUTE_HANDLER;
}
 
void main()
{
    __try
    {
           __try
           {
                printf("try");
            __leave;
            RaiseException(
                        1,                    // exception code
                        0,                    // continuable exception
                        0, NULL);             // no arguments
           }
           __finally
           {
                   printf("2 ");             // this is printed second
           }
    }
    __except ( FilterFunction() )
    {
           printf("3\n");                // this is printed last
    }
}

위 코드 수행시 try 2 가 수행됩니다. __leave가 수행됐음에도 __finaly가 안에 포함된 코드가 수행됐음을 알 수 있죠. 이상이 SEH의 기본적인 사용법이었습니다.

C++ EH 사용 예제

try
{
    throw "Memory allocation failure!";
}
catch( char * str )
{
    std::cout << "Exception raised: " << str << '\n';
}

위 코드 수행시 Exception raised : Memory allocation failure! 문장이 출력됩니다.

즉, try {} 는 수행 구문, catch(캐치할 예외 타입) {}, throw 예외 로 처리되는 것입니다. 여기서 주의할 점은 throw; 는 예외의 전파로써 사용이 되기도 한다는 점 입니다.

여기서 모호한 점은 throw; 가 try안에서 사용될 때와, catch에서 사용 될 때와 차이가 있다는 점입니다.

void main()
{
   try
   {
       try
       {
           throw "Memory allocation failure!";
       }
       catch( char * str )
       {
           std::cout << "Exception raised: " << str << '\n';
           throw;
       }
   }
   catch(...)
   {
       std::cout << "catched" << '\n';
   }
}

위 코드의 경우가 catch에서 예외의 재전파에서 사용되는 예입니다.

위 구문 수행시 Exception raised : Memory allocation failure! catched 가 출력됩니다. 만약 예외만 발생시키려 throw; 를 try안에서 사용했을경우를 볼까요?

try
{
    try
    {
        throw;
    }
    catch( ... )
    {
        std::cout << "Exception raised: \n";
    }
}
catch(...)
{
    std::cout << "catched" << '\n';
}

위 코드를 수행할 경우 예외가 발생하게 됩니다.

C++ 표준 15.1.8에 따르면 “If no exception is presently being handled, executing a throw-expression with no operand calls std::terminate().” 즉, 현재 예외가 없는데 throw; 하면 프로그램이 종료됩니다. 예외는 함수 경계를 넘어서 전파될 수 있으므로 throw; 가 문법적으로 catch 안에 있을 필요는 없습니다.

위 내용에 대해서는 아래 링크를 따라가 보시면, 논의가 이루어졌습니다.

KLDP의 try 안에서의 throw에 대한 논의 : http://kldp.org/node/106380

SEH to C++ EH

SEH 를 C++ Exception으로 자동으로 변환하도록 만들었을 때의 장점은 0xC0000005 같은 잘못된 메모리 참조 예외같은 하드웨어 예외까지도 C++ EH를 사용해서 한곳에서 감지할 수 있다는 점이 될 수 있겠죠.

다행이도 Windows Exception이 발생했을 때 콜백 받을 수 있는 함수가 존재하므로, 아주 간단한 구현이 가능합니다.

_set_se_translator( TranslateSEHtoCE );

위와 같이 해주면, Windows Exception이 발생할 때마다 TranslateSEHtoCE 이라는 이름의 함수가 호출됩니다.

TranslateSEHtoCE안에서는 C++ Exception을 발생시키면 되겠죠.  

// a C++ exception class that contains the SEH information
class CSEHException
{
public:
       CSEHException( UINT code, PEXCEPTION_POINTERS pep)
       {
                  m_exceptionCode        = code;
                  m_exceptionRecord    = *pep->ExceptionRecord;
 
                  m_context            = *pep->ContextRecord;
 
              _ASSERTE(m_exceptionCode == m_exceptionRecord.ExceptionCode);
       }
 
       operator unsigned int() { return m_exceptionCode; }
 
       // same as exceptionRecord.ExceptionCode
       UINT m_exceptionCode;
 
       // exception code, crash address, etc.
       EXCEPTION_RECORD m_exceptionRecord;
 
       // CPU registers and flags
       CONTEXT m_context;
};
 
// the SEH to C++ exception translator
void _cdecl TranslateSEHtoCE( UINT code, PEXCEPTION_POINTERS pep)
{
      throw CSEHException(code, pep);
}
 
int main(int argc, char* argv[])
{
       // install the translator
       _set_se_translator( TranslateSEHtoCE);
 
 
       try
       {
                  char* p = NULL;
 
              *p = 'A';
       }
       catch( CSEHException& e)
       {
 
              if( EXCEPTION_ACCESS_VIOLATION == e)
                  {
 
                      _RPT0( _CRT_WARN, "Access Violationn");
                  }
       }
       return 0;
}

참고 자료

Serious-Code SEH Microsoft Exception MSDN SEH MSDN C++ Exception Handling MSDN의 Managed Exception /EH (예외처리 모델)

프로그래밍 개발시 좋은 습관

Open-Close 원칙

  • 객체는 수정(modification)에는 닫혀있고(Close), 확장(extension)에는 열려있어야(Open) 한다.

중복을 제거하라

  • 같은 기능을 하는 코드를 묶어, 중복을 제거하자는 원칙.

메소드 간소화

  • 메소드는 두가지 일을 하게 만들지 말아라.
  • 메소드의 이름에서 벗어나는 일을 하지 말라.
  • 큰 규모의 행동이 필요하다면, 더 큰 범위의 단어로 메소드 명을 짓고, 그 큰 동작을 완성 시키기 위한 메소드의 연결만으로 구현하라.

직교성

  • 클래스 끼리의 의존도를 낮춰, 해당 클래스가 변화를 국소화 시키고, 재사용을 촉진하라

가역성

  • 변하지 않는 것은 없다. 코드도, 설계도 변화에 대비하라.

계약에 의한 설계

  • 이 함수에 들어오기 전에 기대하는 선행 조건(PreCondition)과, 함수 종료시에 보장해야 될 후행 조건(PostCondition)을 검사하라.

80-20법칙

  • 20% 코드가 수행시간의 80%를 차지한다.
  • 20%의 자주 불리는 코드를 수행하면 80% 이상의 성능향상을 거둘 수 있다.