引言
gomonkey 是 Go 的一款打桩框架,目标是让用户在单元测试中低成本的完成打桩,从而将精力聚焦于业务功能的开发。gomonkey 接口友好,功能强大,目前已被很多项目使用,用户遍及世界多个国家。
提出问题
众所周知,这几年基于 arm64 架构的设备越来越多,包括服务器和终端等,尤其是 Apple M1芯片发布后,国内外多个 gopher 在 github 上提了希望 gomonkey 支持 arm64 架构 的 Issues
。
解决问题
新增一个 go 文件 jmp_arm64.go
,实现 arm64 架构下的构建跳转指令函数 buildJmpDirective
。
buildJmpDirective
直接上代码:
func buildJmpDirective(double uintptr) []byte {
res := make([]byte, 0, 24)
d0d1 := double & 0xFFFF
d2d3 := double >> 16 & 0xFFFF
d4d5 := double >> 32 & 0xFFFF
d6d7 := double >> 48 & 0xFFFF
res = append(res, movImm(0B10, 0, d0d1)...) // MOVZ x26, double[16:0]
res = append(res, movImm(0B11, 1, d2d3)...) // MOVK x26, double[32:16]
res = append(res, movImm(0B11, 2, d4d5)...) // MOVK x26, double[48:32]
res = append(res, movImm(0B11, 3, d6d7)...) // MOVK x26, double[64:48]
res = append(res, []byte{0x4A, 0x03, 0x40, 0xF9}...) // LDR x10, [x26]
res = append(res, []byte{0x40, 0x01, 0x1F, 0xD6}...) // BR x10
return res
}
amd64 架构是 CISC 指令集,因此可以直接把立即数存放在指令中:
func buildJmpDirective(double uintptr) []byte {
d0 := byte(double)
d1 := byte(double >> 8)
d2 := byte(double >> 16)
d3 := byte(double >> 24)
d4 := byte(double >> 32)
d5 := byte(double >> 40)
d6 := byte(double >> 48)
d7 := byte(double >> 56)
return []byte{
0x48, 0xBA, d0, d1, d2, d3, d4, d5, d6, d7, // MOV rdx, double
0xFF, 0x22, // JMP [rdx]
}
}
但 arm64 架构的指令长度只有 32 bit,所以指令中不能存放那么大的立即数,从而需要将立即数按 16 bit 值分四次移动到 x26 寄存器中,对应的汇编代码如下:
MOVZ x26, double[16:0]
MOVK x26, double[32:16]
MOVK x26, double[48:32]
MOVK x26, double[64:48]
为什么选择 arm64 架构下的 x26 寄存器?
因为 x26 与 amd64 架构下的 rdx 寄存器对应,属于闭包指针寄存器。如果选择其他寄存器,比如 x27,就会导致桩序列相关的所有测试用例运行失败,直接原因是 callReflect
函数的入参 ctxt 为空, 导致 reflect.MakeFunc
调用的地方出了问题,而所有桩序列的测试替身函数 double 都是调用 reflect.MakeFunc
函数动态创建的。
arm64 没有类似 amd64 的间接跳转指令 JMP,因此考虑将空闲的寄存器 x10 作为跳板,通过 LDR 和 BR 指令完成跳转:
LDR x10, [x26]
BR x10
movImm
buildJmpDirective
函数的实现依赖 movImm
函数,该函数用于移动立即数,同时兼容 MOVZ 和 MOVK 指令,代码如下:
func movImm(opc, shift int, val uintptr) []byte {
var m uint32 = 26 // rd
m |= uint32(val) << 5 // imm16
m |= uint32(shift&3) << 21 // hw
m |= 0b100101 << 23 // const
m |= uint32(opc&0x3) << 29 // opc
m |= 0b1 << 31 // sf
res := make([]byte, 4)
*(*uint32)(unsafe.Pointer(&res[0])) = m
return res
}
movImm
函数的实现需要参考 arm64 的指令书册。
致谢
感谢 Go 社区国内外多个 gopher 对“支持 arm64”特性的关注和贡献,特别需要提到的是[@benshi001,@hengwu0,@sirkon, @chenxu2048, @Spongecaptain, @dgofman, @User979269852, @fran96, @JoanWu5, @nathan-jiao],没有你们的付出和接力,就不会有该特性的完整支持!
笔者在这里还要再强调一位大神。Bouke 是 Go 语言 monkey工程的创建者,在 2015 年就发表了 Go 语言猴子补丁原理的文章。毫无疑问,gomonkey 的思维底座主要来自 Bouke 的贡献,向他致敬,非常感谢!
当 Bouke 在 github 上看到 gomonkey 支持 arm64 架构后,他给笔者写了一封信:
收到信后,笔者心情有点小激动。这几年对 gomonkey 的贡献被大神肯定了,说明是非常值得的,后续要加倍努力,为用户发布更多既强大又易用的特性。
希望读者关注 gomonkey ,如果你感觉 gomonkey 对你打桩有帮助的话,那么请你将 gomonkey 推荐给你的朋友,同时期待你参与 gomonkey 社区的共建!
我们相信,持续演进的 gomonkey ,一定会变得越来越强大,越来越贴心,逐步成为国内外 gopher 们爱不释手的 all-in-one 的打桩神器。