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