关于C++ 的 this指针

关于this指针

在编程过程中,我们都使用过this指针,可是this指针究竟从何而来却很少有人知道,现在我们一起来看一下this指针的由来

测试代码如下:

class CTest
{
public:
    void SetNumber(int number)
    {
        m_nInt = number;
    }

    int m_nInt = 0;
};

int main(int argc, char *argv[])
{
    CTest test;
    test.SetNumber(5);
    printf("CTest : m_nInt = %d\n", test.m_nInt);

    system("pause");
    return 0;
}

对编译生成的程序进行反汇编:

main函数部分的汇编代码如下

int main(int argc, char *argv[])
{
01183CD0  push        ebp  
01183CD1  mov         ebp,esp  
01183CD3  sub         esp,0D0h  
01183CD9  push        ebx  
01183CDA  push        esi  
01183CDB  push        edi  
01183CDC  lea         edi,[ebp-0D0h]  
01183CE2  mov         ecx,34h  
01183CE7  mov         eax,0CCCCCCCCh  
01183CEC  rep stos    dword ptr es:[edi]  
01183CEE  mov         eax,dword ptr ds:[01188000h]  
01183CF3  xor         eax,ebp  
01183CF5  mov         dword ptr [ebp-4],eax  
    CTest test;
01183CF8  lea         ecx,[test]  
01183CFB  call        CTest::CTest (011811E5h)  
    test.SetNumber(5);
01183D00  push        5  
01183D02  lea         ecx,[test]  
01183D05  call        CTest::SetNumber (011810EBh)  
    printf("CTest : m_nInt = %d\n", test.m_nInt);
01183D0A  mov         esi,esp  
01183D0C  mov         eax,dword ptr [test]  
01183D0F  push        eax  
01183D10  push        1185858h  
01183D15  call        dword ptr ds:[1189118h]  
01183D1B  add         esp,8  
01183D1E  cmp         esi,esp  
01183D20  call        __RTC_CheckEsp (01181140h)  

    system("pause");
01183D25  mov         esi,esp  
01183D27  push        1185874h  
01183D2C  call        dword ptr ds:[1189110h]  
01183D32  add         esp,4  
01183D35  cmp         esi,esp  
01183D37  call        __RTC_CheckEsp (01181140h)  
    return 0;
01183D3C  xor         eax,eax  
}
01183D3E  push        edx  
01183D3F  mov         ecx,ebp  
01183D41  push        eax  
01183D42  lea         edx,ds:[1183D70h]  
01183D48  call        @_RTC_CheckStackVars@8 (01181087h)  
01183D4D  pop         eax  
01183D4E  pop         edx  
01183D4F  pop         edi  
01183D50  pop         esi  
01183D51  pop         ebx  
01183D52  mov         ecx,dword ptr [ebp-4]  
01183D55  xor         ecx,ebp  
01183D57  call        @__security_check_cookie@4 (0118101Eh)  
01183D5C  add         esp,0D0h  
01183D62  cmp         ebp,esp  
01183D64  call        __RTC_CheckEsp (01181140h)  
01183D69  mov         esp,ebp  
01183D6B  pop         ebp  
01183D6C  ret  

我们先简单理解一下上述的汇编代码:

01183CD0  push        ebp  
01183CD1  mov         ebp,esp  
01183CD3  sub         esp,0D0h  
01183CD9  push        ebx  
01183CDA  push        esi  
01183CDB  push        edi  

这是main函数中最开头的一部分代码,主要用于利用栈来保护现场,保存外部调用函数的基址(ebp寄存器中),并将其设置当前函数的基址,然后保存之后要用到的几个寄存器中的数据,这样当函数返回后,保证其调用函数可以继续正常向下执行。并将栈指针寄存器esp向栈低地址移动, 这主要用于开辟栈空间给当前函数块中使用。

01183CDC  lea         edi,[ebp-0D0h]  
01183CE2  mov         ecx,34h  
01183CE7  mov         eax,0CCCCCCCCh  
01183CEC  rep stos    dword ptr es:[edi]  

这部分代码主要用于初始化当前的栈空间,将esp 至 ebp 之间的内存全部初始化为0xCC(0xCC 表示当前内存暂未使用过)。

01183CEE  mov         eax,dword ptr ds:[01188000h]  
01183CF3  xor         eax,ebp  
01183CF5  mov         dword ptr [ebp-4],eax  

这段代码主要在基址指针上的前四个字节插入随机数,之后会利用这个随机数进行检查,防止栈溢出攻击。

    CTest test;
01183CF8  lea         ecx,[test]  
01183CFB  call        CTest::CTest (011811E5h)  

这里很明显开始调用CTest的构造函数,跟进去

CTest::CTest:
011811E5  jmp         CTest::CTest (011813D0h)  

发现这里执行的是一次跳转,再跟:

CTest::CTest:
011813D0  push        ebp  
011813D1  mov         ebp,esp  
011813D3  sub         esp,0CCh  
011813D9  push        ebx  
011813DA  push        esi  
011813DB  push        edi  
011813DC  push        ecx  
011813DD  lea         edi,[ebp-0CCh]  
011813E3  mov         ecx,33h  
011813E8  mov         eax,0CCCCCCCCh  
011813ED  rep stos    dword ptr es:[edi]  
011813EF  pop         ecx  
011813F0  mov         dword ptr [this],ecx  
011813F3  mov         eax,dword ptr [this]  
011813F6  mov         dword ptr [eax],0  
011813FC  mov         eax,dword ptr [this]  
011813FF  pop         edi  
01181400  pop         esi  
01181401  pop         ebx  
01181402  mov         esp,ebp  
01181404  pop         ebp  
01181405  ret  

这里可以看出是由编译器提供的默认构造函数的汇编指令,并在此处将m_nInt 初始化为0了,可见在类中初始化的变量,即使不是在初始化函数中初始化的成员变量,最后还是在构造函数中初始化。

    test.SetNumber(5);
01183D00  push        5  
01183D02  lea         ecx,[test]  
01183D05  call        CTest::SetNumber (011810EBh)  

lea 为加载地址指令
这里我们看一下test的地址


02.png

test的地址为0x0044fd74,在内存中此时成员m_nInt已被初始化为0了(如下图):

01.png

而基址指针ebp为0x0044fd80,比test的首地址大12(8+4),即对象的大小+随机数保存位置的内存大小。
这里是VS的优化后显示的结果,实际应该是 [test] 等价于 [ebp - 12]

所以在调用函数之前不但传入了5,还通过ecx (隐含地)传入了对象的首地址,即所谓的“this指针”

接着看函数内部的实现,以证明上述观点!

同理由此jmp跳转,直接看内部实现:

    void SetNumber(int number)
    {
01181420  push        ebp  
01181421  mov         ebp,esp  
01181423  sub         esp,0CCh  
01181429  push        ebx  
0118142A  push        esi  
0118142B  push        edi  
0118142C  push        ecx  
0118142D  lea         edi,[ebp-0CCh]  
01181433  mov         ecx,33h  
01181438  mov         eax,0CCCCCCCCh  
0118143D  rep stos    dword ptr es:[edi]  
0118143F  pop         ecx  
01181440  mov         dword ptr [this],ecx  
        m_nInt = number;
01181443  mov         eax,dword ptr [this]  
01181446  mov         ecx,dword ptr [number]  
01181449  mov         dword ptr [eax],ecx  
    }
0118144B  pop         edi  
0118144C  pop         esi  
0118144D  pop         ebx  
0118144E  mov         esp,ebp  
01181450  pop         ebp  
01181451  ret         4  

这段代码一直到 0118143D所在行都是日常任务,开辟并初始化栈空间。

0118143F  pop         ecx  
01181440  mov         dword ptr [this],ecx  

从这行开始真正的工作了,这里将外部传进来得对象首地址存进了 “this”中,我们查看一下this的首地址是多少
在当前ebp之上,保存着ecx中传入的0x0044fd74的值之处就是this指针的首地址。

03.png
04.png

从04图中可以看到0x0044FC90 即为栈上为this分配的内存的首地址,这里将ecx中保存的对象所在的首地址(即指针)保存在了为this所分配的内存中,指针本身占用4个字节,由于内存对齐,此处编译器分配了8个字节。0x0044FC90 + 8 正好等于ebp中保存的值。

01181443  mov         eax,dword ptr [this]  
01181446  mov         ecx,dword ptr [number]  
01181449  mov         dword ptr [eax],ecx  

再看看[number] 指代的是什么(当然是形参number啦,但实际反汇编时不会这么明显,我们来看看实际到底指代的是什么)

由前面代码推得,在进入函数前,5已经压人栈中,所以5的地址必然大于当前的ebp的值,由图4可见number的地址为ebp+8。

ebp为首地址的指针为调用该函数的函数的栈的基址。
ebp + 4 为调用该函数处的下一行指令的的地址,用于ret 返回时使用。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

友情链接更多精彩内容