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

ch3-05: 完善细节

This commit is contained in:
chai2010 2018-07-21 17:47:08 +08:00
parent 56a1fcc498
commit a408ed6325

View File

@ -1,6 +1,6 @@
# 3.5. 控制流
程序执行的流程主要有顺序、分支和循环几种执行流程。本节主要讨论如何将Go语言的控制流比较直观地转译为汇编程序或者说如何以汇编思维来编写Go语言代码。
程序主要有顺序、分支和循环几种执行流程。本节主要讨论如何将Go语言的控制流比较直观地转译为汇编程序或者说如何以汇编思维来编写Go语言代码。
## 顺序执行
@ -56,7 +56,7 @@ TEXT ·main(SB), $24-0
CALL runtime·printint
CALL runtime·printnl
// 函数调用后, AX/BX 可能被污染, 需要重新加载
// 函数调用后, AX/BX 寄存器可能被污染, 需要重新加载
MOVQ a-8*2(SP), AX // AX = a
MOVQ b-8*1(SP), BX // BX = b
@ -80,9 +80,9 @@ TEXT ·main(SB), $24-0
然后给a变量分配一个AX寄存器并且通过AX寄存器将a变量对应的内存设置为10AX也是10。为了输出a变量需要将AX寄存器的值放到`0(SP)`位置这个位置的变量将在调用runtime·printint函数时作为它的参数被打印。因为我们之前已经将AX的值保存到a变量内存中了因此在调用函数前并不需要再进行寄存器的备份工作。
在调用函数返回之后全部的寄存器将被视为被调用的函数修改因此我们需要从a、b对应的内存中重新恢复寄存器AX和BX。然后参考上面Go语言中b变量的计算方式更新BX对应的值计算完成后同样将BX的值写入到b对应的内存。
在调用函数返回之后,全部的寄存器将被视为可能被调用的函数修改因此我们需要从a、b对应的内存中重新恢复寄存器AX和BX。然后参考上面Go语言中b变量的计算方式更新BX对应的值计算完成后同样将BX的值写入到b对应的内存。
最后以b变量作为参数再次调用runtime·printint函数进行输出工作。所有的寄存器同样可能被污染不过main马上就返回不再需要使用AX、BX等寄存器因此就不需要再次恢复寄存器的值了。
最后以b变量作为参数再次调用runtime·printint函数进行输出工作。所有的寄存器同样可能被污染不过main函数马上就返回了因此不再需要恢复AX、BX等寄存器了。
重新分析汇编改写后的整个函数会发现里面很多的冗余代码。我们并不需要a、b两个临时变量分配两个内存空间而且也不需要在每个寄存器变化之后都要写入内存。下面是经过优化的汇编函数
@ -113,7 +113,7 @@ TEXT ·main(SB), $16-0
## if/goto跳转
早期的Go虽然提供了goto语句但是并不推荐在编程中使用。有一个和cgo类似的原则如果可以不使用goto语句那么就不要使用goto语句。Go语言中的goto语句是有严格限制的它无法跨越代码块并且在被跨越的代码中不能含有变量定义的语句。虽然Go语言不喜欢goto但是goto确实每个汇编语言码农的最爱。goto近似等价于汇编语言中的无条件跳转指令JMP配合if条件goto就组成了有条件跳转指令而有条件跳转指令正是构建整个汇编代码控制流的基石。
Go语言刚刚开源的时候并没有goto语句后来Go语言虽然增加了goto语句但是并不推荐在编程中使用。有一个和cgo类似的原则如果可以不使用goto语句那么就不要使用goto语句。Go语言中的goto语句是有严格限制的它无法跨越代码块并且在被跨越的代码中不能含有变量定义的语句。虽然Go语言不推荐goto语句但是goto确实每个汇编语言码农的最爱。因为goto近似等价于汇编语言中的无条件跳转指令JMP配合if条件goto就组成了有条件跳转指令而有条件跳转指令正是构建整个汇编代码控制流的基石。
为了便于理解我们用Go语言构造一个模拟三元表达式的If函数
@ -123,9 +123,9 @@ func If(ok bool, a, b int) int {
}
```
比如求两个数最大值的三元表达式`(a>b)?a:b`用If函数可以这样表达`If(a>b, a, b)`。因为语言的限制用来模拟三元表达式的If函数不支持泛型可以将a、b和返回类型改为空接口使用会繁琐一些
比如求两个数最大值的三元表达式`(a>b)?a:b`用If函数可以这样表达`If(a>b, a, b)`。因为语言的限制用来模拟三元表达式的If函数不支持泛型可以将a、b和返回类型改为空接口不过使用会繁琐一些)。
这个函数虽然看似只有简单的一行但是包含了if分支语句。在改用汇编实现前我们还是先用汇编的思维来重If函数。在改写时同样要遵循每个表达式只能有一个运算符的限制同时if语句的条件部分必须只有一个比较符号组成if语句的body部分只能是一个goto语句。
这个函数虽然看似只有简单的一行但是包含了if分支语句。在改用汇编实现前我们还是先用汇编的思维来重新审视If函数。在改写时同样要遵循每个表达式只能有一个运算符的限制同时if语句的条件部分必须只有一个比较符号组成if语句的body部分只能是一个goto语句。
用汇编思维改写后的If函数实现如下
@ -138,7 +138,7 @@ L:
}
```
因为汇编语言中没有bool类型我们改用int类型代替bool类型真实的汇编是用byte表示bool类型可以通过MOVBQZX指令加载byte类型的值。当ok参数非0时返回变量a否则返回变量b。我们将ok的逻辑反转下当ok参数为0时表示返回b否则返回变量a。在if语句中当ok参数为0时goto到L标号指定的语句也就是返回变量b。如果if条件不满足也就是ok参数非0执行后面的语句返回变量a。
因为汇编语言中没有bool类型我们改用int类型代替bool类型真实的汇编是用byte表示bool类型可以通过MOVBQZX指令加载byte类型的值,这里做了简化处理。当ok参数非0时返回变量a否则返回变量b。我们将ok的逻辑反转下当ok参数为0时表示返回b否则返回变量a。在if语句中当ok参数为0时goto到L标号指定的语句也就是返回变量b。如果if条件不满足也就是ok参数非0执行后面的语句返回变量a。
上述函数的实现已经非常接近汇编语言,下面是改为汇编实现的代码:
@ -149,7 +149,7 @@ TEXT ·If(SB), NOSPLIT, $0-32
MOVQ b+8*2(FP), BX // b
CMPQ CX, $0 // test ok
JZ L // if ok == 0, skip 2 line
JZ L // if ok == 0, goto L
MOVQ AX, ret+24(FP) // return a
RET
@ -158,9 +158,9 @@ L:
RET
```
是将三个参数加载到寄存器中ok参数对应CX寄存器a、b分别对应AX、BX寄存器。然后使用CMPQ比较指令将CX寄存器和常数0进行比较。如果比较的结果为0那么下一条JZ为0时跳转指令将跳转到L标号对应的指令也就是返回变量b的值。如果比较的结果不为0那么JZ指令将没有效果继续执行后面的指令也就是返回变量a的值。
是将三个参数加载到寄存器中ok参数对应CX寄存器a、b分别对应AX、BX寄存器。然后使用CMPQ比较指令将CX寄存器和常数0进行比较。如果比较的结果为0那么下一条JZ为0时跳转指令将跳转到L标号对应的语句也就是返回变量b的值。如果比较的结果不为0那么JZ指令将没有效果继续执行后面的指令也就是返回变量a的值。
在跳转指令中跳转的目标一般是通过一个标号表示。不过在有些通过宏实现的函数中更希望通过相对位置跳转这时候可以通过PC寄存器的来计算跳转的位置。
在跳转指令中跳转的目标一般是通过一个标号表示。不过在有些通过宏实现的函数中更希望通过相对位置跳转这时候可以通过PC寄存器的偏移量来计算临近跳转的位置。
## for循环
@ -178,7 +178,7 @@ func LoopAdd(cnt, v0, step int) int {
}
```
比如`1+2+...+100`可以这样计算`LoopAdd(100, 1, 1)``10+8+...+0`可以这样计算`LoopAdd(5, 10, -2)`在采用前面`if/goto`类似的技术来改造for循环。
比如`1+2+...+100`等差数列可以这样计算`LoopAdd(100, 1, 1)``10+8+...+0`等差数列则可以这样计算`LoopAdd(5, 10, -2)`。在用汇编彻底重写之前先采用前面`if/goto`类似的技术来改造for循环。
新的LoopAdd函数只有if/goto语句构成
@ -238,4 +238,4 @@ LOOP_END:
其中v0和result变量复用了一个BX寄存器。在LOOP_BEGIN标号对应的指令部分用MOVQ将DX寄存器初始化为0DX对应变量i循环的迭代变量。在LOOP_IF标号对应的指令部分使用CMPQ指令比较DX和AX如果循环没有结束则跳转到LOOP_BODY部分否则跳转到LOOP_END部分结束循环。在LOOP_BODY部分更新迭代变量并且执行循环体中的累加语句然后直接跳转到LOOP_IF部分进入下一轮循环条件判断。LOOP_END标号之后就是返回累加结果的语句。
循环是最复杂控制流,循环中隐含了分支和跳转语句。掌握了循环的写法基本也就掌握了汇编语言的写法。掌握规律之后,其实汇编语言编程会变得异常简单。
循环是最复杂控制流,循环中隐含了分支和跳转语句。掌握了循环的写法基本也就掌握了汇编语言的基础写法。更极客的玩法是通过汇编语言打破传统的控制流比如跨越多层函数直接返回比如参考基因编辑的手段直接执行一个从C语言构建的代码片段等。总之掌握规律之后,你会发现其实汇编语言编程会变得异常简单和有趣