关于golang:Go学习笔记汇编

60次阅读

共计 35098 个字符,预计需要花费 88 分钟才能阅读完成。

前言

本文是笔者学习 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 两个非凡的寄存器外,还有 AXBXCXDXSIDIBPSP 几个通用寄存器。在 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,5eax 就是上文提到的通用寄存器AX,X86-64 指令集外面咱们能够不写后面的r
  • 咱们看到代码中 .data.code别离示意数据区和代码区,验证了上文提到的 X86-64 内存模型中的 textdata局部;
  • 程序实现的性能是 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,$0x37
    DATA ·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);
    联合 DATAGLOBL指令,咱们就能够初始化并导出一个全局变量

    GLOBL ·Id, $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
  • 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 存 BX
    SUBQ AX, BX      // BX = BX - AX 存 BX
    IMULQ AX, BX     // BX = BX * AX 存 BX
    MOVQ AX, BX      // BX = AX 将 AX 中的值赋给 BX
    MOVQ (AX), BX    // BX = *AX 加载 AX 中指向内存地址的值给 BX
    MOVQ 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 = 1234
GLOBL ·MyInt(SB),NOPTR,$8
DATA ·MyInt+0(SB)/8,$1234

// var MyFloat float64 = 56.78
GLOBL ·MyFloat(SB),NOPTR,$8
DATA ·MyFloat+0(SB)/8,$56.78

// var MyBool bool = true
GLOBL ·MyBool(SB),NOPTR,$1
DATA ·MyBool+0(SB)/1,$1

咱们略微解释一下下面的 Go 汇编 代码:

  • GLOBLDATA 命令联合起来,能够初始化一个全局变量并导出;
  • 全局变量都是通过伪 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 string
GLOBL NameData<>(SB),NOPTR,$8
DATA  NameData<>(SB)/8,$"abc"

GLOBL ·MyStr0(SB),NOPTR,$16
DATA  ·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,$16
    DATA  ·MyArray+0(SB)/8,$12
    DATA  ·MyArray+8(SB)/8,$34

    4. 定义切片

// 定义三个 string 长期变量,作为切片元素
GLOBL str0<>(SB),NOPTR,$48
DATA  str0<>(SB)/48,$"Thoughts in the Still of the Night"

GLOBL str1<>(SB),NOPTR,$48
DATA  str1<>(SB)/48,$"A pool of moonlight before the bed"

GLOBL str2<>(SB),NOPTR,$8
DATA  str2<>(SB)/8,$"libai"

// 定义一个[3]string 的数组,元素就是下面的三个 string 变量
GLOBL strarray<>(SB),NOPTR,$48
DATA  strarray<>+0(SB)/8,$str0<>(SB)
DATA  strarray<>+8(SB)/8,$34
DATA  strarray<>+16(SB)/8,$str1<>(SB)
DATA  strarray<>+24(SB)/8,$34
DATA  strarray<>+32(SB)/8,$str2<>(SB)
DATA  strarray<>+40(SB)/8,$5

// var MySlice []string
GLOBL ·MySlice(SB),NOPTR,$24
// 下面[3]string 数组的首地址用来初始化切片的 Data 字段
DATA  ·MySlice+0(SB)/8,$strarray<>(SB)
DATA  ·MySlice+8(SB)/8,$3
DATA  ·MySlice+16(SB)/8,$4

// 定义一个[3]*string 的数组,元素就是下面三个 string 变量的地址
GLOBL strptrarray<>(SB),NOPTR,$24
DATA  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 []*string
GLOBL ·MyPtrSlice(SB),NOPTR,$24
// 下面[3]*string 数组的首地址用来初始化切片的 Data 字段
DATA  ·MyPtrSlice+0(SB)/8,$strptrarray<>(SB)
DATA  ·MyPtrSlice+8(SB)/8,$3
DATA  ·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 函数能够写成上面两种模式:

// 残缺定义,蕴含 argsize
TEXT ·Swap(SB), NOSPLIT, $0-32
// 简略定义,不蕴含 argsize
TEXT ·Swap(SB), NOSPLIT, $0

上面是 2 种函数定义的比照图

2. 咱们留神到函数定义中有NOSPLIT flag,目前可能遇到的函数标记有NOSPLITWRAPPERNEEDCTXT几个。其中 NOSPLIT 不会生成或蕴含栈决裂代码,这个别用于没有任何其它函数调用的叶子函数,这样能够适当进步性能。WRAPPER标记则示意这个是一个包装函数,在 panicruntime.caller等某些处理函数帧的中央不会减少函数帧计数。最初的 NEEDCTXT 示意须要一个上下文参数,个别用于闭包函数。这些 falg 都定义在文件 textflag.h 中。
3. 函数定义中也是没有类型的,下面的 Swap 函数签名能够改写成上面任意一种,只有满足入加入上返回值,占用内存是 32 字节就能够了

func Swap(a, b, c int) int
func 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,$8
DATA ·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 汇编来实现函数 printsumsum

// 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) int
TEXT ·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 汇编 的角度来探讨下程序中控制流的实现。

  1. 程序执行
    首先,后面的很多例子都是程序执行流程,咱们就不额定举例子了。
  2. 条件跳转
    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) int
TEXT ·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
    RET
L:
    MOVQ BX, ret+24(FP) // retrun b
    RET
  1. 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) int
TEXT ·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 = 0
LOOPIF:
    CMPQ DX, AX
    JL LOOPBODY
    JMP LOOPEND

LOOPBODY:
    ADDQ BX, ret+24(FP) // result += v0
    ADDQ CX, BX // v0 += step
    ADDQ $1, DX // i++
    JMP LOOPIF

LOOPEND:
    RET
几种非凡的函数

咱们之前介绍的函数都是比较简单的函数,本节咱们将介绍一些简单的函数实现;首先咱们介绍一下函数的调用标准,更加深刻的理解一下函数的调用细节;而后咱们别离介绍一下:办法函数、递归函数和闭包。

  1. Go 函数调用标准
    咱们之前讲过 Go 汇编 中函数的调用,只是简略说了下被调函数的入参和返回值如何传递的问题,本节咱们更加深刻的解说函数调用的细节。感兴趣的小伙伴能够参考这篇文章:图解函数调用过程,文章外面说的很具体,咱们这里就不赘述了。
    咱们这里给出一张 Go 汇编 函数调用标准图解阐明:
  • 从图中咱们能够看出函数调用过程中,栈都是间断的,例如函数 A 中调用函数 B,那么这两个函数所占用的栈空间是连贯在一起的一块间断空间;
  • 图示中开展了 CALLRET两个命令,简略来说就是 CALL 命令约等于 PUSH IPJMP callee_func两个命令的组合,其中 PUSH IP 命令相当于 SUBQ $-1, SPMOVQ IP, (SP),即栈帧扩大 1 个字节,而后把 IP 寄存器的存储值写入 SP 寄存器对应的内存地址中,因为 IP 寄存器中存储的值能够简略了解为 CALL 命令的下一条指令的地址,这条指令就是 CALL 命令调用子函数返回后,咱们须要执行的命令地址,所以咱们也称之为 return addressJMP callee_func 命令意思是将子函数的地址退出 IP 寄存器,这样就实现了函数跳转;RET命令刚好想反,把之前栈中存储的 return address 地址写入 IP 寄存器并跳转回去继续执行上面的命令;
  • 个别一个函数的栈帧包含局部变量和调用子函数所需的入参和返回值,当执行到一个函数的时候,函数的栈帧对应的栈内存的栈底地址是存储在 BP 寄存器中的,这个值个别是不变的,所以 BP 叫做基址指针寄存器,栈帧的栈顶地址是存储在 SP 寄存器中的,这个值是在一直变动的,因为函数执行过程中常常须要栈扩大来存储局部变量等,所以 SP 寄存器也叫栈指针寄存器;图中 caller's BP 保留的就是调用函数过后 BP 寄存器中的值,也就是调用函数栈帧的栈底地址,咱们子函数返回后,须要通过这个值来回复调用函数的栈帧的栈底地址;
  • 留神图中的 argsizeframesize,他们都是栈中一部分,不是全副,Go 汇编 暗藏了一部分调用细节;
  • 下面提到的 CALLRET两个命令是不解决被调用函数的入参和返回值的,因为它们是由调用函数来筹备的,切记这一点;
    上面通过一个示例来验证一下函数调用标准,咱们先构建一个多级函数调用的示例,文件命名为asm_call.go
package main

import "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 汇编 来重写 Fun01Fun02两个函数

// func Fun01(a, b int) int
TEXT ·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) int
TEXT ·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 到 CX
0x0009 00009 (asm_caller/asm_call.go:20)    CMPQ    SP, 16(CX) // 比拟以后 SP 中的值和 g 中的 stackguard0
0x000d 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 初始化
  • PCDATAFUNCDATA 都是 Go 编译器主动生成的指令,它们都是和函数表格相干,具体能够参考文章:PCDATA 和 FUNCDATA
  • 指标代码的其余局部和咱们 Go 汇编 根本一样,只有留神一下真伪 SP 寄存器的不同;

本节波及的知识点其实很多,想要长篇累牍的说分明其实不容易,笔者倡议大家看看文章:函数调用标准。
有了下面的根底,上面咱们具体来剖析几种非凡的函数,咱们采纳的分析方法是:先构建 Go 代码示例,而后输入汇编指标代码,查看汇编实现,剖析实现逻辑,最初咱们采纳 Go 汇编的形式实现一遍。

  1. 办法函数
    咱们先来构建一个简略的办法函数
package main

type MyInt int

func (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 int

func (v MyInt) Twice01() int {return int(v)*2
}

func (v *MyInt) Twice02() int {return int(*v)*2
}

第一个办法函数的汇编实现如下:

// func (v MyInt) Twice() int
TEXT ·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 汇编语言中,星号和小括弧都无奈用作函数名字,也就是无奈用汇编间接实现接管参数是指针类型的办法。这个可能是官网的成心限度。

  1. 递归函数
    递归函数是比拟非凡的函数,递归函数通过调用本身并且在栈上保留状态,这能够简化很多问题的解决。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_END

L_STEP_TO_END:
    AX -= 1
    BX = sum(AX)

    AX = n // 调用函数后, AX 从新复原为 n
    BX += AX

    return BX

L_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_END

L_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
    RET

L_END:
    MOVQ $0, result+8(FP) // return 0
    RET

留神这里不能加上 NOSPLIT 标识,因为递归函数会因为调用层级过大而导致栈空间有余,NOSPLIT标识限度了栈的扩容,显然是 Go 编译器不容许的。

  1. 闭包函数
    闭包函数是最弱小的函数,因为闭包函数能够捕捉外层部分作用域的局部变量,因而闭包函数自身就具备了状态。从实践上来说,全局的函数也是闭包函数的子集,只不过全局函数并没有捕捉外层变量而已。
    老规矩,咱们先来构建一个简略的闭包函数:
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() int
func GenAddFuncClosure02func1() int

Go 汇编代码:

GLOBL ·MyClosureStruct(SB),NOPTR,$16

TEXT ·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)
    RET

TEXT ·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 语言 的使用能力。

参考

  1. Go 汇编语言
  2. 汇编语言入门教程
  3. 大白话 golang 教程 -29- 反汇编和内存构造
  4. Go ASM
  5. 汇编语言学习
  6. 有栈协程与无栈协程
  7. 图解函数调用过程

正文完
 0