In programming languages, a function is a syntactic construct used to encapsulate reusable code. Functions can receive data from the external calling environment, and use these data to construct independent functional logic units in the form of compound statements in the function body. With the help of functions, we can split the implementation of a program into substeps and build the program in a structured way. This approach reduces repetitive code in the program and improves overall code readability and traceability through abstraction and replacement.
The calling convention specifies a series of issues that need to be paid attention to when a function is called, such as: how to pass arguments to the called function, how to return the return value from the called function, how to manage registers, and how to manage stack memory, etc. The calling convention is not part of the C language standard, so in fact each compiler can use its own unique calling convention to implement the calling process of C functions. But correspondingly, this also leads to another problem: when a function with external linkage is used in multiple different compilation units, and the source files corresponding to these different compilation units are compiled by different compilers, then they each generate The object files may no longer be brought together and produce the final executable.
Fortunately for C, compilers running on the x86-64 platform basically choose to use several common de facto calling conventions, depending on the operating system. For Windows, for example, the compiler uses the proprietary Microsoft x64 or Vector calling convention. On Unix and Unix-like systems, a calling convention called the System V AMD64 ABI (hereafter “SysV”) is used.
Next, let’s see what important implementation details are specified in the SysV calling convention. In order to observe these contents more intuitively, let us first write a simple C code, and use the default optimization level on the x86-64 platform to generate its corresponding assembly code through GCC compilation. Specifically as shown in the figure below:
In fact, in x86-64 machine instructions, function calls are done through the
call instruction. After each function body is executed, it needs to exit the execution of the function through the
ret instruction, and transfer the code execution flow to the next instruction of the previous function call instruction. You can intuitively feel this process through the following picture. Among them, the arrows mark the overall execution order of the code.
Next, let’s take a look at what the
SysV calling convention specifies when a function is called.
The first rule of the
SysV calling convention is: when calling a function, for the actual parameters of integer and pointer types, the registers
rdi, rsi, rdx, rcx, r8, and r9 need to be used respectively, and the parameters are defined from left to right when the function is defined. value is passed in order. And if a function receives more than 6 parameters, the remaining parameters will be transferred through the stack memory. At this time, the extra parameters will be pushed into the stack one by one in the order
from right to left (RTL). You can verify this by looking at the assembly code in the red box on lines 30 to 40 to the right of the figure (example code for a C function call).
Here, before the function foo is called, the registers
edi and esi are used to store the values of the local variables x and y, respectively, and the registers
edx, ecx, r8d, and r9d are used to store the literal values 3, 4, 5, and 6. The other two literal value parameters 7 and 8 are placed in the stack memory directly through the push instruction. You need to pay attention to the order in which this instruction operates on them, because the parameters are guaranteed to be placed on the stack in right-to-left order. Also, since x and y are local variables, they are initially stored in stack memory.
In addition, for floating point parameters, the compiler will use additional
xmm7, a total of 8 registers for storage. For wider values, ymm and zmm registers may also be used instead of xmm registers. The
xmm, ymm, and zmm registers mentioned above are all used by the extended instruction set called
AVX (Advanced Vector Extensions) in the x86 instruction set architecture. These instruction sets are generally dedicated to floating-point calculations and SIMD-related processing.
For the return value generated by the function call, the
SysV calling convention also has corresponding rules: when the function call generates a return value of integer type and is less than or equal to 64 bits, it is passed through the register
rax; when it is greater than 64 bits, less than or equal to 128 bits, Then use the registers rax and rdx to store the low 64 bits and high 64 bits of the return value respectively. You can refer to the code in the blue box on the right side of the figure (C function call sample code) 4, 21, 47 to verify this rule. These three lines of code deal with the return value of the functions bar, foo, and main respectively. It should be noted that for the return value of a composite type (such as a structure), the compiler may directly use the stack memory for “transit”.
For the return value of floating point type, similar to parameter passing, the compiler will use the xmm0 and xmm1 registers for storage by default. When the return value is too large,
ymm and zmm are selectively used instead of the
SysV calling convention also stipulates the use of registers: for registers
rbx, rbp, rsp, and r12 to r15, if the called function needs to use them, the function needs to temporarily store the values in these registers before using them. and restore their values (callee-saved) before the function exits. For other registers, their values are saved and restored according to the needs of the caller (caller-saved).
Each function needs to clean up the stack by itself before the end of the call. For example, in the code shown in the figure (C function call sample code), when the foo function is called, it allocates the corresponding space in the stack memory to store the value of the local variable n. After the function is executed and ready to exit, it needs to clean up the data allocated on the stack by itself. And this task can be done by the leave instruction.
In addition, the cleanup of the arguments passed in before the foo function is called is done by the calling function, which is the main function here. It can be seen that when the foo function call ends and the program execution flow returns to the next instruction of the previous call instruction, the program modifies the value of the rsp register through the add instruction. In this way, the main function cleans up the arguments passed to the function foo that were previously placed on the stack.
In addition, the
SysV calling convention has the following provisions:
- Before the function is called by the call instruction, it needs to ensure that the top of the stack is aligned with 16 bytes, that is, the address value (in bytes) of the top of the stack is a multiple of 16;
- Reserve 128 bytes from the top of the stack as the “Red Zone”; the ed Zone is a fixed-length memory segment located on the top of the stack (low address direction), which can usually be called by the “leaf” function in the function stack. (i.e. functions that no longer call other functions) use. In this way, when additional stack memory is required, the process of adjusting the size of the stack memory first can be omitted under certain conditions.
- Different from the calling process of the user function, the system call function needs to use the registers
rdi, rsi, rdx, r10, r8, r9to pass parameters.