调试实战 | 一个隐蔽的崩溃:当 this 指针在构造函数中“杀死”自己

摘要

在最近的项目开发中,我遇到了一个由智能指针误用导致的程序崩溃问题。问题的根源在于 SheetDataHandler 类的构造函数中,将 this 指针传递给了一个接收 SheetDataHandlerPtr(智能指针类型)参数的静态函数 HandleMissingColumn。这个看似简单的操作,却导致了对象在构造函数执行期间被意外释放,最终引发空指针访问异常。

通过深入调试和反汇编分析,我发现当 this 指针被隐式转换为智能指针时,引用计数会从 0 增加到 1,而在函数调用结束后,智能指针对象析构时引用计数又减回 0,从而触发了对象的 delete 操作。这导致构造函数尚未执行完毕,对象就已经被销毁,后续对成员变量的访问变成了访问已释放内存的非法操作。

示例代码

以下是我精简整理后的模拟代码,大家可以先锻炼一下眼力,看看是否可以一眼看出问题所在。关于 RefCountedPtr 的代码就不列出来了,可以参考之前的文章

提示:问题出在 SheetDataHandler.cpp 中。

  • 表格数据处理类头文件 SheetDataHandler.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// SheetDataHandler.h
#pragma once

#include <unordered_map>

#include <vector>

#include "RefCountedPtr.h"

class SheetData
{
public:
std::vector<std::wstring> headers;
std::vector<std::vector<SheetCellData>> datasVec;
};

class SheetDataHandler;
typedef RefCountedPtr<SheetDataHandler> SheetDataHandlerPtr;

class SheetDataHandler : public RefCounted<IRefCounted>
{
public:
SheetDataHandler(const SheetData& sheetData);

public:
static void HandleMissingColumn(SheetDataHandlerPtr helper, const SheetData& sheetData);

public:
std::vector<SheetRowDataPtr> rowDatas;
std::vector<std::wstring> headers;
};
  • 表格数据处理类实现文件 SheetDataHandler.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// SheetDataHandler.cpp
#include "SheetRowData.h"
#include "SheetDataHandler.h"

SheetDataHandler::SheetDataHandler(const SheetData& sheetData)
{
HandleMissingColumn(this, sheetData);
}

void SheetDataHandler::HandleMissingColumn(SheetDataHandlerPtr helper, const SheetData& sheetData)
{
auto headers = sheetData.headers;
auto datasVec = sheetData.datasVec;

// handle logic

helper->headers = headers;
for (auto& rowData : datasVec)
{
helper->rowDatas.push_back(std::make_shared<SheetRowData>(helper, rowData));
}
}
  • 表格数据类头文件 SheetRowData.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
//SheetRowData.h
#pragma once
#include <vector>
#include <string>
#include <memory>

#include "RefCountedPtr.h"

class SheetCellData
{
public:
SheetCellData() = default;

SheetCellData(const std::wstring& data_) : data(data_), type(0){}

public:
std::wstring data;
int type;
};

class SheetDataHandler;
typedef RefCountedPtr<SheetDataHandler> SheetDataHandlerPtr;

class SheetRowData
{
public:
SheetRowData() = default;
SheetRowData(SheetDataHandlerPtr sdhPtr, std::vector<SheetCellData> cells);

public:
std::vector<SheetCellData> cells;
SheetDataHandlerPtr sdhPtr;
};

typedef std::shared_ptr<SheetRowData> SheetRowDataPtr;
  • 表格数据类实现文件 SheetRowData.cpp
1
2
3
4
5
6
7
8
// SheetRowData.cpp
#include "SheetRowData.h"
#include "SheetDataHandler.h"

SheetRowData::SheetRowData(SheetDataHandlerPtr sdhPtr_, std::vector<SheetCellData> cells_) : sdhPtr(sdhPtr_), cells(cells_)
{

}
  • 表格数据处理流程管理类头文件 SheetDataProcessManager.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//SheetDataProcessManager.h

#pragma once

class SheetDataProcessManager
{
public:
static SheetDataProcessManager& Instance();

static std::unordered_map<std::wstring, SheetData> ReadSheetDatas(const std::wstring& path);

public:
void Init(const std::wstring& path);

void Handle();

public:
std::unordered_map<std::wstring, SheetDataHandlerPtr> sdhMap;
};
  • 表格数据处理流程管理类实现文件 SheetDataProcessManager.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
//SheetDataProcessManager.cpp

#include "SheetRowData.h"
#include "SheetDataHandler.h"

#include "SheetDataProcessManager.h"

SheetDataProcessManager& SheetDataProcessManager::Instance()
{
static SheetDataProcessManager s_instance;
return s_instance;
}

std::unordered_map<std::wstring, SheetData> SheetDataProcessManager::ReadSheetDatas(const std::wstring& path)
{
std::unordered_map<std::wstring, SheetData> result;

// read from excel

SheetData sheet1;
sheet1.headers = std::vector<std::wstring>{ L"ID", L"Name", L"Age" };
sheet1.datasVec = std::vector<std::vector<SheetCellData>>{ std::vector<SheetCellData>{ SheetCellData(L"001"), SheetCellData(L"test"), SheetCellData(L"18") }};
result[L"Sheet1"] = sheet1;

result[L"Sheet2"] = SheetData();

return result;
}


void SheetDataProcessManager::Init(const std::wstring& path)
{
auto sheetDatas = ReadSheetDatas(path);
for (auto& it : sheetDatas)
{
auto handler = new SheetDataHandler(it.second);
sdhMap[it.first] = handler;
}
}

void SheetDataProcessManager::Handle()
{
}
  • 主文件 TestRefCountedPtrCrash.cpp
1
2
3
4
5
6
7
8
9
10
//TestRefCountedPtrCrash.cpp
#include <iostream>
#include "SheetRowData.h"
#include "SheetDataHandler.h"
#include "SheetDataProcessManager.h"
int main()
{
SheetDataProcessManager::Instance().Init(L"test.xlsx");
std::cout << "Hello World!\n";
}

初遇错误

优化完代码,执行测试时,遇到了一个崩溃问题,从错误提示看是读取非法地址(0xFFFFFFFFFFFFFFF7)导致的异常。从调用栈看是在执行 RefCountedPtr 的构造函数时发生的异常,如下图:

refcountedptr-assignment-error

好奇怪,其基类成员 m_refCount 的值是 -572662307,一个负数,按理说不应该是负数才对。this->p_ 的值是 0x000002519d2d2960,并不是 0xFFFFFFFFFFFFFFF7,为什么会提示 this->_p->0xFFFFFFFFFFFFFFF7 呢?先不管了,翻看一下上下文相关代码,没看到明显错误,应该是执行以下代码导致的异常:

1
sdhMap[it.first] = handler;

可是这行代码简单到不能再简单了——把对象保存到 map 中,这能出什么问题?既然代码看不出什么问题,那就还是回到发生异常的代码处看看吧。

查看反汇编

查看最顶层栈帧对应的代码,根据经验,最可能发生异常的是这句话 p_->AddRef()。具体查看一下发生异常时的汇编指令,如下图:

call-virtual-function-AddRef

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

view-values-in-hex

真是不看不知道,一看吓一跳啊!!! 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() 函数加断点,再次运行程序,果然命中,如下图:

delete-self-in-constructor

找到问题根源,解决起来就简单了,只需要修改 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 指令查看异常发生时的指令及寄存器信息,如下图:

check-exception-in-windbg

确实是无法访问 0xdddddddddddddddd + 8 地址对应的内存。不知道 vs 为什么会给出这么一个提示,希望有知道的朋友不吝赐教!

亲自动手

相关工程代码已经上传到 github 了,感兴趣的小伙伴儿可以下载验证。

参考资料

https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/crtsetdbgflag?view=msvc-170

总结

  • 在构造函数中一定不能把 this 指针当作 RefCountedPtr 使用
  • 0xdddddddd 是常用的删除后的填充数据,需要提高敏感度
BianChengNan wechat
扫描左侧二维码关注公众号,扫描右侧二维码加我个人微信:)
0%