Calling conventions of the C language
1. Introduction
First we need to know how the function is called, so that we can have a deeper understanding of the calling conventions.
How the function is called
If we want to understand calling conventions, we need to understand how functions are called. This section can be regarded as prerequisite knowledge.
Stack : We won’t go into detail about this. This is a data structure with consecutive addresses, first in, last out.
Stack frame : Each call will add an independent stack frame to the call stack to store some jump point information.
The picture comes from the Internet and has been deleted due to infringement.
Each time a function is called, a portion of the storage space is taken away from the stack space and its own stack frame is generated.
what is calling convention
Calling Convention Calling Convention is actually an agreement between the caller and the callee. Generally speaking, calling conventions include the following aspects.
- The order in which the parameters of the function are passed and how it is called.
- How to maintain the stack means how to perform an operation on the stack, that is, whether the called function itself processes the stack, or whether it waits for the calling function to clear the stack.
- Modifies the name. In order to distinguish calling conventions during linking, the calling convention modifies the name of the function itself.
2. Commonly used calling conventions
The main calling conventions in C language are cdecl, stdcall, fastcall, and there is also thiscall in C++
We will briefly introduce the following calling conventions.
calling convention | Who is responsible for clearing the stack (the stacker) | Parameter passing method | Name modification (questionable) |
---|---|---|---|
cdecl | function caller | Push from right to left | Underscore + function name |
stdcall | the function itself | Push from right to left | Underscore + function name + @ + the number of bytes of the parameter |
fastcall | the function itself | The first two parameters of type DWORD (4 bytes) or less are put into registers, and the rest are pushed onto the stack from right to left. | @+function name+@+number of bytes of parameter |
Thiscall is unique to C++ and is dedicated to calling functions of class members. Different compilers will vary.
In the C language, cdecl is used by default.
3. Compilation View
We first write a program. The system I use is ubuntu 22.04, and the compiler is gcc version 11.3.0
Then we write a C code
Because we are mainly looking at function calls, the code should be written as simple as possible.
void __attribute__ (( __cdecl)) function(int a,int b) {
}
void main() {
function(2, 3);
}
We use GCC to view its assembly code
First open the terminal in the hello.c folder and enter
gcc -m32 -S hello.c -o hello.s
Then we look at the assembly code. If you have an inexplicable fear of assembly, you can directly look at the analysis.
.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
Then we use stdcall to compile
.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
result
It’s a bit confusing, isn’t it? It doesn’t matter. I will list the main differences and you will understand completely. The first is cdecl.
这是function调用后最后的几句
ret
.cfi_endproc
这是main函数调用后的最后几句
call function
addl $8, %esp
nop
leave
and then stdcall's
这是function调用后最后的几句
ret $8
.cfi_endproc
这是main函数调用后的最后几句
call function
nop
leave
Did you see that one is ret, one is ret 8 (the dollar sign will cause that conflict, I will not fight), then one is addl 8, esp after the call, and the other is nothing.
This is actually the issue we talked about before about who is responsible for cleaning up the stack. You see that cdecl is ret, indicating that when the function is called, this function does not clean up, but after returning to the main function, the main function cleans up through addl 8. On the contrary, we found that after stdcall, there will be a ret 8. Ret 8 is to clean up the stack, so it cleans the stack first and then returns to the main function. It is amazing, right?
4. View the compilation of fastcall
As for why we took this one out to view it separately, we thought that in order to better show the difference, we need to change the C language function.
void __attribute__ ((stdcall)) function(int a,int b,int c,int d) {
}
void main() {
function(1,2,3,4);
}
Notice that we have four functions, and then we first have the assembly language of 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
Then the assembly code of 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
result
So what is the difference? The main difference is in 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
You will find that the biggest difference is that in stdcall, pushl is pushed to the stack, while in fastcall, the last two are pushed to the stack first, and the last two are directly placed in the register if they are less than 32 bits. We all know that our calculations are performed using registers. Fastcall transfers parameters directly to registers, so the calculation speed will be faster. And you can also find that during the final cleaning, fastcall only cleaned 8, while stdcall cleaned 16 (ret 8 and ret 16). This is also because fastcall has two parameters in the register, so there are two less to clean.
5. Some existing problems
Regarding function name modification, I checked a lot of information and talked about the problem of function name modification, but from my personal operation, I did not find that the function name changes according to the changes in calling conventions.
I first thought that this situation might exist in C++, and then I found that in C language, the function name is always function, and in C++, the function name will become _Z8functioniiii, but it still did not appear according to the change of calling convention. changing circumstances. Maybe it's because I will be different with different compilers. I hope someone with relevant knowledge can answer it. Thank you very much.