C函数参数传递与返回值传递
2016-09-25 17:54
260 查看
(1)参数传递
__stdcall和__cdecl都是函数调用约定关键字,先给出这两者的区别,然后举实例分析:
__stdcall:参数由右向左压入堆栈;堆栈由函数本身清理。
__cdecl:参数也是由右向左压入堆栈;但堆栈由调用者清理。
另外,这两者在同一名字修饰约定下,编译过后变量和函数的名字也不一样,具体见另一博文:名字修饰约定extern "C"与extern "C++"浅析
下面给出实例分析:
[cpp] view
plain copy
#include "stdio.h"
#include <iostream>
#include <Windows.h>
#include <conio.h>
using namespace std;
int __stdcall Func_stdcall(int nParam1, int nParam2)
{
return 1;
}
int __cdecl Func_cdecl(int nParam1, int nParam2)
{
return 1;
}
int main()
{
int a = Func_stdcall(1, 2);
a = Func_cdecl(1, 2);
return 0;
}
以上代码在XP + VC++6.0 SP6环境下编译,编译后的汇编代码如下:
首先要明确上图汇编代码中几个指令的作用:
1.call:将call下一条指令的EIP压入堆栈,然后跳到@后标号地址处执行;EIP相当于保存着当前程序的计数器值。
2.ret:将堆栈的当前数据弹出给EIP,然后继续执行;
3.ret n:n表示一个整数,将堆栈的当前数据弹出给EIP,再将ESP的值加上n,然后继续执行。
我们再看汇编代码,调用Func_stdcall和Func_cdecl时,都是由调用者(main函数)将参数压入堆栈,注意地址0x00401127、0x00401129和0x00401133、0x00401135都是先压入2,再压入1,这个顺序就是函数参数由右向左的顺序。
再注意地址0x0040110F,这是调用Func_stdcall时的出口指令,"ret 8"先把EIP的值弹出,然后再将ESP的值加8,相当于执行两次出栈的操作。因为编译环境是32位的,调用Func_stdcall时压入的2和1,其实是压入的两个32位整数值,刚好占8个字节。然后再继续执行EIP处的指令,此时EIP的值应为0x00401130,为call指令的下一条指令,这条指令是将返回的值赋给变量a。可见,堆栈的清理是由Func_stdcall内部处理的,外部调用者并不处理。
然后再来看看__cdecl修饰的Func_cdecl,注意地址0x0040111B,只有一个指令“ret”,只将堆栈当前的值弹出给EIP,然后继续执行。但是在调用前已经压入了两个32位的整数值,堆栈还没有被清理。我们再来看看继续执行的指令,地址0x0040113C处的指令为继续执行的指令,指令为“add esp,8“,这个很好理解了,直接将esp的值加上8,也相当于执行两次出栈操作。但这是由调用者(main参数)进行的,因此堆栈是由调用者进行清理的。
__stdcall通常用于Windows API中,可见如下代码:
[cpp] view
plain copy
#define CALLBACK __stdcall
#define WINAPI __stdcall
#define WINAPIV __cdecl
#define APIENTRY WINAPI
#define APIPRIVATE __stdcall
#define PASCAL __stdcall
#define cdecl _cdecl
#ifndef CDECL
#define CDECL _cdecl
#endif
而C和C++程序的缺省调用方式则为__cdecl,下图为VC++6.0的默认设置,因此在不显式写明调用约定的情况下,一般都是采用__cdecl方式,而在与Windows
API打交道的场景下,通常都是显式的写明使用__stdcall,才能与Windows API保持一致。
另外,还要注意的是,如printf此类支持可变参数的函数,由于不知道调用者会传递多少个参数,也不知道会压多少个参数入栈,因此函数本身内部不可能清理堆栈,只能由调用者清理了。
(2)函数返回值传递
出自《程序员的自我修养-链接、装载与库》P299
eax是函数传递返回值的一个通道。
1.对于小于4个字节的数据函数将返回值存储在eax中。
2.5~8个字节对象的情况调用惯例都是采用eax和edx的联合返回方式进行。
3.大于8个字节的返回类型,用一下代码测试:
首先main函数在栈额外开辟了一片空间,并将这块空间的一部分作为传递返回值的临时对象,这里称为temp
将temp对象的地址作为隐藏参数传递个return_test函数
return_test 函数将数据拷贝给temp对象,并将temp对象的地址用eax传出。
return_test返回以后,mian函数将eax 指向的temp对象的内容拷贝给n。
如果返回值的类型的尺寸太大,c语言在函数的返回时会使用一个临时的栈上内存作为中转,结果返回值对象会被拷贝两次。因而不到万不得已,不要轻易返回大尺寸对象。
再来看看函数返回一个C++对象会如何:
运行后的输出结果可以得出:函数返回之后,进行了一个拷贝函数的调用,以及一次operator=的调用,也就是说,仍然产生了两次拷贝。因此C++的对象同样会产生临时对象。
在这段代码中我们还能看到在c++返回一个对象时,对象要经过两次拷贝构造函数的调用才能够完成返回对象的传递,1次拷贝到栈上的临时对象里,另一次把临时对象拷贝到存储返回值的对象里。在某些编译器里,返回一个对象甚至要经过更多的步骤。
为了减少返回对象的开销,C++提出了返回值优化(RVO)技术,可以将某些场合下的对象拷贝减少一次,例如:
目的是直接将对象的构造在传出时使用的临时对象上,减少一次复制过程。
__stdcall和__cdecl都是函数调用约定关键字,先给出这两者的区别,然后举实例分析:
__stdcall:参数由右向左压入堆栈;堆栈由函数本身清理。
__cdecl:参数也是由右向左压入堆栈;但堆栈由调用者清理。
另外,这两者在同一名字修饰约定下,编译过后变量和函数的名字也不一样,具体见另一博文:名字修饰约定extern "C"与extern "C++"浅析
下面给出实例分析:
[cpp] view
plain copy
#include "stdio.h"
#include <iostream>
#include <Windows.h>
#include <conio.h>
using namespace std;
int __stdcall Func_stdcall(int nParam1, int nParam2)
{
return 1;
}
int __cdecl Func_cdecl(int nParam1, int nParam2)
{
return 1;
}
int main()
{
int a = Func_stdcall(1, 2);
a = Func_cdecl(1, 2);
return 0;
}
以上代码在XP + VC++6.0 SP6环境下编译,编译后的汇编代码如下:
首先要明确上图汇编代码中几个指令的作用:
1.call:将call下一条指令的EIP压入堆栈,然后跳到@后标号地址处执行;EIP相当于保存着当前程序的计数器值。
2.ret:将堆栈的当前数据弹出给EIP,然后继续执行;
3.ret n:n表示一个整数,将堆栈的当前数据弹出给EIP,再将ESP的值加上n,然后继续执行。
我们再看汇编代码,调用Func_stdcall和Func_cdecl时,都是由调用者(main函数)将参数压入堆栈,注意地址0x00401127、0x00401129和0x00401133、0x00401135都是先压入2,再压入1,这个顺序就是函数参数由右向左的顺序。
再注意地址0x0040110F,这是调用Func_stdcall时的出口指令,"ret 8"先把EIP的值弹出,然后再将ESP的值加8,相当于执行两次出栈的操作。因为编译环境是32位的,调用Func_stdcall时压入的2和1,其实是压入的两个32位整数值,刚好占8个字节。然后再继续执行EIP处的指令,此时EIP的值应为0x00401130,为call指令的下一条指令,这条指令是将返回的值赋给变量a。可见,堆栈的清理是由Func_stdcall内部处理的,外部调用者并不处理。
然后再来看看__cdecl修饰的Func_cdecl,注意地址0x0040111B,只有一个指令“ret”,只将堆栈当前的值弹出给EIP,然后继续执行。但是在调用前已经压入了两个32位的整数值,堆栈还没有被清理。我们再来看看继续执行的指令,地址0x0040113C处的指令为继续执行的指令,指令为“add esp,8“,这个很好理解了,直接将esp的值加上8,也相当于执行两次出栈操作。但这是由调用者(main参数)进行的,因此堆栈是由调用者进行清理的。
__stdcall通常用于Windows API中,可见如下代码:
[cpp] view
plain copy
#define CALLBACK __stdcall
#define WINAPI __stdcall
#define WINAPIV __cdecl
#define APIENTRY WINAPI
#define APIPRIVATE __stdcall
#define PASCAL __stdcall
#define cdecl _cdecl
#ifndef CDECL
#define CDECL _cdecl
#endif
而C和C++程序的缺省调用方式则为__cdecl,下图为VC++6.0的默认设置,因此在不显式写明调用约定的情况下,一般都是采用__cdecl方式,而在与Windows
API打交道的场景下,通常都是显式的写明使用__stdcall,才能与Windows API保持一致。
另外,还要注意的是,如printf此类支持可变参数的函数,由于不知道调用者会传递多少个参数,也不知道会压多少个参数入栈,因此函数本身内部不可能清理堆栈,只能由调用者清理了。
(2)函数返回值传递
出自《程序员的自我修养-链接、装载与库》P299
eax是函数传递返回值的一个通道。
1.对于小于4个字节的数据函数将返回值存储在eax中。
2.5~8个字节对象的情况调用惯例都是采用eax和edx的联合返回方式进行。
3.大于8个字节的返回类型,用一下代码测试:
1 typedef struct big_thing 2 { 3 char buf[128]; 4 }big_thing; 5 6 big_thing return_test() 7 { 8 big_thing b; 9 b.buf[] = 0; 10 return b; 11 } 12 13 int main() 14 { 15 big_thing n = return_test(); 16 }
首先main函数在栈额外开辟了一片空间,并将这块空间的一部分作为传递返回值的临时对象,这里称为temp
将temp对象的地址作为隐藏参数传递个return_test函数
return_test 函数将数据拷贝给temp对象,并将temp对象的地址用eax传出。
return_test返回以后,mian函数将eax 指向的temp对象的内容拷贝给n。
如果返回值的类型的尺寸太大,c语言在函数的返回时会使用一个临时的栈上内存作为中转,结果返回值对象会被拷贝两次。因而不到万不得已,不要轻易返回大尺寸对象。
再来看看函数返回一个C++对象会如何:
1 #include <iostream> 2 using namespace std; 3 4 struct cpp_obj 5 { 6 cpp_obj() 7 { 8 cout << "ctor\n"; 9 } 10 11 cpp_obj(const cpp_obj& c) 12 { 13 cout << "copy ctor\n"; 14 } 15 16 cpp_obj& opearator=(const cpp_obj& rhs) 17 { 18 cout << "operator = \n"; 19 return *this; 20 } 21 22 ~cpp_obj() 23 { 24 cout << "dtor\n"; 25 } 26 }; 27 28 cpp_obj return_test() 29 { 30 cpp_obj b; 31 cout << "before return\n"; 32 return b; 33 } 34 int main() 35 { 36 cpp_obj n; 37 n = return_test(); 38 }
运行后的输出结果可以得出:函数返回之后,进行了一个拷贝函数的调用,以及一次operator=的调用,也就是说,仍然产生了两次拷贝。因此C++的对象同样会产生临时对象。
在这段代码中我们还能看到在c++返回一个对象时,对象要经过两次拷贝构造函数的调用才能够完成返回对象的传递,1次拷贝到栈上的临时对象里,另一次把临时对象拷贝到存储返回值的对象里。在某些编译器里,返回一个对象甚至要经过更多的步骤。
为了减少返回对象的开销,C++提出了返回值优化(RVO)技术,可以将某些场合下的对象拷贝减少一次,例如:
1 cpp_obj return_test() 2 { 3 return cpp_obj(); 4 }
目的是直接将对象的构造在传出时使用的临时对象上,减少一次复制过程。
相关文章推荐
- C/C++ 函数参数和返回值传递机制
- C函数与汇编函数之间参数及返回值传递方法
- 实现数组作为函数参数传递,切返回值也是数组
- 170518 逆向-寄存器传递参数、函数返回值
- C++函数参数和返回值三种传递方式:值传递、指针传递和引用传递(着重理解)
- 有关java的函数调用返回值的问题和参数传递问题
- 汇编学习第五课之函数参数传递,函数返回值
- C函数只能有一个返回值,如果需要返回多个值,怎么办,其实很简单,只要将指针作为函数参数传递就可以了
- 2017033000C++函数参数和返回值三种传递方式:值传递、指针传递和引用传递(着重理解)
- C语言学习4: 函数返回值与传入参数,关于函数值传递和类型隐性转换,变量不同的作用域,static变量,多文件编译例如两个C文件,显示函数调用语句跳转,递归,斐波那契数列,多文件编译相同变量的问题。
- X86和ARM:函数调用参数传递和返回值
- x64 调用约定,参数传递以及函数返回值
- C函数与汇编函数之间参数及返回值传递方法
- C函数与汇编函数之间参数及返回值传递方法
- 函数的参数和返回值的传递方式
- C/C++ 函数参数和返回值传递机制
- perl 函数参数传递与返回值(一)
- JavaScript中函数当作参数传递或当作返回值
- C++函数的参数传递、返回值及函数声明
- 函数参数和返回值的传递