feat(asm): symbol-valued DATA, division slash in symbols and plain semicolons
Assisted-by: GLM 5.3 Flash
This commit is contained in:
@@ -245,3 +245,104 @@ func main() {
|
||||
t.Error("binary does not contain expected symbol")
|
||||
}
|
||||
}
|
||||
|
||||
// TestGOObjectAARCH64DataSymbolLink does for symbol-valued DATA fields what
|
||||
// the rt0 files do ("DATA _rt0…lib+0(SB)/8, $_rt0…lib(SB)"): the gasm object
|
||||
// carries an R_ADDR against the file's own TEXT symbol, the toolchain links
|
||||
// it, and the binary is checked for the symbol (no arm64 host to run it).
|
||||
func TestGOObjectAARCH64DataSymbolLink(t *testing.T) {
|
||||
goBin, err := exec.LookPath("go")
|
||||
if err != nil {
|
||||
t.Skip("no Go toolchain available")
|
||||
}
|
||||
dir := t.TempDir()
|
||||
asmSrc := `#include "textflag.h"
|
||||
GLOBL entry(SB), NOPTR, $8
|
||||
DATA entry+0(SB)/8, $·keepme(SB)
|
||||
|
||||
TEXT ·keepme(SB), NOSPLIT, $0-0
|
||||
RET
|
||||
|
||||
TEXT ·entryptr(SB), NOSPLIT, $0-8
|
||||
MOVD entry+0(SB), R4
|
||||
MOVD R4, ret+0(FP)
|
||||
RET
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(dir, "main_arm64.s"), []byte(asmSrc), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
mainSrc := `package main
|
||||
|
||||
func keepme()
|
||||
func entryptr() uintptr
|
||||
|
||||
func main() {
|
||||
if entryptr() == 0 {
|
||||
panic("the entry word is empty")
|
||||
}
|
||||
}
|
||||
`
|
||||
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 a64dlink\n\ngo 1.21\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
build := exec.Command(goBin, "build", "-x", "-work", "-o", filepath.Join(dir, "prog"), ".")
|
||||
build.Dir = dir
|
||||
build.Env = append(os.Environ(), "GOARCH=arm64")
|
||||
buildLog, err := build.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("baseline build: %v\n%s", err, buildLog)
|
||||
}
|
||||
var work, linkLine, asmObj 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_arm64.s") && !strings.Contains(line, "-gensymabis"):
|
||||
asmObj = fieldAfter(line, "-o")
|
||||
case strings.Contains(line, "/link ") && strings.Contains(line, "-importcfg"):
|
||||
linkLine = line
|
||||
}
|
||||
}
|
||||
if work == "" || asmObj == "" || linkLine == "" {
|
||||
t.Skipf("could not parse build log (work=%q asmObj=%q link=%q)", work, asmObj, linkLine)
|
||||
}
|
||||
defer os.RemoveAll(work)
|
||||
asmObj = strings.ReplaceAll(asmObj, "$WORK", work)
|
||||
linkLine = strings.ReplaceAll(linkLine, "$WORK", work)
|
||||
|
||||
src, err := os.ReadFile(filepath.Join(dir, "main_arm64.s"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f, errs := parser.Parse("main_arm64.s", string(src))
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFileARM64(f)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFileARM64: %v", err)
|
||||
}
|
||||
gasmObj, err := img.GOObjectAARCH64("a64dlink", "main_arm64.s")
|
||||
if err != nil {
|
||||
t.Fatalf("GOObjectAARCH64: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(asmObj, gasmObj, 0o644); err != nil {
|
||||
t.Fatalf("write gasm object: %v", err)
|
||||
}
|
||||
linkCmd := exec.Command("bash", "-c", "cd "+dir+" && "+linkLine)
|
||||
linkCmd.Env = append(os.Environ(), "GOARCH=arm64")
|
||||
if out, err := linkCmd.CombinedOutput(); err != nil {
|
||||
t.Fatalf("re-link with gasm object: %v\n%s", err, out)
|
||||
}
|
||||
binData, err := os.ReadFile(filepath.Join(dir, "prog"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(string(binData), "keepme") {
|
||||
t.Error("binary does not contain the keepme symbol")
|
||||
}
|
||||
}
|
||||
|
||||
+65
-10
@@ -43,6 +43,9 @@ const (
|
||||
stInfoShift = 4
|
||||
|
||||
rX8664PC32 = 2
|
||||
// R_X86_64_32 (debug/elf): the absolute 32-bit address of a symbol, the
|
||||
// R_ADDR shape a 4-byte DATA field carries.
|
||||
rX8664Abs32 = 10
|
||||
// R_X86_64_TPOFF32 (debug/elf): the local-exec TLS offset the stack
|
||||
// guard loads from FS. 20 is R_X86_64_TLSLD, a different relocation.
|
||||
rX8664TPOFF32 = 23
|
||||
@@ -158,6 +161,50 @@ func (img *Image) ELFObject() ([]byte, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// The data symbols' symbol-valued DATA fields ("DATA s+0(SB)/8,
|
||||
// $other(SB)") become .rela.data entries: an absolute relocation of the
|
||||
// DATA line's width at the field's data-section offset, S + A with no
|
||||
// PC term. Widths 4 and 8 have ELF relocation shapes; narrower fields
|
||||
// cannot hold an address, so they are refused rather than truncated.
|
||||
var dataRelas []elfRela
|
||||
for _, d := range img.DataSyms {
|
||||
for _, r := range d.Relocs {
|
||||
idx, ok := symIdx[r.Name]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("data relocation references unknown symbol %q", r.Name)
|
||||
}
|
||||
var typ uint32
|
||||
switch r.Siz {
|
||||
case 8:
|
||||
typ = rX8664Abs64
|
||||
case 4:
|
||||
typ = rX8664Abs32
|
||||
default:
|
||||
return nil, fmt.Errorf("DATA %q: a symbol value of width %d has no ELF relocation", d.Name, r.Siz)
|
||||
}
|
||||
dataRelas = append(dataRelas, elfRela{
|
||||
off: uint64(d.Offset + r.Off),
|
||||
sym: idx,
|
||||
typ: typ,
|
||||
addend: r.Addend,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Section presence: .rela.text only when there are code relocations,
|
||||
// .rela.data only when a DATA line holds a symbol value.
|
||||
hasRela := len(relas) > 0
|
||||
hasDataRela := len(dataRelas) > 0
|
||||
nSections := 6 // NULL, .text, .data, .symtab, .strtab, .shstrtab
|
||||
if hasRela {
|
||||
nSections++
|
||||
}
|
||||
if hasDataRela {
|
||||
nSections++
|
||||
}
|
||||
secSymtab, secStrtab := 3, 4
|
||||
secShstr := nSections - 1
|
||||
|
||||
// Serialise the string tables.
|
||||
stNames := newElfStrtab()
|
||||
for _, s := range syms {
|
||||
@@ -167,19 +214,13 @@ func (img *Image) ELFObject() ([]byte, error) {
|
||||
for _, n := range []string{".text", ".data", ".symtab", ".strtab", ".rela.text", ".shstrtab"} {
|
||||
stSections.add(n)
|
||||
}
|
||||
if hasDataRela {
|
||||
stSections.add(".rela.data")
|
||||
}
|
||||
for _, n := range dwarfSectionNames {
|
||||
stSections.add(n)
|
||||
}
|
||||
|
||||
// Section presence: .rela.text only when there are relocations.
|
||||
hasRela := len(relas) > 0
|
||||
nSections := 6 // NULL, .text, .data, .symtab, .strtab, .shstrtab
|
||||
if hasRela {
|
||||
nSections = 7
|
||||
}
|
||||
secSymtab, secStrtab := 3, 4
|
||||
secShstr := nSections - 1
|
||||
|
||||
// Lay the file out: header, section data, section headers.
|
||||
var out []byte
|
||||
out = append(out, make([]byte, 64)...) // ELF header, filled last
|
||||
@@ -214,7 +255,7 @@ func (img *Image) ELFObject() ([]byte, error) {
|
||||
strtabOff := len(out)
|
||||
out = append(out, stNames.bytes()...)
|
||||
|
||||
var relaOff int
|
||||
var relaOff, relaDataOff int
|
||||
if hasRela {
|
||||
align(8)
|
||||
relaOff = len(out)
|
||||
@@ -226,6 +267,17 @@ func (img *Image) ELFObject() ([]byte, error) {
|
||||
out = append(out, b[:]...)
|
||||
}
|
||||
}
|
||||
if hasDataRela {
|
||||
align(8)
|
||||
relaDataOff = len(out)
|
||||
for _, r := range dataRelas {
|
||||
var b [24]byte
|
||||
le.PutUint64(b[0:], r.off)
|
||||
le.PutUint64(b[8:], uint64(r.sym)<<32|uint64(r.typ))
|
||||
le.PutUint64(b[16:], uint64(r.addend))
|
||||
out = append(out, b[:]...)
|
||||
}
|
||||
}
|
||||
|
||||
shstrOff := len(out)
|
||||
out = append(out, stSections.bytes()...)
|
||||
@@ -284,6 +336,9 @@ func (img *Image) ELFObject() ([]byte, error) {
|
||||
if hasRela {
|
||||
putSh(".rela.text", shtRela, 0, relaOff, 24*len(relas), secSymtab, secText, 8, 24)
|
||||
}
|
||||
if hasDataRela {
|
||||
putSh(".rela.data", shtRela, 0, relaDataOff, 24*len(dataRelas), secSymtab, secData, 8, 24)
|
||||
}
|
||||
putSh(".shstrtab", shtStrtab, 0, shstrOff, len(stSections.bytes()), 0, 0, 1, 0)
|
||||
|
||||
// DWARF section headers; their indices follow the write order.
|
||||
|
||||
+108
@@ -750,3 +750,111 @@ func readFormSkip(t *testing.T, r *ulebIter, form uint64) {
|
||||
t.Fatalf("unsupported form %#x", form)
|
||||
}
|
||||
}
|
||||
|
||||
// TestELFObjectDataRelocation checks that a symbol-valued DATA field ("DATA
|
||||
// s+0(SB)/8, $other(SB)") reaches the ELF object as a .rela.data entry: an
|
||||
// absolute 64-bit relocation at the field's offset within .data, against
|
||||
// the named symbol, external targets included.
|
||||
func TestELFObjectDataRelocation(t *testing.T) {
|
||||
f, errs := parser.Parse("t_amd64.s", `#include "textflag.h"
|
||||
TEXT ·Keep(SB), NOSPLIT, $0-8
|
||||
RET
|
||||
GLOBL holder(SB), NOPTR, $24
|
||||
DATA holder+0(SB)/8, $·Keep+5(SB)
|
||||
DATA holder+8(SB)/8, $holder(SB)
|
||||
DATA holder+16(SB)/8, $extvar(SB)
|
||||
`)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFile(f)
|
||||
if err != nil {
|
||||
t.Fatalf("assemble: %v", err)
|
||||
}
|
||||
obj, err := img.ELFObject()
|
||||
if err != nil {
|
||||
t.Fatalf("ELFObject: %v", err)
|
||||
}
|
||||
ef, err := elf.NewFile(bytes.NewReader(obj))
|
||||
if err != nil {
|
||||
t.Fatalf("parse emitted object: %v", err)
|
||||
}
|
||||
defer ef.Close()
|
||||
relaData := ef.Section(".rela.data")
|
||||
if relaData == nil {
|
||||
t.Fatal("missing .rela.data section")
|
||||
}
|
||||
if relaData.Link == 0 || ef.Sections[relaData.Link].Name != ".symtab" {
|
||||
t.Errorf(".rela.data sh_link = %d, want the .symtab index", relaData.Link)
|
||||
}
|
||||
if ef.Sections[relaData.Info].Name != ".data" {
|
||||
t.Errorf(".rela.data sh_info = %d, want the .data index", relaData.Info)
|
||||
}
|
||||
relas, err := relaData.Data()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var got []struct {
|
||||
off uint64
|
||||
sym uint32
|
||||
typ uint32
|
||||
addend int64
|
||||
}
|
||||
for i := 0; i+24 <= len(relas); i += 24 {
|
||||
got = append(got, struct {
|
||||
off uint64
|
||||
sym uint32
|
||||
typ uint32
|
||||
addend int64
|
||||
}{
|
||||
off: binary.LittleEndian.Uint64(relas[i:]),
|
||||
// r_info packs the type in the low dword and the symbol index
|
||||
// in the high dword.
|
||||
typ: binary.LittleEndian.Uint32(relas[i+8:]),
|
||||
sym: binary.LittleEndian.Uint32(relas[i+12:]),
|
||||
addend: int64(binary.LittleEndian.Uint64(relas[i+16:])),
|
||||
})
|
||||
}
|
||||
// debug/elf hides the table's null entry, so raw index s names syms[s-1].
|
||||
syms, err := ef.Symbols()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
name := func(idx uint32) string {
|
||||
if idx >= 1 && int(idx) <= len(syms) {
|
||||
return syms[idx-1].Name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
// The offsets are data-section-relative: the field's DATA offset plus
|
||||
// the symbol's position in .data (the layout aligns each symbol to 16).
|
||||
base := uint64(0)
|
||||
for _, d := range img.DataSyms {
|
||||
if d.Name == "holder" {
|
||||
base = uint64(d.Offset)
|
||||
}
|
||||
}
|
||||
want := []struct {
|
||||
off uint64
|
||||
typ uint32
|
||||
addend int64
|
||||
target string
|
||||
}{
|
||||
{off: base + 0, typ: uint32(elf.R_X86_64_64), addend: 5, target: "Keep"},
|
||||
{off: base + 8, typ: uint32(elf.R_X86_64_64), addend: 0, target: "holder"},
|
||||
{off: base + 16, typ: uint32(elf.R_X86_64_64), addend: 0, target: "extvar"},
|
||||
}
|
||||
if len(got) != len(want) {
|
||||
t.Fatalf(".rela.data entries = %d, want %d", len(got), len(want))
|
||||
}
|
||||
for i, w := range want {
|
||||
g := got[i]
|
||||
if g.off != w.off || g.typ != w.typ || g.addend != w.addend {
|
||||
t.Errorf("entry %d = {off %d typ %d addend %d}, want {off %d typ %d addend %d}",
|
||||
i, g.off, g.typ, g.addend, w.off, w.typ, w.addend)
|
||||
}
|
||||
if n := name(g.sym); n != w.target {
|
||||
t.Errorf("entry %d names %q, want %q", i, n, w.target)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -463,6 +463,61 @@ func (img *Image) emitGOObject(pkgPath, srcPath string, pre []byte, minLC int, r
|
||||
symRelocs[si] = append(symRelocs[si], rec[:]...)
|
||||
}
|
||||
}
|
||||
// The data symbols' own relocations: the symbol-valued DATA fields
|
||||
// ("DATA s+0(SB)/8, $other(SB)"). The toolchain patches each field
|
||||
// with the target's absolute address through an R_ADDR of the DATA
|
||||
// line's width, on every architecture (the code relocations are
|
||||
// per-architecture PC-relative shapes; a data pointer word is not), so
|
||||
// this mapping bypasses relocField. The definitions were appended in
|
||||
// DataSyms order, so data symbol i is definition index i.
|
||||
for i, d := range img.DataSyms {
|
||||
for _, r := range d.Relocs {
|
||||
if r.Kind != RelAddr {
|
||||
return nil, fmt.Errorf("GOOBJ emission: data symbol %q carries a non-data relocation", d.Name)
|
||||
}
|
||||
var rec [23]byte
|
||||
binary.LittleEndian.PutUint32(rec[0:], uint32(int32(r.Off)))
|
||||
rec[4] = r.Siz
|
||||
binary.LittleEndian.PutUint16(rec[5:], relocAddr)
|
||||
binary.LittleEndian.PutUint64(rec[7:], uint64(r.Addend))
|
||||
switch {
|
||||
case r.External && r.Name == goobjBuiltinMorestack:
|
||||
binary.LittleEndian.PutUint32(rec[15:], pkgIdxBuiltin)
|
||||
binary.LittleEndian.PutUint32(rec[19:], goobjBuiltinMorestackNoctxt)
|
||||
case r.External:
|
||||
pkg, name := splitQualified(r.Name)
|
||||
if pkg == "" {
|
||||
return nil, fmt.Errorf("GOOBJ emission: external symbol %q has no package prefix", r.Name)
|
||||
}
|
||||
pIdx, ok := extPkgIdx[pkg]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("GOOBJ emission: package %q not resolved", pkg)
|
||||
}
|
||||
sIdx, ok := extSymIdx[pkg+"·"+name]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("GOOBJ emission: symbol %s·%s not resolved", pkg, name)
|
||||
}
|
||||
binary.LittleEndian.PutUint32(rec[15:], uint32(pIdx))
|
||||
binary.LittleEndian.PutUint32(rec[19:], uint32(sIdx))
|
||||
default:
|
||||
if di, ok := defIdx[r.Name]; ok {
|
||||
binary.LittleEndian.PutUint32(rec[15:], pkgIdxSelf)
|
||||
binary.LittleEndian.PutUint32(rec[19:], uint32(di))
|
||||
break
|
||||
}
|
||||
// A DATA field may hold the address of a TEXT function of
|
||||
// the same file (the rt0 lib entry spelling), which is a
|
||||
// non-package definition.
|
||||
ni, isText := textNpIdx[r.Name]
|
||||
if !isText {
|
||||
return nil, fmt.Errorf("GOOBJ emission: reference to unknown symbol %q", r.Name)
|
||||
}
|
||||
binary.LittleEndian.PutUint32(rec[15:], pkgIdxNone)
|
||||
binary.LittleEndian.PutUint32(rec[19:], uint32(ni))
|
||||
}
|
||||
symRelocs[i] = append(symRelocs[i], rec[:]...)
|
||||
}
|
||||
}
|
||||
// The DWARF symbols' own relocations (the function address references).
|
||||
for _, ds := range dwarfRelocs {
|
||||
for _, r := range ds.relocs {
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||
)
|
||||
|
||||
// The differential kernels for the DATA-path and front-end gaps are kept in
|
||||
// testdata/verify beside the campaign's other kernels; the verify package's
|
||||
// suites are not open to the asm package, so this test is their runner: each
|
||||
// kernel assembles through gasm and through go tool asm, and the functions'
|
||||
// bytes must agree with the relocation sites masked on both sides.
|
||||
|
||||
// toolAsmObject assembles path with the installed toolchain's assembler for
|
||||
// goarch ("" = the host) and returns the object bytes.
|
||||
func toolAsmObject(t *testing.T, path, goarch string) []byte {
|
||||
t.Helper()
|
||||
goBin, err := exec.LookPath("go")
|
||||
if err != nil {
|
||||
t.Skip("no Go toolchain available")
|
||||
}
|
||||
out, err := exec.Command(goBin, "env", "GOROOT").Output()
|
||||
if err != nil {
|
||||
t.Fatalf("go env GOROOT: %v", err)
|
||||
}
|
||||
includeDir := filepath.Join(strings.TrimSpace(string(out)), "pkg", "include")
|
||||
|
||||
pkg := strings.TrimSuffix(filepath.Base(path), ".s")
|
||||
pkg = strings.TrimSuffix(pkg, "_amd64")
|
||||
pkg = strings.TrimSuffix(pkg, "_arm64")
|
||||
|
||||
objPath := filepath.Join(t.TempDir(), "oracle.o")
|
||||
cmd := exec.Command(goBin, "tool", "asm", "-I", includeDir, "-p", pkg, "-o", objPath, path)
|
||||
if goarch != "" {
|
||||
environ := os.Environ()
|
||||
env := make([]string, 0, len(environ)+1)
|
||||
for _, e := range environ {
|
||||
if !strings.HasPrefix(e, "GOARCH=") {
|
||||
env = append(env, e)
|
||||
}
|
||||
}
|
||||
cmd.Env = append(env, "GOARCH="+goarch)
|
||||
}
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
t.Fatalf("go tool asm %s: %v\n%s", filepath.Base(path), err, out)
|
||||
}
|
||||
obj, err := os.ReadFile(objPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return obj
|
||||
}
|
||||
|
||||
// oracleFuncCode extracts the non-package TEXT functions' code bytes from a
|
||||
// toolchain object, keyed by the name the object records (pkg.name).
|
||||
func oracleFuncCode(t *testing.T, obj []byte) map[string][]byte {
|
||||
t.Helper()
|
||||
v := openGoobj(t, obj)
|
||||
le := binary.LittleEndian
|
||||
const symSize = 21
|
||||
nps := v.syms(blkNonpkgdef)
|
||||
data := v.blk(blkData)
|
||||
didx := v.blk(blkDataIdx)
|
||||
preceding := 0
|
||||
for _, bi := range []int{blkSymdef, blkHashed64def, blkHasheddef} {
|
||||
preceding += len(v.blk(bi)) / symSize
|
||||
}
|
||||
total := preceding + len(nps)
|
||||
out := make(map[string][]byte, len(nps))
|
||||
for i, s := range nps {
|
||||
if s.typ != kindSTEXT {
|
||||
continue
|
||||
}
|
||||
start := le.Uint32(didx[4*(preceding+i):])
|
||||
end := uint32(len(data))
|
||||
if preceding+i+1 < total {
|
||||
end = le.Uint32(didx[4*(preceding+i+1):])
|
||||
}
|
||||
out[s.name] = data[start:end]
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// maskCode zeroes every relocation field, the way the toolchain's object
|
||||
// leaves them for the linker.
|
||||
func maskCode(code []byte, relocs []Reloc) []byte {
|
||||
for _, r := range relocs {
|
||||
for j := r.Off; j < r.Off+4 && j < len(code); j++ {
|
||||
code[j] = 0
|
||||
}
|
||||
}
|
||||
return code
|
||||
}
|
||||
|
||||
// code assembles src for amd64 and returns the image's code bytes.
|
||||
func code(path, src string) []byte {
|
||||
f, errs := parser.Parse(path, src)
|
||||
if len(errs) > 0 {
|
||||
return nil
|
||||
}
|
||||
img, err := AssembleFile(f)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return img.Code
|
||||
}
|
||||
|
||||
// TestDifferentialKernels pins the new kernels against the oracle.
|
||||
func TestDifferentialKernels(t *testing.T) {
|
||||
if runtime.GOARCH != "amd64" {
|
||||
t.Skip("the amd64 kernels assume an amd64 host assembler default")
|
||||
}
|
||||
for _, k := range []struct {
|
||||
path string
|
||||
goarch string
|
||||
arm64 bool
|
||||
}{
|
||||
{filepath.Join("..", "testdata", "verify", "datarel_amd64.s"), "", false},
|
||||
{filepath.Join("..", "testdata", "verify", "divslash_amd64.s"), "", false},
|
||||
{filepath.Join("..", "testdata", "verify", "semicolons_amd64.s"), "", false},
|
||||
{filepath.Join("..", "testdata", "verify", "datarel_arm64.s"), "arm64", true},
|
||||
{filepath.Join("..", "testdata", "verify", "divslash_arm64.s"), "arm64", true},
|
||||
} {
|
||||
t.Run(filepath.Base(k.path), func(t *testing.T) {
|
||||
src, err := os.ReadFile(k.path)
|
||||
if err != nil {
|
||||
t.Fatalf("read: %v", err)
|
||||
}
|
||||
f, errs := parser.Parse(k.path, string(src))
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
var img *Image
|
||||
if k.arm64 {
|
||||
img, err = AssembleFileARM64(f)
|
||||
} else {
|
||||
img, err = AssembleFile(f)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("assemble: %v", err)
|
||||
}
|
||||
gt := oracleFuncCode(t, toolAsmObject(t, k.path, k.goarch))
|
||||
// The oracle keys its functions by the qualified object name
|
||||
// (pkg.name); match on the local part.
|
||||
byLocal := make(map[string][]byte, len(gt))
|
||||
for name, code := range gt {
|
||||
if _, after, ok := strings.Cut(name, "."); ok {
|
||||
name = after
|
||||
}
|
||||
byLocal[name] = code
|
||||
}
|
||||
|
||||
matched := 0
|
||||
for _, fn := range img.Funcs {
|
||||
gasmCode := maskCode(append([]byte(nil), img.Code[fn.Offset:fn.Offset+fn.Size]...), fn.Relocs)
|
||||
goCode, ok := byLocal[fn.Name]
|
||||
if !ok {
|
||||
t.Errorf("%s: not in ground truth (%d functions: %v)", fn.Name, len(gt), keysOf(byLocal))
|
||||
continue
|
||||
}
|
||||
goCode = maskCode(append([]byte(nil), goCode...), fn.Relocs)
|
||||
cmpLen := min(len(goCode), len(gasmCode))
|
||||
if !bytes.Equal(gasmCode[:cmpLen], goCode[:cmpLen]) {
|
||||
t.Errorf("%s: MISMATCH gasm=%d go=%d bytes\ngasm %x\ngo %x", fn.Name, len(gasmCode), len(goCode), gasmCode, goCode)
|
||||
continue
|
||||
}
|
||||
for _, b := range goCode[len(gasmCode):] {
|
||||
if b != 0 {
|
||||
t.Errorf("%s: non-zero trailing bytes in go tool asm output", fn.Name)
|
||||
break
|
||||
}
|
||||
}
|
||||
matched++
|
||||
t.Logf("%s: MATCH (%d bytes)", fn.Name, len(gasmCode))
|
||||
}
|
||||
if matched == 0 {
|
||||
t.Fatal("no functions matched")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func keysOf(m map[string][]byte) []string {
|
||||
out := make([]string, 0, len(m))
|
||||
for k := range m {
|
||||
out = append(out, k)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// TestSemicolonSpellingParity pins that the ';' statement separator changes
|
||||
// nothing about the encoding: the one-line spelling assembles to exactly the
|
||||
// bytes of the same statements written one per line.
|
||||
func TestSemicolonSpellingParity(t *testing.T) {
|
||||
for _, tt := range []struct{ one, two string }{
|
||||
{"\tROLQ $3, DI; ROLQ $13, DI\n", "\tROLQ $3, DI\n\tROLQ $13, DI\n"},
|
||||
{"\tREP; MOVSQ\n", "\tREP\n\tMOVSQ\n"},
|
||||
{"\tXORQ AX, AX; XORQ CX, CX\n", "\tXORQ AX, AX\n\tXORQ CX, CX\n"},
|
||||
} {
|
||||
one := code("t.s", "TEXT \u00b7f(SB), NOSPLIT, $0\n"+tt.one+"\tRET\n")
|
||||
two := code("t.s", "TEXT \u00b7f(SB), NOSPLIT, $0\n"+tt.two+"\tRET\n")
|
||||
if !bytes.Equal(one, two) {
|
||||
t.Errorf("semicolon spelling %q: %x, want the two-line bytes %x", tt.one, one, two)
|
||||
}
|
||||
}
|
||||
}
|
||||
+68
-3
@@ -103,6 +103,7 @@ const (
|
||||
RelArm64Branch // R_CALLARM64 (BL instruction)
|
||||
RelArm64LDST64 // R_ARM64_PCREL_LDST64 (ADRP + 64-bit LDR/STR pair)
|
||||
RelLoong64Branch // R_CALLLOONG64 (BL instruction)
|
||||
RelAddr // R_ADDR: the absolute address of a symbol held in a DATA field
|
||||
)
|
||||
|
||||
type Reloc struct {
|
||||
@@ -112,13 +113,17 @@ type Reloc struct {
|
||||
// Addend select the target: the symbol plus the byte offset. An
|
||||
// External relocation names a symbol no GLOBL in the file defines;
|
||||
// the object-file emitters carry it into the output's relocation
|
||||
// table.
|
||||
// table. Siz is the width of the patched field and is set only for
|
||||
// data-field relocations (RelAddr, Off relative to the data symbol),
|
||||
// whose width is the DATA line's; code relocations take their width
|
||||
// from the architecture's instruction encoding.
|
||||
Off int
|
||||
After int
|
||||
Name string
|
||||
Addend int64
|
||||
External bool
|
||||
Kind RelocKind
|
||||
Siz uint8
|
||||
}
|
||||
|
||||
// DataSymbol describes one GLOBL symbol laid out in the data section.
|
||||
@@ -130,6 +135,11 @@ type DataSymbol struct {
|
||||
Static bool // the <> marker: file-local, not exported
|
||||
Rodata bool // the RODATA flag: read-only data
|
||||
Dupok bool // the DUPOK flag: duplicate-OK
|
||||
// Relocs carries the symbol-valued DATA initialisers ("DATA s+0(SB)/8,
|
||||
// $other(SB)"): fields of this symbol's data that hold another symbol's
|
||||
// address, resolved by the linker. Off is relative to the symbol's
|
||||
// data start.
|
||||
Relocs []Reloc
|
||||
}
|
||||
|
||||
// Bytes returns the whole image: code, then data.
|
||||
@@ -257,6 +267,22 @@ func AssembleFile(f *ast.File) (*Image, error) {
|
||||
img.Funcs[i].Relocs = append(img.Funcs[i].Relocs, reloc)
|
||||
}
|
||||
}
|
||||
// The data symbols' symbol-valued DATA fields resolve the same way the
|
||||
// code references do: a name the file defines (GLOBL or TEXT) stays an
|
||||
// internal reference the emitters resolve, anything else is external.
|
||||
// img.DataSyms was laid out in dataSyms order, so the indexes line up.
|
||||
for i := range img.DataSyms {
|
||||
for _, r := range dataSyms[i].relocs {
|
||||
reloc := r
|
||||
if _, ok := img.Symbols[reloc.Name]; !ok {
|
||||
if _, ok := textOff[reloc.Name]; !ok {
|
||||
reloc.External = true
|
||||
externals[reloc.Name] = true
|
||||
}
|
||||
}
|
||||
img.DataSyms[i].Relocs = append(img.DataSyms[i].Relocs, reloc)
|
||||
}
|
||||
}
|
||||
for name := range externals {
|
||||
img.Externals = append(img.Externals, name)
|
||||
}
|
||||
@@ -429,6 +455,23 @@ func markExternals(img *Image, dataSyms []dataSym) {
|
||||
}
|
||||
}
|
||||
}
|
||||
// The declared data symbols carry the file's own relocations (the
|
||||
// symbol-valued DATA fields); the layouts appended img.DataSyms in
|
||||
// dataSyms order, so the indexes line up. The trailing entries (the
|
||||
// pooled arm64 literals) have no source relocations.
|
||||
for i := range img.DataSyms {
|
||||
if i >= len(dataSyms) {
|
||||
break
|
||||
}
|
||||
for _, r := range dataSyms[i].relocs {
|
||||
reloc := r
|
||||
if !known[reloc.Name] {
|
||||
reloc.External = true
|
||||
externals[reloc.Name] = true
|
||||
}
|
||||
img.DataSyms[i].Relocs = append(img.DataSyms[i].Relocs, reloc)
|
||||
}
|
||||
}
|
||||
for name := range externals {
|
||||
img.Externals = append(img.Externals, name)
|
||||
}
|
||||
@@ -444,6 +487,9 @@ type dataSym struct {
|
||||
static bool
|
||||
rodata bool
|
||||
dupok bool
|
||||
// relocs are the symbol-valued DATA fields, in declaration order; Off
|
||||
// is relative to the symbol's data start.
|
||||
relocs []Reloc
|
||||
}
|
||||
|
||||
// collectData gathers the file's static symbols (GLOBL) and their initial
|
||||
@@ -511,8 +557,8 @@ func collectData(f *ast.File) ([]dataSym, error) {
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("DATA %q: no matching GLOBL", dd.Name.Name)
|
||||
}
|
||||
if dd.Value == nil || !dd.Value.Imm.HasVal {
|
||||
return nil, fmt.Errorf("DATA %q: value must be an integer immediate", dd.Name.Name)
|
||||
if dd.Value == nil {
|
||||
return nil, fmt.Errorf("DATA %q: missing value", dd.Name.Name)
|
||||
}
|
||||
w := dd.Width
|
||||
switch w {
|
||||
@@ -525,6 +571,25 @@ func collectData(f *ast.File) ([]dataSym, error) {
|
||||
if off < 0 || off+int64(w) > int64(len(buf)) {
|
||||
return nil, fmt.Errorf("DATA %q+%d/%d exceeds GLOBL size %d", dd.Name.Name, off, w, len(buf))
|
||||
}
|
||||
// A symbol value ("DATA s+0(SB)/8, $other(SB)", the rt0 spelling)
|
||||
// leaves the field zero and records a relocation against the named
|
||||
// symbol: the linker patches the absolute address at this data
|
||||
// offset. The toolchain emits the same shape, an R_ADDR of the
|
||||
// DATA width with the value's offset as the addend, on every
|
||||
// architecture.
|
||||
if sym := dd.Value.Imm.Sym; !dd.Value.Imm.HasVal && sym != nil {
|
||||
syms[i].relocs = append(syms[i].relocs, Reloc{
|
||||
Off: int(off),
|
||||
Name: sym.Name,
|
||||
Addend: sym.Offset,
|
||||
Kind: RelAddr,
|
||||
Siz: uint8(w),
|
||||
})
|
||||
continue
|
||||
}
|
||||
if !dd.Value.Imm.HasVal {
|
||||
return nil, fmt.Errorf("DATA %q: value must be an integer immediate or a symbol address", dd.Name.Name)
|
||||
}
|
||||
v := dd.Value.Imm.Val
|
||||
if dd.Value.Imm.Neg {
|
||||
v = -v
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
package asm
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -166,3 +170,251 @@ func TestCollectDataNumericFlags(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestCollectDataSymbolValue covers the symbol-valued DATA field ("DATA
|
||||
// s+0(SB)/8, $other(SB)", the rt0 spelling): the field stays zero in the
|
||||
// image and the relocation is recorded against the named symbol, whatever
|
||||
// the file defines (a TEXT function, a GLOBL) or leaves external.
|
||||
func TestCollectDataSymbolValue(t *testing.T) {
|
||||
src := `#include "textflag.h"
|
||||
TEXT ·Keep(SB), NOSPLIT, $0-8
|
||||
MOVQ target+0(FP), AX
|
||||
RET
|
||||
GLOBL holder(SB), NOPTR, $32
|
||||
DATA holder+0(SB)/8, $·Keep(SB)
|
||||
DATA holder+8(SB)/8, $·Keep+5(SB)
|
||||
DATA holder+16(SB)/8, $holder(SB)
|
||||
GLOBL spare(SB), NOPTR, $8
|
||||
DATA spare+0(SB)/8, $extvar(SB)
|
||||
`
|
||||
f, errs := parser.Parse("f_amd64.s", src)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFile(f)
|
||||
if err != nil {
|
||||
t.Fatalf("assemble: %v", err)
|
||||
}
|
||||
byName := map[string]DataSymbol{}
|
||||
for _, d := range img.DataSyms {
|
||||
byName[d.Name] = d
|
||||
}
|
||||
want := []struct {
|
||||
sym string
|
||||
off int
|
||||
name string
|
||||
addend int64
|
||||
ext bool
|
||||
}{
|
||||
{"holder", 0, "Keep", 0, false},
|
||||
{"holder", 8, "Keep", 5, false},
|
||||
{"holder", 16, "holder", 0, false},
|
||||
{"spare", 0, "extvar", 0, true},
|
||||
}
|
||||
var flat []struct {
|
||||
sym string
|
||||
r Reloc
|
||||
}
|
||||
for _, d := range img.DataSyms {
|
||||
for _, r := range d.Relocs {
|
||||
flat = append(flat, struct {
|
||||
sym string
|
||||
r Reloc
|
||||
}{d.Name, r})
|
||||
}
|
||||
}
|
||||
if len(flat) != len(want) {
|
||||
t.Fatalf("data relocations = %d, want %d", len(flat), len(want))
|
||||
}
|
||||
for i, w := range want {
|
||||
g := flat[i]
|
||||
r := g.r
|
||||
if g.sym != w.sym {
|
||||
t.Errorf("relocation %d sits on %q, want %q", i, g.sym, w.sym)
|
||||
continue
|
||||
}
|
||||
if r.Off != w.off || r.Name != w.name || r.Addend != w.addend || r.External != w.ext {
|
||||
t.Errorf("relocation %d = {+%d %q addend %d ext %v}, want {+%d %q addend %d ext %v}",
|
||||
i, r.Off, r.Name, r.Addend, r.External, w.off, w.name, w.addend, w.ext)
|
||||
}
|
||||
if r.Kind != RelAddr {
|
||||
t.Errorf("relocation %d kind = %v, want RelAddr", i, r.Kind)
|
||||
}
|
||||
if r.Siz != 8 {
|
||||
t.Errorf("relocation %d siz = %d, want 8", i, r.Siz)
|
||||
}
|
||||
}
|
||||
// The fields themselves stay zero: only the linker fills them.
|
||||
for _, b := range img.Data {
|
||||
if b != 0 {
|
||||
t.Fatal("data section is not all zero before relocation")
|
||||
}
|
||||
}
|
||||
if len(img.Externals) != 1 || img.Externals[0] != "extvar" {
|
||||
t.Errorf("Externals = %v, want [extvar]", img.Externals)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGOObjectDataSymbolReloc pins the GOOBJ record a symbol-valued DATA
|
||||
// field produces, against the shape the toolchain emits for the same
|
||||
// source: an R_ADDR of the DATA width at the field offset, pkgIdxNone plus
|
||||
// the non-package definition index when the target is the file's own TEXT
|
||||
// function (the rt0 lib entry spelling).
|
||||
func TestGOObjectDataSymbolReloc(t *testing.T) {
|
||||
f, errs := parser.Parse("f_amd64.s", `#include "textflag.h"
|
||||
TEXT ·Keep(SB), NOSPLIT, $0-8
|
||||
RET
|
||||
GLOBL holder(SB), NOPTR, $16
|
||||
DATA holder+0(SB)/8, $·Keep+5(SB)
|
||||
`)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFile(f)
|
||||
if err != nil {
|
||||
t.Fatalf("assemble: %v", err)
|
||||
}
|
||||
obj, err := img.GOObject("main", "f_amd64.s")
|
||||
if err != nil {
|
||||
t.Fatalf("GOObject: %v", err)
|
||||
}
|
||||
v := openGoobj(t, obj)
|
||||
// Walk every relocation record; the data record is the one of Siz 8
|
||||
// and type R_ADDR.
|
||||
var off, add int64
|
||||
var pkg, sym uint32
|
||||
found := false
|
||||
for data := v.blk(blkReloc); len(data) >= 23; data = data[23:] {
|
||||
if data[4] != 8 || binary.LittleEndian.Uint16(data[5:]) != relocAddr {
|
||||
continue
|
||||
}
|
||||
found = true
|
||||
off = int64(int32(binary.LittleEndian.Uint32(data[0:])))
|
||||
add = int64(binary.LittleEndian.Uint64(data[7:]))
|
||||
pkg = binary.LittleEndian.Uint32(data[15:])
|
||||
sym = binary.LittleEndian.Uint32(data[19:])
|
||||
break
|
||||
}
|
||||
if !found {
|
||||
t.Fatal("no data relocation record in the object")
|
||||
}
|
||||
if off != 0 || add != 5 {
|
||||
t.Errorf("data reloc = {off %d addend %d}, want {off 0 addend 5}", off, add)
|
||||
}
|
||||
if pkg != pkgIdxNone {
|
||||
t.Errorf("data reloc pkg = %#x, want pkgIdxNone (the TEXT function)", pkg)
|
||||
}
|
||||
// The function's non-package definition index: the four pc tables
|
||||
// precede it, so index 4.
|
||||
if sym != 4 {
|
||||
t.Errorf("data reloc sym = %d, want 4", sym)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGOObjectDataSymbolLink is the end-to-end proof for symbol-valued DATA
|
||||
// fields: the gasm object is substituted for the toolchain's and re-linked,
|
||||
// then executed, and the linked data word must hold the real address of the
|
||||
// function the DATA line named (runtime.FuncForPC identifies it).
|
||||
func TestGOObjectDataSymbolLink(t *testing.T) {
|
||||
goBin, err := exec.LookPath("go")
|
||||
if err != nil {
|
||||
t.Skip("no Go toolchain available")
|
||||
}
|
||||
dir := t.TempDir()
|
||||
asmSrc := `#include "textflag.h"
|
||||
GLOBL entry(SB), NOPTR, $8
|
||||
DATA entry+0(SB)/8, $·keepme(SB)
|
||||
|
||||
TEXT ·keepme(SB), NOSPLIT, $0-0
|
||||
RET
|
||||
|
||||
TEXT ·entryptr(SB), NOSPLIT, $0-8
|
||||
MOVQ entry+0(SB), AX
|
||||
MOVQ AX, ret+0(FP)
|
||||
RET
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(dir, "main_amd64.s"), []byte(asmSrc), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
mainSrc := `package main
|
||||
|
||||
import "runtime"
|
||||
|
||||
func keepme()
|
||||
func entryptr() uintptr
|
||||
|
||||
func main() {
|
||||
pc := entryptr()
|
||||
fn := runtime.FuncForPC(pc)
|
||||
if fn == nil {
|
||||
panic("the entry word does not point at a function")
|
||||
}
|
||||
if fn.Name() != "main.keepme" {
|
||||
panic("the entry word points at " + fn.Name())
|
||||
}
|
||||
}
|
||||
`
|
||||
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 dlink\n\ngo 1.21\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Capture the build: the package archive's asm object and the link line.
|
||||
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 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, "/link ") && strings.Contains(line, "-importcfg"):
|
||||
linkLine = line
|
||||
}
|
||||
}
|
||||
if work == "" || asmObj == "" || linkLine == "" {
|
||||
t.Skipf("could not parse build log (work=%q asmObj=%q link=%q)", work, asmObj, linkLine)
|
||||
}
|
||||
defer os.RemoveAll(work)
|
||||
asmObj = strings.ReplaceAll(asmObj, "$WORK", work)
|
||||
linkLine = strings.ReplaceAll(linkLine, "$WORK", work)
|
||||
|
||||
// Assemble the same source with gasm and substitute the object.
|
||||
src, err := os.ReadFile(filepath.Join(dir, "main_amd64.s"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f, errs := parser.Parse("main_amd64.s", string(src))
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFile(f)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFile: %v", err)
|
||||
}
|
||||
gasmObj, err := img.GOObject("dlink", "main_amd64.s")
|
||||
if err != nil {
|
||||
t.Fatalf("GOObject: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(asmObj, gasmObj, 0o644); err != nil {
|
||||
t.Fatalf("write gasm object: %v", err)
|
||||
}
|
||||
linkCmd := exec.Command("bash", "-c", "cd "+dir+" && "+linkLine)
|
||||
if out, err := linkCmd.CombinedOutput(); err != nil {
|
||||
t.Fatalf("re-link with gasm object: %v\n%s", err, out)
|
||||
}
|
||||
|
||||
// The linked program must run and find the right function behind the
|
||||
// data word.
|
||||
out, err := exec.Command(filepath.Join(dir, "prog")).CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("linked program failed: %v\n%s", err, out)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user