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

ch3: 调整目录

This commit is contained in:
chai2010 2018-07-24 11:34:08 +08:00
parent 76a19ddafa
commit f5504198aa
9 changed files with 271 additions and 47 deletions

View File

@ -28,9 +28,10 @@
* [3.4. 函数](ch3-asm/ch3-04-func.md) * [3.4. 函数](ch3-asm/ch3-04-func.md)
* [3.5. 控制流](ch3-asm/ch3-05-control-flow.md) * [3.5. 控制流](ch3-asm/ch3-05-control-flow.md)
* [3.6. 再论函数](ch3-asm/ch3-06-func-again.md) * [3.6. 再论函数](ch3-asm/ch3-06-func-again.md)
* [3.7. 例子Goroutine ID](ch3-asm/ch3-07-goroutine-id.md) * [3.7. 汇编语言的威力](ch3-asm/ch3-07-hack-asm.md)
* [3.8. Delve调试器](ch3-asm/ch3-08-debug.md) * [3.8. 例子Goroutine ID](ch3-asm/ch3-08-goroutine-id.md)
* [3.9. 补充说明](ch3-asm/ch3-09-ext.md) * [3.9. Delve调试器](ch3-asm/ch3-09-debug.md)
* [3.10. 补充说明](ch3-asm/ch3-10-ext.md)
* [第四章 RPC和Protobuf](ch4-rpc/readme.md) * [第四章 RPC和Protobuf](ch4-rpc/readme.md)
* [4.1. RPC入门](ch4-rpc/ch4-01-rpc-intro.md) * [4.1. RPC入门](ch4-rpc/ch4-01-rpc-intro.md)
* [4.2. Protobuf](ch4-rpc/ch4-02-pb-intro.md) * [4.2. Protobuf](ch4-rpc/ch4-02-pb-intro.md)

207
ch3-asm/ch3-07-hack-asm.md Normal file
View File

@ -0,0 +1,207 @@
# 3.7. 汇编语言的威力
汇编语言的真正威力来自两个维度一是突破框架限制实现看似不可能的任务二是突破指令限制通过高级指令挖掘极致的性能。对于第一个问题我们将演示如何通过Go汇编语言直接访问系统调用和直接调用C语言函数。对于第二个问题我们将演示X64指令中AVX等高级指令的简单用法。
## 系统调用
系统调用是操作系统为外提供的公共接口。因为操作系统彻底接管了各种底层硬件设备因此操作系统提供的系统调用成了实现某些操作的唯一方法。从另一个角度看系统调用更像是一个RPC远程过程调用不过信道是寄存器和内存。在系统调用时我们向操作系统发送调用的编号和对应的参数然后阻塞等待系统调用地返回。因为涉及到阻塞等待因此系统调用期间的CPU利用率一般是可以忽略的。另一个和RPC地远程调用类似的地方是操作系统内核处理系统调用时不会依赖用户的栈空间一般不会导致爆栈发生。因此系统调用是最简单安全的一种调用了。
系统调用虽然简单但是它是操作系统对外的接口因此不同的操作系统调用规范可能有很大地差异。我们先看看Linux在AMD64架构上的系统调用规范`syscall/asm_linux_amd64.s`文件中有注释说明:
```go
//
// System calls for AMD64, Linux
//
// func Syscall(trap int64, a1, a2, a3 uintptr) (r1, r2, err uintptr);
// Trap # in AX, args in DI SI DX R10 R8 R9, return in AX DX
// Note that this differs from "standard" ABI convention, which
// would pass 4th arg in CX, not R10.
```
这是`syscall.Syscall`函数的内部注释简要说明了Linux系统调用的规范。系统调用的前6个参数直接由DI、SI、DX、R10、R8和R9寄存器传输结果由AX和DX寄存器返回。macOS等类UINX系统调用的参数传输大多数都采用类似的规则。
macOS的系统调用编号在`/usr/include/sys/syscall.h`头文件Linux的系统调用号在`/usr/include/asm/unistd.h`头文件。虽然在UNIX家族中是系统调用的参数和返回值的传输规则类似但是不同操作系统提供的系统调用却不是完全相同的因此系统调用编号也有很大的差异。以UNIX系统中著名的write系统调用为例在macOS的系统调用编号为4而在Linux的系统调用编号却是1。
我们将基于write系统调用包装一个字符串输出函数。下面的代码是macOS版本
```
// func SyscallWrite_Darwin(fd int, msg string) int
TEXT ·SyscallWrite_Darwin(SB), NOSPLIT, $0
MOVQ $(0x2000000+4), AX // #define SYS_write 4
MOVQ fd+0(FP), DI
MOVQ msg_data+8(FP), SI
MOVQ msg_len+16(FP), DX
SYSCALL
MOVQ AX, ret+0(FP)
RET
```
其中第一个参数是输出文件的文件描述符编号第二个参数是字符串的头部。字符串头部是由reflect.StringHeader结构定义第一成员是8字节的数据指针第二个成员是8字节的数据长度。在macOS系统中执行系统调用时还需要将系统调用的编号加上0x2000000后再行传入AX。然后再将fd、数据地址和长度作为write系统调用的三个参数输入分别对应DI、SI和DX三个寄存器。最后通过SYSCALL指令执行系统调用系统调用返回后从AX获取返回值。
这样我们就基于系统调用包装了一个定制的输出函数。在UNIX系统中标准输入stdout的文件描述符编号是1因此我们可以用1作为参数实现字符串的输出
```go
func SyscallWrite_Darwin(fd int, msg string) int
func main() {
if runtime.GOOS == "darwin" {
SyscallWrite_Darwin(1, "hello syscall!\n")
}
}
```
如果是Linux系统只需要将编号改为write系统调用对应的1即可。而Windows的系统调用则有另外的参数传输规则。在X64环境Windows的系统调用参数传输规则和默认的C语言规则非常相似在后续的直接调用C函数部分再行讨论。
## 直接调用C函数
在计算机的发展的过程中C语言和UNIX操作系统有着不可替代的作用。因此操作系统的系统调用、汇编语言和C语言函数调用规则几个技术是密切相关的。
在X86的32位系统时代C语言一般默认的是用栈传递参数并用AX寄存器返回结果称为cdecl调用约定。Go语言函数和cdecl调用约定非常相似它们都是以栈来传递参数并且返回地址和BP寄存器的布局都是类似的。但是Go语言函数将返回值也通过栈返回因此Go语言函数可以支持多个返回值。我们可以将Go语言函数看作是没有返回值的C语言函数同时将Go语言函数中的返回值挪到C语言函数参数的尾部这样栈不仅仅用于传入参数也用于返回多个结果。
在X64时代AMD架构增加了8个通用寄存器为了提高效率C语言也默认改用寄存器来传递参数。在X64系统默认有System V AMD64 ABI和Microsoft x64两种C语言函数调用规范。其中System V的规范适用于Linux、FreeBSD、macOS等诸多类UNIX系统而Windows则是用自己特有的调用规范。
在理解了C语言函数的调用规范之后汇编代码就可以绕过CGO技术直接调用C语言函数。为了便于演示我们先用C语言构造一个简单的加法函数myadd
```c
#include <stdint.h>
int64_t myadd(int64_t a, int64_t b) {
return a+b;
}
```
然后我们需要实现一个asmCallCAdd函数
```go
func asmCallCAdd(cfun uintptr, a, b int64) int64
```
因为Go汇编语言和CGO特性不能同时在一个包中使用因为CGO会调用gcc而gcc会将Go汇编语言当做普通的汇编程序处理从而导致错误我们通过一个参数传入C语言myadd函数的地址。asmCallCAdd函数的其余参数和C语言myadd函数的参数保持一致。
我们只实现System V AMD64 ABI规范的版本。在System V版本中寄存器可以最多传递六个参数分别对应DI、SI、DX、CX、R8和R9六个寄存器如果是浮点数则需要通过XMM寄存器传送返回值依然通过AX返回。通过对比系统调用的规范可以发现系统调用的第四个参数是用R10寄存器传递而C语言函数的第四个参数是用CX传递。
下面是System V AMD64 ABI规范的asmCallCAdd函数的实现
```
// System V AMD64 ABI
// func asmCallCAdd(cfun uintptr, a, b int64) int64
TEXT ·asmCallCAdd(SB), NOSPLIT, $0
MOVQ cfun+0(FP), AX // cfun
MOVQ a+8(FP), DI // a
MOVQ b+16(FP), SI // b
CALL AX
MOVQ AX, ret+24(FP)
RET
```
首先是将第一个参数表示的C函数地址保存到AX寄存器便于后续调用。然后分别将第二和第三个参数加载到DI和SI寄存器。然后CALL指令通过AX中保持的C语言函数地址调用C函数。最后从AX寄存器获取C函数的返回值并通过asmCallCAdd函数返回。
Win64环境的C语言调用规范类似。不过Win64规范中只有CX、DX、R8和R9四个寄存器传递参数如果是浮点数则需要通过XMM寄存器传送返回值依然通过AX返回。虽然是可以通过寄存器传输参数但是调用这依然要为前四个参数准备栈空间。需要注意的是Windows x64的系统调用和C语言函数可能是采用相同的调用规则。因为没有Windows测试环境我们这里就不提供了Windows版本的代码实现了Windows用户可以自己尝试实现类似功能。
然后我们就可以使用asmCallCAdd函数直接调用C函数了
```go
/*
#include <stdint.h>
int64_t myadd(int64_t a, int64_t b) {
return a+b;
}
*/
import "C"
import (
asmpkg "path/to/asm"
)
func main() {
if runtime.GOOS != "windows" {
println(asmpkg.asmCallCAdd(
uintptr(unsafe.Pointer(C.myadd)),
123, 456,
))
}
}
```
在上面的代码中,通过`C.myadd`获取C函数的地址然后转换为合适的类型再传人asmCallCAdd函数。在这个例子中汇编函数假设调用的C语言函数需要的栈很小可以直接复用Go函数中多余的空间。如果C语言函数可能需要较大的栈可以尝试像CGO那样切换到系统线程的栈上运行。
## AVX指令
从Go1.11开始Go汇编语言引入了AVX512指令的支持。AVX指令集是属于Intel家的SIMD指令集中的一部分。AVX512的最大特点是数据有512位宽度可以一次计算8个64位数或者是等大小的数据。因此AVX指令可以用于优化矩阵或图像等并行度很高的算法。不过并不是每个X86体系的CPU都支持了AVX指令因此首要的任务是如何判断CPU支持了哪些高级指令。
在Go语言标准库的`internal/cpu`包提供了CPU是否支持某些高级指令的基本信息但是只有标准库才能引用这个包因为internal路径的限制。该包底层是通过X86提供的CPUID指令来识别处理器的详细信息。最简便的方法是直接将`internal/cpu`包克隆一份。不过这个包为了避免复杂的依赖没有使用init函数自动初始化因此需要根据情况手工调整代码执行doinit函数初始化。
`internal/cpu`包针对X86处理器提供了以下特性检测
```go
package cpu
var X86 x86
// The booleans in x86 contain the correspondingly named cpuid feature bit.
// HasAVX and HasAVX2 are only set if the OS does support XMM and YMM registers
// in addition to the cpuid feature bit being set.
// The struct is padded to avoid false sharing.
type x86 struct {
HasAES bool
HasADX bool
HasAVX bool
HasAVX2 bool
HasBMI1 bool
HasBMI2 bool
HasERMS bool
HasFMA bool
HasOSXSAVE bool
HasPCLMULQDQ bool
HasPOPCNT bool
HasSSE2 bool
HasSSE3 bool
HasSSSE3 bool
HasSSE41 bool
HasSSE42 bool
}
```
因此我们可以用以下的代码测试运行时的CPU是否支持AVX2指令集
```go
import (
cpu "path/to/cpu"
)
func main() {
if cpu.X86.HasAVX2 {
// support AVX2
}
}
```
AVX512是比较新的指令集只有高端的CPU才会提供支持。为了主流的CPU也能运行代码测试我们选择AVX2指令来构造例子。AVX2指令每次可以处理32字节的数据可以用来提升数据复制的工作的效率。
下面的例子是用AVX2指令复制数据每次复制数据32字节倍数大小的数据
```
// func CopySlice_AVX2(dst, src []byte, len int)
TEXT ·CopySlice_AVX2(SB), NOSPLIT, $0
MOVQ dst_data+0(FP), DI
MOVQ src_data+24(FP), SI
MOVQ len+32(FP), BX
MOVQ $0, AX
LOOP:
VMOVDQU 0(SI)(AX*1), Y0
VMOVDQU Y0, 0(DI)(AX*1)
ADDQ $32, AX
CMPQ AX, BX
JL LOOP
RET
```
其中VMOVDQU指令先将`0(SI)(AX*1)`地址开始的32字节数据复制到Y0寄存器中然后再复制到`0(DI)(AX*1)`对应的目标内存中。VMOVDQU指令操作的数据地址可以不用对齐。
AVX2共有16个Y寄存器每个寄存器有256bit位。如果要复制的数据很多可以多个寄存器同时复制这样可以利用更高效的流水特性优化性能。

View File

@ -1,4 +1,4 @@
# 3.7. 例子Goroutine ID # 3.8. 例子Goroutine ID
在操作系统中每个进程都会有一个唯一的进程编号每个线程也有自己唯一的线程编号。同样在Go语言中每个Goroutine也有自己唯一的Go程编号这个编号在panic等场景下经常遇到。虽然Goroutine有内在的编号但是Go语言却刻意没有提供获取该编号的接口。本节我们尝试通过Go汇编语言获取Goroutine ID。 在操作系统中每个进程都会有一个唯一的进程编号每个线程也有自己唯一的线程编号。同样在Go语言中每个Goroutine也有自己唯一的Go程编号这个编号在panic等场景下经常遇到。虽然Goroutine有内在的编号但是Go语言却刻意没有提供获取该编号的接口。本节我们尝试通过Go汇编语言获取Goroutine ID。

View File

@ -1,41 +0,0 @@
# 3.8. 汇编语言的威力
汇编语言的真正威力来自两个维度一是突破框架限制实现看似不可能的任务二是突破指令限制通过高级指令挖掘极致的性能。对于一个问题我们将讨论如何通过Go汇编语言直接访问系统调用和直接调用C语言函数。对于第二个问题我们将简单讨论X64指令中AVX等高级指令的简单用法。
## 系统调用
系统调用是操作系统为外提供地公共接口。因为操作系统彻底接管了各种底层硬件设备因此操作系统提供的系统调用成了某些操作地唯一方法。从另一个角度看系统调用更像是一个RPC远程过程调用。在系统调用时我们只是像操作系统发送某个调用地编号和对应地参数然后阻塞等待系统调用地返回。因为涉及到阻塞等待因此系统调用期间对应CPU的利用率一般是可以忽略地。同时系统调用时类似RPC地远程调用操作系统内核处理系统调用时不会依赖用户的栈空间一般不会导致爆栈发生。因此可以认为系统调用是最简单的一种调用了。
系统调用虽然是最简单地调用但是它是操作系统对外的接口因此不同的操作系统调用规范可能有很大地差异。我们先关注Linux在AMD64架构上的系统调用规范`syscall/asm_linux_amd64.s`文件中有注释说明:
```go
//
// System calls for AMD64, Linux
//
// func Syscall(trap int64, a1, a2, a3 uintptr) (r1, r2, err uintptr);
// Trap # in AX, args in DI SI DX R10 R8 R9, return in AX DX
// Note that this differs from "standard" ABI convention, which
// would pass 4th arg in CX, not R10.
```
这是`syscall.Syscall`函数的内部注释简要说明了Linux系统调用的规范。系统调用一般不超过6个参数直接由DI、SI、DX、R10、R8和R9寄存器传输结果由AX和DX寄存器返回。
TODO
## 直接调用C函数
TODO
## AVX指令
TODO
<!--
学习汇编的原因是汇编可以挖掘功能
同时也可以挖掘性能
syscall类似rpc没有栈爆问题
cgoall
avx2
-->

View File

@ -1,4 +1,4 @@
# 3.8. Delve调试器 # 3.9. Delve调试器
目前Go语言支持GDB、LLDB和Delve几种调试器。其中GDB是最早支持的调试工具LLDB是macOS系统推荐的标准调试工具。但是GDB和LLDB对Go语言的专有特性都缺乏很大支持而只有Delve是专门为Go语言设计开发的调试工具。而且Delve本身也是采用Go语言开发对Windows平台也提供了一样的支持。本节我们基于Delve简单解释如何调试Go汇编程序。 目前Go语言支持GDB、LLDB和Delve几种调试器。其中GDB是最早支持的调试工具LLDB是macOS系统推荐的标准调试工具。但是GDB和LLDB对Go语言的专有特性都缺乏很大支持而只有Delve是专门为Go语言设计开发的调试工具。而且Delve本身也是采用Go语言开发对Windows平台也提供了一样的支持。本节我们基于Delve简单解释如何调试Go汇编程序。

View File

@ -1,4 +1,4 @@
## 3.9. 补充说明 ## 3.10. 补充说明
如果是纯粹学习汇编语言则可以从《深入理解程序设计使用Linux汇编语言》开始该书讲述了如何以C语言的思维变现汇编程序。如果是学习X86汇编则可以从《汇编语言基于x86处理器》一开始然后再结合《现代x86汇编语言程序设计》学习AVX等高级汇编指令的使用。 如果是纯粹学习汇编语言则可以从《深入理解程序设计使用Linux汇编语言》开始该书讲述了如何以C语言的思维变现汇编程序。如果是学习X86汇编则可以从《汇编语言基于x86处理器》一开始然后再结合《现代x86汇编语言程序设计》学习AVX等高级汇编指令的使用。

View File

@ -17,6 +17,13 @@ import (
) )
func main() { func main() {
if runtime.GOOS == "darwin" {
asmpkg.SyscallWrite_Darwin(1, "hello syscall!\n")
}
if runtime.GOOS == "linux" {
asmpkg.SyscallWrite_Linux(1, "hello syscall!\n")
}
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
fmt.Println(asmpkg.CallCAdd_Win64_ABI( fmt.Println(asmpkg.CallCAdd_Win64_ABI(
uintptr(unsafe.Pointer(C.myadd)), uintptr(unsafe.Pointer(C.myadd)),
@ -28,4 +35,9 @@ func main() {
123, 456, 123, 456,
)) ))
} }
var dst = make([]byte, 32)
var src = []byte("1234567890123456789012345678901234567890")
asmpkg.CopySlice_AVX2(dst, src, 32)
fmt.Println(string(dst))
} }

View File

@ -2,3 +2,9 @@ package asmpkg
func CallCAdd_SystemV_ABI(cfun uintptr, a, b int64) int64 func CallCAdd_SystemV_ABI(cfun uintptr, a, b int64) int64
func CallCAdd_Win64_ABI(cfun uintptr, a, b int64) int64 func CallCAdd_Win64_ABI(cfun uintptr, a, b int64) int64
func SyscallWrite_Darwin(fd int, msg string) int
func SyscallWrite_Linux(fd int, msg string) int
func SyscallWrite_Windows(fd int, msg string) int
func CopySlice_AVX2(dst, src []byte, len int)

View File

@ -112,3 +112,42 @@ TEXT ·CallCAdd_Win64_ABI(SB), NOSPLIT, $0
CALL AX CALL AX
MOVQ AX, ret+24(FP) MOVQ AX, ret+24(FP)
RET RET
// func SyscallWrite_Darwin(fd int, msg string) int
TEXT ·SyscallWrite_Darwin(SB), NOSPLIT, $0
MOVQ $(0x2000000+4), AX // #define SYS_write 4
MOVQ fd+0(FP), DI
MOVQ msg_data+8(FP), SI
MOVQ msg_len+16(FP), DX
SYSCALL
MOVQ AX, ret+0(FP)
RET
// func SyscallWrite_Linux(fd int, msg string) int
TEXT ·SyscallWrite_Linux(SB), NOSPLIT, $0
MOVQ $1, AX // #define SYS_write 1
MOVQ fd+0(FP), DI
MOVQ msg_data+8(FP), SI
MOVQ msg_len+16(FP), DX
SYSCALL
MOVQ AX, ret+0(FP)
RET
// func SyscallWrite_Windows(fd int, msg string) int
TEXT ·SyscallWrite_Windows(SB), NOSPLIT, $0
RET // TODO
// func CopySlice_AVX2(dst, src []byte, len int)
TEXT ·CopySlice_AVX2(SB), NOSPLIT, $0
MOVQ dst_data+0(FP), DI
MOVQ src_data+24(FP), SI
MOVQ len+32(FP), BX
MOVQ $0, AX
LOOP:
VMOVDQU 0(SI)(AX*1), Y0
VMOVDQU Y0, 0(DI)(AX*1)
ADDQ $32, AX
CMPQ AX, BX
JL LOOP
RET