C/C++函数调用时参数传递过程、调用约定与可变参函数的实现

目录

1、参数传递过程

2、参数压栈顺序从右至左的影响

3、调用约定

3.1、__cdecl C/C++ 缺省调用约定

3.2、 __stdcall调用约定

3.3、 __fastcall (快速调用约定,通过寄存器来传送参数)

3.4、C++的函数名修饰方式

4、thiscall (本身调用,仅用于“C++”成员函数)

5、C语言可变参函数的实现

5.1、实现

5.2、需要使用__cdecl调用约定的原因


1、参数传递过程

        C/C++在函数调用时将参数传递给被调函数是通过将参数压入栈中传递给被调函数的:先将对应内存位置的参数值取出保存到寄存器中,再将值压入栈中。

        并且压栈顺序是从右往左的,由上图可知在fun(a,b,c)调用时,参数顺序是按照c、b、a压栈的。

        在fun函数(被调函数)中,通过dword ptr [参数名] 的方式取出参数名所代表地址的参数(dword:双字 4字节,word:单字 2字节,byte:1字节),或是保存下来,或是直接进行计算。

        此时因为只是简单的算术运算,所以直接在进行了计算。中间结果用寄存器保存,再赋值给r。最终结果用寄存器带回。

        对于结构体、string等复杂数据类型也是通过压栈方式传参的,虽然过程比较复杂。

2、参数压栈顺序从右至左的影响

        参数压栈的顺序是从右至左的,若在传两个参数为同一个变量时,对其进行了后置++,则右侧参数先入栈 值为 10 ,左侧参数后入栈 值为11。

3、调用约定

        上述描述的参数传递方式是C/C++默认的参数传递方式,使用__cdecl调用约定。调用约定不但决定了参数传递的方式,还决定了参数使用完后参数栈空间是如何维护的与函数名的修饰方式。除此之外还有 __stdcall、_fastcall、thiscall等调用约定。

        整体使用上行为一致,但C和C++对函数名有各自的修饰方式。下面对调用约定的介绍是基于C的修饰方式介绍的。

        可以用 extern "C"; 和 extern "C++"; 的方式指定修饰方式。未知原因,目前无法在vs反汇编中查看函数名的修饰情况。

3.1、__cdecl C/C++ 缺省调用约定

        1.压栈顺序:从右到左。

        2.参数栈维护:由调用函数把参数弹出栈, 传送参数的内存栈由调用函数来维护。正因为如此,要想实现可变参数vararg的函数(如printf)只能使用该调用约定。

        3.函数修饰名约定:函数编译后会在函数名前面加上下划线前缀。

        4.每一个调用它的函数都包含清空堆栈的代码,所以产生的可执行文件大小会比调用_stdcall函数的大。

3.2、 __stdcall调用约定

        1.压栈顺序:从右到左。

        2.参数栈维护:被调用函数把参数弹出栈(在退出时清空堆栈)。

        3.函数修饰名约定:函数编译后会在函数名前面加上下划线前缀,在函数名后加上"@"和参数的字节数。

        例如: int f(void *p)  (编译后)=>  _f@4

3.3、 __fastcall (快速调用约定,通过寄存器来传送参数)

        1.压栈顺序:用ECX和EDX传送前两个双字(DWORD)或更小的参数,剩下的参数仍旧自右向左压栈传送。

        2.参数栈维护:被调用函数在返回前清理传送参数的内存栈。

        2.函数修饰名约定:函数编译后会在函数名前面加上"@"前缀,在函数名后加上"@"和参数的字节数。

3.4、C++的函数名修饰方式

函数重载中C++函数名修饰约定规则

4、thiscall (本身调用,仅用于“C++”成员函数)

        1.压栈顺序:this指针存放于CX/ECX寄存器中,参数从右到左的压栈顺序。

        2.thiscall不是关键词,因此不能被程序员指定,只能用于C++成员函数。

5、C语言可变参函数的实现

5.1、实现

        C语言可变参函数是通过使用stdarg.h头文件中的函数来实现的。可变参数函数的定义必须以省略号(…)结尾,同时在函数体内使用va_list(实际上是char*)、va_startva_argva_end等宏和函数来访问可变参数列表。

        可变参数入栈规则是:可变参数是从右向左入栈的,即最后一个参数先入栈,第一个参数最后入栈。

        在函数内部需要先使用va_start宏来初始化可变参数列表,然后使用va_arg宏来逐个获取参数值。va_arg宏需要传入两个参数,第一个参数是va_list类型的可变参数列表,第二个参数是需要获取的参数类型,它会返回该类型的参数值并将可变参数列表指针指向下一个参数。最后,在函数结束时需要使用va_end宏来清理可变参数列表

        可变参数指的是在不同的调用中可以传递不同数量的参数,但在每次调用中都要指明参数数量。

#include <stdio.h>
#include <stdarg.h>    //头文件

// count指明当前调用中参数个数
int sum(int count, ...) {
	va_list ap;	//定义可变参数列表
	int i, total = 0;
	//根据传入的参数初始化可变参列表
	va_start(ap, count);	

	//依次使用va_arg取出参数
	for (i = 0; i < count; i++) {
		total += va_arg(ap, int);
	}
	va_end(ap);	//清理参数列表
	return total;
}
int main() {
	int result = sum( 3, 1, 2, 3);
	printf("result = %d\n", result);
	return 0;
}

        在下图汇编代码中,我们可以看出参数是从右至左压栈传递的,并且函数调用完后的栈平衡由main()维护,表明使用的是__cdecl调用约定。

5.2、需要使用__cdecl调用约定的原因

        C语言可变参数函数实现需要使用__cdecl调用约定,主要是因为在函数定义时可变参数函数的参数数量和类型是不确定的无法在编译期确定参数的内存布局,因此需要在调用处动态地获取参数(其他调用约定在编译时就确定了参数类型与数量,并且添加到名字修饰中了)。__cdecl调用约定按照从右向左的顺序将参数压入栈中,由调用函数负责栈平衡。这可以保证可变参数在栈中的内存布局与固定参数的内存布局一致,从而实现可变参数的访问。

        另外,__cdecl调用约定还有一个好处是在函数调用时不需要进行参数的内存对齐,因为参数是依次压入栈中的,不需要考虑内存对齐的问题。而其他调用约定(如__stdcall)则需要进行参数的内存对齐,这对于可变参数函数来说会增加实现的复杂度。因此,__cdecl调用约定是实现可变参数函数的首选调用约定。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值