关于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的地址
test的地址为0x0044fd74,在内存中此时成员m_nInt已被初始化为0了(如下图):
而基址指针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指针的首地址。
从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 返回时使用。