段错误造成的常见诡异宕机情况总结(下)

   国庆长假终于结束了,从拥堵的噩梦中醒来,该收收心重新回到工作中来了(顺便吐槽一下闹心的长假,平时工作没时间出去,放了长假了 又不敢出去,路上耗费大量的时间和金钱也算了,弄的整个人也身心疲惫的……)
   言归正传,接着上回宕机情况说。之前比较难找的宕机错误已经在前两篇随笔里说过了,这次要说的是前不久一个同事遇到的。他要做一个录像功能,每次把客户端的消息转储成文件时挂掉。大致代码如下:
  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

posted on 2012-10-08 16:13 peakflys 阅读(4405) 评论(1)  编辑 收藏 引用 所属分类: 服务器

评论

# re: 段错误造成的常见诡异宕机情况总结(下) 2012-10-12 13:14 还要输入名字

这个,不需要看栈信息的吧。

在栈里搞一个char c[1024*xxx]的语句,你就应该马上敏感了。不管这句话以后会不会造成运行时错误,都是需要警惕和优化的地方。

亡羊补牢,不如见洞先补。  回复  更多评论   


只有注册用户登录后才能发表评论。
【推荐】超50万行VC++源码: 大型组态工控、电力仿真CAD与GIS源码库
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理


<2012年12月>
2526272829301
2345678
9101112131415
16171819202122
23242526272829
303112345

导航

统计

公告

人不淡定的时候,就爱表现出来,敲代码如此,偶尔的灵感亦如此……

常用链接

留言簿(4)

随笔分类

随笔档案

文章档案

搜索

最新评论

阅读排行榜

评论排行榜