1
0
mirror of https://github.com/chai2010/advanced-go-programming-book.git synced 2025-05-29 08:12:21 +00:00
advanced-go-programming-book/ch3-asm/ch3-06-func-again.md
2018-06-08 08:04:29 +08:00

90 lines
3.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 3.6. 再论函数(Doing)
在前面的章节中我们已经简单讨论过Go的汇编函数但是那些主要是叶子函数。叶子函数的最大特点是不会调用其他函数也就是栈的大小是可以预期的叶子函数也就是可以基本忽略爆栈的问题如果已经爆了那也是上级函数的问题。如果没有爆栈问题那么也就是不会有栈的分裂问题如果没有栈的分裂也就不需要移动栈上的指针也就不会有栈上指针管理的问题。但是是现实中Go语言的函数是可以任意深度调用的永远不用担心爆栈的风险。那么这些近似黑科技的特殊是如何通过低级的汇编语言实现的呢这些都是本节尝试讨论的问题。
## 递归函数: 1到n求和
递归函数是比较特殊的函数递归函数通过调用自身并且在栈上保存状态这可以简化很多问题的处理。Go语言中递归函数的强大之处是不用担心爆栈问题因为栈可以根据需要进行扩容和收缩。我们现在尝试通过汇编语言实现一个递归调用的函数为了简化目前先不考虑栈的变化。
先通过Go递归函数实现一个1到n的求和函数
```go
// sum = 1+2+...+n
// sum(100) = 5050
func sum(n int) int {
if n > 0 { return n+sum(n-1) } else { return 0 }
}
```
然后通过if/goto构型重新上面的递归函数以便于转义为汇编版本
```go
func sum(n int) (result int) {
var AX = n
var BX int
if n > 0 { goto L_STEP_TO_END }
goto L_END
L_STEP_TO_END:
AX -= 1
BX = sum(AX)
AX = n // 调用函数后, AX重新恢复为n
BX += AX
return BX
L_END:
return 0
}
```
在改写之后递归调用的参数需要引入局部变量保存中间结果也需要引入局部变量。而通过栈来保存中间的调用状态正是递归函数的核心。因为输入参数也在栈上因为我们可以通过输入参数来保存少量的状态。同时我们模拟定义了AX和BX寄存器寄存器在使用前需要初始化并且在函数调用后也需要重新初始化。
下面继续改造为汇编语言版本:
```
// func sum(n int) (result int)
TEXT ·sum(SB), NOSPLIT, $16-16
MOVQ n+0(FP), AX // n
MOVQ result+8(FP), BX // result
CMPQ AX, $0 // test n - 0
JG L_STEP_TO_END // if > 0: goto L_STEP_TO_END
JMP L_END // goto L_STEP_TO_END
L_STEP_TO_END:
SUBQ $1, AX // AX -= 1
MOVQ AX, 0(SP) // arg: n-1
CALL ·sum(SB) // call sum(n-1)
MOVQ 8(SP), BX // BX = sum(n-1)
MOVQ n+0(FP), AX // AX = n
ADDQ AX, BX // BX += AX
MOVQ BX, result+8(FP) // return BX
RET
L_END:
MOVQ $0, result+8(FP) // return 0
RET
```
在汇编版本函数中并没有定义局部变量只有用于调用自身的临时栈空间。因为函数本身的参数和返回值有16个字节因此栈帧的大小也为16字节。L_STEP_TO_END标号部分用于处理递归调用是函数比较复杂的部分。L_END用于处理递归终结的部分。
调用sum函数的参数在`0(SP)`位置,调用结束后的返回值在`8(SP)`位置。在函数调用之后要需要重新为需要的寄存器注入值,因为被调用的函数内部很可能会破坏了寄存器的状态。同时调用函数的参数值也可信任的,输入参数也可能在被调用函数内部被修改了值。
总得来说用汇编实现递归函数和普通函数并没有什么区别当然是在没有考虑爆栈的前提下。我们的函数应该可以对较小的n进行求和但是当n大到一定层度也就是栈达到一定的深度必然会出现爆栈的问题。爆栈是C语言的特性不应该在哪怕是Go汇编语言中出现。
## 栈的扩容和收缩
TODO
## PCDATA和PCDATA
TODO
## 方法函数
TODO