在.go文件中声明全局变量,在.s文件中初始化。先看操作。
第一步:新建main.go和var.s文件
第二步:在main.go中输入如下代码
package main
import (
"fmt"
)
var Id int
func main() {
fmt.Println(Id)
}
第三步:在var.s中输入如下代码
#include "textflag.h"
GLOBL ·Id(SB),NOPTR,$8
DATA ·Id+0(SB)/1,$0x37
DATA ·Id+1(SB)/1,$0x25
DATA ·Id+2(SB)/1,$0x00
DATA ·Id+3(SB)/1,$0x00
DATA ·Id+4(SB)/1,$0x00
DATA ·Id+5(SB)/1,$0x00
DATA ·Id+6(SB)/1,$0x00
DATA ·Id+7(SB)/1,$0x00
第四步:go run main.go
如无意外,以上代码将输出9527.
我们主要来看看汇编代码都干了啥。
第一行引入头文件是为了导入NOPTR
符号,它表示变量中不包含指针,加这个标识是为了能通过GC扫描,否则编译时会报错。textflag.h
文件在Go源码目录的runtime目录下,代码不多,不妨看下。
// 该文件定义的表使可用于函数和数据,由编译器、汇编器、链接器支持
// 和 src/cmd/internal/obj/textflag.go 保持一致
// Don't profile the marked routine. This flag is deprecated.
#define NOPROF 1
// It is ok for the linker to get multiple of these symbols. It will
// pick one of the duplicates to use.
#define DUPOK 2
// Don't insert stack check preamble.
#define NOSPLIT 4
// Put this data in a read-only section.
#define RODATA 8
// This data contains no pointers.
#define NOPTR 16
// This is a wrapper function and should not count as disabling 'recover'.
#define WRAPPER 32
// This function uses its incoming context register.
#define NEEDCTXT 64
// Allocate a word of thread local storage and store the offset from the
// thread local base to the thread local storage in this variable.
#define TLSBSS 256
// Do not insert instructions to allocate a stack frame for this function.
// Only valid on functions that declare a frame size of 0.
// TODO(mwhudson): only implemented for ppc64x at present.
#define NOFRAME 512
// Function can call reflect.Type.Method or reflect.Type.MethodByName.
#define REFLECTMETHOD 1024
// Function is the top of the call stack. Call stack unwinders should stop
// at this function.
#define TOPFRAME 2048
英文比较简单,懒得翻译了,将就看吧。
GLOBL
用来导出变量 Id
,这样它才能和main.go中的Id
产生关联。接下来是一个·
,注意它并不是小数点,而是中文的中点。
似乎有点绕,其实就是中文输入法的反引号,Unicode码是U+00B7
。接下来我们思考两个问题:
·
的作用是什么?- 为什么是
·
而不是.
?
先说·
的作用,其实和小数点是一样的。什么时候会用到小数点呢?当然是访问某个包的某个符号(这里的符号包括变量或函数)时,比如fmt.Println
。·Id
其实就是指当前main包中的Id
变量,甚至你也可以写作main·Id
,当然后面也要全部加上main
前缀,如下:
#include "textflag.h"
GLOBL main·Id(SB),NOPTR,$8
DATA main·Id+0(SB)/1,$0x37
DATA main·Id+1(SB)/1,$0x25
DATA main·Id+2(SB)/1,$0x00
DATA main·Id+3(SB)/1,$0x00
DATA main·Id+4(SB)/1,$0x00
DATA main·Id+5(SB)/1,$0x00
DATA main·Id+6(SB)/1,$0x00
DATA main·Id+7(SB)/1,$0x00
这个故事告诉我们在汇编中访问当前包的变量可以省略包名,但是不能省略·
。
那么为什么不是小数点,和go代码保持一致呢?众所周知,Go汇编是从plan9汇编移植过来的,为了支持Go的包特性,同时简化汇编器词法扫描程序的实现,就用·
代替了小数点以区别浮点数中的小数点。同时被替换的还有/
,在Go汇编中被替换为大写除法∕
,它比/
要更倾斜一点,Unicode码是U+2215
,靠键盘好像还打不出来的样子,可以从这里复制。∕
的作用就是包路径分隔符,在最后会看到它的例子。当然编译后·
会被替换回小数点,∕
也会被替换回正常的/
。
·Id
就对应main包的符号Id
,再后面是(SB)
。这可不是傻逼,它是Go汇编的一个伪寄存器,全称是Static Base pointer,是静态内存的起始地址。全局变量和全局函数就放在静态内存,它们都是通过SB
加上一个偏移量来访问的。
刚学C语言的时候有人告诉我变量就是一片内存,可是不知道变量是如何跟内存产生联系的。变量(更准确的说是符号)其实只是一个偏移量,其出现的地方决定了基于谁来偏移,这就是变量的本质。
再后面NOPTR
表示Id
中不包含指针,$8
表示Id
变量的宽度是8字节。$8
其实就是8,常量必须以$
开头,别问为什么,问就是语法。
问题又来了,我们说的符号究竟是Id
还是·Id
呢?
答案是后者,符号应该包含完整包路径。所以GLOBL
的语法是GLOBL 符号(SB),标识,宽度
。
最后我们通过DATA
来初始化内存。·Id
我们已经很熟悉了,它是基于SB
的一个偏移量。后面又+
了一个数字,表示的是基于·Id
再偏移的字节数。也就是说·Id+0(SB)
就是一个新的偏移量,·Id
是在寻址符号Id,+x
就是在寻址符号Id
内部的第x
个字节,因为它占了8个字节。/
后的1表示内存宽度,也就是你要初始化几个字节,这里的/
是正常的斜线,不是大写除法。最后的常量是你要初始化的值。
所以DATA
的语法格式是:DATA 符号+偏移(SB)/宽度,值
。
这里又两个问题:
- 可不可以一次初始化多个字节?
- 初始化内存宽度是否可以任意?
- 偏移量是否可以超出符号宽度?
首先第一个问题肯定是可以的,否则按照Go少即是多的原则就是不会有/
去指定宽度了。比如我们可以将Id
的初始化写成下面这样:
DATA ·Id+0(SB)/4,$0x2537
DATA ·Id+4(SB)/4,$0x00
甚至可以是:
DATA ·Id+0(SB)/8,$0x2537
或者只是:
DATA ·Id+0(SB)/2,$0x2537
因为Go内存是默认零值初始化的,所以哪些本身就是0的字节可以不初始化。
关于第二个问题,的确可以,但必须是2的整数次幂,比如1,2,4,8,16这样。
第三个问题只要是非负整数就可以,它并不会限制你初始化的这片内存是不是在该变量区域内,也不会检查偏移量有没有超出变量宽度,因为在汇编里并没有类型的概念,一切皆是内存。但是这样的代码也会带来严重的安全问题,没有人会写出这样有害的代码,而且孙悟空也不会答应。
虽然对偏移量和宽度没什么像样的限制,但是有一条,初始化内存不能相互覆盖。比如像下面这样的代码是不行的:
DATA ·Id+0(SB)/4,$0x2537
DATA ·Id+3(SB)/4,$0x00
最后的最后,还有一个空行。注意,最后的空行是必须的。
到这里我们已经介绍完了示例代码的所有内容,不过似乎少了一点东西。我们还没有看到注释呢,Go汇编的注释和Go是一样的,通过//
和/**/
注释。
最后一个例子,看看如何访问其他包的符号。
我们先新建一个variable
包,并新建一个variable.go文件,输入以下代码:
package variable
var Count int
main.go修改如下:
package main
import (
"asm/variable"
"fmt"
)
var Id int
func main() {
fmt.Println(Id)
fmt.Println(variable.Count)
}
var.s修改如下:
#include "textflag.h"
GLOBL ·Id(SB),NOPTR,$8
GLOBL asm∕variable·Count(SB),NOPTR,$8
//初始化Id
DATA ·Id+0(SB)/4,$0x2537
DATA ·Id+4(SB)/4,$0x00
//初始化Count
DATA asm∕variable·Count+0(SB)/2,$0x2537
不出意外,你会看到打印两个9527。