在编译器中如何隐式生成Golang运行时调用

在编译器中如何隐式生成Golang运行时调用 大家好,

我目前正在尝试向自己创建的库中添加运行时调用。我正在开发一个系统,该系统将增加类型安全性,需要检查我正在跟踪的引用计数变量。我遇到的问题是需要在每个变量赋值前插入运行时调用。通过研究编译器,我发现了三个可能实现此功能的修改方向:

  1. 在赋值AST节点(OP=OAS)之前添加新的AST节点
  2. 在为赋值生成SSA时,为我的运行时调用创建SSA值
  3. 在最终为赋值生成机器码时,插入我的运行时调用

理想情况下,我认为选项1是最佳选择。在编译链的早期阶段进行操作似乎是正确的方向。然而,我对如何推进这些选项中的任何一个都毫无头绪。如果有人有任何建议,我很乐意听取。我主要尝试了选项2,因为它看起来最简单,可以通过类似“s.newValue1A(ssa.OpStaticCall, types.TypeMem, myFunction, s.mem())”的方式调用我的运行时函数信息,但这会以各种方式破坏编译器,而我不理解具体原因。具体来说,我想请教:

  1. 如何创建额外的AST节点并在该阶段正确插入到语法树中?或者
  2. 如何为我的运行时调用生成SSA值?

我是这个论坛的新用户,如果问题过于复杂或者需要进一步分解说明,请告知。谢谢!


更多关于在编译器中如何隐式生成Golang运行时调用的实战教程也可以访问 https://www.itying.com/category-94-b0.html

3 回复

使用这种方法,原始的Go文件将被重写以包含额外的调用,然后需要重新编译,对吗?是否有办法直接修改编译器,隐式注入这个运行时调用,这样用户就不需要知道它的存在/执行任何额外步骤?

更多关于在编译器中如何隐式生成Golang运行时调用的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


Go 语言提供了一些用于分析 Go 程序的标准包

https://github.com/golang/example/tree/master/gotypes

向语法树中插入节点很简单

  1. 使用 go/parser 解析文件
  2. 找到目标父节点
  3. 插入节点并重写文件

以下是语法树解析器示例

https://golang.org/src/go/ast/example_test.go

要在Go编译器中隐式插入运行时调用,确实需要在编译流程的适当阶段进行操作。以下是针对你提出的两种主要方法的实现方案:

方法1:在AST阶段插入运行时调用

src/cmd/compile/internal/gc/typecheck.go的赋值检查函数中插入调用:

// 在 typecheckas 函数中添加
func typecheckas(n *Node) {
    // 原有的类型检查逻辑...
    
    // 插入运行时调用
    if n.Left != nil && n.Right != nil {
        call := mkcall("myRuntimeCheck", nil, nil, n.Left)
        n.Ninit.Append(call)
    }
}

// 创建运行时调用节点
func mkcall(name string, t *Type, init *Nodes, args ...*Node) *Node {
    call := nod(OCALL, nil, nil)
    call.Left = newname(Lookup(name))
    call.List.Set(args)
    call.Type = t
    if init != nil {
        call.Ninit.Set(init.Slice())
    }
    return call
}

src/cmd/compile/internal/gc/walk.go中确保调用不被优化掉:

func walkstmt(n *Node) *Node {
    switch n.Op {
    case OAS:
        // 确保运行时调用在walk阶段仍然存在
        if len(n.Ninit.Slice()) > 0 {
            for _, init := range n.Ninit.Slice() {
                if init.Op == OCALL && isRuntimeCall(init) {
                    // 保留这个调用
                }
            }
        }
    }
    return n
}

方法2:在SSA阶段生成运行时调用

src/cmd/compile/internal/ssa/gen/genericOps.go中定义新的SSA操作:

// 添加新的SSA操作码
const (
    OpMyRuntimeCall Op = opBase + iota
)

src/cmd/compile/internal/ssa/compile.gobuildssa函数中插入调用:

func (s *state) stmt(n *Node) {
    switch n.Op {
    case OAS:
        // 在赋值前插入运行时调用
        if n.Left != nil {
            // 为运行时调用创建参数
            ptr := s.addr(n.Left)
            
            // 创建运行时调用
            call := s.newValue1A(ssa.OpStaticCall, types.TypeMem, 
                ssa.AuxCall{Lsym: s.f.fe.Func.LSym.Pkg.Lookup("myRuntimeCheck")}, 
                s.mem())
            
            // 设置调用参数
            s.vars[&memVar] = call
            
            // 继续正常的赋值处理
            s.assign(n.Left, n.Right)
        }
    }
}

src/cmd/compile/internal/ssa/opGen.go中注册操作:

var opcodeTable = [...]opData{
    // ... 其他操作
    {name: "MyRuntimeCall", argLen: 1, hasSideEffects: true},
}

运行时函数声明

在库中声明运行时函数:

//go:linkname myRuntimeCheck runtime.myRuntimeCheck
func myRuntimeCheck(ptr unsafe.Pointer)

// 实际的实现
func myRuntimeCheck(ptr unsafe.Pointer) {
    // 你的引用计数检查逻辑
    if ptr != nil {
        // 执行安全检查
    }
}

关键注意事项

  1. 内存顺序:确保在SSA阶段正确处理内存依赖,使用types.TypeMem类型
  2. 调用约定:运行时调用必须遵循Go的调用约定
  3. 优化器:在src/cmd/compile/internal/ssa/deadcode.go中标记调用有副作用,防止被消除
  4. 调试:使用GOSSAFUNC环境变量查看SSA生成过程:
GOSSAFUNC=yourFunctionName go build

AST方法更适合你的用例,因为它在编译流程的早期阶段操作,受后续优化阶段的影响较小。SSA方法需要更深入地理解编译器的中间表示和优化流程。

回到顶部