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

简介: Windows核心编程《十六》线程栈

系列文章目录



[笔记]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 线程栈的地址空间区域最初创建的样子

图片.png

在基地址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 即将用尽的栈地址空间区域

图片.png


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

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


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

图片.png

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


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

图片.png

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


如果线程在引发栈异常以后继续使用栈,会用尽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

图片.png


二、Summation示例程序


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

源码地址


总结


   1.访问越界和栈溢出异常,栈溢出由于访问到保护页面,所以会抛出栈溢出异常,如果继续访问会抛出访问越界异常;栈下溢不一定导致访问越界,取决于访问空间是否调拨了,调拨了则不会导致,反之。上周遇到memset初始化时 地址空间算错,导致随机崩溃。

  2. 堆:堆是向高地址扩展的数据结构

  3. 栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。

   4.线程栈的大小是由可以由编译器参数和链接器参数设置的,一般Windows 1M或2M。

   5.系统不给栈地址空间最底部的页面调拨物理存储器是为了保护进程使用的其他数据,使它们不会因为意外的内存写越界而遭到破坏。


相关:

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

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

windows内存结构概述(转)

相关文章
|
15天前
|
安全 Java 数据处理
Python网络编程基础(Socket编程)多线程/多进程服务器编程
【4月更文挑战第11天】在网络编程中,随着客户端数量的增加,服务器的处理能力成为了一个重要的考量因素。为了处理多个客户端的并发请求,我们通常需要采用多线程或多进程的方式。在本章中,我们将探讨多线程/多进程服务器编程的概念,并通过一个多线程服务器的示例来演示其实现。
|
17天前
|
安全 Java 开发者
深入理解Java并发编程:线程安全与性能优化
【4月更文挑战第9天】本文将深入探讨Java并发编程的核心概念,包括线程安全和性能优化。我们将详细解析Java中的同步机制,包括synchronized关键字、Lock接口以及并发集合等,并探讨它们如何影响程序的性能。此外,我们还将讨论Java内存模型,以及它如何影响并发程序的行为。最后,我们将提供一些实用的并发编程技巧和最佳实践,帮助开发者编写出既线程安全又高效的Java程序。
23 3
|
20天前
|
Java 调度
Java并发编程:深入理解线程池的原理与实践
【4月更文挑战第6天】本文将深入探讨Java并发编程中的重要概念——线程池。我们将从线程池的基本原理入手,逐步解析其工作过程,以及如何在实际开发中合理使用线程池以提高程序性能。同时,我们还将关注线程池的一些高级特性,如自定义线程工厂、拒绝策略等,以帮助读者更好地掌握线程池的使用技巧。
|
20天前
|
设计模式 安全 Java
Java并发编程实战:使用synchronized关键字实现线程安全
【4月更文挑战第6天】Java中的`synchronized`关键字用于处理多线程并发,确保共享资源的线程安全。它可以修饰方法或代码块,实现互斥访问。当用于方法时,锁定对象实例或类对象;用于代码块时,锁定指定对象。过度使用可能导致性能问题,应注意避免锁持有时间过长、死锁,并考虑使用`java.util.concurrent`包中的高级工具。正确理解和使用`synchronized`是编写线程安全程序的关键。
|
23天前
|
Java
深入理解Java并发编程:线程池的应用与优化
【4月更文挑战第3天】 在Java并发编程中,线程池是一种重要的资源管理工具,它能有效地控制和管理线程的数量,提高系统性能。本文将深入探讨Java线程池的工作原理、应用场景以及优化策略,帮助读者更好地理解和应用线程池。
|
18天前
|
Java
Java 并发编程:深入理解线程池
【4月更文挑战第8天】本文将深入探讨 Java 中的线程池技术,包括其工作原理、优势以及如何使用。线程池是 Java 并发编程的重要工具,它可以有效地管理和控制线程的执行,提高系统性能。通过本文的学习,读者将对线程池有更深入的理解,并能在实际开发中灵活运用。
|
16天前
|
调度 Windows
|
15天前
|
安全 算法 Java
深入理解Java并发编程:线程安全与性能优化
【4月更文挑战第11天】 在Java中,高效的并发编程是提升应用性能和响应能力的关键。本文将探讨Java并发的核心概念,包括线程安全、锁机制、线程池以及并发集合等,同时提供实用的编程技巧和最佳实践,帮助开发者在保证线程安全的前提下,优化程序性能。我们将通过分析常见的并发问题,如竞态条件、死锁,以及如何利用现代Java并发工具来避免这些问题,从而构建更加健壮和高效的多线程应用程序。
|
19天前
|
Java
Java并发编程:深入理解线程池
【4月更文挑战第7天】在现代软件开发中,多线程编程已经成为一种不可或缺的技术。为了提高程序性能和资源利用率,Java提供了线程池这一强大工具。本文将深入探讨Java线程池的原理、使用方法以及如何根据实际需求定制线程池,帮助读者更好地理解和应用线程池技术。
15 0
|
1天前
|
API C++ Windows
windows编程入门_链接错误的配置
windows编程入门_链接错误的配置
7 0