// Copyright (c) 2026 Petr BalvĂ­n (https://petrbalvin.org) // SPDX-License-Identifier: BSD-3-Clause package asm import ( "bytes" "encoding/binary" "testing" ) // TestDWSelectOpcode checks the special-opcode selection against // hand-computed values for the boundary cases: line deltas below, inside // and above the line range, and pc deltas at and beyond PC_RANGE (24). func TestDWSelectOpcode(t *testing.T) { cases := []struct { deltaPC uint64 deltaLC int64 want int64 }{ {0, 2, 17}, // the common single-instruction step {0, -4, 11}, // deltaLC == LINE_BASE {0, -5, 11}, // deltaLC below LINE_BASE: opcode adds nothing {0, 6, 20}, // deltaLC == LINE_BASE+LINE_RANGE, remainder via advance_line {4, 1, 56}, // the 4-byte loong64 instruction step {23, 1, 246}, // deltaPC == PC_RANGE-1 {24, 1, 246}, // deltaPC == PC_RANGE: wraps past 255 {25, 1, 246}, // deltaPC past PC_RANGE (the const_add_pc remainder adjusts it later) {100, 1, 246}, {151, 10, 255}, // deltaPC-PC_RANGE == (1<<7)-1, large line delta {100, 10, 250}, // deltaPC-PC_RANGE not on a switch boundary {23, 10, 250}, // large line delta inside PC_RANGE } for _, c := range cases { if got := dwSelectOpcode(c.deltaPC, c.deltaLC); got != c.want { t.Errorf("dwSelectOpcode(%d, %d) = %d, want %d", c.deltaPC, c.deltaLC, got, c.want) } } } // decodeDWLineProgram decodes a .debug_line state-machine program (as // emitted by goobjDwarfLines) into (pc, line) rows. func decodeDWLineProgram(t *testing.T, b []byte) (pcs []uint64, lines []int64) { t.Helper() pc, line := uint64(0), int64(1) emit := func() { if len(pcs) == 0 || pcs[len(pcs)-1] != pc || lines[len(lines)-1] != line { pcs = append(pcs, pc) lines = append(lines, line) } } advancePC := func(delta uint64) { pc += delta } advanceLine := func(delta int64) { line += delta } for i := 0; i < len(b); { op := b[i] i++ switch { case op == 0: // extended opcode ln, n := binary.Uvarint(b[i:]) i += n sub := b[i] i++ _ = ln switch sub { case 2: // DW_LNE_set_address: 8-byte address pc = binary.LittleEndian.Uint64(b[i:]) i += 8 case 1: // DW_LNE_end_sequence // terminates the sequence; no new row } case op == 2: // DW_LNS_advance_pc v, n := binary.Uvarint(b[i:]) i += n advancePC(v) case op == 3: // DW_LNS_advance_line v, n := binary.Varint(b[i:]) i += n advanceLine(v) case op == 8: // DW_LNS_const_add_pc advancePC(uint64(dwPCRange)) case op == 9: // DW_LNS_fixed_advance_pc advancePC(uint64(binary.LittleEndian.Uint16(b[i:]))) i += 2 case op >= dwOpcodeBase: // special opcode advancePC(uint64((int64(op) - dwOpcodeBase) / dwLineRange)) advanceLine((int64(op)-dwOpcodeBase)%dwLineRange + dwLineBase) emit() } } return pcs, lines } // TestGoobjDwarfLinesRows checks the emitted line program's rows for // synthetic functions: a zero-frame function with one instruction per // line, a framed function (the prologue row is prepended on the TEXT // line), instructions sharing a line, and a function with a large pc gap // (the const_add_pc remainder path). func TestGoobjDwarfLinesRows(t *testing.T) { cases := []struct { name string fn FuncLayout want [][2]int64 // (pc, line) }{ { "one instruction per line", FuncLayout{Size: 20, Line: 2, Lines: []LineEntry{ {0, 3}, {4, 4}, {8, 5}, {12, 6}, {16, 7}, }}, [][2]int64{{0, 3}, {4, 4}, {8, 5}, {12, 6}, {16, 7}}, }, { "framed: prologue row on the TEXT line", FuncLayout{Size: 24, Line: 2, Lines: []LineEntry{ {12, 3}, {16, 4}, }}, [][2]int64{{0, 2}, {12, 3}, {16, 4}}, }, { "instructions sharing a line fold into one row", FuncLayout{Size: 16, Line: 2, Lines: []LineEntry{ {0, 3}, {4, 3}, {8, 4}, {12, 4}, }}, [][2]int64{{0, 3}, {8, 4}}, }, { "large gap crosses PC_RANGE", FuncLayout{Size: 60, Line: 2, Lines: []LineEntry{ {0, 3}, {40, 4}, }}, [][2]int64{{0, 3}, {40, 4}}, }, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { prog, relocs := goobjDwarfLines(c.fn, 0) if len(relocs) != 1 || relocs[0].off != 3 || relocs[0].siz != 8 || relocs[0].typ != relocAddr || relocs[0].sym != 0 { t.Fatalf("relocs = %+v", relocs) } pcs, lines := decodeDWLineProgram(t, prog) if len(pcs) != len(c.want) { t.Fatalf("rows = %d (%v / %v), want %d", len(pcs), pcs, lines, len(c.want)) } for i, w := range c.want { if pcs[i] != uint64(w[0]) || lines[i] != w[1] { t.Errorf("row %d = (%d, %d), want (%d, %d)", i, pcs[i], lines[i], w[0], w[1]) } } }) } } // TestGoobjDwarfInfo checks the subprogram DIE for an exported and a // static function: the abbrev, name, high_pc, frame base, decl file/line, // the external flag and the addrx relocation position. func TestGoobjDwarfInfo(t *testing.T) { fn := FuncLayout{Size: 20, Line: 2} die, relocs := goobjDwarfInfo(fn, "pkg.f", 3) want := []byte{ 0x03, 'p', 'k', 'g', '.', 'f', 0, 0, 0, 0, 0, // addrx slot at offset 7 0x14, // high_pc: 20 0x01, 0x9c, // frame_base 0x01, 0, 0, 0, // decl_file 1 0x02, // decl_line 2 0x01, // external 0x00, // end of children } if !bytes.Equal(die, want) { t.Errorf("DIE = %x, want %x", die, want) } if len(relocs) != 1 || relocs[0].off != 7 || relocs[0].siz != 4 || relocs[0].typ != relocDWTXTADDRU4 || relocs[0].sym != 3 { t.Errorf("relocs = %+v", relocs) } // A static function carries no external flag and no package prefix. fn.Static = true die, _ = goobjDwarfInfo(fn, "f", 1) if die[len(die)-2] != 0 { t.Errorf("static external flag = %d, want 0", die[len(die)-2]) } } // TestDwPutPCLCDeltaRemainders checks the standard-opcode remainders: // const_add_pc and fixed_advance_pc after a special opcode. func TestDwPutPCLCDeltaRemainders(t *testing.T) { // deltaPC 25 past PC_RANGE: opcode 26 covers (1, 1), const_add_pc // covers the remaining 23 pc and 0 line. got := dwPutPCLCDelta(nil, 25, 1) if !bytes.Equal(got, []byte{8, 26}) { t.Errorf("25/1 = %x, want [8 1a]", got) } // deltaPC 20000: opcode 246 covers 23, fixed_advance_pc covers the // remaining 19977. got = dwPutPCLCDelta(nil, 20000, 1) if got[0] != 9 || binary.LittleEndian.Uint16(got[1:]) != 19977 || got[3] != 246 { t.Errorf("20000/1 = %x, want fixed_advance_pc 19977 then 246", got) } // Line remainder: deltaLC 10 leaves 5 past the opcode's reach, encoded // as advance_line 5 (zigzag 0x0a) before opcode 250. got = dwPutPCLCDelta(nil, 23, 10) if !bytes.Equal(got, []byte{3, 0x0a, 250}) { t.Errorf("23/10 = %x, want [03 0a fa]", got) } // Negative line remainder: deltaLC -5 leaves advance_line -1 (zigzag // 0x01) after opcode 11. got = dwPutPCLCDelta(nil, 0, -5) if !bytes.Equal(got, []byte{3, 1, 11}) { t.Errorf("0/-5 = %x, want [03 01 0b]", got) } }