Go internal ABI specification
2021-04-19
最近在研究 ebpf trace golang 程序,在 ebpf 读取 golang function 的函数如参和返回值时,遇到一些关于 golang ABI 相关的问题, 所以有必要研读下 golang 的 ABI specification,这篇文章完全是 Go internal ABI specification 的一个翻译,大家可以自行查看原文,本文只是一个 ebpf 研究过程中帮助作者理解的一个过程文档记录。
Memory layout
built-in types
| Type | 64-bit | 32-bit | ||
|---|---|---|---|---|
| Size | Align | Size | Align | |
| bool, uint8, int8 | 1 | 1 | 1 | 1 |
| uint16, int16 | 2 | 2 | 2 | 2 |
| uint32, int32 | 4 | 4 | 4 | 4 |
| uint64, int64 | 8 | 8 | 8 | 4 |
| int, uint | 8 | 8 | 4 | 4 |
| float32 | 4 | 4 | 4 | 4 |
| float64 | 8 | 8 | 8 | 4 |
| complex64 | 8 | 4 | 8 | 4 |
| complex128 | 16 | 8 | 16 | 4 |
| uintptr, *T, unsafe.Pointer | 8 | 8 | 4 | 4 |
byte和rune分别是uint8和int32的别名map,chan, 和func等同于*T类型
composite types
我们做如下定义,S 是由 N 个属性组成了,N 个属性类型分别为 t1,t2,…,tn。
offset(S, i) = 0 if i = 1
= align(offset(S, i-1) + sizeof(t_(i-1)), alignof(t_i))
alignof(S) = 1 if N = 0
= max(alignof(t_i) | 1 <= i <= N)
sizeof(S) = 0 if N = 0
= align(offset(S, N) + sizeof(t_N), alignof(S))
[N]T是由N个T类型的属性组成.- 切片
[]T是由一个*[cap]T的指针, 一个int类型存储长度, 和一个int类型存储容量,总共 3 个属性组成. string类型是由一个*[len]byte指针, 一个int类型存储长度.- 一个 struct
{ f1 t1; ...; fM tM }是由t1, …,tM,tP顺序排列组成, 其中tP:- Type byte if
sizeof(tM) = 0and any ofsizeof(ti) ≠ 0. - 否则为空 (size 0 and align 1) .
- Type byte if
Function call argument and result passing
golang 函数之间的调用传递参数和结果是通过使用 stack 和 registers 的联合方式。
每个参数或者结果都可以完全使用 stack 或者 registers 的方式。
由于使用 registers 的方式比 stack 的方式性能更优,所有会优先使用 registers 的方式。
但是参数或者结果包含 non-trivial array 或者不适合存储在寄存器中,那么就只能通过 stack 的方式。
每种架构下都会定义一系列的 integer 和 floating-point 类型的寄存器。
总体上将,参数和结果集合会将负载类型拆解为基本类型,然后分配给上述两种寄存器种。
参数和结果集合可以共享寄存器,但是不能共享栈空间。
除了栈上传递参数和结果集外,caller 会为寄存器参数保留栈空间在溢出空间,但是不会初始化这个空间。
函数调用者,参数,结构集 和 函数 F 在栈,寄存器上的分配遵循下面的算法:
- 令
NI和NFP分别为integer和floating-point寄存器的总数,令I和FP为 0,它们表明下个integer和floating-pointer寄存器的下标, 令S是这个类型的栈贞,为空。 - 如果
F是函数方法,分配F的调用者。 - 分配
F的每一个参数A - 添加指针对齐的属性到
S。该字段大小为 0,对其方式为uintptr类型。 - 设置
I和FP从 0 开始。 - 分配函数
F的每一个结果R - 添加指针对齐的属性到
S。 - 对于函数
F的每一个寄存器分配的类型参数,令T类型添加到栈空间S中。并且这个参数属于栈溢出区,不会被调用者初始化。 - 添加指针对齐的属性到
S。
分配调用者,参数,或者结果集中的 T 类型结果 V,遵循下述流程:
- 记住
I和FP下标。 - 尝试在寄存器中分配
V。 - 如果第二部失败,设置
I和FP的值到第一步中的只,在栈空间添加T,并且分配V到栈空间的这个属性上。
寄存器上分配 V 遵循下述流程:
- 如果
T是一个boolean或者integral类型,适合integer寄存器,分配V到寄存器I并且I的下标增加 1。 - 如果
T是适合两个integer寄存器,那么分配V的最高/最低 有效位分配给I和I+1,并且I增加 2。 - 如果
T是适合floating-point类型的寄存器,分配V到寄存器FP并且FP的下标增加 1。 - 如果
T是complex类型,那么分别分配复数的实数和虚数部分 - 如果
T是pointermapchannelfunction类型,分配 V 到寄存器 I 并且 I 的下标增加 1。 - 如果
T是stringinterfaceslice类型,递归调用V的各个部分(strings和interfaces有两个属性组成,slices是 3 )。 - 如果
T是struct类型,递归分配V的各个属性。 - 如果
T是数组类型,并且长度为 0, 不需要做任何事情。 - 如果
T是数组类型,并且长度为 1, 递归分配该元素。 - 如果
T是数组类型,并且长度大于 1,分配失败。 - 如果
I>NI或者FP>NFP,分配失败。 - 如果任何上诉递归分配失败,则失败。
通过上面的算法分配好的 参数,结果集,在栈的最终结果大体上如下:
- stack-assigned receiver
- stack-assigned arguments
- pointer-alignment
- stack-assigned results
- pointer-alignment
- spill space for each register-assigned argument
- pointer-alignment
入下图所示,0 地址在最下端:
+------------------------------+
| . . . |
| 2nd reg argument spill space |
| 1st reg argument spill space |
| <pointer-sized alignment> |
| . . . |
| 2nd stack-assigned result |
| 1st stack-assigned result |
| <pointer-sized alignment> |
| . . . |
| 2nd stack-assigned argument |
| 1st stack-assigned argument |
| stack-assigned receiver |
+------------------------------+ ↓ lower addresses
Example
假设在 64-bit architecture 架构上存在 R0–R9 寄存器,并且函数签名如下:
func f(a1 uint8, a2 [2]uintptr, a3 uint8) (r1 struct { x uintptr; y [2]uintptr }, r2 string)
在函数进入时, a1 分配到 R0, a3 分配到 R1 上,其他参数在栈上内存布局如下:
a2 [2]uintptr
r1.x uintptr
r1.y [2]uintptr
a1Spill uint8
a2Spill uint8
_ [6]uint8 // alignment padding
在栈上只有 a2 会在函数进入时初始化,剩下到栈都不会初始化。
在函数退出时,r2.base 会分配到 R0, r2.len 分配到 R1, r1.x 和 r1.y 是在栈上初始化.
在上面到列子中,我们需要注意以下几点:
a2和r1在栈上分配时由于包含了数组,其他参数分配在寄存器上.r2被分解为 2 个单独的属性,并且在寄存器上分配。
在栈上,基于栈分配的参数时出现在基于栈分配的返回值的低地址空间。