[Windows] 进程和线程

进程的地址空间

以32位系统为例,进程的4GB虚拟内存中有一半属于用户空间,一半属于内核空间。用户空间地址范围是64kb---->0x80000000-64kb(所以才会有空指针异常,因为0x0这个地址用户空间不能访问,0~64kb属于保留区),系统地址范围是0x80000000---->0xffffffff

另外内核空间的0xffdf0000 与用户空间的0x7ffe0000区域共享。

具体分布情况如下(从低到高):

位置 内容
64kb 保留区/禁区
64kb 环境变量块
64kb 参数块
1MB 主线程的栈
其它空间
一般在0x00400000处 exe文件各个节
其他空间
n*4kb 各teb
4kb peb
4kb 内核用户共享区
60kb 无效区
64kb 隔离区
0x80000000开始 系统空间

进程的创建过程

  • 打开目标可执行文件
    若是exe文件,先检查‘映像劫持’键,然后打开文件,创建一个section,等候映射
    若是 bat、cmd脚本文件,则启动的是cmd.exe进程,脚本文件作为命令行参数
    若是DOS的exe、com文件,启动ntvdm.exe v86进程,原文件作为命令行参数
    若是posix、os2文件,启动对应的子系统服务进程

  • 创建、初始化进程对象;创建初始化地址空间;加载映射exe和ntdll文件;分配一个PEB

  • 创建、初始化主线程对象;创建TEB;构造初始的运行环境(内核初始栈帧)

  • 通知windows子系统(csrss.exe进程)新进程创建事件(csrss.exe进程含有绝大多数进程的句柄)
    这样,进程、主线程都创建起来了,只需等待得到cpu调度便可投入运行。

相关内核结构

// 每个线程的初始内核栈帧
typedef struct _KUINIT_FRAME  
{
    
    
    KSWITCHFRAME CtxSwitchFrame;	//切换帧
    KSTART_FRAME StartFrame;		//KiThreadStartup函数的参数帧
    KTRAP_FRAME TrapFrame;			//trap现场帧
    FX_SAVE_AREA FxSaveArea;		//浮点保存区
} KUINIT_FRAME, *PKUINIT_FRAME;

//KiThreadStartup的参数帧
typedef struct _KSTART_FRAME 
{
    
    
    PKSYSTEM_ROUTINE SystemRoutine; 	//用户线程为PspUserThreadStartup
    PKSTART_ROUTINE StartRoutine;		//用户线程为NULL(表示使用公共总入口)
    PVOID StartContext;					//入口参数
    BOOLEAN UserThread;					//标志
} KSTART_FRAME, *PKSTART_FRAME;

//切换帧
typedef struct _KSWITCHFRAME  
{
    
    
	PVOID ExceptionList;		//保存线程切换时的内核she链表(不是用户空间中的seh)
	Union
	{
    
    
	BOOLEAN ApcBypassDisable;	//用于首次调度
	UCHAR WaitIrql;				//用于保存切换时的WaitIrql
	};
	    PVOID RetAddr;			//保存发生切换时的断点地址(以后切换回来时从这儿继续执行)
} KSWITCHFRAME, *PKSWITCHFRAME;

//处理器控制块(内核中的fs寄存器总是指向这个结构体的基址)
Struct KPCR 
{
    
    
   KPCR_TIB Tib;
   KPCR* self;				//方便寻址
   KPRCB* Prcb;
   KIRQL irql;				//物理上表示cpu的当前中断级,逻辑上理解为当前线程的中断级更好
   USHORT* IDT;				//本cpu的中断描述符表的地址
   USHORT* GDT;				//本cpu的全局描述符表的地址
   KTSS* TSS;				//本cpu上当前线程的信息(ESP0)}

Struct KPCR_TIB
{
    
    
   Void* ExceptionList;	//当前线程的内核seh链表头结点地址
   Void* StackBase;		//内核栈底地址
   Void* StackLimit;	//栈的提交边界
   …
   KPCR_TIB* self;		//方便寻址
}

Struct KPRCB
{
    
    
   …
   	KTHREAD* CurrentThread;		//本cpu上当前正在运行的线程
   	KTHREAD* NextThread;		//将剥夺(即抢占)当前线程的下一个线程
  	KTHREAD* IdleThread;		//空转线程
  	BOOL QuantumEnd;			//重要字段。指当前线程的时间片是否已经用完。
  	LIST_ENTRY WaitListHead;	//本cpu的等待线程队列
  	ULONG ReadSummary;			//各就绪队列中是否为空的标志
  	ULONG SelectNextLast;
  	LIST_ENTRY DispatcherReadyListHead[32];		//对应32个优先级的32个就绪线程队列
  	FX_SAVE_AREA NpxSaveArea;}

//切换帧(用来保存切换线程)
typedef struct _KSWITCHFRAME  
{
    
    
	PVOID ExceptionList;		//保存线程切换时的内核she链表(不是用户空间中的seh)
	Union
	{
    
    
	BOOLEAN ApcBypassDisable;	//用于首次调度
	UCHAR WaitIrql;				//用于保存切换时的WaitIrql
	};
	
	//实际上首次时为KiThreadStartup,以后都固定为call KiSwapContextInternal后面的那条指令
    PVOID RetAddr;//保存发生切换时的断点地址(以后切换回来时从这儿继续执行)
} KSWITCHFRAME, *PKSWITCHFRAME;

//Trap现场帧
typedef struct _KTRAP_FRAME 
{
    
    
   ------------------这些是KiSystemService保存的---------------------------
    ULONG DbgEbp;
    ULONG DbgEip;
    ULONG DbgArgMark;
    ULONG DbgArgPointer;
    ULONG TempSegCs;
    ULONG TempEsp;
    ULONG Dr0;
    ULONG Dr1;
    ULONG Dr2;
    ULONG Dr3;
    ULONG Dr6;
    ULONG Dr7;
    ULONG SegGs;
    ULONG SegEs;
    ULONG SegDs;
    ULONG Edx;			//xy 这个位置不是用来保存edx的,而是用来保存上个Trap帧,因为Trap帧是可以嵌套的
    ULONG Ecx; 			//中断和异常引起的自陷要保存eax,系统调用则不需保存ecx
    ULONG Eax;			//中断和异常引起的自陷要保存eax,系统调用则不需保存eax
    ULONG PreviousPreviousMode;
    struct _EXCEPTION_REGISTRATION_RECORD FAR *ExceptionList;//上次seh链表的开头地址
    ULONG SegFs;
    ULONG Edi;
    ULONG Esi;
    ULONG Ebx;
ULONG Ebp;
----------------------------------------------------------------------------------------
ULONG ErrCode;//发生的不是中断,而是异常时,cpu还会自动在栈中压入对应的具体异常码在这儿
-----------下面5个寄存器是由int 2e内部本身保存的或KiFastCallEntry模拟保存的现场---------
    ULONG Eip;
    ULONG SegCs;
    ULONG EFlags;
    ULONG HardwareEsp;
ULONG HardwareSegSs;
---------------以下用于用于保存V86模式的4个寄存器也是cpu自动压入的-------------------
    ULONG V86Es;
    ULONG V86Ds;
    ULONG V86Fs;
ULONG V86Gs;
} KTRAP_FRAME, *PKTRAP_FRAME;

线程调度与切换

每个 cpu 都有一个TSS(任务状态段),TSS 记录着当前运行的线程以及该线程的信息,其中 ESP0 记录着该线程的内核栈位置,IO权限位图记录着当前线程的IO空间权限。

每当一个线程内部,从用户模式进入内核模式时,需要将cpu中的esp换成该线程的内核栈(各线程的内核栈是不同的)每当进入内核模式时,cpu就自动从TSS中找到ESP0,然后MOV ESP, TSS.ESP0,换成内核栈后,cpu然后在内核栈中压入浮点寄存器和标准的5个寄存器:原cs、原eip、原ss、原esp、原eflags。

所以说每当切换线程时,就要修改 TSS 中的 ESP0 和 IO权限位图。

Windows严格按优先级调度线程(优先级分成32个),当一个线程得到调度执行时,如果一直没有任何其他就绪线程的优先级高于本线程,本线程就可以畅通无阻地一直执行下去,直到本次的时间片用完。但是如果本次执行的过程中,如果有个就绪线程的优先级突然高于了本线程,那么本线程将被抢占,cpu将转去执行那个线程。但是,这种抢占可能不是立即性的,只有在当前线程的irql在DISPATCH_LEVEL以下(不包括),才会被立即抢占,否则,推迟抢占(即把那个高优先级的就绪线程暂时记录到当前cpu的KPCR结构中的NextThread字段中,标记要将抢占)。

总结,线程切换的时机:
1.时间片耗尽
2.被抢占
3.因等待事件、资源、信号时主动放弃cpu(WaitForSingleObject / Sleep)
4.主动切换(SwitchToThread)

总结,线程状态:
1.Ready就绪态(挂入相应的就绪队列)
2.某一时刻得到调度变成Running运行态
3.因等待某一事件、信号、资源等变成Waiting等待状态
4.Standby状态。指处于抢占者状态(NextThread就是自己)
5.DeferredReady状态。指‘将’进入就绪态

进程挂靠

当父进程要创建一个子进程时:会在父进程中调用CreateProcess。这个函数本身是运行在父进程的地址空间中的,但是由它创建了子进程,创建了子进程的地址空间,创建了子进程的PEB。当要初始化子进程的PEB结构时,由于PEB本身位于子进程的地址空间中,如果直接访问PEB那是不对的,那将会映射到不同的物理内存。所以必须挂靠到子进程的地址空间中,去读写PEB结构体中的值。

进程挂靠的实质工作,就是将cr3寄存器改为目标寄存器的地址空间,这样,线程的所有有关内存的操作,操作的都是目标进程的地址空间。

线程同步

一个线程可以等待一个对象或多个对象,而一个对象也可以同时被N个线程等待,所以线程与等待对象之间是多对多的关系,他们之间的等待关系由一个队列和一个 等待块 来控制。

WaitForSingleObject可以等待那些 可等待对象(进程、线程、作业、文件对象、IO完成端口、可等待定时器、互斥、事件、信号量等)。
可等待对象又分为

  • 可直接等待对象(存在 DISPATCHER_HEADER),比如:互斥、事件、信号量、进程、线程
  • 可间接等待对象 (不存在 DISPATCHER_HEADER),比如:文件

猜你喜欢

转载自blog.csdn.net/Simon798/article/details/108631459