861 lines
23 KiB
Go
861 lines
23 KiB
Go
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||
// SPDX-License-Identifier: BSD-3-Clause
|
||
|
||
package asm
|
||
|
||
import (
|
||
"bytes"
|
||
"debug/elf"
|
||
"encoding/binary"
|
||
"os"
|
||
"os/exec"
|
||
"path/filepath"
|
||
"testing"
|
||
|
||
"sourcedock.dev/petrbalvin/gasm-sdk/ast"
|
||
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||
)
|
||
|
||
// The object-file tests share one source: two exported functions, one
|
||
// file-local constant reached through a relocation, and one external symbol
|
||
// the linker must resolve. The functions take their arguments in the System
|
||
// V registers (not the Go stack ABI) so a C driver can call them directly.
|
||
const elfTestSrc = `
|
||
#include "textflag.h"
|
||
|
||
TEXT ·addq(SB), NOSPLIT, $0
|
||
LEAQ (DI)(SI*1), AX
|
||
RET
|
||
|
||
TEXT ·getanswer(SB), NOSPLIT, $0
|
||
MOVQ answer<>(SB), AX
|
||
RET
|
||
|
||
TEXT ·useextern(SB), NOSPLIT, $0
|
||
MOVQ extvar(SB), AX
|
||
RET
|
||
|
||
GLOBL answer<>(SB), RODATA, $8
|
||
DATA answer<>+0(SB)/8, $42
|
||
`
|
||
|
||
func elfTestImage(t *testing.T) *Image {
|
||
t.Helper()
|
||
f, errs := parser.Parse("t_amd64.s", elfTestSrc)
|
||
if len(errs) > 0 {
|
||
t.Fatalf("parse: %v", errs)
|
||
}
|
||
img, err := AssembleFile(f)
|
||
if err != nil {
|
||
t.Fatalf("AssembleFile: %v", err)
|
||
}
|
||
return img
|
||
}
|
||
|
||
// TestAssembleFileExternals checks that a reference to a symbol no GLOBL
|
||
// defines is recorded as an external relocation instead of failing; the
|
||
// raw image leaves the displacement zero, the object emitters carry it.
|
||
func TestAssembleFileExternals(t *testing.T) {
|
||
img := elfTestImage(t)
|
||
if len(img.Externals) != 1 || img.Externals[0] != "extvar" {
|
||
t.Fatalf("Externals = %v, want [extvar]", img.Externals)
|
||
}
|
||
var ext, local int
|
||
for _, fn := range img.Funcs {
|
||
for _, r := range fn.Relocs {
|
||
if r.External {
|
||
ext++
|
||
if r.Name != "extvar" {
|
||
t.Errorf("external reloc names %q, want extvar", r.Name)
|
||
}
|
||
} else {
|
||
local++
|
||
if r.Name != "answer" {
|
||
t.Errorf("local reloc names %q, want answer", r.Name)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if ext != 1 || local != 1 {
|
||
t.Errorf("relocs = %d external, %d local; want 1 and 1", ext, local)
|
||
}
|
||
}
|
||
|
||
// TestELFObject checks the structure of the emitted ELF64 relocatable
|
||
// object: sections, the symbol table (bindings, types, values, sizes) and
|
||
// the .rela.text relocations, parsed back with debug/elf.
|
||
func TestELFObject(t *testing.T) {
|
||
img := elfTestImage(t)
|
||
obj, err := img.ELFObject()
|
||
if err != nil {
|
||
t.Fatalf("ELFObject: %v", err)
|
||
}
|
||
f, err := elf.NewFile(bytes.NewReader(obj))
|
||
if err != nil {
|
||
t.Fatalf("parse emitted object: %v", err)
|
||
}
|
||
defer f.Close()
|
||
|
||
if f.Type != elf.ET_REL || f.Machine != elf.EM_X86_64 {
|
||
t.Errorf("type/machine = %v/%v, want ET_REL/EM_X86_64", f.Type, f.Machine)
|
||
}
|
||
|
||
text := f.Section(".text")
|
||
data := f.Section(".data")
|
||
if text == nil || data == nil {
|
||
t.Fatal("missing .text or .data section")
|
||
}
|
||
if text.Flags&elf.SHF_EXECINSTR == 0 || text.Flags&elf.SHF_ALLOC == 0 {
|
||
t.Errorf(".text flags = %v", text.Flags)
|
||
}
|
||
if data.Flags&elf.SHF_WRITE == 0 {
|
||
t.Errorf(".data flags = %v", data.Flags)
|
||
}
|
||
textData, err := text.Data()
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if !bytes.Equal(textData, img.Code) {
|
||
t.Errorf(".text contents differ from the image code")
|
||
}
|
||
|
||
syms, err := f.Symbols()
|
||
if err != nil {
|
||
t.Fatalf("symbols: %v", err)
|
||
}
|
||
byName := map[string]elf.Symbol{}
|
||
for _, s := range syms {
|
||
byName[s.Name] = s
|
||
}
|
||
wantSym := func(name string, bind elf.SymBind, typ elf.SymType, section elf.SectionIndex, size uint64) {
|
||
t.Helper()
|
||
s, ok := byName[name]
|
||
if !ok {
|
||
t.Errorf("symbol %q not found", name)
|
||
return
|
||
}
|
||
if elf.ST_BIND(s.Info) != bind || elf.ST_TYPE(s.Info) != typ {
|
||
t.Errorf("%s: bind/type = %v/%v, want %v/%v", name, elf.ST_BIND(s.Info), elf.ST_TYPE(s.Info), bind, typ)
|
||
}
|
||
if s.Section != section {
|
||
t.Errorf("%s: section = %v, want %v", name, s.Section, section)
|
||
}
|
||
if s.Size != size {
|
||
t.Errorf("%s: size = %d, want %d", name, s.Size, size)
|
||
}
|
||
}
|
||
// The emitted layout is fixed: 0 NULL, 1 .text, 2 .data.
|
||
if f.Sections[1].Name != ".text" || f.Sections[2].Name != ".data" {
|
||
t.Fatalf("section layout = %s, %s; want .text, .data", f.Sections[1].Name, f.Sections[2].Name)
|
||
}
|
||
textIdx := elf.SectionIndex(1)
|
||
dataIdx := elf.SectionIndex(2)
|
||
wantSym("addq", elf.STB_GLOBAL, elf.STT_FUNC, textIdx, 5)
|
||
wantSym("getanswer", elf.STB_GLOBAL, elf.STT_FUNC, textIdx, 8)
|
||
wantSym("useextern", elf.STB_GLOBAL, elf.STT_FUNC, textIdx, 8)
|
||
wantSym("answer", elf.STB_LOCAL, elf.STT_OBJECT, dataIdx, 8)
|
||
wantSym("extvar", elf.STB_GLOBAL, elf.STT_NOTYPE, elf.SHN_UNDEF, 0)
|
||
|
||
// Relocations: one for the file-local constant (resolving against the
|
||
// local data symbol) and one for the external (against the undefined
|
||
// global), both R_X86_64_PC32 with the −4 addend the PC-relative form
|
||
// needs. debug/elf does not surface rela entries, so read the section
|
||
// directly.
|
||
relaSec := f.Section(".rela.text")
|
||
if relaSec == nil {
|
||
t.Fatal("missing .rela.text")
|
||
}
|
||
raw, err := relaSec.Data()
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if len(raw)%24 != 0 || len(raw)/24 != 2 {
|
||
t.Fatalf(".rela.text has %d bytes, want two 24-byte entries", len(raw))
|
||
}
|
||
// Symbol names straight from the raw tables: r_info carries an index
|
||
// into .symtab including the null entry, which debug/elf's Symbols()
|
||
// slice may not mirror.
|
||
symtabRaw, err := f.Section(".symtab").Data()
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
strtabRaw, err := f.Section(".strtab").Data()
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
symName := func(idx int) string {
|
||
stName := binary.LittleEndian.Uint32(symtabRaw[idx*24:])
|
||
end := bytes.IndexByte(strtabRaw[stName:], 0)
|
||
return string(strtabRaw[stName : int(stName)+end])
|
||
}
|
||
for i := range 2 {
|
||
e := raw[i*24 : (i+1)*24]
|
||
off := binary.LittleEndian.Uint64(e[0:])
|
||
info := binary.LittleEndian.Uint64(e[8:])
|
||
addend := int64(binary.LittleEndian.Uint64(e[16:]))
|
||
typ := info & 0xffffffff
|
||
sym := int(info >> 32)
|
||
if typ != uint64(elf.R_X86_64_PC32) {
|
||
t.Errorf("reloc %d: type %d, want R_X86_64_PC32", i, typ)
|
||
}
|
||
if addend != -4 {
|
||
t.Errorf("reloc %d: addend %d, want -4", i, addend)
|
||
}
|
||
if name := symName(sym); name != "answer" && name != "extvar" {
|
||
t.Errorf("reloc %d: symbol %q, want answer or extvar", i, name)
|
||
}
|
||
// The relocation offset lands on the disp32 field: the four bytes
|
||
// before a RET-terminated eight-byte MOVQ.
|
||
if off+4 > uint64(len(textData)) {
|
||
t.Errorf("reloc %d: offset %d outside .text", i, off)
|
||
}
|
||
}
|
||
}
|
||
|
||
// TestELFObjectTLSGuardReloc checks that a non-NOSPLIT function's stack
|
||
// guard carries an R_X86_64_TPOFF32 relocation against the null symbol in
|
||
// .rela.text. The serialisation must honour the record's type field: a
|
||
// hardcoded R_X86_64_PC32 mislinks the TLS load as an ordinary
|
||
// PC-relative reference.
|
||
func TestELFObjectTLSGuardReloc(t *testing.T) {
|
||
f, errs := parser.Parse("g_amd64.s", `
|
||
#include "textflag.h"
|
||
|
||
TEXT ·grow(SB), $0
|
||
CALL ·other(SB)
|
||
RET
|
||
|
||
TEXT ·other(SB), NOSPLIT, $0
|
||
RET
|
||
`)
|
||
if len(errs) > 0 {
|
||
t.Fatalf("parse: %v", errs)
|
||
}
|
||
img, err := AssembleFile(f)
|
||
if err != nil {
|
||
t.Fatalf("AssembleFile: %v", err)
|
||
}
|
||
var haveTLS bool
|
||
for _, fn := range img.Funcs {
|
||
for _, r := range fn.Relocs {
|
||
if r.Kind == RelTLSLE {
|
||
haveTLS = true
|
||
}
|
||
}
|
||
}
|
||
if !haveTLS {
|
||
t.Fatal("test source produced no RelTLSLE relocation")
|
||
}
|
||
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()
|
||
relaSec := ef.Section(".rela.text")
|
||
if relaSec == nil {
|
||
t.Fatal("missing .rela.text")
|
||
}
|
||
raw, err := relaSec.Data()
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
found := false
|
||
for i := 0; i+24 <= len(raw); i += 24 {
|
||
e := raw[i:]
|
||
info := binary.LittleEndian.Uint64(e[8:])
|
||
typ := info & 0xffffffff
|
||
sym := int(info >> 32)
|
||
if typ == uint64(elf.R_X86_64_TPOFF32) {
|
||
found = true
|
||
if sym != 0 {
|
||
t.Errorf("TPOFF32 relocation against symbol %d, want 0 (the null symbol)", sym)
|
||
}
|
||
}
|
||
}
|
||
if !found {
|
||
t.Errorf("no R_X86_64_TPOFF32 relocation in .rela.text (%d bytes)", len(raw))
|
||
}
|
||
}
|
||
|
||
// TestELFObjectNoRelocations checks a file with no static-symbol references
|
||
// emits a valid object without a .rela.text section.
|
||
func TestELFObjectNoRelocations(t *testing.T) {
|
||
f, errs := parser.Parse("n_amd64.s", `
|
||
#include "textflag.h"
|
||
TEXT ·nop(SB), NOSPLIT, $0
|
||
RET
|
||
`)
|
||
if len(errs) > 0 {
|
||
t.Fatalf("parse: %v", errs)
|
||
}
|
||
img, err := AssembleFile(f)
|
||
if err != nil {
|
||
t.Fatalf("AssembleFile: %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()
|
||
if ef.Section(".rela.text") != nil {
|
||
t.Error("unexpected .rela.text section")
|
||
}
|
||
syms, err := ef.Symbols()
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
found := false
|
||
for _, s := range syms {
|
||
if s.Name == "nop" && elf.ST_TYPE(s.Info) == elf.STT_FUNC {
|
||
found = true
|
||
}
|
||
}
|
||
if !found {
|
||
t.Error("function symbol nop not found")
|
||
}
|
||
}
|
||
|
||
// elfSectionHeaderCount returns the e_shnum the ELF header declares.
|
||
func elfSectionHeaderCount(t *testing.T, obj []byte) int {
|
||
t.Helper()
|
||
return int(binary.LittleEndian.Uint16(obj[60:]))
|
||
}
|
||
|
||
// checkELFSectionAccounting verifies the number of section headers the
|
||
// writer physically laid out equals e_shnum: every DWARF section written
|
||
// after .shstrtab must be counted, or the last ones (always .debug_frame)
|
||
// are invisible to every consumer, debug/elf included.
|
||
func checkELFSectionAccounting(t *testing.T, obj []byte) {
|
||
t.Helper()
|
||
shoff := int(binary.LittleEndian.Uint64(obj[40:]))
|
||
shentsize := int(binary.LittleEndian.Uint16(obj[58:]))
|
||
shnum := elfSectionHeaderCount(t, obj)
|
||
if shentsize != 64 {
|
||
t.Fatalf("e_shentsize = %d, want 64", shentsize)
|
||
}
|
||
if (len(obj)-shoff)%shentsize != 0 {
|
||
t.Fatalf("section header table is not a whole number of entries: shoff=%d len=%d", shoff, len(obj))
|
||
}
|
||
if present := (len(obj) - shoff) / shentsize; present != shnum {
|
||
t.Errorf("e_shnum = %d but %d section headers are laid out", shnum, present)
|
||
}
|
||
}
|
||
|
||
// TestELFDWARFSectionAccounting runs the header accounting check over all
|
||
// four architecture emitters, and additionally checks the .debug_frame
|
||
// section is visible (its data aligned as its header declares).
|
||
func TestELFDWARFSectionAccounting(t *testing.T) {
|
||
parse := func(name, src string) *ast.File {
|
||
f, errs := parser.Parse(name, src)
|
||
if len(errs) > 0 {
|
||
t.Fatalf("parse %s: %v", name, errs)
|
||
}
|
||
return f
|
||
}
|
||
cases := []struct {
|
||
name string
|
||
img *Image
|
||
emit func(*Image) ([]byte, error)
|
||
}{
|
||
{"amd64", elfTestImage(t), (*Image).ELFObject},
|
||
{"arm64", mustImage(t, func() (*Image, error) {
|
||
return AssembleFileARM64(parse("k_arm64.s", `
|
||
#include "textflag.h"
|
||
|
||
TEXT ·add(SB), NOSPLIT, $0-24
|
||
MOVD a+0(FP), R4
|
||
MOVD b+8(FP), R5
|
||
ADD R5, R4, R4
|
||
MOVD R4, ret+16(FP)
|
||
RET
|
||
`))
|
||
}), (*Image).ELFAARCH64Object},
|
||
{"riscv64", mustImage(t, func() (*Image, error) {
|
||
return AssembleFileRISCV(parse("k_riscv64.s", `
|
||
#include "textflag.h"
|
||
|
||
TEXT ·sb(SB), NOSPLIT, $0-0
|
||
MOV $answer<>(SB), X10
|
||
RET
|
||
|
||
GLOBL answer<>(SB), RODATA, $8
|
||
DATA answer<>+0(SB)/8, $42
|
||
`))
|
||
}), (*Image).ELFRISCVObject},
|
||
{"loong64", mustImage(t, func() (*Image, error) {
|
||
return AssembleFileLOONG64(parse("k_loong64.s", `
|
||
#include "textflag.h"
|
||
|
||
TEXT ·add(SB), NOSPLIT, $0-24
|
||
MOVV a+0(FP), R4
|
||
MOVV b+8(FP), R5
|
||
ADDV R5, R4, R4
|
||
MOVV R4, ret+16(FP)
|
||
RET
|
||
`))
|
||
}), (*Image).ELFLOONG64Object},
|
||
}
|
||
for _, tc := range cases {
|
||
obj, err := tc.emit(tc.img)
|
||
if err != nil {
|
||
t.Fatalf("%s: emit: %v", tc.name, err)
|
||
}
|
||
checkELFSectionAccounting(t, obj)
|
||
ef, err := elf.NewFile(bytes.NewReader(obj))
|
||
if err != nil {
|
||
t.Fatalf("%s: parse emitted object: %v", tc.name, err)
|
||
}
|
||
frame := ef.Section(".debug_frame")
|
||
if frame == nil {
|
||
t.Errorf("%s: .debug_frame invisible to debug/elf (e_shnum too small?)", tc.name)
|
||
ef.Close()
|
||
continue
|
||
}
|
||
if frame.Offset%8 != 0 || frame.Addralign != 8 {
|
||
t.Errorf("%s: .debug_frame offset %d align %d, want offset%%8==0 align 8", tc.name, frame.Offset, frame.Addralign)
|
||
}
|
||
ef.Close()
|
||
}
|
||
}
|
||
|
||
func mustImage(t *testing.T, f func() (*Image, error)) *Image {
|
||
t.Helper()
|
||
img, err := f()
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
return img
|
||
}
|
||
|
||
// TestELFDWARFRelocations checks the .rela.debug_info and .rela.debug_line
|
||
// sections exist and carry absolute 64-bit relocations against the
|
||
// function symbols, with r_offsets inside their target sections.
|
||
func TestELFDWARFRelocations(t *testing.T) {
|
||
img := elfTestImage(t)
|
||
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()
|
||
// The DWARF must record the assembled file's path (threaded through
|
||
// Image.SourcePath), not a placeholder name.
|
||
info, err := ef.Section(".debug_info").Data()
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if img.SourcePath != "t_amd64.s" || !bytes.Contains(info, []byte(img.SourcePath)) {
|
||
t.Errorf("DWARF compilation unit does not name the source %q", img.SourcePath)
|
||
}
|
||
for _, tc := range []struct {
|
||
rela string
|
||
target string
|
||
want uint32
|
||
}{
|
||
{".rela.debug_info", ".debug_info", rX8664Abs64},
|
||
{".rela.debug_line", ".debug_line", rX8664Abs64},
|
||
{".rela.debug_frame", ".debug_frame", rX8664Abs64},
|
||
} {
|
||
rs := ef.Section(tc.rela)
|
||
if rs == nil {
|
||
t.Fatalf("missing %s", tc.rela)
|
||
}
|
||
if rs.Type != elf.SHT_RELA {
|
||
t.Errorf("%s: type %v, want SHT_RELA", tc.rela, rs.Type)
|
||
}
|
||
target := ef.Section(tc.target)
|
||
if target == nil {
|
||
t.Fatalf("missing %s", tc.target)
|
||
}
|
||
if rs.Link == 0 || ef.Sections[rs.Info] != target {
|
||
t.Errorf("%s: link %d info %d, want the symtab and %s", tc.rela, rs.Link, rs.Info, tc.target)
|
||
}
|
||
b, err := rs.Data()
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
// .debug_line has one address per function; .debug_info adds the
|
||
// compile unit's own low_pc.
|
||
want := len(img.Funcs)
|
||
if tc.target == ".debug_info" {
|
||
want++
|
||
}
|
||
if len(b)/24 != want {
|
||
t.Errorf("%s: %d entries, want %d", tc.rela, len(b)/24, want)
|
||
}
|
||
for i := 0; i+24 <= len(b); i += 24 {
|
||
r_offset := binary.LittleEndian.Uint64(b[i:])
|
||
info := binary.LittleEndian.Uint64(b[i+8:])
|
||
typ := uint32(info)
|
||
sym := int(info >> 32)
|
||
if typ != tc.want {
|
||
t.Errorf("%s entry %d: type %d, want R_X86_64_64 (%d)", tc.rela, i/24, typ, tc.want)
|
||
}
|
||
if r_offset >= uint64(target.Size) {
|
||
t.Errorf("%s entry %d: r_offset %d outside %s (%d bytes)", tc.rela, i/24, r_offset, tc.target, target.Size)
|
||
}
|
||
if sym == 0 {
|
||
t.Errorf("%s entry %d: against the null symbol", tc.rela, i/24)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// TestELFDataOnly checks a source with GLOBL data and no TEXT emits a valid
|
||
// ELF object: the DWARF compilation unit of a code-less image has no
|
||
// function to relocate against and must not reach for one.
|
||
func TestELFDataOnly(t *testing.T) {
|
||
f, errs := parser.Parse("d0_amd64.s", `
|
||
GLOBL table<>(SB), RODATA, $8
|
||
DATA table<>+0(SB)/8, $12345
|
||
`)
|
||
if len(errs) > 0 {
|
||
t.Fatalf("parse: %v", errs)
|
||
}
|
||
img, err := AssembleFile(f)
|
||
if err != nil {
|
||
t.Fatalf("AssembleFile: %v", err)
|
||
}
|
||
obj, err := img.ELFObject()
|
||
if err != nil {
|
||
t.Fatalf("ELFObject: %v", err)
|
||
}
|
||
checkELFSectionAccounting(t, obj)
|
||
ef, err := elf.NewFile(bytes.NewReader(obj))
|
||
if err != nil {
|
||
t.Fatalf("parse emitted object: %v", err)
|
||
}
|
||
defer ef.Close()
|
||
syms, err := ef.Symbols()
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
found := false
|
||
for _, s := range syms {
|
||
if s.Name == "table" && s.Size == 8 {
|
||
found = true
|
||
}
|
||
}
|
||
if !found {
|
||
t.Errorf("data symbol table missing: %v", syms)
|
||
}
|
||
if ef.Section(".rela.debug_info") != nil || ef.Section(".rela.debug_line") != nil {
|
||
t.Error("data-only image must not emit DWARF address relocations")
|
||
}
|
||
}
|
||
|
||
// TestELFLinkAndRun is the end-to-end check: assemble the test functions,
|
||
// link the emitted object with a C driver that defines the external symbol,
|
||
// and run the result. Skipped when no C compiler is available.
|
||
func TestELFLinkAndRun(t *testing.T) {
|
||
cc, err := exec.LookPath("cc")
|
||
if err != nil {
|
||
t.Skip("no C compiler available")
|
||
}
|
||
dir := t.TempDir()
|
||
|
||
img := elfTestImage(t)
|
||
obj, err := img.ELFObject()
|
||
if err != nil {
|
||
t.Fatalf("ELFObject: %v", err)
|
||
}
|
||
objPath := filepath.Join(dir, "t.o")
|
||
if err := os.WriteFile(objPath, obj, 0o644); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
const driver = `
|
||
#include <stdio.h>
|
||
|
||
long addq(long a, long b);
|
||
long getanswer(void);
|
||
long useextern(void);
|
||
|
||
long extvar = 7;
|
||
|
||
int main(void) {
|
||
printf("%ld %ld %ld\n", addq(41, 1), getanswer(), useextern());
|
||
return 0;
|
||
}
|
||
`
|
||
driverPath := filepath.Join(dir, "driver.c")
|
||
if err := os.WriteFile(driverPath, []byte(driver), 0o644); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
// -no-pie: the encoder emits R_X86_64_PC32 for external references,
|
||
// which a position-independent executable would reject (it wants
|
||
// PLT32/GOT relocations, a future increment).
|
||
appPath := filepath.Join(dir, "app")
|
||
out, err := exec.Command(cc, "-no-pie", "-o", appPath, driverPath, objPath).CombinedOutput()
|
||
if err != nil {
|
||
t.Fatalf("link failed: %v\n%s", err, out)
|
||
}
|
||
run, err := exec.Command(appPath).CombinedOutput()
|
||
if err != nil {
|
||
t.Fatalf("run failed: %v\n%s", err, run)
|
||
}
|
||
if got := string(run); got != "42 42 7\n" {
|
||
t.Errorf("output %q, want \"42 42 7\\n\"", got)
|
||
}
|
||
|
||
// The DWARF addresses must have resolved at link time: the .debug_info
|
||
// placeholders were carried by .rela.debug_info, so every subprogram's
|
||
// low_pc must now equal its linked symbol address.
|
||
bin, err := os.ReadFile(appPath)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
lef, err := elf.NewFile(bytes.NewReader(bin))
|
||
if err != nil {
|
||
t.Fatalf("parse linked binary: %v", err)
|
||
}
|
||
defer lef.Close()
|
||
syms, err := lef.Symbols()
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
addrByName := map[string]uint64{}
|
||
for _, s := range syms {
|
||
if elf.ST_TYPE(s.Info) == elf.STT_FUNC && s.Value != 0 {
|
||
addrByName[s.Name] = s.Value
|
||
}
|
||
}
|
||
lowPCs := dwarfSubprogramLowPCs(t, lef)
|
||
if len(lowPCs) == 0 {
|
||
t.Fatal("no subprogram DW_AT_low_pc parsed from the linked binary")
|
||
}
|
||
for name, pc := range lowPCs {
|
||
addr, ok := addrByName[name]
|
||
if !ok {
|
||
t.Errorf("subprogram %q not in the linked symbol table", name)
|
||
continue
|
||
}
|
||
if pc != addr {
|
||
t.Errorf("subprogram %q: DW_AT_low_pc = %#x, linked address %#x (DWARF relocation unresolved)", name, pc, addr)
|
||
}
|
||
}
|
||
}
|
||
|
||
// dwarfSubprogramLowPCs walks the linked binary's .debug_info with its own
|
||
// .debug_abbrev and returns each DW_TAG_subprogram's DW_AT_low_pc by name.
|
||
func dwarfSubprogramLowPCs(t *testing.T, ef *elf.File) map[string]uint64 {
|
||
t.Helper()
|
||
abbrevSec := ef.Section(".debug_abbrev")
|
||
infoSec := ef.Section(".debug_info")
|
||
if abbrevSec == nil || infoSec == nil {
|
||
t.Fatal("linked binary lacks .debug_abbrev or .debug_info")
|
||
}
|
||
abbrev, err := abbrevSec.Data()
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
info, err := infoSec.Data()
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
abs := parseAbbrevs(t, abbrev)
|
||
le := binary.LittleEndian
|
||
out := map[string]uint64{}
|
||
r := &ulebIter{b: info}
|
||
r.uint32At(t) // unit_length
|
||
if v := le.Uint16(info[4:]); v != 5 {
|
||
t.Fatalf(".debug_info version %d, want 5", v)
|
||
}
|
||
r.i = 6
|
||
r.byteAt(t) // unit_type
|
||
r.byteAt(t) // address_size
|
||
r.uint32At(t) // debug_abbrev_offset
|
||
var name string
|
||
var lowPC uint64
|
||
for r.i < len(r.b) {
|
||
code := r.uleb(t)
|
||
if code == 0 {
|
||
continue // end of the CU's children
|
||
}
|
||
ab, ok := abs[code]
|
||
if !ok {
|
||
t.Fatalf("unknown abbreviation code %d", code)
|
||
}
|
||
name, lowPC = "", 0
|
||
for _, a := range ab.attrs {
|
||
switch a.attr {
|
||
case dwAtName:
|
||
readFormKeep(t, r, a.form, &name, nil)
|
||
case dwAtLowPC:
|
||
readFormKeep(t, r, a.form, nil, &lowPC)
|
||
default:
|
||
readFormSkip(t, r, a.form)
|
||
}
|
||
}
|
||
if ab.tag == dwTagSubprog && name != "" {
|
||
out[name] = lowPC
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
// readFormKeep reads one DIE attribute value, keeping a string or an
|
||
// address into the pointer it was given (nil keeps nothing).
|
||
func readFormKeep(t *testing.T, r *ulebIter, form uint64, name *string, addr *uint64) {
|
||
t.Helper()
|
||
switch form {
|
||
case dwFormString:
|
||
end := r.i
|
||
for end < len(r.b) && r.b[end] != 0 {
|
||
end++
|
||
}
|
||
if name != nil {
|
||
*name = string(r.b[r.i:end])
|
||
}
|
||
r.i = end + 1
|
||
case dwFormAddr:
|
||
if addr != nil {
|
||
*addr = binary.LittleEndian.Uint64(r.b[r.i:])
|
||
}
|
||
r.i += 8
|
||
default:
|
||
readFormSkip(t, r, form)
|
||
}
|
||
}
|
||
|
||
func readFormSkip(t *testing.T, r *ulebIter, form uint64) {
|
||
t.Helper()
|
||
switch form {
|
||
case dwFormString:
|
||
for r.i < len(r.b) && r.b[r.i] != 0 {
|
||
r.i++
|
||
}
|
||
r.i++
|
||
case dwFormAddr, dwFormData8:
|
||
r.i += 8
|
||
case dwFormSecOff:
|
||
r.i += 4
|
||
case dwFormExprloc:
|
||
r.i += int(r.uleb(t))
|
||
case dwFormData1, 0x0c:
|
||
r.i++
|
||
default:
|
||
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)
|
||
}
|
||
}
|
||
}
|