diff --git a/ch2-cgo/ch2-03-cgo-types.md b/ch2-cgo/ch2-03-cgo-types.md index 71d0310..47584bd 100644 --- a/ch2-cgo/ch2-03-cgo-types.md +++ b/ch2-cgo/ch2-03-cgo-types.md @@ -380,6 +380,9 @@ p = (*X)(unsafe.Pointer(q)) // *Y => *X ![](../images/ch2.3-1-x-ptr-to-y-ptr.uml.png) +*图 2.3-1 X类型指针转Y类型指针* + + 任何类型的指针都可以通过强制转换为`unsafe.Pointer`指针类型去掉原有的类型信息,然后再重新赋予新的指针类型而达到指针间的转换的目的。 ## 2.3.6 数值和指针的转换 @@ -392,6 +395,9 @@ p = (*X)(unsafe.Pointer(q)) // *Y => *X ![](../images/ch2.3-2-int32-to-char-ptr.uml.png) +*图 2.3-2 int32和`char*`指针转换* + + 转换分为几个阶段,在每个阶段实现一个小目标:首先是int32到uintptr类型,然后是uintptr到`unsafe.Pointr`指针类型,最后是`unsafe.Pointr`指针类型到`*C.char`类型。 ## 2.3.7 切片间的转换 @@ -418,4 +424,7 @@ pHdr.Cap = qHdr.Cap * unsafe.Sizeof(q[0]) / unsafe.Sizeof(p[0]) ![](../images/ch2.3-3-x-slice-to-y-slice.uml.png) +*图 2.3-3 X类型切片转Y类型切片* + + 针对CGO中常用的功能,作者封装了 "github.com/chai2010/cgo" 包,提供基本的转换功能,具体的细节可以参考实现代码。 diff --git a/ch2-cgo/ch2-05-internal.md b/ch2-cgo/ch2-05-internal.md index 8a7cfe8..6bff6f1 100644 --- a/ch2-cgo/ch2-05-internal.md +++ b/ch2-cgo/ch2-05-internal.md @@ -10,6 +10,9 @@ ![](../images/ch2.5-1-cgo-generated-files.dot.png) +*图 2.5-1 cgo生成的中间文件* + + 包中有4个Go文件,其中nocgo开头的文件中没有`import "C"`指令,其它的2个文件则包含了cgo代码。cgo命令会为每个包含了cgo代码的Go文件创建2个中间文件,比如 main.go 会分别创建 main.cgo1.go 和 main.cgo2.c 两个中间文件。然后会为整个包创建一个 `_cgo_gotypes.go` Go文件,其中包含Go语言部分辅助代码。此外还会创建一个 `_cgo_export.h` 和 `_cgo_export.c` 文件,对应Go语言导出到C语言的类型和函数。 ## 2.5.2 Go调用C函数 @@ -134,6 +137,8 @@ void _cgo_506f45f9fa85_Cfunc_sum(void *v) { ![](../images/ch2.5-2-call-c-sum-v1.uml.png) +*图 2.5-2 调用C函数* + 其中`runtime.cgocall`函数是实现Go语言到C语言函数跨界调用的关键。更详细的细节可以参考 https://golang.org/src/cmd/cgo/doc.go 内部的代码注释和 `runtime.cgocall` 函数的实现。 ## 2.5.3 C调用Go函数 @@ -245,5 +250,7 @@ func runtime.cgocallback(fn, frame unsafe.Pointer, framesize, ctxt uintptr) ![](../images/ch2.5-3-call-c-sum-v2.uml.png) +*图 2.5-3 调用导出的Go函数* + 其中`runtime.cgocallback`函数是实现C语言到Go语言函数跨界调用的关键。更详细的细节可以参考相关函数的实现。 diff --git a/ch3-asm/ch3-02-arch.md b/ch3-asm/ch3-02-arch.md index 8ea14b0..125c753 100644 --- a/ch3-asm/ch3-02-arch.md +++ b/ch3-asm/ch3-02-arch.md @@ -67,6 +67,9 @@ ![](../images/ch3.2-1-arch-hsm-zero.jpg) +*图 3.2-1 人力资源机器* + + 整个程序只有一个输入指令、一个输出指令和两个跳转指令共四个指令: ``` @@ -88,6 +91,9 @@ X86其实是是80X86的简称(后面三个字母),包括Intel 8086、80286 ![](../images/ch3.2-2-arch-amd64-01.ditaa.png) +*图 3.2-2 AMD64架构* + + 左边是内存部分是常见的内存布局。其中text一般对应代码段,用于存储要执行指令数据,代码段一般是只读的。然后是rodata和data数据段,数据段一般用于存放全局的数据,其中rodata是只读的数据段。而heap段则用于管理动态的数据,stack段用于管理每个函数调用时相关的数据。在汇编语言中一般重点关注text代码段和data数据段,因此Go汇编语言中专门提供了对应TEXT和DATA命令用于定义代码和数据。 中间是X86提供的寄存器。寄存器是CPU中最重要的资源,每个要处理的内存数据原则上需要先放到寄存器中才能由CPU处理,同时寄存器中处理完的结果需要再存入内存。X86中除了状态寄存器FLAGS和指令寄存器IP两个特殊的寄存器外,还有AX、BX、CX、DX、SI、DI、BP、SP几个通用寄存器。在X86-64中又增加了八个以R8-R15方式命名的通用寄存器。因为历史的原因R0-R7并不是通用寄存器,它们只是X87开始引入的MMX指令专有的寄存器。在通用寄存器中BP和SP是两个比较特殊的寄存器:其中BP用于记录当前函数帧的开始位置,和函数调用相关的指令会隐式地影响SP的值;SP则对应当前栈指针的位置,和栈相关的指令会隐式地影响SP的值;而某些调试工具需要BP寄存器才能正常工作。 @@ -103,6 +109,9 @@ Go汇编为了简化汇编代码的编写,引入了PC、FP、SP、SB四个伪 ![](../images/ch3.2-3-arch-amd64-02.ditaa.png) +*图 3.2-3 Go汇编的伪寄存器* + + 在AMD64环境,伪PC寄存器其实是IP指令计数器寄存器的别名。伪FP寄存器对应的是函数的帧指针,一般用来访问函数的参数和返回值。伪SP栈指针对应的是当前函数栈帧的底部(不包括参数和返回值部分),一般用于定位局部变量。伪SP是一个比较特殊的寄存器,因为还存在一个同名的SP真寄存器。真SP寄存器对应的是栈的顶部,一般用于定位调用其它函数的参数和返回值。 当需要区分伪寄存器和真寄存器的时候只需要记住一点:伪寄存器一般需要一个标识符和偏移量为前缀,如果没有标识符前缀则是真寄存器。比如`(SP)`、`+8(SP)`没有标识符前缀为真SP寄存器,而`a(SP)`、`b+8(SP)`有标识符为前缀表示伪寄存器。 diff --git a/ch3-asm/ch3-03-const-and-var.md b/ch3-asm/ch3-03-const-and-var.md index fe80e90..1fd6c11 100644 --- a/ch3-asm/ch3-03-const-and-var.md +++ b/ch3-asm/ch3-03-const-and-var.md @@ -112,6 +112,9 @@ DATA ·num+8(SB)/8,$0 ![](../images/ch3.3-1-pkg-var-decl-01.ditaa.png) +*图 3.3-1 变量定义* + + 汇编代码中并不需要NOPTR标志,因为Go编译器会从Go语言语句声明的`[2]int`类型中推导出该变量内部没有指针数据。 @@ -173,6 +176,9 @@ IEEE754标准中,最高位1bit为符号位,然后是指数位(指数为采 ![](../images/ch3.3-2-ieee754.jpg) +*图 3.3-2 IEEE754浮点数结构* + + IEEE754浮点数还有一些奇妙的特性:比如有正负两个0;除了无穷大和无穷小Inf还有非数NaN;同时如果两个浮点数有序那么对应的有符号整数也是有序的(反之则不一定成立,因为浮点数中存在的非数是不可排序的)。浮点数是程序中最难琢磨的角落,因为程序中很多手写的浮点数字面值常量根本无法精确表达,浮点数计算涉及到的误差舍入方式可能也的随机的。 下面是在Go语言中声明两个浮点数(如果没有在汇编中定义变量,那么声明的同时也会定义变量)。 @@ -302,12 +308,18 @@ func makechan(chanType *byte, size int) (hchan chan any) ![](../images/ch3.3-3-pkg-var-decl-02.ditaa.png) +*图 3.3-3 变量定义* + + 变量在data段分配空间,数组的元素地址依次从低向高排列。 然后再查看下标准库图像包中`image.Point`结构体类型变量的内存布局: ![](../images/ch3.3-4-pkg-var-decl-03.ditaa.png) +*图 3.3-4 结构体变量定义* + + 变量也时在data段分配空间,变量结构体成员的地址也是依次从低向高排列。 因此`[2]int`和`image.Point`类型底层有着近似相同的内存布局。 diff --git a/ch3-asm/ch3-04-func.md b/ch3-asm/ch3-04-func.md index 5b1568b..d1b1cb9 100644 --- a/ch3-asm/ch3-04-func.md +++ b/ch3-asm/ch3-04-func.md @@ -40,6 +40,9 @@ TEXT ·Swap(SB), NOSPLIT, $0 ![](../images/ch3.4-1-func-decl-01.ditaa.png) +*图 3.4-1 函数定义* + + 第一种是最完整的写法:函数名部分包含了当前包的路径,同时指明了函数的参数大小为32个字节(对应参数和返回值的4个int类型)。第二种写法则比较简洁,省略了当前包的路径和参数的大小。如果有NOSPLIT标注,会禁止汇编器为汇编函数插入栈分裂的代码。NOSPLIT对应Go语言中的`//go:nosplit`注释。 目前可能遇到的函数标志有NOSPLIT、WRAPPER和NEEDCTXT几个。其中NOSPLIT不会生成或包含栈分裂代码,这一般用于没有任何其它函数调用的叶子函数,这样可以适当提高性能。WRAPPER标志则表示这个是一个包装函数,在panic或runtime.caller等某些处理函数帧的地方不会增加函数帧计数。最后的NEEDCTXT表示需要一个上下文参数,一般用于闭包函数。 @@ -81,6 +84,7 @@ TEXT ·Swap(SB), $0-32 ![](../images/ch3.4-2-func-decl-02.ditaa.png) +*图 3.4-2 函数定义* 下面的代码演示了如何在汇编函数中使用参数和返回值: @@ -138,6 +142,8 @@ Foo函数的参数和返回值的大小和内存布局: ![](../images/ch3.4-3-func-arg-01.ditaa.png) +*图 3.4-3 函数的参数* + 下面的代码演示了Foo汇编函数参数和返回值的定位: @@ -210,6 +216,9 @@ func Foo() { ![](../images/ch3.4-4-func-local-var-01.ditaa.png) +*图 3.4-4 函数的局部变量* + + 从图中可以看出Foo函数局部变量和前一个例子中参数和返回值的内存布局是完全一样的,这也是我们故意设计的结果。但是参数和返回值是通过伪FP寄存器定位的,FP寄存器对应第一个参数的开始地址(第一个参数地址较低),因此每个变量的偏移量是正数。而局部变量是通过伪SP寄存器定位的,而伪SP寄存器对应的是第一个局部变量的结束地址(第一个局部变量地址较大),因此每个局部变量的便宜量都是负数。 ## 3.4.5 调用其它函数 @@ -241,6 +250,9 @@ func sum(a, b int) int { ![](../images/ch3.4-5-func-call-frame-01.ditaa.png) +*图 3.4-5 函数帧* + + 为了便于理解,我们对真实的内存布局进行了简化。要记住的是调用函数时,被调用函数的参数和返回值内存空间都必须由调用者提供。因此函数的局部变量和为调用其它函数准备的栈空间总和就确定了函数帧的大小。调用其它函数前调用方要选择保存相关寄存器到栈中,并在调用函数返回后选择要恢复的寄存器进行保存。最终通过CALL指令调用函数的过程和调用我们熟悉的调用println函数输出的过程类似。 Go语言中函数调用是一个复杂的问题,因为Go函数不仅仅要了解函数调用参数的布局,还会涉及到栈的跳转,栈上局部变量的生命周期管理。本节只是简单了解函数调用参数的布局规则,在后续的章节中会更详细的讨论函数的细节。 diff --git a/ch3-asm/ch3-06-func-again.md b/ch3-asm/ch3-06-func-again.md index f8e78ab..f1f7070 100644 --- a/ch3-asm/ch3-06-func-again.md +++ b/ch3-asm/ch3-06-func-again.md @@ -11,6 +11,9 @@ ![](../images/ch3.6-1-func-stack-frame-layout-01.ditaa.png) +*图 3.6-1 函数调用参数布局* + + 首先是调用函数前准备的输入参数和返回值空间。然后CALL指令将首先触发返回地址入栈操作。在进入到被调用函数内之后,汇编器自动插入了BP寄存器相关的指令,因此BP寄存器和返回地址是紧挨着的。再下面就是当前函数的局部变量的空间,包含再次调用其它函数需要准备的调用参数空间。被调用的函数执行RET返回指令时,先从栈恢复BP和SP寄存器,接着取出的返回地址跳转到对应的指令执行。 diff --git a/ch4-rpc/ch4-06-grpc-ext.md b/ch4-rpc/ch4-06-grpc-ext.md index 1b44291..5cc5761 100644 --- a/ch4-rpc/ch4-06-grpc-ext.md +++ b/ch4-rpc/ch4-06-grpc-ext.md @@ -167,6 +167,9 @@ grpc-gateway的工作原理如下图: ![](../images/ch4.6-1-grpc-gateway.png) +*图 4.6-1 Grpc-Gateway工作流程* + + 通过在Protobuf文件中添加路由相关的元信息,通过自定义的代码插件生成路由相关的处理代码,最终将REST请求转给更后端的GRPC服务处理。 路由扩展元信息也是通过Protobuf的元数据扩展用法提供: