1. This指针
This指针只有在调用class中的method的时候才会被当成参数传进去。假设我们有一个Person类,如下:
1 | class Person |
我们调用一下washFace(),然后反调试看一下底层到底发生了什么:
在washFace()的call之前,我们可以看到编译器把ebp-10
的值放到ecx中了,其实现在ecx的值也就相当于第一个参数,有点__fastcall
的意思。这个ebp-10
很明显就是我们创建Person这个对象的地方,也就是Main中的第一个局部变量的地址。这个局部变量的地址就是this
指针。
我们step in这个function看一看:
我们可以看到在填充完缓冲区之后,直接把ecx
(即传进来的第一个参数)放到了[ebp-8]
,也就是第一个局部变量里面中。那么现在WashFace()这个函数的第一个局部变量就变成了this
指针。之后获取成员和改变成员的操作也是通过这个this指针(即Main函数中创建对象的地址)进行修改的。
2. 构造函数
如果没有初始化你的成员,而且直接使用成员,这是很危险的。因为编译器不会给你自动初始化,那些成员现在的值都是stack或者heap中的垃圾。所以定义一个类,你一定要初始化成员。
第一种情况:在栈中创建的对象
假设我们有如下的class:
1 | class Person |
第一种情况:假设我们再栈中创建的对象:
我们底层看一下构造函数何时被调用:
我们创建的时候直接call了constructor。并且传入了三个参数:this 指针, 12h 和 0BEh。
我们看一下构造函数内部怎么做的:
还是一样的,把this指针里面的地址放到构造函数的第一个局部变量里。构造函数接下来使用this指针对Main中我们创建的对象修改。
所以本质上,我们修改成员变量都是通过this
指针在创建对象的stack里面对成员变量进行修改。
就拿我们的例子来说,成员变量其实一直都在Main函数的Stack中。这些成员变量本质上其实是Main函数中连续的局部变量。
同样的,如果我们在Heap中创建对象,那么这个对象的成员其实都在heap中,不管怎么改成员都是通过this指针来修改heap。
第二种情况:使用new在堆中创建对象:
底层代码如下:
我们可以看到首先调用了new这个方法(其实new最终调用的Win32API 为HeapAlloc)。之后再调用了构造函数。
我们可以看到调用了new之后还有个if…else… 这个判断就是如果内存分配成功执行构造函数,如果没有分配成功就跳过。因为可以看到cmp dword ptr [ebp-110h],0
,这个里面的[ebp-110h]
就来自调用new之后的返回值(第CE560E)的eax
中,new返回的eax
中放的其实就是开辟的地址,如果开辟成功就是开辟的地址,如果开辟失败则eax
为0。
所以用new在堆中创建对象可以分为两个步骤
一, malloc再堆中开辟内存
二, 判断是否malloc成功,如果成功则执行构造函数;如果malloc失败则不执行构造函数
3. 析构函数
第一种情况: 对象在栈中
大家都知道析构函数是在对象被销毁的时候被调用。这种概念很模棱两可,什么时候才是销毁的时候呢?
我们可以通过底层来看看析构函数到底是什么时候被调用的。
假设我们有如下的析构函数:
1 | class Person |
我们反汇编一下看一下到底在哪调用~Person这个析构函数。
我们可以看到,如果我们在main函数里面创建了Person对象,那么Main函数结束时就会马上调用析构函数,并且还是传入this指针。
所以如果是在某个函数中创建了对象,那么析构函数会在这个函数结束时被调用。
如果是在全局区创建了对象,那么析构函数会在整个程序关闭的同时被调用。
第二种情况:对象在堆中,使用delete摧毁对象
首先需要call一个scalar deleting destructor的函数,这个函数第二个参数。我们可以看到push 1
其实就是需要执行删除的次数。
我们跳进去看一下:
所以呢,delete在这里实际上也做了两件事情:
一, 调用所有对象的析构函数
二, free堆中的内存