一、回顾

在之前的课程中,我们学习了 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)模拟线程切换相关推荐

  1. (58)模拟线程切换——添加挂起、恢复线程功能

    一.回顾 我们在上一篇博客分析了模拟线程切换的源码. <模拟线程切换> 我们着重分析了 Scheduling 和 SwitchContext 这两个函数,对线程切换的过程有了新的认识: 线 ...

  2. Windows进程与线程学习笔记(五)—— 模拟线程切换

    Windows进程与线程学习笔记(五)-- 模拟线程切换 ThreadSwitch代码分析 ThreadSwitch.cpp ThreadCore.h ThreadCore.cpp 总结 Thread ...

  3. 进程线程003 模拟线程切换

    文章目录 示例代码 关键结构体 调度链表 初始化线程堆栈 线程切换 被动切换 主动切换 线程调度 总结 之前我们已经了解过线程的等待链表和调度链表,为了更好的学习Windows的线程切换,我们要先读一 ...

  4. 模拟线程切换 C++

    为什么80%的码农都做不了架构师?>>>    前言: 本文主要是剖析NachOs的线程切换原理,并通过一个简化的例子(就是将线程部分代码抽取出来再加以修改) 来说明.本文 gith ...

  5. (59)逆向分析 KiSwapContext 和 SwapContext —— 线程切换核心代码

    一.前言 在前面的课程中,我们研究了模拟线程切换的代码,学习了 _KPCR,ETHREAD,EPROCESS 等内核结构体,这些都是为了学习Windows线程切换做的准备. 线程切换是操作系统的核心内 ...

  6. Windows进程与线程学习笔记(六)—— 线程切换

    Windows进程与线程学习笔记(六)-- 线程切换 主动切换 分析KiSwapContext 分析SwapContext 分析KiSWapThread 总结 时钟中断切换 系统时钟 分析INT 0x ...

  7. 进程线程004 Windows线程切换的三种方式

    文章目录 主动切换(调用API) KiSwapContext函数分析 哪些API调用了SwapContext函数 总结 时钟中断切换 如何中断一个正在执行的程序 系统时钟 时钟中断的执行流程 总结 时 ...

  8. 6.windows线程切换_主动切换

    ida 分析KiSwapThread sub esp, 10h mov [esp+10h+var_4], ebx ;保存当前线程寄存器现场 mov [esp+10h+var_8], esi mov [ ...

  9. 【python】Python实现模拟按键切换浏览器标签

    Python实现模拟按键切换浏览器标签 author:juliusyang 原理:模拟按下浏览器标签切换快捷键:ctrl + 数字键 import time import win32api impor ...

最新文章

  1. js中push和pop的用法
  2. 程序员面试题精选100题(06)-二元查找树的后序遍历结果[数据结构]
  3. Android GridView,recycleview,栅格布局
  4. java web远程调试工具_java远程调试 - Dendy的个人页面 - OSCHINA - 中文开源技术交流社区...
  5. 数组求和forEach方法
  6. python自动化办公 51cto_Python办公自动化之从Word到Excel
  7. vscode插件之常用插件
  8. 老鸟成长之路:菜鸟入门八种安全工具(转)
  9. HBase简介、搭建环境及安装部署
  10. Python编程实现后剪枝的CART决策树
  11. 使用MarkDown来写一份漂亮简约的简历
  12. 印度IT行业普遍高薪-印度互联网泡沫判断
  13. 移动硬盘格式化了,要怎么恢复数据
  14. 安卓搜不到airpods_真心丢不起!那就教教你AirPods丢失后如何查找吧
  15. 租用国外服务器应该注意哪些?
  16. MySQL数据库比较工具 - mysqldbcompare
  17. 大数据时代:架构师该具备什么?
  18. php课设源代码网站,php精品课程教学网站在线发布系统
  19. linux手写数字识别,mnist手写数字识别与图片预处理
  20. 【笔记】贝叶斯估计(Bayesian Estimation)

热门文章

  1. 成功解决AttributeError: module 'cv2.cv2' has no attribute 'xfeatures2d'
  2. AI+5G:2019.03.31第11届中国(深圳)IT领袖峰会【IT新未来: 5G与人工智能】内容概要
  3. TF之LSTM:利用LSTM算法对Boston(波士顿房价)数据集【13+1,506】进行回归预测(房价预测)
  4. ML之GMM:Gaussian Mixture Model高斯混合模型相关论文、算法步骤相关配图
  5. Py之argparse:Python库之argparse(命令行解析)简介、安装、使用方法之详细攻略
  6. Hyperopt 入门指南
  7. About The FTP
  8. 数据采集与分析的那些事——从数据埋点到AB测试
  9. jenkins的安装
  10. PHP5.6通过CURL上传图片@符无效的兼容问题