国庆长假终于结束了,从拥堵的噩梦中醒来,该收收心重新回到工作中来了(顺便吐槽一下闹心的长假,平时工作没时间出去,放了长假了 又不敢出去,路上耗费大量的时间和金钱也算了,弄的整个人也身心疲惫的……)
言归正传,接着上回宕机情况说。之前比较难找的宕机错误已经在前两篇随笔里说过了,这次要说的是前不久一个同事遇到的。他要做一个录像功能,每次把客户端的消息转储成文件时挂掉。大致代码如下:
1 /**
2 *\author peakflys
3 *\brief 堆栈崩溃问题
4 */
5 #include <iostream>
6 #include <fstream>
7 using namespace std;
8
9 const unsigned int DATA_BUFFER = 64 * 1024;
10
11 class Test
12 {
13 public:
14 Test(const string _name) : name(_name)
15 {
16 }
17 void test();
18 private:
19 string name;
20 };
21
22 void Test::test()
23 {
24 ofstream out(name.c_str(),ios_base::binary);
25 if(!out)
26 return;
27 cout<<name<<endl;
28 char data[DATA_BUFFER * 1024];
29 bzero(data,sizeof(data));
30 }
31
32 int main()
33 {
34 Test t("test");
35 t.test();
36 return 0;
37 }
每次宕机的情况和上面示例中test函数大致相同,都在第24行挂掉(此例子在我本地虚拟机上是挂在第22行),可能大家看到这个例子就知道原因了,但是实际项目代码比这个复杂的多、也隐蔽的多,core文件显示不出宕机的具体情况,堆栈没被破坏,但是也没有实际可用的信息。单步跟进去每次都是到创建
ofstream 对象时挂掉。刚开始怀疑是文件名字的问题,因为录像文件名称是一个std::string,它是经过几部分最终拼接成的,所以我一直在查看前面的代码,检查之后看不出有什么问题,下断点,在函数调用前发现一切数据都是正常的,这就奇怪了?为什么程序每次都是在运行到创建
ofstream 对象时直接报内存非法访问的段错误?
当时的思路刚开始觉得既然是在函数内挂掉的,肯定是函数内前面执行的代码引起的问题,但是宕机的位置是在函数内第一行,况且没有参数的传递,所以就没有能影响这行代码的可能,然后怀疑是其他线程造成的(程序环境是多线程的),但是查看了一下,这个类的所有操作的执行只在一个线程内调用。这就比较诡异了……
因为
当时有点其他的工作要处理,并且那个同事宕机执行的代码要经过好多操作才会执行,每次调试起来也不方便,所以就先放下了。第二天来到时,看到他还在为那个问题纠结,他想了很多办法,包括把文件名直接写死、文件名改为字符数组等等,都没用,问题依然存在。当时感觉应该是函数内代码的问题,大致看了一下函数后面的代码,也没发现什么问题,就是把一些数据序列化成二进制,然后创建一个数组,把客户端发来的消息序列化进去,最后都写入文件,但是究竟哪里引起的宕机还真不清楚。后来我让他把序列化客户端消息的那几行代码注释掉试试,结果函数执行正常,没有宕机。那看来就是这几行代码的问题,然后继续缩小注释范围,最终大致定位到类似于上例中第28行处。计算了一下数据的大小,发现是64*1024*1024,总的大小也就是64M,马上 ulimit -s
查看了一下当前线程的堆栈上限,显示10240,这时候明白是怎么回事了,用户态堆栈大小为64M超出了线程默认的最大值10M(ulimit指令显示的单位是KB)。具体宕机情况可以通过上面示例跟踪时的汇编来模拟。具体如下:
0x0000000000400c54 <_ZN4Test4testEv+0>: push %rbp
0x0000000000400c55 <_ZN4Test4testEv+1>: mov %rsp,%rbp
0x0000000000400c58 <_ZN4Test4testEv+4>: push %rbx
0x0000000000400c59 <_ZN4Test4testEv+5>: sub $0x10000218,%rsp
0x0000000000400c60 <_ZN4Test4testEv+12>: mov %rdi,-0x10000218(%rbp)
0x0000000000400c67 <_ZN4Test4testEv+19>: mov -0x10000218(%rbp),%rdi
0x0000000000400c6e <_ZN4Test4testEv+26>: callq 0x4009f0 <_ZNKSs5c_strEv@plt>
0x0000000000400c73 <_ZN4Test4testEv+31>: mov %rax,%rsi
0x0000000000400c76 <_ZN4Test4testEv+34>: lea -0x210(%rbp),%rdi
0x0000000000400c7d <_ZN4Test4testEv+41>: mov $0x4,%edx
0x0000000000400c82 <_ZN4Test4testEv+46>: callq 0x400ac0 <_ZNSt14basic_ofstreamIcSt11char_traitsIcEEC1EPKcSt13_Ios_Openmode@plt>
0x0000000000400c87 <_ZN4Test4testEv+51>: lea -0x210(%rbp),%rax
0x0000000000400c8e <_ZN4Test4testEv+58>: lea 0xf8(%rax),%rdi
0x0000000000400c95 <_ZN4Test4testEv+65>: callq 0x400a60 <_ZNKSt9basic_iosIcSt11char_traitsIcEEntEv@plt>
0x0000000000400c9a <_ZN4Test4testEv+70>: test %al,%al
0x0000000000400c9c <_ZN4Test4testEv+72>: je 0x400ca0 <_ZN4Test4testEv+76>
0x0000000000400c9e <_ZN4Test4testEv+74>: jmp 0x400d06 <_ZN4Test4testEv+178>
0x0000000000400ca0 <_ZN4Test4testEv+76>: mov -0x10000218(%rbp),%rsi
0x0000000000400ca7 <_ZN4Test4testEv+83>: mov $0x6013c0,%edi
0x0000000000400cac <_ZN4Test4testEv+88>: callq 0x400a90 <_ZStlsIcSt11char_traitsIcESaIcEERSt13basic_ostreamIT_T0_ES7_RKSbIS4_S5_T1_E@plt>
程序停在
0x0000000000400c60 位置,在rdi寄存器保存时挂掉,原因很简单,是因为函数内栈地址空间溢出,导致rdi保存位置非法。
这类宕机的特点:宕机位置在函数执行处或者函数执行的第一行,而且是必宕,core文件基本看不出什么(info locals指令有时可以显示出异常数据
)。解决方法:一、缩小数据大小,分批序列化;二、增大默认的栈地址空间。采纳第一种,重新编译、运行,一切正常。至此问题算是解决了。本来这种函数栈溢出引起的宕机应该很容易想到的,但是在我们项目开发中还没遇到过,因为当时定义的最大处理数据长度是64K,以宏的方式定义,以后使用时 如果数据大于这个宏,就把数据分隔,分批使用,奈何当时同事使用时直接把那个宏数据大小又放大了一个数量级,而且当时代码写的挺隐蔽,也很靠后,数组定义时大小问题也就没太在意。 最后还是要特别说一下,数组是除了野指针引起的宕机外,其他通过core文件看不出宕机原因的诸多诡异问题的最大元凶。
至此诡异的宕机问题基本先告一段落,以后有时间再总结一下野指针宕机的一些心得。 ---peakflys