Go语言汇编快速指南

开发 后端
本文以Go官方文档 A Quick Guide to Go's Assembler 为基础对Go汇编进行介绍。

如果想要深入了解Go语言,Go汇编是一个绕不过的环节。

本文以Go官方文档 A Quick Guide to Go's Assembler 为基础对Go汇编进行介绍。

Go汇编是在 Plan 9 汇编的基础上进化出的新版本。如果需要进一步深入学习,还是建议阅读A Manual for the Plan 9 assembler 。

关于 Go 的汇编,最重要的一点是它不是底层机器码的直接表示。而是进行了一层抽象,但是抽象的也不是很理想,所以称为semi-abstract instruction set(半抽象)。所以一些细节指令精确地映射到机器码,但有些没有,这可以在本文的示例代码中看到。

因为每种处理器架构的指令集、寄存器各不相同,所以汇编代码实现功能需要适配各种处理器架构;当然,也要适配各种操作系统。

环境

OS : Ubuntu 20.04.2 LTS; x86_64
Go : go version go1.16.2 linux/amd64

声明

操作系统、处理器架构、Go版本不同,均有可能造成相同的源码编译后运行时的寄存器值、内存地址、数据结构等存在差异。

本文仅包含 linux/amd64 系统架构下的 64 位可执行程序的示例。

本文仅保证学习过程中的分析数据在当前环境下的准确有效性。

代码清单

go.mod

module go-asm-guide
go 1.16

main.go

package main
import (
"fmt"
"unsafe"
)
type Text struct {
Language string
_ uint8
Length int
}
func add(a, b int) int
func addX(a, b int) int
// 获取 Text 的 Length 字段的值
func length(text *Text) int
// 获取 Text 结构体的大小
func sizeOfTextStruct() int
func main() {
println(add(1, 2))
println(addX(1, 2))
text := &Text{
Language: "chinese",
Length: 1024,
}
fmt.Println(text)
println(length(text))
println(sizeOfTextStruct())
println(unsafe.Sizeof(*text))
}

main.s

#include "textflag.h"
#include "go_asm.h" // 该文件自动生成
TEXT ·add(SB),NOSPLIT,$0-24
MOVQ a+0(FP), AX // 读取第一个参数
MOVQ b+8(FP), BX // 读取第二个参数
ADDQ BX, AX
MOVQ AX, ret+16(FP) // 保存结果
RET
TEXT ·addX(SB),NOSPLIT,$0-24
MOVQ a+0(FP), AX
MOVQ b+8(FP), BX
ADDQ BX, AX
MOVQ $x(SB), BX // 读取全局变量 x 的地址
MOVQ 0(BX), BX // 读取全局变量 x 的值
ADDQ BX, AX
MOVQ AX, ret+16(FP)
RET
TEXT ·length(SB),NOSPLIT,$0-16
MOVQ text+0(FP), AX
MOVQ Text_Length(AX), AX // 通过字段在结构体中的偏移量读取字段值
MOVQ AX, ret+8(FP)
RET
TEXT ·sizeOfTextStruct(SB),NOSPLIT,$0-8
MOVQ $Text__size, AX // 保存结构体的大小到 AX 寄存器
MOVQ AX, ret+0(FP)
RET
DATA x+0(SB)/8, $10 // 初始化全局变量 x, 赋值为 10
GLOBL x(SB), RODATA, $8 // 声明全局变量 x

常用命令

Go汇编的学习需要时间,不是天才、没有牢固的汇编基础的话,看一篇教程,花一天时间是不能立即熟练掌握的。

但是熟练使用工具能够使我们有效地学习Go汇编。

编译生成汇编

这是Go语言自带的功能,通过以下命令即可查看。当然还可以指定各种参数进行详细的研究,此处不再赘述。

  • go tool compile -S x.go
  • go build -gcflags -S x.go

$ cat x.go
package main

func main() {
println(3)
}
$ go tool compile -S x.go
"".main STEXT size=77 args=0x0 locals=0x10 funcid=0x0
0x0000 00000 (x.go:3) TEXT "".main(SB), ABIInternal, $16-0
0x0000 00000 (x.go:3) MOVQ (TLS), CX
0x0009 00009 (x.go:3) CMPQ SP, 16(CX)
0x000d 00013 (x.go:3) PCDATA $0, $-2
0x000d 00013 (x.go:3) JLS 70
0x000f 00015 (x.go:3) PCDATA $0, $-1
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:4) PCDATA $1, $0
0x001d 00029 (x.go:4) NOP
0x0020 00032 (x.go:4) CALL runtime.printlock(SB)
0x0025 00037 (x.go:4) MOVQ $3, (SP)
0x002d 00045 (x.go:4) CALL runtime.printint(SB)
0x0032 00050 (x.go:4) CALL runtime.printnl(SB)
0x0037 00055 (x.go:4) CALL runtime.printunlock(SB)
0x003c 00060 (x.go:5) MOVQ 8(SP), BP
0x0041 00065 (x.go:5) ADDQ $16, SP
0x0045 00069 (x.go:5) RET
0x0046 00070 (x.go:5) NOP
0x0046 00070 (x.go:3) PCDATA $1, $-1
0x0046 00070 (x.go:3) PCDATA $0, $-2
0x0046 00070 (x.go:3) CALL runtime.morestack_noctxt(SB)
0x004b 00075 (x.go:3) PCDATA $0, $-1
0x004b 00075 (x.go:3) JMP 0
0x0000 64 48 8b 0c 25 00 00 00 00 48 3b 61 10 76 37 48 dH..%....H;a.v7H
0x0010 83 ec 10 48 89 6c 24 08 48 8d 6c 24 08 0f 1f 00 ...H.l$.H.l$....
0x0020 e8 00 00 00 00 48 c7 04 24 03 00 00 00 e8 00 00 .....H..$.......
0x0030 00 00 e8 00 00 00 00 e8 00 00 00 00 48 8b 6c 24 ............H.l$
0x0040 08 48 83 c4 10 c3 e8 00 00 00 00 eb b3 .H...........
rel 5+4 t=17 TLS+0
rel 33+4 t=8 runtime.printlock+0
rel 46+4 t=8 runtime.printint+0
rel 51+4 t=8 runtime.printnl+0
rel 56+4 t=8 runtime.printunlock+0
rel 71+4 t=8 runtime.morestack_noctxt+0
go.cuinfo.packagename. SDWARFCUINFO dupok size=0
0x0000 6d 61 69 6e main
""..inittask SNOPTRDATA size=24
0x0000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0x0010 00 00 00 00 00 00 00 00 ........
gclocals·33cdeccccebe80329f1fdbee7f5874cb SRODATA dupok size=8
0x0000 01 00 00 00 00 00 00 00

反编译程序

go tool objdump

这是Go语言自带的反编译命令。

$ cat x.go
package main
func main() {
println(3)
}
$ go build x.go
$ go tool objdump -s main.main x
TEXT main.main(SB) /home/foo/codes/goinmemory/go_asm/x.go
x.go:3 0x45ec60 64488b0c25f8ffffff MOVQ FS:0xfffffff8, CX
x.go:3 0x45ec69 483b6110 CMPQ 0x10(CX), SP
x.go:3 0x45ec6d 7637 JBE 0x45eca6
x.go:3 0x45ec6f 4883ec10 SUBQ $0x10, SP
x.go:3 0x45ec73 48896c2408 MOVQ BP, 0x8(SP)
x.go:3 0x45ec78 488d6c2408 LEAQ 0x8(SP), BP
x.go:4 0x45ec7d 0f1f00 NOPL 0(AX)
x.go:4 0x45ec80 e8fb05fdff CALL runtime.printlock(SB)
x.go:4 0x45ec85 48c7042403000000 MOVQ $0x3, 0(SP)
x.go:4 0x45ec8d e8ee0dfdff CALL runtime.printint(SB)
x.go:4 0x45ec92 e8a908fdff CALL runtime.printnl(SB)
x.go:4 0x45ec97 e86406fdff CALL runtime.printunlock(SB)
x.go:5 0x45ec9c 488b6c2408 MOVQ 0x8(SP), BP
x.go:5 0x45eca1 4883c410 ADDQ $0x10, SP
x.go:5 0x45eca5 c3 RET
x.go:3 0x45eca6 e8b5afffff CALL runtime.morestack_noctxt(SB)
x.go:3 0x45ecab ebb3 JMP main.main(SB)

objdump

这是Linux环境中一个通用的反编译工具,不仅仅适用于Go程序。

$ objdump --disassemble=main.main x
x: file format elf64-x86-64
Disassembly of section .text:
000000000045ec60 <main.main>:
45ec60: 64 48 8b 0c 25 f8 ff mov %fs:0xfffffffffffffff8,%rcx
45ec67: ff ff
45ec69: 48 3b 61 10 cmp 0x10(%rcx),%rsp
45ec6d: 76 37 jbe 45eca6 <main.main+0x46>
45ec6f: 48 83 ec 10 sub $0x10,%rsp
45ec73: 48 89 6c 24 08 mov %rbp,0x8(%rsp)
45ec78: 48 8d 6c 24 08 lea 0x8(%rsp),%rbp
45ec7d: 0f 1f 00 nopl (%rax)
45ec80: e8 fb 05 fd ff callq 42f280 <runtime.printlock>
45ec85: 48 c7 04 24 03 00 00 movq $0x3,(%rsp)
45ec8c: 00
45ec8d: e8 ee 0d fd ff callq 42fa80 <runtime.printint>
45ec92: e8 a9 08 fd ff callq 42f540 <runtime.printnl>
45ec97: e8 64 06 fd ff callq 42f300 <runtime.printunlock>
45ec9c: 48 8b 6c 24 08 mov 0x8(%rsp),%rbp
45eca1: 48 83 c4 10 add $0x10,%rsp
45eca5: c3 retq
45eca6: e8 b5 af ff ff callq 459c60 <runtime.morestack_noctxt>
45ecab: eb b3 jmp 45ec60 <main.main>

伪寄存器

某些符号(例如R1或LR)是预定义的并指代寄存器。确切的集合取决于架构。

有四个预声明的符号表示伪寄存器。这些不是真正的寄存器,而是工具链维护的虚拟寄存器。所有架构的伪寄存器集都是相同的:

  • FP: 帧指针:用于引用参数和局部变量。
  • PC: 程序计数器:用于跳转和分支。
  • SB:静态基指针:用于声明全局符号。
  • SP:堆栈指针:本地栈帧内的最高地址。

所有开发者定义的符号都作为伪寄存器 FP 和 SB 的偏移量进行引用。

SB

例如,foo(SB)表示一个全局符号foo,也就是我们在Go开发中声明的函数名称或者全局变量名称。

声明一个全局变量使用GLOBL关键字。在Go汇编中声明一个uint64类型的全局变量的代码如下:

GLOBL foo(SB), NOPTR, $8

全局变量初始化使用DATA关键字。给全局变量foo初始值设置为10的汇编代码如下:

DATA foo+0(SB)/8, $10

声明一个函数使用TEXT关键字。在Go汇编中声明函数的代码如下:

TEXT ·foo(SB), NOSPLIT, $0-0
RET

FP

伪寄存器是用于引用函数参数的虚拟帧指针 。编译器维护一个虚拟帧指针,并将堆栈上的参数作为与该伪寄存器的偏移量进行访问。因此,在 64 位机器上,0(FP)表示函数的第一个参数,8(FP)表示函数的第二个参数,依此类推。

但是,当以这种方式引用函数参数时,必须在开头放置一个名称,如first_arg+0(FP)、 second_arg+8(FP)。

汇编器强制执行这个约定,拒绝普通的0(FP)和8(FP)。实际名称在语义上无关紧要,但应用于记录参数的名称。值得强调的是FP始终是伪寄存器,而不是硬件寄存器,即使在具有硬件帧指针的架构上也是如此。

在上述代码清单中,add函数的汇编实现,清楚地演示了这一点。

通常情况下,在Go语言中,函数返回值其实也是函数的一种参数。

SP

SP 伪寄存器是一个虚拟堆栈指针,用于引用帧局部变量和为函数调用准备参数 。它指向本地栈帧内的最高地址,因此引用应使用 [−framesize, 0): x-8(SP),y-4(SP)等范围内的负偏移量。

在具有名为 SP 的硬件寄存器的体系结构上,名称前缀区分对虚拟堆栈指针的引用和对体系结构 SP 寄存器的引用。例如,x-8(SP)和-8(SP)是不同的内存位置:第一个是指虚拟堆栈指针伪寄存器,而第二个是指硬件的SP寄存器。

Directives

在英文中,instruction表示指令,directive也表示指令,但是他们的含义实际差别比较大。

instruction一般和处理器架构、汇编语言的指令集相关。

  • 例如:MOVQ、RET都是instruction。

directive一般表示编程语言定义的一些特殊的、用于辅助编码的关键字。

  • 例如:GLOBL、DATA、TEXT都是directive。

在本文中,笔者约定将directive称为关键字。

声明全局变量的语法格式为:

GLOBL symbol(SB), flags, width

GLOBL声明全局变量时相当于var或者const。

初始化全局变量的语法格式为:

DATA symbol+offset(SB)/width, value

实现函数的语法格式为:

TEXT [package]·symbol(SB), flags, $framesize-argumentsize
// instructions
RET

TEXT声明函数时相当于func。

定义宏的语法格式为:

单行:

#define NOSPLIT  4

多行:

#define DISPATCH(NAME,MAXSIZE)    \
CMPQ CX, $MAXSIZE; \
JA 3(PC); \
MOVQ $NAME(SB), AX; \
JMP AX

引用头文件的格式为:

#include "textflag.h"

flags

Go语言在runtime/textflag.h源文件中定义了一些特殊的宏,用于标记全局符号(函数和全局变量)。

#define NOPROF  1
#define DUPOK 2
#define NOSPLIT 4
#define RODATA 8
#define NOPTR 16
#define WRAPPER 32
#define NEEDCTXT 64
#define TLSBSS 256
#define NOFRAME 512
#define REFLECTMETHOD 1024
#define TOPFRAME 2048

它们的含义,只有开发者在对可执行文件的结构、数据在可执行文件中的存储方式、函数调用约定等都有比较深入的了解之后,才会变得显而易见。否则,这玩意不是三言两语能解释清楚的。

与 Go 类型以及常量交互

如果一个包有任何 .s 文件,那么go build将指示编译器生成(emit)一个名为go_asm.h的特殊头文件,然后 .s 文件可以通过#include引用该文件。

该文件包含通过#define指令定义的 Go 结构字段的偏移量、Go 结构类型的大小以及当前包中定义的大多数使用const声明的符号常量。Go 汇编应该避免对 Go 类型的布局做出假设,而是使用这些常量。这提高了汇编代码的可读性,并使其对 Go 类型定义或 Go 编译器使用的布局规则中的数据布局变化保持稳健。

在上述代码清单中,length函数的汇编代码中,读取Text结构体的Length字段的值,是通过该字段的偏移量Text_Length读取的。 Text_Length这个定义在go_asm.h文件中。虽然我们从来没有定义这个头文件,但是可以通过#include "go_asm.h"直接引用该文件。

同样,sizeOfTextStruct函数的汇编代码中,常量Text__size表示Text结构体对象的大小。

运行时协调

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

  • 标有NOPTR标志的数据符号被视为不包含指向运行时分配数据的指针。
  • 带有RODATA标志的数据符号被分配在只读存储器中,因此被视为隐式标记了NOPTR。
  • 总大小小于指针的数据符号也被视为隐式标记了NOPTR。

无法在汇编源文件中定义包含指针的符号,这样的符号必须在 Go 源文件中定义;在汇编代码中可以直接通过名称应用这些符号。一个好的一般经验法则是在 Go 中定义所有非RODATA数据,而不是在汇编中定义 。

汇编函数应该总是被赋予 Go 原型,既可以为参数和结果提供指针信息,也可以使用go vet检查用于访问它们的偏移量是否正确。例如代码清单中的函数声明:

func add(a, b int) int
func addX(a, b int) int
func length(text *Text) int
func sizeOfTextStruct() int

运行效果

编译并运行程序,然后反编译可执行程序,对比源代码,我们可以看到一些变化。

本文先介绍到这里。

责任编辑:姜华 来源: Golang In Memor
相关推荐

2022-10-28 18:36:18

2023-04-17 14:32:20

2012-11-20 10:20:57

Go

2023-05-08 07:55:05

快速排序Go 语言

2022-12-05 09:32:29

Go 语言风格规范

2023-10-31 08:16:16

Go语言二维码

2024-02-28 23:07:42

GolangBase64编码

2018-03-12 22:13:46

GO语言编程软件

2011-01-14 14:08:17

Linux汇编语言

2017-10-26 11:44:19

工具语言编写

2022-09-29 10:01:05

Go编程语言文本文件

2011-01-14 14:39:32

Linux汇编语言

2019-10-15 14:16:45

编程语言Go 开发

2010-07-13 10:21:19

2023-11-22 08:00:56

Go命名规范

2011-03-08 16:50:35

2010-06-10 18:27:00

UML语言

2010-08-10 10:32:02

Flex语言

2011-01-04 17:08:10

汇编语言

2019-11-13 15:44:17

Kafka架构数据
点赞
收藏

51CTO技术栈公众号