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

Merge branch 'master' into pr3-29

This commit is contained in:
wahaha 2018-06-28 21:06:38 +08:00 committed by GitHub
commit eeb7fab36b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -80,12 +80,12 @@ TEXT ·Foo(SB), $0
如果是参数和返回值类型比较复杂的情况该如何处理呢?下面我们再尝试一个更复杂的函数参数和返回值的计算。比如有以下一个函数: 如果是参数和返回值类型比较复杂的情况该如何处理呢?下面我们再尝试一个更复杂的函数参数和返回值的计算。比如有以下一个函数:
```go ```go
func SomeFunc(a, b int, c bool) (d float64, err error) int func SomeFunc(a, b int, c bool) (d float64, err error)
``` ```
函数的参数有不同的类型,同时含有多个返回值,而且返回值中含有更复杂的接口类型。我们该如何计算每个参数的位置和总的大小呢? 函数的参数有不同的类型,同时含有多个返回值,而且返回值中含有更复杂的接口类型。我们该如何计算每个参数的位置和总的大小呢?
其实函数参数和返回值的大小以及对齐问题和结构体的大小和成员对齐问题是一致的。我们先看看如用Go语言函数来模拟Foo函数中参数和返回值的地址 其实函数参数和返回值的大小以及对齐问题和结构体的大小和成员对齐问题是一致的。我们先看看如用Go语言函数来模拟Foo函数中参数和返回值的地址
```go ```go
func Foo(FP *struct{a, b, c int}) { func Foo(FP *struct{a, b, c int}) {
@ -147,9 +147,9 @@ func SomeFunc(FP *SomeFunc_args_and_returns) {
为了便于访问局部变量Go汇编语言引入了伪SP寄存器对应当前栈帧的底部。因为在当前栈帧时栈的底部是固定不变的因此局部变量的相对于伪SP的偏移量也就是固定的这可以简化局部变量的维护工作。SP真伪区分只有一个原则如果使用SP时有一个临时标识符前缀就是伪SP否则就是真SP寄存器。比如`a(SP)``b+8(SP)`有a和b临时前缀这里都是伪SP而前缀部分一般用于表示局部变量的名字。而`(SP)``+8(SP)`没有临时标识符作为前缀它们都是真SP寄存器。 为了便于访问局部变量Go汇编语言引入了伪SP寄存器对应当前栈帧的底部。因为在当前栈帧时栈的底部是固定不变的因此局部变量的相对于伪SP的偏移量也就是固定的这可以简化局部变量的维护工作。SP真伪区分只有一个原则如果使用SP时有一个临时标识符前缀就是伪SP否则就是真SP寄存器。比如`a(SP)``b+8(SP)`有a和b临时前缀这里都是伪SP而前缀部分一般用于表示局部变量的名字。而`(SP)``+8(SP)`没有临时标识符作为前缀它们都是真SP寄存器。
在X86平台函数的调用栈是从高地址向低地址增长的因此伪SP寄存器对应栈帧的底部其实是对应更大的地址。当前栈的顶部对应真实存在的SP寄存器对应当前函数栈帧的栈顶对应更小的地址。如果整个内容是用Memory数组表示那么`Memory[0(SP):end-0(SP)]`就是对应当前栈帧的切片其中开始位置是真SP结尾部分是伪SP。真SP一般用于表示调用其它函数时的参数和返回值真SP对应内存较低的地址所以被访问变量的偏移量是正数而伪SP对应高地址对应的局部变量的偏移量都是负数。 在X86平台函数的调用栈是从高地址向低地址增长的因此伪SP寄存器对应栈帧的底部其实是对应更大的地址。当前栈的顶部对应真实存在的SP寄存器对应当前函数栈帧的栈顶对应更小的地址。如果整个内用Memory数组表示那么`Memory[0(SP):end-0(SP)]`就是对应当前栈帧的切片其中开始位置是真SP结尾部分是伪SP。真SP一般用于表示调用其它函数时的参数和返回值真SP对应内存较低的地址所以被访问变量的偏移量是正数而伪SP对应高地址对应的局部变量的偏移量都是负数。
我们现在Go语言定义一个Foo函数并在函数内部定义几个局部变量 我们现在Go语言定义一个Foo函数并在函数内部定义几个局部变量
```go ```go
func Foo() { var a, b, c int } func Foo() { var a, b, c int }
@ -165,7 +165,7 @@ TEXT ·Foo(SB), $24-0
RET RET
``` ```
Foo函数有3个int类型的局部变量但是没有调用其它的函数所以函数的栈帧大小为24个字节。因为Foo函数没有参数和返回值因此参数和返回值大小为0个字节当然这个部分可以省略不写。而局部变量中先定义的变量a离为SP对应的地址最远最后定义的变量c里伪SP最近。有两个原因导致出现这种逆序的结果一个从Go语言函数角度理解先定义的a变量地址要比后定义的变量的地址更小另一个是伪SP对应栈帧的底部而栈是从高向地生长的所以有着更小地址的a变量离栈的底部伪SP更远。 Foo函数有3个int类型的局部变量但是没有调用其它的函数所以函数的栈帧大小为24个字节。因为Foo函数没有参数和返回值因此参数和返回值大小为0个字节当然这个部分可以省略不写。而局部变量中先定义的变量a离伪SP对应的地址最远最后定义的变量c离伪SP最近。有两个原因导致出现这种逆序的结果一个从Go语言函数角度理解先定义的a变量地址要比后定义的变量的地址更小另一个是伪SP对应栈帧的底部而栈是从高向地生长的所以有着更小地址的a变量离栈的底部伪SP更远。
我们同样可以通过结构体来模拟局部变量的布局: 我们同样可以通过结构体来模拟局部变量的布局:
@ -182,14 +182,14 @@ func Foo() {
我们将之前的三个局部变量挪到一个结构体中。然后构造一个SP变量对应伪SP寄存器对应局部变量结构体的顶部。然后根据局部变量总大小和每个变量对应成员的偏移量计算相对于伪SP的距离最终偏移量是一个负数。 我们将之前的三个局部变量挪到一个结构体中。然后构造一个SP变量对应伪SP寄存器对应局部变量结构体的顶部。然后根据局部变量总大小和每个变量对应成员的偏移量计算相对于伪SP的距离最终偏移量是一个负数。
通过这种方式可以处理复杂的局部变量的偏移,同时也能包装每个变量地址的对齐要求。当然,除了地址对齐外,局部变量的布局并没有顺序要求。对于汇编比较熟悉同学可以根据字节的习惯组织变量的布局。 通过这种方式可以处理复杂的局部变量的偏移,同时也能保证每个变量地址的对齐要求。当然,除了地址对齐外,局部变量的布局并没有顺序要求。对于汇编比较熟悉同学可以根据自己的习惯组织变量的布局。
## 调用其它函数 ## 调用其它函数
常见的用Go汇编实现的函数都是叶子函数也就是被其它函数调用但是很少调用其它函数。这主要是因为叶子函数比较简单可以简化汇编函数的编写同时一般性能或特性的瓶颈也处于叶子函数。但是能够调用其它函数和能够被其它函数调用同样重要否则Go汇编就不是一个完整的汇编语言。 常见的用Go汇编实现的函数都是叶子函数也就是被其它函数调用但是很少调用其它函数。这主要是因为叶子函数比较简单可以简化汇编函数的编写同时一般性能或特性的瓶颈也处于叶子函数。但是能够调用其它函数和能够被其它函数调用同样重要否则Go汇编就不是一个完整的汇编语言。
在前文中我们已经学习过一些汇编实现的函数参数和返回值处理的规则。那么一个显然的问题是,汇编函数的参数是从哪里来的?答案同样明显,被调用函数的参数是调用方准备的调用方在栈上设置好空间和数据后调用函数被调用方在返回前将返回值放在对应的位置函数通过RET指令返回调用方函数之后调用方从返回值对应的栈内存位置取出结果。Go语言函数的调用参数和返回值均是通过栈传输的这样做的优点是函数调用栈比较清晰缺点是函数调用有一定的性能损耗Go编译器是通过函数内联来缓解这个问题的影响 在前文中我们已经学习过一些汇编实现的函数参数和返回值处理的规则。那么一个显然的问题是,汇编函数的参数是从哪里来的?答案同样明显,被调用函数的参数是调用方准备的调用方在栈上设置好空间和数据后调用函数被调用方在返回前将返回值放在对应的位置函数通过RET指令返回调用方函数之后调用方从返回值对应的栈内存位置取出结果。Go语言函数的调用参数和返回值均是通过栈传输的这样做的优点是函数调用栈比较清晰缺点是函数调用有一定的性能损耗Go编译器是通过函数内联来缓解这个问题的影响
![](../images/ch3-func-call-frame-01.ditaa.png) ![](../images/ch3-func-call-frame-01.ditaa.png)
@ -225,7 +225,7 @@ TEXT ·bar(SB), $0-16
RET RET
``` ```
首先分析foo函数的栈帧的大小foo函数内部有a、b两个局部变量占用16个字节然后要需给要调用的bar函数准备的参数和返回值准备16字节的空间因此总共有32字节的栈帧大小。在调用bar函数前我们已经计算好了栈帧的大小Go汇编语言环境已将真实的SP寄存器调整到合适的大小在调用函数时并不需要再手动调整SP寄存器。在调用函数bar前真SP对应向下增长的栈顶部因此顶部的16个字节和bar函数的参数和返回值是对应的相同的内存空间。我们将保存了b的BX寄存器内容放入`+0(SP)`位置也就是准备bar函数的第一个参数。然后通过CALL指令进行函数调用。在bar函数内首先从第一个参数对应的`+0(FP)`位置去取参数值存入AX寄存器然后再将AX内容放入返回值对应的`ret+8(FP)`内存位置最后调用RET返回。在foo函数中调用bar函数返回后从bar函数返回值对应的`+8(SP)`位置取出结果放到CX寄存从而完成函数调用。 首先分析foo函数的栈帧的大小foo函数内部有a、b两个局部变量占用16个字节然后要需给要调用的bar函数的参数和返回值准备16字节的空间因此总共有32字节的栈帧大小。在调用bar函数前我们已经计算好了栈帧的大小Go汇编语言环境已将真实的SP寄存器调整到合适的大小在调用函数时并不需要再手动调整SP寄存器。在调用函数bar前真SP对应向下增长的栈顶部因此顶部的16个字节和bar函数的参数和返回值是对应的相同的内存空间。我们将保存了b的BX寄存器内容放入`+0(SP)`位置也就是准备bar函数的第一个参数。然后通过CALL指令进行函数调用。在bar函数内首先从第一个参数对应的`+0(FP)`位置去取参数值存入AX寄存器然后再将AX内容放入返回值对应的`ret+8(FP)`内存位置最后调用RET返回。在foo函数中调用bar函数返回后从bar函数返回值对应的`+8(SP)`位置取出结果放到CX寄存从而完成函数调用。
调用其它函数前调用方要选择保存相关寄存器到栈中并在调用函数返回后选择要恢复的寄存器进行保存。Go语言中函数调用是一个复杂的问题因为Go函数不仅仅要了解函数调用函数的布局还会涉及到栈的跳转栈上局部变量的生命周期管理。本节只是简单了解函数调用参数的布局规则在后续的章节中会更详细的讨论函数的细节。 调用其它函数前调用方要选择保存相关寄存器到栈中并在调用函数返回后选择要恢复的寄存器进行保存。Go语言中函数调用是一个复杂的问题因为Go函数不仅仅要了解函数调用函数的布局还会涉及到栈的跳转栈上局部变量的生命周期管理。本节只是简单了解函数调用参数的布局规则在后续的章节中会更详细的讨论函数的细节。