前两节介绍的 词法与语法分析 以及 类型检查 两个部分都属于编译器前端,它们负责对源代码进行分析并检查其中存在的词法和语法错误,经过这两个阶段生成的抽象语法树已经不存在任何的结构上的错误了,从这一节开始就进入了编译器后端的工作 — 中间代码生成 和 机器码生成 了,这里会介绍 Go 语言编译的中间代码生成阶段。

中间代码 是一种应用于抽象机器的编程语言,它设计的目的主要是帮助我们分析计算机程序,在编译的过程中,编译器会在将语言的源代码转换成目标机器上机器码的过程中,先把源代码转换成一种中间的表述形式,这里要介绍的就是 Go 语言如何将抽象语法树转换成 SSA 表示的中间代码。

中间代码生成

Go 语言编译器的中间代码具有静态单赋值(SSA)的特性,我们在介绍 Go 语言编译过程 中曾经介绍过静态单赋值,对这个特性不了解的读者可以回到上面的文章阅读相应的部分,当然也可以自行搜索学习相关的知识,不过在这里哪怕对 SSA 一无所知,也不会影响对这一节的理解。

funccompile
func Main(archInit func(*Arch)) {
    // ...

    initssaconfig()

    for i := 0; i < len(xtop); i++ {
        n := xtop[i]
        if n.Op == ODCLFUNC {
            funccompile(n)
        }
    }

    compileFunctions()
}
initssaconfigfunccompile

配置初始化

initssaconfigTypes
func initssaconfig() {
    types_ := ssa.NewTypes()

    _ = types.NewPtr(types.Types[TINTER])
    _ = types.NewPtr(types.NewPtr(types.Types[TSTRING]))
    _ = types.NewPtr(types.NewPtr(types.Idealstring))
    _ = types.NewPtr(types.NewSlice(types.Types[TINTER]))
    _ = types.NewPtr(types.NewPtr(types.Bytetype))
    _ = types.NewPtr(types.NewSlice(types.Bytetype))
    // ...
    _ = types.NewPtr(types.Errortype)
BoolInt8StringNewPtr

详解 Golang 中间代码生成_Javagolang-type-and-pointe

NewPtr
func NewPtr(elem *Type) *Type {
    if t := elem.Cache.ptr; t != nil {
        if t.Elem() != elem {
            Fatalf("NewPtr: elem mismatch")
        }
        return t
    }

    t := New(TPTR)
    t.Extra = Ptr{Elem: elem}
    t.Width = int64(Widthptr)
    t.Align = uint8(Widthptr)
    if NewPtrCacheEnabled {
        elem.Cache.ptr = t
    }
    return t
}
ssaConfigNewConfigTypes
    ssaConfig = ssa.NewConfig(thearch.LinkArch.Name, *types_, Ctxt, Debug['N'] == 0)

该函数会根据传入的 CPU 架构设置用于生成中间代码和机器码的操作:

func NewConfig(arch string, types Types, ctxt *obj.Link, optimize bool) *Config {
    c := &Config{arch: arch, Types: types}
    c.useAvg = true
    c.useHmul = true
    switch arch {
    case "amd64":
        c.PtrSize = 8
        c.RegSize = 8
        c.lowerBlock = rewriteBlockAMD64
        c.lowerValue = rewriteValueAMD64
        c.registers = registersAMD64[:]
        c.gpRegMask = gpRegMaskAMD64
        c.fpRegMask = fpRegMaskAMD64
        c.FPReg = framepointerRegAMD64
        c.LinkReg = linkRegAMD64
        c.hasGReg = false
    case "amd64p32":
    case "386":
    case "arm":
    case "arm64":
    // ...
    case "wasm":
    default:
        ctxt.Diag("arch %s not implemented", arch)
    }
    c.ctxt = ctxt
    c.optimize = optimize

    // ...
    return c
}

这里会设置当前编译器使用的指针和寄存器大小、可用寄存器列表、掩码等编译选项,所有的配置项一旦被创建,在整个编译期间都是只读的并且被全部编译阶段共享,也就是中间代码生成和机器码生成这两部分都会使用这一份配置完成自己的工作。

initssaconfig
    assertE2I = sysfunc("assertE2I")
    assertE2I2 = sysfunc("assertE2I2")
    assertI2I = sysfunc("assertI2I")
    assertI2I2 = sysfunc("assertI2I2")
    deferproc = sysfunc("deferproc")
    Deferreturn = sysfunc("deferreturn")
    Duffcopy = sysvar("duffcopy")
    Duffzero = sysvar("duffzero")
    // ...
Pkgobj.LSym

遍历和替换

walkwalk
func walk(fn *Node)
func walkappend(n *Node, init *Nodes, dst *Node) *Node
func walkAppendArgs(n *Node, init *Nodes)
func walkclosure(clo *Node, init *Nodes) *Node
func walkCall(n *Node, init *Nodes)
func walkcompare(n *Node, init *Nodes) *Node
func walkcompareInterface(n *Node, init *Nodes) *Node
func walkcompareString(n *Node, init *Nodes) *Node
func walkexpr(n *Node, init *Nodes) *Node
func walkexprlist(s []*Node, init *Nodes)
func walkexprlistcheap(s []*Node, init *Nodes)
func walkexprlistsafe(s []*Node, init *Nodes)
func walkprint(nn *Node, init *Nodes) *Node
func walkinrange(n *Node, init *Nodes) *Node
func walkpartialcall(n *Node, init *Nodes) *Node
func walkrange(n *Node) *Node
func walkselect(sel *Node)
func walkselectcases(cases *Nodes) []*Node
func walkstmt(n *Node) *Node
func walkstmtlist(s []*Node)
func walkswitch(sw *Node)
panicrecovergopanicgorecover

golang-keyword-and-builtin-mapping

makenewselect

转换后的全部函数都属于运行时 runtime 包,我们能在 src/cmd/compile/internal/gc/builtin/runtime.go 文件中找到这里出现的函数,但是这里的函数都没有任何的实现,其中只包含了函数签名和定义。

func makemap64(mapType *byte, hint int64, mapbuf *any) (hmap map[any]any)
func makemap(mapType *byte, hint int, mapbuf *any) (hmap map[any]any)
func makemap_small() (hmap map[any]any)
func mapaccess1(mapType *byte, hmap map[any]any, key *any) (val *any)
// ...

func makechan64(chanType *byte, size int64) (hchan chan any)
func makechan(chanType *byte, size int) (hchan chan any)
// ...

上面的代码只是让编译器能够找到对应符号的函数定义而已,真正的函数实现都在另一个 runtime 包中,Go 语言的主程序在执行时会调用 runtime 中的函数,也就是说关键字和内置函数的功能其实是由语言的编译器和运行时共同完成的。

Channel

OSENDORECV
func walkexpr(n *Node, init *Nodes) *Node {
    // ...
    case OSEND:
        n1 := n.Right
        n1 = assignconv(n1, n.Left.Type.Elem(), "chan send")
        n1 = walkexpr(n1, init)
        n1 = nod(OADDR, n1, nil)
        n = mkcall1(chanfn("chansend1", 2, n.Left.Type), nil, init, n.Left, n1)
    // ...
}
OSENDmkcall1OCALLchansend1OCALLOSEND
ORECVOSENDchansend1chanrecv1
        n = mkcall1(chanfn("chanrecv1", 2, n.Left.Type), nil, &init, n.Left, nodnil())
closeOCLOSEwalkexprclosechanOCALL
func walkexpr(n *Node, init *Nodes) *Node {
    // ...
    case OCLOSE:
        fn := syslook("closechan")

        fn = substArgTypes(fn, n.Left.Type)
        n = mkcall1(fn, nil, init, n.Left)
    // ...
}
chanrecv1chansend1closechan

编译

walkcompileSSA
func compileSSA(fn *Node, worker int) {
    f := buildssa(fn, worker)
    pp := newProgs(fn, worker)
    genssa(f, pp)

    pp.Flush()
}
buildssa
// hello.go
package hello

func hello(a int) int {
    c := a + 2
    return c
}

我们可以使用如下的命令来获取上述代码在生成最后中间代码期间经历的 N 个版本的 SSA 中间代码以及最后的汇编代码:

$ GOSSAFUNC=hello go build hello.go
generating SSA for hello
buildssa-enter
.   AS l(3)
.   .   NAME-hello.~r1 a(true) g(1) l(3) x(8) class(PPARAMOUT) int
buildssa-body
.   DCL l(4)
.   .   NAME-hello.c a(true) g(3) l(4) x(0) class(PAUTO) tc(1) used int

.   AS l(4) colas(true) tc(1)
.   .   NAME-hello.c a(true) g(3) l(4) x(0) class(PAUTO) tc(1) used int
.   .   ADD l(4) tc(1) int
.   .   .   NAME-hello.a a(true) g(2) l(3) x(0) class(PPARAM) tc(1) used int
.   .   .   LITERAL-2 l(4) tc(1) int

.   RETURN l(5) tc(1)
.   RETURN-list
.   .   AS l(5) tc(1)
.   .   .   NAME-hello.~r1 a(true) g(1) l(3) x(8) class(PPARAMOUT) int
.   .   .   NAME-hello.c a(true) g(3) l(4) x(0) class(PAUTO) tc(1) used int
buildssa-exit
// ...
helloEnterNBodyExit
func buildssa(fn *Node, worker int) *ssa.Func {
    name := fn.funcname()
    var astBuf *bytes.Buffer

    var s state

    fe := ssafn{
        curfn: fn,
        log:   printssa && ssaDumpStdout,
    }
    s.curfn = fn

    s.f = ssa.NewFunc(&fe)
    s.config = ssaConfig
    s.f.Type = fn.Type
    s.f.Config = ssaConfig

    // ...

    s.stmtList(fn.Func.Enter)
    s.stmtList(fn.Nbody)

    ssa.Compile(s.f)
    return s.f
}
ssaConfigstmtListCompile

AST 到 SSA

stmtListstmt
func (s *state) stmt(n *Node) {
    // ...

    switch n.Op {
    case OCALLFUNC:
        if isIntrinsicCall(n) {
            s.intrinsicCall(n)
            return
        }
        fallthrough

    case OCALLMETH, OCALLINTER:
        s.call(n, callNormal)
        if n.Op == OCALLFUNC && n.Left.Op == ONAME && n.Left.Class() == PFUNC {
            if fn := n.Left.Sym.Name; compiling_runtime && fn == "throw" ||
                n.Left.Sym.Pkg == Runtimepkg && (fn == "throwinit" || fn == "gopanic" || fn == "panicwrap" || fn == "block" || fn == "panicmakeslicelen" || fn == "panicmakeslicecap") {
                m := s.mem()
                b := s.endBlock()
                b.Kind = ssa.BlockExit
                b.SetControl(m)
            }
        }
    case ODEFER:
        s.call(n.Left, callDefer)
    case OGO:
        s.call(n.Left, callGo)
    // ...

    }

    // ...
}
call
func (s *state) call(n *Node, k callKind) *ssa.Value {
    var sym *types.Sym
    fn := n.Left
    switch n.Op {
    case OCALLFUNC:
        sym = fn.Sym
    case OCALLMETH:
        // ...
    case OCALLINTER:
        // ...
    }
    dowidth(fn.Type)
    stksize := fn.Type.ArgWidth()

    s.stmtList(n.List)

    t := n.Left.Type
    args := n.Rlist.Slice()
    for i, n := range args {
        f := t.Params().Field(i)
        s.storeArg(n, f.Type, argStart+f.Offset)
    }

    var call *ssa.Value
    switch {
    case k == callDefer:
        call = s.newValue1A(ssa.OpStaticCall, types.TypeMem, deferproc, s.mem())
    case k == callGo:
        call = s.newValue1A(ssa.OpStaticCall, types.TypeMem, newproc, s.mem())
    case sym != nil:
        call = s.newValue1A(ssa.OpStaticCall, types.TypeMem, sym.Linksym(), s.mem())
    // ...
    }
    call.AuxInt = stksize
    s.vars[&memVar] = call

    res := n.Left.Type.Results()
    fp := res.Field(0)
    return s.constOffPtrSP(types.NewPtr(fp.Type), fp.Offset+Ctxt.FixedFrameSize())
}
ssa.OpStaticCalldeferprocnewproc

在上述方法中生成的 SSA 中间代码其实就是如下的形式:

compiling hello
hello func(int) int
  b1:
    v1 = InitMem <mem>
    v2 = SP <uintptr>
    v3 = SB <uintptr> DEAD
    v4 = LocalAddr <*int> {a} v2 v1 DEAD
    v5 = LocalAddr <*int> {~r1} v2 v1
    v6 = Arg <int> {a}
    v7 = Const64 <int> [0] DEAD
    v8 = Const64 <int> [2]
    v9 = Add64 <int> v6 v8 (c[int])
    v10 = VarDef <mem> {~r1} v1
    v11 = Store <mem> {int} v5 v9 v10
    Ret v11
GOSSAFUNC=hello go build hello.go

多轮转换

stmtssa.Compile
func Compile(f *Func) {
    if f.Log() {
        f.Logf("compiling %s\n", f.Name)
    }

    phaseName := "init"

    for _, p := range passes {
        f.pass = &p
        p.fn(f)
    }

    phaseName = ""
}
Compilepassesrequired
var passes = [...]pass{
    {name: "number lines", fn: numberLines, required: true},
    {name: "early phielim", fn: phielim},
    {name: "early copyelim", fn: copyelim},
    // ...
    {name: "loop rotate", fn: loopRotate},
    {name: "stackframe", fn: stackframe, required: true},
    {name: "trim", fn: trim},
}
GOSSAFUNC=hello go build hello.go
  pass trim begin
  pass trim end [738 ns]
hello func(int) int
  b1:
    v1 = InitMem <mem>
    v10 = VarDef <mem> {~r1} v1
    v2 = SP <uintptr> : SP
    v6 = Arg <int> {a} : a[int]
    v8 = LoadReg <int> v6 : AX
    v9 = ADDQconst <int> [2] v8 : AX (c[int])
    v11 = MOVQstore <mem> {~r1} v2 v9 v10
    Ret v11

经过将近 50 轮处理的 SSA 中间代码相比处理之前已经有了非常大的改变,执行效率和过程也会有比较大的提升,多轮的处理已经包含了一些机器特定的修改,包括根据目标架构对代码进行改写,不过这里就不会展开介绍每一轮处理的具体内容了。

总结

中间代码的生成过程其实就是从 AST 抽象语法树到 SSA 中间代码的转换过程,在这期间会对语法树中的关键字在进行一次更新,更新后的语法树会经过多轮处理转变成最后的 SSA 中间代码,这里的代码大都是巨长的 switch 语句和复杂的函数以及调用栈,分析和阅读起来也非常困难。

很多 Go 语言中的关键字和内置函数都是在这个阶段被转换成运行时包中方法的,作者在后面的章节会从具体的语言关键字和内置函数的角度介绍一些数据结构和函数的实现。