读书笔记_栈的创建过程

来源:互联网 发布:排版软件免费下载 编辑:程序博客网 时间:2024/06/09 18:48

首先看内核栈的创建过程:PspCreateThread函数是windows内核中用于创建线程的一个重要的内部函数,无论是创建系统线程(PsCreateSystemThread)还是用户线程(NtCreateThread),都离不开这个函数,除了创建重要的ETHREAD结构,PspCreateThread函数的另一个重要的任务是创建内核态栈。

对于GUI线程,Windows会为其创建大内核栈,但是在创建线程时,所有的线程都不是GUI线程,而windows是在线程第一次调用windows子系统内核服务(Win32K)时将其转变为GUI线程。因此当创建线程时,PspCreateThread函数总是调用MmCreateKernelStack函数创建一个默认大小的内核态栈,这个栈的大小是固定的,而且不是可增长的。

当一个线程被转化为GUI线程时,系统的PsConvertToGuiThread函数会为该线程重新创建一个栈,然后使用KeSwitchKernelStack切换到新的栈,新的栈是可以改变大小的,被称为大内核栈(Large Kernel Stack)。大内核态栈的最大值记录在名为MmLargeStackSize的全局变量中。

另外两个全局变量MmLargeStacks和MmSmallStacks分别用来记录系统中大型栈和小型栈的总个数。

在一个线程被转变为GUI线程后,其KTHREAD结构的LargeStack字段会被改为1,同时其Win32Thread字段也会由0变为非0(这可以作为判断是否是GUI线程的一个依据)。转换后,新的栈通常不会立即增大,而是当需要时,调用MmGrowKernelStack函数来增长栈,每次增长的幅度至少为一个页面的大小(x86中为4KB)。内核态的代码(如驱动程序)可以调用IoGetStackLimits函数和IoGetRemainingStackSize函数分别得到当前栈的边界和剩余大小。

来看用户态栈的创建过程:Windows线程的用户态栈是由KERNEL32.DLL中的BaseCreateStack函数创建的。进程中的初始线程的调用过程如下:

 

CreateProcess

NtCreateProcess

BaseCreateStack

NtCreateThread

 

 


初始线程之外的其他线程通常是调用CreateThread或CreateRemoteThread函数创建的。实际上,CreateThread函数只是简单地调用CreateRemoteThread函数,CreateRemoteThread函数在调用内核服务NtCreateThread前,会调用BaseCreateStack函数来创建用户态栈。

BaseCreateStack是位于KERNEL32.DLL中一个未公开的函数,其原型大致如下:

NTSTATUS BaseCreateStack(IN HANDLE hProcess,

IN DWORD dwCommitStackSize, IN DWORD dwReservedStackSize,

OUT OINITIAL_TEB pInitialTeb)

简单来说,BaseCreateStack函数就是在hProcess所指定的进程空间中根据dwReserverdStackSize参数保留一段内存区域,并在这个区域中按照dwCommitStackSize参数所指定的大小提交出一部分作为栈的初始空间。BaseCreateStack函数将所保留和提交内存区域的参数保存到pInitialTeb指向的结构中。而后,这些参数会传递给NtCreateThread内核服务,最终被保存到线程的环境块(TEB)结构中。可以使用!teb命令来观察TEB中包含的栈信息。

CreateThread与CreateRemoteThread的原型如下:

BOOL CreateThread(

   DWORD dwCreateFlags = 0,

   UINT nStackSize = 0,

   LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL

);

HANDLE WINAPI CreateRemoteThread(

  __in   HANDLE hProcess,

  __in   LPSECURITY_ATTRIBUTES lpThreadAttributes,

  __in   SIZE_T dwStackSize,

  __in   LPTHREAD_START_ROUTINE lpStartAddress,

  __in   LPVOID lpParameter,

  __in   DWORD dwCreationFlags,

  __out  LPDWORD lpThreadId

);

这两个函数可以通过参数dwStackSize指定栈的保留大小(需要在参数dwCreationFlags中设置STACK_SIZE_PARAM_IS_A_RESERVATION标志)初始提交大小。栈的初始大小是在PE文件的结构IMAGE_OPTIONAL_HEADER中,其中的SizeOfStackReserve和SizeOfStackCommit是用来指定栈的默认保留大小和提交大小。即使当我们调用CreateThread时将dwStackSize设为0,系统也会使用IMAGE_OPTIONAL_HEADER中指定的大小。SizeofStackReserve和SizeOfStackCommit字段的值是连接程序(Linker)在生成EXE文件时根据连接选项写入的。对于VC编译器,可以通过编译选项 STACKSIZE reserve [, commit] 来设置。

对于链接好的EXE文件,还可以通过EDITBIN工具来修改这两个值。(EDITBIN本人没有使用过,WinXP操作系统下没有现成的Editbin命令。需要安装Visual Studio才能获得这个命令。可以在主Visual Studio目录的VC98BIN目录下找到它。Editbin的用法可以参见MSDN:http://msdn.microsoft.com/library/chs/default.asp?url=/library/CHS/vccore/html/_core_editbin_options.asp)

保留和提交内存都是通过系统的虚拟内存分配函数来完成的,SDK中公开了VirtualAlloc和VirtualAllocEx API,事实上它们都是调用了内核服务NtAllocateVirtualMemory

BaseCreateStack创建用户态栈的过程主要有以下几个步骤:

1.  将提交空间大小取为内存页大小的倍数,将总保留大小取整为内存分配的最小粒度(4字节)

2.  调用内存分配函数(NtAllocateVirtualMemory)保留内存地址空间,内存分配类型为MEM_RESERVE,分配的大小为栈空间保留大小。

3.  调用NtAllocateVirtualMemory在保留空间的高地址端提交初始栈空间,内存分配类型为MEM_COMMIT,分配的大小为初始提交大小

4.  如果保留空间大于初始提交空间,则第3步会多提交一个页面用作栈保护页面。保护的方法是调用虚拟内存保护函数(VirtualProtect)对这个页面设置PAGE_GUARD属性。

 

原创粉丝点击