# 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