最近项目要进行最后一次线上测试,对一些新增的server进行压测,在对同事用我写的协程框架搭建的ranksvr的时候进行压测的时候,ranksvr挂掉了,由于是压了一段时间才挂掉,所以有特定的业务逻辑触发了栈溢出,由于涉及到调用公司第三方API所以,查起来还是比较麻烦,一开始经过加日志,查看栈使用情况,但是发现在栈溢出前业务逻辑的栈使用情况正常。
这里我查问题的时候犯了一个错误,就是栈的使用情况不能简单的通过栈上变量的地址变化来对比查看。因为栈上的变量不一定是按照定义顺序从栈顶向下分配地址空间。特别是在开启编译优化的时候。
虽然说栈的使用是遵循LIFO,其实编译器只要保证函数之间调用时栈帧的使用符合LIFO就可以了,在进入函数时,压入caller的RBP和RIP到栈顶,然后构建当前函数的栈帧,退出函数时,弹出caller的RBP和RIP就恢复到了caller的栈帧。对于函数内部栈帧的使用就看编译器本身的优化了。有兴趣的可以试试在开启优化时下面输出结果和定义的顺序:
1 | int main() |
还需要注意的是:栈空间的分配是在编译后都确定好的,以及一些临时变量都是占用栈空间的。于是可以查看coredump业务逻辑代码的栈使用情况,如下:
可以看到该函数使用了0x1e178 = 120KB的栈空间本身协程栈只设置了132KB,所以问题定位到UpdateRankCoro的函数使用了大的栈上变量,于是我查看同事的代码,结果看了半天没有发现有大的栈空间的使用,凡是大的结构都是new出来的,pb结构的数据也都是堆上的。没办法,只能原始带比较相对比较高效的方法:折半注释代码,然后查看更改后函数的反汇编代码,查看栈空间大小的变化,最终发现占用大空间的是如下代码:
1 | shared_ptr<TB_RANK_EXTRA_DATA> tmp_extra_data_buffer(new(std::nothrow) TB_RANK_EXTRA_DATA); |
TB_RANK_EXTRA_DATA是一个120K的POD数据,正是{ *tmp_extra_data_buffer }
占用了120K的栈上空间。我们知道c++11中引入的新功能:list-initialization(列表初始化,注意不是成员初始化列表),通过braced-init-list(大括号列表)对容器进行初始化,为了提供这个新特性,c++11中引入了initializer_list
:
1 | template< class T > |
为了分析问题,我们来探究一下braced-init-list和initializer_list两个的实现
braced-init-list
大括号初始化列表的底层实现是数组,其中每个元素都从列表的对应元素复制初始化的, 但是其存储为未指定的,引用
底层数组不保证在原始 initializer_list 对象的生存期结束后继续存在。其中每个元素都从原始初始化器列表的对应元素复制初始化,
std::initializer_list
的存储是未指定的(即它可以是自动、临时或静态只读内存,依赖场合)
1 | int main() |
我们看一下反汇编的代码:
1 | # 通过命令objdump -S a.out查看elf的代码段的反汇编信息 |
上面的示例我们可以看到{1,2,3,4,5}的底层数组是存放在只读数据段的,那么我们的业务代码中都是变量而不是常量,所以**{ *tmp_extra_data_buffer }
是存放在栈空间上的**。
initializer_list的实现
std::initializer_list<T>
的引入是为了能够访问 const T
类型对象数组,是一个代理类,std::initializer_list
对象在很多时候会自动构造:
- 用花括号初始化器列表列表初始化一个对象,其中对应构造函数接受一个
std::initializer_list
参数 - 以花括号初始化器列表为赋值的右运算数,或函数调用参数,而对应的赋值运算符/函数接受 std::initializer_list 参数
- 绑定花括号初始化器列表到 auto ,包括在范围 for 循环中
initializer_list 可由一对指针或指针与其长度实现。复制一个 std::initializer_list
不会复制其底层对象。
我们看看gcc 5.4.0中initializer_list的实现
1 | template<class _E> |
我们可以看到initializer_list中只有两个成员:_M_array和_M_len
- _M_array:指向底层数组的起始地址
- _M_len:表示底层数组的长度
所以我们看到上一小节中的反汇编代码中拷贝了这两个数值到initializer_list的这个成员中。