前言
前一阵子遇到了 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 objectconst 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