原副标题:Go词汇示例 | 旋量群的同时实现基本原理
说到Go词汇的旋量群,较为简单的体会是个有状况的Function Value。
在Go词汇中较为众所周知的旋量群情景是在某一表达式内表述了另两个表达式,内层表达式采用了内层表达式的codice,因此内层表达式最后被内层表达式做为回到值回到,标识符如下表所示:
//第3章/code_3_19.go
funcmc(n int) funcint{
returnfuncint{
returnn
}
}
每天调用mc表达式单厢回到两个捷伊旋量群,旋量群读懂了模块n的值,因此是有状况的。如前所述现阶段对表达式栈帧的介绍,表达式栈帧随著表达式回到而封存,无法用以留存状况,科学研究表达式指针和Function Value的这时候也没辨认出这儿用以留存状况,因此这儿就有个难题: 旋量群的状况留存在这儿呢?
01
旋量群第一类
为的是弄清楚那个难题,先来试著呵呵LLVM,从编订标识符中找标准答案,LLVM标识符如下表所示:
$ gotool objdump -S -s ^main.mc$gom.exe
TEXT main.mc(SB) C:/gopath/src/fengyoulin.com/gom/code_3_19. go
funcmc(n int) funcint{
0x488ec065488b0c2528000000 MOVQ GS: 0x28, CX
0x488ec9488b8900000000 MOVQ 0(CX), CX
0x488ed0 483b6110 CMPQ 0x10(CX), SP
0x488ed4 7645JBE 0x488f1b
0x488ed6 4883ec18 SUBQ $ 0x18, SP
0x488eda 48896c2410 MOVQ BP, 0x10(SP)
0x488edf488d6c2410 LEAQ 0x10(SP), BP
returnfuncint{
0x488ee4488d0595640100 LEAQ RunTime.types+ 91008(SB), AX
0x488eeb48890424MOVQ AX, 0(SP)
0x488eefe89c34f8ff CALL RunTime.newobject(SB)
0x488ef4 488b442408 MOVQ 0x8(SP), AX
0x488ef9 488d0d30000000 LEAQ main.mc.func1(SB), CX
0x488f00 488908MOVQ CX, 0(AX)
0x488f03 488b4c2420 MOVQ0x20(SP), CX
0x488f08 48894808MOVQ CX, 0x8(AX)
0x488f0c 4889442428MOVQ AX, 0x28(SP)
0x488f11 488b6c2410 MOVQ0x10(SP), BP
0x488f16 4883c418 ADDQ $ 0x18, SP
0x488f1a c3 RET
funcmc(n int) funcint{
0x488f1b e82020fdff CALL RunTime.morestack_noctxt(SB)
0x488f20 eb9e JMP main.mc(SB)
标识符中负责栈增长、栈帧分配和操作BP的部分在3.3.2节已经介绍过,此
(1) 第1~4行标识符采用runtime.types+91008做为模块调用了runtime.newobject表达式,并把回到值存储在AX寄存器中,那个值是个地址,指向分配在堆上的两个第一类。
(2) 第5行和第6行把main.mc.func1表达式的地址复制到了AX所指向第一类的头部,0(AX)表示用AX做为基址且位移为0。
(3) 第7行和第8行把mc表达式的模块n的值复制到了AX 所指向第一类的第2个字段,0x8(AX)表示用AX做为基址且位移为8。
(4) 第9 行把AX 的值复制到mc表达式栈帧上的回到值处,也是最后回到的Function Value。
根据第2步和第3步的标识符逻辑,可以推断出第1步动态分配的第一类的类型。应该是个struct类型,第1个字段是个表达式地址,第2个字段是int类型,标识符如下表所示:
struct{
F uintptr
n int
}
旋量群第一类的成员可以进一步划分,第1个字段F用以存储目标表达式的地址,这在所有的旋量群第一类中都是一致的,后文中将那个目标表达式称为旋量群表达式。从第2个字段开始,后续的字段称为旋量群的捕获列表,也是内层表达式中用到的所有表述在内层表达式中的变量。编译器认为这些变量被旋量群捕获了,会把它们追加到旋量群第一类的struct表述中。上例中只捕获了两个变量n,如果捕获的变量增多,struct的捕获列表也会加长。两个捕获两个变量的旋量群示例标识符如下表所示:
//第3章/code_3_20.go
funcmc2(a, b int) func( int, int) {
returnfunc( int, int) {
returna, b
}
}
上述标识符对应的旋量群第一类表述标识符如下表所示:
struct{
F uintptr
a int
b int
}
02
看到旋量群
通过LLVM来逆向推断旋量群第一类的结构还是较为烦琐的,如果能有一种方法,能够简单地看到旋量群对象的结构表述,那真是再好不过了。下面介绍一种方法,将旋量群逮个正着。
根据之前的探索,已经知道Go程序在运行阶段会通过runtime.newobject表达式动态分配旋量群第一类。Go源码中newobject函数的原型如下表所示:
funcnewobject(typ *_type)unsafe. Pointer
表达式的回到值是个指针,也是新分配的第一类的地址,模块是个_type类型的指针。通过源码可以得知那个_type是个struct,在Go词汇的runtime中被用以描述两个数据类型,通过它可以找到目标数据类型的大小、对齐边界、类型名称等。笔者习惯将这些用以描述数据类型的数据称为类型元数据,它们是由编译器生成的,Go词汇的反射机制依赖的是这些类型元数据。
假如能够获得传递给runtime.newobject表达式的类型元数据指针typ,再通过反射进行解析,就能打印出旋量群第一类的结构表述了。那如何才能获得那个typ模块呢?
在C词汇中有种常用的表达式Hook技术,是在运行阶段将目标表达式头部的标识符替换为一条跳转指令,跳转到两个捷伊表达式。在x86平台上是在进程地址空间中找到要Hook的表达式,将其头部替换为一条JMP指令,同时指定JMP指令要跳转到的新表达式的地址。这项技术在Go 程序中依然适用,可以用两个自己同时实现的表达式替换掉runtime.newobject表达式,在那个表达式中就能获得typ模块并进行解析了。
还有一个难题是runtime.newobject表达式属于未导出的表达式,在runtime包外无法访问。这一点可以通过linkname机制来绕过,在当前包中声明两个类似的表达式,让链接器将其链接到runtime.newobject表达式即可。
本书采用开源模块github.com/fengyoulin/hookingo同时实现运行阶段表达式替换,打印旋量群第一类结构的完整标识符如下表所示:
//第3章/code_3_21.go
packagemain
import(
“github.com/fengyoulin/hookingo”
“reflect”
“unsafe”
)
varhno hookingo.Hook
//go:linkname newobject RunTime.newobject
funcnewobject(typ unsafe.Pointer)unsafe. Pointer
funcfno(typ unsafe.Pointer)unsafe. Pointer{
t := reflect.TypeOf( 0)
(*(*[ 2]unsafe.Pointer)(unsafe.Pointer(&t)))[ 1] = typ //相当于反射了旋量群对象类型
println(t.String)
iffn, ok := hno.Origin.( func(typ unsafe.Pointer)unsafe. Pointer); ok{
returnfn(typ)//调用原RunTime.newobject
}
returnnil
}
//创建两个旋量群,make closure
funcmc(start int) funcint{
returnfuncint{
start++
returnstart
}
}
funcmain{
varerr error
hno, err = hookingo.Apply(newobject, fno) //应用钩子,替换表达式
iferr != nil{
panic(err)
}
f := mc( 10)
println(f)
}
在64位Windows 10下执行命令及运行结果如下表所示:
$ ./code_3_21.exe
int
struct{ F uintptr; start *int}
11
运行结果第2行的int和第3行的struct表述都是被fno表达式中的println表达式打印出来的,最后一行的11是被main表达式中的println表达式打印出来的。第3行的struct就是旋量群第一类的结构表述,旋量群捕获列表中的start是个int指针,那是因为start变量逃逸了,第2行打印的int是通过runtime.newobject表达式动态分配造成的。如果把旋量群表达式中的start++一行删除,旋量群捕获的start是个值而不是指针,本节的最后将解释旋量群捕获与变量逃逸的关系。某些读者可能会对fno表达式中的反射标识符感到困惑,读完本书第5章与接口相关的内容就能够理解了。
此时再回过头去看Function Value的两级指针结构,结合旋量群第一类的结构表述就很好理解了。如果忽略掉旋量群第一类中的捕获列表部分,剩下的是两个两级指针结构了,如图3-12所示。
Go词汇在设计上用这种两级指针结构将表达式指针和旋量群统一为Function Value,运行阶段调用者不需要关心调用的表达式是个普通的表达式还是个旋量群表达式,一致对待就可以了。
如果每天把两个普通表达式赋值给两个Function Value的这时候都要在堆上分配两个指针,那就有些浪费了。因为普通表达式不构成旋量群也没捕获列表,没必要动态分配。事实上编译器早就考虑到了这一点,对于不构成旋量群的Function Value,第二层的那个指针是编译阶段静态分配的,只分配两个就够了。
■ 图3-12Function Value和旋量群第一类
下期预告
Go词汇示例:堆内存管理之heapArena
Go词汇示例:类型系统
03
参考书籍
书名:深度探索Go词汇——第一类模型与runtime的基本原理、特性及应用
扫码京东优惠购书