멀티프로그래밍 위키로 바로가기 → http://www.devnote.net/wiki

코맨드라인 디버거인 NTSD, KDB 등을 사용하다보면 전체 스레드의 스택을 살펴 보는 (~*kb) 일을 자주하게 됩니다. 이럴 때면 여러 스레드 콜스택 중에서 특정 함수 호출 부분을 모두 찾는 다거나 특정 모듈을 찾을 경우가 많은데, 커맨드 콘솔의 텍스트 출력을 눈으로 훑어 나가기가 쉽지는 않습니다.


아래와 같은 레지스트리를 설정하면, 콘솔 창에서 "마우스로 원하는 스트링을 선택한 후" 1, 2, 3, 4 중 하나의 방법을 사용하여 색깔을 변경할 수 있습니다. 특히 3과 4가 디버깅 중에 매우 유용합니다.


[HKEY_CURRENT_USER\Console]

"QuickEdit"=dword:00000001

"EnableColorSelection"=dword:00000001     


1.       Alt + [0..9] : 텍스트 색깔 설정

2.       Ctrl + [0..9] : 배경색 설정

3.       Alt + Shift + [0..9] : 선택된 스트링과 같은 모든 스트링의 텍스트 색깔 설정

4.       Ctrl + Shift + [0..9] : 선택된 스트링과 같은 모든 스트링의 배경 색깔 설정

크리에이티브 커먼즈 라이센스
Creative Commons License

윈도우즈 디버깅의 기초에관한 블로그 문서가 처음 디버깅에 입문하려는 사람들에게 매우 유익한 자료가 될 것 같습니다. 물론 비주얼스튜디오의 디버깅 기능에 충분히 만족하는 사람에겐 큰 도움이 되지 못하겠습니다만, 커널레벨의 디버깅을 하려는 사람이나 보다 강력한 기능의 디버거를 원하는 사람에겐 좋은 문서가될 것 입니다.
크리에이티브 커먼즈 라이센스
Creative Commons License

전에 언급한 디버깅 책이 출간되어 주문을 하였는데 어제 도착했습니다. 아마존에서도 판매 중인 이 책은 이른바 (POD: Print On Demand) 방식으로 출판되어 상당히 흥미롭습니다. 이제 개인이나 초보 저자들이 책을 출간하는 것이 정말 쉽게될 것 같습니다.

이 책은 700여 페이지에 달할 만큼 두꺼운 편이나, 많은 부분이 디버깅이나 덤프 파일 분석 과정의  아웃풋을 리스팅해 놓아서 실제 내용은 그렇게 많지 않습니다. 복잡한 문제를 디버깅하는 동안 좋은 레퍼런스가  될 것 같습니다. (물론 대부분 블로그에 있는 내용을 책으로 옮겨 놓은 것입니다). 저자는 메모리 덤프파일을 비트맵 이미지와 wav파일로 변환하여 보고 듣기까지 하는 등 상당히 재미있는 부분도 많이 있습니다.

크리에이티브 커먼즈 라이센스
Creative Commons License

"Advanced Windows Debugging"이란 책이 곧 발간될 예정입니다. 아래 링크에 샘플 챕터가 있는데, 윈도우즈 힙 메모리 내부 구조에 관해 상세히 다루고 있습니다. 언젠가 쓴 적이 있는데 Visual C의 동적 메모리 할당 함수인 malloc/free 는 결국 HeapAlloc/HeapFree를 기본적으로 호출합니다. 그리고 이 힙메모리는 상당히 효율적으로 만들어져 있습니다. 그 내부 구조가 알고 싶다면, 아래 챕터가 큰 도움이 될 것입니다.

http://advancedwindowsdebugging.com/ch06.pdf
크리에이티브 커먼즈 라이센스
Creative Commons License

전에 최대 생성 가능 스레드를 얘기하며, 스택오버플로우 예외처리에 관해 잠시 언급하였다. 오늘은 이에 관해 자세히 짚어 보자.

사실 이 주제는 멀티스레드 프로그래밍과 어느 정도 연관성을 가지고 있다.
예외처리(Exception handling)는 정상적인 프로그램의 흐름을 바꾸는 상황을 처리하기 위해 프로그래밍 언어에서 지원되는 것이다. 간단한 예로 "page fault" 혹은 "access violation" 이라고 불리우는 잘못된 메모리를 참조하는 경우이다이 문제는 가장 흔하게 발생하는 문제이며, 가장 골칫 거리이기도 하다. 초기화가 잘못된 메모리 변수를 사용하거나, 잘못된 메모리의 동적 할당 및 해제, 버퍼 오버플로우(buffer overflow) 등등 여러가지 원인으로 나타난다. , "divde by zero"와 같은 것도 흔한 예외 상황 중의 하나이다.

이러한 예외가 발생하면 프로그램은 스스로 오류를 복구하여 계속 진행하는 것이 불가능해지며, 예외처리를 하지 않은 경우 프로그램은 종료되고, 윈도우즈 같은 경우
윈도우즈 에러 리포트 다이얼로그가 나타난다.

그런데 멀티스레드 프로그램에서, 어떤 하나의 스레드가 실행하는 코드에서 예외상황이 발생했을 경우를 생각해 보자. 특히, 이 프로그램이 안정성이 매우 중요시되는 서버 프로그램일 경우, 그리고, 보통 이러한 서버 프로그램은 스레드풀(thread pool)을 가지고 병렬수행이나 비동기 작업을 수행하고 있다고 생각하면, 하나의 스레드에서 발생한 예외상황으로 전체 프로그램이 종료된다면 큰 문제가 될 수 있다. 그래서, 멀티스레드 프로그램에서는 예외상황에서도 안전한 코드(
exception safety)를 작성하는 것이 매우 중요하다.

Visual C/C++에서는 예외처리를 위한 try/catch를 비롯한 몇가지 기능을 제공한다. 여기서는 윈도우즈의 SEH(Structured Exception Handler)를 지원하는 __try/__except 를 사용하여 스택오버플로우 예외를 처리하는 방법을 알아보자. (일반 C++ try/catch 로는 스택오버플로우를 처리할 수 없다.)

다른 예외와는 달리 스택오버플로우의 특징은, 스택을 더 이상 사용할 수 없기 때문에 __except 구문 안에서 스택을 많이 사용하는 함수 호출이나 지역변수의 사용을 금해야한다는 것이다. 그리고 가장 중요한 것은 스택의 메모리 보호페이지(guard page)가 사라진다는 것이다. 이와 관련된 좋은 MSDN의 예제가 아래 웹페이지에 있다.

http://support.microsoft.com/kb/315937

MSDN에 나오는 예제를 보면 스택오버플로우를 두 번 이상 발생시키면 두번째부터는 access violation으로 바뀌는 것을 알 수 있다. 그 이유는 스택오버플로우 예외가 발생한 경우 스택이 손상되기 떄문이다. 스택의 맨 위 페이지는 PAGE_GUARD 페이지로 이 페이지를 액세스하려고 하면 윈도우즈는 이 guard page commit하여 사용가능하게 만들고 새로운 guard page를 그 바로 위에 만든다. 그런데 스택의 최대 크기는 프로그램 컴파일 당시 "/STACK:reserve[,commit]" 옵션으로 설정되거나 CreateThread API의 파라미터로 결정되며 이 크기 이상으로 더 커지려고할 경우 stack overflow가 발생되는 것이다. 이 경우 더이상 예약해놓은 메모리가 없으므로 guard page설정이 불가능해 진다.

프로그램에서 스택오버플로우가 생기는 주요인은 재귀함수의 잘못된 사용에 의한 것이다. 보통 디버깅을 위해서, __except 구문안에서 현재 콜스택을 로그 파일로 남기서나 덤프파일을 만드는데, 여기서 스택오버플로의 경우 특별히 처리해야할 사항은 다음과 같다.

1.  스택오버플로우가 발생했을 경우 최대한 스택 사용을 자제해야만 한다. 만약 로깅(logging)을 위해 스택사용을 할 수 밖에 없다면, 동적메모리를 어느정도 할당한 후, 현재 스택포인터 (ESP)를 이 메모리를 가르키게 하여 새로운 가상 스택을 만들고, 로깅이 끝나면 다시 ESP를 복구해주는 방법이 있다.

2. 버그 있는 재귀함수가 연속적으로 호출되면, 두번째 스택오버플로우 발생시에  guard 페이지가 이미 사라졌으므로 access violation을 일으킨다. 여기서 부터가 큰 문제이다.  __except 구문 안에서 로깅을 위해 지역변수 값을 참조한다거나 하면, 또 다른 AV를 일으켜 프로그램이 갑자기 종료된다. 해결책은 예외코드가 EXCEPTION_STACK_OVERFLOW 일 경우__except 구문 안에서
_resetstkoflw()를 호출하여 guard page를 복구하면, 두번째 스택오버플로우가 발생해도, AV 예외코드로 처리되지 않는다.


이밖에도 멀티스레드 프로그램에서는 어떤 스레드에서 예외상황이 발생했을 경우 이를 정상적으로 복구하는 것은 쉽지 않다. 특히 그 스레드가 Critical Section과 같은 lock을 가지고 있다면,  dead lock을 발생시킬 확률이 매우 높다. 왜냐면 예외처리 루틴은 그 스레드가 어떤 lock을 가지고 있었는지 알기 힘들기 때문이다. , 동적메모리 할당을 했을 경우 그것을 해제 하기 어렵기 때문에, memory leak이 발생한다.

크리에이티브 커먼즈 라이센스
Creative Commons License

오늘 TSF (Text Service Framework)를 지원하는 Application을 개발하는데 있어, 디버깅 하는데  큰 문제가 있음을 알아 냈습니다.

먼저 자세한 설명을 하기 전에 증상부터 알아 봅시다. 아래 소스는 Vista SDK에 TSF 예제로 포함된, TSFAPP의 TEXTSTOR.CPP의 일부입니다. InsertTextAtSelection 함수는 새로운 글자가 입력될 때 마다 호출되는 함수입니다. 함수 시작 부분에 메모리 주소 0 에 쓰기를 시도하여 Access Violation을 유발 시켜보았습니다. 이것은 심각한 버그가 있는 것을 시뮬레이션 하는 것입니다.


/**************************************************************************

  CTSFEditWnd::InsertTextAtSelection()

**************************************************************************/

STDMETHODIMP CTSFEditWnd::InsertTextAtSelection(    DWORD dwFlags,
                                                   const WCHAR *pwszText,
                                                   ULONG cch,
                                                   LONG *pacpStart,
                                                   LONG *pacpEnd,
                                                   TS_TEXTCHANGE *pChange)
{
  OutputDebugString(TEXT("CTSFEditWnd::InsertTextAtSelection \n"));

  *(int *) 0 = 0;  // <-- Access Violation 유발


이제 TSFAPP.EXE를 컴파일시켜, 한글을 입력해 봅시다.  그런데,  예상과 달리 이 어플리케이션은 크래시되지도 않고 우리가 흔히 보는 윈도우즈 에러 리포트 다이얼로그도 나타나지 않습니다. 단지 원하는 한글 입력은  되지 않습니다. 이것이 왜 디버깅에 문제가 되는 지, 아마 어느 정도 짐작하였을 것입니다. 어플리케이션이 제공하는 TSF 인터페이스 내부에, 아무리 심각한 버그가 있어도 프로그램이 죽거나 에러를 리포트하지 않는다는 것입니다.  이유는 무엇일까요? 다름 아닌 윈도우즈의 TSF입력 지원 모듈이 자체 예외처리기를 가지고 있기 때문입니다.

따라서, 디버거를 처음에 attach시켜 프로그램을 실행하지 않는 한, access violation과 같은 심각한  버그가 있어도, 어느 순간 입력에 이상이 생기는 현상만 나타나며, 원인을 알아내기 어렵다는 점 입니다. 특히 어플리케이션이 자체 예외처리기(SEH)를 가지고 콜스택을 로그파일로 만들거나 덤프파일을 만드는 것이 불가능해 집니다. 따라서, 어플리케이션 버그로 인한 입력 문제가 발생함에도 불구하고 사용자나 개발자는 OS에 문제가 있는 것으로 오인할 가능성도 많습니다.

윈도우 TSF 모듈이 자체 예외처리기를 가지는 것을 확인하기 위해서, 우선 위에서 만든 TSFAPP.EXE를 실행해 봅시다. 그리고 VS에서 Attach to Process를 선택하거나, 이 프로세스의 ID를 알아낸 다음, "ntsd -p <프로세스 ID>"와 같이 커맨드라인 디버거를 이용하여 디버거를 attch 합니다. 이제 한글을 입력하려고 하면 디버거에서 이 메모리 오류(Access Violation)을 캐치함을 알 수 있습니다. 이것은 First chance exception 입니다. 여기서 다시 계속 진행시키기 위해 디버거에서 "go"를 선택하면, 아래와 같은 콜스택을 볼 수 있습니다.

TSF 핵심 모듈인 MSCTF.DLL이 자체 exception handler를 가지고 있는 것을 알 수 있다. 여기서 다시 "go"를 하면 프로그램은 종료되지 않고 계속 실행된다.

여기서 CicExceptionFilter가 EXCEPTION_EXECUTE_HANDLER 값을 리턴하는 것을 짐작할 수 있습니다. 즉, MSCTF 자체 예외처리기에 걸리면 다른 SEH는 이것을 알지 못합니다.  내가 아무리 최고의 unhandled exception handler를 가지고 있다 해도 무용지물입니다. 여기서 한가지 해결책은 XP이후부터 지원되는 Vectored Exception Handler를 사용하는 것입니다.


0012f434 77999b31 ntdll!DbgBreakPoint
0012f46c 77999b94 ntdll!RtlUnhandledExceptionFilter2+0x2a4
0012f47c 767ec8f5 ntdll!RtlUnhandledExceptionFilter+0x12
0012f488 767dc902 MSCTF!CicExceptionFilter+0xe
0012f490 7672669b MSCTF!CInputContext::_DoEditSession+0x49
0012f4a4 76726620 msvcrt!_EH4_CallFilterFunc+0x12
0012f4d0 76812a3b msvcrt!_except_handler4_common+0x8e
0012f4f0 77951039 MSCTF!_except_handler4+0x20
0012f514 7795100b ntdll!ExecuteHandler2+0x26
0012f5bc 77950e97 ntdll!ExecuteHandler+0x24
0012f5bc 00402a26 ntdll!KiUserExceptionDispatcher+0xf
0012f8d0 76803991 TSFApp!CTSFEditWnd::InsertTextAtSelection+0x6
0012f90c 767ee809 MSCTF!CACPWrap::InsertTextAtSelection+0x38

0:000> uf MSCTF!CicExceptionFilter
MSCTF!CicExceptionFilter:
77bbc8e7 mov     edi,edi
77bbc8e9 push    ebp
77bbc8ea mov     ebp,esp
77bbc8ec push    dword ptr [ebp+8]
77bbc8ef call    dword ptr [MSCTF!_imp__RtlUnhandledExceptionFilter (77b811d8)]
77bbc8f5 xor     eax,eax
77bbc8f7 inc     eax => retrun
EXCEPTION_EXECUTE_HANDLER
77bbc8f8 pop     ebp
77bbc8f9 ret     4




그런데, 왜 MSCTF는 Access Violation과 같은 심각한 예외상황을 무시하고 지나가도록 한 것일까? MSCTF가 모든 경우 자체 예외 처리를 통해 이런 심각한 예외를 무시하도록 하고 있지는 않다. 간단한 예로 InsertTextAtSelection가 아닌 다른 함수들 안에서 잘못된 메모리 참조하면 정상적으로 프로그램이 종료되고, 윈도우즈 오류 리포팅과 같은 Post Mortem Debugger가 실행된다. 이유는 좀 황당하지만 이렇다.

MS 내부에서 TSF를 지원하는 어플리케이션과 입력기들이 많이 개발되었고, 어플리케이션 혹은 입력기들의 자체 버그로 인해 중요 어플리케이션이 종료되는 현상이 자주 나타나자, TSF 팀은 많이 사용되는 Callback 함수들을 아예 자체 예외처리기로 묶어서 심각한 문제가 발생해도 어플리케이션을 죽이지 않고, 현재 입력만을 취소한 채로 계속 수행되도록 만든 것이다.

특히 TSF의 내부 구조의 복잡성으로 인해, 사소하고 미묘한 문제가 어플리케이션을 죽이는 상황으로 쉽게 니티나기 시작하였다. TSF 팀은 이들 입력기를 개발한 팀에 수정을 요구하기 보다는 자체적으로 이러한 버그를 피해가는 방법을 선택하였다. (많은 종류의 어플리케이션들이 TSF를 잘못 사용하여 문제를 일으켰기 때문일 것이다)

어플리케이션이 죽지 않아 입력된 데이터의 손실은 막을 수 있겠으나, 이로인한 부작용은 앞서 설명한 바와 같이 TSF를 사용하는 어플리케이션이나 입력기의 심각한 문제를 발견하기 어렵게 되었을 뿐 아니라, 입력 중인 글자가 취소되거나 한영 변환이 되지 않는 등 가끔 매우 이상한 현상이 감지될 수 있다.

TSF 어플리케이션을 제작할 때 입력에 문제가 발생한다면, 디버거를 attach시키고 실행하여야만 access violation과 같은 심각한 버그가 숨어 있음을 알 수 있다.



크리에이티브 커먼즈 라이센스
Creative Commons License

앞서 말쓸드린 윈도우즈 command line  디버거인 NTSD는 실행 중인 프로세스에 Attach하는 기능뿐만 아니라 Detach하는 기능도 포함되어 있습니다. 아래는 이 기능을 이용하여 덤프 파일을 만드는 예제입니다. "CrashProgrm"이라는 프로그램이 버그로 인해 간혹 크래시되고 있다면 아래와 같은 배치파일로 NTSD를 attach 시켜놓으면, 크래시가 일어나는 순간 "c:\crash.dmp"라는 파일을 만들고 종료합니다.

혹은 아래 배치를 실행한 후 나타나는 디버거 command 윈도우창에서 Ctrl+C를 누르면 현재 CrashProgram의 메모리와 스레드 (콜스택 포함) 정보를 덤프파일로 만들고, 디버거는 바로 detach되며, CrashProgram는 종료되지 않고 계속 실행하게 됩니다.  만들어진 덤프파일은 나중에 "ntsd -z crash.dmp"로 로드하여 분석할 수 있습니다. 가끔 hang되어 응답이 없는 프로그램을 디버그 하려고 할 때 편리합니다.

특히, 내 컴퓨터가 아니고 다른 사람의 컴퓨터에서 일어나는 문제를 살펴볼 때, 비주얼 스튜디오를 설치하지 않고도 디버깅 할 수 있습니다.  (NTSD는 XP에 포함되어 있으며, OS에 없다고 해도 디버거 파일 몇개를 플래시 메모리에 넣어 가지고 다니다가 사용하면 됩니다.)

for /f %%i in ('tlist CrashProgram') do set PID=%%i
if "%PID%"=="" goto :EndScript
ntsd -g -c ".logopen /d;.dump /o /mFuth c:\crash.dmp;.detach;q" -p %PID%

:EndScript
goto :EOF

크리에이티브 커먼즈 라이센스
Creative Commons License

Windows XP에는 command line debugger인 NTSD.EXE가 내장되어 있습니다. 그런데 NTSD는 계속 업그레이드되고 있으며 최신 버전은 아래 링크에서 다운로드 할 수 있습니다.

http://www.microsoft.com/whdc/devtools/debugging/default.mspx

이 디버거 패키지에는 WinDbg라는 UI지원 디버거도 같이 있으나 역시 사용하기는 어렵습니다. 하지만, OS 시스템 레벨의 디버깅을 원할 경우 아주 막강한 기능들을 제공합니다. 또, 윈도우즈 시스템 파일들의 디버그 심볼을 디버깅 도중 자동으로 다운로드해서 시스템 함수들의 이름을 보여주는 기능이 있습니다. 아래와 환경변수를 셋팅하면 됩니다. 자세한 것은 디버거에 포함된 도움말을 참고 하시기 바랍니다.

set _NT_SYMBOL_PATH=SRV*c:\websymbols*http://msdl.microsoft.com/download/symbols
크리에이티브 커먼즈 라이센스
Creative Commons License