From bfb993cdf97e3f6ef8147579962c6eeef616fa53 Mon Sep 17 00:00:00 2001 From: chai2010 Date: Sun, 5 Aug 2018 08:20:00 +0800 Subject: [PATCH] =?UTF-8?q?ch3:=20=E5=AE=8C=E5=96=84=E5=86=85=E5=AE=B9?= =?UTF-8?q?=E7=BC=96=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ch3-asm/ch3-01-basic.md | 12 ++++++------ ch3-asm/ch3-02-arch.md | 10 +++++----- ch3-asm/ch3-03-const-and-var.md | 24 ++++++++++++------------ ch3-asm/ch3-04-func.md | 12 ++++++------ ch3-asm/ch3-05-control-flow.md | 6 +++--- ch3-asm/ch3-06-func-again.md | 10 +++++----- ch3-asm/ch3-07-hack-asm.md | 6 +++--- ch3-asm/ch3-08-goroutine-id.md | 10 +++++----- ch3-asm/ch3-09-debug.md | 4 ++-- 9 files changed, 47 insertions(+), 47 deletions(-) diff --git a/ch3-asm/ch3-01-basic.md b/ch3-asm/ch3-01-basic.md index 035be83..ed537bb 100644 --- a/ch3-asm/ch3-01-basic.md +++ b/ch3-asm/ch3-01-basic.md @@ -2,11 +2,11 @@ Go汇编程序始终是幽灵一样的存在。我们将通过分析简单的Go程序输出的汇编代码,然后照猫画虎用汇编实现一个简单的输出程序。 -## 实现和声明 +## 3.1.1 实现和声明 Go汇编语言并不是一个独立的语言,因为Go汇编程序无法独立使用。Go汇编代码必须以Go包的方式组织,同时包中至少要有一个Go语言文件用于指明当前包名等基本包信息。如果Go汇编代码中定义的变量和函数要被其它Go语言代码引用,还需要通过Go语言代码将汇编中定义的符号声明出来。用于变量的定义和函数的定义Go汇编文件类似于C语言中的.c文件,而用于导出汇编中定义符号的Go源文件类似于C语言的.h文件。 -## 定义整数变量 +## 3.1.2 定义整数变量 为了简单,我们先用Go语言定义并赋值一个整数变量,然后查看生成的汇编代码。 @@ -100,7 +100,7 @@ func main() { 对于Go包的用户来说,用Go汇编语言或Go语言实现并无任何区别。 -## 定义字符串变量 +## 3.1.3 定义字符串变量 在前一个例子中,我们通过汇编定义了一个整数变量。现在我们提高一点难度,尝试通过汇编定义一个字符串变量。虽然从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语言程序员经常使用档技巧。 -## 定义main函数 +## 3.1.4 定义main函数 前面的例子已经展示了如何通过汇编定义整型和字符串类型变量。我们现在将尝试用汇编实现函数,然后输出一个字符串。 @@ -244,7 +244,7 @@ TEXT ·main(SB), $16-0 Go语言函数在函数调用时,完全通过栈传递调用参数和返回值。先通过MOVQ指令,将helloworld对应的字符串头部结构体的16个字节复制到栈指针SP对应的16字节的空间,然后通过CALL指令调用对应函数。最后使用RET指令表示当前函数返回。 -## 特殊字符 +## 3.1.5 特殊字符 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输入法可能是最安全可靠等输入方式。 -## 没有分号 +## 3.1.6 没有分号 Go汇编语言中分号可以用于分隔同一行内的多个语句。下面是用分号混乱排版的汇编代码: diff --git a/ch3-asm/ch3-02-arch.md b/ch3-asm/ch3-02-arch.md index 9381f38..9532e63 100644 --- a/ch3-asm/ch3-02-arch.md +++ b/ch3-asm/ch3-02-arch.md @@ -5,7 +5,7 @@ 汇编语言其实是一种非常简单的编程语言,因为它面向的计算机模型就是非常简单的。让人觉得汇编语言难学主要有几个原因:不同类型的CPU都有自己的一套指令;即使是相同的CPU,32位和64位的运行模式依然会有差异;不同的汇编工具同样有自己特有的汇编指令;不同的操作系统和高级编程语言和底层汇编的调用规范并不相同。本节将描述几个有趣的汇编语言模型,最后精简出一个适用于AMD64架构的精简指令集,以便于Go汇编语言的学习。 -## 图灵机和BF语言 +## 3.2.1 图灵机和BF语言 图灵机是由图灵提出的一种抽象计算模型。机器有一条无限长的纸带,纸带分成了一个一个的小方格,每个方格有不同的颜色,这类似于计算机中的内存。同时机器有一个探头在纸带上移来移去,类似于通过内存地址来读写内存上的数据。机器头有一组内部计算状态,还有一些固定的程序(更像一个哈佛结构)。在每个时刻,机器头都要从当前纸带上读入一个方格信息,然后根据自己的内部状态和当前要执行的程序指令将信息输出到纸带方格上,同时更新自己的内部状态并进行移动。 @@ -34,7 +34,7 @@ 理论上我们可以将BF语言当作目标机器语言,将其它高级语言编译为BF语言后就可以在BF机器上运行了。 -## 人力资源机器游戏 +## 3.2.2 人力资源机器游戏 《人力资源机器》(Human Resource Machine)是一款设计精良汇编语言编程游戏。在游戏中,玩家扮演一个职员角色,来模拟人力资源机器的运行。通过完成上司给的每一份任务来实现晋升的目标,完成任务的途径就是用游戏提供的11个机器指令编写正确的汇编程序,最终得到正确的输出结果。人力资源机器的汇编语言可以认为是跨平台、跨操作系统的通用的汇编语言,因为在macOS、Windows、Linux和iOS上该游戏的玩法都是完全一致的。 @@ -80,7 +80,7 @@ LOOP: 首先通过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环境。 @@ -95,7 +95,7 @@ X86其实是是80X86的简称(后面三个字母),包括Intel 8086、80286 右边是X86的指令集。CPU是由指令和寄存器组成,指令是每个CPU内置的算法,指令处理的对象就是全部的寄存器和内存。我们可以将每个指令看作是CPU内置标准库中提供的一个个函数,然后基于这些函数构造更复杂的程序的过程就是用汇编语言编程的过程。 -## Go汇编中的伪寄存器 +## 3.2.4 Go汇编中的伪寄存器 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)`有标识符为前缀表示伪寄存器。 -## X86-64指令集 +## 3.2.5 X86-64指令集 很多汇编语言的教程都会强调汇编语言是不可移植的。严格来说汇编语言是在不同的CPU类型、或不同的操作系统环境、或不同的汇编工具链下是不可移植的,而在同一种CPU中运行的机器指令是完全一样的。汇编语言这种不可移植性正是其普及的一个极大的障碍。虽然CPU指令集的差异是导致不好移植的较大因素,但是汇编语言的相关工具链对此也有不可推卸的责任。而源自Plan9的Go汇编语言对此做了一定的改进:首先Go汇编语言在相同CPU架构上是完全一致的,也就是屏蔽了操作系统的差异;同时Go汇编语言将一些基础并且类似的指令抽象为相同名字的伪指令,从而减少不同CPU架构下汇编代码的差异(寄存器名字和数量的差异是一直存在的)。本节的目的也是找出一个较小的精简指令集,以简化Go汇编语言的学习。 diff --git a/ch3-asm/ch3-03-const-and-var.md b/ch3-asm/ch3-03-const-and-var.md index e4d56dc..43f0dab 100644 --- a/ch3-asm/ch3-03-const-and-var.md +++ b/ch3-asm/ch3-03-const-and-var.md @@ -2,7 +2,7 @@ 程序中的一切变量的初始值都直接或间接地依赖常量或常量表达式生成。在Go语言中很多变量是默认零值初始化的,但是Go汇编中定义的变量最好还是手工通过常量初始化。有了常量之后,就可以衍生定义全局变量,并使用常量组成的表达式初始化其它各种变量。本节将简单讨论Go汇编语言中常量和全局变量的用法。 -## 常量 +## 3.3.1 常量 Go汇编语言中常量以$美元符号为前缀。常量的类型有整数常量、浮点数常量、字符常量和字符串常量等几种类型。以下是几种类型常量的例子: @@ -42,7 +42,7 @@ DATA ·Name+8(SB)/8,$6 其中`$·NameData(SB)`也是以$美元符号为前缀,因此也可以将它看作是一个常量,它对应的是NameData包变量的地址。在汇编指令中,我们也可以通过LEA指令来获取NameData变量的地址。 -## 全局变量 +## 3.3.2 全局变量 在Go语言中,变量根据作用域和生命周期有全局变量和局部变量之分。全局变量是包一级的变量,全局变量一般有着较为固定的内存地址,声明周期跨越整个程序运行时间。而局部变量一般是函数内定义的的变量,只有在函数被执行的时间才被在栈上创建,当函数调用完成后将回收(暂时不考虑闭包对局部变量捕获的问题)。 @@ -90,7 +90,7 @@ DATA ·count+0(SB)/4,$0x01020304 最后还需要在Go语言中声明对应的变量(和C语言头文件声明变量的作用类似),这样垃圾回收器会根据变量的类型来管理其中的指针相关的内存数据。 -### 数组类型 +### 3.3.2.1 数组类型 汇编中数组也是一种非常简单的类型。Go语言中数组是一种有着扁平内存结构的基础类型。因此`[2]byte`类型和`[1]uint16`类型有着相同的内存结构。只有当数组和结构体结合之后情况才会变的稍微复杂。 @@ -115,7 +115,7 @@ DATA ·num+8(SB)/8,$0 汇编代码中并不需要NOPTR标志,因为Go编译器会从Go语言语句声明的`[2]int`类型中推导出该变量内部没有指针数据。 -### bool型变量 +### 3.3.2.2 bool型变量 Go汇编语言定义变量无法指定类型信息,因此需要先通过Go语言声明变量的类型。以下是在Go语言中声明的几个bool类型变量: @@ -141,7 +141,7 @@ DATA ·falseValue(SB)/1,$0 bool类型的内存大小为1个字节。并且汇编中定义的变量需要手工指定初始化值,否则将可能导致产生未初始化的变量。当需要将1个字节的bool类型变量加载到8字节的寄存器时,需要使用MOVBQZX指令将不足的高位用0填充。 -### int型变量 +### 3.3.2.3 int型变量 所有的整数类型均有类似的定义的方式,比较大的差异是整数类型的内存大小和整数是否是有符号。下面是声明的int32和uint32类型变量: @@ -165,7 +165,7 @@ DATA ·uint32Value(SB)/4,$0x01020304 // 第1-4字节 汇编定义变量时初始化数据并不区分整数是否有符号。只有在CPU指令处理该寄存器数据时,才会根据指令的类型来取分数据的类型或者是否带有符号位。 -### float型变量 +### 3.3.2.4 float型变量 Go汇编语言通常无法区分变量是否是浮点数类型,与之相关的浮点数机器指令会将变量当作浮点数处理。Go语言的浮点数遵循IEEE754标准,有float32单精度浮点数和float64双精度浮点数之分。 @@ -195,7 +195,7 @@ DATA ·float64Value(SB)/4,$0x01020304 // bit 方式初始化 我们在上一节精简的算术指令中都是针对整数,如果要通过整数指令处理浮点数的加减法必须根据浮点数的运算规则进行:先对齐小数点,然后进行整数加减法,最后再对结果进行归一化并处理精度舍入问题。不过在目前的主流CPU中,都提针对浮点数提供了专有的计算指令。 -### string类型变量 +### 3.3.2.5 string类型变量 从Go汇编语言角度看,字符串只是一种结构体。string的头结构定义如下: @@ -235,7 +235,7 @@ DATA ·helloworld+8(SB)/8,$12 // StringHeader.Len 需要注意的是,字符串是只读类型,要避免在汇编中直接修改字符串底层数据的内容。 -### slice类型变量 +### 3.3.2.6 slice类型变量 slice变量和string变量相似,只不过是对应的是切片头结构体而已。切片头的结构如下: @@ -266,7 +266,7 @@ DATA text<>+8(SB)/8,$"rld!" // ...string data... 因为切片和字符串的相容性,我们可以将切片头的前16个字节临时作为字符串使用,这样可以省去不必要的转换。 -### map/channel类型变量 +### 3.3.2.7 map/channel类型变量 map/channel等类型并没有公开的内部结构,它们只是一种未知类型的指针,无法直接初始化。在汇编代码中我们只能为类似变量定义并进行0值初始化: @@ -294,7 +294,7 @@ func makechan(chanType *byte, size int) (hchan chan any) 需要注意的是,makemap是一种范型函数,可以创建不同类型的map,map的具体类型是通过mapType参数指定。 -## 变量的内存布局 +## 3.3.3 变量的内存布局 我们已经多次强调,在Go汇编语言中变量是没有类型的。因此在Go语言中有着不同类型的变量,底层可能对应的是相同的内存结构。深刻理解每个变量的内存布局是汇编编程时的必备条件。 @@ -312,7 +312,7 @@ func makechan(chanType *byte, size int) (hchan chan any) 因此`[2]int`和`image.Point`类型底层有着近似相同的内存布局。 -## 标识符规则和特殊标志 +## 3.3.4 标识符规则和特殊标志 Go语言的标识符可以由绝对的包路径加标识符本身定位,因此不同包中的标识符即使同名也不会有问题。Go汇编是通过特殊的符号来表示斜杠和点符号,因为这样可以简化汇编器词法扫描部分代码的编写,只要通过字符串替换就可以了。 @@ -351,7 +351,7 @@ DATA ·const_id+0(SB)/8,$9527 变量一般也叫可取地址的值,但是const_id虽然可以取地址,但是确实不能修改。不能修改的限制并不是由编译器提供,而是因为对该变量的修改会导致对只读内存段进行写导致,从而导致异常。 -## 小结 +## 3.3.5 小结 以上我们初步展示了通过汇编定义全局变量的用法。但是真实的环境中我们并不推荐通过汇编定义变量——因为用Go语言定义变量更加简单和安全。在Go语言中定义变量,编译器可以帮助我们计算好变量的大小,生成变量的初始值,同时也包含了足够的类型信息。汇编语言的优势是挖掘机器的特性和性能,用汇编定义变量则无法发挥这些优势。因此在理解了汇编定义变量的用法后,建议大家谨慎使用。 diff --git a/ch3-asm/ch3-04-func.md b/ch3-asm/ch3-04-func.md index 7a13860..c969f64 100644 --- a/ch3-asm/ch3-04-func.md +++ b/ch3-asm/ch3-04-func.md @@ -2,7 +2,7 @@ 终于到函数了!因为Go汇编语言中,可以也建议通过Go语言来定义全局变量,那么剩下的也就是函数了。只有掌握了汇编函数的基本用法,才能真正算是Go汇编语言入门。本章将简单讨论Go汇编中函数的定义和用法。 -## 基本语法 +## 3.4.1 基本语法 函数标识符通过TEXT汇编指令定义,表示该行开始的指令定义在TEXT内存段。TEXT语句后的指令一般对应函数的实现,但是对于TEXT指令本身来说并不关心后面是否有指令。因此TEXT和LABEL定义的符号是类似的,区别只是LABEL是用于跳转标号,但是本质上他们都是通过标识符映射一个内存地址。 @@ -56,7 +56,7 @@ func Swap() (a []int, d int) 对于汇编函数来说,只要是函数的名字和参数大小一致就可以是相同的函数了。而且在Go汇编语言中,输入参数和返回值参数是没有任何的区别的。 -## 函数参数和返回值 +## 3.4.2 函数参数和返回值 对于函数来说,最重要的是函数对外提供的API约定,包含函数的名称、参数和返回值。当这些都确定之后,如何精确计算参数和返回值的大小是第一个需要解决的问题。 @@ -96,7 +96,7 @@ TEXT ·Swap(SB), $0 从代码可以看出a、b、ret0和ret1的内存地址是依次递增的,FP伪寄存器是第一个变量的开始地址。 -## 参数和返回值的内存布局 +## 3.4.3 参数和返回值的内存布局 如果是参数和返回值类型比较复杂的情况该如何处理呢?下面我们再尝试一个更复杂的函数参数和返回值的计算。比如有以下一个函数: @@ -153,7 +153,7 @@ TEXT ·Foo(SB), $0 其中a和b参数之间出现了一个字节的空洞,b和c之间出现了4个字节的空洞。出现空洞的原因是要包装每个参数变量地址都要对齐到相应的倍数。 -## 函数中的局部变量 +## 3.4.4 函数中的局部变量 从Go语言函数角度讲,局部变量是函数内明确定义的变量,同时也包含函数的参数和返回值变量。但是从Go汇编角度看,局部变量是指函数运行时,在当前函数栈帧所对应的内存内的变量,不包含函数的参数和返回值(因为访问方式有差异)。函数栈帧的空间主要由函数参数和返回值、局部变量和被调用其它函数的参数和返回值空间组成。为了便于理解,我们可以将汇编函数的局部变量类比为Go语言函数中显式定义的变量,不包含参数和返回值部分。 @@ -212,7 +212,7 @@ func Foo() { 从图中可以看出Foo函数局部变量和前一个例子中参数和返回值的内存布局是完全一样的,这也是我们故意设计的结果。但是参数和返回值是通过伪FP寄存器定位的,FP寄存器对应第一个参数的开始地址(第一个参数地址较低),因此每个变量的偏移量是正数。而局部变量是通过伪SP寄存器定位的,而伪SP寄存器对应的是第一个局部变量的结束地址(第一个局部变量地址较大),因此每个局部变量的便宜量都是负数。 -## 调用其它函数 +## 3.4.5 调用其它函数 常见的用Go汇编实现的函数都是叶子函数,也就是被其它函数调用的函数,但是很少调用其它函数。这主要是因为叶子函数比较简单,可以简化汇编函数的编写;同时一般性能或特性的瓶颈也处于叶子函数。但是能够调用其它函数和能够被其它函数调用同样重要,否则Go汇编就不是一个完整的汇编语言。 @@ -245,7 +245,7 @@ func sum(a, b int) int { Go语言中函数调用是一个复杂的问题,因为Go函数不仅仅要了解函数调用参数的布局,还会涉及到栈的跳转,栈上局部变量的生命周期管理。本节只是简单了解函数调用参数的布局规则,在后续的章节中会更详细的讨论函数的细节。 -## 宏函数 +## 3.4.6 宏函数 宏函数并不是Go汇编语言所定义,而是Go汇编引入的预处理特性自带的特性。 diff --git a/ch3-asm/ch3-05-control-flow.md b/ch3-asm/ch3-05-control-flow.md index 4e5ac3f..b4d1bd9 100644 --- a/ch3-asm/ch3-05-control-flow.md +++ b/ch3-asm/ch3-05-control-flow.md @@ -2,7 +2,7 @@ 程序主要有顺序、分支和循环几种执行流程。本节主要讨论如何将Go语言的控制流比较直观地转译为汇编程序,或者说如何以汇编思维来编写Go语言代码。 -## 顺序执行 +## 3.5.1 顺序执行 顺序执行是我们比较熟悉的工作模式,类似俗称流水账编程。所有不含分支、循环和goto语句,并且没有递归调用的Go函数一般都是顺序执行的。 @@ -111,7 +111,7 @@ TEXT ·main(SB), $16-0 首先是将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就组成了有条件跳转指令,而有条件跳转指令正是构建整个汇编代码控制流的基石。 @@ -162,7 +162,7 @@ L: 在跳转指令中,跳转的目标一般是通过一个标号表示。不过在有些通过宏实现的函数中,更希望通过相对位置跳转,这时候可以通过PC寄存器的偏移量来计算临近跳转的位置。 -## for循环 +## 3.5.3 for循环 Go语言的for循环有多种用法,我们这里只选择最经典的for结构来讨论。经典的for循环由初始化、结束条件、迭代步长三个部分组成,再配合循环体内部的if条件语言,这种for结构可以模拟其它各种循环类型。 diff --git a/ch3-asm/ch3-06-func-again.md b/ch3-asm/ch3-06-func-again.md index 4ed92a6..156c5fb 100644 --- a/ch3-asm/ch3-06-func-again.md +++ b/ch3-asm/ch3-06-func-again.md @@ -3,7 +3,7 @@ 在前面的章节中我们已经简单讨论过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寄存器,实现函数的返回。 @@ -14,7 +14,7 @@ 首先是调用函数前准备的输入参数和返回值空间。然后CALL指令将首先触发返回地址入栈操作。在进入到被调用函数内之后,汇编器自动插入了BP寄存器相关的指令,因此BP寄存器和返回地址是紧挨着的。再下面就是当前函数的局部变量的空间,包含再次调用其它函数需要准备的调用参数空间。被调用的函数执行RET返回指令时,先从栈恢复BP和SP寄存器,接着取出的返回地址跳转到对应的指令执行。 -## 高级汇编语言 +## 3.6.2 高级汇编语言 Go汇编语言其实是一种高级的汇编语言。在这里高级一词并没有任何褒义或贬义的色彩,而是要强调Go汇编代码和最终真实执行的代码并不完全等价。Go汇编语言中一个指令在最终的目标代码中可能会被编译为其它等价的机器指令。Go汇编实现的函数或调用函数的指令在最终代码中也会被插入额外的指令。要彻底理解Go汇编语言就需要彻底了解汇编器到底插入了哪些指令。 @@ -112,7 +112,7 @@ type stack struct { 以上是栈的扩容,但是栈的收缩是在何时处理的呢?我们知道Go运行时会定期进行垃圾回收操作,这其中包含栈的回收工作。如果栈使用到比例小于一定到阈值,则分配一个较小到栈空间,然后将栈上面到数据移动到新的栈中,栈移动的过程和栈扩容的过程类似。 -## PCDATA和FUNCDATA +## 3.6.3 PCDATA和FUNCDATA Go语言中有个runtime.Caller函数可以获取当前函数的调用者列表。我们可以非常容易在运行时定位每个函数的调用位置,以及函数的调用链。因此在panic异常或用log输出信息时,可以精确定位代码的位置。 @@ -168,7 +168,7 @@ PCDATA和FUNCDATA的数据一般是由编译器自动生成的,手工编写并 对于PCDATA和FUNCDATA细节感兴趣的同学可以尝试从debug/gosym包入手,参考包的实现和测试代码。 -## 方法函数 +## 3.6.4 方法函数 Go语言中方法函数和全局函数非常相似,比如有以下的方法: @@ -209,7 +209,7 @@ func (p *MyInt) Ptr() *MyInt { 在最终的目标文件中的标识符名字中还有很多Go汇编语言不支持的特殊符号(比如`type.string."hello"`中的双引号),这导致了无法通过手写的汇编代码实现全部的特性。或许是Go语言官方故意限制了汇编语言的特性。 -## 递归函数: 1到n求和 +## 3.6.5 递归函数: 1到n求和 递归函数是比较特殊的函数,递归函数通过调用自身并且在栈上保存状态,这可以简化很多问题的处理。Go语言中递归函数的强大之处是不用担心爆栈问题,因为栈可以根据需要进行扩容和收缩。 diff --git a/ch3-asm/ch3-07-hack-asm.md b/ch3-asm/ch3-07-hack-asm.md index 5564556..05f1aa7 100644 --- a/ch3-asm/ch3-07-hack-asm.md +++ b/ch3-asm/ch3-07-hack-asm.md @@ -2,7 +2,7 @@ 汇编语言的真正威力来自两个维度:一是突破框架限制,实现看似不可能的任务;二是突破指令限制,通过高级指令挖掘极致的性能。对于第一个问题,我们将演示如何通过Go汇编语言直接访问系统调用,和直接调用C语言函数。对于第二个问题,我们将演示X64指令中AVX等高级指令的简单用法。 -## 系统调用 +## 3.7.1 系统调用 系统调用是操作系统为外提供的公共接口。因为操作系统彻底接管了各种底层硬件设备,因此操作系统提供的系统调用成了实现某些操作的唯一方法。从另一个角度看,系统调用更像是一个RPC远程过程调用,不过信道是寄存器和内存。在系统调用时,我们向操作系统发送调用的编号和对应的参数,然后阻塞等待系统调用地返回。因为涉及到阻塞等待,因此系统调用期间的CPU利用率一般是可以忽略的。另一个和RPC地远程调用类似的地方是,操作系统内核处理系统调用时不会依赖用户的栈空间,一般不会导致爆栈发生。因此系统调用是最简单安全的一种调用了。 @@ -54,7 +54,7 @@ func main() { 如果是Linux系统,只需要将编号改为write系统调用对应的1即可。而Windows的系统调用则有另外的参数传输规则。在X64环境Windows的系统调用参数传输规则和默认的C语言规则非常相似,在后续的直接调用C函数部分再行讨论。 -## 直接调用C函数 +## 3.7.2 直接调用C函数 在计算机的发展的过程中,C语言和UNIX操作系统有着不可替代的作用。因此操作系统的系统调用、汇编语言和C语言函数调用规则几个技术是密切相关的。 @@ -129,7 +129,7 @@ func main() { 在上面的代码中,通过`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支持了哪些高级指令。 diff --git a/ch3-asm/ch3-08-goroutine-id.md b/ch3-asm/ch3-08-goroutine-id.md index 50360a6..2be476e 100644 --- a/ch3-asm/ch3-08-goroutine-id.md +++ b/ch3-asm/ch3-08-goroutine-id.md @@ -2,11 +2,11 @@ 在操作系统中,每个进程都会有一个唯一的进程编号,每个线程也有自己唯一的线程编号。同样在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 +## 3.8.2 纯Go方式获取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。 -## 从g结构体获取goid +## 3.8.3 从g结构体获取goid 根据官方的Go汇编语言文档,每个运行的Goroutine结构的g指针保存在当前运行Goroutine的系统线程的局部存储TLS中。可以先获取TLS线程局部存储,然后再从TLS中获取g结构的指针,最后从g结构中取出goid。 @@ -163,7 +163,7 @@ var g_goid_offset = func() int64 { 现在的goid偏移量已经终于可以自动适配已经发布的Go语言版本。 -## 获取g结构体对应的接口对象 +## 3.8.4 获取g结构体对应的接口对象 枚举和暴力穷举虽然够直接,但是对于正在开发中的未发布的Go版本支持并不好,我们无法提前知晓开发中的某个版本的goid成员的偏移量。 @@ -270,7 +270,7 @@ TEXT ·getg(SB), NOSPLIT, $32-16 其中NO_LOCAL_POINTERS表示函数没有局部指针变量。同时对返回的接口进行零值初始化,初始化完成后通过GO_RESULTS_INITIALIZED告知GC。这样可以在保证栈分裂时,GC能够正确处理返回值和局部变量中的指针。 -## goid的应用: 局部存储 +## 3.8.5 goid的应用: 局部存储 有了goid之后,构造Goroutine局部存储就非常容易了。我们可以定义一个gls包提供goid的特性: diff --git a/ch3-asm/ch3-09-debug.md b/ch3-asm/ch3-09-debug.md index b483088..0e52a28 100644 --- a/ch3-asm/ch3-09-debug.md +++ b/ch3-asm/ch3-09-debug.md @@ -2,7 +2,7 @@ 目前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的简单用法。 @@ -246,7 +246,7 @@ Goroutine 1: 最后完成调试工作后输入quit命令退出调试器。至此我们已经掌握了Delve调试器器的简单用法。 -## 调试汇编程序 +## 3.9.2 调试汇编程序 用Delve调试Go汇编程序的过程比调试Go语言程序更加简单。调试汇编程序时,我们需要时刻关注寄存器的状态,如果涉及函数调用或局部变量或参数还需要重点关注栈寄存器SP的状态。