深入探讨this指针:从汇编的角度考虑

来源:互联网 发布:淘宝客服头像图片女 编辑:程序博客网 时间:2024/04/29 06:44

深入探讨this指针:从汇编的角度考虑

        

        说道this指针想必大家都不陌生,在C++中,this指针是一个常量指针,形如(类名::*const this)。它指向当前正在起作用的对象自身。成员函数中的this指针貌似有些奇特,到底从何来?记得很久以前我认为他理所当然,当时我在想自己的函数认识自己很正常啊,但是当我认识到,类中函数其实只有一份拷贝的时候,才开始考虑如此多的对象,在调用同一份拷贝的函数时,不同的对象调用时this指针是不一样的,也就是说函数内部其实并没有一个固定的this指针,那么this指针到底是如何与函数绑定的呢?后来接触到一种说话成员函数其实有一个默认参数,那就是this指针,也就是说this指针也不过是对象在调用函数时将自身地址传递给了所谓的this函数参数罢了,这在大多数场合都是完全正确的,在不涉及细节的情况下我们完全有理由这么认为。不过今天我发现似乎这个说法还不够严谨(win7  vs2010测试结果)。

知识引入:
《深入探索C++对象模型》中提到成员函数时,当成员函数不是静态的,虚函数,那么我们有以下结论:
(1) &类名::函数名 获取的是成员函数的实际地址;
(2) 对于函数x来讲obj.x()编译器转化后表现为x(&obj),&obj作为this指针传入。
(3) 无法通过强制类型转换在类成员函数指针与其外形几乎一样的普通函数指针之间进行有效的转换。

案例一:正常的获取地址
#include <stdio.h>#include <string>using namespace std;class Point{public:void x(){printf("call Point::x()\n");}};int main(){printf("&Point::x = 0x%x\n",&Point::x);Point obj;obj.x();void (Point::*xaddr)() = &Point::x;typedef void (*FUNC)();//FUNC px = (FUNC)xaddr;  //语法错误 无效的转换//FUNC ppx = static_cast<FUNC>(&Point::x); //语法错误  无效的转换FUNC fun;memcpy(&fun,&xaddr,sizeof(FUNC));fun();getchar();return 0;}
fun的调用成功似乎让我们觉得从类的成员函数到普通函数之间找到了一种有效的转换方法,但是现实往往没有那么理想,既然标准不允许你转换,你也休想那么完美的绕开的这项规定,看来想要钻“法律”的空子也并非易事啊。也许你会想到如下代码:
typedef void (*F)(Point *const);F fx = (F)xaddr;
结果一样:无效的转换,语法错误。尽管如此,我还是不满足于现状,怎么会这样,不是说this只是默认的第一个参数吗?如果是这样我就一定打破这道封锁,为所欲为~。

案例二:法网恢恢
#include <stdio.h>#include <string>using namespace std;class Point{public:void x(){printf("call Point::x()\n");sdata = 100;data = 1000;}private:int data;static int sdata;};int Point::sdata = 0;int main(){printf("&Point::x = 0x%x\n",&Point::x);Point obj;obj.x();void (Point::*xaddr)() = &Point::x;typedef void (*FUNC)();FUNC fun;memcpy(&fun,&xaddr,sizeof(FUNC));fun();getchar();return 0;}
该案例在调用fun时当运行到data = 100时程序崩溃,认真分析一下还是很明朗的,data = 100相当于this->data = 100;而实际调用函数根本没有传入任何参数。这就导致该调用的this根本不存在。不挂掉才怪,遇到这种情况,我直接做了以下尝试:
(1)这节fun中传入&obj结果编译器提示函数不接受任何参数;
(2)然后就将FUNC的声明改为 typedef void (*FUNC)(Point *const);然后传入&obj,此时仍然不行,这就不得不让我怀疑this指针的传递并没有那么简单,它很有可能做过特殊处理。
经过(1)(2)的尝试失败后,我想到了嵌入汇编语言的方法来验证一下this是不是作为第一个参数传递的,具体结果请看案例三。
知识点:函数参数的获取:嵌入汇编的方法

案例三:瞒天过海
#include <stdio.h>#include <string>using namespace std;class Point{public:void x(){printf("call Point::x()\n");Point *pAddr = 0;__asm  {  lea eax,pAddr  mov edx,[ebp+8]   // ebp+8 为第一个形参的地址,ebp+C 为第二个形参的地址,以此类推  mov [eax],edx  }sdata = 100;pAddr->data = 1000;}private:int data;static int sdata;};int Point::sdata = 0;int main(){printf("&Point::x = 0x%x\n",&Point::x);Point obj;void (Point::*xaddr)() = &Point::x;typedef void (*FUNC)(Point *const);FUNC fun;memcpy(&fun,&xaddr,sizeof(FUNC));fun(&obj); //打擦边球obj.x();   //正规调用getchar();return 0;}
        案例三中,fun调用成功,且功能如你所见的正常执行,这说明正常传参时第一个参数在ebp+8这个地址中,然而obj.x()的调用方式与x(&obj)的形式表现的貌似并不完全符合,因为程序在pAddr->data这一行崩溃了,这说明this指针并不在第一个参数中,它的传递还是做过特殊处理的。
         经过网上查资料和查看x函数的反汇编后的汇编代码最终发现,原来this指针存储在ecx中,而不是ebp+8这个地址中,所以我们进一步测试.

案例四:拨云见日
#include <stdio.h>#include <string>using namespace std;class Point{public:void x(){//printf("call Point::x()\n");Point *pAddr = 0;__asm  {  lea eax,pAddr  mov edx,ecx   //ecx换成[ebp-8]也可mov [eax],edx  }pAddr->data = 1000;}private:int data;static int sdata;};int Point::sdata = 0;int main(){printf("&Point::x = 0x%x\n",&Point::x);Point obj;obj.x();   //正规调用void (Point::*xaddr)() = &Point::x;typedef void (*FUNC)(Point *const);FUNC fun;memcpy(&fun,&xaddr,sizeof(FUNC));fun(&obj); //打擦边球getchar();return 0;}
案例四中可以成功通过ecx寄存器获得this指针的正确值,且通过查看汇编得知ecx同样被保存在[ebp-8]中,所以替换成[ebp-8]也行。此时可以不通过this指针,或者说隐式的使用this指针来改变data的值,不过此时fun调用失败,也能够为案例三中讲到fun中传递的参数作为第一个参数被保存在[ebp+8]中,这里使用的是[ebp-8]自然取不到正确值了。通过三四我们似乎看出无法找出是两种调用方式统一的代码,这个以后慢慢研究吧。不过值得注意的地方是,当案例四中第8行的注释去掉之后,正常调用也会崩溃,而改用[ebp-8]就又正常了。这个具体的细节不是很懂,参考这篇文章看看:this指针的常见误区 (ECX寄存器存放this指针)

总结:至此我们对this指针和成员函数调用有了更进一步的认识,
(1) this 指针只存在于成员函数内部,
(2) 成员函数的调用时,this指针并没有那么神秘,那完全可以认为对象在调用成员函数时将自身地址作为函数的第一个参数在默认情况下传递给了函数的默认形参,只不过这个默认形参被命名为this罢了,仅此而已,小样,不就是穿了一个马甲吗!
(3) 尽管我们可以把this指针当做成员函数的第一个默认参数,但是我们心里应该明白,事实上它还是有点特殊待遇的,它与正常的函数参数的传递过程在行为上还是有些差距的。

分析片段:正常使用this时的反汇编代码
源代码:
<span style="font-size:14px;">#include <stdio.h>#include <string>using namespace std;class Point{public:void x(){this->data = 100;}private:int data;static int sdata;};int Point::sdata = 0;int main(){Point obj;obj.x();getchar();return 0;}</span>
反汇编:
void x(){00A71B60  push        ebp  00A71B61  mov         ebp,esp  00A71B63  sub         esp,0CCh  00A71B69  push        ebx  00A71B6A  push        esi  00A71B6B  push        edi  00A71B6C  push        ecx  00A71B6D  lea         edi,[ebp-0CCh]  00A71B73  mov         ecx,33h  00A71B78  mov         eax,0CCCCCCCCh  00A71B7D  rep stos    dword ptr es:[edi]  00A71B7F  pop         ecx  00A71B80  mov         dword ptr [ebp-8],ecx  this->data = 100;00A71B83  mov         eax,dword ptr [this]  00A71B86  mov         dword ptr [eax],64h  }
虽然看出哪是存放this指针的寄存器没有那么简单,但是通过
00A71B80  mov         dword ptr [ebp-8],ecx  
我们可以看出this指针同样被存放在[ebp-8]中,至于不同的编译器和环境,我们需要手动反汇编一下查找this指针存放的具体位置才可以,这说明该文讲到的方法仅仅用于帮助理解语言特性,可移植性较差。



警示:本文并没有对this指针的存放位置下任何定论,ecx,[ebp-8]这些都只是VS2010上的一个可行性方案,不过可以肯定的时this指针作为参数传入做了特殊处理,并不像普通的参数表现同样的行为。






0 0
原创粉丝点击