사고뭉치를 위한 디버깅 방법 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한 함수를 사용할 것을 권장합니다.

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

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

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

Comments

comments powered by Disqus