函数的调用
一、函数
函数是指一段在一起的、可以做某一件事儿的程序。也叫做子程序、(OOP中)方法。
一个较大的程序一般应分为若干个程序块,每一个模块用来实现一个特定的功能。所有的高级语言中都有子程序这个概念,用子程序实现模块的功能。在C语言中,子程序的作用是由一个主函数和若干个函数构成。由主函数调用其他函数,其他函数也可以互相调用。同一个函数可以被一个或多个函数调用任意多次。
在程序设计中,常将一些常用的功能模块编写成函数。
函数一般由:返回值;函数名;函数参数组成(一个或多个)。
二、栈和栈帧
1.栈
栈(stack)又名堆栈,它是一种运算受限的线性表。其限制是仅允许在表的一端进行插入和删除运算。这一端被称为栈顶,相对地,把另一端称为栈底。向一个栈插入新元素又称作进栈、入栈或压栈,它是把新元素放到栈顶元素的上面,使之成为新的栈顶元素;从一个栈删除元素又称作出栈或退栈,它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素。
在Win32中有ESP和EBP两个寄存器来完成栈的操作。
ESP(Extended Stack Pointer)为扩展栈指针寄存器,是指针寄存器的一种,用于存放函数栈顶指针。与之对应的是EBP(Extended Base Pointer),扩展基址指针寄存器,也被称为帧指针寄存器,用于存放函数栈底指针。
对应的汇编操作指令是PUSH(压栈、入栈) POP(出栈);在win32程序中PUSH POP每次操作4字节(32位数据)。
2.栈帧
栈帧也叫过程活动记录,是编译器用来实现函数调用过程的一种数据结构。C语言中,每个栈帧对应着一个未运行完的函数。从逻辑上讲,栈帧就是一个函数执行的环境:函数调用框架、函数参数、函数的局部变量、函数执行完后返回到哪里等等。栈是从高地址向低地址延伸的。每个函数的每次调用,都有它自己独立的一个栈帧,这个栈帧中维持着所需要的各种信息。寄存器ebp指向当前的栈帧的底部(高地址),寄存器esp指向当前的栈帧的顶部(低地址)。
三、局部变量
局部变量(Local variables)指在程序中只在特定过程或函数中可以访问的变量。
他伴随着函数的初始化而开始,函数的结束而消亡。局部变量分配在栈上,一般分布在栈帧的顶端,从低地址向高地址延伸。这样的数据排列和分配为栈溢出造就了先天的条件。
四、函数的调用约定
函数调用约定}描述了函数传递参数方式和栈协同工作的技术细节。不同系统,不同语言,不通的编译器在实现函数调用时的原理虽然相同,但具体的调用约定还是有差异的。这包涵了参数的传递方式,参数的入栈顺序,参数从左向右依次入栈,还是从右向左依次入栈,还有的利用寄存器来保存参数,实现参数的传递,但是寄存器有限,这就局限了这种传参方式;函数返回时恢复栈平衡的操作在子函数中还是在母函数中完成。
VS函数一般使用_stdcall约定,不特殊表明的情况下都是_stdcall约定。
五、EIP寄存器
在16位汇编中 IP是指令寄存器,存放当前指令的下一条指令的地址。CPU该执行哪条指令就是通过IP来指示的。
而EIP是win32汇编中的指令寄存器,EIP中存放的32位的值,就是当前进程中CPU所执行的那条指令;在win64中的指令寄存器是RIP和EIP、IP的功能是一样的。
六、函数调用的流程和细节
下面我们我们会利用VS的调试功能来观察函数调用的过程,以及函数返回时是如何处理的。这里我们要注意函数参数是如何传递的,函数为何能在调用结束时准确的回到母函数并能继续进行下去,多注意观察栈的变化。
下面是一段高级语言的C代码,我们主要关注main函数中调用sum函数的细节。
下面是VS调试下的对应的汇编代码
首先将sum函数的参数由右至左依次压入栈中。
接着会将sum函数调用完成后的地址入栈(也叫返回地址),即printf(“%d...这条代码在内存中的起始地址(这样当sum函数结束时,EIP才能找到回家的路)。
进入sum函数后,sum函数会重新组织新栈帧,为局部变量申请栈空间,接着将用到的寄存器入栈保存起来,并对栈帧空间进行0xcc填充,(为什么用0xcc来填充,0xcc对应的int 3指令,如果EIP误入其中就会引发断点;我们看到Debug模式下的函数与函数间的空隙都是int 3指令,就是这个原理)。
当sum函数返回时,会利用eax存放返回值;将事先保存好的寄存器的值从栈中一一取出还给寄存器,并将新栈帧销毁,恢复母函数的栈帧,当ret执行时,会将栈中的返回地址赋值给EIP,返回到sum函数调用后的下一个地址,并继续母函数的流程(即main函数)。我们可以看到sum函数调用压入栈中参数造成的栈不平衡有母函数进行堆栈平衡操作。