#include "stdafx.h"
#include <windows.h>
#include <stdio.h>
#define VAR_WATCH() printf("nDividen=%d,nDivisor=%d, nResult=%d.\n",nDividen,nDivisor,nResult)
int main()
{
int nDividen = 22, nDivisor = 0, nResult = 100;
__try{
printf("Before div in __try block:");
VAR_WATCH();
nResult = nDividen / nDivisor;
printf("After div in __try block");
VAR_WATCH();
}
__except (printf("in __except block:"),VAR_WATCH(),
GetExceptionCode()==EXCEPTION_INT_DIVIDE_BY_ZERO?
(nDivisor=1,
printf("Divide Zero exception detected:"),VAR_WATCH(),
EXCEPTION_CONTINUE_EXECUTION):
EXCEPTION_CONTINUE_SEARCH)
{
printf("In handle block.\n");
}
return getchar();
}
//except后面的括号里面是一个过滤表达式,这里是一个三目运算符
//如果异常代码是除0的话,就会把除数改为1!然后再返回一个EXCEPTION_CONTINUE_EXECUTION继续执行!
编译器为了支持try......except......;会产生额外的指令,
00C117A3 push 0FFFFFFFEh //特殊的信息是记录在特殊的板块里面的
00C117A5 push 0C17F20h
00C117AA push offset _except_handler4 (0C11D00h)
把特殊的异常处理结构通过压栈的方式在栈上形成一个动态的结构体,然后再赋给FS:[00000000h],就相当于在fs:[00000000h]上注册了一个异常处理结构(2'03"),注册了之后,等会发生异常,就会找到异常处理结构;
一除0,就飞到CPU内核去了,开始跳到除0错误那边,然后分发异常,分发异常的时候给调试器第一轮,visual studio会收到这个通知,但是对于除0的第一轮,visual studio不会处理,它会报告不处理,它不处理之后,内核就会继续分发,把这些异常信息copy到用户栈上,然后到用户态来找,来找这些异常分发函数;
int main()
{
00C117A0 push ebp
00C117A1 mov ebp,esp
00C117A3 push 0FFFFFFFEh //特殊的信息是记录在特殊的板块里面的!
00C117A5 push 0C17F20h
00C117AA push offset _except_handler4 (0C11D00h) //这里是vc运行时默认处理的函数
00C117AF mov eax,dword ptr fs:[00000000h]
00C117B5 push eax
00C117B6 add esp,0FFFFFF04h
00C117BC push ebx
00C117BD push esi
00C117BE push edi
00C117BF lea edi,[ebp-10Ch]
00C117C5 mov ecx,3Dh
00C117CA mov eax,0CCCCCCCCh
00C117CF rep stos dword ptr es:[edi]
00C117D1 mov eax,dword ptr [__security_cookie (0C19004h)]
00C117D6 xor dword ptr [ebp-8],eax
00C117D9 xor eax,ebp
00C117DB push eax
00C117DC lea eax,[ebp-10h]
00C117DF mov dword ptr fs:[00000000h],eax
00C117E5 mov dword ptr [ebp-18h],esp
int nDividen = 22, nDivisor = 0, nResult = 100;
00C117E8 mov dword ptr [nDividen],16h
00C117EF mov dword ptr [nDivisor],0
00C117F6 mov dword ptr [nResult],64h
__try{
00C117FD mov dword ptr [ebp-4],0
printf("Before div in __try block:");
00C11804 push offset string "Before div in __try block:" (0C16B30h)
00C11809 call _printf (0C1131Bh)
00C1180E add esp,4
VAR_WATCH();
00C11811 mov eax,dword ptr [nResult]
00C11814 push eax
00C11815 mov ecx,dword ptr [nDivisor]
00C11818 push ecx
00C11819 mov edx,dword ptr [nDividen]
00C1181C push edx
00C1181D push offset string "nDividen=%d,nDivisor=%d, nResult"... (0C16B50h)
00C11822 call _printf (0C1131Bh)
00C11827 add esp,10h
00C1182A mov ecx,dword ptr [nDivisor]
nResult = nDividen / nDivisor;
00C1182D mov eax,dword ptr [nDividen]
00C11830 cdq
00C11831 idiv eax,ecx
00C11833 mov dword ptr [nResult],eax
printf("After div in __try block");
00C11836 push offset string "After div in __try block" (0C16B80h)
00C1183B call _printf (0C1131Bh)
00C11840 add esp,4
VAR_WATCH();
00C11843 mov eax,dword ptr [nResult]
00C11846 push eax
00C11847 mov ecx,dword ptr [nDivisor]
00C1184A push ecx
00C1184B mov edx,dword ptr [nDividen]
00C1184E push edx
00C1184F push offset string "nDividen=%d,nDivisor=%d, nResult"... (0C16B50h)
00C11854 call _printf (0C1131Bh)
00C11859 add esp,10h
}
00C1185C mov dword ptr [ebp-4],0FFFFFFFEh
00C11863 jmp $LN8+17h (0C11908h)
__except (printf("in __except block:"),VAR_WATCH(),
00C11868 mov eax,dword ptr [ebp-14h]
00C1186B mov ecx,dword ptr [eax]
00C1186D mov edx,dword ptr [ecx]
00C1186F mov dword ptr [ebp-104h],edx
00C11875 push offset string "in __except block:" (0C16BA0h)
00C1187A call _printf (0C1131Bh)
00C1187F add esp,4
00C11882 mov eax,dword ptr [nResult]
00C11885 push eax
00C11886 mov ecx,dword ptr [nDivisor]
00C11889 push ecx
00C1188A mov edx,dword ptr [nDividen]
00C1188D push edx
00C1188E push offset string "nDividen=%d,nDivisor=%d, nResult"... (0C16B50h)
00C11893 call _printf (0C1131Bh)
00C11898 add esp,10h
00C1189B cmp dword ptr [ebp-104h],0C0000094h
00C118A5 jne main+140h (0C118E0h)
00C118A7 mov dword ptr [nDivisor],1
00C118AE push offset string "Divide Zero exception detected:" (0C16BB8h)
00C118B3 call _printf (0C1131Bh)
__except (printf("in __except block:"),VAR_WATCH(),
00C118B8 add esp,4
00C118BB mov eax,dword ptr [nResult]
00C118BE push eax
00C118BF mov ecx,dword ptr [nDivisor]
00C118C2 push ecx
00C118C3 mov edx,dword ptr [nDividen]
00C118C6 push edx
00C118C7 push offset string "nDividen=%d,nDivisor=%d, nResult"... (0C16B50h)
00C118CC call _printf (0C1131Bh)
00C118D1 add esp,10h
00C118D4 mov dword ptr [ebp-10Ch],0FFFFFFFFh
00C118DE jmp main+14Ah (0C118EAh)
00C118E0 mov dword ptr [ebp-10Ch],0
00C118EA mov eax,dword ptr [ebp-10Ch]
$LN15:
00C118F0 ret
$LN8:
00C118F1 mov esp,dword ptr [ebp-18h]
GetExceptionCode()==EXCEPTION_INT_DIVIDE_BY_ZERO?
(nDivisor=1,
printf("Divide Zero exception detected:"),VAR_WATCH(),
EXCEPTION_CONTINUE_EXECUTION):
EXCEPTION_CONTINUE_SEARCH) //except后面的括号里面是一个过滤表达式,这里是一个三目运算符
{ //如果异常代码是除0的话,就会把除数改为1!然后再返回一个EXCEPTION_CONTINUE_EXECUTION继续执行!
printf("In handle block.\n");
00C118F4 push offset string "In handle block.\n" (0C16BE0h)
00C118F9 call _printf (0C1131Bh)
00C118FE add esp,4
}
00C11901 mov dword ptr [ebp-4],0FFFFFFFEh
}
return getchar();
00C11908 mov esi,esp
00C1190A call dword ptr [__imp__getchar (0C1A164h)]
00C11910 cmp esi,esp
00C11912 call __RTC_CheckEsp (0C11113h)
下面用WinDbg来看一看从内核态到用户态:
image.png image.png image.png
现在呢,cpu已经跳到内核的除0函数,内核进行分发,分发之后发现是用户态导致的异常,然后把异常信息复制到用户态栈,复制到用户态栈之后来找当前线程的异常处理链条,也就是fs:[0]链条,在找fs:[0]链条的时候找到了我们的异常处理器,即seh handler;(4'44'');在seh handler里面再执行我们的过滤表达式,过滤表达式呢,可以认为是一个特殊的函数,编译器会把他编译成一个特殊的函数,
在WinDbg中:
0:000> k
ChildEBP RetAddr
00cff80c 00db215e seh__!main+0x8d [e:\总复习\总复习\软件调试\软件调试\seh演示.cpp @ 18]
00cff820 00db1fc0 seh__!invoke_main+0x1e [f:\dd\vctools\crt\vcstartup\src\startup\exe_common.inl @ 64]
00cff878 00db1e5d seh__!__scrt_common_main_seh+0x150 [f:\dd\vctools\crt\vcstartup\src\startup\exe_common.inl @ 253]
00cff880 00db2178 seh__!__scrt_common_main+0xd [f:\dd\vctools\crt\vcstartup\src\startup\exe_common.inl @ 296]
00cff888 74978654 seh__!mainCRTStartup+0x8 [f:\dd\vctools\crt\vcstartup\src\startup\exe_main.cpp @ 17]
00cff89c 779e4a77 KERNEL32!BaseThreadInitThunk+0x24
00cff8e4 779e4a47 ntdll!__RtlUserThreadStart+0x2f
00cff8f4 00000000 ntdll!_RtlUserThreadStart+0x1b
0:000> r
eax=00000025 ebx=00b25000 ecx=00000000 edx=1013281c esi=00db104b edi=00cff7f4
eip=00db182d esp=00cff6f0 ebp=00cff80c iopl=0 nv up ei pl nz na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000206
seh__!main+0x8d:
00db182d cc int 3
自己做实验的:(先是ctrl+E,再是ctrl+O打开反汇编界面以及.cpp界面,在.cpp下断点(F9),再按F10单步走)
image.png
0:000> k //栈回溯!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
ChildEBP RetAddr
0057f7cc 008a215e seh___!main+0x91 [e:\总复习\总复习\软件调试\软件调试\seh演示.cpp @ 18]
0057f7e0 008a1fc0 seh___!invoke_main+0x1e [f:\dd\vctools\crt\vcstartup\src\startup\exe_common.inl @ 64]
0057f838 008a1e5d seh___!__scrt_common_main_seh+0x150 [f:\dd\vctools\crt\vcstartup\src\startup\exe_common.inl @ 253]
0057f840 008a2178 seh___!__scrt_common_main+0xd [f:\dd\vctools\crt\vcstartup\src\startup\exe_common.inl @ 296]
0057f848 759a8654 seh___!mainCRTStartup+0x8 [f:\dd\vctools\crt\vcstartup\src\startup\exe_main.cpp @ 17]
WARNING: Stack unwind information not available. Following frames may be wrong.
0057f85c 77ea4a77 KERNEL32!BaseThreadInitThunk+0x24
0057f8a4 77ea4a47 ntdll!RtlGetAppContainerNamedObjectPath+0x137
0057f8b4 00000000 ntdll!RtlGetAppContainerNamedObjectPath+0x107
0:000> r //查看寄存器!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
eax=00000016 ebx=002c2000 ecx=00000000 edx=00000000 esi=008a104b edi=0057f7b4
eip=008a1831 esp=0057f6b0 ebp=0057f7cc iopl=0 nv up ei pl nz na po nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010202
seh___!main+0x91:
008a1831 f7f9 idiv eax,ecx
0:000> dd 0057f6b0 //0057f6b0是上面中的esp的信息,可以查看内存以及栈中的信息!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
0057f6b0 1c208cec 008a104b 008a104b 002c2000
0057f6c0 ffffffff cccccccc c0000094 cccccccc
0057f6d0 cccccccc cccccccc cccccccc cccccccc
0057f6e0 cccccccc cccccccc cccccccc cccccccc
0057f6f0 cccccccc cccccccc cccccccc cccccccc
0057f700 cccccccc cccccccc cccccccc cccccccc
0057f710 cccccccc cccccccc cccccccc cccccccc
0057f720 cccccccc cccccccc cccccccc cccccccc
dd 0057f6b0
这句话的意思是说从0057f6b0这个地址开始,以4个字节为一个单位开始查看内存,比如下面的1c208cec
等等都是四个字节,
cccccccc
是局部变量区域,由于栈是向低地址空间生长,
context结构有一个著名的0001007f
标志,
这是著名的结构体Exception-record;
这个结构体的我第一个字段就是异常代码:
c0000094
是除0异常的代码,代表除0异常,异常结构体里面有一个导致异常的地址,这是CPU记录下来的,上图中是0040108b
;这个地址是导致除0异常的那个地址,
识别寄存器上下文(Context)就看0001007F
;Context第一个字段是mask字段,来标记哪些寄存器是有效哪些寄存器是无效的,
有了context结构体,可以这样回到上下文,可以这样:
.cxr xxxxxxxx(0001007f所对应的地址)
正是单步这一刹那,正是cpu找出这一触发指令,导致除0,然后cpu报告异常,把这个异常地址(此处为0040108b)压到栈上,这是why我们能够知道准确除0的原因,然后在内核分发异常的时候,也把这个著名的结构体,通过栈复制到用户态,
image.png
通过这条指令可以看到cpu寄存器的上下文(图中的0018fa68是context的第一个字段对应地址),
image.png
综上:内核把重要的异常结构体和context上下文结构体复制到用户态,使得用户态知道异常的详细信息
微软公布的一些API:GetExceptionCode()可以从栈上取得异常代码
image.png
结构化异常处理是有能力让软件回到导致异常的位置重新执行.
网友评论