寒假捉虫记——从一段损坏的调用栈开始折腾

  放假在家,继续调试《家园》。目前的进度是MinGW上的编译链接都已通过,游戏程序也已经可以跑起来并进入主菜单界面,但加载关卡之后就会闪退。这让我想起了以前上中学时玩盗版游戏的日子。那个年代的单机游戏估计大多是用C/C++写的,一个不小心的内存操作就会让进程崩掉;而且那个年代的操作系统没现在稳定,可能破解技术也不够先进,从电脑城里买来的五六块钱的盗版游戏质量参差不齐。很多游戏跑着跑着就闪退,有的甚至连打都打不开,让人甚为恼火。如今源代码在手,并且我也是程序员了,可以对闪退的原因一探究竟,再也不用怕。

 

  不过让人失望的是,用MinGW构建出的程序不会像Linux程序那样在崩溃时吐核。还好这回的闪退是可以必现的,所以就在gdb中运行程序,看看它崩在什么地方。

 

  结果程序如预期崩溃后,调用栈成了下面这个样子:

Program received signal SIGSEGV, Segmentation fault.
0x0ddcf5c0 in ?? ()
(gdb) bt
#0  0x0ddcf5c0 in ?? ()
#1  0xabababab in ?? ()
#2  0x0000abab in ?? ()
#3  0x00000000 in ?? ()
(gdb)

  看样子调用栈已经坏掉了。记得以前在老东家遇到过这种损坏的调用栈,但后来很喜感地发现原来是机器的内存坏了。我相信我家电脑还没有到如此风烛残年的地步。

 

  在StackOverflow上搜到一篇帖子:http://stackoverflow.com/questions/9809810/gdb-corrupted-stack-frame-how-to-debug,正好是我想问的问题:如何在gdb中调试这种已经损坏的调用栈。帖子里的答案说这种情况99%是因为调用了非法的函数指针。在32位环境中可以用如下方法恢复调用栈:

(gdb) set $pc = *(void **)$esp
(gdb) set $esp = $esp + 4

  可是我检查了一下esp寄存器指向的内存:

(gdb) p $esp
$1 = (void *) 0x22fa34
(gdb) x $esp
0x22fa34:       0x0000007f
(gdb) x/i 0x7f
   0x7f:        Cannot access memory at address 0x7f
(gdb)

  0x7f显然不可能是一个合法的指令地址。看来我落到剩下那1%的区间里了=A=。

 

  把这事情分享到朋友圈里后,主程建议我让程序链接tcmalloc试试,看看能否让程序在应用代码进行非法内存操作时就崩溃,兴许那时调用栈还没损坏。可是试过后情况并无改变。不过这倒是提醒了我,以后不妨让自己的程序都链接tcmalloc,这样可以让很多问题都提前暴露。顺便写下,我的tcmalloc链接选项是-L/local/lib -ltcmalloc_minimal -fno-builtin-malloc -fno-builtin-calloc -fno-builtin-realloc -fno-builtin-free。

 

  无奈,最后还是通过打日志和单步调试的方法,通过应用代码本身的逻辑定位到了具体崩溃位置。原来程序崩在了一个OpenGL接口——glDrawElements的调用。多留日志和熟练掌握项目代码逻辑真是重要啊。

 

  如果是在工作中,我的排查工作一般在这一步也就该结束了。因为我从未接触过OpenGL,所以此时我应该让有OpenGL经验的同事来帮忙处理。不过这回不是在工作而是在玩耍,所以我打算满足下自己用牛刀杀鸡、用导弹打蚊子的癖好,好好研究一番,一是看看能否在gdb中恢复出调用栈,二是研究下glDrawElements这个调用为啥会崩溃,趁机接触下OpenGL。

 

调用栈的恢复

 

  先看看崩溃的直接原因是什么,看看崩溃时执行的汇编指令是什么:

(gdb) x/i $pc
=> 0xddcf5c0:   mov    (%esi),%edi

  看来esi寄存器里存了一个非法内存地址。

(gdb) p/x $esi
$3 = 0xfeee4
(gdb) p *(void **)$esi
Cannot access memory at address 0xf00d4
(gdb)

  果然如此。

 

  再看看这之前还执行了什么指令。

(gdb) x/40i $pc-90
   0xc54f7a6:   add    %al,(%eax)
   0xc54f7a8:   add    %al,(%eax)
   0xc54f7aa:   add    %al,(%eax)
   0xc54f7ac:   add    %al,(%eax)
   0xc54f7ae:   add    %al,(%eax)
   0xc54f7b0:   add    %al,(%eax)
   0xc54f7b2:   add    %al,(%eax)
   0xc54f7b4:   add    %al,(%eax)
   0xc54f7b6:   add    %al,(%eax)
   0xc54f7b8:   cmp    $0xff,%bh
   0xc54f7bb:   incl   0x550000f7(%eax)
   0xc54f7c1:   mov    %esp,%ebp
   0xc54f7c3:   push   %ebx
   0xc54f7c4:   push   %esi
   0xc54f7c5:   push   %edi
   0xc54f7c6:   mov    0x8(%ebp),%ebx
   0xc54f7c9:   mov    0xc(%ebp),%eax
   0xc54f7cc:   mov    0x14(%ebp),%ebp
   0xc54f7cf:   mov    %ebp,%edi
   0xc54f7d1:   shl    $0x14,%edi
   0xc54f7d4:   lea    0x40003640(%edi),%esi
   0xc54f7da:   mov    %esi,(%eax)
   0xc54f7dc:   add    $0x4,%eax
   0xc54f7df:   mov    0x1c(%esp),%edx
   0xc54f7e3:   lea    (%edx,%ebp,2),%ebp
   0xc54f7e6:   mov    %ebp,0x20(%esp)
   0xc54f7ea:   movzwl (%edx),%ecx
   0xc54f7ed:   add    $0x2,%edx
   0xc54f7f0:   mov    0xc509890,%esi
   0xc54f7f6:   mov    0x4(%esi),%esi
   0xc54f7f9:   mov    %ecx,%edi
   0xc54f7fb:   shl    $0x4,%edi
   0xc54f7fe:   add    %edi,%esi
=> 0xc54f800:   mov    (%esi),%edi
   0xc54f802:   mov    0x4(%esi),%ebp
   0xc54f805:   mov    %edi,(%eax)
   0xc54f807:   mov    %ebp,0x4(%eax)
   0xc54f80a:   mov    0x8(%esi),%edi
   0xc54f80d:   mov    %edi,0x8(%eax)
   0xc54f810:   mov    0xc509890,%esi
(gdb)

  看样子在0xc54f7bb附近很可能有一个函数头。函数开头通常由两条汇编指令组成——第一条指令保存当前栈帧的帧底地址,第二条指令将当前的栈顶指为栈帧底,开启新栈帧:

push %ebp
move %esp %ebp

  于是从0xc54f7bc开始,一路用x命令检查:

(gdb) x/40i 0xc54f7bc
...
(gdb) x/40i 0xc54f7bd
...
(gdb) x/40i 0xc54f7be
   0xc54f7be:   add    %al,(%eax)
  
0xc54f7c0:   push   %ebp
   0xc54f7c1:   mov    %esp,%ebp
   0xc54f7c3:   push   %ebx
   0xc54f7c4:   push   %esi
   0xc54f7c5:   push   %edi
   0xc54f7c6:   mov    0x8(%ebp),%ebx
   0xc54f7c9:   mov    0xc(%ebp),%eax
   0xc54f7cc:   mov    0x14(%ebp),%ebp
   0xc54f7cf:   mov    %ebp,%edi
   0xc54f7d1:   shl    $0x14,%edi
   0xc54f7d4:   lea    0x40003640(%edi),%esi
   0xc54f7da:   mov    %esi,(%eax)
   0xc54f7dc:   add    $0x4,%eax
   0xc54f7df:   mov    0x1c(%esp),%edx
   0xc54f7e3:   lea    (%edx,%ebp,2),%ebp
   0xc54f7e6:   mov    %ebp,0x20(%esp)
   0xc54f7ea:   movzwl (%edx),%ecx
   0xc54f7ed:   add    $0x2,%edx
   0xc54f7f0:   mov    0xc509890,%esi
   0xc54f7f6:   mov    0x4(%esi),%esi
   0xc54f7f9:   mov    %ecx,%edi
   0xc54f7fb:   shl    $0x4,%edi
   0xc54f7fe: 

  • 4
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值