4.2.11 上下文保存和切换的底层函数
在上面的描述中,提到了两个完成线程切换和上下文保护的底层函数SwtichTo和SaveContext,在本节中,我们对这两个函数的实现进行描述。
SaveContext函数完成线程上下文的保护,该函数的代码如下。
static VOID SaveContext(__KERNEL_THREAD_CONTEXT* lpContext,DWORD* lpdwEsp) { if((NULL==lpContext) || (NULL==lpdwEsp)) //Parameters check. return; DisableInterrupt(); #ifdef __I386__ //i386's implementation. lpContext->dwEBP=*lpdwEsp; lpdwEsp++; lpContext->dwEDI=*lpdwEsp; lpdwEsp++; lpContext->dwESI=*lpdwEsp; lpdwEsp++; lpContext->dwEDX=*lpdwEsp; lpdwEsp++; lpContext->dwECX=*lpdwEsp; lpdwEsp++; lpContext->dwEBX=*lpdwEsp; lpdwEsp++; lpContext->dwEAX=*lpdwEsp; lpdwEsp++; lpContext->dwEIP=*lpdwEsp; lpdwEsp++; lpdwEsp++; //Skip the CS's space. lpContext->dwEFlags=*lpdwEsp; lpdwEsp++; lpContext->dwESP=(DWORD)lpdwEsp; #else #endif EnableInterrupt(); return; }
代码十分简单,没有采用任何汇编语句,因为作为参数之一的lpdwEsp指针实际上指向了被时钟中断处理程序保存的当前线程的上下文信息,这些信息位于当前线程的堆栈中。因此,该函数直接把堆栈中的上下文信息复制到当前线程的上下文数据结构中即可。从此也可以看出,该函数只能被中断处理程序调用,用来完成当前线程的上下文保护。
另外一个函数SwitchTo,既可以被中断处理程序调用,也可以被系统调用函数调用,完成线程的切换。该函数接受一个线程上下文结构指针作为参数,先把要切换到的目标线程的上下文恢复到其堆栈中,然后执行iretd指令,切换到目标线程。代码如下所示,为了便于阅读,我们分段解释。
__declspec(naked) static VOID SwitchTo(__KERNEL_THREAD_CONTEXT* lpContext) { #ifdef __I386__ //Intel's x86 CPU implementation. __asm{ cli push ebp mov ebp,esp //Build the stack frame. mov eax,dword ptr [ebp+0x08]
到此为止,是在当前线程(调用SwitchTo函数的线程)堆栈中执行的,即ESP寄存器的值,指向的是当前线程的堆栈位置。上述代码通过EBP寄存器的值来访问lpContext参数(目标线程的上下文信息指针),把lpContext参数复制到EAX寄存器中。下面的代码的执行堆栈则切换到了目标线程的堆栈上(注意下面黑体部分的代码),但在执行iretd之前,所有的代码仍然属于当前线程(即当前线程的执行堆栈切换了):
mov esp,dword ptr [eax+CONTEXT_OFFSET_ESP]
push dword ptr [eax+CONTEXT_OFFSET_EFLAGS]
xor ebx,ebx
mov bx,word ptr [eax+CONTEXT_OFFSET_CS]
push ebx
push dword ptr [eax+CONTEXT_OFFSET_EIP] //Push EIP to stack.
//Now,we have built the
correct
//stack frame.
上述代码首先切换到目标线程的堆栈,然后把目前线程的EFlags寄存器、CS寄存器和EIP寄存器的值恢复到堆栈中。完成上述操作后,目标线程的堆栈情形如图4-15所示。
图4-15 切换线程前建立的堆栈框架
这个堆栈框架读者应该是十分熟悉的,即中断发生后CPU建立的堆栈框架(只考虑相同执行权限下的情况)。在这个堆栈框架下,只要执行一条iretd指令,就可以完成线程的切换了。但这时候还不是切换的时候,因为EAX、EBX等通用寄存器的值还没有恢复到目标线程所对应的值,因此,下面的代码完成这个过程。
push dword ptr[eax+CONTEXT_OFFSET_EAX] //Save eax to stack. push dword ptr[eax+CONTEXT_OFFSET_EBX] //Save ebx to stack. push dword ptr [eax+CONTEXT_OFFSET_ECX] //ecx push dword ptr [eax+CONTEXT_OFFSET_EDX] //edx push dword ptr [eax+CONTEXT_OFFSET_ESI] //esi push dword ptr [eax+CONTEXT_OFFSET_EDI] //edi push dword ptr [eax+CONTEXT_OFFSET_EBP] //ebp mov ebp,dword ptr [esp] //Restore the ebp register. add esp,0x04 mov edi,dword ptr [esp] //Restore edi add esp,0x04 mov esi,dword ptr [esp] //Restore esi add esp,0x04 mov edx,dword ptr [esp] //Restore edx add esp,0x04 mov ecx,dword ptr [esp] //Restore ecx add esp,0x04 mov ebx,dword ptr [esp] //Restore ebx add esp,0x04
上述代码很简单,就是从目标线程的Context中,把保存的通用寄存器的值复制到堆栈中,然后从堆栈中恢复到通用寄存器中。之所以从堆栈中恢复,而不是直接从Context结构中恢复,是考虑到实现上的统一(因为在保存这些寄存器的时候,也是从堆栈中保存的)。下面的代码首先解除硬件中断(通知中断控制器),然后执行iretd指令,切换到目标线程。
mov al,0x20 out 0x20,al out 0xa0,al mov eax,dword ptr [esp] add esp,0x04 //Now,the esp pointer points to the current position. sti iretd retn //This instruction will never be reached. } #else #endif }
在iretd指令执行完毕后,当前的执行线程就已经是目标线程了,因此,iretd后面的指令(retn)永远不会被执行,即该函数永远不会返回。实际上,该函数没有返回前就已经终止执行了,当然,当前线程就已经被挂起或终止了。在当前线程再次被恢复执行的时候,开始执行地点是当初保存的EIP寄存器所指向的位置,而不是继续执行上述函数中的retn指令。