📄 漫谈兼容内核之十九windows线程间的强相互作用.txt
字号:
*Status = STATUS_SUCCESS;
KeSetEvent(Event, IO_NO_INCREMENT, FALSE);
}
注意此时的“当前线程”就是目标线程,所以KeGetCurrentThread()所返回的就是目标线程的KTHREAD结构指针,结构中的TrapFrame则指向其内核堆栈中的“陷阱框架”,那就是这个线程进入内核时所保留的“现场”,也即“上下文”。
[KeSetContextKernelRoutine() > KeContextToTrapFrame()]
VOID KeContextToTrapFrame(PCONTEXT Context, PKTRAP_FRAME TrapFrame)
{
if ((Context->ContextFlags & CONTEXT_CONTROL) == CONTEXT_CONTROL)
{
TrapFrame->Esp = Context->Esp;
TrapFrame->Ss = Context->SegSs;
TrapFrame->Cs = Context->SegCs;
TrapFrame->Eip = Context->Eip;
TrapFrame->Eflags = Context->EFlags;
TrapFrame->Ebp = Context->Ebp;
}
if ((Context->ContextFlags & CONTEXT_INTEGER) == CONTEXT_INTEGER)
{
TrapFrame->Eax = Context->Eax;
TrapFrame->Ebx = Context->Ebx;
TrapFrame->Ecx = Context->Ecx;
TrapFrame->Edx = Context->Edx;
TrapFrame->Esi = Context->Esi;
TrapFrame->Edi = Context->Edi;
}
if ((Context->ContextFlags & CONTEXT_SEGMENTS) == CONTEXT_SEGMENTS)
{
TrapFrame->Ds = Context->SegDs;
TrapFrame->Es = Context->SegEs;
TrapFrame->Fs = Context->SegFs;
TrapFrame->Gs = Context->SegGs;
}
if ((Context->ContextFlags & CONTEXT_FLOATING_POINT) ==
CONTEXT_FLOATING_POINT)
{
/* Not handled. This should be handled separately I think. - blight */
}
if ((Context->ContextFlags & CONTEXT_DEBUG_REGISTERS) ==
CONTEXT_DEBUG_REGISTERS)
{
/* Not handled */
}
}
这个函数的作用就是把CONTEXT数据结构中的内容复制到当前进程的内核堆栈中去,只是作为KTRAP_FRAME数据结构的“陷阱框架”与CONTEXT数据结构略有不同。后者有个字段ContextFlags是一些标志位,用来控制把哪一些内容复制到陷阱框架中,例如CONTEXT_CONTROL就表示要把Eip、Esp等等控制着程序执行的寄存器内容复制过去。当然,这些标志位是在调用NtSetContextThread()之前就设置好了的。
复制完了以后,回到KeSetContextKernelRoutine(),下一步就是通过KeSetEvent()唤醒NtSetContextThread()的调用者了。
前面提出APC请求时还有个函数是KeGetSetContextRundownRoutine()。所谓RundownRoutine都是为要结束生命的线程准备的,目的是对于跟APC请求有关的事务作个了断,因为此时再执行常规的APC函数已经没有意义了。当一个线程要结束生命退出运行的时候,就要检查是否有APC请求存在,以及APC请求中是否有这样的RundownRoutine函数存在,如果有的话就要加以执行。不过在0.2.6版ReactOS的代码中还没有实现这一点,在0.2.9版中倒是已经有了。那么这些RundownRoutine函数到底干些什么,作些什么样的了断呢?看一下这里KeGetSetContextRundownRoutine()就可以明白:
VOID STDCALL
KeGetSetContextRundownRoutine(PKAPC Apc)
{
PKEVENT Event;
PNTSTATUS Status;
Event = (PKEVENT)Apc->SystemArgument1;
Status = (PNTSTATUS)Apc->SystemArgument2;
(*Status) = STATUS_THREAD_IS_TERMINATING;
KeSetEvent(Event, IO_NO_INCREMENT, FALSE);
}
显而易见,就NtSetContextThread()的APC请求而言,这就是唤醒正在睡眠等待前述事件发生的线程。要不然,NtSetContextThread()的调用者可就长眠不起了。
读者也许会问,改变目标线程的上下文,为什么要采取这么曲折的方法呢?把线程调度锁住,然后根据目标线程数据结构中的指针TrapFrame找到其内核堆栈上的“陷阱框架”,不是也能修改其上下文吗?这里有个问题,就是当目标线程被唤醒的时侯很可能还要在内核中运行一阵,在这个过程中可能会改变其(用户空间)上下文中某些寄存器的值,这样就可能会把所设置的值给覆盖了。而APC函数是在返回用户空间的途中执行的,此时上下文中各寄存器的值已是板上钉钉,一经改变就是最后版本了。所以,用这样显得有些曲折的方法来实现对目标线程上下文的修改,确实还是很有道理的。
此外,Windows还提供了将另一个线程从睡眠等待中“惊醒(Alert)”过来的手段,这就是系统调用NtAlertThread()。所谓“惊醒”,是说目标线程本来在睡眠等待某些条件得到满足,如果得不到满足就会一直睡眠等待下去,但是现在另一个线程要它醒过来别等了。笼统地说,这也是唤醒,但是“唤醒”一般用于条件得到满足的时侯,所以就另称之为“惊醒”,以示区别。其实,“超时(Timeout)”和惊醒是很相似的,只不过前者是由内核发起,而后者是由另一个线程发起的。不过读者很快就会看到,并非所有的睡眠等待都可以被惊醒。
下面我们看NtAlertThread()的代码。
NTSTATUS
STDCALL
NtAlertThread (IN HANDLE ThreadHandle)
{
KPROCESSOR_MODE PreviousMode = ExGetPreviousMode();
. . . . . .
/* Reference the Object */
Status = ObReferenceObjectByHandle(ThreadHandle, THREAD_SUSPEND_RESUME,
PsThreadType, PreviousMode, (PVOID*)&Thread, NULL);
/* Check for Success */
if (NT_SUCCESS(Status)) {
/*
* Do an alert depending on the processor mode. If some kmode code wants to
* enforce a umode alert it should call KeAlertThread() directly. If kmode
* code wants to do a kmode alert it's sufficient to call it with Zw or just
* use KeAlertThread() directly
*/
KeAlertThread(&Thread->Tcb, PreviousMode);
/* Dereference Object */
ObDereferenceObject(Thread);
}
/* Return status */
return Status;
}
这段代码就不需要什么解释了,我们往下看KeAlertThread():
[NtAlertThread() > KeAlertThread()]
BOOLEAN STDCALL
KeAlertThread(PKTHREAD Thread, KPROCESSOR_MODE AlertMode)
{
. . . . . .
/* Acquire the Dispatcher Database Lock */
OldIrql = KeAcquireDispatcherDatabaseLock();
/* Save the Previous State */
PreviousState = Thread->Alerted[AlertMode];
/* Return if Thread is already alerted. */
if (PreviousState == FALSE) {
/* If it's Blocked, unblock if it we should */
if (Thread->State == THREAD_STATE_BLOCKED &&
(AlertMode == KernelMode || Thread->WaitMode == AlertMode) &&
Thread->Alertable)
{
KiAbortWaitThread(Thread, STATUS_ALERTED,
THREAD_ALERT_INCREMENT);
} else {
/* If not, simply Alert it */
Thread->Alerted[AlertMode] = TRUE;
}
}
/* Release the Dispatcher Lock */
KeReleaseDispatcherDatabaseLock(OldIrql);
/* Return the old state */
return PreviousState;
}
如前所述,线程的有些睡眠是可惊醒的,有些是不可惊醒的。对此我们不仿看一下KeWaitForSingleObject()的调用界面:
NTSTATUS KeWaitForSingleObject(
IN PVOID Object,
IN KWAIT_REASON WaitReason,
IN KPROCESSOR_MODE WaitMode,
IN BOOLEAN Alertable,
IN PLARGE_INTEGER Timeout OPTIONAL);
这里的参数Alertable就规定了本次睡眠是否可惊醒。
而WaitMode,则说明本次睡眠是用户模式、出于用户程序的要求,还是内核模式。其取值范围为KernelMode和UserMode。用户模式的睡眠既可以被用户模式的要求惊醒,也可以被内核模式的要求惊醒。但是内核模式的睡眠不能被用户模式的要求惊醒。注意不要混淆WaitMode和WaitType,后者的取值范围为WaitAll和WaitAny。
所以,KTHREAD结构中的Alertable字段就表明了当前的睡眠是否可惊醒。而hread->WaitMode 则记录着本次睡眠的模式。如果各种条件都允许惊醒,就通过KiAbortWaitThread()结束目标线程的等待状态,否则就只是记录下已经有过惊醒的要求。
由于具体的惊醒涉及将目标线程挂入就绪队列,这部分操作只能放在禁止线程调度的条件下进行,所以这里先通过KeAcquireDispatcherDatabaseLock()将运行级别提高到DISPATHER级,并锁定线程调度队列,待操作完成以后再予恢复。
实际的操作则是由KiAbortWaitThread()完成的:
[NtAlertThread() > KeAlertThread() > KiAbortWaitThread()]
VOID FASTCALL
KiAbortWaitThread(PKTHREAD Thread, NTSTATUS WaitStatus, KPRIORITY Increment)
{
. . . . . .
. . . . . .
/* Remove the Wait Blocks from the list */
WaitBlock = Thread->WaitBlockList;
while (WaitBlock) {
RemoveEntryList(&WaitBlock->WaitListEntry);
WaitBlock = WaitBlock->NextWaitBlock;
};
/* Check if there's a Thread Timer */
if (Thread->Timer.Header.Inserted) {
/* Cancel the Thread Timer with the no-lock fastpath */
Thread->Timer.Header.Inserted = FALSE;
RemoveEntryList(&Thread->Timer.TimerListEntry);
}
/* Increment the Queue's active threads */
if (Thread->Queue) {
DPRINT("Incrementing Queue's active threads\n");
Thread->Queue->CurrentCount++;
}
/* Reschedule the Thread */
PsUnblockThread((PETHREAD)Thread, &WaitStatus, 0);
}
处于睡眠等待状态的线程通过其WaitBlockList中的WaitBlock与所等待的对象挂勾,每一个WaitBlock代表着一个对象,所以现在要把它们全部解脱出来。此外,如果进入睡眠等待时规定了超时,那就也要和定时器脱钩。最后则通过PsUnblockThread()解除目标线程的阻塞,这个函数的代码我们以前已经看到过了。
最后,还有个系统调用NtTerminateThread(),是用来结束一个已打开线程的生命。这个已打开线程当然可以是当前线程本身,但是更重要的是它也可以是别的线程。
总之,Windows线程之间的作用是强作用,一个线程只要能打开另一个线程,不管是否属于同一个进程,就有了控制、支配这个被打开线程的权力。另一方面,读者在“跨进程操作”那篇漫谈中已经看到,一旦打开了另一个进程,也就取得了控制、支配这个被打开进程、包括其用户空间的权力。相比之下,Linux的进程之间、线程之间是比较平等、独立的。就像“权力导致腐败”一样,Windows线程之间的这种强作用也会带来问题、特别是安全问题。为此,Windows在对象的打开和操作两个环节上采取了安全保护措施:
l 要求打开对象时,尤其是要求打开进程对象和线程对象时,要根据要求者的身份及其证章所示的权限、目标对象的性质和访问控制表、还有所要求的操作范围进行综合的判断,如果条件不合就拒绝打开。?
l 要求对已打开的对象进行操作时,要检查是否与打开该对象时所申明的操作范围相符,如果不符就拒绝操作。
但是,就像对文件的访问权限常常会设置不当一样,对进程、线程、以及其它对象的访问控制也常常会管理不当,这就带来了安全问题。这二者本质上是同一回事,但是后者往往更容易被忽略,因而更容易发生。
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -