前言

Go plan9 汇编: 打通应用到底层的任督二脉 一文中介绍了从应用程序到汇编指令的转换。本文将结合汇编和 Go 程序实现手写基本的汇编指令,以加深对 Go plan9 汇编的了解。

手写汇编

全局变量

首先写一个打印整型变量的函数如下:

1// ex1/ex1.go
2package main
3
4var a = 9527
5
6func main() {
7	print(a)
8}

使用 go tool compile -S -N -l 输出程序的汇编代码:

1# go tool compile -S -N -l ex0.go 
2main.main STEXT size=50 args=0x0 locals=0x10 funcid=0x0 align=0x0
3    ...
4main.a SNOPTRDATA size=8
5        0x0000 37 25 00 00 00 00 00 00                          7%......

这里省略了 main.main 的汇编输出,重点关注 main.a 这个变量。输出的 main.a 表示汇编的标识符,SNOPTRDATA 表示这个变量是不包括指针的,这是给垃圾回收器看的,当扫描到这个变量时,垃圾回收器会跳过这个变量的回收。size=8 是这个变量的大小。重点在 0x0000 37 25 00 00 00 00 00 00,这段是 9527 在内存中的排列,0x2537 是 9527 的十六进制表示。

汇编实现全局变量

我们可以写汇编实现全局变量的输出。注意,本文不是汇编的教程,不会过多介绍 Go plan9 汇编的语法内容,关于这方面可以看曹大的 Go 语言高级编程:汇编语言(写的真是太好了!)。

首先,Go plan9 汇编是需要和 Go 文件一起协同工作的。这里

 1// ex1/ex1.go
 2package main
 3
 4import (
 5	"ex1/pkg"
 6)
 7
 8func main() {
 9	println(pkg.Id)
10}

main 包中打印 pkg 包的 Id 变量。

1// ex1/pkg/pkg.go
2package pkg
3
4var Id int

我们可以写汇编实现 Id 变量的定义,如下:

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

NOPTR 表示变量 Id 不包括指针,$8 表示变量占 8 个字节。DATA 声明变量存储在内存中的 data 段,内存中的段如下。

memory

运行上述程序:

1# go run ex1.go
29527

输出变量 9527,其内存分布如下:

memory id

从变量内存分布可以看出,我们申请的 int(8 字节) 内存,只有 2 个字节是真正被用到的。其它字节都是 0。我们可以节省空间申请 Id 为 2 个字节如下:

 1// ex1/pkg/pkg.go
 2package pkg
 3
 4var Id int16
 5
 6
 7// ex1/pkg/pkg_amd64.s
 8#include "textflag.h"
 9
10GLOBL ·Id(SB),NOPTR,$8
11
12DATA ·Id+0(SB)/1,$0x37
13DATA ·Id+1(SB)/1,$0x25

输出:

1# go run ex1.go 
29527

改写 pkg_amd64.s

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

输出:

1# go run ex1.go 
29527

0x2537 之上的 1 个字节 0x20 并不会被 CPU 寻址到,CPU 会根据变量声明从内存中读取 2 个字节的 Id 变量送入寄存器中处理。

字符串

结合 Go 和汇编打印字符串:

 1// ex2/main.go
 2package main
 3
 4import (
 5	"ex2/pkg"
 6	"fmt"
 7)
 8
 9func main() {
10	fmt.Println(pkg.Name)
11}
12
13// ex2/pkg/pkg.go
14package pkg
15
16var Name string

字符串 Name 的声明在 pkg 包中,使用汇编定义变量 Name

 1// ex2/pkg/pkg_amd64.s
 2#include "textflag.h"
 3
 4GLOBL string<>(SB),NOPTR,$16
 5DATA string<>+0(SB)/8,$"Hello Wo"
 6DATA string<>+8(SB)/8,$"rld!"
 7
 8GLOBL ·Name(SB),NOPTR|RODATA,$16
 9DATA ·Name+0(SB)/8,$string<>(SB)
10DATA ·Name+8(SB)/8,$12

这里字符串变量实际是一个 16 字节的包括长度和指针的结构体。变量定义在:

1GLOBL ·Name(SB),NOPTR|RODATA,$16
2DATA ·Name+0(SB)/8,$string<>(SB)
3DATA ·Name+8(SB)/8,$12

前 8 个字节指向的是存储实际字符串的内存地址,后 8 个字节是字符串的长度。真实的字符串存储在内存中的数据段。这里 string<> 表示该变量 string 时不可导出变量,否则外部 Go 程序可直接访问字符串变量。

画出内存分布图如下:

string

小结

我们顺着上述思路可以继续函数的汇编实现,不过本文重点是了解汇编的写法,不是真正去写汇编。我们通过两个简单的全局变量和字符串的汇编示例,了解汇编代码的写法。在实际应用中几乎不会自己去写,重在了解。更多关于汇编实现的内容可以参考曹大的 Go 高级编程