gomonkey 全面支持 arm64 了

引言

gomonkey 是 Go 的一款打桩框架,目标是让用户在单元测试中低成本的完成打桩,从而将精力聚焦于业务功能的开发。gomonkey 接口友好,功能强大,目前已被很多项目使用,用户遍及世界多个国家。

image.png

提出问题

众所周知,这几年基于 arm64 架构的设备越来越多,包括服务器和终端等,尤其是 Apple M1芯片发布后,国内外多个 gopher 在 github 上提了希望 gomonkey 支持 arm64 架构 的 Issues

image.png

解决问题

新增一个 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 的指令书册。

image.png

image.png

致谢

感谢 Go 社区国内外多个 gopher 对“支持 arm64”特性的关注和贡献,特别需要提到的是[@benshi001,@hengwu0,@sirkon, @chenxu2048, @Spongecaptain, @dgofman, @User979269852, @fran96, @JoanWu5, @nathan-jiao],没有你们的付出和接力,就不会有该特性的完整支持!

笔者在这里还要再强调一位大神。Bouke 是 Go 语言 monkey工程的创建者,在 2015 年就发表了 Go 语言猴子补丁原理的文章。毫无疑问,gomonkey 的思维底座主要来自 Bouke 的贡献,向他致敬,非常感谢!

image.png

当 Bouke 在 github 上看到 gomonkey 支持 arm64 架构后,他给笔者写了一封信:


image.png

收到信后,笔者心情有点小激动。这几年对 gomonkey 的贡献被大神肯定了,说明是非常值得的,后续要加倍努力,为用户发布更多既强大又易用的特性。

希望读者关注 gomonkey ,如果你感觉 gomonkey 对你打桩有帮助的话,那么请你将 gomonkey 推荐给你的朋友,同时期待你参与 gomonkey 社区的共建!

我们相信,持续演进的 gomonkey ,一定会变得越来越强大,越来越贴心,逐步成为国内外 gopher 们爱不释手all-in-one 的打桩神器。

你可能感兴趣的:(gomonkey 全面支持 arm64 了)