github.com/4ad/go@v0.0.0-20161219182952-69a12818b605/src/cmd/vet/asmdecl.go (about)

     1  // Copyright 2013 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // Identify mismatches between assembly files and Go func declarations.
     6  
     7  package main
     8  
     9  import (
    10  	"bytes"
    11  	"fmt"
    12  	"go/ast"
    13  	"go/token"
    14  	"regexp"
    15  	"strconv"
    16  	"strings"
    17  )
    18  
    19  // 'kind' is a kind of assembly variable.
    20  // The kinds 1, 2, 4, 8 stand for values of that size.
    21  type asmKind int
    22  
    23  // These special kinds are not valid sizes.
    24  const (
    25  	asmString asmKind = 100 + iota
    26  	asmSlice
    27  	asmInterface
    28  	asmEmptyInterface
    29  )
    30  
    31  // An asmArch describes assembly parameters for an architecture
    32  type asmArch struct {
    33  	name      string
    34  	ptrSize   int
    35  	intSize   int
    36  	maxAlign  int
    37  	bigEndian bool
    38  	stack     string
    39  	lr        bool
    40  }
    41  
    42  // An asmFunc describes the expected variables for a function on a given architecture.
    43  type asmFunc struct {
    44  	arch        *asmArch
    45  	size        int // size of all arguments
    46  	vars        map[string]*asmVar
    47  	varByOffset map[int]*asmVar
    48  }
    49  
    50  // An asmVar describes a single assembly variable.
    51  type asmVar struct {
    52  	name  string
    53  	kind  asmKind
    54  	typ   string
    55  	off   int
    56  	size  int
    57  	inner []*asmVar
    58  }
    59  
    60  var (
    61  	asmArch386      = asmArch{"386", 4, 4, 4, false, "SP", false}
    62  	asmArchArm      = asmArch{"arm", 4, 4, 4, false, "R13", true}
    63  	asmArchArm64    = asmArch{"arm64", 8, 8, 8, false, "RSP", true}
    64  	asmArchAmd64    = asmArch{"amd64", 8, 8, 8, false, "SP", false}
    65  	asmArchAmd64p32 = asmArch{"amd64p32", 4, 4, 8, false, "SP", false}
    66  	asmArchMips64   = asmArch{"mips64", 8, 8, 8, true, "R29", true}
    67  	asmArchMips64LE = asmArch{"mips64", 8, 8, 8, false, "R29", true}
    68  	asmArchPpc64    = asmArch{"ppc64", 8, 8, 8, true, "R1", true}
    69  	asmArchPpc64LE  = asmArch{"ppc64le", 8, 8, 8, false, "R1", true}
    70  	asmArchSparc64  = asmArch{"sparc64", 8, 8, 8, true, "RSP", true}
    71  
    72  	arches = []*asmArch{
    73  		&asmArch386,
    74  		&asmArchArm,
    75  		&asmArchArm64,
    76  		&asmArchAmd64,
    77  		&asmArchAmd64p32,
    78  		&asmArchMips64,
    79  		&asmArchMips64LE,
    80  		&asmArchPpc64,
    81  		&asmArchPpc64LE,
    82  		&asmArchSparc64,
    83  	}
    84  )
    85  
    86  var (
    87  	re           = regexp.MustCompile
    88  	asmPlusBuild = re(`//\s+\+build\s+([^\n]+)`)
    89  	asmTEXT      = re(`\bTEXT\b.*ยท([^\(]+)\(SB\)(?:\s*,\s*([0-9A-Z|+]+))?(?:\s*,\s*\$(-?[0-9]+)(?:-([0-9]+))?)?`)
    90  	asmDATA      = re(`\b(DATA|GLOBL)\b`)
    91  	asmNamedFP   = re(`([a-zA-Z0-9_\xFF-\x{10FFFF}]+)(?:\+([0-9]+))\(FP\)`)
    92  	asmUnnamedFP = re(`[^+\-0-9](([0-9]+)\(FP\))`)
    93  	asmSP        = re(`[^+\-0-9](([0-9]+)\(([A-Z0-9]+)\))`)
    94  	asmOpcode    = re(`^\s*(?:[A-Z0-9a-z_]+:)?\s*([A-Z]+)\s*([^,]*)(?:,\s*(.*))?`)
    95  	ppc64Suff    = re(`([BHWD])(ZU|Z|U|BR)?$`)
    96  )
    97  
    98  func asmCheck(pkg *Package) {
    99  	if !vet("asmdecl") {
   100  		return
   101  	}
   102  
   103  	// No work if no assembly files.
   104  	if !pkg.hasFileWithSuffix(".s") {
   105  		return
   106  	}
   107  
   108  	// Gather declarations. knownFunc[name][arch] is func description.
   109  	knownFunc := make(map[string]map[string]*asmFunc)
   110  
   111  	for _, f := range pkg.files {
   112  		if f.file != nil {
   113  			for _, decl := range f.file.Decls {
   114  				if decl, ok := decl.(*ast.FuncDecl); ok && decl.Body == nil {
   115  					knownFunc[decl.Name.Name] = f.asmParseDecl(decl)
   116  				}
   117  			}
   118  		}
   119  	}
   120  
   121  Files:
   122  	for _, f := range pkg.files {
   123  		if !strings.HasSuffix(f.name, ".s") {
   124  			continue
   125  		}
   126  		Println("Checking file", f.name)
   127  
   128  		// Determine architecture from file name if possible.
   129  		var arch string
   130  		var archDef *asmArch
   131  		for _, a := range arches {
   132  			if strings.HasSuffix(f.name, "_"+a.name+".s") {
   133  				arch = a.name
   134  				archDef = a
   135  				break
   136  			}
   137  		}
   138  
   139  		lines := strings.SplitAfter(string(f.content), "\n")
   140  		var (
   141  			fn                 *asmFunc
   142  			fnName             string
   143  			localSize, argSize int
   144  			wroteSP            bool
   145  			haveRetArg         bool
   146  			retLine            []int
   147  		)
   148  
   149  		flushRet := func() {
   150  			if fn != nil && fn.vars["ret"] != nil && !haveRetArg && len(retLine) > 0 {
   151  				v := fn.vars["ret"]
   152  				for _, line := range retLine {
   153  					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)
   154  				}
   155  			}
   156  			retLine = nil
   157  		}
   158  		for lineno, line := range lines {
   159  			lineno++
   160  
   161  			badf := func(format string, args ...interface{}) {
   162  				f.Badf(token.NoPos, "%s:%d: [%s] %s: %s", f.name, lineno, arch, fnName, fmt.Sprintf(format, args...))
   163  			}
   164  
   165  			if arch == "" {
   166  				// Determine architecture from +build line if possible.
   167  				if m := asmPlusBuild.FindStringSubmatch(line); m != nil {
   168  				Fields:
   169  					for _, fld := range strings.Fields(m[1]) {
   170  						for _, a := range arches {
   171  							if a.name == fld {
   172  								arch = a.name
   173  								archDef = a
   174  								break Fields
   175  							}
   176  						}
   177  					}
   178  				}
   179  			}
   180  
   181  			if m := asmTEXT.FindStringSubmatch(line); m != nil {
   182  				flushRet()
   183  				if arch == "" {
   184  					f.Warnf(token.NoPos, "%s: cannot determine architecture for assembly file", f.name)
   185  					continue Files
   186  				}
   187  				fnName = m[1]
   188  				fn = knownFunc[m[1]][arch]
   189  				if fn != nil {
   190  					size, _ := strconv.Atoi(m[4])
   191  					if size != fn.size && (m[2] != "7" && !strings.Contains(m[2], "NOSPLIT") || size != 0) {
   192  						badf("wrong argument size %d; expected $...-%d", size, fn.size)
   193  					}
   194  				}
   195  				localSize, _ = strconv.Atoi(m[3])
   196  				localSize += archDef.intSize
   197  				if archDef.lr {
   198  					// Account for caller's saved LR
   199  					localSize += archDef.intSize
   200  				}
   201  				argSize, _ = strconv.Atoi(m[4])
   202  				if fn == nil && !strings.Contains(fnName, "<>") {
   203  					badf("function %s missing Go declaration", fnName)
   204  				}
   205  				wroteSP = false
   206  				haveRetArg = false
   207  				continue
   208  			} else if strings.Contains(line, "TEXT") && strings.Contains(line, "SB") {
   209  				// function, but not visible from Go (didn't match asmTEXT), so stop checking
   210  				flushRet()
   211  				fn = nil
   212  				fnName = ""
   213  				continue
   214  			}
   215  
   216  			if strings.Contains(line, "RET") {
   217  				retLine = append(retLine, lineno)
   218  			}
   219  
   220  			if fnName == "" {
   221  				continue
   222  			}
   223  
   224  			if asmDATA.FindStringSubmatch(line) != nil {
   225  				fn = nil
   226  			}
   227  
   228  			if archDef == nil {
   229  				continue
   230  			}
   231  
   232  			if strings.Contains(line, ", "+archDef.stack) || strings.Contains(line, ",\t"+archDef.stack) {
   233  				wroteSP = true
   234  				continue
   235  			}
   236  
   237  			for _, m := range asmSP.FindAllStringSubmatch(line, -1) {
   238  				if m[3] != archDef.stack || wroteSP {
   239  					continue
   240  				}
   241  				off := 0
   242  				if m[1] != "" {
   243  					off, _ = strconv.Atoi(m[2])
   244  				}
   245  				if off >= localSize {
   246  					if fn != nil {
   247  						v := fn.varByOffset[off-localSize]
   248  						if v != nil {
   249  							badf("%s should be %s+%d(FP)", m[1], v.name, off-localSize)
   250  							continue
   251  						}
   252  					}
   253  					if off >= localSize+argSize {
   254  						badf("use of %s points beyond argument frame", m[1])
   255  						continue
   256  					}
   257  					badf("use of %s to access argument frame", m[1])
   258  				}
   259  			}
   260  
   261  			if fn == nil {
   262  				continue
   263  			}
   264  
   265  			for _, m := range asmUnnamedFP.FindAllStringSubmatch(line, -1) {
   266  				off, _ := strconv.Atoi(m[2])
   267  				v := fn.varByOffset[off]
   268  				if v != nil {
   269  					badf("use of unnamed argument %s; offset %d is %s+%d(FP)", m[1], off, v.name, v.off)
   270  				} else {
   271  					badf("use of unnamed argument %s", m[1])
   272  				}
   273  			}
   274  
   275  			for _, m := range asmNamedFP.FindAllStringSubmatch(line, -1) {
   276  				name := m[1]
   277  				off := 0
   278  				if m[2] != "" {
   279  					off, _ = strconv.Atoi(m[2])
   280  				}
   281  				if name == "ret" || strings.HasPrefix(name, "ret_") {
   282  					haveRetArg = true
   283  				}
   284  				v := fn.vars[name]
   285  				if v == nil {
   286  					// Allow argframe+0(FP).
   287  					if name == "argframe" && off == 0 {
   288  						continue
   289  					}
   290  					v = fn.varByOffset[off]
   291  					if v != nil {
   292  						badf("unknown variable %s; offset %d is %s+%d(FP)", name, off, v.name, v.off)
   293  					} else {
   294  						badf("unknown variable %s", name)
   295  					}
   296  					continue
   297  				}
   298  				asmCheckVar(badf, fn, line, m[0], off, v)
   299  			}
   300  		}
   301  		flushRet()
   302  	}
   303  }
   304  
   305  // asmParseDecl parses a function decl for expected assembly variables.
   306  func (f *File) asmParseDecl(decl *ast.FuncDecl) map[string]*asmFunc {
   307  	var (
   308  		arch   *asmArch
   309  		fn     *asmFunc
   310  		offset int
   311  		failed bool
   312  	)
   313  
   314  	addVar := func(outer string, v asmVar) {
   315  		if vo := fn.vars[outer]; vo != nil {
   316  			vo.inner = append(vo.inner, &v)
   317  		}
   318  		fn.vars[v.name] = &v
   319  		for i := 0; i < v.size; i++ {
   320  			fn.varByOffset[v.off+i] = &v
   321  		}
   322  	}
   323  
   324  	addParams := func(list []*ast.Field) {
   325  		for i, fld := range list {
   326  			// Determine alignment, size, and kind of type in declaration.
   327  			var align, size int
   328  			var kind asmKind
   329  			names := fld.Names
   330  			typ := f.gofmt(fld.Type)
   331  			switch t := fld.Type.(type) {
   332  			default:
   333  				switch typ {
   334  				default:
   335  					f.Warnf(fld.Type.Pos(), "unknown assembly argument type %s", typ)
   336  					failed = true
   337  					return
   338  				case "int8", "uint8", "byte", "bool":
   339  					size = 1
   340  				case "int16", "uint16":
   341  					size = 2
   342  				case "int32", "uint32", "float32":
   343  					size = 4
   344  				case "int64", "uint64", "float64":
   345  					align = arch.maxAlign
   346  					size = 8
   347  				case "int", "uint":
   348  					size = arch.intSize
   349  				case "uintptr", "iword", "Word", "Errno", "unsafe.Pointer":
   350  					size = arch.ptrSize
   351  				case "string", "ErrorString":
   352  					size = arch.ptrSize * 2
   353  					align = arch.ptrSize
   354  					kind = asmString
   355  				}
   356  			case *ast.ChanType, *ast.FuncType, *ast.MapType, *ast.StarExpr:
   357  				size = arch.ptrSize
   358  			case *ast.InterfaceType:
   359  				align = arch.ptrSize
   360  				size = 2 * arch.ptrSize
   361  				if len(t.Methods.List) > 0 {
   362  					kind = asmInterface
   363  				} else {
   364  					kind = asmEmptyInterface
   365  				}
   366  			case *ast.ArrayType:
   367  				if t.Len == nil {
   368  					size = arch.ptrSize + 2*arch.intSize
   369  					align = arch.ptrSize
   370  					kind = asmSlice
   371  					break
   372  				}
   373  				f.Warnf(fld.Type.Pos(), "unsupported assembly argument type %s", typ)
   374  				failed = true
   375  			case *ast.StructType:
   376  				f.Warnf(fld.Type.Pos(), "unsupported assembly argument type %s", typ)
   377  				failed = true
   378  			}
   379  			if align == 0 {
   380  				align = size
   381  			}
   382  			if kind == 0 {
   383  				kind = asmKind(size)
   384  			}
   385  			offset += -offset & (align - 1)
   386  
   387  			// Create variable for each name being declared with this type.
   388  			if len(names) == 0 {
   389  				name := "unnamed"
   390  				if decl.Type.Results != nil && len(decl.Type.Results.List) > 0 && &list[0] == &decl.Type.Results.List[0] && i == 0 {
   391  					// Assume assembly will refer to single unnamed result as r.
   392  					name = "ret"
   393  				}
   394  				names = []*ast.Ident{{Name: name}}
   395  			}
   396  			for _, id := range names {
   397  				name := id.Name
   398  				addVar("", asmVar{
   399  					name: name,
   400  					kind: kind,
   401  					typ:  typ,
   402  					off:  offset,
   403  					size: size,
   404  				})
   405  				switch kind {
   406  				case 8:
   407  					if arch.ptrSize == 4 {
   408  						w1, w2 := "lo", "hi"
   409  						if arch.bigEndian {
   410  							w1, w2 = w2, w1
   411  						}
   412  						addVar(name, asmVar{
   413  							name: name + "_" + w1,
   414  							kind: 4,
   415  							typ:  "half " + typ,
   416  							off:  offset,
   417  							size: 4,
   418  						})
   419  						addVar(name, asmVar{
   420  							name: name + "_" + w2,
   421  							kind: 4,
   422  							typ:  "half " + typ,
   423  							off:  offset + 4,
   424  							size: 4,
   425  						})
   426  					}
   427  
   428  				case asmEmptyInterface:
   429  					addVar(name, asmVar{
   430  						name: name + "_type",
   431  						kind: asmKind(arch.ptrSize),
   432  						typ:  "interface type",
   433  						off:  offset,
   434  						size: arch.ptrSize,
   435  					})
   436  					addVar(name, asmVar{
   437  						name: name + "_data",
   438  						kind: asmKind(arch.ptrSize),
   439  						typ:  "interface data",
   440  						off:  offset + arch.ptrSize,
   441  						size: arch.ptrSize,
   442  					})
   443  
   444  				case asmInterface:
   445  					addVar(name, asmVar{
   446  						name: name + "_itable",
   447  						kind: asmKind(arch.ptrSize),
   448  						typ:  "interface itable",
   449  						off:  offset,
   450  						size: arch.ptrSize,
   451  					})
   452  					addVar(name, asmVar{
   453  						name: name + "_data",
   454  						kind: asmKind(arch.ptrSize),
   455  						typ:  "interface data",
   456  						off:  offset + arch.ptrSize,
   457  						size: arch.ptrSize,
   458  					})
   459  
   460  				case asmSlice:
   461  					addVar(name, asmVar{
   462  						name: name + "_base",
   463  						kind: asmKind(arch.ptrSize),
   464  						typ:  "slice base",
   465  						off:  offset,
   466  						size: arch.ptrSize,
   467  					})
   468  					addVar(name, asmVar{
   469  						name: name + "_len",
   470  						kind: asmKind(arch.intSize),
   471  						typ:  "slice len",
   472  						off:  offset + arch.ptrSize,
   473  						size: arch.intSize,
   474  					})
   475  					addVar(name, asmVar{
   476  						name: name + "_cap",
   477  						kind: asmKind(arch.intSize),
   478  						typ:  "slice cap",
   479  						off:  offset + arch.ptrSize + arch.intSize,
   480  						size: arch.intSize,
   481  					})
   482  
   483  				case asmString:
   484  					addVar(name, asmVar{
   485  						name: name + "_base",
   486  						kind: asmKind(arch.ptrSize),
   487  						typ:  "string base",
   488  						off:  offset,
   489  						size: arch.ptrSize,
   490  					})
   491  					addVar(name, asmVar{
   492  						name: name + "_len",
   493  						kind: asmKind(arch.intSize),
   494  						typ:  "string len",
   495  						off:  offset + arch.ptrSize,
   496  						size: arch.intSize,
   497  					})
   498  				}
   499  				offset += size
   500  			}
   501  		}
   502  	}
   503  
   504  	m := make(map[string]*asmFunc)
   505  	for _, arch = range arches {
   506  		fn = &asmFunc{
   507  			arch:        arch,
   508  			vars:        make(map[string]*asmVar),
   509  			varByOffset: make(map[int]*asmVar),
   510  		}
   511  		offset = 0
   512  		addParams(decl.Type.Params.List)
   513  		if decl.Type.Results != nil && len(decl.Type.Results.List) > 0 {
   514  			offset += -offset & (arch.maxAlign - 1)
   515  			addParams(decl.Type.Results.List)
   516  		}
   517  		fn.size = offset
   518  		m[arch.name] = fn
   519  	}
   520  
   521  	if failed {
   522  		return nil
   523  	}
   524  	return m
   525  }
   526  
   527  // asmCheckVar checks a single variable reference.
   528  func asmCheckVar(badf func(string, ...interface{}), fn *asmFunc, line, expr string, off int, v *asmVar) {
   529  	m := asmOpcode.FindStringSubmatch(line)
   530  	if m == nil {
   531  		if !strings.HasPrefix(strings.TrimSpace(line), "//") {
   532  			badf("cannot find assembly opcode")
   533  		}
   534  		return
   535  	}
   536  
   537  	// Determine operand sizes from instruction.
   538  	// Typically the suffix suffices, but there are exceptions.
   539  	var src, dst, kind asmKind
   540  	op := m[1]
   541  	switch fn.arch.name + "." + op {
   542  	case "386.FMOVLP":
   543  		src, dst = 8, 4
   544  	case "arm.MOVD":
   545  		src = 8
   546  	case "arm.MOVW":
   547  		src = 4
   548  	case "arm.MOVH", "arm.MOVHU":
   549  		src = 2
   550  	case "arm.MOVB", "arm.MOVBU":
   551  		src = 1
   552  	// LEA* opcodes don't really read the second arg.
   553  	// They just take the address of it.
   554  	case "386.LEAL":
   555  		dst = 4
   556  	case "amd64.LEAQ":
   557  		dst = 8
   558  	case "amd64p32.LEAL":
   559  		dst = 4
   560  	default:
   561  		switch fn.arch.name {
   562  		case "386", "amd64":
   563  			if strings.HasPrefix(op, "F") && (strings.HasSuffix(op, "D") || strings.HasSuffix(op, "DP")) {
   564  				// FMOVDP, FXCHD, etc
   565  				src = 8
   566  				break
   567  			}
   568  			if strings.HasPrefix(op, "P") && strings.HasSuffix(op, "RD") {
   569  				// PINSRD, PEXTRD, etc
   570  				src = 4
   571  				break
   572  			}
   573  			if strings.HasPrefix(op, "F") && (strings.HasSuffix(op, "F") || strings.HasSuffix(op, "FP")) {
   574  				// FMOVFP, FXCHF, etc
   575  				src = 4
   576  				break
   577  			}
   578  			if strings.HasSuffix(op, "SD") {
   579  				// MOVSD, SQRTSD, etc
   580  				src = 8
   581  				break
   582  			}
   583  			if strings.HasSuffix(op, "SS") {
   584  				// MOVSS, SQRTSS, etc
   585  				src = 4
   586  				break
   587  			}
   588  			if strings.HasPrefix(op, "SET") {
   589  				// SETEQ, etc
   590  				src = 1
   591  				break
   592  			}
   593  			switch op[len(op)-1] {
   594  			case 'B':
   595  				src = 1
   596  			case 'W':
   597  				src = 2
   598  			case 'L':
   599  				src = 4
   600  			case 'D', 'Q':
   601  				src = 8
   602  			}
   603  		case "ppc64", "ppc64le":
   604  			// Strip standard suffixes to reveal size letter.
   605  			m := ppc64Suff.FindStringSubmatch(op)
   606  			if m != nil {
   607  				switch m[1][0] {
   608  				case 'B':
   609  					src = 1
   610  				case 'H':
   611  					src = 2
   612  				case 'W':
   613  					src = 4
   614  				case 'D':
   615  					src = 8
   616  				}
   617  			}
   618  		case "mips64", "mips64le":
   619  			switch op {
   620  			case "MOVB", "MOVBU":
   621  				src = 1
   622  			case "MOVH", "MOVHU":
   623  				src = 2
   624  			case "MOVW", "MOVWU", "MOVF":
   625  				src = 4
   626  			case "MOVV", "MOVD":
   627  				src = 8
   628  			}
   629  		case "sparc64":
   630  			switch op[len(op)-1] {
   631  			case 'B':
   632  				src = 1
   633  			case 'H':
   634  				src = 2
   635  			case 'W':
   636  				src = 4
   637  			case 'D':
   638  				src = 8
   639  			}
   640  		}
   641  	}
   642  	if dst == 0 {
   643  		dst = src
   644  	}
   645  
   646  	// Determine whether the match we're holding
   647  	// is the first or second argument.
   648  	if strings.Index(line, expr) > strings.Index(line, ",") {
   649  		kind = dst
   650  	} else {
   651  		kind = src
   652  	}
   653  
   654  	vk := v.kind
   655  	vt := v.typ
   656  	switch vk {
   657  	case asmInterface, asmEmptyInterface, asmString, asmSlice:
   658  		// allow reference to first word (pointer)
   659  		vk = v.inner[0].kind
   660  		vt = v.inner[0].typ
   661  	}
   662  
   663  	if off != v.off {
   664  		var inner bytes.Buffer
   665  		for i, vi := range v.inner {
   666  			if len(v.inner) > 1 {
   667  				fmt.Fprintf(&inner, ",")
   668  			}
   669  			fmt.Fprintf(&inner, " ")
   670  			if i == len(v.inner)-1 {
   671  				fmt.Fprintf(&inner, "or ")
   672  			}
   673  			fmt.Fprintf(&inner, "%s+%d(FP)", vi.name, vi.off)
   674  		}
   675  		badf("invalid offset %s; expected %s+%d(FP)%s", expr, v.name, v.off, inner.String())
   676  		return
   677  	}
   678  	if kind != 0 && kind != vk {
   679  		var inner bytes.Buffer
   680  		if len(v.inner) > 0 {
   681  			fmt.Fprintf(&inner, " containing")
   682  			for i, vi := range v.inner {
   683  				if i > 0 && len(v.inner) > 2 {
   684  					fmt.Fprintf(&inner, ",")
   685  				}
   686  				fmt.Fprintf(&inner, " ")
   687  				if i > 0 && i == len(v.inner)-1 {
   688  					fmt.Fprintf(&inner, "and ")
   689  				}
   690  				fmt.Fprintf(&inner, "%s+%d(FP)", vi.name, vi.off)
   691  			}
   692  		}
   693  		badf("invalid %s of %s; %s is %d-byte value%s", op, expr, vt, vk, inner.String())
   694  	}
   695  }