⭐ 欢迎来到虫虫下载站! | 📦 资源下载 📁 资源专辑 ℹ️ 关于我们
⭐ 虫虫下载站

📄 漫谈兼容内核之十九windows线程间的强相互作用.txt

📁 漫谈系统内核内幕 收集得很辛苦 呵呵 大家快下在吧
💻 TXT
📖 第 1 页 / 共 3 页
字号:
  *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 + -