1
0
mirror of https://github.com/chai2010/advanced-go-programming-book.git synced 2025-05-24 12:32:21 +00:00

ch3: 完善内容编号

This commit is contained in:
chai2010 2018-08-05 08:20:00 +08:00
parent f97219b07b
commit bfb993cdf9
9 changed files with 47 additions and 47 deletions

View File

@ -2,11 +2,11 @@
Go汇编程序始终是幽灵一样的存在。我们将通过分析简单的Go程序输出的汇编代码然后照猫画虎用汇编实现一个简单的输出程序。 Go汇编程序始终是幽灵一样的存在。我们将通过分析简单的Go程序输出的汇编代码然后照猫画虎用汇编实现一个简单的输出程序。
## 实现和声明 ## 3.1.1 实现和声明
Go汇编语言并不是一个独立的语言因为Go汇编程序无法独立使用。Go汇编代码必须以Go包的方式组织同时包中至少要有一个Go语言文件用于指明当前包名等基本包信息。如果Go汇编代码中定义的变量和函数要被其它Go语言代码引用还需要通过Go语言代码将汇编中定义的符号声明出来。用于变量的定义和函数的定义Go汇编文件类似于C语言中的.c文件而用于导出汇编中定义符号的Go源文件类似于C语言的.h文件。 Go汇编语言并不是一个独立的语言因为Go汇编程序无法独立使用。Go汇编代码必须以Go包的方式组织同时包中至少要有一个Go语言文件用于指明当前包名等基本包信息。如果Go汇编代码中定义的变量和函数要被其它Go语言代码引用还需要通过Go语言代码将汇编中定义的符号声明出来。用于变量的定义和函数的定义Go汇编文件类似于C语言中的.c文件而用于导出汇编中定义符号的Go源文件类似于C语言的.h文件。
## 定义整数变量 ## 3.1.2 定义整数变量
为了简单我们先用Go语言定义并赋值一个整数变量然后查看生成的汇编代码。 为了简单我们先用Go语言定义并赋值一个整数变量然后查看生成的汇编代码。
@ -100,7 +100,7 @@ func main() {
对于Go包的用户来说用Go汇编语言或Go语言实现并无任何区别。 对于Go包的用户来说用Go汇编语言或Go语言实现并无任何区别。
## 定义字符串变量 ## 3.1.3 定义字符串变量
在前一个例子中我们通过汇编定义了一个整数变量。现在我们提高一点难度尝试通过汇编定义一个字符串变量。虽然从Go语言角度看定义字符串和整数变量的写法基本相同但是字符串底层却有着比单个整数更复杂的数据结构。 在前一个例子中我们通过汇编定义了一个整数变量。现在我们提高一点难度尝试通过汇编定义一个字符串变量。虽然从Go语言角度看定义字符串和整数变量的写法基本相同但是字符串底层却有着比单个整数更复杂的数据结构。
@ -214,7 +214,7 @@ DATA ·Name+16(SB)/8,$"gopher"
在新的结构中Name符号对应的内存从16字节变为24字节多出的8个字节存放底层的“gopher”字符串。·Name符号前16个字节依然对应reflect.StringHeader结构体Data部分对应`$·Name+16(SB)`表示数据的地址为Name符号往后偏移16个字节的位置Len部分依然对应6个字节的长度。这是C语言程序员经常使用档技巧。 在新的结构中Name符号对应的内存从16字节变为24字节多出的8个字节存放底层的“gopher”字符串。·Name符号前16个字节依然对应reflect.StringHeader结构体Data部分对应`$·Name+16(SB)`表示数据的地址为Name符号往后偏移16个字节的位置Len部分依然对应6个字节的长度。这是C语言程序员经常使用档技巧。
## 定义main函数 ## 3.1.4 定义main函数
前面的例子已经展示了如何通过汇编定义整型和字符串类型变量。我们现在将尝试用汇编实现函数,然后输出一个字符串。 前面的例子已经展示了如何通过汇编定义整型和字符串类型变量。我们现在将尝试用汇编实现函数,然后输出一个字符串。
@ -244,7 +244,7 @@ TEXT ·main(SB), $16-0
Go语言函数在函数调用时完全通过栈传递调用参数和返回值。先通过MOVQ指令将helloworld对应的字符串头部结构体的16个字节复制到栈指针SP对应的16字节的空间然后通过CALL指令调用对应函数。最后使用RET指令表示当前函数返回。 Go语言函数在函数调用时完全通过栈传递调用参数和返回值。先通过MOVQ指令将helloworld对应的字符串头部结构体的16个字节复制到栈指针SP对应的16字节的空间然后通过CALL指令调用对应函数。最后使用RET指令表示当前函数返回。
## 特殊字符 ## 3.1.5 特殊字符
Go语言函数或方法符号在编译为目标文件后目标文件中的每个符号均包含对应包的绝对导入路径。因此目标文件的符号可能非常复杂比如“path/to/pkg.(*SomeType).SomeMethod”或“go.string."abc"”等名字。目标文件的符号名中不仅仅包含普通的字母还可能包含点号、星号、小括弧和双引号等诸多特殊字符。而Go语言的汇编器是从plan9移植过来的二把刀并不能处理这些特殊的字符导致了用Go汇编语言手工实现Go诸多特性时遇到种种限制。 Go语言函数或方法符号在编译为目标文件后目标文件中的每个符号均包含对应包的绝对导入路径。因此目标文件的符号可能非常复杂比如“path/to/pkg.(*SomeType).SomeMethod”或“go.string."abc"”等名字。目标文件的符号名中不仅仅包含普通的字母还可能包含点号、星号、小括弧和双引号等诸多特殊字符。而Go语言的汇编器是从plan9移植过来的二把刀并不能处理这些特殊的字符导致了用Go汇编语言手工实现Go诸多特性时遇到种种限制。
@ -255,7 +255,7 @@ Go汇编语言同样遵循Go语言少即是多的哲学它只保留了最基
如果是macOS系统则有以下几种方法输入中点`·`:在不开输入法时,可直接用 option+shift+9 输入;如果是自带的简体拼音输入法,输入左上角`~`键对应`·`如果是自带的Unicode输入法则可以输入对应的Unicode码点。其中Unicode输入法可能是最安全可靠等输入方式。 如果是macOS系统则有以下几种方法输入中点`·`:在不开输入法时,可直接用 option+shift+9 输入;如果是自带的简体拼音输入法,输入左上角`~`键对应`·`如果是自带的Unicode输入法则可以输入对应的Unicode码点。其中Unicode输入法可能是最安全可靠等输入方式。
## 没有分号 ## 3.1.6 没有分号
Go汇编语言中分号可以用于分隔同一行内的多个语句。下面是用分号混乱排版的汇编代码 Go汇编语言中分号可以用于分隔同一行内的多个语句。下面是用分号混乱排版的汇编代码

View File

@ -5,7 +5,7 @@
汇编语言其实是一种非常简单的编程语言因为它面向的计算机模型就是非常简单的。让人觉得汇编语言难学主要有几个原因不同类型的CPU都有自己的一套指令即使是相同的CPU32位和64位的运行模式依然会有差异不同的汇编工具同样有自己特有的汇编指令不同的操作系统和高级编程语言和底层汇编的调用规范并不相同。本节将描述几个有趣的汇编语言模型最后精简出一个适用于AMD64架构的精简指令集以便于Go汇编语言的学习。 汇编语言其实是一种非常简单的编程语言因为它面向的计算机模型就是非常简单的。让人觉得汇编语言难学主要有几个原因不同类型的CPU都有自己的一套指令即使是相同的CPU32位和64位的运行模式依然会有差异不同的汇编工具同样有自己特有的汇编指令不同的操作系统和高级编程语言和底层汇编的调用规范并不相同。本节将描述几个有趣的汇编语言模型最后精简出一个适用于AMD64架构的精简指令集以便于Go汇编语言的学习。
## 图灵机和BF语言 ## 3.2.1 图灵机和BF语言
图灵机是由图灵提出的一种抽象计算模型。机器有一条无限长的纸带,纸带分成了一个一个的小方格,每个方格有不同的颜色,这类似于计算机中的内存。同时机器有一个探头在纸带上移来移去,类似于通过内存地址来读写内存上的数据。机器头有一组内部计算状态,还有一些固定的程序(更像一个哈佛结构)。在每个时刻,机器头都要从当前纸带上读入一个方格信息,然后根据自己的内部状态和当前要执行的程序指令将信息输出到纸带方格上,同时更新自己的内部状态并进行移动。 图灵机是由图灵提出的一种抽象计算模型。机器有一条无限长的纸带,纸带分成了一个一个的小方格,每个方格有不同的颜色,这类似于计算机中的内存。同时机器有一个探头在纸带上移来移去,类似于通过内存地址来读写内存上的数据。机器头有一组内部计算状态,还有一些固定的程序(更像一个哈佛结构)。在每个时刻,机器头都要从当前纸带上读入一个方格信息,然后根据自己的内部状态和当前要执行的程序指令将信息输出到纸带方格上,同时更新自己的内部状态并进行移动。
@ -34,7 +34,7 @@
理论上我们可以将BF语言当作目标机器语言将其它高级语言编译为BF语言后就可以在BF机器上运行了。 理论上我们可以将BF语言当作目标机器语言将其它高级语言编译为BF语言后就可以在BF机器上运行了。
## 人力资源机器游戏 ## 3.2.2 人力资源机器游戏
《人力资源机器》Human Resource Machine是一款设计精良汇编语言编程游戏。在游戏中玩家扮演一个职员角色来模拟人力资源机器的运行。通过完成上司给的每一份任务来实现晋升的目标完成任务的途径就是用游戏提供的11个机器指令编写正确的汇编程序最终得到正确的输出结果。人力资源机器的汇编语言可以认为是跨平台、跨操作系统的通用的汇编语言因为在macOS、Windows、Linux和iOS上该游戏的玩法都是完全一致的。 《人力资源机器》Human Resource Machine是一款设计精良汇编语言编程游戏。在游戏中玩家扮演一个职员角色来模拟人力资源机器的运行。通过完成上司给的每一份任务来实现晋升的目标完成任务的途径就是用游戏提供的11个机器指令编写正确的汇编程序最终得到正确的输出结果。人力资源机器的汇编语言可以认为是跨平台、跨操作系统的通用的汇编语言因为在macOS、Windows、Linux和iOS上该游戏的玩法都是完全一致的。
@ -80,7 +80,7 @@ LOOP:
首先通过INBOX指令读取一个数据包然后判断包裹的数据是否为0如果是0的话就跳转到开头继续读取下一个数据包否则将输出数据包然后再跳转到开头。以此循环无休止地处理数据包裹直到任务完成晋升到更高一级的岗位然后处理类似的但更复杂的任务。 首先通过INBOX指令读取一个数据包然后判断包裹的数据是否为0如果是0的话就跳转到开头继续读取下一个数据包否则将输出数据包然后再跳转到开头。以此循环无休止地处理数据包裹直到任务完成晋升到更高一级的岗位然后处理类似的但更复杂的任务。
## X86-64体系结构 ## 3.2.3 X86-64体系结构
X86其实是是80X86的简称后面三个字母包括Intel 8086、80286、80386以及80486等指令集合因此其架构被称为x86架构。x86-64是AMD公司于1999年设计的x86架构的64位拓展向后兼容于16位及32位的x86架构。X86-64目前正式名称为AMD64也就是Go语言中GOARCH环境变量指定的AMD64。如果没有特殊说明的话本章中的汇编程序都是针对64位的X86-64环境。 X86其实是是80X86的简称后面三个字母包括Intel 8086、80286、80386以及80486等指令集合因此其架构被称为x86架构。x86-64是AMD公司于1999年设计的x86架构的64位拓展向后兼容于16位及32位的x86架构。X86-64目前正式名称为AMD64也就是Go语言中GOARCH环境变量指定的AMD64。如果没有特殊说明的话本章中的汇编程序都是针对64位的X86-64环境。
@ -95,7 +95,7 @@ X86其实是是80X86的简称后面三个字母包括Intel 8086、80286
右边是X86的指令集。CPU是由指令和寄存器组成指令是每个CPU内置的算法指令处理的对象就是全部的寄存器和内存。我们可以将每个指令看作是CPU内置标准库中提供的一个个函数然后基于这些函数构造更复杂的程序的过程就是用汇编语言编程的过程。 右边是X86的指令集。CPU是由指令和寄存器组成指令是每个CPU内置的算法指令处理的对象就是全部的寄存器和内存。我们可以将每个指令看作是CPU内置标准库中提供的一个个函数然后基于这些函数构造更复杂的程序的过程就是用汇编语言编程的过程。
## Go汇编中的伪寄存器 ## 3.2.4 Go汇编中的伪寄存器
Go汇编为了简化汇编代码的编写引入了PC、FP、SP、SB四个伪寄存器。四个伪寄存器加其它的通用寄存器就是Go汇编语言对CPU的重新抽象该抽象的结构也适用于其它非X86类型的体系结构。 Go汇编为了简化汇编代码的编写引入了PC、FP、SP、SB四个伪寄存器。四个伪寄存器加其它的通用寄存器就是Go汇编语言对CPU的重新抽象该抽象的结构也适用于其它非X86类型的体系结构。
@ -107,7 +107,7 @@ Go汇编为了简化汇编代码的编写引入了PC、FP、SP、SB四个伪
当需要区分伪寄存器和真寄存器的时候只需要记住一点:伪寄存器一般需要一个标识符和偏移量为前缀,如果没有标识符前缀则是真寄存器。比如`(SP)``+8(SP)`没有标识符前缀为真SP寄存器`a(SP)``b+8(SP)`有标识符为前缀表示伪寄存器。 当需要区分伪寄存器和真寄存器的时候只需要记住一点:伪寄存器一般需要一个标识符和偏移量为前缀,如果没有标识符前缀则是真寄存器。比如`(SP)``+8(SP)`没有标识符前缀为真SP寄存器`a(SP)``b+8(SP)`有标识符为前缀表示伪寄存器。
## X86-64指令集 ## 3.2.5 X86-64指令集
很多汇编语言的教程都会强调汇编语言是不可移植的。严格来说汇编语言是在不同的CPU类型、或不同的操作系统环境、或不同的汇编工具链下是不可移植的而在同一种CPU中运行的机器指令是完全一样的。汇编语言这种不可移植性正是其普及的一个极大的障碍。虽然CPU指令集的差异是导致不好移植的较大因素但是汇编语言的相关工具链对此也有不可推卸的责任。而源自Plan9的Go汇编语言对此做了一定的改进首先Go汇编语言在相同CPU架构上是完全一致的也就是屏蔽了操作系统的差异同时Go汇编语言将一些基础并且类似的指令抽象为相同名字的伪指令从而减少不同CPU架构下汇编代码的差异寄存器名字和数量的差异是一直存在的。本节的目的也是找出一个较小的精简指令集以简化Go汇编语言的学习。 很多汇编语言的教程都会强调汇编语言是不可移植的。严格来说汇编语言是在不同的CPU类型、或不同的操作系统环境、或不同的汇编工具链下是不可移植的而在同一种CPU中运行的机器指令是完全一样的。汇编语言这种不可移植性正是其普及的一个极大的障碍。虽然CPU指令集的差异是导致不好移植的较大因素但是汇编语言的相关工具链对此也有不可推卸的责任。而源自Plan9的Go汇编语言对此做了一定的改进首先Go汇编语言在相同CPU架构上是完全一致的也就是屏蔽了操作系统的差异同时Go汇编语言将一些基础并且类似的指令抽象为相同名字的伪指令从而减少不同CPU架构下汇编代码的差异寄存器名字和数量的差异是一直存在的。本节的目的也是找出一个较小的精简指令集以简化Go汇编语言的学习。

View File

@ -2,7 +2,7 @@
程序中的一切变量的初始值都直接或间接地依赖常量或常量表达式生成。在Go语言中很多变量是默认零值初始化的但是Go汇编中定义的变量最好还是手工通过常量初始化。有了常量之后就可以衍生定义全局变量并使用常量组成的表达式初始化其它各种变量。本节将简单讨论Go汇编语言中常量和全局变量的用法。 程序中的一切变量的初始值都直接或间接地依赖常量或常量表达式生成。在Go语言中很多变量是默认零值初始化的但是Go汇编中定义的变量最好还是手工通过常量初始化。有了常量之后就可以衍生定义全局变量并使用常量组成的表达式初始化其它各种变量。本节将简单讨论Go汇编语言中常量和全局变量的用法。
## 常量 ## 3.3.1 常量
Go汇编语言中常量以$美元符号为前缀。常量的类型有整数常量、浮点数常量、字符常量和字符串常量等几种类型。以下是几种类型常量的例子: Go汇编语言中常量以$美元符号为前缀。常量的类型有整数常量、浮点数常量、字符常量和字符串常量等几种类型。以下是几种类型常量的例子:
@ -42,7 +42,7 @@ DATA ·Name+8(SB)/8,$6
其中`$·NameData(SB)`也是以$美元符号为前缀因此也可以将它看作是一个常量它对应的是NameData包变量的地址。在汇编指令中我们也可以通过LEA指令来获取NameData变量的地址。 其中`$·NameData(SB)`也是以$美元符号为前缀因此也可以将它看作是一个常量它对应的是NameData包变量的地址。在汇编指令中我们也可以通过LEA指令来获取NameData变量的地址。
## 全局变量 ## 3.3.2 全局变量
在Go语言中变量根据作用域和生命周期有全局变量和局部变量之分。全局变量是包一级的变量全局变量一般有着较为固定的内存地址声明周期跨越整个程序运行时间。而局部变量一般是函数内定义的的变量只有在函数被执行的时间才被在栈上创建当函数调用完成后将回收暂时不考虑闭包对局部变量捕获的问题 在Go语言中变量根据作用域和生命周期有全局变量和局部变量之分。全局变量是包一级的变量全局变量一般有着较为固定的内存地址声明周期跨越整个程序运行时间。而局部变量一般是函数内定义的的变量只有在函数被执行的时间才被在栈上创建当函数调用完成后将回收暂时不考虑闭包对局部变量捕获的问题
@ -90,7 +90,7 @@ DATA ·count+0(SB)/4,$0x01020304
最后还需要在Go语言中声明对应的变量和C语言头文件声明变量的作用类似这样垃圾回收器会根据变量的类型来管理其中的指针相关的内存数据。 最后还需要在Go语言中声明对应的变量和C语言头文件声明变量的作用类似这样垃圾回收器会根据变量的类型来管理其中的指针相关的内存数据。
### 数组类型 ### 3.3.2.1 数组类型
汇编中数组也是一种非常简单的类型。Go语言中数组是一种有着扁平内存结构的基础类型。因此`[2]byte`类型和`[1]uint16`类型有着相同的内存结构。只有当数组和结构体结合之后情况才会变的稍微复杂。 汇编中数组也是一种非常简单的类型。Go语言中数组是一种有着扁平内存结构的基础类型。因此`[2]byte`类型和`[1]uint16`类型有着相同的内存结构。只有当数组和结构体结合之后情况才会变的稍微复杂。
@ -115,7 +115,7 @@ DATA ·num+8(SB)/8,$0
汇编代码中并不需要NOPTR标志因为Go编译器会从Go语言语句声明的`[2]int`类型中推导出该变量内部没有指针数据。 汇编代码中并不需要NOPTR标志因为Go编译器会从Go语言语句声明的`[2]int`类型中推导出该变量内部没有指针数据。
### bool型变量 ### 3.3.2.2 bool型变量
Go汇编语言定义变量无法指定类型信息因此需要先通过Go语言声明变量的类型。以下是在Go语言中声明的几个bool类型变量 Go汇编语言定义变量无法指定类型信息因此需要先通过Go语言声明变量的类型。以下是在Go语言中声明的几个bool类型变量
@ -141,7 +141,7 @@ DATA ·falseValue(SB)/1,$0
bool类型的内存大小为1个字节。并且汇编中定义的变量需要手工指定初始化值否则将可能导致产生未初始化的变量。当需要将1个字节的bool类型变量加载到8字节的寄存器时需要使用MOVBQZX指令将不足的高位用0填充。 bool类型的内存大小为1个字节。并且汇编中定义的变量需要手工指定初始化值否则将可能导致产生未初始化的变量。当需要将1个字节的bool类型变量加载到8字节的寄存器时需要使用MOVBQZX指令将不足的高位用0填充。
### int型变量 ### 3.3.2.3 int型变量
所有的整数类型均有类似的定义的方式比较大的差异是整数类型的内存大小和整数是否是有符号。下面是声明的int32和uint32类型变量 所有的整数类型均有类似的定义的方式比较大的差异是整数类型的内存大小和整数是否是有符号。下面是声明的int32和uint32类型变量
@ -165,7 +165,7 @@ DATA ·uint32Value(SB)/4,$0x01020304 // 第1-4字节
汇编定义变量时初始化数据并不区分整数是否有符号。只有在CPU指令处理该寄存器数据时才会根据指令的类型来取分数据的类型或者是否带有符号位。 汇编定义变量时初始化数据并不区分整数是否有符号。只有在CPU指令处理该寄存器数据时才会根据指令的类型来取分数据的类型或者是否带有符号位。
### float型变量 ### 3.3.2.4 float型变量
Go汇编语言通常无法区分变量是否是浮点数类型与之相关的浮点数机器指令会将变量当作浮点数处理。Go语言的浮点数遵循IEEE754标准有float32单精度浮点数和float64双精度浮点数之分。 Go汇编语言通常无法区分变量是否是浮点数类型与之相关的浮点数机器指令会将变量当作浮点数处理。Go语言的浮点数遵循IEEE754标准有float32单精度浮点数和float64双精度浮点数之分。
@ -195,7 +195,7 @@ DATA ·float64Value(SB)/4,$0x01020304 // bit 方式初始化
我们在上一节精简的算术指令中都是针对整数如果要通过整数指令处理浮点数的加减法必须根据浮点数的运算规则进行先对齐小数点然后进行整数加减法最后再对结果进行归一化并处理精度舍入问题。不过在目前的主流CPU中都提针对浮点数提供了专有的计算指令。 我们在上一节精简的算术指令中都是针对整数如果要通过整数指令处理浮点数的加减法必须根据浮点数的运算规则进行先对齐小数点然后进行整数加减法最后再对结果进行归一化并处理精度舍入问题。不过在目前的主流CPU中都提针对浮点数提供了专有的计算指令。
### string类型变量 ### 3.3.2.5 string类型变量
从Go汇编语言角度看字符串只是一种结构体。string的头结构定义如下 从Go汇编语言角度看字符串只是一种结构体。string的头结构定义如下
@ -235,7 +235,7 @@ DATA ·helloworld+8(SB)/8,$12 // StringHeader.Len
需要注意的是,字符串是只读类型,要避免在汇编中直接修改字符串底层数据的内容。 需要注意的是,字符串是只读类型,要避免在汇编中直接修改字符串底层数据的内容。
### slice类型变量 ### 3.3.2.6 slice类型变量
slice变量和string变量相似只不过是对应的是切片头结构体而已。切片头的结构如下 slice变量和string变量相似只不过是对应的是切片头结构体而已。切片头的结构如下
@ -266,7 +266,7 @@ DATA text<>+8(SB)/8,$"rld!" // ...string data...
因为切片和字符串的相容性我们可以将切片头的前16个字节临时作为字符串使用这样可以省去不必要的转换。 因为切片和字符串的相容性我们可以将切片头的前16个字节临时作为字符串使用这样可以省去不必要的转换。
### map/channel类型变量 ### 3.3.2.7 map/channel类型变量
map/channel等类型并没有公开的内部结构它们只是一种未知类型的指针无法直接初始化。在汇编代码中我们只能为类似变量定义并进行0值初始化 map/channel等类型并没有公开的内部结构它们只是一种未知类型的指针无法直接初始化。在汇编代码中我们只能为类似变量定义并进行0值初始化
@ -294,7 +294,7 @@ func makechan(chanType *byte, size int) (hchan chan any)
需要注意的是makemap是一种范型函数可以创建不同类型的mapmap的具体类型是通过mapType参数指定。 需要注意的是makemap是一种范型函数可以创建不同类型的mapmap的具体类型是通过mapType参数指定。
## 变量的内存布局 ## 3.3.3 变量的内存布局
我们已经多次强调在Go汇编语言中变量是没有类型的。因此在Go语言中有着不同类型的变量底层可能对应的是相同的内存结构。深刻理解每个变量的内存布局是汇编编程时的必备条件。 我们已经多次强调在Go汇编语言中变量是没有类型的。因此在Go语言中有着不同类型的变量底层可能对应的是相同的内存结构。深刻理解每个变量的内存布局是汇编编程时的必备条件。
@ -312,7 +312,7 @@ func makechan(chanType *byte, size int) (hchan chan any)
因此`[2]int``image.Point`类型底层有着近似相同的内存布局。 因此`[2]int``image.Point`类型底层有着近似相同的内存布局。
## 标识符规则和特殊标志 ## 3.3.4 标识符规则和特殊标志
Go语言的标识符可以由绝对的包路径加标识符本身定位因此不同包中的标识符即使同名也不会有问题。Go汇编是通过特殊的符号来表示斜杠和点符号因为这样可以简化汇编器词法扫描部分代码的编写只要通过字符串替换就可以了。 Go语言的标识符可以由绝对的包路径加标识符本身定位因此不同包中的标识符即使同名也不会有问题。Go汇编是通过特殊的符号来表示斜杠和点符号因为这样可以简化汇编器词法扫描部分代码的编写只要通过字符串替换就可以了。
@ -351,7 +351,7 @@ DATA ·const_id+0(SB)/8,$9527
变量一般也叫可取地址的值但是const_id虽然可以取地址但是确实不能修改。不能修改的限制并不是由编译器提供而是因为对该变量的修改会导致对只读内存段进行写导致从而导致异常。 变量一般也叫可取地址的值但是const_id虽然可以取地址但是确实不能修改。不能修改的限制并不是由编译器提供而是因为对该变量的修改会导致对只读内存段进行写导致从而导致异常。
## 小结 ## 3.3.5 小结
以上我们初步展示了通过汇编定义全局变量的用法。但是真实的环境中我们并不推荐通过汇编定义变量——因为用Go语言定义变量更加简单和安全。在Go语言中定义变量编译器可以帮助我们计算好变量的大小生成变量的初始值同时也包含了足够的类型信息。汇编语言的优势是挖掘机器的特性和性能用汇编定义变量则无法发挥这些优势。因此在理解了汇编定义变量的用法后建议大家谨慎使用。 以上我们初步展示了通过汇编定义全局变量的用法。但是真实的环境中我们并不推荐通过汇编定义变量——因为用Go语言定义变量更加简单和安全。在Go语言中定义变量编译器可以帮助我们计算好变量的大小生成变量的初始值同时也包含了足够的类型信息。汇编语言的优势是挖掘机器的特性和性能用汇编定义变量则无法发挥这些优势。因此在理解了汇编定义变量的用法后建议大家谨慎使用。

View File

@ -2,7 +2,7 @@
终于到函数了因为Go汇编语言中可以也建议通过Go语言来定义全局变量那么剩下的也就是函数了。只有掌握了汇编函数的基本用法才能真正算是Go汇编语言入门。本章将简单讨论Go汇编中函数的定义和用法。 终于到函数了因为Go汇编语言中可以也建议通过Go语言来定义全局变量那么剩下的也就是函数了。只有掌握了汇编函数的基本用法才能真正算是Go汇编语言入门。本章将简单讨论Go汇编中函数的定义和用法。
## 基本语法 ## 3.4.1 基本语法
函数标识符通过TEXT汇编指令定义表示该行开始的指令定义在TEXT内存段。TEXT语句后的指令一般对应函数的实现但是对于TEXT指令本身来说并不关心后面是否有指令。因此TEXT和LABEL定义的符号是类似的区别只是LABEL是用于跳转标号但是本质上他们都是通过标识符映射一个内存地址。 函数标识符通过TEXT汇编指令定义表示该行开始的指令定义在TEXT内存段。TEXT语句后的指令一般对应函数的实现但是对于TEXT指令本身来说并不关心后面是否有指令。因此TEXT和LABEL定义的符号是类似的区别只是LABEL是用于跳转标号但是本质上他们都是通过标识符映射一个内存地址。
@ -56,7 +56,7 @@ func Swap() (a []int, d int)
对于汇编函数来说只要是函数的名字和参数大小一致就可以是相同的函数了。而且在Go汇编语言中输入参数和返回值参数是没有任何的区别的。 对于汇编函数来说只要是函数的名字和参数大小一致就可以是相同的函数了。而且在Go汇编语言中输入参数和返回值参数是没有任何的区别的。
## 函数参数和返回值 ## 3.4.2 函数参数和返回值
对于函数来说最重要的是函数对外提供的API约定包含函数的名称、参数和返回值。当这些都确定之后如何精确计算参数和返回值的大小是第一个需要解决的问题。 对于函数来说最重要的是函数对外提供的API约定包含函数的名称、参数和返回值。当这些都确定之后如何精确计算参数和返回值的大小是第一个需要解决的问题。
@ -96,7 +96,7 @@ TEXT ·Swap(SB), $0
从代码可以看出a、b、ret0和ret1的内存地址是依次递增的FP伪寄存器是第一个变量的开始地址。 从代码可以看出a、b、ret0和ret1的内存地址是依次递增的FP伪寄存器是第一个变量的开始地址。
## 参数和返回值的内存布局 ## 3.4.3 参数和返回值的内存布局
如果是参数和返回值类型比较复杂的情况该如何处理呢?下面我们再尝试一个更复杂的函数参数和返回值的计算。比如有以下一个函数: 如果是参数和返回值类型比较复杂的情况该如何处理呢?下面我们再尝试一个更复杂的函数参数和返回值的计算。比如有以下一个函数:
@ -153,7 +153,7 @@ TEXT ·Foo(SB), $0
其中a和b参数之间出现了一个字节的空洞b和c之间出现了4个字节的空洞。出现空洞的原因是要包装每个参数变量地址都要对齐到相应的倍数。 其中a和b参数之间出现了一个字节的空洞b和c之间出现了4个字节的空洞。出现空洞的原因是要包装每个参数变量地址都要对齐到相应的倍数。
## 函数中的局部变量 ## 3.4.4 函数中的局部变量
从Go语言函数角度讲局部变量是函数内明确定义的变量同时也包含函数的参数和返回值变量。但是从Go汇编角度看局部变量是指函数运行时在当前函数栈帧所对应的内存内的变量不包含函数的参数和返回值因为访问方式有差异。函数栈帧的空间主要由函数参数和返回值、局部变量和被调用其它函数的参数和返回值空间组成。为了便于理解我们可以将汇编函数的局部变量类比为Go语言函数中显式定义的变量不包含参数和返回值部分。 从Go语言函数角度讲局部变量是函数内明确定义的变量同时也包含函数的参数和返回值变量。但是从Go汇编角度看局部变量是指函数运行时在当前函数栈帧所对应的内存内的变量不包含函数的参数和返回值因为访问方式有差异。函数栈帧的空间主要由函数参数和返回值、局部变量和被调用其它函数的参数和返回值空间组成。为了便于理解我们可以将汇编函数的局部变量类比为Go语言函数中显式定义的变量不包含参数和返回值部分。
@ -212,7 +212,7 @@ func Foo() {
从图中可以看出Foo函数局部变量和前一个例子中参数和返回值的内存布局是完全一样的这也是我们故意设计的结果。但是参数和返回值是通过伪FP寄存器定位的FP寄存器对应第一个参数的开始地址第一个参数地址较低因此每个变量的偏移量是正数。而局部变量是通过伪SP寄存器定位的而伪SP寄存器对应的是第一个局部变量的结束地址第一个局部变量地址较大因此每个局部变量的便宜量都是负数。 从图中可以看出Foo函数局部变量和前一个例子中参数和返回值的内存布局是完全一样的这也是我们故意设计的结果。但是参数和返回值是通过伪FP寄存器定位的FP寄存器对应第一个参数的开始地址第一个参数地址较低因此每个变量的偏移量是正数。而局部变量是通过伪SP寄存器定位的而伪SP寄存器对应的是第一个局部变量的结束地址第一个局部变量地址较大因此每个局部变量的便宜量都是负数。
## 调用其它函数 ## 3.4.5 调用其它函数
常见的用Go汇编实现的函数都是叶子函数也就是被其它函数调用的函数但是很少调用其它函数。这主要是因为叶子函数比较简单可以简化汇编函数的编写同时一般性能或特性的瓶颈也处于叶子函数。但是能够调用其它函数和能够被其它函数调用同样重要否则Go汇编就不是一个完整的汇编语言。 常见的用Go汇编实现的函数都是叶子函数也就是被其它函数调用的函数但是很少调用其它函数。这主要是因为叶子函数比较简单可以简化汇编函数的编写同时一般性能或特性的瓶颈也处于叶子函数。但是能够调用其它函数和能够被其它函数调用同样重要否则Go汇编就不是一个完整的汇编语言。
@ -245,7 +245,7 @@ func sum(a, b int) int {
Go语言中函数调用是一个复杂的问题因为Go函数不仅仅要了解函数调用参数的布局还会涉及到栈的跳转栈上局部变量的生命周期管理。本节只是简单了解函数调用参数的布局规则在后续的章节中会更详细的讨论函数的细节。 Go语言中函数调用是一个复杂的问题因为Go函数不仅仅要了解函数调用参数的布局还会涉及到栈的跳转栈上局部变量的生命周期管理。本节只是简单了解函数调用参数的布局规则在后续的章节中会更详细的讨论函数的细节。
## 宏函数 ## 3.4.6 宏函数
宏函数并不是Go汇编语言所定义而是Go汇编引入的预处理特性自带的特性。 宏函数并不是Go汇编语言所定义而是Go汇编引入的预处理特性自带的特性。

View File

@ -2,7 +2,7 @@
程序主要有顺序、分支和循环几种执行流程。本节主要讨论如何将Go语言的控制流比较直观地转译为汇编程序或者说如何以汇编思维来编写Go语言代码。 程序主要有顺序、分支和循环几种执行流程。本节主要讨论如何将Go语言的控制流比较直观地转译为汇编程序或者说如何以汇编思维来编写Go语言代码。
## 顺序执行 ## 3.5.1 顺序执行
顺序执行是我们比较熟悉的工作模式类似俗称流水账编程。所有不含分支、循环和goto语句并且没有递归调用的Go函数一般都是顺序执行的。 顺序执行是我们比较熟悉的工作模式类似俗称流水账编程。所有不含分支、循环和goto语句并且没有递归调用的Go函数一般都是顺序执行的。
@ -111,7 +111,7 @@ TEXT ·main(SB), $16-0
首先是将main函数的栈帧大小从24字节减少到16字节。唯一需要保存的是a变量的值因此在调用runtime·printint函数输出时全部的寄存器都可能被污染我们无法通过寄存器备份a变量的值只有在栈内存中的值才是安全的。然后在BX寄存器并不需要保存到内存。其它部分的代码基本保持不变。 首先是将main函数的栈帧大小从24字节减少到16字节。唯一需要保存的是a变量的值因此在调用runtime·printint函数输出时全部的寄存器都可能被污染我们无法通过寄存器备份a变量的值只有在栈内存中的值才是安全的。然后在BX寄存器并不需要保存到内存。其它部分的代码基本保持不变。
## if/goto跳转 ## 3.5.2 if/goto跳转
Go语言刚刚开源的时候并没有goto语句后来Go语言虽然增加了goto语句但是并不推荐在编程中使用。有一个和cgo类似的原则如果可以不使用goto语句那么就不要使用goto语句。Go语言中的goto语句是有严格限制的它无法跨越代码块并且在被跨越的代码中不能含有变量定义的语句。虽然Go语言不推荐goto语句但是goto确实每个汇编语言码农的最爱。因为goto近似等价于汇编语言中的无条件跳转指令JMP配合if条件goto就组成了有条件跳转指令而有条件跳转指令正是构建整个汇编代码控制流的基石。 Go语言刚刚开源的时候并没有goto语句后来Go语言虽然增加了goto语句但是并不推荐在编程中使用。有一个和cgo类似的原则如果可以不使用goto语句那么就不要使用goto语句。Go语言中的goto语句是有严格限制的它无法跨越代码块并且在被跨越的代码中不能含有变量定义的语句。虽然Go语言不推荐goto语句但是goto确实每个汇编语言码农的最爱。因为goto近似等价于汇编语言中的无条件跳转指令JMP配合if条件goto就组成了有条件跳转指令而有条件跳转指令正是构建整个汇编代码控制流的基石。
@ -162,7 +162,7 @@ L:
在跳转指令中跳转的目标一般是通过一个标号表示。不过在有些通过宏实现的函数中更希望通过相对位置跳转这时候可以通过PC寄存器的偏移量来计算临近跳转的位置。 在跳转指令中跳转的目标一般是通过一个标号表示。不过在有些通过宏实现的函数中更希望通过相对位置跳转这时候可以通过PC寄存器的偏移量来计算临近跳转的位置。
## for循环 ## 3.5.3 for循环
Go语言的for循环有多种用法我们这里只选择最经典的for结构来讨论。经典的for循环由初始化、结束条件、迭代步长三个部分组成再配合循环体内部的if条件语言这种for结构可以模拟其它各种循环类型。 Go语言的for循环有多种用法我们这里只选择最经典的for结构来讨论。经典的for循环由初始化、结束条件、迭代步长三个部分组成再配合循环体内部的if条件语言这种for结构可以模拟其它各种循环类型。

View File

@ -3,7 +3,7 @@
在前面的章节中我们已经简单讨论过Go的汇编函数但是那些主要是叶子函数。叶子函数的最大特点是不会调用其他函数也就是栈的大小是可以预期的叶子函数也就是可以基本忽略爆栈的问题如果已经爆了那也是上级函数的问题。如果没有爆栈问题那么也就是不会有栈的分裂问题如果没有栈的分裂也就不需要移动栈上的指针也就不会有栈上指针管理的问题。但是是现实中Go语言的函数是可以任意深度调用的永远不用担心爆栈的风险。那么这些近似黑科技的特性是如何通过低级的汇编语言实现的呢这些都是本节尝试讨论的问题。 在前面的章节中我们已经简单讨论过Go的汇编函数但是那些主要是叶子函数。叶子函数的最大特点是不会调用其他函数也就是栈的大小是可以预期的叶子函数也就是可以基本忽略爆栈的问题如果已经爆了那也是上级函数的问题。如果没有爆栈问题那么也就是不会有栈的分裂问题如果没有栈的分裂也就不需要移动栈上的指针也就不会有栈上指针管理的问题。但是是现实中Go语言的函数是可以任意深度调用的永远不用担心爆栈的风险。那么这些近似黑科技的特性是如何通过低级的汇编语言实现的呢这些都是本节尝试讨论的问题。
## 函数调用规范 ## 3.6.1 函数调用规范
在Go汇编语言中CALL指令用于调用函数RET指令用于从调用函数返回。但是CALL和RET指令并没有处理函数调用时输入参数和返回值的问题。CALL指令类似`PUSH IP``JMP somefunc`两个指令的组合首先将当前的IP指令寄存器的值压入栈中然后通过JMP指令将要调用函数的地址写入到IP寄存器实现跳转。而RET指令则是和CALL相反的操作基本和`POP IP`指令等价也就是将执行CALL指令时保存在SP中的返回地址重新载入到IP寄存器实现函数的返回。 在Go汇编语言中CALL指令用于调用函数RET指令用于从调用函数返回。但是CALL和RET指令并没有处理函数调用时输入参数和返回值的问题。CALL指令类似`PUSH IP``JMP somefunc`两个指令的组合首先将当前的IP指令寄存器的值压入栈中然后通过JMP指令将要调用函数的地址写入到IP寄存器实现跳转。而RET指令则是和CALL相反的操作基本和`POP IP`指令等价也就是将执行CALL指令时保存在SP中的返回地址重新载入到IP寄存器实现函数的返回。
@ -14,7 +14,7 @@
首先是调用函数前准备的输入参数和返回值空间。然后CALL指令将首先触发返回地址入栈操作。在进入到被调用函数内之后汇编器自动插入了BP寄存器相关的指令因此BP寄存器和返回地址是紧挨着的。再下面就是当前函数的局部变量的空间包含再次调用其它函数需要准备的调用参数空间。被调用的函数执行RET返回指令时先从栈恢复BP和SP寄存器接着取出的返回地址跳转到对应的指令执行。 首先是调用函数前准备的输入参数和返回值空间。然后CALL指令将首先触发返回地址入栈操作。在进入到被调用函数内之后汇编器自动插入了BP寄存器相关的指令因此BP寄存器和返回地址是紧挨着的。再下面就是当前函数的局部变量的空间包含再次调用其它函数需要准备的调用参数空间。被调用的函数执行RET返回指令时先从栈恢复BP和SP寄存器接着取出的返回地址跳转到对应的指令执行。
## 高级汇编语言 ## 3.6.2 高级汇编语言
Go汇编语言其实是一种高级的汇编语言。在这里高级一词并没有任何褒义或贬义的色彩而是要强调Go汇编代码和最终真实执行的代码并不完全等价。Go汇编语言中一个指令在最终的目标代码中可能会被编译为其它等价的机器指令。Go汇编实现的函数或调用函数的指令在最终代码中也会被插入额外的指令。要彻底理解Go汇编语言就需要彻底了解汇编器到底插入了哪些指令。 Go汇编语言其实是一种高级的汇编语言。在这里高级一词并没有任何褒义或贬义的色彩而是要强调Go汇编代码和最终真实执行的代码并不完全等价。Go汇编语言中一个指令在最终的目标代码中可能会被编译为其它等价的机器指令。Go汇编实现的函数或调用函数的指令在最终代码中也会被插入额外的指令。要彻底理解Go汇编语言就需要彻底了解汇编器到底插入了哪些指令。
@ -112,7 +112,7 @@ type stack struct {
以上是栈的扩容但是栈的收缩是在何时处理的呢我们知道Go运行时会定期进行垃圾回收操作这其中包含栈的回收工作。如果栈使用到比例小于一定到阈值则分配一个较小到栈空间然后将栈上面到数据移动到新的栈中栈移动的过程和栈扩容的过程类似。 以上是栈的扩容但是栈的收缩是在何时处理的呢我们知道Go运行时会定期进行垃圾回收操作这其中包含栈的回收工作。如果栈使用到比例小于一定到阈值则分配一个较小到栈空间然后将栈上面到数据移动到新的栈中栈移动的过程和栈扩容的过程类似。
## PCDATA和FUNCDATA ## 3.6.3 PCDATA和FUNCDATA
Go语言中有个runtime.Caller函数可以获取当前函数的调用者列表。我们可以非常容易在运行时定位每个函数的调用位置以及函数的调用链。因此在panic异常或用log输出信息时可以精确定位代码的位置。 Go语言中有个runtime.Caller函数可以获取当前函数的调用者列表。我们可以非常容易在运行时定位每个函数的调用位置以及函数的调用链。因此在panic异常或用log输出信息时可以精确定位代码的位置。
@ -168,7 +168,7 @@ PCDATA和FUNCDATA的数据一般是由编译器自动生成的手工编写并
对于PCDATA和FUNCDATA细节感兴趣的同学可以尝试从debug/gosym包入手参考包的实现和测试代码。 对于PCDATA和FUNCDATA细节感兴趣的同学可以尝试从debug/gosym包入手参考包的实现和测试代码。
## 方法函数 ## 3.6.4 方法函数
Go语言中方法函数和全局函数非常相似比如有以下的方法 Go语言中方法函数和全局函数非常相似比如有以下的方法
@ -209,7 +209,7 @@ func (p *MyInt) Ptr() *MyInt {
在最终的目标文件中的标识符名字中还有很多Go汇编语言不支持的特殊符号比如`type.string."hello"`中的双引号这导致了无法通过手写的汇编代码实现全部的特性。或许是Go语言官方故意限制了汇编语言的特性。 在最终的目标文件中的标识符名字中还有很多Go汇编语言不支持的特殊符号比如`type.string."hello"`中的双引号这导致了无法通过手写的汇编代码实现全部的特性。或许是Go语言官方故意限制了汇编语言的特性。
## 递归函数: 1到n求和 ## 3.6.5 递归函数: 1到n求和
递归函数是比较特殊的函数递归函数通过调用自身并且在栈上保存状态这可以简化很多问题的处理。Go语言中递归函数的强大之处是不用担心爆栈问题因为栈可以根据需要进行扩容和收缩。 递归函数是比较特殊的函数递归函数通过调用自身并且在栈上保存状态这可以简化很多问题的处理。Go语言中递归函数的强大之处是不用担心爆栈问题因为栈可以根据需要进行扩容和收缩。

View File

@ -2,7 +2,7 @@
汇编语言的真正威力来自两个维度一是突破框架限制实现看似不可能的任务二是突破指令限制通过高级指令挖掘极致的性能。对于第一个问题我们将演示如何通过Go汇编语言直接访问系统调用和直接调用C语言函数。对于第二个问题我们将演示X64指令中AVX等高级指令的简单用法。 汇编语言的真正威力来自两个维度一是突破框架限制实现看似不可能的任务二是突破指令限制通过高级指令挖掘极致的性能。对于第一个问题我们将演示如何通过Go汇编语言直接访问系统调用和直接调用C语言函数。对于第二个问题我们将演示X64指令中AVX等高级指令的简单用法。
## 系统调用 ## 3.7.1 系统调用
系统调用是操作系统为外提供的公共接口。因为操作系统彻底接管了各种底层硬件设备因此操作系统提供的系统调用成了实现某些操作的唯一方法。从另一个角度看系统调用更像是一个RPC远程过程调用不过信道是寄存器和内存。在系统调用时我们向操作系统发送调用的编号和对应的参数然后阻塞等待系统调用地返回。因为涉及到阻塞等待因此系统调用期间的CPU利用率一般是可以忽略的。另一个和RPC地远程调用类似的地方是操作系统内核处理系统调用时不会依赖用户的栈空间一般不会导致爆栈发生。因此系统调用是最简单安全的一种调用了。 系统调用是操作系统为外提供的公共接口。因为操作系统彻底接管了各种底层硬件设备因此操作系统提供的系统调用成了实现某些操作的唯一方法。从另一个角度看系统调用更像是一个RPC远程过程调用不过信道是寄存器和内存。在系统调用时我们向操作系统发送调用的编号和对应的参数然后阻塞等待系统调用地返回。因为涉及到阻塞等待因此系统调用期间的CPU利用率一般是可以忽略的。另一个和RPC地远程调用类似的地方是操作系统内核处理系统调用时不会依赖用户的栈空间一般不会导致爆栈发生。因此系统调用是最简单安全的一种调用了。
@ -54,7 +54,7 @@ func main() {
如果是Linux系统只需要将编号改为write系统调用对应的1即可。而Windows的系统调用则有另外的参数传输规则。在X64环境Windows的系统调用参数传输规则和默认的C语言规则非常相似在后续的直接调用C函数部分再行讨论。 如果是Linux系统只需要将编号改为write系统调用对应的1即可。而Windows的系统调用则有另外的参数传输规则。在X64环境Windows的系统调用参数传输规则和默认的C语言规则非常相似在后续的直接调用C函数部分再行讨论。
## 直接调用C函数 ## 3.7.2 直接调用C函数
在计算机的发展的过程中C语言和UNIX操作系统有着不可替代的作用。因此操作系统的系统调用、汇编语言和C语言函数调用规则几个技术是密切相关的。 在计算机的发展的过程中C语言和UNIX操作系统有着不可替代的作用。因此操作系统的系统调用、汇编语言和C语言函数调用规则几个技术是密切相关的。
@ -129,7 +129,7 @@ func main() {
在上面的代码中,通过`C.myadd`获取C函数的地址然后转换为合适的类型再传人asmCallCAdd函数。在这个例子中汇编函数假设调用的C语言函数需要的栈很小可以直接复用Go函数中多余的空间。如果C语言函数可能需要较大的栈可以尝试像CGO那样切换到系统线程的栈上运行。 在上面的代码中,通过`C.myadd`获取C函数的地址然后转换为合适的类型再传人asmCallCAdd函数。在这个例子中汇编函数假设调用的C语言函数需要的栈很小可以直接复用Go函数中多余的空间。如果C语言函数可能需要较大的栈可以尝试像CGO那样切换到系统线程的栈上运行。
## AVX指令 ## 3.7.3 AVX指令
从Go1.11开始Go汇编语言引入了AVX512指令的支持。AVX指令集是属于Intel家的SIMD指令集中的一部分。AVX512的最大特点是数据有512位宽度可以一次计算8个64位数或者是等大小的数据。因此AVX指令可以用于优化矩阵或图像等并行度很高的算法。不过并不是每个X86体系的CPU都支持了AVX指令因此首要的任务是如何判断CPU支持了哪些高级指令。 从Go1.11开始Go汇编语言引入了AVX512指令的支持。AVX指令集是属于Intel家的SIMD指令集中的一部分。AVX512的最大特点是数据有512位宽度可以一次计算8个64位数或者是等大小的数据。因此AVX指令可以用于优化矩阵或图像等并行度很高的算法。不过并不是每个X86体系的CPU都支持了AVX指令因此首要的任务是如何判断CPU支持了哪些高级指令。

View File

@ -2,11 +2,11 @@
在操作系统中每个进程都会有一个唯一的进程编号每个线程也有自己唯一的线程编号。同样在Go语言中每个Goroutine也有自己唯一的Go程编号这个编号在panic等场景下经常遇到。虽然Goroutine有内在的编号但是Go语言却刻意没有提供获取该编号的接口。本节我们尝试通过Go汇编语言获取Goroutine ID。 在操作系统中每个进程都会有一个唯一的进程编号每个线程也有自己唯一的线程编号。同样在Go语言中每个Goroutine也有自己唯一的Go程编号这个编号在panic等场景下经常遇到。虽然Goroutine有内在的编号但是Go语言却刻意没有提供获取该编号的接口。本节我们尝试通过Go汇编语言获取Goroutine ID。
## 故意设计没有goid ## 3.8.1 故意设计没有goid
根据官方的相关资料显示Go语言刻意没有提供goid的原因是为了避免被滥用。因为大部分用户在轻松拿到goid之后在之后的编程中会不自觉地编写出强依赖goid的代码。强依赖goid将导致这些代码不好移植同时也会导致并发模型复杂化。同时Go语言中可能同时存在海量的Goroutine但是每个Goroutine何时被销毁并不好实时监控这也会导致依赖goid的资源无法很好地自动回收需要手工回收。不过如果你是Go汇编语言用户则完全可以忽略这些借口。 根据官方的相关资料显示Go语言刻意没有提供goid的原因是为了避免被滥用。因为大部分用户在轻松拿到goid之后在之后的编程中会不自觉地编写出强依赖goid的代码。强依赖goid将导致这些代码不好移植同时也会导致并发模型复杂化。同时Go语言中可能同时存在海量的Goroutine但是每个Goroutine何时被销毁并不好实时监控这也会导致依赖goid的资源无法很好地自动回收需要手工回收。不过如果你是Go汇编语言用户则完全可以忽略这些借口。
## 纯Go方式获取goid ## 3.8.2 纯Go方式获取goid
为了便于理解我们先尝试用纯Go的方式获取goid。使用纯Go的方式获取goid的方式虽然性能较低但是代码有着很好的移植性同时也可以用于测试验证其它方式获取的goid是否正确。 为了便于理解我们先尝试用纯Go的方式获取goid。使用纯Go的方式获取goid的方式虽然性能较低但是代码有着很好的移植性同时也可以用于测试验证其它方式获取的goid是否正确。
@ -79,7 +79,7 @@ func GetGoid() int64 {
GetGoid函数的细节我们不再赘述。需要补充说明的是`runtime.Stack`函数不仅仅可以获取当前Goroutine的栈信息还可以获取全部Goroutine的栈信息通过第二个参数控制。同时在Go语言内部的 [net/http2.curGoroutineID](https://github.com/golang/net/blob/master/http2/gotrack.go) 函数正是采用类似方式获取的goid。 GetGoid函数的细节我们不再赘述。需要补充说明的是`runtime.Stack`函数不仅仅可以获取当前Goroutine的栈信息还可以获取全部Goroutine的栈信息通过第二个参数控制。同时在Go语言内部的 [net/http2.curGoroutineID](https://github.com/golang/net/blob/master/http2/gotrack.go) 函数正是采用类似方式获取的goid。
## 从g结构体获取goid ## 3.8.3 从g结构体获取goid
根据官方的Go汇编语言文档每个运行的Goroutine结构的g指针保存在当前运行Goroutine的系统线程的局部存储TLS中。可以先获取TLS线程局部存储然后再从TLS中获取g结构的指针最后从g结构中取出goid。 根据官方的Go汇编语言文档每个运行的Goroutine结构的g指针保存在当前运行Goroutine的系统线程的局部存储TLS中。可以先获取TLS线程局部存储然后再从TLS中获取g结构的指针最后从g结构中取出goid。
@ -163,7 +163,7 @@ var g_goid_offset = func() int64 {
现在的goid偏移量已经终于可以自动适配已经发布的Go语言版本。 现在的goid偏移量已经终于可以自动适配已经发布的Go语言版本。
## 获取g结构体对应的接口对象 ## 3.8.4 获取g结构体对应的接口对象
枚举和暴力穷举虽然够直接但是对于正在开发中的未发布的Go版本支持并不好我们无法提前知晓开发中的某个版本的goid成员的偏移量。 枚举和暴力穷举虽然够直接但是对于正在开发中的未发布的Go版本支持并不好我们无法提前知晓开发中的某个版本的goid成员的偏移量。
@ -270,7 +270,7 @@ TEXT ·getg(SB), NOSPLIT, $32-16
其中NO_LOCAL_POINTERS表示函数没有局部指针变量。同时对返回的接口进行零值初始化初始化完成后通过GO_RESULTS_INITIALIZED告知GC。这样可以在保证栈分裂时GC能够正确处理返回值和局部变量中的指针。 其中NO_LOCAL_POINTERS表示函数没有局部指针变量。同时对返回的接口进行零值初始化初始化完成后通过GO_RESULTS_INITIALIZED告知GC。这样可以在保证栈分裂时GC能够正确处理返回值和局部变量中的指针。
## goid的应用: 局部存储 ## 3.8.5 goid的应用: 局部存储
有了goid之后构造Goroutine局部存储就非常容易了。我们可以定义一个gls包提供goid的特性 有了goid之后构造Goroutine局部存储就非常容易了。我们可以定义一个gls包提供goid的特性

View File

@ -2,7 +2,7 @@
目前Go语言支持GDB、LLDB和Delve几种调试器。其中GDB是最早支持的调试工具LLDB是macOS系统推荐的标准调试工具。但是GDB和LLDB对Go语言的专有特性都缺乏很大支持而只有Delve是专门为Go语言设计开发的调试工具。而且Delve本身也是采用Go语言开发对Windows平台也提供了一样的支持。本节我们基于Delve简单解释如何调试Go汇编程序。 目前Go语言支持GDB、LLDB和Delve几种调试器。其中GDB是最早支持的调试工具LLDB是macOS系统推荐的标准调试工具。但是GDB和LLDB对Go语言的专有特性都缺乏很大支持而只有Delve是专门为Go语言设计开发的调试工具。而且Delve本身也是采用Go语言开发对Windows平台也提供了一样的支持。本节我们基于Delve简单解释如何调试Go汇编程序。
## Delve入门 ## 3.9.1 Delve入门
首先根据官方的文档正确安装Delve调试器。我们会先构造一个简单的Go语言代码用于熟悉下Delve的简单用法。 首先根据官方的文档正确安装Delve调试器。我们会先构造一个简单的Go语言代码用于熟悉下Delve的简单用法。
@ -246,7 +246,7 @@ Goroutine 1:
最后完成调试工作后输入quit命令退出调试器。至此我们已经掌握了Delve调试器器的简单用法。 最后完成调试工作后输入quit命令退出调试器。至此我们已经掌握了Delve调试器器的简单用法。
## 调试汇编程序 ## 3.9.2 调试汇编程序
用Delve调试Go汇编程序的过程比调试Go语言程序更加简单。调试汇编程序时我们需要时刻关注寄存器的状态如果涉及函数调用或局部变量或参数还需要重点关注栈寄存器SP的状态。 用Delve调试Go汇编程序的过程比调试Go语言程序更加简单。调试汇编程序时我们需要时刻关注寄存器的状态如果涉及函数调用或局部变量或参数还需要重点关注栈寄存器SP的状态。