当前位置: 首页 > 科技观察

Go语言汇编速成指南

时间:2023-03-19 18:12:16 科技观察

如果你想深入了解Go语言,Go语言汇编是一个绕不过去的环节。本文根据Go官方文档AQuickGuidetoGo'sAssembler介绍Go汇编。Go汇编是在Plan9汇编的基础上演化而来的新版本。如果需要进一步深入学习,推荐阅读Plan9汇编器手册。关于Go的汇编最重要的一点是它不是底层机器代码的直接表示。而是进行了一层抽象,但是抽象的不是很理想,所以称为半抽象指令集(semi-abstract)。因此,一些细节指令可以精确映射到机器代码,但有些则不能,正如您在本文的示例代码中看到的那样。由于各个处理器架构的指令集和寄存器不同,汇编代码实现需要适应各种处理器架构;当然,它还必须适应各种操作系统。环境操作系统:Ubuntu20.04.2LTS;x86_64Go:goversiongo1.16.2linux/amd64操作系统、处理器架构、Go版本不同,可能导致同一源码编译运行时的寄存器值、内存地址、数据结构有差异,等本文仅包含linux/amd64系统架构下的64位可执行程序示例。本文仅保证当前环境下学习过程中分析数据的准确性和有效性。代码清单go.modmodulego-asm-guidego1.16main.gopackagemainimport("fmt""unsafe")typeTextstruct{Languagestring_uint8Lengthint}funcadd(a,bint)intfuncaddX(a,bint)int//获取Text的Length域的值funclength(text*Text)int//获取Text结构体的大小funcsizeOfTextStruct()intfuncmain(){println(add(1,2))println(addX(1,2))text:=&Text{Language:"chinese",Length:1024,}fmt.Println(text)println(length(text))println(sizeOfTextStruct())println(unsafe.Sizeof(*text))}main.s#include"textflag.h"#include"go_asm.h"//这个文件自动生成TEXTadd(SB),NOSPLIT,$0-24MOVQa+0(FP),AX//读取第一个一个参数MOVQb+8(FP),BX//读取第二个参数ADDQBX,AXMOVQAX,ret+16(FP)//保存结果RETTEXTaddX(SB),NOSPLIT,$0-24MOVQa+0(FP),AXMOVQb+8(FP),BXADDQBX,AXMOVQ$x(SB),BX//读取全局变量x的地址MOVQ0(BX),BX//读取全局变量xADDQ的值BX、AXMOVQAX、ret+16(FP)RETTEXT长度(SB),NOSPLIT,$0-16MOVQ文本+0(FP),AXMOVQText_Length(AX),AX//通过字段在结构体中的偏移量读取字段值MOVQAX,ret+8(FP)RETTEXTsizeOfTextStruct(SB),NOSPLIT,$0-8MOVQ$Text__size,AX//保存结构体的大小到AX寄存器MOVQAX,ret+0(FP)RETDATAx+0(SB)/8,$10//初始化全局变量x,赋值10GLOBLx(SB),RODATA,$8//声明全局变量x常用命令学习Go汇编需要时间。如果你不是天才,没有扎实的汇编基础,看教程花一天时间就能马上掌握,但是熟练使用工具可以让我们有效地学习Go汇编。编译生成程序集这是Go语言的内置函数,可以通过以下命令查看。当然也可以指定各种参数进行详细研究,这里不再赘述。gotoolcompile-Sx.gogobuild-gcflags-Sx.go$catx.gopackagemainfuncmain(){println(3)}$gotoolcompile-Sx.go"".mainSTEXTsize=77args=0x0locals=0x10funcid=0x00x000000000(x.go:3)TEXT"".main(SB),ABIInternal,$16-00x000000000(x.go:3)MOVQ(TLS),CX0x000900009(x.go:3)CMPQSP,16(CX)0x000d00013(x.go:3)PCDATA$0,$-20x000d00013(x.go:3)JLS700x000f00015(x.go:3)PCDATA$0,$-10x000f00015(x.go:3)SUBQ$16,SP0x001300019(x.go:3)MOVQBP,8(SP)0x001800024(x.go:3)LEAQ8(SP),BP0x001d00029(X.Go:3)funcdata$0,gclocals·33cdeccccebe80329f1fdbee7f5874cb(sb)0x001d00029(x.go:3)funcdata$1,gclocals·gclocals.go:4)NOP0x002000032(x.go:4)CALLruntime.printlock(SB)0x002500037(x.go:4)MOVQ$3,(SP)0x002d00045(x.go:4)CALLruntime.printint(SB)0x003200050(x.go:4)CALLruntime.printnl(SB)0x003700055(x.go:4)CALLruntime.printunlock(SB)0x003c00060(x.go:5)MOVQ8(SP),BP0x004100065(x.go:5)ADDQ$16,SP0x004500069(x.go:5)RET0x004600070(x.go:5)NOP0x004600070(x.go:3)PCDATA$1,$-10x004600070(x.go:3)PCDATA$0,$-20x004600070(x.go:3)CALLruntime.morestack_noctxt(SB)0x004b00075(x.go:3)PCDATA$0,$-10x004b00075(x.go:3)JMP00x000064488b0c2500000000483b6110763748dH..%..H;a.v7H0x001083ec1048896c2408488d6c24080f1f00...H.l$.H.l$....0x0020e80000000048c7042403000000e80000.....H..$......0x00300000e800000000e800000000488b6c24.......H.l$0x0040084883c410c3e800000000ebb3.H......rel5+4t=17TLS+0rel33+4t=8runtime.printlock+0rel46+4t=8runtime.printint+0rel51+4t=8runtime.printnl+0rel56+4t=8runtime.printunlock+0rel71+4t=8runtime.morestack_noctxt+0go.cuinfo.packagename.SDWARFCUINFOdupoksize=00x00006d61696emain""..inittaskSNOPTRDATAsize=240x00000000000000000000000000000000..............0x00100000000000000000.....gclocals·33cdeccccebe80329f1fdbee7f5874cbSRODATAdupoksize=80x00000100000000000000decompilergotoolobjdump这个是自带的反编译命令围棋语言$catx.gopackagemainfuncmain(){println(3)}$gobuildx.go$gotoolobjdump-smain.mainxTEXTmain.main(SB)/home/foo/codes/goinmemory/go_asm/x.去x.go:30x45ec6064488b0c25f8ffffffMOVQFS:0xfffffff8,CXx.go:30x45ec69483b6110CMPQ0x10(CX),SPx.go:30x45ec6d7637JBE0x45eca6x.go:3xec0x45ec61$4883,SPx45ec6f01083.go:30x45ec7348896c2408MOVQBP,0x8(SP)x.go:30x45ec78488d6c2408LEAQ0x8(SP),BPx.go:40x45ec7d0f1f00NOPL0(AX)x.go:40x45ec80e8fb05fdffCALLruntime.printfdff调用(SB)x.go:40x45ec8548c7042403000000MOVQ$0x3,0(SP)x.go:40x45ec8de8ee0dfdffCALLruntime.printint(SB)x.go:40x45ec92e8a908fdffCALLruntime.printnl(SB)x.go:40x45ec97e86406fdffCALLruntime.printunlock(SB)x.go:50x45ec9c488b6c2408MOVQ0x8(SP),BPx.go:50x45eca14883c410ADDQ$0x10,SPx.go:50x45eca5c3RETx.go:30x45eca6e8b5afffffCALLruntime.morestack_noctxt(SB)x.go:30x45ecabebb3JMPmain.main(SB)objdumpthis是Linux环境下的通用反编译工具,不仅仅针对Go程序$objdump--disassemble=main.mainxx:文件格式elf64-x86-64Disassemblyofsection.text:000000000045ec60:45ec60:64488b0c25f8ffmov%fs:0xfffffffffffffff8,%rcx45ec67:ffff45ec69:483b6110cmp0x10(%rcx),%rsp45ec6d:7637jbe45eca645ec6f:4883ec10sub$0x10,%rsp45ec73:48896c2408mov%rbp,0x8(%rsp)45ec78:488d6c2408lea0x8(%rsp),%rbp45ec7d:0f1f00nopl(%rax)45ec80:e8fb05fdffcallq42f28045ec85:48c70424030000movq$0x3,(%rsp)45ec8c:0045ec8d:e8ee0dfdffcallq42fa8045ec92:e8a908fdffcallq42f54045ec97:e86406fdffcallq42f30045ec9c:488b6c2408mov0x8(%rsp),%rbp45eca1:4883c410add$0x10,%rsp45eca5:c3retq45eca6:e8b5afffffcallq459c6045ecab:ebb3jmp45ec60伪注册某些符号(例如R1或LR)是预定义的,并根据体系结构引用确切的寄存器集。有四个预先声明的符号代表伪寄存器。这些不是真实的寄存器,而是由工具链维护的虚拟寄存器。伪寄存器组对于所有体系结构都是相同的:FP:帧指针:用于引用参数和局部变量。PC:程序计数器:用于跳转和分支。SB:静态基指针:用于声明全局符号。SP:堆栈指针:本地堆栈帧中的最高地址。所有开发人员定义的符号都被引用为伪寄存器FP和SB的偏移量。SB比如foo(SB)代表一个全局符号foo,就是我们在Go开发中声明的函数名或者全局变量名。要声明全局变量,请使用GLOBL关键字。Go汇编中声明一个uint64类型的全局变量的代码如下:GLOBLfoo(SB),NOPTR,$8全局变量使用DATA关键字进行初始化。将全局变量foo的初始值设置为10的汇编代码如下:DATAfoo+0(SB)/8,$10使用TEXT关键字声明了一个函数。Go汇编中声明函数的代码如下:TEXTfoo(SB),NOSPLIT,$0-0RETFP伪寄存器是一个用来引用函数参数的虚拟帧指针。编译器维护一个虚拟帧指针并访问堆栈上的参数作为此伪寄存器的偏移量。因此,在64位机器上,0(FP)表示函数的第一个参数,8(FP)表示函数的第二个参数,依此类推。但是,这样引用函数参数时,必须在开头加上名字,如first_arg+0(FP)、second_arg+8(FP)。汇编程序执行此约定,拒绝普通的0(FP)和8(FP)。实际名称在语义上无关紧要,但应该用于记录参数名称。值得强调的是,FP始终是一个伪寄存器,而不是硬件寄存器,即使在具有硬件帧指针的体系结构上也是如此。在上面的代码清单中,add函数的汇编实现清楚地证明了这一点。通常,在Go语言中,函数的返回值其实就是函数的一个参数。SPSP伪寄存器是一个虚拟堆栈指针,用于引用帧局部变量并为函数调用准备参数。它指向本地堆栈帧中的最高地址,因此引用应使用[?framesize,0]范围内的负偏移量:x-8(SP)、y-4(SP)等。在具有硬件寄存器的体系结构上命名为SP,名称前缀将对虚拟堆栈指针的引用与对体系结构SP寄存器的引用区分开来。例如,x-8(SP)和-8(SP)是不同的内存位置:第一个是指虚拟堆栈指针伪寄存器,而第二个是指硬件的SP寄存器。Directives在英语中,instruction是instruction的意思,directive也是instruction的意思,但是它们的意思其实是有很大区别的。指令一般与处理器体系结构和汇编语言的指令集有关。例如:MOVQ、RET是指令。指令一般代表编程语言定义的一些特殊关键字,用于辅助编码。例如:GLOBL、DATA、TEXT都是指令。在本文中,作者同意将指令称为关键字。声明全局变量的语法格式为:GLOBLsymbol(SB),flags,widthGLOBL在声明全局变量时等同于var或const。初始化全局变量的语法格式为:DATAsymbol+offset(SB)/width,实现值函数的语法格式为:TEXT[package]symbol(SB),flags,$framesize-argumentssize//instructionsRETTEXTisequivalent在func中声明函数时。定义宏的语法格式为:单行:#defineNOSPLIT4多行:#defineDISPATCH(NAME,MAXSIZE)\CMPQCX,$MAXSIZE;\JA3(PC);\MOVQ$NAME(SB),AX;\jmpAX引用头文件的格式为:#include"textflag.h"flagsGo语言在runtime/textflag.h源文件中定义了一些特殊的宏,用于标记全局符号(函数和全局变量)。#defineNOPROF1#defineDUPOK2#defineNOSPLIT4#defineRODATA8#defineNOPTR16#defineWRAPPER32#defineNEEDCTXT64#defineTLSBSS256#defineNOFRAME512#defineREFLECTMETHOD1024#defineTOPFRAME2048之后才会变得明显读者对可执行文件的结构、数据在可执行文件中的存储方式、函数调用约定有了比较深入的了解。否则,这件事不是三言两语能说清楚的。与Go类型和常量交互如果一个包有任何.s文件,那么gobuild将指示编译器生成(发出)一个名为go_asm.h的特殊头文件,然后可以通过#include由.s文件引用。该文件包含通过#define指令定义的Go结构字段的偏移量、Go结构类型的大小以及当前包中定义的大多数const声明的符号常量。Go汇编应该避免对Go类型的布局做出假设,而是使用这些常量。这提高了汇编代码的可读性,并使其对Go类型定义中的数据布局更改或Go编译器使用的布局规则具有鲁棒性。在上面的代码列表中,在length函数的汇编代码中,通过Text_Length这个字段的偏移量读取Text结构的Length字段的值。Text_Length在go_asm.h文件中定义。虽然我们从来没有定义这个头文件,但是可以通过#include"go_asm.h"直接引用它。同样,在sizeOfTextStruct函数的汇编代码中,常量Text__size表示Text结构对象的大小。运行时协调为了正确运行垃圾收集,运行时必须知道指针在所有全局数据和大多数堆栈帧中的位置。Go编译器在编译Go源文件时生成此信息,但汇编器必须显式定义它。标有NOPTR标志的数据符号被认为不包含指向运行时分配数据的指针。标记有RODATA的数据符号分配在只读存储器中,因此被认为隐式标记有NOPTR。总大小小于指针的数据符号也被认为是用NOPTR隐式标记的。包含指针的符号不能在汇编源文件中定义,此类符号必须在Go源文件中定义;在汇编代码中,这些符号可以通过名称直接应用。一个好的通用经验法则是在Go中定义所有非RODATA数据,而不是在汇编中。应始终为汇编函数提供Go原型,以提供参数和结果的指针信息,并使用govet检查用于访问它们的偏移量是否正确。例如代码列表中的函数声明:funcadd(a,bint)intfuncaddX(a,bint)intfunclength(text*Text)intfuncsizeOfTextStruct()intrunningeffect编译运行程序,然后反编译可执行程序,对比源代码,我们可以看到一些变化。本文先介绍到这里。