diff --git a/asm/aarch64_goobj_test.go b/asm/aarch64_goobj_test.go index e8e11d6..247b440 100644 --- a/asm/aarch64_goobj_test.go +++ b/asm/aarch64_goobj_test.go @@ -4,6 +4,7 @@ package asm import ( + "encoding/binary" "os" "os/exec" "path/filepath" @@ -61,6 +62,62 @@ TEXT ·add(SB), NOSPLIT, $0-24 } } +// TestGOObjectAARCH64PairReloc pins the ADRP-pair relocation shape against +// the toolchain's own object for the same source: exactly one R_ADDRARM64 +// of Siz 8 at the ADRP word (cmd/internal/obj/arm64/asm7.go adds a single +// Siz-8 relocation per pair and the linker patches both instructions from +// it). gasm's assembler records the ADRP+ADD form as two word relocs; the +// emitter must coalesce them, not emit two Siz-4 records. +func TestGOObjectAARCH64PairReloc(t *testing.T) { + f, errs := parser.Parse("gv_arm64.s", ` +#include "textflag.h" + +TEXT ·getv(SB), NOSPLIT, $0-8 + MOVD $v<>(SB), R4 + MOVD R4, ret+0(FP) + RET + +GLOBL v<>(SB), RODATA, $8 +DATA v<>+0(SB)/8, $7 +`) + if len(errs) > 0 { + t.Fatalf("parse: %v", errs) + } + img, err := AssembleFileARM64(f) + if err != nil { + t.Fatalf("AssembleFileARM64: %v", err) + } + obj, err := img.GOObjectAARCH64("main", "gv_arm64.s") + if err != nil { + t.Fatalf("GOObjectAARCH64: %v", err) + } + v := openGoobj(t, obj) + relocs := v.blk(blkReloc) + le := binary.LittleEndian + // Two DWARF relocs on the lines/DIE symbols, then the code's one pair + // relocation. + if len(relocs) != 3*23 { + t.Fatalf("relocs = %d bytes, want three entries", len(relocs)) + } + cr := relocs[2*23:] + if off := int32(le.Uint32(cr[0:])); off != 0 { + t.Errorf("pair reloc off = %d, want 0 (the ADRP word)", off) + } + if siz := cr[4]; siz != 8 { + t.Errorf("pair reloc siz = %d, want 8", siz) + } + if typ := le.Uint16(cr[5:]); typ != relocArm64Addr { + t.Errorf("pair reloc type = %d, want %d (R_ADDRARM64)", typ, relocArm64Addr) + } + if pkg := le.Uint32(cr[15:]); pkg != pkgIdxSelf { + t.Errorf("pair reloc PkgIdx = %#x, want pkgIdxSelf", pkg) + } + // The GLOBL is the first package definition. + if sym := le.Uint32(cr[19:]); sym != 0 { + t.Errorf("pair reloc SymIdx = %d, want 0 (the GLOBL definition)", sym) + } +} + // TestGOObjectAARCH64Link does an end-to-end link test: it cross-compiles a // Go program for arm64, substitutes the gasm-produced object into the package // archive, re-links with cmd/link, and verifies the symbol appears in the @@ -79,6 +136,14 @@ TEXT ·add(SB), NOSPLIT, $0-24 ADD R5, R4, R4 MOVD R4, ret+16(FP) RET + +TEXT ·getv(SB), NOSPLIT, $0-8 + MOVD $v<>(SB), R4 + MOVD R4, ret+0(FP) + RET + +GLOBL v<>(SB), RODATA, $8 +DATA v<>+0(SB)/8, $7 ` if err := os.WriteFile(filepath.Join(dir, "main_arm64.s"), []byte(asmSrc), 0o644); err != nil { t.Fatal(err) @@ -86,11 +151,15 @@ TEXT ·add(SB), NOSPLIT, $0-24 mainSrc := `package main func add(a, b int64) int64 +func getv() *int64 func main() { if add(20, 22) != 42 { panic("bad add") } + if getv() == nil { + panic("bad getv") + } } ` if err := os.WriteFile(filepath.Join(dir, "main.go"), []byte(mainSrc), 0o644); err != nil { diff --git a/asm/goobj.go b/asm/goobj.go index ac73064..2f92e33 100644 --- a/asm/goobj.go +++ b/asm/goobj.go @@ -68,11 +68,12 @@ const ( kindSDWARFLINES = 20 ) -// Symbol flags (cmd/internal/goobj). +// Symbol flags (cmd/internal/goobj). The linkname flag is set only for +// //go:linkname symbols (and main.main); ordinary assembly symbols carry +// none, matching cmd/asm's output. const ( symFlagDupok = 0x01 symFlagNoSplit = 0x10 - symFlag2Link = 0x10 // asm objects flag every named symbol as linkname symABIStatic = 0xffff ) @@ -287,7 +288,7 @@ func (img *Image) emitGOObject(pkgPath, srcPath string, pre []byte, minLC int, r } } nps = append(nps, npSym{ - sym: goSym{name: name, abi: abi, typ: kindSTEXT, flag: flag, flag2: symFlag2Link, size: uint32(fn.Size)}, + sym: goSym{name: name, abi: abi, typ: kindSTEXT, flag: flag, size: uint32(fn.Size)}, data: code, }) } @@ -317,7 +318,7 @@ func (img *Image) emitGOObject(pkgPath, srcPath string, pre []byte, minLC int, r abi = symABIStatic } defIdx[d.Name] = len(defs) - defs = append(defs, goSym{name: name, abi: abi, typ: typ, flag: flag, flag2: symFlag2Link, size: uint32(d.Size)}) + defs = append(defs, goSym{name: name, abi: abi, typ: typ, flag: flag, size: uint32(d.Size)}) defData = append(defData, img.Data[d.Offset:d.Offset+d.Size]) } fnFiIdx := make([]int, len(img.Funcs)) diff --git a/asm/goobj_resolve.go b/asm/goobj_resolve.go index b27f459..e002d6e 100644 --- a/asm/goobj_resolve.go +++ b/asm/goobj_resolve.go @@ -40,8 +40,11 @@ func exportPath(importPath string) (string, error) { // // refs maps package import paths to the symbol names referenced from that // package. The returned pkgIdx maps each import path to its position in -// the blkPkgIdx table (0-based), and symIdx gives each symbol's index within -// its package. +// the blkPkgIdx table, which reserves index 0 for the dummy invalid +// package (cmd/internal/obj/sym.go: "0 is invalid index"; the loader's +// reader loop starts at 1), so package i sits at block index i+1 and its +// relocations carry i+1. symIdx gives each symbol's index within its +// package. func resolveExternalGOOBJ(refs map[string][]string) (pkgIdx map[string]int, symIdx map[string]int, err error) { pkgIdx = make(map[string]int, len(refs)) symIdx = make(map[string]int) @@ -50,7 +53,9 @@ func resolveExternalGOOBJ(refs map[string][]string) (pkgIdx map[string]int, symI packages := sortedPkgRefs(refs) for i, pkg := range packages { - pkgIdx[pkg.path] = i + // Block index 0 is the dummy invalid package; the first real + // package starts at 1. + pkgIdx[pkg.path] = i + 1 exp, err := exportPath(pkg.path) if err != nil { return nil, nil, err @@ -145,40 +150,65 @@ func parseArDecimal(b []byte) int { } // goobjFile is a parsed GOOBJ file: the string table and the symbol-definition -// block. +// blocks. The hashed blocks are kept raw: their symbols carry no names, only +// the loader needs their counts. type goobjFile struct { - strTab []byte // string table, at headerSize + n - symdef []byte // blkSymdef raw block - npdef []byte // blkNonpkgdef raw block + strTab []byte // string table, at headerSize + n + symdef []byte // blkSymdef raw block + hashed64 []byte // blkHashed64def raw block + hashed []byte // blkHasheddef raw block + npdef []byte // blkNonpkgdef raw block } -// symbols returns all symbol names in definition order by scanning the -// symdef and nonpkgdef blocks and resolving each name through the string -// table. Package definitions (blkSymdef) use fully-qualified names like -// "runtime.morestack"; non-package definitions (blkNonpkgdef) use bare -// names like "morestack". This combined list matches the index the -// linker expects for cross-package references. +// loaderIndexBase returns the index the first nonpkgdef symbol occupies in the +// loader's per-object symbol array. cmd/link lays the definition blocks out as +// symdef, hashed64def, hasheddef, nonpkgdef, nonpkgref (loader.go: preloadSyms +// fills r.syms in exactly that order, and resolve() indexes PkgIdxNone and +// cross-package SymIdx into it), so a symbol found in blkNonpkgdef carries the +// three leading blocks' symbol counts as its base. +func (f *goobjFile) loaderIndexBase() int { + return len(f.symdef)/recSymSize + len(f.hashed64)/recSymSize + len(f.hashed)/recSymSize +} + +// symbols returns the names of the symdef and nonpkgdef blocks in +// definition order. Package definitions (blkSymdef) use fully-qualified +// names like "runtime.morestack"; non-package definitions (blkNonpkgdef) +// use bare names like "morestack". For lookups by index prefer +// findSymbol: it adds the hashed blocks' count the loader's array +// interleaves between the two. func (f *goobjFile) symbols() []string { return append(f.defNames(), f.npdefNames()...) } -// findSymbol returns the index of a symbol within the combined symbol list, -// or -1 if not found. It first tries the fully-qualified name (pkg.name), -// then the bare name. +// findSymbol returns the index of a symbol within the loader's per-object +// symbol array, or -1 if not found. It first tries the fully-qualified +// name (pkg.name), then the bare name (assembly objects store dotless +// names, e.g. runtime's "gogo", for symbols other packages reach through +// a linkname). func (f *goobjFile) findSymbol(pkg, name string) int { + base := f.loaderIndexBase() qualified := pkg + "." + name - syms := f.symbols() - for i, s := range syms { + for i, s := range f.defNames() { if s == qualified { return i } } - // Try bare name (for non-package definitions). - for i, s := range syms { + for i, s := range f.npdefNames() { + if s == qualified { + return base + i + } + } + // Try bare name (for dotless assembly definitions). + for i, s := range f.defNames() { if s == name { return i } } + for i, s := range f.npdefNames() { + if s == name { + return base + i + } + } return -1 } @@ -192,12 +222,16 @@ func (f *goobjFile) npdefNames() []string { return f.readSymNames(f.npdef) } +// recSymSize is the size of one Sym record in the definition blocks +// (goobj.SymSize: stringRefSize + 2 + 1 + 1 + 1 + 4 + 4). +const recSymSize = 21 + // readSymNames reads symbol names from a symdef/nonpkgdef block. Each record // is 21 bytes: nameLen (u32), nameOff (u32), abi (u16), typ, flag, flag2, // size (u32), align (u32). nameOff is an absolute offset into the string // table. func (f *goobjFile) readSymNames(block []byte) []string { - const recSize = 21 + const recSize = recSymSize if len(block) < recSize { return nil } @@ -247,16 +281,18 @@ func parseGOOBJ(data []byte) (*goobjFile, error) { // [16:20] flags // [20:96] 19 × uint32 offsets var offs [blkEnd + 1]uint32 - for i := 0; i <= blkEnd; i++ { + for i := range blkEnd + 1 { offs[i] = binary.LittleEndian.Uint32(payload[20+4*i:]) } // The string table lives at headerSize. strTabStart := uint32(goobjHeaderSize) f := &goobjFile{ - strTab: payload[strTabStart:offs[0]], - symdef: blockSlice(payload, offs, blkSymdef, blkSymdef+1), - npdef: blockSlice(payload, offs, blkNonpkgdef, blkNonpkgdef+1), + strTab: payload[strTabStart:offs[0]], + symdef: blockSlice(payload, offs, blkSymdef, blkSymdef+1), + hashed64: blockSlice(payload, offs, blkHashed64def, blkHashed64def+1), + hashed: blockSlice(payload, offs, blkHasheddef, blkHasheddef+1), + npdef: blockSlice(payload, offs, blkNonpkgdef, blkNonpkgdef+1), } return f, nil } @@ -306,22 +342,26 @@ func resolveExternalSymbols(externals []string) (pkgTable []string, pkgIdxMap ma return nil, nil, nil, err } - // Build the package table in pkgIdx order. + // Build the package table in pkgIdx order. The indices are 1-based + // (0 is the dummy invalid package, written by the emitter itself), so + // the table without the dummy is indexed one below. pkgTable = make([]string, len(pkgIdx1)) for pkg, idx := range pkgIdx1 { - pkgTable[idx] = pkg + pkgTable[idx-1] = pkg } return pkgTable, pkgIdx1, symIdx1, nil } // splitQualified splits a qualified Go symbol name (pkgpath·name) into its -// package path and local name. The separator is the middle dot (U+00B7). -// If no separator is found, the symbol is assumed to be in the current -// package (empty pkg). +// package path and local name. The separator is the middle dot (U+00B7), +// whose UTF-8 encoding is two bytes, so the search must be string-based: +// IndexByte would match only the second byte and leave the lead byte on +// the package path. If no separator is found, the symbol is assumed to be +// in the current package (empty pkg). func splitQualified(full string) (pkg, name string) { - if idx := strings.IndexByte(full, '\u00b7'); idx >= 0 { - return full[:idx], full[idx+len("\u00b7"):] + if before, after, ok := strings.Cut(full, "\u00b7"); ok { + return before, after } if before, after, ok := strings.Cut(full, "."); ok { return before, after diff --git a/asm/goobj_resolve_test.go b/asm/goobj_resolve_test.go index 1e75a89..7a7280a 100644 --- a/asm/goobj_resolve_test.go +++ b/asm/goobj_resolve_test.go @@ -56,8 +56,12 @@ func TestResolveExternalSymbols(t *testing.T) { if err != nil { t.Fatalf("resolveExternalGOOBJ: %v", err) } - if len(pkgIdx) != 1 || pkgIdx["runtime"] != 0 { - t.Errorf("pkgIdx = %v, want runtime→0", pkgIdx) + if len(pkgIdx) != 1 || pkgIdx["runtime"] != 1 { + // Index 0 is the dummy invalid package in the blkPkgIdx table; + // the loader's reader loop starts at 1 (cmd/link/internal/ + // loader/loader.go: "PkgIdx 0 is a dummy invalid package"), so + // the first real package must carry index 1. + t.Errorf("pkgIdx = %v, want runtime→1", pkgIdx) } if _, ok := symIdx["runtime·g0"]; !ok { t.Errorf("symIdx missing runtime·g0, got %v", symIdx) diff --git a/asm/goobj_test.go b/asm/goobj_test.go index 7061f27..393d185 100644 --- a/asm/goobj_test.go +++ b/asm/goobj_test.go @@ -117,7 +117,9 @@ DATA mask<>+8(SB)/8, $0x800f0e0d0c0b0a09 if len(defs) != 7 { t.Fatalf("symdefs = %d, want 7", len(defs)) } - if defs[0].name != "mask" || defs[0].abi != 0xffff || defs[0].typ != kindSRODATA || defs[0].size != 16 || defs[0].flag2 != symFlag2Link { + // The linkname flag stays clear: the toolchain sets it only for + // //go:linkname symbols, and an ordinary static GLOBL is not one. + if defs[0].name != "mask" || defs[0].abi != 0xffff || defs[0].typ != kindSRODATA || defs[0].size != 16 || defs[0].flag2 != 0 { t.Errorf("mask symbol = %+v", defs[0]) } if defs[1].name != "" || defs[1].typ != kindSDATA || defs[1].size != 28 { @@ -511,3 +513,188 @@ func fieldAfter(line, flag string) string { } return "" } + +// TestGOObjectExternalPackageLink is the cross-package end-to-end check: a +// GOOBJ whose code references a real external package symbol (runtime's +// morestack, a plain reference rather than the builtin noctxt form) must +// carry a package index that points past the blkPkgIdx table's dummy entry +// 0, and the object must link against the real runtime. Pre-fix, the +// relocations carried block index 0, which the loader never fills, so the +// reference resolved against whatever object was loaded first and the link +// failed. The binary is not run: morestack returns to the call site's +// stack check, which a hand-written caller has none of. +func TestGOObjectExternalPackageLink(t *testing.T) { + goBin, err := exec.LookPath("go") + if err != nil { + t.Skip("no Go toolchain available") + } + dir := t.TempDir() + + const asmSrc = ` +#include "textflag.h" + +TEXT ·fn(SB), NOSPLIT, $0-0 + CALL ·helper(SB) + RET + +TEXT ·helper(SB), NOSPLIT, $0-0 + RET +` + const mainSrc = `package main + +func fn() +func helper() + +func main() { + fn() + helper() +} +` + if err := os.WriteFile(filepath.Join(dir, "main_amd64.s"), []byte(asmSrc), 0o644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, "main.go"), []byte(mainSrc), 0o644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module extlink\n\ngo 1.27\n"), 0o644); err != nil { + t.Fatal(err) + } + + // Capture the build the toolchain performs and re-run only its link + // step with our object swapped into the package archive, mirroring + // TestGOObjectLinkAndRun. + build := exec.Command(goBin, "build", "-x", "-work", "-o", filepath.Join(dir, "prog"), ".") + build.Dir = dir + buildLog, err := build.CombinedOutput() + if err != nil { + t.Fatalf("baseline build: %v\n%s", err, buildLog) + } + var work, linkLine, asmObj, pkgArch string + for line := range strings.SplitSeq(string(buildLog), "\n") { + switch { + case strings.HasPrefix(line, "WORK="): + work = strings.TrimPrefix(line, "WORK=") + case strings.Contains(line, "/asm ") && strings.Contains(line, "main_amd64.s") && !strings.Contains(line, "-gensymabis"): + asmObj = fieldAfter(line, "-o") + case strings.Contains(line, "pack r") && strings.Contains(line, "_pkg_.a"): + pkgArch = strings.TrimSpace(strings.SplitN(line, "pack r", 2)[1]) + pkgArch = strings.Fields(strings.SplitN(pkgArch, "#", 2)[0])[0] + case strings.Contains(line, "/link ") && strings.Contains(line, "-importcfg"): + linkLine = line + } + } + if work == "" || asmObj == "" || pkgArch == "" || linkLine == "" { + t.Skipf("could not parse build log (work=%q asmObj=%q)", work, asmObj) + } + defer os.RemoveAll(work) + asmObj = strings.ReplaceAll(asmObj, "$WORK", work) + pkgArch = strings.ReplaceAll(pkgArch, "$WORK", work) + + // Assemble the source with gasm, then retarget fn's internal call at + // a real external package symbol: the reloc's qualified name drives + // the export-data resolution the way a source-level runtime·sym(SB) + // reference would. + f, errs := parser.Parse("main_amd64.s", asmSrc) + if len(errs) > 0 { + t.Fatalf("parse: %v", errs) + } + img, err := AssembleFile(f) + if err != nil { + t.Fatalf("AssembleFile: %v", err) + } + fn := &img.Funcs[0] + for i := range fn.Relocs { + fn.Relocs[i].Name = "runtime\u00b7morestack" + fn.Relocs[i].External = true + } + img.Externals = []string{"runtime\u00b7morestack"} + obj, err := img.GOObject("main", "main_amd64.s") + if err != nil { + t.Fatalf("GOObject: %v", err) + } + + // Structural check: the blkPkgIdx block reserves entry 0 for the + // dummy invalid package and places runtime at entry 1, and fn's call + // relocation carries PkgIdx 1. + v := openGoobj(t, obj) + pkgBlk := v.blk(blkPkgIdx) + if len(pkgBlk) != 2*8 { + t.Fatalf("blkPkgIdx = %d bytes, want two entries", len(pkgBlk)) + } + le := binary.LittleEndian + strEntry := func(i int) string { + e := pkgBlk[i*8 : (i+1)*8] + return v.str(le.Uint32(e[4:]), le.Uint32(e[0:])) + } + if s := strEntry(0); s != "" { + t.Errorf("blkPkgIdx[0] = %q, want the dummy empty package", s) + } + if s := strEntry(1); s != "runtime" { + t.Errorf("blkPkgIdx[1] = %q, want runtime", s) + } + relocs := v.blk(blkReloc) + // fn is the last non-package symbol (two functions, four pc tables + // each); its one reloc is the final record. + fnRec := relocs[len(relocs)-23:] + if pIdx := le.Uint32(fnRec[15:]); pIdx != 1 { + t.Errorf("external reloc PkgIdx = %d, want 1 (runtime)", pIdx) + } + + // Swap the object into the package archive and link with cmd/link; + // the link line consumes the archive, not the loose object file. + membersDir := filepath.Join(dir, "members") + if err := os.MkdirAll(membersDir, 0o755); err != nil { + t.Fatal(err) + } + extract := exec.Command(goBin, "tool", "pack", "x", pkgArch) + extract.Dir = membersDir + if out, err := extract.CombinedOutput(); err != nil { + t.Fatalf("pack x: %v\n%s", err, out) + } + member := filepath.Join(membersDir, filepath.Base(asmObj)) + if err := os.Chmod(member, 0o644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(member, obj, 0o644); err != nil { + t.Fatal(err) + } + listCmd := exec.Command(goBin, "tool", "pack", "t", pkgArch) + listOut, err := listCmd.CombinedOutput() + if err != nil { + t.Fatalf("pack t: %v\n%s", err, listOut) + } + newArch := filepath.Join(dir, "pkg.a") + args := []string{"tool", "pack", "c", newArch} + seen := map[string]bool{} + for m := range strings.FieldsSeq(string(listOut)) { + if seen[m] { + continue + } + seen[m] = true + if err := os.Chmod(filepath.Join(membersDir, m), 0o644); err != nil { + t.Fatal(err) + } + args = append(args, filepath.Join(membersDir, m)) + } + pack := exec.Command(goBin, args...) + pack.Dir = membersDir + if out, err := pack.CombinedOutput(); err != nil { + t.Fatalf("pack c: %v\n%s", err, out) + } + linkLine = strings.ReplaceAll(linkLine, "$WORK", work) + linkLine = strings.ReplaceAll(linkLine, pkgArch, newArch) + linkLine = strings.ReplaceAll(linkLine, filepath.Join(work, "b001", "exe", "a.out"), filepath.Join(dir, "prog2")) + linkCmd := exec.Command("sh", "-c", "cd "+dir+" && "+linkLine) + if out, err := linkCmd.CombinedOutput(); err != nil { + t.Fatalf("re-link with gasm object: %v\n%s", err, out) + } + + // The call must have resolved to the real runtime symbol. + dump, err := exec.Command(goBin, "tool", "objdump", "-s", "main.fn", filepath.Join(dir, "prog2")).CombinedOutput() + if err != nil { + t.Fatalf("objdump main.fn: %v\n%s", err, dump) + } + if !bytes.Contains(dump, []byte("runtime.morestack")) { + t.Errorf("main.fn does not call runtime.morestack:\n%s", dump) + } +} diff --git a/asm/goobjarm64.go b/asm/goobjarm64.go index 2a6e044..74e1139 100644 --- a/asm/goobjarm64.go +++ b/asm/goobjarm64.go @@ -18,19 +18,40 @@ import ( // reloc/aux/data index arrays, with the arm64 preamble, the MinLC of 4 // for the pc-value deltas, and the arm64 relocation types for the ADRP // pairs and BL calls. +// +// The toolchain records one relocation per ADRP pair: a single R_ADDRARM64 +// or R_ARM64_PCREL_LDST64 of Siz 8 at the ADRP word, from which the linker +// patches both instructions of the pair (cmd/internal/obj/arm64/asm7.go, +// the ADRP cases: one AddRel with Off at the pair's pc and Siz 8). gasm's +// assembler records the ADRP+ADD form as two word relocs, so the second +// word's twin is dropped here before emission. func (img *Image) GOObjectAARCH64(pkgPath, srcPath string) ([]byte, error) { pre, err := toolchainObjectPreambleAARCH64() if err != nil { return nil, err } - return img.emitGOObject(pkgPath, srcPath, pre, 4, func(r Reloc) (uint16, uint8) { + coalesced := *img + coalesced.Funcs = append([]FuncLayout(nil), img.Funcs...) + for i := range coalesced.Funcs { + rs := coalesced.Funcs[i].Relocs + var keep []Reloc + for j := 0; j < len(rs); j++ { + keep = append(keep, rs[j]) + if rs[j].Kind == RelArm64Addr && j+1 < len(rs) && + rs[j+1].Kind == RelArm64Addr && rs[j+1].Off == rs[j].Off+4 { + j++ // the ADD word's twin: the Siz-8 pair reloc covers it + } + } + coalesced.Funcs[i].Relocs = keep + } + return coalesced.emitGOObject(pkgPath, srcPath, pre, 4, func(r Reloc) (uint16, uint8) { switch r.Kind { case RelArm64Branch: return relocArm64Branch, 4 case RelArm64LDST64: - return relocArm64LDST64, 4 + return relocArm64LDST64, 8 default: - return relocArm64Addr, 4 + return relocArm64Addr, 8 } }) } diff --git a/asm/riscv_goobj_test.go b/asm/riscv_goobj_test.go index 3ba768d..fdef996 100644 --- a/asm/riscv_goobj_test.go +++ b/asm/riscv_goobj_test.go @@ -74,6 +74,89 @@ DATA callee<>+0(SB)/8, $42 t.Error("ELF object missing R_RISCV_JAL relocation") } } + +// TestELFRISCVPCRELLO12Anchor checks the psABI's LO12 pairing rule: the +// R_RISCV_PCREL_LO12_I/S relocation must reference a symbol whose value is +// the AUIPC site of its HI20 partner (psABI §8.4.9; cmd/link generates one +// local text symbol per AUIPC for exactly this). The emitter pairs each +// HI20 (against the target symbol) with a LO12 against the .text section +// symbol whose addend is the AUIPC's section-relative offset, so S + A is +// the AUIPC address. +func TestELFRISCVPCRELLO12Anchor(t *testing.T) { + f, errs := parser.Parse("k_riscv64.s", ` +#include "textflag.h" + +TEXT ·sb(SB), NOSPLIT, $0-0 + MOV $answer<>(SB), X10 + MOV answer<>(SB), X11 + MOV X12, answer<>(SB) + RET + +GLOBL answer<>(SB), RODATA, $8 +DATA answer<>+0(SB)/8, $42 +`) + if len(errs) > 0 { + t.Fatalf("parse: %v", errs) + } + img, err := AssembleFileRISCV(f) + if err != nil { + t.Fatalf("AssembleFileRISCV: %v", err) + } + obj, err := img.ELFRISCVObject() + if err != nil { + t.Fatalf("ELFRISCVObject: %v", err) + } + ef, err := elf.NewFile(bytes.NewReader(obj)) + if err != nil { + t.Fatalf("parse ELF: %v", err) + } + defer ef.Close() + if flags := binary.LittleEndian.Uint32(obj[48:]); flags != efRISCVFloatAbiDouble { + t.Errorf("e_flags = %#x, want %#x (EF_RISCV_FLOAT_ABI_DOUBLE)", flags, efRISCVFloatAbiDouble) + } + rela := ef.Section(".rela.text") + if rela == nil { + t.Fatal("missing .rela.text") + } + b, err := rela.Data() + if err != nil { + t.Fatal(err) + } + if len(b) != 6*24 { + t.Fatalf(".rela.text holds %d entries, want six (three HI20/LO12 pairs)", len(b)/24) + } + le := binary.LittleEndian + wantLo := []uint32{rRISCVPCRELLO12I, rRISCVPCRELLO12I, rRISCVPCRELLO12S} + for p := range 3 { + auipc := 8 * p + hi := b[p*2*24:] + lo := b[(p*2+1)*24:] + if off := le.Uint64(hi[0:]); off != uint64(auipc) { + t.Errorf("pair %d: HI20 r_offset = %d, want %d (the AUIPC)", p, off, auipc) + } + if typ := uint32(le.Uint64(hi[8:])); typ != rRISCVPCRELHI20 { + t.Errorf("pair %d: HI20 type = %d, want %d", p, typ, rRISCVPCRELHI20) + } + if sym := int(le.Uint64(hi[8:]) >> 32); sym == 0 || sym == 1 { + t.Errorf("pair %d: HI20 against symbol %d, want the target", p, sym) + } + if off := le.Uint64(lo[0:]); off != uint64(auipc+4) { + t.Errorf("pair %d: LO12 r_offset = %d, want %d", p, off, auipc+4) + } + if typ := uint32(le.Uint64(lo[8:])); typ != wantLo[p] { + t.Errorf("pair %d: LO12 type = %d, want %d", p, typ, wantLo[p]) + } + // The LO12 must denote the AUIPC site: the .text section symbol + // (index 1) plus the AUIPC's section-relative offset as addend. + if sym := int(le.Uint64(lo[8:]) >> 32); sym != 1 { + t.Errorf("pair %d: LO12 against symbol %d, want 1 (the .text section symbol)", p, sym) + } + if add := int64(le.Uint64(lo[16:])); add != int64(auipc) { + t.Errorf("pair %d: LO12 addend = %d, want %d (S + A = the AUIPC address)", p, add, auipc) + } + } +} + func TestGOObjectRISCVStructure(t *testing.T) { f, errs := parser.Parse("k_riscv64.s", ` #include "textflag.h"