C++逆向分析——this指针

this指针

概述

C++是对C的拓展,C原有的语法C++都支持,并在此基础上拓展了一些语法:封装、继承、多态、模板等等。C++拓展新的语法是为了让使用更加方便、高效,这样就需要编译器多做了很多事情,接下来我们就需要一一学习这些概念。

封装

之前我们学习过结构体这个概念,那么结构体可以做参数传递吗?我们来看一下如下代码:

struct Student {
int a;
int b;
int c;
int d;
};
 
int Plus(Student s) {
return s.a + s.b + s.c + s.d;
}
 
void main() {
Student s = {1, 2, 3, 4};
int res = Plus(s);
return;
}

上面这段代码是定义一个结构体,然后将该结构体传入Plus函数(将结构体成员相加返回),那么问题来了,结构体它是否跟数组一样,传递的是指针呢?来看一下反汇编代码:

images/download/attachments/12714553/image2021-3-28_0-9-23.png

可以很清晰的看见,结构体作为参数传递时栈顶(ESP)提升了0x10(16个字节,也就是结构体的四个成员【int】的宽度),而后将ESP的值给了EAX,再通过EAX(ESP)将结构体的成员传入函数,结构体成员从左到右依次从栈顶向下复制进入堆栈。

也就是说当我们将结构体作为参数传递时与我们传整数什么的是没有本质区别的,唯一的区别就是传递结构体时不是使用的push来传递的,而是一次性的提升堆栈,然后mov赋值。

虽然我们可以使用结构体进行传参,但是这也存在一个问题,就是当我们使用结构体传参时,假设结构体有40个成员,那么就存在着大量的内存被复制,这样效率很低,是不推荐使用的

那如果非要这样使用该怎么办呢?我们可以使用指针传递的方式来,修改一下代码:

struct Student {
int a;
int b;
int c;
int d;
};
 
int Plus(Student* p) {
return p->a + p->b + p->c + p->d;
}
 
void main() {
Student s = {1, 2, 3, 4};
int res = Plus(&s);
return;
}

images/download/attachments/12714553/image2021-3-28_0-24-53.png

这样我们就可以使用指针的方式来避免内存的重复使用,效率更高。

可能很多人看到这就很疑惑了,那这跟C++有什么关系呢?我们之前说过C++和C的本质区别,就是编译器替代我们做了很多事情;别着急,慢慢来看。

我们使用指针优化过的代码,实际上还是存在小缺陷的,当结构体成员很多的时候,我们在Plus函数体内就要用指针的调用方式,一堆成员相加...

那么是否可以让我们调用更加简单,更加方便呢?如下代码就可以:

struct Student {
int a;
int b;
int c;
int d;
 
int Plus() {
return a + b + c + d;
}
};
 
void main() {
Student s = {1, 2, 3, 4};
int res = s.Plus();
return;
}

将函数放在结构体内,就不需要我们再去写传参、再去使用指针的调用方式了,因为这些工作编译器帮我们完成了,而本质上这与指针调用没有区别:

images/download/attachments/12714553/image2021-3-28_0-37-54.png

而这种写法就是C++的概念:封装;也就是说将函数写在结构体内的形式就称之为封装,其带来的好处就是我们可以更加方便的使用结构体的成员。

讲到了封装,我们就要知道另外两个概念:

  1. :带有函数的结构体,称为类;

  2. 成员函数:结构体里的函数,称为成员函数

    1. 函数本身不占用结构体的空间(函数不属于结构体

    2. 调用成员函数的方法与调用结构体成员的语法是一样的 → 结构体名称.函数名()

this指针

之前我们学过了封装,如下代码:

struct Student {
int a;
int b;
int c;
int d;
 
int Plus() {
return a + b + c + d;
}
};
 
void main() {
Student s = {1, 2, 3, 4};
int res = s.Plus();
return;
}

其对应的反汇编代码如下:

images/download/attachments/12714553/image2021-3-28_0-37-54.png

可以看见我们使用s.Plus()的时候,传递的参数是一个指针,这个指针就是当前结构体的地址,这个指针就是this指针。(通常情况下编译器会使用ecx来传递当前结构体的指针)

 

我自己实验如下:

#include "StdAfx.h"


struct Student {
	int a;
	int b;
	int c;
	int d;
 
	int Plus() {
		return a + b + c + d;
	}
};
 
void main() {
	Student s = {1, 2, 3, 4};
	int res = s.Plus();
	return;
}

 

 

 

那么当我们将Plus函数修改成无返回值,不调用结构体成员后,这个指针还会传递过来么?

struct Student {
int a;
int b;
int c;
int d;
 
void Plus() {
 
}
};
 
void main() {
Student s = {1, 2, 3, 4};
s.Plus();
return;
}

我们看下反汇编代码,发现指针依然会传递过来:

images/download/attachments/12714553/image2021-3-28_0-51-7.png

那也就是说this指针是编译器默认传入的,通常会通过ecx进行参数的传递,不管你用还是不用,它都存在着

既然this指针会作为参数传递,我们是否也可以直接使用这个指针呢?答案是可以的:

struct Student {
int a;
int b;
 
void Init(int a, int b) {
this->a = a;
this->b = b;
}
 
};

我们在结构体的成员函数内使用this这个关键词就可以调用了,如上代码所示。

那么this指针有什么作用呢?我们可以看下如下代码:

struct Student {
int a;
int b;
 
void Init(int a, int b) {
a = a;
b = b;
}
 
};
 
void main() {
Student s;
s.Init(1,2);
return;
}

这段代码我们要实现的就是,使用成员函数初始化成员的值,但是实际运行却不符合我们的预期:

images/download/attachments/12714553/image2021-3-28_1-29-45.png

跟进反汇编代码发现,这里就是将传入的参数赋值给了参数本身,并没有改变成员的值,这是因为编译器根本不知道你这里的a到底是谁,所以我们就需要借助this指针来实现:

#include <stdio.h>
 
struct Student {
int a;
int b;
 
void Init(int a, int b) {
this->a = a;
this->b = b;
}
 
void Print() {
printf("%d %d", this->a, this->b);
}
 
};
 
void main() {
Student s;
s.Init(1,2);
s.Print();
return;
}

为了方便,添加一个成员函数,用于打印输出成员的值:

images/download/attachments/12714553/image2021-3-28_1-37-52.png

可以看见,这里成功进行初始化了。

总结:

  1. this指针是编译器默认传入的,通常会使用ecx进行参数的传递

  2. 成员函数都有this指针,无论是否使用

  3. this指针不能做++ --等运算,也不可以被重新赋值

  4. this指针不占用结构体的宽度

 

 

this指针和函数都不占用struct的空间,我们验证下:

#include <cstdio>

struct A {
    char* hello() {
        return "hi";
    }
};

int main() {
    A a;
    printf("empty struct size=%d\n", sizeof(a));
}

 

输出为1。

所以可以知道,没有任何成员变量的struct大小为1.

 

恶意代码分析实战里20.1.1章节有一个例子:

#include "StdAfx.h"


class S {
public:
	int x; 
	
	void hello() {
		if (x == 10) printf("x is 10\n");
	}
};
 
void main() {
	S s1;
	s1.x = 9;
	s1.hello();

	S s2;
	s2.x = 10;
	s2.hello();
	return;
}

 我贴下其汇编结果(vc6下的)

13:   void main() {
0040D400   push        ebp
0040D401   mov         ebp,esp
0040D403   sub         esp,48h
0040D406   push        ebx
0040D407   push        esi
0040D408   push        edi
0040D409   lea         edi,[ebp-48h]
0040D40C   mov         ecx,12h
0040D411   mov         eax,0CCCCCCCCh
0040D416   rep stos    dword ptr [edi]
14:       S s1;
15:       s1.x = 9;
0040D418   mov         dword ptr [ebp-4],9
16:       s1.hello();
0040D41F   lea         ecx,[ebp-4]
0040D422   call        @ILT+10(S::hello) (0040100f)
17:
18:       S s2;
19:       s2.x = 10;
0040D427   mov         dword ptr [ebp-8],0Ah
20:       s2.hello();
0040D42E   lea         ecx,[ebp-8]
0040D431   call        @ILT+10(S::hello) (0040100f)
21:       return;
22:   }

 

8:        void hello() {
0040D4D0   push        ebp
0040D4D1   mov         ebp,esp
0040D4D3   sub         esp,44h
0040D4D6   push        ebx
0040D4D7   push        esi
0040D4D8   push        edi
0040D4D9   push        ecx
0040D4DA   lea         edi,[ebp-44h]
0040D4DD   mov         ecx,11h
0040D4E2   mov         eax,0CCCCCCCCh
0040D4E7   rep stos    dword ptr [edi]
0040D4E9   pop         ecx
0040D4EA   mov         dword ptr [ebp-4],ecx
9:            if (x == 10) printf("x is 10\n");
0040D4ED   mov         eax,dword ptr [ebp-4]
0040D4F0   cmp         dword ptr [eax],0Ah
0040D4F3   jne         S::hello+32h (0040d502)
0040D4F5   push        offset string "x is 10\n" (0042201c)
0040D4FA   call        printf (0040d740)
0040D4FF   add         esp,4
10:       }
0040D502   pop         edi
0040D503   pop         esi
0040D504   pop         ebx
0040D505   add         esp,44h
0040D508   cmp         ebp,esp
0040D50A   call        __chkesp (0040d490)
0040D50F   mov         esp,ebp
0040D511   pop         ebp
0040D512   ret

注意,和书里的汇编代码还不一样。

奇怪,为啥我的vc6出来的结果和书里不一样。。。

 

 

https://www.jaiminton.com/Tutorials/PracticalMalwareAnalysis/# 也说使用的vc6编译。

 

 

posted @ 2023-04-05 20:52  bonelee  阅读(133)  评论(0编辑  收藏  举报