《windows核心编程》- 线程栈

简介:

当系统创建线程的时候,会为线程栈预订一块地址空间区域,并给该区域调拨一些物理存储器。默认会预订1MB的地址空间并调拨两个页面的存储器。但是在构建 应用程序的时候可以改变这个默认值

在构建应用程序的时候链接器会把栈的大小写入到exe和dll文件的pe文件头中,当创建线程的时候会根据PE文件头中的大小来预订空间区域。在调用CreateThread或_beginthreadex的时候开发人员可以指定需要在一开始就调拨的地址空间大小和存储器大小。

下面显示了一台页面大小为4KB的机器上线程栈的地址空间区域(基址为0x08100000 )。该线程栈的地址空间区域和所调拨给该区域的都具有PAGE_READWRITE保护属性。

image

在预订地址空间后,系统会给区域顶部的两个页面调拨物理存储器。在线程开始之前系统会把线程栈的指针指向最上面那两个页面的末尾。这个页面就是线程开始使用栈的地方。区域顶部往下的第二个页面被称为防护页面。随着调用的越来越多,调用树也越来越深,线程也需要越来越多的栈空间

当线程试图访问防护页中的内存时,系统会得到通知这时系统会先给防护页面下面的那个页面调拨存储器,接着去除当前防护页面的PAGE_GUARD保护属性标志,然后给刚调拨的存储页指定PAGE_GUARD保护属性标志。该项技术使得系统能够在线程需要的时候才增加栈存储器大小。如果线程的调用树 断加深,那么栈空间区域看起来会像下图这样

image

 

假设线程调用树非常深,CPu的栈指针寄存器指向的内存地址为0x08003004现在当线程调用另一个函数时,系统必须调拨更多的物理存储器。但是当系统给0x0800100的页面调拨物理存储器时,它的做法和给区域中的其他部分调拨物理存储器有所不同。

image

由上图可知,系统会除去0x08001000处的PAGE_GUARD保护属性标志,然后给地址为0x08001000的页面调拨物理存器。区别在于系统不会给刚调拨的物理存储器0x09001000指定防护属性。意味着栈的地址空间已经放满了字所能容纳得下的所有物理存储器。系统永远不会给区域底部的那个页面调拨存储器。

当系统给0x080000000调用物理存储器时,它会执行一个额外的操作:抛出EXCEPTION_STACK_OVERFLOW异常。

可以用函数SetThreadStackGuarante函数来设置抛出前面讲的EXCEPTION_STACK_OVERFLOW异常。

为什么系统始终不给栈地址空间最底部的页面调拨物理存储器。这样做目的是为了保护进程中其它数据。使他们不会因为意外的内存写越界而遭到破坏。如果栈越过了所预订的区域,那么线程就会覆盖进程地址空间中其它数据。

另一种很难找到的缺陷是 栈下溢 看下面代码

 

复制代码
int WINAPI WinMain(HINSTANCE hInstExe,HINSTANCE ,PTSTR pszCmdLine,int nCmdShow)
{
    BYTE aBytes[100];
    aBytes[10000] = 0; //stack underflow
    
    return 0;
}
复制代码

这段代码试图访问线程栈之外的内存。编译器和链接器无法发现代码中的此类错误。这条语句可能引发访问违规,也可能不会(如果访问的刚好是被调拨的话),发生这种情况程序可能会破坏进程另外的一部分内存,而系统是无法检测到的。下面代码中的栈一溢总会引起内存破坏,因为程序刚好在线程栈的后面分配了一块内存。

复制代码
DWORD WINAPI ThreadFunc(PVOID pvParam)
{
    BYTE aBytes[0x10];
    
    MEMORY_BASIC_INFOMATION mbi;
    SIZE_T size = VirtualQuery(aBytes,&mbi,sizeof(mbi));
    SIZE_T s = (SIZE_T) mbi.AllocationBase + 1024 * 1024;
    PBYTE pAddress = (PBYTE) s;
    
    BYTE * pBytes = (BYTE*) VirtualAlloc(pAddress,0x10000,MEM_COMMIT | MEM_RESERVE,PAGE_READWRITE);
    aBytes[0x1000] = 1; //Write in the allocated block,past the stack
    return 0
}
复制代码

16.1 C/C++运行库的栈检查函数

c++运行库中有一个栈检查函数。在编译代码时,编译器会在必要时生成代码来调用该函数。这个函数的目的是为了确保已经给线程栈调拨了物理存储器。

相关文章
|
6月前
|
调度 Windows
|
3月前
|
Java Windows
【Azure Developer】Windows中通过pslist命令查看到Java进程和线程信息,但为什么和代码中打印出来的进程号不一致呢?
【Azure Developer】Windows中通过pslist命令查看到Java进程和线程信息,但为什么和代码中打印出来的进程号不一致呢?
|
4月前
|
Java 运维
开发与运维命令问题之使用jstack命令查看Java进程的线程栈如何解决
开发与运维命令问题之使用jstack命令查看Java进程的线程栈如何解决
66 2
|
4月前
|
云计算
云计算问题之线程请求的栈深度大于虚拟机所允许的深度如何解决
云计算问题之线程请求的栈深度大于虚拟机所允许的深度如何解决
31 1
|
5月前
|
存储 Java C++
Java虚拟机(JVM)管理内存划分为多个区域:程序计数器记录线程执行位置;虚拟机栈存储线程私有数据
Java虚拟机(JVM)管理内存划分为多个区域:程序计数器记录线程执行位置;虚拟机栈存储线程私有数据,如局部变量和操作数;本地方法栈支持native方法;堆存放所有线程的对象实例,由垃圾回收管理;方法区(在Java 8后变为元空间)存储类信息和常量;运行时常量池是方法区一部分,保存符号引用和常量;直接内存非JVM规范定义,手动管理,通过Buffer类使用。Java 8后,永久代被元空间取代,G1成为默认GC。
65 2
|
4月前
|
Java 开发者
Java面试题:Java内存管理精要与多线程协同策略,Java内存管理:堆内存、栈内存、方法区、垃圾收集机制等,多线程编程的掌握,包括线程创建、同步机制的原理
Java面试题:Java内存管理精要与多线程协同策略,Java内存管理:堆内存、栈内存、方法区、垃圾收集机制等,多线程编程的掌握,包括线程创建、同步机制的原理
40 0
|
4月前
|
算法 Java 开发者
Java面试题:Java内存探秘与多线程并发实战,Java内存模型及分区:理解Java堆、栈、方法区等内存区域的作用,垃圾收集机制:掌握常见的垃圾收集算法及其优缺点
Java面试题:Java内存探秘与多线程并发实战,Java内存模型及分区:理解Java堆、栈、方法区等内存区域的作用,垃圾收集机制:掌握常见的垃圾收集算法及其优缺点
39 0
|
4月前
|
存储 算法 Java
Java面试题:解释JVM的内存结构,并描述堆、栈、方法区在内存结构中的角色和作用,Java中的多线程是如何实现的,Java垃圾回收机制的基本原理,并讨论常见的垃圾回收算法
Java面试题:解释JVM的内存结构,并描述堆、栈、方法区在内存结构中的角色和作用,Java中的多线程是如何实现的,Java垃圾回收机制的基本原理,并讨论常见的垃圾回收算法
67 0
|
5月前
|
安全 API C++
逆向学习Windows篇:C++中多线程的使用和回调函数的实现
逆向学习Windows篇:C++中多线程的使用和回调函数的实现
185 0
|
C++ Windows
[笔记]Windows核心编程《番外篇》几种常见的执行命令行方法
[笔记]Windows核心编程《番外篇》几种常见的执行命令行方法
109 0
下一篇
无影云桌面