Golang的学习(2)——函数调用约定

w1n9 Lv1

函数调用约定(Calling Convention)定义了函数调用时参数的传递方式、堆栈的清理责任以及寄存器的使用规则。

C/C++函数调用约定

顺便先来复习一下C/C++的函数调用约定

_cdecl (C declaration)

C的默认调用方式,通过栈传递参数,参数从右往左依次入栈,由caller清理栈

_stdcall(StandardCall)

又叫做Pascal调用,C++的标准调用,和前者主要差别就是他是由callee来清理栈

fastcall

快速调用,用寄存器传递参数(前两个参数通过ecx和edx传入,后续参数依旧从右至入栈,由callee清理栈。

windows x64下默认为用四个寄存器,整数参数在寄存器 RCX、RDX、R8 和 R9 中传递。 浮点数参数在 XMM0L、XMM1L、XMM2L 和 XMM3L 中传递。由caller来清栈;Linux/Unix 64位则用6个寄存器(RDI, RSI, RDX, RCX, R8, R9),同样由caller清栈

Golang函数调用约定

下面主要是说的Go1.17版本之后的

参数传递

参数的传递上Golang的调用也是基于快速调用的原理,都是先通过寄存器传参,不够了再通过栈传递。不同的是寄存器的数量使用,函数的第 1~9 个参数依次使用 AX、BX、CX、DI、SI、R8、R9、R10、R11 寄存器。从第 10 个参数开始,使用栈传递,并且是从栈顶向栈低依次排列。执行顺序上先入栈,然后再对寄存器赋值。

下面来看一下同样一段实现加法的函数,go和c/c++在函数调用栈上有什么差别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// go
package main

import "fmt"

func ADD(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12 int) int {
return a1 + a2 + a3 + a4 + a5 + a6 + a7 + a8 + a9 + a10 + a11 + a12
}

func main() {
var i = ADD(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12)
fmt.Printf("%d\n", i)
}

//c/c++
#include <stdio.h>

int add(int a1, int a2,int a3,int a4,int a5,int a6,int a7,int a8,int a9,int a10,int a11,int a12){
return a1 + a2 + a3 + a4 + a5 + a6 + a7 + a8 + a9 + a10 + a11 + a12;
}

int main(){
int i = add(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12);
printf("%d\n", i);
}

懒得画图了,直接看他们的反汇编实现(这里go的反汇编我是通过dlv源代码级调试的,直接编译成二进制文件的话,go的编译器有函数内联和常数传播机制,会把我的这段代码直接优化掉,后面再来详细学习了解一下)

image-20250801195938156
image-20250801195938156

这里c我就直接ida里看了

image-20250801200223989
image-20250801200223989

可以看到同样是12个参数,go采用了九个寄存器,之后的参数通过压栈传递,并且他是通过定位sp的偏移来寻址变量,这一点也与c通过bp寻址也有所不同

栈空间管理

c/c++的函数栈是固定大小的,所以会存在栈溢出

go的栈空间是动态可扩的,主要是在函数入口前插入一段栈检查代码,调用runtime中的morestack函数来检查当前的栈帧是否超出了分配给 Goroutine 的栈空间,如果栈空间足够,runtime.morestack_noctxt会立即返回,执行流继续往下走,反之则会进行栈扩容操作,分配一个更大的栈,并将当前栈上的数据复制过去,然后返回。

image-20250801210106318
image-20250801210106318

下面是morestack中栈扩容的核心newstack函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func newstack() {
oldsize := gp.stack.hi - gp.stack.lo

// 两倍于原来大小
newsize := oldsize * 2

// 需要的栈太大,直接溢出
if newsize > maxstacksize {
throw( "stack overflow" )
}

// goroutine必须是正在执行过程中才会调用newstack
// 所以这个状态一定是Grunning或者Gscanrunning
casgstatus(gp, _Grunning, _Gcopystack)

// gp的处于Gcopystack状态,当我们对栈进行复制时并发GC不会扫描此栈
// 栈的复制
copystack(gp, newsize)
casgstatus(gp, _Gcopystack, _Grunning)

// 继续执行
gogo(&gp.sched)
}

多返回值

go语言是支持多返回值的,主要原因是他将返回值保存在参数列表,然后和参数同样的待遇,优先通过寄存器传递值,不够了便通过压栈传递

image-20250801221309663
image-20250801221309663

  • 标题: Golang的学习(2)——函数调用约定
  • 作者: w1n9
  • 创建于 : 2025-07-25 16:05:56
  • 更新于 : 2026-01-14 19:48:44
  • 链接: https://vv1n9.github.io/2025/07/25/Golang八股学习(2)——函数调用约定/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论