购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

1.2 实战故事1:数据类型的不一致

服务器程序在测试阶段出现了随机崩溃,经过一段时间的调试,我们怀疑这可能是由于内存越界错误引起的,问题似乎出在一个特定的数据对象上。当程序尝试更新该对象的一个数据成员时,紧随其后的数据对象就会被损坏(这一问题是由将在第10章讨论的内存调试工具发现的)。

然而,这段代码看起来似乎是无辜的,因为它只是在访问自己的数据成员。令人困惑的是,这个简单操作如何会损坏另一个数据对象。通过深入调查,我们发现这个问题对象是在一个模块中创建的,然后传递到另一个模块,在那里进行数据成员的更新。

在一番探索后,我们发现两个模块对同一个数据对象的大小存在不一致的看法——调试器在第一个模块环境中显示一个尺寸,而在第二个模块中打印出另一个更大的尺寸。这令人大为惊讶,因为这个对象是在一个头文件中声明的,且这个头文件被两个项目共享。通过更深入地打印并比较每个模块内的对象布局及其数据成员的偏移量(对象的类型调试符号),明显看出编译器对对象的布局有所不同:一个模块中的所有数据成员都适当地对齐,而另一个模块则没有,而是将所有的数据成员打包在一起。数据对象以较小的尺寸被打包创建。

这种情况也得到了底层内存管理器分配的内存块大小的证实(在第2章将详细讨论如何获取此类信息)。当对象从打包对齐的模块传入未打包布局的模块时,更新数据成员的操作就会覆写内存并损坏附近的对象。图1-3以更简洁的方式描述了这个bug;一个T类型的结构体对象在模块A中被创建为打包格式,然后传到模块B,模块B却认为它是未打包的格式。模块B中的数据成员data3覆写了已分配的内存块。

图1-3 由于数据类型不一致导致的内存覆写问题

那么这种情况是如何发生的呢?从结果中可以看到,尽管对象在头文件中被正确地声明,但bug却来自另一个头文件,其中使用了以下编译指令:

开发工程师打算将编译指令中间的结构体打包为4字节边界。这个指令被微软的Visual Studio编译器正确解析。然而,当同一份代码被AIX的Visual Age C++编译器编译时,问题就出现了。该编译器有一个类似但略有不同的编译指令语法来结束打包作用域:

由于这个语法差异,Visual Age C++编译器只识别了打包的开始编译指令(第一行),却忽略了结束打包的编译指令(最后一行)。在程序员试图结束数据打包的地方,编译器仍然在继续打包数据结构。在模块A中,受害对象在引入包含上述编译指令的头文件之后声明,而在模块B中,问题头文件没有被引入,所以对象没有被打包。这就是不一致性产生的原因。

数据类型的调试符号准确地反映了编译器如何解析数据类型,生成的机器指令也根据这个解析结果来操作数据对象。具体来说,当创建新对象时,编译器会申请与结构体大小相等的内存块;数据成员的访问地址是通过从内存块开头的偏移量来计算的。 NFNewwz6Nw0TL/hR+trrkU1wt7BEmuabzJGF4+LOe4oNsNtCi2jcR+YjvF6ZKalv

点击中间区域
呼出菜单
上一章
目录
下一章
×

打开