[笔记]Windows核心编程《十六》线程栈

简介: [笔记]Windows核心编程《十六》线程栈

前言

系统会在用户进程的地址控件中预定区域情况:

  • 分配进程环境块
  • 分配线程环境块

当系统创建线程时,会为线程栈预定一块地址空间区域(每个线程都有自己的栈)并给区域调拨一些物理存储器。

默认情况系统会预定1MB的地址空间并调拨两个页的存储器。

(进程最大地址空间大小Windows平台似乎是2G)

开发人员可以通过两种方式改变该默认值:

  • 可以通过MSVC++编译器的/F选项
  • 使用MSVC++ 链接器的/STACK选项:/Freserve /STACK:reserve[,commit]

链接器会将想要的栈大小写入exe或dll的PE文件头中。系统要创建线程栈的时候会根据PE文件头的大小类预定地址空间。

但是在调用CreateThread或_beginthreadexh函数也可以指定需要在一开始调拨的存储器数量(即栈初始大小)。这两个函数都有一个参数,可以用来指定调拨给线程栈的地址空间区域的存储器大小,如果传递0则使用PE文件头中默认定义的大小。后面我们假设都是使用的默认值:即区域大小为1MB,每次调拨一个存储页面。

线程栈变化的几种情况

16-1 线程栈的地址空间区域最初创建的样子

在基地址0x08000000 上预定的栈空间。

栈地址空间和所有已经调拨的物理存储器都具有PAGE_READWRITE保护属性。

系统会给最高地址(顶部)的两个页面调拨物理存储器。

区域顶部往下的第二个页面被称为 防护页面(guard page) 随着线程调用越来越多的函数,调用树也越来越深,线程也需要越来越多的栈空间。

当线程试图访问防护页面中的内存时,系统会得到通知。

系统会给防护页下面的那个页面调拨存储器 ,接着除去当前防护页面的PAGE_GUARD保护属性,然后给刚调拨的存储页指定PAGE_GUARD保护属性。

如果线程栈不断加深,那么栈的地址空间区域看起来像16-2

栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在WINDOWS下,栈的大小是2M(也可能是1M,它是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小

堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。

参考来源:https://blog.csdn.net/willib/article/details/21086207

16-2 即将用尽的栈地址空间区域

当调用接近栈底的时候,系统会给栈底的前的最后一个页面调拨物理存储器。同时去除其PAGE_GUARD保护属性。

但是此时系统不会再给栈底的页面调拨物理存储器并设置PAGE_GUARD 如16-3。

16-3 已用尽的栈地址空间区域

当线程访问最后一个已预定的页面 系统抛出EXCEPTION_STACK_OVERFLOW异常

当系统给0x0800100的页面调拨物理存储器的时候(即最后一个已预定的页面时),会执行一个额外操作-抛出EXCEPTION_STACK_OVERFLOW 值为0xC00000FD 可以使用SEH来捕获并处理。

当线程继续访问未预定页面时 系统会抛出访问违规异常

如果线程在引发栈异常以后继续使用栈,会用尽0x8001000的页面中的内存,并试图访问0x8000000页面中的内存。

当线程试图访问未预定的页面,系统会抛出访问违规异常。

此时控制权会交给Windows Error Reporting service 并弹出一个框,然后结束当前进程。

处理STACK_OVERFLOW办法是使用SetThreadStackGuarante函数

为了避免该情况应该调用SetThreadStackGuarante函数,来抛出EXCEPTION_STACK_OVERFLOW 可以确保在Windows错误报告服务接管并终止进程之前,地址空间还有指定数量的内存可以使用,这使得应用程序能处理栈异常并恢复运行。

当线程访问最后一个防护页面时,系统会抛出EXCEPTION_STACK_oVERFLOW异常。如果线程捕获了该异常并继续执行,那么系统将不会在同一个线程中再次抛出EXCEPTION_STACK_OVERFLOW异常,这是因为后面再也没有防护页面了.

如果希望在同一线程中继续收到EXCEPTION_STACK_OVERFLOW异常,那么应用程序必须重置防护页面。这很容易办到,只需调用C运行库的_resetstkoflw 函数(在malloc.h中定义).

为什么系统始终不给栈地址空间最底部的页面调拨物理存储器?

这样做的目的是为了保护进程使用的其他数据,使它们不会因为意外的内存写越界而遭到破坏。

栈下溢(stack underflow)

看下面代码:

int WINPAI WinMain(HINSTANCE hInstExe,HINSTANCE,PTSTRpszCmdLine,int nCmdShow){
  BYTE aBytes[100];
  aBytes[10000] = 0;//Stack Underflow
  return 0;
}

代码试图访问线程栈之外的内存,编译器和链接器无法发现这类错误。

这条语句可能会引发访问违规,也可能不会, 因为紧接着线程栈后面可能有另一块已调拨的地址空间区域。如果发生这种情况,程序可能会破坏属于进程的另一部分内存,而系统是无法检测到这种错误的。

偶然间 我也遇到过这种破坏内存的情况,使用memset时初始化内存区域算错。导致破坏内存,随机崩溃。这种例子不胜枚举。

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

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

void SomeFunction() {
  int nValues[4000];
  // Do some processing with the array.
  nValues[0] = 0; // Some assignment
}

该函数至少需要16000字节(4000x sizeof(int))通常情况下编译器生成的用来分配栈空间的代码会直接把cpu的指针减去16000字节。除非程序试图访问其中的数据,否则系统是不会给这块区域调拨物理存储器的。

在页面大小为4KB或8KB的系统中,这个限制可能会产生问题。如果第一次访问的地址要低于防护页面(例如前面的赋值语句所示),线程会访问尚未调拨的内存并引发访问违规。为了确保开发人员编写的类似代码能够正常运行,编译器需要插入一些代码来调用C运行库的检查函数。编译器知道目标系统的页面大小,并且在处理程序的每个函数时,能算出函数需要多大的栈空间。如果需要的占空间大于目标系统的页面大小,编译器会自动插入代码来调用检查函数。

以下伪代码展示了检查函数到底做了什么事。(通常由编译器使用汇编语言来实现)

// The C run-time library knows the page size for the target system.
#ifdef _M_IA64
#define PAGESIZE (8 * 1024)   // 8-KB page
#else
#define PAGESIZE (4 * 1024)   // 4-KB page
#endif
void StackCheck(int nBytesNeededFromStack) {
  // Get the stack pointer position.
  // At this point, the stack pointer has NOT been decremented
  // to account for the function's local variables.
  PBYTE pbStackPtr = (CPU's sack pointer);
  //
  while (nBytesNeededFromStack >= PAGESIZE) {
    // Move down a page on the stack-- should be a gurad page.
    pbStackPtr -= PAGESIZE;
    // Access a byte on the gurad page--force new page to be
    // committed and gurad page to move down a page.
    pbStackPtr[0] = 0;
    // Reduce the number of bytes needed from the stack.
    nBytesNeededFromStack -= PAGESIZE;
  }
  // Before returning, the StackCheck function sets the CPU's
  // stack pointer to the address below the function's
  // local variables.
}

编译器会根据目标平台的页面大小自动插入代码来调用StackCheck。为了让开发人员对所使用页面大小的阈值进行控制,VC++提供了/Gs编译器开关。详细参考MSDN

二、Summation示例程序

演示如何使用SEH处理程序来使的程序从栈溢出中的得体的恢复并继续运行。

源码地址

总结

  1. 访问越界和栈溢出异常,栈溢出由于访问到保护页面,所以会抛出栈溢出异常,如果继续访问会抛出访问越界异常;栈下溢不一定导致访问越界,取决于访问空间是否调拨了,调拨了则不会导致,反之。上周遇到memset初始化时 地址空间算错,导致随机崩溃。
  2. 堆:堆是向高地址扩展的数据结构
  3. 栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。
  4. 线程栈的大小是由可以由编译器参数和链接器参数设置的,一般Windows 1M或2M。
  5. 系统不给栈地址空间最底部的页面调拨物理存储器是为了保护进程使用的其他数据,使它们不会因为意外的内存写越界而遭到破坏。

相关:

参考部分sesiria大佬的《Windows核心编程》读书笔记十六 线程栈

进程线程及堆栈之间内存分配和关系总结

windows内存结构概述(转)


相关文章
|
7天前
|
安全 Java 调度
Java编程时多线程操作单核服务器可以不加锁吗?
Java编程时多线程操作单核服务器可以不加锁吗?
21 2
|
7天前
|
Windows
[原创]用MASM32编程获取windows类型
[原创]用MASM32编程获取windows类型
|
7天前
|
JavaScript 前端开发 API
MASM32编程通过WMI获取Windows计划任务
MASM32编程通过WMI获取Windows计划任务
|
6天前
|
网络协议 API Windows
MASM32编程调用 API函数RtlIpv6AddressToString,windows 10 容易,Windows 7 折腾
MASM32编程调用 API函数RtlIpv6AddressToString,windows 10 容易,Windows 7 折腾
|
7天前
|
Java
COMATE插件实现使用线程池高级并发模型简化多线程编程
本文介绍了COMATE插件的使用,该插件通过线程池实现高级并发模型,简化了多线程编程的过程,并提供了生成结果和代码参考。
|
8天前
|
API Windows
MASM32编程获取Windows当前桌面主题名
MASM32编程获取Windows当前桌面主题名
|
9天前
|
Java 数据处理 调度
Java中的多线程编程:从基础到实践
本文深入探讨了Java中多线程编程的基本概念、实现方式及其在实际项目中的应用。首先,我们将了解什么是线程以及为何需要多线程编程。接着,文章将详细介绍如何在Java中创建和管理线程,包括继承Thread类、实现Runnable接口以及使用Executor框架等方法。此外,我们还将讨论线程同步和通信的问题,如互斥锁、信号量、条件变量等。最后,通过具体的示例展示了如何在实际项目中有效地利用多线程提高程序的性能和响应能力。
|
9天前
|
安全 算法 Java
Java中的多线程编程:从基础到高级应用
本文深入探讨了Java中的多线程编程,从最基础的概念入手,逐步引导读者了解并掌握多线程开发的核心技术。无论是初学者还是有一定经验的开发者,都能从中获益。通过实例和代码示例,本文详细讲解了线程的创建与管理、同步与锁机制、线程间通信以及高级并发工具等主题。此外,还讨论了多线程编程中常见的问题及其解决方案,帮助读者编写出高效、安全的多线程应用程序。
|
2月前
|
存储 监控 Java
Java多线程优化:提高线程池性能的技巧与实践
Java多线程优化:提高线程池性能的技巧与实践
64 1
|
7天前
|
数据采集 负载均衡 安全
LeetCode刷题 多线程编程九则 | 1188. 设计有限阻塞队列 1242. 多线程网页爬虫 1279. 红绿灯路口
本文提供了多个多线程编程问题的解决方案,包括设计有限阻塞队列、多线程网页爬虫、红绿灯路口等,每个问题都给出了至少一种实现方法,涵盖了互斥锁、条件变量、信号量等线程同步机制的使用。
LeetCode刷题 多线程编程九则 | 1188. 设计有限阻塞队列 1242. 多线程网页爬虫 1279. 红绿灯路口