Go的汇编器快速指南

Posted xxx小M

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Go的汇编器快速指南相关的知识,希望对你有一定的参考价值。

本文档简要介绍了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),使名称仅在当前源文件中可见,例如staticC文件中的顶级声明。在名称中添加偏移量是指距符号地址的偏移量,因此 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不会这样。)在符号之后,参数是标志(请参见下文)和帧大小,常数(但请参见下文):

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使用简单名称从Go源代码中调用以进行打包profileloop

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

DATA    symbol+offset(SB)/width, value

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

GLOBL指令将符号声明为全局符号。参数是可选标志,数据的大小声明为全局,除非DATA指令已将其初始化,否则其初始值为全零。该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

声明并初始化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项。)函数是调用堆栈的顶部。回溯应在此功能处停止。

与Go类型和常量进行交互

如果一个包中有任何.s文件,go build则将指示编译器发出一个特殊的标头go_asm.h,该标头可以被.s文件使用#include。该文件包含#define用于Go结构字段的偏移量,Go结构类型的大小以及const当前包中定义的大多数Go声明的符号常量。Go汇编应避免对Go类型的布局进行假设,而应使用这些常量。这提高了汇编代码的可读性,并使它对于Go类型定义或Go编译器使用的布局规则中的数据布局更改具有鲁棒性。

常数是形式。例如,给定Go声明,汇编代码可以将此常量的值引用为。 const__name_`const bufSize = 1024`const_bufSize

字段偏移量的格式为。结构尺寸为形式。例如,考虑以下Go定义: _type___field_`_type___size`

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

程序集可以将此结构的大小称为reader__size,两个字段的偏移量称为reader_bufreader_r。因此,如果寄存器R1包含指向的指针reader,则程序集可以将该r字段引用为reader_r(R1)

如果这些#define名称中的任何一个含糊不清(例如,具有_size字段的结构),#include "go_asm.h"将失败并显示“重新定义宏”错误。

运行时协调

为了使垃圾回收正确运行,运行时必须知道指针在所有全局数据和大多数堆栈帧中的位置。Go编译器在编译Go源文件时会发出此信息,但是汇编程序必须明确定义它。

标记有NOPTR标志的数据符号(请参见上文)被视为不包含指向运行时分配的数据的指针。具有RODATA标志的数据符号分配在只读存储器中,因此被视为隐式标记NOPTR。总大小小于指针的数据符号也被视为隐式标记NOPTR。不能在程序集源文件中定义包含指针的符号。这样的符号必须在Go源文件中定义。即使没有DATAandGLOBL指令,汇编源仍然可以按名称引用符号。一个好的通用经验法则是RODATA 在Go中而不是在汇编中定义所有非符号。

每个函数还需要注释,以在其参数,结果和本地堆栈框架中提供活动指针的位置。对于没有指针结果且没有本地堆栈框架或没有函数调用的汇编函数,唯一的要求是在同一包中的Go源文件中为该函数定义Go原型。汇编函数的名称不得包含程序包名称组件(例如,Syscall程序包中的函数syscall应使用该名称·Syscall而不是syscall·SyscallTEXT伪指令中的等效名称 )。对于更复杂的情况,需要显式注释。这些注释使用标准#include文件中定义的伪指令 funcdata.h

如果函数没有参数且没有结果,则可以省略指针信息。这由指令 上的参数大小注释表示。否则,Go原型必须为Go源文件中的函数提供指针信息,即使不是直接从Go调用的汇编函数也是如此。(原型还将让我们检查参数引用。)在函数开始时,假定参数已初始化,但结果假定未初始化。如果结果将在调用指令期间保留活动指针,则该函数应首先将结果清零,然后执行伪指令$_n_-0`TEXTgo` `vetGO_RESULTS_INITIALIZED。该指令记录了结果现在已初始化,并且应该在堆栈移动和垃圾回收期间对其进行扫描。通常更容易安排汇编函数不返回指针或不包含调用指令。在标准库中没有使用汇编函数 GO_RESULTS_INITIALIZED`。

如果函数没有本地堆栈帧,则可以省略指针信息。这由指令 上的本地帧大小注释指示。如果该函数不包含任何调用指令,则指针信息也可以省略。否则,本地堆栈帧不得包含指针,并且程序集必须通过执行伪指令来确认这一事实。由于通过移动堆栈来实现堆栈大小调整,因此在任何函数调用期间堆栈指针都可能发生变化:即使堆栈数据指针也不得保留在局部变量中。 $0-_n_`TEXT`NO_LOCAL_POINTERS

应当始终为汇编函数提供Go原型,以提供参数和结果的指针信息,并go vet检查用于访问它们的偏移量是否正确。

以上是关于Go的汇编器快速指南的主要内容,如果未能解决你的问题,请参考以下文章

Android 逆向使用 Python 解析 ELF 文件 ( Capstone 反汇编 ELF 文件中的机器码数据 | 创建反汇编解析器实例对象 | 设置汇编解析器显示细节 )(代码片段

Golang内部构件,第5部分:运行时引导程序

X.86 X64 汇编器中的正确堆栈操作

JAVA开发者的Golang快速指南

[Go] go-nsq 使用指南

go 生成汇编代码