C++逆向分析——模版

模版

假设有一个冒泡排序的函数:

void Sort(int* arr, int nLength)
{
int i, k;
for (i = 0; i < nLength; i++)
{
for (k = 0; k < nLength-1-i; k++)
{
if(arr[k] > arr[k+1]) {
int temp = arr[k];
arr[k] = arr[k+1];
arr[k+1] = temp;
}
}
}
}

但是这个冒泡排序的函数只能对int类型,如果我们想要使用char类型的时候就要重新写一个函数,这就违背了C++的初衷,重复造轮子了:

images/download/attachments/12714553/image2021-4-17_11-17-32.png

那么如何避免重复造轮子呢?C++中使用模板来解决这个问题。

函数模板的语法是这样的:

template<class 形参名, class 形参名, ...>
返回类型 函数名(参数列表) {
函数体;
}

用模板的方式来修改一下这个冒泡排序函数:

template<class T>
void Sort(T* arr, int nLength)
{
int i, k;
for (i = 0; i < nLength; i++)
{
for (k = 0; k < nLength-1-i; k++)
{
if(arr[k] > arr[k+1])
{
T temp = arr[k];
arr[k] = arr[k+1];
arr[k+1] = temp;
}
}
}
}

在当前这个函数中我们只有一个地方需要替换修改,所以在写模板关键词时候尖括号内的class形参只有一个,而我们只需要将需要替换的地方改成形参的名字即可。

那么模版其原理是什么,编译器做了什么工作呢?我们可以看一下如下代码的反汇编代码:

images/download/attachments/12714553/image2021-4-17_11-34-34.png

使用不同类型的数组传入冒泡排序函数,观察一下执行地址:

images/download/attachments/12714553/image2021-4-17_11-33-30.png

可以看见,两个函数的地址完全不一样,这就说明模板的本质就是编译器会在看见不同的传入类型时创建不同的函数

模板除了可以在函数中使用也可以在结构体(类)中使用模板,其格式如下所示:

template<class 形参名, class 形参名, ...>
class 类名 {
...;
}

如下代码,一个结构体,有两个成员函数,一个是比较返回最大的数,一个则是最小的数:

struct Base {
int a;
int b;
 
char x;
char y;
 
int Max() {
if (a>b) {
return a;
} else {
return b;
}
}
 
char Min() {
if (x<y) {
return x;
} else {
return y;
}
}
};

但这个结构体已经指定了成员变量的数据宽度int、char,而我们想要比较任意类型的话,可以使用模板改造下这段代码:

 

这个模板想使用的话,我们就需要告诉编译器模板中的T、M分别对应什么,所以如果直接使用Base b;则无法编译。

使用如下格式即可:

Base<int, char> b;

images/download/attachments/12714553/image2021-4-17_15-11-54.png

 

在我的vs2022里实验下:

#include <stdio.h>


template<class T, class M>
struct Base {
	T a;
	T b;

	M x;
	M y;

	T Max() {
		if (a > b) {
			return a;
		}
		else {
			return b;
		}
	}

	M Min() {
		if (x < y) {
			return x;
		}
		else {
			return y;
		}
	}
};

void main() {
	Base<int, float> base;
	base.a = 3;
	base.b = 4;
	base.x = 1.2f;
	base.y = 3.4f;

	int max = base.Max();
	float min = base.Min();
	printf("%d %f\n", max, min);
	return;
}

 看下反汇编代码:

	Base<int, float> base;
	base.a = 3;
004C197F C7 45 E8 03 00 00 00 mov         dword ptr [base],3  
	base.b = 4;
004C1986 C7 45 EC 04 00 00 00 mov         dword ptr [ebp-14h],4  
	base.x = 1.2f;
004C198D F3 0F 10 05 38 7B 4C 00 movss       xmm0,dword ptr [__real@3f99999a (04C7B38h)]  
004C1995 F3 0F 11 45 F0       movss       dword ptr [ebp-10h],xmm0  
	base.y = 3.4f;
004C199A F3 0F 10 05 3C 7B 4C 00 movss       xmm0,dword ptr [__real@4059999a (04C7B3Ch)]  
004C19A2 F3 0F 11 45 F4       movss       dword ptr [ebp-0Ch],xmm0  

	int max = base.Max();
004C19A7 8D 4D E8             lea         ecx,[base]  
004C19AA E8 CE F6 FF FF       call        Base<int,float>::Max (04C107Dh)  
004C19AF 89 45 DC             mov         dword ptr [max],eax  
	float min = base.Min();
004C19B2 8D 4D E8             lea         ecx,[base]  
004C19B5 E8 AF F6 FF FF       call        Base<int,float>::Min (04C1069h)  
004C19BA D9 5D D0             fstp        dword ptr [min]  
	printf("%d %f\n", max, min);
004C19BD F3 0F 5A 45 D0       cvtss2sd    xmm0,dword ptr [min]  
004C19C2 83 EC 08             sub         esp,8  
004C19C5 F2 0F 11 04 24       movsd       mmword ptr [esp],xmm0  
004C19CA 8B 45 DC             mov         eax,dword ptr [max]  
004C19CD 50                   push        eax  
004C19CE 68 30 7B 4C 00       push        offset string "%d %f\n" (04C7B30h)  
004C19D3 E8 FF F6 FF FF       call        _printf (04C10D7h)  

 

分析下:

 

看下1.2的16进制表示:

 

 

可以看到就是1.2和3.4,只是编译器,将这两个数字放到了常量区。

 

因此,本质上,模板就是给不同的类和函数在编译期间根据类型取了一个新的名字

 

 

 

 

 

posted @ 2023-04-09 10:51  bonelee  阅读(77)  评论(0编辑  收藏  举报