| // Copyright 2013 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| // Identify mismatches between assembly files and Go func declarations. |
| |
| package main |
| |
| import ( |
| "bytes" |
| "fmt" |
| "go/ast" |
| "go/token" |
| "regexp" |
| "strconv" |
| "strings" |
| ) |
| |
| // 'kind' is a kind of assembly variable. |
| // The kinds 1, 2, 4, 8 stand for values of that size. |
| type asmKind int |
| |
| // These special kinds are not valid sizes. |
| const ( |
| asmString asmKind = 100 + iota |
| asmSlice |
| asmInterface |
| asmEmptyInterface |
| ) |
| |
| // An asmArch describes assembly parameters for an architecture |
| type asmArch struct { |
| name string |
| ptrSize int |
| intSize int |
| maxAlign int |
| bigEndian bool |
| stack string |
| lr bool |
| } |
| |
| // An asmFunc describes the expected variables for a function on a given architecture. |
| type asmFunc struct { |
| arch *asmArch |
| size int // size of all arguments |
| vars map[string]*asmVar |
| varByOffset map[int]*asmVar |
| } |
| |
| // An asmVar describes a single assembly variable. |
| type asmVar struct { |
| name string |
| kind asmKind |
| typ string |
| off int |
| size int |
| inner []*asmVar |
| } |
| |
| var ( |
| asmArch386 = asmArch{"386", 4, 4, 4, false, "SP", false} |
| asmArchArm = asmArch{"arm", 4, 4, 4, false, "R13", true} |
| asmArchArm64 = asmArch{"arm64", 8, 8, 8, false, "RSP", true} |
| asmArchAmd64 = asmArch{"amd64", 8, 8, 8, false, "SP", false} |
| asmArchAmd64p32 = asmArch{"amd64p32", 4, 4, 8, false, "SP", false} |
| asmArchPpc64 = asmArch{"ppc64", 8, 8, 8, true, "R1", true} |
| asmArchPpc64LE = asmArch{"ppc64le", 8, 8, 8, false, "R1", true} |
| |
| arches = []*asmArch{ |
| &asmArch386, |
| &asmArchArm, |
| &asmArchArm64, |
| &asmArchAmd64, |
| &asmArchAmd64p32, |
| &asmArchPpc64, |
| &asmArchPpc64LE, |
| } |
| ) |
| |
| var ( |
| re = regexp.MustCompile |
| asmPlusBuild = re(`//\s+\+build\s+([^\n]+)`) |
| asmTEXT = re(`\bTEXT\b.*ยท([^\(]+)\(SB\)(?:\s*,\s*([0-9A-Z|+]+))?(?:\s*,\s*\$(-?[0-9]+)(?:-([0-9]+))?)?`) |
| asmDATA = re(`\b(DATA|GLOBL)\b`) |
| asmNamedFP = re(`([a-zA-Z0-9_\xFF-\x{10FFFF}]+)(?:\+([0-9]+))\(FP\)`) |
| asmUnnamedFP = re(`[^+\-0-9](([0-9]+)\(FP\))`) |
| asmSP = re(`[^+\-0-9](([0-9]+)\(([A-Z0-9]+)\))`) |
| asmOpcode = re(`^\s*(?:[A-Z0-9a-z_]+:)?\s*([A-Z]+)\s*([^,]*)(?:,\s*(.*))?`) |
| ppc64Suff = re(`([BHWD])(ZU|Z|U|BR)?$`) |
| ) |
| |
| func asmCheck(pkg *Package) { |
| if !vet("asmdecl") { |
| return |
| } |
| |
| // No work if no assembly files. |
| if !pkg.hasFileWithSuffix(".s") { |
| return |
| } |
| |
| // Gather declarations. knownFunc[name][arch] is func description. |
| knownFunc := make(map[string]map[string]*asmFunc) |
| |
| for _, f := range pkg.files { |
| if f.file != nil { |
| for _, decl := range f.file.Decls { |
| if decl, ok := decl.(*ast.FuncDecl); ok && decl.Body == nil { |
| knownFunc[decl.Name.Name] = f.asmParseDecl(decl) |
| } |
| } |
| } |
| } |
| |
| Files: |
| for _, f := range pkg.files { |
| if !strings.HasSuffix(f.name, ".s") { |
| continue |
| } |
| Println("Checking file", f.name) |
| |
| // Determine architecture from file name if possible. |
| var arch string |
| var archDef *asmArch |
| for _, a := range arches { |
| if strings.HasSuffix(f.name, "_"+a.name+".s") { |
| arch = a.name |
| archDef = a |
| break |
| } |
| } |
| |
| lines := strings.SplitAfter(string(f.content), "\n") |
| var ( |
| fn *asmFunc |
| fnName string |
| localSize, argSize int |
| wroteSP bool |
| haveRetArg bool |
| retLine []int |
| ) |
| |
| flushRet := func() { |
| if fn != nil && fn.vars["ret"] != nil && !haveRetArg && len(retLine) > 0 { |
| v := fn.vars["ret"] |
| for _, line := range retLine { |
| f.Badf(token.NoPos, "%s:%d: [%s] %s: RET without writing to %d-byte ret+%d(FP)", f.name, line, arch, fnName, v.size, v.off) |
| } |
| } |
| retLine = nil |
| } |
| for lineno, line := range lines { |
| lineno++ |
| |
| badf := func(format string, args ...interface{}) { |
| f.Badf(token.NoPos, "%s:%d: [%s] %s: %s", f.name, lineno, arch, fnName, fmt.Sprintf(format, args...)) |
| } |
| |
| if arch == "" { |
| // Determine architecture from +build line if possible. |
| if m := asmPlusBuild.FindStringSubmatch(line); m != nil { |
| Fields: |
| for _, fld := range strings.Fields(m[1]) { |
| for _, a := range arches { |
| if a.name == fld { |
| arch = a.name |
| archDef = a |
| break Fields |
| } |
| } |
| } |
| } |
| } |
| |
| if m := asmTEXT.FindStringSubmatch(line); m != nil { |
| flushRet() |
| if arch == "" { |
| f.Warnf(token.NoPos, "%s: cannot determine architecture for assembly file", f.name) |
| continue Files |
| } |
| fnName = m[1] |
| fn = knownFunc[m[1]][arch] |
| if fn != nil { |
| size, _ := strconv.Atoi(m[4]) |
| if size != fn.size && (m[2] != "7" && !strings.Contains(m[2], "NOSPLIT") || size != 0) { |
| badf("wrong argument size %d; expected $...-%d", size, fn.size) |
| } |
| } |
| localSize, _ = strconv.Atoi(m[3]) |
| localSize += archDef.intSize |
| if archDef.lr { |
| // Account for caller's saved LR |
| localSize += archDef.intSize |
| } |
| argSize, _ = strconv.Atoi(m[4]) |
| if fn == nil && !strings.Contains(fnName, "<>") { |
| badf("function %s missing Go declaration", fnName) |
| } |
| wroteSP = false |
| haveRetArg = false |
| continue |
| } else if strings.Contains(line, "TEXT") && strings.Contains(line, "SB") { |
| // function, but not visible from Go (didn't match asmTEXT), so stop checking |
| flushRet() |
| fn = nil |
| fnName = "" |
| continue |
| } |
| |
| if strings.Contains(line, "RET") { |
| retLine = append(retLine, lineno) |
| } |
| |
| if fnName == "" { |
| continue |
| } |
| |
| if asmDATA.FindStringSubmatch(line) != nil { |
| fn = nil |
| } |
| |
| if archDef == nil { |
| continue |
| } |
| |
| if strings.Contains(line, ", "+archDef.stack) || strings.Contains(line, ",\t"+archDef.stack) { |
| wroteSP = true |
| continue |
| } |
| |
| for _, m := range asmSP.FindAllStringSubmatch(line, -1) { |
| if m[3] != archDef.stack || wroteSP { |
| continue |
| } |
| off := 0 |
| if m[1] != "" { |
| off, _ = strconv.Atoi(m[2]) |
| } |
| if off >= localSize { |
| if fn != nil { |
| v := fn.varByOffset[off-localSize] |
| if v != nil { |
| badf("%s should be %s+%d(FP)", m[1], v.name, off-localSize) |
| continue |
| } |
| } |
| if off >= localSize+argSize { |
| badf("use of %s points beyond argument frame", m[1]) |
| continue |
| } |
| badf("use of %s to access argument frame", m[1]) |
| } |
| } |
| |
| if fn == nil { |
| continue |
| } |
| |
| for _, m := range asmUnnamedFP.FindAllStringSubmatch(line, -1) { |
| off, _ := strconv.Atoi(m[2]) |
| v := fn.varByOffset[off] |
| if v != nil { |
| badf("use of unnamed argument %s; offset %d is %s+%d(FP)", m[1], off, v.name, v.off) |
| } else { |
| badf("use of unnamed argument %s", m[1]) |
| } |
| } |
| |
| for _, m := range asmNamedFP.FindAllStringSubmatch(line, -1) { |
| name := m[1] |
| off := 0 |
| if m[2] != "" { |
| off, _ = strconv.Atoi(m[2]) |
| } |
| if name == "ret" || strings.HasPrefix(name, "ret_") { |
| haveRetArg = true |
| } |
| v := fn.vars[name] |
| if v == nil { |
| // Allow argframe+0(FP). |
| if name == "argframe" && off == 0 { |
| continue |
| } |
| v = fn.varByOffset[off] |
| if v != nil { |
| badf("unknown variable %s; offset %d is %s+%d(FP)", name, off, v.name, v.off) |
| } else { |
| badf("unknown variable %s", name) |
| } |
| continue |
| } |
| asmCheckVar(badf, fn, line, m[0], off, v) |
| } |
| } |
| flushRet() |
| } |
| } |
| |
| // asmParseDecl parses a function decl for expected assembly variables. |
| func (f *File) asmParseDecl(decl *ast.FuncDecl) map[string]*asmFunc { |
| var ( |
| arch *asmArch |
| fn *asmFunc |
| offset int |
| failed bool |
| ) |
| |
| addVar := func(outer string, v asmVar) { |
| if vo := fn.vars[outer]; vo != nil { |
| vo.inner = append(vo.inner, &v) |
| } |
| fn.vars[v.name] = &v |
| for i := 0; i < v.size; i++ { |
| fn.varByOffset[v.off+i] = &v |
| } |
| } |
| |
| addParams := func(list []*ast.Field) { |
| for i, fld := range list { |
| // Determine alignment, size, and kind of type in declaration. |
| var align, size int |
| var kind asmKind |
| names := fld.Names |
| typ := f.gofmt(fld.Type) |
| switch t := fld.Type.(type) { |
| default: |
| switch typ { |
| default: |
| f.Warnf(fld.Type.Pos(), "unknown assembly argument type %s", typ) |
| failed = true |
| return |
| case "int8", "uint8", "byte", "bool": |
| size = 1 |
| case "int16", "uint16": |
| size = 2 |
| case "int32", "uint32", "float32": |
| size = 4 |
| case "int64", "uint64", "float64": |
| align = arch.maxAlign |
| size = 8 |
| case "int", "uint": |
| size = arch.intSize |
| case "uintptr", "iword", "Word", "Errno", "unsafe.Pointer": |
| size = arch.ptrSize |
| case "string", "ErrorString": |
| size = arch.ptrSize * 2 |
| align = arch.ptrSize |
| kind = asmString |
| } |
| case *ast.ChanType, *ast.FuncType, *ast.MapType, *ast.StarExpr: |
| size = arch.ptrSize |
| case *ast.InterfaceType: |
| align = arch.ptrSize |
| size = 2 * arch.ptrSize |
| if len(t.Methods.List) > 0 { |
| kind = asmInterface |
| } else { |
| kind = asmEmptyInterface |
| } |
| case *ast.ArrayType: |
| if t.Len == nil { |
| size = arch.ptrSize + 2*arch.intSize |
| align = arch.ptrSize |
| kind = asmSlice |
| break |
| } |
| f.Warnf(fld.Type.Pos(), "unsupported assembly argument type %s", typ) |
| failed = true |
| case *ast.StructType: |
| f.Warnf(fld.Type.Pos(), "unsupported assembly argument type %s", typ) |
| failed = true |
| } |
| if align == 0 { |
| align = size |
| } |
| if kind == 0 { |
| kind = asmKind(size) |
| } |
| offset += -offset & (align - 1) |
| |
| // Create variable for each name being declared with this type. |
| if len(names) == 0 { |
| name := "unnamed" |
| if decl.Type.Results != nil && len(decl.Type.Results.List) > 0 && &list[0] == &decl.Type.Results.List[0] && i == 0 { |
| // Assume assembly will refer to single unnamed result as r. |
| name = "ret" |
| } |
| names = []*ast.Ident{{Name: name}} |
| } |
| for _, id := range names { |
| name := id.Name |
| addVar("", asmVar{ |
| name: name, |
| kind: kind, |
| typ: typ, |
| off: offset, |
| size: size, |
| }) |
| switch kind { |
| case 8: |
| if arch.ptrSize == 4 { |
| w1, w2 := "lo", "hi" |
| if arch.bigEndian { |
| w1, w2 = w2, w1 |
| } |
| addVar(name, asmVar{ |
| name: name + "_" + w1, |
| kind: 4, |
| typ: "half " + typ, |
| off: offset, |
| size: 4, |
| }) |
| addVar(name, asmVar{ |
| name: name + "_" + w2, |
| kind: 4, |
| typ: "half " + typ, |
| off: offset + 4, |
| size: 4, |
| }) |
| } |
| |
| case asmEmptyInterface: |
| addVar(name, asmVar{ |
| name: name + "_type", |
| kind: asmKind(arch.ptrSize), |
| typ: "interface type", |
| off: offset, |
| size: arch.ptrSize, |
| }) |
| addVar(name, asmVar{ |
| name: name + "_data", |
| kind: asmKind(arch.ptrSize), |
| typ: "interface data", |
| off: offset + arch.ptrSize, |
| size: arch.ptrSize, |
| }) |
| |
| case asmInterface: |
| addVar(name, asmVar{ |
| name: name + "_itable", |
| kind: asmKind(arch.ptrSize), |
| typ: "interface itable", |
| off: offset, |
| size: arch.ptrSize, |
| }) |
| addVar(name, asmVar{ |
| name: name + "_data", |
| kind: asmKind(arch.ptrSize), |
| typ: "interface data", |
| off: offset + arch.ptrSize, |
| size: arch.ptrSize, |
| }) |
| |
| case asmSlice: |
| addVar(name, asmVar{ |
| name: name + "_base", |
| kind: asmKind(arch.ptrSize), |
| typ: "slice base", |
| off: offset, |
| size: arch.ptrSize, |
| }) |
| addVar(name, asmVar{ |
| name: name + "_len", |
| kind: asmKind(arch.intSize), |
| typ: "slice len", |
| off: offset + arch.ptrSize, |
| size: arch.intSize, |
| }) |
| addVar(name, asmVar{ |
| name: name + "_cap", |
| kind: asmKind(arch.intSize), |
| typ: "slice cap", |
| off: offset + arch.ptrSize + arch.intSize, |
| size: arch.intSize, |
| }) |
| |
| case asmString: |
| addVar(name, asmVar{ |
| name: name + "_base", |
| kind: asmKind(arch.ptrSize), |
| typ: "string base", |
| off: offset, |
| size: arch.ptrSize, |
| }) |
| addVar(name, asmVar{ |
| name: name + "_len", |
| kind: asmKind(arch.intSize), |
| typ: "string len", |
| off: offset + arch.ptrSize, |
| size: arch.intSize, |
| }) |
| } |
| offset += size |
| } |
| } |
| } |
| |
| m := make(map[string]*asmFunc) |
| for _, arch = range arches { |
| fn = &asmFunc{ |
| arch: arch, |
| vars: make(map[string]*asmVar), |
| varByOffset: make(map[int]*asmVar), |
| } |
| offset = 0 |
| addParams(decl.Type.Params.List) |
| if decl.Type.Results != nil && len(decl.Type.Results.List) > 0 { |
| offset += -offset & (arch.maxAlign - 1) |
| addParams(decl.Type.Results.List) |
| } |
| fn.size = offset |
| m[arch.name] = fn |
| } |
| |
| if failed { |
| return nil |
| } |
| return m |
| } |
| |
| // asmCheckVar checks a single variable reference. |
| func asmCheckVar(badf func(string, ...interface{}), fn *asmFunc, line, expr string, off int, v *asmVar) { |
| m := asmOpcode.FindStringSubmatch(line) |
| if m == nil { |
| if !strings.HasPrefix(strings.TrimSpace(line), "//") { |
| badf("cannot find assembly opcode") |
| } |
| return |
| } |
| |
| // Determine operand sizes from instruction. |
| // Typically the suffix suffices, but there are exceptions. |
| var src, dst, kind asmKind |
| op := m[1] |
| switch fn.arch.name + "." + op { |
| case "386.FMOVLP": |
| src, dst = 8, 4 |
| case "arm.MOVD": |
| src = 8 |
| case "arm.MOVW": |
| src = 4 |
| case "arm.MOVH", "arm.MOVHU": |
| src = 2 |
| case "arm.MOVB", "arm.MOVBU": |
| src = 1 |
| // LEA* opcodes don't really read the second arg. |
| // They just take the address of it. |
| case "386.LEAL": |
| dst = 4 |
| case "amd64.LEAQ": |
| dst = 8 |
| case "amd64p32.LEAL": |
| dst = 4 |
| default: |
| switch fn.arch.name { |
| case "386", "amd64": |
| if strings.HasPrefix(op, "F") && (strings.HasSuffix(op, "D") || strings.HasSuffix(op, "DP")) { |
| // FMOVDP, FXCHD, etc |
| src = 8 |
| break |
| } |
| if strings.HasPrefix(op, "F") && (strings.HasSuffix(op, "F") || strings.HasSuffix(op, "FP")) { |
| // FMOVFP, FXCHF, etc |
| src = 4 |
| break |
| } |
| if strings.HasSuffix(op, "SD") { |
| // MOVSD, SQRTSD, etc |
| src = 8 |
| break |
| } |
| if strings.HasSuffix(op, "SS") { |
| // MOVSS, SQRTSS, etc |
| src = 4 |
| break |
| } |
| if strings.HasPrefix(op, "SET") { |
| // SETEQ, etc |
| src = 1 |
| break |
| } |
| switch op[len(op)-1] { |
| case 'B': |
| src = 1 |
| case 'W': |
| src = 2 |
| case 'L': |
| src = 4 |
| case 'D', 'Q': |
| src = 8 |
| } |
| case "ppc64", "ppc64le": |
| // Strip standard suffixes to reveal size letter. |
| m := ppc64Suff.FindStringSubmatch(op) |
| if m != nil { |
| switch m[1][0] { |
| case 'B': |
| src = 1 |
| case 'H': |
| src = 2 |
| case 'W': |
| src = 4 |
| case 'D': |
| src = 8 |
| } |
| } |
| } |
| } |
| if dst == 0 { |
| dst = src |
| } |
| |
| // Determine whether the match we're holding |
| // is the first or second argument. |
| if strings.Index(line, expr) > strings.Index(line, ",") { |
| kind = dst |
| } else { |
| kind = src |
| } |
| |
| vk := v.kind |
| vt := v.typ |
| switch vk { |
| case asmInterface, asmEmptyInterface, asmString, asmSlice: |
| // allow reference to first word (pointer) |
| vk = v.inner[0].kind |
| vt = v.inner[0].typ |
| } |
| |
| if off != v.off { |
| var inner bytes.Buffer |
| for i, vi := range v.inner { |
| if len(v.inner) > 1 { |
| fmt.Fprintf(&inner, ",") |
| } |
| fmt.Fprintf(&inner, " ") |
| if i == len(v.inner)-1 { |
| fmt.Fprintf(&inner, "or ") |
| } |
| fmt.Fprintf(&inner, "%s+%d(FP)", vi.name, vi.off) |
| } |
| badf("invalid offset %s; expected %s+%d(FP)%s", expr, v.name, v.off, inner.String()) |
| return |
| } |
| if kind != 0 && kind != vk { |
| var inner bytes.Buffer |
| if len(v.inner) > 0 { |
| fmt.Fprintf(&inner, " containing") |
| for i, vi := range v.inner { |
| if i > 0 && len(v.inner) > 2 { |
| fmt.Fprintf(&inner, ",") |
| } |
| fmt.Fprintf(&inner, " ") |
| if i > 0 && i == len(v.inner)-1 { |
| fmt.Fprintf(&inner, "and ") |
| } |
| fmt.Fprintf(&inner, "%s+%d(FP)", vi.name, vi.off) |
| } |
| } |
| badf("invalid %s of %s; %s is %d-byte value%s", op, expr, vt, vk, inner.String()) |
| } |
| } |