进程的地址空间
以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),比如:文件