Repository: cilium/ebpf
Pull request #606 (2022-03-28 05:30:42)

Use instruction metadata to simplify linking and CO-RE relocation
btf: move CORERelocate to core.go

btf: add constructors for various *Info

    Pull constructors for FuncInfo, LineInfo and CORERelocation out of
    splitExtInfos. This makes the function smaller and makes removing it
    later easier.

    Instead of taking a *Spec in the constructors, take a []Type and
    stringTable directly. This makes splitting ExtInfos out of *Spec
    easier later on.

btf: track bpfCORERelo.InsnOff in instructions instead of bytes

    This makes bpfCORERelo consistent with bpfFuncInfo and bpfLineInfo. It seems like
    there is a latent bug here as well, since we used to subtract bpfFuncInfo.InsnOff
    from bpfCORERelo.InsnOff, but for some reason the tests pass.

btf: store ExtInfos in instruction metadata

    Use per-instruction metadata to store FuncInfo, LineInfo and CO-RE relocations.
    As a result, simply concatenating two Instruction slices preserves the ext_info
    without extra code to keep track of offsets. The simplest way to achieve is to
    assign ext_infos per section, before we split into individual functions. This
    also avoids having to split ext_infos in the first place.

    Storing ext_infos in metadata in turn allows / requires removing code that relies
    on stable offsets, the most notable being applying COREFixups. Instead of tracking
    which offset a fixup should be applied to we change coreRelocate to instead return
    results in the same order as CORERelocations are passed. In the caller we remember
    which instruction originated the relocation and apply the fixup directly instead of
    iterating all instructions once more.

    Instead of storing ExtInfos in Spec we split it off and keep a reference to the
    necessary types and string table around. This makes more sense since every BTF ELF
    has a Spec, but ExtInfos are optional. It turns out that most code except the ELF
    loader doesn't care about ExtInfos in the first place, so we might be able to
    add an API that doesn't parse ExtInfos in the future.

    Finally we can remove btf.Program since we don't need an intermediate type to hold
    metadata anymore.

btf: remove Map

    btf.Program is gone, so let's get rid of Map as well.

lmb: Split off the `WithSource` + sparse metadata changes into #609 
lmb: I'll move `program: remove references from ProgramSpec` to a follow up PR once #609 is merged.
lmb: This is now in a state where it can be reviewed! I've updated the PR description with the latest commit messages, please take a look.

I'm a little bit unhappy with `ExtInfos.Assign`, it seems like it belongs into the ELF loader. I'd prefer to convert bpfFuncInfo to FuncInfo, etc. at BTF load time and then expose something like the following:

```
type ExtInfos struct{
    []FuncInfo
    []LineInfo
    []CORERelocation
}

// Per-section ExtInfos
func LoadSpecFromReader(...) (*Spec ,map[string]ExtInfos, error)
```

The snag is that `FuncInfo`, etc. don't contain the InsnOff, since having an offset doesn't make sense when they are attached to an instruction. I can see two ways to fix this:

```
type ExtInfo struct {
    map[uint64]FuncInfo
    ...
}
```

By abusing the map we can avoid introducing a new type, but none of our code really wants to use a map. An ordered slice is much better. For that we'd need:

```
type FuncInfoWithOffset struct {
    FuncInfo
    Offset uint32
}

type ExtInfo struct {
    []FuncInfoWithOffset
    ...
}
```

This means a lot of additional types, which I don't particularly like. Maybe you have another idea?
lmb: As a follow up to https://github.com/cilium/ebpf/pull/606#issuecomment-1104201245, what if ExtInfos becomes this:

```
type ExtInfos struct{
    Relocations []*CORERelocations
    RelocationOffsets []uint32
    ...
}

func Foo(...) (map[string]*ExtInfos, error)
```
lmb: Please take a look at https://github.com/cilium/ebpf/pull/606/commits/64dbe60367826214020d9600f599d6038aadf3c0:

- I've gotten rid of ApplyRelocations and ExtInfos.Assign and MarshalExtInfos and moved them into linker.go instead. This is much closer to what we had before. This is nice because instruction metadata is now private to the ebpf package.
- FuncInfo, LineInfo, CORERelocationInfo are now used to expose "metadata + offset" via `ExtInfos`. The real metadata is called `Func` (as you suggested), `Line` and `CORERelocation`.
- I've split the constuctor for Spec in two, with one returning ExtInfos along with the Spec. The nice side effect is that we don't decode useless ExtInfo when loading BTF via `handleCache`.

Patch:
diff --git a/cmd/bpf2go/output.go b/cmd/bpf2go/output.go
index 924946c..8b9a6a9 100644
--- a/cmd/bpf2go/output.go
+++ b/cmd/bpf2go/output.go
@@ -343,16 +343,12 @@ func collectCTypes(types *btf.Spec, names []string) ([]btf.Type, error) {
 func collectMapTypes(maps map[string]*ebpf.MapSpec) []btf.Type {
 	var result []btf.Type
 	for _, m := range maps {
-		if m.BTF == nil {
-			continue
-		}
-
-		if m.BTF.Key != nil && m.BTF.Key.TypeName() != "" {
-			result = append(result, m.BTF.Key)
+		if m.Key != nil && m.Key.TypeName() != "" {
+			result = append(result, m.Key)
 		}
 
-		if m.BTF.Value != nil && m.BTF.Value.TypeName() != "" {
-			result = append(result, m.BTF.Value)
+		if m.Value != nil && m.Value.TypeName() != "" {
+			result = append(result, m.Value)
 		}
 	}
 	return result
diff --git a/collection.go b/collection.go
index eed743f..0e7a83d 100644
--- a/collection.go
+++ b/collection.go
@@ -145,7 +145,7 @@ func (cs *CollectionSpec) RewriteConstants(consts map[string]interface{}) error
 	buf := make([]byte, len(value))
 	copy(buf, value)
 
-	err := patchValue(buf, rodata.BTF.Value, consts)
+	err := patchValue(buf, rodata.Value, consts)
 	if err != nil {
 		return err
 	}
@@ -454,7 +454,7 @@ func (cl *collectionLoader) loadMap(mapName string) (*Map, error) {
 		return nil, fmt.Errorf("missing map %s", mapName)
 	}
 
-	if mapSpec.BTF != nil && cl.coll.Types != mapSpec.BTF.Spec {
+	if mapSpec.BTF != nil && cl.coll.Types != mapSpec.BTF {
 		return nil, fmt.Errorf("map %s: BTF doesn't match collection", mapName)
 	}
 
@@ -494,7 +494,7 @@ func (cl *collectionLoader) loadProgram(progName string) (*Program, error) {
 		return nil, fmt.Errorf("cannot load program %s: program type is unspecified", progName)
 	}
 
-	if progSpec.BTF != nil && cl.coll.Types != progSpec.BTF.Spec() {
+	if progSpec.BTF != nil && cl.coll.Types != progSpec.BTF {
 		return nil, fmt.Errorf("program %s: BTF doesn't match collection", progName)
 	}
 
diff --git a/elf_reader.go b/elf_reader.go
index bc64e5e..8c0a0a1 100644
--- a/elf_reader.go
+++ b/elf_reader.go
@@ -26,6 +26,7 @@ type elfCode struct {
 	license  string
 	version  uint32
 	btf      *btf.Spec
+	extInfo  *btf.ExtInfos
 }
 
 // LoadCollectionSpec parses an ELF file into a CollectionSpec.
@@ -94,7 +95,7 @@ func LoadCollectionSpecFromReader(rd io.ReaderAt) (*CollectionSpec, error) {
 		return nil, fmt.Errorf("load version: %w", err)
 	}
 
-	btfSpec, err := btf.LoadSpecFromReader(rd)
+	btfSpec, btfExtInfo, err := btf.LoadSpecAndExtInfosFromReader(rd)
 	if err != nil && !errors.Is(err, btf.ErrNotFound) {
 		return nil, fmt.Errorf("load BTF: %w", err)
 	}
@@ -105,6 +106,7 @@ func LoadCollectionSpecFromReader(rd io.ReaderAt) (*CollectionSpec, error) {
 		license:     license,
 		version:     version,
 		btf:         btfSpec,
+		extInfo:     btfExtInfo,
 	}
 
 	symbols, err := f.Symbols()
@@ -309,13 +311,7 @@ func (ec *elfCode) loadProgramSections() (map[string]*ProgramSpec, error) {
 				KernelVersion: ec.version,
 				Instructions:  insns,
 				ByteOrder:     ec.ByteOrder,
-			}
-
-			if ec.btf != nil {
-				spec.BTF, err = ec.btf.Program(name)
-				if err != nil && !errors.Is(err, btf.ErrNoExtendedInfo) {
-					return nil, fmt.Errorf("program %s: %w", name, err)
-				}
+				BTF:           ec.btf,
 			}
 
 			// Function names must be unique within a single ELF blob.
@@ -383,6 +379,10 @@ func (ec *elfCode) loadFunctions(section *elfSection) (map[string]asm.Instructio
 		}
 	}
 
+	if ec.extInfo != nil {
+		ec.extInfo.Assign(insns, section.Name)
+	}
+
 	return splitSymbols(insns)
 }
 
@@ -904,7 +904,9 @@ func mapSpecFromBTF(es *elfSection, vs *btf.VarSecinfo, def *btf.Struct, spec *b
 		ValueSize:  valueSize,
 		MaxEntries: maxEntries,
 		Flags:      flags,
-		BTF:        &btf.Map{Spec: spec, Key: key, Value: value},
+		Key:        key,
+		Value:      value,
+		BTF:        spec,
 		Pinning:    pinType,
 		InnerMap:   innerMapSpec,
 		Contents:   contents,
@@ -1039,7 +1041,9 @@ func (ec *elfCode) loadDataSections(maps map[string]*MapSpec) error {
 			ValueSize:  uint32(len(data)),
 			MaxEntries: 1,
 			Contents:   []MapKV{{uint32(0), data}},
-			BTF:        &btf.Map{Spec: ec.btf, Key: &btf.Void{}, Value: datasec},
+			Key:        &btf.Void{},
+			Value:      datasec,
+			BTF:        ec.btf,
 		}
 
 		switch sec.Name {
diff --git a/internal/btf/btf.go b/internal/btf/btf.go
index 47eb2d9..ccd4f9e 100644
--- a/internal/btf/btf.go
+++ b/internal/btf/btf.go
@@ -11,7 +11,6 @@ import (
 	"math"
 	"os"
 	"reflect"
-	"sort"
 	"sync"
 
 	"github.com/cilium/ebpf/internal"
@@ -38,17 +37,12 @@ type Spec struct {
 	strings  *stringTable
 
 	// Inflated Types.
-	types []Type
+	types types
 
 	// Types indexed by essential name.
 	// Includes all struct flavors and types with the same name.
 	namedTypes map[essentialName][]Type
 
-	// Data from .BTF.ext. indexed by function name.
-	funcInfos map[string]FuncInfo
-	lineInfos map[string]LineInfos
-	coreRelos map[string]CORERelos
-
 	byteOrder binary.ByteOrder
 }
 
@@ -78,14 +72,16 @@ func (h *btfHeader) stringStart() int64 {
 
 // LoadSpecFromReader reads from an ELF or a raw BTF blob.
 //
-// Returns ErrNotFound if reading from an ELF which contains no BTF.
+// Returns ErrNotFound if reading from an ELF which contains no BTF. ExtInfos
+// may be nil.
 func LoadSpecFromReader(rd io.ReaderAt) (*Spec, error) {
 	file, err := internal.NewSafeELFFile(rd)
 	if err != nil {
 		if bo := guessRawBTFByteOrder(rd); bo != nil {
 			// Try to parse a naked BTF blob. This will return an error if
 			// we encounter a Datasec, since we can't fix it up.
-			return loadRawSpec(io.NewSectionReader(rd, 0, math.MaxInt64), bo, nil, nil)
+			spec, err := loadRawSpec(io.NewSectionReader(rd, 0, math.MaxInt64), bo, nil, nil)
+			return spec, err
 		}
 
 		return nil, err
@@ -94,6 +90,29 @@ func LoadSpecFromReader(rd io.ReaderAt) (*Spec, error) {
 	return loadSpecFromELF(file)
 }
 
+// LoadSpecAndExtInfosFromReader reads from an ELF.
+//
+// ExtInfos may be nil if the ELF doesn't contain section metadta.
+// Returns ErrNotFound if the ELF contains no BTF.
+func LoadSpecAndExtInfosFromReader(rd io.ReaderAt) (*Spec, *ExtInfos, error) {
+	file, err := internal.NewSafeELFFile(rd)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	spec, err := loadSpecFromELF(file)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	extInfos, err := loadExtInfosFromELF(file, spec.types, spec.strings)
+	if err != nil && !errors.Is(err, ErrNotFound) {
+		return nil, nil, err
+	}
+
+	return spec, extInfos, nil
+}
+
 // variableOffsets extracts all symbols offsets from an ELF and indexes them by
 // section and variable name.
 //
@@ -132,17 +151,14 @@ func variableOffsets(file *internal.SafeELFFile) (map[variable]uint32, error) {
 
 func loadSpecFromELF(file *internal.SafeELFFile) (*Spec, error) {
 	var (
-		btfSection    *elf.Section
-		btfExtSection *elf.Section
-		sectionSizes  = make(map[string]uint32)
+		btfSection   *elf.Section
+		sectionSizes = make(map[string]uint32)
 	)
 
 	for _, sec := range file.Sections {
 		switch sec.Name {
 		case ".BTF":
 			btfSection = sec
-		case ".BTF.ext":
-			btfExtSection = sec
 		default:
 			if sec.Type != elf.SHT_PROGBITS && sec.Type != elf.SHT_NOBITS {
 				break
@@ -169,137 +185,7 @@ func loadSpecFromELF(file *internal.SafeELFFile) (*Spec, error) {
 		return nil, fmt.Errorf("compressed BTF is not supported")
 	}
 
-	spec, err := loadRawSpec(btfSection.ReaderAt, file.ByteOrder, sectionSizes, vars)
-	if err != nil {
-		return nil, err
-	}
-
-	if btfExtSection == nil {
-		return spec, nil
-	}
-
-	if btfExtSection.ReaderAt == nil {
-		return nil, fmt.Errorf("compressed ext_info is not supported")
-	}
-
-	extInfo, err := loadExtInfos(btfExtSection, file.ByteOrder, spec.strings)
-	if err != nil {
-		return nil, fmt.Errorf("can't parse ext info: %w", err)
-	}
-
-	if err := spec.splitExtInfos(extInfo); err != nil {
-		return nil, fmt.Errorf("linking funcInfos and lineInfos: %w", err)
-	}
-
-	return spec, nil
-}
-
-// splitExtInfos takes FuncInfos, LineInfos and CORERelos indexed by section and
-// transforms them to be indexed by function. Retrieves function names from
-// the BTF spec.
-func (spec *Spec) splitExtInfos(info *extInfo) error {
-	ofi := make(map[string]FuncInfo)
-	oli := make(map[string]LineInfos)
-	ocr := make(map[string]CORERelos)
-
-	for secName, secFuncInfos := range info.funcInfos {
-		// Collect functions from each section and organize them by name.
-		var funcs []*Func
-		for _, fi := range secFuncInfos {
-			typ, err := spec.TypeByID(fi.TypeID)
-			if err != nil {
-				return err
-			}
-
-			fn, ok := typ.(*Func)
-			if !ok {
-				return fmt.Errorf("type ID %d is a %T, but expected a Func", fi.TypeID, typ)
-			}
-
-			// C doesn't have anonymous functions, but check just in case.
-			if fn.Name == "" {
-				return fmt.Errorf("func with type ID %d doesn't have a name", fi.TypeID)
-			}
-
-			ofi[fn.Name] = FuncInfo{fn}
-			funcs = append(funcs, fn)
-		}
-
-		sort.Slice(secFuncInfos, func(i, j int) bool {
-			return secFuncInfos[i].InsnOff < secFuncInfos[j].InsnOff
-		})
-
-		// Consider an ELF section that contains 3 functions (a, b, c)
-		// at offsets 0, 10 and 15 respectively. Offset 5 will return function a,
-		// offset 12 will return b, offset >= 15 will return c, etc.
-		funcForInstruction := func(offset uint32) (fn *Func, fnOffset uint32) {
-			for i, fi := range secFuncInfos {
-				if fi.InsnOff > offset {
-					break
-				}
-				fn = funcs[i]
-				fnOffset = fi.InsnOff
-			}
-			return fn, fnOffset
-		}
-
-		// Attribute LineInfo records to their respective functions, if any.
-		if lines := info.lineInfos[secName]; lines != nil {
-			for _, li := range lines {
-				fn, fnOffset := funcForInstruction(li.InsnOff)
-				if fn == nil {
-					return fmt.Errorf("section %s: error looking up FuncInfo for LineInfo %v", secName, li)
-				}
-
-				line, err := spec.strings.Lookup(li.LineOff)
-				if err != nil {
-					return fmt.Errorf("lookup of line: %w", err)
-				}
-
-				fileName, err := spec.strings.Lookup(li.FileNameOff)
-				if err != nil {
-					return fmt.Errorf("lookup of filename: %w", err)
-				}
-
-				lineNumber := li.LineCol >> bpfLineShift
-				lineColumn := li.LineCol & bpfColumnMax
-
-				oli[fn.Name] = append(oli[fn.Name], LineInfo{
-					fileName,
-					line,
-					lineNumber,
-					lineColumn,
-					// Offsets are ELF section-scoped, make them function-scoped by
-					// subtracting the function's start offset.
-					li.InsnOff - fnOffset,
-					li.FileNameOff,
-					li.LineOff,
-				})
-			}
-		}
-
-		// Attribute CO-RE relocations to their respective functions, if any.
-		if relos := info.relos[secName]; relos != nil {
-			for _, r := range relos {
-				fn, fnOffset := funcForInstruction(r.insnOff)
-				if fn == nil {
-					return fmt.Errorf("section %s: error looking up FuncInfo for CO-RE relocation %v", secName, r)
-				}
-
-				// Offsets are ELF section-scoped, make them function-scoped by
-				// subtracting the function's start offset.
-				r.insnOff -= fnOffset
-
-				ocr[fn.Name] = append(ocr[fn.Name], r)
-			}
-		}
-	}
-
-	spec.funcInfos = ofi
-	spec.lineInfos = oli
-	spec.coreRelos = ocr
-
-	return nil
+	return loadRawSpec(btfSection.ReaderAt, file.ByteOrder, sectionSizes, vars)
 }
 
 func loadRawSpec(btf io.ReaderAt, bo binary.ByteOrder, sectionSizes map[string]uint32, variableOffsets map[variable]uint32) (*Spec, error) {
@@ -541,9 +427,6 @@ func (s *Spec) Copy() *Spec {
 		s.strings,
 		types,
 		namedTypes,
-		s.funcInfos,
-		s.lineInfos,
-		s.coreRelos,
 		s.byteOrder,
 	}
 }
@@ -616,35 +499,12 @@ func (sw sliceWriter) Write(p []byte) (int, error) {
 	return copy(sw, p), nil
 }
 
-// Program finds the BTF for a specific function.
-//
-// Returns an error which may wrap ErrNoExtendedInfo if the Spec doesn't
-// contain extended BTF info.
-func (s *Spec) Program(name string) (*Program, error) {
-	if s.funcInfos == nil && s.lineInfos == nil && s.coreRelos == nil {
-		return nil, fmt.Errorf("BTF for function %s: %w", name, ErrNoExtendedInfo)
-	}
-
-	funcInfo, funcOK := s.funcInfos[name]
-	lineInfos, lineOK := s.lineInfos[name]
-	relos, coreOK := s.coreRelos[name]
-
-	if !funcOK && !lineOK && !coreOK {
-		return nil, fmt.Errorf("no extended BTF info for function %s", name)
-	}
-
-	return &Program{s, funcInfo, lineInfos, relos}, nil
-}
-
 // TypeByID returns the BTF Type with the given type ID.
 //
 // Returns an error wrapping ErrNotFound if a Type with the given ID
 // does not exist in the Spec.
 func (s *Spec) TypeByID(id TypeID) (Type, error) {
-	if int(id) > len(s.types) {
-		return nil, fmt.Errorf("type ID %d: %w", id, ErrNotFound)
-	}
-	return s.types[id], nil
+	return s.types.ByID(id)
 }
 
 // AnyTypesByName returns a list of BTF Types with the given name.
@@ -829,44 +689,6 @@ func (h *Handle) FD() int {
 	return h.fd.Int()
 }
 
-// Map is the BTF for a map.
-type Map struct {
-	Spec       *Spec
-	Key, Value Type
-}
-
-// Program is the BTF information for a stream of instructions.
-type Program struct {
-	spec      *Spec
-	FuncInfo  FuncInfo
-	LineInfos LineInfos
-	CORERelos CORERelos
-}
-
-// Spec returns the BTF spec of this program.
-func (p *Program) Spec() *Spec {
-	return p.spec
-}
-
-// CORERelocate returns the changes required to adjust the program to the target.
-//
-// Passing a nil target will relocate against the running kernel.
-func CORERelocate(local, target *Spec, relos CORERelos) (COREFixups, error) {
-	if len(relos) == 0 {
-		return nil, nil
-	}
-
-	if target == nil {
-		var err error
-		target, err = LoadKernelSpec()
-		if err != nil {
-			return nil, err
-		}
-	}
-
-	return coreRelocate(local, target, relos)
-}
-
 func marshalBTF(types interface{}, strings []byte, bo binary.ByteOrder) []byte {
 	const minHeaderLength = 24
 
diff --git a/internal/btf/core.go b/internal/btf/core.go
index 417f1e8..c0de823 100644
--- a/internal/btf/core.go
+++ b/internal/btf/core.go
@@ -6,7 +6,6 @@ import (
 	"fmt"
 	"math"
 	"reflect"
-	"sort"
 	"strconv"
 	"strings"
 
@@ -42,7 +41,10 @@ func (f COREFixup) String() string {
 
 func (f COREFixup) apply(ins *asm.Instruction) error {
 	if f.poison {
-		return errors.New("can't poison individual instruction")
+		const badRelo = 0xbad2310
+
+		*ins = asm.BuiltinFunc(badRelo).Call()
+		return nil
 	}
 
 	switch class := ins.OpCode.Class(); class {
@@ -101,49 +103,6 @@ func (f COREFixup) isNonExistant() bool {
 	return f.kind.checksForExistence() && f.target == 0
 }
 
-type COREFixups map[uint64]COREFixup
-
-// Apply returns a copy of insns with CO-RE relocations applied.
-func (fs COREFixups) Apply(insns asm.Instructions) (asm.Instructions, error) {
-	if len(fs) == 0 {
-		cpy := make(asm.Instructions, len(insns))
-		copy(cpy, insns)
-		return insns, nil
-	}
-
-	cpy := make(asm.Instructions, 0, len(insns))
-	iter := insns.Iterate()
-	for iter.Next() {
-		fixup, ok := fs[iter.Offset.Bytes()]
-		if !ok {
-			cpy = append(cpy, *iter.Ins)
-			continue
-		}
-
-		ins := *iter.Ins
-		if fixup.poison {
-			const badRelo = asm.BuiltinFunc(0xbad2310)
-
-			cpy = append(cpy, badRelo.Call())
-			if ins.OpCode.IsDWordLoad() {
-				// 64 bit constant loads occupy two raw bpf instructions, so
-				// we need to add another instruction as padding.
-				cpy = append(cpy, badRelo.Call())
-			}
-
-			continue
-		}
-
-		if err := fixup.apply(&ins); err != nil {
-			return nil, fmt.Errorf("instruction %d, offset %d: %s: %w", iter.Index, iter.Offset.Bytes(), fixup.kind, err)
-		}
-
-		cpy = append(cpy, ins)
-	}
-
-	return cpy, nil
-}
-
 // coreKind is the type of CO-RE relocation as specified in BPF source code.
 type coreKind uint32
 
@@ -197,15 +156,67 @@ func (k coreKind) String() string {
 	}
 }
 
-func coreRelocate(local, target *Spec, relos CORERelos) (COREFixups, error) {
+// CORERelocate calculates the difference in types between local
+// and target and adjusts insns to match.
+//
+// Passing a nil target will relocate against the running kernel. insns are
+// modified in place.
+func CORERelocate(insns asm.Instructions, local, target *Spec) error {
+	var relos []*CORERelocation
+	var reloInsns []*asm.Instruction
+	iter := insns.Iterate()
+	for iter.Next() {
+		if relo, ok := iter.Ins.Metadata.Get(coreRelocationMeta{}).(*CORERelocation); ok {
+			relos = append(relos, relo)
+			reloInsns = append(reloInsns, iter.Ins)
+		}
+	}
+
+	if len(relos) == 0 {
+		return nil
+	}
+
+	if target == nil {
+		var err error
+		target, err = LoadKernelSpec()
+		if err != nil {
+			return err
+		}
+	}
+
+	fixups, err := coreRelocate(local, target, relos)
+	if err != nil {
+		return err
+	}
+
+	for i, fixup := range fixups {
+		if err := fixup.apply(reloInsns[i]); err != nil {
+			return fmt.Errorf("apply fixup %s: %w", fixup, err)
+		}
+	}
+
+	return nil
+}
+
+// coreRelocate calculates the difference in types between local and target.
+//
+// Fixups are returned in the order of relos, e.g. fixup[i] is the solution
+// for relos[i].
+func coreRelocate(local, target *Spec, relos []*CORERelocation) ([]COREFixup, error) {
 	if local.byteOrder != target.byteOrder {
 		return nil, fmt.Errorf("can't relocate %s against %s", local.byteOrder, target.byteOrder)
 	}
 
-	var ids []TypeID
-	relosByID := make(map[TypeID]CORERelos)
-	result := make(COREFixups, len(relos))
-	for _, relo := range relos {
+	type reloGroup struct {
+		relos []*CORERelocation
+		// Position of each relocation in relos.
+		indices []int
+	}
+
+	// Split relocations into per Type lists.
+	relosByType := make(map[Type]*reloGroup)
+	result := make([]COREFixup, len(relos))
+	for i, relo := range relos {
 		if relo.kind == reloTypeIDLocal {
 			// Filtering out reloTypeIDLocal here makes our lives a lot easier
 			// down the line, since it doesn't have a target at all.
@@ -213,46 +224,37 @@ func coreRelocate(local, target *Spec, relos CORERelos) (COREFixups, error) {
 				return nil, fmt.Errorf("%s: unexpected accessor %v", relo.kind, relo.accessor)
 			}
 
-			result[uint64(relo.insnOff)] = COREFixup{
+			result[i] = COREFixup{
 				kind:   relo.kind,
-				local:  uint32(relo.typeID),
-				target: uint32(relo.typeID),
+				local:  uint32(relo.typ.ID()),
+				target: uint32(relo.typ.ID()),
 			}
 			continue
 		}
 
-		relos, ok := relosByID[relo.typeID]
+		group, ok := relosByType[relo.typ]
 		if !ok {
-			ids = append(ids, relo.typeID)
+			group = &reloGroup{}
+			relosByType[relo.typ] = group
 		}
-		relosByID[relo.typeID] = append(relos, relo)
+		group.relos = append(group.relos, relo)
+		group.indices = append(group.indices, i)
 	}
 
-	// Ensure we work on relocations in a deterministic order.
-	sort.Slice(ids, func(i, j int) bool {
-		return ids[i] < ids[j]
-	})
-
-	for _, id := range ids {
-		if int(id) >= len(local.types) {
-			return nil, fmt.Errorf("invalid type id %d", id)
-		}
-
-		localType := local.types[id]
+	for localType, group := range relosByType {
 		localTypeName := localType.TypeName()
 		if localTypeName == "" {
 			return nil, fmt.Errorf("relocate unnamed or anonymous type %s: %w", localType, ErrNotSupported)
 		}
 
-		relos := relosByID[id]
 		targets := target.namedTypes[newEssentialName(localTypeName)]
-		fixups, err := coreCalculateFixups(local.byteOrder, localType, targets, relos)
+		fixups, err := coreCalculateFixups(local.byteOrder, localType, targets, group.relos)
 		if err != nil {
 			return nil, fmt.Errorf("relocate %s: %w", localType, err)
 		}
 
-		for i, relo := range relos {
-			result[uint64(relo.insnOff)] = fixups[i]
+		for j, index := range group.indices {
+			result[index] = fixups[j]
 		}
 	}
 
@@ -267,7 +269,7 @@ var errImpossibleRelocation = errors.New("impossible relocation")
 //
 // The best target is determined by scoring: the less poisoning we have to do
 // the better the target is.
-func coreCalculateFixups(byteOrder binary.ByteOrder, local Type, targets []Type, relos CORERelos) ([]COREFixup, error) {
+func coreCalculateFixups(byteOrder binary.ByteOrder, local Type, targets []Type, relos []*CORERelocation) ([]COREFixup, error) {
 	localID := local.ID()
 	local = Copy(local, UnderlyingType)
 
@@ -325,7 +327,7 @@ func coreCalculateFixups(byteOrder binary.ByteOrder, local Type, targets []Type,
 
 // coreCalculateFixup calculates the fixup for a single local type, target type
 // and relocation.
-func coreCalculateFixup(byteOrder binary.ByteOrder, local Type, localID TypeID, target Type, targetID TypeID, relo CORERelocation) (COREFixup, error) {
+func coreCalculateFixup(byteOrder binary.ByteOrder, local Type, localID TypeID, target Type, targetID TypeID, relo *CORERelocation) (COREFixup, error) {
 	fixup := func(local, target uint32) (COREFixup, error) {
 		return COREFixup{kind: relo.kind, local: local, target: target}, nil
 	}
diff --git a/internal/btf/ext_info.go b/internal/btf/ext_info.go
index 11e4057..a9c2a3d 100644
--- a/internal/btf/ext_info.go
+++ b/internal/btf/ext_info.go
@@ -1,27 +1,44 @@
 package btf
 
 import (
+	"bytes"
 	"encoding/binary"
 	"errors"
 	"fmt"
 	"io"
 	"math"
+	"sort"
 
 	"github.com/cilium/ebpf/asm"
 	"github.com/cilium/ebpf/internal"
 )
 
-// extInfo contains extended program metadata.
+// ExtInfos contains ELF section metadata.
+type ExtInfos struct {
+	// The slices are sorted by offset in ascending order.
+	funcInfos       map[string][]funcInfo
+	lineInfos       map[string][]lineInfo
+	relocationInfos map[string][]coreRelocationInfo
+}
+
+// loadExtInfosFromELF parses ext infos from the .BTF.ext section in an ELF.
 //
-// It is indexed per section.
-type extInfo struct {
-	funcInfos map[string][]bpfFuncInfo
-	lineInfos map[string][]bpfLineInfo
-	relos     map[string]CORERelos
+// Returns an error wrapping ErrNotFound if no ext infos are present.
+func loadExtInfosFromELF(file *internal.SafeELFFile, ts types, strings *stringTable) (*ExtInfos, error) {
+	section := file.Section(".BTF.ext")
+	if section == nil {
+		return nil, fmt.Errorf("btf ext infos: %w", ErrNotFound)
+	}
+
+	if section.ReaderAt == nil {
+		return nil, fmt.Errorf("compressed ext_info is not supported")
+	}
+
+	return loadExtInfos(section.ReaderAt, file.ByteOrder, ts, strings)
 }
 
-// loadExtInfos parses the .BTF.ext section into its constituent parts.
-func loadExtInfos(r io.ReaderAt, bo binary.ByteOrder, strings *stringTable) (*extInfo, error) {
+// loadExtInfos parses bare ext infos.
+func loadExtInfos(r io.ReaderAt, bo binary.ByteOrder, ts types, strings *stringTable) (*ExtInfos, error) {
 	// Open unbuffered section reader. binary.Read() calls io.ReadFull on
 	// the header structs, resulting in one syscall per header.
 	headerRd := io.NewSectionReader(r, 0, math.MaxInt64)
@@ -36,27 +53,110 @@ func loadExtInfos(r io.ReaderAt, bo binary.ByteOrder, strings *stringTable) (*ex
 	}
 
 	buf := internal.NewBufferedSectionReader(r, extHeader.funcInfoStart(), int64(extHeader.FuncInfoLen))
-	funcInfos, err := parseFuncInfos(buf, bo, strings)
+	btfFuncInfos, err := parseFuncInfos(buf, bo, strings)
 	if err != nil {
 		return nil, fmt.Errorf("parsing BTF function info: %w", err)
 	}
 
+	funcInfos := make(map[string][]funcInfo, len(btfFuncInfos))
+	for section, bfis := range btfFuncInfos {
+		funcInfos[section], err = newFuncInfos(bfis, ts)
+		if err != nil {
+			return nil, fmt.Errorf("section %s: func infos: %w", section, err)
+		}
+	}
+
 	buf = internal.NewBufferedSectionReader(r, extHeader.lineInfoStart(), int64(extHeader.LineInfoLen))
-	lineInfos, err := parseLineInfos(buf, bo, strings)
+	btfLineInfos, err := parseLineInfos(buf, bo, strings)
 	if err != nil {
 		return nil, fmt.Errorf("parsing BTF line info: %w", err)
 	}
 
-	relos := make(map[string]CORERelos)
-	if coreHeader != nil && coreHeader.COREReloOff > 0 && coreHeader.COREReloLen > 0 {
-		buf = internal.NewBufferedSectionReader(r, extHeader.coreReloStart(coreHeader), int64(coreHeader.COREReloLen))
-		relos, err = parseCORERelos(buf, bo, strings)
+	lineInfos := make(map[string][]lineInfo, len(btfLineInfos))
+	for section, blis := range btfLineInfos {
+		lineInfos[section], err = newLineInfos(blis, strings)
 		if err != nil {
-			return nil, fmt.Errorf("parsing CO-RE relocation info: %w", err)
+			return nil, fmt.Errorf("section %s: line infos: %w", section, err)
+		}
+	}
+
+	if coreHeader == nil || coreHeader.COREReloLen == 0 {
+		return &ExtInfos{funcInfos, lineInfos, nil}, nil
+	}
+
+	var btfCORERelos map[string][]bpfCORERelo
+	buf = internal.NewBufferedSectionReader(r, extHeader.coreReloStart(coreHeader), int64(coreHeader.COREReloLen))
+	btfCORERelos, err = parseCORERelos(buf, bo, strings)
+	if err != nil {
+		return nil, fmt.Errorf("parsing CO-RE relocation info: %w", err)
+	}
+
+	coreRelos := make(map[string][]coreRelocationInfo, len(btfCORERelos))
+	for section, brs := range btfCORERelos {
+		coreRelos[section], err = newRelocationInfos(brs, ts, strings)
+		if err != nil {
+			return nil, fmt.Errorf("section %s: CO-RE relocations: %w", section, err)
+		}
+	}
+
+	return &ExtInfos{funcInfos, lineInfos, coreRelos}, nil
+}
+
+type funcInfoMeta struct{}
+type coreRelocationMeta struct{}
+
+// Assign per-section metadata from BTF to a section's instructions.
+func (ei *ExtInfos) Assign(insns asm.Instructions, section string) {
+	funcInfos := ei.funcInfos[section]
+	lineInfos := ei.lineInfos[section]
+	reloInfos := ei.relocationInfos[section]
+
+	iter := insns.Iterate()
+	for iter.Next() {
+		if len(funcInfos) > 0 && funcInfos[0].offset == iter.Offset {
+			iter.Ins.Metadata.Set(funcInfoMeta{}, funcInfos[0].fn)
+			funcInfos = funcInfos[1:]
+		}
+
+		if len(lineInfos) > 0 && lineInfos[0].offset == iter.Offset {
+			*iter.Ins = iter.Ins.WithSource(lineInfos[0].line)
+			lineInfos = lineInfos[1:]
+		}
+
+		if len(reloInfos) > 0 && reloInfos[0].offset == iter.Offset {
+			iter.Ins.Metadata.Set(coreRelocationMeta{}, reloInfos[0].relo)
+			reloInfos = reloInfos[1:]
 		}
 	}
+}
+
+// MarshalExtInfos encodes function and line info embedded in insns into kernel
+// wire format.
+func MarshalExtInfos(insns asm.Instructions) (funcInfos, lineInfos []byte, _ error) {
+	iter := insns.Iterate()
+	var fiBuf, liBuf bytes.Buffer
+	for iter.Next() {
+		if fn, ok := iter.Ins.Metadata.Get(funcInfoMeta{}).(*Func); ok {
+			fi := &funcInfo{
+				fn:     fn,
+				offset: iter.Offset,
+			}
+			if err := fi.marshal(&fiBuf); err != nil {
+				return nil, nil, fmt.Errorf("write func info: %w", err)
+			}
+		}
 
-	return &extInfo{funcInfos, lineInfos, relos}, nil
+		if line, ok := iter.Ins.Source().(*Line); ok {
+			li := &lineInfo{
+				line:   line,
+				offset: iter.Offset,
+			}
+			if err := li.marshal(&liBuf); err != nil {
+				return nil, nil, fmt.Errorf("write line info: %w", err)
+			}
+		}
+	}
+	return fiBuf.Bytes(), liBuf.Bytes(), nil
 }
 
 // btfExtHeader is found at the start of the .BTF.ext section.
@@ -200,8 +300,9 @@ func parseExtInfoRecordSize(r io.Reader, bo binary.ByteOrder) (uint32, error) {
 // The size of a FuncInfo in BTF wire format.
 var FuncInfoSize = uint32(binary.Size(bpfFuncInfo{}))
 
-type FuncInfo struct {
-	fn *Func
+type funcInfo struct {
+	fn     *Func
+	offset asm.RawInstructionOffset
 }
 
 type bpfFuncInfo struct {
@@ -210,16 +311,47 @@ type bpfFuncInfo struct {
 	TypeID  TypeID
 }
 
-func (fi *FuncInfo) Func() *Func {
-	return fi.fn
+func newFuncInfo(fi bpfFuncInfo, ts types) (*funcInfo, error) {
+	typ, err := ts.ByID(fi.TypeID)
+	if err != nil {
+		return nil, err
+	}
+
+	fn, ok := typ.(*Func)
+	if !ok {
+		return nil, fmt.Errorf("type ID %d is a %T, but expected a Func", fi.TypeID, typ)
+	}
+
+	// C doesn't have anonymous functions, but check just in case.
+	if fn.Name == "" {
+		return nil, fmt.Errorf("func with type ID %d doesn't have a name", fi.TypeID)
+	}
+
+	return &funcInfo{
+		fn,
+		asm.RawInstructionOffset(fi.InsnOff),
+	}, nil
 }
 
-// Marshal into the BTF wire format.
-//
-// The offset is converted from bytes to instructions.
-func (fi *FuncInfo) Marshal(w io.Writer, offset uint64) error {
+func newFuncInfos(bfis []bpfFuncInfo, ts types) ([]funcInfo, error) {
+	fis := make([]funcInfo, 0, len(bfis))
+	for _, bfi := range bfis {
+		fi, err := newFuncInfo(bfi, ts)
+		if err != nil {
+			return nil, fmt.Errorf("offset %d: %w", bfi.InsnOff, err)
+		}
+		fis = append(fis, *fi)
+	}
+	sort.Slice(fis, func(i, j int) bool {
+		return fis[i].offset <= fis[j].offset
+	})
+	return fis, nil
+}
+
+// marshal into the BTF wire format.
+func (fi *funcInfo) marshal(w io.Writer) error {
 	bfi := bpfFuncInfo{
-		InsnOff: uint32(offset / asm.InstructionSize),
+		InsnOff: uint32(fi.offset),
 		TypeID:  fi.fn.TypeID,
 	}
 	return binary.Write(w, internal.NativeEndian, &bfi)
@@ -285,9 +417,9 @@ func parseFuncInfoRecords(r io.Reader, bo binary.ByteOrder, recordSize uint32, r
 
 var LineInfoSize = uint32(binary.Size(bpfLineInfo{}))
 
-// LineInfo represents the location and contents of a single line of source
+// Line represents the location and contents of a single line of source
 // code a BPF ELF was compiled from.
-type LineInfo struct {
+type Line struct {
 	fileName   string
 	line       string
 	lineNumber uint32
@@ -296,12 +428,35 @@ type LineInfo struct {
 	// TODO: We should get rid of the fields below, but for that we need to be
 	// able to write BTF.
 
-	// Instruction offset of the line within its enclosing function, in instructions.
-	insnOff     uint32
 	fileNameOff uint32
 	lineOff     uint32
 }
 
+func (li *Line) FileName() string {
+	return li.fileName
+}
+
+func (li *Line) Line() string {
+	return li.line
+}
+
+func (li *Line) LineNumber() uint32 {
+	return li.lineNumber
+}
+
+func (li *Line) LineColumn() uint32 {
+	return li.lineColumn
+}
+
+func (li *Line) String() string {
+	return li.line
+}
+
+type lineInfo struct {
+	line   *Line
+	offset asm.RawInstructionOffset
+}
+
 // Constants for the format of bpfLineInfo.LineCol.
 const (
 	bpfLineShift = 10
@@ -317,61 +472,68 @@ type bpfLineInfo struct {
 	LineCol     uint32
 }
 
-func (li *LineInfo) FileName() string {
-	return li.fileName
-}
+func newLineInfo(li bpfLineInfo, strings *stringTable) (*lineInfo, error) {
+	line, err := strings.Lookup(li.LineOff)
+	if err != nil {
+		return nil, fmt.Errorf("lookup of line: %w", err)
+	}
 
-func (li *LineInfo) Line() string {
-	return li.line
-}
+	fileName, err := strings.Lookup(li.FileNameOff)
+	if err != nil {
+		return nil, fmt.Errorf("lookup of filename: %w", err)
+	}
 
-func (li *LineInfo) LineNumber() uint32 {
-	return li.lineNumber
-}
+	lineNumber := li.LineCol >> bpfLineShift
+	lineColumn := li.LineCol & bpfColumnMax
 
-func (li *LineInfo) LineColumn() uint32 {
-	return li.lineColumn
+	return &lineInfo{
+		&Line{
+			fileName,
+			line,
+			lineNumber,
+			lineColumn,
+			li.FileNameOff,
+			li.LineOff,
+		},
+		asm.RawInstructionOffset(li.InsnOff),
+	}, nil
 }
 
-func (li *LineInfo) String() string {
-	return li.line
+func newLineInfos(blis []bpfLineInfo, strings *stringTable) ([]lineInfo, error) {
+	lis := make([]lineInfo, 0, len(blis))
+	for _, bli := range blis {
+		li, err := newLineInfo(bli, strings)
+		if err != nil {
+			return nil, fmt.Errorf("offset %d: %w", bli.InsnOff, err)
+		}
+		lis = append(lis, *li)
+	}
+	sort.Slice(lis, func(i, j int) bool {
+		return lis[i].offset <= lis[j].offset
+	})
+	return lis, nil
 }
 
-// Marshal writes the binary representation of the LineInfo to w.
-// The instruction offset is converted from bytes to instructions.
-func (li *LineInfo) Marshal(w io.Writer, offset uint64) error {
-	if li.lineNumber > bpfLineMax {
-		return fmt.Errorf("line %d exceeds %d", li.lineNumber, bpfLineMax)
+// marshal writes the binary representation of the LineInfo to w.
+func (li *lineInfo) marshal(w io.Writer) error {
+	line := li.line
+	if line.lineNumber > bpfLineMax {
+		return fmt.Errorf("line %d exceeds %d", line.lineNumber, bpfLineMax)
 	}
 
-	if li.lineColumn > bpfColumnMax {
-		return fmt.Errorf("column %d exceeds %d", li.lineColumn, bpfColumnMax)
+	if line.lineColumn > bpfColumnMax {
+		return fmt.Errorf("column %d exceeds %d", line.lineColumn, bpfColumnMax)
 	}
 
 	bli := bpfLineInfo{
-		li.insnOff + uint32(offset/asm.InstructionSize),
-		li.fileNameOff,
-		li.lineOff,
-		(li.lineNumber << bpfLineShift) | li.lineColumn,
+		uint32(li.offset),
+		line.fileNameOff,
+		line.lineOff,
+		(line.lineNumber << bpfLineShift) | line.lineColumn,
 	}
 	return binary.Write(w, internal.NativeEndian, &bli)
 }
 
-type LineInfos []LineInfo
-
-// Marshal writes the BTF wire format of the LineInfos to w.
-//
-// offset is the start of the enclosing function in bytes.
-func (li LineInfos) Marshal(w io.Writer, offset uint64) error {
-	for _, info := range li {
-		if err := info.Marshal(w, offset); err != nil {
-			return err
-		}
-	}
-
-	return nil
-}
-
 // parseLineInfos parses a line_info sub-section within .BTF.ext ito a map of
 // line infos indexed by section name.
 func parseLineInfos(r io.Reader, bo binary.ByteOrder, strings *stringTable) (map[string][]bpfLineInfo, error) {
@@ -439,30 +601,62 @@ type bpfCORERelo struct {
 }
 
 type CORERelocation struct {
-	insnOff  uint32
-	typeID   TypeID
+	typ      Type
 	accessor coreAccessor
 	kind     coreKind
 }
 
-type CORERelos []CORERelocation
+type coreRelocationInfo struct {
+	relo   *CORERelocation
+	offset asm.RawInstructionOffset
+}
+
+func newRelocationInfo(relo bpfCORERelo, ts types, strings *stringTable) (*coreRelocationInfo, error) {
+	typ, err := ts.ByID(relo.TypeID)
+	if err != nil {
+		return nil, err
+	}
 
-// Offset adds offset to the instruction offset of all CORERelos
-// and returns the result.
-func (cr CORERelos) Offset(offset uint32) CORERelos {
-	var relos CORERelos
-	for _, relo := range cr {
-		relo.insnOff += offset
-		relos = append(relos, relo)
+	accessorStr, err := strings.Lookup(relo.AccessStrOff)
+	if err != nil {
+		return nil, err
 	}
-	return relos
+
+	accessor, err := parseCOREAccessor(accessorStr)
+	if err != nil {
+		return nil, fmt.Errorf("accessor %q: %s", accessorStr, err)
+	}
+
+	return &coreRelocationInfo{
+		&CORERelocation{
+			typ,
+			accessor,
+			relo.Kind,
+		},
+		asm.RawInstructionOffset(relo.InsnOff),
+	}, nil
+}
+
+func newRelocationInfos(brs []bpfCORERelo, ts types, strings *stringTable) ([]coreRelocationInfo, error) {
+	rs := make([]coreRelocationInfo, 0, len(brs))
+	for _, br := range brs {
+		relo, err := newRelocationInfo(br, ts, strings)
+		if err != nil {
+			return nil, fmt.Errorf("offset %d: %w", br.InsnOff, err)
+		}
+		rs = append(rs, *relo)
+	}
+	sort.Slice(rs, func(i, j int) bool {
+		return rs[i].offset < rs[j].offset
+	})
+	return rs, nil
 }
 
 var extInfoReloSize = binary.Size(bpfCORERelo{})
 
 // parseCORERelos parses a core_relos sub-section within .BTF.ext ito a map of
 // CO-RE relocations indexed by section name.
-func parseCORERelos(r io.Reader, bo binary.ByteOrder, strings *stringTable) (map[string]CORERelos, error) {
+func parseCORERelos(r io.Reader, bo binary.ByteOrder, strings *stringTable) (map[string][]bpfCORERelo, error) {
 	recordSize, err := parseExtInfoRecordSize(r, bo)
 	if err != nil {
 		return nil, err
@@ -472,7 +666,7 @@ func parseCORERelos(r io.Reader, bo binary.ByteOrder, strings *stringTable) (map
 		return nil, fmt.Errorf("expected record size %d, got %d", extInfoReloSize, recordSize)
 	}
 
-	result := make(map[string]CORERelos)
+	result := make(map[string][]bpfCORERelo)
 	for {
 		secName, infoHeader, err := parseExtInfoSec(r, bo, strings)
 		if errors.Is(err, io.EOF) {
@@ -482,7 +676,7 @@ func parseCORERelos(r io.Reader, bo binary.ByteOrder, strings *stringTable) (map
 			return nil, err
 		}
 
-		records, err := parseCOREReloRecords(r, bo, recordSize, infoHeader.NumInfo, strings)
+		records, err := parseCOREReloRecords(r, bo, recordSize, infoHeader.NumInfo)
 		if err != nil {
 			return nil, fmt.Errorf("section %v: %w", secName, err)
 		}
@@ -494,8 +688,8 @@ func parseCORERelos(r io.Reader, bo binary.ByteOrder, strings *stringTable) (map
 // parseCOREReloRecords parses a stream of CO-RE relocation entries into a
 // coreRelos. These records appear after a btf_ext_info_sec header in the
 // core_relos sub-section of .BTF.ext.
-func parseCOREReloRecords(r io.Reader, bo binary.ByteOrder, recordSize uint32, recordNum uint32, strings *stringTable) (CORERelos, error) {
-	var out CORERelos
+func parseCOREReloRecords(r io.Reader, bo binary.ByteOrder, recordSize uint32, recordNum uint32) ([]bpfCORERelo, error) {
+	var out []bpfCORERelo
 
 	var relo bpfCORERelo
 	for i := uint32(0); i < recordNum; i++ {
@@ -507,22 +701,11 @@ func parseCOREReloRecords(r io.Reader, bo binary.ByteOrder, recordSize uint32, r
 			return nil, fmt.Errorf("offset %v is not aligned with instruction size", relo.InsnOff)
 		}
 
-		accessorStr, err := strings.Lookup(relo.AccessStrOff)
-		if err != nil {
-			return nil, err
-		}
-
-		accessor, err := parseCOREAccessor(accessorStr)
-		if err != nil {
-			return nil, fmt.Errorf("accessor %q: %s", accessorStr, err)
-		}
+		// ELF tracks offset in bytes, the kernel expects raw BPF instructions.
+		// Convert as early as possible.
+		relo.InsnOff /= asm.InstructionSize
 
-		out = append(out, CORERelocation{
-			relo.InsnOff,
-			relo.TypeID,
-			accessor,
-			relo.Kind,
-		})
+		out = append(out, relo)
 	}
 
 	return out, nil
diff --git a/internal/btf/types.go b/internal/btf/types.go
index 2e6bc41..b896e4d 100644
--- a/internal/btf/types.go
+++ b/internal/btf/types.go
@@ -59,6 +59,18 @@ var (
 	_ Type = (*Float)(nil)
 )
 
+// types is a list of Type.
+//
+// The order determines the ID of a type.
+type types []Type
+
+func (ts types) ByID(id TypeID) (Type, error) {
+	if int(id) > len(ts) {
+		return nil, fmt.Errorf("type ID %d: %w", id, ErrNotFound)
+	}
+	return ts[id], nil
+}
+
 // Void is the unit type of BTF.
 type Void struct{}
 
diff --git a/linker.go b/linker.go
index e4facf7..1d661c9 100644
--- a/linker.go
+++ b/linker.go
@@ -1,12 +1,10 @@
 package ebpf
 
 import (
-	"bytes"
 	"errors"
 	"fmt"
 
 	"github.com/cilium/ebpf/asm"
-	"github.com/cilium/ebpf/internal/btf"
 )
 
 // splitSymbols splits insns into subsections delimited by Symbol Instructions.
@@ -111,52 +109,6 @@ func findReferences(progs map[string]*ProgramSpec) error {
 	return nil
 }
 
-// collectCORERelos returns a list of CO-RE relocations of the layout's progs
-// in order.
-func collectCORERelos(layout []reference) (btf.CORERelos, error) {
-	var relos btf.CORERelos
-	for _, sym := range layout {
-		if sym.spec.BTF == nil {
-			return nil, fmt.Errorf("program %s: missing BTF", sym.spec.Name)
-		}
-		relos = append(relos, sym.spec.BTF.CORERelos.Offset(uint32(sym.offset))...)
-	}
-
-	return relos, nil
-}
-
-// marshalFuncInfos returns the BTF func infos of all progs in order.
-func marshalFuncInfos(layout []reference) ([]byte, error) {
-	if len(layout) == 0 {
-		return nil, nil
-	}
-
-	var buf bytes.Buffer
-	for _, sym := range layout {
-		if err := sym.spec.BTF.FuncInfo.Marshal(&buf, sym.offset); err != nil {
-			return nil, fmt.Errorf("marshaling prog %s func info: %w", sym.spec.Name, err)
-		}
-	}
-
-	return buf.Bytes(), nil
-}
-
-// marshalLineInfos returns the BTF line infos of all progs in order.
-func marshalLineInfos(layout []reference) ([]byte, error) {
-	if len(layout) == 0 {
-		return nil, nil
-	}
-
-	var buf bytes.Buffer
-	for _, sym := range layout {
-		if err := sym.spec.BTF.LineInfos.Marshal(&buf, sym.offset); err != nil {
-			return nil, fmt.Errorf("marshaling prog %s line infos: %w", sym.spec.Name, err)
-		}
-	}
-
-	return buf.Bytes(), nil
-}
-
 // fixupAndValidate is called by the ELF reader right before marshaling the
 // instruction stream. It performs last-minute adjustments to the program and
 // runs some sanity checks before sending it off to the kernel.
diff --git a/map.go b/map.go
index 21c1633..6ae2683 100644
--- a/map.go
+++ b/map.go
@@ -76,8 +76,11 @@ type MapSpec struct {
 	// Must be nil or empty before instantiating the MapSpec into a Map.
 	Extra *bytes.Reader
 
+	// The key and value type of this map. May be nil.
+	Key, Value btf.Type
+
 	// The BTF associated with this map.
-	BTF *btf.Map
+	BTF *btf.Spec
 }
 
 func (ms *MapSpec) String() string {
@@ -398,15 +401,15 @@ func (spec *MapSpec) createMap(inner *sys.FD, opts MapOptions, handles *handleCa
 	}
 
 	if spec.hasBTF() {
-		handle, err := handles.btfHandle(spec.BTF.Spec)
+		handle, err := handles.btfHandle(spec.BTF)
 		if err != nil && !errors.Is(err, btf.ErrNotSupported) {
 			return nil, fmt.Errorf("load BTF: %w", err)
 		}
 
 		if handle != nil {
 			attr.BtfFd = uint32(handle.FD())
-			attr.BtfKeyTypeId = uint32(spec.BTF.Key.ID())
-			attr.BtfValueTypeId = uint32(spec.BTF.Value.ID())
+			attr.BtfKeyTypeId = uint32(spec.Key.ID())
+			attr.BtfValueTypeId = uint32(spec.Value.ID())
 		}
 	}
 
diff --git a/prog.go b/prog.go
index cf4189f..043274a 100644
--- a/prog.go
+++ b/prog.go
@@ -97,7 +97,7 @@ type ProgramSpec struct {
 	// The BTF associated with this program. Changing Instructions
 	// will most likely invalidate the contained data, and may
 	// result in errors when attempting to load it into the kernel.
-	BTF *btf.Program
+	BTF *btf.Spec
 
 	// The byte order this program was compiled for, may be nil.
 	ByteOrder binary.ByteOrder
@@ -162,46 +162,6 @@ func (spec *ProgramSpec) flatten(visited map[*ProgramSpec]bool) (asm.Instruction
 	return insns, progs
 }
 
-// A reference describes a byte offset an Symbol Instruction pointing
-// to another ProgramSpec.
-type reference struct {
-	offset uint64
-	spec   *ProgramSpec
-}
-
-// layout returns a unique list of programs that must be included
-// in spec's instruction stream when inserting it into the kernel.
-// Always returns spec itself as the first entry in the chain.
-func (spec *ProgramSpec) layout() ([]reference, error) {
-	out := []reference{{0, spec}}
-
-	name := spec.Instructions.Name()
-
-	var ins *asm.Instruction
-	iter := spec.Instructions.Iterate()
-	for iter.Next() {
-		ins = iter.Ins
-
-		// 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 {
-			continue
-		}
-
-		// Failure to look up a reference is not an error. There are existing tests
-		// 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()]
-		if ref != nil {
-			out = append(out, reference{iter.Offset.Bytes(), ref})
-		}
-	}
-
-	return out, nil
-}
-
 // Program represents BPF program loaded into the kernel.
 //
 // It is not safe to close a Program which is used by other goroutines.
@@ -290,24 +250,16 @@ func newProgramWithOptions(spec *ProgramSpec, opts ProgramOptions, handles *hand
 		}
 	}
 
-	layout, err := spec.layout()
-	if err != nil {
-		return nil, fmt.Errorf("get program layout: %w", err)
-	}
+	insns := make(asm.Instructions, len(spec.Instructions))
+	copy(insns, spec.Instructions)
 
 	var btfDisabled bool
-	var core btf.COREFixups
 	if spec.BTF != nil {
-		relos, err := collectCORERelos(layout)
-		if err != nil {
-			return nil, fmt.Errorf("collecting CO-RE relocations: %w", err)
-		}
-		core, err = btf.CORERelocate(spec.BTF.Spec(), targetBTF, relos)
-		if err != nil {
-			return nil, fmt.Errorf("generating CO-RE fixups: %w", err)
+		if err := btf.CORERelocate(insns, spec.BTF, targetBTF); err != nil {
+			return nil, fmt.Errorf("apply CO-RE relocations: %w", err)
 		}
 
-		handle, err := handles.btfHandle(spec.BTF.Spec())
+		handle, err := handles.btfHandle(spec.BTF)
 		btfDisabled = errors.Is(err, btf.ErrNotSupported)
 		if err != nil && !btfDisabled {
 			return nil, fmt.Errorf("load BTF: %w", err)
@@ -316,29 +268,21 @@ func newProgramWithOptions(spec *ProgramSpec, opts ProgramOptions, handles *hand
 		if handle != nil {
 			attr.ProgBtfFd = uint32(handle.FD())
 
-			fib, err := marshalFuncInfos(layout)
+			fib, lib, err := btf.MarshalExtInfos(insns)
 			if err != nil {
 				return nil, err
 			}
+
 			attr.FuncInfoRecSize = btf.FuncInfoSize
 			attr.FuncInfoCnt = uint32(len(fib)) / btf.FuncInfoSize
 			attr.FuncInfo = sys.NewSlicePointer(fib)
 
-			lib, err := marshalLineInfos(layout)
-			if err != nil {
-				return nil, err
-			}
 			attr.LineInfoRecSize = btf.LineInfoSize
 			attr.LineInfoCnt = uint32(len(lib)) / btf.LineInfoSize
 			attr.LineInfo = sys.NewSlicePointer(lib)
 		}
 	}
 
-	insns, err := core.Apply(spec.Instructions)
-	if err != nil {
-		return nil, fmt.Errorf("applying CO-RE fixups: %w", err)
-	}
-
 	if err := fixupAndValidate(insns); err != nil {
 		return nil, err
 	}


Test patch:
diff --git a/elf_reader_test.go b/elf_reader_test.go
index acbe13d..96c597e 100644
--- a/elf_reader_test.go
+++ b/elf_reader_test.go
@@ -142,9 +142,10 @@ func TestLoadCollectionSpec(t *testing.T) {
 			}
 			return false
 		}),
-		cmpopts.IgnoreTypes(new(btf.Map), new(btf.Program)),
+		cmpopts.IgnoreTypes(new(btf.Spec)),
 		cmpopts.IgnoreFields(CollectionSpec{}, "ByteOrder", "Types"),
 		cmpopts.IgnoreFields(ProgramSpec{}, "Instructions", "ByteOrder"),
+		cmpopts.IgnoreFields(MapSpec{}, "Key", "Value"),
 		cmpopts.IgnoreUnexported(ProgramSpec{}),
 		cmpopts.IgnoreMapEntries(func(key string, _ *MapSpec) bool {
 			switch key {
diff --git a/internal/btf/btf_test.go b/internal/btf/btf_test.go
index 41d1e34..9af5e50 100644
--- a/internal/btf/btf_test.go
+++ b/internal/btf/btf_test.go
@@ -222,14 +222,6 @@ func TestLoadSpecFromElf(t *testing.T) {
 	testutils.Files(t, testutils.Glob(t, "../../testdata/loader-e*.elf"), func(t *testing.T, file string) {
 		spec := parseELFBTF(t, file)
 
-		if prog, err := spec.Program("xdp_prog"); err != nil || prog == nil {
-			t.Error("Can't get BTF for program xdp_prog:", err)
-		}
-
-		if prog, err := spec.Program("no_relocation"); err != nil || prog == nil {
-			t.Error("Can't get BTF for program no_relocation:", err)
-		}
-
 		vt, err := spec.TypeByID(0)
 		if err != nil {
 			t.Error("Can't retrieve void type by ID:", err)
diff --git a/internal/btf/core_test.go b/internal/btf/core_test.go
index 6957551..dbc717a 100644
--- a/internal/btf/core_test.go
+++ b/internal/btf/core_test.go
@@ -526,25 +526,29 @@ func TestCORERelocation(t *testing.T) {
 		}
 		defer rd.Close()
 
-		spec, err := LoadSpecFromReader(rd)
+		spec, extInfos, err := LoadSpecAndExtInfosFromReader(rd)
 		if err != nil {
 			t.Fatal(err)
 		}
 
+		if extInfos == nil {
+			t.Skip("No ext_infos")
+		}
+
 		errs := map[string]error{
 			"err_ambiguous":         errAmbiguousRelocation,
 			"err_ambiguous_flavour": errAmbiguousRelocation,
 		}
 
-		for section := range spec.funcInfos {
+		for section := range extInfos.funcInfos {
 			name := strings.TrimPrefix(section, "socket_filter/")
 			t.Run(name, func(t *testing.T) {
-				prog, err := spec.Program(section)
-				if err != nil {
-					t.Fatal("Retrieve program:", err)
+				var relos []*CORERelocation
+				for _, reloInfo := range extInfos.relocationInfos[section] {
+					relos = append(relos, reloInfo.relo)
 				}
 
-				relos, err := CORERelocate(prog.Spec(), spec, prog.CORERelos)
+				fixups, err := coreRelocate(spec, spec, relos)
 				if want := errs[name]; want != nil {
 					if !errors.Is(err, want) {
 						t.Fatal("Expected", want, "got", err)
@@ -556,11 +560,11 @@ func TestCORERelocation(t *testing.T) {
 					t.Fatal("Can't relocate against itself:", err)
 				}
 
-				for offset, relo := range relos {
-					if want := relo.local; !relo.skipLocalValidation && want != relo.target {
+				for offset, fixup := range fixups {
+					if want := fixup.local; !fixup.skipLocalValidation && want != fixup.target {
 						// Since we're relocating against ourselves both values
 						// should match.
-						t.Errorf("offset %d: local %v doesn't match target %d (kind %s)", offset, relo.local, relo.target, relo.kind)
+						t.Errorf("offset %d: local %v doesn't match target %d (kind %s)", offset, fixup.local, fixup.target, fixup.kind)
 					}
 				}
 			})
diff --git a/internal/btf/fuzz_test.go b/internal/btf/fuzz_test.go
index 1da3284..bbbb04a 100644
--- a/internal/btf/fuzz_test.go
+++ b/internal/btf/fuzz_test.go
@@ -69,7 +69,7 @@ func FuzzExtInfo(f *testing.F) {
 			t.Skip("invalid string table")
 		}
 
-		info, err := loadExtInfos(bytes.NewReader(data), internal.NativeEndian, table)
+		info, err := loadExtInfos(bytes.NewReader(data), internal.NativeEndian, nil, table)
 		if err != nil {
 			if info != nil {
 				t.Fatal("info is not nil")
diff --git a/map_test.go b/map_test.go
index 01afb71..94dde31 100644
--- a/map_test.go
+++ b/map_test.go
@@ -1698,7 +1698,7 @@ func TestMapPinning(t *testing.T) {
 	// This is a terrible hack: if loading a pinned map tries to load BTF,
 	// it will get a nil *btf.Spec from this *btf.Map. This is turn will make
 	// btf.NewHandle fail.
-	spec.BTF = new(btf.Map)
+	spec.BTF = new(btf.Spec)
 
 	m2, err := NewMapWithOptions(spec, MapOptions{PinPath: tmp})
 	testutils.SkipIfNotSupported(t, err)