前言
前一阵子遇到了
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>
classshared_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 = newint();
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
constauto _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