fix(goobj): external package and symbol indices and arm64 pair relocations
Assisted-by: GLM 5.3
This commit is contained in:
@@ -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 {
|
||||
|
||||
+5
-4
@@ -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))
|
||||
|
||||
+72
-32
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
+188
-1
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
+24
-3
@@ -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
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user