記事のディレクトリ
APCの自然
スレッドは、他の人が彼をコントロールすることができるか、CPUを占有中断し、自分自身の実装でスレッドを再開殺害することはできませんか?あなたがAPIを呼び出さない場合は、極端な例を挙げれば、割り込みマスク、およびコードが異常な表示されないことを保証するために、スレッドは永続的にCPUを占有されます。あなたは、スレッドの終了をしたい場合は他の人が、スレッドの終了が存在しないのであれば、あなたは、自分自身を殺すためにコードを実行されている必要があります。
あなたがスレッドの動作を変更したいのであれば、どのように行うには?彼が呼び出すために聞かせて、彼のための機能を提供することができ、この機能は、非同期プロシージャ・コールであるAPC、あります
APCキュー
私たちが今必要なのは、議論のある私は、特定のスレッドに機能を提供する場合、この関数はどこにハングアップしますか?その答えは、APCキュー、現在のスレッドの構造で最初の外観です
kd> dt _KTHREAD
ntdll!_KTHREAD
+0x040 ApcState : _KAPC_STATE
スレッド構造KTHREAD
部材位置0x40のは、APCキューであるサブ構造ApcState、あります
kd> dt _KAPC_STATE
nt!_KAPC_STATE
+0x000 ApcListHead //2个APC队列 用户APC和内核APC
+0x010 Process //线程所属进程或者所挂靠的进程
+0x014 KernelApcInProgress //内核APC是否正在执行
+0x015 KernelApcPending //是否有正在等待执行的内核APC
+0x016 UserApcPending //是否有正在等待执行的用户APC
_KAPC_STATE
APCキューの最初の二つのメンバーは、各メンバーが二重にリンクされたリストで、二重にリンクされたリストは、APCキューです。
キューは、カーネルモードAPC、APC機能が格納されている内部で、2 APC、APCのユーザーモードがキューであります。
あなたは、リンクリストにリンクされている機能を提供することができる特定の瞬間に、現在のスレッドが時間の関数がある場合、我々は、通話に行きます、機能の現在のリストをチェックしたときに、スレッドが特定の操作を実行します。これは、スレッドの動作を変更することと同じです。
今、私たちはあなたがスレッドの動作を変更したい場合、正確には、APCキューのスレッドにリンクされている機能を提供することが必要であることを知っていることはAPCを提供することで、APCの構造を理解するために年かかりました。
APC構造
kd> dt _KAPC
ntdll!_KAPC
+0x000 Type : UChar
+0x001 SpareByte0 : UChar
+0x002 Size : UChar
+0x003 SpareByte1 : UChar
+0x004 SpareLong0 : Uint4B
+0x008 Thread : Ptr32 _KTHREAD
+0x00c ApcListEntry : _LIST_ENTRY
+0x014 KernelRoutine : Ptr32 void
+0x018 RundownRoutine : Ptr32 void
+0x01c NormalRoutine : Ptr32 void
+0x020 NormalContext : Ptr32 Void
+0x024 SystemArgument1 : Ptr32 Void
+0x028 SystemArgument2 : Ptr32 Void
+0x02c ApcStateIndex : Char
+0x02d ApcMode : Char
+0x02e Inserted : UChar
最も重要なの一つは、ということです+0x01c NormalRoutine
。このメンバーは、メンバーがあなたが提供するAPC機能により求めることができます。
今、私たちは、フォーマットが従ってAPCを提供することを知っているだけでなく、スレッドを保存する場所が、別の問題があり、それはAPCが提供する機能を実行する現在のスレッドを
あなたはこの問題を解決したい場合は、コア機能を知っておく必要があります。KiServiceExit
APC相関関数
KiServiceExit
この関数は、システムコール例外であるか、ユーザ空間に戻るための唯一の方法を中断します
KiDeliveApc
APC機能の実装を担当
APCバックアップキュー
kd> dt _KTHREAD
ntdll!_KTHREAD
+0x040 ApcState : _KAPC_STATE
+0x170 SavedApcState : _KAPC_STATE
スレッド構造では0x40のAPCキューの位置で、0x170 APCの位置は、これら二つの部材のキュー構造を持っているとまったく同じです
ApcState意味
APC機能のスレッドキューがプロセスに関連している、ことを特定点:Tは、アクセス・メモリ・アドレスに、APC機能の全ての処理をスレッドが処理されています。
しかし、スレッドは他のプロセスにリンクすることができます。たとえば、プロセスAスレッドTは、CR3を変更することによって、あなたはBのプロセスのアドレス空間にアクセスすることができ、いわゆるプロセスが停泊しました。
スレッドがT B工程を固定されたときに、キューに格納されたAPCは、まだ元のAPCです。読み取るために、この時間は、読み出しアドレス空間Bのプロセスになる場合、具体的に、APC機能のようなデータ・アドレス0x12345678のを読み取るために、そのような論理は間違っています。
スレッドB Tプロセスを呼び出すときに回避の混乱に、値が一時的にその後、APCキュー回復ApcState SavedApcState、オリジナルのプロセスAにアイソクロナスバック、中に保存されます
したがって、SavedApcStateも知られているAPCバックアップキュー
つまりアンカー環境下でApcState
環境を呼び出すことではなく、この場合には、の使用はAPCがそれをキューイングすることを、キューのスレッドAPC APCに挿入することができますか?
TのTスレッドA処理アンカープロセスB、Aが所有されているプロセスは、B Tは、プロセスとリンクされています
- ApcState:APC機能の処理に関連するB
- SavedApcState:プロセス関連のAPC機能
通常の状況下では、現在のプロセスがあり、あなたのプロセス Aは、ケースがリンクされている場合、現在のプロセスがされ、プロセスとリンク B
APCのその他の関連するメンバー
ApcStatePointer
+0x168 ApcStatePointer : [2] Ptr32 _KAPC_STATE
0x168 KTHREAD構造の位置に部材がApcStateへの2つのポインタ、各ポインタ点で、ポインタの配列であります
操作を容易にするために、KTHREAD構造は、ポインタアレイApcStatePointer、2の長さを規定します。
通常の状況下では:
ApcStatePointer [0] ApcStateポイント
ApcStatePointer [1]指向SavedApcState
ケースに固定:
ApcStatePointer [0]指向SavedApcState
ApcStatePointer [1] ApcStateポイント
ApcStateIndex
+0x134 ApcStateIndex : UChar
0通常1つの傘下の状態:ApcStateIndexは、現在のスレッドを述べるものを識別するために使用されます
アドレッシングApcStatePointer ApcStateIndexとの組み合わせで
通常の状況下では、APCキュー挿入ApcState:
ApcStatePointer [0]ポイントApcState、このときApcStateIndexで0の値
ApcStatePointer [ApcStateIndex]ポイントApcState
アンカーの場合は、APCは、キューApcStateに挿入されています。
ApcStatePointer [1] ApcState、このときの値1 ApcStateIndexポイント
ApcStatePointer [ApcStateIndex]ポイントApcState
要約:
状況は、ApcStatePointer [ApcStateIndex]ポイントはApcState、ApcStateは常にAPC使用するスレッドの現在の状態を表しているものに関係なく
ApcQueueable
+0x0b8 ApcQueueable : Pos 5, 1 Bit
APC APCは、キュースレッド挿入することができるかどうかを示すApcQueueable。
コードは、スレッドが終了を実行している場合、この値は0に設定され、それが0である場合、この時点で挿入APCコード場合、挿入機能は、状態の値を決定し、挿入は失敗。
APCは、プロセスにリンク
それが正常な状態か、アンカー状態であるかどうか、それは2つの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
- タイプ:タイプを。Windowsでは、任意のカーネルオブジェクトは、番号を持って、この数は、あなたが属するタイプを識別するために使用され、APC自体がカーネルオブジェクトである、それはまた、番号を持っているの0x12であります
- サイズ:これはKAPCの構造の現在のメンバーのサイズを参照
- スレッド:各スレッドは、独自のAPCキューを持ち、このメンバーは、スレッドに属してAPCを指定します
- ApcListEntry:APCキュー吊り位置、二重にリンクされたリストは、次のAPC二重リンクリストで見つけることができています
- KernelRoutine:関数(コールExFreePoolWithTagリリースAPC)を指します。我々はAPCを終了したら、このメモリは、リリースにKernelRoutine現在KAPC自体で指定された機能します
- NormalRoutine:値FOUNDによって現在のカーネルAPCは、実際のカーネルAPC機能である場合に、現在のAPCユーザAPC場合、ユーザAPC総エントリこの位置点は、総入口によってすべてのユーザーにAPCを見出すことができます機能
- NormalContext:この値によって、現在のカーネルAPCは、空の場合、現在のユーザーは、APC APCであれば、実際のユーザーのAPC機能には、この値のポイント
- SystemArgument1 SystemArgument2 APC関数パラメータ
- ApcStateIndex:ハングアップすると、現在のAPCキュー
- ApcMode:現在のAPCのユーザーまたはカーネルAPC APC
- 挿入:APC APCの現在の構造は、キューに挿入されています
プロセス従事
Keinitialiseapc
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機能の役割は、現在の構造の割り当てを与えることですKAPC
ApcStateIndex
そして、同じ名前KTHREAD(+ 0x134)ではなく、同じ意味のプロパティ:
ApcStateIndexは、4つの値があります。
- 0元の環境では - >挿入プロセスは、現在のスレッドに属しているにかかわらず、プロセスに挿入されているアンカーかどうかの、現在のスレッドのAPCキューに属します。
- 1つのアンカー環境
- リンクされている場合、親プロセスは、現在のプロセスは、プロセスとリンクされている> APCキューの現在のプロセスの中に、固定されていない場合、現在のプロセスがされる - 2現在の環境
- 3現在の環境APC挿入 - 挿入APCは、現在のスレッドは、次にAPC挿入固定状態であるか否かを判断する前に、スイッチング状態の任意の時点3の値に>スレッド
KiInsertQueueApc
- KAPC ApcStateIndex APCキューに基づいて対応する構造を探します
- 構造はKAPC ApcModeは、ユーザまたはカーネルキューキューを決定し、その後で
- KAPCはKAPCでハングApcListEntryの対応するキューにリンクされています
- 構造は、現在の状態がKAPCとして挿入されている識別し、次いでKAPC挿入されたセットであります
- KAPC_STATE修正構造KernelApcPending / UserApcPending
APCのカーネル実行
挿入およびAPC機能の実装は、同じスレッド、特定のポイントことはありません。
スレッドは、アクションがスレッドで行われる挿入、APCにスレッドBに挿入されるが、スレッドBの決定によって実行されたとき。だから、非同期プロシージャ・コールと呼ばれます。
実行と実装時間カーネルAPC APC機能およびユーザー機能が異なります。私たちは、カーネルAPCの実装プロセスを理解するために開始します
実行のポイント:スレッド切り替え
IDAオープンntkrnlpaは、SwapContext機能を見つけます
この完了したときに実行される約関数、現在のカーネルAPCを実行するかどうかを決定する、EAXを維持する決意のその結果、その後リターン
次に、関数KiSwapContext機能をフォローアップしていき層を見つけます
この関数は、処理のためのAPCはありませんが、戻って、親機能に従うことを継続していき
ここに戻り、現在のカーネルAPCが処理されるかどうかを判断するために、ある値KiSwapContextを判断するために戻ります、そうであれば、KiDeliverApc処理を呼び出します。
这个函数有三个参数,第一个参数如果是0,就意味着KiDeliverApc在执行的时候只会处理内核APC,第一个参数如果是1,KiDeliverApc除了处理内核APC以外,还会处理用户APC
流程总结:
- SwapContext 判断是否有内核APC
- KiSwapThread 切换线程
- KiDeliverApc
执行点:系统调用 中断或者异常(_KiServiceExit)
找到_KiServiceExit函数,这里会判断是否有要执行的用户APC,如果有的话则会调用KiDeliverApc函数进行处理,此时KiDeliverApc第一个参数为1,代表执行用户APC和内核APC。
当要执行用户APC之前,先要执行内核APC
KiDeliverApc函数分析
无论是执行内核APC还是执行用户APC都会调用KiDeliverApc函数,接下来分析KiDeliverApc函数主要做了什么事情
首先这里会取出内核APC列表,然后执行跳转
接着判断第一个链表是否为空(内核APC队列),如果不为空则跳转
跳转以后,首先得到KACP的首地址,然后取出KACP结构体的各个参数,放到局部变量里
在这里,因为我们要处理的是内核APC,所以NormalRoutine代表是内核APC函数地址,这里会判断内核APC函数地址是否为空,不为空的话则进行跳转
跳转以后,先判断是否有正在执行内核APC,然后判断是否禁用内核APC,接着将APC从内核队列中摘除。
接着先调用KAPC.KernelRoutine指定的函数 释放KAPC结构体占用的空间
然后将ApcState.KernelApcInProgress 设置为1 标识正在执行内核APC。
接着将三个参数压入栈里,开始执行真正的内核APC函数
执行完毕以后,将ApcState.KernelApcInProgress 置0,接着再次判断内核APC队列,开始下一轮循环
内核APC执行流程总结:
- 判断第一个链表(内核APC队列)是否为空
- 判断KTHREAD.ApcState.KernelApcInProgress(是否正在执行内核APC)是否为1
- 判断是否禁用内核APC(KTHREAD.KernelApcDisable是否为1)
- 将当前KAPC结构体从链表中摘除
- 执行KAPC.KernelRoutine指定的函数 释放KAPC结构体占用的空间
- 将KTHREAD.ApcState.KernelApcInProgress设置为1 标识正在执行内核APC
- 执行真正的内核APC函数(KAPC.NormalRoutine)
- 执行完毕 将KernelApcInProgress改为0
总结
- 内核APC在线程切换的时候就会执行,这也就意味着,只要插入内核APC很快就会被执行
- 在执行用户APC之前会先执行内核APC
- 内核APC在内核空间执行,不需要换栈,一个循环全部执行完毕
用户APC的执行过程
当产生系统调用 中断或者异常,线程在返回用户空间前都会调用_KiServiceExit
函数,在_KiServiceExit
函数里会判断是否有要执行的用户APC,如果有则调用KiDeliverApc函数进行处理
执行用户APC时的堆栈操作
处理用户APC要比处理内核APC复杂的多,因为用户APC函数要在用户空间执行,这里涉及到大量的换栈操作:
当线程从用户层进入内核层时,要保留原来的运行环境,比如各种寄存器 栈的位置等等,然后切换成内核的堆栈,如果正常返回,恢复堆栈环境即可
但如果有用户APC要执行的话,就意味着线程要提前返回到用户空间去执行,而且返回的位置不是线程进入内核时的位置,而是返回到真正执行APC的位置
每处理一个用户APC就会涉及到:内核—>用户空间—>再回到内核空间
执行用户APC最为关键的就是理解堆栈操作的细节
KiDeliverApc函数分析
KiDeliverApc函数会push三个参数,第一个参数如果为0,代表只处理内核APC,如果为1,代表处理用户APC和内核APC。
也就是说内核APC是无论如何都会执行的。
取出内核APC队列之后会再次取出用户APC队列,并判断用户APC队列是否为空
.text:00426063 cmp [ebp+arg_0], 1
接着判断KiDeliverApc第一个参数是否为1 如果不是1 说明不处理用户APC,直接返回
.text:00426069 cmp byte ptr [esi+4Ah], 0 ;
+0x4A=UserApcPending 表示是否正在执行用户APC,为0说明正在执行的用户APC,继续往下走
.text:0042606F mov byte ptr [esi+4Ah], 0
先将UserApcPending置0,表示当前正在执行用户APC
.text:00426073 lea edi, [eax-0Ch]
-0xC 得到KPCR首地址
.text:00426076 mov ecx, [edi+1Ch] ; +0x1C=NormalRoutine 用户APC总入口
.text:00426079 mov ebx, [edi+14h] ; +0x14=KernelRoutine 释放APC的函数
.text:0042607C mov [ebp+var_4], ecx
.text:0042607F mov ecx, [edi+20h] ; +0x20 NormalContext 用户APC:真正的APC函数
.text:00426082 mov [ebp+var_10], ecx
.text:00426085 mov ecx, [edi+24h] ; +0x24 SystemArgument1
.text:00426088 mov [ebp+var_C], ecx
.text:0042608B mov ecx, [edi+28h] ; SystemArgument2
接着取出KAPC结构体的成员,放到局部变量里保存
.text:00426091 mov ecx, [eax] ; --------------------------
.text:00426093 mov eax, [eax+4]
.text:00426096 mov [eax], ecx ; 链表操作 将用户APC从链表中移除
.text:00426098 mov [ecx+4], eax ; --------------------------
然后将当前的用户APC从链表中摘除
.text:004260B7 push eax
.text:004260B8 push edi
.text:004260B9 call ebx ; 调用KAPC.KernelRoutine 释放KAPC结构体内存
接着调用调用KAPC.KernelRoutine指定的函数, 释放KAPC结构体内存
到这里为止,用户APC和内核APC的处理方式就发生了变化。
如果是内核APC这里会直接调用APC入口函数,执行内核APC,但是用户APC执行的方式不一样。当前的堆栈处于0环,而用户APC需要在三环执行。
.text:004260CA push [ebp+var_8]
.text:004260CD push [ebp+var_C]
.text:004260D0 push [ebp+var_10]
.text:004260D3 push [ebp+var_4]
.text:004260D6 push [ebp+arg_8]
.text:004260D9 push [ebp+arg_4]
.text:004260DC call _KiInitializeUserApc
接着这里调用了KiInitializeUserApc函数,接下来就要研究一下这个函数是如何实现的
用户APC执行流程总结:
- 判断用户APC链表是否为空
- 判断第一个参数是为1,为1说明处理用户APC和内核APC
- 判断ApcState.UserApcPending(是否正在执行用户APC)是否为1
- 将ApcState.UserApcPending设置为0,表示正在处理用户APC
- 链表操作 将当前APC从用户队列中拆除
- 调用函数(KAPC.KernelRoutine)释放KAPC结构体内存空间
- 调用KiInitializeUserApc函数
KiInitializeUserApc函数分析:备份CONTEXT
线程进0环时,原来的运行环境(寄存器栈顶等)保存到_Trap_Frame
结构体中,如果要提前返回3环去处理用户APC,就必须修改_Trap_Frame
结构体,因为此时Trap_Frame
中存储的EIP是从三环进零环时保存的EIP,而不是用户APC函数的地址
比如:进0环时的位置存储在EIP中,现在要提前返回,而且返回的并不是原来的位置,那就意味着必须要修改EIP为新的返回位置,还有堆栈ESP也要修改为处理APC需要的堆栈。那原来的值怎么办?处理完APC后该如何返回原来的位置呢?
KiInitializeUserApc要做的第一件事就是备份:
将原来_Trap_Frame
的值备份到一个新的结构体中(CONTEXT),这个功能由其子函数KeContextFromKframes来完成
找到KiInitializeUserApc函数,首先调用了KeContextFromKframes,将Trap_Frame备份到Context
第一个参数ebx是Trap_Frame结构体首地址,第三个参数ecx是CONTEXT结构体首地址
那么问题在于CONTEXT结构体存到哪?肯定不能存到当前函数的局部变量里。Windows想了一个办法,把这个结构体和APC需要的参数,直接存到三环的堆栈里
KiInitializeUserApc函数分析:堆栈图
.text:00429EFC mov esi, [ebp+var_224] ; 2E8-224=C4 刚好是CONTEXT结构体ESP的偏移
.text:00429F02 and esi, 0FFFFFFFCh ; 进行4字节对齐
.text:00429F05 sub esi, eax ; 在0环直接修改3环的栈 将用户3环的栈减0x2DC个字节
首先esi是CONTEXT结构体里ESP的偏移,也就是三环的堆栈,然后进行4字节对齐。
接着将用户3环的栈减0x2DC个字节,此时三环的堆栈被拉伸,为什么是2DC个字节呢?
因为CONTEXT结构体的大小加上用户APC所需要的4个参数正好是2DC个字节,如下图:
.text:00429F16 lea edi, [esi+10h]
此时的esi指向的是-2DC的位置,也就是上图的NormalRoutine,+10降低堆栈,将指针指向SystemArgument2
.text:00429F19 mov ecx, 0B3h
.text:00429F1E lea esi, [ebp+var_2E8]
.text:00429F24 rep movsd
这几行代码将CONTEXT复制到了三环的堆栈
.text:00429FAC push 4
.text:00429FAE pop ecx
.text:00429FAF add eax, ecx
.text:00429FB1 mov [ebp+var_2EC], eax
.text:00429FB7 mov edx, [ebp+arg_C]
.text:00429FBA mov [eax], edx
.text:00429FBC add eax, ecx
.text:00429FBE mov [ebp+var_2EC], eax
.text:00429FC4 mov edx, [ebp+arg_10]
.text:00429FC7 mov [eax], edx
.text:00429FC9 add eax, ecx
.text:00429FCB mov [ebp+var_2EC], eax
.text:00429FD1 mov edx, [ebp+arg_14]
.text:00429FD4 mov [eax], edx
.text:00429FD6 add eax, ecx
.text:00429FD8 mov [ebp+var_2EC], eax ; 修正3环堆栈栈顶
接着这几行代码就是将APC函数执行时需要的4个值压入到3环的堆栈
KiInitializeUserApc函数分析:准备用户层执行环境
当KiInitializeUserApc将CONTEXT和执行用户APC所需要的4个值备份到3环的堆栈时,就开始准备用户层的执行环境了
.text:00429F2D push 23h
.text:00429F2F pop eax ; eax=0x23
.text:00429F30 mov [ebx+78h], eax ; 修改Trap_Frame中的SS
.text:00429F33 mov [ebx+38h], eax ; 修改Trap_Frame中的DS
.text:00429F36 mov [ebx+34h], eax ; 修改Trap_Frame中的ES
.text:00429F39 mov dword ptr [ebx+50h], 3Bh ; 修改Trap_Frame中的FS
.text:00429F40 and dword ptr [ebx+30h], 0 ; 修改Trap_Frame中的GS
首先修改段寄存器 SS DS FS GS
.text:00429F78 mov [ebx+70h], eax ; 修改Trap_Frame中的EFLAGS
接着修改EFLAGS寄存器
.text:00429F97 mov [ebx+74h], eax ; 修改Trap_Frame中的ESP
.text:00429F9A mov ecx, _KeUserApcDispatcher
.text:00429FA0 mov [ebx+68h], ecx ; 修改Trap_Frame中的EIP
然后修改ESP和EIP。这个EIP就是执行用户APC时返回到3环的位置。
这个位置是固定的,是一个全局变量:KeUserApcDispatcher。这个值在系统启动的时候已经赋值好了,是3环的一个函数:ntdll.KiUserApcDispatcher
然后回到3环,由KiUserApcDispatcher执行用户APC
总结:
- 段寄存器 SS DS FS GS
- 修改EFLAGS寄存器
- 修改ESP
- 修改EIP->ntdll.KiUserApcDispatcher
ntdll.KiUserApcDispatcher函数分析
找到KiUserApcDispatcher函数,结合上面的堆栈图我们可以得知,esp+0x10的位置就是CONTEXT指针
此时的ESP指向的是NormalRoutine,pop eax
将NormalRoutine赋值给了eax,然后call eax
开始处理用户APC的总入口
处理完用户的APC函数之后,会调用ZwContinue,这个函数的意义在于:
- 返回内核,如果还有用户APC,重复上面的执行过程
- 如果没有需要执行的用户APC,会将CONTEXT赋值给Trap_Frame结构体,回到0环
总结
- 内核APC在线程切换时执行,不需要换栈,比较简单,一个循环执行完毕
- 用户APC在系统调用、中断或异常返回3环前会进行判断,如果有要执行的用户APC,再执行。
- 用户APC执行前会先执行内核APC