mirror of
https://github.com/chai2010/advanced-go-programming-book.git
synced 2025-05-24 12:32:21 +00:00
ch03-1: done
This commit is contained in:
parent
537decf3bd
commit
f26f0f255a
@ -23,7 +23,7 @@
|
||||
* [2.11. 编译和链接参数](ch2-cgo/ch2-11-link.md)
|
||||
* [2.12. 补充说明](ch2-cgo/ch2-12-faq.md)
|
||||
* [第三章 汇编语言](ch3-asm/readme.md)
|
||||
* [3.1. 快速入门(Doing)](ch3-asm/ch3-01-basic.md)
|
||||
* [3.1. 快速入门](ch3-asm/ch3-01-basic.md)
|
||||
* [3.2. 冯·诺伊曼计算机(TODO)](ch3-asm/ch3-02-arch.md)
|
||||
* [3.3. 常量和变量(TODO)](ch3-asm/ch3-03-const-and-var.md)
|
||||
* [3.4. 函数(TODO)](ch3-asm/ch3-04-func.md)
|
||||
|
@ -102,122 +102,185 @@ func main() {
|
||||
|
||||
## 定义字符串变量
|
||||
|
||||
TODO
|
||||
在前一个例子中,我们通过汇编定义了一个整数变量。现在我们尝试通过汇编定义一个字符串变量。
|
||||
|
||||
## Go语言版本
|
||||
虽然从Go语言角度看,定义字符串和整数变量的写法基本相同,但是字符串底层却有着比单个整数更复杂的数据结构。
|
||||
|
||||
|
||||
创建pkg.go文件,内容如下:
|
||||
|
||||
```go
|
||||
package pkg
|
||||
|
||||
var helloworld = "Hello World!"
|
||||
var Name = "gopher"
|
||||
```
|
||||
|
||||
func HelloWorld() {
|
||||
println(helloworld)
|
||||
然后用以下命令查看的Go语言程序对应的伪汇编代码:
|
||||
|
||||
```
|
||||
$ go tool compile -S pkg.go
|
||||
go.string."gopher" SRODATA dupok size=6
|
||||
0x0000 67 6f 70 68 65 72 gopher
|
||||
"".Name SDATA size=16
|
||||
0x0000 00 00 00 00 00 00 00 00 06 00 00 00 00 00 00 00 ................
|
||||
rel 0+8 t=1 go.string."gopher"+0
|
||||
```
|
||||
|
||||
输出中出现了一个新的符号go.string."gopher",根据其长度和内容分析可以猜测是对应底层的"gopher"字符串数据。因为Go语言的字符串并不是值类型,Go字符串只是一中只读的引用类型。假设多个代码中出现了相同的"gopher"字符串时,程序链接后其实都是引用的同一个符号go.string."gopher"。因此,该符号有一个SRODATA标志表示这个数据在只读内存段,dupok表示出现多个相同符号时只保留一个就可以了。
|
||||
|
||||
而真正的Go字符串变量Name对应的大小却只有16个字节了。其实Name变量并没有直接对应“gopher”字符串,而是对应reflect.StringHeader结构体:
|
||||
|
||||
```go
|
||||
type reflect.StringHeader struct {
|
||||
Data uintptr
|
||||
Len int
|
||||
}
|
||||
```
|
||||
|
||||
## Go汇编版本
|
||||
从汇编角度看,Name变量其实对应的是reflect.StringHeader结构体类型。前8个字节对应底层真实字符串数据的指针,也就是符号go.string."gopher"对应的地址。后8个字节对应底层真实字符串数据的有效长度,这里是6个字节。
|
||||
|
||||
创建pkg_amd64.s文件,我们尝试通过汇编代码重新定义并初始化Name字符串:
|
||||
|
||||
```
|
||||
GLOBL ·NameData(SB),$8
|
||||
DATA ·NameData(SB)/8,$"gopher"
|
||||
|
||||
GLOBL ·Name(SB),$16
|
||||
DATA ·Name+0(SB)/8,$·NameData(SB)
|
||||
DATA ·Name+8(SB)/8,$6
|
||||
```
|
||||
|
||||
因为在Go汇编语言中,go.string."gopher"不是一个合法的符号,我们无法手工创建(这是给编译器保留的部分特权,因为手工创建类似符号可能打破编译器输出代码的某些规则)。因此我们新创建了一个·NameData符号表示底层的字符串数据。
|
||||
|
||||
然后定义·Name符号为两个16字节,其中前8个字节用·NameData符号对应的地址初始化,后8个字节为常量6表示字符串长度。
|
||||
|
||||
通过以下代码测试输出Name变量:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import pkg "pkg包的路径"
|
||||
|
||||
func main() {
|
||||
println(pkg.Name)
|
||||
}
|
||||
```
|
||||
|
||||
在运行时将会产生类似以下错误:
|
||||
|
||||
```
|
||||
pkgpath.NameData: missing Go //type information for global symbol: size 8
|
||||
```
|
||||
|
||||
提示汇编中定义的NameData符号没有类型信息。其实Go汇编语言中定义的数据并没有所谓的类型,每个符号只不过是对应一个内存而且。出现这种错误的原因是,Go语言的垃圾回收器在扫描NameData变量的时候,无法知晓该变量内部是否包含指针。因此,真正错误的原因并不是NameData没有类型,二是NameData变量没有标注是否会含有指针信息。
|
||||
|
||||
通过给NameData变量增加一个标志,表示其中不会包含指针数据可以修复该错误:
|
||||
|
||||
```
|
||||
#include "textflag.h"
|
||||
|
||||
// var helloworld string
|
||||
GLOBL ·helloworld(SB),NOPTR,$32 // var helloworld [32]byte
|
||||
DATA ·helloworld+0(SB)/8,$·helloworld+16(SB) // StringHeader.Data
|
||||
DATA ·helloworld+8(SB)/8,$12 // StringHeader.Len
|
||||
DATA ·helloworld+16(SB)/8,$"Hello Wo" // ...string data...
|
||||
DATA ·helloworld+24(SB)/8,$"rld!" // ...string data...
|
||||
GLOBL ·NameData(SB),NOPTR,$8
|
||||
```
|
||||
|
||||
// func HelloWorld()
|
||||
TEXT ·HelloWorld(SB), $16-0
|
||||
通过给·NameData增加NOPTR,表示其中不含指针数据。那么垃圾回收器在遇到该变量的时候就会停止内部数据的扫描。
|
||||
|
||||
我们也可以通过给·NameData变量在Go语言中增加一个不含指针并且大小为8个字节的类型来修改该错误:
|
||||
|
||||
```go
|
||||
package pkg
|
||||
|
||||
var NameData [8]byte
|
||||
var Name string
|
||||
```
|
||||
|
||||
我们将NameData声明为长度为8的字节数组。因为编译器可以通过类型分析出该变量不会包含指针,因此汇编代码中可以NOPTR标志信息。
|
||||
|
||||
在这个实现中,Name字符串底层其实引用的是NameData内存对应的“gopher”字符串数据。因此,如果NameData发生变化的化,Name字符串的数据也会跟着变化的。
|
||||
|
||||
```go
|
||||
func main() {
|
||||
println(pkg.Name)
|
||||
|
||||
pkg.NameData[0] = '?'
|
||||
println(pkg.Name)
|
||||
}
|
||||
```
|
||||
|
||||
当然这和字符串的只读定义是冲突的,正常的代码需要避免出现这种情况。最好的方法是不要导出内部的NameData变量,这样可以避免内部数据被无意破坏。
|
||||
|
||||
在用汇编定义字符串时,我们完全一个换一种思维:将底层的字符串数据和字符串头结构体定义在一起,这样可以避免引入NameData符号:
|
||||
|
||||
```
|
||||
GLOBL ·Name(SB),$24
|
||||
|
||||
DATA ·Name+0(SB)/8,$·Name+16(SB)
|
||||
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个字节的长度。
|
||||
|
||||
|
||||
## 定义main函数
|
||||
|
||||
前面的例子已经展示的如何通过汇编定义整型和字符串类型变量。我们现在将尝试用汇编实现函数,然后输出一个字符串。
|
||||
|
||||
先创建main.go文件,创建并初始化字符串变量,同时声明main函数:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
var helloworld = "你好, 世界"
|
||||
|
||||
func main()
|
||||
```
|
||||
|
||||
然后创建main_amd64.s文件,里面对应main函数的实现:
|
||||
|
||||
```
|
||||
TEXT ·main(SB), $16-0
|
||||
MOVQ ·helloworld+0(SB), AX; MOVQ AX, 0(SP)
|
||||
MOVQ ·helloworld+8(SB), BX; MOVQ BX, 8(SP)
|
||||
CALL runtime·printstring(SB)
|
||||
CALL runtime·printnl(SB)
|
||||
RET
|
||||
```
|
||||
`TEXT ·main(SB), $16-0`用于定义`main`函数,其中`$16-0`表示`main`函数的帧大小是16个字节(对应string头的大小,用于给`runtime·printstring`函数传递参数),`0`表示`main`函数没有参数和返回值。`main`函数内部通过调用运行时内部的`runtime·printstring(SB)`函数来打印字符串。然后调用runtime·printnl打印换行符号。
|
||||
|
||||
## 汇编语法
|
||||
Go语言函数在函数调用时,完全通过栈传递调用参数和返回值。先通过MOVQ指令,将helloworld对应的字符串头部结构体的16个字节复制到栈指针SP对应的16字节的空间,然后通过CALL指令调用对应函数。最后使用RET指令表示当前函数返回。
|
||||
|
||||
- 变量要在Go语言中声明, 但不能赋值
|
||||
- 函数要在Go语言中声明, 但不包含函数实现
|
||||
|
||||
- Go语言中的标识符x对应汇编语言中的·x
|
||||
|
||||
- GLOBL: 定义全局标识符, 分配内存空间
|
||||
- DATA: 初始化对应内存空间
|
||||
- TEXT: 定义函数
|
||||
|
||||
## 字符串的结构
|
||||
|
||||
```go
|
||||
var helloworld string // 只能声明, 不能赋值
|
||||
```
|
||||
|
||||
```
|
||||
// +---------------------------+ ·helloworld+0(SB)
|
||||
// | reflect.StringHeader.Data | ----------\ $·helloworld+16(SB)
|
||||
// +---------------------------+ |
|
||||
// | reflect.StringHeader.Len | |
|
||||
// +---------------------------+ <---------/ ·helloworld+16(SB)
|
||||
// | "Hello World!" |
|
||||
// +---------------------------+
|
||||
```
|
||||
|
||||
- 字符串的数据紧挨字符串头结构体
|
||||
- $·helloworld+16(SB) 表示符号地址
|
||||
- ·helloworld+16(SB) 表示符号地址内的数据
|
||||
|
||||
## HelloWorld函数
|
||||
|
||||
```go
|
||||
func HelloWorld() // 只能声明, 不能定义
|
||||
```
|
||||
|
||||
```
|
||||
TEXT ·HelloWorld(SB), $16-0
|
||||
MOVQ ·helloworld+0(SB), AX; MOVQ AX, 0(SP)
|
||||
MOVQ ·helloworld+8(SB), BX; MOVQ BX, 8(SP)
|
||||
CALL runtime·printstring(SB)
|
||||
CALL runtime·printnl(SB)
|
||||
RET
|
||||
```
|
||||
|
||||
- $16-0中的16: 表示函数内部有16字节用于局部变量
|
||||
- $16-0中的0: 表示函数参数和返回值总大小为0
|
||||
|
||||
- printstring的参数类型为StringHeader
|
||||
- 0(SP)为StringHeader.Data
|
||||
- 8(SP)为StringHeader.Len
|
||||
|
||||
## 简化: 在Go中定义变量
|
||||
|
||||
```go
|
||||
var helloworld string = "你好, 中国!"
|
||||
|
||||
func HelloWorld()
|
||||
```
|
||||
|
||||
```
|
||||
TEXT ·HelloWorld(SB), $16-0
|
||||
MOVQ ·helloworld+0(SB), AX; MOVQ AX, 0(SP)
|
||||
MOVQ ·helloworld+8(SB), BX; MOVQ BX, 8(SP)
|
||||
CALL runtime·printstring(SB)
|
||||
CALL runtime·printnl(SB)
|
||||
RET
|
||||
```
|
||||
|
||||
- 汇编定义变量没有太多优势, 性价比较低
|
||||
- 汇编的优势是挖掘芯片的功能和性能
|
||||
|
||||
## 特殊字符
|
||||
|
||||
TODO
|
||||
Go语言函数或方法符号在编译为目标文件后,目标文件中的每个符号均包含对应包的觉得导入路径。因此目标文件的符号可能非常复杂,比如“path/to/pkg.(*SomeType).SomeMethod”或“go.string."abc"”。目标文件的符号名中不仅仅包含普通的字母,还可能包含诸多特殊字符。而Go语言的汇编器是从plan9移植过来的二把刀,并不能处理这些特殊的字符,导致了用Go汇编语言手工实现Go诸多特性时遇到种种限制。
|
||||
|
||||
Go汇编语言同样遵循Go语言少即是多的哲学,它只保留了最基本的特性:定义变量和全局函数。同时为了简化Go汇编器的词法扫描程序的实现,特别引入了Unicode中的中点`·`和大写的除法`/`,对应的Unicode码点为`U+00B7`和`U+2215`。汇编器编译后,中点`·`会被替换为ASCII中的点“.”,大写点除法会被替换为ASCII码中的除法“/”,比如`math/rand·Int`会被替换为`math/rand.Int`。这样可以将点和浮点数中的小数点、大写的除法和表达式中的除法符号分开,可以简化汇编程序此法分析部分的实现。
|
||||
|
||||
即使暂时抛开Go汇编语言设计取舍的问题,中点`·`和除法`/`两个字符的如何输入就是一个挑战。这两个字符在 https://golang.org/doc/asm 文档中均有描述,因此直接从该页面复制是最简单可靠的方式。
|
||||
|
||||
如果是macOS系统,则有以下几种方法输入中点`·`:在不开输入法时,可直接用 option+shift+9 输入;如果是自带的简体拼音输入法,输入左上角`~`键对应`·`,如果是自带的Unicode输入法,则可以输入对应的Unicode码点。
|
||||
|
||||
|
||||
## 没有分号
|
||||
|
||||
- 分号用于分隔多个汇编语句
|
||||
- 行末尾自动添加分号
|
||||
Go汇编语言中分号可以用于分隔同一行内的多个语句。下面是用分号混乱排版的汇编代码:
|
||||
|
||||
<!-- 宏中的分号和注释 -->
|
||||
```
|
||||
TEXT ·main(SB), $16-0; MOVQ ·helloworld+0(SB), AX; MOVQ ·helloworld+8(SB), BX;
|
||||
MOVQ AX, 0(SP);MOVQ BX, 8(SP);CALL runtime·printstring(SB);
|
||||
CALL runtime·printnl(SB);
|
||||
RET;
|
||||
```
|
||||
|
||||
和Go语言一样,也可以省略行尾的分号。当遇到末尾时,汇编器会自动插入分号。下面是省略分号后的代码:
|
||||
|
||||
```
|
||||
TEXT ·main(SB), $16-0
|
||||
MOVQ ·helloworld+0(SB), AX; MOVQ AX, 0(SP)
|
||||
MOVQ ·helloworld+8(SB), BX; MOVQ BX, 8(SP)
|
||||
CALL runtime·printstring(SB)
|
||||
CALL runtime·printnl(SB)
|
||||
RET
|
||||
```
|
||||
|
||||
和Go语言一样,语句之间多个连续的空白字符和一个空格是等价的。
|
||||
|
10
examples/ch3-01-quick-guide/main-01/Makefile
Normal file
10
examples/ch3-01-quick-guide/main-01/Makefile
Normal file
@ -0,0 +1,10 @@
|
||||
# Copyright 2018 <chaishushan{AT}gmail.com>. All rights reserved.
|
||||
# Use of this source code is governed by a BSD-style
|
||||
# license that can be found in the LICENSE file.
|
||||
|
||||
default:
|
||||
-go build -o a.out && ./a.out
|
||||
-@rm a.out
|
||||
|
||||
clean:
|
||||
-@rm a.out
|
5
examples/ch3-01-quick-guide/main-01/main.go
Normal file
5
examples/ch3-01-quick-guide/main-01/main.go
Normal file
@ -0,0 +1,5 @@
|
||||
package main
|
||||
|
||||
var helloworld = "你好, 世界"
|
||||
|
||||
func main()
|
7
examples/ch3-01-quick-guide/main-01/main_amd64.s
Normal file
7
examples/ch3-01-quick-guide/main-01/main_amd64.s
Normal file
@ -0,0 +1,7 @@
|
||||
|
||||
TEXT ·main(SB), $16-0
|
||||
MOVQ ·helloworld+0(SB), AX; MOVQ AX, 0(SP)
|
||||
MOVQ ·helloworld+8(SB), BX; MOVQ BX, 8(SP)
|
||||
CALL runtime·printstring(SB)
|
||||
CALL runtime·printnl(SB)
|
||||
RET
|
3
examples/ch3-01-quick-guide/str-01/pkg.go
Normal file
3
examples/ch3-01-quick-guide/str-01/pkg.go
Normal file
@ -0,0 +1,3 @@
|
||||
package pkg
|
||||
|
||||
var Name = "gopher"
|
5
examples/ch3-01-quick-guide/str-02/pkg.go
Normal file
5
examples/ch3-01-quick-guide/str-02/pkg.go
Normal file
@ -0,0 +1,5 @@
|
||||
package pkg
|
||||
|
||||
var NameData [8]byte
|
||||
|
||||
var Name string
|
8
examples/ch3-01-quick-guide/str-02/pkg_amd64.s
Normal file
8
examples/ch3-01-quick-guide/str-02/pkg_amd64.s
Normal file
@ -0,0 +1,8 @@
|
||||
|
||||
|
||||
GLOBL ·NameData(SB),$8
|
||||
DATA ·NameData(SB)/8,$"gopher"
|
||||
|
||||
GLOBL ·Name(SB),$16
|
||||
DATA ·Name+0(SB)/8,$·NameData(SB)
|
||||
DATA ·Name+8(SB)/8,$6
|
14
examples/ch3-01-quick-guide/str-02/runme.go
Normal file
14
examples/ch3-01-quick-guide/str-02/runme.go
Normal file
@ -0,0 +1,14 @@
|
||||
// +build ignore
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
pkg "."
|
||||
)
|
||||
|
||||
func main() {
|
||||
println(pkg.Name)
|
||||
|
||||
pkg.NameData[0] = '?'
|
||||
println(pkg.Name)
|
||||
}
|
5
examples/ch3-01-quick-guide/str-03/pkg.go
Normal file
5
examples/ch3-01-quick-guide/str-03/pkg.go
Normal file
@ -0,0 +1,5 @@
|
||||
package pkg
|
||||
|
||||
var NameData [8]byte
|
||||
|
||||
var Name string
|
5
examples/ch3-01-quick-guide/str-03/pkg_amd64.s
Normal file
5
examples/ch3-01-quick-guide/str-03/pkg_amd64.s
Normal file
@ -0,0 +1,5 @@
|
||||
GLOBL ·Name(SB),$24
|
||||
|
||||
DATA ·Name+0(SB)/8,$·Name+16(SB)
|
||||
DATA ·Name+8(SB)/8,$6
|
||||
DATA ·Name+16(SB)/8,$"gopher"
|
11
examples/ch3-01-quick-guide/str-03/runme.go
Normal file
11
examples/ch3-01-quick-guide/str-03/runme.go
Normal file
@ -0,0 +1,11 @@
|
||||
// +build ignore
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
pkg "."
|
||||
)
|
||||
|
||||
func main() {
|
||||
println(pkg.Name)
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user