APC异步过程调用

85 篇文章 6 订阅
83 篇文章 9 订阅

线程是不能被“杀掉”、“挂起”、“恢复”的,线程在执行的时候自己占据着CPU,别人怎么可能控制它呢?
举个极端的例子:如果不调用API,屏蔽中断,并保证代码不出现异常,线程将永久占用CPU,何谈控制呢?所以说线程如果想“死”,一定是自己执行代码把自己杀死,不存在“他杀”这种情况!
那如果想改变一个线程的行为该怎么办呢?
可以给他提供一个函数,让它自己去调用,这个函数就是APC(Asyncroneus Procedure Call),即异步过程调用。
APC队列

kd> dt _KTHREAD
nt!_KTHREAD
    ...
       +0x034 ApcState         : _KAPC_STATE
    ...

kd> dt _KAPC_STATE
nt!_KAPC_STATE
   +0x000 ApcListHead   //2个APC队列 用户APC和内核APC 
   +0x010 Process       //线程所属或者所挂靠的进程
   +0x014 KernelApcInProgress   //内核APC是否正在执行
   +0x015 KernelApcPending  //是否有正在等待执行的内核APC
   +0x016 UserApcPending    //是否有正在等待执行的用户APC

用户APC:APC函数地址位于用户空间,在用户空间执行.
内核APC:APC函数地址位于内核空间,在内核空间执行.

如果想一个线程行为,给他提供个APC,挂到+0x034ApcState的第一个成员0x000ApcListHead上,从而改变这个线程行为,下面是APC结构

kd> dt _KAPC
nt!_KAPC
   +0x000 Type
   +0x002 Size
   +0x004 Spare0                               
   +0x008 Thread                             +0x00c ApcListEntry
   +0x014 KernelRoutine
   +0x018 RundownRoutine
   +0x01c NormalRoutine// 找到你提供的APC函数,并不完全等于APC函数的地址,后面会讲。
   +0x020 NormalContext
   +0x024 SystemArgument1
   +0x028 SystemArgument2
   +0x02c ApcStateIndex
   +0x02d ApcMode
   +0x02e Inserted

那么什么时候执行APC呢?

28

内核APC是一定会处理,再看有没有用户APC,所以不需要判断内核APC。
KiServiceExit函数:

这个函数是系统调用、异常或中断返回用户空间的必经之路。

KiDeliverApc函数:

负责执行APC函数

备用APC队列

kd> dt _KTHREAD
nt!_KTHREAD
   ...
   +0x034 ApcState         : _KAPC_STATE
   ...
   +0x138 ApcStatePointer  : [2] Ptr32 _KAPC_STATE
  ...
   +0x14c SavedApcState    : _KAPC_STATE
  ...
   +0x165 ApcStateIndex    : UChar
   +0x166 ApcQueueable     : UChar
  ...

SavedApcState的意义

线程APC队列中的APC函数都是与进程相关联的,具体点说:A进程的T线程中的所有APC函数,要访问的内存地址都是A进程的。
但线程是可以挂靠到其他的进程:比如A进程的线程T,通过修改Cr3(改为B进程的页目录基址),就可以访问B进程地址空间,即所谓“进程挂靠”。
当T线程挂靠B进程后,APC队列中存储的却仍然是原来的APC!具体点说,比如某个APC函数要读取一个地址为0x12345678的数据,如果此时进行读取,读到的将是B进程的地址空间,这样逻辑就错误了!

为了避免混乱,在T线程挂靠B进程时,会将ApcState中的值暂时存储到SavedApcState中,等回到原进程A时,再将APC队列恢复。
所以,SavedApcState又称为备用APC队列。

挂靠环境下ApcState的意义

在挂靠的环境下,也是可以先线程APC队列插入APC的,那这种情况下,使用的是哪个APC队列呢?

A进程的T线程挂靠B进程  A是T的所属进程  B是T的挂靠进程

    ApcState        B进程相关的APC函数     

    SavedApcState   A进程相关的APC函数

在正常情况下,当前进程就是所属进程A,如果是挂靠情况下,当前进程就是挂靠进程B。

ApcStatePointer

为了操作方便,_KTHREAD结构体中定义了一个指针数组ApcStatePointer ,长度为2。

正常情况下:
ApcStatePointer[0] 指向 ApcState
ApcStatePointer[1] 指向 SavedApcState
挂靠情况下:
ApcStatePointer[0] 指向 SavedApcState
ApcStatePointer[1] 指向 ApcState

ApcStateIndex

ApcStateIndex用来标识当前线程处于什么状态:

   0 正常状态  1 挂靠状态

ApcStatePointer 与 ApcStateIndex组合寻址

正常情况下,向ApcState队列中插入APC时:

  ApcStatePointer[0] 指向 ApcState 此时ApcStateIndex的值为0
ApcStatePointer[ApcStateIndex] 指向 ApcState

挂靠情况下,向ApcState队列中插入APC时:
ApcStatePointer[1] 指向 ApcState 此时ApcStateIndex的值为1
ApcStatePointer[ApcStateIndex] 指向 ApcState

总结:
无论什么环境下,ApcStatePointer[ApcStateIndex] 指向的都是ApcState
ApcState则总是表示线程当前使用的apc状态

ApcQueueable

ApcQueueable用于表示是否可以向线程的APC队列中插入APC
当线程正在执行退出的代码时,会将这个值设置为0 ,如果此时执行
插入APC的代码(KeInsertQueueApc 后面会讲),在插入函数中会判断这个值的状态,如果为0,则插入失败。

APC挂入过程

无论是正常状态还是挂靠状态,都有两个APC队列,一个内核队列,一个用户队列。
每当要挂入一个APC函数时,不管是内核APC还是用户APC,内核都要准备一个KAPC的数据结构,并且将这个KAPC结构挂到相应的APC队列中。

KAPC结构

kd> dt _KAPC
nt!_KAPC
   +0x000 Type      //类型  APC类型为0x12
   +0x002 Size      //本结构体的大小  0x30
   +0x004 Spare0        //未使用                             
   +0x008 Thread        //目标线程                                  
   +0x00c ApcListEntry  //APC队列挂的位置
   +0x014 KernelRoutine //指向一个函数(调用ExFreePoolWithTag 释放APC)
   +0x018 RundownRoutine//略 
   +0x01c NormalRoutine //用户APC总入口  或者 真正的内核apc函数
   +0x020 NormalContext //内核APC:NULL  用户APC:真正的APC函数
   +0x024 SystemArgument1//APC函数的参数 
   +0x028 SystemArgument2//APC函数的参数
   +0x02c ApcStateIndex //挂哪个队列,有四个值:0 1 2 3
   +0x02d ApcMode   //内核APC 用户APC
   +0x02e Inserted  //表示本apc是否已挂入队列 挂入前:0  挂入后  1

挂入流程:

14

KeInitializeApc函数说明

VOID KeInitializeApc
(
IN PKAPC Apc,//KAPC指针,刚分配好还没有初始化的指针
IN PKTHREAD Thread,//目标线程
IN KAPC_ENVIRONMENT TargetEnvironment,//0 1 2 3四种状态
IN PKKERNEL_ROUTINE KernelRoutine,//销毁KAPC的函数地址
IN PKRUNDOWN_ROUTINE RundownRoutine OPTIONAL,
IN PKNORMAL_ROUTINE NormalRoutine,//用户APC总入口或者内核apc函数
IN KPROCESSOR_MODE Mode,//要插入用户apc队列还是内核apc队列
IN PVOID Context//内核APC:NULL  用户APC:真正的APC函数
) 

参考:KeInitializeApc函数(内核函数)  

ApcStateIndex

与KTHREAD(+0x165)的属性同名,但含义不一样:

ApcStateIndex 有四个值:
0. 原始环境
1. 挂靠环境
2. 当前环境
3. 插入APC时的当前环境

正常情况下:
ApcStatePointer[0] 指向 ApcState
ApcStatePointer[1] 指向 SavedApcState
挂靠情况下:
ApcStatePointer[0] 指向 SavedApcState
ApcStatePointer[1] 指向 ApcState

2 初始化的时候,当前进程的ApcState

3 插入的时候,再做一次判断,判断当前进程的ApcState

KiInsertQueueApc函数说明

1) 根据KAPC结构中的ApcStateIndex找到对应的APC队列

2) 再根据KAPC结构中的ApcMode确定是用户队列还是内核队列

3) 将KAPC挂到对应的队列中(挂到KAPC的ApcListEntry处)

4) 再根据KAPC结构中的Inserted置1,标识当前的KAPC为已插入状态

5) 修改KAPC_STATE结构中的KernelApcPending/UserApcPending(有内核/用户APC要执行)

Alertable属性说明(是否运行被APC吵醒)

kd> dt _KTHREAD
ntdll!_KTHREAD
   ...
   +0x164 Alertable        : UChar
   ...

DWORD SleepEx(
  DWORD dwMilliseconds, // time-out interval
  BOOL bAlertable           // early completion option
);
DWORD WaitForSingleObjectEx(
  HANDLE hHandle,           // handle to object
  DWORD dwMilliseconds, // time-out interval
          BOOL bAlertable           // alertable, option、//当前线程是否能被APC唤醒

Alertable总结

1、Alertable=0 当前插入的APC函数未必有机会执行:UserApcPending = 0

2、Alertable=1

UserApcPending = 1

将目标线程唤醒(从等待链表中摘出来,并挂到调度链表)

内核APC执行过程

APC函数的执行与插入并不是同一个线程,具体点说:
在A线程中向B线程插入一个APC,插入的动作是在A线程中完成的,但什么时候执行则由B线程决定!,所以叫“异步过程调用”。
内核APC函数与用户APC函数的执行时间和执行方式也有区别,我们本节课主要学习内核APC的执行过程。

执行点:线程切换

graph LR
SwapContext_判断是否有内核APC-->KiSwapThread
KiSwapThread-->KiDeliverApc_执行内核APC函数

执行点:系统调用、中断或者异常(_KiServiceExit)Ring0返回Ring3环时候

当要执行用户APC之前,先要执行内核APC
有用户APC才会执行内核APC。

KiDeliverApc函数执行流程

1) 判断第一个链表是否为空

2) 判断KTHREAD.ApcState.KernelApcInProgress是否为1

3) 判断是否禁用内核APC(KTHREAD.KernelApcDisable是否为1)

4) 将当前KAPC结构体从链表中摘除

5) 执行KAPC.KernelRoutine指定的函数 释放KAPC结构体占用的空间

6) 将KTHREAD.ApcState.KernelApcInProgress设置为1 标识正在执行内核APC

7) 执行真正的内核APC函数(KAPC.NormalRoutine)

8) 执行完毕 将KernelApcInProgress改为0

9) 循环

总结

1) 内核APC在线程切换的时候就会执行,这也就意味着,只要插入内核APC
很快就会执行。

2) 在执行用户APC之前会先执行内核APC。

3) 内核APC在内核空间执行,不需要换栈,一个循环全部执行完毕。

用户APC执行过程

当产生系统调用、中断或者异常,线程在返回用户空间前都会调用_KiServiceExit函数,在_KiServiceExit会判断是否有要执行的用户APC,如果有则调用KiDeliverApc函数(第一个参数为1)进行处理。

执行用户APC时的堆栈操作

处理用户APC要比内核APC复杂的多,因为,用户APC函数要在用户空间执行的,这里涉及到大量换栈的操作:
当线程从用户层进入内核层时,要保留原来的运行环境,比如各种寄存器,栈的位置等等 (_Trap_Frame),然后切换成内核的堆栈,如果正常返回,恢复堆栈环境即可。
但如果有用户APC要执行的话,就意味着线程要提前返回到用户空间去执行,而且返回的位置不是线程进入内核时的位置,而是返回到其他的位置,每处理一个用户APC都会涉及到:

graph LR
内核-->用户空间
用户空间-->再回到内核空间

堆栈的操作比较复杂,如果不了解堆栈的操作细节不可能理解用户APC是如何执行的!

KiDeliverApc函数分析

1) 判断用户APC链表是否为空

2) 判断第一个参数是为1

3) 判断ApcState.UserApcPending是否为1

4) 将ApcState.UserApcPending设置为0

5) 链表操作 将当前APC从用户队列中拆除

6) 调用函数(KAPC.KernelRoutine)释放KAPC结构体内存空间

7) 调用KiInitializeUserApc函数

KiInitializeUserApc函数分析:备份CONTEXT

线程进0环时,原来的运行环境(寄存器栈顶等)保存到_Trap_Frame结构体中,如果要提前返回3环去处理用户APC,就必须要修改_Trap_Frame结构体:
比如:进0环时的位置存储在EIP中,现在要提前返回,而且返回的并不是原来的位置,那就意味着必须要修改EIP为新的返回位置。还有堆栈ESP,也要修改为处理APC需要的堆栈。那原来的值怎么办呢?处理完APC后该如何返回原来的位置呢?
KiInitializeUserApc要做的第一件事就是备份:
将原来_Trap_Frame的值备份到一个新的结构体中(CONTEXT),这个功能由其子函数KeContextFromKframes来完成。

KiInitializeUserApc函数分析:堆栈图

15

KiInitializeUserApc函数分析:准备用户层执行环境

16

ntdll.KiUserApcDispatcher分析(参加代码)

1、当用户在3环调用QueueUserAPC函数来插入APC时,不需要提供
NormalRoutine,这个参数是在QueueUserAPC内部指定的:

BaseDispatchAPC

2、ZwContinue函数的意义:

1) 返回内核,如果还有用户APC,重复上面的执行过程。

2) 如果没有需要执行的用户APC,会将CONTEXT赋值给Trap_Frame结构体。就像从来没有修改过一样。ZwContinue后面的代码不会执行,线程从哪里进0环仍然会从哪里回去。

总结

1) 内核APC在线程切换时执行,不需要换栈,比较简单,一个循环执行完毕。

2) 用户APC在系统调用、中断或异常返回3环前会进行判断,如果有要执行的用户APC,再执行。

3) 用户APC执行前会先执行内核APC。

参考资料

[1] 滴水视频

  • 0
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Windows APC(Asynchronous Procedure Call)是一种异步过程调用机制,它允许一个线程在另一个线程上异步执行指定的函数。APC 机制是 Windows 操作系统中非常重要的一部分,它被广泛用于实现各种系统功能,例如异步 I/O、线程池等。 在 Windows 中,每个线程都有一个 APC 队列,该队列中存储了需要在该线程上异步执行的函数。当一个线程进入 Alertable 状态时(例如调用 Sleep、WaitForSingleObject 等函数),它会检查自己的 APC 队列中是否有待处理的 APC,如果有,则会立即执行 APC 中指定的函数。 APC 机制的具体原理如下: 1. 创建 APC 对象 首先,创建一个 APC 对象,该对象包含要在目标线程上执行的函数和参数。 2. 将 APC 对象插入到目标线程的 APC 队列 使用 QueueUserAPC 函数将 APC 对象插入到目标线程的 APC 队列中。当目标线程进入 Alertable 状态时,它会检查自己的 APC 队列中是否有待处理的 APC,如果有,则会立即执行 APC 中指定的函数。 3. 触发目标线程进入 Alertable 状态 为了让目标线程进入 Alertable 状态,可以使用 Sleep、WaitForSingleObject 等函数来实现。当目标线程进入 Alertable 状态时,它会检查自己的 APC 队列中是否有待处理的 APC,如果有,则会立即执行 APC 中指定的函数。 总之,APC 机制是 Windows 操作系统中非常重要的一部分,它提供了一种有效的异步过程调用机制,可以在不阻塞目标线程的情况下异步执行指定的函数。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值