七叶笔记 » golang编程 » Golang 汇编器快速指南

Golang 汇编器快速指南

汇编器基于 Plan 9 汇编器的输入风格。详细文档在这里。如果你准备写一些汇编语言,那么虽然此文档是基于 Plan 9 的,你也应该通读。本文提供了语法摘要和与其解释内容的区别,并且描述了编写汇编与 Go 交互时所适用的特性。

最重要的是,Go 的汇编器并不是底层的直接表示。有一些是直接的映射,有一些不是。这是因为编译套件在常规流程中并不需要汇编器。相反,编译器针对一种半抽象的指令集操作,而且指令选择发生在一部分发生在代码生成之后。汇编器工作在半抽象状态下,所以当你看到类似指令 MOV,工具链实际生成的操作也许并不是移动,而是清除指令或者加载指令。或者也可能与实际指令完全对应。一般来说,特定机器的操作倾向于它们自身,而更通用的概念(如内存移动、子程序调用、返回等)则更加抽象。细节根据体系架构会有不同,我们非常抱歉关于这种不精确,情况还没有完全定义。

汇编器程序是一种解析半抽象指令集的描述并将其转换为输入给链接器指令的方法。如果想要看到给定体系的汇编指令(如:amd64),在标准库源码中有很多实例(比如 runtime math/big)。你可以测试编译器生成的汇编代码(实际输出可能和这里有出入):

 $ 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
...
复制代码  

FUNCDATA 和 PCDATA 包含了垃圾回收使用的信息,会在编译器中介绍。

想要查看链接后放入二进制文件中的内容,使用 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)
复制代码  

常量

尽管汇编器遵循 Plan 9 的规范,但这是一个独立程序,会有一些不同。其中一个就是常量评估。汇编器中的常量表达式是按照 Go 的操作符优先级来解析的,而不是类 C 的优先级。所以, 3&1<<2 是 4 而不是 0。它按照 (3&1)<<2 来解析,而不是 3&(1<<2)。而且,常量会被解析为 64位无符号整形。所以 -2 不是整形数减2,而是具有同样位信息的64位无符号整形。这个区别基本没什么影响,除了要注意避免在对右操作数高位置位的情况下进行歧义操作、除法、右移。

符号

某些符号,例如 R1 或者 LR,已经预定义,并且指向了寄存器。具体的符号集取决于体系架构。

有4个预先声明的符号,指向了位寄存器。它们不是真的寄存器,而是工具链维护的虚拟寄存器,例如栈帧指针。伪寄存器集合在所有体系架构上都是一样的:

  • FP: 栈帧指针:参数和本的变量;
  • PC: 程序计数器:跳转和分支;
  • SB: 静态基础指针:全局符号;
  • SP栈指针:栈顶

所有用户定义的符号会被写为伪寄存器 FP 和 SB 加偏移量的方式。

伪寄存器 SB 可以看做是原始内存,所以符号 foo(SB) 是 foo 在内存中的地址。这种形式被用作全局命名函数和数据。给名字加上 <> ,foo<>(SB) 使得名字只在当前源文件可见,类似 C文件 中顶级静态声明。给名字添加偏移量指向从符号的地址开始加上偏移量的地址,所以 foo+4(SB) 表示从 foo 开始偏移4个字节的地址。

伪寄存器 FP 是虚拟栈帧指针,被用作指向函数的参数。编译器维护了一个虚拟的栈帧指针指向了栈中的参数,表示为伪寄存器+偏移量的形式。所以, 0(FP) 是函数的第一个参数,8(FP) 是第二个(64位机器上),以此类推。当指向一个函数的参数时,必须要在开头加上名字,比如 first_arg+0(FP) 或者 second_arg+8(FP) 。

汇编器强制做了要求,不会解析原生 0(FP) 和 8(FP) 这种形式。真实的名字是和语义无关的,但是应该被用作记录变量的名字。这样可以保持 FP 始终是伪寄存器,而不是一个硬件寄存器,即使是架构体系中有硬件的栈帧指针。

对于带有 Go 原始类型的汇编函数,go vet 会检查参数名称和偏移量是否吻合。在32位系统上,64位值的低32位和高32位通过添加 _lo 或者 _hi 的后缀来区分,写作 arg_lo+0(FP) 或者 arg_hi+4(FP) 。如果 Go 原始类型的返回值没有命名,那么汇编名字就是 ret。

伪寄存器 SP 是一个虚拟栈指针,被用作指向栈帧本的变量和函数调用的参数。它指向本地栈帧的顶部,所以引用时应该使用负的 offset 值,范围是:[-栈帧大小,0)。比如:x-8(SP) 或者 y-4(SP) 等等。

如果体系架构带有硬件寄存器 SP,名字前缀就会区分使用的是虚拟栈帧指针还是体系架构中的 SP 寄存器。x-8(SP) 和 -8(SP) 是不同的内存地址,前者表示虚拟栈指针为寄存器,后者表示硬件 SP 寄存器。

在机器上,SP 和 PC 是物理编号寄存器的传统别名。在 Go 汇编器中, SP 和 PC也会被特殊处理。例如,引用 SP 需要一个符号,类似 FP。为了访问实际的硬件寄存器,使用真实的 R 名称,比如在 ARM 架构下, 硬件 SP 和 PC 同样可以使用 R13/R15 来访问。

分支和直接跳转一直被写作 PC + offset,或跳转到 Labels。

 label:
    MOVW $0, R1
    JMP label
复制代码  

每个 label 只在其定义函数的内部可见。因为可以在一个文件中、不同的函数中定义使用相同名称的 label。直接跳转和调用指令可以以文字符号为目标(name(SB)),但不能是 符号+偏移量(name+4(SB))。

指令,寄存器和汇编器指令会一直使用大写,来提醒你汇编编程是一个大工程。

在 Go 目标文件和二进制文件中,符号的全名是包路径后面跟一个句点和符号名:fmt.Printf 或者 math/rand.Int 。由于汇编器的解析器将句点和斜杠视作标点符号,这些字符就不能直接被用作标识名称。而汇编器允许在标识符中使用中间点字符 U+00B7 和 分割斜杠 U+2215,并重写为纯句点和斜杠。在汇编源文件中,上面的符号被写为 fmt·Printf/ math/rand·Int。编译器在使用-S参数时生成的汇编列表直接显示了句号和斜杠,而不是汇编程序要求的Unicode来替代。

大部分手写汇编文件在符号名称上不包含包全路径,因为链接器在任何以句点开头的名字前,插入了当前对象文件的包路径。比如:在 math/rand 包的视线中,包中 Int 函数可以引用为 .Int。这个约定避免了在自己的源码中硬编码包的导入路径,使得将代码迁移变得更加容易。

指令

汇编器使用了多种指令将 text 和 数据整合到符号名称上。比如,这有一个简单的完整函数定义。TEXT 指令表明符号 runtime·profileloop ,下面的指令是函数体。TEXT 最后一条指令必须是一种跳转指令,一般是 RET伪指令。如果没有的话, 链接器会添加跳转到自己的指令,TEXT 中没有直接通往下一个 TEXT 的能力。符号之后,参数是标识符、帧大小、指令:

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

一般情况下,栈帧大小跟着参数大小,用减号分割(不是减法,知识一种特殊语法)。栈帧大小 $24-8 表示函数有 24个字节的栈帧,而且调用时,参数是 8个字节,参数在调用方的栈帧中。如果 NOSPLIT 在 TEXT 没有指定,则必须要提供参数大小。对于带有 Go 原始类型的汇编函数, go vet 将会检查参数大小是否正确。

注意,符号名称使用中句点来分割组件,并且表示为 静态基础伪寄存器 SB +偏移量。这个函数可以被 runtime 包使用 profileloop 直接调用。

全局数据符号被GLOBL指令和一系列初始化的 DATA 指令所定义。每一个 DATA 指令初始化一块对应内存。默认内存不会被初始化为0。DATA 指令一般形式是:

 DATA    symbol+offset(SB)/width, value
复制代码  

DATA 使用给定的值,在给定的偏移量和宽度上,初始化符号内存。给 DATA 指令的符号,必须写成正偏移量。

GLOBL 指令声明了一个符号是全局的。参数是可选的标志,数据大小也被声明为全局的,如果没有被 DATA初始化,则将会被用0初始化。GLOBL 指令后面必须跟着对应的 DATA 指令。举例如下:

 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
复制代码  

声明和初始化一个只读的4字节整数值的64字节表 divtab<>,并声明一个4字节不包含指针的0值变量 runtime·tlsoffset。

这里可能有一两个参数给到指令。如果有两个,第一个是标志位掩码,可以将标志写为数字表达式,或者累加起来或者可以符号化以便人类可读。这些值定义在标准 #include 文件 textflag.h , 它们是:

  • NOPROF = 1 (TEXT部分) 不要分析标记的函数,这个标志已经废弃了。
  • DUPOK = 2 在一个二进制文件中可以有多个此符号实例。链接器会挑选一个去使用。
  • NOSPLIT = 4 (TEXT 部分) 不要插入序言来检查栈是否需要被分割。协程的帧加上其调用,必须在栈帧顶部的空余空间放得下。通常为了保护协程,例如栈拆分代码本身。
  • RODATA = 8 (DATA 和 GLOBL 部分) 将数据放到只读段。
  • NOPTR = 16 (DATA 和 GLOBL 部分) 此数据不包含指针,所以不需要被 GC 扫描。
  • WRAPPER = 32 (TEXT 部分) 这是个封装函数,而且不应该被视为禁用 recover。
  • NEEDCTXT = 64 (TEXT 部分) 这个函数是闭包,所以使用其传入的上下文寄存器。
  • LOCAL = 128 这个符号是在动态共享库本地。
  • TLSBSS = 256 (DATA 和 GLOBL 部分) 将此数据放入线程本地存储。
  • NOFRAME = 512 (TEXT 部分) 即使不是叶子函数,也不要插入指令来分配栈帧和存储、恢复返回地址。仅在声明栈帧大小为0时有用。
  • TOPFRAME = 2048 (TEXT 部分) 函数在调用栈顶部,调用追踪在此处停止。

运行时的协调

为了让垃圾回收器正确运行,运行时必须知道所有全局数据中和大部分的栈帧中的指针所在地址。Go 编译器在编译 Go 源文件时会暴露这些信息,但是汇编程序必须显式的定义它。

如果数据符号被带有 NOPTR 标志,就会被当做不包含运行时数据指针。如果数据符号带有 RODATA 标志,表示在只读内存分配,也相当于显示声明 NOPTR 标记。一个数据符号总大小小于一个指针,则相当于显示声明 NOPTR。在汇编源文件不可能定义包含指针的符号,这种符号必须在 Go 源文件中定义。汇编源代码还可以不用 DATA 和 GLOBL ,用名称指向一个符号。一个好的通用经验法则是,在 Go 中定义定义所有不是 RODATA 的符号,而不是在汇编中定义。

每个函数还需要注解,以给出在参数、返回、本地栈帧中存活指针的位置。对于没有指针返回,也没有本地栈帧和函数调用的汇编函数,仅要求在同包下的 Go 源文件中定义函数的 Go 原始类型。汇编函数的名字必须包含包名部分(比如:函数 Syscall 在包 syscall 下面,在它的TEXT部分应该使用名字 ·Syscall 替代相同名字 syscall·Syscall)。对于更加复杂的情形来说,需要显示注解。这些注解使用标准 #include 文件 funcdata.h 中定义的伪寄存器。

如果函数没有参数、返回值,那么指针信息可以省略。这个会被 TEXT 指令下的 $*n*-0 参数大小注解指定。否则,Go源文件中函数的指针信息必须被 Go 原始类型提供,甚至不是被 Go 直接调用的汇编函数。(原型也会使用 go“vet 检查参数引用) 函数开始的地方,参数假设已经被初始化,而返回值则假设没有被初始化。如果调用指令期间返回值持有存活的指针,那么函数应该以0值化的返回值开始,并且执行GO_RESULTS_INITALIZED 伪指令。这个指令记录了返回值已经被初始化,并且应该在栈移动时或垃圾回收期间被扫描。一般来说让汇编函数不返回指针或者不包含调用指令会更加容易一点。标准库中没有汇编函数使用了 GO_RESULTS_INITIALIZED。

如果一个函数没有本地栈帧,那么指针信息可以被省略。这个会被 TEXT 指令下的 $*n*-0 参数大小注解指定。如果函数没有调用指令,那么指针信息也可以被省略。除此之外,本地栈帧不得包含指针,而且汇编必须使用伪指令 NO_LOCAL_POINTERS 来确认。因为调整栈大小是通过移动栈来实现的,所以在任何函数调用期间栈指针都可能改变:即使指向栈数据的指针,也禁止保存在本地变量中。

应该始终为汇编函数提供 Go 原始类型,以便为参数和返回值提供指针信息,同时使用 go vet 检查使用的偏移量是否正确。

特殊体系架构中的细节

列出每种机型的所有指令和其他细节是不切实际的。要查看给定机型定义了哪种指令集,比如 ARM,可以在目录 src/cmd/internal/obj/arm 查看该架构的 obj 支持库。在目录下有一个文件 a.out.go,它包含了以 A 开头的长长的常量列表,类似下面:

 const (
    AAND = obj.ABaseARM + obj.A_ARCHSPECIFIC + iota
    AEOR
    ASUB
    ARSB
    AADD
    ...
复制代码  

这是该架构的汇编器和链接器已知的指令及其拼写列表。在列表中,每个指令以一个大写的 A 开头,所以 AAND 是代表了按位与指令 AND(去掉前缀 A),并在汇编代码中写作 AND。命令枚举大部分按照字母顺序(在 cmd/internal/obj 中定义的架构体系无关的 AXXX 指令,代表无效的指令)。名字顺序和实际编码无关。cmd/internal/obj 负责细节。

386和 AMD64 共有的指令列在了 cmd/internal/obj/x86/a.out.go

架构体系共享了一些通用寻址模式的语法,例如:(R1)间接寄存器,4(R1) 间接寄存器+偏移量,$foo(SB) 绝对地址。汇编器同样支持每种架构体系中一些特殊的寻址模式(不一定是全部),列在了下面。

前面各节的示例证明了数据指令的流向是从左到右:MOVQ $0, CX 清除了 CX。这个规则甚至可以用于那些与常规表示相反方向的体系结构。

下面是一些在支持的体系结构下, Go特定的关键细节说明:

32-bit Intel 386

MMU中一些未使用的寄存器(就Go而言)维护着指向 g 结构的运行时指针。如果源码是在 runtime 包,而且包含了特定的 header go_tls.h,那么,会为汇编器定义一个操作系统相关的宏 get_tls:

 #include "go_tls.h"
复制代码  

在运行时中,宏 get_tls 加载 g 的指针的指针到它的参数寄存器中,g 结构体包含了一个 m 指针。这有另外一个特殊的 header 包含了 g 每个元素的偏移量,叫做 go_asm.h 。用 CX 读取 g 和 m 的顺序如下:

 #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.
复制代码  

注意:上面的代码仅可以在 runtime 包有效, get_tls.h 也适用于 arm, amd64 and amd64p32, go_asm.h 适用于所有的体系结构。

寻址模式:

  • (DI)(BX*2): DI 加 BX*2 的地址。
  • 64(DI)(BX*2): DI 加 BX*2 加 64 的地址位置。这个模式仅仅接受1,2,4,8 作为比例因子。

当使用 编译器和汇编器的 -dynlink 和 -shared 模式,任何在固定内存地址加载和存储(例如:全局变量),必须假设覆盖了 CX。因此,为了用户安全使用这些模式的原因,除了在内存引用上,汇编源码应该避免使用 CX 。

64-bit Intel 386 (别名 amd64)

两种体系在汇编器层面上,行为基本一致。64位版本的汇编代码访问 m 和 g 指针和32位 386 一样,只是前者使用 MOVQ 而不是 MOVL。

 get_tls(CX)
MOVQ    g(CX), AX     // Move g into AX.
MOVQ    g_m(AX), BX   // Move g.m into BX.
复制代码  

ARM

R10 和 R11 寄存器被编译器和链接器保留。

R10 指向 g (goroutine) 结构体。在汇编器源码中,这个指针必须叫做 g 。不会识别R10这个名称。

为了让人类和编译器更简单的写汇编,ARM 链接器允许使用单个硬件指令无法表达的普通寻址方式和伪指令,例如: DIV 和 MOD 。它用多个指令实现了这种形式,通常使用 R11 来保存临时变量。手写汇编可以使用 R11 ,但是要求确认链接器没有同样使用它来实现任何函数中其他的指令。

当定义一个 TEXT 时,指定栈帧大小 $-4 表示告诉链接器,这是一个叶子函数,也就是不需要在入口保存 LR 。

之前说过,名字 SP 总是表示一个虚拟的栈指针。如果是硬件寄存器,使用 R13。

条件代码语法是在指令后面添加句点和一两个字母的代码,比如:MOVW.EQ 。可能添加多种代码:MOVM.IA.W 。 代码修饰符顺序无关紧要。

寻址模式:

  • R0->16 R0>>16 R0<<16 R0@>16: 对于 << 左移 R0 16位。其他代码是: -> 算数右移, >> 逻辑右移, @> 向右旋转。
  • R0->R1 R0>>R1 R0<<R1 R0@>R1: 对于 << 左移 R0 R1位。其他代码是: -> 算数右移,>> 逻辑右移,@> 向右旋转。
  • [R0,g,R12-R15]: 多寄存器指令中,包含 R0 g, R12-R15 的集合。
  • (R5, R6): 目标寄存器对。

ARM64

ARM64端口处于试验状态。

R18 是平台寄存器,被 Apple 平台保留。为了不被误用,该寄存器命名为 R18_PLATFORM。R27 和 R28 被编译器和链接器保留。R29 是栈帧指针。R30 是链接寄存器。

指令修饰符会加在指令后,跟在句点后面。仅有 P 后递增 和 W 预递增 两个修饰符:MOVW.P MOVW.W。

地址模式:

  • R0->16 R0>>16 R0<<16 R0@>16: 和 32位 ARM 一样。
  • $(8<<12): 将8 左移 12位。
  • 8(R0): 将 R0的值 和 8 相加。
  • (R2)(R0): R0的值加上 R2的值。
  • R0.UXTB R0.UXTB<<imm: UXTB:
  • R0.SXTB R0.SXTB<<imm: SXTB: 提取R0的第8位,并扩展零值到R0的大小。R0.SXTB<<imm: 左移 R0.SXTB imm 位。imm 可以是 0,1,2,3,4。其他扩展包括 SXTH (32位) 和 SXTX(64位)。
  • (R5, R6): 寄存器对给LDAXP/LDP/LDXP/STLXP/STP/STP。

引用: Go ARM64 Assembly Instructions Reference Manual

PPC64

GOARCH 等于 ppc64 和 ppc64le 使用此汇编器。

引用: Go PPC64 Assembly Instructions Reference Manual

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

R10 和 R11 两个寄存器被保留。汇编器在汇编某些指令时用它们存放临时值。

R13 指向了 g(goroutine)结构体。此寄存器必须使用名字 g ,R13 这个名字不会被识别。

R15 指向栈帧,它应该只能通过虚拟寄存器 SP 和 FP 访问。

多个加载和存储指令对一系列寄存器进行操作。寄存器的范围由开始寄存器和结束寄存器指定。例如,LMG (R9), R5, R7 会使用 0(R9), 8(R9) 和 16(R9) 的 64位的值 加载到 R5, R6 和 R7 中。

像 MVC 和 XC 这样的存储-存储指令以长度作为第一个参数。比如:XC $8, (R9), (R9) 会清空R9 特定地址的 8个字节。

如果以长度或索引作为向量指令的参数,那么是第一个参数。比如,VLEIF $1, $16, V2 会将16加载到 v2 的 索引1 中。使用向量指令要确保他们在运行时是可用的。要使用向量指令,机器必须同时有向量功能(功能列表中的位129)和 内核支持。没有内核的支持,向量指令是无效的(将等同于一个 NOP 指令)。

寻址模式:

  • (R5)(R6*1): R5 值加 R6 的位置。 与x86一样它是一种缩放模式,但只有 1 被允许。

MIPS, MIPS64

通用寄存器的名字是 R0 – R31 ,浮点数寄存器是 F0 到 F31。

R30 被保留,指向 g 。R23 被用作临时寄存器。

在 TEXT 指令中, MIPS的为栈帧大小 $-4 ,MIPS64的栈帧大小为 $-8 表示链接器不需要保存 LR 。

SP 表示虚拟栈指针。硬件寄存器使用 R2 。

寻址模式:

  • 16(R1): R1 + 16 的位置。
  • (R1): 0(R1) 的别名。

通过预定义 GOMIPS_hardfloat 或者 GOMIPS_softfloat 可以使汇编代码使用 GOMIPS 环境变量 (hardfloat 或者 softfloat) 。

通过预定义 GOMIPS64_hardfloat 或者 GOMIPS64_softfloat 可以使汇编代码使用 GOMIPS64 环境变量 (hardfloat 或者 softfloat) 。

不支持的操作码

汇编器的设计宗旨是为了支援编译器,因此,不是所有体系平台的硬件指令都被定义出来:如果编译器不生成,那么就没有。如果你需要使用丢失的指令,有两个方法。一是升级汇编器以支持指令,这是最简单的,但是只有在多次使用时才值得去做。相反,对于简单的一次使用的情况,可以使用 BYTE 和 WORD 指令显示的将数据放到 TEXT 指令流中。这就是 386 运行时怎么定义 64位 原子加载函数的。

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

相关文章