Go 汇编器指南

2023-05-16

A Quick Guide to Go’s Assembler - Go汇编器指南

This document is a quick outline of the unusual form of assembly language used by the gc Go compiler. The document is not comprehensive.

本文简单介绍了 Go 编译器 gc 使用的独特汇编形式。

The assembler is based on the input style of the Plan 9 assemblers, which is documented in detail elsewhere. If you plan to write assembly language, you should read that document although much of it is Plan 9-specific. The current document provides a summary of the syntax and the differences with what is explained in that document, and describes the peculiarities that apply when writing assembly code to interact with Go.

汇编器基于 Plan 9 汇编器的输入方式。本文档描述了语法总结和与 Plan 9 的差异,并描述了写汇编时与 Go 交互的特殊之处。

The most important thing to know about Go’s assembler is that it is not a direct representation of the underlying machine. Some of the details map precisely to the machine, but some do not. This is because the compiler suite (see this description) needs no assembler pass in the usual pipeline. Instead, the compiler operates on a kind of semi-abstract instruction set, and instruction selection occurs partly after code generation. The assembler works on the semi-abstract form, so when you see an instruction like MOV what the toolchain actually generates for that operation might not be a move instruction at all, perhaps a clear or load. Or it might correspond exactly to the machine instruction with that name. In general, machine-specific operations tend to appear as themselves, while more general concepts like memory move and subroutine call and return are more abstract. The details vary with architecture, and we apologize for the imprecision; the situation is not well-defined.

关于 Go 语言汇编器最重要的一点是它不是底层机器的直接表示,所以有些细节不能映射到机器。编译器在一种半抽象的指令集上运行,部分指令选择发生在代码生成之后。因此当看到 MOV 这类指令之后,工具链生成的操作可能根本就不是移动指令而是清除或加载,也可能恰好对应机器指令。 一般来说机器专用指令与 Go 编译器的表示一致,而对于通用概念(比如 MOV 或者子协程调用)的表示更抽象。

The assembler program is a way to parse a description of that semi-abstract instruction set and turn it into instructions to be input to the linker. If you want to see what the instructions look like in assembly for a given architecture, say amd64, there are many examples in the sources of the standard library, in packages such as runtime and math/big. You can also examine what the compiler emits as assembly code (the actual output may differ from what you see here):

Go 的编译过程涉及组件是:编译器 → \to 汇编器 → \to 链接器。汇编器是一种解析半抽象指令集并转换成链接器输入的方法。可以用下面的方式查看编译器提交的汇编代码。

$ cat x.go
package main

func main() {
	println(3)
}
$ GOOS=linux GOARCH=amd64 go tool compile -S x.go        # or: go build -gcflags -S x.go
"".main STEXT size=74 args=0x0 locals=0x10
	0x0000 00000 (x.go:3)	TEXT	"".main(SB), $16-0
	0x0000 00000 (x.go:3)	MOVQ	(TLS), CX
	0x0009 00009 (x.go:3)	CMPQ	SP, 16(CX)
	0x000d 00013 (x.go:3)	JLS	67
	0x000f 00015 (x.go:3)	SUBQ	$16, SP
	0x0013 00019 (x.go:3)	MOVQ	BP, 8(SP)
	0x0018 00024 (x.go:3)	LEAQ	8(SP), BP
	0x001d 00029 (x.go:3)	FUNCDATA	$0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
	0x001d 00029 (x.go:3)	FUNCDATA	$1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
	0x001d 00029 (x.go:3)	FUNCDATA	$2, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
	0x001d 00029 (x.go:4)	PCDATA	$0, $0
	0x001d 00029 (x.go:4)	PCDATA	$1, $0
	0x001d 00029 (x.go:4)	CALL	runtime.printlock(SB)
	0x0022 00034 (x.go:4)	MOVQ	$3, (SP)
	0x002a 00042 (x.go:4)	CALL	runtime.printint(SB)
	0x002f 00047 (x.go:4)	CALL	runtime.printnl(SB)
	0x0034 00052 (x.go:4)	CALL	runtime.printunlock(SB)
	0x0039 00057 (x.go:5)	MOVQ	8(SP), BP
	0x003e 00062 (x.go:5)	ADDQ	$16, SP
	0x0042 00066 (x.go:5)	RET
	0x0043 00067 (x.go:5)	NOP
	0x0043 00067 (x.go:3)	PCDATA	$1, $-1
	0x0043 00067 (x.go:3)	PCDATA	$0, $-1
	0x0043 00067 (x.go:3)	CALL	runtime.morestack_noctxt(SB)
	0x0048 00072 (x.go:3)	JMP	0
...

The FUNCDATA and PCDATA directives contain information for use by the garbage collector; they are introduced by the compiler.

FUNCDATAPCDATA 是编译器引入的包含垃圾回收信息的指令。

To see what gets put in the binary after linking, use go tool objdump:

要想看链接后的二进制文件,可以用 go tool objdump

$ go build -o x.exe x.go
$ go tool objdump -s main.main x.exe
TEXT main.main(SB) /tmp/x.go
  x.go:3		0x10501c0		65488b0c2530000000	MOVQ GS:0x30, CX
  x.go:3		0x10501c9		483b6110		CMPQ 0x10(CX), SP
  x.go:3		0x10501cd		7634			JBE 0x1050203
  x.go:3		0x10501cf		4883ec10		SUBQ $0x10, SP
  x.go:3		0x10501d3		48896c2408		MOVQ BP, 0x8(SP)
  x.go:3		0x10501d8		488d6c2408		LEAQ 0x8(SP), BP
  x.go:4		0x10501dd		e86e45fdff		CALL runtime.printlock(SB)
  x.go:4		0x10501e2		48c7042403000000	MOVQ $0x3, 0(SP)
  x.go:4		0x10501ea		e8e14cfdff		CALL runtime.printint(SB)
  x.go:4		0x10501ef		e8ec47fdff		CALL runtime.printnl(SB)
  x.go:4		0x10501f4		e8d745fdff		CALL runtime.printunlock(SB)
  x.go:5		0x10501f9		488b6c2408		MOVQ 0x8(SP), BP
  x.go:5		0x10501fe		4883c410		ADDQ $0x10, SP
  x.go:5		0x1050202		c3			RET
  x.go:3		0x1050203		e83882ffff		CALL runtime.morestack_noctxt(SB)
  x.go:3		0x1050208		ebb6			JMP main.main(SB)

Constants 常数

Although the assembler takes its guidance from the Plan 9 assemblers, it is a distinct program, so there are some differences. One is in constant evaluation. Constant expressions in the assembler are parsed using Go’s operator precedence, not the C-like precedence of the original. Thus 3&1<<2 is 4, not 0—it parses as (3&1)<<2 not 3&(1<<2). Also, constants are always evaluated as 64-bit unsigned integers. Thus -2 is not the integer value minus two, but the unsigned 64-bit integer with the same bit pattern. The distinction rarely matters but to avoid ambiguity, division or right shift where the right operand’s high bit is set is rejected.

Go 的汇编器与 Plan 9 汇编器有一些差异。一是常数计算,Go 汇编器使用的常数表达式使用 Go 的运算符优先级。二是常量永远使用 64 位无符号整数表示,所以 -2 不是负 2,而是二进制位完全一样的无符号 64 位整数。为避免歧义,在右操作数的最高位为 1 时禁止除法或右移操作。

Symbols 符号

Some symbols, such as R1 or LR, are predefined and refer to registers. The exact set depends on the architecture.

某些符号是预定义的,执行寄存器,比如 R1LR,具体包含哪些符号取决于体系结构。

There are four predeclared symbols that refer to pseudo-registers. These are not real registers, but rather virtual registers maintained by the toolchain, such as a frame pointer. The set of pseudo-registers is the same for all architectures:

有 4 个预定义的符号引用伪寄存器,他们不是真的寄存器,而是工具链维护的虚拟寄存器:

  • FP: Frame pointer: arguments and locals. 帧指针:参数和局部变量
  • PC: Program counter: jumps and branches. 程序计数器:跳转和分支
  • SB: Static base pointer: global symbols. 静态基址寄存器:全局符号
  • SP: Stack pointer: the highest address within the local stack frame. 栈指针:本地栈的最高地址

All user-defined symbols are written as offsets to the pseudo-registers FP (arguments and locals) and SB (globals).

所有用户定义的符号都写作 FPSB 的偏移量。

The SB pseudo-register can be thought of as the origin of memory, so the symbol foo(SB) is the name foo as an address in memory. This form is used to name global functions and data. Adding <> to the name, as in foo<>(SB), makes the name visible only in the current source file, like a top-level static declaration in a C file. Adding an offset to the name refers to that offset from the symbol’s address, so foo+4(SB) is four bytes past the start of foo.

SB 伪寄存器可以认为是内存的起点,符号 foo(SB) 是符号 foo 在内存中的地址,这种形式用于对全局变量和函数。添加 <> 使得变量只在当前源文件中可见。添加偏移量指的是相对于符号地址的偏移量,比如 foo+4(SB)foo 的 4 个字节之后。

The FP pseudo-register is a virtual frame pointer used to refer to function arguments. The compilers maintain a virtual frame pointer and refer to the arguments on the stack as offsets from that pseudo-register. Thus 0(FP) is the first argument to the function, 8(FP) is the second (on a 64-bit machine), and so on. However, when referring to a function argument this way, it is necessary to place a name at the beginning, as in first_arg+0(FP) and second_arg+8(FP). (The meaning of the offset—offset from the frame pointer—distinct from its use with SB, where it is an offset from the symbol.) The assembler enforces this convention, rejecting plain 0(FP) and 8(FP). The actual name is semantically irrelevant but should be used to document the argument’s name. It is worth stressing that FP is always a pseudo-register, not a hardware register, even on architectures with a hardware frame pointer.

FP 伪寄存器是指向函数参数的虚拟栈指针。0(FP) 是函数的第一个参数,8(FP) 是第二个(对于 64 位机),以此类推。当以这种方式引用函数参数时必须在开头写参数名,例如 first_arg+0(FP) 以及 second_arg+8(FP)。偏移量的含义是相对于栈指针的,而不是跟 SB 一样相对于符号。汇编器强制使用这个约定,禁止纯的 0(FP)8(FP)。实际的参数名无关紧要,但应该用于记录参数名称。

For assembly functions with Go prototypes, go vet will check that the argument names and offsets match. On 32-bit systems, the low and high 32 bits of a 64-bit value are distinguished by adding a _lo or _hi suffix to the name, as in arg_lo+0(FP) or arg_hi+4(FP). If a Go prototype does not name its result, the expected assembly name is ret.

对于 Go 原型的汇编函数,go vet 会检查参数名和偏移量是否匹配。在 32 位系统上,64 位数的低和高 32 位通过添加 _lo_hi 后缀区分,例如 arg_lo+0(FP)。如果 Go 原型未对结果命名则其预期的名称是 ret

The SP pseudo-register is a virtual stack pointer used to refer to frame-local variables and the arguments being prepared for function calls. It points to the highest address within the local stack frame, so references should use negative offsets in the range [−framesize, 0): x-8(SP), y-4(SP), and so on.

SP 伪寄存器指向本地栈帧变量和用于函数调用的参数,它指向了本地栈帧的最高地址,所以应该用负偏移量引用(范围是 [ − 帧大小 , 0 ) [-\text{帧大小}, 0) [帧大小,0)),例如 x-8(SP)y-4(SP)等等。

On architectures with a hardware register named SP, the name prefix distinguishes references to the virtual stack pointer from references to the architectural SP register. That is, x-8(SP) and -8(SP) are different memory locations: the first refers to the virtual stack pointer pseudo-register, while the second refers to the hardware’s SP register.

对于拥有名为 SP 的硬件寄存器的体系结构,名称前缀可以用于区分对虚拟栈指针的引用或硬件 SP 寄存器的引用,比如 x-8(SP)-8(SP) 是不同的内存位置,第一个引用虚拟栈指针伪寄存器,第二个引用硬件 SP 寄存器。

On machines where SP and PC are traditionally aliases for a physical, numbered register, in the Go assembler the names SP and PC are still treated specially; for instance, references to SP require a symbol, much like FP. To access the actual hardware register use the true R name. For example, on the ARM architecture the hardware SP and PC are accessible as R13 and R15.

对于 SP 和 PC 是传统物理编号寄存器别名的机器上,SP 和 PC 仍然被特殊处理:引用 SP 寄存器序号符号,很像 FP,访问真实的的硬件寄存器要使用实际的 R 名称,例如通过 R13R15 访问硬件 SPPC 寄存器。

Branches and direct jumps are always written as offsets to the PC, or as jumps to labels:

分支和指令跳转是相对于 PC 的偏移量,或者跳转到标签:

label:
	MOVW $0, R1
	JMP label

Each label is visible only within the function in which it is defined. It is therefore permitted for multiple functions in a file to define and use the same label names. Direct jumps and call instructions can target text symbols, such as name(SB), but not offsets from symbols, such as name+4(SB).

每个标签都只在它所在的函数内有效。直接跳转指令以标签名为目标,但不能以标签名的偏移量为目标。

Instructions, registers, and assembler directives are always in UPPER CASE to remind you that assembly programming is a fraught endeavor. (Exception: the g register renaming on ARM.)

指令、寄存器和汇编指令永远是大写,时刻提醒你汇编是是一项艰巨的任务。

In Go object files and binaries, the full name of a symbol is the package path followed by a period and the symbol name: fmt.Printf or math/rand.Int. Because the assembler’s parser treats period and slash as punctuation, those strings cannot be used directly as identifier names. Instead, the assembler allows the middle dot character U+00B7 and the division slash U+2215 in identifiers and rewrites them to plain period and slash. Within an assembler source file, the symbols above are written as fmt·Printf and math∕rand·Int. The assembly listings generated by the compilers when using the -S flag show the period and slash directly instead of the Unicode replacements required by the assemblers.

在 Go 目标文件和二进制文件中,符号的全名是包名+句点+符号名:fmt.Printfmath/rand.Int,但是由于汇编器的解析器将句点和斜杠视为标点,因此不能用于标识符名称。但是汇编器允许中间点符号<U+00B7>和除法斜线号<U+2215>重写为原来的句点和斜杠。所以在汇编器源文件中,上面的符号写成 fmt·Printfmath∕rand·Int。编译器生成的汇编列表可以用 -S 标志展示句点而不是被汇编器替换的 Unicode 字符。

Most hand-written assembly files do not include the full package path in symbol names, because the linker inserts the package path of the current object file at the beginning of any name starting with a period: in an assembly source file within the math/rand package implementation, the package’s Int function can be referred to as ·Int. This convention avoids the need to hard-code a package’s import path in its own source code, making it easier to move the code from one location to another.

大多数手写的汇编文件中的符号不包含完整包路径,因为连接器会在所有以句点开头的名称插入当前目标文件的包路径,比如 math/rand 包中的 Int 函数写成了 ·Int。这种习惯便于将代码移动到新包中,因为没有对包名硬编码。

Directives 指令

The assembler uses various directives to bind text and data to symbol names. For example, here is a simple complete function definition. The TEXT directive declares the symbol runtime·profileloop and the instructions that follow form the body of the function. The last instruction in a TEXT block must be some sort of jump, usually a RET (pseudo-)instruction. (If it’s not, the linker will append a jump-to-itself instruction; there is no fallthrough in TEXTs.) After the symbol, the arguments are flags (see below) and the frame size, a constant (but see below):

汇编器使用各种指令将文本和数据绑定到符号名,例如下面是一个完整的函数定义。TEXT 指令声明了符号 runtime.profileloop,后面的指令形成了函数体。最后一个指令必须是某种形式的跳转,通常是 RET 伪指令。(如果不是,链接器会添加一个跳转到自身的指令,不会穿透到下面。在符号 runtime.profileloop 之后的参数是一些标志和帧大小 ,帧大小是一个常数。

TEXT runtime·profileloop(SB),NOSPLIT,$8
	MOVQ	$runtime·profileloop1(SB), CX
	MOVQ	CX, 0(SP)
	CALL	runtime·externalthreadhandler(SB)
	RET

In the general case, the frame size is followed by an argument size, separated by a minus sign. (It’s not a subtraction, just idiosyncratic syntax.) The frame size $24-8 states that the function has a 24-byte frame and is called with 8 bytes of argument, which live on the caller’s frame. If NOSPLIT is not specified for the TEXT, the argument size must be provided. For assembly functions with Go prototypes, go vet will check that the argument size is correct.

通常情况下,帧大小后跟参数大小,用减号分隔(但不是减法只是一种语法)。比如帧大小 $24-8 表示函数是 24 字节的帧和 8 字节的参数要放到调用者的帧上。如果没有为TEXT声明 NOSPLIT则必须提供参数大小(NOSPLIT)。对于Go原型的汇编函数,go vet 会检查参数大小是否正确。

Note that the symbol name uses a middle dot to separate the components and is specified as an offset from the static base pseudo-register SB. This function would be called from Go source for package runtime using the simple name profileloop.

注意符号名用中间点分隔组件,并且是静态基地址的偏移量。对于 runtime 包中的 Go 源代码,该函数会以简单名称 profileloop 的名称被调用。

Global data symbols are defined by a sequence of initializing DATA directives followed by a GLOBL directive. Each DATA directive initializes a section of the corresponding memory. The memory not explicitly initialized is zeroed. The general form of the DATA directive is

全局数据符号以一系列 DATA 指令初始化,后面跟一个 GLOBL 指令。每个 DATA 指令初始化对应内存的一节。没有被明确初始化的内存是 0,DATA 指令的通用形式是:

DATA	symbol+offset(SB)/width, value

which initializes the symbol memory at the given offset and width with the given value. The DATA directives for a given symbol must be written with increasing offsets.

在给定的偏移量以给定的值初始化符号内存。一个符号的 DATA 指令偏移量必须递增。

The GLOBL directive declares a symbol to be global. The arguments are optional flags and the size of the data being declared as a global, which will have initial value all zeros unless a DATA directive has initialized it. The GLOBL directive must follow any corresponding DATA directives.

GLOBL 指定声明了全局符号,参数是 flags(可选) 和数据大小,初始值是 0 除非 DATA 指令已经被初始化。GLOBL 指令必须跟在对应的 DATA 指令之后。

For example,

DATA divtab<>+0x00(SB)/4, $0xf4f8fcff
DATA divtab<>+0x04(SB)/4, $0xe6eaedf0
...
DATA divtab<>+0x3c(SB)/4, $0x81828384
GLOBL divtab<>(SB), RODATA, $64

GLOBL runtime·tlsoffset(SB), NOPTR, $4

declares and initializes divtab<>, a read-only 64-byte table of 4-byte integer values, and declares runtime·tlsoffset, a 4-byte, implicitly zeroed variable that contains no pointers.

例如上述代码初始化了 divtab<>,一个 4 字节整数组成的只读 64 字节表,并且声明了 tlsoffset,一个 4 字节隐式初始化为 0,不包含指针。

There may be one or two arguments to the directives. If there are two, the first is a bit mask of flags, which can be written as numeric expressions, added or or-ed together, or can be set symbolically for easier absorption by a human. Their values, defined in the standard #include file textflag.h, are:

这些指令可能有 1 个或 2 个参数,如果有 2 个,第一个 flags 是位掩码(可以写成数值形式,使用 and 和 or 运算符,也可以写符号可读性好),他们的值定义在标准 #include 文件 textflag.h 中,包括:

  • NOPROF = 1
    (For TEXT items.) Don’t profile the marked function. This flag is deprecated. (用于TEXT指令)禁止分析给定函数。已被废弃。
  • DUPOK = 2
    It is legal to have multiple instances of this symbol in a single binary. The linker will choose one of the duplicates to use. 允许在一个二进制文件中包含符号的多个实例。
  • NOSPLIT = 4
    (For TEXT items.) Don’t insert the preamble to check if the stack must be split. The frame for the routine, plus anything it calls, must fit in the spare space remaining in the current stack segment. Used to protect routines such as the stack splitting code itself. 不要插入前导码来检查堆栈是否必须拆分。协程的帧及其调用的任何内容都必须适合当前栈分片中剩余的空闲空间。用于保护协程,例如栈拆分代码。
  • RODATA = 8
    (For DATA and GLOBL items.) Put this data in a read-only section.
  • NOPTR = 16
    (For DATA and GLOBL items.) This data contains no pointers and therefore does not need to be scanned by the garbage collector.
  • WRAPPER = 32
    (For TEXT items.) This is a wrapper function and should not count as disabling recover.
  • NEEDCTXT = 64
    (For TEXT items.) This function is a closure so it uses its incoming context register.
  • LOCAL = 128
    This symbol is local to the dynamic shared object.
  • TLSBSS = 256
    (For DATA and GLOBL items.) Put this data in thread local storage.
  • NOFRAME = 512
    (For TEXT items.) Do not insert instructions to allocate a stack frame and save/restore the return address, even if this is not a leaf function. Only valid on functions that declare a frame size of 0.
  • TOPFRAME = 2048
    (For TEXT items.) Function is the outermost frame of the call stack. Traceback should stop at this function.

Interacting with Go types and constants 与 Go 类型和常数的交互

If a package has any .s files, then go build will direct the compiler to emit a special header called go_asm.h, which the .s files can then #include. The file contains symbolic #define constants for the offsets of Go struct fields, the sizes of Go struct types, and most Go const declarations defined in the current package. Go assembly should avoid making assumptions about the layout of Go types and instead use these constants. This improves the readability of assembly code, and keeps it robust to changes in data layout either in the Go type definitions or in the layout rules used by the Go compiler.

对于任何包含 .s 文件的包,go build 会让编译器提交一个特殊的头文件名为 go_asm.h.s 文件可以引用。此文件包含通过 #define 定义的符号常量,表示 Go 结构体成员、结构体类型的大小和多数 Go 常量声明。Go 汇编器应该避免猜测 Go 类型布局,而是用定义的常量。这会提升汇编代码的可读性,提升在 Go 类型定义的布局和 Go 编译器布局规则改变时的鲁棒性。

Constants are of the form const_<name>. For example, given the Go declaration const bufSize = 1024, assembly code can refer to the value of this constant as const_bufSize.

常数的形式是 const_变量名,例如,给定 Go 声明的常数 const bufSize = 1024,汇编码可以指向通过 const_bufSize 指向这个常数。

Field offsets are of the form <type>_<field>. Struct sizes are of the form <type>__size. For example, consider the following Go definition:

成员偏移量的形式是 类型_成员,结构体大小的形式是 类型__size。例如,对于下面的定义:

type reader struct {
	buf [bufSize]byte
	r   int
}

Assembly can refer to the size of this struct as reader__size and the offsets of the two fields as reader_buf and reader_r. Hence, if register R1 contains a pointer to a reader, assembly can reference the r field as reader_r(R1).

汇编器可以通过 reader__size 引用结构体的大小,通过 reader_bufreader_r 访问两个成员。因此,如果寄存器 R1 包含一个指向 reader的指针,汇编器可以通过 reader_r(R1) 访问成员 r

If any of these #define names are ambiguous (for example, a struct with a _size field), #include "go_asm.h" will fail with a “redefinition of macro” error.

如果任意一个上面的定义存在歧义(比如,一个结构体的成员是 _size),#include "go_asm.h" 会抛出 宏重复定义 的错误。

Runtime Coordination 运行时交互

For garbage collection to run correctly, the runtime must know the location of pointers in all global data and in most stack frames. The Go compiler emits this information when compiling Go source files, but assembly programs must define it explicitly.

为了正确获取运行垃圾信息,runtime 必须知道全局变量和多数栈帧中指针的位置,汇编程序需要手动提交。

A data symbol marked with the NOPTR flag (see above) is treated as containing no pointers to runtime-allocated data. A data symbol with the RODATA flag is allocated in read-only memory and is therefore treated as implicitly marked NOPTR. A data symbol with a total size smaller than a pointer is also treated as implicitly marked NOPTR. It is not possible to define a symbol containing pointers in an assembly source file; such a symbol must be defined in a Go source file instead. Assembly source can still refer to the symbol by name even without DATA and GLOBL directives. A good general rule of thumb is to define all non-RODATA symbols in Go instead of in assembly.

NOPTR 标记的符号视为不包含指向运行时数据的指针,用 RODATA 标记的数据被分配在只读内存中因此隐含 NOPTR,总大小小于指针的符号也被视为没有指针。在汇编中定义包含指针的符号是不可能的,这样的符号必须定义在 Go 源文件中。汇编源文件仍然能引用没有 DATA 和 GLOBAL 指令的符号。总之,最好把所以非只读数据定义在 Go 中而非汇编中。

Each function also needs annotations giving the location of live pointers in its arguments, results, and local stack frame. For an assembly function with no pointer results and either no local stack frame or no function calls, the only requirement is to define a Go prototype for the function in a Go source file in the same package. The name of the assembly function must not contain the package name component (for example, function Syscall in package syscall should use the name ·Syscall instead of the equivalent name syscall·Syscall in its TEXT directive). For more complex situations, explicit annotation is needed. These annotations use pseudo-instructions defined in the standard #include file funcdata.h.

每个函数都需要在参数、结果和本地栈帧中添加关于指针位置的注释。对于结果非指针或没有本地栈帧或函数调用的汇编函数,唯一的要求是在同一个包中的 Go 源文件中定义一个原型函数。汇编函数的名称不得包含包名,例如 Syscall 包中 syscall 函数应该在 TEXT 指令中使用名称 ·Syscall 而不是等效名称 syscall·Syscall

If a function has no arguments and no results, the pointer information can be omitted. This is indicated by an argument size annotation of $n-0 on the TEXT instruction. Otherwise, pointer information must be provided by a Go prototype for the function in a Go source file, even for assembly functions not called directly from Go. (The prototype will also let go vet check the argument references.) At the start of the function, the arguments are assumed to be initialized but the results are assumed uninitialized. If the results will hold live pointers during a call instruction, the function should start by zeroing the results and then executing the pseudo-instruction GO_RESULTS_INITIALIZED. This instruction records that the results are now initialized and should be scanned during stack movement and garbage collection. It is typically easier to arrange that assembly functions do not return pointers or do not contain call instructions; no assembly functions in the standard library use GO_RESULTS_INITIALIZED.

如果函数中没有参数和返回值,那么指针信息可以省略,这可以用 TEXT 指令上的 $n-0(参数大小注释 )表示。否则 Go 源文件中的 Go 原型函数必须提供指针信息,即使是没有直接被 Go 调用的函数。在函数的开头,参数被假定已初始化但是返回值假定为未初始化。如果返回值会在函数调用期间持有活动指针,则函数应该首先将结果归零然后执行伪指令 GO_RESULTS_INITIALIZED,该指令表明返回值已经被初始化,应该在栈移动和垃圾垃圾回收时扫描。通常情况下对于不返回指针和不包含调用指令的函数进行重排序很容易,标准库中汇编函数没有使用过 GO_RESULT_INITIALIZED

If a function has no local stack frame, the pointer information can be omitted. This is indicated by a local frame size annotation of $0-n on the TEXT instruction. The pointer information can also be omitted if the function contains no call instructions. Otherwise, the local stack frame must not contain pointers, and the assembly must confirm this fact by executing the pseudo-instruction NO_LOCAL_POINTERS. Because stack resizing is implemented by moving the stack, the stack pointer may change during any function call: even pointers to stack data must not be kept in local variables.

如果函数没有本地栈帧,指针信息可以省略,这可以用 TEXT 指令的 $0-n (帧大小注释表示)。如果函数中没有函数调用那么指针信息可以省略。否则本地栈帧不能包含指针,汇编必须通过执行 NO_LOCAL_POINTERS 确认。因为栈大小和重新调整是通过移到栈实现的,栈指针可能在函数调用期间改变:在本地变量中也不能有指向栈数据的指针。

Assembly functions should always be given Go prototypes, both to provide pointer information for the arguments and results and to let go vet check that the offsets being used to access them are correct.

汇编函数必须通过 Go 原型给定,既要为参数的返回值提供指针信息,又要让 go vet 检查访问他们的偏移量是否正确。

Architecture-specific details 特定体系结构的细节(略)

It is impractical to list all the instructions and other details for each machine. To see what instructions are defined for a given machine, say ARM, look in the source for the obj support library for that architecture, located in the directory src/cmd/internal/obj/arm. In that directory is a file a.out.go; it contains a long list of constants starting with A, like this:

const (
	AAND = obj.ABaseARM + obj.A_ARCHSPECIFIC + iota
	AEOR
	ASUB
	ARSB
	AADD
	...

This is the list of instructions and their spellings as known to the assembler and linker for that architecture. Each instruction begins with an initial capital A in this list, so AAND represents the bitwise and instruction, AND (without the leading A), and is written in assembly source as AND. The enumeration is mostly in alphabetical order. (The architecture-independent AXXX, defined in the cmd/internal/obj package, represents an invalid instruction). The sequence of the A names has nothing to do with the actual encoding of the machine instructions. The cmd/internal/obj package takes care of that detail.

The instructions for both the 386 and AMD64 architectures are listed in cmd/internal/obj/x86/a.out.go.

The architectures share syntax for common addressing modes such as (R1) (register indirect), 4(R1) (register indirect with offset), and $foo(SB) (absolute address). The assembler also supports some (not necessarily all) addressing modes specific to each architecture. The sections below list these.

One detail evident in the examples from the previous sections is that data in the instructions flows from left to right: MOVQ $0, CX clears CX. This rule applies even on architectures where the conventional notation uses the opposite direction.

Here follow some descriptions of key Go-specific details for the supported architectures.

32-bit Intel 386

The runtime pointer to the g structure is maintained through the value of an otherwise unused (as far as Go is concerned) register in the MMU. In the runtime package, assembly code can include go_tls.h, which defines an OS- and architecture-dependent macro get_tls for accessing this register. The get_tls macro takes one argument, which is the register to load the g pointer into.

For example, the sequence to load g and m using CX looks like this:

#include "go_tls.h"
#include "go_asm.h"
...
get_tls(CX)
MOVL	g(CX), AX     // Move g into AX.
MOVL	g_m(AX), BX   // Move g.m into BX.

The get_tls macro is also defined on amd64.

Addressing modes:

  • (DI)(BX*2): The location at address DI plus BX*2.
  • 64(DI)(BX*2): The location at address DI plus BX*2 plus 64. These modes accept only 1, 2, 4, and 8 as scale factors.

When using the compiler and assembler’s -dynlink or -shared modes, any load or store of a fixed memory location such as a global variable must be assumed to overwrite CX. Therefore, to be safe for use with these modes, assembly sources should typically avoid CX except between memory references.

64-bit Intel 386 (a.k.a. amd64)

The two architectures behave largely the same at the assembler level. Assembly code to access the m and g pointers on the 64-bit version is the same as on the 32-bit 386, except it uses MOVQ rather than MOVL:

get_tls(CX)
MOVQ	g(CX), AX     // Move g into AX.
MOVQ	g_m(AX), BX   // Move g.m into BX.

Register BP is callee-save. The assembler automatically inserts BP save/restore when frame size is larger than zero. Using BP as a general purpose register is allowed, however it can interfere with sampling-based profiling.

ARM

The registers R10 and R11 are reserved by the compiler and linker.

R10 points to the g (goroutine) structure. Within assembler source code, this pointer must be referred to as g; the name R10 is not recognized.

To make it easier for people and compilers to write assembly, the ARM linker allows general addressing forms and pseudo-operations like DIV or MOD that may not be expressible using a single hardware instruction. It implements these forms as multiple instructions, often using the R11 register to hold temporary values. Hand-written assembly can use R11, but doing so requires being sure that the linker is not also using it to implement any of the other instructions in the function.

When defining a TEXT, specifying frame size $-4 tells the linker that this is a leaf function that does not need to save LR on entry.

The name SP always refers to the virtual stack pointer described earlier. For the hardware register, use R13.

Condition code syntax is to append a period and the one- or two-letter code to the instruction, as in MOVW.EQ. Multiple codes may be appended: MOVM.IA.W. The order of the code modifiers is irrelevant.

Addressing modes:

  • R0->16
    R0>>16
    R0<<16
    R0@>16: For <<, left shift R0 by 16 bits. The other codes are -> (arithmetic right shift), >> (logical right shift), and @> (rotate right).
  • R0->R1
    R0>>R1
    R0<<R1
    R0@>R1: For <<, left shift R0 by the count in R1. The other codes are -> (arithmetic right shift), >> (logical right shift), and @> (rotate right).
  • [R0,g,R12-R15]: For multi-register instructions, the set comprising R0, g, and R12 through R15 inclusive.
  • (R5, R6): Destination register pair.

ARM64

R18 is the “platform register”, reserved on the Apple platform. To prevent accidental misuse, the register is named R18_PLATFORM. R27 and R28 are reserved by the compiler and linker. R29 is the frame pointer. R30 is the link register.

Instruction modifiers are appended to the instruction following a period. The only modifiers are P (postincrement) and W (preincrement): MOVW.P, MOVW.W

Addressing modes:

  • R0->16
    R0>>16
    R0<<16
    R0@>16: These are the same as on the 32-bit ARM.
  • $(8<<12): Left shift the immediate value 8 by 12 bits.
  • 8(R0): Add the value of R0 and 8.
  • (R2)(R0): The location at R0 plus R2.
  • R0.UXTB
    R0.UXTB<<imm: UXTB: extract an 8-bit value from the low-order bits of R0 and zero-extend it to the size of R0. R0.UXTB<<imm: left shift the result of R0.UXTB by imm bits. The imm value can be 0, 1, 2, 3, or 4. The other extensions include UXTH (16-bit), UXTW (32-bit), and UXTX (64-bit).
  • R0.SXTB
    R0.SXTB<<imm: SXTB: extract an 8-bit value from the low-order bits of R0 and sign-extend it to the size of R0. R0.SXTB<<imm: left shift the result of R0.SXTB by imm bits. The imm value can be 0, 1, 2, 3, or 4. The other extensions include SXTH (16-bit), SXTW (32-bit), and SXTX (64-bit).
  • (R5, R6): Register pair for LDAXP/LDP/LDXP/STLXP/STP/STP.

Reference: Go ARM64 Assembly Instructions Reference Manual

PPC64

This assembler is used by GOARCH values ppc64 and ppc64le.

Reference: Go PPC64 Assembly Instructions Reference Manual

IBM z/Architecture, a.k.a. s390x

The registers R10 and R11 are reserved. The assembler uses them to hold temporary values when assembling some instructions.

R13 points to the g (goroutine) structure. This register must be referred to as g; the name R13 is not recognized.

R15 points to the stack frame and should typically only be accessed using the virtual registers SP and FP.

Load- and store-multiple instructions operate on a range of registers. The range of registers is specified by a start register and an end register. For example, LMG (R9), R5, R7 would load R5, R6 and R7 with the 64-bit values at 0(R9), 8(R9) and 16(R9) respectively.

Storage-and-storage instructions such as MVC and XC are written with the length as the first argument. For example, XC $8, (R9), (R9) would clear eight bytes at the address specified in R9.

If a vector instruction takes a length or an index as an argument then it will be the first argument. For example, VLEIF $1, $16, V2 will load the value sixteen into index one of V2. Care should be taken when using vector instructions to ensure that they are available at runtime. To use vector instructions a machine must have both the vector facility (bit 129 in the facility list) and kernel support. Without kernel support a vector instruction will have no effect (it will be equivalent to a NOP instruction).

Addressing modes:

  • (R5)(R6*1): The location at R5 plus R6. It is a scaled mode as on the x86, but the only scale allowed is 1.

MIPS, MIPS64

General purpose registers are named R0 through R31, floating point registers are F0 through F31.

R30 is reserved to point to g. R23 is used as a temporary register.

In a TEXT directive, the frame size $-4 for MIPS or $-8 for MIPS64 instructs the linker not to save LR.

SP refers to the virtual stack pointer. For the hardware register, use R29.

Addressing modes:

  • 16(R1): The location at R1 plus 16.
  • (R1): Alias for 0(R1).

The value of GOMIPS environment variable (hardfloat or softfloat) is made available to assembly code by predefining either GOMIPS_hardfloat or GOMIPS_softfloat.

The value of GOMIPS64 environment variable (hardfloat or softfloat) is made available to assembly code by predefining either GOMIPS64_hardfloat or GOMIPS64_softfloat.

Unsupported opcodes

The assemblers are designed to support the compiler so not all hardware instructions are defined for all architectures: if the compiler doesn’t generate it, it might not be there. If you need to use a missing instruction, there are two ways to proceed. One is to update the assembler to support that instruction, which is straightforward but only worthwhile if it’s likely the instruction will be used again. Instead, for simple one-off cases, it’s possible to use the BYTE and WORD directives to lay down explicit data into the instruction stream within a TEXT. Here’s how the 386 runtime defines the 64-bit atomic load function.

// uint64 atomicload64(uint64 volatile* addr);
// so actually
// void atomicload64(uint64 *res, uint64 volatile *addr);
TEXT runtime·atomicload64(SB), NOSPLIT, $0-12
	MOVL	ptr+0(FP), AX
	TESTL	$7, AX
	JZ	2(PC)
	MOVL	0, AX // crash with nil ptr deref
	LEAL	ret_lo+4(FP), BX
	// MOVQ (%EAX), %MM0
	BYTE $0x0f; BYTE $0x6f; BYTE $0x00
	// MOVQ %MM0, 0(%EBX)
	BYTE $0x0f; BYTE $0x7f; BYTE $0x03
	// EMMS
	BYTE $0x0F; BYTE $0x77
	RET
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Go 汇编器指南 的相关文章

  • linux下搭建redis哨兵

    1 准备三台Linux服务器 span class token variable 准备以下三台服务器 span span class token number 192 168 span span class token number 227
  • linux下安装elasticsearch

    目录 1 准备一台服务器2 下载elasticsearch安装包3 安装elasticsearch 1 准备一台服务器 这里使用的时redhat8 5 红帽新版的系统 xff0c 这里给的内存大小时4G 2 下载elasticsearch安
  • 用命令语句修改mysql某字段长度

    在MySQL中修改某个字段的长度 xff0c 需要使用ALTER TABLE语句 xff0c 具体操作如下 xff1a 假设要修改表A中的字段col1的长度为50 ALTER TABLE A MODIFY col1 VARCHAR 50 以
  • maven打普通包jar包(依赖一并打入)

    1 创建一个maven项目 这里可以看到新创建的maven项目 2 在pom xml添加项目需要的依赖 span class token generics span class token punctuation lt span depen
  • 纯Java项目批处理(打包方法二)

    1 创建一个普通的Java项目 主类 span class token keyword public span span class token keyword class span span class token class name
  • myCat搭建mysql的两主两从读写分离

    目录 前言1 搭建两主两从 xff08 主从复制 xff09 2 测试主从复制效果3 修改myCat配置文件4 启动myCat5 测试效果 前言 这里为了方便快速搭建我是使用docker容器完成的mysql实例 和使用在linux上安装的m
  • Red Hat Enterprise Linux9安装

    目录 前言1 启动iso镜像进入安装首页2 选择语言3 进入安装摘要界面4 选择安装目的地5 禁用KDUMP6 软件选择7 网络和主机名8 设置root用户密码9 开始安装10 重启11 登录系统12 挂载系统iso镜像13 配置本地镜像源
  • epel在线镜像源

    目录 前言阿里云镜像源腾讯镜像源华为镜像源清华大学镜像源中国科技大学镜像源 前言 此epel镜像源适用于红帽系列系统 xff0c 如Red Hat Enterprise CentOS AnoliOS Rocky Linux Almalinu
  • SUSE本地镜像源配置

    SUSE Enterprise Linux 11 12镜像源配置 mount dev sr0 mnt zypper ar file mnt local sles SUSE Enterprise Linux 15镜像源配置 先执行 mount
  • MyCat2搭建mysql主从分离

    前言 此次搭建MyCat2读写分离使用的Linux环境是Debian11 3 xff0c 使用的mysql版本是8 0 16 1 准备三台服务器 准备如下三台Debian服务器 xff0c 而且这三台服务器之间能够相互通信 xff0c 可以
  • Debian11安装redis报错解决方法

    1 报错需要安装C语言编译环境 执行apt install gcc 安装C语言编译环境 2 报错冲突 Conflicts 这里在执行apt install gcc命令后出现两gcc的版本 xff0c 这里执行apt install gcc
  • sqlserver在linux下安装

    前言 本次安装的sqlserver用的是sqlserver2017 linux环境是redhat Linux8 6 xff0c 本次采用是离线rpm安装 安装过程总体感觉比MySQL还要容易 xff0c 初步使用起来感觉和mysql差不多
  • DB2在Linux下静默安装

    目录 前言1 下载并上传db2压缩包到Linux2 检测db2安装环境3 安装db2数据库软件4 配置db2数据库系统用户5 创建数据库实例6 配置TCP IP通信服务7 配置数据库8 启动和关闭数据库实例9 修改权限10 数据库客户端和工
  • ubuntu12.04开启Framebuffer

    一 xff0e framebuffer概述 Framebuffer在Linux中是作为 设备 来实现的 xff0c 它是对图形硬件的一种抽象 xff0c 代表着显卡中的帧缓冲区 xff08 Framebuffer xff09 通过Frame
  • Redhat系列系统在线镜像源

    目录 前言Redhat7镜像源1 阿里云镜像源2 清华大学镜像源3 网易镜像源4 华为镜像源 Redhat8镜像源1 阿里云镜像源2 清华大学镜像源3 网易镜像源4 华为镜像源5 阿里云Rocky镜像源6 阿里云anolis镜像源 Redh
  • SuSE Enterprise linux安装mysql笔记

    目录 前言1 下载mysql二进制安装包2 解压MySQL安装包3 创建MySQL用户4 初始化mysql实例5 首次登录mysql6 修改登录密码 前言 本次安装MySQL的版本是8 0 30的二进制压缩包 xff0c 安装环境是SuSE
  • PostgresSql在linux下源码安装笔记

    目录 前言1 下载源码包并上传2 编译源码并安装3 本地登录PostgreSql4 客户端登录PostgreSql 前言 PostgreSql安装版本是14 5 xff0c 安装环境是Redhat Enterprise Linux serv
  • 判断两个IP地址(ipv4)是否在同一个网段

    我们通常会遇到的ip地址是这样的 xff1a ip地址 xff1a 192 168 227 205 子网掩码 xff1a 255 255 255 0 ip地址 xff1a 192 168 226 202 子网掩码 xff1a 255 255
  • 局域网搭建Linux镜像源

    前言 一般情况在企业的局域网内 xff0c 是不连接外网的 xff0c 所以像阿里云这样的在线的镜像源就用不了 xff0c 我相信大家个人在虚拟机里面连的就是阿里云镜像源了 xff0c 而且局域网内服务器较多的话 xff0c 本地挂载镜像源

随机推荐

  • ubuntu22.04 server安装

    目录 1 安装首页2 选择安装语言3 安装器4 选择键盘布局5 选择安装类型6 设置网络连接7 配置镜像源地址8 磁盘分区9 创建登录用户10 配置安装openssh server11 配置安装其他额外的软件12 开始安装系统13 重启系统
  • linux安装OceanBase数据库

    1 下载OceanBase数据库安装包 OceanBase官网下载页面 2 解压安装包并安装 tar xzf oceanbase all in one 4 0 0 0 beta 100120221102135736 el7 x86 64 t
  • linux下安装mysql客户端client

    1 下载mysql客户端 MySQL的Linux客户端官网下载地址 根据Linux的系统版本选择下载对应的rpm安装包 xff08 如下所示 xff09 xff0c 这里选择的是mysql8 0 27版本的redhat8系列的MySQL客户
  • linux下mysql的三种安装方法

    目录 1 离线安装 xff08 tar gz安装包 xff09 2 离线安装 xff08 rpm安装包 xff09 3 在线安装 xff08 yum安装 xff09 前言 安装环境 Redhat Enterprise Linux 8 1 离
  • linux+window+macos下的JDK安装

    1 Linux中安装JDK xff08 1 xff09 下载Linux版本的jdk压缩包 xff08 2 xff09 解压 tar zxvf 压缩包名 例如 xff1a tar zxvf jdk 8u251 linux x64 tar gz
  • bootstrap-table源码函数解读-sprintf

    var sprintf 61 function str var args 61 arguments flag 61 true i 61 1 str 61 str replace s g function var arg 61 args i
  • openGauss数据库的使用

    目录 前言1 启动 停止 重启数据库 xff08 1 xff09 极简版启动 停止 重启命令 xff08 2 xff09 企业版启动 停止 重启命令 2 登录数据库 xff08 1 xff09 登录数据库时的基本连接参数 xff08 2 x
  • openGauss数据库的安装(2.0.0极简版安装)

    目录 前言1 安装环境准备2 创建用户和用户组3 正式安装4 启动数据库实例并测试 前言 这里主要结合官网的文档 xff0c 安装系统环境是官网推荐的openEuler 20 03LTS openGauss数据库版本是openGauss 2
  • openGauss数据库安装(2.0.0企业版安装)

    目录 1 准备环境2 预安装3 正式安装4 启动并登录数据库 前言 此次数据库的系统安装环境仍然是openEuler20 03LTS openGauss安装版本是2 0 0版本 xff0c 相对于极简版安装 xff0c 确实多了一些工具 x
  • openEuler22.03安装

    目录 1 安装2 登录3 修改登录密码输错限制次数 1 安装 如果在此时没有设置网络 xff0c 那么需要在登录后可以编辑 etc sysconfig network scripts ifcfg ens160文件 xff0c 如下红框部分所
  • Linux查看日志常用命令

    第一种 xff1a 查看实时变化的日志 比较吃内存 最常用的 xff1a tail f app log 默认最后10行 xff0c 相当于增加参数 n 10 tail 200f app log 最后200行 xff0c 某一时刻往前推 Ct
  • ubuntu查看文件和文件夹大小

    在实际使用ubuntu时候 xff0c 经常要碰到需要查看文件以及文件夹大小的情况 有时候 xff0c 自己创建压缩文件 xff0c 可以使用 ls hl 查看文件大小 参数 h 表示Human Readable xff0c 使用GB MB
  • NLTK下载错误的终极解决办法

    Downloading package brown to C Users Ken AppData Roaming nltk data Error downloading 39 brown 39 from lt https raw githu
  • Tensorboard 不显示数据的问题

    No dashboards are active for the current data set Probable causes You haven 39 t written any data to your event files Te
  • Pytorch学习(2)——训练词向量的代码

    教程 xff1a https www bilibili com video BV1vz4y1R7Mm p 61 2 span class token keyword import span torch span class token ke
  • Windows 在不修改主题色的情况下将标题栏修改为黑色

    有些软件使用深色模式之后标题栏仍然是白色的 xff0c 很不美观 如果在 Windows 10 的设置中 xff0c 将个性化 颜色 在以下区域显示主题色 标题栏和窗口边框 选中 xff0c 那么标题栏可以带颜色 此时如果将主题色改为彩色
  • 解决debian(jessie)没有声音的问题

    先检查系统声卡驱动 lspci grep Audio 00 1b 0 Audio device Intel Corporation 82801I ICH9 Family HD Audio Controller rev 03 说明系统已经识别
  • 笔记类软件总结

    我大致把笔记类软件分为三类 xff1a 传统文档 思维导图 专业软件 1 传统文档 Typora 最经典的本地软件应该是 Typora 支持 Markdown 的实时预览 xff0c 界面简洁美观 使用基于 Chromium 浏览器的 El
  • Golang Map 基本原理

    Go 语言中的 map 即哈希表 哈希表把元素分到多个桶里 xff0c 每个桶里最多放8个元素 在访问元素时 xff0c 首先用哈希算法根据 key 和哈希表种子获得哈希值 暂将其命名为 h xff0c 然后利用 h 的低 b b b 位得
  • Go 汇编器指南

    A Quick Guide to Go s Assembler Go汇编器指南 This document is a quick outline of the unusual form of assembly language used b