摘要
在最近的项目开发中,我遇到了一个由智能指针误用导致的程序崩溃问题。问题的根源在于 SheetDataHandler 类的构造函数中,将 this 指针传递给了一个接收 SheetDataHandlerPtr(智能指针类型)参数的静态函数 HandleMissingColumn。这个看似简单的操作,却导致了对象在构造函数执行期间被意外释放,最终引发空指针访问异常。
通过深入调试和反汇编分析,我发现当 this 指针被隐式转换为智能指针时,引用计数会从 0 增加到 1,而在函数调用结束后,智能指针对象析构时引用计数又减回 0,从而触发了对象的 delete 操作。这导致构造函数尚未执行完毕,对象就已经被销毁,后续对成员变量的访问变成了访问已释放内存的非法操作。
示例代码
以下是我精简整理后的模拟代码,大家可以先锻炼一下眼力,看看是否可以一眼看出问题所在。关于 RefCountedPtr 的代码就不列出来了,可以参考之前的文章。
提示:问题出在
SheetDataHandler.cpp中。
- 表格数据处理类头文件
SheetDataHandler.h
1 | // SheetDataHandler.h |
- 表格数据处理类实现文件
SheetDataHandler.cpp
1 | // SheetDataHandler.cpp |
- 表格数据类头文件
SheetRowData.h
1 | //SheetRowData.h |
- 表格数据类实现文件
SheetRowData.cpp
1 | // SheetRowData.cpp |
- 表格数据处理流程管理类头文件
SheetDataProcessManager.h
1 | //SheetDataProcessManager.h |
- 表格数据处理流程管理类实现文件
SheetDataProcessManager.cpp
1 | //SheetDataProcessManager.cpp |
- 主文件
TestRefCountedPtrCrash.cpp
1 | //TestRefCountedPtrCrash.cpp |
初遇错误
优化完代码,执行测试时,遇到了一个崩溃问题,从错误提示看是读取非法地址(0xFFFFFFFFFFFFFFF7)导致的异常。从调用栈看是在执行 RefCountedPtr 的构造函数时发生的异常,如下图:

好奇怪,其基类成员 m_refCount 的值是 -572662307,一个负数,按理说不应该是负数才对。this->p_ 的值是 0x000002519d2d2960,并不是 0xFFFFFFFFFFFFFFF7,为什么会提示 this->_p-> 是 0xFFFFFFFFFFFFFFF7 呢?先不管了,翻看一下上下文相关代码,没看到明显错误,应该是执行以下代码导致的异常:
1 | sdhMap[it.first] = handler; |
可是这行代码简单到不能再简单了——把对象保存到 map 中,这能出什么问题?既然代码看不出什么问题,那就还是回到发生异常的代码处看看吧。
查看反汇编
查看最顶层栈帧对应的代码,根据经验,最可能发生异常的是这句话 p_->AddRef()。具体查看一下发生异常时的汇编指令,如下图:

红色高亮部分是明显的虚函数调用汇编代码、rcx 指向 p_,rax 指向虚表。rax+8 指向虚表第一个函数。等等,rax 的值怎么这么大?而且好像是十进制的,这时候我才发现我没开启十六进制显示。那赶紧看看对应的十六进制是什么?如下图:

真是不看不知道,一看吓一跳啊!!! rax 的值是 0xdddddddddddddddd。而且注意看,m_refCount 的值也是 0xdddddddd。这个值可太熟悉了,当一个对象被删除时,debug 模式下会用 0xdddddddd 填充。难道这个指针被删除后,还在继续使用?查看一下这个指针的来源。查看代码可知,指针来源于 auto handler = new SheetDataHandler(it.second);。难道 new 返回的指针被释放了?代码如此简单,难道是构造函数内部出问题了?赶紧查看构造函数的实现。
构造函数惹的祸
构造函数内部只调用了静态函数 HandleMissingColumn(this, sheetData);,并且把当前对象地址当作第一个参数传过去了。再看下这个静态函数的声明,void SheetDataHandler::HandleMissingColumn(SheetDataHandlerPtr helper, const SheetData& sheetData)。第一个参数是一个基于引用计数的智能指针对象。至此,思索片刻,我明白了问题所在,正是这个函数的第一个参数导致了问题。执行构造函数时,引用计数是 0,通过 this 指针构造一个 RefCountedPtr,引用计数 +1,当这个 RefCountedPtr 对象声明周期结束时,引用计数 -1 ,引用计数会变成 0,当引用计数变成 0 的时候,会触发执行 delete。怎么证明呢?直接在 RefCounted::Release() 函数加断点,再次运行程序,果然命中,如下图:

找到问题根源,解决起来就简单了,只需要修改 void SheetDataHandler::HandleMissingColumn(SheetDataHandlerPtr helper, const SheetData& sheetData) 的第一个参数为原生指针即可。或者在构造函数外部调用此函数也可以解决问题。
但是,这段代码之前运行一直没问题,为什么最近才暴雷呢?
消除疑问
仔细想了一下,之前一直没问题,是因为一直能读取到数据。当有数据时,HandleMissingColumn() 函数内部会调用下面这行代码 helper->rowDatas.push_back(std::make_shared<SheetRowData>(helper, rowData)); ,此行代码会构造一个 SheetRowData 对象,该对象内部有一个成员变量 SheetDataHandlerPtr sdhHelper,会增加引用计数。所以一直没发现这个问题。这下终于把所有疑问都搞清楚了,但是正是因为这番刨根问题,让我又意识到了这段代码中存在的另外一个问题——潜在的内存泄漏。
潜在的内存泄漏
为什么会导致内存泄漏呢?如果外部只是把 sdhMap 直接清空,当有数据时,SheetDataHandler 的析构函数是不会被调用的,因为 SheetRowData 中还持有着 SheetDataHandlerPtr。要想释放掉内存,就要在清空 sdhMap 前,先清空 SheetDataHandler 对象中的 rowDatas。这样就不会泄漏了。其实,更优雅的做法是使用标准库提供的 std::weak_ptr,让 SheetRowData 中持有 std::weak_ptr<SheetDataHandler>,使用的时候提升成 std::shared_ptr,如果提升成功,可以正常使用,如果失败,说明已经被删除,不能继续使用。
无解的 vs 异常提示
当异常发生时,vs 中报的错误是 引发了异常:读取访问权限冲突。this->p_ -> 是 0xFFFFFFFFFFFFFFF7。分析完整个异常,也没找到这个 0xFFFFFFFFFFFFFFF7 来自哪里,按理说应该访问 0xdddddddddddddddd + 8 这个地址触发的异常才对。于是保存了一个 dump,在 windbg 中打开,输入 .ecxr 指令查看异常发生时的指令及寄存器信息,如下图:

确实是无法访问 0xdddddddddddddddd + 8 地址对应的内存。不知道 vs 为什么会给出这么一个提示,希望有知道的朋友不吝赐教!
亲自动手
相关工程代码已经上传到 github 了,感兴趣的小伙伴儿可以下载验证。
参考资料
https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/crtsetdbgflag?view=msvc-170
总结
- 在构造函数中一定不能把
this指针当作RefCountedPtr使用 0xdddddddd是常用的删除后的填充数据,需要提高敏感度