diff --git a/SUMMARY.md b/SUMMARY.md index ddd32b7..d0af8d1 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -26,7 +26,7 @@ * [3.1. 快速入门](ch3-asm/ch3-01-basic.md) * [3.2. 计算机结构](ch3-asm/ch3-02-arch.md) * [3.3. 常量和全局变量](ch3-asm/ch3-03-const-and-var.md) - * [3.4. 函数(Doing)](ch3-asm/ch3-04-func.md) + * [3.4. 函数](ch3-asm/ch3-04-func.md) * [3.5. 控制流(TODO)](ch3-asm/ch3-05-control-flow.md) * [3.6. 再论函数(TODO)](ch3-asm/ch3-06-func-again.md) * [3.7. FUNCDATA和PCDATA(TODO)](ch3-asm/ch3-07-funcdata-pcdata.md) @@ -37,8 +37,8 @@ * [3.12. AVX/SSE/JIT高级优化(TODO)](ch3-asm/ch3-12-avx-sse-jit.md) * [3.13. ARM汇编(TODO)](ch3-asm/ch3-13-arm.md) * [3.14. 补充说明(TODO)](ch3-asm/ch3-14-faq.md) -* [第四章 移动平台(TODO)](ch4-mobile/readme.md) -* [第五章 这是一个坑(TODO)](ch5-wtf/readme.md) +* [第四章 Go&JS编程(TODO)](ch4-js/readme.md) +* [第五章 移动平台(TODO)](ch5-mobile/readme.md) * [第六章 Go和Web](ch6-web/readme.md) * [6.1. Web开发简介](ch6-web/ch6-01-introduction.md) * [6.2. Router请求路由](ch6-web/ch6-02-router.md) diff --git a/ch3-asm/ch3-04-func.md b/ch3-asm/ch3-04-func.md index da03127..045b0b9 100644 --- a/ch3-asm/ch3-04-func.md +++ b/ch3-asm/ch3-04-func.md @@ -1,4 +1,4 @@ -# 3.4. 函数(Doing) +# 3.4. 函数 终于到函数了!因为Go汇编语言中,可以也建议通过Go语言来定义全局变量,那么剩下的也就是函数了。只有掌握了汇编函数的基本用法,才能真正算是Go汇编语言入门。本章将简单讨论Go汇编中函数的定义和用法。 @@ -131,23 +131,120 @@ func SomeFunc(FP *SomeFunc_args_and_returns) { ## 函数中的局部变量 -TODO +从Go语言函数角度讲,局部变量是函数内明确定义的变量,同时也包含函数的参数和返回值变量。但是从Go汇编角度看,局部变量是指函数运行时,在当前函数栈帧所对应的内存内的变量,不包含函数的参数和返回值(因为访问方式有差异)。函数栈帧的空间主要由函数参数和返回值、局部变量和被调用其它函数的参数和返回值空间组成。为了便于理解,我们可以将汇编函数的局部变量类比为Go语言函数中显式定义的变量,不包含参数和返回值部分。 -## 函数中的(真)(伪)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对应高地址,对应的局部变量的偏移量都是负数。 + +我们现在Go语言定义一个Foo函数,并在函数内部定义几个局部变量: + +```go +func Foo() { var a, b, c int } +``` + +然后通过汇编语言重新实现Foo函数,并通过伪SP来定位局部变量: + +``` +TEXT ·Foo(SB), $24-0 + MOVQ a-8*3(SP), AX // a + MOVQ b-8*2(SP), BX // b + MOVQ c-8*1(SP), CX // c + RET +``` + +Foo函数有3个int类型的局部变量,但是没有调用其它的函数,所以函数的栈帧大小为24个字节。因为Foo函数没有参数和返回值,因此参数和返回值大小为0个字节,当然这个部分可以省略不写。而局部变量中先定义的变量a离为SP对应的地址最远,最后定义的变量c里伪SP最近。有两个隐私导致出现这种逆序的结果:一个从Go语言函数角度理解,先定义的a变量地址要比后定义的变量的地址更小;另一个是伪SP对应栈帧的底部,而栈是从高向地生长的,所以有着更小地址的a变量离栈的底部伪SP更远。 + +我们同样可以通过结构体来模拟局部变量的布局: + +```go +func Foo() { + var local [1]struct{a, b, c int}; + var SP = &local[1]; + + _ = -(unsafe.Sizeof(local)-unsafe.Offsetof(local.a)) + uintptr(&SP) // a + _ = -(unsafe.Sizeof(local)-unsafe.Offsetof(local.b)) + uintptr(&SP) // b + _ = -(unsafe.Sizeof(local)-unsafe.Offsetof(local.c)) + uintptr(&SP) // c +} +``` + +我们将之前的三个局部变量挪到一个结构体中。然后构造一个SP变量对应伪SP寄存器,对应局部变量结构体的顶部。然后根据局部变量总大小和每个变量对应成员的偏移量计算相对于伪SP的距离,最终偏移量是一个负数。 + +通过这种方式可以处理复制的局部变量的偏移,同时也能包装每个变量地址的对齐要求。当然,除了地址对齐外,局部变量的布局并没有顺序要求。对于汇编比较熟悉同学可以根据字节的习惯组织变量的布局。 -TODO ## 调用其它函数 - +常见的用Go汇编实现的函数都是叶子函数,也就是被其它函数调用,但是很少调用其它函数。这主要是因为叶子函数比较简单,可以简化汇编函数的编写;同时一般性能或特性的瓶颈也处于叶子函数。但是能够调用其它函数和能够被其它函数调用通用重要,否则Go汇编就不是一个完整的汇编语言。 +在前文中我们已经学习过一些汇编实现的函数参数和返回值处理的规则。那么一个显然的问题是,汇编函数的参数是从哪里来的?答案同样明显,被调用函数的参数是有调用方准备的:调用方在栈上设置好空间和数据后调用函数,被调用方在返回前将返回值放如对应的位置,函数通过RET指令返回调用放函数之后,调用方从返回值对应的栈内存位置取出结果。Go语言函数的调用参数和返回值均是通过栈传输的,这样做的有点是函数调用栈比较清晰,缺点是函数调用有一定的性能损耗(Go编译器是通过函数内联来缓解这个问题的影响)。 -TODO +为了便于演示,我们先用Go语言构造foo和bar两个函数,其中foo函数内部调用bar函数: + +```go +func foo() { + var a, b int + bar(b) +} + +func bar(a int) int { + return a +} +``` + +然后用汇编重新实现类似的函数: + +``` +TEXT ·foo(SB), $32-0 + MOVQ a-8*2(SP), AX // a + MOVQ b-8*1(SP), BX // b + + MOVQ BX, +0(SP) // bar(BX) + CALL ·bar(SB) // + MOVQ +8(SP), CX // CX = bar(a) + RET + +TEXT ·bar(SB), $0-16 + MOVQ a-0(FP), AX // a + + MOVQ AX, ret+8(FP) // return a + 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寄存,从而完成函数调用。 + +Go语言中函数调用时一个复杂的问题,因为Go函数不仅仅要了解函数调用函数的布局,还会涉及到栈的跳转,栈上局部变量的生命周期管理。本节只是简单了解函数调用参数的布局规则,在后续的章节中会更详细的讨论函数的细节。 ## 宏函数 - +宏函数并不是Go汇编语言所定义,二是Go汇编引入的预处理特性自带的特性。 + +在C语言中我们可以通过带参数的宏定义一个交换2个数的宏函数: + +```c +#define SWAP(x, y) do{ int t = x; x = y; y = t; }while(0) +``` + +我们可以用类似的方式定义一个交换两个寄存器的宏: + +```c +#define SWAP(x, y, t) MOVQ x, t; MOVQ y, x; MOVQ t, y +``` + +因为汇编语言中无法定义临时变量,我们增加一个参数用于临时寄存器。下面是通过SWAP宏函数交换AX和BX寄存器的值,然后返回结果: + +``` +// func Swap(a, b int) (int, int) +TEXT ·Swap(SB), $0-32 + MOVQ a-8*2(SP), AX // a + MOVQ b-8*1(SP), BX // b + + SWAP(AX, BX, CX) // AX, BX = b, a + + MOVQ AX, ret0+16(FP) // return + MOVQ BX, ret1+24(FP) // + RET +``` + +因为预处理器可以通过条件编译针对不同的平台定义宏的实现,这样可以简化平台带来的差异。 -TODO diff --git a/ch4-js/readme.md b/ch4-js/readme.md new file mode 100644 index 0000000..40fc5cc --- /dev/null +++ b/ch4-js/readme.md @@ -0,0 +1,3 @@ +# 第四章 Go&JS编程 + +TODO diff --git a/ch4-mobile/readme.md b/ch4-mobile/readme.md deleted file mode 100644 index e874579..0000000 --- a/ch4-mobile/readme.md +++ /dev/null @@ -1,3 +0,0 @@ -# 第四章 移动平台 - -TODO diff --git a/ch5-mobile/readme.md b/ch5-mobile/readme.md new file mode 100644 index 0000000..725ee1e --- /dev/null +++ b/ch5-mobile/readme.md @@ -0,0 +1,3 @@ +# 第五章 移动平台 + +TODO