Repository: cilium/ebpf
Pull request #370 (2021-08-11 10:35:55)

btf: prepare for making the package public
I think this is the MVP to make `btf` accessible to users.

lmb: @brb I pushed 4f00d3d3333b911474142732ece1e8dac29a7e52 which changes the type copy behaviour we talked about.

@ti-mo are you OK with proceeding with this?

Patch:
diff --git a/collection.go b/collection.go
index 129b31a..cf45db5 100644
--- a/collection.go
+++ b/collection.go
@@ -129,7 +129,7 @@ func (cs *CollectionSpec) RewriteConstants(consts map[string]interface{}) error
 	buf := make([]byte, len(value))
 	copy(buf, value)
 
-	err := patchValue(buf, btf.MapValue(rodata.BTF), consts)
+	err := patchValue(buf, rodata.BTF.Value, consts)
 	if err != nil {
 		return err
 	}
diff --git a/elf_reader.go b/elf_reader.go
index 77ba6eb..7377e7f 100644
--- a/elf_reader.go
+++ b/elf_reader.go
@@ -798,7 +798,12 @@ func mapSpecFromBTF(es *elfSection, vs *btf.VarSecinfo, def *btf.Struct, spec *b
 		}
 	}
 
-	bm := btf.NewMap(spec, key, value)
+	if key == nil {
+		key = &btf.Void{}
+	}
+	if value == nil {
+		value = &btf.Void{}
+	}
 
 	return &MapSpec{
 		Name:       SanitizeName(name, -1),
@@ -807,7 +812,7 @@ func mapSpecFromBTF(es *elfSection, vs *btf.VarSecinfo, def *btf.Struct, spec *b
 		ValueSize:  valueSize,
 		MaxEntries: maxEntries,
 		Flags:      flags,
-		BTF:        &bm,
+		BTF:        &btf.Map{Spec: spec, Key: key, Value: value},
 		Pinning:    pinType,
 		InnerMap:   innerMapSpec,
 		Contents:   contents,
@@ -859,7 +864,7 @@ func resolveBTFValuesContents(es *elfSection, vs *btf.VarSecinfo, member btf.Mem
 	// The offset of the 'values' member within the _struct_ (in bits)
 	// is the starting point of the array. Convert to bytes. Add VarSecinfo
 	// offset to get the absolute position in the ELF blob.
-	start := (member.Offset / 8) + vs.Offset
+	start := (member.OffsetBits / 8) + vs.Offset
 	// 'values' is encoded in BTF as a zero (variable) length struct
 	// member, and its contents run until the end of the VarSecinfo.
 	// Add VarSecinfo offset to get the absolute position in the ELF blob.
@@ -921,9 +926,9 @@ func (ec *elfCode) loadDataSections(maps map[string]*MapSpec) error {
 			return errors.New("data sections require BTF, make sure all consts are marked as static")
 		}
 
-		btfMap, err := ec.btf.Datasec(sec.Name)
-		if err != nil {
-			return err
+		var datasec btf.Datasec
+		if err := ec.btf.FindType(sec.Name, &datasec); err != nil {
+			return fmt.Errorf("data section %s: can't get BTF: %w", sec.Name, err)
 		}
 
 		data, err := sec.Data()
@@ -942,7 +947,7 @@ func (ec *elfCode) loadDataSections(maps map[string]*MapSpec) error {
 			ValueSize:  uint32(len(data)),
 			MaxEntries: 1,
 			Contents:   []MapKV{{uint32(0), data}},
-			BTF:        btfMap,
+			BTF:        &btf.Map{Spec: ec.btf, Key: &btf.Void{}, Value: &datasec},
 		}
 
 		switch sec.Name {
diff --git a/internal/btf/btf.go b/internal/btf/btf.go
index 80d0ed9..8efebb8 100644
--- a/internal/btf/btf.go
+++ b/internal/btf/btf.go
@@ -35,7 +35,7 @@ type Spec struct {
 	rawTypes   []rawType
 	strings    stringTable
 	types      []Type
-	namedTypes map[string][]namedType
+	namedTypes map[string][]NamedType
 	funcInfos  map[string]extInfo
 	lineInfos  map[string]extInfo
 	coreRelos  map[string]coreRelos
@@ -360,6 +360,30 @@ func fixupDatasec(rawTypes []rawType, rawStrings stringTable, sectionSizes map[s
 	return nil
 }
 
+// Copy creates a copy of Spec.
+func (s *Spec) Copy() *Spec {
+	types, _ := copyTypes(s.types, nil)
+	namedTypes := make(map[string][]NamedType)
+	for _, typ := range types {
+		if named, ok := typ.(NamedType); ok {
+			name := essentialName(named.TypeName())
+			namedTypes[name] = append(namedTypes[name], named)
+		}
+	}
+
+	// NB: Other parts of spec are not copied since they are immutable.
+	return &Spec{
+		s.rawTypes,
+		s.strings,
+		types,
+		namedTypes,
+		s.funcInfos,
+		s.lineInfos,
+		s.coreRelos,
+		s.byteOrder,
+	}
+}
+
 type marshalOpts struct {
 	ByteOrder        binary.ByteOrder
 	StripFuncLinkage bool
@@ -450,17 +474,6 @@ func (s *Spec) Program(name string, length uint64) (*Program, error) {
 	return &Program{s, length, funcInfos, lineInfos, relos}, nil
 }
 
-// Datasec returns the BTF required to create maps which represent data sections.
-func (s *Spec) Datasec(name string) (*Map, error) {
-	var datasec Datasec
-	if err := s.FindType(name, &datasec); err != nil {
-		return nil, fmt.Errorf("data section %s: can't get BTF: %w", name, err)
-	}
-
-	m := NewMap(s, &Void{}, &datasec)
-	return &m, nil
-}
-
 // FindType searches for a type with a specific name.
 //
 // hint determines the type of the returned Type.
@@ -479,7 +492,7 @@ func (s *Spec) FindType(name string, typ Type) error {
 		}
 
 		// Match against the full name, not just the essential one.
-		if typ.name() != name {
+		if typ.TypeName() != name {
 			continue
 		}
 
@@ -494,15 +507,15 @@ func (s *Spec) FindType(name string, typ Type) error {
 		return fmt.Errorf("type %s: %w", name, ErrNotFound)
 	}
 
-	cpy, _ := copyType(candidate, nil)
-	value := reflect.Indirect(reflect.ValueOf(cpy))
+	value := reflect.Indirect(reflect.ValueOf(candidate))
 	reflect.Indirect(reflect.ValueOf(typ)).Set(value)
 	return nil
 }
 
 // Handle is a reference to BTF loaded into the kernel.
 type Handle struct {
-	fd *internal.FD
+	spec *Spec
+	fd   *internal.FD
 }
 
 // NewHandle loads BTF into the kernel.
@@ -544,7 +557,7 @@ func NewHandle(spec *Spec) (*Handle, error) {
 		return nil, internal.ErrorWithLog(err, logBuf, logErr)
 	}
 
-	return &Handle{fd}, nil
+	return &Handle{spec.Copy(), fd}, nil
 }
 
 // NewHandleFromID returns the BTF handle for a given id.
@@ -558,20 +571,18 @@ func NewHandleFromID(id ID) (*Handle, error) {
 		return nil, fmt.Errorf("get BTF by id: %w", err)
 	}
 
-	return &Handle{fd}, nil
-}
-
-// HandleSpec returns the Spec that defined the BTF loaded into the kernel.
-//
-// This is a free function instead of a method to hide it from users
-// of package ebpf.
-func HandleSpec(s *Handle) (*Spec, error) {
-	info, err := newInfoFromFd(s.fd)
+	info, err := newInfoFromFd(fd)
 	if err != nil {
+		_ = fd.Close()
 		return nil, fmt.Errorf("get BTF spec for handle: %w", err)
 	}
 
-	return info.BTF, nil
+	return &Handle{info.BTF, fd}, nil
+}
+
+// Spec returns the Spec that defined the BTF loaded into the kernel.
+func (h *Handle) Spec() *Spec {
+	return h.spec
 }
 
 // Close destroys the handle.
@@ -593,43 +604,8 @@ func (h *Handle) FD() int {
 
 // Map is the BTF for a map.
 type Map struct {
-	spec       *Spec
-	key, value Type
-}
-
-// NewMap returns a new Map containing the given values.
-// The key and value arguments are initialized to Void if nil values are given.
-func NewMap(spec *Spec, key Type, value Type) Map {
-	if key == nil {
-		key = &Void{}
-	}
-	if value == nil {
-		value = &Void{}
-	}
-
-	return Map{
-		spec:  spec,
-		key:   key,
-		value: value,
-	}
-}
-
-// MapSpec should be a method on Map, but is a free function
-// to hide it from users of the ebpf package.
-func MapSpec(m *Map) *Spec {
-	return m.spec
-}
-
-// MapKey should be a method on Map, but is a free function
-// to hide it from users of the ebpf package.
-func MapKey(m *Map) Type {
-	return m.key
-}
-
-// MapValue should be a method on Map, but is a free function
-// to hide it from users of the ebpf package.
-func MapValue(m *Map) Type {
-	return m.value
+	Spec       *Spec
+	Key, Value Type
 }
 
 // Program is the BTF information for a stream of instructions.
@@ -640,68 +616,65 @@ type Program struct {
 	coreRelos            coreRelos
 }
 
-// ProgramSpec returns the Spec needed for loading function and line infos into the kernel.
-//
-// This is a free function instead of a method to hide it from users
-// of package ebpf.
-func ProgramSpec(s *Program) *Spec {
-	return s.spec
+// Spec returns the BTF spec of this program.
+func (p *Program) Spec() *Spec {
+	return p.spec
 }
 
-// ProgramAppend the information from other to the Program.
-//
-// This is a free function instead of a method to hide it from users
-// of package ebpf.
-func ProgramAppend(s, other *Program) error {
-	funcInfos, err := s.funcInfos.append(other.funcInfos, s.length)
+// Append the information from other to the Program.
+func (p *Program) Append(other *Program) error {
+	if other.spec != p.spec {
+		return fmt.Errorf("can't append program with different BTF specs")
+	}
+
+	funcInfos, err := p.funcInfos.append(other.funcInfos, p.length)
 	if err != nil {
 		return fmt.Errorf("func infos: %w", err)
 	}
 
-	lineInfos, err := s.lineInfos.append(other.lineInfos, s.length)
+	lineInfos, err := p.lineInfos.append(other.lineInfos, p.length)
 	if err != nil {
 		return fmt.Errorf("line infos: %w", err)
 	}
 
-	s.funcInfos = funcInfos
-	s.lineInfos = lineInfos
-	s.coreRelos = s.coreRelos.append(other.coreRelos, s.length)
-	s.length += other.length
+	p.funcInfos = funcInfos
+	p.lineInfos = lineInfos
+	p.coreRelos = p.coreRelos.append(other.coreRelos, p.length)
+	p.length += other.length
 	return nil
 }
 
-// ProgramFuncInfos returns the binary form of BTF function infos.
-//
-// This is a free function instead of a method to hide it from users
-// of package ebpf.
-func ProgramFuncInfos(s *Program) (recordSize uint32, bytes []byte, err error) {
-	bytes, err = s.funcInfos.MarshalBinary()
+// FuncInfos returns the binary form of BTF function infos.
+func (p *Program) FuncInfos() (recordSize uint32, bytes []byte, err error) {
+	if p.spec.byteOrder != internal.NativeEndian {
+		return 0, nil, fmt.Errorf("can't marshal func infos different endianness")
+	}
+
+	bytes, err = p.funcInfos.MarshalBinary()
 	if err != nil {
 		return 0, nil, err
 	}
 
-	return s.funcInfos.recordSize, bytes, nil
+	return p.funcInfos.recordSize, bytes, nil
 }
 
-// ProgramLineInfos returns the binary form of BTF line infos.
-//
-// This is a free function instead of a method to hide it from users
-// of package ebpf.
-func ProgramLineInfos(s *Program) (recordSize uint32, bytes []byte, err error) {
-	bytes, err = s.lineInfos.MarshalBinary()
+// LineInfos returns the binary form of BTF line infos.
+func (p *Program) LineInfos() (recordSize uint32, bytes []byte, err error) {
+	if p.spec.byteOrder != internal.NativeEndian {
+		return 0, nil, fmt.Errorf("can't marshal line infos different endianness")
+	}
+
+	bytes, err = p.lineInfos.MarshalBinary()
 	if err != nil {
 		return 0, nil, err
 	}
 
-	return s.lineInfos.recordSize, bytes, nil
+	return p.lineInfos.recordSize, bytes, nil
 }
 
-// ProgramFixups returns the changes required to adjust the program to the target.
-//
-// This is a free function instead of a method to hide it from users
-// of package ebpf.
-func ProgramFixups(s *Program, target *Spec) (COREFixups, error) {
-	if len(s.coreRelos) == 0 {
+// Fixups returns the changes required to adjust the program to the target.
+func (p *Program) Fixups(target *Spec) (COREFixups, error) {
+	if len(p.coreRelos) == 0 {
 		return nil, nil
 	}
 
@@ -713,7 +686,11 @@ func ProgramFixups(s *Program, target *Spec) (COREFixups, error) {
 		}
 	}
 
-	return coreRelocate(s.spec, target, s.coreRelos)
+	if p.spec.byteOrder != target.byteOrder {
+		return nil, fmt.Errorf("can't calculate fixups for mixed endianness")
+	}
+
+	return coreRelocate(p.spec, target, p.coreRelos)
 }
 
 type bpfLoadBTFAttr struct {
diff --git a/internal/btf/core.go b/internal/btf/core.go
index 7c888f6..749799c 100644
--- a/internal/btf/core.go
+++ b/internal/btf/core.go
@@ -234,13 +234,13 @@ func coreRelocate(local, target *Spec, relos coreRelos) (COREFixups, error) {
 		}
 
 		localType := local.types[id]
-		named, ok := localType.(namedType)
-		if !ok || named.name() == "" {
+		named, ok := localType.(NamedType)
+		if !ok || named.TypeName() == "" {
 			return nil, fmt.Errorf("relocate unnamed or anonymous type %s: %w", localType, ErrNotSupported)
 		}
 
 		relos := relosByID[id]
-		targets := target.namedTypes[named.essentialName()]
+		targets := target.namedTypes[essentialName(named.TypeName())]
 		fixups, err := coreCalculateFixups(localType, targets, relos)
 		if err != nil {
 			return nil, fmt.Errorf("relocate %s: %w", localType, err)
@@ -262,7 +262,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(local Type, targets []namedType, relos coreRelos) ([]COREFixup, error) {
+func coreCalculateFixups(local Type, targets []NamedType, relos coreRelos) ([]COREFixup, error) {
 	localID := local.ID()
 	local, err := copyType(local, skipQualifierAndTypedef)
 	if err != nil {
@@ -564,7 +564,7 @@ func coreFindField(local Type, localAcc coreAccessor, target Type) (_, _ coreFie
 
 				// This is an anonymous struct or union, ignore it.
 				local = localMember.Type
-				localOffset += localMember.Offset
+				localOffset += localMember.OffsetBits
 				localMaybeFlex = false
 				continue
 			}
@@ -585,10 +585,10 @@ func coreFindField(local Type, localAcc coreAccessor, target Type) (_, _ coreFie
 
 			local = localMember.Type
 			localMaybeFlex = acc == len(localMembers)-1
-			localOffset += localMember.Offset
+			localOffset += localMember.OffsetBits
 			target = targetMember.Type
 			targetMaybeFlex = last
-			targetOffset += targetMember.Offset
+			targetOffset += targetMember.OffsetBits
 
 		case *Array:
 			// For arrays, acc is the index in the target.
@@ -639,7 +639,7 @@ func coreFindField(local Type, localAcc coreAccessor, target Type) (_, _ coreFie
 
 // coreFindMember finds a member in a composite type while handling anonymous
 // structs and unions.
-func coreFindMember(typ composite, name Name) (Member, bool, error) {
+func coreFindMember(typ composite, name string) (Member, bool, error) {
 	if name == "" {
 		return Member{}, false, errors.New("can't search for anonymous member")
 	}
@@ -670,7 +670,7 @@ func coreFindMember(typ composite, name Name) (Member, bool, error) {
 		for j, member := range members {
 			if member.Name == name {
 				// NB: This is safe because member is a copy.
-				member.Offset += target.offset
+				member.OffsetBits += target.offset
 				return member, j == len(members)-1, nil
 			}
 
@@ -685,7 +685,7 @@ func coreFindMember(typ composite, name Name) (Member, bool, error) {
 				return Member{}, false, fmt.Errorf("anonymous non-composite type %T not allowed", member.Type)
 			}
 
-			targets = append(targets, offsetTarget{comp, target.offset + member.Offset})
+			targets = append(targets, offsetTarget{comp, target.offset + member.OffsetBits})
 		}
 	}
 
@@ -704,9 +704,9 @@ func coreFindEnumValue(local Type, localAcc coreAccessor, target Type) (localVal
 		return nil, nil, errImpossibleRelocation
 	}
 
-	localName := localValue.Name.essentialName()
+	localName := essentialName(localValue.Name)
 	for i, targetValue := range targetEnum.Values {
-		if targetValue.Name.essentialName() != localName {
+		if essentialName(targetValue.Name) != localName {
 			continue
 		}
 
@@ -853,11 +853,11 @@ func coreAreMembersCompatible(localType Type, targetType Type) error {
 
 	case *Enum:
 		tv := targetType.(*Enum)
-		return doNamesMatch(lv.name(), tv.name())
+		return doNamesMatch(lv.Name, tv.Name)
 
 	case *Fwd:
 		tv := targetType.(*Fwd)
-		return doNamesMatch(lv.name(), tv.name())
+		return doNamesMatch(lv.Name, tv.Name)
 
 	case *Int:
 		tv := targetType.(*Int)
diff --git a/internal/btf/strings.go b/internal/btf/strings.go
index 8782643..7414f85 100644
--- a/internal/btf/strings.go
+++ b/internal/btf/strings.go
@@ -53,8 +53,3 @@ func (st stringTable) Lookup(offset uint32) (string, error) {
 
 	return string(str[:end]), nil
 }
-
-func (st stringTable) LookupName(offset uint32) (Name, error) {
-	str, err := st.Lookup(offset)
-	return Name(str), err
-}
diff --git a/internal/btf/types.go b/internal/btf/types.go
index baec67f..5c8e7c6 100644
--- a/internal/btf/types.go
+++ b/internal/btf/types.go
@@ -30,27 +30,26 @@ type Type interface {
 	walk(*typeDeque)
 }
 
-// namedType is a type with a name.
-//
-// Most named types simply embed Name.
-type namedType interface {
+// NamedType is a type with a name.
+type NamedType interface {
 	Type
-	name() string
-	essentialName() string
-}
-
-// Name identifies a type.
-//
-// Anonymous types have an empty name.
-type Name string
 
-func (n Name) name() string {
-	return string(n)
+	// Name of the type, empty for anonymous types.
+	TypeName() string
 }
 
-func (n Name) essentialName() string {
-	return essentialName(string(n))
-}
+var (
+	_ NamedType = (*Int)(nil)
+	_ NamedType = (*Struct)(nil)
+	_ NamedType = (*Union)(nil)
+	_ NamedType = (*Enum)(nil)
+	_ NamedType = (*Fwd)(nil)
+	_ NamedType = (*Func)(nil)
+	_ NamedType = (*Typedef)(nil)
+	_ NamedType = (*Var)(nil)
+	_ NamedType = (*Datasec)(nil)
+	_ NamedType = (*Float)(nil)
+)
 
 // Void is the unit type of BTF.
 type Void struct{}
@@ -72,19 +71,17 @@ const (
 // Int is an integer of a given length.
 type Int struct {
 	TypeID
-	Name
+	Name string
 
 	// The size of the integer in bytes.
 	Size     uint32
 	Encoding IntEncoding
-	// Offset is the starting bit offset. Currently always 0.
+	// OffsetBits is the starting bit offset. Currently always 0.
 	// See https://www.kernel.org/doc/html/latest/bpf/btf.html#btf-kind-int
-	Offset uint32
-	Bits   byte
+	OffsetBits uint32
+	Bits       byte
 }
 
-var _ namedType = (*Int)(nil)
-
 func (i *Int) String() string {
 	var s strings.Builder
 
@@ -110,15 +107,16 @@ func (i *Int) String() string {
 	return s.String()
 }
 
-func (i *Int) size() uint32    { return i.Size }
-func (i *Int) walk(*typeDeque) {}
+func (i *Int) TypeName() string { return i.Name }
+func (i *Int) size() uint32     { return i.Size }
+func (i *Int) walk(*typeDeque)  {}
 func (i *Int) copy() Type {
 	cpy := *i
 	return &cpy
 }
 
 func (i *Int) isBitfield() bool {
-	return i.Offset > 0
+	return i.OffsetBits > 0
 }
 
 // Pointer is a pointer to another type.
@@ -158,7 +156,7 @@ func (arr *Array) copy() Type {
 // Struct is a compound type of consecutive members.
 type Struct struct {
 	TypeID
-	Name
+	Name string
 	// The size of the struct including padding, in bytes
 	Size    uint32
 	Members []Member
@@ -168,6 +166,8 @@ func (s *Struct) String() string {
 	return fmt.Sprintf("struct#%d[%q]", s.TypeID, s.Name)
 }
 
+func (s *Struct) TypeName() string { return s.Name }
+
 func (s *Struct) size() uint32 { return s.Size }
 
 func (s *Struct) walk(tdq *typeDeque) {
@@ -189,7 +189,7 @@ func (s *Struct) members() []Member {
 // Union is a compound type where members occupy the same memory.
 type Union struct {
 	TypeID
-	Name
+	Name string
 	// The size of the union including padding, in bytes.
 	Size    uint32
 	Members []Member
@@ -199,6 +199,8 @@ func (u *Union) String() string {
 	return fmt.Sprintf("union#%d[%q]", u.TypeID, u.Name)
 }
 
+func (u *Union) TypeName() string { return u.Name }
+
 func (u *Union) size() uint32 { return u.Size }
 
 func (u *Union) walk(tdq *typeDeque) {
@@ -236,17 +238,17 @@ var (
 //
 // It is not a valid Type.
 type Member struct {
-	Name
+	Name string
 	Type Type
-	// Offset is the bit offset of this member.
-	Offset       uint32
+	// OffsetBits is the bit offset of this member.
+	OffsetBits   uint32
 	BitfieldSize uint32
 }
 
 // Enum lists possible values.
 type Enum struct {
 	TypeID
-	Name
+	Name   string
 	Values []EnumValue
 }
 
@@ -254,11 +256,13 @@ func (e *Enum) String() string {
 	return fmt.Sprintf("enum#%d[%q]", e.TypeID, e.Name)
 }
 
+func (e *Enum) TypeName() string { return e.Name }
+
 // EnumValue is part of an Enum
 //
 // Is is not a valid Type
 type EnumValue struct {
-	Name
+	Name  string
 	Value int32
 }
 
@@ -294,7 +298,7 @@ func (fk FwdKind) String() string {
 // Fwd is a forward declaration of a Type.
 type Fwd struct {
 	TypeID
-	Name
+	Name string
 	Kind FwdKind
 }
 
@@ -302,6 +306,8 @@ func (f *Fwd) String() string {
 	return fmt.Sprintf("fwd#%d[%s %q]", f.TypeID, f.Kind, f.Name)
 }
 
+func (f *Fwd) TypeName() string { return f.Name }
+
 func (f *Fwd) walk(*typeDeque) {}
 func (f *Fwd) copy() Type {
 	cpy := *f
@@ -311,7 +317,7 @@ func (f *Fwd) copy() Type {
 // Typedef is an alias of a Type.
 type Typedef struct {
 	TypeID
-	Name
+	Name string
 	Type Type
 }
 
@@ -319,6 +325,8 @@ func (td *Typedef) String() string {
 	return fmt.Sprintf("typedef#%d[%q #%d]", td.TypeID, td.Name, td.Type.ID())
 }
 
+func (td *Typedef) TypeName() string { return td.Name }
+
 func (td *Typedef) walk(tdq *typeDeque) { tdq.push(&td.Type) }
 func (td *Typedef) copy() Type {
 	cpy := *td
@@ -379,7 +387,7 @@ func (r *Restrict) copy() Type {
 // Func is a function definition.
 type Func struct {
 	TypeID
-	Name
+	Name    string
 	Type    Type
 	Linkage FuncLinkage
 }
@@ -388,6 +396,8 @@ func (f *Func) String() string {
 	return fmt.Sprintf("func#%d[%s %q proto=#%d]", f.TypeID, f.Linkage, f.Name, f.Type.ID())
 }
 
+func (f *Func) TypeName() string { return f.Name }
+
 func (f *Func) walk(tdq *typeDeque) { tdq.push(&f.Type) }
 func (f *Func) copy() Type {
 	cpy := *f
@@ -426,14 +436,14 @@ func (fp *FuncProto) copy() Type {
 }
 
 type FuncParam struct {
-	Name
+	Name string
 	Type Type
 }
 
 // Var is a global variable.
 type Var struct {
 	TypeID
-	Name
+	Name    string
 	Type    Type
 	Linkage VarLinkage
 }
@@ -442,6 +452,8 @@ func (v *Var) String() string {
 	return fmt.Sprintf("var#%d[%s %q]", v.TypeID, v.Linkage, v.Name)
 }
 
+func (v *Var) TypeName() string { return v.Name }
+
 func (v *Var) walk(tdq *typeDeque) { tdq.push(&v.Type) }
 func (v *Var) copy() Type {
 	cpy := *v
@@ -451,7 +463,7 @@ func (v *Var) copy() Type {
 // Datasec is a global program section containing data.
 type Datasec struct {
 	TypeID
-	Name
+	Name string
 	Size uint32
 	Vars []VarSecinfo
 }
@@ -460,6 +472,8 @@ func (ds *Datasec) String() string {
 	return fmt.Sprintf("section#%d[%q]", ds.TypeID, ds.Name)
 }
 
+func (ds *Datasec) TypeName() string { return ds.Name }
+
 func (ds *Datasec) size() uint32 { return ds.Size }
 
 func (ds *Datasec) walk(tdq *typeDeque) {
@@ -487,7 +501,7 @@ type VarSecinfo struct {
 // Float is a float of a given length.
 type Float struct {
 	TypeID
-	Name
+	Name string
 
 	// The size of the float in bytes.
 	Size uint32
@@ -497,8 +511,9 @@ func (f *Float) String() string {
 	return fmt.Sprintf("float%d#%d[%q]", f.Size*8, f.TypeID, f.Name)
 }
 
-func (f *Float) size() uint32    { return f.Size }
-func (f *Float) walk(*typeDeque) {}
+func (f *Float) TypeName() string { return f.Name }
+func (f *Float) size() uint32     { return f.Size }
+func (f *Float) walk(*typeDeque)  {}
 func (f *Float) copy() Type {
 	cpy := *f
 	return &cpy
@@ -585,14 +600,36 @@ func Sizeof(typ Type) (int, error) {
 //
 // Returns any errors from transform verbatim.
 func copyType(typ Type, transform func(Type) (Type, error)) (Type, error) {
-	var (
-		copies = make(map[Type]Type)
-		work   typeDeque
-	)
+	copies := make(copier)
+	return typ, copies.copy(&typ, transform)
+}
 
-	for t := &typ; t != nil; t = work.pop() {
+// copy a slice of Types recursively.
+//
+// Types may form a cycle.
+//
+// Returns any errors from transform verbatim.
+func copyTypes(types []Type, transform func(Type) (Type, error)) ([]Type, error) {
+	result := make([]Type, len(types))
+	copy(result, types)
+
+	copies := make(copier)
+	for i := range result {
+		if err := copies.copy(&result[i], transform); err != nil {
+			return nil, err
+		}
+	}
+
+	return result, nil
+}
+
+type copier map[Type]Type
+
+func (c copier) copy(typ *Type, transform func(Type) (Type, error)) error {
+	var work typeDeque
+	for t := typ; t != nil; t = work.pop() {
 		// *t is the identity of the type.
-		if cpy := copies[*t]; cpy != nil {
+		if cpy := c[*t]; cpy != nil {
 			*t = cpy
 			continue
 		}
@@ -601,21 +638,21 @@ func copyType(typ Type, transform func(Type) (Type, error)) (Type, error) {
 		if transform != nil {
 			tf, err := transform(*t)
 			if err != nil {
-				return nil, fmt.Errorf("copy %s: %w", typ, err)
+				return fmt.Errorf("copy %s: %w", *t, err)
 			}
 			cpy = tf.copy()
 		} else {
 			cpy = (*t).copy()
 		}
 
-		copies[*t] = cpy
+		c[*t] = cpy
 		*t = cpy
 
 		// Mark any nested types for copying.
 		cpy.walk(&work)
 	}
 
-	return typ, nil
+	return nil
 }
 
 // typeDeque keeps track of pointers to types which still
@@ -626,6 +663,10 @@ type typeDeque struct {
 	mask        uint64
 }
 
+func (dq *typeDeque) empty() bool {
+	return dq.read == dq.write
+}
+
 // push adds a type to the stack.
 func (dq *typeDeque) push(t *Type) {
 	if dq.write-dq.read < uint64(len(dq.types)) {
@@ -652,7 +693,7 @@ func (dq *typeDeque) push(t *Type) {
 
 // shift returns the first element or null.
 func (dq *typeDeque) shift() *Type {
-	if dq.read == dq.write {
+	if dq.empty() {
 		return nil
 	}
 
@@ -665,7 +706,7 @@ func (dq *typeDeque) shift() *Type {
 
 // pop returns the last element or null.
 func (dq *typeDeque) pop() *Type {
-	if dq.read == dq.write {
+	if dq.empty() {
 		return nil
 	}
 
@@ -694,7 +735,7 @@ func (dq *typeDeque) all() []*Type {
 // Returns a map of named types (so, where NameOff is non-zero) and a slice of types
 // indexed by TypeID. Since BTF ignores compilation units, multiple types may share
 // the same name. A Type may form a cyclic graph by pointing at itself.
-func inflateRawTypes(rawTypes []rawType, rawStrings stringTable) (types []Type, namedTypes map[string][]namedType, err error) {
+func inflateRawTypes(rawTypes []rawType, rawStrings stringTable) (types []Type, namedTypes map[string][]NamedType, err error) {
 	type fixupDef struct {
 		id           TypeID
 		expectedKind btfKind
@@ -711,17 +752,17 @@ func inflateRawTypes(rawTypes []rawType, rawStrings stringTable) (types []Type,
 		// work, since otherwise append might re-allocate members.
 		members := make([]Member, 0, len(raw))
 		for i, btfMember := range raw {
-			name, err := rawStrings.LookupName(btfMember.NameOff)
+			name, err := rawStrings.Lookup(btfMember.NameOff)
 			if err != nil {
 				return nil, fmt.Errorf("can't get name for member %d: %w", i, err)
 			}
 			m := Member{
-				Name:   name,
-				Offset: btfMember.Offset,
+				Name:       name,
+				OffsetBits: btfMember.Offset,
 			}
 			if kindFlag {
 				m.BitfieldSize = btfMember.Offset >> 24
-				m.Offset &= 0xffffff
+				m.OffsetBits &= 0xffffff
 			}
 			members = append(members, m)
 		}
@@ -733,7 +774,7 @@ func inflateRawTypes(rawTypes []rawType, rawStrings stringTable) (types []Type,
 
 	types = make([]Type, 0, len(rawTypes))
 	types = append(types, (*Void)(nil))
-	namedTypes = make(map[string][]namedType)
+	namedTypes = make(map[string][]NamedType)
 
 	for i, raw := range rawTypes {
 		var (
@@ -743,7 +784,7 @@ func inflateRawTypes(rawTypes []rawType, rawStrings stringTable) (types []Type,
 			typ Type
 		)
 
-		name, err := rawStrings.LookupName(raw.NameOff)
+		name, err := rawStrings.Lookup(raw.NameOff)
 		if err != nil {
 			return nil, nil, fmt.Errorf("get name for type id %d: %w", id, err)
 		}
@@ -785,7 +826,7 @@ func inflateRawTypes(rawTypes []rawType, rawStrings stringTable) (types []Type,
 			rawvals := raw.data.([]btfEnum)
 			vals := make([]EnumValue, 0, len(rawvals))
 			for i, btfVal := range rawvals {
-				name, err := rawStrings.LookupName(btfVal.NameOff)
+				name, err := rawStrings.Lookup(btfVal.NameOff)
 				if err != nil {
 					return nil, nil, fmt.Errorf("get name for enum value %d: %s", i, err)
 				}
@@ -832,7 +873,7 @@ func inflateRawTypes(rawTypes []rawType, rawStrings stringTable) (types []Type,
 			rawparams := raw.data.([]btfParam)
 			params := make([]FuncParam, 0, len(rawparams))
 			for i, param := range rawparams {
-				name, err := rawStrings.LookupName(param.NameOff)
+				name, err := rawStrings.Lookup(param.NameOff)
 				if err != nil {
 					return nil, nil, fmt.Errorf("get name for func proto parameter %d: %s", i, err)
 				}
@@ -877,8 +918,8 @@ func inflateRawTypes(rawTypes []rawType, rawStrings stringTable) (types []Type,
 
 		types = append(types, typ)
 
-		if named, ok := typ.(namedType); ok {
-			if name := essentialName(named.name()); name != "" {
+		if named, ok := typ.(NamedType); ok {
+			if name := essentialName(named.TypeName()); name != "" {
 				namedTypes[name] = append(namedTypes[name], named)
 			}
 		}
diff --git a/link/freplace.go b/link/freplace.go
index 9fafb70..0eaaa01 100644
--- a/link/freplace.go
+++ b/link/freplace.go
@@ -47,12 +47,8 @@ func AttachFreplace(targetProg *ebpf.Program, name string, prog *ebpf.Program) (
 			return nil, err
 		}
 		defer btfHandle.Close()
-		spec, err := btf.HandleSpec(btfHandle)
-		if err != nil {
-			return nil, err
-		}
 
-		if err := spec.FindType(name, &function); err != nil {
+		if err := btfHandle.Spec().FindType(name, &function); err != nil {
 			return nil, err
 		}
 
diff --git a/linker.go b/linker.go
index 0338596..f3b1629 100644
--- a/linker.go
+++ b/linker.go
@@ -4,7 +4,6 @@ import (
 	"fmt"
 
 	"github.com/cilium/ebpf/asm"
-	"github.com/cilium/ebpf/internal/btf"
 )
 
 // link resolves bpf-to-bpf calls.
@@ -40,7 +39,7 @@ func link(prog *ProgramSpec, libs []*ProgramSpec) error {
 			pending = append(pending, lib.Instructions)
 
 			if prog.BTF != nil && lib.BTF != nil {
-				if err := btf.ProgramAppend(prog.BTF, lib.BTF); err != nil {
+				if err := prog.BTF.Append(lib.BTF); err != nil {
 					return fmt.Errorf("linking BTF of %s: %w", lib.Name, err)
 				}
 			}
diff --git a/map.go b/map.go
index 058766e..2ca5062 100644
--- a/map.go
+++ b/map.go
@@ -380,7 +380,7 @@ func (spec *MapSpec) createMap(inner *internal.FD, opts MapOptions, handles *han
 
 	var btfDisabled bool
 	if spec.BTF != nil {
-		handle, err := handles.btfHandle(btf.MapSpec(spec.BTF))
+		handle, err := handles.btfHandle(spec.BTF.Spec)
 		btfDisabled = errors.Is(err, btf.ErrNotSupported)
 		if err != nil && !btfDisabled {
 			return nil, fmt.Errorf("load BTF: %w", err)
@@ -388,8 +388,8 @@ func (spec *MapSpec) createMap(inner *internal.FD, opts MapOptions, handles *han
 
 		if handle != nil {
 			attr.BTFFd = uint32(handle.FD())
-			attr.BTFKeyTypeID = uint32(btf.MapKey(spec.BTF).ID())
-			attr.BTFValueTypeID = uint32(btf.MapValue(spec.BTF).ID())
+			attr.BTFKeyTypeID = uint32(spec.BTF.Key.ID())
+			attr.BTFValueTypeID = uint32(spec.BTF.Value.ID())
 		}
 	}
 
diff --git a/prog.go b/prog.go
index 4ec051b..1f3b493 100644
--- a/prog.go
+++ b/prog.go
@@ -195,12 +195,12 @@ func newProgramWithOptions(spec *ProgramSpec, opts ProgramOptions, handles *hand
 	var btfDisabled bool
 	var core btf.COREFixups
 	if spec.BTF != nil {
-		core, err = btf.ProgramFixups(spec.BTF, targetBTF)
+		core, err = spec.BTF.Fixups(targetBTF)
 		if err != nil {
 			return nil, fmt.Errorf("CO-RE relocations: %w", err)
 		}
 
-		handle, err := handles.btfHandle(btf.ProgramSpec(spec.BTF))
+		handle, err := handles.btfHandle(spec.BTF.Spec())
 		btfDisabled = errors.Is(err, btf.ErrNotSupported)
 		if err != nil && !btfDisabled {
 			return nil, fmt.Errorf("load BTF: %w", err)
@@ -209,7 +209,7 @@ func newProgramWithOptions(spec *ProgramSpec, opts ProgramOptions, handles *hand
 		if handle != nil {
 			attr.ProgBTFFd = uint32(handle.FD())
 
-			recSize, bytes, err := btf.ProgramLineInfos(spec.BTF)
+			recSize, bytes, err := spec.BTF.LineInfos()
 			if err != nil {
 				return nil, fmt.Errorf("get BTF line infos: %w", err)
 			}
@@ -217,7 +217,7 @@ func newProgramWithOptions(spec *ProgramSpec, opts ProgramOptions, handles *hand
 			attr.LineInfoCnt = uint32(uint64(len(bytes)) / uint64(recSize))
 			attr.LineInfo = internal.NewSlicePointer(bytes)
 
-			recSize, bytes, err = btf.ProgramFuncInfos(spec.BTF)
+			recSize, bytes, err = spec.BTF.FuncInfos()
 			if err != nil {
 				return nil, fmt.Errorf("get BTF function infos: %w", err)
 			}
@@ -262,7 +262,8 @@ func newProgramWithOptions(spec *ProgramSpec, opts ProgramOptions, handles *hand
 				return nil, fmt.Errorf("load target BTF: %w", err)
 			}
 			defer btfHandle.Close()
-			targetBTF, err = btf.HandleSpec(btfHandle)
+
+			targetBTF = btfHandle.Spec()
 			if err != nil {
 				return nil, fmt.Errorf("load target BTF: %w", err)
 			}


Test patch:
diff --git a/internal/btf/btf_test.go b/internal/btf/btf_test.go
index 22c7d5a..c036352 100644
--- a/internal/btf/btf_test.go
+++ b/internal/btf/btf_test.go
@@ -58,8 +58,8 @@ func TestParseVmlinux(t *testing.T) {
 			if u8int.Encoding != 0 {
 				t.Fatalf("incorrect encoding of an __u8 int: expected: 0 actual: %x", u8int.Encoding)
 			}
-			if u8int.Offset != 0 {
-				t.Fatalf("incorrect int offset of an __u8 int: expected: 0 actual: %d", u8int.Offset)
+			if u8int.OffsetBits != 0 {
+				t.Fatalf("incorrect int offset of an __u8 int: expected: 0 actual: %d", u8int.OffsetBits)
 			}
 			break
 		}
@@ -161,6 +161,37 @@ func TestLoadKernelSpec(t *testing.T) {
 	}
 }
 
+func TestSpecCopy(t *testing.T) {
+	fh, err := os.Open("../../testdata/loader-el.elf")
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer fh.Close()
+
+	spec, err := LoadSpecFromReader(fh)
+	if err != nil {
+		t.Fatal("Can't load BTF:", err)
+	}
+
+	if len(spec.types) < 1 {
+		t.Fatal("Not enough types")
+	}
+
+	cpy := spec.Copy()
+	for i := range cpy.types {
+		if _, ok := cpy.types[i].(*Void); ok {
+			// Since Void is an empty struct, a Type interface value containing
+			// &Void{} stores (*Void, nil). Since interface equality first compares
+			// the type and then the concrete value, Void is always equal.
+			continue
+		}
+
+		if cpy.types[i] == spec.types[i] {
+			t.Fatalf("Type at index %d is not a copy: %T == %T", i, cpy.types[i], spec.types[i])
+		}
+	}
+}
+
 func TestHaveBTF(t *testing.T) {
 	testutils.CheckFeatureTest(t, haveBTF)
 }
diff --git a/internal/btf/core_test.go b/internal/btf/core_test.go
index d3e614c..c5cea5d 100644
--- a/internal/btf/core_test.go
+++ b/internal/btf/core_test.go
@@ -24,7 +24,7 @@ func TestCoreAreTypesCompatible(t *testing.T) {
 		{&Enum{Name: "a"}, &Enum{Name: "b"}, true},
 		{&Fwd{Name: "a"}, &Fwd{Name: "b"}, true},
 		{&Int{Name: "a", Size: 2}, &Int{Name: "b", Size: 4}, true},
-		{&Int{Offset: 1}, &Int{}, false},
+		{&Int{OffsetBits: 1}, &Int{}, false},
 		{&Pointer{Target: &Void{}}, &Pointer{Target: &Void{}}, true},
 		{&Pointer{Target: &Void{}}, &Void{}, false},
 		{&Array{Type: &Void{}}, &Array{Type: &Void{}}, true},
@@ -98,7 +98,7 @@ func TestCoreAreMembersCompatible(t *testing.T) {
 		{&Fwd{Name: "a"}, &Fwd{Name: "a___foo"}, true},
 		{&Fwd{Name: "a"}, &Fwd{Name: ""}, true},
 		{&Int{Name: "a", Size: 2}, &Int{Name: "b", Size: 4}, true},
-		{&Int{Offset: 1}, &Int{}, false},
+		{&Int{OffsetBits: 1}, &Int{}, false},
 		{&Pointer{Target: &Void{}}, &Pointer{Target: &Void{}}, true},
 		{&Pointer{Target: &Void{}}, &Void{}, false},
 		{&Array{Type: &Int{Size: 1}}, &Array{Type: &Int{Encoding: Signed}}, true},
@@ -230,13 +230,13 @@ func TestCoreFindField(t *testing.T) {
 	u16 := &Int{Size: 2}
 	u32 := &Int{Size: 4}
 	aFields := []Member{
-		{Name: "foo", Type: ptr, Offset: 1},
-		{Name: "bar", Type: u16, Offset: 2},
+		{Name: "foo", Type: ptr, OffsetBits: 1},
+		{Name: "bar", Type: u16, OffsetBits: 2},
 	}
 	bFields := []Member{
-		{Name: "foo", Type: ptr, Offset: 10},
-		{Name: "bar", Type: u32, Offset: 20},
-		{Name: "other", Offset: 4},
+		{Name: "foo", Type: ptr, OffsetBits: 10},
+		{Name: "bar", Type: u32, OffsetBits: 20},
+		{Name: "other", OffsetBits: 4},
 	}
 	aStruct := &Struct{Members: aFields, Size: 2}
 	bStruct := &Struct{Members: bFields, Size: 7}
@@ -316,7 +316,7 @@ func TestCoreFindField(t *testing.T) {
 	}
 
 	anon := func(t Type, offset uint32) []Member {
-		return []Member{{Type: t, Offset: offset}}
+		return []Member{{Type: t, OffsetBits: offset}}
 	}
 
 	anonStruct := func(m ...Member) Member {
@@ -508,7 +508,7 @@ func TestCoreRelocation(t *testing.T) {
 					t.Fatal("Retrieve program:", err)
 				}
 
-				relos, err := ProgramFixups(prog, spec)
+				relos, err := prog.Fixups(spec)
 				if want := errs[name]; want != nil {
 					if !errors.Is(err, want) {
 						t.Fatal("Expected", want, "got", err)
diff --git a/link/freplace_test.go b/link/freplace_test.go
index 98de30d..93d5105 100644
--- a/link/freplace_test.go
+++ b/link/freplace_test.go
@@ -21,9 +21,7 @@ func TestFreplace(t *testing.T) {
 			return
 		}
 
-		target, err := ebpf.NewProgramWithOptions(spec.Programs["sched_process_exec"], ebpf.ProgramOptions{
-			LogLevel: 1,
-		})
+		target, err := ebpf.NewProgram(spec.Programs["sched_process_exec"])
 		testutils.SkipIfNotSupported(t, err)
 		if err != nil {
 			t.Fatal("Can't create target program:", err)
@@ -32,9 +30,7 @@ func TestFreplace(t *testing.T) {
 
 		// Test attachment specified at load time
 		spec.Programs["replacement"].AttachTarget = target
-		replacement, err := ebpf.NewProgramWithOptions(spec.Programs["replacement"], ebpf.ProgramOptions{
-			LogLevel: 1,
-		})
+		replacement, err := ebpf.NewProgram(spec.Programs["replacement"])
 		testutils.SkipIfNotSupported(t, err)
 		if err != nil {
 			t.Fatal("Can't create replacement program:", err)