From 5fd049437530dae521b93e36b5b76501c5b46bd9 Mon Sep 17 00:00:00 2001 From: chai2010 Date: Thu, 7 Jun 2018 11:25:26 +0800 Subject: [PATCH] =?UTF-8?q?ch3-06:=20=E5=AE=8C=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ch3-asm/ch3-06-func-again.md | 84 ++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/ch3-asm/ch3-06-func-again.md b/ch3-asm/ch3-06-func-again.md index a6990ea..3496a9a 100644 --- a/ch3-asm/ch3-06-func-again.md +++ b/ch3-asm/ch3-06-func-again.md @@ -2,4 +2,88 @@ 在前面的章节中我们已经简单讨论过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