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
서브 프로그램에 도달하는 것을 볼 수 있다.