问题1: 包装方式是什么?
我们来验证一下打包方法是否存在:
首先定义一个Point类型来表示一维坐标系中的一个点,并按照Go语言风格为其实现一个Get方法和一个Set方法。
package gom
type Point struct {
x float64
}
func (p Point) X() float64 {
return p.x
}
func (p *Point) SetX(x float64) {
p.x = x
}
然后,仅编译而不链接得到OBJ文件,然后对编译后的OBJ文件进行反编译和分析。 编译命令如下:
$ go tool compile -trimpath="`pwd`=>" -l -p gom point.go
上述命令禁用内联优化。 编译完成后,会在当前工作目录下生成一个point.o文件,这就是我们想要的OBJ文件。
接下来使用go工具nm查看文件中实现了哪些功能。 nm 将输出 OBJ 文件中定义或使用的符号信息。 使用grep命令过滤代码段符号对应的T标记,可以查看文件中实现的功能。 功能:
$ go tool nm point.o | grep T
1562 T gom.(*Point).SetX
1899 T gom.(*Point).X
1555 T gom.Point.X
可以看到point.o中实现了3个方法,它们都定义在Point类型所在的gom包中:
第一个是Point的SetX方法,其接收者类型是*Point,第三个是Point的X方法,其接收者类型是Point,与源码一致。
比较奇怪的是第二个方法,它是一个X方法,接收者类型是*Point。 源码中没有这个方法。 它是怎么来的? 只能由编译器生成。
编译器会为接收者是值类型的方式生成一种接收者是指针类型的方式,也就是所谓的“包装方式”。
那么编译器为什么会生成它呢?
问题2:为什么要生成包装器?
如果是支持通过指针直接调用值接收者的方法,那么在调用端直接解引用指针就可以了,这样就不需要生成一个包装方法了?
为了验证这个问题,作者又写了一个函数来反编译:
实验:封装方法是否支持通过指针直接调用值接收者的方法
func PointX(p *Point) float64 {
return p.X()
}
大致思路是:用指针调用值接收者方法,然后反编译看看实际调用的方法是否是包装方法。 反编译得到的汇编代码如下:
go tool objdump -S -s '^gom.PointX$' point.o
TEXT gom.PointX(SB) gofile..point.go
func PointX(p *Point) float64 {
0x1a17 65488b0c2528000000 MOVQ GS:0x28, CX
0x1a20 488b8900000000 MOVQ 0(CX), CX [3:7]R_TLS_LE
0x1a27 483b6110 CMPQ 0x10(CX), SP
0x1a2b 7637 JBE 0x1a64
0x1a2d 4883ec18 SUBQ $0x18, SP
0x1a31 48896c2410 MOVQ BP, 0x10(SP)
0x1a36 488d6c2410 LEAQ 0x10(SP), BP
return p.X()
0x1a3b 488b442420 MOVQ 0x20(SP), AX
0x1a40 f20f1000 MOVSD_XMM 0(AX), X0
0x1a44 f20f110424 MOVSD_XMM X0, 0(SP)
0x1a49 e800000000 CALL 0x1a4e [1:5]R_CALL:gom.Point.X
0x1a4e f20f10442408 MOVSD_XMM 0x8(SP), X0
0x1a54 f20f11442428 MOVSD_XMM X0, 0x28(SP)
0x1a5a 488b6c2410 MOVQ 0x10(SP), BP
0x1a5f 4883c418 ADDQ $0x18, SP
0x1a63 c3 RET
func PointX(p *Point) float64 {
0x1a64 e800000000 CALL 0x1a69 [1:5]R_CALL:runtime.morestack_noctxt
0x1a69 ebac JMP gom.PointX(SB)
可以看到,pX()实际上是在调用端解引用了指针,然后调用值接收者方法(本质上是编译器提供的语法糖),并没有调用编译器生成的包装方法。 那么这种封装方式有什么用呢?
真正的触发
之前我们已经介绍过socket的数据结构iface,它包括itab指针和data指针,data指针存储的是数据的地址。
type iface struct {
tab *itab
data unsafe.Pointer
}
对于socket来说,调用指针接收者方法时传递地址是非常方便的objdump 反编译源码,而且不需要关心数据的具体类型,地址的大小总是相同的。
如果通过socket调用值接收器方法,则需要通过socket中的数据指针将数据的值复制到堆栈中。 由于在编译阶段无法确定socket背后的具体类型,因此编译器无法生成相关指令来完成复制,所以换句话说,接口不能直接使用值接收方法,这就是编译器生成的根本原因包装方法。
那么,有没有办法让socket间接使用值接收者方法呢?
还记得介绍defer相关内容时提到的runtime.reflectcall函数吗? 它还可以在运行时动态复制参数并完成函数调用。
如果是基于reflectcall的话,是否可以通过socket调用值接收者呢? 这绝对是可以实现的。 接口的itab中有特定类型的元数据,reflectcall确实可以应用。 但有一个明显的问题:性能太差。 与几条传递参数的 MOV 指令加上一条普通的 CALL 指令相比,reflectcall 的开销太大objdump 反编译源码,因此 Go 语言选择为值接收者方法生成一个包装方法。
但如果你反编译或者使用nm命令分析可执行文件,你会发现不仅是这种打包方法,而且代码中原来的方法也可能不存在于可执行文件中。 进展如何?
延伸问题:
为什么并非所有包装器都存在于可执行文件中
(未完待续~)