SEH를 거치는 Windows C++ Exception
윈도우에서 사용되고 있는 Structured Exception Handling(SEH)는 Exception 관리 시스템이다. 하지만 C++은 자체적으로 Exceptoin을 관리한다. 우리는 예제를 작성하여 WinDbg에서 어떻게 Exception이 관리되는 지 볼 것이다.
Structured Exception Handling(SEH)
Exception은 프로그램이 실행되는 동안 발생되는 이벤트로 프로그램이 정상적인 제어 흐름을 벗어날 때 야기된다. Exception은 hardware exception과 software exception 크게 두가지 종류로 분류된다. Hardware exception은 CPU에 의해 시작된다. 0 나누기 연산이나, 접근 불가 메모리에 접근을 시도하는 명령어 구절을 실행하려고 할 때 발생한다. Sofrware exception은 응용 프로그램이나 운영체제에 의해 발생한다. 예를 들어, 유효하지 않은 인자에 값을 지정할 때 감지될 수 있다.Structured exception handling은 hardware와 software exception 양쪽을 처리하는 메커니즘이다. 그리므로 코드는 hardware와 software exception을 동일하게 처리할 수 있다. Structured exception handling은 디버거, 모든 프로그래밍 언어와 기계를 지원하며 제어를 가능하게 한다. (Vectored exception handling은 structured exception handling이 확장된 개념이다.)
시스템 또한 termination handling을 지원하므로 보호된 코드 본문이 실행될 때 마다 지정된 종료 코드 블록이 실행되도록 할 수 있다. 종료 코드는 어떻게 보호된 본문을 빠져나갈 것인지에 대한 제어 흐름과 관계없이 실행된다. 예를 들어, termination handler는 코드의 보호된 본문이 실행되는 동안 예외 또는 오류가 발생하더라도 정리 작업이 수행되도록 보장할 수 있다.
Source code
Zero Exception에 의해 Catch하여 분기되는 간단한 소스코드를 살펴보자.
#include <iostream>
#include <stdexcept>
int divEx(int numerator, int denominator) {
if (denominator == 0) {
throw std::overflow_error("Divide by zero exception");
}
return numerator / denominator;
}
int main() {
int b = 0;
try {
divEx(0x42, 0);
} catch(std::overflow_error e) {
std::cout << "Error " << e.what();
}
return 0;
}
!exchain
Windbg를 실행하고 예제 프로그램을 불러온 후, 메인 함수에 브레이크 포인트를 설정한다. !exchain 명령어는 SEH는 chain list을 출력해준다.
0:000:x86> bp seh_analysis!main
breakpoint 0 redefined
0:000:x86> bl
0 e Disable Clear x86 00bc1060 0001 (0001) 0:**** seh_analysis!main
0:000:x86> g
Breakpoint 0 hit
seh_analysis!main:
00bc1060 55 push ebp
0:000:x86> !exchain
005ef7d8: seh_analysis!_except_handler4+0 (00bc1ebb)
CRT scope 0, filter: seh_analysis!__scrt_common_main_seh+12b (00bc1740)
func: seh_analysis!__scrt_common_main_seh+13f (00bc1754)
005ef834: ntdll_77380000!_except_handler4+0 (773f6a20)
CRT scope 0, filter: ntdll_77380000!__RtlUserThreadStart+3ed78 (77423790)
func: ntdll_77380000!__RtlUserThreadStart+3edbf (774237d7)
005ef84c: ntdll_77380000!FinalExceptionHandlerPad21+0 (77409ec5)
CPU가 인터럽트를 일으킬 때, 커널은 chain list를 탐색하고 함수를 호출하여 오류를 catch할 것이다. 만약 오류를 catch할 함수가 설계되지 않았다면, 커널은 자체적으로 처리한다. (werfalut를 호출하거나 프로세스 종료 처리)
!exchain 명령이 호출하는 전체 chain list를 출력하는 방법을 알아두면 유용하게 활용할 수 있다. (fs:[0]은 chain list 포인터를 포함한다.)
스레드는 자신의 스레드 정보 블록의 시작에서 _EXCEPTION_REGISTRATION_RECORD 리스트와 연결된다. __try는 컴파일러에 정의된 EH_prolog 함수를 호출한다. 이 함수는 스택에 _EXCEPTION_REGISTRATION_RECORD 를 할당하고, 이것은 msvcrt.dll의 __except_handler3 함수를 가리켜, 리스트 헤드에 레코드를 추가한다.
__try 블록의 끝에서는 역 연산을 수행하는 EH_epilog 함수가 호출된다. (컴파일러에서 정의된 이 루틴들은 인라인화될 수 있다.) __except 와 __finally블록들은 __except_handler3 내부에서 호출된다. 이러한 블록들이 존재한다면 생성된 _EXCEPTION_REGISTRATION_RECORD 는 필드를 추가 확장하여 __except_handler3에 의해 사용된다.
사용자 모드 코드에서의 예외일 경우, 운영체제는 핸들러 신호로 예외를 처리하거나 리스트가 끝날 때까지 스레드의 _EXCEPTION_REGISTRATION_RECORD 리스트를 분석하고 순서에 따라 각 예외 처리기를 호출한다. 리스트의 마지막은 항상 일반 보호 장애를 표시하는 kernel32!UnhandledExceptionFilter이다. 그 후 리스트는 처리기에게 사용된 자원들을 정리하기 위해 한번 더 순회된다. 마지막으로 실행은 커널 모드로 반환되어 프로세스가 재개되거나 종료된다.
dt 구조체타입이름 멤버필드이름 [구조체주소]
- 구조체에서 특정한 멤버필드만 확인
0:000:x86> dd fs:[0]
0053:00000000 005ef7d8 005f0000 005ed000 00000000
0053:00000010 00001e00 00000000 006ea000 00000000
0053:00000020 00000164 000011b4 00000000 006ea02c
0053:00000030 006e7000 00000000 00000000 00000000
0053:00000040 00000000 00000000 00000000 00000000
0053:00000050 00000000 00000000 00000000 00000000
0053:00000060 00000000 00000000 00000000 00000000
0053:00000070 00000000 00000000 00000000 00000000
0:000:x86> dt seh_analysis!_EXCEPTION_REGISTRATION_RECORD 005ef7d8
+0x000 Next : 0x005ef834 _EXCEPTION_REGISTRATION_RECORD
+0x004 Handler : 0x00bc1ebb _EXCEPTION_DISPOSITION seh_analysis!_except_handler4+0
0:000:x86> dt seh_analysis!_EXCEPTION_REGISTRATION_RECORD 005ef834
+0x000 Next : 0x005ef84c _EXCEPTION_REGISTRATION_RECORD
+0x004 Handler : 0x773f6a20 _EXCEPTION_DISPOSITION ntdll_77380000!_except_handler4+0
0:000:x86> dt seh_analysis!_EXCEPTION_REGISTRATION_RECORD 005ef84c
+0x000 Next : 0xffffffff _EXCEPTION_REGISTRATION_RECORD
+0x004 Handler : 0x77409ec5 _EXCEPTION_DISPOSITION ntdll_77380000!FinalExceptionHandlerPad21+0
마지막 함수 (FinalExceptionHandlePad21)을 따라가면 일반적인 메시지를 찾을 수 있다.
ntdll_77380000!_FinalExceptionHandler+0x4f:
7745a678 6882473877 push offset ntdll_77380000!`string' (77384782)
7745a67d 50 push eax
7745a67e e88d240000 call ntdll_77380000!RtlUnhandledExceptionFilter2 (7745cb10)
ntdll_77380000!RtlUnhandledExceptionFilter2+0x7a:
7745cb8a ff30 push dword ptr [eax]
7745cb8c 68ac813877 push offset ntdll_77380000!`string' (773881ac)
0:000:x86> dc 773881ac
773881ac 2a200a0a 55202a2a 6e61686e 64656c64 .. *** Unhandled
773881bc 63786520 69747065 30206e6f 38302578 exception 0x%08
773881cc 202c786c 20746968 25206e69 253a7377 lx, hit in %ws:%
773881dc 000a0a73 2a2a2a20 746e6520 2e207265 s... *** enter .
773881ec 20727865 66207025 7420726f 65206568 exr %p for the e
773881fc 70656378 6e6f6974 63657220 0a64726f xception record.
7738820c 00000000 2a2a2a20 6e652020 20726574 .... *** enter
7738821c 7278632e 20702520 20726f66 20656874 .cxr %p for the
Traditional exception
이제 본격적으로 예제를 살펴보자. 먼저 SEH 상단에 함수를 추가하는 것을 확인 할 수 있다.
seh_analysis!main:
00bc1060 55 push ebp
00bc1061 8bec mov ebp,esp
00bc1063 6aff push 0FFFFFFFFh
00bc1065 685821bc00 push offset seh_analysis!CxxThrowException+0x17 (00bc2158)
00bc106a 64a100000000 mov eax,dword ptr fs:[00000000h]seh_analysis!main:
00bc1060 55 push ebp
00bc1061 8bec mov ebp,esp
00bc1063 6aff push 0FFFFFFFFh
00bc1065 685821bc00 push offset seh_analysis!CxxThrowException+0x17 (00bc2158)
00bc106a 64a100000000 mov eax,dword ptr fs:[00000000h]
00bc1070 50 push eax
00bc1071 83ec14 sub esp,14h
00bc1074 a10050bc00 mov eax,dword ptr [seh_analysis!__security_cookie (00bc5000)]
00bc1079 33c5 xor eax,ebp
00bc107b 8945ec mov dword ptr [ebp-14h],eax
00bc107e 53 push ebx
00bc107f 56 push esi
00bc1080 57 push edi
00bc1081 50 push eax
00bc1082 8d45f4 lea eax,[ebp-0Ch]
00bc1085 64a300000000 mov dword ptr fs:[00000000h],eax
SEH 초기화한 후, divEX(0x42, 0) 함수를 호출한다.
00bc108b 8965f0 mov dword ptr [ebp-10h],esp
00bc108e c745fc00000000 mov dword ptr [ebp-4],0
00bc1095 e866ffffff call seh_analysis!divEx (00bc1000)
denominator확인 후 CxxThrowException을 호출한다.
00bc100f 68b838bc00 push offset seh_analysis!_TI3?AVoverflow_errorstd (00bc38b8)
00bc1014 8d45f4 lea eax,[ebp-0Ch]
00bc1017 50 push eax
00bc1018 e824110000 call seh_analysis!CxxThrowException (00bc2141)
*CxxThrowException은 빌드 Exception을 기록하고 런타임 환경에서 Exception 프로세스를 시작한다. (MSDN _CxxThrowException)
extern "C" void __stdcall _CxxThrowException(
void* pExceptionObject
_ThrowInfo* pThrowInfo
);
0:000:x86> dt -r4 seh_analysis!_s__ThrowInfo 00bc38b8
+0x000 attributes : 0
+0x004 pmfnUnwind : 0x00bc13e0 void seh_analysis!std::overflow_error::~overflow_error+0
+0x008 pForwardCompat : (null)
+0x00c pCatchableTypeArray : 0x00bc3854 _s__CatchableTypeArray
+0x000 nCatchableTypes : 0n3
+0x004 arrayOfCatchableTypes : [0] 0x00bc389c _s__CatchableType
+0x000 properties : 0
+0x004 pType : 0x00bc5030 _TypeDescriptor
+0x000 pVFTable : 0x00bc3124 Void
+0x004 spare : (null)
+0x008 name : [0] ".?AVoverflow_error@std@@"
+0x008 thisDisplacement : _PMD
+0x000 mdisp : 0n0
+0x004 pdisp : 0n-1
+0x008 vdisp : 0n0
+0x014 sizeOrOffset : 0n12
+0x018 copyFunction : 0x00bc1020 void seh_analysis!std::overflow_error::overflow_error+0
CxxThrowException이후 NtRaiseException을 호출하고 NtRaiseException은 KiRaiseException을 호출한다.
0:000:x86> u NtRaiseException
ntdll_77380000!NtRaiseException:
773efd20 b85c010000 mov eax,15Ch
773efd25 baf09c4077 mov edx,offset ntdll_77380000!Wow64SystemServiceCall (77409cf0)
773efd2a ffd2 call edx
773efd2c c20c00 ret 0Ch
773efd2f 90 nop
0:000:x86> u 77409cf0
ntdll_77380000!Wow64SystemServiceCall:
77409cf0 ff2518924977 jmp dword ptr [ntdll_77380000!Wow64Transition (77499218)]
0:000:x86> u poi(77499218)
wow64cpu!KiFastSystemCall:
579d7000 ea09709d573300 jmp 0033:579D7009
이제 커널은 SEH에서 첫 번째 함수를 호출하고 catch 서브 프로그램에 도달하는 것을 볼 수 있다.