关于函数如何调用的注意事项
这是一篇关于编译器如何实现函数调用以将参数传递给被调用函数并从中获取返回值的说明。
背景
本说明并非关于调用函数的语法,而是关于编译器如何实现函数调用以将参数传递给被调用函数并从中获取返回值的。本说明中的示例是在 Linux Mint 17.3 Cinnamon 64位计算机上编写的。我使用 GNU GCC 和 GAS 来构建 C 和汇编代码。我使用 Eclipse for C/C++ Developers 作为我的 IDE。如果您想运行附加代码,可以参考 此链接 来设置您的环境。由于本说明中的汇编代码是 64 位的,因此您需要一台 64 位机器来运行它。我只在 Linux 上运行过代码。它可能在其他操作系统上运行良好,但我不能保证。
用 C 函数热身
这是一个热身环节。我们将回顾在高级语言中如何调用函数。
附带的 1095241/a-simple-c-function-call.zip 是一个 C 程序。a-simple-c-function-call.c 的实现如下:
#include <stdio.h>
// The function for addition
int add(int i, int j) {
return i + j;
}
int main() {
// Make a call to the add function
int result = add(100, 200);
printf("Expected Result = 300\n");
printf("The actual Result = %d\n", result);
return 0;
}
add()
函数接受两个整数参数并返回一个整数值;main()
函数调用add()
函数并打印结果。
运行 C 程序,我们可以看到以下结果:
本说明旨在回答以下问题:
- 调用函数将参数放在哪里,以便被调用函数可以获取它们?
- 被调用函数将返回值放在哪里,以便调用函数在函数返回时可以获取它?
尽管本说明是使用 64 位 Linux 机器和 C 语言编写的,但我希望它能对其他高级语言有所参考价值。
X64 架构背景知识
为了回答这两个问题,我们需要了解一些计算机架构的知识。我为本说明编写的汇编语言程序是针对 X64 的。
与本说明最相关的重要部分如下:
- 通用寄存器 RAX - R15 可用于多种用途,但主要可用于存储整数值(
int
、long
等)和进行整数计算; - XMM 寄存器 XMM0 - XMM15 用于存储浮点值(
float
、double
等)和进行浮点计算; - RSP 寄存器有特殊用途,它指向栈顶。
我不会深入讨论 X64 架构的细节,也不会讨论汇编语言编程。如果您有兴趣进一步探索,可以查看以下链接:
- http://cs.lmu.edu/~ray/notes/gasexamples/
- https://software.intel.com/en-us/articles/introduction-to-x64-assembly
- http://www.popoloski.com/posts/sse_move_instructions/
函数调用是如何实现的
高级语言函数在汇编层面是如何调用的,一直是一个有趣的主题。不幸的是,我所有的汇编经验都来自于一些廉价的早期 CPU。在这些 CPU 上实现函数调用会非常繁琐。幸运的是,我找到了链接 http://cs.lmu.edu/~ray/notes/gasexamples/。它向我展示了在 X86/X64 计算机上,函数调用实际上相当简单。
附带的示例程序有 8 个程序文件:
- how-function-calls-are-made.c 是用 C 编写的程序的入口点;
- 7 个汇编文件(.s)as1_*.s - as7_*.s 各演示了函数调用实现的一个方面。
以下是 how-function-calls-are-made.c 文件:
#include <stdio.h>
int return_an_integer();
double return_a_double();
int first_6_int_parameters(int i1, int i2, int i3, int i4,
int i5, int i6);
int the_7th_int_parameter(int i1, int i2, int i3, int i4,
int i5, int i6, int i7);
double first_8_dbl_parameters(double d1, double d2, double d3, double d4,
double d5, double d6, double d7, double d8);
double the_9th_dbl_parameter(double d1, double d2, double d3, double d4,
double d5, double d6, double d7, double d8, double d9);
void pass_a_pointer(char* s);
int main() {
// Get the return value - Integer and double
// 1. Get an integer return value
printf("Calling return_an_integer() => %d\n", return_an_integer());
// 2. Get an double return value
printf("Calling return_a_double() => %.3f\n", return_a_double());
// Pass integer parameters into functions
// 1. Pass the first 6 integer parameters to a function
int result = first_6_int_parameters(1, 2, 1, 1, 2, 1);
printf("Calling first_6_int_parameters() => %d\n", result);
// 2. Pass more than 6 integer parameters to a function
result = the_7th_int_parameter(1, 2, 1, 1, 2, 1, 10);
printf("Calling the_7th_int_parameter() => %d\n", result);
// Pass double parameters into functions
// 1. Pass the first 8 double parameters to a function
double dResult = first_8_dbl_parameters(0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1);
printf("Calling first_8_dbl_parameters() => %.1f\n", dResult);
// 2. Pass more than 8 parameters to a function
dResult = the_9th_dbl_parameter(0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 10.0);
printf("Calling the_9th_dbl_parameter() => %.1f\n", dResult);
// Pass a pointer
char s[13];
pass_a_pointer(s);
printf("Calling pass_a_pointer() => %s\n", s);
return 0;
}
makefile 可用于编译/清理/运行程序。
CC=gcc
SRC=src
BUILD=Default
all: $(BUILD)/how-function-calls-are-made
$(BUILD)/how-function-calls-are-made: $(BUILD)/main.o \
$(BUILD)/as1_return_an_integer.o \
$(BUILD)/as2_return_a_double.o \
$(BUILD)/as3_first_6_int_parameters.o \
$(BUILD)/as4_the_7th_int_parameter.o \
$(BUILD)/as5_first_8_dbl_parameters.o \
$(BUILD)/as6_the_9th_dbl_parameter.o \
$(BUILD)/as7_pass_a_pointer.o
$(CC) -o $(BUILD)/how-function-calls-are-made $(BUILD)/*.o
$(BUILD)/main.o: $(SRC)/how-function-calls-are-made.c
$(CC) -c -g0 -o $(BUILD)/main.o $(SRC)/how-function-calls-are-made.c
$(BUILD)/as1_return_an_integer.o: $(SRC)/as1_return_an_integer.s
as -c -o $(BUILD)/as1_return_an_integer.o $(SRC)/as1_return_an_integer.s
$(BUILD)/as2_return_a_double.o: $(SRC)/as2_return_a_double.s
as -c -o $(BUILD)/as2_return_a_double.o $(SRC)/as2_return_a_double.s
$(BUILD)/as3_first_6_int_parameters.o: $(SRC)/as3_first_6_int_parameters.s
as -c -o $(BUILD)/as3_first_6_int_parameters.o $(SRC)/as3_first_6_int_parameters.s
$(BUILD)/as4_the_7th_int_parameter.o: $(SRC)/as4_the_7th_int_parameter.s
as -c -o $(BUILD)/as4_the_7th_int_parameter.o $(SRC)/as4_the_7th_int_parameter.s
$(BUILD)/as5_first_8_dbl_parameters.o: $(SRC)/as5_first_8_dbl_parameters.s
as -c -o $(BUILD)/as5_first_8_dbl_parameters.o $(SRC)/as5_first_8_dbl_parameters.s
$(BUILD)/as6_the_9th_dbl_parameter.o: $(SRC)/as6_the_9th_dbl_parameter.s
as -c -o $(BUILD)/as6_the_9th_dbl_parameter.o $(SRC)/as6_the_9th_dbl_parameter.s
$(BUILD)/as7_pass_a_pointer.o: $(SRC)/as7_pass_a_pointer.s
as -c -o $(BUILD)/as7_pass_a_pointer.o $(SRC)/as7_pass_a_pointer.s
run: $(BUILD)/how-function-calls-are-made
$(BUILD)/how-function-calls-are-made
clean:
find $(BUILD) -type f -delete
1. 如何从函数返回值?
X64 计算机上的 GNU C 实现非常简单。我们可以将返回值放在指定的寄存器中。让我们看看 as1_return_an_integer.s 和 as2_return_a_double.s 文件。
.globl return_an_integer
.text
return_an_integer:
mov $2106, %rax
# The return value is in %rax
ret
.globl return_a_double
.data
v1: .double 2016.422
return_a_double:
movsd v1, %xmm0
# The return value is in %xmm0
ret
- 要返回一个
integer
/long
值,我们可以将该值放入RAX
寄存器。GNU C 中的调用函数知道从该寄存器中获取返回值; - 要返回一个
float
/double
值,我们可以将该值放入XMM0
寄存器。GNU C 中的调用函数知道从该寄存器中获取返回值。
2. 如何将参数传递给被调用函数?
让我们先看看在文件 as3_first_6_int_parameters.s 和 as4_the_7th_int_parameter.s 中,整数/长整型值是如何传递给函数的。
.globl first_6_int_parameters
first_6_int_parameters:
mov $0, %rax
# First 6 integers - left to right
# rdi, rsi, rdx, rcx, r8, r9
add %rdi, %rax
add %rsi, %rax
add %rdx, %rax
add %rcx, %rax
add %r8, %rax
add %r9, %rax
# The return value is in %rax
ret
.globl the_7th_int_parameter
the_7th_int_parameter:
mov $0, %rax
# First 6 integers
add %rdi, %rax
add %rsi, %rax
add %rdx, %rax
add %rcx, %rax
add %r8, %rax
add %r9, %rax
# The 7th and above integer parameters
# are pushed to the stack by the caller
# and it is the caller's responsibility to pop them
# The order of the parameters is right to left
add 8(%rsp), %rax
# The return value is in %rax
ret
- 在 GNU C 中,前 6 个整数/长整型参数(按函数声明中的从左到右顺序)分别由调用者复制到
RDI
、RSI
、RDX
、RCX
、R8
和R9
寄存器中; - 如果有多于 6 个整数/长整型参数,则从第 7 个参数开始(按函数声明中的从右到左顺序),它们被压入栈。当调用函数时,返回地址被压入栈顶。当被调用函数检索第 7 个及以上的参数时,它需要为 8 个字节偏移。
对于 float
/double
参数,让我们看看 as5_first_8_dbl_parameters.s 和 as6_the_9th_dbl_parameter.s 文件。
.globl first_8_dbl_parameters
first_8_dbl_parameters:
# First 8 double - left to right
# xmm0, xmm1, xmm2, xmm3, xmm4, xmm5, xmm6, xmm7
addsd %xmm1, %xmm0
addsd %xmm2, %xmm0
addsd %xmm3, %xmm0
addsd %xmm4, %xmm0
addsd %xmm5, %xmm0
addsd %xmm6, %xmm0
addsd %xmm7, %xmm0
# The return value is in %xmm0
ret
.globl the_9th_dbl_parameter
the_9th_dbl_parameter:
# First 8 double
addsd %xmm1, %xmm0
addsd %xmm2, %xmm0
addsd %xmm3, %xmm0
addsd %xmm4, %xmm0
addsd %xmm5, %xmm0
addsd %xmm6, %xmm0
addsd %xmm7, %xmm0
# The 9th and above double parameters
# are pushed to the stack by the caller
# and it is the caller's responsibility to pop them
# The order of the parameters is right to left
addsd 8(%rsp), %xmm0
# The return value is in %xmm0
ret
- 在 GNU C 中,前 8 个
float
/double
参数(按函数声明中的从左到右顺序)分别由调用者复制到xmm0
、xmm1
、xmm2
、xmm3
、xmm4
、xmm5
、xmm6
和xmm7
寄存器中; - 与
integer
/long
情况类似,如果有多于 8 个float
/double
参数,则从第 9 个参数开始(按函数声明中的从右到左顺序),它们被压入栈。当被调用函数检索第 9 个及以上的参数时,它还需要为栈顶的返回地址偏移 8 个字节。
3. 如何处理指针?
在 C 和汇编语言中,指针是整数(X64 具有 64 位地址空间),整数值代表内存地址。让我们看看 as7_pass_a_pointer.s 文件。
.globl pass_a_pointer
pass_a_pointer:
# %rdi is the first parameter
mov %rdi, %rax
movb $72, (%rax)
movb $101, 1(%rax)
movb $108, 2(%rax)
movb $108, 3(%rax)
movb $111, 4(%rax)
movb $32, 5(%rax)
movb $87, 6(%rax)
movb $111, 7(%rax)
movb $114, 8(%rax)
movb $108, 9(%rax)
movb $100, 10(%rax)
movb $33, 11(%rax)
movb $0, 12(%rax)
ret
pass_a_pointer
函数接受一个指向数组的指针作为参数。由于这是唯一的参数,指针值由调用函数通过RDI
寄存器传递;- 一组 ASCII 字节(
Hello World!
)被写入从RDI
寄存器指向的内存地址开始的位置。
运行示例
由于汇编语言是 CPU 相关的,您需要一台 X64 Linux 计算机来运行程序。您可以通过以下命令获取 GNU C/C++/GAS:
sudo apt-get install build-essential
您可以发出以下命令来检查所需的编译器和汇编器是否安装成功。
- which gcc
- which as
- which make
如果它们安装成功,which
命令会告诉您安装目录。如果您有兴趣了解如何在 Eclipse 中运行程序,此链接 是关于使用 Eclipse 进行 C/C++ 开发的相当全面的说明。如果您只想运行程序,只需转到 makefile
所在的目录并运行以下命令:
make run
摘要
本说明应该已经回答了关于 GNU GCC 的以下问题:
- 如何从函数返回值?
integer
/long
值在RAX
寄存器中返回;float
/double
值在XMM0
寄存器中返回。
- 如何将参数传递给被调用函数?
- 前 6 个
integer
/long
参数(按函数声明中的从左到右顺序)分别复制到RDI
、RSI
、RDX
、RCX
、R8
和R9
寄存器中; - 如果有多于 6 个
integer
/long
参数,则从第 7 个参数开始(按函数声明中的从右到左顺序),它们被压入栈; - 前 8 个
float
/double
参数(按函数声明中的从左到右顺序)分别复制到xmm0
、xmm1
、xmm2
、xmm3
、xmm4
、xmm5
、xmm6
和xmm7
寄存器中; - 如果有多于 8 个
float
/double
参数,则从第 9 个参数开始(按函数声明中的从右到左顺序),它们被压入栈; - 当调用一个函数时,栈顶是返回地址。在从栈中检索参数时,在 X64 架构中我们需要偏移 8 个字节。
- 前 6 个
- 如何处理指针?
- 指针是一个整数,因此它与整数一样传递进出函数。X64 CPU 具有 64 位地址空间。
- 还有什么需要注意的吗?
- 根据我本说明的主要参考资料 http://cs.lmu.edu/~ray/notes/gasexamples/(非常感谢 ray),被调用函数需要保留
RBP
、RBX
、R12
、R13
、R14
、R15
寄存器。其他寄存器可以由被调用函数随意更改。
- 根据我本说明的主要参考资料 http://cs.lmu.edu/~ray/notes/gasexamples/(非常感谢 ray),被调用函数需要保留
需要注意的是,本说明中的所有观察结果仅适用于 GNU GCC。C 语言没有标准化的 ABI,因此其他编译器可能会以不同的方式实现函数调用。本说明应能帮助程序员理解在高级语言编程时“按值/按引用传递”的概念。
关注点
- 这是一篇关于在汇编语言层面如何调用函数的说明;
- 希望您喜欢我的帖子,希望这篇说明能以某种方式帮助您。
历史
- 2016 年 4 月 26 日:初始修订