首页
论坛
课程
招聘
[原创]从反汇编的角度学C/C++之函数
2021-9-29 21:29 6094

[原创]从反汇编的角度学C/C++之函数

2021-9-29 21:29
6094

一.总体流程

    函数作为编程中重要的组成部分,我们需要频繁使用函数来完成编码。而在程序运行过程中,为了保存我们的在函数中使用的临时数据,计算机使用了后进先出被称为栈的数据结构来保存这些临时数据。为了维持栈的平衡以及使用这些临时数据,我们需要用到两个非常重要的寄存器,分别是esp和ebp。它们分别指向了栈的顶部和栈的底部,由于栈的增长是从高地址到低地址增长的,所以栈顶的地址小于栈底的地址,也就是说esp<=ebp。而对于入栈和出栈的操作分别是push和pop,指向push就会把数据压入栈中,pop就会把数据从栈中取出来。

    那么它在内存中的表现形式究竟如何?下面先通过一个简单的函数实例来看看在内存中函数的运行机制是如何的。

#include <cstdio>

int add(int x, int y)
{
	int z = 0;

	z = x + y;

	return z;
}

int main()
{
	int x = 3, y = 4, z = 0;


	z = add(x, y);

	return 0;
}

    这段代码非常简单,函数add只是实现了一个加法操作,最后将返回值给了z。我们从调用函数,也就是z=add(x,y)开始看看函数是如何使用栈帧。

	int x = 3, y = 4, z = 0;
00C01088  mov         dword ptr [x],3      
00C0108F  mov         dword ptr [y],4  
00C01096  mov         dword ptr [z],0          //为x, y, z分别赋值


	z = add(x, y);
00C0109D  mov         eax,dword ptr [y]      //取出y的值赋给eax
00C010A0  push        eax                    //将eax压入栈中
00C010A1  mov         ecx,dword ptr [x]      //取出x的值赋给ecx
00C010A4  push        ecx                    //将ecx的值压入栈中
00C010A5  call        add (0C01000h)         //调用函数
00C010AA  add         esp,8                  //esp + 8
00C010AD  mov         dword ptr [z],eax      //此时eax保存着返回值,将返回值赋给z

    在函数开始调用之前也就是运行到0x00C0109D之前,我们的esp等于0x006FF788,ebp等于0x006FF878。

    调用函数的时候,首先会从右往左依次将所需要用到的参数push到栈里面,当程序运行到0x00C010A5的时候,也就是调用函数之前,我的电脑中esp等于0x006FF780,ebp等于0x006FF878此时栈中的内容如下图所示。

                                            

    可以看出此时参数已经被压入栈中,进一步跟进函数,我们就来到了函数的首地址,0x00C01000,而在运行第一条指令之前,此时我们的esp再次改变,变成0x006FF77C,地址减少了4,我们再次查看栈中数据,如下图

                                            

    可以看出此时栈中多了一个数据,值为0x00C010AA,而这个数据其实就是我们调用函数的地址的下一条指令的地址,也就是上面call add的后面一条指令add esp, 8的地址,由此可以得出结论,函数调用的时候先是从右往左压入需要用到的参数,然后调用call指令的时候,先是把下一条指令的地址压入栈中,在跳转到需要运行的函数地址。此时我们函数的反汇编结果如下:

int add(int x, int y)
{
00C01000  push        ebp                  //压入ebp
00C01001  mov         ebp,esp              //把esp的值赋给ebp
00C01003  sub         esp,0CCh             //将esp减少0x0CC,这就扩大了栈,此时的esp和ebp都指向了新得地方,而这块地方我们就称为这个函数的栈空间
00C01009  push        ebx  
00C0100A  push        esi  
00C0100B  push        edi                  //保存寄存器得值
00C0100C  lea         edi,[ebp-0CCh]      //将ebp-0x0CC的地址赋给edi,此时edi的值就是栈顶地址
00C01012  mov         ecx,33h  
00C01017  mov         eax,0CCCCCCCCh  
00C0101C  rep stos    dword ptr es:[edi]  //这三次汇编就是在将扩展开了这段栈空间初始化为0xCC
00C0101E  mov         ecx,offset _D3472036_test@cpp (0C92004h)  
00C01023  call        __CheckForDebuggerJustMyCode (0C010D0h)  //这两句汇编作用是vs2017生成得,不需要理会
	int z = 0;
00C01028  mov         dword ptr [ebp-8],0         //将0赋值给ebp-8地址中, 此时得ebp指向我们函数栈得底部,而这个地址也就作为z这个变量存储数据得内存地址 

	z = x + y;
00C0102F  mov         eax,dword ptr [ebp+8]      //由于压入了返回地址和esp,所以此时ebp+8得位置才能拿到我们得第一个参数也就是x得值
00C01032  add         eax,dword ptr [ebp+0Ch]    //ebp+0xC就是我们第二个参数得值两者相加
00C01035  mov         dword ptr [ebp-8],eax      //在赋值到ebp-8得地址也就是赋值给z

	return z;
00C01038  mov         eax,dword ptr [ebp-8]      //将z得值赋值给eax,作为返回值返回给调用函数使用
00C0103B  pop         edi                          
00C0103C  pop         esi  
00C0103D  pop         ebx                        //弹出寄存器得值,这三个pop和上面得3个push配合就可以保证在调用函数前后这三个寄存器得值不改变
00C0103E  add         esp,0CCh                   //增加esp得值,这和上面sub得值一样都是0xCC
00C01044  cmp         ebp,esp  
00C01046  call        _RTC_CheckEsp (0C012A0h)   //这两句是编译器检查栈帧是否平衡,可先不看
00C0104B  mov         esp,ebp                    //将ebp得值赋给esp
00C0104D  pop         ebp                        //弹出ebp得值
00C0104E  ret                                    //函数返回到被调函数
}

    可以看到,当进入一个函数得时候,首先是指向push ebp操作将原来得ebp压入栈中,然后在把esp赋值给ebp,也就是把ebp移上来。当程序运行到sub esp,0xCC得时候,此时得esp==ebp==0x006FF778。此时在从内存中查看这个地址中得内容如下:

                                            

    也就是说这个时候ebp地址中保存得数据是上一次得ebp得地址,而ebp+4地址中保存得就是返回地址,ebp+8保存的就是第一个参数,依次往后类推。

    随后在减少esp的值,此时esp到ebp之间这段内存就是这个函数的栈空间,随后程序保存了三个寄存器的值为了之后恢复并且对这段栈空间初始化为0xCC,当程序运行到0x00C0101E的时候,此时esp==0x006FF6A0,ebp==0x006FF778。此时在查看栈中内容如下:

                                    

    此时这些0xcc就是我们这个函数的栈空间,用来存储我们的局部变量。

    随后我们对ebp-8这个地址赋值为0,也就是局部变量z保存在这个地址,由于此时ebp==0x006FF778,所以为了拿到传入的参数,我们需要从ebp+8和ebp+0xC这两个地址中拿。当程序运行到0x00C01038的时候,查看ebp-8的内容,也就是z的值,可以看到此时等于7,在把这个值赋值给eax作为返回地址。

                          

    随后执行了3个pop操作把栈顶部保存的三个寄存器的值恢复回去,在对esp增加0x0CC大小的值,在把ebp的值赋给esp,此时esp与ebp的值再次相等,等于0x006FF778,也就是开辟栈空间之前的值。此时的栈空间再次如图

                       

    ebp中保存的就是上一个ebp的地址,为了成功恢复之前函数的栈空间,我们首先需要恢复ebp,所以我们执行pop ebp,把保存的ebp的值恢复到ebp中。执行完pop ebp之后,就恢复到了esp等于0x006FF77C,ebp就等于0x006FF878的状态,也就是说此时栈中的数据再一次变成这样

                          

    此时esp地址中保存的数据就是我们前面调用函数的地址的下一条指令的地址,随后执行ret,就会把0x00C010AA放入eip中,程序就会转到0x00C010AA执行,如下图

    此时esp的值等于0x006FF780,栈中数据重新恢复为

                      

    这是保存的两个值就是传入的两个参数,而此时这两个参数已经不需要使用了,所以随后就执行add esp,8。此时esp与ebp的值真正恢复到了函数调用前的状态。而此时eax保存着函数的返回值,根据程序的代码,我们需要把eax赋值给z。

    至此,整个函数从调用到返回的分析结束,整个调用过程可用下面的图来表示。

    随后我们将执行push ebp和mov ebp, esp操作来进行移动

    接下来我们sub esp, 0xCC来开辟新函数的栈空间并保存了三个寄存器的值,随后在这个栈空间中进行操作。

    执行完函数功能后,程序开始恢复栈,先pop掉三个寄存器,在执行完add esp,0xCC,mov esp, ebp操作后栈空间重新恢复如图:


    接下来就是函数开始调用的逆向过程,让栈恢复到最开始的状态,过程如下:

    以上就是全部内容,当然这里还需要说明的是由于对栈的操作push或者pop一次都是四个字节,所以我们传递参数的时候,即使你传递的参数的类型是char或者short这种只占一个或者两个字节的参数,它也是按照4个字节进行。

二.浮点数和函数

    浮点数在计算机中的运算法则和普通的数据类型不太一样,这个在之前的文章中已经说明了,那么在函数调用过程中是否有异同呢?下面这段代码实现的功能和上面的一样,只不过这次是对浮点数进行操作。

#include <cstdio>

float add(float x, float y)
{
	float z = 0;

	z = x + y;
	
	return z;
}

int main()
{
	float x = 3, y = 4, z = 0;

	z = add(x, y);

	return 0;
}

    首先看main函数中的反汇编代码

14:       float x = 3, y = 4, z = 0;
00401078   mov         dword ptr [ebp-4],40400000h
0040107F   mov         dword ptr [ebp-8],40800000h
00401086   mov         dword ptr [ebp-0Ch],0            //对三个浮点数进行初始化
15:       z = add(x, y);
0040108D   mov         eax,dword ptr [ebp-8]
00401090   push        eax
00401091   mov         ecx,dword ptr [ebp-4]
00401094   push        ecx                              //入栈操作没区别
00401095   call        @ILT+0(add) (00401005)
0040109A   add         esp,8
0040109D   fst         dword ptr [ebp-0Ch]             //从ST(0)中取出返回值放到z

    由此可以看出入栈操作并没区别,只是返回值是从ST(0)中取出,说明函数返回值应当是被放到ST(0)中。继续看add函数的反汇编

3:    float add(float x, float y)
4:    {
00401020   push        ebp
00401021   mov         ebp,esp
00401023   sub         esp,44h
00401026   push        ebx
00401027   push        esi
00401028   push        edi
00401029   lea         edi,[ebp-44h]
0040102C   mov         ecx,11h
00401031   mov         eax,0CCCCCCCCh
00401036   rep stos    dword ptr [edi]        //上述操作无区别
5:        float z = 0;
00401038   mov         dword ptr [ebp-4],0    //将z赋值为0
6:
7:        z = x + y;
0040103F   fld         dword ptr [ebp+8]     //将第一个参数放入ST(0)中
00401042   fadd        dword ptr [ebp+0Ch]    //将第一个参数与第二个参数相加在把结果放入ST(0)中
00401045   fst         dword ptr [ebp-4]     //将ST(0)的数据放到z中
8:
9:        return z;
10:   }
00401048   pop         edi             //此时ST(0)中数据就是返回值,所以并没继续操作
00401049   pop         esi
0040104A   pop         ebx
0040104B   mov         esp,ebp
0040104D   pop         ebp
0040104E   ret

三.数组,指针和函数

    如果理解了上面的内容,那应该对局部变量,局部变量作用域等等有了不一样的认识。以及我们知道在函数中传递参数的时候,如果在函数内部对这些参数进行更改是不会影响到原来的数值的,比如上面的x,y即使在add函数里面进行修改,在main函数中也不会发生改变,这是因为函数传参过程中对其进行了备份。那么对于数组和指针是不是也是这样的呢,以及将他们作为返回值有什么不同呢?请看下面的实例。

#include <cstdio>

char* test(int arr[], int *pInt)
{
	char arrRet[] = { "Hello 1900" };

	for (int i = 0; i < 10; i++)
	{
		arr[i] = 0;
		*(pInt + i) = 0;
	}

	return arrRet;
}

int main()
{
	int arrInt[10] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
	int *pInt = arrInt;
	char *pChar = NULL;

	pChar = test(arrInt, pInt);

	return 0;
}

    代码内容比较简单,主要是参数和返回值都是数值或指针。首先我们依然先对main函数进行反汇编

	int arrInt[10] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
00B01108  mov         dword ptr [arrInt],0  
00B0110F  mov         dword ptr [ebp-28h],1  
00B01116  mov         dword ptr [ebp-24h],2  
00B0111D  mov         dword ptr [ebp-20h],3  
00B01124  mov         dword ptr [ebp-1Ch],4  
00B0112B  mov         dword ptr [ebp-18h],5  
00B01132  mov         dword ptr [ebp-14h],6  
00B01139  mov         dword ptr [ebp-10h],7  
00B01140  mov         dword ptr [ebp-0Ch],8  
00B01147  mov         dword ptr [ebp-8],9     //对数组进行初始化
	int *pInt = arrInt;
00B0114E  lea         eax,[arrInt]          
00B01151  mov         dword ptr [pInt],eax    //将数组的首地址赋给pInt
	char *pChar = NULL;
00B01154  mov         dword ptr [pChar],0     //初始化pChar

	pChar = test(arrInt, pInt);
00B0115B  mov         eax,dword ptr [pInt]  
00B0115E  push        eax                     //取出pInt保存的地址并压栈
00B0115F  lea         ecx,[arrInt]  
00B01162  push        ecx                     //取出数组首地址并压栈
00B01163  call        test (0B01000h)  
00B01168  add         esp,8  
00B0116B  mov         dword ptr [pChar],eax    //将eax的值赋给pChar

    从上面可以看出,将数组作为参数传递的时候所入栈的元素其实是数组的首地址,而指针则是本身的数值,根据对pChar的赋值可以想到,函数最后的返回值是放到eax中。继续看test反汇编的结果

char* test(int arr[], int *pInt)
{
00B01000  push        ebp  
00B01001  mov         ebp,esp  
00B01003  sub         esp,0E0h  
00B01009  push        ebx  
00B0100A  push        esi  
00B0100B  push        edi  
00B0100C  lea         edi,[ebp-0E0h]  
00B01012  mov         ecx,38h  
00B01017  mov         eax,0CCCCCCCCh  
00B0101C  rep stos    dword ptr es:[edi]  
00B0101E  mov         ecx,offset _D3472036_test@cpp (0B92004h)  
00B01023  call        __CheckForDebuggerJustMyCode (0B01390h)  
	char arrRet[] = { "Hello 1900" };
00B01028  mov         eax,dword ptr [string "Hello 1900" (0B731B0h)]  
00B0102D  mov         dword ptr [arrRet],eax  
00B01030  mov         ecx,dword ptr ds:[0B731B4h]  
00B01036  mov         dword ptr [ebp-0Ch],ecx  
00B01039  mov         dx,word ptr ds:[0B731B8h]  
00B01040  mov         word ptr [ebp-8],dx  
00B01044  mov         al,byte ptr ds:[00B731BAh]  
00B01049  mov         byte ptr [ebp-6],al              //对arrRet数组进行赋值
	for (int i = 0; i < 10; i++)
00B0104C  mov         dword ptr [ebp-1Ch],0  
00B01053  jmp         00B0105E  
00B01055  mov         eax,dword ptr [ebp-1Ch]  
00B01058  add         eax,1  
00B0105B  mov         dword ptr [ebp-1Ch],eax  
00B0105E  cmp         dword ptr [ebp-1Ch],0Ah  
00B01062  jge         00B01080  
	{
		arr[i] = 0;
00B01064  mov         eax,dword ptr [ebp-1Ch]    //取出i的值
00B01067  mov         ecx,dword ptr [ebp+8]      //取出数组首地址
00B0106A  mov         dword ptr [ecx+eax*4],0    //计算地址
		*(pInt + i) = 0;
00B01071  mov         eax,dword ptr [ebp-1Ch]    //取出i的值
00B01074  mov         ecx,dword ptr [ebp+0Ch]    //取出保存的指针的值
00B01077  mov         dword ptr [ecx+eax*4],0    //计算偏移
	}
00B0107E  jmp         00B01055
	return arrRet;
00B01080  lea         eax,[arrRet]              //将arrRet的地址赋给eax作为返回值
}
00B01083  push        edx  
00B01084  mov         ecx,ebp  
00B01086  push        eax  
00B01087  lea         edx,ds:[00B010A8h]  
00B0108D  call        00B012E0  
00B01092  pop         eax  
00B01093  pop         edx  
00B01094  pop         edi  
00B01095  pop         esi  
00B01096  pop         ebx  
00B01097  add         esp,0E0h  
00B0109D  cmp         ebp,esp  
00B0109F  call        00B01350  
00B010A4  mov         esp,ebp  
00B010A6  pop         ebp  
00B010A7  ret

       由上可以看出,数组作为参数传递的时候传递的是原数组的首地址,所以在函数内对数组进行更改会影响到原数组。以及,指针作为返回值的时候,最终返回的是一个地址,而这个地址是新函数内开辟的地址空间所用的数组变量。如果函数退出栈关闭,我们在函数外使用这个指针的话就很有可能导致程序错误。

四.结构体和函数

    上面的实例参数和返回值都是4个字节,可是我们知道自定义的结构体是可以超过4个字节,那如果超过四个字节的时候,传参和返回值有什么不同呢?请看如下代码:

#include <cstdio>
#include <cstring>

#pragma pack(1)
typedef struct _Test
{
	char cTest;
	int iTest;
	char arrTest[11];
} Test;

Test structTest(Test test)
{
	Test test_ret = { 0 };

	test_ret.cTest = test.cTest;
	test_ret.iTest = test.iTest;
	strcpy(test_ret.arrTest, test.arrTest);

	return test_ret;
}

int main()
{
	Test test1 = { 0 }, test2 = { 0 };

	test1.cTest = 'a';
	test1.iTest = 1900;
	strcpy(test1.arrTest, "Hello 1900");
	test2 = structTest(test1);

	return 0;
}

    首先依然先对main函数进行反汇编

	Test test1 = { 0 }, test2 = { 0 };
003310F8  xor         eax,eax  
003310FA  mov         dword ptr [test1],eax  
003310FD  mov         dword ptr [ebp-10h],eax  
00331100  mov         dword ptr [ebp-0Ch],eax  
00331103  mov         dword ptr [ebp-8],eax  
00331106  xor         eax,eax  
00331108  mov         dword ptr [test2],eax  
0033110B  mov         dword ptr [ebp-28h],eax  
0033110E  mov         dword ptr [ebp-24h],eax  
00331111  mov         dword ptr [ebp-20h],eax      //对两个结构体进行初始化

	test1.cTest = 'a';
00331114  mov         byte ptr [test1],61h  
	test1.iTest = 1900;
00331118  mov         dword ptr [ebp-13h],76Ch  
	strcpy(test1.arrTest, "Hello 1900");
0033111F  push        offset string "Hello 1900" (03A31B0h)  
00331124  lea         eax,[ebp-0Fh]  
00331127  push        eax  
00331128  call        strcpy (03439E0h)        //对test1进行赋值
0033112D  add         esp,8  
	test2 = structTest(test1);
00331130  sub         esp,10h                  //提高栈空间,这个提高的空间为0x10,刚好容纳我们的结构体
00331133  mov         eax,esp                  //将提升到的栈地址赋值给eax
00331135  mov         ecx,dword ptr [test1]        
00331138  mov         dword ptr [eax],ecx         
0033113A  mov         edx,dword ptr [ebp-10h]      
0033113D  mov         dword ptr [eax+4],edx        
00331140  mov         ecx,dword ptr [ebp-0Ch]  
00331143  mov         dword ptr [eax+8],ecx  
00331146  mov         edx,dword ptr [ebp-8]  
00331149  mov         dword ptr [eax+0Ch],edx  //这里对新提升的这片栈空间赋值给test1中的内容
0033114C  lea         eax,[ebp-11Ch]           //这里将ebp-0x11C的地址压入栈中
00331152  push        eax  
00331153  call        structTest (0331000h)  
00331158  add         esp,14h                  //平横掉压入栈中的4字节和sub掉的0x10
0033115B  mov         ecx,dword ptr [eax]      //可以看到eax依然是返回值,只不过此时eax存的是地址
0033115D  mov         dword ptr [ebp-104h],ecx   
00331163  mov         edx,dword ptr [eax+4]  
00331166  mov         dword ptr [ebp-100h],edx  
0033116C  mov         ecx,dword ptr [eax+8]  
0033116F  mov         dword ptr [ebp-0FCh],ecx    
00331175  mov         edx,dword ptr [eax+0Ch]  
00331178  mov         dword ptr [ebp-0F8h],edx  //这段代码是从eax中的地址值开始取出内容,而这些内容就是结构体的值,赋值到ebp-0x104
0033117E  mov         eax,dword ptr [ebp-104h]  
00331184  mov         dword ptr [test2],eax  
00331187  mov         ecx,dword ptr [ebp-100h]  
0033118D  mov         dword ptr [ebp-28h],ecx  
00331190  mov         edx,dword ptr [ebp-0FCh]  
00331196  mov         dword ptr [ebp-24h],edx  
00331199  mov         eax,dword ptr [ebp-0F8h]  
0033119F  mov         dword ptr [ebp-20h],eax   //这段代码就是从ebp-0x104从开始取出结构体内容放到test2中

    由上可以看出,结构体作为参数传递的时候,程序会开辟出一段空间来存储这个变量并且会压入一个地址作为参数,那么此时进入函数后,ebp+8中的内容就是这个地址,而从ebp+0xC开始则是我们新开辟的这段空间,里面保存了我们的参数内容。那么这个被压入的地址有什么作用呢?

    接下来看看函数的反汇编

Test structTest(Test test)
{
00331000  push        ebp  
00331001  mov         ebp,esp  
00331003  sub         esp,0D8h  
00331009  push        ebx  
0033100A  push        esi  
0033100B  push        edi  
0033100C  lea         edi,[ebp+FFFFFF28h]  
00331012  mov         ecx,36h  
00331017  mov         eax,0CCCCCCCCh  
0033101C  rep stos    dword ptr es:[edi]  
0033101E  mov         ecx,3C2008h  
00331023  call        003313D0  
	Test test_ret = { 0 };
00331028  xor         eax,eax  
0033102A  mov         dword ptr [ebp-14h],eax  
0033102D  mov         dword ptr [ebp-10h],eax  
00331030  mov         dword ptr [ebp-0Ch],eax  
00331033  mov         dword ptr [ebp-8],eax      //ebp-0x14就是test_ret的首地址

	test_ret.cTest = test.cTest;
00331036  mov         al,byte ptr [ebp+0Ch]      //由于我们传参时候压入了一个地址所以ebp+0xC开始才是我们test1的参数地址
00331039  mov         byte ptr [ebp-14h],al  
	test_ret.iTest = test.iTest;
0033103C  mov         eax,dword ptr [ebp+0Dh]  
0033103F  mov         dword ptr [ebp-13h],eax  
	strcpy(test_ret.arrTest, test.arrTest);
00331042  lea         eax,[ebp+11h]  
00331045  push        eax  
00331046  lea         ecx,[ebp-0Fh]  
00331049  push        ecx  
0033104A  call        003439E0              
0033104F  add         esp,8                      //这段代码就是将test1中的内容赋值给test

	return test_ret;
00331052  mov         eax,dword ptr [ebp+8]      //取出第一个参数就是我们压入的一个地址
00331055  mov         ecx,dword ptr [ebp-14h]  
00331058  mov         dword ptr [eax],ecx  
0033105A  mov         edx,dword ptr [ebp-10h]  
0033105D  mov         dword ptr [eax+4],edx  
00331060  mov         ecx,dword ptr [ebp-0Ch]  
00331063  mov         dword ptr [eax+8],ecx  
00331066  mov         edx,dword ptr [ebp-8]  
00331069  mov         dword ptr [eax+0Ch],edx   //这段代码就是在把test_ret的内容赋值到第一个参数也就是我们压入的地址
0033106C  mov         eax,dword ptr [ebp+8]     //将这个压入的地址作为返回值给eax
}
0033106F  push        edx  
00331070  mov         ecx,ebp  
00331072  push        eax  
00331073  lea         edx,ds:[00331094h]  
00331079  call        00331320  
0033107E  pop         eax  
0033107F  pop         edx  
}
00331080  pop         edi  
00331081  pop         esi  
00331082  pop         ebx  
00331083  add         esp,0D8h  
00331089  cmp         ebp,esp  
0033108B  call        00331390  
00331090  mov         esp,ebp  
00331092  pop         ebp  
00331093  ret

    由上可以看出这个新压入的地址被用来保存程序需要返回的结构体的内容,而后程序将这个地址作为返回值放回给主函数。最终这个主函数根据这个地址将结构体的内容备份到一个新的内存地址中,在从这个备份的内存地址中将内容赋值到我们需要的变量中。


看雪招聘平台创建简历并且简历完整度达到90%及以上可获得500看雪币~

最后于 2021-10-20 11:20 被1900编辑 ,原因:
收藏
点赞2
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回