实现和声明

Go汇编语言并不是一个独立的语言,因为Go汇编程序无法独立使用。Go汇编代码必须以Go包的方式组织,同时包中至少要有一个Go语言文件用于指明当前包名等基本包信息。如果Go汇编代码中定义的变量和函数要被其它Go语言代码引用,还需要通过Go语言代码将汇编中定义的符号声明出来。

定义整数变量

为了简单,我们先用Go语言定义并赋值一个整数变量,然后查看生成的汇编代码。
首先创建一个pkg.go文件,内容如下:

  1. package pkg
  2. var Id = 9527

代码中只定义了一个int类型的包级变量,并进行了初始化。然后用以下命令查看的Go语言程序对应的伪汇编代码:

  1. $ go tool compile -S pkg.go
  2. "".Id SNOPTRDATA size=8
  3. 0x0000 37 25 00 00 00 00 00 00 '.......
  • go tool compile命令用于调用Go语言提供的底层命令工具,其中-S参数表示输出汇编格式。
  • “”.Id对应Id变量符号,变量的内存大小为8个字节。变量的初始化内容为37 25 00 00 00 00 00 00,对应十六进制格式的0x2537,对应十进制为9527
  • SNOPTRDATA是相关的标志,其中NOPTR表示数据中不包含指针数据。

以上的内容只是目标文件对应的汇编,和Go汇编语言虽然相似当并不完全等价。Go语言官网自带了一个Go汇编语言的入门教程,地址在:https://golang.org/doc/asm

Go汇编语言提供了DATA命令用于初始化包变量,DATA命令的语法如下

  1. DATA symbol+offset(SB)/width, value
  • symbol为变量在汇编语言中对应的标识符
  • offset是符号开始地址的偏移量
  • width是要初始化内存的宽度大小
  • value是要初始化的值
  • 当前包中Go语言定义的符号symbol,在汇编代码中对应·symbol,其中“·”为一个特殊的unicode符号

我们采用以下命令可以给Id变量初始化为十六进制的0x2537,对应十进制的9527(常量需要以美元符号$开头表示):

  1. DATA ·Id+0(SB)/1,$0x37
  2. DATA ·Id+1(SB)/1,$0x25

变量定义好之后需要导出以供其它代码引用。Go汇编语言提供了GLOBL命令用于将符号导出:

  1. GLOBL symbol(SB), width

其中symbol对应汇编中符号的名字,width为符号对应内存的大小。用以下命令将汇编中的·Id变量导出:

  1. GLOBL ·Id, $8

现在已经初步完成了用汇编定义一个整数变量的工作。
为了便于其它包使用该Id变量,我们还需要在Go代码中声明该变量,同时也给变量指定一个合适的类型。修改pkg.go的内容如下:

  1. package pkg
  2. var Id int

现状Go语言的代码不再是定义一个变量,语义变成了声明一个变量(声明一个变量时不能再进行初始化操作)。而Id变量的定义工作已经在汇编语言中完成了。
我们将完整的汇编代码放到pkg_amd64.s文件中:

  1. #include "textflag.h"
  2. GLOBL ·Id(SB),NOPTR,$8
  3. DATA ·Id+0(SB)/1,$0x37
  4. DATA ·Id+1(SB)/1,$0x25
  5. DATA ·Id+2(SB)/1,$0x00
  6. DATA ·Id+3(SB)/1,$0x00
  7. DATA ·Id+4(SB)/1,$0x00
  8. DATA ·Id+5(SB)/1,$0x00
  9. DATA ·Id+6(SB)/1,$0x00
  10. DATA ·Id+7(SB)/1,$0x00

文件名pkg_amd64.s的后缀名表示AMD64环境下的汇编代码文件。
虽然pkg包是用汇编实现,但是用法和之前的Go语言版本完全一样:

  1. package main
  2. import pkg "pkg包的路径"
  3. func main() {
  4. println(pkg.Id)
  5. }

定义字符串变量

在前一个例子中,我们通过汇编定义了一个整数变量。现在我们提高一点难度,尝试通过汇编定义一个字符串变量。虽然从Go语言角度看,定义字符串和整数变量的写法基本相同,但是字符串底层却有着比单个整数更复杂的数据结构。
实验的流程和前面的例子一样,还是先用Go语言实现类似的功能,然后观察分析生成的汇编代码,最后用Go汇编语言仿写。首先创建pkg.go文件,用Go语言定义字符串:

  1. package pkg
  2. var Name = "gopher"

然后用以下命令查看的Go语言程序对应的伪汇编代码:

  1. $ go tool compile -S pkg.go
  2. go.string."gopher" SRODATA dupok size=6
  3. 0x0000 67 6f 70 68 65 72 gopher
  4. "".Name SDATA size=16
  5. 0x0000 00 00 00 00 00 00 00 00 06 00 00 00 00 00 00 00 ................
  6. rel 0+8 t=1 go.string."gopher"+0

输出中出现了一个新的符号go.string.”gopher”,根据其长度和内容分析可以猜测是对应底层的”gopher”字符串数据。
因为Go语言的字符串并不是值类型,Go字符串其实是一种只读的引用类型。如果多个代码中出现了相同的”gopher”只读字符串时,程序链接后可以引用的同一个符号go.string.”gopher”。
因此:

  • SRODATA标志表示这个数据在只读内存段
  • dupok表示出现多个相同标识符的数据时只保留一个就可以了

而真正的Go字符串变量Name对应的大小却只有16个字节了。其实Name变量并没有直接对应“gopher”字符串,而是对应16字节大小的reflect.StringHeader结构体:

  1. type reflect.StringHeader struct {
  2. Data uintptr
  3. Len int
  4. }

从汇编角度看,Name变量其实对应的是reflect.StringHeader结构体类型。前8个字节对应底层真实字符串数据的指针,也就是符号go.string.”gopher”对应的地址。后8个字节对应底层真实字符串数据的有效长度,这里是6个字节。

现在创建pkg_amd64.s文件,尝试通过汇编代码重新定义并初始化Name字符串:

  1. #include "textflag.h"
  2. GLOBL ·NameData(SB),NOPTR,$8
  3. DATA ·NameData(SB)/8,$"gopher"
  4. GLOBL ·Name(SB),NOPTR,$16
  5. DATA ·Name+0(SB)/8,$·NameData(SB)
  6. DATA ·Name+8(SB)/8,$6

因为在Go汇编语言中,go.string.”gopher”不是一个合法的符号,因此我们无法通过手工创建(这是给编译器保留的部分特权,因为手工创建类似符号可能打破编译器输出代码的某些规则)。因此我们新创建了一个·NameData符号表示底层的字符串数据。然后定义·Name符号内存大小为16字节,其中前8个字节用·NameData符号对应的地址初始化,后8个字节为常量6表示字符串长度。

引用

  1. package main
  2. import (
  3. "commons/study/demo1/pkg"
  4. "fmt"
  5. )
  6. func main() {
  7. fmt.Println(pkg.Name)
  8. }

定义main函数

前面的例子已经展示了如何通过汇编定义整型和字符串类型变量。我们现在将尝试用汇编实现函数,然后输出一个字符串。
先创建main.go文件,创建并初始化字符串变量,同时声明main函数:

  1. package main
  2. var helloworld = "你好, 世界"
  3. func main()

然后创建main_amd64.s文件,里面对应main函数的实现:

  1. TEXT ·main(SB), $16-0
  2. MOVQ ·helloworld+0(SB), AX; MOVQ AX, 0(SP)
  3. MOVQ ·helloworld+8(SB), BX; MOVQ BX, 8(SP)
  4. CALL runtime·printstring(SB)
  5. CALL runtime·printnl(SB)
  6. RET

TEXT ·main(SB), $16-0用于定义main函数

  • 其中$16-0表示main函数的帧大小是16个字节(对应string头部结构体的大小,用于给runtime·printstring函数传递参数)
  • 0表示main函数没有参数和返回值

main函数内部通过调用运行时内部的runtime·printstring(SB)函数来打印字符串。然后调用runtime·printnl打印换行符号。

Go语言函数在函数调用时,完全通过栈传递调用参数和返回值。先通过MOVQ指令,将helloworld对应的字符串头部结构体的16个字节复制到栈指针SP对应的16字节的空间,然后通过CALL指令调用对应函数。最后使用RET指令表示当前函数返回。

执行会出错
image.png
问题:链接
根据issue上的答案解决后,报错
image.png
后续继续深入了解

Go汇编语言中分号可以用于分隔同一行内的多个语句。也可以省略行尾的分号。当遇到末尾时,汇编器会自动插入分号。