Go: ABI
1 · Go ABIInternal: application binary interface#
internal-abi.md https://golang.org/design/40724-register-calling
Go’s ABI defines the layout of data in memory and the conventions for calling between Go functions. 定义数据在内存中的布局,函数之间的调用规约。
Go uses a common ABI design across all architectures. We first describe the common ABI, and then cover per-architecture specifics.
跨架构通用ABI设计,然后描述每个架构的特定性。
NOTE: ABIInternal 与 ABI0 可通过透明 ABI 包装器互相调用。ABI0 是面向汇编的稳定 ABI。
1.1 · 内存布局 Memory layout#
Size (sizeof) - 占用内存大小 表示一个类型在内存中实际占用的字节数。
Align (alignof) - 对齐要求 表示该类型的值在内存中必须从哪个地址边界(align的倍数)开始存放。
Offset (offset) - 字节偏移量 表示某个字段在结构体/序列中,相对于起始地址的字节位置。
三者关系总结:offset 是 size 和 align 共同作用的结果。
| 概念 | 含义 | 问题 |
|---|---|---|
| size | 占用多少字节 | 这个数据多大? |
| align | 起始地址必须是几的倍数 | 这个数据从哪里开始放? |
| offset | 相对于结构体起始的字节位置 | 这个数据具体在哪个相对位置? |
unsafe.Sizeof()
unsafe.Offsetof()
unsafe.Alignof()
1.1.1 · 为什么需要对齐?
CPU 访问对齐的内存地址效率更高。例如:
- 64 位 CPU 一次可以读取 8 字节
- 如果数据从 8 的倍数地址开始,一次读取就能拿到
- 如果不对齐,可能需要两次内存访问,还要拼接结果
NOTE: align 只需要满足数据的自然边界(4 字节数据对齐到 4 的倍数),而不是对齐到 CPU 的最大位宽。
定义:
alignof(S) = 1 if N = 0 = max(alignof(t_i) | 1 <= i <= N)
翻译:
- 空序列(没有字段) align为 1,可以理解为不需要对齐,从任意位置放
- 有字段 取所有字段中最大的 align
1.1.2 · 字节偏移量
定义:
offset(S, i) = 0 if i = 1 = align(offset(S, i-1) + sizeof(t_(i-1)), alignof(t_i))
翻译:
- 第 1 个字段的 offset = 0(从起始位置开始)
- 第 i 个字段的 offset = 上一个字段的位置 + 上一个字段的大小,然后对齐到当前字段要求的边界
1.1.3 · 字节占用量
定义:
sizeof(S) = 0 if N = 0 = align(offset(S, N) + sizeof(t_N), alignof(S))
翻译:
- 空序列(没有字段) size为 1
- 有字段 最后一个字段的结束位置,向上对齐到整个序列的 align
1.2 · 函数调用规约 Function call argument and result passing#
Function calls pass arguments and results using a combination of the stack and machine registers.
函数调用使用栈(内存)和机器寄存器的组合来传递参数和返回值。
访问寄存器通常比栈(内存)要快。参数和返回值优先通过寄存器传递。 但是,任何参数或者返回值超过可用寄存器的大小,都会被传递到栈上。
每一架构都定义了一系列的整数寄存器和浮点寄存器;
参数和返回值都递归分解为基础类型,再分配到寄存器中。
参数和返回值可以共享相同的寄存器,但不能共享相同的栈空间。 除了在栈上传递的参数和返回值外,函数调用者caller还为所有基于寄存器的参数保留在栈上的溢出空间(但不填充此空间 callee可能需要保存这些寄存器值)。
1.2.1 · 分配算法
核心流程:
- 初始化:设置整数寄存器索引 I=0,浮点寄存器索引 FP=0,栈序列 S 为空
- 分配参数:为接收器、参数逐个分配
- 分配结果:重置寄存器索引I/FP,为返回值分配
- 溢出空间:在栈S上为寄存器分配的参数预留空间(仅预留大小,不初始化值;caller分配, callee可用可不用)
NOTE: 分配优先级(尝试寄存器 → 失败降级到栈)
| 类型 | 分配方案 |
|---|---|
| 布尔/单字整数 | I 寄存器 |
| 双字整数 | I, I+1 寄存器 |
| 浮点 | FP 寄存器 |
| 复数 | 递归分配实部和虚部 |
| 指针/map/channel/函数 | I 寄存器 |
| 字符串/interface/slice | 递归分配内部组件 |
| 结构体 | 递归分配各字段 |
| 长度1数组 | 递归分配元素 |
| 长度0或>1数组 | 无/失败 |
最终栈序列看起来像:栈分配的接收器,栈分配的参数,指针对齐,栈分配的返回值,指针对齐,所有分配到寄存器的值的保留溢出空间,指针对齐(低->高)。
+------------------------------+
| . . . |
| 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
- 调用者在栈低地址预留空间(超过寄存器数量或容量的参数)给被调用函数的栈帧
- 参数通过寄存器和栈传递
- 被调用函数返回前必须在指定寄存器/栈位置存储结果
- 没有被调用者保护寄存器(Callee-save)——调用其他函数会覆盖任何非固定寄存器
- 调用时某些区域(溢出空间、结果字段)未初始化,由被调用者负责初始化
1.2.2 · example#
https://github.com/golang/go/blob/master/src/cmd/compile/abi-internal.md#example
1.3 · 闭包
闭包的内存结构:函数值是指向闭包对象的指针
闭包对象包含:
- 指针大小的程序计数器(函数入口点)
- 捕获的环境变量(零个或多个字节
闭包调用机制:
- 遵循静态函数/方法调用约定
- 额外操作:调用前,将闭包对象地址存储到相应架构的闭包上下文指针寄存器中
NOTE: 本质上,闭包通过在调用时传递上下文指针(含环境信息)的方式来访问捕获的变量。
1.4 · 架构特定
https://github.com/golang/go/blob/master/src/cmd/compile/abi-internal.md#architecture-specifics