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