系列文章目录

[笔记]Windows核心编程《一》错误处理、字符编码
[笔记]Windows核心编程《二》内核对象
[笔记]Windows核心编程《三》进程
[笔记]Windows核心编程《四》作业
[笔记]快乐的LInux命令行《五》什么是shell
[笔记]Windows核心编程《五》线程基础
[笔记]Windows核心编程《六》线程调度、优先级和关联性
[笔记]Windows核心编程《七》用户模式下的线程同步
[笔记]Windows核心编程《八》用内核对象进行线程同步
[笔记]Windows核心编程《九》同步设备I/O和异步设备I/O
[笔记]Windows核心编程《十一》Windows线程池
[笔记]Windows核心编程《十二》纤程
[笔记]Windows核心编程《十三》windows内存体系结构
[笔记]Windows核心编程《十四》探索虚拟内存
[笔记]Windows核心编程《十五》在应用程序中使用虚拟内存
[笔记]Windows核心编程《十六》线程栈
[笔记]Windows核心编程《十七》内存映射文件
[笔记]Windows核心编程《十八》堆栈
[笔记]Windows核心编程《十九》DLL基础
[笔记]Windows核心编程《二十》DLL的高级操作技术
[笔记]Windows核心编程《二十一》线程本地存储器TLS
[笔记]Windows核心编程《二十二》注入DLL和拦截API
[笔记]Windows核心编程《二十三》结构化异常处理

文章目录

  • 系列文章目录
  • 前言
    • 线程栈变化的几种情况
      • 16-1 线程栈的地址空间区域最初创建的样子
      • 16-2 即将用尽的栈地址空间区域
      • 16-3 已用尽的栈地址空间区域
        • 当线程访问最后一个已预定的页面 系统抛出EXCEPTION_STACK_OVERFLOW异常
        • 当线程继续访问未预定页面时 系统会抛出访问违规异常
        • 处理STACK_OVERFLOW办法是使用SetThreadStackGuarante函数
      • 为什么系统始终不给栈地址空间最底部的页面调拨物理存储器?
      • 栈下溢(stack underflow)
  • 一、C/C++运行库的栈检查函数
  • 二、Summation示例程序
  • 总结

前言

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

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

当系统创建线程时,会为线程栈预定一块地址空间区域(每个线程都有自己的栈)并给区域调拨一些物理存储器。
默认情况系统会预定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 Underflowreturn 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
#endifvoid 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内存结构概述(转)

[笔记]Windows核心编程《十六》线程栈相关推荐

  1. [笔记]Windows核心编程《二十》DLL的高级操作技术

    系列文章目录 [笔记]Windows核心编程<一>错误处理.字符编码 [笔记]Windows核心编程<二>内核对象 [笔记]Windows核心编程<三>进程 [笔记 ...

  2. [笔记]Windows核心编程《十九》DLL基础

    系列文章目录 [笔记]Windows核心编程<一>错误处理.字符编码 [笔记]Windows核心编程<二>内核对象 [笔记]Windows核心编程<三>进程 [笔记 ...

  3. [笔记]Windows核心编程《十三》windows内存体系结构

    系列文章目录 [笔记]Windows核心编程<一>错误处理.字符编码 [笔记]Windows核心编程<二>内核对象 [笔记]Windows核心编程<三>进程 [笔记 ...

  4. Windows核心编程_远线程方式实现Dll注入

    之前有介绍过HOOK的方式注入,这次介绍以其它方式注入,而无须HOOK,要知道在Windows这个浩荡的海洋里,API就是宝藏,找到足够多的宝藏那么你就是海贼王~! 实现思路如下: 首先打开一个进程的 ...

  5. Windows核心编程 第六章 线程基础知识 (上)

    第6章 线程的基础知识 理解线程是非常关键的,因为每个进程至少需要一个线程.本章将更加详细地介绍线程的知识.尤其是要讲述进程与线程之间存在多大的差别,它们各自具有什么作用.还要介绍系统如何使用线程内核 ...

  6. Windows核心编程 第六章 线程基础知识 (下)

    6.6 线程的一些性质 到现在为止,讲述了如何实现线程函数和如何让系统创建线程以便执行该函数.本节将要介绍系统如何使这些操作获得成功. 图6 - 1显示了系统在创建线程和对线程进行初始化时必须做些什么 ...

  7. Windows核心编程 第九章 线程与内核对象的同步(下)

    9.4 等待定时器内核对象 等待定时器是在某个时间或按规定的间隔时间发出自己的信号通知的内核对象.它们通常用来在某个时间执行某个操作. 若要创建等待定时器,只需要调用C r e a t e Wa i ...

  8. [笔记]Windows核心编程《番外篇》几种常见的执行命令行方法

    文章目录 前言 WinExec 作用 实例 CreateProcess 作用 实例 System popen ShellExecute ShellExecute ShellExecuteEx 区别比较 ...

  9. Windows核心编程 第九章 线程与内核对象的同步(上)

    第9章 线程与内核对象的同步 上一章介绍了如何使用允许线程保留在用户方式中的机制来实现线程同步的方法.用户方式同步的优点是它的同步速度非常快.如果强调线程的运行速度,那么首先应该确定用户方式的线程同步 ...

最新文章

  1. 提供第三种代码生成方式——通过自定义BuildProvider为ASP.NET提供代码生成
  2. AI一分钟 | 阿里NLP技术连破两项世界纪录,玉泉一号AI试验卫星明年发射
  3. GAN在信息检索领域的应用
  4. JAVA基础:JDK、JRE、JVM的概念
  5. 记录一个前端架构的想法
  6. Diango博客--21.实现简单的全文搜索
  7. 删除单元格_VBA(实验1)用VBA 删除某列空单元格的3种方法:删除法,转移到其他列方法,数组方法...
  8. 今天走了一天, 才回到家
  9. Android 最新原生定位折腾总结科普
  10. 2017 年最受欢迎的 10个编程挑战网站
  11. Python实现批量处理扫描特定目录
  12. linux系统查看usb转串口驱动,Linux下使用USB转串口驱动的方法
  13. 领航机器人广告段子_医院机器人物流科技宣传广告语_段子网收录最新段子
  14. 傅里叶变换之掐死教程
  15. 一文了解CDN加速服务
  16. 《软件工程》-用户界面设计
  17. Spring错误——Spring 注解——factory-bean reference points back to the same bean definition
  18. 反爬虫SSL TLS指纹识别和绕过JA3算法.md
  19. Echars象形图、3D柱状图基本实现
  20. 四均线交易系统(Four Set of MA Crossover System)

热门文章

  1. 这个世界有病,我们都有病
  2. 为什么说电销企业需要智能电销系统原因?
  3. redisson究极爽文-手把手带你实现redisson的发布订阅,消息队列,延迟队列(死信队列),(模仿)分布式线程池
  4. poi word设置字体背景颜色(也叫底纹)
  5. PDF如何插入空白页面,来教你试试这种方法
  6. 【unity-Max】A polygon of Mesh ‘XXX‘ in Assets/XXX/XXX.FBX is self-intersecting and has been discarded
  7. 开源项目贡献者_如何管理开源项目的临时贡献者
  8. 使用nexus-3.0.2-02-win64搭建自己的Maven nexus私服
  9. 轩逸酷我音乐显示服务器错误,第14代轩逸车机系统体验:基本满足日常需求 随车流量模糊不清...
  10. 柱形图如何在每根柱子上都显示数据