Skip to content

Latest commit

 

History

History
432 lines (314 loc) · 28.6 KB

abi-internal.md

File metadata and controls

432 lines (314 loc) · 28.6 KB

Go内部ABI规范(翻译&注解)

原文

翻译

本文描述了Go的内部应用程序二进制接口(ABI),称为ABIInternal。Go的ABI定义了内存中数据的布局以及Go函数之间调用的约定。该ABI不稳定,会随着Go版本发生变化。如果您正在编写汇编代码,请参阅Go的汇编文档,该文档描述了Go的稳定ABI,称为 ABI0。

Go代码中定义的所有函数都遵循ABIInternal。但是ABIInternal和ABI0函数能够通过透明的ABI包装器相互调用,如内部调用约定提案中所述。

Go在所有架构中使用通用的ABI设计。 我们首先描述常见的ABI,然后介绍每个架构的细节。

基本原理:关于跨架构使用通用ABI而不是跨平台ABI背后的原因,请参阅基于寄存器的Go调用约定提案

内存布局

Go的内置类型具有以下大小(Size)和对齐方式(Align)。语言规范保证了许多类型的布局(尽管不是全部)。那些不能保证的可能会在Go的未来版本中发生变化(例如,我们考虑过在32位上更改int64的Align)。

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

byterune类型分别是uint8int32的别名,因此具有与这些类型相同的大小和对齐方式。

mapchanfunc类型的布局等价于*T。

为了描述其余复合类型的布局,我们首先定义具有N个字段的类型序列S的布局t1, t2, ..., tN。 我们定义每个字段相对于0的基地址,以及序列的Size和Align如下:

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))

其中sizeof(T)和alignof(T)分别是类型T的Size和Align,而align(x, y)是将x向上舍入为y的倍数。

interface{}类型是由 1. 一个指向代表接口动态类型的运行时类型描述的指针 2. 一个unsafe.Pointer数据字段 组成的序列。 任何其他接口类型(除了空接口)都是由 1. 一个指向给出方法指针和数据字段的类型的运行时“itab”指针 2. 一个unsafe.Pointer数据字段 组成的序列。 接口可以是“直接的(direct)”或“间接的(indirect)”,取决于其动态类型:direct接口将值直接存储在数据字段中,indirect接口存储指向数据字段值的指针。 只有当值由单个指针组成时,接口才能是direct的。

数组类型“[N]T”是由N个类型为T的字段组成的序列。

切片类型[]T是指向切片后备存储的*[cap]T指针序列,还有一个表示lenint字段和一个表示capint字段。

string类型是一个指向字符串后备存储的*[len]byte指针序列,以及一个表示lenint字段。

结构体类型struct { f1 t1; ...; fM tM }被布局为序列t1, ..., tM, tP,其中tP为以下其一:

  • 如果sizeof(tM) = 0并且sizeof(ti) ≠ 0,则为byte数据(指padding byte)。
  • 否则为空(Size为0,Align为1)。

填充字节(padding byte)通过获取最终的空fN字段的地址来防止创建超出终点的指针。

请注意,用户编写的汇编代码通常不应依赖于Go类型布局,而应使用go_asm.h中定义的常量。

函数调用参数和结果的传递

函数调用使用栈和寄存器组合传递参数和结果。 每个参数或结果要么完全在寄存器中传递,要么完全在栈中传递。 因为访问寄存器通常比访问栈快,所以参数和结果优先在寄存器中传递。 但是,任何包含非平凡数组或不完全适合剩余可用寄存器的参数或结果都会在栈上传递。

每个体系结构都定义了一个整数寄存器序列和一个浮点寄存器序列。 在高层次上,参数和结果被递归地分解为基本类型的值,并且将这些基本值分配给这些序列中的寄存器。

参数和结果可以共享相同的寄存器,但不共享相同的栈空间。 除了在栈上传递的参数和结果之外,caller还在栈上为所有基于寄存器的参数保留溢出空间(但不填充此空间)。1

函数或方法F的接收器、参数和结果使用以下算法分配给寄存器或栈:

  1. 令NI和NFP为架构定义的整数和浮点寄存器序列的长度。令I和FP为0,这些分别是下一个整数和浮点寄存器的索引。令定义栈帧的类型序列S为空。2
  2. 如果F是个方法,分配F的接收器。
  3. 遍历F的每个参数A,分配A。
  4. 添加一个指针对齐(pointer-alignment)字段到S中。它的Size为0,Align与uintptr相同。
  5. 重置I和FP为0。
  6. 遍历F的每个结果R,分配R。
  7. 添加一个指针对齐字段到S中。
  8. 遍历F的每个寄存器分配的(register-assigned)接收器和参数, 令T为它的类型并将T添加到栈序列S中。这是参数(或接收者)的溢出空间,在调用时是未初始化的。
  9. 添加一个指针对齐字段到S中。

分配一个底层类型为T的接收者、参数或结果V的工作方式如下:

  1. 记忆I和FP。
  2. 如果T的Size为0,添加T到栈序列S中然后返回。
  3. 尝试寄存器分配V。
  4. 如果第三步失败,重置I和FP为第一步的值,添加T到栈序列S中,并且分配V到S中的这个字段上。

底层类型为T的值V的寄存器分配工作方式如下:

  1. 如果T是适合整数寄存器的布尔或整数类型,则将V分配给寄存器I,并递增I。
  2. 如果T是适合两个整数寄存器的整数类型,则将V的最低有效部分和最高有效部分分别分配给寄存器I和I+1,并将I增加2。
  3. 如果T是浮点类型并且可以在浮点寄存器中不损失精度地表示,则将V分配给寄存器FP,并递增FP。
  4. 如果T是一个复数类型,则递归地寄存器分配它的实部和虚部。
  5. 如果T是指针类型、映射类型、通道类型或函数类型,则将V分配给寄存器I,并递增I。
  6. 如果T是字符串类型、接口类型或切片类型,则递归地寄存器分配V的组件(字符串和接口有2个,切片有3个)。
  7. 如果T是结构体类型,则递归地寄存器分配V的每个字段。
  8. 如果T是长度为0的数组类型,则什么也不做。
  9. 如果T是长度为1的数组类型,则递归地寄存器分配它的一个元素。
  10. 如果T是长度 > 1的数组类型,则失败。
  11. 如果I > NI或FP > NFP,则失败。
  12. 如果上述任何递归分配失败,则失败。

上述算法将每个接收器、参数和结果分配给寄存器或栈序列中的字段。 最终的栈序列如下所示:栈分配的(stack-assigned)接收器、栈分配的参数、指针对齐字段、栈分配的结果、指针对齐字段、每个寄存器分配的参数的溢出空间,指针对齐字段。 下图显示了这个栈帧在栈上的样子,使用地址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

为了执行函数调用,caller从其栈帧中的最低地址开始为调用栈帧保留空间,将参数存储在由上述算法确定的寄存器和参数栈字段中,并执行调用。 在调用时,溢出空间、结果栈字段和结果寄存器未初始化。 返回时,callee必须将结果存储到由上述算法确定的所有结果寄存器和结果栈字段中。

没有callee保存的(callee-save)寄存器,因此调用可能会覆盖任何没有固定含义的寄存器,包括参数寄存器。

例子

考虑具有假想的整数寄存器R0-R9的64位架构上的函数:
func f(a1 uint8, a2 [2]uintptr, a3 uint8) (r1 struct { x uintptr; y [2]uintptr }, r2 string)

进入时,a1分配给R0a3分配给R1,栈帧按以下顺序排列:3

    a2      [2]uintptr
    r1.x    uintptr
    r1.y    [2]uintptr
    a1Spill uint8
    a3Spill uint8
    _       [6]uint8  // alignment padding

在栈帧中,只有a2字段在进入时被初始化;帧的其余部分未初始化。

退出时,r2.base分配给R0r2.len分配给R1,并且r1.xr1.y在栈帧中被初始化。

在这个例子中有几件事需要注意。 首先,a2r1是栈分配的,因为它们包含数组。其他参数和结果是寄存器分配的。 结果r2被分解成它的组件,这些组件分别是寄存器分配的。 在栈上,栈分配的参数出现在低于栈分配结果的地址,栈分配的结果出现在低于参数溢出区域的地址。 只有参数,而不是结果,被分配到栈上的溢出区域。4

基本原理

每个基本值都分配给属于自己的寄存器以优化构造和访问。 另一种方法是将多个sub-word值打包到寄存器中5,或者简单地将参数的内存布局映射到寄存器(这在C ABI中很常见),但这通常会增加打包和解包这些值的成本。 现代架构有足够多的寄存器来以这种方式为几乎所有函数传递所有参数和结果(参见附录),因此在寄存器之间传播基本值几乎没有缺点。

不能完全分配给寄存器的参数将完全在栈上传递,以防callee获取该参数的地址。 如果一个参数可以在栈和寄存器中拆分,并且callee获取它的地址,则需要在内存中重建它,这个过程与参数的大小成正比。

非平凡的数组总是在栈上传递,因为索引数组通常需要计算出的偏移量,而这对于寄存器来说通常是不可能的。 数组通常在函数签名中很少见(Go 1.15标准库中只有0.7%,kubelet中有0.2%)。 我们考虑允许数组字段在栈上传递,同时参数的其余字段在寄存器中传递,除了callee获取参数的地址时会产生与其他大型结构体相同的问题外,会对< 0.1%的kubelet中的函数(即使很少)有益。

我们对0个元素和1个元素的数组进行了例外处理,因为它们不需要计算偏移量,并且1个元素的数组已经在编译器的SSA表示中分解。

如果架构寄存器为零,则上面的ABI分配算法等效于Go的基于栈的ABI0调用约定。 这旨在简化向基于寄存器的内部ABI的过渡,并使编译器可以轻松生成任一调用约定。 架构可能仍然定义了与ABI0不兼容的寄存器含义,但这些差异应该很容易在编译器中解释。

分配算法将零大小的值分配给栈(分配步骤2)以支持与ABI0的等效性。 虽然这些值本身不占用空间,但它们确实会导致ABI0中栈上的对齐填充。 如果没有这一步,即使在不提供参数寄存器的架构上,内部ABI也会寄存器分配零大小的值,因为它们不消耗任何寄存器,因此不会向栈添加对齐填充。

该算法为caller帧中的参数保留溢出空间,以便编译器可以生成溢出到该保留空间中的栈增长路径。 如果callee必须增加栈,它可能无法在自己的帧中保留足够的额外栈空间来溢出这些,这就是caller这样做很重要的原因。 如果出于任何其他原因需要溢出这些参数,这些插槽也可以作为主位置,这简化了traceback打印。

如何布置参数溢出空间有多种选择。 我们选择根据其类型通常的内存布局来布置每个参数,但将溢出空间与常规参数空间分开。 使用通常的内存布局可以简化编译器,因为它已经理解了这种布局。 此外,如果函数获取寄存器分配参数的地址,编译器必须在其通常的内存布局中将该参数溢出到内存中,并且为此目的使用参数溢出空间更方便。

或者溢出空间可以围绕参数寄存器构建。 在这种方法中,栈增长溢出路径会将每个参数寄存器溢出到一个寄存器大小的栈字(stack word)。 然而,如果函数获取寄存器分配参数的地址,编译器将不得不在栈上其他位置的内存布局中重建它。

溢出空间也可以与栈分配的参数交错,因此无论它们是寄存器分配还是栈分配,参数都会按顺序出现。 这将接近ABI0,除了寄存器分配的参数将在栈上未初始化并且无需为寄存器分配的结果保留栈空间之外。 由于内存局部性,我们希望分离溢出空间以便于更好地执行。 对于reflect调用,分隔空间也可能更简单,因为这允许reflect将溢出空间汇总为一个数字。 最后,长期意图是完全删除保留的溢出槽 ———— 允许在没有任何栈设置的情况下调用大多数函数,使引入callee-save的寄存器更容易 ———— 分离溢出空间使这种转换更容易。

闭包

func值(例如,var x func())是指向闭包对象的指针。 闭包对象以表示函数入口点的指针大小的PC(程序计数器)开始,后跟包含封闭环境的零个或多个字节。

闭包调用遵循与静态函数和方法调用相同的约定,但增加了一个。 每个体系结构都指定一个闭包上下文指针寄存器,并且对闭包的调用在调用之前将闭包对象的地址存储在闭包上下文指针寄存器中。

软件浮点模式

在“软浮点”模式下,ABI只是将硬件视为具有零浮点寄存器的设备。 因此,任何包含浮点值的参数都将在栈上传递。

基本原理:软浮点模式是关于兼容性而不是性能,并不常用。 因此在这种情况下,我们使ABI尽可能简单,而不是添加额外的规则来在整数寄存器中传递浮点值。

架构细节

本节描述了每个体系结构的寄存器映射,以及其他每个体系结构的特殊情况。

amd64架构

amd64架构对整数参数和结果使用以下9个寄存器序列:

RAX, RBX, RCX, RDI, RSI, R8, R9, R10, R11

它将X0–X14用于浮点参数和结果。

基本原理:这些序列是从可用的寄存器中选择的,比较容易记住。

寄存器R12和R13是永久暂存(permanent scratch)寄存器。 R15是一个暂存(scratch)寄存器,在动态链接的二进制文件中除外。

基本原理:栈增长和反射调用等一些操作需要专用的暂存寄存器,以便在不破坏参数或结果的情况下操作调用帧。

专用寄存器如下:

Register Call meaning Return meaning Body meaning
RSP Stack pointer Same Same
RBP Frame pointer Same Same
RDX Closure context pointer Scratch Scratch
R12 Scratch Scratch Scratch
R13 Scratch Scratch Scratch
R14 Current goroutine Same Same
R15 GOT reference temporary if dynlink Same Same
X15 Zero value (*) Same Scratch

(*)Plan9除外,其中X15是暂存寄存器,因为SSE寄存器不能在标记的处理程序中使用(因此编译器会避免使用它们,除非绝对必要)。

基本原理:这些寄存器含义与Go的基于栈的调用约定兼容,除了R14和X15,它们必须在从ABI0代码转换到ABIInternal代码时恢复。 在ABI0中,这些是未定义的,因此从ABIInternal到ABI0的转换可以忽略这些寄存器。

基本原理:对于当前的goroutine指针,我们选择了一个需要额外REX字节(REX前缀)的寄存器。 虽然这为每个函数序言(function prologue6)添加了一个字节,但它几乎不会在函数序言之外访问,我们希望提供更多的单字节寄存器以实现净赢。

基本原理:我们可以允许R14(当前的goroutine指针)作为函数体中的暂存寄存器,因为它总是可以从amd64上的TLS恢复。 但是为了简单起见,我们将其指定为固定寄存器,并与其他可能没有TLS中当前goroutine指针副本的架构保持一致。

基本原理:我们将X15指定为固定零寄存器,因为函数通常必须将其栈帧批量归零,而使用指定的零寄存器更高效。

实现说明:在调用时具有固定含义但不在函数体中的寄存器必须由“注入”调用初始化,例如基于信号的panics。

栈布局

堆栈指针RSP向下增长并始终对齐到8个字节。

amd64架构不使用链接寄存器。

一个函数的栈帧布局如下:

    +------------------------------+
    | return PC                    |
    | RBP on entry                 |
    | ... locals ...               |
    | ... outgoing arguments ...   |
    +------------------------------+ ↓ lower addresses

“return PC”作为标准amd64 CALL操作的一部分被推送。 进入时,函数减小RSP以开放其栈帧并将RBP的值保存在return PC的正下方。 不需要任何栈空间的末端函数可以省略保存的RBP。

Go ABI使用RBP作为帧指针寄存器与amd64平台约定兼容,因此Go可以与平台调试器和分析器互操作。

标志寄存器7

调用时,方向标志(DF)始终被清除(设置为“前进”方向)。 算术状态标志被视为暂存寄存器,并且不会在调用之间保留。 RFLAGS中的所有其他位都是系统标志。

在函数调用和返回时,CPU处于x87模式(不是MMX技术模式)。

基本原理:amd64不使用x87寄存器或MMX寄存器。因此我们遵循SysV平台约定以简化与C ABI之间的转换。

在调用时,MXCSR控制位总是如下设置:

Flag Bit Value Meaning
FZ 15 0 Do not flush to zero
RC 14/13 0 (RN) Round to nearest
PM 12 1 Precision masked
UM 11 1 Underflow masked
OM 10 1 Overflow masked
ZM 9 1 Divide-by-zero masked
DM 8 1 Denormal operations masked
IM 7 1 Invalid operations masked
DAZ 6 0 Do not zero de-normals

MXCSR状态位是callee保存的。

基本原理:拥有固定的MXCSR控制配置允许Go函数使用SSE操作,而无需修改或保存MXCSR。 允许函数在调用之间修改它(只要能恢复它),但在撰写本文时,Go代码从未这样做过。 上述固定配置与ELF AMD64 ABI指定的进程初始化控制位相匹配。

在amd64上Go不使用x87浮点控制字。

其它架构

请阅读原文。

未来发展方向

溢出路径改进

ABI目前为参数寄存器保留溢出空间,因此编译器可以在调用runtime.morestack增大栈之前静态生成参数的溢出路径。 这确保即使在栈几乎耗尽时也有足够的溢出空间,并保持栈增长和栈扫描的行为与ABI0基本不变。

然而,这会浪费栈空间(每次调用浪费了16字节,取中间值),从而导致栈更大,并且增加了缓存占用空间。 更好的方法是仅在溢出时保留栈空间。 确保有足够空间可供溢出的一种方法是为每个函数保留足够的空间用于函数自己的帧以及它调用的所有函数的溢出空间。 对于大多数函数,这将更改序言栈(prologue stack)增长检查的阈值。 对于nosplit函数,这将更改链接器的静态栈大小检查中使用的阈值。

在callee而不是caller中分配溢出空间的方式,可以使反射调用更快(在函数只接受寄存器参数的常见情况下),因为它可以不分配任何帧就能让反射直接进行这些调用。

静态生成溢出路径也会增加代码大小。 使用运行时通用溢出路径作为“morestack”的一部分来替代也是可能的。 然而,这使得保留溢出空间变得复杂,因为在大多数情况下,溢出所有可能的寄存器参数比仅溢出特定函数的占用更多的空间。 有一些选择是,溢出到临时空间并仅复制回函数使用的寄存器,或者在溢出到它之前按需增大栈(需要的话使用临时空间),或者如果栈空间不足则使用堆分配的空间。 这些选择都增加了足够的复杂性,我们必须根据静态溢出路径导致的实际代码大小的增长情况来做出这个决定。

重写集(Clobber sets)

正如定义的那样,ABI不使用callee保存寄存器。 这显着简化了垃圾收集器和编译器的寄存器分配器,但会带来一些性能成本。 对于Go代码来说可能更平衡的方案是使用重写集:对于每个函数,编译器记录它重写的的寄存器集合(包括那些被它调用的函数重写的寄存器),并且任何未被函数F重写的寄存器都可以在F的调用处保留下来。

这通常非常适合Go,因为Go语言的包DAG允许函数元数据(如重写集)沿调用图向上流动,甚至跨越包边界。 与一般的callee保存寄存器不同,重写集需要对垃圾收集器进行相对较少的更改。 与callee保存寄存器相比,重写集的一个缺点是它们对间接函数调用或接口方法调用没有帮助,因为在这些情况下静态信息不可用。

大聚集体

Go鼓励按值传递复合值,这简化了关于变化和竞争的推断。 但是对于大的复合值还是会带来性能开销。 可以改为通过引用来透明地传递大的复合值,并且延迟复制直到真正需要的时候才进行。

附录:寄存器使用分析

为了了解上述设计对寄存器使用的影响,我们分析上述ABI对大型代码库的影响:来自Kubernetesv1.18.8的cmd/kubelet。

下表显示了不同数量的可用整数和浮点寄存器对参数分配的影响:

|      |        |       |      stack args |          spills |     stack total |
| ints | floats | % fit | p50 | p95 | p99 | p50 | p95 | p99 | p50 | p95 | p99 |
|    0 |      0 |  6.3% |  32 | 152 | 256 |   0 |   0 |   0 |  32 | 152 | 256 |
|    0 |      8 |  6.4% |  32 | 152 | 256 |   0 |   0 |   0 |  32 | 152 | 256 |
|    1 |      8 | 21.3% |  24 | 144 | 248 |   8 |   8 |   8 |  32 | 152 | 256 |
|    2 |      8 | 38.9% |  16 | 128 | 224 |   8 |  16 |  16 |  24 | 136 | 240 |
|    3 |      8 | 57.0% |   0 | 120 | 224 |  16 |  24 |  24 |  24 | 136 | 240 |
|    4 |      8 | 73.0% |   0 | 120 | 216 |  16 |  32 |  32 |  24 | 136 | 232 |
|    5 |      8 | 83.3% |   0 | 112 | 216 |  16 |  40 |  40 |  24 | 136 | 232 |
|    6 |      8 | 87.5% |   0 | 112 | 208 |  16 |  48 |  48 |  24 | 136 | 232 |
|    7 |      8 | 89.8% |   0 | 112 | 208 |  16 |  48 |  56 |  24 | 136 | 232 |
|    8 |      8 | 91.3% |   0 | 112 | 200 |  16 |  56 |  64 |  24 | 136 | 232 |
|    9 |      8 | 92.1% |   0 | 112 | 192 |  16 |  56 |  72 |  24 | 136 | 232 |
|   10 |      8 | 92.6% |   0 | 104 | 192 |  16 |  56 |  72 |  24 | 136 | 232 |
|   11 |      8 | 93.1% |   0 | 104 | 184 |  16 |  56 |  80 |  24 | 128 | 232 |
|   12 |      8 | 93.4% |   0 | 104 | 176 |  16 |  56 |  88 |  24 | 128 | 232 |
|   13 |      8 | 94.0% |   0 |  88 | 176 |  16 |  56 |  96 |  24 | 128 | 232 |
|   14 |      8 | 94.4% |   0 |  80 | 152 |  16 |  64 | 104 |  24 | 128 | 232 |
|   15 |      8 | 94.6% |   0 |  80 | 152 |  16 |  64 | 112 |  24 | 128 | 232 |
|   16 |      8 | 94.9% |   0 |  16 | 152 |  16 |  64 | 112 |  24 | 128 | 232 |
|    ∞ |      8 | 99.8% |   0 |   0 |   0 |  24 | 112 | 216 |  24 | 120 | 216 |

前两列显示可用整数和浮点寄存器的数量。 第一行显示了0个整数和0个浮点寄存器的结果,相当于ABI0。 我们发现任何合理数量的浮点寄存器都具有相同的效果,因此我们将所有其他行固定为8。

“% fit”列给出了所有参数和结果都被寄存器分配并且没有参数在栈上传递的函数的比例。 三个“stack args”列给出栈参数字节的中位数、第95和第99个百分位数。 “spills”列同样总结了栈溢出空间中的字节数。 “stack total”列总结了栈参数和栈溢出槽的总和。 请注意,这是三种不同的分布;例如,没有一个函数需要0个栈参数字节、16个溢出字节和24个总栈字节。

由此,我们可以看到完全适合寄存器的函数比例在达到90%左右时增长非常缓慢,虽然有一小部分函数奇怪地可以从大量寄存器中受益。 在amd64上提供9个整数寄存器可以达到这个效果。 我们还看到大多数函数所需的栈空间相当小。 随着可用寄存器数量的增加,溢出所需空间的增加在很大程度上抵消了栈参数所需的空间减少,但随着可用寄存器的增加,所需的总栈空间普遍减少。 无论如何这确实表明在未来消除溢出槽会显着降低栈需求。

扩展阅读

注解

Footnotes

  1. 在callee的栈帧创建后的执行过程中会使用寄存器的值填充相应空间。

  2. 最后一句原文是Let S, the type sequence defining the stack frame, be empty.,实际上这个序列S主要保存的是abiStep结构体切片,abiStep中包含分配方式、内存布局中的位置信息、栈上的偏移量和寄存器索引等数据。

  3. 图示中的排列顺序,从上到下是从低地址到高地址的顺序,也就是说上面是栈顶方向,下面是栈底方向。

  4. 原文是Only arguments, not results, are assigned a spill area on the stack.,经笔者测试发现,未开启优化的情况下(-gcflags='-N -l'),函数返回的结果也会在栈上保留溢出空间。笔者用类似的示例代码做了实验(基于Go1.17),分别给出了未开启优化(-gcflags='-N -l')和开启优化(-gcflags='-l')的情况下,栈和寄存器的分配情况共读者参考。

  5. 原文是An alternative would be to pack multiple sub-word values into registers。网友lanthora的见解:一个寄存器是64位的,也就是一个word的大小,不到一个word大小的参数就是sub-word,当寄存器个数不够用的时候(或者什么其他的情况下).可以把多个小的参数放到同一个寄存器里。举个例子,假如有4个参数,分别是 uint8 uint8 uint16 uint32,在go的实现里,会被放到4个不同的寄存器,但是理论上也可以把这4个参数以某种特定方式放到一个寄存器里。(来自TG群

  6. 在汇编语言中,存在函数序言(prologue)、函数尾声(epilogue)的概念,详情见wiki以及CSDN

  7. CPU提供的FLAGS寄存器,详情见wiki以及CSDN