Windows核心编程016.pdf

上传人:qwe****56 文档编号:70009062 上传时间:2023-01-14 格式:PDF 页数:12 大小:1.16MB
返回 下载 相关 举报
Windows核心编程016.pdf_第1页
第1页 / 共12页
Windows核心编程016.pdf_第2页
第2页 / 共12页
点击查看更多>>
资源描述

《Windows核心编程016.pdf》由会员分享,可在线阅读,更多相关《Windows核心编程016.pdf(12页珍藏版)》请在得力文库 - 分享文档赚钱的网站上搜索。

1、下载第1 6章线程的堆栈有时系统会在你自己进程的地址空间中保留一些区域。第 3章讲过,对于进程和线程环境块来说,就会出现这种情况。另外,系统也可以在你自己进程的地址空间中为线程的堆栈保留一些区域。每当创建一个线程时,系统就会为线程的堆栈(每个线程有它自己的堆栈)保留一个堆栈空间区域,并将一些物理存储器提交给这个已保留的区域。按照默认设置,系统保留 1 MB的地址空间并提交两个页面的内存。但是,这些默认值是可以修改的,方法是在你链接应用程序时设定M i c r o s o f t的链接程序的/S TA C K选项:当创建一个线程的堆栈时,系统将会保留一个链接程序的/S TA C K开关指明的地址

2、空间区域。但是,当调用C r e a t e T h r e a d或_ b e g i n t h r e a d e x函数时,可以重载原先提交的内存数量。这两个函数都有一个参数,可以用来重载原先提交给堆栈的地址空间的内存数量。如果设定这个参数为0,那么系统将使用/S TA C K开关指明的已提交的堆栈大小值。后面将假定我们使用默认的堆栈大小值,即1 MB的保留区域,每次提交一个页面的内存。图16-1显示了在页面大小为 4 KB的计算机上的一个堆栈区域的样子(保留的起始地址是0 x 0 8 0 0 0 0 0 0)。该堆栈区域和提交给它的所有物理存储器均拥有页面保护属性PA G E _R

3、E A D W R I T E。图16-1 线程的堆栈区域刚刚创建时的样子内存地址0 x080FF0000 x080FD0000 x080FD0000 x080030000 x080020000 x080010000 x08000000堆栈底部:保留页面保留页面保留页面保留页面保留页面带有保护属性标志的已提交页面页面状态堆栈顶部:已提示的页面当保留了这个区域后,系统将物理存储器提交给区域的顶部的两个页面。在允许线程启动运行之前,系统将线程的堆栈指针寄存器设置为指向堆栈区域的最高页面的结尾处(一个非常接近0 x 0 8 1 0 0 0 0 0的地址)。这个页面就是线程开始使用它的堆栈的位置。从顶

4、部向下的第二个页面称为保护页面。当线程调用更多的函数来扩展它的调用树状结构时,线程将需要更多的堆栈空间。每当线程试图访问保护页面中的存储器时,系统就会得到关于这个情况的通知。作为响应,系统将提交紧靠保护页面下面的另一个存储器页面。然后,系统从当前保护页面中删除保护页面的保护标志,并将它赋予新提交的存储器页面。这种方法使得堆栈存储器只有在线程需要时才会增加。最终,如果线程的调用树继续扩展,堆栈区域就会变成图 1 6-2所示的样子。如图 1 6-2所示,假定线程的调用树非常深,堆栈指针C P U寄存器指向堆栈内存地址0 x 0 8 0 0 3 0 0 4。这时,当线程调用另一个函数时,系统必须提交

5、更多的物理存储器。但是,当系统将物理存储器提交给0 x 0 8 0 0 1 0 0 0地址上的页面时,系统执行的操作与它给堆栈的其他内存区域提交物理存储器时的操作并不完全一样。图1 6-3显示了堆栈的保留内存区域的样子。如你预计的那样,从地址0 x 0 8 0 0 2 0 0 0开始的页面的保护属性已经被删除,物理存储器被提交给从0 x 0 8 0 0 1 0 0 0地址开始的页面。它们的差别是,系统并不将保护属性应用于新的物理存储器页面(0 x 0 8 0 0 1 0 0 0)。这意味着该堆栈已保留的地址空间区域包含了它能够包含的全部物理存储器。最底下的页面总是被保留的,从来不会被提交。下面

6、将要说明它的原因。当系统将物理存储器提交给0 x 0 8 0 0 1 0 0 0地址上的页面时,它必须再执行一个操作,即它要引发一个 E X C E P T I O N _ S TA C K _ O V E R F L O W异常处理(在 Wi n N T.h文件中定义为0 x C 0 0 0 0 0 F D)。通过使用结构化异常处理(S E H),你的程序将能得到关于这个异常处理条件的通知,并且能够实现适度恢复。关于S E H的详细说明,请参见第2 3、2 4和2 5章的内容。本章结尾处的S u m m a t i o n示例应用程序将展示如何对堆栈溢出进行适度恢复。图16-2 几乎完整的线

7、程堆栈区域386计计第三部分 内 存 管 理下载内存地址0 x080FF0000 x080FD0000 x080FD0000 x080030000 x080020000 x080010000 x08000000堆栈底部:保留页面保留页面带有保护属性标志的已提交页面已提交的页面已提交的页面已提交的页面页面状态堆栈顶部:已提交的页面图16-3 完整的线程堆栈区域如果在出现堆栈溢出异常条件之后,线程继续使用该堆栈,那么在 0 x 0 8 0 0 1 0 0 0地址上的页面中的全部内存均将被使用,同时,该线程将试图访问从 0 x 0 8 0 0 0 0 0 0开始的页面中的内存。当该线程试图访问这个保

8、留的(未提交的)内存时,系统就会引发一个访问违规异常条件。如果在线程试图访问该堆栈时引发了这个访问违规异常条件,线程就会陷入很大的麻烦之中。这时,系统就会接管控制权,并终止进程的运行不仅终止线程的运行,而切终止整个进程的运行。系统甚至不向用户显示一个消息框,整个进程都消失了!下面要说明为什么堆栈区域的最后一个页面始终被保留着。这样做的目的是为了防止不小心改写进程使用的其他数据。可以看到,在 0 x 0 7 F F 0 0 0这个地址上(0 x 0 8 0 0 0 0 0 0下面的一个页面),另一个地址空间区域已经提交了物理存储器。如果 0 x 0 8 0 0 0 0 0 0地址上的页面包含物理

9、存储器,系统将无法抓住线程访问已保留堆栈区域的尝试。如果堆栈深入到已保留堆栈区域的下面,那么线程中的代码就会改写进程的地址空间中的其他数据,这是个非常难以抓住的错误。16.1 Windows 98下的线程堆栈在Windows 98下,堆栈的行为特性与Windows 2000下的堆栈非常相似。但是它们之间存在某些重大的差别。图1 6-4显示了Windows 98下1 MB的堆栈的各个区域的样子(从0 x 0 0 5 3 0 0 0 0地址上开始保留)。首先请注意,尽管我们想要创建的堆栈大小最大只有 1 MB,但是堆栈区域的大小实际上是1 MB加128 KB。在Windows 98中,每当为一个堆

10、栈保留一个区域时,系统保留的区域实际上比要求的尺寸要大128 KB。该堆栈位于该区域的中间,堆栈的前面有一个64 KB的块,堆栈的后面是另一个64 KB的块。第 1 6章线程的堆栈计计387下载内存地址0 x080FF0000 x080FE0000 x080FD0000 x080030000 x080020000 x080010000 x08000000堆栈底部:保留页面已提交的页面已提交的页面已提交的页面已提交的页面已提交的页面页面状态堆栈顶部:已提交的页面图16-4 Windows 98下线程的堆栈区域刚刚创建时的样子堆栈开始处的64 KB用于抓取堆栈的溢出条件,而堆栈后面的 64 KB则

11、用于抓取堆栈的下溢条件。若要了解为什么需要检测堆栈下溢条件,请看下面这个代码段:当该函数的赋值语句执行时,便尝试访问线程堆栈结尾处之外的内存。当然,编译器和链接程序不会抓住上面代码中的错误,但是,如果应用程序是在 Windows 98下运行,那么当该语句执行时,就会引发访问违规。这是Windows 98的一个出色特性,而Windows 2000是没有的。在Wi n d o w s2 0 0 0中,可以在紧跟线程堆栈的后面建立另一个区域。如果出现这种情况,并且你试图访问你的堆栈外面的内存,那么你将会破坏与进程的另一个部分相关的内存,而系统将不会发现这个情况。需要指出的第二个重要差别是,没有一个页

12、面具有 PA G E _ G U A R D保护属性标志。由于Windows 98不支持这个标志,所以它使用一个不同的方法来扩展线程的堆栈。Windows 98将紧靠堆栈下面的已提交页面标记为PA G E _ N O A C C E S S保护属性(图1 6-4中的地址0 x 0 0 6 3 E 0 0 0)。然后,当线程接触读/写页面下面的页面时,将会发生访问违规。系统抓住这个访问违规,将不能访问的页面改为读写页面,并提交前一个保护页面下面的一个新保护页面。第三个应该注意的差别是图1 6-4中的0 x 0 0 6 3 7 0 0 0地址上的单个PA G E _ R E A D W R I T

13、 E内存页面。这个页面是为了实现与 1 6位Wi n d o w s相兼容而存在的。虽然 M i c r o s o f t从未将它纳入文档,但是开发人员发现1 6位应用程序的堆栈段(S S)开始处的1 6个字节包含了关于1 6位应用程序的堆栈、本地堆栈和本地原子表的信息。由于在 Windows 98上运行的Wi n 3 2应用程序常常调用1 6位D L L组件,有些1 6位组件认为这些信息可以在堆栈段的开始处得到,因此 M i c r o s o f t不得不在Windows 98中仿真这些字节的设置。当3 2位代码转换为1 6位代码时,Windows 98 将把一个1 6388计计第三部分

14、 内 存 管 理下载内存地址0 x006400000 x0063F0000 x0063E0000 x006380000 x006370000 x0065400000 x00530000大小16页面(65 536字节)1页面(4096字节)1页面(4096字节)6页面(24 576字节)1页(4096字节)247页面(1 011 712字节)16页面(65 536字节)页面状态堆栈顶部:保留供堆栈下溢时使用带有PA G E _ R E A D W R I T E保护属性的已提交页面,堆栈在用仿真PA G E _ G U A R D标志的PA G E _ N O _ACCESS页面保留供堆栈溢出时

15、使用的页面保留页面,供堆栈扩展时使用带有PAGE_READ_ WRITE保护属性的已提交页面,用于与16位组件相兼容堆栈底部:保留供堆栈溢出时使用位C P U选择器映射到3 2位堆栈,并且将堆栈段寄存器设置为指向0 x 0 0 6 3 7 0 0 0地址上的页面。这时该1 6位代码就可以访问堆栈段的开始处的1 6个字节,并且可以继续运行而不会出任何问题。现在,当Windows 98扩大它的线程堆栈时,它将继续扩大 0 x 0 0 6 3 F 0 0 0地址上的内存块。它也会不断地将保护页面下移,直到 1 MB的堆栈内存被提交为止。然后保护页面消失,就像在Windows 2000下运行的情况一样

16、。系统还继续为了1 6位Wi n d o w s组件的兼容性而将页面下移,最后该页面将进入堆栈区域开始处的64 KB的内存块中。因此,Windows 98中一个完全提交的堆栈将类似图1 6-5所示的样子。图16-5 Windows 98下的一个完整的线程堆栈区域16.2 C/C+运行期库的堆栈检查函数C/C+运行期库包含一个堆栈检查函数。当编译源代码时,编译器将在必要时自动生成对该函数的调用。堆栈检查函数的作用是确保页面被适当地提交给线程的堆栈。下面让我们来看一个例子。这是一个小型函数,它需要相当多的内存用于它的局部变量:该函数至少需要16 000个字节(4000 x sizeof(int),

17、每个整数是4个字节)的堆栈空间,以便放置整数数组。通常情况下,编译器生成的用于分配该堆栈空间的代码只是将 C P U的堆栈指针递减16 000个字节。但是,在程序试图访问内存地址之前,系统并不将物理存储器分配给堆栈区域的这个较低区域。在使用4 KB或8 KB页面的系统上,这个局限性可能导致一个问题出现。如果初次访问堆栈是在低于保护页面的一个地址上进行的(如上面这个代码中的赋值行所示),那么线程将访问已经保留的内存并且引发访问违规。为了确保能够成功地编写上面所示的函数,编译器将插入对C运行期库的堆栈检查函数的调用。当编译程序时,编译器知道你针对的 C P U系统的页面大小。x 8 6编译器知道页

18、面大小是 4K B,A l p h a编译器知道页面大小是8 KB。当编译器遇到程序中的每个函数时,它能确定该函数第 1 6章线程的堆栈计计389下载内存地址0 x006400000 x005400000 x005390000 x005380000 x00538000大小16页面(65 536字节)256页面(1MB)7页面(28 672字节)1页面(4096字节)8页面(32 768字节)页面状态堆栈顶部:保留供堆栈下溢时使用带有PA G E_R E A D W R I T E保护属性的已提交页面,堆栈在用供堆栈溢出时使用的保留页面堆栈底部:保留供堆栈溢出时使用带有PA G E _ R E

19、A D W R I T E保护属性的已提交页面,用于与16位组件相兼容需要的堆栈空间的数量。如果该函数需要的堆栈空间大于目标系统的页面大小,编译器将自动插入对堆栈检查函数的调用。下面这个伪代码显示了堆栈检查函数执行什么操作。之所以称它是伪代码,是因为这个函数通常是由编译器供应商用汇编语言来实现的:Microsoft 的Visual C+确实提供了一个编译器开关,使你能够控制一个页面大小的阈值,这个阈值可供编译器用来确定何时添加对 S t a c k C h e c k函数的自动调用。只有当确切地知道究竟在进行什么操作并且有着特殊需要时,才能使用这个编译器开关。对于绝大多数应用程序和D L L来

20、说,都不应该使用这个开关。16.3 Summation示例应用程序本章后面清单1 6-1中的S u m m a t i o n(“16 Summation.exe”)示例应用程序展示了如何使用异常过滤器和异常处理程序以便对堆栈溢出进行适度恢复的方法。该应用程序的源代码和资源文件均位于本书所附光盘上的1 6-S u m m a t i o n目录下。若要全面了解该应用程序是如何运行的,可以参见关于S E H的有关章节。S u m m a t i o n应用程序用于计算从 0到x的全部数字的总和,其中 x是用户输入的一个数字。当然,进行这项操作的最简单的方法是创建一个称为S u m的函数,它只是进

21、行下面的计算:然而对于这个例子来说,我将S u m编写为一个递归函数,这样,如果输入较大的数字,它将使用大量的堆栈空间。当程序启动运行时,它将显示图1 6-6所示的对话框。390计计第三部分 内 存 管 理下载在这个对话框中,你将一个数字输入编辑控件,然后单击 C a l c u l a t e按钮。这使程序创建一个新线程,该线程的唯一作用是将 0到x的全部数字进行相加。当这个新线程运行时,程序的主线程通过调用Wa i t F o r S i n g l e O b j e c t函数并传递新线程的句柄,等待线程运行的结果。当新线程运行终止时,系统就唤醒主线程。主线程取出合计的总数,方法是调用

22、G e t E x i t C o d e T h r e a d函数来获得新线程的退出代码。最后,最重要的一点是,主线程要关闭新线程的句柄,这样,系统就能完全撤消线程对象,并且使应用程序不会出现资源的泄漏。这时,主线程要查看合计线程的退出代码。退出代码 U N I T _ M A X指明出现了一个错误,即合计线程在计算数字的总数时产生了堆栈溢出,为此,主线程显示一个消息框,以说明这个情况。如果退出代码不是 U N I X _ M A X,那么合计线程将成功地结束其运行,而退出代码则是合计。在这种情况下,主线程只是将合计的结果放入对话框。下面让我们转入合计线程的介绍。该线程的线程函数称为 S

23、u m T h r e a d F u n c。当主线程创建该线程时,它将应该合计的各个整数的数量作为唯一的参数来传递,这个参数就是 p v P a r a m。然后,函数将u S u m变量初始化为U N I X _ M A X,这意味着该函数认为它将不会成功地完成运行。接着,S u m T h r e a d F u n c建立S E H,这样,它就能够抓住线程运行时出现的任何异常条件。然后调用递归函数S u m来计算总数。如果成功地计算出总数,S u m T h r e a d F u n c函数返回u S u m变量的值,这是线程的退出代码。但是,如果在s u m函数运行时引发了一个异

24、常条件,系统将立即对S E H过滤器表达式进行计算。换句话说,系统将调用 F i l t e r F u n c函数,并为它传递用于标识引发的异常条件的代码。如果是堆栈溢出异常,那么该代码是 E X C E P T I O N _ S TA C K _ O V E R F L O W。如果想要观察程序适度处理堆栈溢出的异常条件,那么请告诉程序计算前面的44 000个数字的总数。我的F i l t e r F u n c函数非常简单。它查看是否出现了堆栈溢出异常条件。如果没有出现,它返回E X C E P T I O N _ C O N T I N U E _ S E A R C H。否则,该过

25、滤器返回 E X C E P T I O N _ E X E C U T E _H A N D L E R。它向系统指明过滤器预计到了这个异常条件,同时,E x c e p t块中包含的代码应该执行。对于这个示例应用程序来说,异常处理程序没有什么特殊的操作需要执行,而是让线程恰当地退出并返回代码U N I T _ M A X(这是u S u m N u m中的值)。父线程将会看到这个特殊的返回值,并向用户显示一条警告消息。要说明的最后一点是,为什么要在 S u m函数自己的线程中运行S u m函数,而不是在主线程中建立一个S E H块,并从t r y块中调用S u m函数。创建这个独立线程的理

26、由有三:首先,每次创建一个线程时,它会得到它自己的 1 MB堆栈区域。如果我从主线程中调用S u m函数,那么有些堆栈空间就已经在使用了,因此S u m函数将无法使用完整的1 MB堆栈空间。然而,我的示例应用程序是个简单的程序,也许它不需要使用那么多完整的堆栈空间,不过其他程序可能要复杂得多。我能够非常容易地想像到这样一种情况,即 S u m函数能够成功地计算出从1到1 0 0 0的所有整数的总和。然后,当 S u m在以后被再次调用时,堆栈可能变得更深,从而在S u m试图只计算从0到7 5 0之间的整数的总和时,导致堆栈溢出的发生。因此,为了使 S u m函数的运行具备更好的一致性,我设法

27、使它拥有一个尚未被其他代码使用过的完整的堆栈。使用独立线程的第二个原因是,关于堆栈溢出的异常条件,线程只能得到一次通知。如果我调用主线程中的S u m函数,并且发生了堆栈溢出,那么就可以跟踪和恰当地处理该异常条件。但是,这时已经向堆栈的所有已保留地址空间提交了物理存储器,并且没有更多的带有已打开的保护属性标志的页面。如果用户执行另一个总数的计算,S u m函数就会使堆栈溢出,但是却不会引发堆栈溢第 1 6章线程的堆栈计计391下载图16-6 Summation 对话框出异常条件。相反,一个访问违规异常将会发生,而这时恰当地处理这个异常条件就太晚了。使用独立的线程的最后一个原因是,该堆栈的物理存

28、储器可以释放。请看下面这个例子。用户要求S u m函数计算从0到30 000之间的整数的总和。这需要将相当数量的物理存储器提交给堆栈区域。然后,用户要进行若干个合计操作,其中最高的数字是5 0 0 0。在这种情况下,大量的内存被提交给堆栈区域,但是却不再被使用。该物理存储器是从页文件那里分配来的。不应该使该物理存储器保持提交状态,最好是释放该内存,重新将它交给系统和其他进程。通过使S u m T h r e a d F u n c的线程终止运行,系统将自动收回已经提交给堆栈区域的物理存储器。清单16-1 Summation示例应用程序392计计第三部分 内 存 管 理下载第 1 6章线程的堆栈计计393下载394计计第三部分 内 存 管 理下载第 1 6章线程的堆栈计计395下载396计计第三部分 内 存 管 理下载

展开阅读全文
相关资源
相关搜索

当前位置:首页 > 技术资料 > 其他杂项

本站为文档C TO C交易模式,本站只提供存储空间、用户上传的文档直接被用户下载,本站只是中间服务平台,本站所有文档下载所得的收益归上传人(含作者)所有。本站仅对用户上传内容的表现方式做保护处理,对上载内容本身不做任何修改或编辑。若文档所含内容侵犯了您的版权或隐私,请立即通知得利文库网,我们立即给予删除!客服QQ:136780468 微信:18945177775 电话:18904686070

工信部备案号:黑ICP备15003705号-8 |  经营许可证:黑B2-20190332号 |   黑公网安备:91230400333293403D

© 2020-2023 www.deliwenku.com 得利文库. All Rights Reserved 黑龙江转换宝科技有限公司 

黑龙江省互联网违法和不良信息举报
举报电话:0468-3380021 邮箱:hgswwxb@163.com