go内置函数make主要用于创建map, slice, chan等数据结构。下面简要分析下编译器对于make的处理过程。
一 内置函数的定义
universe.go源文件定义了go内置函数列表,Main函数调用initUniverse,进而调用lexinit对builtinpkg进行了初始化。SetSubOp调用会将Node结构(下文给出)中的Op设置为对应的值,用于标识该Node结构为内置函数。
var builtinFuncs = [...]struct {
name string
op Op
}{
{"append", OAPPEND},
{"cap", OCAP},
{"close", OCLOSE},
{"complex", OCOMPLEX},
{"copy", OCOPY},
{"delete", ODELETE},
{"imag", OIMAG},
{"len", OLEN},
{"make", OMAKE},
{"new", ONEW},
{"panic", OPANIC},
{"print", OPRINT},
{"println", OPRINTN},
{"real", OREAL},
{"recover", ORECOVER},
}
func lexinit(){
for _, s := range builtinFuncs {
s2 := builtinpkg.Lookup(s.name)
s2.Def = asTypesNode(newname(s2))
asNode(s2.Def).SetSubOp(s.op)
}
}
初始化builtinpkg包后,调用下面的代码片码使得内置函数对当前包(localpkg)可见。这样使得在后续解析源码时,可以识别出make为内置函数。
for _, s := range builtinpkg.Syms {
if s.Def == nil {
continue
}
s1 := lookup(s.Name)
if s1.Def != nil {
continue
}
s1.Def = s.Def
s1.Block = s.Block
}
二 词法分析
编译器在词法分析阶段会用如下的CallExpr结构来保存make函数调用。
// Fun(ArgList[0], ArgList[1], ...)
CallExpr struct {
Fun Expr
ArgList []Expr // nil means no arguments
HasDots bool // last argument is followed by ...
expr
}
待词法解析阶段完成后,编译器将CallExpr结构转成用Node结构表示的语法树。
type Node struct {
Left *Node
Right *Node
Ninit Nodes
Nbody Nodes
List Nodes
Op Op
}
转换代码如下所示,将Node结构中的Op设置为OCALL,Left设置为函数名,List设置为函数参数列表。
case *syntax.CallExpr:
n := p.nod(expr, OCALL, p.expr(expr.Fun), nil)
n.List.Set(p.exprs(expr.ArgList))
n.SetIsDDD(expr.HasDots)
return n
当然到目前为止,make只是被识别为一个普通的函数调用。
三 语法分析
编译器在解析函数调用OCALL时,会识别出OCALL结构的Left结点的Op值为OMAKE,那么会如下图所示代码片段进行改写。最终将OCALL结构成为OMAKE结构。
case OCALL:
typecheckslice(n.Ninit.Slice(), ctxStmt) // imported rewritten f(g()) calls (#30907)
n.Left = typecheck(n.Left, ctxExpr|Etype|ctxCallee)
l := n.Left
if l.Op == ONAME && l.SubOp() != 0 {
// builtin: OLEN, OCAP, etc.
n.Op = l.SubOp()
n.Left = n.Right
n.Right = nil
n = typecheck1(n, top)
return n
}
四 语义分析
接下来对make进行语义分析,make目前只能创建slice, map, chan三种数据结构,其他数据结构无法创建,因此在下述代码中default分支将报错。在TMAP分支中,代码检查make的第二个参数是否是整型。最后将Node.Op改成OMAKEMAP。在TSLICE和TCHAN分支中也会做类似的检查,确保参数是正确的。
case OMAKE:
args := n.List.Slice()
if len(args) == 0 {
yyerror("missing argument to make")
n.Type = nil
return n
}
n.List.Set(nil)
l := args[0]
l = typecheck(l, Etype)
t := l.Type
i := 1
switch t.Etype {
default:
yyerror("cannot make type %v", t)
n.Type = nil
return n
case TSLICE:
....
case TMAP:
if i < len(args) {
l = args[i]
i++
l = typecheck(l, ctxExpr)
l = defaultlit(l, types.Types[TINT])
if l.Type == nil {
n.Type = nil
return n
}
if !checkmake(t, "size", l) {
n.Type = nil
return n
}
n.Left = l
} else {
n.Left = nodintconst(0)
}
n.Op = OMAKEMAP
case TCHAN:
....
if i < len(args) {
yyerror("too many arguments to make(%v)", t)
n.Op = OMAKE
n.Type = nil
return n
}
n.Type = t
五 替换runtime实现
walkexpr函数会在语义分析后将make函数替换成runtime包中定义的对应的函数。创建chan是由makechan等完成,创建map是由makemap等完成,创建slice是由makeslice等完成的。下面代码以OMAKEMAP对应的makemap函数为例进行分析。
首先,在builtin.go源文件中预定义了若干内置函数,如下代码片段所示,67,68,69对应makemap64,makemap, makemap_small,都是用于创建map的内置函数。
var runtimeDecls = [...]struct {
name string
tag int
typ int
}{
...
{"makemap64", funcTag, 67},
{"makemap", funcTag, 68},
{"makemap_small", funcTag, 69},
...
}
func runtimeTypes() []*types.Type {
var typs [117]*types.Type
...
typs[67] = functype(nil, []*Node{anonfield(typs[1]), anonfield(typs[19]), anonfield(typs[3])}, []*Node{anonfield(typs[66])})
typs[68] = functype(nil, []*Node{anonfield(typs[1]), anonfield(typs[11]), anonfield(typs[3])}, []*Node{anonfield(typs[66])})
typs[69] = functype(nil, nil, []*Node{anonfield(typs[66])})
...
}
其次,Main调用loadsys初始化Runtimepkg。
func loadsys() {
typs := runtimeTypes()
for _, d := range runtimeDecls {
sym := Runtimepkg.Lookup(d.name)
typ := typs[d.typ]
switch d.tag {
case funcTag:
importfunc(Runtimepkg, src.NoXPos, sym, typ)
case varTag:
importvar(Runtimepkg, src.NoXPos, sym, typ)
default:
Fatalf("unhandled declaration tag %v", d.tag)
}
}
}
然后,walkexp调用syslook函数从Runtimepkg中找出对应的Node结点,调用substArgTypes函数替换原先的通用的函数原型为特定的函数原型,并在调用typename函数时将创建的map类型(包括key类型,value类型)保存在signatslice中。signatslice保存了调用make(map[int32]string, 1000)中的map类型(key为int32, value为string)。调用mkcall1创建了OCALL结构。
fnname := "makemap64"
argtype := types.Types[TINT64]
if hint.Type.IsKind(TIDEAL) || maxintval[hint.Type.Etype].Cmp(maxintval[TUINT]) <= 0 {
fnname = "makemap"
argtype = types.Types[TINT]
}
fn := syslook(fnname)
fn = substArgTypes(fn, hmapType, t.Key(), t.Elem())
n = mkcall1(fn, n.Type, init, typename(n.Type), conv(hint, argtype), h)
最后,编译器会将signatslice中保存的map类型写入到文件中。这个是在dtypesym中完成的。
func dtypesym(t *types.Type) *obj.LSym {
switch t.Etype {
case TMAP:
s1 := dtypesym(t.Key())
s2 := dtypesym(t.Elem())
s3 := dtypesym(bmap(t))
ot = dcommontype(lsym, t)
ot = dsymptr(lsym, ot, s1, 0)
ot = dsymptr(lsym, ot, s2, 0)
ot = dsymptr(lsym, ot, s3, 0)
...
}
}
dtypesym按照runtime包中定义的数据结构写组织数据。这一步准备的数据是用于makemap函数的第一个参数。
type _type struct {
size uintptr
ptrdata uintptr // size of memory prefix holding all pointers
hash uint32
tflag tflag
align uint8
fieldalign uint8
kind uint8
alg *typeAlg
// gcdata stores the GC type data for the garbage collector.
// If the KindGCProg bit is set in kind, gcdata is a GC program.
// Otherwise it is a ptrmask bitmap. See mbitmap.go for details.
gcdata *byte
str nameOff
ptrToThis typeOff
}
type maptype struct {
typ _type
key *_type
elem *_type
bucket *_type // internal type representing a hash bucket
keysize uint8 // size of key slot
valuesize uint8 // size of value slot
bucketsize uint16 // size of bucket
flags uint32
}
六 一个例子
创建test.go文件如下,mytestfunc创建了一个map结构,大小为0x2222。
package main
func mytestfunc() {
var m = make(map[int64]int64, 0x2222)
_ = m
}
func main() {
mytestfunc()
}
编译好go文件后,再使用objdump反编译ELF文件。main.mytestfunc中mytestfunc函数的反编译。44f9a5将45e180这个值写到RAX寄存器,RAX存储着makemap(runtime.makemap)的第一个参数,该参数是一个指针。44f9b0将0x2222写入到内存,这个值是调用make时指定的值。
000000000044f970 <main.mytestfunc>:
44f970: 64 48 8b 0c 25 f8 ff mov %fs:0xfffffffffffffff8,%rcx
44f977: ff ff
44f979: 48 3b 61 10 cmp 0x10(%rcx),%rsp
44f97d: 76 5d jbe 44f9dc <main.mytestfunc+0x6c>
44f97f: 48 83 ec 60 sub $0x60,%rsp
44f983: 48 89 6c 24 58 mov %rbp,0x58(%rsp)
44f988: 48 8d 6c 24 58 lea 0x58(%rsp),%rbp
44f98d: 0f 57 c0 xorps %xmm0,%xmm0
44f990: 0f 11 44 24 28 movups %xmm0,0x28(%rsp)
44f995: 0f 57 c0 xorps %xmm0,%xmm0
44f998: 0f 11 44 24 38 movups %xmm0,0x38(%rsp)
44f99d: 0f 57 c0 xorps %xmm0,%xmm0
44f9a0: 0f 11 44 24 48 movups %xmm0,0x48(%rsp)
44f9a5: 48 8d 05 d4 e7 00 00 lea 0xe7d4(%rip),%rax # 45e180 <type.*+0xe180>
44f9ac: 48 89 04 24 mov %rax,(%rsp)
44f9b0: 48 c7 44 24 08 22 22 movq $0x2222,0x8(%rsp)
44f9b7: 00 00
44f9b9: 48 8d 44 24 28 lea 0x28(%rsp),%rax
44f9be: 48 89 44 24 10 mov %rax,0x10(%rsp)
44f9c3: e8 f8 b8 fb ff callq 40b2c0 <runtime.makemap>
44f9c8: 48 8b 44 24 18 mov 0x18(%rsp),%rax
44f9cd: 48 89 44 24 20 mov %rax,0x20(%rsp)
44f9d2: 48 8b 6c 24 58 mov 0x58(%rsp),%rbp
44f9d7: 48 83 c4 60 add $0x60,%rsp
44f9db: c3 retq
44f9dc: e8 9f 7e ff ff callq 447880 <runtime.morestack_noctxt>
44f9e1: eb 8d jmp 44f970 <main.mytestfunc>
45e180处的数据如下。45e1c8, 45e1c9处的两个0x08表示map的key和value的大小都是8字节。其实的值可以按照maptype定义逐个分析。
45e180: 08 00 or %al,(%rax)
45e182: 00 00 add %al,(%rax)
45e184: 00 00 add %al,(%rax)
45e186: 00 00 add %al,(%rax)
45e188: 08 00 or %al,(%rax)
45e18a: 00 00 add %al,(%rax)
45e18c: 00 00 add %al,(%rax)
45e18e: 00 00 add %al,(%rax)
45e190: 9c pushfq
45e191: 10 d3 adc %dl,%bl
45e193: 02 02 add (%rdx),%al
45e195: 08 08 or %cl,(%rax)
45e197: 35 a0 fe 4b 00 xor $0x4bfea0,%eax
45e19c: 00 00 add %al,(%rax)
45e19e: 00 00 add %al,(%rax)
45e1a0: 6c insb (%dx),%es:(%rdi)
45e1a1: bd 47 00 00 00 mov $0x47,%ebp
45e1a6: 00 00 add %al,(%rax)
45e1a8: ce (bad)
45e1a9: 44 00 00 add %r8b,(%rax)
45e1ac: 00 00 add %al,(%rax)
45e1ae: 00 00 add %al,(%rax)
45e1b0: 20 ac 45 00 00 00 00 and %ch,0x0(%rbp,%rax,2)
45e1b7: 00 20 add %ah,(%rax)
45e1b9: ac lods %ds:(%rsi),%al
45e1ba: 45 00 00 add %r8b,(%r8)
45e1bd: 00 00 add %al,(%rax)
45e1bf: 00 20 add %ah,(%rax)
45e1c1: 57 push %rdi
45e1c2: 46 00 00 rex.RX add %r8b,(%rax)
45e1c5: 00 00 add %al,(%rax)
45e1c7: 00 08 add %cl,(%rax)
45e1c9: 08 90 00 04 00 00 or %dl,0x400(%rax)