环境:VC++
作用:
函数是完成特定任务的独立程序代码单元
1、创建和使用函数
- 函数原型:声明函数是什么类型,指明函数的返回值和函数接收的参数类型,函数和变量一样,有多种类型,任何程序在使用函数之前都要声明该函数的类型
- 函数调用:表明在此处执行函数,执行到函数调用的语句时,程序会找到该函数的定义并执行其中的内容,执行完返回调用函数继续执行下一行
- 函数定义:详细说明函数要干啥
#include "stdio.h"int add(int a,int b); //函数原型int main(void)
{int a=1,b=1,sum=0;sum=add(a,b); //函数调用printf("sum=%d\n",sum);return 0;
}//函数定义
int add(int a,int b)
{return a+b;
}
我们看看反汇编:
函数原型:
我们可以看出,函数原型这里没有生成机器码,这个是给编译器看得,告诉编译器这个函数的返回值和函数接收的参数类型,并在别处查看该函数类型,机器码是给CPU执行的,所以CPU执行到这里,不会干任何事情
函数调用:
8: sum=add(a,b); //函数调用
0040104D 8B 45 F8 mov eax,dword ptr [ebp-8]
00401050 50 push eax
00401051 8B 4D FC mov ecx,dword ptr [ebp-4]
00401054 51 push ecx
00401055 E8 AB FF FF FF call @ILT+0(add) (00401005)
0040105A 83 C4 08 add esp,8
0040105D 89 45 F4 mov dword ptr [ebp-0Ch],eax
函数调用之前,我们可以看到会先把参数存放到栈里面,也就是a,b的值,然后到00401005地址执行,这个地址有个jmp语句,会跳转到函数的定义出执行
函数定义:
13: //函数定义
14: int add(int a,int b)
15: {
004010A0 55 push ebp
004010A1 8B EC mov ebp,esp
004010A3 83 EC 40 sub esp,40h
004010A6 53 push ebx
004010A7 56 push esi
004010A8 57 push edi
004010A9 8D 7D C0 lea edi,[ebp-40h]
004010AC B9 10 00 00 00 mov ecx,10h
004010B1 B8 CC CC CC CC mov eax,0CCCCCCCCh
004010B6 F3 AB rep stos dword ptr [edi]
16: return a+b;
004010B8 8B 45 08 mov eax,dword ptr [ebp+8]
004010BB 03 45 0C add eax,dword ptr [ebp+0Ch]
17: }
004010BE 5F pop edi
004010BF 5E pop esi
004010C0 5B pop ebx
004010C1 8B E5 mov esp,ebp
004010C3 5D pop ebp
004010C4 C3 ret
从上面的程序我们可以看出,函数定义会先把esp存放到栈里面,然后把esp的值给ebp,接着开辟一个40h的栈,然后把ebx、 esi、 edi存放到栈里面,接着在一些连续地址存放0CCCCCCCCh,把这些做好后,再执行函数定义里的语句。
16: return a+b;
004010B8 8B 45 08 mov eax,dword ptr [ebp+8]
004010BB 03 45 0C add eax,dword ptr [ebp+0Ch]
我们看看函数的最后
004010BE 5F pop edi
004010BF 5E pop esi
004010C0 5B pop ebx
004010C1 8B E5 mov esp,ebp
004010C3 5D pop ebp
004010C4 C3 ret
函数的最后,把函数开始存放这些寄存器的内容又给了他们,ebp的值给esp,ebp恢复函数之前的ebp,接着返回。与函数调用的作用是一样的
函数的作用只完成特定任务,其他什么都没变,从函数调用到函数定义,最后返回,看起来是只对了a和b的值进行了操作,其他啥都没变
总结:
函数原型没有生成机器码,告诉编译器我的参数是那些和返回值是那些,函数调用会把参数先压入栈,接着执行call到一个地址执行,这个地址有一个jmp命令,会到函数定义出执行,函数定义会先把一些寄存器先压入栈,然后给一些内存赋值,在最后又会把这些寄存器给弹出,恢复成原值,执行ret命令,返回调用函数继续执行下一行。
2、传值和传址的区别
首先我们要认识几个小知识
- &运算符:取变量的存储地址
- *间接运算符:取存储在指针指向地址上的值,也可以用来声明指针
- 声明指针变量:类型 * 变量名,声明指针变量必须指定指针所指向变量的类型,因为不同变量类型占用不同的存储空间
#include "stdio.h"int add1(int a,int b); //函数原型
int add2(int *a,int *b); //函数原型
int main(void)
{int a=1,b=1,sum1,sum2;sum1=add1(a,b); //函数调用printf("sum1=%d\n",sum1);sum2=add2(&a,&b); //函数调用printf("sum2=%d\n",sum2);return 0;
}//函数定义
int add1(int a,int b)
{return a+b;
}int add2(int *a,int *b)
{return *a+*b;
}
传值:
8: sum1=add1(a,b); //函数调用
0040D786 8B 45 F8 mov eax,dword ptr [ebp-8]
0040D789 50 push eax
0040D78A 8B 4D FC mov ecx,dword ptr [ebp-4]
0040D78D 51 push ecx
0040D78E E8 7C 38 FF FF call @ILT+10(add) (0040100f)
0040D793 83 C4 08 add esp,8
0040D796 89 45 F4 mov dword ptr [ebp-0Ch],eax
值传给eax寄存器,然后入栈
传址:
10: sum2=add2(&a,&b); //函数调用
0040D7AA 8D 45 F8 lea eax,[ebp-8]
0040D7AD 50 push eax
0040D7AE 8D 4D FC lea ecx,[ebp-4]
0040D7B1 51 push ecx
0040D7B2 E8 5D 38 FF FF call @ILT+15(add2) (00401014)
0040D7B7 83 C4 08 add esp,8
0040D7BA 89 45 F0 mov dword ptr [ebp-10h],eax
把地址传给eax,然后入栈
我们知道传值不可以修改变量的值,而传址却可以,从汇编角度看,我们可以更加的清晰明白,传值只是将值传过去了,函数调用是去函数定义处执行,不知道变量在哪里,所以没办法修改,传地址到函数定义时就知道变量的地址在哪里了,所以能修改变量得内容