摘要
本文记录并剖析了一次由看似低级的代码错误引发的、令人意想不到的程序崩溃。问题的根源在于 GetErrorStr 函数中一个容易被忽略的重复 switch语句。这个错误导致函数返回的 std::wstring 对象未被正确初始化,最终在构造 CResult 对象时引发了空指针访问异常 。
本文通过深入分析崩溃调用栈和反汇编代码,清晰地总结了从函数异常返回(未初始化字符串)到调用端使用无效数据(触发崩溃)的完整过程。希望这个案例能提醒各位,编译警告是发现潜在风险的第一道防线,而基础的汇编知识则是深入调试的利器。文末有可复现问题的代码,欢迎动手实践。
缘起
好久没写文章了,一个是懒,一个是没有好的素材。最近在研发过程中遇到了几个崩溃问题,挺有意思的,值得总结。今天要总结的问题比较低级,而且编译的时候会报警告,但还是挺有教育意义的。一起看看吧。
示例代码
以下是我精简整理后的模拟代码,大家可以先锻炼一下眼力,看看是否可以一眼看出问题所在。
1 |
|
初遇错误
优化完代码后,运行突然崩溃了,简单查看后,是非常典型的空指针异常。

心想这不是小菜一碟吗?空指针异常?老朋友了!但是简单翻看调用栈,好像不是普通的空指针异常,在构造字符串对象的时候抛出了异常(上图红色高亮部分调用栈)。难道字符串出问题了?当时脑子里就一句话:字符串能出什么问题???
但是通过调试查看,好像还真是字符串出问题了,如下图:

既然是字符串出问题了,那就看下字符串的来源,来自函数 std::wstring GetErrorStr(int error) 的返回值。查看这个函数的实现。真是不看不知道,一看吓一跳!怎么有两行一样的 switch(error) ?这代码肯定是不对的,删掉一行,再次运行,果然没问题了。
为什么有重复的 swtich 就会崩溃呢?源码猜不出来,那就看看反汇编代码吧。
查看反汇编
反汇编代码如下,红色高亮部分是第一个 switch 对应的代码,什么有意义的事情都没做,直接跳转到函数结尾00007FF7800F3A34 了。

从图中反汇编代码可知,GetErrorStr() 没对字符串对象做任何初始化操作。那么调用端呢?是否初始化了字符串对象呢?我们接着看一下调用端的反汇编代码,从下图中可知,调用函数也没有初始化字符串对象。

综上,该字符串对象没有被初始化,其内容完全是随机的。
至此,此次崩溃问题已经很清楚了。但是,这种低级错误,按理说应该有警告才对啊?!
编译警告
再次编译,果然有警告,当时我应该忽略了,看来一定要注意编译警告!

汇编小知识
简单解释以上汇编代码,对于刚接触汇编的小伙伴会有帮助。先了解一些基本知识:
x64下的默认调用约定是stdcall,前四个参数(非浮点型)通过rcx rdx r8 r9传递- 对于
std::wstring GetErrorStr(int error)这种返回复杂类型(非POD类型)的函数,生成的汇编代码相当于std::wstring* GetErrorStr(std::wstring*, int error)。第一个参数 指向了返回值的地址 - 调用结束后,返回值会保存在
rax中
所以,上图中第二句汇编代码 00007FF7800F3924 mov qword ptr [rsp+8],rcx 把字符串对象地址保存到了 rsp+0x8 的位置。接下来的几句汇编指令把 rsp 向下移动了 0x8 + 0x8 + 0x108 = 0x118。
1 | 00007FF7800F3924 mov qword ptr [rsp+8],rcx |
接下来的一条汇编指令 00007FF7800F3932 lea rbp,[rsp+20h] 使 rbp 指向了 rsp+0x20 的位置,那么 rbp+0x100 就指向了 第一个参数的地址。
亲自动手
相关工程代码已经上传到 github 了,感兴趣的小伙伴儿可以下载验证。
总结
- 务必要关注编译器的警告
- 掌握汇编知识非常重要,尤其对调试更是如此
64位程序默认的调用约定是stdcall,前四个参数(非浮点型)通过rcx rdx r8 r9传递