前言:内核函数 return,并不等于系统调用结束
在上一篇文章中,我们已经跟踪到:
call ebx;ebx=NtOpenProcess这条指令意味着:
系统调用框架代码已经完成了所有“准备工作”,
CPU 正式进入了具体内核服务函数(如 NtOpenProcess)。
但一个非常容易被误解的点是:
内核函数 return ≠ 系统调用结束
事实上,在NtOpenProcess返回之后,
Windows 还要完成一整套“返回前校验 + APC 处理 + TrapFrame 回收 + Ring3 恢复”的流程。
这,正是本文要讲清楚的内容。
本文完整汇编代码如下:
.text:0043E1E8 loc_43E1E8:;CODE XREF:_KiFastCallEntry+4BB↓j.text:0043E1E8 call ebx;直接调用系统服务函数---------------------------.text:0043E1EA.text:0043E1EA loc_43E1EA:;CODE XREF:_KiFastCallEntry+334↓j.text:0043E1EA;DATA XREF:_KiTrap0E+18C↓o.text:0043E1EA test byte ptr[ebp+_KTRAP_FRAME.SegCs],1;判断来自r3还是r0.text:0043E1EE jzshortloc_43E224;如果来自r0,直接跳到“纯内核返回路径.text:0043E1F0 mov esi,eax;备份系统调用函数的返回值.text:0043E1F2 call ds:__imp__KeGetCurrentIrql@0;获取当前IRQL.text:0043E1F8oral,al.text:0043E1FA jnz loc_43E53B;返回r3的IRQL必须是0.text:0043E1FA;如果IRQL不等于0,就跳转蓝屏.text:0043E200mov eax,esi;恢复系统调用返回值.text:0043E202mov ecx,large fs:_KPCR.PrcbData.CurrentThread;ecx=当前正在运行的 KTHREAD.text:0043E209test[ecx+_KTHREAD.ApcStateIndex],0FFh;判断当前线程是否处于挂靠环境.text:0043E210jnz loc_43E559;如果处于挂靠环境,跳转蓝屏.text:0043E216mov edx,dword ptr[ecx+_KTHREAD.___u26.__s0.KernelApcDisable].text:0043E21Coredx,edx;如果KernelApcDisable=1.text:0043E21E jnz loc_43E559;跳转蓝屏.text:0043E224.text:0043E224loc_43E224:;CODE XREF:_KiFastCallEntry+12E↑j.text:0043E224;_KiCallbackReturn+8D↓j.text:0043E224;DATA XREF:....text:0043E224mov esp,ebp.text:0043E226cmp[ebp+_KTRAP_FRAME.Logging],0;记录函数调用后的日志.text:0043E226;检查 TrapFrame.Logging 决定走特殊分支.text:0043E22A jnz loc_43E580.text:0043E230.text:0043E230loc_43E230:;CODE XREF:_KiBBTUnexpectedRange+3C↑j.text:0043E230;_KiBBTUnexpectedRange+47↑j....text:0043E230mov ecx,large fs:_KPCR.PrcbData.CurrentThread.text:0043E237mov edx,[ebp+_KTRAP_FRAME._Edx];-若来自 Ring0:该字段保存的是嵌套调用前的 TrapFrame.text:0043E23A mov[ecx+_KTHREAD.TrapFrame],edx;恢复当前线程的 TrapFrame 指针.text:0043E23A _KiFastCallEntry endp;即:弹出当前系统调用的 TrapFrame.text:0043E23A.text:0043E240.text:0043E240;===============S U B R O U T I N E=======================================.text:0043E240.text:0043E240.text:0043E240_KiServiceExit proc near;CODE XREF:NtContinue(x,x)+43↓j.text:0043E240;NtRaiseException(x,x,x)+38↓j....text:0043E240.text:0043E240arg_C=word ptr10h.text:0043E240arg_10=dword ptr14h.text:0043E240arg_28=dword ptr2Ch.text:0043E240arg_40=dword ptr44h.text:0043E240arg_44=dword ptr48h.text:0043E240arg_48=dword ptr4Ch.text:0043E240arg_60=dword ptr64h.text:0043E240arg_64=dword ptr68h.text:0043E240arg_68=dword ptr6Ch.text:0043E240arg_6C=dword ptr70h.text:0043E240.text:0043E240;FUNCTION CHUNK AT.text:0043E35C SIZE00000089BYTES.text:0043E240.text:0043E240cli;关闭中断,因为要恢复现场.text:0043E241test byte ptr[ebp+(_KTRAP_FRAME.EFlags+2)],2;判断是否8086模式.text:0043E245jnzshortloc_43E24D;如果是虚拟8086模式跳走不执行下面代码.text:0043E247test byte ptr[ebp+_KTRAP_FRAME.SegCs],1;判断来自r0还是r3.text:0043E24B jzshortloc_43E2B4;如果来自r0就跳转.text:0043E24D.text:0043E24D loc_43E24D:;CODE XREF:_KiServiceExit+5↑j.text:0043E24D;_KiServiceExit+6F↓j.text:0043E24D mov ebx,large fs:_KPCR.PrcbData.CurrentThread.text:0043E254test[ebx+_KTHREAD.Header.___u0.__s0.ThreadControlFlags],2.text:0043E258jzshortloc_43E262.text:0043E25A push eax.text:0043E25B push ebx.text:0043E25C call _KiCopyCounters@4;性能统计.text:0043E261pop eax.text:0043E262.text:0043E262loc_43E262:;CODE XREF:_KiServiceExit+18↑j.text:0043E262mov[ebx+_KTHREAD.Alerted],0.text:0043E266cmp[ebx+_KTHREAD.___u12.ApcState.UserApcPending],0.text:0043E26A jzshortloc_43E2B4.text:0043E26C mov ebx,ebp;ebx=trap_frame.text:0043E26E mov[ebx+_KTRAP_FRAME._Eax],eax.text:0043E271mov[ebx+_KTRAP_FRAME.SegFs],3Bh;';'.text:0043E278mov[ebx+_KTRAP_FRAME.SegDs],23h;'#'.text:0043E27Fmov[ebx+_KTRAP_FRAME.SegEs],23h;'#'.text:0043E286mov[ebx+_KTRAP_FRAME.SegGs],0.text:0043E28D mov ecx,1;NewIrql.text:0043E292call ds:__imp_@KfRaiseIrql@4;KfRaiseIrql(x).text:0043E298push eax.text:0043E299sti.text:0043E29A push ebx;trap_frame.text:0043E29B push0;kexcep_frame.text:0043E29D push1;ApcMode.text:0043E29Fcall _KiDeliverApc@12;派发apc.text:0043E2A4 pop ecx;NewIrql.text:0043E2A5 call ds:__imp_@KfLowerIrql@4;KfLowerIrql(x).text:0043E2AB mov eax,[ebx+44h].text:0043E2AE cli.text:0043E2AF jmpshortloc_43E24D;循环执行apc,直到UserApcPending=0.text:0043E2AF;---------------------------------------------------------------------------.text:0043E2B1 align4.text:0043E2B4.text:0043E2B4 loc_43E2B4:;CODE XREF:_KiServiceExit+B↑j.text:0043E2B4;_KiServiceExit+2A↑j.text:0043E2B4 mov edx,[esp+_KTRAP_FRAME.ExceptionList];在.text:0043E224有一条esp=ebp的命令所以,.text:0043E2B4;所以此处esp是trap_frame.text:0043E2B8 mov large fs:_KPCR,edx;还原现场.text:0043E2BF mov ecx,[esp+_KTRAP_FRAME.PreviousPreviousMode].text:0043E2C3 mov esi,large fs:_KPCR.PrcbData.CurrentThread.text:0043E2CA mov[esi+_KTHREAD.PreviousMode],cl;KTHREAD.PreviousMode=_KTRAP_FRAME.PreviousPreviousMode.text:0043E2D0 test[esp+_KTRAP_FRAME.Dr7],0FFFF23FFh.text:0043E2D8 jnz loc_43E35C.text:0043E2DE.text:0043E2DE loc_43E2DE:;CODE XREF:_KiServiceExit+12C↓j.text:0043E2DE;_KiServiceExit+15B↓j.text:0043E2DE test[esp+_KTRAP_FRAME.EFlags],20000h;判断是不是虚拟8086模式.text:0043E2E6 jnz loc_43ED2C;是虚拟8086模式就跳转.text:0043E2EC test word ptr[esp+_KTRAP_FRAME.SegCs],0FFF8h.text:0043E2F3 jz loc_43E3B2.text:0043E2F9 cmp word ptr[esp+_KTRAP_FRAME.SegCs],1Bh;判断cs是不是r3的.text:0043E2FFbt word ptr[esp+_KTRAP_FRAME.SegCs],0.text:0043E306cmc.text:0043E307ja loc_43E3A0;是r3的就跳.text:0043E30D cmp word ptr[ebp+_KTRAP_FRAME.SegCs],8.text:0043E312jzshortloc_43E319;如果是r3就跳转.text:0043E314.text:0043E314loc_43E314:;CODE XREF:_KiServiceExit+16D↓j.text:0043E314lea esp,[ebp+_KTRAP_FRAME.SegFs];让esp指向_KTRAP_FRAME.SegFs.text:0043E317pop fs.text:0043E319assume fs:nothing.text:0043E319.text:0043E319loc_43E319:;CODE XREF:_KiServiceExit+D2↑j.text:0043E319lea esp,[ebp+_KTRAP_FRAME._Edi];让esp指向_KTRAP_FRAME._Edi的位置.text:0043E31C pop edi;还原寄存器.text:0043E31D pop esi.text:0043E31E pop ebx.text:0043E31Fpop ebp.text:0043E320cmp word ptr[esp+8],80h;'€';esp+8的位置是_KTRAP_FRAME.SegCs.text:0043E327ja loc_43ED48;判断cs是不是8086模式的段,如果是就跳转.text:0043E32D add esp,4;此时esp指向_KTRAP_FRAME.Eip.text:0043E330test dword ptr[esp+4],1;再次判断_KTRAP_FRAME.SegCs是不是r3的段.text:0043E330_KiServiceExit endp;sp-analysis failed.text:0043E330.text:0043E338.text:0043E338_KiSystemCallExitBranch:;DATA XREF:KiRestoreFastSyscallReturnState():loc_412819↑r.text:0043E338;KiRestoreFastSyscallReturnState()+7D↑w....text:0043E338jnzshort_KiSystemCallExit;如果是r3跳转.text:0043E33A pop edx.text:0043E33B pop ecx.text:0043E33C popf.text:0043E33D jmp edx.text:0043E33F;---------------------------------------------------------------------------.text:0043E33F;START OF FUNCTION CHUNK FOR _KiSystemCallExit2.text:0043E33F.text:0043E33F_KiSystemCallExit:;CODE XREF:.text:_KiSystemCallExitBranch↑j.text:0043E33F;_KiSystemCallExit2+8↓j.text:0043E33F;DATA XREF:....text:0043E33Firet一、call ebx 之后:系统调用的“返回检查阶段”
.text:0043E1E8 call ebx从这一刻开始:
EAX = 系统调用返回值(NTSTATUS)
当前线程仍处于:
Ring0
使用当前线程的内核栈
KTHREAD.TrapFrame 有效
中断已开启(sti 已执行)
但 Windows 还不能立刻返回用户态。
原因只有一个:
返回 Ring3 是一件“高风险操作”,必须满足一系列严格条件。
1.1 判断这次系统调用来自 Ring3 还是 Ring0
.text:0043E1EA.text:0043E1EA test byte ptr[ebp+_KTRAP_FRAME.SegCs],1.text:0043E1EE jzshortloc_43E224判断依据:
_KTRAP_FRAME.SegCs 的 RPL 位
bit0 = 1 → 来自 Ring3
bit0 = 0 → 来自 Ring0
如果 来自 Ring0:
不涉及用户态返回,
不需要 IRQL / APC / 上下文安全检查,
直接走“纯内核返回路径”。
1.2 返回 Ring3 前,必须满足的三个硬条件
如果这次系统调用来自 Ring3,代码继续执行:
.text:0043E1F0 mov esi,eax;备份系统调用函数的返回值.text:0043E1F2 call ds:__imp__KeGetCurrentIrql@0;获取当前IRQL.text:0043E1F8oral,al.text:0043E1FA jnz loc_43E53B;返回r3的IRQL必须是0.text:0043E1FA;如果IRQL不等于0,就跳转蓝屏.text:0043E200mov eax,esi;恢复系统调用返回值条件一:IRQL 必须为 PASSIVE_LEVEL
返回用户态时,IRQL ≠ 0 是绝对非法的。
原因很简单:
用户态代码允许:
页面错误
APC
抢占
高 IRQL 下返回用户态 = 系统不再可控
因此:
只要 IRQL ≠ 0,立刻蓝屏。
.text:0043E202mov ecx,large fs:_KPCR.PrcbData.CurrentThread;ecx=当前正在运行的 KTHREAD.text:0043E209test[ecx+_KTHREAD.ApcStateIndex],0FFh;判断当前线程是否处于挂靠环境.text:0043E210jnz loc_43E559;如果处于挂靠环境,跳转蓝屏条件二:线程不能处于 Attached APC 环境
如果线程:
通过 KeStackAttachProcess
切换到了其他进程的地址空间
那么:
当前 TrapFrame 对应的用户态上下文已经不再安全。
这种状态下返回 Ring3,属于严重内核错误直接蓝屏
.text:0043E216mov edx,dword ptr[ecx+_KTHREAD.___u26.__s0.KernelApcDisable].text:0043E21Coredx,edx;如果KernelApcDisable=1.text:0043E21E jnz loc_43E559;跳转蓝屏条件三:Kernel APC 必须是可投递状态否则
如果:
KernelApcDisable ≠ 0
说明线程当前不允许 APC
那么:
系统调用不能返回用户态
否则 APC 语义将被破坏,导致直接蓝屏
二、回收 TrapFrame,准备进入 KiServiceExit
在完成系统调用函数(如 NtOpenProcess)的执行之后,
执行流并不会立刻返回用户态。
相反,Windows 内核必须先完成一项非常关键的工作:
将当前线程的 TrapFrame 状态,恢复为“进入 sysenter 之前”的状态。
这一步,发生在下面这几条指令中。
2.1 执行流回到 TrapFrame 基址
.text:0043E224mov esp,ebp这条指令意味着:
当前执行流不再使用普通内核栈
ESP 被强制指向 当前系统调用构造的 KTRAP_FRAME
从这一刻开始:
接下来的所有操作,都是围绕 TrapFrame 的“回收与恢复”。
三、KiServiceExit:真正的“系统调用出口”
接下来执行的函数是:
_KiServiceExit这是 Windows 中:
所有系统调用共享的“统一返回路径”
3.1 为什么一开始就 cli?
.text:0043E240cli;关闭中断,因为要恢复现场原因非常明确:
恢复现场 ≠ 可中断操作
从这一刻开始:
内核即将:
恢复段寄存器
恢复通用寄存器
恢复 EFLAGS
恢复 CS:EIP / SS:ESP
任何中断、异常:
都会直接破坏返回现场
3.2 User APC 的最后投递机会
.text:0043E266cmp[ebx+_KTHREAD.___u12.ApcState.UserApcPending],0.text:0043E26A jzshortloc_43E2B4如果存在 User APC:
当前线程即将返回 Ring3
这是 最后一次合法投递 APC 的时机
于是内核:
.text:0043E29Fcall KiDeliverApc这一步极其重要:
User APC 的执行点,严格发生在“系统调用返回用户态之前”。
四、真正的现场恢复:KTRAP_FRAME 的“反向消费”
接下来的代码,是对 TrapFrame 的逐字段反向恢复。
4.1 恢复 PreviousMode / 调试寄存器
.text:0043E2BF mov ecx,[esp+_KTRAP_FRAME.PreviousPreviousMode].text:0043E2C3 mov esi,large fs:_KPCR.PrcbData.CurrentThread.text:0043E2CA mov[esi+_KTHREAD.PreviousMode],cl;KTHREAD.PreviousMode=_KTRAP_FRAME.PreviousPreviousMode含义:
线程重新回到 UserMode
内核不再以“内核信任模型”对待该线程
4.2 判断返回路径:iret 还是 fast path
.text:0043E30D cmp word ptr[ebp+SegCs],8Windows 在这里区分:
返回 Ring0 → Ring0
返回 Ring0 → Ring3
是否虚拟 8086 模式
是否需要 iret
4.3 恢复通用寄存器
.text:0043E319pop edi.text:0043E31D pop esi.text:0043E31E pop ebx.text:0043E31Fpop ebp.text:0043E320cmp word ptr[esp+8],80h;'€';esp+8的位置是_KTRAP_FRAME.SegCs.text:0043E327ja loc_43ED48;判断cs是不是8086模式的段,如果是就跳转.text:0043E32D add esp,4;此时esp指向_KTRAP_FRAME.Eip.text:0043E330test dword ptr[esp+4],1;再次判断_KTRAP_FRAME.SegCs是不是r3的段.text:0043E338jnzshort_KiSystemCallExit;如果是r3跳转KTRAP_FRAME 中保存的“被打断前寄存器”,已全部回到 CPU
我们还需要特别关注的是此时ESP指向_KTRAP_FRAME.Eip处
五、最终返回:为什么这里必须是 iret?
_KiSystemCallExit:iret原因只有一个:
sysenter 没有返回指令
而 iret 能一次性恢复:
EIP
CS
EFLAGS
ESP
SS
这正是:KTRAP_FRAME 从一开始就必须完整保存这些字段的原因。
六、结语
至此,我们已经完整走完:
Ring3 ↓ sysenter KiFastCallEntry ↓ 构造 KTRAP_FRAME SSDT 查表 ↓ NtOpenProcess ↓returnKiServiceExit ↓ iret Ring3也就是说:
系统调用真正的“入口”和“出口”,都发生在 Ring0。
而 KTRAP_FRAME:
既是进入 Ring0 的“凭证”
也是返回 Ring3 的“钥匙”