-
2019-03-07 18:40:44
如果函数要使用参数,则必须声明接受参数值的变量。这些变量称为函数的形式参数。
形式参数就像函数内的其他局部变量,在进入函数时被创建,退出函数时被销毁。
当调用函数时,有三种向函数传递参数的方式:调用类型 描述 传值调用 该方法把参数的实际值复制给函数的形式参数。在这种情况下,修改函数内的形式参数对实际参数没有影响。 指针调用 该方法把参数的地址复制给形式参数。在函数内,该地址用于访问调用中要用到的实际参数。这意味着,修改形式参数会影响实际参数。 引用调用 该方法把参数的引用复制给形式参数。在函数内,该引用用于访问调用中要用到的实际参数。这意味着,修改形式参数会影响实际参数。 传值调用
向函数传递参数的传值调用方法,把参数的实际值复制给函数的形式参数。在这种情况下,修改函数内的形式参数不会影响实际参数。
默认情况下,C++ 使用传值调用方法来传递参数。一般来说,这意味着函数内的代码不会改变用于调用函数的实际参数。函数 swap() 定义如下:// 函数定义 void swap(int x, int y) { int temp; temp = x; /* 保存 x 的值 */ x = y; /* 把 y 赋值给 x */ y = temp; /* 把 x 赋值给 y */ return; }
现在,让我们通过传递实际参数来调用函数 swap():
实例
#include <iostream> using namespace std; // 函数声明 void swap(int x, int y); int main () { // 局部变量声明 int a = 100; int b = 200; cout << "交换前,a 的值:" << a << endl; cout << "交换前,b 的值:" << b << endl; // 调用函数来交换值 swap(a, b); cout << "交换后,a 的值:" << a << endl; cout << "交换后,b 的值:" << b << endl; return 0; }
当上面的代码被编译和执行时,它会产生下列结果:
交换前,a 的值: 100 交换前,b 的值: 200 交换后,a 的值: 100 交换后,b 的值: 200
上面的实例表明了,虽然在函数内改变了 a 和 b 的值,但是实际上 a 和 b 的值没有发生变化。
指针调用
向函数传递参数的指针调用方法,把参数的地址复制给形式参数。在函数内,该地址用于访问调用中要用到的实际参数。这意味着,修改形式参数会影响实际参数。
按指针传递值,参数指针被传递给函数,就像传递其他值给函数一样。因此相应地,在下面的函数 swap() 中,您需要声明函数参数为指针类型,该函数用于交换参数所指向的两个整数变量的值。// 函数定义 void swap(int *x, int *y) { int temp; temp = *x; /* 保存地址 x 的值 */ *x = *y; /* 把 y 赋值给 x */ *y = temp; /* 把 x 赋值给 y */ return; }
现在,让我们通过指针传值来调用函数 swap():
#include <iostream> using namespace std; // 函数声明 void swap(int *x, int *y); int main () { // 局部变量声明 int a = 100; int b = 200; cout << "交换前,a 的值:" << a << endl; cout << "交换前,b 的值:" << b << endl; /* 调用函数来交换值 * &a 表示指向 a 的指针,即变量 a 的地址 * &b 表示指向 b 的指针,即变量 b 的地址 */ swap(&a, &b); cout << "交换后,a 的值:" << a << endl; cout << "交换后,b 的值:" << b << endl; return 0; }
当上面的代码被编译和执行时,它会产生下列结果:
交换前,a 的值: 100 交换前,b 的值: 200 交换后,a 的值: 200 交换后,b 的值: 100
引用调用
向函数传递参数的引用调用方法,把引用的地址复制给形式参数。在函数内,该引用用于访问调用中要用到的实际参数。这意味着,修改形式参数会影响实际参数。
按引用传递值,参数引用被传递给函数,就像传递其他值给函数一样。因此相应地,在下面的函数 swap() 中,您需要声明函数参数为引用类型,该函数用于交换参数所指向的两个整数变量的值。
// 函数定义 void swap(int &x, int &y) { int temp; temp = x; /* 保存地址 x 的值 */ x = y; /* 把 y 赋值给 x */ y = temp; /* 把 x 赋值给 y */ return; }
现在,让我们通过引用传值来调用函数 swap():
#include <iostream> using namespace std; // 函数声明 void swap(int &x, int &y); int main () { // 局部变量声明 int a = 100; int b = 200; cout << "交换前,a 的值:" << a << endl; cout << "交换前,b 的值:" << b << endl; /* 调用函数来交换值 */ swap(a, b); cout << "交换后,a 的值:" << a << endl; cout << "交换后,b 的值:" << b << endl; return 0; }
当上面的代码被编译和执行时,它会产生下列结果:
交换前,a 的值: 100 交换前,b 的值: 200 交换后,a 的值: 200 交换后,b 的值: 100
更多相关内容 -
C语言函数调用时候内存中栈的动态变化详细分析
2019-05-02 00:23:41先了解如下几点知识和过程: ...这些指令代码是存放在内存中进程的代码段,同一个函数内的指令代码是按照地址顺序存储的(编译器决定的)(也就是说只要指令地址+1就可以自动得到下一条指令的地址...先了解如下几点知识和过程:
* 冯诺伊曼体系计算机程序指令代码都是提前从硬盘加载进入内存从而执行的(如果是哈佛体系结构的计算机指令代码是直接在外存里面执行的,具体可以看我这篇文章,计算机冯诺伊曼体系结构和哈佛体系结构区别和处理器性能评判标准),这些指令代码是存放在内存中进程的代码段,同一个函数内的指令代码是按照地址顺序存储的(编译器决定的)(也就是说只要指令地址+1就可以自动得到下一条指令的地址),那么当发生函数调用的时候,就是进入另一个函数的连续地址代码段了,所以才有了调用函数时候得提前入栈保存此函数后的一条指令的地址。
* 栈的栈顶和栈底定义不是按照地址高低定义的,是按照入栈出栈的位置定义的,入栈出栈的地方就叫做栈顶(尽管在Windows操作系统中栈的增长是从高地址到低地址),栈是后入先出的,被调用函数后进栈,那么函数返回的时候,也是它先被回收,即一层层回退的回收空间
* stack是栈,但是也经常被人们称为堆栈,heap是堆,不要乱取名。关于内存中堆栈如何分配空间以及区别是什么,可以看我这篇文章,计算机程序存储分配详解和c语言函数调用过程概述
* 整个程序维护一个栈(如果是跑操作系统的话,可能会有多个进程,那么就会有多个独立的栈,而比如单片机裸机程序就只有一个栈),这个栈在动态改变,变量的分配和释放都是栈顶指针在动态的移动罢了,如果需要释放的栈内变量是需要保存起来继续使用的,那么就用pop方式,出栈同时会进行保存在某个cpu寄存器里,比如EAX,这个寄存器可以用来临时保存变量值或者最后的函数返回值
* cpu有多个寄存器,主要是用于当前活动函数(一个函数在正运行的时候,我这里称他为活动函数)的一些暂存值,而且可能会动态改变寄存器里面内容,跟当前活动函数进行交互啥的,但是我们比较关心的就四个,EAX,ESP,EBP(注:8086中的EIP寄存器就相当于现代cpu的PC寄存器了,都是存放下一条待执行指令的地址的作用,不同cpu叫法不一样),其解释如下
* 活动函数会把局部变量和一些寄存器里面的值入栈(而不是寄存器的地址入栈,因为寄存器的地址就是通过宏定义为ebx等等名字了,也就是寄存器地址已经公之于众的了,关于这个具体可以看我这篇文章详细讲解,嵌入式微处理器结构和上电启动到开始运行程序的过程讲解),因为整个cpu只有这么一组寄存器,但是函数调用却可以有很多层,所以当前函数调用了下一个函数后,所以活动函数就变成下一个函数了,此时这组寄存器的值就先入栈也就是保存起来,然后就得用于支持新的活动函数运行了,当新的活动函数结束后,就会把刚刚入栈保存的值重新赋值给这组寄存器,因此恢复调用前的执行状态。
* 程序只有一个栈,但是却可以有函数的层级调用,而每个函数都会在这个总栈里有一个局部栈,也叫做栈帧
比如当前处于主函数main中,栈内如下:
这里面的局部变量就是main函数内定义的,而ebx,esi,edi具体是干啥的,为什么要入栈(肯定是一些记录信息),不用明白,只要知道每个活动函数都会把这三个寄存器里的值入栈就行了。EBP寄存器的值存储的是当前活动函数栈帧的栈底地址,而ESP存的就是当前栈帧的栈顶地址了。cpu下一条执行的指令地址是直接去EIP寄存器里面读取的,EIP这个寄存器每次就是用来存放 即将执行的指令的地址的,那么每次执行之前就得把下一指令的地址我们手动填进去,也就是后面会看到的函数调用完成后从栈内pop出那个提前入栈的地址。(pop那个下一指令的地址的话,这个汇编指令应该是同时还把此地址给填进EIP寄存器了)
// main函数里面具体执行的指令过程的汇编代码,第一列代表的是指令的地址,我自己去掉了无关指令代码 011C1540 push ebp //压栈,保存ebp(这个是调用main函数那个函数的栈帧的栈底地址,我也不知道到底是谁调用了main函数,应该是操作系统了吧),注意push操作隐含esp-4 011C1541 mov ebp,esp //把esp的值传递给ebp,设置当前ebp 011C1543 sub esp,0F0h //给函数开辟空间,范围是(ebp, ebp-0xF0) 011C1549 push ebx 011C154A push esi 011C154B push edi 011C154C lea edi,[ebp-0F0h] //把edi赋值为ebp-0xF0 接下来这几条指令可以不用看 011C1552 mov ecx,3Ch //函数空间的dword数目,0xF0>>2 = 0x3C 011C1557 mov eax,0CCCCCCCCh 011C155C rep stos dword ptr es:[edi] //rep指令的目的是重复其上面的指令.ECX的值是重复的次数. //STOS指令的作用是将eax中的值拷贝到ES:EDI指向的地址,然后EDI+4
// 这里就是开始调用 print_out(0, 2)了 013D155E push 2 //第二个实参压栈 013D1560 push 0 //第一个实参压栈 013D1562 call print_out (13D10FAh)//返回地址压栈,本例中是013D1567,然后调用print_out函数 013D1567 add esp,8 //两个实参出栈 //注意在call命令中,隐含操作是把下一条指令的地址压栈,也就是所谓的返回地址
// 被调用函数执行到了return语句时候,即准备结束此函数了,做的返回过程 013D141C mov eax,1 //返回值传入eax中 013D1421 pop edi 013D1422 pop esi 013D1423 pop ebx //寄存器出栈 013D1424 add esp,0D0h //以下3条命令是调用VS的__RTC_CheckEsp,检查栈溢出 013D142A cmp ebp,esp 013D142C call @ILT+315(__RTC_CheckEsp) (13D1140h) 013D1431 mov esp,ebp //ebp的值传给esp,也就是恢复调用前esp的值 013D1433 pop ebp //弹出ebp,恢复ebp的值 013D1434 ret //把返回地址写入EIP中,相当于pop EIP
现在在main函数里调用了另一个函数print_out函数,其栈变化如下:
我们可以看出函数的层级调用实际上就是重复的入栈不同活动函数的内容(相同的方式),如果print_out函数再调用一个函数,也是同样的再加一个栈帧罢了。
现在我们来分析这个入栈的过程和顺序:
main函数也是被其他某函数调用的,这里我们就不追究了,因为栈是往低地址增长的,我们可以看出main函数执行过程(即还现在是当前活动函数)是先把main内定义的局部变量入栈了,紧接着是那3个寄存器的内容,此时继续往下执行,发现遇到函数prin_out调用了,这时首先会在栈内开辟两个4字节的空间(因为只发现两个int型的形参),也就是C语言中的声明了两个变量,同时把这两个空间中分别填入0和2,即完成了函数形参的声明和初始化(因为现在还处在main函数栈帧内,所以我们可以看出被调用函数的形参的声明和赋值都是在调用函数中完成的,而不是被调函数自己分配的空间),也就是上面看到的实参1,2在栈中存在了,接下来即将进入print_out函数之前,main函数还得把print_out函数的下一条指令地址(也就是上图的返回地址)给入栈保存起来(这个过程是call print_out汇编指令就会自动完成的,其实这个下一条指令的地址就是回收刚刚分配的那两个实参占用空间的操作,即add esp,8这句指令的地址,不急,我后面会详细分析为什么是这条),因为print_out函数运行完后,此时main函数才知道应该继续怎样运行。(疑问点:这个print_out函数的下一条指令的地址不能是print_out函数执行快执行完时自己告诉main函数吗,当然不行,因为print_out函数自己根本不知道自己下一条指令是谁,自己都可能被不同函数调用呢,对外层函数(调用者)一点也不知情)。当把返回地址也压入栈后,就可以进入print_out函数了。
进入print_out函数后,还是同曾经进入main函数一样的方式,首先入栈调用者(main函数)的栈帧的栈底地址
main函数栈帧的栈底地址,也就是图中红色箭头指向内存单元的地址,在栈中就是ebp(main)这个值(目的是为了print_out函数调用完成后,main函数又成为了活动函数,main的栈帧也就成了当前栈帧,填入EBP寄存器这个地址值,使得EBP能够迅速指到正确位置,即红色箭头处,此时ESP当然就得指向edi那儿的位置也就是main函数栈帧的栈顶位置了,这样一看,就是还原回未调用print_out函数时候的栈的模样了,就是上面右图,perfect,如此的完美),这时就可以进入print_out函数内部了。
然后为当前活动函数(print_out函数)分配局部变量需要的总空间(这里分配8个不一定准确,因为ebx,esi,edi三个寄存器内的值也要接着入栈,准确应该是20个字节,但是这里为了简便,就没有这么严谨,但是原理是对的),接着入栈局部变量,ebx,esi,edi三个寄存器内的值,然后进行相应的运算过程后,一旦遇到了return语句,此时print_out函数才知道自己即将执行结束了,所以就开始做本函数栈帧的回收工作了,仅仅把返回值给保存到EXA寄存器即可(存在返回值的情况,如果是无返回值,函数是void类型,那就不需要保存返回值到EXA寄存器了),由于局部变量以及ebx,esi,edi三个寄存器内的值都是无意义的值了,直接丢掉即可,即把esp寄存器的内容直接赋值为ebp寄存器里的地址值,即esp和ebp指向同一个内存单元,此时的栈顶就变成ebp(main)这儿了,就实现了栈内存的回收,如下图所示,具体对应的汇编代码就是 mov esp,ebp,
此时再把ebp寄存器填入提前压入栈内的main函数栈帧的栈底地址,也就是ebp(main)出栈,同时赋值进ebp寄存器里
即:pop ebp //弹出ebp,会同时进行把这个地址值赋值进入ebp寄存器,即恢复ebp的值,即ebp指向了main函数栈帧的栈底了,如下图
此时已经回收完print_out函数的栈帧了,此时,已经来到了main函数栈帧了,但是并没有来到main函数的指令代码段
然后print_out函数里面就来到ret指令了,即把返回地址(存在main栈帧里的)写入EIP中,相当于pop EIP,如下图
此时,print_out函数完全执行完了,就回到了main函数指令段了,很明显接下来的一条指令就是继续回收一下main函数内当初为print_out函数形参分配的两个变量空间(当初main函数为调用函数分配形参的过程也是属于main函数的指令),即下面这句指令
add esp,8 //两个实参出栈,即回收两个实参的空间,如下图
那也就是说在main函数内调用print_out函数这个指令之后的指令就是add esp,8这句指令(编译器就可以知道这两个指令的前后关系,所以这个并不是动态的),所以当初即将调用print_out函数时入栈的那个返回地址就是add esp,8这句指令的地址,也就是回收实参的空间这句指令的地址,那再下一条指令的地址呢,因为最开始我们就说了同一个函数的指令代码都是在连续的地址空间存放的,所以只需要把add esp,8这句指令的地址+1即可得到下一条待执行指令的地址了。
就这样,整个print_out函数调用完成了,同时main函数栈帧也恢复原来未调用print_out函数时候的模样了,如上图所示,perfect,完成。
接下来我们再来看一个例子,有了上面的分析基础,那么下面这个也就很容易同理分析出来了,里面的汇编指令代码清晰明了,整个过程清清楚楚了
/-------------------------------------------------------------------------------------------------------------------/
现在,我们来总结一下函数调用时候栈的变化过程:
1.调用者在自己的栈帧里开辟好被调函数形参需要的空间
2.入栈 函数调用结束后应该执行的地址值,即返回地址,其实就是回收第一步为形参开辟的空间的指令的地址
3.进入被调函数了,入栈调用函数栈帧的栈底地址
4.在新函数的当前栈帧内为局部变量分配空间后,入栈局部变量
5.被调函数遇到return语句了,说明即将结束本函数了,就开始做回收本栈帧的空间的事了:
1)如果有返回值,那么把返回值赋值给EAX,如果没有则忽略这一步。
2)回收局部变量空间,即esp指向调用函数栈帧的栈顶了
3)提前存好的main函数栈帧的栈底地址赋值进入ebp寄存器,从而使得ebp指向main函数栈帧的栈底
4)把返回地址填入EIP寄存器,接着就会指向,回收main函数当初为被调函数开辟的两个形参的空间的指令地址
5)回收形参空间
这样就还原了main函数栈帧,回到了未调用那个函数的时候栈帧的模样。
从上面可以得出一些结论,一个函数未运行时候就是一串指令的集合,且有一个首地址(即函数地址),是一个静态的概念,当此函数运行时候就成为一种动态的概念,它的存在体现在内存中而已,也就是它对应的栈帧,当它的栈帧被回收了,那么这个函数就结束了。从函数执行与调用过程的栈的变化分析,实际上数组越界,堆栈溢出,程序崩溃的问题我们已经知道答案了,就是被调用函数的某些变量对调用者函数栈帧里的返回地址进行了覆盖等等,从而被调函数返回到了错误的地址,造成运行错误。
最后再来讨论一下这样一个问题:刚刚我们看到被调用函数是通过cpu的eax,edx两个寄存器传递返回值,然后调用函数只需要去读取这两个寄存器的值就得到了被调函数的返回值,但是eax,edx这两个寄存器都是32位的,也就是总共能够返回8字节的数据,对于基本类型的数据(比如char,int,float,double(占8字节),指针类型)的返回没问题,但是假如我们想返回一个结构体类型的数据且成员总大小超过了8字节(常见方法是传递结构体指针。但作为语言上允许的方式,有必要弄清楚编译器如何实现这种方式),其原理是怎样的呢?
答:我们的编译器编译同样一个程序一般支持生成两个版本的目标代码,debug版本和release版本,debug版本编译结果一般是针对调试程序使用的,代码优化较低,更好的还原了开发者写的C语言源程序结构。release版本是指发行版,即软件上架发布使用,编译器对代码优化程度较高,对无用代码和不可达状态等等都进行了删除(有兴趣具体了解代码优化的可以查阅编译原理一书),不便于调试,但是运行效率更高。其实这两者的原理都是基本一样的,这里我们对debug版本和release版本分别进行简单讲解。
第一种情况,不超过8字节的结构体返回过程:如下图所示:
总结:
(1.1)用 edx:eax 传递返回值。调用方不需要在栈上向 add 函数传递接受返回值的地址。也就是跟基本数据类型变量返回是一样的过程。
(2.2)debug 版本在调用方生成临时对象返回值(而release版本就不是这样,上图中红色方框内的内存空间就不会存在,而是寄存器的值直接拷贝到main函数的t变量里面,所以release版本效率更高),然后再把临时对象拷贝到 main 指定变量t所在地址。效率低。我们可以看到临时对象是在main函数的栈帧里的,也就是说main函数在调用add函数前分析了一下它的返回值类型大小,就分配了空间,当调用完成后就会把临时对象(返回值内容)的值复制到左边的指定赋值的变量t,此时临时对象就完成了它的使命,main函数就回收临时对象的空间了。
第二种情况,超过8字节的结构体返回过程:如下图所示:
总结:
(1)当结构体超过 8 bytes,不能用 EDX:EAX 传递,这时调用者在自己栈帧栈上保留有一个用于填充返回值的结构体,其地址在入栈实参后 push 到栈上(也就是该结构体地址被当作一个实参处理),如上图蓝色箭头处。被调用函数add将会根据这个地址,把返回值设置到这个地址,红色箭头处。
(2)在 main 函数中,debug 版本比 release 版本还多了一个临时对象,效率低。而 release 版本中只有返回值和临时变量 t(图中红色方框的临时对象不存在),效率略高于 debug。但两者模型基本一致,还是得从返回值那儿的空间的内容复制到左边指定的赋值变量t的空间(指的都是main函数内的t),然后回收返回值所对应的空间,总体效率还是低于传结构体指针(因为指针只占用4个字节,直接通过eax寄存器即可返回,然后赋值给指针t即可),所以建议在C语言中返回结构体类型数据时候尽量用指针返回,代码运行效率更高。
(3)对于上述两个实验,release 版本优化都比较厉害,main 函数中对 t 的赋值是不完整的,因为编译器认为有些成员没有使用到(比如t.b,t.c两成员的赋值,即无用代码),所以没有必要复制,只要满足代码等效即可(具体知识可参考编译原理一书代码优化章节)。
这里就不贴出上述两个实验对应的汇编代码了,编译器优化功能不是万能的,我们知道底层这样的过程后,以后写代码时候才能心中有数,写出更高质量更高效率的代码。
欢迎关注我的博客,我有空会写一些关于计算机基础理论方面通俗易懂的科普性文章,一方面用于记录自己的学习过程,另一方面可以分享给其他人,让更多的人了解如今生活中无处不在的计算机的工作原理。
参考文章:
函数调用--函数栈 函数调用--函数栈 - 小雨淅淅 - 博客园
-
C函数的调用过程原理和栈分析
2018-07-13 12:01:57在编程中,相信每个人对函数都不陌生,那么你真正理解函数的调用过程吗?当一个c函数被调用时,一个栈帧(stack frame)是如何被建立,又如何被消除的。本文主要就是来解决这些问题的,不同的操作系统和编译器可能有所...在编程中,相信每个人对函数都不陌生,那么你真正理解函数的调用过程吗?当一个c函数被调用时,一个栈帧(stack frame)是如何被建立,又如何被消除的。本文主要就是来解决这些问题的,不同的操作系统和编译器可能有所不同,本文主要介绍在linux下的gcc编译器。
栈帧
我们先来看一下,一个典型的栈帧的样子:
首先介绍一下这里面非两个重要的指针:ebp和esp;
ebp(base pointer )可称为“帧指针”或“基址指针”,其实语意是相同的。在未受改变之前始终指向栈帧的开始,也就是栈底,所以ebp的用途是在堆栈中寻址用的。esp(stack pointer)可称为“ 栈指针”。 esp是会随着数据的入栈和出栈移动的,也就是说,esp始终指向栈顶。
了解内存结构的伙伴肯定知道,从上往下来说,地址从高向低,栈位于内核态之下,是向下生长的,所谓向下生长是指从内存高地址->低地址的路径延伸,那么就很明显了,栈有栈底和栈顶,那么栈顶的地址要比栈底低。
在了解了栈帧的结构之后,下面我们就来看一看函数的调用过程及栈帧的变化。函数调用过程
比如说main函数中有如下函数:
int func(int a , int b);
func有两个局部的int变量。这里,main是调用者,func是被调用者。
ESP被func使用来指示栈顶。EBP相当于一个“基准指针”。从main传递到func的参数以及func函数本身的局部变量都可以通过这个基准指针为参考,加上偏移量找到。
由于被调用者允许使用EAX,ECX和EDX寄存器,所以如果调用者希望保存这些寄存器的值,就必须在调用子函数之前显式地把他们压栈,保存在栈中。另一方面,如果除了上面提到的几个寄存器,被调用者还想使用别的寄存器,比如EBX,ESI和EDI,那么,被调用者就必须在栈中保存这些被额外使用的寄存器,并在调用返回前恢复他们。也就是说,如果被调用者只使用约定的EAX,ECX和EDX寄存器,他们由调用者负责保存(push)并恢复(pop),但如果被调用这还额外使用了别的寄存器,则必须有他们自己保存并回复这些寄存器的值。函数的入参
传递给func的参数被压到栈中,最后一个参数先进栈,所以第一个参数是位于栈顶的。所以说函数是从右往左进行参数的入栈的,这和变长参数有关。此外,func中声明的局部变量以及函数执行过程中需要用到的一些临时变量也都存在栈中。
返回值
小于等于4个字节的返回值会被保存到EAX中,如果大于4字节,小于8字节,那么EDX也会被用来保存返回值。如果返回值占用的空间还要大,那么调用者会向被调用者传递一个额外的参数,这个额外的参数指向将要保存返回值的地址。用C语言来说,就是函数调用:
x = foo(a, b, c); 被转化为: func(&x, a, b, c);
注意,这仅仅在返回值占用大于8个字节时才发生。有的编译器不用EDX保存返回值,所以当返回值大于4个字节时,就用这种转换。
当然,并不是所有函数调用都直接赋值给一个变量,还可能是直接参与到某个表达式的计算中,如:m = foo(a, b, c) + foo(d, e, f);
有或者作为另外的函数的参数, 如:
fooo(foo(a, b, c), 3);
这些情况下,foo的返回值会被保存在一个临时变量中参加后续的运算,所以,foo(a, b, c)还是可以被转化成
foo(&tmp, a, b, c)。
调用过程
假设函数A调用函数B,我们称A函数为”调用者”,B函数为“被调用者”则函数调用过程可以这么描述:
(1)先将调用者(A)的堆栈的基址(ebp)入栈,以保存之前任务的信息,函数返回之后可以继续执行之前的逻辑。
(2)然后将调用者(A)的栈顶指针(esp)的值赋给ebp,作为新的基址(即被调用者B的栈底)。
(3)然后在这个基址(被调用者B的栈底)上开辟(一般用sub指令)相应的空间用作被调用者B的栈空间,进行函数入参的压栈等操作。
(4)函数B返回后,从当前栈帧的ebp即恢复为调用者A的栈顶(esp),使栈顶恢复函数B被调用前的位置;然后调用者A再从恢复后的栈顶可弹出之前的ebp值(可以这么做是因为这个值在函数调用前一步被压入堆栈)。这样,ebp和esp就都恢复了调用函数B前的位置,也就是栈恢复函数B调用前的状态。下面,让我们一步步地看一下在c函数调用过程中,一个栈帧是如何建立及消除的。
函数调用前调用者的动作
在上面的示例中,调用者是main,它准备调用函数func。在函数调用前,main正在用ESP和EBP寄存器指示它自己的栈帧。
首先,main把EAX,ECX和EDX压栈。这是一个可选的步骤,只在这三个寄存器内容需要保留的时候执行此步骤。
接着,main把传递给func的参数一一进栈,最后的参数最先进栈。
最后,main用call指令调用子函数:call func。当call指令执行的时候,EIP指令指针寄存器的内容会先被压入栈中。因为EIP寄存器是指向main中的下一条指令,所以现在返回地址就在栈顶了。在call指令执行完之后,下一个执行周期将从名为foo的标记处开始。
图2展示了call指令完成后栈的内容。图2及后续图中的粗线指示了函数调用前栈顶的位置。我们将会看到,当整个函数调用过程结束后,栈顶又回到了这个位置。
被调用者在函数调用后的动作
①、建立它自己的栈帧,
②、为局部变量分配空间
③、如果函数中需要使用寄存器EBX,ESI和EDI,则压栈保存寄存器的值,出栈时恢复。此时栈空间如下:
具体过程如下:
首先被调用的函数必须建立它自己的栈帧。EBP寄存器现在正指向main的栈帧中的某个位置,这个值必须被保留,因此,EBP进栈。然后ESP的内容赋值给了EBP。这使得函数的参数可以通过对EBP附加一个偏移量得到,而栈寄存器ESP便可以空出来做其他事情。第一个参数的地址是EBP加8,因为main的EBP和返回地址各在栈中占了4个字节。
下一步,被调用的函数必须为它的局部变量分配空间,同时,也必须为它可能用到的一些临时变量分配空间。比如,foo中的一些C语句可能包括复杂的表达式,其子表达式的中间值就必须得有地方存放。这些存放中间值的地方同城被称为临时的,因为他们可以为下一个复杂表达式所复用。
最后,如果foo用到EBX,ESI和EDI寄存器,则它f必须在栈里保存它们。被调用者返回前的动作
被调用的函数返回前,必须先把返回值保存在EAX寄存器中。当返回值占用多于4个或8个字节时,接收返回值的变量地址会作为一个额外的指针参数被传到函数中,而函数本身就不需要返回值了。这种情况下,被调用者直接通过内存拷贝把返回值直接拷贝到接收地址,从而省去了一次通过栈的中转拷贝。
其次,被调用的函数必须恢复EBX,ESI和EDI寄存器的值。如果这些寄存器被修改,正如我们前面所说,我们会在foo执行开始时把它们的原始值压入栈中。
这两步之后,我们不再需要foo的局部变量和临时存储了,我们可以通过下面的指令消除栈帧:mov esp, ebp pop ebp
最后直接执行返回指令。从栈里弹出返回地址,赋值给EIP寄存器。
调用者在返回后的动作
在程序控制权返回到调用者后,传递给被调函数的参数已经不需要了。我们可以把所有个参数一起弹出栈,实现堆栈平衡。
如果在函数调用前,EAX,ECX和EDX寄存器的值被保存在栈中,调用者main函数现在可以把它们弹出。这个动作之后,栈顶就回到了我们开始整个函数调用过程前的位置。至此,函数的调用过程就已经分析完毕了。下面,看个具体的实例:
实例
源代码
c源码:
#include <stdio.h> int add(int a , int b) { int c = a + b; return c; } int main() { int result = 0; result = add(1 , 2); printf("%d\n",result); return 0; }
在linux下,通过: gcc -S test.c -o test.s 命令将源文件编译成汇编文件,若对c语言的编译过程感兴趣的可以看我的博文c程序编译全过程
相应的汇编代码如下:汇编代码
.file "test.c" .text .globl add .type add, @function add: .LFB0: .cfi_startproc pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 movl %edi, -20(%rbp) movl %esi, -24(%rbp) movl -24(%rbp), %eax movl -20(%rbp), %edx addl %edx, %eax movl %eax, -4(%rbp) movl -4(%rbp), %eax popq %rbp .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE0: .size add, .-add .section .rodata .LC0: .string "%d\n" .text .globl main .type main, @function main: .LFB1: .cfi_startproc pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 subq $16, %rsp movl $0, -4(%rbp) movl $2, %esi movl $1, %edi call add movl %eax, -4(%rbp) movl -4(%rbp), %eax movl %eax, %esi movl $.LC0, %edi movl $0, %eax call printf movl $0, %eax leave .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE1: .size main, .-main .ident "GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-28)" .section .note.GNU-stack,"",@progbits
在linux下的汇编代码和windows下的有些许差别,但依然类似,比如:windows下的push就是linux下的pushp,不过linux下的源操作数在左边,目的操作数在右边。在linux下,%开头表示寄存器,$开头表示立即数;在一个汇编函数的开头和结尾, 分别有.cfi_startproc和.cfi_endproc标示着函数的起止。
下面,分别对main函数和被调函数的执行过程分析:main:函数段
函数调用前调用者的动作
主要汇编代码如下:
pushq %rbp #rbp入栈 ,保存main的栈帧中的某个位置 movq %rsp, %rbp #ESP的内容赋值给了EBP。解放esp用于指向栈顶 movl $2, %esi #参数放入寄存器 movl $1, %edi call add #调用add函数
被调函数的动作
主要汇编代码如下
pushq %rbp movq %rsp, %rbp movl %edi, -20(%rbp) movl %esi, -24(%rbp) movl -24(%rbp), %eax movl -20(%rbp), %edx addl %edx, %eax movl %eax, -4(%rbp) movl -4(%rbp), %eax popq %rbp ret
函数调用后调用者的动作
movl %eax, -4(%rbp) movl -4(%rbp), %eax movl %eax, %esi movl $.LC0, %edi movl $0, %eax call printf movl $0, %eax leave ret
-
C语言函数调用三种方式:传值调用,引用调用和传地址调用
2017-03-18 16:47:36C语言函数调用三种方式:传值调用,引用调用和传地址调用我想,你只要看了C语言上关于传值函数调用的测试题,一切都会了然于胸:
考题一:程序代码如下:
void Exchg1(int x, int y)
{
int tmp;
tmp=x;
x=y;
y=tmp;
printf(“x=%d,y=%d/n”,x,y)
}
void main()
{
int a=4,b=6;
Exchg1 (a,b) ;
printf(“a=%d,b=%d/n”,a,b)
}
输出的结果:
x=_, y=_
a=_, b=_
问下划线的部分应是什么,请完成。考题二:代码如下。
Exchg2(int *px, int *py)
{
int tmp=*px;
*px=*py;
*py=tmp;
print(“*px=%d,*py=%d/n”,*px,*py);
}
main()
{
int a=4;
int b=6;
Exchg2(&a,&b);
Print(“a=%d,b=%d/n”, a, b);
}
输出的结果为:
*px=_, *py=_
a=_, b=_
问下划线的部分应是什么,请完成。考题三:
Exchg2(int &x, int &y)
{
int tmp=x;
x=y;
y=tmp;
print(“x=%d,y=%d/n”,x,y);
}
main()
{
int a=4;
int b=6;
Exchg2(a,b);
Print(“a=%d,b=%d/n”, a, b);
}
二. 函数参数传递方式之一:值传递
1. 值传递的一个错误认识
先看题一中Exchg1函数的定义:
void Exchg1(int x, int y) //定义中的x,y变量被称为Exchg1函数的形式参数
{
int tmp;
tmp=x;
x=y;
y=tmp;
printf(“x=%d,y=%d/n”,x,y)
}
问:你认为这个函数是在做什么呀?
答:好像是对参数x,y的值对调吧?
请往下看,我想利用这个函数来完成对a,b两个变量值的对调,程序如下:
void main()
{
int a=4,b=6;
Exchg1 (a,b) //a,b变量为Exchg1函数的实际参数。
/ printf(“a=%d,b=%d/n”,a,b)
}
我问:Exchg1 ()里头的 printf(“x=%d,y=%d/n”,x,y)语句会输出什么啊?
我再问:Exchg1 ()后的 printf(“a=%d,b=%d/n”,a,b)语句输出的是什么?
程序输出的结果是:
x=6 , y=4
a=4 , b=6 //为什么不是a=6,b=4呢?奇怪,明明我把a,b分别代入了x,y中,并在函数里完成了两个变量值的交换,为什么a,b变量值还是没有交换(仍然是a==4,b==6,而不是a==6,b==4)?如果你也会有这个疑问,那是因为你跟本就不知实参a,b与形参x,y的关系了。
2. 一个预备的常识
为了说明这个问题,我先给出一个代码:
int a=4;
int x;
x=a;
x=x+3;
看好了没,现在我问你:最终a值是多少,x值是多少?
(怎么搞的,给我这个小儿科的问题。还不简单,不就是a==4 x==7嘛!)
在这个代码中,你要明白一个东西:虽然a值赋给了x,但是a变量并不是x变量哦。我们对x任何的修改,都不会改变a变量。呵呵!虽然简单,并且一看就理所当然,不过可是一个很重要的认识喔。3. 理解值传递的形式
看调用Exch1函数的代码:main()
{
int a=4,b=6;
Exchg1(a,b) //这里调用了Exchg1函数
printf(“a=%d,b=%d”,a,b)
}Exchg1(a,b)时所完成的操作代码如下所示。
int x=a;//←
int y=b;//←注意这里,头两行是调用函数时的隐含操作
int tmp;
tmp=x;
x=y;
y=tmp;
请注意在调用执行Exchg1函数的操作中我人为地加上了头两句:
int x=a;
int y=b;
这是调用函数时的两个隐含动作。它确实存在,现在我只不过把它显式地写了出来而已。问题一下就清晰起来啦。(看到这里,现在你认为函数里面交换操作的是a,b变量或者只是x,y变量呢?)
原来 ,其实函数在调用时是隐含地把实参a,b 的值分别赋值给了x,y,之后在你写的Exchg1函数体内再也没有对a,b进行任何的操作了。交换的只是x,y变量。并不是a,b。当然a,b的值没有改变啦!函数只是把a,b的值通过赋值传递给了x,y,函数里头操作的只是x,y的值并不是a,b的值。这就是所谓的参数的值传递了。
哈哈,终于明白了,正是因为它隐含了那两个的赋值操作,才让我们产生了前述的迷惑(以为a,b已经代替了x,y,对x,y的操作就是对a,b的操作了,这是一个错误的观点啊!)。三. 函数参数传递方式之二:地址传递
继续——地址传递的问题!
看题二的代码:
Exchg2(int *px, int *py)
{
int tmp=*px;
*px=*py;
*py=tmp;
print(“*px=%d,*py=%d/n”,*px,*py);
}
main()
{
int a=4;
int b=6;
Exchg2(&a,&b);
Print(“a=%d,b=%d/n”, a, b);
}
它的输出结果是:
*px=6,*py=4
a=6,b=4看函数的接口部分:Exchg2(int *px,int *py),请注意:参数px,py都是指针。 再看调用处:Exchg2(&a, &b); 它将a的地址(&a)代入到px,b的地址(&b)代入到py。同上面的值传递一样,函数调用时作了两个隐含的操作:将&a,&b的值赋值给了px,py。
px=&a;
py=&b;
呵呵!我们发现,其实它与值传递并没有什么不同,只不过这里是将a,b的地址值传递给了px,py,而不是传递的a,b的内容,而(请好好地在比较比较啦)
整个Exchg2函数调用是如下执行的:
px=&a; //
py=&b; //请注意这两行,它是调用Exchg2的隐含动作。
int tmp=*px;
*px=*py;
*py=tmp;
print(“*px=%d,*py=%d/n”,*px,*py);
这样,有了头两行的隐含赋值操作。我们现在已经可以看出,指针px,py的值已经分别是a,b变量的地址值了。接下来,对*px,*py的操作当然也就是对a,b变量本身的操作了。所以函数里头的交换就是对a,b值的交换了,这就是所谓的地址传递(传递a,b的地址给了px,py),你现在明白了吗?四. 函数参数传递方式之三:引用传递
看题三的代码:
Exchg3(int &x, int &y) //注意定义处的形式参数的格式与值传递不同
{
int tmp=x;
x=y;
y=tmp;
print(“x=%d,y=%d/n”,x,y);
}
main()
{
int a=4;
int b=6;
Exchg3(a,b); //注意:这里调用方式与值传递一样
Print(“a=%d,b=%d/n”, a, b);
}
输出结果:
x=6, y=4
a=6, b=4 //这个输出结果与值传递不同。
看到没有,与值传递相比,代码格式上只有一处是不同的,即在定义处:
Exchg3(int &x, int &y)。
但是我们发现a与b的值发生了对调。这说明了Exchg3(a,b)里头修改的是a,b变量,而不只是修改x,y了。
我们先看Exchg3函数的定义处Exchg3(int &x,int &y)。参数x,y是int的变量,调用时我们可以像值传递(如: Exchg1(a,b); )一样调用函数(如: Exchg3(a,b); )。但是x,y前都有一个取地址符号&。有了这个,调用Exchg3时函数会将a,b 分别代替了x,y了,我们称x,y分别引用了a,b变量。这样函数里头操作的其实就是实参a,b本身了,也就是说函数里是可以直接修改到a,b的值了。最后对值传递与引用传递作一个比较:
1. 在函数定义格式上有不同:
值传递在定义处是:Exchg1(int x, int y);
引用传递在这义处是:Exchg1(int &x, int &y);2. 调用时有相同的格式:
值传递:Exchg1(a,b);
引用传递:Exchg3(a,b);3. 功能上是不同的:
值传递的函数里操作的不是a,b变量本身,只是将a,b值赋给了x,y函数里操作的只是x,y变量而不是a,b,显示a,b的值不会被Exchg1函数所修改。
引用传递Exchg3(a,b)函数里是用a,b分别代替了x,y。函数里操作的是a,b。转自:http://blog.csdn.net/xiaosong2008/article/details/25430261
-
构造函数和析构函数的调用顺序
2019-09-03 15:47:29目录构造函数和析构函数构造函数复制构造函数析构函数构造函数与析构函数的调用顺序单继承多继承 构造函数和析构函数 构造函数 构造函数是特殊的成员函数,与类同名,没有返回类型,而且允许重载。如果没有为类显式... -
( )函数是指直接或间接调用函数本身的函数
2021-02-03 10:20:49【单选题】推动超现实主义艺术产生的精神动力是( ) 【单选题】胆道感染最常见的致病菌是( ) 【判断题...你认为这一说法: 【单选题】X 是线性规划的基本可行解则有: 【单选题】( )函数是指直接或间接调用函数本身的函数 -
python def 定义函数,调用函数
2019-07-13 22:04:09python def 定义函数,调用函数 def sum_2_nums(a,b): #def 定义函数 result = a+b print('%d+%d=%d'%(a,b,result)) num1 = int(input('请输入第一个数字:')) num2 = int(input('请输入第二个数字:')) sum_2_... -
在非虚函数中调用虚函数
2017-07-21 10:51:40一,下面是一个在非虚函数中调用虚函数的例子 #include using namespace std; class Base { protected: int i; public: Base(int j):i(j){} virtual ~Base(){} virtual void func2(){ i ++; } void fu -
系统调用和函数调用的区别
2018-10-10 13:34:49系统调用与函数调用的区别 系统调用 1.使用INT和IRET指令,内核和应用程序使用的是不同的堆栈,因此存在堆栈的切换,从用户态切换到内核态,从而可以使用特权指令操控设备 2.依赖于内核,不保证移植性 3.在用户... -
系统调用和函数调用区别
2017-10-20 10:53:15系统调用和函数调用 1. 系统调用 a. 什么是系统调用 系统调用,通俗的讲,说的是操作系统提供给用户程序调用的一组“特殊”接口。用户程序可以通过这组“特殊”接口来获得操作系统内核提供的服务,比如用户可以... -
C++构造函数调用顺序
2019-06-24 09:49:26C++构造函数按下列顺序被调用: (1)任何虚拟基类的构造函数按照它们被继承的顺序构造; (2)任何非虚拟基类的构造函数按照它们被继承的顺序构造; (3)任何成员对象的构造函数按照它们声明的顺序调用; (4)类自己... -
C++中可以在构造函数中调用另一个构造函数吗?
2017-09-15 10:16:16题目如下:问下列代码的打印结果为0吗? [cpp] view plain copy #include #include using namespace std; struct CLS { int m_i; CLS( int i ) : m_i... -
Linux----统计所有C语言源程序中,下列系统函数的被调用次数
2019-12-28 19:34:27要求编写一个程序,统计所有C语言源程序中,下列系统函数的被调用次数。 printf open close read write fork signal 统计结果输出到myresult.txt文件按中,格式如下: printf 49 open 13 close 13 read 24 write 16.... -
C++中函数调用时的三种参数传递方式详解
2017-08-31 20:44:51原文地址:http://blog.csdn.net/cocohufei/article/details/6143476; ... 在C++中,参数传递的方式是“实虚结合”。 按值传递(pass by value) 地址传递(pass by pointer) 引用传递(pass b... -
JavaScript调用函数的三种方式(直接调用,call()方法调用,apply()方法调用)
2018-09-30 00:40:22直接调用函数是我们平时使用JS函数最简单最直接的方法,举例如下: window.alert("aaa"); 这段代码将在浏览器中弹出文本为“aaa”的提示,通常,我们可以把JS中window对象的方法的“window.”... -
C++ 函数与函数调用(传值、指针、引用调用)
2018-04-15 00:24:42如何划分代码到不同的函数中是由您来决定的,但在逻辑上,划分通常是根据每个函数执行一个特定的任务来进行的。 函数声明告诉编译器函数的名称、返回类型和参数。函数定义提供了函数的实际主体。 ... -
python打印函数调用关系
2018-05-30 19:20:12使用python时,当想知道是哪里 调用的特定代码时,可在此特定代码前加下列语句: 网上搜到的其他信息 不如这个全 :)import sysprint ("--- current function is ", sys._getframe().f_code.co_name)... -
C 函数的定义与调用及例题
2019-02-12 16:36:40一,函数的定义与调用 · 在C语言中,函数(Function)是一个处理过程,即一段程序的工作放在函数中进行,函数结束时可以携带或不带处理结果。 · C语言程序处理过程全部都是以函数形式出现,最简单的程序至少也有... -
python中函数定义与调用的顺序的关系
2019-04-28 20:40:44python中的函数应该先定义再调用、还先调用后定义呢?或者说与顺序无关呢? 根据C/C++ 中关于函数定义与调用的关系的规定,应该是先定义后调用,但是大家都晓得python是一门格式十分自由的编程语言,会不会不受此... -
c语言复习--函数的递归调用
2021-12-22 19:08:48函数A直接调用函数A为直接递归,函数A调用函数B,函数B又调用函数C,称为间接递归。 一个简单的例子:用递归函数求n的阶乘 #include<stdio.h> void main(){ int n,p; scanf("%d",&n); p=facto(n); ... -
禁止:C++在构造函数中调用构造函数
2013-11-25 09:07:421)在c++里,由于构造函数允许有默认参数,使得这种构造函数调用构造函数来重用代码的需求大为减少 2)如果仅仅为了一个构造函数重用另一个构造函数的代码,那么完全可以把构造函数中的公共部分抽取出来定义一个... -
C++析构函数何时被调用
2020-03-18 00:36:21构造从类层次的最根处开始,在每一层中,首先调用基类的构造函数,然后调用成员对象的构造函数。析构则严格按照与构造相反的次序执行,该次序是唯一的,否则编译器将无法自动执行析构过程。 构造函数和析构函数都是... -
实验5:使用函数计算分段函数的值:输入x,计算并输出下列分段函数sign(x)的值。要求定义和调用函数sign...
2020-12-05 16:59:53// 函数声明, XY形式参数 int main() { int x,y;//xy实际参数 scanf("%d",&x); y=sign(x); printf("sign(%d)=%d",x,y); return 0; } int sign(int X)//定义函数 { int Y; if (X>0) Y=1; else if... -
c#的构造函数及构造函数的调用
2015-12-01 16:53:07C#构造函数的特性一、 什么是C#构造函数?Construct,Function C#构造函数是一种特殊的成员函数,它主要用于为对象分配存储空间,对数据成员进行初始化. C#构造函数具有一些特殊的性质: (1)C#构造... -
python函数的定义与调用
2019-09-01 13:47:20一、如何定义函数: 函数是组织好的,可以重复使用的,用来实现一定功能的代码段。...2、python中函数定义: def funName(parmerslist): "函数__文档字符串" 函数体 return [表达式] #可选,没... -
系统调用和函数调用
2015-11-24 13:42:25系统调用和函数调用1. 系统调用a. 什么是系统调用系统调用,通俗的讲,说的是操作系统提供给用户程序调用的一组“特殊”接口。用户程序可以通过这组“特殊”接口来获得操作系统内核提供的服务,比如用户可以通过文件... -
c语言的问题若有以下调用语句,则不正确的fun函数的首部是?
2021-05-22 09:46:2439若有以下调用语句,则不正确的fun函数的首部是?...avoid fun(int m,int x[])bvoid fun(int s, int h[41])cvoid fun(int p, int *s)dvoid fun(int n, int a)解答:答案d是错误的,因为调用函数中的实在参数为一个是... -
函数调用,题目练习
2016-11-05 00:17:30标题:函数调用,题目练习 关键词:函数调用50%,题目练习1个 内容(A):(1)依据函数参数的情况,函数分为有参函数和无参函数。函数定义时的参数称为形式参数,简称形参;函数调用时的参数称为实际参数,简称实参。... -
JavaScript/jQuery中函数调用加不加括号,加不加引号的解析
2017-06-19 18:39:30function show(){ alert("XXXX"); } 触发show方法我们看到过得的会有三种写法: 1.$(this).click(show); 2:$(this).click("show()"); 3.$(this).click(show());...括号内是作为一个参数传递,要把show函数传递进