// 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. // Package asmdecl defines an Analyzer that reports mismatches between // assembly files and Go declarations. package asmdecl import ( "bytes" "fmt" "go/ast" "go/build" "go/token" "go/types" "log" "regexp" "strconv" "strings" "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/passes/internal/analysisutil" ) const Doc = "report mismatches between assembly files and Go declarations" var Analyzer = &analysis.Analyzer{ Name: "asmdecl", Doc: Doc, Run: run, } // '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 asmArray asmInterface asmEmptyInterface asmStruct asmComplex ) // An asmArch describes assembly parameters for an architecture type asmArch struct { name string bigEndian bool stack string lr bool // calculated during initialization sizes types.Sizes intSize int ptrSize int maxAlign int } // 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{name: "386", bigEndian: false, stack: "SP", lr: false} asmArchArm = asmArch{name: "arm", bigEndian: false, stack: "R13", lr: true} asmArchArm64 = asmArch{name: "arm64", bigEndian: false, stack: "RSP", lr: true} asmArchAmd64 = asmArch{name: "amd64", bigEndian: false, stack: "SP", lr: false} asmArchMips = asmArch{name: "mips", bigEndian: true, stack: "R29", lr: true} asmArchMipsLE = asmArch{name: "mipsle", bigEndian: false, stack: "R29", lr: true} asmArchMips64 = asmArch{name: "mips64", bigEndian: true, stack: "R29", lr: true} asmArchMips64LE = asmArch{name: "mips64le", bigEndian: false, stack: "R29", lr: true} asmArchPpc64 = asmArch{name: "ppc64", bigEndian: true, stack: "R1", lr: true} asmArchPpc64LE = asmArch{name: "ppc64le", bigEndian: false, stack: "R1", lr: true} asmArchRISCV64 = asmArch{name: "riscv64", bigEndian: false, stack: "SP", lr: true} asmArchS390X = asmArch{name: "s390x", bigEndian: true, stack: "R15", lr: true} asmArchWasm = asmArch{name: "wasm", bigEndian: false, stack: "SP", lr: false} arches = []*asmArch{ &asmArch386, &asmArchArm, &asmArchArm64, &asmArchAmd64, &asmArchMips, &asmArchMipsLE, &asmArchMips64, &asmArchMips64LE, &asmArchPpc64, &asmArchPpc64LE, &asmArchRISCV64, &asmArchS390X, &asmArchWasm, } ) func init() { for _, arch := range arches { arch.sizes = types.SizesFor("gc", arch.name) if arch.sizes == nil { // TODO(adonovan): fix: now that asmdecl is not in the standard // library we cannot assume types.SizesFor is consistent with arches. // For now, assume 64-bit norms and print a warning. // But this warning should really be deferred until we attempt to use // arch, which is very unlikely. Better would be // to defer size computation until we have Pass.TypesSizes. arch.sizes = types.SizesFor("gc", "amd64") log.Printf("unknown architecture %s", arch.name) } arch.intSize = int(arch.sizes.Sizeof(types.Typ[types.Int])) arch.ptrSize = int(arch.sizes.Sizeof(types.Typ[types.UnsafePointer])) arch.maxAlign = int(arch.sizes.Alignof(types.Typ[types.Int64])) } } 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)?$`) abiSuff = re(`^(.+)$`) ) func run(pass *analysis.Pass) (interface{}, error) { // No work if no assembly files. var sfiles []string for _, fname := range pass.OtherFiles { if strings.HasSuffix(fname, ".s") { sfiles = append(sfiles, fname) } } if sfiles == nil { return nil, nil } // Gather declarations. knownFunc[name][arch] is func description. knownFunc := make(map[string]map[string]*asmFunc) for _, f := range pass.Files { for _, decl := range f.Decls { if decl, ok := decl.(*ast.FuncDecl); ok && decl.Body == nil { knownFunc[decl.Name.Name] = asmParseDecl(pass, decl) } } } Files: for _, fname := range sfiles { content, tf, err := analysisutil.ReadFile(pass.Fset, fname) if err != nil { return nil, err } // Determine architecture from file name if possible. var arch string var archDef *asmArch for _, a := range arches { if strings.HasSuffix(fname, "_"+a.name+".s") { arch = a.name archDef = a break } } lines := strings.SplitAfter(string(content), "\n") var ( fn *asmFunc fnName string localSize, argSize int wroteSP bool noframe 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 { pass.Reportf(analysisutil.LineStart(tf, line), "[%s] %s: RET without writing to %d-byte ret+%d(FP)", arch, fnName, v.size, v.off) } } retLine = nil } trimABI := func(fnName string) string { m := abiSuff.FindStringSubmatch(fnName) if m != nil { return m[1] } return fnName } for lineno, line := range lines { lineno++ badf := func(format string, args ...interface{}) { pass.Reportf(analysisutil.LineStart(tf, lineno), "[%s] %s: %s", arch, fnName, fmt.Sprintf(format, args...)) } if arch == "" { // Determine architecture from +build line if possible. if m := asmPlusBuild.FindStringSubmatch(line); m != nil { // There can be multiple architectures in a single +build line, // so accumulate them all and then prefer the one that // matches build.Default.GOARCH. var archCandidates []*asmArch for _, fld := range strings.Fields(m[1]) { for _, a := range arches { if a.name == fld { archCandidates = append(archCandidates, a) } } } for _, a := range archCandidates { if a.name == build.Default.GOARCH { archCandidates = []*asmArch{a} break } } if len(archCandidates) > 0 { arch = archCandidates[0].name archDef = archCandidates[0] } } } // Ignore comments and commented-out code. if i := strings.Index(line, "//"); i >= 0 { line = line[:i] } if m := asmTEXT.FindStringSubmatch(line); m != nil { flushRet() if arch == "" { // Arch not specified by filename or build tags. // Fall back to build.Default.GOARCH. for _, a := range arches { if a.name == build.Default.GOARCH { arch = a.name archDef = a break } } if arch == "" { log.Printf("%s: cannot determine architecture for assembly file", fname) continue Files } } fnName = m[2] if pkgPath := strings.TrimSpace(m[1]); pkgPath != "" { // The assembler uses Unicode division slash within // identifiers to represent the directory separator. pkgPath = strings.Replace(pkgPath, "∕", "/", -1) if pkgPath != pass.Pkg.Path() { // log.Printf("%s:%d: [%s] cannot check cross-package assembly function: %s is in package %s", fname, lineno, arch, fnName, pkgPath) fn = nil fnName = "" continue } } // Trim off optional ABI selector. fnName := trimABI(fnName) flag := m[3] fn = knownFunc[fnName][arch] if fn != nil { size, _ := strconv.Atoi(m[5]) if size != fn.size && (flag != "7" && !strings.Contains(flag, "NOSPLIT") || size != 0) { badf("wrong argument size %d; expected $...-%d", size, fn.size) } } localSize, _ = strconv.Atoi(m[4]) localSize += archDef.intSize if archDef.lr && !strings.Contains(flag, "NOFRAME") { // Account for caller's saved LR localSize += archDef.intSize } argSize, _ = strconv.Atoi(m[5]) noframe = strings.Contains(flag, "NOFRAME") if fn == nil && !strings.Contains(fnName, "<>") && !noframe { 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) || strings.Contains(line, "NOP "+archDef.stack) || strings.Contains(line, "NOP\t"+archDef.stack) { wroteSP = true continue } if arch == "wasm" && strings.Contains(line, "CallImport") { // CallImport is a call out to magic that can write the result. haveRetArg = true } for _, m := range asmSP.FindAllStringSubmatch(line, -1) { if m[3] != archDef.stack || wroteSP || noframe { 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, archDef) } } flushRet() } return nil, nil } func asmKindForType(t types.Type, size int) asmKind { switch t := t.Underlying().(type) { case *types.Basic: switch t.Kind() { case types.String: return asmString case types.Complex64, types.Complex128: return asmComplex } return asmKind(size) case *types.Pointer, *types.Chan, *types.Map, *types.Signature: return asmKind(size) case *types.Struct: return asmStruct case *types.Interface: if t.Empty() { return asmEmptyInterface } return asmInterface case *types.Array: return asmArray case *types.Slice: return asmSlice } panic("unreachable") } // A component is an assembly-addressable component of a composite type, // or a composite type itself. type component struct { size int offset int kind asmKind typ string suffix string // Such as _base for string base, _0_lo for lo half of first element of [1]uint64 on 32 bit machine. outer string // The suffix for immediately containing composite type. } func newComponent(suffix string, kind asmKind, typ string, offset, size int, outer string) component { return component{suffix: suffix, kind: kind, typ: typ, offset: offset, size: size, outer: outer} } // componentsOfType generates a list of components of type t. // For example, given string, the components are the string itself, the base, and the length. func componentsOfType(arch *asmArch, t types.Type) []component { return appendComponentsRecursive(arch, t, nil, "", 0) } // appendComponentsRecursive implements componentsOfType. // Recursion is required to correct handle structs and arrays, // which can contain arbitrary other types. func appendComponentsRecursive(arch *asmArch, t types.Type, cc []component, suffix string, off int) []component { s := t.String() size := int(arch.sizes.Sizeof(t)) kind := asmKindForType(t, size) cc = append(cc, newComponent(suffix, kind, s, off, size, suffix)) switch kind { case 8: if arch.ptrSize == 4 { w1, w2 := "lo", "hi" if arch.bigEndian { w1, w2 = w2, w1 } cc = append(cc, newComponent(suffix+"_"+w1, 4, "half "+s, off, 4, suffix)) cc = append(cc, newComponent(suffix+"_"+w2, 4, "half "+s, off+4, 4, suffix)) } case asmEmptyInterface: cc = append(cc, newComponent(suffix+"_type", asmKind(arch.ptrSize), "interface type", off, arch.ptrSize, suffix)) cc = append(cc, newComponent(suffix+"_data", asmKind(arch.ptrSize), "interface data", off+arch.ptrSize, arch.ptrSize, suffix)) case asmInterface: cc = append(cc, newComponent(suffix+"_itable", asmKind(arch.ptrSize), "interface itable", off, arch.ptrSize, suffix)) cc = append(cc, newComponent(suffix+"_data", asmKind(arch.ptrSize), "interface data", off+arch.ptrSize, arch.ptrSize, suffix)) case asmSlice: cc = append(cc, newComponent(suffix+"_base", asmKind(arch.ptrSize), "slice base", off, arch.ptrSize, suffix)) cc = append(cc, newComponent(suffix+"_len", asmKind(arch.intSize), "slice len", off+arch.ptrSize, arch.intSize, suffix)) cc = append(cc, newComponent(suffix+"_cap", asmKind(arch.intSize), "slice cap", off+arch.ptrSize+arch.intSize, arch.intSize, suffix)) case asmString: cc = append(cc, newComponent(suffix+"_base", asmKind(arch.ptrSize), "string base", off, arch.ptrSize, suffix)) cc = append(cc, newComponent(suffix+"_len", asmKind(arch.intSize), "string len", off+arch.ptrSize, arch.intSize, suffix)) case asmComplex: fsize := size / 2 cc = append(cc, newComponent(suffix+"_real", asmKind(fsize), fmt.Sprintf("real(complex%d)", size*8), off, fsize, suffix)) cc = append(cc, newComponent(suffix+"_imag", asmKind(fsize), fmt.Sprintf("imag(complex%d)", size*8), off+fsize, fsize, suffix)) case asmStruct: tu := t.Underlying().(*types.Struct) fields := make([]*types.Var, tu.NumFields()) for i := 0; i < tu.NumFields(); i++ { fields[i] = tu.Field(i) } offsets := arch.sizes.Offsetsof(fields) for i, f := range fields { cc = appendComponentsRecursive(arch, f.Type(), cc, suffix+"_"+f.Name(), off+int(offsets[i])) } case asmArray: tu := t.Underlying().(*types.Array) elem := tu.Elem() // Calculate offset of each element array. fields := []*types.Var{ types.NewVar(token.NoPos, nil, "fake0", elem), types.NewVar(token.NoPos, nil, "fake1", elem), } offsets := arch.sizes.Offsetsof(fields) elemoff := int(offsets[1]) for i := 0; i < int(tu.Len()); i++ { cc = appendComponentsRecursive(arch, elem, cc, suffix+"_"+strconv.Itoa(i), off+i*elemoff) } } return cc } // asmParseDecl parses a function decl for expected assembly variables. func asmParseDecl(pass *analysis.Pass, decl *ast.FuncDecl) map[string]*asmFunc { var ( arch *asmArch fn *asmFunc offset int ) // addParams adds asmVars for each of the parameters in list. // isret indicates whether the list are the arguments or the return values. // TODO(adonovan): simplify by passing (*types.Signature).{Params,Results} // instead of list. addParams := func(list []*ast.Field, isret bool) { argnum := 0 for _, fld := range list { t := pass.TypesInfo.Types[fld.Type].Type // Work around https://golang.org/issue/28277. if t == nil { if ell, ok := fld.Type.(*ast.Ellipsis); ok { t = types.NewSlice(pass.TypesInfo.Types[ell.Elt].Type) } } align := int(arch.sizes.Alignof(t)) size := int(arch.sizes.Sizeof(t)) offset += -offset & (align - 1) cc := componentsOfType(arch, t) // names is the list of names with this type. names := fld.Names if len(names) == 0 { // Anonymous args will be called arg, arg1, arg2, ... // Similarly so for return values: ret, ret1, ret2, ... name := "arg" if isret { name = "ret" } if argnum > 0 { name += strconv.Itoa(argnum) } names = []*ast.Ident{ast.NewIdent(name)} } argnum += len(names) // Create variable for each name. for _, id := range names { name := id.Name for _, c := range cc { outer := name + c.outer v := asmVar{ name: name + c.suffix, kind: c.kind, typ: c.typ, off: offset + c.offset, size: c.size, } 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 } } 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, false) if decl.Type.Results != nil && len(decl.Type.Results.List) > 0 { offset += -offset & (arch.maxAlign - 1) addParams(decl.Type.Results.List, true) } fn.size = offset m[arch.name] = fn } return m } // asmCheckVar checks a single variable reference. func asmCheckVar(badf func(string, ...interface{}), fn *asmFunc, line, expr string, off int, v *asmVar, archDef *asmArch) { m := asmOpcode.FindStringSubmatch(line) if m == nil { if !strings.HasPrefix(strings.TrimSpace(line), "//") { badf("cannot find assembly opcode") } return } addr := strings.HasPrefix(expr, "$") // 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 addr = true case "amd64.LEAQ": dst = 8 addr = true 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, "P") && strings.HasSuffix(op, "RD") { // PINSRD, PEXTRD, etc src = 4 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 op == "MOVO" || op == "MOVOU" { src = 16 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 } } case "mips", "mipsle", "mips64", "mips64le": switch op { case "MOVB", "MOVBU": src = 1 case "MOVH", "MOVHU": src = 2 case "MOVW", "MOVWU", "MOVF": src = 4 case "MOVV", "MOVD": src = 8 } case "s390x": switch op { case "MOVB", "MOVBZ": src = 1 case "MOVH", "MOVHZ": src = 2 case "MOVW", "MOVWZ", "FMOVS": src = 4 case "MOVD", "FMOVD": 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 vs := v.size vt := v.typ switch vk { case asmInterface, asmEmptyInterface, asmString, asmSlice: // allow reference to first word (pointer) vk = v.inner[0].kind vs = v.inner[0].size vt = v.inner[0].typ case asmComplex: // Allow a single instruction to load both parts of a complex. if int(kind) == vs { kind = asmComplex } } if addr { vk = asmKind(archDef.ptrSize) vs = archDef.ptrSize vt = "address" } 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, vs, inner.String()) } }