前言
本文是笔者学习Go汇编
基础知识的笔记。本文的次要内容都是借鉴文章Go汇编语言,笔者在原文根底上扩大了局部写的比拟简略的内容,删除了一些笔者本人没有了解的内容。笔者心愿读者敌人们浏览本文后,可能:
- 理解
Go汇编
呈现的背景、起因和作用; - 理解
汇编
、Go汇编
和Go语言
三者之间的关系,以及它们呈现的背景和起因; - 利用所学的
Go汇编
根本语法常识,看懂golang库中一些汇编代码,同时也能手工写一些简略的Go汇编代码
; - 通过golang代码输入的
Go汇编代码
来学习golang语言的一些底层实现原理;
不过笔者能力无限,本文中必然存在不少谬误或者脱漏之处,欢送读者敌人们给予批评和斧正。
Go汇编简介
Go汇编语言中写道:
Go语言中很多设计思维和工具都是传承自Plan9操作系统,Go汇编语言也是基于Plan9汇编演变而来。依据Rob Pike的介绍,大神Ken Thompson在1986年为Plan9零碎编写的C语言编译器输入的汇编伪代码就是Plan9汇编的前身。所谓的Plan9汇编语言只是便于以手工形式书写该C语言编译器输入的汇编伪代码而已。
Go汇编
继承自Plan9汇编
,其绝对于常见的汇编语言
一个很大的特点是:可跨平台的,可移植的,与具体的底层操作系统无关的。咱们在golang的库中,常常能看到*.s
文件,这些都是汇编代码,咱们利用Go汇编
,能够更加高效的操作计算机CPU和内存,因而这些*.s
文件通常都是为了进步代码运行效率。Go汇编
语法和常见的汇编语言
语法相似,一些中央做了简化,例如Go汇编
中设置了一些伪寄存器,不便开发者应用。
Go汇编入门
汇编基本知识
Go汇编语言中写道:
汇编语言其实是一种非常简单的编程语言,因为它面向的计算机模型就是非常简单的。让人感觉汇编语言难学次要有几个起因:不同类型的CPU都有本人的一套指令;即便是雷同的CPU,32位和64位的运行模式仍然会有差别;不同的汇编工具同样有本人特有的汇编指令;不同的操作系统和高级编程语言和底层汇编的调用标准并不相同。
咱们要想学习Go汇编
,首先须要理解汇编
中很根底的两个常识:
- 寄存器
- 内存模型
咱们编写汇编
代码过程中,始终贯通着这两个知识点,Go汇编
也不例外,所以学习这些常识能够帮忙咱们更好的了解Go汇编
。
寄存器
CPU只能计算,不能存储数据,程序计算中须要的数据都存储专用的硬件设施中,包含寄存器,一级缓存,二级缓存,RAM内存,磁盘等;因为CPU计算速度很快,读取/存储数据就会成为计算性能的瓶颈,古代计算机应用很多不同个性的存储设备,放慢数据的I/O,尽量进步程序的计算速度,其中寄存器就是一种容量很小,然而数据I/O十分高的专用存储设备,CPU会利用存放机来存储程序中罕用的数据,例如循环中的变量。下图是计算机中的存储设备阐明:
寄存器不依附地址辨别数据,而依附名称,每一个寄存器都有本人的名称,咱们通知 CPU 去具体的哪一个寄存器拿数据,这样的速度是最快的。有人比喻寄存器是 CPU 的零级缓存。寄存器是CPU中最重要的资源,每个要解决的内存数据原则上须要先放到寄存器中能力由CPU解决,同时寄存器中解决完的后果须要再存入内存。Tips: 下面的粗体字表明的准则,咱们简称为“寄存器很重要准则”,请记住这条准则,前面咱们在剖析汇编代码的时候,会反复强调这条准则
内存模型
内存模型是一个比拟形象的概念,笔者并不能很好解释这个概念。笔者本人的了解是寄存器容量很小,程序运行所需的大部分数据还是要存储在其余内存中,因而咱们须要理解,程序运行过程中,CPU如何操作各种内存,内存又是如何调配来存储各种数据,特地是在多cpu,多线程的状况下,所以咱们人为的制订了一套协定来标准这些行为,笔者把这个协定了解成内存模型。
上面笔者将展现一张X86-64(十分罕用的计算机架构,它是AMD公司于1999年设计的x86架构的64位拓展,向后兼容于16位及32位的x86架构,X86-64目前正式名称为AMD64;咱们前面的汇编代码都是基于X86-64架构)架构的体系结构图,图中咱们将略微具体解说下内存模型,以及上文提到的寄存器和一些汇编指令。
1.咱们先来关注下图中最右边的Memory
局部,其示意的就是内存模型
- 内存从text局部到stack局部是从低地址(low)到高地址(high)增长;
- text示意代码段,个别用于存储要执行的指令数据,能够了解成存储咱们要执行的程序的代码,代码段个别都是只读的;
- rodata和data都是数据段,个别用于存储全局数据,其中rodata示意只读数据(read only data);
- heap示意堆,个别用于存储动态数据,堆的空间比拟大,能够存储较大的数据,例如go中创立的构造体;很多具备垃圾回收(gc)的语言中,如java,go,堆中的数据都是gc扫描后被主动清理回收的;
- stack示意栈,个别用于存储函数调用中相干的数据,例如函数中的局部变量,栈的特点就是FIFO,函数调用开始的时候,数据入栈,函数调用完结后,数据出栈,栈空间发出;go中,函数的入参和返回值也是通过栈来存储;
汇编语言入门教程中对于内存模型有比拟具体的阐明,读者敌人们能够自行查看。
2.而后咱们来看下两头Register
局部,它是X86提供的寄存器。寄存器是CPU中最重要的资源,每个要解决的内存数据原则上须要先放到寄存器中能力由CPU解决,同时寄存器中解决完的后果须要再存入内存。X86中除了状态寄存器FLAGS
和指令寄存器IP
两个非凡的寄存器外,还有AX
、BX
、CX
、DX
、SI
、DI
、BP
、SP
几个通用寄存器。在X86-64中又减少了八个以R8-R15
形式命名的通用寄存器。提前记住这些寄存器名字,前面的示例汇编代码中会波及到。
3.最初咱们看下左边的Instructions
局部,它是X86的指令集。CPU是由指令和寄存器组成,指令是每个CPU内置的算法,指令解决的对象就是全副的寄存器和内存。咱们能够将每个指令看作是CPU内置规范库中提供的一个个函数,而后基于这些函数结构更简单的程序的过程就是用汇编语言编程的过程。指令集指令的具体含意,能够自行百度。
咱们参考汇编语言学习中的一个示例来展现一下汇编语言编程。这个例子是一个性能很简略的函数:两数相加,后果赋值给一个变量sum
,即sum = 5 + 6
。
.data ;此为数据区sum DWORD 0 ;定义名为sum的变量.code ;此为代码区main PROC mov eax,5 ;将数字5送入而eax寄存器 add eax,6 ;eax寄存器加6 mox sum,eax INVOKE ExitProcess,0 ;完结程序main ENDP
略微解释一下:
mov eax,5
中eax
就是上文提到的通用寄存器AX
,X86-64指令集外面咱们能够不写后面的r
;- 咱们看到代码中
.data
和.code
别离示意数据区和代码区,验证了上文提到的X86-64内存模型中的text
和data
局部; - 程序实现的性能是
sum = 5 + 6
,然而咱们不能间接在sum
的内存外面做5 + 6
的计算,记住上文提到的寄存器的一条重要准则:每个要解决的内存数据原则上须要先放到寄存器中能力由CPU解决,同时寄存器中解决完的后果须要再存入内存。
Go汇编语法
本文不会全面的介绍Go汇编
语法,笔者本人也不分明到底有多少汇编命令;咱们会介绍一些罕用的命令,帮忙咱们前面浏览和了解Go汇编
代码。
1.咱们首先会介绍下Go汇编
的伪寄存器。
Go汇编为了简化汇编代码的编写,引入了PC、FP、SP、SB四个伪寄存器。四个伪寄存器加其它的通用寄存器就是Go汇编语言对CPU的从新形象,该形象的构造也实用于其它非X86类型的体系结构。
四个伪寄存器和X86/AMD64的内存和寄存器的互相关系如下图:
FP: Frame pointer
:伪FP寄存器对应函数的栈帧指针,个别用来拜访函数的参数和返回值;golang语言中,函数的参数和返回值,函数中的局部变量,函数中调用子函数的参数和返回值都是存储在栈中的,咱们把这一段栈内存称为栈帧(frame),伪FP寄存器对应栈帧的底部,然而伪FP只包含函数的参数和返回值这部分内存,其余局部由伪SP寄存器示意;留神golang中函数的返回值也是通过栈帧返回的,这也是golang函数能够有多个返回值的起因;PC: Program counter
:指令计数器,用于分支和跳转,它是汇编的IP寄存器的别名;SB: Static base pointer
:个别用于申明函数或者全局变量,对应代码区(text)内存段底部;SP: Stack pointer
:指向以后栈帧的局部变量的开始地位,个别用来援用函数的局部变量,这里须要留神汇编中也有一个SP寄存器,它们的区别是:1.伪SP寄存器指向栈帧(不包含函数参数和返回值局部)的底部,真SP寄存器对应栈的顶部;所以伪SP寄存器个别用于寻址函数局部变量,真SP寄存器个别用于调用子函数时,寻址子函数的参数和返回值(前面会有具体示例演示);2.当须要辨别伪寄存器和真寄存器的时候只须要记住一点:伪寄存器个别须要一个标识符和偏移量为前缀,如果没有标识符前缀则是真寄存器。比方(SP)、+8(SP)没有标识符前缀为真SP寄存器,而a(SP)、b+8(SP)有标识符为前缀示意伪寄存器;
2.Go汇编
罕用语法
上面咱们重点介绍几个Go汇编
罕用语法,不便咱们后续浏览了解示例Go汇编
代码。
常量
Go汇编
语言中常量以$
美元符号为前缀。常量的类型有整数常量、浮点数常量、字符常量和字符串常量等几种类型。以下是几种类型常量的例子:$1 // 十进制$0xf4f8fcff // 十六进制$1.5 // 浮点数$'a' // 字符$"abcd" // 字符串
DATA
命令用于初始化包变量,DATA
命令的语法如下:DATA symbol+offset(SB)/width, value
其中symbol
为变量在汇编语言中对应的标识符,offset
是符号开始地址的偏移量,width
是要初始化内存的宽度大小,value
是要初始化的值。其中以后包中Go语言定义的符号symbol
,在汇编代码中对应·symbol
,其中·
中点符号为一个非凡的unicode符号;DATA
命令示例如下DATA ·Id+0(SB)/1,$0x37DATA ·Id+1(SB)/1,$0x25
这两条指令的含意是将全局变量
Id
赋值为16进制数0x2537,也就是十进制的9527;
咱们也能够合并成一条指令DATA ·Id+0(SB)/2,$9527
GLOBL
命令用于将符号导出,例如将全局变量导出(所谓导出就是把汇编中的全局变量导出到go代码中申明的雷同变量上,否则go代码中申明的变量感知不到汇编中变量的值的变动),其语法如下:GLOBL symbol(SB), width
其中symbol
对应汇编中符号的名字,width
为符号对应内存的大小;GLOBL命令
示例如下:GLOBL ·Id, $8
这条指令的含意是导出一个全局变量Id
,其大小是8字节(byte);
联合DATA
和GLOBL
指令,咱们就能够初始化并导出一个全局变量GLOBL ·Id, $8DATA ·Id+0(SB)/1,$0x37DATA ·Id+1(SB)/1,$0x25DATA ·Id+2(SB)/1,$0x00DATA ·Id+3(SB)/1,$0x00DATA ·Id+4(SB)/1,$0x00DATA ·Id+5(SB)/1,$0x00DATA ·Id+6(SB)/1,$0x00DATA ·Id+7(SB)/1,$0x00
TEXT
命令是用于定义函数符号,其语法如下TEXT symbol(SB), [flags,] $framesize[-argsize]
函数的定义局部由5个局部组成:TEXT指令、函数名、可选的flags标记、函数帧大小和可选的函数参数大小。
其中TEXT
用于定义函数符号,函数名中以后包的门路能够省略。函数的名字前面是(SB),示意是函数名符号绝对于SB伪寄存器的偏移量,二者组合在一起最终是相对地址。作为全局的标识符的全局变量和全局函数的名字个别都是基于SB伪寄存器的绝对地址。标记局部用于批示函数的一些非凡行为,标记在textlags.h
文件中定义,常见的NOSPLIT
次要用于批示叶子函数不进行栈决裂。framesize
局部示意函数的局部变量须要多少栈空间,其中蕴含调用其它函数时筹备调用参数的隐式栈空间。最初是能够省略的参数大小,之所以能够省略是因为编译器能够从Go语言的函数申明中推导出函数参数的大小。TEXT
命令后续会在解说函数局部具体阐明。其余常用命令
SUBQ $0x18, SP // 调配函数栈,操作数 8 个字节ADDQ $0x18, SP // 革除函数栈,操作数据 8 个字节MOVB $1, DI // 拷贝 1个字节MOVW $0x10, BX // 拷贝 2 个字节MOVD $1, DX // 拷贝 4 个字节MOVQ $-10, AX // 拷贝 8 个字节ADDQ AX, BX // BX = BX + AX 存 BXSUBQ AX, BX // BX = BX - AX 存 BXIMULQ AX, BX // BX = BX * AX 存 BXMOVQ AX, BX // BX = AX 将 AX 中的值赋给 BXMOVQ (AX), BX // BX = *AX 加载 AX 中指向内存地址的值给 BXMOVQ 16(AX), BX // BX = *(AX + 16) 偏移 16 个字节后地址中的值
留神在X86-64指令集中,指令决定操作数尺寸,例如
XXXB = 1 XXXW = 2 XXXD = 4 XXXQ = 8
;
Go汇编编程
本节开始,咱们将利用Go汇编
编写一些简略的代码(终于开始撸代码了orz),包含:定义变量、定义函数、控制流代码、零碎调用。笔者演示的go版本是go1.15.4 darwin/amd64
。
首先须要阐明Go汇编语言并不是一个独立的语言,因为Go汇编程序无奈独立应用。Go汇编代码必须以Go包的形式组织,同时包中至多要有一个Go语言文件用于指明以后包名等根本包信息。如果Go汇编代码中定义的变量和函数要被其它Go语言代码援用,还须要通过Go语言代码将汇编中定义的符号申明进去。用于变量的定义和函数的定义Go汇编文件相似于C语言中的.c文件,而用于导出汇编中定义符号的Go源文件相似于C语言的.h文件。
咱们在goland中创立一个工程,将go代码和汇编代码放到同一个pkg上面,咱们在go代码中申明了3个变量,同时在汇编代码中初始化这3个变量,咱们能够间接运行main函数查看变量打印后果。
定义变量
1.定义根本数据类型变量
#include "textflag.h"// var MyInt int = 1234GLOBL ·MyInt(SB),NOPTR,$8DATA ·MyInt+0(SB)/8,$1234// var MyFloat float64 = 56.78GLOBL ·MyFloat(SB),NOPTR,$8DATA ·MyFloat+0(SB)/8,$56.78// var MyBool bool = trueGLOBL ·MyBool(SB),NOPTR,$1DATA ·MyBool+0(SB)/1,$1
咱们略微解释一下下面的Go汇编
代码:
GLOBL
和DATA
命令联合起来,能够初始化一个全局变量并导出;- 全局变量都是通过伪SB寄存器寻址的;
GLOBL
命令中须要加上NOPTR
flag,示意这个变量不是一个指针,因为golang自带垃圾回收,当gc扫描到该变量时候,须要明确晓得该变量是否蕴含指针;Go汇编语言中示例代码是没有加上NOPTR
flag,理由是go代码中曾经申明变量类型,然而咱们理论运行中发现不加NOPTR
flag会报错:runtime.gcdata: missing Go type information for global symbol main.MyInt: size 8
,具体起因笔者也没搞清楚;- 汇编文件头须要加上
#include "textflag.h"
,示意引入了runtime/textflag.h
文件,该文件预约义了一些flag,其中包含NOPTR
flag,具体含意咱们前面会阐明;
2.定义string
// var MyStr0 stringGLOBL NameData<>(SB),NOPTR,$8DATA NameData<>(SB)/8,$"abc"GLOBL ·MyStr0(SB),NOPTR,$16DATA ·MyStr0+0(SB)/8,$NameData<>(SB)DATA ·MyStr0+8(SB)/8,$3
咱们晓得golang中
string
类型能够通过stringHeader
来示意,所以MyStr0
对应一个16字节的内存块,留神就像咱们之前说的,汇编中是没有类型概念的,咱们操作的都是内存和内存地址;type stringHeader struct { Data unsafe.Pointer Len int}
·NameData<>(SB)
是长期变量,是用来辅助示意string
中字符串内容的,其中<>
示意是公有变量;DATA ·MyStr0+0(SB)/8,$·NameData<>(SB)
示意把·NameData<>(SB)
对应的地址赋值给·MyStr0+0(SB)
,宽度是8;
3.定义数组// var MyArray [2]int = {12, 34}GLOBL ·MyArray(SB),NOPTR,$16DATA ·MyArray+0(SB)/8,$12DATA ·MyArray+8(SB)/8,$34
4.定义切片
// 定义三个string长期变量,作为切片元素GLOBL str0<>(SB),NOPTR,$48DATA str0<>(SB)/48,$"Thoughts in the Still of the Night"GLOBL str1<>(SB),NOPTR,$48DATA str1<>(SB)/48,$"A pool of moonlight before the bed"GLOBL str2<>(SB),NOPTR,$8DATA str2<>(SB)/8,$"libai"// 定义一个[3]string的数组,元素就是下面的三个string变量GLOBL strarray<>(SB),NOPTR,$48DATA strarray<>+0(SB)/8,$str0<>(SB)DATA strarray<>+8(SB)/8,$34DATA strarray<>+16(SB)/8,$str1<>(SB)DATA strarray<>+24(SB)/8,$34DATA strarray<>+32(SB)/8,$str2<>(SB)DATA strarray<>+40(SB)/8,$5// var MySlice []stringGLOBL ·MySlice(SB),NOPTR,$24// 下面[3]string数组的首地址用来初始化切片的Data字段DATA ·MySlice+0(SB)/8,$strarray<>(SB)DATA ·MySlice+8(SB)/8,$3DATA ·MySlice+16(SB)/8,$4// 定义一个[3]*string的数组,元素就是下面三个string变量的地址GLOBL strptrarray<>(SB),NOPTR,$24DATA strptrarray<>+0(SB)/8,$strarray<>+0(SB)DATA strptrarray<>+8(SB)/8,$strarray<>+16(SB)DATA strptrarray<>+16(SB)/8,$strarray<>+32(SB)// var MyPtrSlice []*stringGLOBL ·MyPtrSlice(SB),NOPTR,$24// 下面[3]*string数组的首地址用来初始化切片的Data字段DATA ·MyPtrSlice+0(SB)/8,$strptrarray<>(SB)DATA ·MyPtrSlice+8(SB)/8,$3DATA ·MyPtrSlice+16(SB)/8,$4
- golang中切片是用上面的数据结构来示意的
// sliceHeader is a safe version of SliceHeader used within this package.type sliceHeader struct { Data unsafe.Pointer Len int Cap int}
- 咱们结构
[]string
和[]*string
切片的思路是同样的,先结构出数组[3]string
和[3]*string
,再把数组的地址赋值给切片的Data
字段;
定义函数
函数是golang中的一等公民(first-class),函数在go汇编
中比拟重要也比较复杂,只有把握了汇编函数的根本用法,能力真正算是Go汇编语言入门。
接下来咱们将会介绍汇编函数的定义、参数和返回值、局部变量和调用子函数。
函数定义
回顾之前的TEXT
语法
TEXT symbol(SB), [flags,] $framesize[-argsize]
咱们示范一个简略的Swap
函数
// func Swap(a, b int) (ret0 int, ret1 int)TEXT ·Swap(SB), NOSPLIT, $0-32 MOVQ a+0(FP), AX // AX = a MOVQ b+8(FP), BX // BX = b MOVQ BX, ret0+16(FP) // ret0 = BX MOVQ AX, ret1+24(FP) // ret1 = AX RET
咱们先疏忽函数的具体实现,只专一函数的定义局部:TEXT ·Swap(SB), NOSPLIT, $0-32
。
1.因为Swap
函数没有局部变量,有2个int
入参,2个int
返回值,所以$framesize[-argsize]
须要写成$0-32
或者$0
,其实能够省略argsize
局部,go编译器能依据go文件中的函数定义推断出argsize
大小,所以Swap
函数能够写成上面两种模式:
// 残缺定义,蕴含argsizeTEXT ·Swap(SB), NOSPLIT, $0-32// 简略定义,不蕴含argsizeTEXT ·Swap(SB), NOSPLIT, $0
上面是2种函数定义的比照图
2.咱们留神到函数定义中有NOSPLIT
flag,目前可能遇到的函数标记有NOSPLIT
、WRAPPER
和NEEDCTXT
几个。其中NOSPLIT
不会生成或蕴含栈决裂代码,这个别用于没有任何其它函数调用的叶子函数,这样能够适当进步性能。WRAPPER
标记则示意这个是一个包装函数,在panic
或runtime.caller
等某些处理函数帧的中央不会减少函数帧计数。最初的NEEDCTXT
示意须要一个上下文参数,个别用于闭包函数。这些falg都定义在文件textflag.h
中。
3.函数定义中也是没有类型的,下面的Swap
函数签名能够改写成上面任意一种,只有满足入加入上返回值,占用内存是32字节就能够了
func Swap(a, b, c int) intfunc Swap(a, b, c, d int)func Swap() (a, b, c, d int)func Swap() (a []int, d int)// ...
对于汇编函数来说,只有是函数的名字和参数大小统一就能够是雷同的函数了。而且在Go汇编
语言中,输出参数和返回值参数是没有任何的区别的。
函数参数和返回值
本节咱们将讨论一下函数参数和返回值如何在Go汇编
中寻址和函数参数和返回值的内存布局。
1.函数参数和返回值,在Go汇编
中能够通过伪寄存器FP
寻址找到,例如下面Swap
函数中,咱们通过伪寄存器FP
偏移相应的数量,就能找到对应的入参和返回值,留神在栈中,咱们是依照ret1,ret0,b,a
的程序入栈,伪寄存器FP
指向的是第一参数的地址。
a+0(FP) // 入参a b+8(FP) // 入参b ret0+16(FP) // 返回值ret0 ret1+24(FP) // 返回值ret1
2.Swap
函数中的入参和返回值在内存中布局如下:
3.如何计算函数的入参和返回值打下
尽管golang能够通过函数定义推断出函数的入参和返回值大小,不过咱们本人也应该要晓得如何计算。笔者举荐利用golang自带的unsafe
包来计算。咱们假如计算上面函数的入参和返回值大小func Foo(a bool, b int16) (c []byte)
咱们大体思路如下:
- 每个参数占用内存大小,除了受参数自身类型影响,还受内存对齐个性影响;
- 咱们结构构造体,通过
unsafe
包来计算每个参数对齐后占用内存大小(构造体中的字段也会内存对齐);
具体代码如下:
// func Foo(a bool, b int16) (c []byte)type FooArgs struct { a bool b int16 c []byte}func computeFooArgsSize() { args := FooArgs{ a: false, b: 0, c: nil, } // 参数a的偏移量 aOffset := 0 // 参数b的偏移量 bOffset := unsafe.Offsetof(args.b) // 参数c的偏移量 cOffset := unsafe.Offsetof(args.c) // 参数总大小 size := unsafe.Sizeof(args) fmt.Printf("参数a偏移量:%v byte, 定位:a+%v(FP)\n参数b偏移量:%v byte, 定位:b+%v(FP)\n返回值c偏移量:%v byte, 定位:c+%v(FP)\n参数总大小:%v\n", aOffset, aOffset, bOffset, bOffset, cOffset, cOffset, size)}
运行后果如下:
参数a偏移量:0 byte, 定位:a+0(FP)参数b偏移量:2 byte, 定位:b+2(FP)返回值c偏移量:8 byte, 定位:c+8(FP)参数总大小:32
Foo
函数的入参和返回值布局如下:
函数局部变量
Go汇编
中局部变量是指以后函数栈帧内对应的内存内的变量,不蕴含函数的入参和返回值(拜访形式不一样),留神Go汇编
中局部变量不能狭窄的了解成go语法中的局部变量,汇编中局部变量蕴含更多含意,除了蕴含go语法中的局部变量,Go汇编
中函数调用子函数的入参和返回值也是存在调用者(caller)的栈帧中的。咱们首先在看下不调用子函数状况下的局部变量。
咱们利用汇编实现一个简略函数
func Foo() (ret0 bool, ret1 int16, ret2 []byte) { var c []byte = []byte("abc") var b int16 = 234 var a bool = true return a, b, c}
函数Foo
中有三个局部变量,这三个局部变量也同时作为返回值返回,咱们能够利用后面计算Swap
函数中的入参和返回值占用内存大小的办法,计算函数Foo
局部变量和返回值占用内存的大小,同样都是32字节。咱们能够结构Go汇编
代码如下
// 结构长期变量{'a','b','c'}GLOBL ·tmp_bytes(SB),NOPTR,$8DATA ·tmp_bytes+0(SB)/8,$"abc"// func Foo() (bool, int16, []byte)TEXT ·Foo(SB), NOSPLIT, $32-32 MOVQ $1, a-32(SP) // var a bool = true MOVQ $234, b-30(SP) // var b int16 = 234 // var c = []byte("abc") LEAQ ·tmp_bytes(SB),AX MOVQ AX, c_data-8(SP) MOVQ $3, c_len-16(SP) MOVQ $4, c_cap-24(SP) // ret0 = a MOVQ a-32(SP), AX MOVQ AX, ret0+0(FP) // ret1 = b MOVQ b-30(SP), AX MOVQ AX, ret1+2(FP) // ret2 = c MOVQ c_data-24(SP), AX MOVQ AX, ret2_data+24(FP) MOVQ c_len-16(SP), AX MOVQ AX, ret2_len+16(FP) MOVQ c_cap-8(SP), AX MOVQ AX, ret2_cap+8(FP) RET
函数Foo
局部变量的内存布局如下图:
函数中调用子函数
实际中,通过Go汇编
实现的函数个别都是叶子函数,也就是被其余函数调用的函数,不会调用其余子函数,起因一是叶子函数逻辑比较简单,便于用汇编编写,二是个别的性能瓶颈都在叶子函数上,用Go汇编
来编写正是为了优化性能;然而Go汇编
也是能够在函数中调用子函数的,否则Go汇编
就不是一个残缺的汇编语言。
咱们总结了几条调用子函数的规定:
- 调用函数(caller)负责提供内存空间来存储被调用函数(callee)须要的入参和返回值;
- 调用函数(caller)将被调用函数(callee)须要的入参和返回值存储在本人的栈帧中;入栈程序最初的返回值先入栈,第一个入参最初入栈,和被调用函数(callee)用伪寄存器FP寻址程序统一;调用函数(caller)用伪寄存器SP寻址存储被调用函数(callee)的入参和返回值;
- 被调用函数(callee)返回后,返回值会存储到对应的调用函数(caller)的栈帧中,调用函数(caller)用伪寄存器SP寻址读取返回值;
留神Go语言函数的调用参数和返回值均是通过栈传输的,这样做的长处是函数调用栈比拟清晰,毛病是函数调用有肯定的性能损耗(Go编译器是通过函数内联来缓解这个问题的影响);
咱们结构一个多级调用的函数来展现函数调用状况:
func main() { printsum(1, 2)}func myprint(v int) { println(v)}func printsum(a, b int) { var ret = sum(a, b) myprint(ret)}func sum(a, b int) int { return a+b}
咱们用Go
汇编来实现函数printsum
和sum
// func printsum(a, b int)TEXT ·printsum(SB), $24 MOVQ a+0(FP), AX // AX = a MOVQ b+8(FP), BX // BX = b MOVQ AX, ia-24(SP) // sum函数的入参和返回值布局:ret -> b -> a MOVQ BX, ia-16(SP) CALL ·sum(SB) // 调用sum函数 MOVQ ret-8(SP), AX // sum函数返回值在伪寄存器SP的栈底,读取返回值放入栈顶,作为myprint函数的入参 MOVQ AX, ia-24(SP) // 伪寄存器SP的栈顶 CALL ·myprint(SB) // 调用myprint函数 RET// func sum(a, b int) intTEXT ·sum(SB), $0 MOVQ a+0(FP), AX // AX = a MOVQ b+8(FP), BX // BX = b ADDQ AX, BX MOVQ BX, ret0+16(FP) RET
同时,咱们看下内存布局:
宏函数
宏函数并不是Go汇编语言所定义,而是Go汇编引入的预处理个性自带的个性。
笔者本人对宏函数不是很理解,这里只放一个示例:用宏函数来实现Swap
函数。
// 定义宏函数#define SWAP(x, y, t) MOVQ x, t; MOVQ y, x; MOVQ t, y// func Swap(a, b int) (int, int)TEXT ·Swap(SB), $0-32 MOVQ a+0(FP), AX // AX = a MOVQ b+8(FP), BX // BX = b SWAP(AX, BX, CX) // AX, BX = b, a MOVQ AX, ret0+16(FP) // return MOVQ BX, ret1+24(FP) // RET
函数中的控制流
控制流对于一门编程语言是十分重要和十分根底的,个别程序次要有程序、分支和循环几种执行流程。本节咱们从Go汇编
的角度来探讨下程序中控制流的实现。
- 程序执行
首先,后面的很多例子都是程序执行流程,咱们就不额定举例子了。 - 条件跳转
go语言中通过if/else
语句来实现条件跳转,其实go语言也反对goto
语句,咱们能够通过if/goto
来实现条件跳转,然而不举荐大家应用;咱们通过汇编来实现条件跳转,思路却是近似if/goto
。
咱们尝试实现一个If
函数:
func If(ok bool, a, b int) int { if ok { return a } else { return b }}
咱们先用if/goto
的形式来重写一遍:
func If(ok int, a, b int) int { if ok == 0 { goto L } return a L: return b}
这种形式其实也很靠近汇编的思路了,咱们用Go汇编
来实现下
// func If(ok int, a, b int) intTEXT ·If(SB), $0-32 MOVQ ok+0(FP), CX // CX = ok MOVQ a+8(FP), AX // AX = a MOVQ b+16(FP), BX // BX = b CMPQ CX, $0 // 比拟CX是否等于0 JZ L // 如果CX等于0,则跳转到L MOVQ AX, ret+24(FP) // return a RETL: MOVQ BX, ret+24(FP) // retrun b RET
for
循环for
循环有多种形式,最经典的for
循环由初始化,完结条件和步长迭代三局部组成,再配合循环体内的条件跳转,这种经典for
循环模式能够模仿其余各种循环模式。Go汇编
中实现这种经典的for
循环构造,应用的也是相似if/goto
的形式。
咱们基于经典的for
循环模式,结构一个LoopAdd
函数来计算等差数列的和:
func LoopAdd(cnt, v0, step int) int { result := 0 for i := 0; i < cnt; i++ { result += v0 v0 += step } return result}
咱们先用if/goto
的形式来改下下面的函数:
func LoopAdd(cnt, v0, step int) int { result := 0 i := 0 LoopIf: if i < cnt { goto LoopBody } goto LoopEnd LoopBody: result += v0 v0 += step i++ goto LoopIf LoopEnd: return result}
最初咱们用汇编来实现:
// func LoopAdd(cnt, v0, step int) intTEXT ·LoopAdd(SB), $0-32 MOVQ cnt+0(FP), AX // AX = cnt MOVQ v0+8(FP), BX // BX = v0 MOVQ step+16(FP), CX // CX = step MOVQ $0, DX // i = 0 MOVQ $0, ret+24(FP) // result = 0LOOPIF: CMPQ DX, AX JL LOOPBODY JMP LOOPENDLOOPBODY: ADDQ BX, ret+24(FP) // result += v0 ADDQ CX, BX // v0 += step ADDQ $1, DX // i++ JMP LOOPIFLOOPEND: RET
几种非凡的函数
咱们之前介绍的函数都是比较简单的函数,本节咱们将介绍一些简单的函数实现;首先咱们介绍一下函数的调用标准,更加深刻的理解一下函数的调用细节;而后咱们别离介绍一下:办法函数、递归函数和闭包。
- Go函数调用标准
咱们之前讲过Go汇编
中函数的调用,只是简略说了下被调函数的入参和返回值如何传递的问题,本节咱们更加深刻的解说函数调用的细节。感兴趣的小伙伴能够参考这篇文章:图解函数调用过程,文章外面说的很具体,咱们这里就不赘述了。
咱们这里给出一张Go汇编
函数调用标准图解阐明:
- 从图中咱们能够看出函数调用过程中,栈都是间断的,例如函数A中调用函数B,那么这两个函数所占用的栈空间是连贯在一起的一块间断空间;
- 图示中开展了
CALL
和RET
两个命令,简略来说就是CALL
命令约等于PUSH IP
和JMP callee_func
两个命令的组合,其中PUSH IP
命令相当于SUBQ $-1, SP
和MOVQ IP, (SP)
,即栈帧扩大1个字节,而后把IP
寄存器的存储值写入SP
寄存器对应的内存地址中,因为IP
寄存器中存储的值能够简略了解为CALL
命令的下一条指令的地址,这条指令就是CALL
命令调用子函数返回后,咱们须要执行的命令地址,所以咱们也称之为return address
,JMP callee_func
命令意思是将子函数的地址退出IP
寄存器,这样就实现了函数跳转;RET
命令刚好想反,把之前栈中存储的return address
地址写入IP
寄存器并跳转回去继续执行上面的命令; - 个别一个函数的栈帧包含局部变量和调用子函数所需的入参和返回值,当执行到一个函数的时候,函数的栈帧对应的栈内存的栈底地址是存储在
BP
寄存器中的,这个值个别是不变的,所以BP
叫做基址指针寄存器,栈帧的栈顶地址是存储在SP
寄存器中的,这个值是在一直变动的,因为函数执行过程中常常须要栈扩大来存储局部变量等,所以SP
寄存器也叫栈指针寄存器;图中caller's BP
保留的就是调用函数过后BP
寄存器中的值,也就是调用函数栈帧的栈底地址,咱们子函数返回后,须要通过这个值来回复调用函数的栈帧的栈底地址; - 留神图中的
argsize
和framesize
,他们都是栈中一部分,不是全副,Go汇编
暗藏了一部分调用细节; - 下面提到的
CALL
和RET
两个命令是不解决被调用函数的入参和返回值的,因为它们是由调用函数来筹备的,切记这一点;
上面通过一个示例来验证一下函数调用标准,咱们先构建一个多级函数调用的示例,文件命名为asm_call.go
package mainimport "math/rand"func Fun01(a, b int) int { c := Fun02(a, b) return c}func Fun02(a, b int) int { c := Fun03(a,b) d := Fun04(c, b, a) return d}func Fun03(a, b int) int { r := rand.Intn(1000) if r > 500 { return a + b + r } return a + b}func Fun04(c, b, a int) int { r := rand.Intn(1000) if r > 500 { return a + b + c } return a + b + r}
咱们先首先尝试用Go汇编
来重写Fun01
和Fun02
两个函数
// func Fun01(a, b int) intTEXT ·Fun01(SB), $24-24 MOVQ a+0(FP), AX // AX = a MOVQ b+8(FP), BX // BX = b MOVQ AX, f2_a-24(SP) // Fun02入参a MOVQ BX, f2_b-16(SP) // Fun02入参b CALL ·Fun02(SB) // 调用Fun02函数 MOVQ f2_ret-8(SP), CX // Fun02函数的返回值赋值给CX MOVQ CX, ret+16(FP) // 返回值 RET// func Fun02(a, b int) intTEXT ·Fun02(SB), $32-24 MOVQ a+0(FP), AX // AX = a MOVQ b+8(FP), BX // BX = b MOVQ AX, f3_a-32(SP) // Fun03入参a MOVQ BX, f3_b-24(SP) // Fun03入参b CALL ·Fun03(SB) // 调用Fun03函数 MOVQ f3_ret-16(SP), CX // Fun03函数的返回值赋值给CX MOVQ f3_b-24(SP), BX MOVQ f3_a-32(SP), AX MOVQ CX, f4_c-32(SP) // Fun04入参c MOVQ BX, f4_b-24(SP) // Fun04入参b,这里能够省略 MOVQ AX, f4_a-16(SP) // Fun04入参a CALL ·Fun04(SB) // 调用Fun04函数 MOVQ f4_ret-8(SP), AX // 返回值 MOVQ AX, ret+16(FP) // 返回值 RET
咱们再通过go tool compile -S ./asm_call.go >./asm_call.txt
命令输入最终的指标代码,比照两者的不同
"".Fun01 STEXT size=80 args=0x18 locals=0x20 0x0000 00000 (asm_caller/asm_call.go:15) TEXT "".Fun01(SB), ABIInternal, $32-24 0x0000 00000 (asm_caller/asm_call.go:15) MOVQ (TLS), CX 0x0009 00009 (asm_caller/asm_call.go:15) CMPQ SP, 16(CX) 0x000d 00013 (asm_caller/asm_call.go:15) PCDATA $0, $-2 0x000d 00013 (asm_caller/asm_call.go:15) JLS 73 0x000f 00015 (asm_caller/asm_call.go:15) PCDATA $0, $-1 0x000f 00015 (asm_caller/asm_call.go:15) SUBQ $32, SP 0x0013 00019 (asm_caller/asm_call.go:15) MOVQ BP, 24(SP) 0x0018 00024 (asm_caller/asm_call.go:15) LEAQ 24(SP), BP 0x001d 00029 (asm_caller/asm_call.go:15) FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB) 0x001d 00029 (asm_caller/asm_call.go:15) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB) 0x001d 00029 (asm_caller/asm_call.go:16) MOVQ "".a+40(SP), AX 0x0022 00034 (asm_caller/asm_call.go:16) MOVQ AX, (SP) 0x0026 00038 (asm_caller/asm_call.go:16) MOVQ "".b+48(SP), AX 0x002b 00043 (asm_caller/asm_call.go:16) MOVQ AX, 8(SP) 0x0030 00048 (asm_caller/asm_call.go:16) PCDATA $1, $0 0x0030 00048 (asm_caller/asm_call.go:16) CALL "".Fun02(SB) 0x0035 00053 (asm_caller/asm_call.go:16) MOVQ 16(SP), AX 0x003a 00058 (asm_caller/asm_call.go:17) MOVQ AX, "".~r2+56(SP) 0x003f 00063 (asm_caller/asm_call.go:17) MOVQ 24(SP), BP 0x0044 00068 (asm_caller/asm_call.go:17) ADDQ $32, SP 0x0048 00072 (asm_caller/asm_call.go:17) RET 0x0049 00073 (asm_caller/asm_call.go:17) NOP 0x0049 00073 (asm_caller/asm_call.go:15) PCDATA $1, $-1 0x0049 00073 (asm_caller/asm_call.go:15) PCDATA $0, $-2 0x0049 00073 (asm_caller/asm_call.go:15) CALL runtime.morestack_noctxt(SB) 0x004e 00078 (asm_caller/asm_call.go:15) PCDATA $0, $-1 0x004e 00078 (asm_caller/asm_call.go:15) JMP 0"".Fun02 STEXT size=114 args=0x18 locals=0x28 0x0000 00000 (asm_caller/asm_call.go:20) TEXT "".Fun02(SB), ABIInternal, $40-24 0x0000 00000 (asm_caller/asm_call.go:20) MOVQ (TLS), CX 0x0009 00009 (asm_caller/asm_call.go:20) CMPQ SP, 16(CX) 0x000d 00013 (asm_caller/asm_call.go:20) PCDATA $0, $-2 0x000d 00013 (asm_caller/asm_call.go:20) JLS 107 0x000f 00015 (asm_caller/asm_call.go:20) PCDATA $0, $-1 0x000f 00015 (asm_caller/asm_call.go:20) SUBQ $40, SP 0x0013 00019 (asm_caller/asm_call.go:20) MOVQ BP, 32(SP) 0x0018 00024 (asm_caller/asm_call.go:20) LEAQ 32(SP), BP 0x001d 00029 (asm_caller/asm_call.go:20) FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB) 0x001d 00029 (asm_caller/asm_call.go:20) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB) 0x001d 00029 (asm_caller/asm_call.go:21) MOVQ "".a+48(SP), AX 0x0022 00034 (asm_caller/asm_call.go:21) MOVQ AX, (SP) 0x0026 00038 (asm_caller/asm_call.go:21) MOVQ "".b+56(SP), CX 0x002b 00043 (asm_caller/asm_call.go:21) MOVQ CX, 8(SP) 0x0030 00048 (asm_caller/asm_call.go:21) PCDATA $1, $0 0x0030 00048 (asm_caller/asm_call.go:21) CALL "".Fun03(SB) 0x0035 00053 (asm_caller/asm_call.go:21) MOVQ 16(SP), AX 0x003a 00058 (asm_caller/asm_call.go:22) MOVQ AX, (SP) 0x003e 00062 (asm_caller/asm_call.go:22) MOVQ "".b+56(SP), AX 0x0043 00067 (asm_caller/asm_call.go:22) MOVQ AX, 8(SP) 0x0048 00072 (asm_caller/asm_call.go:22) MOVQ "".a+48(SP), AX 0x004d 00077 (asm_caller/asm_call.go:22) MOVQ AX, 16(SP) 0x0052 00082 (asm_caller/asm_call.go:22) CALL "".Fun04(SB) 0x0057 00087 (asm_caller/asm_call.go:22) MOVQ 24(SP), AX 0x005c 00092 (asm_caller/asm_call.go:23) MOVQ AX, "".~r2+64(SP) 0x0061 00097 (asm_caller/asm_call.go:23) MOVQ 32(SP), BP 0x0066 00102 (asm_caller/asm_call.go:23) ADDQ $40, SP 0x006a 00106 (asm_caller/asm_call.go:23) RET 0x006b 00107 (asm_caller/asm_call.go:23) NOP 0x006b 00107 (asm_caller/asm_call.go:20) PCDATA $1, $-1 0x006b 00107 (asm_caller/asm_call.go:20) PCDATA $0, $-2 0x006b 00107 (asm_caller/asm_call.go:20) CALL runtime.morestack_noctxt(SB) 0x0070 00112 (asm_caller/asm_call.go:20) PCDATA $0, $-1 0x0070 00112 (asm_caller/asm_call.go:20) JMP 0
这里咱们一起梳理下Fun02
函数的指标代码
- 函数申明中,
TEXT "".Fun02(SB), ABIInternal, $40-24
,frame-size参数指标代码是40,比Go汇编中的32要大8字节,起因是要多一个caller's BP
的值要存储; - Go编译器会主动退出栈扩容的代码,粗心是判断当初的栈大小是否达到了扩容阈值,如果是的话,就调用扩容函数,扩容完结后会持续跳转到函数结尾从新执行,这个也是Go语言不会产生爆栈的重要起因,对应的指标代码如下
0x0000 00000 (asm_caller/asm_call.go:20) MOVQ (TLS), CX // 加载Goroutine构造g到CX0x0009 00009 (asm_caller/asm_call.go:20) CMPQ SP, 16(CX) // 比拟以后SP中的值和g中的stackguard00x000d 00013 (asm_caller/asm_call.go:20) JLS 107 // 如果SP中的值超出阈值,跳转到107,执行runtime.morestack_noctxt...0x006b 00107 (asm_caller/asm_call.go:20) CALL runtime.morestack_noctxt(SB) // 栈扩容0x0070 00112 (asm_caller/asm_call.go:20) JMP 0 // 跳转回函数结尾从新执行
- 函数调用过程中都会有栈扩大、
caller's BP
入栈和callee BP
初始化过程,对应指标代码如下
0x000f 00015 (asm_caller/asm_call.go:20) SUBQ $40, SP // SP中的栈帧栈顶地址减小40,即栈扩大40字节,就是函数定义的frame-size的大小0x0013 00019 (asm_caller/asm_call.go:20) MOVQ BP, 32(SP) // 将BP的值存储到(SP)+32地址对应的内存,行将caller的栈帧栈底地址存储到callee函数的栈上0x0018 00024 (asm_caller/asm_call.go:20) LEAQ 32(SP), BP // 将此时栈上栈帧栈底地址写入BP寄存器,即callee BP初始化
PCDATA
和FUNCDATA
都是Go编译器主动生成的指令,它们都是和函数表格相干,具体能够参考文章:PCDATA和FUNCDATA- 指标代码的其余局部和咱们
Go汇编
根本一样,只有留神一下真伪SP寄存器的不同;
本节波及的知识点其实很多,想要长篇累牍的说分明其实不容易,笔者倡议大家看看文章:函数调用标准。
有了下面的根底,上面咱们具体来剖析几种非凡的函数,咱们采纳的分析方法是:先构建Go代码示例,而后输入汇编指标代码,查看汇编实现,剖析实现逻辑,最初咱们采纳Go汇编的形式实现一遍。
- 办法函数
咱们先来构建一个简略的办法函数
package maintype MyInt intfunc (i MyInt) Add1() int { return int(i) + 1}func (i *MyInt) Add2() int { return int(*i) + 1}
咱们应用命令go tool compile -S
来输入指标代码(只截取了重要局部):
"".MyInt.Add1 STEXT nosplit size=14 args=0x10 locals=0x0 0x0000 00000 (asm_method/asm_method.go:5) TEXT "".MyInt.Add1(SB), NOSPLIT|ABIInternal, $0-16 0x0000 00000 (asm_method/asm_method.go:5) FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB) 0x0000 00000 (asm_method/asm_method.go:5) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB) 0x0000 00000 (asm_method/asm_method.go:6) MOVQ "".i+8(SP), AX 0x0005 00005 (asm_method/asm_method.go:6) INCQ AX 0x0008 00008 (asm_method/asm_method.go:6) MOVQ AX, "".~r0+16(SP) 0x000d 00013 (asm_method/asm_method.go:6) RET 0x0000 48 8b 44 24 08 48 ff c0 48 89 44 24 10 c3 H.D$.H..H.D$.."".(*MyInt).Add2 STEXT nosplit size=17 args=0x10 locals=0x0 0x0000 00000 (asm_method/asm_method.go:9) TEXT "".(*MyInt).Add2(SB), NOSPLIT|ABIInternal, $0-16 0x0000 00000 (asm_method/asm_method.go:9) FUNCDATA $0, gclocals·1a65e721a2ccc325b382662e7ffee780(SB) 0x0000 00000 (asm_method/asm_method.go:9) FUNCDATA $1, gclocals·69c1753bd5f81501d95132d08af04464(SB) 0x0000 00000 (asm_method/asm_method.go:10) MOVQ "".i+8(SP), AX 0x0005 00005 (asm_method/asm_method.go:10) MOVQ (AX), AX 0x0008 00008 (asm_method/asm_method.go:10) INCQ AX 0x000b 00011 (asm_method/asm_method.go:10) MOVQ AX, "".~r0+16(SP) 0x0010 00016 (asm_method/asm_method.go:10) RET 0x0000 48 8b 44 24 08 48 8b 00 48 ff c0 48 89 44 24 10 H.D$.H..H..H.D$. 0x0010 c3 ."".(*MyInt).Add1 STEXT dupok nosplit size=85 args=0x10 locals=0x8 0x0000 00000 (<autogenerated>:1) TEXT "".(*MyInt).Add1(SB), DUPOK|NOSPLIT|WRAPPER|ABIInternal, $8-16 0x0000 00000 (<autogenerated>:1) MOVQ (TLS), CX 0x0009 00009 (<autogenerated>:1) SUBQ $8, SP 0x000d 00013 (<autogenerated>:1) MOVQ BP, (SP) 0x0011 00017 (<autogenerated>:1) LEAQ (SP), BP 0x0015 00021 (<autogenerated>:1) MOVQ 32(CX), BX 0x0019 00025 (<autogenerated>:1) TESTQ BX, BX 0x001c 00028 (<autogenerated>:1) JNE 70 0x001e 00030 (<autogenerated>:1) NOP 0x001e 00030 (<autogenerated>:1) FUNCDATA $0, gclocals·1a65e721a2ccc325b382662e7ffee780(SB) 0x001e 00030 (<autogenerated>:1) FUNCDATA $1, gclocals·69c1753bd5f81501d95132d08af04464(SB) 0x001e 00030 (<autogenerated>:1) MOVQ ""..this+16(SP), AX 0x0023 00035 (<autogenerated>:1) TESTQ AX, AX 0x0026 00038 (<autogenerated>:1) JEQ 60 0x0028 00040 (<autogenerated>:1) MOVQ (AX), AX 0x002b 00043 (<unknown line number>) NOP 0x002b 00043 (asm_method/asm_method.go:6) INCQ AX 0x002e 00046 (<autogenerated>:1) MOVQ AX, "".~r0+24(SP) 0x0033 00051 (<autogenerated>:1) MOVQ (SP), BP 0x0037 00055 (<autogenerated>:1) ADDQ $8, SP 0x003b 00059 (<autogenerated>:1) RET 0x003c 00060 (<autogenerated>:1) PCDATA $1, $1 0x003c 00060 (<autogenerated>:1) NOP 0x0040 00064 (<autogenerated>:1) CALL runtime.panicwrap(SB) 0x0045 00069 (<autogenerated>:1) XCHGL AX, AX 0x0046 00070 (<autogenerated>:1) LEAQ 16(SP), DI 0x004b 00075 (<autogenerated>:1) CMPQ (BX), DI 0x004e 00078 (<autogenerated>:1) JNE 30 0x0050 00080 (<autogenerated>:1) MOVQ SP, (BX) 0x0053 00083 (<autogenerated>:1) JMP 30
指标文件的汇编代码很好了解,办法函数的实现逻辑大略就是:
- 办法函数在汇编中命名时,须要把类型名作为前缀,例如
.MyInt.Add1(SB)
和.(*MyInt).Add2(SB)
,这也解释了为什么不同的类型能够有同名的办法函数,因为它们在汇编中名称是不一样的;可见办法函数在Go汇编
中和一般的全局函数没有区别,只不过命名加上前缀即可; - 咱们留神到示例中的办法函数的argsize都是16,但其实咱们函数定义中argsize应该是8,起因是办法函数默认第一个入参就是对应类型的变量,例如汇编语句
MOVQ "".i+8(SP), AX
; - 咱们定义了2个办法函数,但其实编译器一共实现了3个办法函数,多进去一个是
.(*MyInt).Add1(SB)
,这也就解释了为什么咱们定义的MyInt
变量无论是否是指针都能够调用两个办法函数,因为编译器帮咱们主动实现了; - 实践上应该有4个办法函数:
.MyInt.Add1(SB)
、.(*MyInt).Add1(SB)
、.MyInt.Add2(SB)
、.(*MyInt).Add2(SB)
,当初只有3个,如果咱们在Go代码中调用MyInt.Add2()
,编译器其实会主动改写成(*MyInt).Add2(SB)
;
上面咱们用Go汇编
示例如下:
type MyInt intfunc (v MyInt) Twice01() int { return int(v)*2}func (v *MyInt) Twice02() int { return int(*v)*2}
第一个办法函数的汇编实现如下:
// func (v MyInt) Twice() intTEXT ·MyInt·Twice01(SB), NOSPLIT, $0-16 // 函数名前缀为·MyInt MOVQ a+0(FP), AX // 第一个参数默认是v ADDQ AX, AX // AX *= 2 MOVQ AX, ret+8(FP) // return v RET
第二个办法函数蕴含指针,实践上命名应为·(*MyInt)·Twice02
,然而在Go汇编语言中,星号和小括弧都无奈用作函数名字,也就是无奈用汇编间接实现接管参数是指针类型的办法。这个可能是官网的成心限度。
- 递归函数
递归函数是比拟非凡的函数,递归函数通过调用本身并且在栈上保留状态,这能够简化很多问题的解决。Go汇编
中递归函数的弱小之处是不必放心爆栈问题,因为栈能够依据须要进行扩容和膨胀。
咱们通过一个递归求和的函数来示例:
func sum(n int) int { if n > 0 { return n + sum(n-1) } else { return 0 }}
咱们应用命令go tool compile -S
来输入指标代码(只截取了重要局部):
"".sum STEXT size=106 args=0x10 locals=0x18 0x0000 00000 (asm_method/asm_method.go:10) TEXT "".sum(SB), ABIInternal, $24-16 0x0000 00000 (asm_method/asm_method.go:10) MOVQ (TLS), CX 0x0009 00009 (asm_method/asm_method.go:10) CMPQ SP, 16(CX) 0x000d 00013 (asm_method/asm_method.go:10) PCDATA $0, $-2 0x000d 00013 (asm_method/asm_method.go:10) JLS 99 0x000f 00015 (asm_method/asm_method.go:10) PCDATA $0, $-1 0x000f 00015 (asm_method/asm_method.go:10) SUBQ $24, SP 0x0013 00019 (asm_method/asm_method.go:10) MOVQ BP, 16(SP) 0x0018 00024 (asm_method/asm_method.go:10) LEAQ 16(SP), BP 0x001d 00029 (asm_method/asm_method.go:10) FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB) 0x001d 00029 (asm_method/asm_method.go:10) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB) 0x001d 00029 (asm_method/asm_method.go:11) MOVQ "".n+32(SP), AX 0x0022 00034 (asm_method/asm_method.go:11) TESTQ AX, AX 0x0025 00037 (asm_method/asm_method.go:11) JLE 80 0x0027 00039 (asm_method/asm_method.go:12) LEAQ -1(AX), CX 0x002b 00043 (asm_method/asm_method.go:12) MOVQ CX, (SP) 0x002f 00047 (asm_method/asm_method.go:12) PCDATA $1, $0 0x002f 00047 (asm_method/asm_method.go:12) CALL "".sum(SB) 0x0034 00052 (asm_method/asm_method.go:12) MOVQ 8(SP), AX 0x0039 00057 (asm_method/asm_method.go:12) MOVQ "".n+32(SP), CX 0x003e 00062 (asm_method/asm_method.go:12) ADDQ CX, AX 0x0041 00065 (asm_method/asm_method.go:12) MOVQ AX, "".~r1+40(SP) 0x0046 00070 (asm_method/asm_method.go:12) MOVQ 16(SP), BP 0x004b 00075 (asm_method/asm_method.go:12) ADDQ $24, SP 0x004f 00079 (asm_method/asm_method.go:12) RET 0x0050 00080 (asm_method/asm_method.go:14) MOVQ $0, "".~r1+40(SP) 0x0059 00089 (asm_method/asm_method.go:14) MOVQ 16(SP), BP 0x005e 00094 (asm_method/asm_method.go:14) ADDQ $24, SP 0x0062 00098 (asm_method/asm_method.go:14) RET 0x0063 00099 (asm_method/asm_method.go:14) NOP 0x0063 00099 (asm_method/asm_method.go:10) PCDATA $1, $-1 0x0063 00099 (asm_method/asm_method.go:10) PCDATA $0, $-2 0x0063 00099 (asm_method/asm_method.go:10) CALL runtime.morestack_noctxt(SB) 0x0068 00104 (asm_method/asm_method.go:10) PCDATA $0, $-1 0x0068 00104 (asm_method/asm_method.go:10) JMP 0
通过指标代码,咱们发现因为Go汇编
会主动为栈扩容,所以递归函数与简略的函数调用并没有不同;
上面咱们用Go汇编
来实现一遍,咱们首先用if/goto
来改写代码,不便咱们后续改写成汇编代码
func sum(n int) (result int) { var AX = n var BX int if n > 0 { goto L_STEP_TO_END } goto L_ENDL_STEP_TO_END: AX -= 1 BX = sum(AX) AX = n // 调用函数后, AX从新复原为n BX += AX return BXL_END: return 0}
最初咱们实现汇编代码
// func sum(n int) (result int)TEXT ·sum(SB), $16-16 MOVQ n+0(FP), AX // n MOVQ result+8(FP), BX // result CMPQ AX, $0 // test n - 0 JG L_STEP_TO_END // if > 0: goto L_STEP_TO_END JMP L_END // goto L_STEP_TO_ENDL_STEP_TO_END: SUBQ $1, AX // AX -= 1 MOVQ AX, 0(SP) // arg: n-1 CALL ·sum(SB) // call sum(n-1) MOVQ 8(SP), BX // BX = sum(n-1) MOVQ n+0(FP), AX // AX = n ADDQ AX, BX // BX += AX MOVQ BX, result+8(FP) // return BX RETL_END: MOVQ $0, result+8(FP) // return 0 RET
留神这里不能加上NOSPLIT
标识,因为递归函数会因为调用层级过大而导致栈空间有余,NOSPLIT
标识限度了栈的扩容,显然是Go编译器不容许的。
- 闭包函数
闭包函数是最弱小的函数,因为闭包函数能够捕捉外层部分作用域的局部变量,因而闭包函数自身就具备了状态。从实践上来说,全局的函数也是闭包函数的子集,只不过全局函数并没有捕捉外层变量而已。
老规矩,咱们先来构建一个简略的闭包函数:
func GenAddFuncClosure01(x int) func() int { return func() int { x += 2 return x }}func main() { f1 := GenAddFuncClosure01(1) f1()}
这次咱们加上了闭包的调用代码,因为闭包调用也比拟非凡。
咱们看下指标文件的汇编代码:
"".GenAddFuncClosure01 STEXT size=157 args=0x10 locals=0x20 0x0000 00000 (asm_demo2/asm_demo2.go:8) TEXT "".GenAddFuncClosure01(SB), ABIInternal, $32-16 0x0000 00000 (asm_demo2/asm_demo2.go:8) MOVQ (TLS), CX // 获取TLS中的g构造体 0x0009 00009 (asm_demo2/asm_demo2.go:8) CMPQ SP, 16(CX) // 判断函数栈是否须要扩容 0x000d 00013 (asm_demo2/asm_demo2.go:8) PCDATA $0, $-2 0x000d 00013 (asm_demo2/asm_demo2.go:8) JLS 147 0x0013 00019 (asm_demo2/asm_demo2.go:8) PCDATA $0, $-1 0x0013 00019 (asm_demo2/asm_demo2.go:8) SUBQ $32, SP // 函数栈扩大32字节 0x0017 00023 (asm_demo2/asm_demo2.go:8) MOVQ BP, 24(SP) // 保留BP 0x001c 00028 (asm_demo2/asm_demo2.go:8) LEAQ 24(SP), BP 0x0021 00033 (asm_demo2/asm_demo2.go:8) FUNCDATA $0, gclocals·2589ca35330fc0fce83503f4569854a0(SB) 0x0021 00033 (asm_demo2/asm_demo2.go:8) FUNCDATA $1, gclocals·9fb7f0986f647f17cb53dda1484e0f7a(SB) 0x0021 00033 (asm_demo2/asm_demo2.go:8) LEAQ type.int(SB), AX // 将type.int(SB)对应地址写入AX 0x0028 00040 (asm_demo2/asm_demo2.go:8) MOVQ AX, (SP) // 将AX写入0+(SP),作为上面将要调用的函数runtime.newobject(SB)入参 0x002c 00044 (asm_demo2/asm_demo2.go:8) PCDATA $1, $0 0x002c 00044 (asm_demo2/asm_demo2.go:8) CALL runtime.newobject(SB) // 调用函数:func newobject(typ *byte) *any 0x0031 00049 (asm_demo2/asm_demo2.go:8) MOVQ 8(SP), AX // 函数newobject返回值写入AX,返回值是个int指针 0x0036 00054 (asm_demo2/asm_demo2.go:8) MOVQ AX, "".&x+16(SP) // AX值写入16(SP),这里都是真SP寄存器 0x003b 00059 (asm_demo2/asm_demo2.go:8) MOVQ "".x+40(SP), CX // GenAddFuncClosure01函数入参x写入CX 0x0040 00064 (asm_demo2/asm_demo2.go:8) MOVQ CX, (AX) // CX值写入AX中存储值所对应的地址中,即下面新创建的int指针赋值为x 0x0043 00067 (asm_demo2/asm_demo2.go:9) LEAQ type.noalg.struct { F uintptr; "".x *int }(SB), CX // 创立了一个struct { F uintptr; "".x *int }的类型变量,相似下面的type.int变量,并将其地址写入CX 0x004a 00074 (asm_demo2/asm_demo2.go:9) MOVQ CX, (SP) // 将CX值写入0+(SP),作为上面将要调用的函数runtime.newobject(SB)入参 0x004e 00078 (asm_demo2/asm_demo2.go:9) PCDATA $1, $1 0x004e 00078 (asm_demo2/asm_demo2.go:9) CALL runtime.newobject(SB) // 调用函数:func newobject(typ *byte) *any 0x0053 00083 (asm_demo2/asm_demo2.go:9) MOVQ 8(SP), AX // 函数newobject返回值写入AX,返回值是个struct { F uintptr; "".x *int }指针 0x0058 00088 (asm_demo2/asm_demo2.go:9) LEAQ "".GenAddFuncClosure01.func1(SB), CX // 编译器创立了一个.GenAddFuncClosure01.func1函数,将其地址写入CX 0x005f 00095 (asm_demo2/asm_demo2.go:9) MOVQ CX, (AX) // 将CX值写入AX存储值所对应的地址,也就是为struct { F uintptr; "".x *int }的F成员变量赋值为.GenAddFuncClosure01.func1函数地址 0x0062 00098 (asm_demo2/asm_demo2.go:9) PCDATA $0, $-2 0x0062 00098 (asm_demo2/asm_demo2.go:9) CMPL runtime.writeBarrier(SB), $0 // gc相干的,先不必管 0x0069 00105 (asm_demo2/asm_demo2.go:9) JNE 131 0x006b 00107 (asm_demo2/asm_demo2.go:9) MOVQ "".&x+16(SP), CX // 将之前存储的int指针写入CX 0x0070 00112 (asm_demo2/asm_demo2.go:9) MOVQ CX, 8(AX) // 将CX值写入AX存储值的偏移量8字节所对应的地址,为struct { F uintptr; "".x *int }的x成员变量赋值为创立的int指针 0x0074 00116 (asm_demo2/asm_demo2.go:9) PCDATA $0, $-1 0x0074 00116 (asm_demo2/asm_demo2.go:9) MOVQ AX, "".~r1+48(SP) // 返回struct { F uintptr; "".x *int }地址 0x0079 00121 (asm_demo2/asm_demo2.go:9) MOVQ 24(SP), BP // 复原BP 0x007e 00126 (asm_demo2/asm_demo2.go:9) ADDQ $32, SP 0x0082 00130 (asm_demo2/asm_demo2.go:9) RET 0x0083 00131 (asm_demo2/asm_demo2.go:9) PCDATA $0, $-2 0x0083 00131 (asm_demo2/asm_demo2.go:9) LEAQ 8(AX), DI 0x0087 00135 (asm_demo2/asm_demo2.go:9) MOVQ "".&x+16(SP), CX 0x008c 00140 (asm_demo2/asm_demo2.go:9) CALL runtime.gcWriteBarrierCX(SB) 0x0091 00145 (asm_demo2/asm_demo2.go:9) JMP 116 0x0093 00147 (asm_demo2/asm_demo2.go:9) NOP 0x0093 00147 (asm_demo2/asm_demo2.go:8) PCDATA $1, $-1 0x0093 00147 (asm_demo2/asm_demo2.go:8) PCDATA $0, $-2 0x0093 00147 (asm_demo2/asm_demo2.go:8) CALL runtime.morestack_noctxt(SB) 0x0098 00152 (asm_demo2/asm_demo2.go:8) PCDATA $0, $-1 0x0098 00152 (asm_demo2/asm_demo2.go:8) JMP 0 "".main STEXT size=71 args=0x0 locals=0x18 0x0000 00000 (asm_demo2/asm_demo2.go:21) TEXT "".main(SB), ABIInternal, $24-0 0x0000 00000 (asm_demo2/asm_demo2.go:21) MOVQ (TLS), CX 0x0009 00009 (asm_demo2/asm_demo2.go:21) CMPQ SP, 16(CX) 0x000d 00013 (asm_demo2/asm_demo2.go:21) PCDATA $0, $-2 0x000d 00013 (asm_demo2/asm_demo2.go:21) JLS 64 0x000f 00015 (asm_demo2/asm_demo2.go:21) PCDATA $0, $-1 0x000f 00015 (asm_demo2/asm_demo2.go:21) SUBQ $24, SP 0x0013 00019 (asm_demo2/asm_demo2.go:21) MOVQ BP, 16(SP) 0x0018 00024 (asm_demo2/asm_demo2.go:21) LEAQ 16(SP), BP 0x001d 00029 (asm_demo2/asm_demo2.go:21) FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB) 0x001d 00029 (asm_demo2/asm_demo2.go:21) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB) 0x001d 00029 (asm_demo2/asm_demo2.go:22) MOVQ $1, (SP) // 函数GenAddFuncClosure01入参是x=1 0x0025 00037 (asm_demo2/asm_demo2.go:22) PCDATA $1, $0 0x0025 00037 (asm_demo2/asm_demo2.go:22) CALL "".GenAddFuncClosure01(SB) // 调用函数GenAddFuncClosure01 0x002a 00042 (asm_demo2/asm_demo2.go:22) MOVQ 8(SP), DX // 函数GenAddFuncClosure01返回值是一个地址,写入DX;这里之所以写入DX,是因为GenAddFuncClosure01.func1函数会从DX中获取struct { F uintptr; "".x *int }地址,从而获取x的值 0x002f 00047 (asm_demo2/asm_demo2.go:23) MOVQ (DX), AX // 取返回值对应地址中存储的值写入AX,即struct { F uintptr; "".x *int }中F成员变量的值 0x0032 00050 (asm_demo2/asm_demo2.go:23) CALL AX // 此时AX中存储的其实是GenAddFuncClosure01.func1函数的地址,也就是闭包中真正的执行逻辑,执行该函数 0x0034 00052 (asm_demo2/asm_demo2.go:24) MOVQ 16(SP), BP 0x0039 00057 (asm_demo2/asm_demo2.go:24) ADDQ $24, SP 0x003d 00061 (asm_demo2/asm_demo2.go:24) RET 0x003e 00062 (asm_demo2/asm_demo2.go:24) NOP 0x003e 00062 (asm_demo2/asm_demo2.go:21) PCDATA $1, $-1 0x003e 00062 (asm_demo2/asm_demo2.go:21) PCDATA $0, $-2 0x003e 00062 (asm_demo2/asm_demo2.go:21) NOP 0x0040 00064 (asm_demo2/asm_demo2.go:21) CALL runtime.morestack_noctxt(SB) 0x0045 00069 (asm_demo2/asm_demo2.go:21) PCDATA $0, $-1 0x0045 00069 (asm_demo2/asm_demo2.go:21) JMP 0 "".GenAddFuncClosure01.func1 STEXT nosplit size=20 args=0x8 locals=0x0 0x0000 00000 (asm_demo2/asm_demo2.go:9) TEXT "".GenAddFuncClosure01.func1(SB), NOSPLIT|NEEDCTXT|ABIInternal, $0-8 0x0000 00000 (asm_demo2/asm_demo2.go:9) FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB) 0x0000 00000 (asm_demo2/asm_demo2.go:9) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB) 0x0000 00000 (asm_demo2/asm_demo2.go:9) MOVQ 8(DX), AX // 将8(DX)中存储值写入AX,即struct { F uintptr; "".x *int }中x成员变量 0x0004 00004 (asm_demo2/asm_demo2.go:10) MOVQ (AX), CX // AX存储的是int指针,当初去指针对应的int值写入CX 0x0007 00007 (asm_demo2/asm_demo2.go:10) ADDQ $2, CX // x += 2 0x000b 00011 (asm_demo2/asm_demo2.go:10) MOVQ CX, (AX) // 计算后的新x值笼罩旧值 0x000e 00014 (asm_demo2/asm_demo2.go:11) MOVQ CX, "".~r0+8(SP) // 返回新x值 0x0013 00019 (asm_demo2/asm_demo2.go:11) RET
- 函数
GenAddFuncClosure01
大略实现逻辑是:创立一个type.noalg.struct { F uintptr; "".x *int }(SB)
,将闭包的真正执行函数.GenAddFuncClosure01.func1
写入F uintptr
,入参x int
复制到变量x *int
,这样闭包的本身状态就保留在x *int
中,不会受到入参x int
的影响;最初返回构造体type.noalg.struct { F uintptr; "".x *int }(SB)
的地址; - 闭包的真正执行函数
.GenAddFuncClosure01.func1
的逻辑大略是:从DX
中获取构造体type.noalg.struct { F uintptr; "".x *int }(SB)
的外部变量x
,对x
进行运算,同时笼罩旧的x
值,最初返回新的x
值;留神函数.GenAddFuncClosure01.func1
减少了NEEDCTXT
标识,示意该函数须要上下文,上下文个别都是存储在DX
中; - 函数
main
中,咱们看到调用函数GenAddFuncClosure01
返回后,咱们曾经晓得返回的是构造体type.noalg.struct { F uintptr; "".x *int }(SB)
地址,并且闭包真正执行函数.GenAddFuncClosure01.func1
须要上下文,所以汇编中把返回地址写入DX
,同时取出.GenAddFuncClosure01.func1
地址进行调用;这些应该都是汇编中对于闭包函数调用的人为约定; - 总结一下,闭包函数的具体实现其实是一个构造体,构造体第一个成员变量是闭包理论执行的函数,其余成员变量都是闭包中会用到的外部变量;并且调用闭包函数的时候,编译器晓得取出真正的执行函数,并将构造体地址作为上下文写入
DX
,这样闭包理论执行函数就能够获取并更新构造体中的外部变量;
上面咱们也依照这个思路用Go汇编
来实现一下闭包:
Go代码:
func main() { f1 := GenAddFuncClosure02(1) fmt.Println(f1()) // 3 fmt.Println(f1()) // 5 fmt.Println(f1()) // 7}func GenAddFuncClosure02(x int) func() intfunc GenAddFuncClosure02func1() int
Go汇编代码:
GLOBL ·MyClosureStruct(SB),NOPTR,$16TEXT ·GenAddFuncClosure02(SB), NOSPLIT, $0-16 MOVQ x+0(FP), AX MOVQ AX, ·MyClosureStruct+8(SB) LEAQ ·GenAddFuncClosure02func1(SB), AX MOVQ AX, ·MyClosureStruct(SB) LEAQ ·MyClosureStruct(SB), BX MOVQ BX, ret+8(FP) RETTEXT ·GenAddFuncClosure02func1(SB), NOSPLIT|NEEDCTXT, $0-8 MOVQ 8(DX), AX ADDQ $2 , AX MOVQ AX , 8(DX) MOVQ AX , ret+0(FP) RET
文章闭包函数中给出的例子可能更容易了解一点,读者敌人们能够自行浏览;另外笔者发现NEEDCTXT
flag不加也不影响调用,这一块有懂行的老铁能够领导领导。
文章汇编语言的威力中,还有对于Go汇编
的零碎调用,AVX指令和一个获取goroutine ID的示例,感兴趣的小伙伴能够自行浏览。
至此,咱们就介绍完Go语言中几种非凡函数的汇编实现了,咱们也介绍完了Go汇编
的入门常识。
总结
本篇学习笔记中,咱们介绍了汇编
的基本知识,介绍了Go汇编
基本知识,也通过汇编常识剖析了一些Go语言
个性的实现形式。笔者感觉最重要的还是了解分明汇编
、Go汇编
、Go语言
三者之间的关系和它们各自呈现的背景和起因,以及通过一些汇编
常识帮忙咱们深刻了解Go语言
的实现形式,进一步加深Go语言
的使用能力。
参考
- Go汇编语言
- 汇编语言入门教程
- 大白话 golang 教程-29-反汇编和内存构造
- Go ASM
- 汇编语言学习
- 有栈协程与无栈协程
- 图解函数调用过程