4.5 V1.5核心线程对象(KernelThreadObject)的实现
在Hello China V1.5的实现中,对核心线程对象(__KERNEL_THREAD_OBJECT)进行了优化,删除了其中不必要的数据成员,并对其中的一些成员进行了更改。相应地,为了实现等待多个同步对象等功能,又增加了部分数据成员。优化后的核心线程对象如下:
BEGIN_DEFINE_OBJECT(__KERNEL_THREAD_OBJECT) INHERIT_FROM_COMMON_OBJECT INHERIT_FROM_COMMON_SYNCHRONIZATION_OBJECT //__KERNEL_THREAD_CONTEXT KernelThreadContext; __KERNEL_THREAD_CONTEXT* lpKernelThreadContext; DWORD dwThreadID; DWORD dwThreadStatus; __PRIORITY_QUEUE* lpWaitingQueue; DWORD dwThreadPriority; //DWORD dwScheduleCounter; DWORD dwReturnValue; DWORD dwTotalRunTime; DWORD dwTotalMemSize; LPVOID lpHeapObject; LPVOID lpDefaultHeap; BOOL bUsedMath; DWORD dwStackSize; LPVOID lpInitStackPointer; DWORD (*KernelThreadRoutine)(LPVOID); LPVOID lpRoutineParam; //The following four members are used to manage the message queue //of the current kernel thread. __KERNEL_THREAD_MESSAGE KernelThreadMsg[MAX_KTHREAD_MSG_NUM]; UCHAR ucMsgQueueHeader; UCHAR ucMsgQueueTrial; UCHAR ucCurrentMsgNum; UCHAR ucAligment; __PRIORITY_QUEUE* lpMsgWaitingQueue; __EVENT* lpMsgEvent; DWORD dwLastError; END_DEFINE_OBJECT()
相对于V1.0,V1.5主要的修改内容有:
(1)删除了在V1.0中用于调度的dwScheduleCounter成员,因为在V1.5的实现中,是严格按照优先级来进行调度的,无须该成员。
(2)在V1.0的实现中,该对象包含了一个类型为__KERNEL_THREAD_CONTEXT的数据结构,用于保存当前线程的硬件上下文。在V1.5的实现中,则把该变量替换成了一个指针,硬件的上下文直接保存在当前线程的堆栈中,新增加的指针指向核心线程的堆栈中的上下文信息。详细信息,请参考后续描述。
(3)在V1.0的实现中,采用了一个事件对象来同步核心线程的消息队列,而在V1.5的实现中,则直接添加了一个消息队列的等待队列(lpMsgWaitingQueue)对象,当核心线程调用GetMessage试图获得消息的时候,若消息队列为空,则会阻塞在该队列中。
4.5.1 V1.5版本中硬件上下文的保存
在V1.0的实现中,对硬件上下文的保存,是直接保存在核心线程对象的KernelThreadContext成员中的。每当线程切换发生的时候,原来线程的硬件上下文,保存在该结构体中,而新选择投入运行的线程,则从该结构体中获得其硬件上下文。当时的设计,是考虑到线程上下文可能被程序更改的情况,这样单独把线程上下文拿出来,会带来很多的方便。但实际表明,这种需求是非常小的,因此在V1.5中做了更改,线程上下文直接保存在线程的堆栈中,而核心线程对象则保存了一个指向核心线程上下文的指针,这样通过该指针,也可以方便地访问到线程的上下文。
而对线程上下文本身的定义,也做了更改,更改后的定义如下:
BEGIN_DEFINE_OBJECT(__KERNEL_THREAD_CONTEXT)
#ifdef __I386
DWORD dwEFlags;
WORD wCS;
WORD wReserved;
DWORD dwEIP;
DWORD dwEAX;
DWORD dwEBX;
DWORD dwECX;
DWORD dwEDX;
DWORD dwESI;
DWORD dwEDI;
DWORD dwEBP;
//DWORD dwESP;
END_DEFINE_OBJECT()
与V1.0不同的是,删除了硬件上下文中的一个数据成员dwESP。在中断或异常发生后,由汇编语言编写的中断处理程序入口模块,会建立如图4-17所示的堆栈框架。
图4-17 V1.5的核心线程堆栈框架
然后调用GeneralIntHandler函数,该函数接受两个参数:一个是中断向量,另外一个是保存的堆栈指针,即图中的ESP值(该值指向了EBP寄存器在堆栈中的位置)。显然,这个堆栈框架,跟核心线程上下文的定义(__KERNEL_THREAD_CONTEXT)是吻合的,因此,我们只要在GeneralIntHandler里面,把传递过来的ESP指针保存到核心线程对象的lpKernelThreadContext里面就可以了。
在切换到新的线程时,只需把新线程的lpKernelThreadContext装载到ESP寄存器中,就切换到了新线程的堆栈,然后恢复所有寄存器,并执行iretd指令即可,下面的汇编代码,示意了该过程:
__declspec(naked) VOID __SwitchTo(__KERNEL_THREAD_CONTEXT* lpContext) { __asm{ push ebp mov ebp,esp mov esp,dword ptr [ebp+0x08] //Switched to new thread. pop ebp pop edi pop esi pop edx pop ecx pop ebx mov al,0x20 out 0x20,al out 0xa0,al pop eax iretd } }
上述切换函数,只能在中断上下文中调用,因为该切换函数解除了8259中断控制器的中断请求。
但在进程上下文中切换的时候,却有些麻烦,因为需要建立上述形式的堆栈框架。在过程上下文中,定义了一个函数__SaveAndSwitch,来完成当前线程上下文的保存,并切换到新选择的核心线程。该函数原型如下:
__declspec(naked) VOID__SaveAndSwitch(__KERNEL_THREAD_CONTEXT** lppOldContext,__KERNEL_THREAD_OBJECT** lppNewContext);
该函数被ScheduleFromProc函数调用,用于完成核心线程在过程上下文中的调度。因此,在调用该函数前,必须获得当前线程的上下文,以及待调度线程的上下文,这些工作都是ScheduleFromProc函数完成的。
__SaveAndSwitch被调用后,当前线程的堆栈框架中只保存了两个参数——lppOldContext和lppNewContext,以及函数返回地址,如图4-18所示。
图4-18 __SaveAndSwitch调用后的堆栈框架
为了建立目标堆栈框架,在__SaveAndSwitch函数所在的源文件内,定义了两个静态全局变量,并借助这两个静态全局变量实现了当前线程堆栈框架的保存。代码如下:
static DWORD dwTmpEip=0; static DWORD dwTmpEax=0; static DWORD dwTmpEbp=0; __declspec(naked) void __SaveAndSwitch(__KERNEL_THREAD_CONTEXT** lppOldContext,__KERNEL_THREAD_CONTEXT** lppNewContext) { __asm{ mov dwTmpEbp,esp //Save ESP to global variable. pop dwTmpEip //Save EIP to global variable. push eax pop dwTmpEax //Save EAX to global variable. pushfd //Save EFlags. xor eax,eax mov ax,cs push eax //Save CS. push dwTmpEip //Restore EIP push eax push ebx push ecx push edx push esi push edi push ebp //Now,we have built the target statck frame,save it to *lppOldContext. mov ebp,dwTmpEbp mov ebx,dword ptr [ebp+0x04] mov dword ptr [ebx],esp //Now,save ESP to *lppOldContext. //Restore the new thread's context,and switch to it. mov ebx,dword ptr [ebp+0x08] mov esp,dword ptr [ebx] //Restored the ESP register. pop ebp pop edi pop esi pop edx pop ecx pop ebx pop eax iretd } }
需要注意的是,该函数被调用的时候,当前核心线程还在执行,尚未切换到新的核心线程。该函数首先保存当前核心线程的一些寄存器信息(保存到当前正在运行的核心线程的堆栈中),然后把堆栈指针保存在*lppOldContext变量中(该变量实际上就是指向当前核心线程对象的lpKernelThreadContext变量)。
保存完当前核心线程的上下文信息之后,通过*lppNewContext变量,获得新核心线程的上下文信息的指针(实际上就是待运行核心线程的堆栈指针),然后把ESP寄存器的值恢复为新核心线程的堆栈指针,这时候操作的堆栈,已经是新核心线程的堆栈了。通过连续的几条POP指令,把新核心线程的上下文进行恢复,然后执行一条iretd指令,就切换到新核心线程被换出的位置并开始运行了。