在上篇文章中介绍了在 windbg
中如何查看非常深的调用栈 —— 使用 kN
命令指定栈帧数。kN
虽好,但最多只能查看 0xffff
个栈帧。如果栈帧数量比 0xffff
还多,该如何查看呢?本文将介绍几种查看方法。
在介绍查看方法前,需要对线程栈的特点有个基本的认识。
线程栈是从高向低扩展的,当向栈上 push
一个值的时候,栈底指针 esp
的值会减小。
函数返回地址会保存到栈上:
函数 A
调用函数 B
的时候,会把 B
需要的参数根据调用约定放到对应的位置,有可能是通过寄存器传递,也有可能通过栈传递。处理完参数后会执行 call B
,而 call
指令可以简单理解为以下两个操作:
如果函数 B
会调用另外一个函数 C
,那么会遵循相同的规律:会把返回地址( call C
后面的地址)入栈,然后跳转到 C
继续执行。当 C
执行结束的时候,CPU
会从栈上把保存的返回地址弹出到 rip
中,这样就可以继续从函数 B
中调用函数 C
的下一条指令继续执行了。
根据以上几点可以得到一个非常重要的结论:如果 A
调用了 B
,B
又调用了 C
,C
又调用了 D
。那么 B
返回到 A
的地址在线程栈的高处,C
返回到 B
的地址在线程栈的低处,D
返回到 C
的地址在线程栈的最低处。
有了以上的基本认识,就可以使用以下几种方法查看调用栈了。
**方法1:**使用 .kframes
设置默认显示的栈帧数量
增大默认显示数量,这样就可以一次性显示更多的调用栈
**方法2:**使用 dps
,自己识别调用栈
可以灵活高效的从指定的位置开始查找
**方法3:**使用 k
命令的时候指定 StackPtr
可以从指定位置开始显示调用栈,不必从头开始显示
为了方便验证每种方法的可行性,我写了一个非常简单的递归调用测试程序,为了让调用栈可以更深一些,我修改了工程设置中的堆栈保留大小为 0x70800000
(大概 1.75 GB
)。
测试工程可以到这里下载。
接下来依次介绍每种查看方法。
在 windbg
的帮助文档中发现可以通过 .kframes
命令来设置 k
命令默认显示的栈帧数量。但是也不是可以显示无限多个栈帧。
那么通过 .kframes
可以设置的最大栈帧数是多少呢?通过几次尝试,我发现 .kframes
可以接受的最大值是 32
位的带符号整数的最大值,也就是 0x7fffffff
(对应的十进制是 2147483647
)。
但是,如果通过 .kframes
命令把栈帧数设置为 0x7fffffff
后,再执行 k
命令,发现 windbg
会直接提示内存分配失败。
尝试把栈帧数设置为 0x1000000
,再执行 k
命令,发现 windbg
的内存占用非常高,高峰期大概消耗了 20GB
的物理内存(下图中的 Working Set
列),经过将近两分钟的努力,最终还是以内存分配失败告终~
又试了几个更小的值,发现在我的机器上(24GB
物理内存)设置为 0x1000000
时可以输出结果,但是因为数据量太大了,等了半个多小时也没执行完~
虽然这次 .kframes
没能成功,但这绝对是一个非常值得了解的命令,可以处理绝大多数情况下的线程栈查看问题。
优点:
缺点:
在 windbg
中可以通过 dps
以指针长度为单位打印出指定内存范围的值,同时会输出匹配的符号。
操作步骤:
实战:
通过 !teb
命令获取栈顶位置(0000002a33800000
)然后减去 64KB
(0x10000
,也可以换成其它值,一般情况下 64KB
足够了)得到地址 0000002a337f0000
,然后执行 dps 0000002a337f0000 0000002a33800000
。或者可以直接直接输入 dps 0000002a33800000-0x10000 0000002a33800000
。
根据 dps
的结果可知,已经包含了关键的递归函数 —— TestDeepRecursive!Recursive
,可以根据此次 dps
的输出结果手动识别调用栈。拉到输出结果的最下方,可以看到输出结果如下图:
从上图可以看到 main
函数,CallRecursive
函数,Recursive
函数。而且与 vs
中的调用栈完美匹配。
说明: 输出结果中极有可能包含很多无关的信息(比如上图中的黄色高亮部分),需要仔细甄别。
优点:
缺点:
前两种方法都有各自的优缺点,可以在前两种方法的基础上使用本方法——使用 k
命令的时候指定正确的 StackPtr
,windbg
会自动帮我们识别调用栈。
使用本方法时需要传递一个正确的 StackPtr
(调试 x64
程序时需要传递 rsp
,调试 x86
程序时需要传递 ebp
,也叫 BasePtr
),也可以同时指定要显示的栈帧数量。
关于 k
命令的帮助文档可以参考下图(截取自 windbg
帮助文档):
如果传递的 StackPtr
不对,那么输出结果很可能是错误的。比如,我使用一个错误的值执行 k=0x0000002a337ff938
输出结果如下:
所以,传递一个正确的 StackPtr
是必须的。那么,该如何获取一个正确的 StackPtr
呢?有两个方法:
执行 k
命令的时候,最左侧那一列就是 rsp
(x86
程序对应着 ebp
)。可以这样处理:先通过 .kframes
设置一个相对合理的值,然后执行 k
命令,等命令执行完,取最后一条输出结果的 rsp
的值,假设是 00000029c3004040
,然后执行 k=00000029c3004040 3
,就可以继续显示后续的三条调用栈了。重复此过程即可。
实际使用的时候,可以尽量每次多显示一些栈帧,如果调用栈非常深,需要重复的次数会很多,但总比不能查看强!
在 dps
的输出结果中 “猜” 一个 ebp
或者 rsp
的值。说是猜,其实是有规律的。
2.1 对于 x86
的程序,ebp
保存了调用者的 ebp
,ebp+4
的位置保存了返回地址。
根据上图可以猜测,一个合法的 ebp
的值是 0x009ef908
。
在 windbg
中输入 k=0x09ef908 0x100
,可以得到下图完美的调用栈:
2.2 对于 x64
的程序,rsp-8
的位置保存了返回地址。可以根据有意义的符号名称对应的最左侧地址值 +8
得到 rsp
的值。
根据上图可知,一个合法的 rsp
的值是 0x0000002a337ffbd0
。在 windbg
中输入 k=0x0000002a337ffbd0 0x100
,可以得到下图完美的调用栈:
优点:
缺点:
所以,dps
+
k=StackPtr [FrameCount]
是最高效,最优雅的解决方案。
说明: 如果知道了一个合法的 StackPtr
,也可以先通过 r rsp = StackPtr
修改 rsp
寄存器的值,然后再执行 k
命令显示调用栈。但是这个方法有一个特别不好的地方,rsp
会被修改,后续用到 rsp
寄存器的命令都会受影响。因此,不推荐使用。
https://devblogs.microsoft.com/oldnewthing/20130906-00/?p=3303
https://blog.aaronballman.com/2011/07/reconstructing-a-corrupted-stack-crawl/
#include <iostream>
void
Recursive(
int
depth)
{
if
(depth > 0)
{
Recursive(--depth);
}
}
void
CallRecursive()
{
Recursive(0x7fffffff);
}
int
main()
{
CallRecursive();
}
#include <iostream>
void
Recursive(
int
depth)
{
if
(depth > 0)
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)