关于golang:Go的汇编器快速指南

3次阅读

共计 6097 个字符,预计需要花费 16 分钟才能阅读完成。

本文档简要介绍了gcGo 编译器应用的非常规模式的汇编语言。该文件不全面。

汇编程序基于 Plan 9 汇编程序的输出款式,在其余中央具体介绍了该款式。如果您打算编写汇编语言,则只管其中大部分是特定于 Plan 9 的,但您仍应浏览该文档。以后文档提供了语法摘要以及与该文档中所解释内容的区别,并形容了编写汇编代码以与 Go 交互时实用的个性。

对于 Go 的汇编器,最重要的事件是它不是底层机器的间接示意。一些细节正好映射到机器,但有些则不然。这是因为编译器套件(请参见此 形容)不须要在惯例管道中传递任何汇编程序。取而代之的是,编译器对一种半形象的指令集进行操作,并且指令抉择局部产生在代码生成之后。汇编程序以半形象模式工作,因而当您看到相似MOV 工具链实际上为该操作生成的内容可能基本不是挪动指令,可能是革除指令或加载指令。或者它可能与该名称的机器指令齐全对应。通常,特定于机器的操作偏向于本人呈现,而更通用的概念(如内存挪动和子例程调用与返回)则更为形象。具体细节因架构而异,咱们对此不谨严深表歉意。状况尚不明确。

汇编程序是解析该半形象指令集的形容并将其转换为要输出到链接器的指令的一种形式。如果要查看给定体系结构(例如 amd64)的汇编指令的外观,则规范库的源代码中有许多示例,例如 runtimemath/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
...

FUNCDATAPCDATA 指令蕴含用于通过垃圾收集器的应用的信息; 它们由编译器引入。
要查看链接后放入二进制文件的内容,请应用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 not 3&(1<<2)。同样,常量始终被评估为 64 位无符号整数。因而-2,不是整数值减去 2,而是具备雷同位模式的无符号 64 位整数。辨别很少有关系,但要防止在设置右操作数的高位时防止不置可否,除法或右移。

符号

一些符号(例如 R1LR)是预约义的,并援用寄存器。确切的设置取决于体系结构。
有四个事后申明的符号援用伪寄存器。这些不是真正的寄存器,而是由工具链保护的虚构寄存器,例如帧指针。伪寄存器的汇合对于所有体系结构都是雷同的:

  • FP:框架指针:参数和局部变量。
  • PC:程序计数器:跳转和分支。
  • SB:动态根本指针:全局符号。
  • SP:堆栈指针:堆栈顶部。
    所有用户定义的符号均作为伪寄存器FP(参数和局部变量)和SB(全局变量)的偏移量写入。

SB 伪寄存器能够被认为是记忆的原点上,所以符号 foo(SB) 的名称是foo 在内存中的地址。该表格用于命名全局性能和数据。将 <> 名称增加到中(如中)foo<>(SB),使名称仅在以后源文件中可见,例如 static C 文件中的顶级申明。在名称中增加偏移量是指距符号地址的偏移量,因而 foo+4(SB) 距的结尾四个字节。

FP伪寄存器是用来指函数参数的虚构帧指针。编译器保护一个虚构帧指针,并将堆栈上的参数援用为该伪寄存器的偏移量。因而,0(FP)是该函数的第一个参数,8(FP)是第二个参数(在 64 位计算机上),依此类推。然而,以这种形式援用函数自变量时,必须在 first_arg+0(FP) 和结尾搁置一个名称 second_arg+8(FP)。(偏移量的含意(与帧指针的偏移量不同)与它与 with 的用法不同SB,此处偏移量是符号的偏移量。)汇编程序强制执行此约定,回绝 plain0(FP)8(FP)。理论名称在语义上无关紧要,但利用于记录自变量名称。值得强调的是FP 即便在具备硬件帧指针的体系结构上,始终是伪寄存器,而不是硬件寄存器。

对于带有 Go 原型的汇编函数,go vet将查看参数名称和偏移量是否匹配。在 32 位零碎上,通过在名称中增加 a _lo_hi 后缀来辨别 64 位值的低 32 位和高 32 位,如 arg_lo+0(FP) 或中所示arg_hi+4(FP)。如果 Go 原型未命名其后果,则预期的程序集名称为ret

SP伪寄存器是用来指帧局部变量的虚构堆栈指针和函数调用正在编写的参数。它指向本地堆栈帧的顶,所以援用应在范畴[-framesize,0)应用负偏移:x-8(SP)y-4(SP),等。

在具备名为的硬件寄存器的体系结构上 SP,名称前缀将对虚构堆栈指针的援用与对体系结构SP 寄存器的援用辨别开。也就是说,x-8(SP)并且 -8(SP) 是不同的内存地位:第一个援用虚构堆栈指针伪寄存器,而第二个援用硬件的SP 寄存器。

在一些机器上 SP,并PC 在传统的物理,地址寄存器中的别名,在围棋汇编的名称 SPPC 依然非凡解决; 例如,援用 SP 须要一个符号,就像 FP。要拜访理论的硬件寄存器,请应用实在R 名称。例如,ARM 架构的硬件 SPPC 是可拜访 R13R15

分支和间接跳转总是写为 PC 的偏移量或标签的跳转:

label:
    MOVW $0, R1
    JMP label

每个标签仅在定义它的函数中可见。因而,容许文件中的多个性能定义和应用雷同的标签名称。间接跳转和调用指令能够定位文本符号,例如name(SB),但不能定位符号的偏移量,例如name+4(SB)

指令,寄存器和汇编器指令始终位于大写字母中,以提醒您汇编编程是一项艰巨的工作。(例外:gARM 上的寄存器重命名。)

在 Go 指标文件和二进制文件中,符号的全名是程序包门路,后跟一个句点和符号名称:fmt.Printfmath/rand.Int。因为汇编程序的解析器将句点和斜杠视为标点符号,因而这些字符串不能间接用作标识符名称。相同,汇编器容许在标识符中应用两头点字符 U + 00B7 和分隔斜杠 U + 2215,并将它们重写为纯句点和斜杠。在汇编程序源文件中,以上符号示意为 fmt·Printfmath∕rand·Int。编译器在应用 -S 标记时生成的汇编清单间接显示了句点和斜杠,而不是汇编程序要求的 Unicode 替换。

大多数手写的汇编文件都没有在符号名称中蕴含残缺的程序包门路,因为链接器会在句点开始的任何源名称的结尾插入以后对象文件的程序包门路:在 math / rand 中的汇编源文件中包实现中,包的 Int 函数能够称为·Int。这种约定防止了在本人的源代码中对包的导入门路进行硬编码的须要,从而使将代码从一个地位挪动到另一个地位变得更加容易。

指令

汇编器应用各种指令将文本和数据绑定到符号名称。例如,这是一个简略的残缺函数定义。该 TEXT 伪指令申明符号runtime·profileloop 和前面的指令形成函数的主体。TEXT块中的最初一条指令必须是某种模式的跳转,通常是RET(伪)指令。(如果不是,则链接器将追加一个跳转至本身的指令;中不存在任何穿透TEXTs。)在符号之后,参数是标记(请参见下文)和帧大小,常数(但请参见下文):

文本运行时·profileloop(SB),NOSPLIT,$ 8 MOVQ $ runtime·profileloop1(SB),CX MOVQ CX,0(SP)CALL 运行时·内部线程处理程序(SB)RET

在个别状况下,帧大小后跟参数大小,并用减号分隔。(这不是减法,只是特有的语法。)框架大小 $24-8 指出该函数具备 24 字节的框架,并应用 8 个字节的参数进行调用,该参数位于调用方的框架上。如果 NOSPLIT 未为指定 TEXT,则必须提供参数大小。对于带有 Go 原型的汇编函数,go vet 将查看参数大小是否正确。

请留神,符号名称应用两头的点分隔组件,并被指定为与动态根本伪寄存器的偏移量 SB。该函数将runtime 应用简略名称从 Go 源代码中调用以进行打包profileloop

全局数据符号由一系列初始化 DATA 指令及其 后的 GLOBL 指令定义。每个 DATA 指令都会初始化相应内存的一部分。未显式初始化的内存将清零。DATA指令的个别模式是

数据符号 + 偏移量(SB)/ 宽度,值

它以给定的偏移量和宽度应用给定的值初始化符号存储。DATA给定符号的指令必须以减少的偏移量编写。

GLOBL 指令将符号申明为全局符号。参数是可选标记,数据的大小申明为全局,除非 DATA 指令已将其初始化,否则其初始值为全零。该 GLOBL 指令必须遵循任何相应的 DATA 指令。

例如,

数据 divtab <> + 0x00(SB)/ 4,$ 0xf4f8fcff 数据 divtab <> + 0x04(SB)/ 4,$ 0xe6eaedf0 … 数据 divtab <> + 0x3c(SB)/ 4,$ 0x81828384 GLOBL divtab <>(SB),RODATA,64 美元
GLOBL 运行时·tlsoffset(SB),NOPTR,$ 4

申明并初始化 divtab<> 一个 4 字节整数值的只读 64 字节表,并申明 runtime·tlsoffset 一个不蕴含指针的 4 字节隐式清零变量。

指令可能有一个或两个参数。如果有两个,则第一个是标记的位掩码,能够将这些标记写为数字表达式,或者加在一起或累加起来,或者能够进行符号设置以不便人类排汇。在规范 #include 文件中定义的它们的值textflag.h 是:

  • NOPROF= 1
    (对于 TEXT 我的项目。)不要剖析标记的性能。不举荐应用此标记。
  • DUPOK= 2
    在单个二进制文件中具备此符号的多个实例是非法的。链接器将抉择要应用的反复项之一。
  • NOSPLIT= 4
    (对于 TEXT 项。)请勿插入序言以查看是否必须拆分堆栈。例程的框架及其所调用的所有内容必须适宜堆栈段顶部的备用空间。用于爱护例程,例如堆栈拆分代码自身。
  • RODATA= 8
    (用于 DATAGLOBL。)将此数据放在只读局部中。
  • NOPTR= 16
    (用于 DATAGLOBL我的项目。)此数据不蕴含指针,因而不须要由垃圾收集器进行扫描。
  • WRAPPER= 32
    (对于 TEXT 项。)这是包装函数,不应算作禁用recover
  • NEEDCTXT= 64
    (对于 TEXT 项。)此函数是一个闭包,因而它将应用其传入的上下文寄存器。
  • LOCAL= 128
    此符号位于动静共享库的本地。
  • TLSBSS= 256
    (用于 DATAGLOBL我的项目。)将此数据放入线程本地存储中。
  • NOFRAME= 512
    (对于 TEXT 项。)即便这不是叶函数,也不要插入指令来调配堆栈帧并保留 / 复原返回地址。仅在申明帧大小为 0 的函数上无效。
  • TOPFRAME= 2048
    (对于 TEXT 项。)函数是调用堆栈的顶部。回溯应在此性能处进行。
正文完
 0