您有一个bug,请注意查收!
这次的标题有点搞怪,工程师最怕的就是“Bug”,希望没有吓到大家。。。
这次起点放低,写点比较基础的,相信大家都能看懂。从曾经遇到的一个Server端代码的bug说起。
背景
几年前的某一段时间,我负责给一款游戏做跨平台移植,准备开发一款技术Demo。由于这款游戏是早些年端游繁盛时代的产品,所以Client和Server都是C++开发。我们先把Client由PC端移植到Android,在解决了所有的问题之后我们开始进行IOS的移植。对这些平台了解的人会知道他们都是类Unix系统,所以移植起来会比较容易。不幸的是我在IOS平台下遇到了一个奇怪的崩溃。。。
调试及解决
分析崩溃栈发现堆栈信息无法准确反应崩溃原因,而且崩溃时机不定(跟游戏逻辑相关)。有经验的人可能已经猜到了,发生了内存越界。通过反复重现,可确定内存越界发生在Server发送某数据包给Client,Client接收之后解析的过程中。这种问题在平时的工作中算是比较难搞的,因为平时调试所依赖的堆栈信息已经靠不住了。
背景知识:
为了保持数据打包和解析的一致,Server端的数据包打包代码以及Client的解析代码使用了相同的数据包结构定义。
由于头文件指导编译期生成代码的内存布局,所以按理说Client和Server两端的数据在内存中的大小应该一致。
2015年2月苹果要求所有新提交审核的应用都必须支持64位。2015年6月1日苹果App Store中所有应用更新都必须支持64位。所以应苹果要求我们把IOS平台代码编译为64位,但是Android和PC始终保持编译为32位程序。测试发现Android和PC平台程序跑起来完全没有问题,所以猜测可能是由于IOS编译为64位程序导致的问题。
科普一下,一般为了保持最小的数据传输量,网络数据包的定义都按一字节对齐来使数据紧密地排列(关于数据对齐在IOS上有个很奇怪的问题,我相信这个问题很少有人知道,我准备在后面的文章中跟大家分享一下)。所以可以通过数据结构的成员定义直接算出数据包占用内存的大小。
拿上面的代码为例,这个数据包大小应该为4 + 2 + 1 + 5 * 1 + 4 = 16 Byte。
调试:
通过输出所有发送包和解析包的大小有如下发现:
1.Client做某数据包解析的时候比Server端实际发送的多读取了4 Byte。然而检查数据包定义并未发现有64位系统与32位系统占内存大小不同的数据类型被使用。
2.Server实际发送的数据大小比数据包定义多出了4 Byte,IOS客户端解析的时候比数据包定义多解析了8 Byte。也就是同一个数据包,32位环境下多了4 Byte,64位环境下多了8 Byte。
问题分析到这里,基本就可以断定:类的定义中存在指针,并且这个指针是隐藏的,它就是指向虚函数表的指针。
虚函数表指针在编译期将类的大小增加了几个字节。在Server端由于是32位程序,指针按照4 Byte编译。IOS端是64位程序,指针按8 Byte编译。这就导致了IOS端做数据解析的时候比实际Server发送的多解析了4 Byte,发生了内存越界。
仔细检查代码发现这个数据包的定义是这样的(由于这是几年前的经历,所以这里我写了个小例子,原来的代码已经不在了):
在我们发送的这个数据包的基类中有一个虚函数定义。由于虚函数的引入,在类对象的头部要增加一个虚函数表指针指向类的虚函数表。C++的动态绑定是由虚函数表来实现的,详细的关于虚函数表的知识我这里不多讲了,不了解的朋友可以去看看《More Effective C++》。
对于虚函数表感兴趣的朋友定义一个类Derived对象,然后在VS中调试一下。我们可以在Watch窗口中观察一个类的内存布局。
上图中可以看到在对象的内存区域的头部有一个名为“_vfptr”的变量,这就是传说中的虚函数表指针,也就是这次bug的元凶。
解决:
检查所有数据包结构定义代码,发现这种带有虚函数定义的数据包类大量存在。分析为什么使用虚函数,然后修改代码,删掉虚函数定义。编译运行,问题解决了。
问题总结:
这个问题在Client与Server同为32位或64位程序的时候不会发生,因为Client和Server两边对这个隐含的指针的大小的解析是一致的(4/8 Byte),但是这样的代码并不是没有问题的。首先这里隐藏着一枚我们没有意识到的炸弹存在,说不定哪一天我们就踩上了。再者如果是为移动应用开发的代码,大量的数据包定义使用虚函数,导致每一次网络通信,每一个数据包都多发送4/8 Byte。这样累积起来对于用户流量的浪费也不容小视。这不是一个严谨的工程师应该犯的错误。
写在最后
解决这个小问题花费了我基本一下午的时间。关于虚函数表的概念很多时候我们会在面试中遇到,但是真正理解为什么会有这样的面试题存在的人很少。甚至有人还跟我说:“你这个问题太底层,我们平时写代码都用不到;我已经写了4、5年C++了,从来没有遇到过关于虚函数表的问题。”
我一直坚持认为把技术的基础打好,才是避免出现各类问题的最根本办法。经常遇到挂着各种头衔的“XXX工程师”,聊起技术给我一种很不舒服的感觉,好像他的代码不是跑在计算机上,他对计算机一无所知,却对XXX领域了如执掌。有人可能觉得这是某一领域的专家,但是我认为计算机的领域分的没有那么细。无论用什么语言写的代码,最终都要跑到硬件上,而且数据结构及算法是通用的;无论使用何种图形API或者某一款图形引擎,图形学的基本原理是不变的;无论做什么方向的开发我们都要接触现代计算机操作系统,而各个系统涉及到的基本理论又是一致的。所以我认为在成为“XXX工程师”之前,应该先把自己定位为优秀的“计算机工程师”。
我发布在这里的文章都是转至我的微信订阅号,如果你想及时获得最新发布的文章,可以关注我的微信订阅号。