三、展开 (unwind)
为了说明这个概念,需要先回顾下异常发生后的处理流程。
我们假设一系列使用 SEH 的函数调用流程:
func1 -> func2 -> func3。在 func3 执行的过程中触发了异常。
看看分发异常流程 RtlRaiseException -> RtlDispatchException -> RtlpExecuteHandlerForException
RtlDispatchException 会遍历异常链表,对每个 EXCEPTION_REGISTRATION 都调用 RtlpExecuteHandlerForException。
RtlpExecuteHandlerForException 会调用 EXCEPTION_REGISTRATION::handler,也就是 PassThrough!_except_handler4。如咱们上面分析,该函数内部遍历 EXCEPTION_REGISTRATION::scopetable,如果遇到有 scopetable_entry::lpfnFilter 返回 EXCEPTION_EXECUTE_HANDLER,那么 scopetable_entry::lpfnHandler 就会被调用,来处理该异常。
因为 lpfnHandler 不会返回到 PassThrough!_except_handler4,于是执行完 lpfnHandler 后,就会从 lpfnHandler 之后的代码继续执行下去。也就是说,假设 func3 中触发了一个异常,该异常被 func1 中的 __except 处理块处理了,那 __except 处理块执行完毕后,就从其后的指令继续执行下去,即异常处理完毕后,接着执行的就是 func1 的代码。不会再回到 func2 或者 func3,这样就有个问题,func2 和 func3 中占用的资源怎么办?这些资源比如申请的内存是不会自动释放的,岂不是会有资源泄漏问题?
这就需要用到“展开”了。
说白了,所谓“展开”就是进行清理。(注:这里的清理主要包含动态分配的资源的清理,栈空间是由 func1 的“mov esp,ebp” 这类操作顺手清理的。当时我被“谁来清理栈空间”这个问题困扰了很久……)
那这个展开工作由谁来完成呢?由 func1 来完成肯定不合适,毕竟 func2 和 func3 有没有申请资源、申请了哪些资源,func1 无从得知。于是这个展开工作还得要交给 func2 和 func3 自己来完成。
展开分为两种:“全局展开”和“局部展开”。
全局展开是指针对异常链表中的某一段,局部展开针对指定 EXCEPTION_REGISTRATION。用上面的例子来讲,局部展开就是针对 func3 或 func2 (某一个函数)内部进行清理,全局展开就是 func2 和 func3 的局部清理的总和。再归纳一下,局部展开是指具体某一函数内部的清理,而全局展开是指,从异常触发点(func3)到异常处理点(func1)之间所有函数(包含异常触发点 func3)的局部清理的总和。
来看反汇编代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | [font=Consolas][color=
PassThrough!_EH4_GlobalUnwind [ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][ /font ]
|
RtlUnwind 的原型:
VOID RtlUnwind(
PEXCEPTION_REGISTRATION pExceptionRegistration
PVOID pReturnEip
PEXCEPTION_RECORD pExceptionRecord,
PVOID pReturnValue
);
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 | [font=Consolas][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
80867361 [ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
:
: [ /color ][color=
: [ /color ][color=
: [ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
: [ /color ][color=
> [ /color ][color=
[ /color ][color=
:
: [ /color ][color=
: [ /color ][color=
[ /color ][color=
::
:: [ /color ][color=
>: [ /color ][color=
[ /color ][color=
: [ /color ][color=
> [ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
:
: [ /color ][color=
: > [ /color ][color=
[ /color ][color=
:: :
:: : [ /color ][color=
:: : [ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
::: :
::: : [ /color ][color=
:>: : [ /color ][color=
[ /color ][color=
: :: :
: :: : [ /color ][color=
: :: : [ /color ][color=
[ /color ][color=
: ::: :
: ::: : [ /color ][color=
: ::: : [ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
: ::: :
: ::: : [ /color ][color=
: >>> : [ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
: : : [ /color ][color=
: : : [ /color ][color=
[ /color ][color=
[ /color ][color=
: :: : [ /color ][color=
: :: : [ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
: ::: : [ /color ][color=
: ::: : [ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
: :::: : [ /color ][color=
: :::: : [ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
: :::: : : [ /color ][color=
: :::: : : [ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
: ::::<: : [ /color ][color=
: :::::: :
: :::::: : [ /color ][color=
: :::::: : [ /color ][color=
[ /color ][color=
[ /color ][color=
: ::::: : [ /color ][color=
: ::::: : [ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
: :::: :
: :::: : [ /color ][color=
: :::: : [ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
: ::: :
: ::: : [ /color ][color=
: ::: : [ /color ][color=
[ /color ][color=
: ::: : [ /color ][color=
[ /color ][color=
[ /color ][color=
: :::: : [ /color ][color=
: :::: : [ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
: ::::: : [ /color ][color=
: ::::: : [ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
: :::::: : [ /color ][color=
: :::::: : [ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
: ::::::: : [ /color ][color=
: ::::::: : [ /color ][color=
[ /color ][color=
[ /color ][color=
: :::::::::
: ::::::::: [ /color ][color=
: ::::::::: [ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
: : ::
: : :: [ /color ][color=
: > >: [ /color ][color=
[ /color ][color=
: : [ /color ][color=
> : [ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
:
: [ /color ][color=
: [ /color ][color=
[ /color ][color=
:< [ /color ][color=
::
:: [ /color ][color=
:: [ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
:
: [ /color ][color=
> [ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
; 然后 pDispatcherContext->RegistrationPointer 就是展开过程中正在被展开的
; 那个 EXCEPTION_REGISTRATION_RECORD
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
nt!RtlpExecuteHandlerForUnwind [ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
nt!ExecuteHandler2 [ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
|
代码不长,主要功能也不复杂:从异常链表头开始遍历,一直遍历到指定 EXCEPTION_REGISTRATION_RECORD,对每个遍历到的 EXCEPTION_REGISTRATION_RECORD,执行 RtlpExecuteHandlerForUnwind 进行局部展开。
这段代码里有一个细节我没想明白,或者我想复杂了。在 nt!RtlUnwind 地址 80867401 处,当展开到指定 EXCEPTION_REGISTRATION 后,RtlUnwind 通过 ZwContinue 返回,而不是使用 ret 指令。通过静态分析和动态分析我都没有找到用 ZwContinue 的好处,或者不可替代的原因。如果有朋友有不同的结论,请分享一下。
在分析全局展开时发生一件很囧的事,我反汇编完成,梳理流程的时候,总感觉“这个逻辑怎么这么熟悉,貌似在哪见过~ 难道 wrk 里有源码?”,翻开 wrk,果然有…… 不过话说回来,在反汇编分析过程让我对一些细节理解的更深刻了(也只能这么安慰自己了……)。
下面我们来看看局部展开。
在前面讲 PassThrough!_except_handler4 时,有提到该函数既负责处理异常也负责局部展开。其区分功能的标志就是判断 EXCEPTION_RECORD::ExceptionFlags 是否包含 EXCEPTION_UNWIND 标志位。可以参考 PassThrough!_except_handler4 中地址为 f87203dc 的指令:
f87203dc test byte ptr [eax+4],66h ; pExceptionRecord->ExceptionFlags & EXCEPTION_UNWIND,判断是异常处理过程还是展开过程
该标志是在 RtlUnwind 中被设置的,可以参考 RtlUnwind 中地址为 808673b7 和 808673bd 出的指令:
808673b7 or dword ptr [esi+4],2 ; l_ExceptionRecord.ExceptionFlags |= EXCEPTION_UNWINDING (0x2)
808673bd or dword ptr [esi+4],6 ; l_ExceptionRecord.ExceptionFlags |= EXCEPTION_UNWINDING | EXCEPTION_EXIT_UNWIND (0x2 | 0x4)
反汇编代码:
首先是我整理的 _EH4_LocalUnwind 的原型:
VOID fastcall _EH4_LocalUnwind(
PEXCEPTION_REGISTRATION pExceptionRegistartion,
ULONG ulUntilTryLevel
)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 | [font=Consolas][color=
PassThrough!_EH4_LocalUnwind [ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
VOID local_unwind4[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
PassThrough!_local_unwind4 [ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
>> [ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
: :: [ /color ][color=
: :: [ /color ][color=
[ /color ][color=
[ /color ][color=
:: ::
:: :: [ /color ][color=
:: :: [ /color ][color=
[ /color ][color=
[ /color ][color=
::::: [ /color ][color=
:>::: [ /color ][color=
: ::: [ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
: : :
: : : [ /color ][color=
: : : [ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
: :
: : [ /color ][color=
> > [ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
PassThrough!_NLG_Call [ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][ /font ]
|
PassThrough!_EH4_LocalUnwind 中 f8720500 处的指令用到的异常处理函数 PassThrough!_unwind_handler4 的反汇编代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | [font=Consolas][color=
PassThrough!_unwind_handler4 [ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
; 参考 f8720511 处的指令
[ /color ][color=
[ /color ][color=
[ /color ][color=
; 参考 f87204fb 处的指令
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][ /font ]
|
到这里概要流程就讲完了。在处理异常和展开过程中多处涉及到遍历操作,咱们来总结一下这些遍历操作。
1. 在异常处理过程中,每个被"卷入是非"的异常都至少会遍历异常链表两次(如果发生嵌套异常,比如在展开过程中
EXCEPTION_REGISTRATION_RECORD::Handler 又触发异常,则会遍历更多次。不过这也可以算作是一个新异常了。看如何理解。)。
一次是在 RtlDispatchException 中,遍历的目的是找到愿意处理该异常的 _EXCEPTION_REGISTRATION_RECORD。
另一次是在展开过程中、RtlUnwind 函数内,遍历的目录是为了对每个遍历到的 EXCEPTION_REGISTRATION_RECORD 进行局部展开。
2. 同样的,每个被"卷入是非"的异常的 scopetable 也会被遍历至少两次,
一次是在 modulename!_except_handler? 中,遍历目的也是找到愿意处理该异常的 scopetable_entry。
另一次是在展开过程中、_local_unwind4 函数内,遍历的目的是找到所有指定范围内的 scopetable_entry::lpfnFilter 为 NULL 的 scopetable_entry,调用它们的 lpfnHandler (即 __finally 处理块)。
在展开过程中,__finally 代码块会被执行,在执行过程中有可能触发新的异常,增强版通过返回 ExceptionCollidedUnwind (3) 来标识这种情况(参考 PassThrough!_unwind_handler4 中 f87205af 处的指令)。咱来回顾下这类返回值:
1 2 3 4 5 6 7 | [font=Consolas][color=
ExceptionContinueExecution[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
} EXCEPTION_DISPOSITION[ /color ][color=
[ /color ][ /font ]
|
前面的代码中已经展示了上述4中的三种:
ExceptionContinueExecution 表示继续执行(异常已经被修复);
ExceptionContinueSearch 表示继续搜索(当前搜索到的异常注册信息不处理);
ExceptionCollidedUnwind 表示在展开过程中再次触发异常。
ExceptionNestedException 呢?到目前咱们还没有遇到过,什么情况会用到它?
从字面上看 ExceptionNestedException 大概意思是“嵌套的异常”,是不是可以理解为“处理异常过程中再次触发的异常”?比如类似于 ExceptionCollidedUnwind,只不过 ExceptionCollidedUnwind 是在展开过程。而 ExceptionNestedException 是在处理异常过程中?
咱们顺着这个思路去寻找,首先来看 PassThrough!_except_handler4,它是异常处理的入口了。处理和展开都是它负责的。可是翻遍了它和它的工具函数的反汇编码也没有找到它直接返回或者通过注册异常处理信息来间接返回。于是我决定继续向上层即调用者方向搜寻,于是找到了如下汇编码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | [font=Consolas][color=
nt!RtlpExecuteHandlerForException [ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
nt!ExecuteHandler2 [ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
80887bcd [ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][color=
[ /color ][ /font ]
|
RtlpExecuteHandlerForException 函数是在 RtlDispatchException 中被调用。RtlDispatchException 在遍历异常链过程中并不直接调用 EXCEPTION_REGISTRATION_RECORD::Handler,而是通过 RtlpExecuteHandlerForException 来间接调用。RtlpExecuteHandlerForException 通过 ExecuteHandler2 建立一个异常处理块。
PassThrough!_except_handler4 处理异常的过程中,如果再次触发异常,则会由地址为 80887bc2 的异常处理函数返回 ExceptionNestedException。因为这几个函数都很简单,而且跟之前分析的函数很类似,不再赘述。
分析完这些,我有个疑问还是没有解开,我个人一直很奇怪增强版的使用方式为何不能这样:
__try
{
}
__except()
{
}
__finally
{
}
其中 __except 过滤块和处理块可以省略。
但是很遗憾,MSC 中 __except 和 __finally 只能选其一。(当然,咱们可以用双层 __try 来实现同样的效果,但是总感觉不太爽,特别是会导致代码块被迫缩进两次)
在分析的过程中我也发现我希望的这种方式更合理一些,__except 负责处理异常,在处理异常代码中被执行。如果没有处理异常,那么在展开过程中 __finally 代码块被执行,做一些清理操作。这样两者都存在,各负责各的,不是更好吗?不知道是不是什么历史原因。
还有一个地方我也觉得不太完美。我们分析原始版本的时候发现,原始版本自身并没有直接使用展开,RtlDispatchException 等异常处理函数并没有直接调用 RtlUnwind。后者实际上是在增强版中才用到。我个人感觉这种模型并不完美,原始版本并没有自成体系,而是与增强版纠结在一起,没有很好的分层。
另外,在实际应用中 SEH 机制可能会导致很难分析的内存泄漏。我们来看一个例子,
调用流程:func1 -> func2
其中,func1 的代码如下:
VOID Func1()
{
__try
{
Func2();
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
// 一些善后处理
}
}
Func2 没有应用 SEH,它申请了一块内存,对这块内存进行操作,然后释放该内存。但是在操作内存时候触发异常,异常被 Func1 处理了,但是因为 Func2 没有 __finally 处理块,于是展开过程中 Func2 并没有机会去释放这块内存。结果就是:程序依然“正常”的在运行,但是实际上已经造成内存泄漏。随着程序执行,泄漏的资源可能越来越多。最后导致严重的系统故障。
遇到这种问题,程序猿通过静态分析是很难找到泄漏的原因的。
到这里差不多就啰嗦完了。最后,来一段总结。
本文只是我分析 x86 下 windows 异常处理机制过程的一些笔记的集合。因为兴趣的原因,我只分析了内核部分,应用层 SEH 我没有琢磨。感兴趣的朋友可以参考《软件调试》中的相关内容,貌似挺详细的。
后续我会抽时间再琢磨琢磨 x64 下 windows 的异常处理机制,前段时间查阅 x64 资料的时候看到其异常处理机制调整了很多,比如 EXCEPTION_REGISTRATION 不在是放在栈上,而是做成表。如果内部实现改变的较多,我会再写一份笔记来分享。sinister 师傅有过这么一段话,我很认同:
“交流都是建立在平等的基础上,在抱怨氛围和环境不好的同时应该先想一想自己究竟付出了多少?只知索取不愿付出的人也就不用抱怨了,要怪也只能怪自己。发自己心得的人无非是两种目的,一是引发一些讨论,好纠正自己错误的认识,以便从中获取更多的知识使自己进步的更快。二是做一份备忘,当自己遗忘的时候能够马上找到相关资料。”
其中提到的两种目的我都有 :-)
还是那句老话,FIXME。