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:
commit
eeb7fab36b
@ -80,12 +80,12 @@ TEXT ·Foo(SB), $0
|
||||
如果是参数和返回值类型比较复杂的情况该如何处理呢?下面我们再尝试一个更复杂的函数参数和返回值的计算。比如有以下一个函数:
|
||||
|
||||
```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
|
||||
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寄存器。
|
||||
|
||||
在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
|
||||
func Foo() { var a, b, c int }
|
||||
@ -165,7 +165,7 @@ TEXT ·Foo(SB), $24-0
|
||||
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的距离,最终偏移量是一个负数。
|
||||
|
||||
通过这种方式可以处理复杂的局部变量的偏移,同时也能包装每个变量地址的对齐要求。当然,除了地址对齐外,局部变量的布局并没有顺序要求。对于汇编比较熟悉同学可以根据字节的习惯组织变量的布局。
|
||||
通过这种方式可以处理复杂的局部变量的偏移,同时也能保证每个变量地址的对齐要求。当然,除了地址对齐外,局部变量的布局并没有顺序要求。对于汇编比较熟悉同学可以根据自己的习惯组织变量的布局。
|
||||
|
||||
|
||||
## 调用其它函数
|
||||
|
||||
常见的用Go汇编实现的函数都是叶子函数,也就是被其它函数调用,但是很少调用其它函数。这主要是因为叶子函数比较简单,可以简化汇编函数的编写;同时一般性能或特性的瓶颈也处于叶子函数。但是能够调用其它函数和能够被其它函数调用同样重要,否则Go汇编就不是一个完整的汇编语言。
|
||||
|
||||
在前文中我们已经学习过一些汇编实现的函数参数和返回值处理的规则。那么一个显然的问题是,汇编函数的参数是从哪里来的?答案同样明显,被调用函数的参数是有调用方准备的:调用方在栈上设置好空间和数据后调用函数,被调用方在返回前将返回值放在对应的位置,函数通过RET指令返回调用方函数之后,调用方从返回值对应的栈内存位置取出结果。Go语言函数的调用参数和返回值均是通过栈传输的,这样做的优点是函数调用栈比较清晰,缺点是函数调用有一定的性能损耗(Go编译器是通过函数内联来缓解这个问题的影响)。
|
||||
在前文中我们已经学习过一些汇编实现的函数参数和返回值处理的规则。那么一个显然的问题是,汇编函数的参数是从哪里来的?答案同样明显,被调用函数的参数是由调用方准备的:调用方在栈上设置好空间和数据后调用函数,被调用方在返回前将返回值放在对应的位置,函数通过RET指令返回调用方函数之后,调用方从返回值对应的栈内存位置取出结果。Go语言函数的调用参数和返回值均是通过栈传输的,这样做的优点是函数调用栈比较清晰,缺点是函数调用有一定的性能损耗(Go编译器是通过函数内联来缓解这个问题的影响)。
|
||||
|
||||

|
||||
|
||||
@ -225,7 +225,7 @@ TEXT ·bar(SB), $0-16
|
||||
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函数不仅仅要了解函数调用函数的布局,还会涉及到栈的跳转,栈上局部变量的生命周期管理。本节只是简单了解函数调用参数的布局规则,在后续的章节中会更详细的讨论函数的细节。
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user