Calling conventions of the C language

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.

Insert image description here

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.

  1. The order in which the parameters of the function are passed and how it is called.
  2. 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.
  3. 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.

Guess you like

Origin blog.csdn.net/qq_52380836/article/details/127755201