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

ch3-03: 重构内容

This commit is contained in:
chai2010 2018-07-21 15:21:39 +08:00
parent 2f4409fcfd
commit 7eb2a08651

View File

@ -48,10 +48,7 @@ DATA ·Name+8(SB)/8,$6
从Go汇编语言角度来看全局变量和局部变量有着非常大的差异。在Go汇编中全局变量和全局函数更为相似都是通过一个人为定义的符号来引用对应的内存区别只是内存中存放是数据还是要执行的指令。因为在冯诺伊曼系统结构的计算机中指令也是数据而且指令和数据存放在统一编址的内存中。因为指令和数据并没有本质的差别因此我们甚至可以像操作数据那样动态生成指令这是所有JIT技术的原理。而局部变量则需在了解了汇编函数之后才能通过SP栈空间来隐式定义。 从Go汇编语言角度来看全局变量和局部变量有着非常大的差异。在Go汇编中全局变量和全局函数更为相似都是通过一个人为定义的符号来引用对应的内存区别只是内存中存放是数据还是要执行的指令。因为在冯诺伊曼系统结构的计算机中指令也是数据而且指令和数据存放在统一编址的内存中。因为指令和数据并没有本质的差别因此我们甚至可以像操作数据那样动态生成指令这是所有JIT技术的原理。而局部变量则需在了解了汇编函数之后才能通过SP栈空间来隐式定义。
![](../images/ch3-pkg-var-decl-01.ditaa.png) 在Go汇编语言中内存是通过SB伪寄存器定位。SB是Static base pointer的缩写意为静态内存的开始地址。我们可以将SB想象为一个和内容容量有相同大小的字节数组所有的静态全局符号通常可以通过SB加一个偏移量定位而我们定义的符号其实就是相对于SB内存开始地址偏移量。对于SB伪寄存器全局变量和全局函数的符号并没有任何区别。
在Go汇编语言中内存是通过SB伪寄存器定位。SB是Static base pointer的缩写意为静态内存的开始地址。所有的静态全局符号通常可以通过SB加一个偏移量定位而我们定义的符号其实就是相对于SB内存开始地址偏移量。对于SB伪寄存器全局变量和全局函数的符号并没有任何区别。
要定义全局变量,首先要声明一个变量对应的符号,以及变量对应的内存大小。导出变量符号的语法如下: 要定义全局变量,首先要声明一个变量对应的符号,以及变量对应的内存大小。导出变量符号的语法如下:
@ -65,7 +62,7 @@ GLOBL汇编指令用于定义名为symbol的变量变量对应的内存宽度
GLOBL ·count(SB),$4 GLOBL ·count(SB),$4
``` ```
其中符号`·count`以中点开头表示是当前包的变量,最终符号名为被展开为`path/to/pkg.count`。count变量的大小是4个字节常量必须以$美元符号开头。内存的宽度必须是2的指数倍编译器最终会保证变量的真实地址对齐到机器字宽度。需要注意的是在Go汇编中我们无法为count变量指定具体的类型。在汇编中定义全局变量时我们只关心变量的名字和内存大小变量最终的类型只能在Go语言中声明。 其中符号`·count`以中点开头表示是当前包的变量,最终符号名为被展开为`path/to/pkg.count`。count变量的大小是4个字节常量必须以$美元符号开头。内存的宽度必须是2的指数倍编译器最终会保证变量的真实地址对齐到机器字倍数。需要注意的是在Go汇编中我们无法为count变量指定具体的类型。在汇编中定义全局变量时我们只关心变量的名字和内存大小变量最终的类型只能在Go语言中声明。
变量定义之后我们可以通过DATA汇编指令指定对应内存中的数据语法如下 变量定义之后我们可以通过DATA汇编指令指定对应内存中的数据语法如下
@ -92,23 +89,33 @@ DATA ·count+0(SB)/4,$0x01020304
最后还需要在Go语言中声明对应的变量和C语言头文件声明变量的作用类似这样垃圾回收器会根据变量的类型来管理其中的指针相关的内存数据。 最后还需要在Go语言中声明对应的变量和C语言头文件声明变量的作用类似这样垃圾回收器会根据变量的类型来管理其中的指针相关的内存数据。
## 变量的布局
### 数组类型
汇编中数组也是一种非常简单的类型。Go语言中数组是一种有着扁平内存结构的基础类型。因此`[2]byte`类型和`[1]uint16`类型有着相同的内存结构。只有当数组和结构体结合之后情况才会变的稍微复杂。
下面我们尝试用汇编定义一个`[2]int`类型的数组变量num
```go ```go
var num [2]int var num [2]int
``` ```
![](../images/ch3-pkg-var-decl-02.ditaa.png) 然后在汇编中定义一个对应16字节大小的变量并用零值进行初始化
```go ```
var pt image.Point GLOBL ·num(SB),$16
DATA ·num+0(SB)/8,$0
DATA ·num+8(SB)/8,$0
``` ```
![](../images/ch3-pkg-var-decl-03.ditaa.png) 下图是Go语句和汇编语句定义变量时的对应关系
<!-- TODO --> ![](../images/ch3-pkg-var-decl-01.ditaa.png)
## bool型变量 汇编代码中并不需要NOPTR标志因为Go编译器会从Go语言语句声明的`[2]int`类型中推导出该变量内部没有指针数据。
### bool型变量
Go汇编语言定义变量无法指定类型信息因此需要先通过Go语言声明变量的类型。以下是在Go语言中声明的几个bool类型变量 Go汇编语言定义变量无法指定类型信息因此需要先通过Go语言声明变量的类型。以下是在Go语言中声明的几个bool类型变量
@ -132,9 +139,9 @@ GLOBL ·falseValue(SB),$1 // var falseValue = true
DATA ·falseValue(SB)/1,$0 DATA ·falseValue(SB)/1,$0
``` ```
bool类型的内存大小为1个字节。并且汇编中定义的变量需要手工指定初始化值否则将可能导致产生未初始化的变量。 bool类型的内存大小为1个字节。并且汇编中定义的变量需要手工指定初始化值否则将可能导致产生未初始化的变量。当需要将1个字节的bool类型变量加载到8字节的寄存器时需要使用MOVBQZX指令将不足的高位用0填充。
## int型变量 ### int型变量
所有的整数类型均有类似的定义的方式比较大的差异是整数类型的内存大小和整数是否是有符号。下面是声明的int32和uint32类型变量 所有的整数类型均有类似的定义的方式比较大的差异是整数类型的内存大小和整数是否是有符号。下面是声明的int32和uint32类型变量
@ -156,9 +163,9 @@ GLOBL ·uint32Value(SB),$4
DATA ·uint32Value(SB)/4,$0x01020304 // 第1-4字节 DATA ·uint32Value(SB)/4,$0x01020304 // 第1-4字节
``` ```
汇编定义变量时并不区分整数是否有符号。 汇编定义变量时初始化数据并不区分整数是否有符号。只有在CPU指令处理该寄存器数据时才会根据指令的类型来取分数据的类型或者是否带有符号位。
## float型变量 ### float型变量
Go汇编语言通常无法区分变量是否是浮点数类型与之相关的浮点数机器指令会将变量当作浮点数处理。Go语言的浮点数遵循IEEE754标准有float32单精度浮点数和float64双精度浮点数之分。 Go汇编语言通常无法区分变量是否是浮点数类型与之相关的浮点数机器指令会将变量当作浮点数处理。Go语言的浮点数遵循IEEE754标准有float32单精度浮点数和float64双精度浮点数之分。
@ -166,9 +173,9 @@ IEEE754标准中最高位1bit为符号位然后是指数位指数为采
![](../images/ch3-03-ieee754.jpg) ![](../images/ch3-03-ieee754.jpg)
IEEE754浮点数还有一些奇妙的特性比如有正负两个0除了无穷大和无穷小Inf还有非数NaN同时如果两个浮点数有序那么bit对应的整数也是有序的。 IEEE754浮点数还有一些奇妙的特性比如有正负两个0除了无穷大和无穷小Inf还有非数NaN同时如果两个浮点数有序那么对应的有符号整数也是有序的(反之则不一定成立,因为浮点数中存在的非数是不可排序的)。浮点数是程序中最难琢磨的角落,因为程序中很多手写的浮点数字面值常量根本无法精确表达,浮点数计算涉及到的误差舍入方式可能也的随机的。
下面是在Go语言中声明两个浮点数(如果没有在汇编中定义变量,那么声明的同时也会定义变量)。 下面是在Go语言中声明两个浮点数如果没有在汇编中定义变量那么声明的同时也会定义变量
```go ```go
var float32Value float32 var float32Value float32
@ -176,7 +183,7 @@ var float32Value float32
var float64Value float64 var float64Value float64
``` ```
然后在汇编中定义并初始化浮点数: 然后在汇编中定义并初始化上面声明的两个浮点数:
``` ```
GLOBL ·float32Value(SB),$4 GLOBL ·float32Value(SB),$4
@ -186,9 +193,9 @@ GLOBL ·float64Value(SB),$8
DATA ·float64Value(SB)/4,$0x01020304 // bit 方式初始化 DATA ·float64Value(SB)/4,$0x01020304 // bit 方式初始化
``` ```
我们在上一节精简的算术指令中都是针对整数,如果要通过整数指令处理浮点数的加减法必须根据浮点数的运算规则进行:先对齐小数点,然后进行整数加减法,最后再对结果进行归一化并处理精度舍入问题。 我们在上一节精简的算术指令中都是针对整数,如果要通过整数指令处理浮点数的加减法必须根据浮点数的运算规则进行:先对齐小数点,然后进行整数加减法,最后再对结果进行归一化并处理精度舍入问题。不过在目前的主流CPU中都提针对浮点数提供了专有的计算指令。
## string类型变量 ### string类型变量
从Go汇编语言角度看字符串只是一种结构体。string的头结构定义如下 从Go汇编语言角度看字符串只是一种结构体。string的头结构定义如下
@ -219,7 +226,7 @@ DATA text<>+8(SB)/8,$"rld!"
虽然text私有变量表示的字符串只有12个字符长度但是我们依然需要将变量的长度扩展为2的指数倍数这里也就是16个字节的长度。 虽然text私有变量表示的字符串只有12个字符长度但是我们依然需要将变量的长度扩展为2的指数倍数这里也就是16个字节的长度。
然后使用text私有变量对应的内存地址来初始化字符串头结构体中的Data部分并且手工指定Len部分为字符串的长度 然后使用text私有变量对应的内存地址对应的常量来初始化字符串头结构体中的Data部分并且手工指定Len部分为字符串的长度
``` ```
DATA ·helloworld+0(SB)/8,$text<>(SB) // StringHeader.Data DATA ·helloworld+0(SB)/8,$text<>(SB) // StringHeader.Data
@ -228,7 +235,7 @@ DATA ·helloworld+8(SB)/8,$12 // StringHeader.Len
需要注意的是,字符串是只读类型,要避免在汇编中直接修改字符串底层数据的内容。 需要注意的是,字符串是只读类型,要避免在汇编中直接修改字符串底层数据的内容。
## slice类型变量 ### slice类型变量
slice变量和string变量相似只不过是对应的是切片头结构体而已。切片头的结构如下 slice变量和string变量相似只不过是对应的是切片头结构体而已。切片头的结构如下
@ -259,7 +266,7 @@ DATA text<>+8(SB)/8,$"rld!" // ...string data...
因为切片和字符串的相容性我们可以将切片头的前16个字节临时作为字符串使用这样可以省去不必要的转换。 因为切片和字符串的相容性我们可以将切片头的前16个字节临时作为字符串使用这样可以省去不必要的转换。
## map/channel类型变量 ### map/channel类型变量
map/channel等类型并没有公开的内部结构它们只是一种未知类型的指针无法直接初始化。在汇编代码中我们只能为类似变量定义并进行0值初始化 map/channel等类型并没有公开的内部结构它们只是一种未知类型的指针无法直接初始化。在汇编代码中我们只能为类似变量定义并进行0值初始化
@ -284,9 +291,27 @@ func makemap(mapType *byte, hint int, mapbuf *any) (hmap map[any]any)
func makechan(chanType *byte, size int) (hchan chan any) func makechan(chanType *byte, size int) (hchan chan any)
``` ```
需要注意的是makemap函数可以创建不同类型的mapmap的具体类型是通过mapType参数指定。 需要注意的是makemap是一种范型函数可以创建不同类型的mapmap的具体类型是通过mapType参数指定。
## 变量的内存布局
我们已经多次强调在Go汇编语言中变量是没有类型的。因此在Go语言中有着不同类型的变量底层可能对应的是相同的内存结构。深刻理解每个变量的内存布局是汇编编程时的必备条件。
首先查看前面已经见过的`[2]int`类型数组的内存布局:
![](../images/ch3-pkg-var-decl-02.ditaa.png)
变量在data段分配空间数组的元素地址依次从低向高排列。
然后再查看下标准库图像包中`image.Point`结构体类型变量的内存布局:
![](../images/ch3-pkg-var-decl-03.ditaa.png)
变量也时在data段分配空间变量结构体成员的地址也是依次从低向高排列。
因此`[2]int``image.Point`类型底层有着近似相同的内存布局。
## 标识符规则和特殊标志 ## 标识符规则和特殊标志
Go语言的标识符可以由绝对的包路径加标识符本身定位因此不同包中的标识符即使同名也不会有问题。Go汇编是通过特殊的符号来表示斜杠和点符号因为这样可以简化汇编器词法扫描部分代码的编写只要通过字符串替换就可以了。 Go语言的标识符可以由绝对的包路径加标识符本身定位因此不同包中的标识符即使同名也不会有问题。Go汇编是通过特殊的符号来表示斜杠和点符号因为这样可以简化汇编器词法扫描部分代码的编写只要通过字符串替换就可以了。
@ -309,7 +334,7 @@ GLOBL file_private<>(SB),$1
此外Go汇编语言还在"textflag.h"文件定义了一些标志。其中用于变量的标志有DUPOK、RODATA和NOPTR几个。DUPOK表示该变量对应的标识符可能有多个在链接时只选择其中一个即可一般用于合并相同的常量字符串减少重复数据占用的空间。RODATA标志表示将变量定义在只读内存段因此后续任何对此变量的修改操作将导致异常panic也无法捕获。NOPTR则表示此变量的内部不含指针数据让垃圾回收器忽略对该变量的扫描。如果变量已经在Go代码中声明过的话Go编译器会自动分析出该变量是否包含指针这种时候可以不用手写NOPTR标志。 此外Go汇编语言还在"textflag.h"文件定义了一些标志。其中用于变量的标志有DUPOK、RODATA和NOPTR几个。DUPOK表示该变量对应的标识符可能有多个在链接时只选择其中一个即可一般用于合并相同的常量字符串减少重复数据占用的空间。RODATA标志表示将变量定义在只读内存段因此后续任何对此变量的修改操作将导致异常panic也无法捕获。NOPTR则表示此变量的内部不含指针数据让垃圾回收器忽略对该变量的扫描。如果变量已经在Go代码中声明过的话Go编译器会自动分析出该变量是否包含指针这种时候可以不用手写NOPTR标志。
下面是通过汇编来定义一个只读的int类型的变量 比如下面的例子是通过汇编来定义一个只读的int类型的变量
```go ```go
var const_id int // readonly var const_id int // readonly
@ -324,9 +349,9 @@ DATA ·const_id+0(SB)/8,$9527
我们使用#include语句包含定义标志的"textflag.h"头文件和C语言中预处理相同。然后GLOBL汇编命令在定义变量时给变量增加了NOPTR和RODATA两个标志多个标志之间采用竖杠分割表示变量中没有指针数据同时定义在只读代码段。 我们使用#include语句包含定义标志的"textflag.h"头文件和C语言中预处理相同。然后GLOBL汇编命令在定义变量时给变量增加了NOPTR和RODATA两个标志多个标志之间采用竖杠分割表示变量中没有指针数据同时定义在只读代码段。
变量一般可取地址的值但是const_id虽然可以取地址但是确实不能修改。不能修改的限制并不是由编译器提供而是因为对该变量的修改会导致对只读内存段进行写导致从而导致异常。 变量一般也叫可取地址的值但是const_id虽然可以取地址但是确实不能修改。不能修改的限制并不是由编译器提供而是因为对该变量的修改会导致对只读内存段进行写导致从而导致异常。
## 小结 ## 小结
以上我们初步演示了通过汇编定义全局变量的用法。但是实际中我们并不推荐通过汇编定义变量——因为用Go语言定义变量更加简单。在Go语言中定义变量编译器可以帮助我们计算好变量的大小生成变量的初始值同时也包含了足够的类型信息。汇编语言的优势是挖掘机器的特性和性能用汇编定义变量则无法发挥这些优势。因此在理解了汇编定义变量的用法后建议大家谨慎使用。 以上我们初步展示了通过汇编定义全局变量的用法。但是真实的环境中我们并不推荐通过汇编定义变量——因为用Go语言定义变量更加简单和安全。在Go语言中定义变量编译器可以帮助我们计算好变量的大小生成变量的初始值同时也包含了足够的类型信息。汇编语言的优势是挖掘机器的特性和性能用汇编定义变量则无法发挥这些优势。因此在理解了汇编定义变量的用法后建议大家谨慎使用。