原文作者:Aaron Ballman
原文时间:2011年07月04日
原文地址:http://blog.aaronballman.com/2011/07/reconstructing-a-corrupted-stack-crawl/

翻译:magictong

时间:2014年05月29日夜

后记:可惜原始的DUMP文件作者并没有上传

在我的日常工作中,我经常阅读来之微软WinQual(译注:https://sysdev.microsoft.com/ http://en.wikipedia.org/wiki/Winqual)的报告。这些报告里面一般包含着dump文件(译注:崩溃转储文件,我们一般都是叫dump文件,是一种软件崩溃之后产生的文件,可用于事后调试),从这些dump文件里面我可以分析出一些常用的软件里面到底出了什么问题,造成它崩溃了。总而言之,这是一个超赞的系统,我强烈建议各个独立软件开发商(原文:ISV)去上面注册(尤其是这个系统对任何人都是免费的,只要你的可执行文件是正确签名的)。最近我拿到了一个堆栈已经被严重破坏了的dump文件,我想和大家讨论一下怎么使用Windbg工具来重建它的调用堆栈(callstack)。

在开始之前,让我们先看看一个原始的调用堆栈是什么样子的,在Windbg里面运行“k”命令即可。
        0:000> k
        ChildEBP RetAddr  
        028b89cc 77c75350 ntdll!KiFastSystemCallRet
        028b89d0 77c4b208 ntdll!ZwTerminateProcess+0xc
        028b89e0 763e41ec ntdll!RtlExitUserProcess+0x7a
        028b89f4 10056386 kernel32!ExitProcess+0x12
        WARNING: Stack unwind information not available. Following frames may be wrong.
        028b89fc 100565a0 EyeOneIO!I1_SynchronizeWhitebases+0xf0f6
        028b8a0c 10054803 EyeOneIO!I1_SynchronizeWhitebases+0xf310
        00000000 00000000 EyeOneIO!I1_SynchronizeWhitebases+0xd573

从上面的调用堆栈来看,有几个特征表明这个堆栈已经被破坏了。首先,调用堆栈的基址不可能从0x00000000开始。通常情况下,它从main函数的入口地址开始,或者从一个线程的入口地址开始,但是从上面的调用堆栈来看我们没看看到这个特征。另外,Windbg也发出了“Stack unwind information not available. Following frames may be wrong.”的警告(译注:这句警告的意思就是说,下面的栈帧可能是错误的)。

第一步,既然堆栈已经错误了,我们当然需要重建当前执行现成的堆栈,并找到当前现成堆栈的起始位置。这里有个简单的扩展命令可以查看,使用!teb即可(译注:!teb用于查看当前线程执行环境):

0:000> !teb
        TEB at 7ffdb000
            ExceptionList:        028b8a28
            StackBase:            028c0000
            StackLimit:           028b6000
            SubSystemTib:         00000000
            FiberData:            00001e00
            ArbitraryUserPointer: 00000000
            Self:                 7ffdb000
            EnvironmentPointer:   00000000
            ClientId:             00000a4c . 00000e3c
            RpcHandle:            00000000
            Tls Storage:          7ffdb02c
            PEB Address:          7ffdf000
            LastErrorValue:       14007
            LastStatusValue:      c0150008
            Count Owned Locks:    0
            HardErrorMode:        0

看上面!teb命令显示的结果里面,StackBase和StackLimit告诉了我们当前线程的堆栈在内存中的范围,因此我们现在可以转储这个范围内的地址,然后从里面寻找一些有意义和有用的东西(译注:就是把内存地址和对应的符号地址对应起来,然后寻找和当前的线程有关的调用堆栈)。Windbg里面有个专门的dds命令就是用来做这个事情的,dds命令需要你指定一个起始地址,然后它从给定的起始地址开始转储一定范围内的地址,并且尝试把每个地址里面的内容和符合(symbol)对应起来(译注:假如可以对应的话)。dds转储的内容包含三列数据,第一列显示的是顺序递增的地址,第二列是显示地址里面的数据,第三列是符号名称,如果地址里面的数据可以被成功解析为一个符号的话,否则第三列就是显示的空白。

把真实的栈转储出来看看(省略了一些无关项):
(译注:使用命令 dds 028b6000,要显示更后面的内容可以在028b6000的后面加上一个偏移之后再对新地址使用 dds 命令)

028b6000  00000000
        ...
        028bf9d8  00000000
        028bf9dc  00000000
        028bf9e0  79035b7f
        028bf9e4  028bfa1c
        028bf9e8  6e760b5b i1IO!i1IO::measureOneStrip+0xbb
        028bf9ec  42b840fc
        ...
        028bfa18  00000000
        028bfa1c  028bfd98
        028bfa20  6e763387 i1IO!i1IO::_measureSingleRowScanThreaded+0x1467
        028bfa24  42b840fc
        ...
        028bfd94  00000006
        028bfd98  028bfe2c
        028bfd9c  6e761062 i1IO!i1IO::_advancedMeasureThreaded+0x222
        028bfda0  013a8520
        028bfda4  79035e2e
        ...
        028bfe28  00000000
        028bfe2c  028bfe38
        028bfe30  763ed0e9 kernel32!BaseThreadInitThunk+0xe
        028bfe34  012118e0
        028bfe38  028bfe78
        028bfe3c  77c516c3 ntdll!__RtlUserThreadStart+0x23
        028bfe40  012118e0
        ...
        028bfe74  00000000
        028bfe78  028bfe90
        028bfe7c  77c51696 ntdll!_RtlUserThreadStart+0x1b
        028bfe80  6e760e40 i1IO!i1IO::_advancedMeasureThreaded
        ...
        028c0000  ????????

实际上转储出来的堆栈比上面列出来的大得多,不过为了简单起见,我只保留一些相关的部分。

现在要做的第一件事情就是定位到callstack的起始位置。在这个例子里面,RtlUserThreadStart看起来很像是这个起始位置,因为它是线程的起始调用函数。在找到起始点之后,获取起始点的前一个堆栈地址A(第一列),然后在堆栈的内容里面(第二列)寻找是否有等于A的堆栈B(向低地址寻找,因为堆栈是向低地址增长的),然后再在堆栈内容里面寻找是否有等于B的堆栈地址C……,按照这种方法不停的搜索内存,直到不能再找到任何东西或者找到空地址。
        (译注:这个就是利用的标准函数栈帧的基本原理,对此处不理解的可以去了解下标准函数栈帧,一般没有经过FPO优化的调用函数链,可以通过EBP的值在整个堆栈上面串联起来,其实Windbg自己也是这么找的,而本文讨论的恰恰是因为堆栈被破坏之后,Windbg找不到正确的callstack之后,我们怎么手动恢复的问题)

在我们这个例子里面,我们从下面的堆栈开始找:

028bfe78  028bfe90
        028bfe7c  77c51696 ntdll!_RtlUserThreadStart+0x1b

搜索地址028bfe78,得到下面的堆栈:

028bfe38  028bfe78
        028bfe3c  77c516c3 ntdll!__RtlUserThreadStart+0x23

搜索地址028bfe38,得到下面的堆栈:

028bfe2c  028bfe38
        028bfe30  763ed0e9 kernel32!BaseThreadInitThunk+0xe

搜索地址028bfe2c,得到下面的堆栈:

028bfd98  028bfe2c
        028bfd9c  6e761062 i1IO!i1IO::_advancedMeasureThreaded+0x222

搜索地址028bfd98,得到下面的堆栈:

028bfa1c  028bfd98
        028bfa20  6e763387 i1IO!i1IO::_measureSingleRowScanThreaded+0x1467

搜索地址028bfa1c,得到下面的堆栈:

028bf9e4  028bfa1c
        028bf9e8  6e760b5b i1IO!i1IO::measureOneStrip+0xbb

现在,继续搜索028bf9e4已经不能再在堆栈里面找到信息了,也就是说我们可能已经找到了最终出问题的函数位置,我们可以使用Windbg尝试修复我们的callstack,当然我们需要给它我们上面找到的这些信息。其实很简单,只要上面没找错,我们给 k 命令指明一个确定地址,通过 L 参数传递进去(译注:用上面我们最后找到的028bfa1c),那么Windbg马上就会给我们一个更加友好的callstack信息。

0:000> k L=028bf9e4
        ChildEBP RetAddr  
        028b89cc 77c75350 ntdll!KiFastSystemCallRet
        028b89d0 77c4b208 ntdll!ZwTerminateProcess+0xc
        028bf9e4 6e760b5b ntdll!RtlExitUserProcess+0x7a
        028bfa1c 6e763387 i1IO!i1IO::measureOneStrip+0xbb
        028bfd98 6e761062 i1IO!i1IO::_measureSingleRowScanThreaded+0x1467
        028bfe2c 763ed0e9 i1IO!i1IO::_advancedMeasureThreaded+0x222
        028bfe38 77c516c3 kernel32!BaseThreadInitThunk+0xe
        028bfe78 77c51696 ntdll!__RtlUserThreadStart+0x23
        028bfe90 00000000 ntdll!_RtlUserThreadStart+0x1b

现在我们看到的callstack是不是更加完整并且合理了?!没有了调用栈帧错误的警告,而且callstack的调用基址也正常了。

希望上面介绍的这种方法能给你的调试工作带来一些帮助。

怎样重建一个损坏的调用堆栈(callstack)相关推荐

  1. 此异常最初是在此调用堆栈中引发的:_【8】进大厂必须掌握的面试题Java面试异常和线程...

    点击上方"全栈程序员社区",星标公众号 重磅干货,第一时间送达 Q1.错误和异常有什么区别? 错误是在运行时发生的不可恢复的情况.如OutOfMemory错误.这些JVM错误无法在 ...

  2. VC++ CallStack调用堆栈应用(调试篇)

    要想学习VC++快,多数人都会选择看视频教程或者学习前辈们的项目源码程序,通常我们是先看视频教程(比如:孙鑫老师的<VC++深入详解>),通过视频教程全面的学习,对VC++有个大致的了解, ...

  3. 一种获取过程调用堆栈信息的简单方法

    在程序崩溃或出现异常时,通常需要给开发人员提供基本的过程调用的信息,这里给出一个简单的C++实现.主要思路是:过程调用的开始时,在栈上创建一个类,利用类的构造函数记录相关信息,在过程调用完毕时会自动调 ...

  4. Iar环境c语言调用汇编函数,如何在IAR EWARM中通过内联汇编程序在另一个模块中调用C函数?...

    我在硬故障处理程序中有一些程序集.程序集基本上是为了传递当前堆栈指针作为参数(在R0中).它看起来像这样...如何在IAR EWARM中通过内联汇编程序在另一个模块中调用C函数? __asm(&quo ...

  5. javascript 堆栈_JavaScript调用堆栈-它是什么以及为什么它是必需的

    javascript 堆栈 The JavaScript engine (which is found in a hosting environment like the browser), is a ...

  6. logback配置控制打印台异常信息_logback异常输出详细信息(调用堆栈)分析

    Logback背景 Logback是一个开源的日志组件,是log4j的作者开发的用来替代log4j的. logback由三个部分组成,logback-core, logback-classic, lo ...

  7. 最大调用堆栈大小超出错误

    我正在使用Direct Web Remoting(DWR)JavaScript库文件,并且仅在Safari(台式机和iPad)中出现错误 它说 超出最大呼叫堆栈大小. 该错误的确切含义是什么,它会完全 ...

  8. linux c 用户态 调试追踪函数 调用堆栈 定位段错误

    一般察看函数运行时堆栈的方法是使用GDB(bt命令)之类的外部调试器,但是,有些时候为了分析程序的BUG,(主要针对长时间运行程序的分析),在程序出错时打印出函数的调用堆栈是非常有用的. 在glibc ...

  9. golang 打印调用堆栈

    这两天看Go的代码,呃,协程太多,无数个携程调用了一个方法,彻底看不清了,所以就想到是不是可以把调用堆栈打印出来. 查了一下,发现Go的 runtime/debug 库可以把调用堆栈打出来.下面看个例 ...

最新文章

  1. 深入理解C++中public、protected及private用法
  2. Spring rabbitmq消息机制--手动确认
  3. R语言机器学习Caret包(Caret包是分类和回归训练的简称)、数据划分、数据预处理、模型构建、模型调优、模型评估、多模型对比、模型预测推理
  4. python安装mysql库
  5. Sql 中取小数点后面两位小数.
  6. oracle asm 配置失败,Oracle ASMLib安装与配置
  7. oracle字符nul怎么输入,oracle - 如何摆脱Oracle数据库中的NUL字符? - 堆栈内存溢出...
  8. 生物医学基础--右腿驱动
  9. java中自定义泛型类_java 自定义一个泛型类
  10. 「代码随想录」121. 买卖股票的最佳时机【贪心】【动态规划】力扣/leetcode详解
  11. USBCAN分析仪的配套CAN和CANFD综合测试软件LKMaster软件解决工程师CAN总线测试难题
  12. php微信实现红包雨,怎么制作微信红包雨(微信红包雨特效)
  13. h5页面预览pdf文件_H5 页面如何查看 PDF 文件
  14. winform,鼠标移动到label标签时变成手的形状
  15. DeepFlow高效的光流匹配算法(下)
  16. java左手画圆右手画方_左手画圆、右手画方,双手齐用同时养护、开发你的左右大脑!...
  17. DB2 使用REORG命令重组优化表和索引
  18. IAR for ARM系列教程(三)_菜单(Ⅰ)
  19. 获取和清除cookie
  20. 5分绩点转4分_张康阳重大利好,国米提前4轮进欧冠!只差尤文5分,争冠还有戏...

热门文章

  1. ucinet使用手册_ucinet使用手册
  2. mysql timestamp _mysql之TIMESTAMP(时间戳)用法详解
  3. Adobe illustrator 直接选择工具删除白板 - 连载 6
  4. Awesome Bioinformatics:假期了,收藏学习吧!
  5. 癌症精准医疗上市公司泛生子基因 - 内推(名额有限)
  6. Bash 使用技巧大补贴
  7. 学生如何提高专业英文阅读能力(施一公)
  8. 苹果发布 macOS 12——Monterey
  9. android 热修复 需要重启应用嘛?_Android热修复方案盘点
  10. Qt文档阅读笔记-Broadcast Sender Example解析