

调试实战 | 记一次有教益的 vs2022 内存分配失败崩溃分析(续)
source link: https://bianchengnan.gitee.io//articles/crazy-vs2022-allocate-memory-failed-part2/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

前一阵子遇到了 vs2022
卡死的问题,在上一篇文章中重点分析了崩溃的原因 —— 当 vs2022
尝试分配 923MB
的内存时,物理内存+页文件大小不足以满足这次分配请求,于是抛出异常。
本篇文章将重点挖掘一下 vs2022
在崩溃之前已经分配的内容。
说明: 本文很早就写了草稿,一直没时间整理发布,Finally~
还是先从调用栈入手,找到关键参数,然后查看参数内容。
查找 vector 对象地址
栈帧 0b
对应的函数是 std::vector<T>::_Emplace_reallocate()
,栈帧 0c
会调用这个函数。根据调用约定可知,调用类成员函数时,rcx
会指向类对象,在这里 rcx
会指向 std::vector<std::shared_ptr<std::stringstream>>
类型的实例。可以通过查看栈帧 0c
的反汇编代码确定 rcx
的来源。
从图中可知,rcx
的值来自 rbp-0x70
。那 rbp
的值是多少呢?使用 uf
查看 vcpkg!code_store::a_store::a_thread_impl::append_code_item_name()
函数的反汇编代码。
由上图可知,先把 rsp-0x920
赋值给 rbp
,然后 rsp
会减小 0xa20
。所以可以通过 rsp+0xa20-0x920
计算出对应的 rbp
的值,再减去 0x70
即可得到 rcx
的值。由此可知 vector
对象的地址是 0x000000b1 6547e5d0
。
查看 vector 内容
查阅 vs
提供的 STL
源码可知,vector
对象起始偏移 0
的位置存储了第一个元素的地址,起始偏移 8
(64
位程序)的位置存储了最后一个元素后面的地址。可以查看 vector
中前 20
个元素。
由调用栈可知,vector
中的元素类型是 shared_ptr<stringstream>
。根据源码可知,shared_ptr<T>
类型的大小是 16
字节,偏移 0
的位置存储了对象的地址,偏移 8
的位置存储了引用计数对象的地址。
template <class _Ty>
class shared_ptr : public _Ptr_base<_Ty> { // class for reference counted resource management
...
};
template <class _Ty>
class _Ptr_base { // base class for shared_ptr and weak_ptr
private:
element_type* _Ptr;
_Ref_count_base* _Rep;
...
};
vector 中有多少个元素
大家应该都知道,vector
中的元素是顺序存储的,知道了起始地址及结束地址,也知道每个元素的大小,可以很容易计算出 vector
中的元素数量。
在 windbg
中输入 ? (000001c2434b7170-000001c21ccdd060) / 0n16
可以得到元素个数 40360465
。
根据上次分析的结果可知,分配的元素数量是60540697
。 通过查看 vs
提供的源码可知,容器扩容时会按 1.5
倍进行扩容。
来验证以一下是否符合这个规律。在 windbg
中输入 ? 0n40360465 + 0n40360465 / 2
可以得到结果 60540697
。
可见,当时 vs
在调用类似 push_back()
之类的方法向容器中增加元素,但是容器正好满了,触发了扩容操作。由此也可以验证之前的分析是正确的。
验证引用计数对象数据
拿第一个元素进行验证,实际对象的地址是 000001be 580056f0
,引用计数对象的地址是 000001be 580056e0
。先验证引用计数对象是否正确。
_Ref_count_base
结构如下图所示:
说明:
devenv
加载的模块所对应的调试符号已经去除了Type
信息,没办法通过dt
显示类型信息。上图是我用windbg
调试新建的测试工程时的截图。
从下图可知,引用计数相关数据是完美匹配的。
一般 shared_ptr<T>
的引用计数和实际的数据是没有关系的,比如下面的代码:
int* p = new int();
std::shared_ptr<int> sp(p);
sp._Ptr
的值是 0x017b9450
,sp._Rep
的地址是 0x017b9640
,两者之间没有明显关系。
但是,如果你观察的比较仔细,可以发现一个非常有趣的现象 —— vector
中的每个元素(智能指针)的引用计数对象的地址 +0x10
正好等于实际对象的地址。
以第一个元素为例,引用计数对象的地址是 000001be 580056e0
,实际对象的地址是 000001be 580056f0
,两者正好相差了 0x10
。
这是怎么回事呢?如果你对 stl
比较熟悉,可能已经想到了 std::make_shared()
,vector
中存储的对象都是通过 std::make_shared()
创建出来的。
make_shared
我摘录了 vs
中提供的源码
template <class _Ty, class... _Types>
shared_ptr<_Ty> make_shared(_Types&&... _Args) { // make a shared_ptr to non-array object
const auto _Rx = new _Ref_count_obj2<_Ty>(_STD forward<_Types>(_Args)...);
shared_ptr<_Ty> _Ret;
_Ret._Set_ptr_rep_and_enable_shared(_STD addressof(_Rx->_Storage._Value), _Rx);
return _Ret;
}
注意代码中 _Ret._Set_ptr_rep_and_enable_shared()
第一个参数的值是 _Rx->_Storage._Value
的地址。
_Rx
的类型是 _Ref_count_obj2<_Ty>*
,_Ref_count_obj2
继承自 _Ref_count_base
。而 _Ref_count_base
的大小是 16
字节:虚表指针 8
字节,两个引用计数各占 4
字节,一共 16
字节。
大概的内存结构图如下:
务必注意 _Ref_count_obj2
中的 _Storage
存储了整个目标对象,而不是指针。
procdump
真是事后调试的好帮手。以管理员权限运行procdump -i -ma d:\dumps\
即可安装。-i
表示安装(如果要卸载,可以使用-u
参数)。-ma
表示执行完整转储,d:\dumps\
表示.dmp
文件保存的位置。相较于
32
位进程的4GB
(2
的32
次方)虚拟内存空间而言,64
位进程的虚拟内存空间超级大,目前是256TB
(总共64
位,目前只用了48
位),内核态和用户态平均分,用户态可以使用一半,也就是128TB
。如果使用
malloc()
或者new()
(内部会调用malloc()
)分配的内存大小超出堆阈值,那么内部会使用NtAllocateVirtualMemory()
分配内存,而且AllocationType
的值是MEM_COMMIT
。分配MEM_COMMIT
类型的内存是受物理内存+分页文件大小限制的。
vs
源码- NTSTATUS Values
</div
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK