您现在的位置是:首页 > 行业发展

Golang 汇编器快速指南

智慧创新站 2025-05-26【行业发展】50人已围观

简介汇编器基于Plan9汇编器的输入风格。详细文档在这里。如果你准备写一些汇编语言,那么虽然此文档是基于Plan9的,你也应该通读。本文提供了语法摘要和与其解释内容的区别,并且描述了编写汇编与Go交互时所适用的特性。最重要的是,Go的汇编器并不是底层的直接表示。有一些是直接的映射,有一些不是。这是因为编...

汇编器基于Plan9汇编器的输入风格。详细文档在这里。如果你准备写一些汇编语言,那么虽然此文档是基于Plan9的,你也应该通读。本文提供了语法摘要和与其解释内容的区别,并且描述了编写汇编与Go交互时所适用的特性。

最重要的是,Go的汇编器并不是底层的直接表示。有一些是直接的映射,有一些不是。这是因为编译套件在常规流程中并不需要汇编器。相反,编译器针对一种半抽象的指令集操作,而且指令选择发生在一部分发生在代码生成之后。汇编器工作在半抽象状态下,所以当你看到类似指令MOV,工具链实际生成的操作也许并不是移动,而是清除指令或者加载指令。或者也可能与实际指令完全对应。一般来说,特定机器的操作倾向于它们自身,而更通用的概念(如内存移动、子程序调用、返回等)则更加抽象。细节根据体系架构会有不同,我们非常抱歉关于这种不精确,情况还没有完全定义。

汇编器程序是一种解析半抽象指令集的描述并将其转换为输入给链接器指令的方法。如果想要看到给定体系的汇编指令(如:amd64),在标准库源码中有很多实例(比如runtimemath/big)。你可以测试编译器生成的汇编代码(实际输出可能和这里有出入):

$(){println(3)}$GOOS=linuxGOARCH=文件,它们是:

NOPROF=1(TEXT部分)不要分析标记的函数,这个标志已经废弃了。

DUPOK=2在一个二进制文件中可以有多个此符号实例。链接器会挑选一个去使用。

RODATA=8(DATA和GLOBL部分)将数据放到只读段。

NOPTR=16(DATA和GLOBL部分)此数据不包含指针,所以不需要被GC扫描。

WRAPPER=32(TEXT部分)这是个封装函数,而且不应该被视为禁用recover。

NEEDCTXT=64(TEXT部分)这个函数是闭包,所以使用其传入的上下文寄存器。

LOCAL=128这个符号是在动态共享库本地。

TLSBSS=256(DATA和GLOBL部分)将此数据放入线程本地存储。

NOFRAME=512(TEXT部分)即使不是叶子函数,也不要插入指令来分配栈帧和存储、恢复返回地址。仅在声明栈帧大小为0时有用。

运行时的协调

为了让垃圾回收器正确运行,运行时必须知道所有全局数据中和大部分的栈帧中的指针所在地址。Go编译器在编译Go源文件时会暴露这些信息,但是汇编程序必须显式的定义它。

如果数据符号被带有NOPTR标志,就会被当做不包含运行时数据指针。如果数据符号带有RODATA标志,表示在只读内存分配,也相当于显示声明NOPTR标记。一个数据符号总大小小于一个指针,则相当于显示声明NOPTR。在汇编源文件不可能定义包含指针的符号,这种符号必须在Go源文件中定义。汇编源代码还可以不用DATA和GLOBL,用名称指向一个符号。一个好的通用经验法则是,在Go中定义定义所有不是RODATA的符号,而不是在汇编中定义。

每个函数还需要注解,以给出在参数、返回、本地栈帧中存活指针的位置。对于没有指针返回,也没有本地栈帧和函数调用的汇编函数,仅要求在同包下的Go源文件中定义函数的Go原始类型。汇编函数的名字必须包含包名部分(比如:函数Syscall在包syscall下面,在它的TEXT部分应该使用名字·Syscall替代相同名字syscall·Syscall)。对于更加复杂的情形来说,需要显示注解。这些注解使用标准include"go_"复制代码

在运行时中,宏get_tls加载g的指针的指针到它的参数寄存器中,g结构体包含了一个m指针。这有另外一个特殊的header包含了g每个元素的偏移量,叫做go_。用CX读取g和m的顺序如下:

include"go_"get_tls(CX)MOVLg(CX),AX//_m(AX),BX//复制代码

注意:上面的代码仅可以在runtime包有效,get_也适用于arm,amd64andamd64p32,go_适用于所有的体系结构。

寻址模式:

(DI)(BX*2):DI加BX*2的地址。

64(DI)(BX*2):DI加BX*2加64的地址位置。这个模式仅仅接受1,2,4,8作为比例因子。

当使用编译器和汇编器的-dynlink和-shared模式,任何在固定内存地址加载和存储(例如:全局变量),必须假设覆盖了CX。因此,为了用户安全使用这些模式的原因,除了在内存引用上,汇编源码应该避免使用CX。

64-bitIntel386(别名amd64)

两种体系在汇编器层面上,行为基本一致。64位版本的汇编代码访问m和g指针和32位386一样,只是前者使用MOVQ而不是MOVL。

get_tls(CX)MOVQg(CX),AX//_m(AX),BX//复制代码
ARM

R10和R11寄存器被编译器和链接器保留。

R10指向g(goroutine)结构体。在汇编器源码中,这个指针必须叫做g。不会识别R10这个名称。

为了让人类和编译器更简单的写汇编,ARM链接器允许使用单个硬件指令无法表达的普通寻址方式和伪指令,例如:DIV和MOD。它用多个指令实现了这种形式,通常使用R11来保存临时变量。手写汇编可以使用R11,但是要求确认链接器没有同样使用它来实现任何函数中其他的指令。

当定义一个TEXT时,指定栈帧大小$-4表示告诉链接器,这是一个叶子函数,也就是不需要在入口保存LR。

之前说过,名字SP总是表示一个虚拟的栈指针。如果是硬件寄存器,使用R13。

条件代码语法是在指令后面添加句点和一两个字母的代码,比如:。可能添加多种代码:。代码修饰符顺序无关紧要。

寻址模式:

R0-16R016R016R0@16:对于左移R016位。其他代码是:-算数右移,逻辑右移,@向右旋转。

R0-R1R0R1R0R1R0@R1:对于左移R0R1位。其他代码是:-算数右移,逻辑右移,@向右旋转。

[R0,g,R12-R15]:多寄存器指令中,包含R0g,R12-R15的集合。

(R5,R6):目标寄存器对。

ARM64

ARM64端口处于试验状态。

R18是平台寄存器,被Apple平台保留。为了不被误用,该寄存器命名为R18_PLATFORM。R27和R28被编译器和链接器保留。R29是栈帧指针。R30是链接寄存器。

指令修饰符会加在指令后,跟在句点后面。仅有P后递增和W预递增两个修饰符:。

地址模式:

R0-16R016R016R0@16:和32位ARM一样。

$(812):将8左移12位。

8(R0):将R0的值和8相加。

(R2)(R0):R0的值加上R2的值。

:UXTB:

:SXTB:提取R0的第8位,并扩展零值到R0的大小。:左移位。imm可以是0,1,2,3,4。其他扩展包括SXTH(32位)和SXTX(64位)。

(R5,R6):寄存器对给LDAXP/LDP/LDXP/STLXP/STP/STP。

引用:GoARM64AssemblyInstructionsReferenceManual

PPC64

GOARCH等于ppc64和ppc64le使用此汇编器。

引用:GoPPC64AssemblyInstructionsReferenceManual

IBMz/Architecture,

R10和R11两个寄存器被保留。汇编器在汇编某些指令时用它们存放临时值。

R13指向了g(goroutine)结构体。此寄存器必须使用名字g,R13这个名字不会被识别。

R15指向栈帧,它应该只能通过虚拟寄存器SP和FP访问。

多个加载和存储指令对一系列寄存器进行操作。寄存器的范围由开始寄存器和结束寄存器指定。例如,LMG(R9),R5,R7会使用0(R9),8(R9)和16(R9)的64位的值加载到R5,R6和R7中。

像MVC和XC这样的存储-存储指令以长度作为第一个参数。比如:XC$8,(R9),(R9)会清空R9特定地址的8个字节。

如果以长度或索引作为向量指令的参数,那么是第一个参数。比如,VLEIF$1,$16,V2会将16加载到v2的索引1中。使用向量指令要确保他们在运行时是可用的。要使用向量指令,机器必须同时有向量功能(功能列表中的位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表示虚拟栈指针。硬件寄存器使用R2。

寻址模式:

16(R1):R1+16的位置。

(R1):0(R1)的别名。

通过预定义GOMIPS_hardfloat或者GOMIPS_softfloat可以使汇编代码使用GOMIPS环境变量(hardfloat或者softfloat)。

通过预定义GOMIPS64_hardfloat或者GOMIPS64_softfloat可以使汇编代码使用GOMIPS64环境变量(hardfloat或者softfloat)。

不支持的操作码

汇编器的设计宗旨是为了支援编译器,因此,不是所有体系平台的硬件指令都被定义出来:如果编译器不生成,那么就没有。如果你需要使用丢失的指令,有两个方法。一是升级汇编器以支持指令,这是最简单的,但是只有在多次使用时才值得去做。相反,对于简单的一次使用的情况,可以使用BYTE和WORD指令显示的将数据放到TEXT指令流中。这就是386运行时怎么定义64位原子加载函数的。

//uint64atomicload64(uint64volatile*addr);//soactually//voidatomicload64(uint64*res,uint64volatile*addr);TEXTruntime·atomicload64(SB),NOSPLIT,$0-12MOVLptr+0(FP),AXTESTL$7,AXJZ2(PC)MOVL0,AX//crashwithnilptrderefLEALret_lo+4(FP),BX//MOVQ(%EAX),%MM0BYTE$0x0f;BYTE$0x6f;BYTE$0x00//MOVQ%MM0,0(%EBX)BYTE$0x0f;BYTE$0x7f;BYTE$0x03//EMMSBYTE$0x0F;BYTE$0x77RET

很赞哦!(126)