Repository: cilium/ebpf
Pull request #582 (2022-02-22 17:36:01)

Moved Instruction.Symbol and Instruction.Reference into per-instruction metadata
This PR is a sub-set of the changes originally presented in #567. That PR became bloated and hard to review, this PR contains less drastic changes which can be merged into master independently of the rest of the features.


Patch:
diff --git a/asm/instruction.go b/asm/instruction.go
index bc3b883..10174c4 100644
--- a/asm/instruction.go
+++ b/asm/instruction.go
@@ -35,17 +35,39 @@ type Instruction struct {
 	Offset   int16
 	Constant int64
 
-	// Reference denotes a reference (e.g. a jump) to another symbol.
-	Reference string
+	// Metadata contains optional metadata about this instruction
+	metadata *metadata
+}
+
+// WithSymbol marks the Instruction as a Symbol, which other Instructions
+// can point to using corresponding calls to WithReference.
+func (ins Instruction) WithSymbol(name string) Instruction {
+	if (ins.metadata != nil && ins.metadata.symbol == name) ||
+		(ins.metadata == nil && name == "") {
+		return ins
+	}
 
-	// Symbol denotes an instruction at the start of a function body.
-	Symbol string
+	ins.metadata = ins.metadata.copy()
+	ins.metadata.symbol = name
+	return ins
 }
 
 // Sym creates a symbol.
+//
+// Deprecated: use WithSymbol instead.
 func (ins Instruction) Sym(name string) Instruction {
-	ins.Symbol = name
-	return ins
+	return ins.WithSymbol(name)
+}
+
+// Symbol returns the value ins has been marked with using WithSymbol,
+// otherwise returns an empty string. A symbol is often an Instruction
+// at the start of a function body.
+func (ins Instruction) Symbol() string {
+	if ins.metadata == nil {
+		return ""
+	}
+
+	return ins.metadata.symbol
 }
 
 // Unmarshal decodes a BPF instruction.
@@ -299,16 +321,63 @@ func (ins Instruction) Format(f fmt.State, c rune) {
 	}
 
 ref:
-	if ins.Reference != "" {
-		fmt.Fprintf(f, " <%s>", ins.Reference)
+	if ins.Reference() != "" {
+		fmt.Fprintf(f, " <%s>", ins.Reference())
 	}
 }
 
+func (ins Instruction) equal(other Instruction) bool {
+	return ins.OpCode == other.OpCode &&
+		ins.Dst == other.Dst &&
+		ins.Src == other.Src &&
+		ins.Offset == other.Offset &&
+		ins.Constant == other.Constant
+}
+
 // Size returns the amount of bytes ins would occupy in binary form.
 func (ins Instruction) Size() uint64 {
 	return uint64(InstructionSize * ins.OpCode.rawInstructions())
 }
 
+// WithReference makes ins reference another Symbol or map by name.
+func (ins Instruction) WithReference(ref string) Instruction {
+	if (ins.metadata != nil && ins.metadata.reference == ref) ||
+		(ins.metadata == nil && ref == "") {
+		return ins
+	}
+
+	ins.metadata = ins.metadata.copy()
+	ins.metadata.reference = ref
+	return ins
+}
+
+// Reference returns the Symbol or map name referenced by ins, if any.
+func (ins Instruction) Reference() string {
+	if ins.metadata == nil {
+		return ""
+	}
+
+	return ins.metadata.reference
+}
+
+// metadata holds metadata about an Instruction.
+type metadata struct {
+	// reference denotes a reference (e.g. a jump) to another symbol.
+	reference string
+	// symbol denotes an instruction at the start of a function body.
+	symbol string
+}
+
+// copy returns a copy of metadata.
+// Always returns a valid pointer, even when called on a nil metadata.
+func (m *metadata) copy() *metadata {
+	var copy metadata
+	if m != nil {
+		copy = *m
+	}
+	return &copy
+}
+
 // Instructions is an eBPF program.
 type Instructions []Instruction
 
@@ -342,7 +411,7 @@ func (insns Instructions) Name() string {
 	if len(insns) == 0 {
 		return ""
 	}
-	return insns[0].Symbol
+	return insns[0].Symbol()
 }
 
 func (insns Instructions) String() string {
@@ -369,7 +438,7 @@ func (insns Instructions) RewriteMapPtr(symbol string, fd int) error {
 	found := false
 	for i := range insns {
 		ins := &insns[i]
-		if ins.Reference != symbol {
+		if ins.Reference() != symbol {
 			continue
 		}
 
@@ -393,15 +462,15 @@ func (insns Instructions) SymbolOffsets() (map[string]int, error) {
 	offsets := make(map[string]int)
 
 	for i, ins := range insns {
-		if ins.Symbol == "" {
+		if ins.Symbol() == "" {
 			continue
 		}
 
-		if _, ok := offsets[ins.Symbol]; ok {
-			return nil, fmt.Errorf("duplicate symbol %s", ins.Symbol)
+		if _, ok := offsets[ins.Symbol()]; ok {
+			return nil, fmt.Errorf("duplicate symbol %s", ins.Symbol())
 		}
 
-		offsets[ins.Symbol] = i
+		offsets[ins.Symbol()] = i
 	}
 
 	return offsets, nil
@@ -418,7 +487,7 @@ func (insns Instructions) FunctionReferences() map[string]bool {
 			continue
 		}
 
-		if ins.Reference == "" {
+		if ins.Reference() == "" {
 			continue
 		}
 
@@ -426,7 +495,7 @@ func (insns Instructions) FunctionReferences() map[string]bool {
 			continue
 		}
 
-		calls[ins.Reference] = true
+		calls[ins.Reference()] = true
 	}
 
 	return calls
@@ -438,11 +507,11 @@ func (insns Instructions) ReferenceOffsets() map[string][]int {
 	offsets := make(map[string][]int)
 
 	for i, ins := range insns {
-		if ins.Reference == "" {
+		if ins.Reference() == "" {
 			continue
 		}
 
-		offsets[ins.Reference] = append(offsets[ins.Reference], i)
+		offsets[ins.Reference()] = append(offsets[ins.Reference()], i)
 	}
 
 	return offsets
@@ -493,8 +562,8 @@ func (insns Instructions) Format(f fmt.State, c rune) {
 
 	iter := insns.Iterate()
 	for iter.Next() {
-		if iter.Ins.Symbol != "" {
-			fmt.Fprintf(f, "%s%s:\n", symIndent, iter.Ins.Symbol)
+		if iter.Ins.Symbol() != "" {
+			fmt.Fprintf(f, "%s%s:\n", symIndent, iter.Ins.Symbol())
 		}
 		fmt.Fprintf(f, "%s%*d: %v\n", indent, offsetWidth, iter.Offset, iter.Ins)
 	}
@@ -552,15 +621,15 @@ func (insns Instructions) resolveFunctionReferences() error {
 	for iter.Next() {
 		ins := iter.Ins
 
-		if ins.Symbol == "" {
+		if ins.Symbol() == "" {
 			continue
 		}
 
-		if _, ok := symbolOffsets[ins.Symbol]; ok {
-			return fmt.Errorf("duplicate symbol %s", ins.Symbol)
+		if _, ok := symbolOffsets[ins.Symbol()]; ok {
+			return fmt.Errorf("duplicate symbol %s", ins.Symbol())
 		}
 
-		symbolOffsets[ins.Symbol] = iter.Offset
+		symbolOffsets[ins.Symbol()] = iter.Offset
 	}
 
 	// Find all instructions tagged as references to other symbols.
@@ -572,23 +641,23 @@ func (insns Instructions) resolveFunctionReferences() error {
 		offset := iter.Offset
 		ins := iter.Ins
 
-		if ins.Reference == "" {
+		if ins.Reference() == "" {
 			continue
 		}
 
 		switch {
 		case ins.IsFunctionReference() && ins.Constant == -1:
-			symOffset, ok := symbolOffsets[ins.Reference]
+			symOffset, ok := symbolOffsets[ins.Reference()]
 			if !ok {
-				return fmt.Errorf("%s at insn %d: symbol %q: %w", ins.OpCode, i, ins.Reference, ErrUnsatisfiedProgramReference)
+				return fmt.Errorf("%s at insn %d: symbol %q: %w", ins.OpCode, i, ins.Reference(), ErrUnsatisfiedProgramReference)
 			}
 
 			ins.Constant = int64(symOffset - offset - 1)
 
 		case ins.OpCode.Class().IsJump() && ins.Offset == -1:
-			symOffset, ok := symbolOffsets[ins.Reference]
+			symOffset, ok := symbolOffsets[ins.Reference()]
 			if !ok {
-				return fmt.Errorf("%s at insn %d: symbol %q: %w", ins.OpCode, i, ins.Reference, ErrUnsatisfiedProgramReference)
+				return fmt.Errorf("%s at insn %d: symbol %q: %w", ins.OpCode, i, ins.Reference(), ErrUnsatisfiedProgramReference)
 			}
 
 			ins.Offset = int16(symOffset - offset - 1)
diff --git a/asm/jump.go b/asm/jump.go
index 199c069..e31e42c 100644
--- a/asm/jump.go
+++ b/asm/jump.go
@@ -63,47 +63,43 @@ func (op JumpOp) Op(source Source) OpCode {
 // Imm compares 64 bit dst to 64 bit value (sign extended), and adjusts PC by offset if the condition is fulfilled.
 func (op JumpOp) Imm(dst Register, value int32, label string) Instruction {
 	return Instruction{
-		OpCode:    op.opCode(JumpClass, ImmSource),
-		Dst:       dst,
-		Offset:    -1,
-		Constant:  int64(value),
-		Reference: label,
-	}
+		OpCode:   op.opCode(JumpClass, ImmSource),
+		Dst:      dst,
+		Offset:   -1,
+		Constant: int64(value),
+	}.WithReference(label)
 }
 
 // Imm32 compares 32 bit dst to 32 bit value, and adjusts PC by offset if the condition is fulfilled.
 // Requires kernel 5.1.
 func (op JumpOp) Imm32(dst Register, value int32, label string) Instruction {
 	return Instruction{
-		OpCode:    op.opCode(Jump32Class, ImmSource),
-		Dst:       dst,
-		Offset:    -1,
-		Constant:  int64(value),
-		Reference: label,
-	}
+		OpCode:   op.opCode(Jump32Class, ImmSource),
+		Dst:      dst,
+		Offset:   -1,
+		Constant: int64(value),
+	}.WithReference(label)
 }
 
 // Reg compares 64 bit dst to 64 bit src, and adjusts PC by offset if the condition is fulfilled.
 func (op JumpOp) Reg(dst, src Register, label string) Instruction {
 	return Instruction{
-		OpCode:    op.opCode(JumpClass, RegSource),
-		Dst:       dst,
-		Src:       src,
-		Offset:    -1,
-		Reference: label,
-	}
+		OpCode: op.opCode(JumpClass, RegSource),
+		Dst:    dst,
+		Src:    src,
+		Offset: -1,
+	}.WithReference(label)
 }
 
 // Reg32 compares 32 bit dst to 32 bit src, and adjusts PC by offset if the condition is fulfilled.
 // Requires kernel 5.1.
 func (op JumpOp) Reg32(dst, src Register, label string) Instruction {
 	return Instruction{
-		OpCode:    op.opCode(Jump32Class, RegSource),
-		Dst:       dst,
-		Src:       src,
-		Offset:    -1,
-		Reference: label,
-	}
+		OpCode: op.opCode(Jump32Class, RegSource),
+		Dst:    dst,
+		Src:    src,
+		Offset: -1,
+	}.WithReference(label)
 }
 
 func (op JumpOp) opCode(class Class, source Source) OpCode {
@@ -118,16 +114,14 @@ func (op JumpOp) opCode(class Class, source Source) OpCode {
 func (op JumpOp) Label(label string) Instruction {
 	if op == Call {
 		return Instruction{
-			OpCode:    OpCode(JumpClass).SetJumpOp(Call),
-			Src:       PseudoCall,
-			Constant:  -1,
-			Reference: label,
-		}
+			OpCode:   OpCode(JumpClass).SetJumpOp(Call),
+			Src:      PseudoCall,
+			Constant: -1,
+		}.WithReference(label)
 	}
 
 	return Instruction{
-		OpCode:    OpCode(JumpClass).SetJumpOp(op),
-		Offset:    -1,
-		Reference: label,
-	}
+		OpCode: OpCode(JumpClass).SetJumpOp(op),
+		Offset: -1,
+	}.WithReference(label)
 }
diff --git a/collection.go b/collection.go
index 9bb0a2d..9a44818 100644
--- a/collection.go
+++ b/collection.go
@@ -28,6 +28,7 @@ type CollectionSpec struct {
 	Programs map[string]*ProgramSpec
 
 	// Types holds type information about Maps and Programs.
+	// Modifications to Types are currently undefined behaviour.
 	Types *btf.Spec
 
 	// ByteOrder specifies whether the ELF was compiled for
@@ -45,7 +46,7 @@ func (cs *CollectionSpec) Copy() *CollectionSpec {
 		Maps:      make(map[string]*MapSpec, len(cs.Maps)),
 		Programs:  make(map[string]*ProgramSpec, len(cs.Programs)),
 		ByteOrder: cs.ByteOrder,
-		Types:     cs.Types.Copy(),
+		Types:     cs.Types,
 	}
 
 	for name, spec := range cs.Maps {
@@ -464,7 +465,7 @@ func (cl *collectionLoader) loadProgram(progName string) (*Program, error) {
 	for i := range progSpec.Instructions {
 		ins := &progSpec.Instructions[i]
 
-		if !ins.IsLoadFromMap() || ins.Reference == "" {
+		if !ins.IsLoadFromMap() || ins.Reference() == "" {
 			continue
 		}
 
@@ -474,17 +475,17 @@ func (cl *collectionLoader) loadProgram(progName string) (*Program, error) {
 			continue
 		}
 
-		m, err := cl.loadMap(ins.Reference)
+		m, err := cl.loadMap(ins.Reference())
 		if err != nil {
 			return nil, fmt.Errorf("program %s: %w", progName, err)
 		}
 
 		fd := m.FD()
 		if fd < 0 {
-			return nil, fmt.Errorf("map %s: %w", ins.Reference, sys.ErrClosedFd)
+			return nil, fmt.Errorf("map %s: %w", ins.Reference(), sys.ErrClosedFd)
 		}
 		if err := ins.RewriteMapPtr(m.FD()); err != nil {
-			return nil, fmt.Errorf("program %s: map %s: %w", progName, ins.Reference, err)
+			return nil, fmt.Errorf("program %s: map %s: %w", progName, ins.Reference(), err)
 		}
 	}
 
diff --git a/elf_reader.go b/elf_reader.go
index 774956c..38cf176 100644
--- a/elf_reader.go
+++ b/elf_reader.go
@@ -349,10 +349,8 @@ func (ec *elfCode) loadFunctions(section *elfSection) (map[string]asm.Instructio
 		insns  asm.Instructions
 	)
 	for {
-		ins := asm.Instruction{
-			// Symbols denote the first instruction of a function body.
-			Symbol: section.symbols[offset].Name,
-		}
+		// Symbols denote the first instruction of a function body.
+		ins := asm.Instruction{}.WithSymbol(section.symbols[offset].Name)
 
 		// Pull one instruction from the instruction stream.
 		n, err := ins.Unmarshal(r, ec.ByteOrder)
@@ -374,7 +372,7 @@ func (ec *elfCode) loadFunctions(section *elfSection) (map[string]asm.Instructio
 
 		// Decoded the first instruction of a function body but insns already
 		// holds a valid instruction stream. Store the result and flush insns.
-		if ins.Symbol != "" && insns.Name() != "" {
+		if ins.Symbol() != "" && insns.Name() != "" {
 			funcs[insns.Name()] = insns
 			insns = nil
 		}
@@ -399,7 +397,7 @@ func (ec *elfCode) loadFunctions(section *elfSection) (map[string]asm.Instructio
 					return nil, fmt.Errorf("offset %d: no jump target found at offset %d", offset, tgt)
 				}
 
-				ins.Reference = sym
+				ins = ins.WithReference(sym)
 				ins.Constant = -1
 			}
 		}
@@ -579,7 +577,7 @@ func (ec *elfCode) relocateInstruction(ins *asm.Instruction, rel elf.Symbol) err
 		return fmt.Errorf("relocation to %q: %w", target.Name, ErrNotSupported)
 	}
 
-	ins.Reference = name
+	*ins = ins.WithReference(name)
 	return nil
 }
 
diff --git a/features/misc.go b/features/misc.go
index 1c2802d..8a81f00 100644
--- a/features/misc.go
+++ b/features/misc.go
@@ -129,7 +129,7 @@ func createMiscProbeAttr(mt miscType) (*sys.ProgLoadAttr, error) {
 	case boundedLoops:
 		insns = asm.Instructions{
 			asm.Mov.Imm(asm.R0, 10),
-			asm.Sub.Imm(asm.R0, 1).Sym("loop"),
+			asm.Sub.Imm(asm.R0, 1).WithSymbol("loop"),
 			asm.JNE.Imm(asm.R0, 0, "loop"),
 			asm.Return(),
 		}
@@ -138,14 +138,14 @@ func createMiscProbeAttr(mt miscType) (*sys.ProgLoadAttr, error) {
 			asm.Mov.Imm(asm.R0, 0),
 			asm.JLT.Imm(asm.R0, 0, "exit"),
 			asm.Mov.Imm(asm.R0, 1),
-			asm.Return().Sym("exit"),
+			asm.Return().WithSymbol("exit"),
 		}
 	case v3ISA:
 		insns = asm.Instructions{
 			asm.Mov.Imm(asm.R0, 0),
 			asm.JLT.Imm32(asm.R0, 0, "exit"),
 			asm.Mov.Imm(asm.R0, 1),
-			asm.Return().Sym("exit"),
+			asm.Return().WithSymbol("exit"),
 		}
 	default:
 		return nil, fmt.Errorf("misc probe %d not implemented", mt)
diff --git a/features/version.go b/features/version.go
new file mode 100644
index 0000000..69e1c39
--- /dev/null
+++ b/features/version.go
@@ -0,0 +1,18 @@
+package features
+
+import "github.com/cilium/ebpf/internal"
+
+// LinuxVersionCode returns the version of the currently running kernel
+// as defined in the LINUX_VERSION_CODE compile-time macro. It is represented
+// in the format described by the KERNEL_VERSION macro from linux/version.h.
+//
+// Do not use the version to make assumptions about the presence of certain
+// kernel features, always prefer feature probes in this package. Some
+// distributions backport or disable eBPF features.
+func LinuxVersionCode() (uint32, error) {
+	v, err := internal.KernelVersion()
+	if err != nil {
+		return 0, err
+	}
+	return v.Kernel(), nil
+}
diff --git a/info.go b/info.go
index cf692c7..c1ad3fa 100644
--- a/info.go
+++ b/info.go
@@ -233,7 +233,7 @@ func (pi *ProgramInfo) Instructions() (asm.Instructions, error) {
 	}
 
 	// Tag the first instruction with the name of the program, if available.
-	insns[0] = insns[0].Sym(pi.Name)
+	insns[0] = insns[0].WithSymbol(pi.Name)
 
 	return insns, nil
 }
diff --git a/linker.go b/linker.go
index 6f2357d..d45c001 100644
--- a/linker.go
+++ b/linker.go
@@ -121,7 +121,7 @@ func fixupAndValidate(insns asm.Instructions) error {
 		ins := iter.Ins
 
 		if ins.IsLoadFromMap() && ins.MapPtr() == -1 {
-			return fmt.Errorf("instruction %d: map %s: %w", iter.Index, ins.Reference, asm.ErrUnsatisfiedMapReference)
+			return fmt.Errorf("instruction %d: map %s: %w", iter.Index, ins.Reference(), asm.ErrUnsatisfiedMapReference)
 		}
 
 		fixupProbeReadKernel(ins)
diff --git a/prog.go b/prog.go
index ccac3f2..61714d4 100644
--- a/prog.go
+++ b/prog.go
@@ -180,7 +180,7 @@ func (spec *ProgramSpec) layout() ([]reference, error) {
 		// Skip non-symbols and symbols that describe the ProgramSpec itself,
 		// which is usually the first instruction in Instructions.
 		// ProgramSpec itself is already included and not present in references.
-		if ins.Symbol == "" || ins.Symbol == name {
+		if ins.Symbol() == "" || ins.Symbol() == name {
 			continue
 		}
 
@@ -188,7 +188,7 @@ func (spec *ProgramSpec) layout() ([]reference, error) {
 		// with valid progs that contain multiple symbols and don't have references
 		// populated. Assume ProgramSpec is used similarly in the wild, so don't
 		// alter this behaviour.
-		ref := spec.references[ins.Symbol]
+		ref := spec.references[ins.Symbol()]
 		if ref != nil {
 			out = append(out, reference{iter.Offset.Bytes(), ref})
 		}


Test patch:
diff --git a/asm/dsl_test.go b/asm/dsl_test.go
index ddbf817..9409fc2 100644
--- a/asm/dsl_test.go
+++ b/asm/dsl_test.go
@@ -25,21 +25,21 @@ func TestDSL(t *testing.T) {
 			OpCode: 0x04, Dst: R1, Constant: 22,
 		}},
 		{"JSGT.Imm", JSGT.Imm(R1, 4, "foo"), Instruction{
-			OpCode: 0x65, Dst: R1, Constant: 4, Offset: -1, Reference: "foo",
-		}},
+			OpCode: 0x65, Dst: R1, Constant: 4, Offset: -1,
+		}.WithReference("foo")},
 		{"JSGT.Imm32", JSGT.Imm32(R1, -2, "foo"), Instruction{
-			OpCode: 0x66, Dst: R1, Constant: -2, Offset: -1, Reference: "foo",
-		}},
+			OpCode: 0x66, Dst: R1, Constant: -2, Offset: -1,
+		}.WithReference("foo")},
 		{"JSLT.Reg", JSLT.Reg(R1, R2, "foo"), Instruction{
-			OpCode: 0xcd, Dst: R1, Src: R2, Offset: -1, Reference: "foo",
-		}},
+			OpCode: 0xcd, Dst: R1, Src: R2, Offset: -1,
+		}.WithReference("foo")},
 		{"JSLT.Reg32", JSLT.Reg32(R1, R3, "foo"), Instruction{
-			OpCode: 0xce, Dst: R1, Src: R3, Offset: -1, Reference: "foo",
-		}},
+			OpCode: 0xce, Dst: R1, Src: R3, Offset: -1,
+		}.WithReference("foo")},
 	}
 
 	for _, tc := range testcases {
-		if tc.have != tc.want {
+		if !tc.have.equal(tc.want) {
 			t.Errorf("%s: have %v, want %v", tc.name, tc.have, tc.want)
 		}
 	}
diff --git a/asm/instruction_test.go b/asm/instruction_test.go
index b18c0f3..8d2bc96 100644
--- a/asm/instruction_test.go
+++ b/asm/instruction_test.go
@@ -151,10 +151,9 @@ func TestInstructionLoadMapValue(t *testing.T) {
 
 func TestInstructionsRewriteMapPtr(t *testing.T) {
 	insns := Instructions{
-		LoadMapPtr(R1, 0),
+		LoadMapPtr(R1, 0).WithReference("good"),
 		Return(),
 	}
-	insns[0].Reference = "good"
 
 	if err := insns.RewriteMapPtr("good", 1); err != nil {
 		t.Fatal(err)
@@ -181,7 +180,7 @@ func TestInstructionsRewriteMapPtr(t *testing.T) {
 // program is stringified.
 func ExampleInstructions_Format() {
 	insns := Instructions{
-		FnMapLookupElem.Call().Sym("my_func"),
+		FnMapLookupElem.Call().WithSymbol("my_func"),
 		LoadImm(R0, 42, DWord),
 		Return(),
 	}
diff --git a/collection_test.go b/collection_test.go
index 714e74a..00f3a91 100644
--- a/collection_test.go
+++ b/collection_test.go
@@ -6,6 +6,7 @@ import (
 	"testing"
 
 	"github.com/cilium/ebpf/asm"
+	"github.com/cilium/ebpf/internal"
 	"github.com/cilium/ebpf/internal/btf"
 	"github.com/cilium/ebpf/internal/testutils"
 )
@@ -24,7 +25,7 @@ func TestCollectionSpecNotModified(t *testing.T) {
 			"test": {
 				Type: SocketFilter,
 				Instructions: asm.Instructions{
-					asm.LoadImm(asm.R1, 0, asm.DWord),
+					asm.LoadImm(asm.R1, 0, asm.DWord).WithReference("my-map"),
 					asm.LoadImm(asm.R0, 0, asm.DWord),
 					asm.Return(),
 				},
@@ -33,8 +34,6 @@ func TestCollectionSpecNotModified(t *testing.T) {
 		},
 	}
 
-	cs.Programs["test"].Instructions[0].Reference = "my-map"
-
 	coll, err := NewCollection(&cs)
 	if err != nil {
 		t.Fatal(err)
@@ -83,15 +82,39 @@ func TestCollectionSpecCopy(t *testing.T) {
 		t.Error("Copy returned same Programs")
 	}
 
-	if cpy.Types == cs.Types {
-		t.Error("Copy returned same Types")
+	if cpy.Types != cs.Types {
+		t.Error("Copy returned different Types")
+	}
+}
+
+func TestCollectionSpecLoadCopy(t *testing.T) {
+	file := fmt.Sprintf("testdata/loader-%s.elf", internal.ClangEndian)
+	spec, err := LoadCollectionSpec(file)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	spec2 := spec.Copy()
+
+	var objs struct {
+		Prog *Program `ebpf:"xdp_prog"`
+	}
+
+	err = spec.LoadAndAssign(&objs, nil)
+	testutils.SkipIfNotSupported(t, err)
+	if err != nil {
+		t.Fatal("Loading original spec:", err)
+	}
+
+	if err := spec2.LoadAndAssign(&objs, nil); err != nil {
+		t.Fatal("Loading copied spec:", err)
 	}
 }
 
 func TestCollectionSpecRewriteMaps(t *testing.T) {
 	insns := asm.Instructions{
 		// R1 map
-		asm.LoadMapPtr(asm.R1, 0),
+		asm.LoadMapPtr(asm.R1, 0).WithReference("test-map"),
 		// R2 key
 		asm.Mov.Reg(asm.R2, asm.R10),
 		asm.Add.Imm(asm.R2, -4),
@@ -100,9 +123,8 @@ func TestCollectionSpecRewriteMaps(t *testing.T) {
 		asm.FnMapLookupElem.Call(),
 		asm.JEq.Imm(asm.R0, 0, "ret"),
 		asm.LoadMem(asm.R0, asm.R0, 0, asm.Word),
-		asm.Return().Sym("ret"),
+		asm.Return().WithSymbol("ret"),
 	}
-	insns[0].Reference = "test-map"
 
 	cs := &CollectionSpec{
 		Maps: map[string]*MapSpec{
diff --git a/elf_reader_test.go b/elf_reader_test.go
index aea7e58..3ec5faa 100644
--- a/elf_reader_test.go
+++ b/elf_reader_test.go
@@ -260,7 +260,7 @@ func TestInlineASMConstant(t *testing.T) {
 	}
 
 	spec := coll.Programs["asm_relocation"]
-	if spec.Instructions[0].Reference != "MY_CONST" {
+	if spec.Instructions[0].Reference() != "MY_CONST" {
 		t.Fatal("First instruction is not a reference to MY_CONST")
 	}
 
diff --git a/example_sock_extract_dist_test.go b/example_sock_extract_dist_test.go
index 25dccf6..cbd1254 100644
--- a/example_sock_extract_dist_test.go
+++ b/example_sock_extract_dist_test.go
@@ -107,11 +107,11 @@ func newDistanceFilter() (*ebpf.Program, *ebpf.Map, error) {
 
 		// 7th byte in IPv6 is Hop count
 		// LDABS requires ctx in R6
-		asm.Mov.Reg(asm.R6, asm.R1).Sym("ipv6"),
+		asm.Mov.Reg(asm.R6, asm.R1).WithSymbol("ipv6"),
 		asm.LoadAbs(-0x100000+7, asm.Byte),
 
 		// stash the load result into FP[-4]
-		asm.StoreMem(asm.RFP, -4, asm.R0, asm.Word).Sym("store-ttl"),
+		asm.StoreMem(asm.RFP, -4, asm.R0, asm.Word).WithSymbol("store-ttl"),
 		// stash the &FP[-4] into r2
 		asm.Mov.Reg(asm.R2, asm.RFP),
 		asm.Add.Imm(asm.R2, -4),
@@ -128,7 +128,7 @@ func newDistanceFilter() (*ebpf.Program, *ebpf.Map, error) {
 
 		// MapUpdate
 		// r1 has map ptr
-		asm.LoadMapPtr(asm.R1, ttls.FD()).Sym("update-map"),
+		asm.LoadMapPtr(asm.R1, ttls.FD()).WithSymbol("update-map"),
 		// r2 has key -> &FP[-4]
 		asm.Mov.Reg(asm.R2, asm.RFP),
 		asm.Add.Imm(asm.R2, -4),
@@ -141,7 +141,7 @@ func newDistanceFilter() (*ebpf.Program, *ebpf.Map, error) {
 		asm.FnMapUpdateElem.Call(),
 
 		// set exit code to -1, don't trunc packet
-		asm.Mov.Imm(asm.R0, -1).Sym("exit"),
+		asm.Mov.Imm(asm.R0, -1).WithSymbol("exit"),
 		asm.Return(),
 	}
 
diff --git a/linker_test.go b/linker_test.go
index 34e028d..38327aa 100644
--- a/linker_test.go
+++ b/linker_test.go
@@ -24,13 +24,13 @@ func TestFindReferences(t *testing.T) {
 		},
 		"my_other_func": {
 			Instructions: asm.Instructions{
-				asm.LoadImm(asm.R0, 1337, asm.DWord).Sym("my_other_func"),
+				asm.LoadImm(asm.R0, 1337, asm.DWord).WithSymbol("my_other_func"),
 				asm.Return(),
 			},
 		},
 		"my_func": {
 			Instructions: asm.Instructions{
-				asm.Call.Label("my_other_func").Sym("my_func"),
+				asm.Call.Label("my_other_func").WithSymbol("my_func"),
 				asm.Return(),
 			},
 		},
@@ -80,7 +80,7 @@ func TestForwardFunctionDeclaration(t *testing.T) {
 
 		// Append the implementation of fwd().
 		spec.Instructions = append(spec.Instructions,
-			asm.Mov.Imm32(asm.R0, 23).Sym("fwd"),
+			asm.Mov.Imm32(asm.R0, 23).WithSymbol("fwd"),
 			asm.Return(),
 		)
 
diff --git a/prog_test.go b/prog_test.go
index f48b3b4..78f1e9a 100644
--- a/prog_test.go
+++ b/prog_test.go
@@ -49,7 +49,7 @@ func TestProgramRun(t *testing.T) {
 	}
 	ins = append(ins,
 		// return 42
-		asm.LoadImm(asm.R0, 42, asm.DWord).Sym("out"),
+		asm.LoadImm(asm.R0, 42, asm.DWord).WithSymbol("out"),
 		asm.Return(),
 	)
 
@@ -695,7 +695,7 @@ func TestProgramInstructions(t *testing.T) {
 		Type: SocketFilter,
 		Name: name,
 		Instructions: asm.Instructions{
-			asm.LoadImm(asm.R0, -1, asm.DWord).Sym(name),
+			asm.LoadImm(asm.R0, -1, asm.DWord).WithSymbol(name),
 			asm.LoadMapPtr(asm.R1, arr.FD()),
 			asm.Mov.Imm32(asm.R0, 0),
 			asm.Return(),
diff --git a/ringbuf/reader_test.go b/ringbuf/reader_test.go
index 9da5622..71278fb 100644
--- a/ringbuf/reader_test.go
+++ b/ringbuf/reader_test.go
@@ -134,7 +134,7 @@ func outputSamplesProg(flags int32, sampleSizes ...int) (*ebpf.Program, *ebpf.Ma
 	}
 
 	insns = append(insns,
-		asm.Mov.Imm(asm.R0, int32(0)).Sym("exit"),
+		asm.Mov.Imm(asm.R0, int32(0)).WithSymbol("exit"),
 		asm.Return(),
 	)