服务端段错误

之所以有上篇文章,是因为那个方法就是我在解决这篇文章中的问题时使用的.为了避免篇幅过大,所以分成了两篇专栏.

先说说这次崩溃吧,第一次崩溃是晚上7点多,运营同学发来了bt(gdb命令,就是backtrace stack),如下图:

服务端段错误_第1张图片 看见这种调用栈我的第一想法就是栈越界了,栈越界我一点都不担心,比什么野指针写坏堆好解决多了,因为离"犯罪现场"近啊.

上一篇专栏里说过,无法回溯调用栈有可能是"调用栈链表头"被写坏了,我们先看看栈顶附近的内容是什么:

服务端段错误_第2张图片 不知道你是否发现了,栈顶向下的内容是0x30303039这样的东西,这东西一看就像字符串,别问我为什么,这是经验加敏锐的直觉,哈哈.那我们看看这是什么字符串: 服务端段错误_第3张图片 你看,还真是字符串,于是我找了下这个字符串的可能来源,发现这东西是聊天窗口里的内容.怎么是聊天窗口里的内容呢?是因为聊天内容在某些情况下过长了?于是检查聊天相关的代码,发现的确有一处有隐患.那我们就试着利用这处隐患,看看是否能重现这次崩溃.

无论我们发什么聊天内容,都无法导致服务端崩溃,最后我们放弃了,把这处隐患改正了.此时外服服务端又开始崩溃了,而且几分钟崩溃一次,必须快速定位到出问题的地方,然后尝试动态修复,否则玩家没有办法游戏了.

于是机智的我想到了一个撞运气的方法,我先来简单描述下栈里的内容:

服务端段错误_第4张图片 贴出这张图的时候,我已经被自己的机智折服了.这个图大家熟悉吧,我用这个东西来解释一下栈的运行时状态:

  1. 我们把它当作栈的保留大小,在msvc下默认是1MB,linux上看ulimit里的配置,默认情况是8MB
  2. 我们把它当作栈的提交大小(这里就不详细解释了,等有机会写一篇专栏介绍下内存相关的东西再解释)
  3. 这里是当前的esp的值,也就是栈顶指针
  4. 这里是栈底
好了,图介绍完了,我们来播放一首音乐,然后你会发现图中的3会随着音量的大小上下振动,这正是我想要的效果.因为程序在运行时,栈顶指针就像图中的3所指的绿色柱形图一样,上下振动.当调用深度比较深的时候,栈顶指针就越靠近上端,当函数返回时,栈顶指针就会向下移动.

好了,现在我们的程序崩溃了,栈顶指针停在了3所指向的位置,那3上面的内容是什么呢?我们看下面的示例代码:

int func()
{
   ......
   func1();
   return 0; 
}

int func1()
{
    ....
    func2();
    return 0;
}

假设在func返回的时候,崩溃了,那他一定调用过func1,func1里再调用func2的时候,就会把返回地址压入栈,而这个返回地址,就是func1 + offset.也就是说,当在func返回的时候崩溃了,那么栈里很有可能会保留func里刚刚调用过的函数产生的栈信息,因为栈顶指针在向下移动的时候并不会清理栈里面的内容.

那好,我们就看看当前栈顶(esp)上面的信息.(处于保密原因,这里就不贴相关的栈里的信息了).查看方法依然是用我上篇专栏里提到的bts扩展指令,只要将起始地址设置为esp - 1000就可以了.

通过分析输出的信息发现,几次崩溃的core文件内,在栈顶上方保留的栈信息都是一模一样的,都调用了FuncA函数,连偏移都一样,而这个FuncA函数是活动A里的,然后我和同事以及领导商量了一下,决定在崩溃的大区中关闭一个线的A活动,如果不再崩溃,那问题就锁定在A活动上了.

说干就干,动态reload了配置表格,停止了一个线的A活动,结果在长达几个小时,其他线各种崩溃的情况下,竟然真的不崩溃了.于是把其他线的A活动也关闭了.

那么接下来的问题就是查查到底A活动出了什么问题,我和几个同事翻遍了svn log,也没看出一点问题,本来这次更新的内容就不多,和这个A活动相关的代码就一条,已经确认N次没有问题了.那是怎么回事呢?这个A活动和聊天有什么关系呢?

此时我又整理了一下思路,首先栈里出现了字符串,这很有可能就是越界.曾经我一度怀疑和A活动没有关系,关闭A活动不崩溃只是巧合而已,因为几个core文件里,栈顶附近的内容都是聊天字符串,应该是聊天的锅.但是我们有没有办法利用聊天来重现这次崩溃,可是聊天内容就是出现在了栈里.

为了搞清楚问题,就有了上篇专栏的内容,我用上篇专栏的方法,找到了几个main函数开始的调用栈,然后分析了一下其中的Receive函数的参数,这个Receive的参数就是从客户端收到的数据,根据协议分发到不同的函数去执行.

我分析了一下包结构,是这样的:

struct Packet
{
    BYTE byCmd;
    BYTE bySubCmd;
    WORD wData[3];
    CHAR szData[1024];
}

这个packet里的byCmd和bySubCmd决定了这个包的类型,正是参加A活动需要发送的包,而其中的szData,正常情况下应该是玩家的名字,而玩家的名字长度在服务端是有限制的,是32,但是奇怪的是,这个Recieve里收到的这个Packet,szData竟然是聊天内容,而服务端里对这个szData的长度没有进行合法性判断,直接memcpy(dst, szData, strlen(szData),导致把栈写坏了(这个包是个通用包,所以szData是1024,只不过不同的包在服务端用到的长度是不一样的,这个A活动用到的长度就必须小于等于32).

原来如此,此时就完美解释了为什么栈里有聊天信息,关闭A活动就不崩溃了,原来是玩家在参加活动发包的时候,把聊天内容写到了szData里,然后服务端判断不严谨,导致服务器崩溃.于是修改服务端代码,判断一下长度,然后更新出去,发现服务端不崩溃了.

好了,接下来的疑问就是为什么客户端会把聊天内容当作名字发过来了,是只写坏了szData,还是其中的byCmd,bySubCmd,wData[3],szData都是错的,恰好导致这个包与参加A活动的协议头相同?

哎,N个人加班检查客户端代码,也没有发现到底是怎么回事,查看内存里的信息发现wData[3]里的内容也是错的,wData里的内容和szData里内容还不是连续的聊天信息.并没有想出如何才能发出这样的包,于是我在客户端发包的地方加了这样一条代码:

int Send(...)
{
    ....
    if(byCmd == 40 && bySubCmd == 120 && strlen(szData) > 32)
    {
        DebugBreak();
    }
}

既然你发非法包,那你就崩溃一下,然后生成dmp文件,我看dmp文件里的调用栈好了.

PS:其实这次关闭活动的方法有一定运气成分,熟悉调用栈的同学应该知道,上面的函数未必就是刚刚调用过的函数,很有可能是很久之前调用的,只不过从那以后并没有更深的调用来覆盖它.但是调试core文件这种事情,本来就和运气有很大关系,我每次解决崩溃后,都有一种我运气真好的感觉.

你可能感兴趣的:(调试)