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

ch3-06: done

This commit is contained in:
chai2010 2018-06-08 09:20:53 +08:00
parent 9b0ad889c6
commit a3434a955d
2 changed files with 96 additions and 5 deletions

View File

@ -28,7 +28,7 @@
* [3.3. 常量和全局变量](ch3-asm/ch3-03-const-and-var.md) * [3.3. 常量和全局变量](ch3-asm/ch3-03-const-and-var.md)
* [3.4. 函数](ch3-asm/ch3-04-func.md) * [3.4. 函数](ch3-asm/ch3-04-func.md)
* [3.5. 控制流](ch3-asm/ch3-05-control-flow.md) * [3.5. 控制流](ch3-asm/ch3-05-control-flow.md)
* [3.6. 再论函数(Doing)](ch3-asm/ch3-06-func-again.md) * [3.6. 再论函数](ch3-asm/ch3-06-func-again.md)
* [3.7. Go核心对象结构(TODO)](ch3-asm/ch3-07-core-type.md) * [3.7. Go核心对象结构(TODO)](ch3-asm/ch3-07-core-type.md)
* [3.8. runtime内置函数(TODO)](ch3-asm/ch3-08-runtime-func.md) * [3.8. runtime内置函数(TODO)](ch3-asm/ch3-08-runtime-func.md)
* [3.9. 调用C函数(TODO)](ch3-asm/ch3-09-call-c-leaf-func.md) * [3.9. 调用C函数(TODO)](ch3-asm/ch3-09-call-c-leaf-func.md)

View File

@ -1,4 +1,4 @@
# 3.6. 再论函数(Doing) # 3.6. 再论函数
在前面的章节中我们已经简单讨论过Go的汇编函数但是那些主要是叶子函数。叶子函数的最大特点是不会调用其他函数也就是栈的大小是可以预期的叶子函数也就是可以基本忽略爆栈的问题如果已经爆了那也是上级函数的问题。如果没有爆栈问题那么也就是不会有栈的分裂问题如果没有栈的分裂也就不需要移动栈上的指针也就不会有栈上指针管理的问题。但是是现实中Go语言的函数是可以任意深度调用的永远不用担心爆栈的风险。那么这些近似黑科技的特殊是如何通过低级的汇编语言实现的呢这些都是本节尝试讨论的问题。 在前面的章节中我们已经简单讨论过Go的汇编函数但是那些主要是叶子函数。叶子函数的最大特点是不会调用其他函数也就是栈的大小是可以预期的叶子函数也就是可以基本忽略爆栈的问题如果已经爆了那也是上级函数的问题。如果没有爆栈问题那么也就是不会有栈的分裂问题如果没有栈的分裂也就不需要移动栈上的指针也就不会有栈上指针管理的问题。但是是现实中Go语言的函数是可以任意深度调用的永远不用担心爆栈的风险。那么这些近似黑科技的特殊是如何通过低级的汇编语言实现的呢这些都是本节尝试讨论的问题。
@ -139,10 +139,101 @@ type stack struct {
以上是栈的扩容但是栈到收缩是在何时处理到呢我们知道Go运行时会定期进行垃圾回收操作这其中栈的回收工作。如果栈使用到比例小于一定到阈值则分配一个较小到栈空间然后将栈上面到数据移动到新的栈中栈移动的过程和栈扩容的过程类似。 以上是栈的扩容但是栈到收缩是在何时处理到呢我们知道Go运行时会定期进行垃圾回收操作这其中栈的回收工作。如果栈使用到比例小于一定到阈值则分配一个较小到栈空间然后将栈上面到数据移动到新的栈中栈移动的过程和栈扩容的过程类似。
## PCDATA和PCDATA ## PCDATA和FUNCDATA
Go语言中有个runtime.Caller函数可以获取当前函数的调用者列表。我们可以非常容易在运行时定位每个函数的调用位置以及函数的调用链。因此在panic异常或用log输出信息时可以精确定位代码的位置。
比如以下代码可以打印程序的启动流程:
```go
func main() {
for skip := 0; ; skip++ {
pc, file, line, ok := runtime.Caller(skip)
if !ok {
break
}
p := runtime.FuncForPC(pc)
fnfile, fnline := p.FileLine(0)
fmt.Printf("skip = %d, pc = 0x%08X\n", skip, pc)
fmt.Printf(" func: file = %s, line = L%03d, name = %s, entry = 0x%08X\n", fnfile, fnline, p.Name(), p.Entry())
fmt.Printf(" call: file = %s, line = L%03d\n", file, line)
}
}
```
其中runtime.Caller先获取当时的PC寄存器值以及文件和行号。然后根据PC寄存器表示的指令位置通过runtime.FuncForPC函数获取函数的基本信息。Go语言是如何实现这种特性的呢
Go语言作为一终静态编译型语言在执行时每个函数的地址都是固定的函数的每条指令也时固定的。如果针对每个函数和函数的每个指令生成一个地址表格也叫PC表格那么在运行时我们就可以根据PC寄存器的值轻松查询到指令当时对应的函数和位置信息。而Go语言也时采用类似的策略只不过地址表格经过裁剪舍弃了不必要的信息。因为要在运行时获取任意一个地址的位置必然是要有一个函数调用因此我们只需要为函数的开始和结束位置以及每个函数调用位置生成地址表格就可以了。同时地址是有大小顺序的在排序后可以通过只记录增量来减少数据的大小在查询时可以通过二分法加快查找的速度。
在汇编中有个PCDATA用于生成PC表格PCDATA的指令用法为`PCDATA tableid, tableoffset`。PCDATA有个两个参数第一个参数为表格的类型第二个是表格的地址。在目前的实现中有PCDATA_StackMapIndex和PCDATA_InlTreeIndex两种表格类型。两种表格的数据是类似的应该包含了代码所在的文件路径、行号和函数的信息只不过PCDATA_InlTreeIndex用于内內联函数的表格。
此外对于汇编函数中返回值包含指针的类型在返回值指针被初始化之后需要执行一个GO_RESULTS_INITIALIZED指令
```c
#define GO_RESULTS_INITIALIZED PCDATA $PCDATA_StackMapIndex, $1
```
GO_RESULTS_INITIALIZED记录的也是PC表格的信息表示PC指针越过某个地址之后返回值才完成被初始化的状态。
Go语言二进制文件中除了有PC表格还有FUNC表格用于记录函数的参数、局部变量的指针信息。FUNCDATA指令和PCDATA的格式类似`FUNCDATA tableid, tableoffset`第一个参数为表格的类型第二个是表格的地址。目前的实现中定义了三种FUNC表格类型FUNCDATA_ArgsPointerMaps表示函数参数的指针信息表FUNCDATA_LocalsPointerMaps表示局部指针信息表FUNCDATA_InlTree表示被内联展开的指针信息表。通过FUNC表格Go语言的垃圾回收器可以跟踪全部指针的生命周期同时根据指针指向的地址在是否被移动的栈范围来确定是否要进行指针移动。
在前面递归函数的例子中我们遇到一个NO_LOCAL_POINTERS宏。它的定义如下
```c
#define FUNCDATA_ArgsPointerMaps 0 /* garbage collector blocks */
#define FUNCDATA_LocalsPointerMaps 1
#define FUNCDATA_InlTree 2
#define NO_LOCAL_POINTERS FUNCDATA $FUNCDATA_LocalsPointerMaps, runtime·no_pointers_stackmap(SB)
```
因此NO_LOCAL_POINTERS宏表示的是FUNCDATA_LocalsPointerMaps对应的局部指针表格而runtime·no_pointers_stackmap是一个空的指针表格也就是表示函数没有指针类型的局部变量。
PCDATA和FUNCDATA的数据一般是由编译器自动生成的手工编写并不现实。如果函数已经有Go语言声明那么编译器可以自动输出参数和返回值的指针表格。同时所有的函数调用一般是对应CALL指令编译器也是可以辅助生成PCDATA表格的。编译器唯一无法自动生成是函数局部变量的表格因此我们一般要在汇编函数的局部变量中谨慎使用指针类型。
对于PCDATA和FUNCDATA细节敢兴趣的同学可以尝试从debug/gosym包入手参考包的实现和测试代码。
TODO
## 方法函数 ## 方法函数
TODO Go语言中方法函数和全局函数非常相似比如有以下的方法
```go
package main
type MyInt int
func (v MyInt) Twice() int {
return int(v)*2
}
func MyInt_Twice(v MyInt) int {
return int(v)*2
}
```
其中MyInt类型的Twice方法和MyInt_Twice函数的类型是完全一样的只不过Twice在目标文件中被修饰为`main.MyInt.Twice`名称。我们可以用汇编实现该方法函数:
```
// func (v MyInt) Twice() int
TEXT ·MyInt·Twice(SB), NOSPLIT, $0-16
MOVQ a+0(FP), AX // v
MOVQ AX, AX // AX *= 2
MOVQ AX, ret+8(FP) // return v
RET
```
不过这只是最多非指针类型的解释函数。现在增加一个接收参数是指针类型的Ptr方法指针返回传入的指针
```go
func (p *MyInt) Ptr() *MyInt {
return p
}
```
在目标文件中Ptr方法名被修饰为`main.(*MyInt).Ptr`,也就是对应汇编中的`·(*MyInt)·Ptr`。不过在Go汇编语言中星号和小括弧都无法用作函数名字也就是无法用汇编直接实现接收参数是指针类型的方法。
在最终的目标文件中的标识符名字中还有很多Go汇编语言不支持的特殊符号比如`type.string."hello"`中的双引号这导致了无法通过手写的汇编代码实现全部的特性。或许是Go语言官方故意限制了汇编语言的特性。