缘起
很早之前,我遇到过几个与栈相关的问题,当时总结过几篇关于线程栈的文章,分别是 《栈大小可以怎么改?》、《栈局部变量优化探究,意外发现了 vs 的一个 bug ?》、《栈又溢出了》、《有趣的异常》。在这几篇总结中,简单的总结了栈溢出的原因,设置线程栈大小的方法。但是还有一点没弄清楚:操作系统是怎么知道一个线程的栈大小的?一定记录在某个位置了,否则就不能正确的在栈溢出的时候抛出异常了。不能根据 PE
头中的字段判断,因为在创建线程的时候可以指定线程栈大小。TEB
中的 StackLimit
是真正的栈底吗?带着这些疑问一起来刨根问底吧~
友情提示:结论在文章末尾。
!teb 命令
相信,很多小伙伴儿都知道,可以使用 !teb
查看线程相关的信息。
其中的 StackBase
和 StackLimit
分别指示了栈顶和当前栈使用情况。因为栈是从上向下增长的,所以 StackBase
的值比较大。
我之前一直认为这两个字段分别指向了栈顶和栈底(线程栈可以到达的最低位置),可以通过这两个字段计算出线程栈大小。后来才发现 StackLimit
并没有指向栈底,而是指向了线程栈当前所到达的最低位置。
线程栈默认的大小是 1MB
。如果计算一下 StackBase - StackLimit
的值即可知道,它们的差值是 256KB
,而不是 1MB
。
那么当前线程栈的大小是不是 1MB
呢?该如何确认呢?可以通过 vmmap
确定。
vmmap
打开 vmmap.exe
,并选择想要查看的进程,即可进行查看。
注意: 当选择的进程已经中断到调试器时,
vmmap.exe
会一直等待,需要让目标进程运行起来。
可以看到,线程 9804
的线程栈大小确实是 1MB
。
根据以上信息,可以确定 StackLimit
并不是真正的线程栈栈底。那么,栈底位置到底记录在哪里了呢?
最近在重翻《软件调试》的时候,发现了一个关键函数。
栈空间自动增长的关键函数
在第 22
章 22.8.1
节 栈空间的自动增长(P617
)中提到了一个关键函数 MiCheckForUserStackOverflow
。该函数是判断栈空间能否增长的关键函数。如果知道该函数是如何实现的,就能找到栈底了。
脑子里很快有了三个选项:google
搜索,ReactOS
和 server03
源码。正好电脑上有源码,不用考虑其它两个选项了。
参考源码
知道了函数名,但是还不知道这个函数在哪个文件中实现的。这个简单,在 File Locator
中输入 MiCheckForUserStackOverflow
,很快就找到了关键的文件。
双击打开 accesschk.c
,找到 MiCheckForUserStackOverflow
。注释很清晰的解释了这个函数的作用。
说明: 该函数的实现在
wrk
中也可以找到,地址是 https://github.com/mic101/windows/blob/master/WRK-v1.2/base/ntos/mm/acceschk.c。
整个函数虽然行数很多,但是有大部分是注释,而且考虑了各种情况。我截取了最关键的部分,如下图:
看样子 teb->DeallocationStack
记录了栈底。 StackBase
减去 teb->DeallocationStack
的值应该是栈大小(默认是 1MB
)。在 windbg
中验证一下。
验证
在 windbg
中输入如下命令 dt _teb 0043f000 -y DeallocationStack -y NtTib.StackBase
,只查看 DeallocationStack
和 NtTib.StackBase
的值。然后计算差值,发现正好是 1MB
(0x100000
)。
总结
_TEB
结构体的DeallocationStack
指向线程栈底,而NtTib.StackLimit
指向的是线程栈当前所到达的最低位置。可以在
dt
命令中通过-y
选项来显示特定字段。vmmap.exe
可以非常详细的展示进程虚拟内存情况。
参考资料
- 《软件调试》第一版 第
22
章 srv03rtm
源码