上篇我们介绍了windows再x86的CPU架构下,使用sysenter指令,进入内核前后的过程,当通过填充KTRAP_FRAME后,保存好用户层的上下文环境(文章地址:https://daliu.net/posts/20250105/),今天通过这篇来介绍后续的调用过程。
我们接着上篇文章介绍的汇编代码,接着向下继续阅读,接下来总该进入系统函数执行的过程了吧,毕竟,进入内核主要要干的事情就是这个呀,首先把后续的汇编代码粘到下面:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
.text:00435449 mov edi, eax
.text:0043544B shr edi, 8
.text:0043544E and edi, 10h
.text:00435451 mov ecx, edi
.text:00435453 add edi, [esi+0BCh]
.text:00435459 mov ebx, eax
.text:0043545B and eax, 0FFFh
.text:00435460 cmp eax, [edi+8]
.text:00435463 jnb _KiEndUnexpectedRange@0 ; KiEndUnexpectedRange()
.text:00435469 cmp ecx, 10h
.text:0043546C jnz short loc_435488
.text:0043546E mov ecx, [esi+88h]
.text:00435474 xor esi, esi
|
第一句提到的eax寄存器,从上文分析可以知道,这个eax至今还没有出现过,他是从用户层传入的,具体可以定位到下面这个图片。
1
|
.text:00435449 mov edi, eax
|
在进入KiFastSystemCall函数前,传入给eax的参数是一个固定的数,而且,在这篇区域,有很多个重复的内容,区别就是每一个调用函数对应传给eax的固定的数是不同的,借此我们可以分析,每一个系统函数对应的一个固定的编号,想要调用哪个就需要给KiFastSystemCall函数传入对应的编号。

那么操作系统一定维护了一个这样的表,描述了函数的信息和编号信息的对应关系。
为了方便,可以把刚刚的汇编部分的代码在IDA中用昨天说过的方式引入结构体,看起来会更方便

接着看后面的代码:
1
2
3
4
5
6
7
8
|
// edi向右移动8位,然后又与上10,如果这个编号是小于256的数,此刻这个值就是0了,
// 但如果是大于0x1000的数(两个字节),经过位运算,这个数一定是0x10
.text:0043544B shr edi, 8
.text:0043544E and edi, 10h
// 将这个值又覆给了ecx,然后又把esi一个成员,结合上文这个是线程块结构体中的系统服务表地址
.text:00435451 mov ecx, edi
.text:00435453 add edi, [esi+_ETHREAD.Tcb.ServiceTable]
|
继续跟下去:
1
2
3
4
5
|
// 继续比较服务号,与0xfff也就是获得索引,然后跟edi+8比较,如果超过了就报错。
.text:00435459 mov ebx, eax
.text:0043545B and eax, 0FFFh
.text:00435460 cmp eax, [edi+8]
.text:00435463 jnb _KiEndUnexpectedRange@0 ; KiEndUnexpectedRange()
|
在这里要简单介绍下,这个系统服务表。从上文汇编代码可以看到,从线程块中可以取到系统服务表的地址,这个服务表不是我们前面提到的序号和函数的对应关系表,这个位置指向的结构体是用来描述对应关系的
这个结构体描述有两种,一种是非UI的函数所对应的描述表,一种是UI相关函数的描述表(比如MessageBox等),非UI的叫做KeServiceDescriptorTable,UI的叫做KeServiceDescriptorTableShadow。
具体的结构如下:
1
2
3
4
5
|
其中有四个成员,每个成员的大小是4个字节,也就是ULONG:
第一:内核服务函数地址表【四个字节】
第二:统计API调用次数(其实也不怎么用了,应该算是废弃的感觉)
第三:内核服务函数地址表的函数数量
第四:内核函数参数表【一个字节Byte数组,参数大小】
|
然后我们再通过windbg查看下实际上面的数据情况:
1
2
3
4
5
6
7
8
9
|
kd> dd keservicedescriptortable
83f77b00 83e6de3c 00000000 00000191 83e6e484
83f77b10 00000000 00000000 00000000 00000000
// 不跟UI相关,eax< 0x1000
kd> dd keservicedescriptortableshadow
83f77b40 83e6de3c 00000000 00000191 83e6e484
83f77b50 9b2d6000 00000000 00000339 9b2d702c
// 跟UI相关,eax > 0x1000
|
可以看到有一些区别,第一行都是一样的,但是后面第二行算是另一个KeServiceDescriptorTable表,某种意义说这是一个结构体的数组,同样数据的不同也是用来区分是UI还是非UI的表,其次,服务号的大小也是,服务号小于0x1000的都是非UI,也就是KeServiceDescriptorTable这个表,而大于的就都是KeServiceDescriptorTableShadow这个表的。
除此,还要说明一点,KeServiceDescriptorTable这个表在32位系统里是导出的,但是在64位系统就不是导出的了。
所以我们书接上文,这句比较,其实就是将当前用户层想要调用的服务编号跟当前服务表里的函数个数进行比较,判断是否越界,如果大于就会跳转。
1
|
.text:00435460 cmp eax, [edi+8]
|
我们先简单向跳转之后的内容看一下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
.text:00434E30 cmp ecx, 10h
.text:00434E33 jnz short loc_434E72
.text:00434E35 push edx
.text:00434E36 push ebx
.text:00434E37 call _PsConvertToGuiThread@0 ; PsConvertToGuiThread()
.text:00434E3C or eax, eax
.text:00434E3E pop eax
.text:00434E3F pop edx
.text:00434E40 mov ebp, esp
.text:00434E42 mov [esi+128h], ebp
.text:00434E48 jz loc_435449
.text:00434E4E lea edx, unk_564AD0
.text:00434E54 mov ecx, [edx+8]
.text:00434E57 mov edx, [edx]
.text:00434E59 and eax, 0FFFh
.text:00434E5E cmp eax, ecx
.text:00434E60 lea edx, [edx+ecx*4]
.text:00434E63 jnb short loc_434E72
.text:00434E65 add edx, eax
.text:00434E67 movsx eax, byte ptr [edx]
.text:00434E6A or eax, eax
.text:00434E6C jle loc_435536
|
首先与0x10比较,因为上文我们了解,如果是一个大于0x1000的数(两个字节大小哈),这个ecx一定是0x10,如果等于0x10,结果是为0,会向下走。转到_PsConvertToGuiThread函数,推测切换成UI相关的线程。
1
2
3
4
5
|
.text:00434E30 cmp ecx, 10h
.text:00434E33 jnz short loc_434E72
.text:00434E35 push edx
.text:00434E36 push ebx
.text:00434E37 call _PsConvertToGuiThread@0 ; PsConvertToGuiThread()
|
好的我们再回到跳转前接着向下看:
1
2
3
4
5
6
|
// 跟0x10比较,判断非UI,也就是不等,然后跳转
.text:00435469 cmp ecx, 10h
.text:0043546C jnz short loc_435488
.text:0043546E mov ecx, [esi+_ETHREAD.Tcb.Teb]
.text:00435474 xor esi, esi
|
我们来看非UI跳转后面的处理步骤:
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
|
// 系统调用次数 自增1
.text:00435488 inc large dword ptr fs:_KPCR.PrcbData.KeSystemCalls
//根据上篇文章的追踪,edx是用户层传入的esp + 8的值,也就是用户层传入的参数的地址
.text:0043548F mov esi, edx
// 清空ecx
.text:00435491 xor ecx, ecx
// edi存的是服务表地址,+c 就是服务函数的参数表
.text:00435493 mov edx, [edi+0Ch]
// edi存储服务函数的函数表地址
.text:00435496 mov edi, [edi]
// eax是编号,也就是函数的索引号
// edx是参数表的起始地址,表中的每个成员是1个字节,用来表示参数的大小
// 所以edx+eax,就是参数大小的值所在的地址
.text:00435498 mov cl, [eax+edx]
// edi是函数表其实位置,eax * 4,也就是索引乘以4个字节
// edi + eax * 4 就是函数的地址(函数表每4个字节一个地址,对应的是函数地址)
.text:0043549B mov edx, [edi+eax*4]
// ecx,也就是上面赋值的cl,是参数的大小
// 所以需要将栈顶降低cl大小以方便填入参数
.text:0043549E sub esp, ecx
// 右移2表示除以4,也就是要参数的个数或者是说,需要栈给多少个4字节。
.text:004354A0 shr ecx, 2
.text:004354A3 mov edi, esp
// ebp存储的是KTRAP_FRAME,+ 0x72是eflags中的某一位
// 查阅资料是VM:
// 虚拟8086模式标志(Virtual 8086 mode flag)是标志寄存器的第17位,
// 当其被设置表示启用虚拟8086模式(在保护模式下模拟实模式),否则退回到保护模式工作。
// 仅供参考哈
.text:004354A5 test byte ptr [ebp+72h], 2
.text:004354A9 jnz short loc_4354B1
// 查看SegCs,也就是上一阶段的代码段,是用户层还是内核,1为用户,0为内核
.text:004354AB test byte ptr [ebp+6Ch], 1
// 如果为0才跳转
.text:004354AF jz short _KiSystemServiceCopyArguments@0 ; KiSystemServiceCopyArguments()
|
以上 jnz short loc_4354B1的位置就是这段代码之后的位置,所以接着向下看:
1
2
|
.text:004354B1 cmp esi, ds:_MmUserProbeAddress
.text:004354B7 jnb loc_43572E
|
以上_MmUserProbeAddress到底值什么可以在windbg中查看下:
1
2
3
|
kd> dd MmUserProbeAddress
83f77854 7fff0000 80741000 0003ffff 80000000
83f77864 85500e2c 85500e24 85500e00 c03fff78
|
发现该值是7fff0000,我们借此可以推断,esi存放的是用户层传入参数的地址,然后跟MmUserProbeAddress进行比较,应该是判断是不是一个合理的用户空间的地址。
如果比这个地址大的话,就会跳转到下图所示的C0000005h,也就是触发异常了

继续向后看:这里就能看到就是在批量拷贝了。
1
2
3
4
5
6
7
8
9
10
11
|
// 这就是选循环,从esi为起始(源参数地址),以edi(栈顶)为目标
// 以ecx为循环次数,开始复制,地址为递增方式(也就是从栈顶到栈底的方向)
.text:004354BD rep movsd
.text:004354BF test byte ptr [ebp+6Ch], 1
.text:004354C3 jz short loc_4354DB
.text:004354C5 mov ecx, large fs:124h
.text:004354CC mov edi, [esp+0]
.text:004354CF mov [ecx+13Ch], ebx
.text:004354D5 mov [ecx+12Ch], edi
|
这段代码用IDA添加结构体的方式来展示:就比较轻松的知道做了什么

继续向下看:
1
2
3
4
5
6
7
8
9
|
// 把函数地址给ebx,然后再后面调用了ebx
.text:004354DB mov ebx, edx
// 这部分是日志相关,就不做展开了
.text:004354DD test byte ptr ds:dword_531FC8, 40h
.text:004354E4 setnz [ebp+_KTRAP_FRAME.Logging]
.text:004354E8 jnz loc_4358F4
.text:004354EE call ebx
|
首先用结构体方式把代码重新展示下,此法屡试不爽
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
.text:004354F0 test byte ptr [ebp+_KTRAP_FRAME.SegCs], 1
.text:004354F4 jz short loc_43552A
.text:004354F6 mov esi, eax
.text:004354F8 call ds:__imp__KeGetCurrentIrql@0 ; KeGetCurrentIrql()
.text:004354FE or al, al
.text:00435500 jnz loc_4358BB
.text:00435506 mov eax, esi
.text:00435508 mov ecx, large fs:_KPCR.PrcbData.CurrentThread
.text:0043550F test [ecx+_ETHREAD.Tcb.ApcStateIndex], 0FFh
.text:00435516 jnz loc_4358D9
.text:0043551C mov edx, dword ptr [ecx+_ETHREAD.Tcb.___u26.__s0.KernelApcDisable]
.text:00435522 or edx, edx
.text:00435524 jnz loc_4358D9
.text:0043552A mov esp, ebp
.text:0043552C cmp [ebp+_KTRAP_FRAME.Logging], 0
.text:00435530 jnz loc_435900
.text:00435536 mov ecx, large fs:_KPCR.PrcbData.CurrentThread
.text:0043553D mov edx, [ebp+_KTRAP_FRAME._Edx]
.text:00435540 mov [ecx+_ETHREAD.Tcb.TrapFrame], edx
|
1
2
3
|
// 依然是判断当前调用是从用户层还是内核进入的
.text:004354F0 test byte ptr [ebp+_KTRAP_FRAME.SegCs], 1
.text:004354F4 jz short loc_43552A
|
上述代码在整个过程中,这个判断出现多次,为什么会出现这样,事实是因为,通过KiFastCallEntry调用,不止用户层,内核层也会调用。
1
|
.text:004354F0 test byte ptr [ebp+_KTRAP_FRAME.SegCs], 1
|
但是为什么内核层也会通过这个函数调用,按理说,内核层直接就可以去调用函数本身就可以了,为什么还要通过这个函数来调用呢?
在用户层有两套除了名字外一模一样的函数,ZwXXX系列和NtXXX系列,这两个函数区别只是导出的名称不一样,其他都是一样的。但是在内核层,这两个函数却长得完成不一样,事实上,在内核的ZwXXX系列函数,跟踪下来会发现,他会跳转到KiFastCallEntry函数中的一个位置,这个位置刚刚好是保存完用户层的上线文代码之后的位置,也就是,今天开篇开始的代码。
故此,内核里的Zw函数也是通过系统调用,找到最终要调用的Nt函数,这样看来,干活的还是内核里的Nt函数,这也就说明了,为什么要判断是用户层还是内核层跳转进来的。
假设是从用户层,那就不跳转:
1
2
3
4
5
6
|
// eax是调用完系统函数之后的结果,做一个保存
.text:004354F6 mov esi, eax
// 又调用了一个函数
.text:004354F8 call ds:__imp__KeGetCurrentIrql@0 ; KeGetCurrentIrql()
.text:004354FE or al, al
|
IRQL,是中断请求级别的概念,更详细的概念可以看这个链接:https://learn.microsoft.com/zh-cn/windows-hardware/drivers/devtest/irql-annotations-for-drivers
0-31表示irql级别,其中数字越大等级越高,优先级越高
PASSIVE_LEVEL,是0,基本上用户层都是这个
APC_LEVEL,是1,
DISPATCH_LEVEL ,是2
中断,是在当前运行的CPU状态下,中断然后做其他事情,而且中断是可以嵌套的,
也就是当一个中端发生,保存状态,然后保存返回位置,进入中断位置,执行中断任务,
中断过程中又被中断,然后接着保存状态,保存返回位置,进入中断,执行中断任务,
。。。。依次类推。
但是如果可以被任意打断,会造成一直在忙于切换而无法完成任务,因而,中断也是有级别的,任何请求中断的级别要高于你当前处在中断任务的中断级别才能去打断该中断或任务,憋得不行一定要上厕所,是很难被旁边要给你讲笑话的人打断的。
1
2
3
4
|
// 获取当前的IRQL,并判断是否为0,如果不为0跳转
.text:004354F8 call ds:__imp__KeGetCurrentIrql@0 ; KeGetCurrentIrql()
.text:004354FE or al, al
.text:00435500 jnz loc_4358BB
|
顺手看一下跳转之后的内容,其实就是kebugcheck函数,记住这个函数,它就是大名鼎鼎的蓝屏函数

1
2
3
4
5
6
7
|
// 这部分是apc相关,进程挂靠的内容,跳转还是蓝屏函数
.text:00435508 mov ecx, large fs:_KPCR.PrcbData.CurrentThread
.text:0043550F test [ecx+_ETHREAD.Tcb.ApcStateIndex], 0FFh
.text:00435516 jnz loc_4358D9
.text:0043551C mov edx, dword ptr [ecx+_ETHREAD.Tcb.___u26.__s0.KernelApcDisable]
.text:00435522 or edx, edx
.text:00435524 jnz loc_4358D9
|
1
2
3
4
5
6
7
8
9
10
11
|
// 判断为当前日志是否开启,如果开启就跳转,然后后面应该就是要关闭这个日志
// 差不多意思就是调用要完成了,日志可以关闭了
.text:0043552A mov esp, ebp
.text:0043552C cmp [ebp+_KTRAP_FRAME.Logging], 0
.text:00435530 jnz loc_435900
.text:00435536 mov ecx, large fs:_KPCR.PrcbData.CurrentThread
// edx存储的是用户层的参数
.text:0043553D mov edx, [ebp+_KTRAP_FRAME._Edx]
.text:00435540 mov [ecx+_ETHREAD.Tcb.TrapFrame], edx
|
上文最后两行,捣鼓的这下令人不是很理解,事实上这部分是给内核层调用准备的,如果是用户层应该用不到,虽然执行了也没什么意义。可以看下在内核层调用的过程中做的事情:

上图可以看到,在内核层开始调用前,就是把线程块的KTRAP_FRAME保存在KTRAP_FRAME的edx里,而执行完,又把它还原给线程块里。
继续向后走:
1
2
3
4
5
6
7
8
9
10
11
|
// 关中断
.text:00435546 cli
// 跟之前一样,还是判断是否为虚拟8086模式,依然不是
.text:00435547 test byte ptr [ebp+(_KTRAP_FRAME.EFlags+2)], 2
.text:0043554B jnz short loc_435553
// 依然是判断是否为用户层还是内核层过来的
.text:0043554D test byte ptr [ebp+_KTRAP_FRAME.SegCs], 1
.text:00435551 jz short loc_4355B8
// 姑且跳转,继续向下
|
后面是一段跟计数统计相关以及跟apc相关,这篇文章姑且不详细描述。
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
|
.text:00435553 ; _KiServiceExit+6F↓j
.text:00435553 mov ebx, large fs:_KPCR.PrcbData.CurrentThread
.text:0043555A test byte ptr [ebx+2], 2
.text:0043555E jz short loc_435568
.text:00435560 push eax
.text:00435561 push ebx
.text:00435562 call _KiCopyCounters@4 ; KiCopyCounters(x)
.text:00435567 pop eax
.text:00435568 mov byte ptr [ebx+3Ah], 0
.text:0043556C cmp byte ptr [ebx+56h], 0
.text:00435570 jz short loc_4355B8
.text:00435572 mov ebx, ebp
.text:00435574 mov [ebx+44h], eax
.text:00435577 mov dword ptr [ebx+50h], 3Bh ; ';'
.text:0043557E mov dword ptr [ebx+38h], 23h ; '#'
.text:00435585 mov dword ptr [ebx+34h], 23h ; '#'
.text:0043558C mov dword ptr [ebx+30h], 0
.text:00435593 mov ecx, 1 ; NewIrql
.text:00435598 call ds:__imp_@KfRaiseIrql@4 ; KfRaiseIrql(x)
.text:0043559E push eax
.text:0043559F sti
.text:004355A0 push ebx
.text:004355A1 push 0
.text:004355A3 push 1
.text:004355A5 call _KiDeliverApc@12 ; KiDeliverApc(x,x,x)
.text:004355AA pop ecx ; NewIrql
.text:004355AB call ds:__imp_@KfLowerIrql@4 ; KfLowerIrql(x)
.text:004355B1 mov eax, [ebx+44h]
.text:004355B4 cli
.text:004355B5 jmp short loc_435553
|
最后到了接下来这段代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
// 恢复异常链地址
.text:004355B8 mov edx, [esp+_KTRAP_FRAME.ExceptionList]
.text:004355BC mov large fs:_KPCR.___u0.NtTib.ExceptionList, edx
// 恢复线程的先前模式
.text:004355C3 mov ecx, [esp+_KTRAP_FRAME.PreviousPreviousMode]
.text:004355C7 mov esi, large fs:_KPCR.PrcbData.CurrentThread
.text:004355CE mov [esi+_ETHREAD.Tcb.PreviousMode], cl
//dr7,调试寄存器,判断是否有硬件端点
.text:004355D4 test [esp+_KTRAP_FRAME.Dr7], 0FFFF23FFh
.text:004355DC jnz loc_4356A0
// 跳转过去的就是恢复dr寄存器
|
继续向下,依然用结构体替换内容展示:
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
|
// 依然判断是否为虚拟8086模式
.text:004355E2 test [esp+_KTRAP_FRAME.EFlags], 20000h
.text:004355EA jnz loc_4367A8
//用户层是1B,与之后是19,内核层是08,与之后是8,都不跳转
.text:004355F0 test word ptr [esp+_KTRAP_FRAME.SegCs], 0FFF9h
.text:004355F7 jz loc_4356FB
// 用户层是1B,内核层是08,比较
.text:004355FD cmp word ptr [esp+_KTRAP_FRAME.SegCs], 1Bh
// bt指令是 bit test,位测试,测试某位
// bt 操作数1,操作数2
// 将操作数1的第n位的值给CF标志位,n是操作数2
.text:00435603 bt word ptr [esp+_KTRAP_FRAME.SegCs], 0
// cmc指令,将CF标志位取反
.text:0043560A cmc
// 用户层CF位最后为0,内核层位1,CF标志位和ZF标志位都为0跳转
// 用户层:cmp之后,ZF为1,cmc之后CF为0 不跳转 内核层也是一样不跳转
.text:0043560B ja loc_4356E4
.text:00435611 mov [esp+_KTRAP_FRAME._Eax], eax
.text:00435615 test byte ptr [ebp+_KTRAP_FRAME.SegCs], 1
.text:00435619 jz short loc_435655
.text:0043561B movzx eax, large byte ptr fs:_KPCR.PrcbData.BpbUserSpecCtrl
.text:00435623 cmp large fs:_KPCR.PrcbData.BpbCurrentSpecCtrl, al
.text:0043562A jz short loc_43563B
.text:0043562C mov large fs:_KPCR.PrcbData.BpbCurrentSpecCtrl, al
.text:00435632 mov ecx, 48h ; 'H'
.text:00435637 xor edx, edx
.text:00435639 wrmsr
|
接着经过一些其他的判断(感觉对当前的分析都没有什么意义可以忽略掉),看下面的代码
1
2
3
4
5
6
7
8
9
|
.text:00435655 mov eax, [esp+_KTRAP_FRAME._Eax]
//内核层会直接跳转到下面
.text:00435659 cmp word ptr [ebp+_KTRAP_FRAME.SegCs], 8
.text:0043565E jz short loc_435665
// 恢复fs寄存器了
.text:00435660 lea esp, [ebp+_KTRAP_FRAME.SegFs]
.text:00435663 pop fs
|
1
2
3
4
5
6
7
8
9
10
|
// 然后就是恢复各种寄存器的值
.text:00435665 lea esp, [ebp+_KTRAP_FRAME._Edi]
.text:00435668 pop edi
.text:00435669 pop esi
.text:0043566A pop ebx
.text:0043566B pop ebp
// 这个其实还是在判断是否为用户层还是内核层
.text:0043566C add esp, 4
.text:0043566F test dword ptr [esp+4], 1
|
在IDA里,最后这部分代码有问题:

我们用windbg来看着这部分是做了什么,通过搜索"pop fs"来搜索相似的代码,最终就找到图如下图所示的位置,以下才是实际的代码内容

首先判断是否为用户层,用户层跳转。
1
2
|
83e42c0c f744240401000000 test dword ptr [esp+4],1
83e42c14 7505 jne nt!KiSystemCallExit2 (83e42c1b) Branch
|
然后判断eflags的第9位是否为1,为1跳转,
1
2
3
|
nt!KiSystemCallExit2:
83e42c1b f744240800010000 test dword ptr [esp+8],100h
83e42c23 751e jne nt!KiSystemCallExit (83e42c43) Branch
|
这个是eflags的TF标志位在 EFLAGS 寄存器中,TF(Trap Flag)是陷阱标志位,用于控制CPU的单步执行模式。当 TF 被设置为1时,CPU将进入单步执行模式,即每执行一条指令后,都会产生一个单步中断请求。这种方式主要用于程序的调试,可以比对下,用户层和内核的值:
内核层:
1
2
3
4
|
kd> r
eax=00000002 ebx=00000000 ecx=83f72b07 edx=00000002 esi=8fcc7800 edi=99b18298
eip=83e6a1bc esp=83f37b48 ebp=83f37b88 iopl=0 nv up ei pl nz na po nc
cs=0008 ss=0010 ds=0023 es=0023 fs=0030 gs=0000 efl=00000202
|
用户层:

可见,用户层为1,会跳转。
然后接着下面的分支:
1
2
3
4
5
6
|
nt!KiSystemCallExit:
83e42c43 33c9 xor ecx,ecx
83e42c45 33d2 xor edx,edx
// 跳转返回了。
83e42c6b cf iretd
|
这里简单说下这个iretd指令:
这个指令跟 iret一样,在32位下没有区别,这个指令执行的时候会从堆栈依次弹出三个值,分别赋给EIP(当前指令的地址)、cs(代码段选择子)、eflags(标志寄存器),在上文情境中,esp已经指向的就是KTRAP_FRAME中的EIP地址,所以当iretd执行的时候,正好就把初次进入这个函数的时候,保存在KTRAP_FRAME中用用户层的对应值又重新赋给了这三个寄存器
并且,返回地址EIP刚好值_KUSER_SHARED_DATA 中的SystemCallReturn的地址,也就是用户层sysenter指令之后的位置,这在上一篇中有提到过(地址如下:https://daliu.net/posts/20250105/#_kuser_shared_data)。所以,iretd这个指令确保了执行过程的顺畅。
至此,系统调用的内核阶段完成,并成功将结果返回。以上内容都是在x86架构下的调用流程,在64位的操作系统中还会有一些区别,我们后续继续补充介绍来,今天就到此为止哈。