今天看到一段同事的代码:
ZeroMemory(&m_PacketInfo, sizeof(packet_info));
struct packet_info
{
string m_strModule; //模块
string m_strProtocol; //协议
string m_strClientHostIP; //客户端IP
string m_strClientHostPort; //客户端端口
...
};
按照我的经验,这种对类对象进行ZeroMemory或者memset的代码会导致程序崩掉。因为会覆盖掉类对象中的关键域,比如虚表。但是同事在vs2005中这样做却很正常。让我很奇怪,于是花了点时间分析了下。
首先,std::string的vc实现并没有虚表,如下:
0:000> dt string
QueueTest!string
+0x000 _Myproxy : Ptr32 std::_Container_proxy
+0x004 _Bx : std::_String_val::_Bxty
+0x014 _Mysize : Uint4B
+0x018 _Myres : Uint4B
+0x01c _Alval : std::allocator
=00000000`01202180
std::basic_string::npos : Uint4B
所以ZeroMemory后不会导致非法访问。
翻了一下std::string的实现,发现就是一普通的模板类,没有用到继承等OO特性。跟一普通的结构体和一系列配套函数的简单组合没太大区别。
于是写了一小段测试代码
class Father
{
public:
Father() {}
~Father(){}
virtual void Show()
{
printf("Father\n");
}
};
class Son: public Father
{
public:
void Show()
{
printf("Son\n");
}
};
void main()
{
Son son;
son.Show();
ZeroMemory(&son, sizeof(Son));
son.Show();
...
}
我对这段代码的预期是,因为有继承以及虚函数,所以会有虚表。ZeroMemory会导致虚表被破坏,然后在第二次Show的时候非法访问。但实际运行效果依然正常。
于是检查了一下man的反汇编代码:
0:000:x86> uf main
QueueTest!main:
002fd120 push ebp
002fd121 mov ebp,esp
...
002fd15d lea ecx,[ebp-14h]
002fd160 call QueueTest!ILT+3860(??0SonQAEXZ) (002faf19)
002fd165 mov dword ptr [ebp-4],0
002fd16c lea ecx,[ebp-14h]
002fd16f call QueueTest!ILT+4840(?ShowSonUAEXXZ) (002fb2ed)
002fd174 push 4
002fd176 push 0
002fd178 lea eax,[ebp-14h]
002fd17b push eax
002fd17c call QueueTest!ILT+1520(_memset) (002fa5f5)
002fd181 add esp,0Ch
002fd184 lea ecx,[ebp-14h]
002fd187 call QueueTest!ILT+4840(?ShowSonUAEXXZ) (002fb2ed)
002fd18c mov dword ptr [ebp-4],0FFFFFFFFh
002fd193 lea ecx,[ebp-14h]
002fd196 call QueueTest!ILT+4125(??1SonQAEXZ) (002fb022)
...
002fd1c9 mov esp,ebp
002fd1cb pop ebp
002fd1cc ret
可以看出来,这段代码里根本就没有用到虚表,而是直接把内嵌函数地址。仔细琢磨琢磨,这样做确实有些道理:
- son是明确的Son类对象,son.Show是一个明确的函数,无需使用虚表进行间接调用。
- 效率更高。
再想想什么时候才需要虚表呢?动态绑定。
于是改了改测试代码,如下(Father和Son的定义不变):
void main()
{
Son son;
Father *pson = (Father*)&son;
pson->Show();
ZeroMemory(&son, sizeof(Son));
pson->Show();
...
}
看看反汇编码:
0:000:x86> uf main
QueueTest!main:
011ad120 push ebp
011ad121 mov ebp,esp
...
011ad15d lea ecx,[ebp-14h]
011ad160 call QueueTest!ILT+3860(??0SonQAEXZ) (011aaf19)
011ad165 mov dword ptr [ebp-4],0
011ad16c lea eax,[ebp-14h]
011ad16f mov dword ptr [ebp-20h],eax
011ad172 mov eax,dword ptr [ebp-20h]
011ad175 mov edx,dword ptr [eax]
011ad177 mov esi,esp
011ad179 mov ecx,dword ptr [ebp-20h]
011ad17c mov eax,dword ptr [edx]
011ad17e call eax <--------
011ad180 cmp esi,esp
011ad182 call QueueTest!ILT+3870(__RTC_CheckEsp) (011aaf23)
011ad187 push 4
011ad189 push 0
011ad18b lea eax,[ebp-14h]
011ad18e push eax
011ad18f call QueueTest!ILT+1520(_memset) (011aa5f5)
011ad194 add esp,0Ch
011ad197 mov eax,dword ptr [ebp-20h]
011ad19a mov edx,dword ptr [eax]
011ad19c mov esi,esp
011ad19e mov ecx,dword ptr [ebp-20h]
011ad1a1 mov eax,dword ptr [edx]
011ad1a3 call eax <--------
011ad1a5 cmp esi,esp
011ad1a7 call QueueTest!ILT+3870(__RTC_CheckEsp) (011aaf23)
011ad1ac mov dword ptr [ebp-4],0FFFFFFFFh
011ad1b3 lea ecx,[ebp-14h]
...
011ad1e9 mov esp,ebp
011ad1eb pop ebp
011ad1ec ret
可以看到这次用了虚表。再运行,果然崩了。
但是gcc的实现不同,于是最开始的那行ZeroMemory一跑就崩。