一、回顾
在之前的课程中,我们学习了 EPROCESS, ETHREAD, KPCR 等重要的内核结构体,学习了存储等待线程的等待链表和调度线程的调度链表,这些知识都是为了后面学习线程切换打的基础。
这次课,我们将学习老师提供的模拟Windows线程切换的源码,这份代码可以在3环模拟线程调度,有助于我们理解真正的Windows线程调度源码。
二、运行结果
这份代码是我照着视频抄的(见文末),在后续的学习中,我们要往里面加东西,所以建议不要做过多的修改。
三、RegisterGMThread, InitGMThread, GMThreadStartup
void RegisterGMThread(char *name, void (*func)(void *lpParameter), void *lpParameter);
void InitGMThread (GMThread_t *GMThreadp, char *name, void (*func)(void *lpParameter), void *lpParameter);
RegisterGMThread 函数负责创建线程,它遍历线程调度队列,找到一个空位作为新线程结构体,然后调用 InitGMThread 初始化。
InitGMThread 函数负责初始化线程结构体;为线程申请堆栈内存;向堆栈压入必要的初始数据,包括线程结构体指针,GMThreadStartup 函数指针,以及一堆寄存器的初始值(PS. 压栈用的 PushStack 函数不解释);最后设置线程状态为“就绪”。
这里所做的所有压栈操作,没有一步是多余的。
7个寄存器是线程恢复时要pop的值,这里设置成0,表示第一次调度时给寄存器设置初始值0,也可以改成其他值。
所有线程都要通过 GMThreadStartup 函数调用自己的线程入口函数,而调用 GMThreadStartup 函数的地方以及传参的过程设计非常巧妙,这一步发生在 SwitchContext 函数中,恢复线程后,pop了7个寄存器,esp就指向了 GMThreadStartup,此时 SwitchContext 调用 ret 指令,就跳转到 GMThreadStartup 函数,完全模拟了 call 调用的堆栈,那个看起来没用的堆栈平衡值其实模拟的是 call 时压入堆栈的返回地址,而 GMThreadp 模拟的是 call 之前push的参数。进入 GMThreadStartup 后,函数从 ebp + 8 处取得参数1 GMThreadp 。
为什么说返回地址是模拟的?因为 GMThreadStartup 永远不会执行它的 return 语句。
// 此函数在 SwitchContext 的 ret 指令执行时调用,功能是调用线程入口函数
void GMThreadStartup(GMThread_t *GMThreadp)
{
GMThreadp->func(GMThreadp->lpParameter);
GMThreadp->Flags = GMTHREAD_EXIT;
Scheduling();
printf("这句永远不会执行,因为修改线程状态为退出,Scheduling 永远不会返回到这里.\n");
return;
}
四、Scheduling 线程调度函数
void Scheduling();
这个函数负责遍历线程调度队列,如果遍历到“等待”状态的线程,判断它是否已经完成了“等待”,如果是,那么修改其状态为就绪。通过遍历,找出第一个“就绪”线程,如果遍历完都没有发现新的就绪线程,那么就认为主函数是“就绪”线程。
最后,调用 SwitchContext 函数“切换”到刚才找到的“就绪”线程。
五、SwitchContext 切换线程函数
SwitchContext 负责切换线程,旧线程调用 SwitchContext 时,首先把7个寄存器压到自己的栈顶,然后保存当前栈顶 esp 到 KernelStack,然后从新线程的线程结构体里取出 KernelStack 填到 esp,就完成了线程切换。
接下来就是从新线程的栈顶 pop 还原7个寄存器。pop 了那7个寄存器后,esp 一定是指向下一条指令的地址的,如果新线程尚未被调度过,那么栈顶一定是 GMThreadStartup;如果新线程曾被调度过,那么栈顶一定是新线程上一次调用 SwitchContext 的返回地址,即 Scheduling 函数的末尾。
最后,附上一张线程A切换到线程B的堆栈图,帮助理解。
六、总结
其他的函数就很简单了,限于篇幅,我不打算花笔墨解释其他函数,因为注释已经说明了我没有提到的细节。
最重要的 SwitchContext 函数,要理解它是怎么通过切换堆栈来实现切换线程的,要理解 ret 指令跳转到什么地方;Scheduling 函数也很关键,它用了一种非常简单的遍历算法寻找“就绪”线程。
这个程序模拟了时钟中断切换线程(主函数的循环),和调用API的主动切换(线程主动调用 GMSleep)。
你可以在主函数循环里加一个打印语句:
for (;;)
{
Sleep(20);
Scheduling();
// 如果回到主线程,说明没有找到就绪线程,CurrentThreadIndex 一定是 0
//printf("时钟中断. %d\n", CurrentThreadIndex);
}
你会发现,这和空闲线程的执行非常像,后续的课程里,会添加很多功能,比如模拟空闲线程,此处暂时不表。
七、单文件版本源码
#include <stdio.h>
#include <tchar.h>
#include <string.h>
#include <Windows.h>
#pragma warning(disable: 4996)
//--------------------------------------------------------------------------------------------
//--------------------------------------------------------------------------------------------
#define MAXGMTHREAD 0x100
#define GMTHREAD_CREATE 0x01
#define GMTHREAD_READY 0x02
#define GMTHREAD_RUNNING 0x04
#define GMTHREAD_SLEEP 0x08
#define GMTHREAD_EXIT 0x100
#define GMTHREADSTACKSIZE 0x80000
//--------------------------------------------------------------------------------------------
//--------------------------------------------------------------------------------------------
// 线程结构体(仿ETHREAD)
typedef struct {
char *name; // 线程名,相当于线程TID
int Flags; // 线程状态
int SleepMillisecondDot; // 休眠时间
void *InitialStack; // 线程堆栈起始位置
void *StackLimit; // 线程堆栈界限
void *KernelStack; // 线程堆栈当前位置,即ESP0
void *lpParameter; // 线程函数参数
void (*func)(void *lpParameter); // 线程函数
} GMThread_t;
//--------------------------------------------------------------------------------------------
//--------------------------------------------------------------------------------------------
// 当前调度线程下标
int CurrentThreadIndex = 0;
// 线程调度队列
GMThread_t GMThreadList[MAXGMTHREAD] = { 0 };
void *WindowsStackLimit = NULL;
//--------------------------------------------------------------------------------------------
//--------------------------------------------------------------------------------------------
void SwitchContext(GMThread_t *OldGMThreadp, GMThread_t *NewGMThreadp);
void GMThreadStartup(GMThread_t *GMThreadp);
void IdleGMThread(void *lpParameter);
void PushStack(unsigned int **Stackpp, unsigned int v);
void InitGMThread (GMThread_t *GMThreadp, char *name, void (*func)(void *lpParameter), void *lpParameter);
int RegisterGMThread(char *name, void (*func)(void *lpParameter), void *lpParameter);
void Scheduling();
void GMSleep(int Milliseconds);
void Thread1(void *lpParameter);
void Thread2(void *lpParameter);
void Thread3(void *lpParameter);
void Thread4(void *lpParameter);
//--------------------------------------------------------------------------------------------
//--------------------------------------------------------------------------------------------
int _tmain(int argc, _TCHAR* argv[])
{
// 初始化线程环境
RegisterGMThread("Thread1", Thread1, NULL);
RegisterGMThread("Thread2", Thread2, NULL);
RegisterGMThread("Thread3", Thread3, NULL);
RegisterGMThread("Thread4", Thread4, NULL);
// 仿Windows线程切换,模拟系统时钟中断,是被动切换
//Scheduling();
for (;;)
{
Sleep(20);
Scheduling();
// 如果回到主线程,说明没有找到就绪线程,CurrentThreadIndex 一定是 0
//printf("时钟中断. %d\n", CurrentThreadIndex);
}
return 0;
}
// 线程切换函数
__declspec(naked) void SwitchContext(GMThread_t *OldGMThreadp, GMThread_t *NewGMThreadp)
{
__asm
{
// 当前线程保存寄存器到自己的栈顶
push ebp;
mov ebp,esp;
push edi;
push esi;
push ebx;
push ecx;
push edx;
push eax;
mov esi,OldGMThreadp; // mov esi, [ebp + 0x08]
mov edi,NewGMThreadp; // mov edi, [ebp + 0x0C]
mov [esi + GMThread_t.KernelStack], esp; // 保存旧ESP
mov esp,[edi + GMThread_t.KernelStack]; // 设置新ESP
// 从新线程的栈里恢复寄存器的值
pop eax;
pop edx;
pop ecx;
pop ebx;
pop esi;
pop edi;
pop ebp;
// 返回到新线程之前调用 SwitchContext 的地方;如果是第一次调度,则跳转到 GMThreadStartup
ret;
}
}
// 此函数在 SwitchContext 的 ret 指令执行时调用,功能是调用线程入口函数
void GMThreadStartup(GMThread_t *GMThreadp)
{
GMThreadp->func(GMThreadp->lpParameter);
GMThreadp->Flags = GMTHREAD_EXIT;
Scheduling();
printf("这句永远不会执行,因为修改线程状态为退出,Scheduling 永远不会返回到这里.\n");
return;
}
// 空闲线程,没事做就调用它
void IdleGMThread(void *lpParameter)
{
printf("IdleGMThread-------------------\n");
Scheduling();
return;
}
// 模拟压栈
void PushStack(unsigned int **Stackpp, unsigned int v)
{
*Stackpp -= 1;
**Stackpp = v;
return;
}
// 初始化线程结构体和线程栈,设置状态为“就绪”
void InitGMThread (GMThread_t *GMThreadp, char *name, void (*func)(void *lpParameter), void *lpParameter)
{
unsigned char *StackPages;
unsigned int *ESP;
// 结构初始化赋值
GMThreadp->Flags = GMTHREAD_CREATE;
GMThreadp->name = name;
GMThreadp->func = func;
GMThreadp->lpParameter = lpParameter;
// 申请栈空间
StackPages = (unsigned char*)VirtualAlloc(NULL,GMTHREADSTACKSIZE, MEM_COMMIT, PAGE_READWRITE);
// 清零
memset(StackPages,0,GMTHREADSTACKSIZE);
// 栈初始化地址
GMThreadp->InitialStack = (StackPages + GMTHREADSTACKSIZE);
// 栈限制
GMThreadp->StackLimit = StackPages;
// 栈地址
ESP = (unsigned int *)GMThreadp->InitialStack;
// 初始化线程栈
PushStack(&ESP, (unsigned int)GMThreadp); // 通过这个指针来找到:线程函数、函数参数
PushStack(&ESP, (unsigned int)0); // 平衡堆栈,此值无意义,详见 SwitchContext 函数注释
PushStack(&ESP, (unsigned int)GMThreadStartup); // 线程入口函数,这个函数负责调用线程函数
PushStack(&ESP, (unsigned int)0); // push ebp,此值无意义,是寄存器初始值
PushStack(&ESP, (unsigned int)0); // push edi,此值无意义,是寄存器初始值
PushStack(&ESP, (unsigned int)0); // push esi,此值无意义,是寄存器初始值
PushStack(&ESP, (unsigned int)0); // push ebx,此值无意义,是寄存器初始值
PushStack(&ESP, (unsigned int)0); // push ecx,此值无意义,是寄存器初始值
PushStack(&ESP, (unsigned int)0); // push edx,此值无意义,是寄存器初始值
PushStack(&ESP, (unsigned int)0); // push eax,此值无意义,是寄存器初始值
GMThreadp->KernelStack = ESP;
GMThreadp->Flags = GMTHREAD_READY;
return;
}
// 添加新线程到调度队列,然后初始化线程
int RegisterGMThread(char *name, void (*func)(void *lpParameter), void *lpParameter)
{
int i;
// 找一个空位置,或者是name已经存在的那个项
// 下标0是当前正在运行的线程,所以从1开始遍历
for (i = 1; GMThreadList[i].name; i++)
{
if (0 == stricmp(GMThreadList[i].name, name))
{
break;
}
}
// 初始化线程结构体
InitGMThread(&GMThreadList[i], name, func, lpParameter);
return (i | 0x55AA0000);
}
// 线程调度函数,功能是遍历调度队列,找到“就绪”线程,然后切换线程
void Scheduling()
{
int i;
int TickCount;
GMThread_t *OldGMThreadp;
GMThread_t *NewGMThreadp;
TickCount = GetTickCount(); // GetTickCount 返回操作系统启动到目前为止经过的毫秒
// 正在调度的线程,第一次是 GMThreadList[0],这个表示主线程
OldGMThreadp = &GMThreadList[CurrentThreadIndex];
// 遍历线程调度队列,找第一个“就绪”线程
// 如果找不到,就回到主函数,模拟时钟中断
NewGMThreadp = &GMThreadList[0];
for (i = 1; GMThreadList[i].name; i++)
{
// 如果达到“等待时间”,就修改状态为“就绪”
if (GMThreadList[i].Flags & GMTHREAD_SLEEP)
{
if (TickCount > GMThreadList[i].SleepMillisecondDot)
{
GMThreadList[i].Flags = GMTHREAD_READY;
}
}
// 找到“就绪”线程
if (GMThreadList[i].Flags & GMTHREAD_READY)
{
NewGMThreadp = &GMThreadList[i];
break;
}
}
// 更新当前调度线程下标
CurrentThreadIndex = NewGMThreadp - GMThreadList;
// 线程切换
SwitchContext(OldGMThreadp, NewGMThreadp);
return;
}
// 正在运行的线程主动调用此函数,将自己设置成“等待”状态,然后让调度函数调度其他线程
void GMSleep(int Milliseconds)
{
GMThread_t *GMThreadp;
GMThreadp = &GMThreadList[CurrentThreadIndex];
if ((GMThreadp->Flags) != 0)
{
GMThreadp->SleepMillisecondDot = GetTickCount() + Milliseconds;
GMThreadp->Flags = GMTHREAD_SLEEP;
}
Scheduling();
return;
}
void Thread1(void *lpParameter)
{
int i;
for (i = 0; i < 3; i++)
{
printf("Thread1\n");
GMSleep(100); // 主动切换,模拟WIN32 API
}
return;
}
void Thread2(void *lpParameter)
{
int i = 0;
while (++i)
{
printf(" Thread2(%d)\n", i);
GMSleep(200); // 主动切换,模拟WIN32 API
}
return;
}
void Thread3(void *lpParameter)
{
int i = 0;
while (++i)
{
printf(" Thread3(%d)\n", i);
GMSleep(200); // 主动切换,模拟WIN32 API
}
return;
}
void Thread4(void *lpParameter)
{
int i = 0;
while (++i)
{
printf(" Thread4(%d)\n", i);
GMSleep(400); // 主动切换,模拟WIN32 API
}
return;
}
---------------------
作者:hambaga
来源:CSDN
原文:https://blog.csdn.net/Kwansy/article/details/109554283
版权声明:本文为作者原创文章,转载请附上博文链接!
内容解析By:CSDN,CNBLOG博客文章一键转载插件