diff --git a/ch3-asm/ch3-01-basic.md b/ch3-asm/ch3-01-basic.md index 03f0a22..35f4f22 100644 --- a/ch3-asm/ch3-01-basic.md +++ b/ch3-asm/ch3-01-basic.md @@ -4,13 +4,13 @@ ## 实现和声明 -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文件。 ## 定义整数变量 为了简单,我们先用Go语言定义并赋值一个整数变量,然后查看生成的汇编代码。 -创建pkg.go文件,内容如下: +首先创建一个pkg.go文件,内容如下: ```go package pkg @@ -18,7 +18,7 @@ package pkg var Id = 9527 ``` -然后用以下命令查看的Go语言程序对应的伪汇编代码: +代码中只定义了一个int类型的包级变量,并进行了初始化。然后用以下命令查看的Go语言程序对应的伪汇编代码: ``` $ go tool compile -S pkg.go @@ -26,19 +26,21 @@ $ go tool compile -S pkg.go 0x0000 37 25 00 00 00 00 00 00 '....... ``` -输出的汇编比较简单,其中`"".Id`对应Id变量符号,变量的内存大小为8个字节。变量的初始化内容为`37 25 00 00 00 00 00 00`,对应十六进制格式的0x2537,对应十进制为9527。SNOPTRDATA是相关的标志,暂时忽略。 +其中`go tool compile`命令用于调用Go语言提供的底层命令工具,其中`-S`参数表示输出汇编格式。输出的汇编比较简单,其中`"".Id`对应Id变量符号,变量的内存大小为8个字节。变量的初始化内容为`37 25 00 00 00 00 00 00`,对应十六进制格式的0x2537,对应十进制为9527。SNOPTRDATA是相关的标志,其中NOPTR表示数据中不包含指针数据。 -以上的内容只是目标文件对应的汇编,和Go汇编语言虽然相似当并不完全等价。Go语言官网自带了一个Go汇编语言的入门教程,地址在:https://golang.org/doc/asm +暂时可以忽略。 -Go汇编语言提供了DATA命令用于初始化变量,DATA命令的语法如下: +以上的内容只是目标文件对应的汇编,和Go汇编语言虽然相似当并不完全等价。Go语言官网自带了一个Go汇编语言的入门教程,地址在:https://golang.org/doc/asm 。 + +Go汇编语言提供了DATA命令用于初始化包变量,DATA命令的语法如下: ``` DATA symbol+offset(SB)/width, value ``` -其中symbol为变量在汇编语言中对应的符号,offset是符号开始地址的偏移量,width是要初始化内存的宽度大小,value是要初始化的值。其中当前包中Go语言定义的符号symbol,在汇编代码中对应`·symbol`,其中·为一个特殊的unicode符号。 +其中symbol为变量在汇编语言中对应的标识符,offset是符号开始地址的偏移量,width是要初始化内存的宽度大小,value是要初始化的值。其中当前包中Go语言定义的符号symbol,在汇编代码中对应`·symbol`,其中“·”中点符号为一个特殊的unicode符号。 -采用以下命令可以给Id变量初始化为十六进制的0x2537,对应十进制的9527,常量需要以美元符号$开头表示: +我们采用以下命令可以给Id变量初始化为十六进制的0x2537,对应十进制的9527(常量需要以美元符号$开头表示): ``` DATA ·Id+0(SB)/1,$0x37 @@ -67,9 +69,9 @@ package pkg var Id int ``` -表示声明一个int类型的Id变量。因为该变量已经在汇编中定义,因此Go语言部分只是声明变量,声明的变量不能含义初始化的操作。 +现状Go语言的代码不再是定义一个变量,语义变成了声明一个变量(声明一个变量时不能再进行初始化操作)。而Id变量的定义工作已经在汇编语言中完成了。 -完整的汇编代码在pkg_amd64.s中: +我们将完整的汇编代码放到pkg_amd64.s文件中: ``` GLOBL ·Id(SB),$8 @@ -84,9 +86,9 @@ DATA ·Id+6(SB)/1,$0x00 DATA ·Id+7(SB)/1,$0x00 ``` -文件名pkg_amd64.s表示为AMD64环境下的汇编代码文件。 +文件名pkg_amd64.s的后缀名表示AMD64环境下的汇编代码文件。 -虽然pkg包改用汇编实现,但是用法和之前完全一样: +虽然pkg包是用汇编实现,但是用法和之前的Go语言版本完全一样: ```go package main @@ -98,16 +100,13 @@ func main() { } ``` -对于Go包的用户来说,用Go汇编语言或Go语言实现并无区别。 +对于Go包的用户来说,用Go汇编语言或Go语言实现并无任何区别。 ## 定义字符串变量 -在前一个例子中,我们通过汇编定义了一个整数变量。现在我们尝试通过汇编定义一个字符串变量。 +在前一个例子中,我们通过汇编定义了一个整数变量。现在我们提高一点难度,尝试通过汇编定义一个字符串变量。虽然从Go语言角度看,定义字符串和整数变量的写法基本相同,但是字符串底层却有着比单个整数更复杂的数据结构。 -虽然从Go语言角度看,定义字符串和整数变量的写法基本相同,但是字符串底层却有着比单个整数更复杂的数据结构。 - - -创建pkg.go文件,内容如下: +实验的流程和前面的例子一样,还是先用Go语言实现类似的功能,然后观察分析生成的汇编代码,最后用Go汇编语言仿写。首先创建pkg.go文件,用Go语言定义字符串: ```go package pkg @@ -126,9 +125,9 @@ go.string."gopher" SRODATA dupok size=6 rel 0+8 t=1 go.string."gopher"+0 ``` -输出中出现了一个新的符号go.string."gopher",根据其长度和内容分析可以猜测是对应底层的"gopher"字符串数据。因为Go语言的字符串并不是值类型,Go字符串只是一种只读的引用类型。假设多个代码中出现了相同的"gopher"字符串时,程序链接后其实都是引用的同一个符号go.string."gopher"。因此,该符号有一个SRODATA标志表示这个数据在只读内存段,dupok表示出现多个相同符号时只保留一个就可以了。 +输出中出现了一个新的符号go.string."gopher",根据其长度和内容分析可以猜测是对应底层的"gopher"字符串数据。因为Go语言的字符串并不是值类型,Go字符串其实是一种只读的引用类型。如果多个代码中出现了相同的"gopher"只读字符串时,程序链接后可以引用的同一个符号go.string."gopher"。因此,该符号有一个SRODATA标志表示这个数据在只读内存段,dupok表示出现多个相同标识符的数据时只保留一个就可以了。 -而真正的Go字符串变量Name对应的大小却只有16个字节了。其实Name变量并没有直接对应“gopher”字符串,而是对应reflect.StringHeader结构体: +而真正的Go字符串变量Name对应的大小却只有16个字节了。其实Name变量并没有直接对应“gopher”字符串,而是对应16字节大小的reflect.StringHeader结构体: ```go type reflect.StringHeader struct { @@ -139,7 +138,7 @@ type reflect.StringHeader struct { 从汇编角度看,Name变量其实对应的是reflect.StringHeader结构体类型。前8个字节对应底层真实字符串数据的指针,也就是符号go.string."gopher"对应的地址。后8个字节对应底层真实字符串数据的有效长度,这里是6个字节。 -创建pkg_amd64.s文件,我们尝试通过汇编代码重新定义并初始化Name字符串: +现在创建pkg_amd64.s文件,尝试通过汇编代码重新定义并初始化Name字符串: ``` GLOBL ·NameData(SB),$8 @@ -150,11 +149,9 @@ DATA ·Name+0(SB)/8,$·NameData(SB) DATA ·Name+8(SB)/8,$6 ``` -因为在Go汇编语言中,go.string."gopher"不是一个合法的符号,我们无法手工创建(这是给编译器保留的部分特权,因为手工创建类似符号可能打破编译器输出代码的某些规则)。因此我们新创建了一个·NameData符号表示底层的字符串数据。 +因为在Go汇编语言中,go.string."gopher"不是一个合法的符号,因此我们无法通过手工创建(这是给编译器保留的部分特权,因为手工创建类似符号可能打破编译器输出代码的某些规则)。因此我们新创建了一个·NameData符号表示底层的字符串数据。然后定义·Name符号内存大小为16字节,其中前8个字节用·NameData符号对应的地址初始化,后8个字节为常量6表示字符串长度。 -然后定义·Name符号内存大小为16字节,其中前8个字节用·NameData符号对应的地址初始化,后8个字节为常量6表示字符串长度。 - -通过以下代码测试输出Name变量: +当用汇编定义好字符串变量并导出之后,还需要在Go语言中声明该字符串变量。然后就可以用Go语言代码测试Name变量了: ```go package main @@ -166,15 +163,15 @@ func main() { } ``` -在运行时将会产生类似以下错误: +不幸的是这次运行产生了以下错误: ``` pkgpath.NameData: missing Go //type information for global symbol: size 8 ``` -提示汇编中定义的NameData符号没有类型信息。其实Go汇编语言中定义的数据并没有所谓的类型,每个符号只不过是对应一块内存而已。出现这种错误的原因是,Go语言的垃圾回收器在扫描NameData变量的时候,无法知晓该变量内部是否包含指针。因此,真正错误的原因并不是NameData没有类型,而是NameData变量没有标注是否会含有指针信息。 +错误提示汇编中定义的NameData符号没有类型信息。其实Go汇编语言中定义的数据并没有所谓的类型,每个符号只不过是对应一块内存而已,因此NameData符号也是没有类型的。但是Go语言是再带垃圾回收器的语言,而Go汇编语言是工作在自动垃圾回收体系框架内的。档Go语言的垃圾回收器在扫描到NameData变量的时候,无法知晓该变量内部是否包含指针,因此就出现了这种错误。错误的根本原因并不是NameData没有类型,而是NameData变量没有标注是否会含有指针信息。 -通过给NameData变量增加一个标志,表示其中不会包含指针数据可以修复该错误: +通过给NameData变量增加一个NOPTR标志,表示其中不会包含指针数据可以修复该错误: ``` #include "textflag.h" @@ -182,9 +179,7 @@ pkgpath.NameData: missing Go //type information for global symbol: size 8 GLOBL ·NameData(SB),NOPTR,$8 ``` -通过给·NameData增加NOPTR,表示其中不含指针数据。那么垃圾回收器在遇到该变量的时候就会停止内部数据的扫描。 - -我们也可以通过给·NameData变量在Go语言中增加一个不含指针并且大小为8个字节的类型来修改该错误: +通过给·NameData增加NOPTR标志档方式表示其中不含指针数据。我们也可以通过给·NameData变量在Go语言中增加一个不含指针并且大小为8个字节的类型来修改该错误: ```go package pkg @@ -193,7 +188,7 @@ var NameData [8]byte var Name string ``` -我们将NameData声明为长度为8的字节数组。编译器可以通过类型分析出该变量不会包含指针,因此汇编代码中可以省略NOPTR标志。 +我们将NameData声明为长度为8的字节数组。编译器可以通过类型分析出该变量不会包含指针,因此汇编代码中可以省略NOPTR标志。现在垃圾回收器在遇到该变量的时候就会停止内部数据的扫描。 在这个实现中,Name字符串底层其实引用的是NameData内存对应的“gopher”字符串数据。因此,如果NameData发生变化,Name字符串的数据也会跟着变化。 @@ -208,7 +203,7 @@ func main() { 当然这和字符串的只读定义是冲突的,正常的代码需要避免出现这种情况。最好的方法是不要导出内部的NameData变量,这样可以避免内部数据被无意破坏。 -在用汇编定义字符串时,我们完全换一种思维:将底层的字符串数据和字符串头结构体定义在一起,这样可以避免引入NameData符号: +在用汇编定义字符串时我们剋月换一种思维:将底层的字符串数据和字符串头结构体定义在一起,这样可以避免引入NameData符号: ``` GLOBL ·Name(SB),$24 @@ -218,13 +213,17 @@ DATA ·Name+8(SB)/8,$6 DATA ·Name+16(SB)/8,$"gopher" ``` -在新的结构中,Name符号对应的内存从16字节变为24字节,多出的8个字节存放底层的“gopher”字符串。·Name符号前16个字节依然对应reflect.StringHeader结构体:Data部分对应`$·Name+16(SB)`,表示数据的地址为Name符号往后偏移16个字节的位置;Len部分依然对应6个字节的长度。 +在新的结构中,Name符号对应的内存从16字节变为24字节,多出的8个字节存放底层的“gopher”字符串。·Name符号前16个字节依然对应reflect.StringHeader结构体:Data部分对应`$·Name+16(SB)`,表示数据的地址为Name符号往后偏移16个字节的位置;Len部分依然对应6个字节的长度。这是C语言程序员经常使用档技巧。 ## 定义main函数 前面的例子已经展示了如何通过汇编定义整型和字符串类型变量。我们现在将尝试用汇编实现函数,然后输出一个字符串。 + + 先创建main.go文件,创建并初始化字符串变量,同时声明main函数: ```go diff --git a/ch3-asm/readme.md b/ch3-asm/readme.md index d33e4c5..202d669 100644 --- a/ch3-asm/readme.md +++ b/ch3-asm/readme.md @@ -4,7 +4,7 @@ Go语言中很多设计思想和工具都是传承自Plan9操作系统,Go汇 无论高级语言如何发展,作为最接近CPU的汇编语言的地方依然是无法彻底被替代的。只有通过汇编语言才能彻底挖掘CPU芯片的全部功能,因此操作系统的引导过程必须要依赖汇编语言的帮助。只有通过汇编语言才能彻底榨干CPU芯片的性能,因此很多底层的加密解密等对性能敏感的算法会考虑通过汇编语言进行性能优化。 -对于每一个严肃的Gopher,Go汇编语言都是一个不可忽视的技术。因为哪怕只懂一点点汇编,也便于更好地理解计算机,将更容易理解Go语言中动态栈/接口等高级特性的实现原理。而且掌握了Go汇编语言之后,你将不用担心再被其它所谓的任何高级编程语言用户鄙视。 +对于每一个严肃的Gopher,Go汇编语言都是一个不可忽视的技术。因为哪怕只懂一点点汇编,也便于更好地理解计算机原理,也更容易理解Go语言中动态栈/接口等高级特性的实现原理。而且掌握了Go汇编语言之后,你将重新站在编程语言鄙视链的顶端,不用担心再被任何其它所谓的高级编程语言用户鄙视。 本章我们将以AMD64为主要开发环境,简单地探讨Go汇编语言的基础用法。