버튼이 동작하지 않습니다.
"이 문서는 https://blogs.msdn.com/ntdebugging blog 의 번역이며 원래의 자료가 통보 없이 변경될 수 있습니다. 이 자료는 법률적 보증이 없으며 의견을 주시기 위해 원래의 blog 를 방문하실 수 있습니다. (https://blogs.msdn.com/ntdebugging/archive/2007/06/15/this-button-doesn-t-do-anything.aspx)"
안녕하세요 Matthew 입니다. 오늘은 Tate 가 이전에 작성한 blog의Hang 시나리오 #1 에 대해서 이야기 해보고자 합니다. Debugging 관점에서 볼 때 Application 은 문제점이 발생하였을 때 오류를 발생시켜야 합니다. 하지만 가끔씩 사용자가 한 동작이 (마우스 클릭) 어떤 오류로 인해 응답하지 않을 경우가 있습니다. 이럴 경우 어떤 부분에 문제가 있는지 어떻게 판단할 수 있을까요?
일반적으로 Sysinternals 에서 나온 Process Monitor 는 이런 문제를 확인하는데 아주 좋은 툴 입니다. 또한 문제점이 file system 이나 registry 관련된 것이라면 Process Explorer 가 문제점을 파악할 수 있을 것 입니다. 만약 Network 을 통해 문제가 발생하고 있는 것이라면 Network 의 동작을 수집해 보는 것이 좋을 것 입니다.
위에서 이야기 한 것과 같은 내용으로 문제점을 설명할 수 없고 Application 의 동작에 대해서도 알 수 없고 소스 코드 마저 없다면 어떻게 할 수 있을까요? 아래 Sample application 을 debugging 하면서 답을 찾아 보도록 하겠습니다.
Download the sample application here.
우리가 알고 있는 사항 입니다.
1. Button 1 을 누를 경우 다이얼로그 창이 나타납니다.
2. 특정 사용자의 경우 Button 1 을 눌러도 응답이 없습니다.
3. 소스코드는 물론 심볼도 사용할 수 없습니다.
4. 아무도 Button 1 이 동작하는 방식을 모릅니다.
5. Application 의 개발자는 3년째 연락이 되지 않습니다.
우리는 Button 1 이 눌렸을 때 어떤 동작이 일어나는지 파악해야 합니다. 모든 Window 는 사용자의 입력을 받기 위해 WindowProc 를 가지고 있습니다. Button 은 "control" 이라고 생각할 수 있습니다. Button 이 눌렸을 때 Application의 Main Window 는 WM_COMMAND 메시지를message 를 전달 받습니다. WM_COMMAND 메시지는 각각 다른 동작을 하기 위한 제어코드를 가지고 있습니다.
우리가 필요한 것은
1. Button 1 의 제어코드를 파악
2. Main application window 에서 WindowProc 찾기
3. WM_COMMAND 명령이 Button 1 을 어디서 처리하는지 찾기
4. 에러 코드가 무엇인지 파악
시작해 보겠습니다.
Button 1 을위한 Control ID 찾기
Spy++ (Visual Studio 에 포함) 가 이 문제를 분석하는데 적합한 Tool 입니다. “Spy” -> “Log Message” 를 클릭 합니다. Finder Tool 을 사용해서 ntdbghang1.exe 의 Main window 를 선택 합니다. (역자주: 조준점 처럼 생긴 것을 Drag 해서 ntdbghang1.exe 의 윈도우 전체를 선택하게 해서 Drop 합니다. )Messages 탭에서 “Clear All” 를 클릭한 후 “WM_COMMAND” 를 선택 합니다. “OK” 를 선택하면 SPY++ 가 로그를 기록하기 시작 합니다. Ntdbghang1.exe 의 Button 1 을 클릭 하면 아래와 같은 메시지가 기록되는 것을 확인할 수 있을 것 입니다. 비교를 위해 Button 2 를 클릭하면 동일한 메시지가 기록되는 것을 확인할 수 있습니다. 이때 화면은 아래와 비슷할 것 입니다.
이 화면을 통해 Button 1 의 Control ID는 257(0x101) 이고 Button 2 의 Control ID는 258(0x102) 라고 할 수 있습니다. 나중에 이 정보가 필요할 것 입니다.
Main application window 를위한 WindowProc 찾기
WindowProc 의 주소를 SPY++ 을 이용해서 찾을 수 있습니다. “SPU” -> “Find Window” 를 클릭한 후 Finder Tool 을 사용해서 ntdbghang1.exe 의 main window 를 선택 한 후 “Show Properties” 를 선택한 후 OK 를 선택 하면 아래와 같은 화면을 볼 수 있습니다.
Window Proc 에 기록되어 있는 값은 Main window 의 window procedure 주소 값 입니다. 우리가 아는 것과 같이 WindowProc 함수이며 다음과 같습니다.
LRESULT CALLBACK WindowProc(
HWND hwnd,
UINT uMsg,
WPARAM wParam,
LPARAM lParam
);
wParam 과 lParam 의 값은 uMsg 에 따라 달라 집니다. WM_COMMAND 메시지가 전달되면 wParam 의 낮은 word 값은 우리가 이미 확인한 0x101 값인 Control ID 입니다. 상위 word 값은 Control 통지 code 로 버튼이 클릭되었다는 의미인 BN_CLICKED(내부적으로 0) 입니다.
그래서 다음과 같은 값을 가졌다고 생각할 수 있습니다.
uMsg = WM_COMMAND (literally 0x111)
wParam = 0x101
WindowProc 를 어셈블리 언어로 보면 다음과 같은 stack 을 가질 것 입니다.
ebp = “old ebp”
ebp+4 = “return address”
ebp+8 = hwnd
ebp+c = uMsg
ebp+10 = wParam
ebp+14 = lParam
Button 1 을 위한 WM_COMMAND message 가 어디서 처리되는지 확인
WindowProc 의 주소를 가지고 있다면 WinDbg 를 이용해서 Assembly code 를 확인할 수 있습니다. WinDbg.exe 를 실행한 후 “File -> Attach to a Process “ 를 선택한 후 리스트에서 “ntdbghang1.exe”를 선택 하고 “OK” 를 선택 합니다. Debugger 가 ntdbghang1.exe 를 멈추면 “u <address>” 명령을 사용해서 WinProc 함수를 unassembled 할 수 있습니다. 저의 시스템에서는 “u 01002830” 명령을 사용하였습니다. 뒷 부분을 더 unassembled 하려면 “u” 명령만 반복해서 입력해 주면 됩니다. 관련된 code 를 unassembled 하고 어떤 의미를 가지는지 확인해 보도록 하겠습니다.
0:001> u 01002830
ntdbghang1+0x2830:
첫 번째 3개의 명령은 함수의 초기명령입니다.
01002830 8bff mov edi,edi
01002832 55 push ebp
01002833 8bec mov ebp,esp
uMsg 값을 ecx 로 넣는 명령 입니다.
01002835 8b4d0c mov ecx,dword ptr [ebp+0Ch]
Application 이 상태를 확인하는 명령으로 들어 갑니다. 우리는 uMsg = WM_COMMAND 를 확인하는 곳을 중점적으로 볼 것 입니다. (역자주 : 실제 코드는 uMsg == WM_COMMAND 일 것입니다.)
if uMsg = WM_CREATE (1) goto 01002893
01002838 49 dec ecx
01002839 7458 je ntdbghang1+0x2893 (01002893)
if uMsg = WM_ DESTROY (2) goto 01002889
0100283b 49 dec ecx
0100283c 744b je ntdbghang1+0x2889 (01002889)
if uMsg = WM_CLOSE (0x10) goto 01002889
0100283e 83e90e sub ecx,0Eh
01002841 743b je ntdbghang1+0x287e (0100287e)
if uMsg = WM_COMMAND (0x111) goto 01002853
01002843 b801010000 mov eax,101h
01002848 2bc8 sub ecx,eax
0100284a 7407 je ntdbghang1+0x2853 (01002853)
위의 상태를 확인하는 코드를 보면 uMsg = WM_COMMAND 에서 01002853 으로 실행이 넘어가는 것을 확인할 수 있습니다. 자 그곳을 확인해 보도록 하겠습니다.
0:001> u 01002853
ntdbghang1+0x2853:
wParam 값을 exd 에 넣습니다.
01002853 8b5510 mov edx,dword ptr [ebp+10h]
만약 LOWORD(wParam) == 0x101 이라면 (button 1의 control ID) 0100286f 으로 넘어갑니다.
01002856 0fb7ca movzx ecx,dx
01002859 2bc8 sub ecx,eax
0100285b 7412 je ntdbghang1+0x286f (0100286f)
위의 Assembly 코드를 확인해 보면 Button 1 의 Control ID 를 확인하는 것을 볼 수 있고 아래는 우리가 확인하고자 하는 코드 입니다.
0:001> u 0100286f
ntdbghang1+0x286f:
만약 HIWORD(wparam) != BN_CLICKED (0) 이라면 0100289b 으로 넘어가고, 아니라면 010027f6 함수를 호출합니다.
0100286f c1ea10 shr edx,10h
01002872 6685d2 test dx,dx
01002875 7524 jne ntdbghang1+0x289b (0100289b)
01002877 e87affffff call ntdbghang1+0x27f6 (010027f6)
Control id 0x101 이고 BN_CLICKED 라면 010027f6 이 실행되는 것을 확인할 수 있습니다.
무엇이 실패 하였는지 확인
버튼이 클릭되었을 때 어떤 코드(010027f6 의 함수)가 실행되는지 알게 되었습니다. 이제 어떤 코드에서 문제가 발생하였는지 확인해 보고자 합니다. “uf” 명령을 사용해서 전체 함수를 unassembled 할 수 있습니다. 첫 번째 단계에서 로컬 변수의 이름을 확인할 수 있고 이것들이 무엇인지 확인해 볼 것 입니다.
0:001> uf 010027f6
ntdbghang1+0x27f6:
Function Prologue
010027f6 8bff mov edi,edi
010027f8 55 push ebp
010027f9 8bec mov ebp,esp
010027fb 51 push ecx
010027fc 56 push esi
func1(0x20) 호출 [func1 의 주소는 01002dca 입니다.]
로컬변수 localvar1=0x10 로 설정합니다.
010027fd 6a20 push 20h
010027ff c745fc10000000 mov dword ptr [ebp-4],10h
01002806 e8bf050000 call ntdbghang1+0x2dca (01002dca)
Func1 의 결과를 esi 에 저장 합니다.(localvar2 임)
0100280b 8bf0 mov esi,eax
func2 호출(localvar2, &localvar1) [func2 address at 0100b484]
0100280d 59 pop ecx
0100280e 8d45fc lea eax,[ebp-4]
01002811 50 push eax
01002812 56 push esi
01002813 e86c8c0000 call ntdbghang1+0xb484 (0100b484)
func2 의 결과가 0이면 , 01002821 로 넘어갑니다.
01002818 85c0 test eax,eax
0100281a 7405 je ntdbghang1+0x2821 (01002821)
func3() 호출 [func3 의 주소는 0100278d 입니다.]
ntdbghang1+0x281c:
0100281c e86cffffff call ntdbghang1+0x278d (0100278d)
func4 호출(localvar2) [func4 주소는01002ce3 입니다.]
ntdbghang1+0x2821:
01002821 56 push esi
01002822 e8bc040000 call ntdbghang1+0x2ce3 (01002ce3)
정리 작업 및 함수에서 빠져 나가는 코드 입니다.
01002827 59 pop ecx
01002828 5e pop esi
01002829 c9 leave
0100282a c3 ret
아래와 같이 가상의 코드를 만들 수 있습니다.
localvar1 = 0x10;
localvar2 = func1(0x20);
if(func2(localvar2, &localvar1)
{
func3();
}
func4(localvar2);
함수의 동작은 Func2 의 결과에 따라 달라집니다. Func2 가 어떤 동작을 하는지 살펴 보고자 합니다.
0:001> u 0100b484
ntdbghang1+0xb484:
0100b484 ff2568110001 jmp dword ptr [ntdbghang1+0x1168 (01001168)]
0100b48a cc int 3
0100b48b cc int 3
0100b48c cc int 3
0:001> dps 01001168 L1
01001168 70b88cb1 WINSPOOL!GetDefaultPrinterW
GetDefaultPrinterW 는 공개된 API 함수로 아래에 MSND에 있는 프로토타입이 있습니다.
BOOL GetDefaultPrinter(
LPTSTR pszBuffer, // printer name buffer
LPDWORD pcchBuffer // size of name buffer
);
이 내용은 이전에 우리가 만든 가상코드와 일치 하고 이 함수는 두 개의 파라미터를 사용하고 BOOL 값을 리턴 합니다. 새로 알아낸 내용을 바탕으로 func2 를 GetDefaultPrinter 로 변경하도록 하겠습니다.
DWORD cchBuffer = 0x10;
LPWSTR pszBuffer = func1(0x20);
if(GetDefaultPrinterW(pszBuffer, &cchBuffer))
{
func3();
}
func4(pszBuffer);
위의 내용을 바탕으로 몇 가지 가정을 할 수 있습니다. Func1 은 무언가를 할당하는 함수(malloc 와 비슷) 이고 Fun4 는 메모리 해제 함수(free 로 생각)로 볼 수 있습니다. Application 이 GetDefaultPrinter (끝이 W 로 끝나는) 함수의 Unicode version 을 사용하고 0x20Byte 를 문자열 Buffer 의 Size로 전달하고 GetDefaultPrinterW 에 0x10 을 전달한다고 볼 수 있습니다. 위의 가정은 Func3 를 제외하고는 맞습니다. Func3 가 무엇을 하는지 확인하기 위해 Unassemble 할 수 있으나 실제로 필요하지 않을 수 있습니다. 가상의 코드에 따르면 GetDefaultPrinterW 에 문제가 있는 것을 확인할 수 있습니다.
이제 확인해 보도록 하겠습니다. GetDefaultPrinterW 에 BreadPoint 를 설정하고 Button 을 클릭했을 때 어떤 문제가 있는지 확인해 보도록 하겠습니다.
0:001> bp WINSPOOL!GetDefaultPrinterW
0:001> g
<Button 1 클릭>
Breakpoint 0 hit
eax=0006fb84 ebx=00000000 ecx=00000020 edx=00dc0e98 esi=00dc0e70 edi=0006fc0c
eip=70b88cb1 esp=0006fb74 ebp=0006fb88 iopl=0 nv up ei pl nz na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000202
WINSPOOL!GetDefaultPrinterW:
70b88cb1 8bff mov edi,edi
// Call stack 보기
0:000> kb
ChildEBP RetAddr Args to Child
0006fb70 01002818 00dc0e70 0006fb84 00000111 WINSPOOL!GetDefaultPrinterW
WARNING: Stack unwind information not available. Following frames may be wrong.
0006fb88 0100287c 0006fbbc 75d41a10 00320f78 ntdbghang1+0x2818
0006fb90 75d41a10 00320f78 00000111 00000101 ntdbghang1+0x287c
0006fbbc 75d41ae8 01002830 00320f78 00000111 USER32!InternalCallWinProc+0x23
0006fc34 75d4286a 00000000 01002830 00320f78 USER32!UserCallWinProcCheckWow+0x14b
0006fc74 75d42bba 00a90b80 0095ee08 00000101 USER32!SendMessageWorker+0x4b7
// return 을 확인해 보면 eax=0 입니다. GetDefaultPrinterW 가 FALSE 를 return 했다는 것을 의미 합니다.
0:000> gu
eax=00000000 ebx=00000000 ecx=76f22033 edx=00e10178 esi=00dc0e70 edi=0006fc0c
eip=01002818 esp=0006fb80 ebp=0006fb88 iopl=0 nv up ei pl zr na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246
ntdbghang1+0x2818:
01002818 85c0 test eax,eax
// Check the last error...
// 마지막 에러 확인
0:000> !gle
LastErrorValue: (Win32) 0x7a (122) - The data area passed to a system call is too small.
// 명령 다시 실행
0:000> g
Error 는 0x7a = ERROR_INSUFFICIENT_BUFFER 이고 GetDefaultPrinterW 함수로 전달된 Buffer 의 크기가 Default printer 의 이름보다 작다는 것 입니다. 이것이 일부 사용자에게 문제가 발생하는 원인 입니다. 기본 프린터의 이름을 16자 이하로 줄이면 문제는 해결될 것 입니다.
정리 하면서 아래 Button1_OnClick 함수의 C 소스코드를 첨부 합니다. (010027f6 주소에 있는 내용)
VOID Button1_OnClick()
{
DWORD cch = 16;
LPTSTR pPrinterName;
pPrinterName = (LPTSTR) malloc(16 * sizeof(TCHAR));
if(GetDefaultPrinter(pPrinterName, &cch))
{
DisplayGoButtonMessage();
}
free(pPrinterName);
return;
}
WinDbg 명령중 “wt” 을 사용해서 위의 문제를 확인해볼 수 도 있습니다. 이 글이 여러분에게 도움이 되었기를 바랍니다.
Comments
- Anonymous
February 16, 2009
The comment has been removed