




已阅读5页,还剩6页未读, 继续免费阅读
版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
笨鸟先飞学编程系列之三 函数提及“函数”这个词儿,很多人都像我一样,感觉很恐慌,因为它总让我想起代数里讲的方方面面。这些对于像我这样的笨鸟来说,真的太深奥,总是不敢去考虑它,去琢磨它。虽然这里讲的跟那个并非同一个东西,但不免总是心有余悸。今天要讲的东西比较多,我尽量把它讲的详细明白,但由于本人笨鸟一个,能力实在有限,大家多多包涵。先列一下今天要讲的目录:1. 什么是函数。2. 函数的定义和使用方法。3. 从调试中看函数的调用机制。4. 撩开函数的面纱。5. 结尾语。好,以上是今天要讲的目录,下面进入正题:一、 什么是函数。函数,就是完成某个或者某种固定功能的最小的模块(总感觉这样写不是很严谨)。当然,如果我就这样解释,相比大家很定会说我应付,说我不负责任,所以,这里我多牢骚几句。在C语言中,默认指定的函数入口点是main函数,所以,我们在很多时候,只在这个函数中写代码,但是当我们的程序大到一定的程度,这个函数未免显的台过臃肿了;而且从方便实用的角度来说,把所有的功能都写在main函数中,看起来很不直观;而且很多的功能我们可能在别的程序里还能用到,如果我们都在一个函数里,那移植起来肯定也很麻烦;从维护方面来讲,这样很不容易维护,牵一处则动全身。比如下面的代码:int main()/初始化变量;initcode001;initcode002;initcode003;/开始实现功能一的代码gn001:code001;code002;code003;/显示结果printf(功能一的执行结果.n请选择下一个功能:);scanf(%d,&bl001);switch (bl001)case gn001: goto gn001; break;case gn002:goto gn002; break;case gn003:goto gn003; break;case gn004:goto gn004; break;/ 开始实现功能二的代码gn002:/显示结果printf(功能二的执行结果.n请选择下一个功能:);scanf(%d,&bl001);switch (bl001)case gn001: goto gn001; break;case gn002:goto gn002; break;case gn003:goto gn003; break;case gn004:goto gn004; break;从上面的代码可以看出,很多的重复代码,而且,如果我要在别的程序里使用功能二的代码,需要认真的将代码提取出来,难免发生错误。而且如果这个程序有70多个功能那这个程序就麻烦了。因此,在写程序的时候,需要根据功能来讲整个程序划分成一个个模块,哪个模块有问题,我们就只要对有问题的模块修改,整理就可以了。在另外的程序中,需要用到哪个模块就将相应的模块移植到指定的程序里,就可以了,而函数,就是模块中最小的单位。以后,根据我们系列的深入,我们会继续讲到DLL,LIB等。彻底的将我们的项目工程模块化。如下面的代码:#include stdio.h/ 这里声明一下函数MaxNum,让编译器知道有一个名字叫MaxNum的函数,它有三个整型参数。int MaxNum(int num001, int num002, int num003);/void main()int num1 = 0, num2 = 0, num3 = 0;int result = 0;scanf(%d,%d,%d, &num1, &num2, &num3);/ 让用户输入任意三个数result = MaxNum(num1, num2, num3);/调用MaxNum 函数printf(%dn, result);/显示MaxNum函数的返回值/下面是函数定义部分/*/ 函数名: MaxNum/ 参 数:/ num001:随便一个整型数据/ num002:随便一个整型数据/ num003:随便一个整型数据/ 功 能:/取出三个参数中最大的一个数并返回。/*/int MaxNum(int num001, int num002, int num003)if (num001 = num002)if (num001 = num003)return num001; elsereturn num003;elseif (num002 = num003)return num002; elsereturn num003;这样下来,我们的程序就比较规范了,也方便任务的分工,写这个函数的人只管这个函数功能的实现,调用这个函数的人只要知道这个函数的功能和怎么使用就可以了,不用管这个函数功能是怎么实现的,OK既然知道函数是什么及为什么要用函数了,那下面我们进入下一节二、 函数的定义和使用方法。通过上一小节的节的代码,我相信很多的朋友已经知道函数是怎么声明并使用的了,这里我再具体的说一下:定义一个函数的格式是:返回值类型 函数调用方式 函数名(参数1, 参数2)函数指令;return 返回值;具体的使用例子,大家就看上一小节中的函数例子就可以,我偷个懒,嘿嘿相信,很多的朋友会问我一些问题:1. 上面的代码中,那个MaxNum函数好像是定义了两次哎,先是声明,再是定义,声明跟定义有什么区别呀。2. 在上面代码中函数的定义好像没有本节函数定义格式中的调用方式好,第一个问题呢,我们可以先把声明的那一条语句删除掉,然后编译一下程序,看看,提示什么呢?Compiling.Func.cppE:项目工程测试例子Func.cpp(11) : error C2065: MaxNum : undeclared identifierE:项目工程测试例子Func.cpp(26) : error C2373: MaxNum : redefinition; different type modifiersError executing cl.exe.Func.exe - 2 error(s), 0 warning(s)好,那我们再把这个MaxNum函数的定义部分移到main函数的前面,再编译,哈哈没有问题了。这说明了什么呢?我们程序再执行的时候,先进入main函数,如果我们自定义的函数再main函数前面,那编译器就会知道,MaxNum是我们自己定义的函数,如果我们定义的函数MaxNum在main函数的后面,编译器再编译我们再main函数调用的代码时由于它不知道我们定义了MaxNum,所以调用MaxNum的代码就不能被识别了。因此,我们应该在调用我们定义的函数前,先声明一下,让编译器知道我们定义了这么个函数,就可以了,当然,如果程序很想,我们完全可以把我们定义的函数放在程序文件的前面,main函数放在最后,免去声明的麻烦,但是定义函数前,先声明函数是个好习惯,因为以后我们写的程序可能会几个程序文件一起编译关于第二个问题,我们看下一节吧三、 从调试中看函数的调用机制。我们直接使用上面的程序做例子,Release编译时,设置生成MAP文件,编译好程序以后,OD打开它,载入MAP文件,当然,如果不会捣鼓的,可以参考MAP文件中的信息:AddressPublics by ValueRva+BaseLib:Object0001:00000000_main00401000 fFunc.obj0001:00000050?MaxNumYAHHHHZ00401050 fFunc.obj0001:00000070_printf00401070 fLIBC:printf.obj0001:000000a1_scanf004010a1 fLIBC:scanf.obj来到我们的main函数中:00401000 /$ 83EC 0C sub esp, 0C; 申请一块堆栈,给局部变量预留空间00401003 |. 33C0 xor eax, eax00401005 |. 8D4C24 04 lea ecx, dword ptr esp+400401009 |. 894424 08 mov dword ptr esp+8, eax0040100D |. 894424 04 mov dword ptr esp+4, eax00401011 |. 894424 00 mov dword ptr esp, eax00401015 |. 8D4424 00 lea eax, dword ptr esp00401019 |. 50 push eax ; Arg4 = 80040101A |. 8D5424 0C lea edx, dword ptr esp+C0040101E |. 51 push ecx ; Arg3 = 50040101F |. 52 push edx ; Arg2 = 300401020 |. 68 34804000 push 00408034 ; Arg1 = ASCII %d,%d,%d00401025 |. E8 77000000 call _scanf0040102A |. 8B4424 10 mov eax, dword ptr esp+100040102E |. 8B4C24 14 mov ecx, dword ptr esp+1400401032 |. 8B5424 18 mov edx, dword ptr esp+1800401036 |. 50 push eax ; Arg3 = 800401037 |. 51 push ecx ; Arg2 = 500401038 |. 52 push edx ; Arg1 = 300401039 |. E8 12000000 call ?MaxNumYAHHHHZ00401050 /$ 8B4C24 04 mov ecx, dword ptr esp+400401054 |. 8B4424 08 mov eax, dword ptr esp+800401058 |. 3BC8 cmp ecx, eax0040105A |. 7C 09 jl short Fu.004010650040105C |. 8B4424 0C mov eax, dword ptr esp+C00401060 |. 3BC8 cmp ecx, eax00401062 |. 7D 09 jge short Fu.0040106D00401064 |. C3 retn00401065 | 8B4C24 0C mov ecx, dword ptr esp+C00401069 |. 3BC1 cmp eax, ecx0040106B |. 7D 02 jge short Fu.0040106F0040106D | 8BC1 mov eax, ecx0040106F C3 retn0040103E |. 50 push eax0040103F |. 68 30804000 push 00408030 ; ASCII %d,LF00401044 |. E8 27000000 call _printf00401049 |. 83C4 30 add esp, 30 ; 回复堆栈平衡0040104C . C3 retn这里面应该没有我们不认识的汇编指令吧。我们再这里就看一下这些代码,当然如果可以的话,你可以单步跟踪这个程序,尤其注意看下堆栈的变化。1. _cdecl 调用方式好的,我们现在来看一下这段代码,先看一下堆栈吧,在函数头,申请了一段大小为0xC的堆栈空间,在函数结尾平衡堆栈的时候,恢复了0x30的大小,也就是说,中间的这些PUSH的函数参数,占用了0x24的堆栈空间,(我们可以算一下,第一个函数scanf有4个参数PUSH了4次,第二个函数MaxNum有3个参数,PUSH了3次,第三个函数是printf,有两个参数,push了两次,一共PUSH了9次,DWORD(4)*9 = 0x24,再加上一开始在函数头申请的0xC大小的堆栈空间,一共是0x30)需要我们的代码再调用完函数后,进行恢复,否则堆栈就不平衡程序就出错误了,由此可以见,我们可以整理一下这类函数的调用方式:push 参数nPush 参数2Push 参数1call 函数首地址add esp, 函数参数的个数*4由于很多的C库函数都是用这样的方式调用它,所以,这种函数的调用方式叫做C类调用(在C语言中用在这个程序中,模式都是这类的调用方式,也可以用_cdecl修饰)在这个程序中,由于main使用的3个函数都是这一种调用方式,编译器为了减少指令把堆栈一起平衡了,而没有分别对每个函数进行堆栈平衡。2. _stdcall 调用方式好的,现在我们把我们自己定义的函数MaxNum的声明和定义都改成这样:int _stdcallMaxNum(int num001, int num002, int num003);这样,Main函数中应该就是有两种调用方式了,我们可以更清楚的看出C类调用的特点:00401000 /$ 83EC 0C sub esp, 0C00401003 |. 33C0 xor eax, eax00401005 |. 8D4C24 04 lea ecx, dword ptr esp+400401009 |. 894424 08 mov dword ptr esp+8, eax0040100D |. 894424 04 mov dword ptr esp+4, eax00401011 |. 894424 00 mov dword ptr esp, eax00401015 |. 8D4424 00 lea eax, dword ptr esp00401019 |. 50 push eax0040101A |. 8D5424 0C lea edx, dword ptr esp+C0040101E |. 51 push ecx0040101F |. 52 push edx00401020 |. 68 34804000 push Func.00408034 ; ASCII %d,%d,%d00401025 |. E8 87000000 call Func.scanf0040102A |. 8B4424 10 mov eax, dword ptr esp+100040102E |. 8B4C24 14 mov ecx, dword ptr esp+1400401032 |. 8B5424 18 mov edx, dword ptr esp+1800401036 |. 83C4 10 add esp, 10; 平衡Scanf的参数使用的堆栈00401039 |. 50 push eax0040103A |? 51 push ecx0040103B |? 52 push edx0040103C |? E8 0F000000 call Func.MaxNum00401050 /$ 8B4C24 04 mov ecx, dword ptr esp+400401054 |. 8B4424 08 mov eax, dword ptr esp+800401058 |. 3BC8 cmp ecx, eax0040105A |. 7C 0B jl short Func.004010670040105C |. 8B4424 0C mov eax, dword ptr esp+C00401060 |. 3BC8 cmp ecx, eax00401062 |. 7D 0B jge short Func.0040106F00401064 |. C2 0C00 retn 0C00401067 |? 8B4C24 0C mov ecx, dword ptr esp+C0040106B |. 3BC1 cmp eax, ecx0040106D | 7D 02 jge short Func.004010710040106F 8BC1 mov eax, ecx00401071 |. C2 0C00 retn 0C; _stdcall的调用方式,在子函数中平衡堆栈00401041 |? 50 push eax00401042 |? 68 30804000 push Func.00408030 ; ASCII %d,LF00401047 |? E8 34000000 call Func.printfGetStringTypeWsWyte0040104C . 83C4 14 add esp, 14; 这里只平衡printf的参数跟一开始申请的0xC的堆栈就可以了。0040104F C3 retn我们在程序中加入的_stdcall就修改程序默认的调用方式为Windows标准调用方式,现在我们留心看一下MaxNum函数的调用和实现部分,总结一下这种Win标准调用的特点:push 参数nPush 参数2Push 参数1call 函数首地址函数的代码;retn 函数参数的个数*4; 这里就相当于add esp, 函数参数的个数*4 ,然后再RETN这类调用就是windows的标准调用,它的修饰符号是_stdcall,几乎所有的windows的API都用这种方式调用,所以,在VS开发环境中,_stdcall又被定义成了WINAPI。3. _fastcall调用方式为了保证本次课题的完整性,我多唠叨几句,说一下_fastcall的调用方式(本来是想在下一次课题面向对象编程中再讲述的),这种方式的调用在面向对象编程中比较常见,这里大概的做一下简单的介绍,等在下一次课题:C+的基础特性 中详细讲述。这种调用方式就是同时使用寄存器和堆栈一起传递参数,为了描述的更清楚,我们还是用上一小节的程序做例子,我们再把程序中的_stdcall改成_fastcall,然后Release编译,OD打开:00401000 /$ 83EC 0C sub esp, 0C00401003 |. 33C0 xor eax, eax00401005 |. 8D4C24 04 lea ecx, dword ptr esp+400401009 |. 894424 08 mov dword ptr esp+8, eax0040100D |. 894424 04 mov dword ptr esp+4, eax00401011 |. 894424 00 mov dword ptr esp, eax00401015 |. 8D4424 00 lea eax, dword ptr esp00401019 |. 50 push eax0040101A |. 8D5424 0C lea edx, dword ptr esp+C0040101E |. 51 push ecx0040101F |. 52 push edx00401020 |. 68 34804000 push Func.00408034 ; ASCII %d,%d,%d00401025 |. E8 77000000 call Func.scanf0040102A |. 8B4424 10 mov eax, dword ptr esp+100040102E |. 8B5424 14 mov edx, dword ptr esp+1400401032 |. 8B4C24 18 mov ecx, dword ptr esp+1800401036 |. 83C4 10 add esp, 1000401039 |. 50 push eax0040103A |. E8 11000000 call Func.MaxNumr; 原本3个参数的函数,现在编程一个参数了00401050 /$ 8B4424 04 mov eax, dword ptr esp+4 ; 从这里明白:它用了ECX和EDX传递了两个参数00401054 |. 3BCA cmp ecx, edx00401056 |. 7C 09 jl short Func.0040106100401058 |. 3BC8 cmp ecx, eax0040105A |. 7C 0B jl short Func.004010670040105C |. 8BC1 mov eax, ecx0040105E |. C2 0400 retn 400401061 | 3BD0 cmp edx, eax00401063 |. 7C 02 jl short Func.0040106700401065 |. 8BC2 mov eax, edx00401067 C2 0400 retn 4看到了么?也不麻烦哦,我们总结一下_fastcall的调用特点:push 参数nmov edx, 参数2mov ecx, 参数1call 函数首地址函数的代码;retn 函数参数的个数*4; 这里就相当于add esp, 函数参数的个数*4 ,然后再RETN当然,也不完全都是使用ECX和EDX两个寄存器,根据编译器的不同,使用的寄存器也不同,如果调试程序调试的多了,我们可以发现:a) VS的编译器如果用_fastcall方式调用函数,一般都是将最左边的两个小于DWORD类型的参数分别用ECX和EDX传递。b) Borland公司的编译器如果用_fastcall方式调用函数,一般都是将最左边的三个小于DWORD类型的参数分别用EAX,EDX和ECX传递。更多的特点还需要大家自己去总结。4. PASCAL 调用方式还有一种调用方式:PASCAL方式调用,限于篇幅,这里就不再举例子了,只是简单的总结一下它的特点,我们就进入下一节:Push 参数2Push 参数1push 参数ncall 函数首地址函数的代码;retn 函数参数的个数*4; 这里就相当于add esp, 函数参数的个数*4 ,然后再RETN很明显,这种调用方式与_stdcall方式十分相似,就是传递的参数顺序不同而已。四、 撩开函数的面纱。如果认真调试了本节的这个程序的朋友,一定会发现如下一些知识点:a) 函数的返回值一般存放再EAX中。b) 用esp+偏移和用ebp+偏移标识函数的参数及局部变量的异同。c) CALL/JMP的区别。a) 区别就是一个CALL就相当于push eip+5 然后再JMP 到指定的代码中。这样,再retn的时候,就知道返回到哪个地址了。b) 利用这个有很多的用法和小技巧,比如代码自定位 等等。其实归根结底,程序的代码本身就是数据,当若我们比较一个数组的二进制数据跟一个函数代码的二进制形式,我们根本无法区别他们,换句话说,我们完全可以把代码当作数据来处理,这里引用一个比较简单的例子,大家可以一起试一下:/showthread.php?t=71790另外,我们知道,一个函数的参数一般都是变量,当我们把一个函数名字(函数的首地址)当作一个变量来处理,那我们完全就可以让一个函数名作为另一个函数的参数,这个最典型的应用就是回调。具体的我们等到提高篇中具体讲解 回调函数。本节为了证明我上面的描述,给出一个小程序,算是开阔视野,也算是最函数的本质做个解释,希望大家能调试跟踪一下。#include stdio.h#include windows.htypedef unsigned char BYTE;typedef VOID (CALLBACK *MYSPRINTF)(char *, const char *, .);typedef VOID (CALLBACK *MYLSTRCAT)(char *, char *);typedef VOID (CALLBACK *MYMSGBOX)(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType);MYSPRINTF mySprintf = (MYSPRINTF)GetProcAddress(LoadLibraryA(msvcr71.dll), sprintf); MYLSTRCAT myStrCat = (MYLSTRCAT)GetProcAddress(LoadLibraryA(KERNEL32.dll), lstrcatA); MYMSGBOX myMsgBox = (MYMSGBOX)GetProcAddress(LoadLibraryA(user32.dll), MessageBoxA); BYTEbuf= 0xB8,0x00,0x12,0x00,0x00,0xE8,0xAE,0x00,0x00,0x00,0x55,0x56,0x57,0xB9,0x7F,0x00,0x00,0x00,0x33,0xC0,0x8D,0x7C,0x24,0x0D,0xC6,0x44,0x24,0x0C,0x00,0xC6,0x84,0x24,0x0C,0x02,0x00,0x00,0x00,0xF3,0xAB,0x66,0xAB,0xAA,0xB9,0xFF,0x03,0x00,0x00,0x33,0xC0,0x8D,0xBC,0x24,0x0D,0x02,0x00,0x00,0xBE,0x01,0x00,0x00,0x00,0xF3,0xAB,0x66,0xAB,0xAA,0xBF,0x01,0x00,0x00,0x00,0x3B,0xF7,0x7C,0x33,0x8B,0xEE,0xA1,0x18,0x61,0x40,0x00,0x55,0x57,0x56,0x8D,0x4C,0x24,0x18,0
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- 国际原油协议书
- 华为协议书栈
- 代缴股本协议书
- 高通端口协议书
- 房产放弃协议书
- 打井保水协议书
- 搬电池协议书
- 主播协议书仲裁
- rip协议书属于哪一层
- (2024年秋季版)七年级道德与法治下册 第三单元 主动了解社会 第11课 有序的社会 第2框 法律维序是保证说课稿 陕教版
- 2025年合肥市社会化工会工作者招聘34人笔试备考试题及答案解析
- 非婚生子女法律抚养权协议范本
- 2025年新版中层副职面试题及答案
- 蜂窝组织炎护理小讲课
- 智慧树知道网课《工业机器人技术基础》课后章节测试满分答案
- (一检)泉州市2026届高三高中毕业班质量监测(一)数学试卷(含标准答案)
- 纤维转盘滤布滤池运行维护技术说明
- 2025至2030中国无烟产品行业发展趋势分析与未来投资战略咨询研究报告
- 2021年全球工作场所状况报告 State of the Global Workplace 2021 Report
- 球墨铸铁管-施工方案(3篇)
- 2025年职业技能鉴定考试(考评员)经典试题及答案
评论
0/150
提交评论