C语言的调用惯例

C语言的调用惯例

一、介绍

首先我们要知道函数是怎样被调用的,我们才能更加深入的理解调用惯例。

函数如何调用

如果我们要了解调用惯例,就要明白函数是如何进行调用的,这一段算是一个前置的知识吧。

:这个我们就不仔细讲了,这个就是一个具有连续地址的,先进后出的一种数据结构。

栈帧:每一次调用都会在调用栈上面加上一个独立的栈帧,用来存储一些跳转点的信息。

在这里插入图片描述

图片来自网络,侵删

函数的每一次调用,都会从栈空间中挖走一部分存储空间,生成和一个它自己的栈帧。

什么是调用惯例

调用惯例Calling Convention其实就是调用方和被调用方的一种协议。一般来说调用惯例包括以下几个方面的内容。

  1. 函数的参数是按什么样的顺序进行传递的,以及它是怎么样被调用的。
  2. 如何对栈进行一个维护,就是如何对栈进行一个操作,也就是说是被调用函数自身将这个栈进行处理,还是说等调用函数进行栈的清空处理。
  3. 对名称进行修饰。为了链接的时候对调用惯例进行区分,调用惯例要对函数本身的名字进行修饰。

二、常用的调用惯例

C语言中主要的调用惯例有cdecl,stdcall,fastcall,C++中还有一个thiscall

我们就简单介绍以下这几种调用惯例。

调用惯例谁负责清栈(出栈方)参数传递方法名字修饰(存疑)
cdecl函数调用方从右往左入栈下划线加+函数名
stdcall函数本身从右往左入栈下划线+函数名+@+参数的字节数
fastcall函数本身头两个DWORD(4字节)类型或者占更少字节的参数被放入寄存器,剩下的从右到左入栈@+函数名+@+参数的字节数

thiscall是C++独有的,专用于类成员的函数的调用,不同编译器会不一样。

在C语言中默认使用cdecl。

三、汇编查看

我们首先写一个程序,本人使用系统是ubuntu 22.04,编译器为gcc version 11.3.0

然后我们编写一个C代码

因为我们主要是查看函数调用,所以代码就尽量写的简单。

void __attribute__ (( __cdecl)) function(int a,int b) {
}
void main() {
	function(2, 3);
}

我们使用GCC来查看其汇编代码

首先在hello.c文件夹中打开终端,然后输入

gcc -m32 -S hello.c -o hello.s

然后我们查看汇编代码,如果对汇编有莫名恐惧的话可以直接看分析

		.file	"hello.c"
	.text
	.globl	function
	.type	function, @function
function:
.LFB0:
	.cfi_startproc
	pushl	%ebp
	.cfi_def_cfa_offset 8
	.cfi_offset 5, -8
	movl	%esp, %ebp
	.cfi_def_cfa_register 5
	call	__x86.get_pc_thunk.ax
	addl	$_GLOBAL_OFFSET_TABLE_, %eax
	nop
	popl	%ebp
	.cfi_restore 5
	.cfi_def_cfa 4, 4
	ret
	.cfi_endproc
.LFE0:
	.size	function, .-function
	.globl	main
	.type	main, @function
main:
.LFB1:
	.cfi_startproc
	pushl	%ebp
	.cfi_def_cfa_offset 8
	.cfi_offset 5, -8
	movl	%esp, %ebp
	.cfi_def_cfa_register 5
	call	__x86.get_pc_thunk.ax
	addl	$_GLOBAL_OFFSET_TABLE_, %eax
	pushl	$3
	pushl	$2
	call	function
	addl	$8, %esp 
	nop
	leave
	.cfi_restore 5
	.cfi_def_cfa 4, 4
	ret
	.cfi_endproc
.LFE1:
	.size	main, .-main
	.section	.text.__x86.get_pc_thunk.ax,"axG",@progbits,__x86.get_pc_thunk.ax,comdat
	.globl	__x86.get_pc_thunk.ax
	.hidden	__x86.get_pc_thunk.ax
	.type	__x86.get_pc_thunk.ax, @function
__x86.get_pc_thunk.ax:
.LFB2:
	.cfi_startproc
	movl	(%esp), %eax
	ret
	.cfi_endproc
.LFE2:
	.ident	"GCC: (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0"
	.section	.note.GNU-stack,"",@progbits

然后我们使用stdcall的方式进行一个编译

	.file	"hello.c"
	.text
	.globl	function
	.type	function, @function
function:
.LFB0:
	.cfi_startproc
	pushl	%ebp
	.cfi_def_cfa_offset 8
	.cfi_offset 5, -8
	movl	%esp, %ebp
	.cfi_def_cfa_register 5
	call	__x86.get_pc_thunk.ax
	addl	$_GLOBAL_OFFSET_TABLE_, %eax
	nop
	popl	%ebp
	.cfi_restore 5
	.cfi_def_cfa 4, 4
	ret	$8
	.cfi_endproc
.LFE0:
	.size	function, .-function
	.globl	main
	.type	main, @function
main:
.LFB1:
	.cfi_startproc
	pushl	%ebp
	.cfi_def_cfa_offset 8
	.cfi_offset 5, -8
	movl	%esp, %ebp
	.cfi_def_cfa_register 5
	call	__x86.get_pc_thunk.ax
	addl	$_GLOBAL_OFFSET_TABLE_, %eax
	pushl	$3
	pushl	$2
	call	function
	nop
	leave
	.cfi_restore 5
	.cfi_def_cfa 4, 4
	ret
	.cfi_endproc
.LFE1:
	.size	main, .-main
	.section	.text.__x86.get_pc_thunk.ax,"axG",@progbits,__x86.get_pc_thunk.ax,comdat
	.globl	__x86.get_pc_thunk.ax
	.hidden	__x86.get_pc_thunk.ax
	.type	__x86.get_pc_thunk.ax, @function
__x86.get_pc_thunk.ax:
.LFB2:
	.cfi_startproc
	movl	(%esp), %eax
	ret
	.cfi_endproc
.LFE2:
	.ident	"GCC: (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0"
	.section	.note.GNU-stack,"",@progbits

结果

看的有点眼花缭乱是不是,没关系,我把主要的区别列出来,你就完全明白了,首先是cdecl的

这是function调用后最后的几句
ret
.cfi_endproc
这是main函数调用后的最后几句
call	function
addl	$8, %esp 
nop
leave

然后是stdcall的

这是function调用后最后的几句
ret	$8
.cfi_endproc
这是main函数调用后的最后几句
call	function
nop
leave

看到没,一个是ret,一个是ret 8(美元符号会产生那个冲突,我就不打了),然后一个在调用完之后是addl 8,esp,另一个啥也没有。

这个其实就是我们之前讲的关于谁负责清理栈的问题。你看cdecl是ret,表明调用函数时,这个函数没有进行清理,而是返回到main函数后,main函数通过addl 8进行清理。相反我们发现stdcall后,会有一个ret 8,ret 8 就是对栈进行一个清理,所以它是先对栈进行清理,然后再返回到main函数中的,很神奇对吧。

四、关于fastcall的汇编查看

至于为什么把这一个拿出来单独查看,是以为为了更好展现区别,所以我们需要改一下C语言的函数。

void __attribute__ ((stdcall)) function(int a,int b,int c,int d) {
}
void main() {
	function(1,2,3,4);
}

注意看,我们有四个函数,然后我们首先是stdcall的汇编语言

	.file	"hello.c"
	.text
	.globl	function
	.type	function, @function
function:
.LFB0:
	.cfi_startproc
	pushl	%ebp
	.cfi_def_cfa_offset 8
	.cfi_offset 5, -8
	movl	%esp, %ebp
	.cfi_def_cfa_register 5
	call	__x86.get_pc_thunk.ax
	addl	$_GLOBAL_OFFSET_TABLE_, %eax
	nop
	popl	%ebp
	.cfi_restore 5
	.cfi_def_cfa 4, 4
	ret	$16
	.cfi_endproc
.LFE0:
	.size	function, .-function
	.globl	main
	.type	main, @function
main:
.LFB1:
	.cfi_startproc
	pushl	%ebp
	.cfi_def_cfa_offset 8
	.cfi_offset 5, -8
	movl	%esp, %ebp
	.cfi_def_cfa_register 5
	call	__x86.get_pc_thunk.ax
	addl	$_GLOBAL_OFFSET_TABLE_, %eax
	pushl	$4
	pushl	$3
	pushl	$2
	pushl	$1
	call	function
	nop
	leave
	.cfi_restore 5
	.cfi_def_cfa 4, 4
	ret
	.cfi_endproc
.LFE1:
	.size	main, .-main
	.section	.text.__x86.get_pc_thunk.ax,"axG",@progbits,__x86.get_pc_thunk.ax,comdat
	.globl	__x86.get_pc_thunk.ax
	.hidden	__x86.get_pc_thunk.ax
	.type	__x86.get_pc_thunk.ax, @function
__x86.get_pc_thunk.ax:
.LFB2:
	.cfi_startproc
	movl	(%esp), %eax
	ret
	.cfi_endproc
.LFE2:
	.ident	"GCC: (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0"
	.section	.note.GNU-stack,"",@progbits

然后是fastcall的汇编代码

	.file	"hello.c"
	.text
	.globl	function
	.type	function, @function
function:
.LFB0:
	.cfi_startproc
	pushl	%ebp
	.cfi_def_cfa_offset 8
	.cfi_offset 5, -8
	movl	%esp, %ebp
	.cfi_def_cfa_register 5
	subl	$8, %esp
	call	__x86.get_pc_thunk.ax
	addl	$_GLOBAL_OFFSET_TABLE_, %eax
	movl	%ecx, -4(%ebp)
	movl	%edx, -8(%ebp)
	nop
	leave
	.cfi_restore 5
	.cfi_def_cfa 4, 4
	ret	$8
	.cfi_endproc
.LFE0:
	.size	function, .-function
	.globl	main
	.type	main, @function
main:
.LFB1:
	.cfi_startproc
	pushl	%ebp
	.cfi_def_cfa_offset 8
	.cfi_offset 5, -8
	movl	%esp, %ebp
	.cfi_def_cfa_register 5
	call	__x86.get_pc_thunk.ax
	addl	$_GLOBAL_OFFSET_TABLE_, %eax
	pushl	$4
	pushl	$3
	movl	$2, %edx
	movl	$1, %ecx
	call	function
	nop
	leave
	.cfi_restore 5
	.cfi_def_cfa 4, 4
	ret
	.cfi_endproc
.LFE1:
	.size	main, .-main
	.section	.text.__x86.get_pc_thunk.ax,"axG",@progbits,__x86.get_pc_thunk.ax,comdat
	.globl	__x86.get_pc_thunk.ax
	.hidden	__x86.get_pc_thunk.ax
	.type	__x86.get_pc_thunk.ax, @function
__x86.get_pc_thunk.ax:
.LFB2:
	.cfi_startproc
	movl	(%esp), %eax
	ret
	.cfi_endproc
.LFE2:
	.ident	"GCC: (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0"
	.section	.note.GNU-stack,"",@progbits

结果

所以区别在哪里呢,最主要的区别在main里面

这是stdcall里面的传递参数的代码
	pushl	$4
	pushl	$3
	pushl	$2
	pushl	$1
	call	function
这是fastcall里面传递参数的代码
	pushl	$4
	pushl	$3
	movl	$2, %edx
	movl	$1, %ecx
	call	function

你会发现最大的区别是stdcall中是pushl到栈里面,而fastcall是先push到栈,最后两个如果是小于32位bit的话就直接放到寄存器里面。我们都知道我们的计算都是用寄存器来进行的,fastcall是直接把参数传递到寄存器中,因而计算的速度会快上一点。而且你还可以发现最后清理的时候,fastcall只清理了8,而stdcall清理了16(ret 8和ret 16),这也是因为fastcall有两个参数在寄存器中,所以就少清理两个。

五、一些存在的问题

就是关于函数名称修饰的,我查阅很多资料都讲到了关于函数名称修饰的问题,但是从我亲自的操作来看并没有发现函数名称根据调用惯例的变化而变化。

我刚开始以为会不会是c++存在这种情况,然后我发现在C语言中,函数名称始终为function,而在C++中,函数名称会变为_Z8functioniiii,但仍然没有出现根据调用惯例变化而变化的情况。可能是因为我会随着编译器的不同而不同,希望有相关了解的大佬能够解答,感激不尽。

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
C语言是一种高级编程语言,而Python也是一种高级编程语言。在C语言调用Python程序可以通过使用Python的解释器来实现。要在C语言调用Python程序,首先需要在C中包含Python的头文件,以便可以使用Python的函数和数据结构。 在C代码中,可以使用Py_Initialize()函数来初始化Python解释器。然后可以使用PyRun_SimpleString()函数或PyRun_SimpleFile()函数来执行Python代码。这样可以将C语言中的数据传递给Python程序进行处理,并获取Python的返回结果。 此外,还可以使用PyImport_ImportModule()函数来导入Python模块,并使用PyObject_CallObject()函数来调用Python函数。通过这些函数的使用,可以在C语言中直接调用Python函数。 在调用Python程序之前,需要先确保Python解释器已经安装在计算机上,并且路径已正确配置好。可以通过设置环境变量或在代码中指定Python解释器的路径来实现。 需要注意的是,在调用Python程序时,C语言和Python存储数据的方式是不同的。所以在C语言和Python之间传递数据时,需要进行数据类型的转换。可以使用PyArg_ParseTuple()函数来将C语言的数据类型转换为Python的数据类型,或使用Py_BuildValue()函数将Python的数据类型转换为C语言的数据类型。 总之,通过上述方法,我们可以在C语言调用Python程序,实现C语言和Python的互操作。这样可以充分利用C语言的性能和Python的灵活性,来开发更加复杂和强大的应用程序。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值