asm

https://github.com/yangyuqian/technical-articles/blob/master/asm/golang-plan9-assembly-cn.md
http://xargin.com/go-and-plan9-asm/
https://davidwong.fr/goasm/
https://github.com/teh-cmc/go-internals/blob/master/chapter1_assembly_primer/README.md
https://golang.org/doc/asm
https://github.com/yangyuqian/technical-articles/blob/master/asm/golang-plan9-assembly-cn.md

https://colobu.com/goasm/
https://laily.net/article/Golang%20%E4%BB%8E%E6%B1%87%E7%BC%96%E7%9C%8B%E5%87%BD%E6%95%B0%E8%B0%83%E7%94%A8



https://cordate.github.io/2018/04/11/golang/Go%E7%9A%84%E6%B1%87%E7%BC%96%E7%A8%8B%E5%BA%8F%E5%BF%AB%E9%80%9F%E6%8C%87%E5%8D%97/



https://9p.io/sys/doc/asm.html
http://www.techclone.cn/post/tech/go/go-assembly/



//golang源码转汇编,会输出生成的汇编指令
GOOS=linux GOARCH=amd64 go tool compile -S direct_topfunc_call.go



//查看可执行程序起始地址
objdump -j .text -t direct_topfunc_call | grep ‘main.add’
汇编指令
寄存器
go 汇编中有4个核心的伪寄存器,这4个寄存器是编译器用来维护上下文、特殊标识等作用的:



FP(Frame pointer): arguments and locals(参数地址)。

指向由调用方提供的参数列表起始地址,通过偏移量指向不同参数或返回值。
通常在偏移量前包含参数名。例如MOVQ size+16(FP), AX




PC(Program counter): jumps and branches,PC(指令地址)

可用来按指令行数条转。
比如 JMP 2(PC) 表示以当前位置为 0 基准,往下跳到第 2 行。



SB(Static base pointer): global symbols(全局符号)

表示一个全局符号地址,通常应用于全局函数或数据。
例如 CALL add(SB) 表示对应符号名字为 add 的内存地址。



SP(Stack pointer): top of stack(栈局部变量内存地址)。用于本地局部变量操作的起始地址。
鉴于栈从底开始的操作方式,SP 实际是栈底位置(等同调整后的 BP 地址)。使用该方式访问局部变量,须添加变量名,如 x-8(SP)。如果省略变量名,则表示硬件寄存器。




BP:x86平台上寄存器,通常用来指示函数栈的起始位置,仅仅其一个指示作用



指令
汇编指令自左往右执行,汇编中数字常量以 $ 开头,十进制($10)和 十六进制($0x10)。 标签仅在函数內有效。



指令 作用
MOV(x) 移动内容,其中x代表字节长度,B:1bytes、W:2byte、L:4byte、Q:8byte
ADD 相加,ADD a,b,表示 b=a+b
SUB 相减,SUB a,b,表示 b=b-a
MUL 乘法,MUL $7,b, 表示 b=7*b
MOV 移动指针,MOV n(R1), R2, R2 = *(n+R1),移动n字节
JMP 跳转到某个标签或者行,JMP 2(pc),跳转到PC+2行
LEA 移动地址
golang底层一些知识
routine
golang中每个goroutine默认分配的栈空间初始大小为2k,如果运行过程中,栈的大小不够,routine会新生成一个2倍当前栈大小的空间,然后将旧栈内的数据copy到新栈中。这过程叫做栈分裂(stack-split);
golang栈底层分配在堆上面的;
为了保证栈分裂的安全执行,而routine不会爆栈,在生成汇编代码时,栈分裂代码被分成 prologue 和 epilogue 两个部分:
prologue会检查当前goroutine是否已经用完了所有的空间,然后如果确实用完了的话,会直接跳转到后部;
epilogue 会触发栈增长 (stack-growth),然后再跳回到前部; 这样就形成了一个反馈循环,使我们的栈在没有达到饥饿的 goroutine 要求之前不断地进行空间扩张。
函数参数栈中布局
Go 的调用规约要求每一个参数都通过栈来传递,这部分空间由 caller 在其栈帧(stack frame)上提供。
调用其它过程之前,caller 就需要按照参数和返回变量的大小来对应地增长(返回后收缩)栈。
对应在main函数中指令就是:
//下面汇编代码,是根据(参考1)中golang代码生成的, 抽取main函数中一部分
//根据Go调用函数规约,在调用add函数前,main中分配了参数和返回值地址
//其中,sp开始字节数:
//0-4变量a; 4-8变量b;
//8-12保存被调用函数add的返回值;12-13存储返回bool类型;13-16用作字节对齐
0x000f 00015 (test.go:6) SUBQ $24, SP
//保存BP寄存器地址
0x0013 00019 (test.go:6) MOVQ BP, 16(SP)
//移动栈地址
0x0018 00024 (test.go:6) LEAQ 16(SP), BP



//其中$137438953482,其实就是两个入参a=10, b=32;
//引用参考1中分析结果
//$ echo ‘obase=2;137438953482’ | bc
//10000000000000000000000000000000001010
//___/___________/
// 32 10
0x001d 00029 (test.go:6) MOVQ $137438953482, AX
//将32, 10赋值给b a
0x0027 00039 (test.go:6) MOVQ AX, (SP)
0x002b 00043 (test.go:6) CALL ““.add(SB)
函数调用时候,入参、返回值地址在栈中分布都是由右向左开始,对应到栈中就是自高地址到低地址,通过”“.a+8(sp)、”“.b+12(sp)可以看出,其中sp是当前函数栈底(即地址最低的位置)



MOVQ $137438953482, AX指令可以看出,该操作系统是小端,a位于低地址,b位于高地址,MOVQ AX, (SP)完成赋值,即$137438953482在内存上低位对应内存低地址



golang的汇编基于plan9汇编,是一个中间汇编方式。这样可以忽略底层不同架构之间的一些差别。汇编主要了解各种寄存器的使用跟寻址方式。根据汇编我们能够一探golang的底层实现。比如内存如何分配,栈如何扩张。接口如何转变。



1.2 寄存器
各种伪计数器:



FP: Frame pointer: arguments and locals.(指向当前栈帧)
PC: Program counter: jumps and branches.(指向指令地址)
SB: Static base pointer: global symbols.(指向全局符号表)
SP: Stack pointer: top of stack.(指向当前栈顶部)
注意: 栈是向下整长 golang的汇编是调用者维护参数返回值跟返回地址。所以FP的值小于参数跟返回值。



1.3 跟例子学汇编
1.3.1 第一个例子
package main
//go:noinline
func add(a, b int32) (int32, bool) {
return a + b, true
}
func main() {
add(10, 32)
}
如上的这段代码会被编译为以下汇编:



”“.add STEXT nosplit size=20 args=0x10 locals=0x0
0x0000 00000 (call.go:4) TEXT ““.add(SB), NOSPLIT, $0-16
0x0000 00000 (call.go:4) FUNCDATA $0, gclocals·f207267fbf96a0178e8758c6e3e0ce28(SB)
0x0000 00000 (call.go:4) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x0000 00000 (call.go:4) MOVL ““.b+12(SP), AX
0x0004 00004 (call.go:4) MOVL ““.a+8(SP), CX
0x0008 00008 (call.go:5) ADDL CX, AX
0x000a 00010 (call.go:5) MOVL AX, “”.~r2+16(SP)
0x000e 00014 (call.go:5) MOVB $1, “”.~r3+20(SP)
0x0013 00019 (call.go:5) RET
0x0000 8b 44 24 0c 8b 4c 24 08 01 c8 89 44 24 10 c6 44 .D$..L$….D$..D
0x0010 24 14 01 c3 $…
TEXT ““.add(SB), NOSPLIT, $0-16 这行代码声明了一个add函数,NOSPLIT告诉编译器不要插入分裂栈检查点。这里可以不用检查栈的大小是因为这里add函数的栈大小为0. $0 表示栈大小为0, $16 表示参数大小为16个字节。 FUNCDATA 跟垃圾回收有关,暂时不用理会 SP 指向当前栈顶部 .a+8(SP) 之前的.a表示助记符,没有特别含义。8(SP)表示地址SP+8 这里是第一参数a, 因为当前栈为0且SP+0存储了函数返回地址。同理12(SP) 表示第二个参数b。16(SP)表示第一个返回值,20(SP)表示第二个返回值。



1.3.2 第二个例子
package main



//go:noinline
func add(a, b int32) (int32, bool) {
return a + b, true
}
//go:noinline
func callAdd() int32 {
a, _ := add(10, 20)
return a
}
func main() {
callAdd()
}
““.callAdd STEXT size=73 args=0x8 locals=0x18
0x0000 00000 (call.go:9) TEXT ““.callAdd(SB), $24-8
; 这三行代码检查是否需要扩展栈, 需要的话,跳到0x0042
0x0000 00000 (call.go:9) MOVQ (TLS), CX
0x0009 00009 (call.go:9) CMPQ SP, 16(CX)
0x000d 00013 (call.go:9) JLS 66



0x000f 00015 (call.go:9) SUBQ $24, SP
0x0013 00019 (call.go:9) MOVQ BP, 16(SP)
0x0018 00024 (call.go:9) LEAQ 16(SP), BP
0x001d 00029 (call.go:9) FUNCDATA $0, gclocals·2a5305abe05176240e61b8620e19a815(SB)
0x001d 00029 (call.go:9) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x001d 00029 (call.go:10) MOVQ $85899345930, AX
0x0027 00039 (call.go:10) MOVQ AX, (SP)
0x002b 00043 (call.go:10) PCDATA $0, $0
0x002b 00043 (call.go:10) CALL ““.add(SB)
0x0030 00048 (call.go:10) MOVL 8(SP), AX
0x0034 00052 (call.go:11) MOVL AX, “”.~r0+32(SP)
0x0038 00056 (call.go:11) MOVQ 16(SP), BP
0x003d 00061 (call.go:11) ADDQ $24, SP
0x0041 00065 (call.go:11) RET
; 扩展栈, 扩展完了后跳到最开始
0x0042 00066 (call.go:11) NOP
0x0042 00066 (call.go:9) PCDATA $0, $-1
0x0042 00066 (call.go:9) CALL runtime.morestack_noctxt(SB)
0x0047 00071 (call.go:9) JMP 0
现在我们添加一层函数调用,看看生成后的样子, 主要关注callAdd函数。在这个例子中我们看到这里的栈大小是24byte(2个参数2个返回值(bool 对齐到4字节, 4 * 4 = 16 byte),再加上一个8byte的bp),参数是8byte(对齐到8byte)。 我们从0x000f说起,前面那段代码主要用于分裂栈检查。 0x000f 把栈减了24个字节。增大了栈空间。(向下增长) 0x0013-0x0018保存老的bp设置新的bp。这里的bp是真实的寄存器,这几行代码属于惯用法。在汇编中可以用bp来引用参数跟本地变量。 0x001d 是10跟20合起来的(八字节)二进制表示。本来需要两次move,现在只需要一次move,也算个小优化了吧。 接下来就是调用add方法,移动返回值。虽然我们没有使用add的第二个返回值,但是我们也要为他分配内存。 0x0038恢复BP寄存器,0x003d缩减栈空间. RET指令加载返回地址,跳到返回地址。



Go语言的汇编是从Plan9继承过来的。根据Go的文档,与每种体系结构标准的汇编语言不同,Go语言的汇编是一个半抽象(semi-abstract)的汇编语言。其目的是通过抽象,得到一套“恰好”满足Go编译器使用的汇编语言。总而言之,Go汇编不是为了大而全,而是为了给Go编译器提供一个抽象层,使之能工作在不同体系架构。



下面举一个例子看看Go汇编到底是什么样的。我们写一个div.go,内容如下:



package main



func div(a, b int) int {
return a / b
}
使用Go1.5在64位架构上编译,执行命令:



go tool compile -S div.go
屏幕输出:



”“.div t=1 size=48 value=0 args=0x18 locals=0x0
0x0000 00000 (div.go:4) TEXT ““.div(SB), $0-24
0x0000 00000 (div.go:4) NOP
0x0000 00000 (div.go:4) NOP
0x0000 00000 (div.go:4) FUNCDATA $0, gclocals·790e5cc5051fc0affc980ade09e929ec(SB)
0x0000 00000 (div.go:4) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x0000 00000 (div.go:5) MOVQ ““.a+8(FP), AX
0x0005 00005 (div.go:5) MOVQ ““.b+16(FP), BP
0x000a 00010 (div.go:5) CMPQ BP, $-1
0x000e 00014 (div.go:5) JEQ $1, 27
0x0010 00016 (div.go:5) CQO
0x0012 00018 (div.go:5) IDIVQ BP
0x0015 00021 (div.go:5) MOVQ AX, “”.~r2+24(FP)
0x001a 00026 (div.go:5) RET
0x001b 00027 (div.go:5) NEGQ AX
0x001e 00030 (div.go:5) MOVQ AX, “”.~r2+24(FP)
0x0023 00035 (div.go:5) JMP 26
TEXT ““.div(SB), $0-24 在TEXT段定义函数div;div的前缀”“.表示div的命名空间,这里为空;SB是Static Base的缩写,也就是TEXT段的起点;$0表示局部变量字节数总和,0表示不存在局部变量;24是传入传出参数的字节数总和,传入两个int,传出一个int,每个int为8个字节,共24字节。
NOP 估计是预留给链接器使用的。
FUNCDATA 为垃圾收集器所用的指令,此外常见的还有PCDATA。
MOVQ ““.a+8(FP), AX 将传入参数a的值从内存拷贝到寄存器AX;这里的AX其实是64位寄存器RAX,Go汇编不是通过EAX或者RAX来区分32位或者64位,而是通过指令名称MOVL和MOVQ区分; FP是Frame Pointer的缩写;”“.a+8(FP)的意思FP偏移8字节名字为a的变量,这里a同样带有命名空间修饰””.。
MOVQ ““.b+16(FP), BP 将传入参数b的值从内存拷贝到寄存器BP。
CMPQ BP, $-1 这是一个快速判断,任何数除以-1相当于取自身的相反数。
JEQ $1, 27 如果上一条指令结果为1,则跳转到27行的NEGQ AX将其取反,并通过MOVQ AX, “”.~r2+24(FP)将结果保存到函数返回值。
CQO 将AX双精度符号扩展,即用DX:AX寄存器组合来表示原先存在AX的值,为除法作准备。
IDIVQ BP 把DX:AX除以BP。
MOVQ AX, “”.~r2+24(FP)执行完除法指令以后,商保存在AX,这条指令把商保存在FP偏移24字节的内存地址,这个地址取名叫作””.~r2。
RET 从函数返回。
参考:



Go还可以通过syso的方式集成体系架构原生的汇编 (https://github.com/golang/go/wiki/GcToolchainTricks)



器,而不是硬件寄存器,即使在具有硬件帧指针的体系结构中也是如此。



对于使用Go的汇编函数,go vet将检查参数名称和偏移量是否匹配。在32位系统上,64位值的低32位和高32位通过在名称中添加_lo或_hi后缀来区分,如arg_lo+0(FP)或arg_hi+4(FP)。如果Go原型没有命名它的结果,那么预期的汇编名称是ret。



SP伪寄存器是虚拟堆栈指针,用来指向帧局部变量和为函数调用准备的参数。它指向本地堆栈帧的顶部,因此引用应使用负数表示的范围[-framesize,0)中:例如:x-8(SP),y-4(SP)等等。



在名SP为硬件寄存器的体系结构中,名称前缀将对虚拟堆栈指针的引用与对帧(frame)SP寄存器的引用进行区分。即,x-8(SP)和-8(SP) 是不同的存储位置:所述第一指虚拟堆栈指针伪寄存器,而第二指硬件的SP寄存器。



在物理机器上SP和PC是传统的物理地址寄存器中的别名,在Go汇编中名称SP和PC仍然特殊处理; 例如,SP引用要求符号,很像FP。要访问实际的硬件寄存器,请使用真实的寄存器名称。例如,在ARM体系结构上的硬件上硬件SP,PC可作为R13和R15访问。



分支和直接跳转总是在PC中写为的偏移量,或跳转到标签:



label:
MOVW $0, R1
JMP label
每个标签只能在其定义的函数中可见。因此允许文件中的多个函数定义和使用相同的标签名称。直接跳转和调用指令可以将文本符号作为目标,例如name(SB),但不是符号的偏移量name+4(SB)。



指令,寄存器和汇编程序指令总是以大写形式提醒您,汇编编程是一项艰巨的工作。(例外:在ARM平台下,代表当前goroutine的g寄存器被重新命名。)



在Go对象文件和二进制文件中,符号的完整名字是包的路径加上一个句点:fmt.Printf或math/rand.Int。由于汇编器的解析器将句点和斜线视为标点符号,因此这些字符串不能直接用作标识符名称。相反,汇编程序允许标识符中的中点字符U+00B7和除法斜杠U+2215,并将它们重写为纯句点和斜杠。在汇编源代码文件中,上面的符号写成fmt·Printf和math∕rand·Int。通过在编译时使用-S标志看到的汇编代码列表中直接显示了句点和斜杠,而不是在汇编程序中需要的Unicode替代字符(指上面的两个特殊Unicode字符)。



大部分手写的汇编文件中,不要在符号名中包含完整的包路径,因为链接器会在任何以句点开头的名字前面插入当前对象文件的路径:包含math/rand包的汇编源文件中,rand包的Int函数可以当做·Int来引用。这种便捷性避免了需要在自身的源代码中硬编码导入路径,可以让代码从一个地方移动到另一个地方时变得更容易。



指令
汇编程序使用各种指令将文本和数据绑定到符号名称。例如,这里是一个简单但是完整的函数定义。TEXT指令声明符号runtime·profileloop, 指令紧接在类似于函数的主体中。TEXT块中的最后一条指令必须是某种跳转,通常是RET(伪)指令。(如果不是,链接器会追加跳转到块自身的指令;TEXT块中没有fallthrough。)在符号之后,参数是标志(见下面)和栈帧的大小,是一个常量(参见下面的代码):



TEXT runtime·profileloop(SB),NOSPLIT,$8
MOVQ $runtime·profileloop1(SB), CX
MOVQ CX, 0(SP)
CALL runtime·externalthreadhandler(SB)
RET
这个函数的栈帧大小为8字节(MOVQ CX, 0(SP)操作栈指针),没有参数



一般情况下,栈帧大小后面跟着一个由减号分隔的参数大小。(这不是一个减法,只是特殊的语法。)栈帧大小是$24-8描述该函数有一个24字节的栈帧,并且需要一个8个字节的参数,它位于调用者的栈帧上。如果没有为TEXT指定NOSPLIT标志,则必须提供参数大小。对于使用Go标准的汇编函数,go vet将检查参数大小是否正确。



请注意,符号名称使用中点分隔组件,并且被定义为从伪寄存器SB开始的一个offsets。在Go源码的runtime包中,使用简称profileloop来调用。



全局数据符号使用初始化的一系列DATA指令来定义,并且跟在一个GLOBAL指令之后。每个DATA指令初始化一块指定的内存区域。没有明确初始化的内存区域会被置为零。标准的DATA指令形式为:



DATA symbol+offset(SB)/width, value
这样就初始化了symbol,内存在指定的offset处,带有指定的width和给定的value。一个symbol中的DATA指令必须是逐渐增长的offsets。



GLOBL指令声明一个符号是全局的。参数是可选的标志和需要声明为全局的数据的大小,除非DATA指令已初始化它,否则初始值将全部为零。GLOBAL指令必须跟在对应的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<>, 一个只读的64位table含有4字节的整数值。并声明runtime·tlsoffset一个4字节的,隐式地置零的变量,该变量不包含指针。



指令可能有一个或两个参数。如果有两个,第一个是比特掩码的标志,它可以写成数字表达式,多个掩码之间可以相加或者做逻辑或运算,或者可以写成友好可读的形式。这些值定义在头文件textflag.h中:



NOPROF = 1 (TEXT项使用.) 不优化NOPROF标记的函数。这个标志已废弃。
DUPOK = 2 在二进制文件中允许一个符号的多个实例。链接器会选择其中之一。
NOSPLIT = 4 (TEXT项使用.) 不插入预先检测是否将栈空间分裂的代码。程序的栈帧中,如果调用任何其他代码都会增加栈帧的大小,必须在栈顶留出可用空间。用来保护处理栈空间分裂的代码本身。
RODATA = 8 (DATA和GLOBAL项使用.) 将这个数据放在只读的块中。
NOPTR = 16 (DATA和GLOBAL项使用.)这个数据不包含指针所以就不需要垃圾收集器来扫描。
WRAPPER = 32 (TEXT项使用.) This is a wrapper function and should not count as disabling recover.
NEEDCTXT= 64 (对于TEXT项目。)这个函数是一个闭包,所以它使用它的传入上下文寄存器。
协调Runtime
要使垃圾收集正确运行,运行时必须知道所有全局数据和大多数堆栈帧中指针的位置。Go编译器在编译Go源文件时会发出此信息,但汇编程序必须明确定义这些信息。



标有NOPTR标志的数据符号(见上面)被视为不包含指向Runtime分配数据的指针。带有RODATA标志的数据符号被分配在只读存储器中,因此被视为隐式标记NOPTR。总的大小小于指针大小的数据符号也被视为隐式标记NOPTR。无法在汇编语言中定义包含指针的符号; 这种符号必须在Go源文件中定义。汇编源文件仍然可以通过名称来引用符号,即使这个符号没有使用DATA和GLOBL指令也是如此。一个很好的通用规则是,在Go代码中定义非只读的数据,而不是在汇编程序中。



每个函数都需要注释,标明在其参数、返回结果和本地栈帧上给出活动指针的位置。如果汇编函数没有指针类型的结果并且没有本地栈帧,或者没有调用函数,唯一需要做的是为函数在同名的包中定义一个Go函数原型(例如,syscall包中的函数Syscall应该在其TEXT指令中使用名称·Syscall而不是等效名称syscall·Syscall)。在更复杂的情况下,需要明确的注释出。这些注释使用在头文件funcdata.h中定义的伪指令。



如果一个函数没有参数并且没有结果,指针信息可以省略。这可以通过在TEXT指令中使用参数大小$n-0指出。否则,指针信息必须由Go源文件中的Go原型函数提供,即使汇编函数不是直接被Go代码调用的。(原型还会使用go vet检查参数引用。)在函数的开头,参数都假设是已经被初始化的,但是函数的返回结果会假设是未初始化的。如果在执行CALL指令时,结果中HOLD住一个指针,函数应该在开头就将返回结果初始化为零值,并且接着执行伪指令GO_RESULTS_INITIALIZED。这个指令记录了当前返回结果已经被初始化,并且在当栈帧转移和垃圾收集的时候扫描返回结果。非常具有代表性的是会安排汇编函数不返回指针或者不包含任何CALL指令;在Go标准库中的汇编函数都没有使用GO_RESULTS_INITIALIZED。



如果函数没有本地堆栈帧,则可以省略指针信息。这可以通过在TEXT指令中使用栈帧大小$0-n指出。如果函数不包含CALL指令,指针信息也可以省略。否则,本地栈帧不能包含指针,汇编必须通过执行NO_LOCAL_POINTERS伪指令来确认这种情况。由于堆栈大小调整是通过移动堆栈来实现的,栈指针可能在函数调用的时候发生改变:即使指向堆栈数据的指针也不能保存在局部变量中。



汇编程序函数应该总是给出Go原型,以提供参数和结果的指针信息,并用go vet检查访问偏移量的偏移量是否正确。



架构相关的细节
列出某种机器的全部指令和细节是不切实际的。如果要查看为特定机器定义了哪些指令,比如ARM,请查看该体系结构支持库的obj源代码,源码在src/cmd/internal/obj/arm目录中。在那个目录中是一个文件a.out.go; 它包含一长串以A开头的常量,如下所示:



const (
AAND = obj.ABaseARM + obj.A_ARCHSPECIFIC + iota
AEOR
ASUB
ARSB
AADD

这是该架构的汇编器和链接器已知的指令及其拼写列表。该列表中的每条指令都以首字母A开始,因此AAND表示按位和指令 AND(不带前导A),并以AND写入汇编源代码。枚举主要按字母顺序排列。(AXXX体系结构无关,在cmd/internal/obj程序包中定义 ,代表无效指令)。这些A名称的顺序与机器指令的实际编码无关。cmd/internal/obj包负责处理这些细节。



有关386和AMD64体系结构的说明均已列入cmd/internal/obj/x86/a.out.go。



这些架构共享共同寻址模式的标签名,例如 (R1)(直接寄存器寻址), 4(R1)(寄存器间接偏移)和 $foo(SB)(绝对地址)。汇编器还支持每种体系结构特有的一些(不一定是全部)寻址模式。下面的部分列出了这些。



前面几部分示例中的一个细节是指令中的数据从左向右流动: MOVQ $0, CX清除CX。即使在某些架构上顺序是相反的,这种规则也是适用的。



这里有一些对于Go所指的架构的相关的细节的描述。



32位英特尔386
指向g(goroutine)结构的Runtime指针通过MMU中其他未使用(就go而言)寄存器的值进行维护。如果源文件包含一个特别的头go_asm.h, 则为汇编程序定义一个与操作系统相关的宏get_tls



#include “go_asm.h”
runtime中,get_tls宏通过一个指向g指针的指针来加载它的参数寄存器,g结构包含m指针。加载g和m的序列使用CX,就像下面这样:



get_tls(CX)
MOVL g(CX), AX // Move g into AX.
MOVL g_m(AX), BX // Move g.m into BX.
寻址模式:



(DI)(BX2):地址DI加上BX2的位置。
64(DI)(BX2):地址DI加上BX2加上64 的位置。这些模式只接受1,2,4和8作为比例因子。
在使用编译器和汇编程序的-dynlink或-shared模式时,固定内存位置任何加载或存储(如全局变量)必须假定重写CX。因此,为了安全使用这些模式,除了在内存引用之间外,汇编源码通常应避免使用CX。



64位Intel 386(又名amd64)
这两种体系结构在汇编程序级别上表现基本相同。访问 64位版本的m和g指针的汇编代码与32位386相同,只是它使用MOVQ而不是MOVL:



get_tls(CX)
MOVQ g(CX), AX // Move g into AX.
MOVQ g_m(AX), BX // Move g.m into BX.
ARM
寄存器R10和R11 由编译器和链接保留使用。



R10指向g(goroutine)结构。在汇编源代码中,这个指针必须被称为g; 名称R10不被识别。



为了让人们和编译器更容易地编写汇编代码,ARM链接器允许一般寻址形式和DIV或者MOD伪指令,这些伪指令使用单个硬件指令可能无法表达。它实现这些多条指令形式,通常使用R11寄存器来保存临时值。手写汇编可以使用R11,但这样做需要确保链接程序不会使用它来实现函数中的任何其他指令。



定义一个TEXT时,指定帧大小$-4告诉链接器,这是一个叶函数,不需要在Entry上保存LR。



SP始终指向前面描述的虚拟堆栈指针。对于硬件寄存器,请使用R13。



条件码的语法是在指令中添加一个句点和一个或两个字母的代码,如下所示MOVW.EQ。可以附加多个代码:MOVM.IA.W。代码修饰符的顺序是无关紧要的。



寻址模式:



R0->16
R0»16
R0«16
R0@>16:对于«,左移16位的R0。其他代码是->(算术右移), »(逻辑右移)和 @>(右旋)。
R0->R1
R0»R1
R0«R1
R0@>R1:因为«,R0计数在左移R1。其他代码是->(算术右移), »(逻辑右移)和 @>(右旋)。
[R0,g,R12-R15]:对于多寄存器指令,该组包括 R0,g,和R12到R15。



(R5, R6):目的寄存器对。
ARM64
ARM64端口处于实验状态。



指令修饰符附加到指令后的句点。只有修饰符P(后置)和W(前递增): MOVW.P,MOVW.W



寻址模式:



(R5, R6):LDP/STP的寄存器对。
64位PowerPC,又名ppc64
64位PowerPC端口处于试验状态。



寻址模式:



(R5)(R61):R5加R6的位置。它是x86上的缩放模式,但是唯一允许的扩展是1。
(R5+R6):(R5)(R6
1)的别名
IBM z/Architecture,又名s390x
寄存器R10和R11保留。汇编程序在汇编某些指令时使用它们来保存临时值。



R13指向g(goroutine)结构。这个寄存器必须被称为g; 名称R13不被识别。



R15指向堆栈帧,通常只能使用虚拟寄存器SP和FP。



加载和存储多条指令在一系列寄存器上运行。寄存器范围由开始寄存器和结束寄存器指定。例如,LMG(R9), R5, R7将加载R5,R6和R7与在64位值0(R9),8(R9)和16(R9)分别。



存储和存储指令(如MVC和XC)的长度作为第一个参数写入。例如,XC $8, (R9), (R9)将在指定的地址处清除R9中八个字节。



如果一个向量指令将长度或索引作为参数,那么它将成为第一个参数。例如,VLEIF $1, $16, V2将16个值加载到V2索引之一中。使用向量指令时应注意确保它们在运行时可用。要使用矢量指令,机器必须同时具有矢量功能(设施列表中的位129)和内核支持。如果没有内核支持,矢量指令将不起作用(它将相当于一条NOP指令)。



寻址模式:



(R5)(R6*1):R5加R6的位置。它是x86上的缩放模式,但是唯一允许的尺寸是1。
MIPS,MIPS64
通用寄存器被命名R0到R31,浮点寄存器F0到F31。



R30保留指向g。 R23被用作临时寄存器。



在TEXT指令中,栈大小MIPS是$-4,对于MIPS64是$-8,指示链接器不保存LR。



SP指的是虚拟堆栈指针。对于硬件寄存器,请使用R29。



寻址模式:



16(R1):位置在R1加16。
(R1):别名0(R1)。
GOMIPS的环境变量的值(hardfloat或 softfloat)由预先定义GOMIPS_hardfloat或GOMIPS_softfloat提供给汇编代码。



不支持的操作码
汇编器旨在支持编译器,因此并非所有硬件指令都针对所有体系结构定义:如果编译器不生成它,它可能不在那里。如果您需要使用缺少的指令,有两种方法可以继续。



一种是直接修改汇编程序以支持该指令,这是直接的,但只有在指令可能再次使用时才值得。
相反,对于简单的一次性修改案例,可以使用BYTE 和WORD指令将明确的数据放入TEXT的指令流中。以下是386运行时如何定义64位原子加载函数。
// uint64 atomicload64(uint64 volatile* addr);
// so actually
// void atomicload64(uint64 *res, uint64 volatile *addr);
TEXT runtime·atomicload64(SB), NOSPLIT, $0-12
MOVL ptr+0(FP), AX
TESTL $7, AX
JZ 2(PC)
MOVL 0, AX // crash with nil ptr deref
LEAL ret_lo+4(FP), BX
// MOVQ (%EAX), %MM0
BYTE $0x0f; BYTE $0x6f; BYTE $0x00
// MOVQ %MM0, 0(%EBX)
BYTE $0x0f; BYTE $0x7f; BYTE $0x03
// EMMS
BYTE $0x0F; BYTE $0x77
RET



Category golang