我有一个简单的多态构造,只有一个纯虚函数Foo
。它使用的大型项目中唯一的缺陷是该项目使用了几个全局静态来集中参数加载和事件记录(不能轻易摆脱遗留代码)。
项目信息:
- 平台工具集:
v110_xp
- 静态库中的 MFC
- MBCS 字符集
- 调用约定:
__cdecl
- 禁用所有优化
- 警告级别4,整个项目没有警告
代码:
class Base
{
public:
Base(){}
virtual ~Base(void){}
virtual void Foo(void) = 0;
};
class Derived
: public Base
{
public:
Derived(void) : Base(){}
virtual void Foo(void) override
{
double a = sqrt(4.9);
double b = -a;
}
调用代码(没关系,到处都是相同的行为)
BOOL MainMFCApp::InitInstance()
{
Derived* d = new Derived();
d->Foo();
delete d;
...
}
问题是,当在调试中运行时(未通过发布测试),当我们最终进入 functionFoo
时,this
指针被“损坏”:
this = 0xcccccccc
this.__vfptr = <unable to read memory>
当我深入研究输入函数的汇编代码时,我看到以下内容:
13:
14:
15: void Derived::Foo(void)
16: {
015E3570 55 push ebp
015E3571 8B EC mov ebp,esp
015E3573 83 E4 F8 and esp,0FFFFFFF8h
015E3576 81 EC EC 00 00 00 sub esp,0ECh
015E357C 53 push ebx
015E357D 56 push esi
015E357E 57 push edi
015E357F 51 push ecx
015E3580 8D BD 14 FF FF FF lea edi,[ebp-0ECh]
015E3586 B9 3B 00 00 00 mov ecx,3Bh
015E358B B8 CC CC CC CC mov eax,0CCCCCCCCh
015E3590 F3 AB rep stos dword ptr es:[edi]
015E3592 59 pop ecx
015E3593 89 8C 24 F0 00 00 00 mov dword ptr [esp+0F0h],ecx
17: double a = sqrt(4.9);
015E359A F2 0F 10 05 00 55 09 02 movsd xmm0,mmword ptr ds:[2095500h]
015E35A2 E8 72 D4 FD FF call __libm_sse2_sqrt_precise (015C0A19h)
015E35A7 F2 0F 11 84 24 E0 00 00 00 movsd mmword ptr [esp+0E0h],xmm0
18: double b = -a;
015E35B0 F2 0F 10 84 24 E0 00 00 00 movsd xmm0,mmword ptr [esp+0E0h]
015E35B9 66 0F 57 05 10 55 09 02 xorpd xmm0,xmmword ptr ds:[2095510h]
015E35C1 F2 0F 11 84 24 D0 00 00 00 movsd mmword ptr [esp+0D0h],xmm0
19: return;
20: }
015E35CA 5F pop edi
015E35CB 5E pop esi
015E35CC 5B pop ebx
015E35CD 8B E5 mov esp,ebp
015E35CF 5D pop ebp
015E35D0 C3 ret
--- No source file -------------------------------------------------------------
015E35D1 CC int 3
...
015E35EF CC int 3
第 17 行的断点,就在进入函数体之前:使用监视窗口检查寄存器后面的对象ecx
(带有强制转换为Derived*
)显示ecx
包含我需要的指针(指向对象),但由于某种原因,它被mov
'ed to看似随机的地址[esp+0F0h]
。
现在真正有趣/令人惊叹的部分:当我改变时
double b = -a;
至
double b = -1.0 * a;
并再次编译,一切都神奇地工作。功能程序集现在已更改为:
13:
14:
15: void Derived::Foo(void)
16: {
00863570 55 push ebp
00863571 8B EC mov ebp,esp
00863573 81 EC EC 00 00 00 sub esp,0ECh
00863579 53 push ebx
0086357A 56 push esi
0086357B 57 push edi
0086357C 51 push ecx
0086357D 8D BD 14 FF FF FF lea edi,[ebp-0ECh]
00863583 B9 3B 00 00 00 mov ecx,3Bh
00863588 B8 CC CC CC CC mov eax,0CCCCCCCCh
0086358D F3 AB rep stos dword ptr es:[edi]
0086358F 59 pop ecx
00863590 89 4D F8 mov dword ptr [this],ecx
17: double a = sqrt(4.9);
00863593 F2 0F 10 05 00 55 31 01 movsd xmm0,mmword ptr ds:[1315500h]
0086359B E8 79 D4 FD FF call __libm_sse2_sqrt_precise (0840A19h)
008635A0 F2 0F 11 45 E8 movsd mmword ptr [a],xmm0
18: double b = -1.0 * a;
008635A5 F2 0F 10 05 10 55 31 01 movsd xmm0,mmword ptr ds:[1315510h]
008635AD F2 0F 59 45 E8 mulsd xmm0,mmword ptr [a]
008635B2 F2 0F 11 45 D8 movsd mmword ptr [b],xmm0
19: return;
20: }
008635B7 5F pop edi
008635B8 5E pop esi
008635B9 5B pop ebx
008635BA 81 C4 EC 00 00 00 add esp,0ECh
008635C0 3B EC cmp ebp,esp
008635C2 E8 32 CD FC FF call __RTC_CheckEsp (08302F9h)
008635C7 8B E5 mov esp,ebp
008635C9 5D pop ebp
008635CA C3 ret
--- No source file -------------------------------------------------------------
008635CB CC int 3
...
008635EF CC int 3
现在生成的代码很好地将寄存器中的指针移动ecx
到this
. 其他区别:
- 不同的内存地址/偏移量
mulsd
而不是xorpd
否定变量and esp,0FFFFFFF8h
消失(??用于对齐堆栈指针esp
??)- 更多清理(在函数体之后)??(
add cmp call
)
对于这两种情况,参数被推送到堆栈的装配部分是相同的:
53: d->Foo();
011A500B 8B 45 E0 mov eax,dword ptr [d]
011A500E 8B 10 mov edx,dword ptr [eax]
011A5010 8B F4 mov esi,esp
011A5012 8B 4D E0 mov ecx,dword ptr [d]
011A5015 8B 42 04 mov eax,dword ptr [edx+4]
011A5018 FF D0 call eax
当然,当我尝试使用最小、完整和可验证的示例来复制它时,一切都按预期进行。但在我的大项目中,它一直失败。我不确定哪些参数会影响编译,并且对汇编了解得不够多,甚至看不到那里发生了什么;因此,我在这里问是希望有人以前见过或认识到这种行为。
注意:当我删除sqrt
呼叫时,它也可以再次工作。
更新:
- 发布没有问题
- VS2012 SP4 (v11.0.61030.00)
- 引用成员变量时问题仍然存在(iso 没有成员引用)
- TODO:尝试不使用全局静态变量