C语言的调用惯例
一、介绍
首先我们要知道函数是怎样被调用的,我们才能更加深入的理解调用惯例。
函数如何调用
如果我们要了解调用惯例,就要明白函数是如何进行调用的,这一段算是一个前置的知识吧。
栈:这个我们就不仔细讲了,这个就是一个具有连续地址的,先进后出的一种数据结构。
栈帧:每一次调用都会在调用栈上面加上一个独立的栈帧,用来存储一些跳转点的信息。
图片来自网络,侵删
函数的每一次调用,都会从栈空间中挖走一部分存储空间,生成和一个它自己的栈帧。
什么是调用惯例
调用惯例Calling Convention其实就是调用方和被调用方的一种协议。一般来说调用惯例包括以下几个方面的内容。
- 函数的参数是按什么样的顺序进行传递的,以及它是怎么样被调用的。
- 如何对栈进行一个维护,就是如何对栈进行一个操作,也就是说是被调用函数自身将这个栈进行处理,还是说等调用函数进行栈的清空处理。
- 对名称进行修饰。为了链接的时候对调用惯例进行区分,调用惯例要对函数本身的名字进行修饰。
二、常用的调用惯例
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,但仍然没有出现根据调用惯例变化而变化的情况。可能是因为我会随着编译器的不同而不同,希望有相关了解的大佬能够解答,感激不尽。