fix(elf): relocation records, DWARF tables and per-architecture frame data
Assisted-by: GLM 5.3
This commit is contained in:
+303
-12
@@ -4,11 +4,313 @@
|
||||
package asm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||
)
|
||||
|
||||
// ulebIter reads ULEB128 values, the .debug_abbrev and line-header
|
||||
// encoding.
|
||||
type ulebIter struct {
|
||||
b []byte
|
||||
i int
|
||||
}
|
||||
|
||||
func (r *ulebIter) uleb(t *testing.T) uint64 {
|
||||
t.Helper()
|
||||
v, n := binary.Uvarint(r.b[r.i:])
|
||||
if n <= 0 {
|
||||
t.Fatalf("bad ULEB at %d", r.i)
|
||||
}
|
||||
r.i += n
|
||||
return v
|
||||
}
|
||||
|
||||
func (r *ulebIter) byteAt(t *testing.T) byte {
|
||||
t.Helper()
|
||||
if r.i >= len(r.b) {
|
||||
t.Fatalf("read past end at %d", r.i)
|
||||
}
|
||||
c := r.b[r.i]
|
||||
r.i++
|
||||
return c
|
||||
}
|
||||
|
||||
func (r *ulebIter) uint32At(t *testing.T) uint32 {
|
||||
t.Helper()
|
||||
v := binary.LittleEndian.Uint32(r.b[r.i:])
|
||||
r.i += 4
|
||||
return v
|
||||
}
|
||||
|
||||
// sleb reads a signed LEB128, the DWARF encoding (sign-extended two's
|
||||
// complement, not Go's zigzag varint).
|
||||
func (r *ulebIter) sleb(t *testing.T) int64 {
|
||||
t.Helper()
|
||||
var v int64
|
||||
var shift uint
|
||||
for {
|
||||
c := r.byteAt(t)
|
||||
v |= int64(c&0x7f) << shift
|
||||
shift += 7
|
||||
if c&0x80 == 0 {
|
||||
if c&0x40 != 0 {
|
||||
v |= -1 << shift
|
||||
}
|
||||
return v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// dwarfAttr is one attribute/form pair of an abbreviation.
|
||||
type dwarfAttr struct{ attr, form uint64 }
|
||||
|
||||
// dwarfAbbrev is one parsed abbreviation declaration.
|
||||
type dwarfAbbrev struct {
|
||||
code uint64
|
||||
tag uint64
|
||||
children bool
|
||||
attrs []dwarfAttr
|
||||
}
|
||||
|
||||
// parseAbbrevs walks a .debug_abbrev table: abbreviation code, tag,
|
||||
// children flag, then attr/form ULEB pairs terminated by a double zero.
|
||||
func parseAbbrevs(t *testing.T, b []byte) map[uint64]dwarfAbbrev {
|
||||
t.Helper()
|
||||
out := map[uint64]dwarfAbbrev{}
|
||||
r := &ulebIter{b: b}
|
||||
for {
|
||||
code := r.uleb(t)
|
||||
if code == 0 {
|
||||
return out
|
||||
}
|
||||
ab := dwarfAbbrev{code: code, tag: r.uleb(t)}
|
||||
ab.children = r.byteAt(t) == 1
|
||||
for {
|
||||
attr := r.uleb(t)
|
||||
form := r.uleb(t)
|
||||
if attr == 0 && form == 0 {
|
||||
break
|
||||
}
|
||||
if attr == 0 || form == 0 {
|
||||
t.Fatalf("abbrev %d: half-terminated attr/form pair (%d, %d)", code, attr, form)
|
||||
}
|
||||
ab.attrs = append(ab.attrs, dwarfAttr{attr, form})
|
||||
}
|
||||
out[code] = ab
|
||||
}
|
||||
}
|
||||
|
||||
func eqAttrs(t *testing.T, ab dwarfAbbrev, want []dwarfAttr) {
|
||||
t.Helper()
|
||||
if len(ab.attrs) != len(want) {
|
||||
t.Fatalf("abbrev %d attrs = %v, want %v", ab.code, ab.attrs, want)
|
||||
}
|
||||
for i, w := range want {
|
||||
if ab.attrs[i] != w {
|
||||
t.Fatalf("abbrev %d attr %d = (%#x, %#x), want (%#x, %#x)", ab.code, i, ab.attrs[i].attr, ab.attrs[i].form, w.attr, w.form)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestDwarfAbbrevTable walks the abbreviation table as a consumer does and
|
||||
// checks the attribute/form sets against the constants the toolchain uses
|
||||
// (cmd/internal/dwarf/dwarf_defs.go). A wrong constant here renames an
|
||||
// attribute (0x1b is comp_dir, not low_pc; 0x29 and 0x37 are bounds and
|
||||
// count) and a wrong form desynchronises the DIE parse: 0x25 is strx1, one
|
||||
// byte, where the writer emits four for a section offset.
|
||||
func TestDwarfAbbrevTable(t *testing.T) {
|
||||
abbrev := dwarfAbbrevTable()
|
||||
if len(abbrev) == 0 {
|
||||
t.Fatal("empty abbrev table")
|
||||
}
|
||||
// Must end with a zero byte (end of table).
|
||||
if abbrev[len(abbrev)-1] != 0 {
|
||||
t.Fatalf("abbrev table last byte = %d, want 0", abbrev[len(abbrev)-1])
|
||||
}
|
||||
abs := parseAbbrevs(t, abbrev)
|
||||
if len(abs) != 2 {
|
||||
t.Fatalf("abbreviations = %d, want 2", len(abs))
|
||||
}
|
||||
cu, ok := abs[1]
|
||||
if !ok {
|
||||
t.Fatal("missing abbreviation 1 (compile unit)")
|
||||
}
|
||||
if cu.tag != dwTagCompUnit || !cu.children {
|
||||
t.Errorf("abbrev 1: tag %#x children %v, want compile unit with children", cu.tag, cu.children)
|
||||
}
|
||||
eqAttrs(t, cu, []dwarfAttr{
|
||||
{dwAtLowPC, dwFormAddr},
|
||||
{dwAtHighPC, dwFormData8},
|
||||
{dwAtStmtList, dwFormSecOff},
|
||||
{dwAtName, dwFormString},
|
||||
})
|
||||
sp, ok := abs[2]
|
||||
if !ok {
|
||||
t.Fatal("missing abbreviation 2 (subprogram)")
|
||||
}
|
||||
if sp.tag != dwTagSubprog || sp.children {
|
||||
t.Errorf("abbrev 2: tag %#x children %v, want subprogram without children", sp.tag, sp.children)
|
||||
}
|
||||
eqAttrs(t, sp, []dwarfAttr{
|
||||
{dwAtName, dwFormString},
|
||||
{dwAtLowPC, dwFormAddr},
|
||||
{dwAtHighPC, dwFormData8},
|
||||
{dwAtFrameBase, dwFormExprloc},
|
||||
{dwAtDeclFile, dwFormData1},
|
||||
{dwAtDeclLine, dwFormData1},
|
||||
{dwAtExternal, 0x0c}, // DW_FORM_flag
|
||||
})
|
||||
}
|
||||
|
||||
// TestDwarfLineHeaderV5 parses the .debug_line header under DWARF5 rules:
|
||||
// the directory and file tables are format-descriptor lists, not the
|
||||
// DWARF2-4 shape of null-terminated strings, and the file entry references
|
||||
// the source name through .debug_line_str.
|
||||
func TestDwarfLineHeaderV5(t *testing.T) {
|
||||
src := `#include "textflag.h"
|
||||
TEXT ·add(SB), NOSPLIT, $0-24
|
||||
MOVQ a+0(FP), AX
|
||||
MOVQ b+8(FP), BX
|
||||
ADDQ BX, AX
|
||||
MOVQ AX, ret+16(FP)
|
||||
RET
|
||||
`
|
||||
f, errs := parser.Parse("test_amd64.s", src)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFile(f)
|
||||
if err != nil {
|
||||
t.Fatalf("assemble: %v", err)
|
||||
}
|
||||
|
||||
ds := emitDWARF(img, "test_amd64.s", cfiAMD64)
|
||||
r := &ulebIter{b: ds.debugLine}
|
||||
r.uint32At(t) // unit_length
|
||||
if v := binary.LittleEndian.Uint16(ds.debugLine[4:]); v != 5 {
|
||||
t.Fatalf("version = %d, want 5", v)
|
||||
}
|
||||
r.i = 6
|
||||
r.byteAt(t) // address_size
|
||||
r.byteAt(t) // segment_selector_size
|
||||
r.uint32At(t) // header_length
|
||||
r.byteAt(t) // minimum_instruction_length
|
||||
r.byteAt(t) // maximum_ops_per_instruction
|
||||
r.byteAt(t) // default_is_stmt
|
||||
r.byteAt(t) // line_base
|
||||
r.byteAt(t) // line_range
|
||||
opcodeBase := r.byteAt(t)
|
||||
for range int(opcodeBase) - 1 {
|
||||
r.byteAt(t) // standard opcode lengths
|
||||
}
|
||||
|
||||
// Directory table (DWARF5 §6.2.4).
|
||||
if n := r.byteAt(t); n != 1 {
|
||||
t.Fatalf("directory_entry_format_count = %d, want 1", n)
|
||||
}
|
||||
if lnct := r.uleb(t); lnct != dwLnctPath {
|
||||
t.Errorf("directory content type = %#x, want DW_LNCT_path", lnct)
|
||||
}
|
||||
if form := r.uleb(t); form != dwFormLineStrp {
|
||||
t.Errorf("directory form = %#x, want DW_FORM_line_strp", form)
|
||||
}
|
||||
if n := r.uleb(t); n != 1 {
|
||||
t.Fatalf("directories_count = %d, want 1", n)
|
||||
}
|
||||
if off := r.uint32At(t); off != 0 {
|
||||
t.Errorf("compilation directory line_strp = %d, want 0 (the empty string)", off)
|
||||
}
|
||||
|
||||
// File table (DWARF5 §6.2.5).
|
||||
if n := r.byteAt(t); n != 2 {
|
||||
t.Fatalf("file_name_entry_format_count = %d, want 2", n)
|
||||
}
|
||||
if lnct := r.uleb(t); lnct != dwLnctPath {
|
||||
t.Errorf("file content type = %#x, want DW_LNCT_path", lnct)
|
||||
}
|
||||
if form := r.uleb(t); form != dwFormLineStrp {
|
||||
t.Errorf("file path form = %#x, want DW_FORM_line_strp", form)
|
||||
}
|
||||
if lnct := r.uleb(t); lnct != dwLnctDirIndex {
|
||||
t.Errorf("file content type = %#x, want DW_LNCT_directory_index", lnct)
|
||||
}
|
||||
if form := r.uleb(t); form != dwFormUdata {
|
||||
t.Errorf("file dir-index form = %#x, want DW_FORM_udata", form)
|
||||
}
|
||||
if n := r.uleb(t); n != 1 {
|
||||
t.Fatalf("file_names_count = %d, want 1", n)
|
||||
}
|
||||
strOff := r.uint32At(t)
|
||||
if dirIdx := r.uleb(t); dirIdx != 0 {
|
||||
t.Errorf("file directory index = %d, want 0", dirIdx)
|
||||
}
|
||||
|
||||
// The file entry's line_strp must resolve to the source name.
|
||||
end := int(strOff) + len("test_amd64.s")
|
||||
if int(strOff) >= len(ds.debugLineStr) || !bytes.Equal(ds.debugLineStr[strOff:end], []byte("test_amd64.s")) {
|
||||
t.Errorf("file entry line_strp %d does not name the source: %q", strOff, ds.debugLineStr)
|
||||
}
|
||||
|
||||
// The fixed header fields: address_size 8 and a header_length that
|
||||
// points just past the file table (the patch site is offset 8 in the
|
||||
// v5 header, and the field counts from its own end).
|
||||
if ds.debugLine[6] != 8 || ds.debugLine[7] != 0 {
|
||||
t.Errorf("address_size/segment_selector = %d/%d, want 8/0", ds.debugLine[6], ds.debugLine[7])
|
||||
}
|
||||
if hl := binary.LittleEndian.Uint32(ds.debugLine[8:]); hl != uint32(r.i-12) {
|
||||
t.Errorf("header_length = %d, want %d (the byte after the file table is %d)", hl, r.i-12, r.i)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDwarfFrameCIEArch checks the shared CIE carries each architecture's
|
||||
// stack-pointer and return-address registers: the values the Go linker
|
||||
// writes (cmd/link/internal/<arch>/l.go dwarfRegSP/dwarfRegLR).
|
||||
func TestDwarfFrameCIEArch(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
cfi cfiArch
|
||||
}{
|
||||
{"amd64", cfiAMD64},
|
||||
{"arm64", cfiARM64},
|
||||
{"riscv64", cfiRISCV64},
|
||||
{"loong64", cfiLOONG64},
|
||||
} {
|
||||
frame := dwarfBuildFrameSection(&Image{}, tc.cfi, &dwarfSections{})
|
||||
r := &ulebIter{b: frame}
|
||||
r.uint32At(t) // length
|
||||
if cid := r.uint32At(t); cid != 0xFFFFFFFF {
|
||||
t.Errorf("%s: CIE id = %#x, want 0xffffffff", tc.name, cid)
|
||||
}
|
||||
if v := r.byteAt(t); v != 3 {
|
||||
t.Errorf("%s: CIE version = %d, want 3", tc.name, v)
|
||||
}
|
||||
if aug := r.byteAt(t); aug != 0 {
|
||||
t.Errorf("%s: CIE augmentation = %d, want 0", tc.name, aug)
|
||||
}
|
||||
if ca := r.uleb(t); ca != 1 {
|
||||
t.Errorf("%s: code alignment = %d, want 1", tc.name, ca)
|
||||
}
|
||||
if da := r.sleb(t); da != -8 {
|
||||
t.Errorf("%s: data alignment = %d, want -8 (signed LEB128, not zigzag)", tc.name, da)
|
||||
}
|
||||
if ra := r.uleb(t); ra != uint64(tc.cfi.raReg) {
|
||||
t.Errorf("%s: return-address register = %d, want %d", tc.name, ra, tc.cfi.raReg)
|
||||
}
|
||||
if op := r.byteAt(t); op != 0x0c {
|
||||
t.Errorf("%s: expected DW_CFA_def_cfa, got opcode %#x", tc.name, op)
|
||||
}
|
||||
if cfa := r.uleb(t); cfa != uint64(tc.cfi.cfaReg) {
|
||||
t.Errorf("%s: CFA register = %d, want %d", tc.name, cfa, tc.cfi.cfaReg)
|
||||
}
|
||||
if off := r.uleb(t); off != 0 {
|
||||
t.Errorf("%s: CFA offset = %d, want 0", tc.name, off)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmitDWARF(t *testing.T) {
|
||||
src := `#include "textflag.h"
|
||||
TEXT ·add(SB), NOSPLIT, $0-24
|
||||
@@ -27,7 +329,7 @@ TEXT ·add(SB), NOSPLIT, $0-24
|
||||
t.Fatalf("assemble: %v", err)
|
||||
}
|
||||
|
||||
ds := emitDWARF(img, "test_amd64.s")
|
||||
ds := emitDWARF(img, "test_amd64.s", cfiAMD64)
|
||||
|
||||
// .debug_abbrev must not be empty and must start with abbrev code 1.
|
||||
if len(ds.debugAbbrev) == 0 {
|
||||
@@ -68,14 +370,3 @@ TEXT ·add(SB), NOSPLIT, $0-24
|
||||
t.Fatal("no .debug_info relocations")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDwarfAbbrevTable(t *testing.T) {
|
||||
abbrev := dwarfAbbrevTable()
|
||||
if len(abbrev) == 0 {
|
||||
t.Fatal("empty abbrev table")
|
||||
}
|
||||
// Must end with a zero byte (end of table).
|
||||
if abbrev[len(abbrev)-1] != 0 {
|
||||
t.Fatalf("abbrev table last byte = %d, want 0", abbrev[len(abbrev)-1])
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user