(57)模拟线程切换
一、回顾
在之前的课程中,我们学习了 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; // 线程名,相当于线程TIDint Flags; // 线程状态int SleepMillisecondDot; // 休眠时间void *InitialStack; // 线程堆栈起始位置void *StackLimit; // 线程堆栈界限void *KernelStack; // 线程堆栈当前位置,即ESP0void *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; // 保存旧ESPmov esp,[edi + GMThread_t.KernelStack]; // 设置新ESP// 从新线程的栈里恢复寄存器的值pop eax;pop edx;pop ecx;pop ebx;pop esi;pop edi;pop ebp;// 返回到新线程之前调用 SwitchContext 的地方;如果是第一次调度,则跳转到 GMThreadStartupret;}
}// 此函数在 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;
}
(57)模拟线程切换相关推荐
- (58)模拟线程切换——添加挂起、恢复线程功能
一.回顾 我们在上一篇博客分析了模拟线程切换的源码. <模拟线程切换> 我们着重分析了 Scheduling 和 SwitchContext 这两个函数,对线程切换的过程有了新的认识: 线 ...
- Windows进程与线程学习笔记(五)—— 模拟线程切换
Windows进程与线程学习笔记(五)-- 模拟线程切换 ThreadSwitch代码分析 ThreadSwitch.cpp ThreadCore.h ThreadCore.cpp 总结 Thread ...
- 进程线程003 模拟线程切换
文章目录 示例代码 关键结构体 调度链表 初始化线程堆栈 线程切换 被动切换 主动切换 线程调度 总结 之前我们已经了解过线程的等待链表和调度链表,为了更好的学习Windows的线程切换,我们要先读一 ...
- 模拟线程切换 C++
为什么80%的码农都做不了架构师?>>> 前言: 本文主要是剖析NachOs的线程切换原理,并通过一个简化的例子(就是将线程部分代码抽取出来再加以修改) 来说明.本文 gith ...
- (59)逆向分析 KiSwapContext 和 SwapContext —— 线程切换核心代码
一.前言 在前面的课程中,我们研究了模拟线程切换的代码,学习了 _KPCR,ETHREAD,EPROCESS 等内核结构体,这些都是为了学习Windows线程切换做的准备. 线程切换是操作系统的核心内 ...
- Windows进程与线程学习笔记(六)—— 线程切换
Windows进程与线程学习笔记(六)-- 线程切换 主动切换 分析KiSwapContext 分析SwapContext 分析KiSWapThread 总结 时钟中断切换 系统时钟 分析INT 0x ...
- 进程线程004 Windows线程切换的三种方式
文章目录 主动切换(调用API) KiSwapContext函数分析 哪些API调用了SwapContext函数 总结 时钟中断切换 如何中断一个正在执行的程序 系统时钟 时钟中断的执行流程 总结 时 ...
- 6.windows线程切换_主动切换
ida 分析KiSwapThread sub esp, 10h mov [esp+10h+var_4], ebx ;保存当前线程寄存器现场 mov [esp+10h+var_8], esi mov [ ...
- 【python】Python实现模拟按键切换浏览器标签
Python实现模拟按键切换浏览器标签 author:juliusyang 原理:模拟按下浏览器标签切换快捷键:ctrl + 数字键 import time import win32api impor ...
最新文章
- js中push和pop的用法
- 程序员面试题精选100题(06)-二元查找树的后序遍历结果[数据结构]
- Android GridView,recycleview,栅格布局
- java web远程调试工具_java远程调试 - Dendy的个人页面 - OSCHINA - 中文开源技术交流社区...
- 数组求和forEach方法
- python自动化办公 51cto_Python办公自动化之从Word到Excel
- vscode插件之常用插件
- 老鸟成长之路:菜鸟入门八种安全工具(转)
- HBase简介、搭建环境及安装部署
- Python编程实现后剪枝的CART决策树
- 使用MarkDown来写一份漂亮简约的简历
- 印度IT行业普遍高薪-印度互联网泡沫判断
- 移动硬盘格式化了,要怎么恢复数据
- 安卓搜不到airpods_真心丢不起!那就教教你AirPods丢失后如何查找吧
- 租用国外服务器应该注意哪些?
- MySQL数据库比较工具 - mysqldbcompare
- 大数据时代:架构师该具备什么?
- php课设源代码网站,php精品课程教学网站在线发布系统
- linux手写数字识别,mnist手写数字识别与图片预处理
- 【笔记】贝叶斯估计(Bayesian Estimation)
热门文章
- 成功解决AttributeError: module 'cv2.cv2' has no attribute 'xfeatures2d'
- AI+5G:2019.03.31第11届中国(深圳)IT领袖峰会【IT新未来: 5G与人工智能】内容概要
- TF之LSTM:利用LSTM算法对Boston(波士顿房价)数据集【13+1,506】进行回归预测(房价预测)
- ML之GMM:Gaussian Mixture Model高斯混合模型相关论文、算法步骤相关配图
- Py之argparse:Python库之argparse(命令行解析)简介、安装、使用方法之详细攻略
- Hyperopt 入门指南
- About The FTP
- 数据采集与分析的那些事——从数据埋点到AB测试
- jenkins的安装
- PHP5.6通过CURL上传图片@符无效的兼容问题