首页
社区
课程
招聘
记一次有教益的 vs2022 内存分配失败崩溃分析(续)
发表于: 2023-10-16 07:19 10122

记一次有教益的 vs2022 内存分配失败崩溃分析(续)

2023-10-16 07:19
10122

前一阵子遇到了 vs2022 卡死的问题,在上一篇文章中重点分析了崩溃的原因 —— 当 vs2022 尝试分配 923MB 的内存时,物理内存+页文件大小不足以满足这次分配请求,于是抛出异常。

本篇文章将重点挖掘一下 vs2022 在崩溃之前已经分配的内容。

说明: 本文很早就写了草稿,一直没时间整理发布,Finally~

还是先从调用栈入手,找到关键参数,然后查看参数内容。

栈帧 0b 对应的函数是 std::vector<T>::_Emplace_reallocate(),栈帧 0c 会调用这个函数。根据调用约定可知,调用类成员函数时,rcx 会指向类对象,在这里 rcx 会指向 std::vector<std::shared_ptr<std::stringstream>> 类型的实例。可以通过查看栈帧 0c 的反汇编代码确定 rcx 的来源。

view-vector-instance-from-stackframe-0c

从图中可知,rcx 的值来自 rbp-0x70。那 rbp 的值是多少呢?使用 uf 查看 vcpkg!code_store::a_store::a_thread_impl::append_code_item_name() 函数的反汇编代码。
view-rbp-in-frame-0c
由上图可知,先把 rsp-0x920 赋值给 rbp,然后 rsp 会减小 0xa20。所以可以通过 rsp+0xa20-0x920 计算出对应的 rbp 的值,再减去 0x70 即可得到 rcx 的值。由此可知 vector 对象的地址是 0x000000b1 6547e5d0
view-rbp-rcx-in-frame-0c

查阅 vs 提供的 STL 源码可知,vector 对象起始偏移 0 的位置存储了第一个元素的地址,起始偏移 864位程序)的位置存储了最后一个元素后面的地址。可以查看 vector 中前 20 个元素。

view-vector-front-20-data
由调用栈可知,vector 中的元素类型是 shared_ptr<stringstream>。根据源码可知,shared_ptr<T> 类型的大小是 16 字节,偏移 0 的位置存储了对象的地址,偏移 8 的位置存储了引用计数对象的地址。

大家应该都知道,vector 中的元素是顺序存储的,知道了起始地址及结束地址,也知道每个元素的大小,可以很容易计算出 vector 中的元素数量。

windbg 中输入 ? (000001c2434b7170-000001c21ccdd060) / 0n16 可以得到元素个数 40360465

根据上次分析的结果可知,分配的元素数量是60540697。 通过查看 vs 提供的源码可知,容器扩容时会按 1.5 倍进行扩容。

来验证以一下是否符合这个规律。在 windbg 中输入 ? 0n40360465 + 0n40360465 / 2 可以得到结果 60540697

view-vector-size
可见,当时 vs 在调用类似 push_back() 之类的方法向容器中增加元素,但是容器正好满了,触发了扩容操作。由此也可以验证之前的分析是正确的。

拿第一个元素进行验证,实际对象的地址是 000001be 580056f0,引用计数对象的地址是 000001be 580056e0。先验证引用计数对象是否正确。

_Ref_count_base 结构如下图所示:

view-_Ref_count_base_detail

说明: devenv 加载的模块所对应的调试符号已经去除了 Type 信息,没办法通过 dt 显示类型信息。上图是我用 windbg 调试新建的测试工程时的截图。

从下图可知,引用计数相关数据是完美匹配的。
verify-reference-count-object

一般 shared_ptr<T> 的引用计数和实际的数据是没有关系的,比如下面的代码:

view_normal_shared_ptr
sp._Ptr 的值是 0x017b9450sp._Rep 的地址是 0x017b9640,两者之间没有明显关系。

但是,如果你观察的比较仔细,可以发现一个非常有趣的现象 —— vector 中的每个元素(智能指针)的引用计数对象的地址 +0x10 正好等于实际对象的地址。

以第一个元素为例,引用计数对象的地址是 000001be 580056e0,实际对象的地址是 000001be 580056f0,两者正好相差了 0x10

这是怎么回事呢?如果你对 stl 比较熟悉,可能已经想到了 std::make_shared()vector 中存储的对象都是通过 std::make_shared() 创建出来的。

我摘录了 vs 中提供的源码

注意代码中 _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 字节。

大概的内存结构图如下:

make_shared_relation

务必注意 _Ref_count_obj2 中的 _Storage 存储了整个目标对象,而不是指针。

procdump 真是事后调试的好帮手。以管理员权限运行 procdump -i -ma d:\dumps\ 即可安装。-i 表示安装(如果要卸载,可以使用 -u 参数)。-ma 表示执行完整转储,d:\dumps\ 表示 .dmp 文件保存的位置。

相较于 32 位进程的 4GB232 次方)虚拟内存空间而言, 64 位进程的虚拟内存空间超级大,目前是 256TB(总共 64 位,目前只用了 48 位),内核态和用户态平均分,用户态可以使用一半,也就是 128TB

如果使用 malloc() 或者 new() (内部会调用 malloc())分配的内存大小超出堆阈值,那么内部会使用 NtAllocateVirtualMemory() 分配内存,而且 AllocationType 的值是 MEM_COMMIT。分配 MEM_COMMIT 类型的内存是受物理内存+分页文件大小限制的。

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;
    ...
};
template <class _Ty>

[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!

最后于 2023-10-17 19:09 被编程难编辑 ,原因: 重新上传图片
收藏
免费 5
支持
分享
最新回复 (4)
雪    币: 3535
活跃值: (31016)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
感谢分享
2023-10-16 09:06
1
雪    币: 23081
活跃值: (3442)
能力值: (RANK:648 )
在线值:
发帖
回帖
粉丝
3

图片挂了,目前只有一张图片正常

最后于 2023-10-18 09:54 被KevinsBobo编辑 ,原因:
2023-10-16 16:21
0
雪    币: 8519
活跃值: (9122)
能力值: ( LV12,RANK:360 )
在线值:
发帖
回帖
粉丝
4
KevinsBobo 图片挂了,目前只有一张图片正常
感谢大佬,已经补充了
2023-10-17 19:09
0
雪    币: 8519
活跃值: (9122)
能力值: ( LV12,RANK:360 )
在线值:
发帖
回帖
粉丝
5
秋狝 感谢分享
感谢阅读
2023-10-17 19:09
0
游客
登录 | 注册 方可回帖
返回
//