Repository: cilium/ebpf
Pull request #888 (2022-12-13 11:26:54)

Remove deprecated `+build` Go build tags
Does what it says on the tin, remove `+build` in favour of `//go:build`. To keep this straightforward, it uses a `[][]string` internally, and the template engine ORs all inner slices and ANDs all outer slices. This causes a clean diff with everything currently in examples/.

There's one problem: specifying multiple exclusions is currently a bit broken. `-tags "!foo !bar"` will result in `//go:build (!foo || !bar)`, which cancels each other out. We could pick out all tags starting with `!` and render them out to a separate clause that's ANDed?

@lmb let me know what you think.


Patch:
diff --git a/.clang-format b/.clang-format
index 4eb94b1..3f74dc0 100644
--- a/.clang-format
+++ b/.clang-format
@@ -14,4 +14,6 @@ KeepEmptyLinesAtTheStartOfBlocks: false
 TabWidth:        4
 UseTab:          ForContinuationAndIndentation
 ColumnLimit:     1000
+# Go compiler comments need to stay unindented.
+CommentPragmas: '^go:.*'
 ...
diff --git a/.semaphore/semaphore.yml b/.semaphore/semaphore.yml
index 3c3a6e3..7280375 100644
--- a/.semaphore/semaphore.yml
+++ b/.semaphore/semaphore.yml
@@ -23,13 +23,13 @@ blocks:
       prologue:
         commands:
           - sudo sh -c 'swapoff -a && fallocate -l 2G /swapfile && chmod 0600 /swapfile && mkswap /swapfile && swapon /swapfile'
-          - sem-version go 1.19.1
+          - sem-version go 1.19.5
           - export PATH="$PATH:$(go env GOPATH)/bin"
           - checkout
           # Disabled, see https://github.com/cilium/ebpf/issues/898
           # - cache restore
           - go mod tidy
-          - curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.48.0
+          - curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.50.1
           - go install gotest.tools/gotestsum@v1.8.1
           - sudo pip3 install https://github.com/amluto/virtme/archive/beb85146cd91de37ae455eccb6ab67c393e6e290.zip
           - sudo apt-get update
diff --git a/btf/btf.go b/btf/btf.go
index a27dcd1..1ff2ce7 100644
--- a/btf/btf.go
+++ b/btf/btf.go
@@ -2,7 +2,6 @@ package btf
 
 import (
 	"bufio"
-	"bytes"
 	"debug/elf"
 	"encoding/binary"
 	"errors"
@@ -31,11 +30,9 @@ var (
 // ID represents the unique ID of a BTF object.
 type ID = sys.BTFID
 
-// Spec represents decoded BTF.
+// Spec allows querying a set of Types and loading the set into the
+// kernel.
 type Spec struct {
-	// Data from .BTF.
-	strings *stringTable
-
 	// All types contained by the spec, not including types from the base in
 	// case the spec was parsed from split BTF.
 	types []Type
@@ -43,10 +40,17 @@ type Spec struct {
 	// Type IDs indexed by type.
 	typeIDs map[Type]TypeID
 
+	// The last allocated type ID.
+	lastTypeID TypeID
+
 	// Types indexed by essential name.
 	// Includes all struct flavors and types with the same name.
 	namedTypes map[essentialName][]Type
 
+	// String table from ELF, may be nil.
+	strings *stringTable
+
+	// Byte order of the ELF we decoded the spec from, may be nil.
 	byteOrder binary.ByteOrder
 }
 
@@ -76,6 +80,18 @@ func (h *btfHeader) stringStart() int64 {
 	return int64(h.HdrLen + h.StringOff)
 }
 
+// NewSpec creates a Spec containing only Void.
+func NewSpec() *Spec {
+	return &Spec{
+		[]Type{(*Void)(nil)},
+		map[Type]TypeID{(*Void)(nil): 0},
+		0,
+		make(map[essentialName][]Type),
+		nil,
+		nil,
+	}
+}
+
 // LoadSpec opens file and calls LoadSpecFromReader on it.
 func LoadSpec(file string) (*Spec, error) {
 	fh, err := os.Open(file)
@@ -214,7 +230,6 @@ func loadSpecFromELF(file *internal.SafeELFFile) (*Spec, error) {
 
 func loadRawSpec(btf io.ReaderAt, bo binary.ByteOrder,
 	baseTypes types, baseStrings *stringTable) (*Spec, error) {
-
 	rawTypes, rawStrings, err := parseBTF(btf, bo, baseStrings)
 	if err != nil {
 		return nil, err
@@ -225,18 +240,19 @@ func loadRawSpec(btf io.ReaderAt, bo binary.ByteOrder,
 		return nil, err
 	}
 
-	typeIDs, typesByName := indexTypes(types, TypeID(len(baseTypes)))
+	typeIDs, typesByName, lastTypeID := indexTypes(types, TypeID(len(baseTypes)))
 
 	return &Spec{
 		namedTypes: typesByName,
 		typeIDs:    typeIDs,
 		types:      types,
+		lastTypeID: lastTypeID,
 		strings:    rawStrings,
 		byteOrder:  bo,
 	}, nil
 }
 
-func indexTypes(types []Type, typeIDOffset TypeID) (map[Type]TypeID, map[essentialName][]Type) {
+func indexTypes(types []Type, typeIDOffset TypeID) (map[Type]TypeID, map[essentialName][]Type, TypeID) {
 	namedTypes := 0
 	for _, typ := range types {
 		if typ.TypeName() != "" {
@@ -250,14 +266,16 @@ func indexTypes(types []Type, typeIDOffset TypeID) (map[Type]TypeID, map[essenti
 	typeIDs := make(map[Type]TypeID, len(types))
 	typesByName := make(map[essentialName][]Type, namedTypes)
 
+	var lastTypeID TypeID
 	for i, typ := range types {
 		if name := newEssentialName(typ.TypeName()); name != "" {
 			typesByName[name] = append(typesByName[name], typ)
 		}
-		typeIDs[typ] = TypeID(i) + typeIDOffset
+		lastTypeID = TypeID(i) + typeIDOffset
+		typeIDs[typ] = lastTypeID
 	}
 
-	return typeIDs, typesByName
+	return typeIDs, typesByName, lastTypeID
 }
 
 // LoadKernelSpec returns the current kernel's BTF information.
@@ -469,15 +487,15 @@ func fixupDatasec(types []Type, sectionSizes map[string]uint32, offsets map[symb
 // Copy creates a copy of Spec.
 func (s *Spec) Copy() *Spec {
 	types := copyTypes(s.types, nil)
-
-	typeIDs, typesByName := indexTypes(types, s.firstTypeID())
+	typeIDs, typesByName, lastTypeID := indexTypes(types, s.firstTypeID())
 
 	// NB: Other parts of spec are not copied since they are immutable.
 	return &Spec{
-		s.strings,
 		types,
 		typeIDs,
+		lastTypeID,
 		typesByName,
+		s.strings,
 		s.byteOrder,
 	}
 }
@@ -492,6 +510,36 @@ func (sw sliceWriter) Write(p []byte) (int, error) {
 	return copy(sw, p), nil
 }
 
+// Add a Type to the Spec, making it queryable via [TypeByName], etc.
+//
+// Adding the identical Type multiple times is valid and will return a stable ID.
+//
+// See [Type] for details on identity.
+func (s *Spec) Add(typ Type) (TypeID, error) {
+	if typ == nil {
+		return 0, fmt.Errorf("can't add nil Type")
+	}
+
+	if id, err := s.TypeID(typ); err == nil {
+		return id, nil
+	}
+
+	id := s.lastTypeID + 1
+	if id < s.lastTypeID {
+		return 0, fmt.Errorf("type ID overflow")
+	}
+
+	s.typeIDs[typ] = id
+	s.types = append(s.types, typ)
+	s.lastTypeID = id
+
+	if name := newEssentialName(typ.TypeName()); name != "" {
+		s.namedTypes[name] = append(s.namedTypes[name], typ)
+	}
+
+	return id, nil
+}
+
 // TypeByID returns the BTF Type with the given type ID.
 //
 // Returns an error wrapping ErrNotFound if a Type with the given ID
@@ -638,6 +686,10 @@ func (s *Spec) firstTypeID() TypeID {
 // Types from base are used to resolve references in the split BTF.
 // The returned Spec only contains types from the split BTF, not from the base.
 func LoadSplitSpecFromReader(r io.ReaderAt, base *Spec) (*Spec, error) {
+	if base.strings == nil {
+		return nil, fmt.Errorf("parse split BTF: base must be loaded from an ELF")
+	}
+
 	return loadRawSpec(r, internal.NativeEndian, base.types, base.strings)
 }
 
@@ -665,55 +717,15 @@ func (iter *TypesIterator) Next() bool {
 	return true
 }
 
-func marshalBTF(types interface{}, strings []byte, bo binary.ByteOrder) []byte {
-	const minHeaderLength = 24
-
-	typesLen := uint32(binary.Size(types))
-	header := btfHeader{
-		Magic:     btfMagic,
-		Version:   1,
-		HdrLen:    minHeaderLength,
-		TypeOff:   0,
-		TypeLen:   typesLen,
-		StringOff: typesLen,
-		StringLen: uint32(len(strings)),
-	}
-
-	buf := new(bytes.Buffer)
-	_ = binary.Write(buf, bo, &header)
-	_ = binary.Write(buf, bo, types)
-	buf.Write(strings)
-
-	return buf.Bytes()
-}
-
 // haveBTF attempts to load a BTF blob containing an Int. It should pass on any
 // kernel that supports BPF_BTF_LOAD.
 var haveBTF = internal.NewFeatureTest("BTF", "4.18", func() error {
-	var (
-		types struct {
-			Integer btfType
-			btfInt
-		}
-		strings = []byte{0}
-	)
-	types.Integer.SetKind(kindInt) // 0-length anonymous integer
-
-	btf := marshalBTF(&types, strings, internal.NativeEndian)
-
-	fd, err := sys.BtfLoad(&sys.BtfLoadAttr{
-		Btf:     sys.NewSlicePointer(btf),
-		BtfSize: uint32(len(btf)),
-	})
+	// 0-length anonymous integer
+	err := probeBTF(&Int{})
 	if errors.Is(err, unix.EINVAL) || errors.Is(err, unix.EPERM) {
 		return internal.ErrNotSupported
 	}
-	if err != nil {
-		return err
-	}
-
-	fd.Close()
-	return nil
+	return err
 })
 
 // haveMapBTF attempts to load a minimal BTF blob containing a Var. It is
@@ -724,37 +736,18 @@ var haveMapBTF = internal.NewFeatureTest("Map BTF (Var/Datasec)", "5.2", func()
 		return err
 	}
 
-	var (
-		types struct {
-			Integer btfType
-			Var     btfType
-			btfVariable
-		}
-		strings = []byte{0, 'a', 0}
-	)
-
-	types.Integer.SetKind(kindPointer)
-	types.Var.NameOff = 1
-	types.Var.SetKind(kindVar)
-	types.Var.SizeType = 1
-
-	btf := marshalBTF(&types, strings, internal.NativeEndian)
+	v := &Var{
+		Name: "a",
+		Type: &Pointer{(*Void)(nil)},
+	}
 
-	fd, err := sys.BtfLoad(&sys.BtfLoadAttr{
-		Btf:     sys.NewSlicePointer(btf),
-		BtfSize: uint32(len(btf)),
-	})
+	err := probeBTF(v)
 	if errors.Is(err, unix.EINVAL) || errors.Is(err, unix.EPERM) {
 		// Treat both EINVAL and EPERM as not supported: creating the map may still
 		// succeed without Btf* attrs.
 		return internal.ErrNotSupported
 	}
-	if err != nil {
-		return err
-	}
-
-	fd.Close()
-	return nil
+	return err
 })
 
 // haveProgBTF attempts to load a BTF blob containing a Func and FuncProto. It
@@ -765,34 +758,16 @@ var haveProgBTF = internal.NewFeatureTest("Program BTF (func/line_info)", "5.0",
 		return err
 	}
 
-	var (
-		types struct {
-			FuncProto btfType
-			Func      btfType
-		}
-		strings = []byte{0, 'a', 0}
-	)
-
-	types.FuncProto.SetKind(kindFuncProto)
-	types.Func.SetKind(kindFunc)
-	types.Func.SizeType = 1 // aka FuncProto
-	types.Func.NameOff = 1
-
-	btf := marshalBTF(&types, strings, internal.NativeEndian)
+	fn := &Func{
+		Name: "a",
+		Type: &FuncProto{Return: (*Void)(nil)},
+	}
 
-	fd, err := sys.BtfLoad(&sys.BtfLoadAttr{
-		Btf:     sys.NewSlicePointer(btf),
-		BtfSize: uint32(len(btf)),
-	})
+	err := probeBTF(fn)
 	if errors.Is(err, unix.EINVAL) || errors.Is(err, unix.EPERM) {
 		return internal.ErrNotSupported
 	}
-	if err != nil {
-		return err
-	}
-
-	fd.Close()
-	return nil
+	return err
 })
 
 var haveFuncLinkage = internal.NewFeatureTest("BTF func linkage", "5.6", func() error {
@@ -800,33 +775,35 @@ var haveFuncLinkage = internal.NewFeatureTest("BTF func linkage", "5.6", func()
 		return err
 	}
 
-	var (
-		types struct {
-			FuncProto btfType
-			Func      btfType
-		}
-		strings = []byte{0, 'a', 0}
-	)
-
-	types.FuncProto.SetKind(kindFuncProto)
-	types.Func.SetKind(kindFunc)
-	types.Func.SizeType = 1 // aka FuncProto
-	types.Func.NameOff = 1
-	types.Func.SetLinkage(GlobalFunc)
-
-	btf := marshalBTF(&types, strings, internal.NativeEndian)
+	fn := &Func{
+		Name:    "a",
+		Type:    &FuncProto{Return: (*Void)(nil)},
+		Linkage: GlobalFunc,
+	}
 
-	fd, err := sys.BtfLoad(&sys.BtfLoadAttr{
-		Btf:     sys.NewSlicePointer(btf),
-		BtfSize: uint32(len(btf)),
-	})
+	err := probeBTF(fn)
 	if errors.Is(err, unix.EINVAL) {
 		return internal.ErrNotSupported
 	}
-	if err != nil {
+	return err
+})
+
+func probeBTF(typ Type) error {
+	buf := getBuffer()
+	defer putBuffer(buf)
+
+	if err := marshalTypes(buf, []Type{&Void{}, typ}, nil, nil); err != nil {
 		return err
 	}
 
-	fd.Close()
-	return nil
-})
+	fd, err := sys.BtfLoad(&sys.BtfLoadAttr{
+		Btf:     sys.NewSlicePointer(buf.Bytes()),
+		BtfSize: uint32(buf.Len()),
+	})
+
+	if err == nil {
+		fd.Close()
+	}
+
+	return err
+}
diff --git a/btf/btf_types.go b/btf/btf_types.go
index dc568a9..a253b7c 100644
--- a/btf/btf_types.go
+++ b/btf/btf_types.go
@@ -4,6 +4,7 @@ import (
 	"encoding/binary"
 	"fmt"
 	"io"
+	"unsafe"
 )
 
 //go:generate stringer -linecomment -output=btf_types_string.go -type=FuncLinkage,VarLinkage,btfKind
@@ -193,13 +194,22 @@ func (bt *btfType) SetSize(size uint32) {
 	bt.SizeType = size
 }
 
+func (bt *btfType) Marshal(w io.Writer, bo binary.ByteOrder) error {
+	buf := make([]byte, unsafe.Sizeof(*bt))
+	bo.PutUint32(buf[0:], bt.NameOff)
+	bo.PutUint32(buf[4:], bt.Info)
+	bo.PutUint32(buf[8:], bt.SizeType)
+	_, err := w.Write(buf)
+	return err
+}
+
 type rawType struct {
 	btfType
 	data interface{}
 }
 
 func (rt *rawType) Marshal(w io.Writer, bo binary.ByteOrder) error {
-	if err := binary.Write(w, bo, &rt.btfType); err != nil {
+	if err := rt.btfType.Marshal(w, bo); err != nil {
 		return err
 	}
 
diff --git a/btf/ext_info.go b/btf/ext_info.go
index 36f3b7b..dba36d6 100644
--- a/btf/ext_info.go
+++ b/btf/ext_info.go
@@ -8,7 +8,6 @@ import (
 	"io"
 	"math"
 	"sort"
-	"sync"
 
 	"github.com/cilium/ebpf/asm"
 	"github.com/cilium/ebpf/internal"
@@ -131,12 +130,6 @@ func (ei *ExtInfos) Assign(insns asm.Instructions, section string) {
 	}
 }
 
-var nativeEncoderPool = sync.Pool{
-	New: func() any {
-		return newEncoder(kernelEncoderOptions, nil)
-	},
-}
-
 // MarshalExtInfos encodes function and line info embedded in insns into kernel
 // wire format.
 //
@@ -157,15 +150,11 @@ func MarshalExtInfos(insns asm.Instructions) (_ *Handle, funcInfos, lineInfos []
 		}
 	}
 
-	// Avoid allocating encoder, etc. if there is no BTF at all.
 	return nil, nil, nil, nil
 
 marshal:
-	enc := nativeEncoderPool.Get().(*encoder)
-	defer nativeEncoderPool.Put(enc)
-
-	enc.Reset()
-
+	stb := newStringTableBuilder(0)
+	spec := NewSpec()
 	var fiBuf, liBuf bytes.Buffer
 	for {
 		if fn := FuncMetadata(iter.Ins); fn != nil {
@@ -173,7 +162,7 @@ marshal:
 				fn:     fn,
 				offset: iter.Offset,
 			}
-			if err := fi.marshal(&fiBuf, enc); err != nil {
+			if err := fi.marshal(&fiBuf, spec); err != nil {
 				return nil, nil, nil, fmt.Errorf("write func info: %w", err)
 			}
 		}
@@ -183,7 +172,7 @@ marshal:
 				line:   line,
 				offset: iter.Offset,
 			}
-			if err := li.marshal(&liBuf, enc.strings); err != nil {
+			if err := li.marshal(&liBuf, stb); err != nil {
 				return nil, nil, nil, fmt.Errorf("write line info: %w", err)
 			}
 		}
@@ -193,12 +182,14 @@ marshal:
 		}
 	}
 
-	btf, err := enc.Encode()
-	if err != nil {
-		return nil, nil, nil, err
+	buf := getBuffer()
+	defer putBuffer(buf)
+
+	if err := marshalTypes(buf, spec.types, stb, kernelMarshalOptions); err != nil {
+		return nil, nil, nil, fmt.Errorf("marshal BTF: %w", err)
 	}
 
-	handle, err := newHandleFromRawBTF(btf)
+	handle, err := newHandleFromRawBTF(buf.Bytes())
 	return handle, fiBuf.Bytes(), liBuf.Bytes(), err
 }
 
@@ -392,8 +383,8 @@ func newFuncInfos(bfis []bpfFuncInfo, ts types) ([]funcInfo, error) {
 }
 
 // marshal into the BTF wire format.
-func (fi *funcInfo) marshal(w *bytes.Buffer, enc *encoder) error {
-	id, err := enc.Add(fi.fn)
+func (fi *funcInfo) marshal(w *bytes.Buffer, spec *Spec) error {
+	id, err := spec.Add(fi.fn)
 	if err != nil {
 		return err
 	}
diff --git a/btf/handle.go b/btf/handle.go
index 9a864d1..75102db 100644
--- a/btf/handle.go
+++ b/btf/handle.go
@@ -30,21 +30,27 @@ func NewHandle(spec *Spec) (*Handle, error) {
 		return nil, fmt.Errorf("can't load %s BTF on %s", spec.byteOrder, internal.NativeEndian)
 	}
 
-	enc := newEncoder(kernelEncoderOptions, newStringTableBuilderFromTable(spec.strings))
+	if spec.firstTypeID() != 0 {
+		return nil, fmt.Errorf("split BTF can't be loaded into the kernel")
+	}
 
-	for _, typ := range spec.types {
-		_, err := enc.Add(typ)
-		if err != nil {
-			return nil, fmt.Errorf("add %s: %w", typ, err)
-		}
+	buf := getBuffer()
+	defer putBuffer(buf)
+
+	var stb *stringTableBuilder
+	if spec.strings != nil {
+		// Use the ELF string table as an estimate of the final
+		// string table size. We don't use the ELF string
+		// table since the types may have been changed in the meantime.
+		stb = newStringTableBuilder(spec.strings.Num())
 	}
 
-	btf, err := enc.Encode()
+	err := marshalTypes(buf, spec.types, stb, kernelMarshalOptions)
 	if err != nil {
 		return nil, fmt.Errorf("marshal BTF: %w", err)
 	}
 
-	return newHandleFromRawBTF(btf)
+	return newHandleFromRawBTF(buf.Bytes())
 }
 
 func newHandleFromRawBTF(btf []byte) (*Handle, error) {
diff --git a/btf/marshal.go b/btf/marshal.go
index 4ae479b..68cd053 100644
--- a/btf/marshal.go
+++ b/btf/marshal.go
@@ -6,141 +6,114 @@ import (
 	"errors"
 	"fmt"
 	"math"
+	"sync"
 
 	"github.com/cilium/ebpf/internal"
 )
 
-type encoderOptions struct {
-	ByteOrder binary.ByteOrder
+type marshalOptions struct {
 	// Remove function linkage information for compatibility with <5.6 kernels.
 	StripFuncLinkage bool
 }
 
-// kernelEncoderOptions will generate BTF suitable for the current kernel.
-var kernelEncoderOptions encoderOptions
-
-func init() {
-	kernelEncoderOptions = encoderOptions{
-		ByteOrder:        internal.NativeEndian,
-		StripFuncLinkage: haveFuncLinkage() != nil,
-	}
+// kernelMarshalOptions will generate BTF suitable for the current kernel.
+var kernelMarshalOptions = &marshalOptions{
+	StripFuncLinkage: haveFuncLinkage() != nil,
 }
 
 // encoder turns Types into raw BTF.
 type encoder struct {
-	opts encoderOptions
-
-	buf          *bytes.Buffer
-	strings      *stringTableBuilder
-	allocatedIDs map[Type]TypeID
-	nextID       TypeID
-	// Temporary storage for Add.
-	pending internal.Deque[Type]
-	// Temporary storage for deflateType.
-	raw rawType
+	marshalOptions
+
+	byteOrder binary.ByteOrder
+	pending   internal.Deque[Type]
+	buf       *bytes.Buffer
+	strings   *stringTableBuilder
+	ids       map[Type]TypeID
+	lastID    TypeID
 }
 
-// newEncoder returns a new builder for the given byte order.
-//
-// See [KernelEncoderOptions] to build BTF for the current system.
-func newEncoder(opts encoderOptions, strings *stringTableBuilder) *encoder {
-	enc := &encoder{
-		opts: opts,
-		buf:  bytes.NewBuffer(make([]byte, btfHeaderLen)),
-	}
-	enc.reset(strings)
-	return enc
-}
+var emptyBTFHeader = make([]byte, btfHeaderLen)
 
-// Reset internal state to be able to reuse the Encoder.
-func (e *encoder) Reset() {
-	e.reset(nil)
+var bufferPool = sync.Pool{
+	New: func() any {
+		return bytes.NewBuffer(make([]byte, btfHeaderLen+128))
+	},
 }
 
-func (e *encoder) reset(strings *stringTableBuilder) {
-	if strings == nil {
-		strings = newStringTableBuilder()
-	}
+func getBuffer() *bytes.Buffer {
+	buf := bufferPool.Get().(*bytes.Buffer)
+	buf.Reset()
+	return buf
+}
 
-	e.buf.Truncate(btfHeaderLen)
-	e.strings = strings
-	e.allocatedIDs = make(map[Type]TypeID)
-	e.nextID = 1
+func putBuffer(buf *bytes.Buffer) {
+	bufferPool.Put(buf)
 }
 
-// Add a Type.
+// marshalTypes encodes a slice of types into BTF wire format.
+//
+// types are guaranteed to be written in the order they are passed to this
+// function. The first type must always be Void.
 //
-// Adding the same Type multiple times is valid and will return a stable ID.
+// Doesn't support encoding split BTF since it's not possible to load
+// that into the kernel and we don't have a use case for writing BTF
+// out again.
 //
-// Calling the method has undefined behaviour if it previously returned an error.
-func (e *encoder) Add(typ Type) (TypeID, error) {
-	if typ == nil {
-		return 0, errors.New("cannot Add a nil Type")
+// w should be retrieved from bufferPool. opts may be nil.
+func marshalTypes(w *bytes.Buffer, types []Type, stb *stringTableBuilder, opts *marshalOptions) error {
+	if len(types) < 1 {
+		return errors.New("types must contain at least Void")
 	}
 
-	hasID := func(t Type) (skip bool) {
-		_, isVoid := t.(*Void)
-		_, alreadyEncoded := e.allocatedIDs[t]
-		return isVoid || alreadyEncoded
+	if _, ok := types[0].(*Void); !ok {
+		return fmt.Errorf("first type is %s, not Void", types[0])
 	}
+	types = types[1:]
 
-	e.pending.Reset()
-
-	allocateID := func(typ Type) {
-		e.pending.Push(typ)
-		e.allocatedIDs[typ] = e.nextID
-		e.nextID++
+	if stb == nil {
+		stb = newStringTableBuilder(0)
 	}
 
-	iter := postorderTraversal(typ, hasID)
-	for iter.Next() {
-		if hasID(iter.Type) {
-			// This type is part of a cycle and we've already deflated it.
-			continue
-		}
-
-		// Allocate an ID for the next type.
-		allocateID(iter.Type)
-
-		for !e.pending.Empty() {
-			t := e.pending.Shift()
+	e := encoder{
+		byteOrder: internal.NativeEndian,
+		buf:       w,
+		strings:   stb,
+		ids:       make(map[Type]TypeID, len(types)),
+	}
 
-			// Ensure that all direct descendants have been allocated an ID
-			// before calling deflateType.
-			walkType(t, func(child *Type) {
-				if !hasID(*child) {
-					// t refers to a type which hasn't been allocated an ID
-					// yet, which only happens for circular types.
-					allocateID(*child)
-				}
-			})
+	if opts != nil {
+		e.marshalOptions = *opts
+	}
 
-			if err := e.deflateType(t); err != nil {
-				return 0, fmt.Errorf("deflate %s: %w", t, err)
-			}
+	// Ensure that passed types are marshaled in the exact order they were
+	// passed.
+	e.pending.Grow(len(types))
+	for _, typ := range types {
+		if err := e.allocateID(typ); err != nil {
+			return err
 		}
 	}
 
-	return e.allocatedIDs[typ], nil
-}
-
-// Encode the raw BTF blob.
-//
-// The returned slice is valid until the next call to Add.
-func (e *encoder) Encode() ([]byte, error) {
-	length := e.buf.Len()
+	// Reserve space for the BTF header.
+	_, _ = e.buf.Write(emptyBTFHeader)
 
-	// Truncate the string table on return to allow adding more types.
-	defer e.buf.Truncate(length)
+	if err := e.deflatePending(); err != nil {
+		return err
+	}
 
+	length := e.buf.Len()
 	typeLen := uint32(length - btfHeaderLen)
 
 	// Reserve space for the string table.
 	stringLen := e.strings.Length()
 	e.buf.Grow(stringLen)
+	buf := e.strings.AppendEncoded(e.buf.Bytes())
 
-	buf := e.buf.Bytes()[:length+stringLen]
-	e.strings.MarshalBuffer(buf[length:])
+	// Add string table to the unread portion of the buffer, otherwise
+	// it isn't return by Bytes().
+	// The copy is optimized out since src == dst.
+	_, _ = e.buf.Write(buf[length:])
 
 	// Fill out the header, and write it out.
 	header := &btfHeader{
@@ -154,23 +127,102 @@ func (e *encoder) Encode() ([]byte, error) {
 		StringLen: uint32(stringLen),
 	}
 
-	err := binary.Write(sliceWriter(buf[:btfHeaderLen]), e.opts.ByteOrder, header)
+	err := binary.Write(sliceWriter(buf[:btfHeaderLen]), e.byteOrder, header)
 	if err != nil {
-		return nil, fmt.Errorf("can't write header: %v", err)
+		return fmt.Errorf("write header: %v", err)
+	}
+
+	return nil
+}
+
+func (e *encoder) allocateID(typ Type) error {
+	id := e.lastID + 1
+	if id < e.lastID {
+		return errors.New("type ID overflow")
+	}
+
+	e.pending.Push(typ)
+	e.ids[typ] = id
+	e.lastID = id
+	return nil
+}
+
+// id returns the ID for the given type or panics with an error.
+func (e *encoder) id(typ Type) TypeID {
+	if _, ok := typ.(*Void); ok {
+		return 0
+	}
+
+	id, ok := e.ids[typ]
+	if !ok {
+		panic(fmt.Errorf("no ID for type %v", typ))
+	}
+
+	return id
+}
+
+func (e *encoder) deflatePending() error {
+	// Declare root outside of the loop to avoid repeated heap allocations.
+	var root Type
+	skip := func(t Type) (skip bool) {
+		if t == root {
+			// Force descending into the current root type even if it already
+			// has an ID. Otherwise we miss children of types that have their
+			// ID pre-allocated in marshalTypes.
+			return false
+		}
+
+		_, isVoid := t.(*Void)
+		_, alreadyEncoded := e.ids[t]
+		return isVoid || alreadyEncoded
+	}
+
+	for !e.pending.Empty() {
+		root = e.pending.Shift()
+
+		// Allocate IDs for all children of typ, including transitive dependencies.
+		iter := postorderTraversal(root, skip)
+		for iter.Next() {
+			if iter.Type == root {
+				// The iterator yields root at the end, do not allocate another ID.
+				break
+			}
+
+			if err := e.allocateID(iter.Type); err != nil {
+				return err
+			}
+		}
+
+		if err := e.deflateType(root); err != nil {
+			id := e.ids[root]
+			return fmt.Errorf("deflate %v with ID %d: %w", root, id, err)
+		}
 	}
 
-	return buf, nil
+	return nil
 }
 
 func (e *encoder) deflateType(typ Type) (err error) {
-	raw := &e.raw
-	*raw = rawType{}
+	defer func() {
+		if r := recover(); r != nil {
+			var ok bool
+			err, ok = r.(error)
+			if !ok {
+				panic(r)
+			}
+		}
+	}()
+
+	var raw rawType
 	raw.NameOff, err = e.strings.Add(typ.TypeName())
 	if err != nil {
 		return err
 	}
 
 	switch v := typ.(type) {
+	case *Void:
+		return errors.New("Void is implicit in BTF wire format")
+
 	case *Int:
 		raw.SetKind(kindInt)
 		raw.SetSize(v.Size)
@@ -184,13 +236,13 @@ func (e *encoder) deflateType(typ Type) (err error) {
 
 	case *Pointer:
 		raw.SetKind(kindPointer)
-		raw.SetType(e.allocatedIDs[v.Target])
+		raw.SetType(e.id(v.Target))
 
 	case *Array:
 		raw.SetKind(kindArray)
 		raw.data = &btfArray{
-			e.allocatedIDs[v.Type],
-			e.allocatedIDs[v.Index],
+			e.id(v.Type),
+			e.id(v.Index),
 			v.Nelems,
 		}
 
@@ -223,36 +275,36 @@ func (e *encoder) deflateType(typ Type) (err error) {
 
 	case *Typedef:
 		raw.SetKind(kindTypedef)
-		raw.SetType(e.allocatedIDs[v.Type])
+		raw.SetType(e.id(v.Type))
 
 	case *Volatile:
 		raw.SetKind(kindVolatile)
-		raw.SetType(e.allocatedIDs[v.Type])
+		raw.SetType(e.id(v.Type))
 
 	case *Const:
 		raw.SetKind(kindConst)
-		raw.SetType(e.allocatedIDs[v.Type])
+		raw.SetType(e.id(v.Type))
 
 	case *Restrict:
 		raw.SetKind(kindRestrict)
-		raw.SetType(e.allocatedIDs[v.Type])
+		raw.SetType(e.id(v.Type))
 
 	case *Func:
 		raw.SetKind(kindFunc)
-		raw.SetType(e.allocatedIDs[v.Type])
-		if !e.opts.StripFuncLinkage {
+		raw.SetType(e.id(v.Type))
+		if !e.StripFuncLinkage {
 			raw.SetLinkage(v.Linkage)
 		}
 
 	case *FuncProto:
 		raw.SetKind(kindFuncProto)
-		raw.SetType(e.allocatedIDs[v.Return])
+		raw.SetType(e.id(v.Return))
 		raw.SetVlen(len(v.Params))
 		raw.data, err = e.deflateFuncParams(v.Params)
 
 	case *Var:
 		raw.SetKind(kindVar)
-		raw.SetType(e.allocatedIDs[v.Type])
+		raw.SetType(e.id(v.Type))
 		raw.data = btfVariable{uint32(v.Linkage)}
 
 	case *Datasec:
@@ -281,7 +333,7 @@ func (e *encoder) deflateType(typ Type) (err error) {
 		return err
 	}
 
-	return raw.Marshal(e.buf, e.opts.ByteOrder)
+	return raw.Marshal(e.buf, e.byteOrder)
 }
 
 func (e *encoder) convertMembers(header *btfType, members []Member) ([]btfMember, error) {
@@ -302,7 +354,7 @@ func (e *encoder) convertMembers(header *btfType, members []Member) ([]btfMember
 
 		bms = append(bms, btfMember{
 			nameOff,
-			e.allocatedIDs[member.Type],
+			e.id(member.Type),
 			uint32(offset),
 		})
 	}
@@ -361,7 +413,7 @@ func (e *encoder) deflateFuncParams(params []FuncParam) ([]btfParam, error) {
 
 		bps = append(bps, btfParam{
 			nameOff,
-			e.allocatedIDs[param.Type],
+			e.id(param.Type),
 		})
 	}
 	return bps, nil
@@ -371,7 +423,7 @@ func (e *encoder) deflateVarSecinfos(vars []VarSecinfo) []btfVarSecinfo {
 	vsis := make([]btfVarSecinfo, 0, len(vars))
 	for _, v := range vars {
 		vsis = append(vsis, btfVarSecinfo{
-			e.allocatedIDs[v.Type],
+			e.id(v.Type),
 			v.Offset,
 			v.Size,
 		})
@@ -383,33 +435,24 @@ func (e *encoder) deflateVarSecinfos(vars []VarSecinfo) []btfVarSecinfo {
 //
 // The function is intended for the use of the ebpf package and may be removed
 // at any point in time.
-func MarshalMapKV(key, value Type) (_ *Handle, keyID, valueID TypeID, _ error) {
-	enc := nativeEncoderPool.Get().(*encoder)
-	defer nativeEncoderPool.Put(enc)
+func MarshalMapKV(key, value Type) (_ *Handle, keyID, valueID TypeID, err error) {
+	spec := NewSpec()
 
-	enc.Reset()
-
-	var err error
 	if key != nil {
-		keyID, err = enc.Add(key)
+		keyID, err = spec.Add(key)
 		if err != nil {
-			return nil, 0, 0, fmt.Errorf("adding map key to BTF encoder: %w", err)
+			return nil, 0, 0, fmt.Errorf("add key type: %w", err)
 		}
 	}
 
 	if value != nil {
-		valueID, err = enc.Add(value)
+		valueID, err = spec.Add(value)
 		if err != nil {
-			return nil, 0, 0, fmt.Errorf("adding map value to BTF encoder: %w", err)
+			return nil, 0, 0, fmt.Errorf("add value type: %w", err)
 		}
 	}
 
-	btf, err := enc.Encode()
-	if err != nil {
-		return nil, 0, 0, fmt.Errorf("marshal BTF: %w", err)
-	}
-
-	handle, err := newHandleFromRawBTF(btf)
+	handle, err := NewHandle(spec)
 	if err != nil {
 		// Check for 'full' map BTF support, since kernels between 4.18 and 5.2
 		// already support BTF blobs for maps without Var or Datasec just fine.
@@ -417,6 +460,5 @@ func MarshalMapKV(key, value Type) (_ *Handle, keyID, valueID TypeID, _ error) {
 			return nil, 0, 0, err
 		}
 	}
-
 	return handle, keyID, valueID, err
 }
diff --git a/btf/strings.go b/btf/strings.go
index deeaeac..d0eb604 100644
--- a/btf/strings.go
+++ b/btf/strings.go
@@ -89,15 +89,6 @@ func (st *stringTable) lookup(offset uint32) (string, error) {
 	return st.strings[i], nil
 }
 
-func (st *stringTable) Length() int {
-	if len(st.offsets) == 0 || len(st.strings) == 0 {
-		return 0
-	}
-
-	last := len(st.offsets) - 1
-	return int(st.offsets[last]) + len(st.strings[last]) + 1
-}
-
 func (st *stringTable) Marshal(w io.Writer) error {
 	for _, str := range st.strings {
 		_, err := io.WriteString(w, str)
@@ -112,6 +103,11 @@ func (st *stringTable) Marshal(w io.Writer) error {
 	return nil
 }
 
+// Num returns the number of strings in the table.
+func (st *stringTable) Num() int {
+	return len(st.strings)
+}
+
 // search is a copy of sort.Search specialised for uint32.
 //
 // Licensed under https://go.dev/LICENSE
@@ -141,25 +137,19 @@ type stringTableBuilder struct {
 // newStringTableBuilder creates a builder with the given capacity.
 //
 // capacity may be zero.
-func newStringTableBuilder() *stringTableBuilder {
-	stb := &stringTableBuilder{0, make(map[string]uint32)}
-	// Ensure that the empty string is at index 0.
-	stb.append("")
-	return stb
-}
+func newStringTableBuilder(capacity int) *stringTableBuilder {
+	var stb stringTableBuilder
 
-// newStringTableBuilderFromTable creates a new builder from an existing string table.
-func newStringTableBuilderFromTable(contents *stringTable) *stringTableBuilder {
-	stb := &stringTableBuilder{0, make(map[string]uint32, len(contents.strings)+1)}
-	stb.append("")
-
-	for _, str := range contents.strings {
-		if str != "" {
-			stb.append(str)
-		}
+	if capacity == 0 {
+		// Use the runtime's small default size.
+		stb.strings = make(map[string]uint32)
+	} else {
+		stb.strings = make(map[string]uint32, capacity)
 	}
 
-	return stb
+	// Ensure that the empty string is at index 0.
+	stb.append("")
+	return &stb
 }
 
 // Add a string to the table.
@@ -203,19 +193,13 @@ func (stb *stringTableBuilder) Length() int {
 	return int(stb.length)
 }
 
-// Marshal a string table into its binary representation.
-func (stb *stringTableBuilder) Marshal() []byte {
-	buf := make([]byte, stb.Length())
-	stb.MarshalBuffer(buf)
-	return buf
-}
-
-// Marshal a string table into a pre-allocated buffer.
-//
-// The buffer must be at least of size Length().
-func (stb *stringTableBuilder) MarshalBuffer(buf []byte) {
+// AppendEncoded appends the string table to the end of the provided buffer.
+func (stb *stringTableBuilder) AppendEncoded(buf []byte) []byte {
+	n := len(buf)
+	buf = append(buf, make([]byte, stb.Length())...)
+	strings := buf[n:]
 	for str, offset := range stb.strings {
-		n := copy(buf[offset:], str)
-		buf[offset+uint32(n)] = 0
+		copy(strings[offset:], str)
 	}
+	return buf
 }
diff --git a/btf/traversal.go b/btf/traversal.go
index fa42815..a3a9dec 100644
--- a/btf/traversal.go
+++ b/btf/traversal.go
@@ -15,7 +15,7 @@ type postorderIterator struct {
 	// The root type. May be nil if skip(root) is true.
 	root Type
 
-	// Contains types which need to be either walked or passed to the callback.
+	// Contains types which need to be either walked or yielded.
 	types typeDeque
 	// Contains a boolean whether the type has been walked or not.
 	walked internal.Deque[bool]
@@ -26,9 +26,8 @@ type postorderIterator struct {
 	Type Type
 }
 
-// postorderTraversal calls fn for all types reachable from root.
-//
-// fn is invoked on children of root before root itself.
+// postorderTraversal iterates all types reachable from root by visiting the
+// leaves of the graph first.
 //
 // Types for which skip returns true are ignored. skip may be nil.
 func postorderTraversal(root Type, skip func(Type) (skip bool)) postorderIterator {
diff --git a/btf/types.go b/btf/types.go
index e344bbd..970b8bc 100644
--- a/btf/types.go
+++ b/btf/types.go
@@ -17,6 +17,17 @@ const maxTypeDepth = 32
 type TypeID uint32
 
 // Type represents a type described by BTF.
+//
+// Identity of Type follows the [Go specification]: two Types are considered
+// equal if they have the same concrete type and the same dynamic value, aka
+// they point at the same location in memory. This means that the following
+// Types are considered distinct even though they have the same "shape".
+//
+//	a := &Int{Size: 1}
+//	b := &Int{Size: 1}
+//	a != b
+//
+// [Go specification]: https://go.dev/ref/spec#Comparison_operators
 type Type interface {
 	// Type can be formatted using the %s and %v verbs. %s outputs only the
 	// identity of the type, without any detail. %v outputs additional detail.
diff --git a/cmd/bpf2go/flags.go b/cmd/bpf2go/flags.go
new file mode 100644
index 0000000..ca8852a
--- /dev/null
+++ b/cmd/bpf2go/flags.go
@@ -0,0 +1,57 @@
+package main
+
+import (
+	"flag"
+	"go/build/constraint"
+)
+
+// buildTags is a comma-separated list of build tags.
+//
+// This follows the pre-Go 1.17 syntax and is kept for compatibility reasons.
+type buildTags struct {
+	Expr constraint.Expr
+}
+
+var _ flag.Value = (*buildTags)(nil)
+
+func (bt *buildTags) String() string {
+	if bt.Expr == nil {
+		return ""
+	}
+
+	return (bt.Expr).String()
+}
+
+func (bt *buildTags) Set(value string) error {
+	ct, err := constraint.Parse("// +build " + value)
+	if err != nil {
+		return err
+	}
+
+	bt.Expr = ct
+	return nil
+}
+
+func andConstraints(x, y constraint.Expr) constraint.Expr {
+	if x == nil {
+		return y
+	}
+
+	if y == nil {
+		return x
+	}
+
+	return &constraint.AndExpr{X: x, Y: y}
+}
+
+func orConstraints(x, y constraint.Expr) constraint.Expr {
+	if x == nil {
+		return y
+	}
+
+	if y == nil {
+		return x
+	}
+
+	return &constraint.OrExpr{X: x, Y: y}
+}
diff --git a/cmd/bpf2go/main.go b/cmd/bpf2go/main.go
index 03ff1f8..734acb7 100644
--- a/cmd/bpf2go/main.go
+++ b/cmd/bpf2go/main.go
@@ -5,6 +5,7 @@ import (
 	"errors"
 	"flag"
 	"fmt"
+	"go/build/constraint"
 	"go/token"
 	"io"
 	"os"
@@ -76,7 +77,7 @@ func run(stdout io.Writer, pkg, outputDir string, args []string) (err error) {
 	fs.StringVar(&b2g.strip, "strip", "", "`binary` used to strip DWARF from compiled BPF (default \"llvm-strip\")")
 	fs.BoolVar(&b2g.disableStripping, "no-strip", false, "disable stripping of DWARF")
 	flagCFlags := fs.String("cflags", "", "flags passed to the compiler, may contain quoted arguments")
-	fs.StringVar(&b2g.tags, "tags", "", "list of Go build tags to include in generated files")
+	fs.Var(&b2g.tags, "tags", "Comma-separated list of Go build tags to include in generated files")
 	flagTarget := fs.String("target", "bpfel,bpfeb", "clang target(s) to compile for (comma separated)")
 	fs.StringVar(&b2g.makeBase, "makebase", "", "write make compatible depinfo files relative to `directory`")
 	fs.Var(&b2g.cTypes, "type", "`Name` of a type to generate a Go declaration for, may be repeated")
@@ -157,10 +158,6 @@ func run(stdout io.Writer, pkg, outputDir string, args []string) (err error) {
 		return fmt.Errorf("-output-stem %q must not contain path separation characters", b2g.outputStem)
 	}
 
-	if strings.ContainsRune(b2g.tags, '\n') {
-		return fmt.Errorf("-tags mustn't contain new line characters")
-	}
-
 	targetArches := strings.Split(*flagTarget, ",")
 	if len(targetArches) == 0 {
 		return fmt.Errorf("no targets specified")
@@ -261,8 +258,8 @@ type bpf2go struct {
 	skipGlobalTypes bool
 	// C types to include in the generatd output.
 	cTypes cTypes
-	// Go tags included in the .go
-	tags string
+	// Build tags to be included in the output.
+	tags buildTags
 	// Base directory of the Makefile. Enables outputting make-style dependencies
 	// in .d files.
 	makeBase string
@@ -292,14 +289,14 @@ func (b2g *bpf2go) convert(tgt target, arches []string) (err error) {
 		return err
 	}
 
-	var tags []string
-	if len(arches) > 0 {
-		tags = append(tags, strings.Join(arches, " "))
-	}
-	if b2g.tags != "" {
-		tags = append(tags, b2g.tags)
+	var archConstraint constraint.Expr
+	for _, arch := range arches {
+		tag := &constraint.TagExpr{Tag: arch}
+		archConstraint = orConstraints(archConstraint, tag)
 	}
 
+	constraints := andConstraints(archConstraint, b2g.tags.Expr)
+
 	cFlags := make([]string, len(b2g.cFlags))
 	copy(cFlags, b2g.cFlags)
 	if tgt.linux != "" {
@@ -342,7 +339,7 @@ func (b2g *bpf2go) convert(tgt target, arches []string) (err error) {
 		ident:           b2g.ident,
 		cTypes:          b2g.cTypes,
 		skipGlobalTypes: b2g.skipGlobalTypes,
-		tags:            tags,
+		constraints:     constraints,
 		obj:             objFileName,
 		out:             goFile,
 	})
diff --git a/cmd/bpf2go/output.go b/cmd/bpf2go/output.go
index 02e026f..01b8c36 100644
--- a/cmd/bpf2go/output.go
+++ b/cmd/bpf2go/output.go
@@ -3,6 +3,7 @@ package main
 import (
 	"bytes"
 	"fmt"
+	"go/build/constraint"
 	"go/token"
 	"io"
 	"os"
@@ -19,9 +20,7 @@ import (
 const ebpfModule = "github.com/cilium/ebpf"
 
 const commonRaw = `// Code generated by bpf2go; DO NOT EDIT.
-{{- range .Tags }}
-// +build {{ . }}
-{{- end }}
+{{ with .Constraints }}//go:build {{ . }}{{ end }}
 
 package {{ .Package }}
 
@@ -161,7 +160,7 @@ var {{ .Name.Bytes }} []byte
 `
 
 var (
-	tplFuncs = map[string]interface{}{
+	tplFuncs = template.FuncMap{
 		"tag": tag,
 	}
 	commonTemplate = template.Must(template.New("common").Funcs(tplFuncs).Parse(commonRaw))
@@ -220,7 +219,7 @@ func (n templateName) CloseHelper() string {
 type outputArgs struct {
 	pkg             string
 	ident           string
-	tags            []string
+	constraints     constraint.Expr
 	cTypes          []string
 	skipGlobalTypes bool
 	obj             string
@@ -297,20 +296,20 @@ func output(args outputArgs) error {
 
 	ctx := struct {
 		*btf.GoFormatter
-		Module    string
-		Package   string
-		Tags      []string
-		Name      templateName
-		Maps      map[string]string
-		Programs  map[string]string
-		Types     []btf.Type
-		TypeNames map[btf.Type]string
-		File      string
+		Module      string
+		Package     string
+		Constraints constraint.Expr
+		Name        templateName
+		Maps        map[string]string
+		Programs    map[string]string
+		Types       []btf.Type
+		TypeNames   map[btf.Type]string
+		File        string
 	}{
 		gf,
 		ebpfModule,
 		args.pkg,
-		args.tags,
+		args.constraints,
 		templateName(args.ident),
 		maps,
 		programs,
diff --git a/collection.go b/collection.go
index 729e5e9..5dec14a 100644
--- a/collection.go
+++ b/collection.go
@@ -107,6 +107,16 @@ func (cs *CollectionSpec) RewriteMaps(maps map[string]*Map) error {
 	return nil
 }
 
+// MissingConstantsError is returned by [CollectionSpec.RewriteConstants].
+type MissingConstantsError struct {
+	// The constants missing from .rodata.
+	Constants []string
+}
+
+func (m *MissingConstantsError) Error() string {
+	return fmt.Sprintf("some constants are missing from .rodata: %s", strings.Join(m.Constants, ", "))
+}
+
 // RewriteConstants replaces the value of multiple constants.
 //
 // The constant must be defined like so in the C program:
@@ -120,7 +130,7 @@ func (cs *CollectionSpec) RewriteMaps(maps map[string]*Map) error {
 //
 // From Linux 5.5 the verifier will use constants to eliminate dead code.
 //
-// Returns an error if a constant doesn't exist.
+// Returns an error wrapping [MissingConstantsError] if a constant doesn't exist.
 func (cs *CollectionSpec) RewriteConstants(consts map[string]interface{}) error {
 	replaced := make(map[string]bool)
 
@@ -184,7 +194,7 @@ func (cs *CollectionSpec) RewriteConstants(consts map[string]interface{}) error
 	}
 
 	if len(missing) != 0 {
-		return fmt.Errorf("spec is missing one or more constants: %s", strings.Join(missing, ","))
+		return fmt.Errorf("rewrite constants: %w", &MissingConstantsError{Constants: missing})
 	}
 
 	return nil
diff --git a/examples/cgroup_skb/bpf_bpfeb.go b/examples/cgroup_skb/bpf_bpfeb.go
index b31840c..46fcc5d 100644
--- a/examples/cgroup_skb/bpf_bpfeb.go
+++ b/examples/cgroup_skb/bpf_bpfeb.go
@@ -1,6 +1,5 @@
 // Code generated by bpf2go; DO NOT EDIT.
 //go:build arm64be || armbe || mips || mips64 || mips64p32 || ppc64 || s390 || s390x || sparc || sparc64
-// +build arm64be armbe mips mips64 mips64p32 ppc64 s390 s390x sparc sparc64
 
 package main
 
diff --git a/examples/cgroup_skb/bpf_bpfel.go b/examples/cgroup_skb/bpf_bpfel.go
index f33d536..11369b8 100644
--- a/examples/cgroup_skb/bpf_bpfel.go
+++ b/examples/cgroup_skb/bpf_bpfel.go
@@ -1,6 +1,5 @@
 // Code generated by bpf2go; DO NOT EDIT.
 //go:build 386 || amd64 || amd64p32 || arm || arm64 || mips64le || mips64p32le || mipsle || ppc64le || riscv64
-// +build 386 amd64 amd64p32 arm arm64 mips64le mips64p32le mipsle ppc64le riscv64
 
 package main
 
diff --git a/examples/cgroup_skb/cgroup_skb.c b/examples/cgroup_skb/cgroup_skb.c
index 9e86f41..a7e125d 100644
--- a/examples/cgroup_skb/cgroup_skb.c
+++ b/examples/cgroup_skb/cgroup_skb.c
@@ -1,4 +1,4 @@
-// +build ignore
+//go:build ignore
 
 #include "common.h"
 
diff --git a/examples/fentry/bpf_bpfeb.go b/examples/fentry/bpf_bpfeb.go
index 1dd6e58..46c4bea 100644
--- a/examples/fentry/bpf_bpfeb.go
+++ b/examples/fentry/bpf_bpfeb.go
@@ -1,6 +1,5 @@
 // Code generated by bpf2go; DO NOT EDIT.
 //go:build arm64be || armbe || mips || mips64 || mips64p32 || ppc64 || s390 || s390x || sparc || sparc64
-// +build arm64be armbe mips mips64 mips64p32 ppc64 s390 s390x sparc sparc64
 
 package main
 
diff --git a/examples/fentry/bpf_bpfel.go b/examples/fentry/bpf_bpfel.go
index d52174d..c82149e 100644
--- a/examples/fentry/bpf_bpfel.go
+++ b/examples/fentry/bpf_bpfel.go
@@ -1,6 +1,5 @@
 // Code generated by bpf2go; DO NOT EDIT.
 //go:build 386 || amd64 || amd64p32 || arm || arm64 || mips64le || mips64p32le || mipsle || ppc64le || riscv64
-// +build 386 amd64 amd64p32 arm arm64 mips64le mips64p32le mipsle ppc64le riscv64
 
 package main
 
diff --git a/examples/fentry/fentry.c b/examples/fentry/fentry.c
index b08994c..ba076f6 100644
--- a/examples/fentry/fentry.c
+++ b/examples/fentry/fentry.c
@@ -1,4 +1,4 @@
-// +build ignore
+//go:build ignore
 
 #include "common.h"
 
diff --git a/examples/kprobe/bpf_bpfeb.go b/examples/kprobe/bpf_bpfeb.go
index 47626aa..5ff2001 100644
--- a/examples/kprobe/bpf_bpfeb.go
+++ b/examples/kprobe/bpf_bpfeb.go
@@ -1,6 +1,5 @@
 // Code generated by bpf2go; DO NOT EDIT.
 //go:build arm64be || armbe || mips || mips64 || mips64p32 || ppc64 || s390 || s390x || sparc || sparc64
-// +build arm64be armbe mips mips64 mips64p32 ppc64 s390 s390x sparc sparc64
 
 package main
 
diff --git a/examples/kprobe/bpf_bpfel.go b/examples/kprobe/bpf_bpfel.go
index c344033..f799c03 100644
--- a/examples/kprobe/bpf_bpfel.go
+++ b/examples/kprobe/bpf_bpfel.go
@@ -1,6 +1,5 @@
 // Code generated by bpf2go; DO NOT EDIT.
 //go:build 386 || amd64 || amd64p32 || arm || arm64 || mips64le || mips64p32le || mipsle || ppc64le || riscv64
-// +build 386 amd64 amd64p32 arm arm64 mips64le mips64p32le mipsle ppc64le riscv64
 
 package main
 
diff --git a/examples/kprobe/kprobe.c b/examples/kprobe/kprobe.c
index c1d49c5..7b97423 100644
--- a/examples/kprobe/kprobe.c
+++ b/examples/kprobe/kprobe.c
@@ -1,4 +1,4 @@
-// +build ignore
+//go:build ignore
 
 #include "common.h"
 
diff --git a/examples/kprobe_percpu/bpf_bpfeb.go b/examples/kprobe_percpu/bpf_bpfeb.go
index 47626aa..5ff2001 100644
--- a/examples/kprobe_percpu/bpf_bpfeb.go
+++ b/examples/kprobe_percpu/bpf_bpfeb.go
@@ -1,6 +1,5 @@
 // Code generated by bpf2go; DO NOT EDIT.
 //go:build arm64be || armbe || mips || mips64 || mips64p32 || ppc64 || s390 || s390x || sparc || sparc64
-// +build arm64be armbe mips mips64 mips64p32 ppc64 s390 s390x sparc sparc64
 
 package main
 
diff --git a/examples/kprobe_percpu/bpf_bpfel.go b/examples/kprobe_percpu/bpf_bpfel.go
index c344033..f799c03 100644
--- a/examples/kprobe_percpu/bpf_bpfel.go
+++ b/examples/kprobe_percpu/bpf_bpfel.go
@@ -1,6 +1,5 @@
 // Code generated by bpf2go; DO NOT EDIT.
 //go:build 386 || amd64 || amd64p32 || arm || arm64 || mips64le || mips64p32le || mipsle || ppc64le || riscv64
-// +build 386 amd64 amd64p32 arm arm64 mips64le mips64p32le mipsle ppc64le riscv64
 
 package main
 
diff --git a/examples/kprobe_percpu/kprobe_percpu.c b/examples/kprobe_percpu/kprobe_percpu.c
index 50e62ff..a0234df 100644
--- a/examples/kprobe_percpu/kprobe_percpu.c
+++ b/examples/kprobe_percpu/kprobe_percpu.c
@@ -1,4 +1,4 @@
-// +build ignore
+//go:build ignore
 
 #include "common.h"
 
diff --git a/examples/kprobepin/bpf_bpfeb.go b/examples/kprobepin/bpf_bpfeb.go
index 47626aa..5ff2001 100644
--- a/examples/kprobepin/bpf_bpfeb.go
+++ b/examples/kprobepin/bpf_bpfeb.go
@@ -1,6 +1,5 @@
 // Code generated by bpf2go; DO NOT EDIT.
 //go:build arm64be || armbe || mips || mips64 || mips64p32 || ppc64 || s390 || s390x || sparc || sparc64
-// +build arm64be armbe mips mips64 mips64p32 ppc64 s390 s390x sparc sparc64
 
 package main
 
diff --git a/examples/kprobepin/bpf_bpfel.go b/examples/kprobepin/bpf_bpfel.go
index c344033..f799c03 100644
--- a/examples/kprobepin/bpf_bpfel.go
+++ b/examples/kprobepin/bpf_bpfel.go
@@ -1,6 +1,5 @@
 // Code generated by bpf2go; DO NOT EDIT.
 //go:build 386 || amd64 || amd64p32 || arm || arm64 || mips64le || mips64p32le || mipsle || ppc64le || riscv64
-// +build 386 amd64 amd64p32 arm arm64 mips64le mips64p32le mipsle ppc64le riscv64
 
 package main
 
diff --git a/examples/kprobepin/kprobe_pin.c b/examples/kprobepin/kprobe_pin.c
index 7b4a215..93457c4 100644
--- a/examples/kprobepin/kprobe_pin.c
+++ b/examples/kprobepin/kprobe_pin.c
@@ -1,4 +1,4 @@
-// +build ignore
+//go:build ignore
 
 #include "common.h"
 
diff --git a/examples/ringbuffer/bpf_bpfeb.go b/examples/ringbuffer/bpf_bpfeb.go
index 4f66e1e..738a9c5 100644
--- a/examples/ringbuffer/bpf_bpfeb.go
+++ b/examples/ringbuffer/bpf_bpfeb.go
@@ -1,6 +1,5 @@
 // Code generated by bpf2go; DO NOT EDIT.
 //go:build arm64be || armbe || mips || mips64 || mips64p32 || ppc64 || s390 || s390x || sparc || sparc64
-// +build arm64be armbe mips mips64 mips64p32 ppc64 s390 s390x sparc sparc64
 
 package main
 
diff --git a/examples/ringbuffer/bpf_bpfel.go b/examples/ringbuffer/bpf_bpfel.go
index a6c5921..6232609 100644
--- a/examples/ringbuffer/bpf_bpfel.go
+++ b/examples/ringbuffer/bpf_bpfel.go
@@ -1,6 +1,5 @@
 // Code generated by bpf2go; DO NOT EDIT.
 //go:build 386 || amd64 || amd64p32 || arm || arm64 || mips64le || mips64p32le || mipsle || ppc64le || riscv64
-// +build 386 amd64 amd64p32 arm arm64 mips64le mips64p32le mipsle ppc64le riscv64
 
 package main
 
diff --git a/examples/ringbuffer/ringbuffer.c b/examples/ringbuffer/ringbuffer.c
index 7566592..8b2a608 100644
--- a/examples/ringbuffer/ringbuffer.c
+++ b/examples/ringbuffer/ringbuffer.c
@@ -1,4 +1,4 @@
-// +build ignore
+//go:build ignore
 
 #include "common.h"
 
diff --git a/examples/tcprtt/bpf_bpfeb.go b/examples/tcprtt/bpf_bpfeb.go
index 7429ee7..9e39d8c 100644
--- a/examples/tcprtt/bpf_bpfeb.go
+++ b/examples/tcprtt/bpf_bpfeb.go
@@ -1,6 +1,5 @@
 // Code generated by bpf2go; DO NOT EDIT.
 //go:build arm64be || armbe || mips || mips64 || mips64p32 || ppc64 || s390 || s390x || sparc || sparc64
-// +build arm64be armbe mips mips64 mips64p32 ppc64 s390 s390x sparc sparc64
 
 package main
 
diff --git a/examples/tcprtt/bpf_bpfel.go b/examples/tcprtt/bpf_bpfel.go
index 935833f..5304b96 100644
--- a/examples/tcprtt/bpf_bpfel.go
+++ b/examples/tcprtt/bpf_bpfel.go
@@ -1,6 +1,5 @@
 // Code generated by bpf2go; DO NOT EDIT.
 //go:build 386 || amd64 || amd64p32 || arm || arm64 || mips64le || mips64p32le || mipsle || ppc64le || riscv64
-// +build 386 amd64 amd64p32 arm arm64 mips64le mips64p32le mipsle ppc64le riscv64
 
 package main
 
diff --git a/examples/tcprtt/tcprtt.c b/examples/tcprtt/tcprtt.c
index e6957bd..9d8dd31 100644
--- a/examples/tcprtt/tcprtt.c
+++ b/examples/tcprtt/tcprtt.c
@@ -1,4 +1,4 @@
-// +build ignore
+//go:build ignore
 
 #include "common.h"
 
diff --git a/examples/tcprtt_sockops/bpf_bpfeb.go b/examples/tcprtt_sockops/bpf_bpfeb.go
index 9d293e0..a821a7d 100644
--- a/examples/tcprtt_sockops/bpf_bpfeb.go
+++ b/examples/tcprtt_sockops/bpf_bpfeb.go
@@ -1,7 +1,5 @@
 // Code generated by bpf2go; DO NOT EDIT.
 //go:build (arm64be || armbe || mips || mips64 || mips64p32 || ppc64 || s390 || s390x || sparc || sparc64) && linux
-// +build arm64be armbe mips mips64 mips64p32 ppc64 s390 s390x sparc sparc64
-// +build linux
 
 package main
 
diff --git a/examples/tcprtt_sockops/bpf_bpfel.go b/examples/tcprtt_sockops/bpf_bpfel.go
index 59358c5..323ef0c 100644
--- a/examples/tcprtt_sockops/bpf_bpfel.go
+++ b/examples/tcprtt_sockops/bpf_bpfel.go
@@ -1,7 +1,5 @@
 // Code generated by bpf2go; DO NOT EDIT.
 //go:build (386 || amd64 || amd64p32 || arm || arm64 || mips64le || mips64p32le || mipsle || ppc64le || riscv64) && linux
-// +build 386 amd64 amd64p32 arm arm64 mips64le mips64p32le mipsle ppc64le riscv64
-// +build linux
 
 package main
 
diff --git a/examples/tcprtt_sockops/main.go b/examples/tcprtt_sockops/main.go
index 4da2ea1..df248c9 100644
--- a/examples/tcprtt_sockops/main.go
+++ b/examples/tcprtt_sockops/main.go
@@ -1,5 +1,4 @@
 //go:build linux
-// +build linux
 
 // This program demonstrates attaching an eBPF program to
 // a cgroupv2 path and using sockops to process TCP socket events.
diff --git a/examples/tcprtt_sockops/tcprtt_sockops.c b/examples/tcprtt_sockops/tcprtt_sockops.c
index 7c7d644..66e0cc4 100644
--- a/examples/tcprtt_sockops/tcprtt_sockops.c
+++ b/examples/tcprtt_sockops/tcprtt_sockops.c
@@ -1,4 +1,4 @@
-// +build ignore
+//go:build ignore
 
 #include "common.h"
 
diff --git a/examples/tracepoint_in_c/bpf_bpfeb.go b/examples/tracepoint_in_c/bpf_bpfeb.go
index 3f0c482..9fc50cb 100644
--- a/examples/tracepoint_in_c/bpf_bpfeb.go
+++ b/examples/tracepoint_in_c/bpf_bpfeb.go
@@ -1,6 +1,5 @@
 // Code generated by bpf2go; DO NOT EDIT.
 //go:build arm64be || armbe || mips || mips64 || mips64p32 || ppc64 || s390 || s390x || sparc || sparc64
-// +build arm64be armbe mips mips64 mips64p32 ppc64 s390 s390x sparc sparc64
 
 package main
 
diff --git a/examples/tracepoint_in_c/bpf_bpfel.go b/examples/tracepoint_in_c/bpf_bpfel.go
index e40ba48..cdd181a 100644
--- a/examples/tracepoint_in_c/bpf_bpfel.go
+++ b/examples/tracepoint_in_c/bpf_bpfel.go
@@ -1,6 +1,5 @@
 // Code generated by bpf2go; DO NOT EDIT.
 //go:build 386 || amd64 || amd64p32 || arm || arm64 || mips64le || mips64p32le || mipsle || ppc64le || riscv64
-// +build 386 amd64 amd64p32 arm arm64 mips64le mips64p32le mipsle ppc64le riscv64
 
 package main
 
diff --git a/examples/tracepoint_in_c/tracepoint.c b/examples/tracepoint_in_c/tracepoint.c
index 844a6ef..fe66395 100644
--- a/examples/tracepoint_in_c/tracepoint.c
+++ b/examples/tracepoint_in_c/tracepoint.c
@@ -1,4 +1,4 @@
-// +build ignore
+//go:build ignore
 
 #include "common.h"
 
diff --git a/examples/uretprobe/bpf_bpfel_x86.go b/examples/uretprobe/bpf_bpfel_x86.go
index ad9e2df..c39d410 100644
--- a/examples/uretprobe/bpf_bpfel_x86.go
+++ b/examples/uretprobe/bpf_bpfel_x86.go
@@ -1,6 +1,5 @@
 // Code generated by bpf2go; DO NOT EDIT.
 //go:build 386 || amd64
-// +build 386 amd64
 
 package main
 
diff --git a/examples/uretprobe/uretprobe.c b/examples/uretprobe/uretprobe.c
index 332a002..12668a2 100644
--- a/examples/uretprobe/uretprobe.c
+++ b/examples/uretprobe/uretprobe.c
@@ -1,4 +1,4 @@
-// +build ignore
+//go:build ignore
 
 #include "common.h"
 
diff --git a/examples/xdp/bpf_bpfeb.go b/examples/xdp/bpf_bpfeb.go
index 3da8abe..5fd2ab7 100644
--- a/examples/xdp/bpf_bpfeb.go
+++ b/examples/xdp/bpf_bpfeb.go
@@ -1,6 +1,5 @@
 // Code generated by bpf2go; DO NOT EDIT.
 //go:build arm64be || armbe || mips || mips64 || mips64p32 || ppc64 || s390 || s390x || sparc || sparc64
-// +build arm64be armbe mips mips64 mips64p32 ppc64 s390 s390x sparc sparc64
 
 package main
 
diff --git a/examples/xdp/bpf_bpfel.go b/examples/xdp/bpf_bpfel.go
index 2fdc996..03e4ab9 100644
--- a/examples/xdp/bpf_bpfel.go
+++ b/examples/xdp/bpf_bpfel.go
@@ -1,6 +1,5 @@
 // Code generated by bpf2go; DO NOT EDIT.
 //go:build 386 || amd64 || amd64p32 || arm || arm64 || mips64le || mips64p32le || mipsle || ppc64le || riscv64
-// +build 386 amd64 amd64p32 arm arm64 mips64le mips64p32le mipsle ppc64le riscv64
 
 package main
 
diff --git a/examples/xdp/xdp.c b/examples/xdp/xdp.c
index 6c5797c..15424dc 100644
--- a/examples/xdp/xdp.c
+++ b/examples/xdp/xdp.c
@@ -1,4 +1,4 @@
-// +build ignore
+//go:build ignore
 
 #include "bpf_endian.h"
 #include "common.h"
diff --git a/info.go b/info.go
index 8f42173..e0168cd 100644
--- a/info.go
+++ b/info.go
@@ -130,6 +130,12 @@ func newProgramInfoFromFd(fd *sys.FD) (*ProgramInfo, error) {
 		pi.maps = make([]MapID, info.NrMapIds)
 		info2.NrMapIds = info.NrMapIds
 		info2.MapIds = sys.NewPointer(unsafe.Pointer(&pi.maps[0]))
+	} else if haveProgramInfoMapIDs() == nil {
+		// This program really has no associated maps.
+		pi.maps = make([]MapID, 0)
+	} else {
+		// The kernel doesn't report associated maps.
+		pi.maps = nil
 	}
 
 	if info.XlatedProgLen > 0 {
@@ -321,3 +327,30 @@ func EnableStats(which uint32) (io.Closer, error) {
 	}
 	return fd, nil
 }
+
+var haveProgramInfoMapIDs = internal.NewFeatureTest("map IDs in program info", "4.15", func() error {
+	prog, err := progLoad(asm.Instructions{
+		asm.LoadImm(asm.R0, 0, asm.DWord),
+		asm.Return(),
+	}, SocketFilter, "MIT")
+	if err != nil {
+		return err
+	}
+	defer prog.Close()
+
+	err = sys.ObjInfo(prog, &sys.ProgInfo{
+		// NB: Don't need to allocate MapIds since the program isn't using
+		// any maps.
+		NrMapIds: 1,
+	})
+	if errors.Is(err, unix.EINVAL) {
+		// Most likely the syscall doesn't exist.
+		return internal.ErrNotSupported
+	}
+	if errors.Is(err, unix.E2BIG) {
+		// We've hit check_uarg_tail_zero on older kernels.
+		return internal.ErrNotSupported
+	}
+
+	return err
+})
diff --git a/internal/cpu.go b/internal/cpu.go
index 3affa1e..9e908b6 100644
--- a/internal/cpu.go
+++ b/internal/cpu.go
@@ -4,24 +4,13 @@ import (
 	"fmt"
 	"os"
 	"strings"
-	"sync"
 )
 
-var sysCPU struct {
-	once sync.Once
-	err  error
-	num  int
-}
-
 // PossibleCPUs returns the max number of CPUs a system may possibly have
 // Logical CPU numbers must be of the form 0-n
-func PossibleCPUs() (int, error) {
-	sysCPU.once.Do(func() {
-		sysCPU.num, sysCPU.err = parseCPUsFromFile("/sys/devices/system/cpu/possible")
-	})
-
-	return sysCPU.num, sysCPU.err
-}
+var PossibleCPUs = Memoize(func() (int, error) {
+	return parseCPUsFromFile("/sys/devices/system/cpu/possible")
+})
 
 func parseCPUsFromFile(path string) (int, error) {
 	spec, err := os.ReadFile(path)
diff --git a/internal/deque.go b/internal/deque.go
index 05be23e..e3a3050 100644
--- a/internal/deque.go
+++ b/internal/deque.go
@@ -24,24 +24,11 @@ func (dq *Deque[T]) Empty() bool {
 	return dq.read == dq.write
 }
 
-func (dq *Deque[T]) remainingCap() int {
-	return len(dq.elems) - int(dq.write-dq.read)
-}
-
 // Push adds an element to the end.
 func (dq *Deque[T]) Push(e T) {
-	if dq.remainingCap() >= 1 {
-		dq.elems[dq.write&dq.mask] = e
-		dq.write++
-		return
-	}
-
-	elems := dq.linearise(1)
-	elems = append(elems, e)
-
-	dq.elems = elems[:cap(elems)]
-	dq.mask = uint64(cap(elems)) - 1
-	dq.read, dq.write = 0, uint64(len(elems))
+	dq.Grow(1)
+	dq.elems[dq.write&dq.mask] = e
+	dq.write++
 }
 
 // Shift returns the first element or the zero value.
@@ -74,16 +61,17 @@ func (dq *Deque[T]) Pop() T {
 	return t
 }
 
-// linearise the contents of the deque.
-//
-// The returned slice has space for at least n more elements and has power
-// of two capacity.
-func (dq *Deque[T]) linearise(n int) []T {
-	length := dq.write - dq.read
-	need := length + uint64(n)
-	if need < length {
+// Grow the deque's capacity, if necessary, to guarantee space for another n
+// elements.
+func (dq *Deque[T]) Grow(n int) {
+	have := dq.write - dq.read
+	need := have + uint64(n)
+	if need < have {
 		panic("overflow")
 	}
+	if uint64(len(dq.elems)) >= need {
+		return
+	}
 
 	// Round up to the new power of two which is at least 8.
 	// See https://jameshfisher.com/2018/03/30/round-up-power-2/
@@ -92,9 +80,12 @@ func (dq *Deque[T]) linearise(n int) []T {
 		capacity = 8
 	}
 
-	types := make([]T, length, capacity)
+	elems := make([]T, have, capacity)
 	pivot := dq.read & dq.mask
-	copied := copy(types, dq.elems[pivot:])
-	copy(types[copied:], dq.elems[:pivot])
-	return types
+	copied := copy(elems, dq.elems[pivot:])
+	copy(elems[copied:], dq.elems[:pivot])
+
+	dq.elems = elems[:capacity]
+	dq.mask = uint64(capacity) - 1
+	dq.read, dq.write = 0, have
 }
diff --git a/internal/endian_be.go b/internal/endian_be.go
index ad33cda..96a2ac0 100644
--- a/internal/endian_be.go
+++ b/internal/endian_be.go
@@ -1,5 +1,4 @@
 //go:build armbe || arm64be || mips || mips64 || mips64p32 || ppc64 || s390 || s390x || sparc || sparc64
-// +build armbe arm64be mips mips64 mips64p32 ppc64 s390 s390x sparc sparc64
 
 package internal
 
diff --git a/internal/endian_le.go b/internal/endian_le.go
index 41a6822..9a3dbd6 100644
--- a/internal/endian_le.go
+++ b/internal/endian_le.go
@@ -1,5 +1,4 @@
 //go:build 386 || amd64 || amd64p32 || arm || arm64 || mipsle || mips64le || mips64p32le || ppc64le || riscv64
-// +build 386 amd64 amd64p32 arm arm64 mipsle mips64le mips64p32le ppc64le riscv64
 
 package internal
 
diff --git a/internal/memoize.go b/internal/memoize.go
new file mode 100644
index 0000000..3de0a3f
--- /dev/null
+++ b/internal/memoize.go
@@ -0,0 +1,26 @@
+package internal
+
+import (
+	"sync"
+)
+
+type memoizedFunc[T any] struct {
+	once   sync.Once
+	fn     func() (T, error)
+	result T
+	err    error
+}
+
+func (mf *memoizedFunc[T]) do() (T, error) {
+	mf.once.Do(func() {
+		mf.result, mf.err = mf.fn()
+	})
+	return mf.result, mf.err
+}
+
+// Memoize the result of a function call.
+//
+// fn is only ever called once, even if it returns an error.
+func Memoize[T any](fn func() (T, error)) func() (T, error) {
+	return (&memoizedFunc[T]{fn: fn}).do
+}
diff --git a/internal/sys/ptr_32_be.go b/internal/sys/ptr_32_be.go
index df903d7..6278c79 100644
--- a/internal/sys/ptr_32_be.go
+++ b/internal/sys/ptr_32_be.go
@@ -1,5 +1,4 @@
 //go:build armbe || mips || mips64p32
-// +build armbe mips mips64p32
 
 package sys
 
diff --git a/internal/sys/ptr_32_le.go b/internal/sys/ptr_32_le.go
index a6a51ed..c27b537 100644
--- a/internal/sys/ptr_32_le.go
+++ b/internal/sys/ptr_32_le.go
@@ -1,5 +1,4 @@
 //go:build 386 || amd64p32 || arm || mipsle || mips64p32le
-// +build 386 amd64p32 arm mipsle mips64p32le
 
 package sys
 
diff --git a/internal/sys/ptr_64.go b/internal/sys/ptr_64.go
index 7c0279e..2d78282 100644
--- a/internal/sys/ptr_64.go
+++ b/internal/sys/ptr_64.go
@@ -1,5 +1,4 @@
 //go:build !386 && !amd64p32 && !arm && !mipsle && !mips64p32le && !armbe && !mips && !mips64p32
-// +build !386,!amd64p32,!arm,!mipsle,!mips64p32le,!armbe,!mips,!mips64p32
 
 package sys
 
diff --git a/internal/unix/types_linux.go b/internal/unix/types_linux.go
index 04b828b..924be2a 100644
--- a/internal/unix/types_linux.go
+++ b/internal/unix/types_linux.go
@@ -49,9 +49,12 @@ const (
 	EPOLL_CLOEXEC             = linux.EPOLL_CLOEXEC
 	O_CLOEXEC                 = linux.O_CLOEXEC
 	O_NONBLOCK                = linux.O_NONBLOCK
+	PROT_NONE                 = linux.PROT_NONE
 	PROT_READ                 = linux.PROT_READ
 	PROT_WRITE                = linux.PROT_WRITE
+	MAP_ANON                  = linux.MAP_ANON
 	MAP_SHARED                = linux.MAP_SHARED
+	MAP_PRIVATE               = linux.MAP_PRIVATE
 	PERF_ATTR_SIZE_VER1       = linux.PERF_ATTR_SIZE_VER1
 	PERF_TYPE_SOFTWARE        = linux.PERF_TYPE_SOFTWARE
 	PERF_TYPE_TRACEPOINT      = linux.PERF_TYPE_TRACEPOINT
diff --git a/internal/unix/types_other.go b/internal/unix/types_other.go
index e87bf83..225fcd0 100644
--- a/internal/unix/types_other.go
+++ b/internal/unix/types_other.go
@@ -53,9 +53,12 @@ const (
 	EPOLL_CLOEXEC
 	O_CLOEXEC
 	O_NONBLOCK
+	PROT_NONE
 	PROT_READ
 	PROT_WRITE
+	MAP_ANON
 	MAP_SHARED
+	MAP_PRIVATE
 	PERF_ATTR_SIZE_VER1
 	PERF_TYPE_SOFTWARE
 	PERF_TYPE_TRACEPOINT
diff --git a/internal/version.go b/internal/version.go
index 370e01e..9b17ffb 100644
--- a/internal/version.go
+++ b/internal/version.go
@@ -2,7 +2,6 @@ package internal
 
 import (
 	"fmt"
-	"sync"
 
 	"github.com/cilium/ebpf/internal/unix"
 )
@@ -15,14 +14,6 @@ const (
 	MagicKernelVersion = 0xFFFFFFFE
 )
 
-var (
-	kernelVersion = struct {
-		once    sync.Once
-		version Version
-		err     error
-	}{}
-)
-
 // A Version in the form Major.Minor.Patch.
 type Version [3]uint16
 
@@ -88,16 +79,9 @@ func (v Version) Kernel() uint32 {
 }
 
 // KernelVersion returns the version of the currently running kernel.
-func KernelVersion() (Version, error) {
-	kernelVersion.once.Do(func() {
-		kernelVersion.version, kernelVersion.err = detectKernelVersion()
-	})
-
-	if kernelVersion.err != nil {
-		return Version{}, kernelVersion.err
-	}
-	return kernelVersion.version, nil
-}
+var KernelVersion = Memoize(func() (Version, error) {
+	return detectKernelVersion()
+})
 
 // detectKernelVersion returns the version of the running kernel.
 func detectKernelVersion() (Version, error) {
diff --git a/link/kprobe_multi.go b/link/kprobe_multi.go
index 151f47d..697c6d7 100644
--- a/link/kprobe_multi.go
+++ b/link/kprobe_multi.go
@@ -28,7 +28,7 @@ type KprobeMultiOptions struct {
 	// limits the attach point to the function entry or return.
 	//
 	// Mutually exclusive with Symbols.
-	Addresses []uint64
+	Addresses []uintptr
 
 	// Cookies specifies arbitrary values that can be fetched from an eBPF
 	// program via `bpf_get_attach_cookie()`.
diff --git a/link/perf_event.go b/link/perf_event.go
index 61f8062..6736dcd 100644
--- a/link/perf_event.go
+++ b/link/perf_event.go
@@ -136,10 +136,14 @@ func (pl *perfEventLink) Unpin() error {
 }
 
 func (pl *perfEventLink) Close() error {
+	if err := pl.fd.Close(); err != nil {
+		return fmt.Errorf("perf link close: %w", err)
+	}
+
 	if err := pl.pe.Close(); err != nil {
-		return fmt.Errorf("perf event link close: %w", err)
+		return fmt.Errorf("perf event close: %w", err)
 	}
-	return pl.fd.Close()
+	return nil
 }
 
 func (pl *perfEventLink) Update(prog *ebpf.Program) error {
diff --git a/link/uprobe.go b/link/uprobe.go
index aa1ad9b..9a03eb7 100644
--- a/link/uprobe.go
+++ b/link/uprobe.go
@@ -323,26 +323,23 @@ func tracefsUprobe(args probeArgs) (*perfEvent, error) {
 // sanitizeSymbol replaces every invalid character for the tracefs api with an underscore.
 // It is equivalent to calling regexp.MustCompile("[^a-zA-Z0-9]+").ReplaceAllString("_").
 func sanitizeSymbol(s string) string {
-	var b strings.Builder
-	b.Grow(len(s))
 	var skip bool
-	for _, c := range []byte(s) {
+	return strings.Map(func(c rune) rune {
 		switch {
 		case c >= 'a' && c <= 'z',
 			c >= 'A' && c <= 'Z',
 			c >= '0' && c <= '9':
 			skip = false
-			b.WriteByte(c)
+			return c
+
+		case skip:
+			return -1
 
 		default:
-			if !skip {
-				b.WriteByte('_')
-				skip = true
-			}
+			skip = true
+			return '_'
 		}
-	}
-
-	return b.String()
+	}, s)
 }
 
 // uprobeToken creates the PATH:OFFSET(REF_CTR_OFFSET) token for the tracefs api.
diff --git a/map.go b/map.go
index 800d59d..de1b69b 100644
--- a/map.go
+++ b/map.go
@@ -6,6 +6,7 @@ import (
 	"fmt"
 	"io"
 	"math/rand"
+	"os"
 	"path/filepath"
 	"reflect"
 	"time"
@@ -168,7 +169,10 @@ func (ms *MapSpec) Compatible(m *Map) error {
 		m.maxEntries != ms.MaxEntries:
 		return fmt.Errorf("expected max entries %v, got %v: %w", ms.MaxEntries, m.maxEntries, ErrMapIncompatible)
 
-	case m.flags != ms.Flags:
+	// BPF_F_RDONLY_PROG is set unconditionally for devmaps. Explicitly allow
+	// this mismatch.
+	case !((ms.Type == DevMap || ms.Type == DevMapHash) && m.flags^ms.Flags == unix.BPF_F_RDONLY_PROG) &&
+		m.flags != ms.Flags:
 		return fmt.Errorf("expected flags %v, got %v: %w", ms.Flags, m.flags, ErrMapIncompatible)
 	}
 	return nil
@@ -792,12 +796,22 @@ func (m *Map) nextKey(key interface{}, nextKeyOut sys.Pointer) error {
 	return nil
 }
 
+var mmapProtectedPage = internal.Memoize(func() ([]byte, error) {
+	return unix.Mmap(-1, 0, os.Getpagesize(), unix.PROT_NONE, unix.MAP_ANON|unix.MAP_SHARED)
+})
+
 // guessNonExistentKey attempts to perform a map lookup that returns ENOENT.
 // This is necessary on kernels before 4.4.132, since those don't support
 // iterating maps from the start by providing an invalid key pointer.
 func (m *Map) guessNonExistentKey() ([]byte, error) {
-	// Provide an invalid value pointer to prevent a copy on the kernel side.
-	valuePtr := sys.NewPointer(unsafe.Pointer(^uintptr(0)))
+	// Map a protected page and use that as the value pointer. This saves some
+	// work copying out the value, which we're not interested in.
+	page, err := mmapProtectedPage()
+	if err != nil {
+		return nil, err
+	}
+	valuePtr := sys.NewSlicePointer(page)
+
 	randKey := make([]byte, int(m.keySize))
 
 	for i := 0; i < 4; i++ {


Test patch:
diff --git a/btf/btf_test.go b/btf/btf_test.go
index d681a16..c98a3c2 100644
--- a/btf/btf_test.go
+++ b/btf/btf_test.go
@@ -7,7 +7,6 @@ import (
 	"fmt"
 	"io"
 	"os"
-	"sync"
 	"testing"
 
 	"github.com/cilium/ebpf/internal"
@@ -32,53 +31,45 @@ func vmlinuxSpec(tb testing.TB) *Spec {
 	return spec
 }
 
-// vmlinuxTestdata caches the result of reading and parsing a BTF blob from
-// testdata.
-var vmlinuxTestdata struct {
-	sync.Once
+type specAndRawBTF struct {
 	raw  []byte
 	spec *Spec
-	err  error
 }
 
-func doVMLinuxTestdata() {
+var vmlinuxTestdata = internal.Memoize(func() (specAndRawBTF, error) {
 	b, err := internal.ReadAllCompressed("testdata/vmlinux.btf.gz")
 	if err != nil {
-		vmlinuxTestdata.err = fmt.Errorf("uncompressing vmlinux testdata: %w", err)
-		return
+		return specAndRawBTF{}, err
 	}
-	vmlinuxTestdata.raw = b
 
-	s, err := loadRawSpec(bytes.NewReader(b), binary.LittleEndian, nil, nil)
+	spec, err := loadRawSpec(bytes.NewReader(b), binary.LittleEndian, nil, nil)
 	if err != nil {
-		vmlinuxTestdata.err = fmt.Errorf("parsing vmlinux testdata types: %w", err)
-		return
+		return specAndRawBTF{}, err
 	}
-	vmlinuxTestdata.spec = s
-}
+
+	return specAndRawBTF{b, spec}, nil
+})
 
 func vmlinuxTestdataReader(tb testing.TB) *bytes.Reader {
 	tb.Helper()
 
-	vmlinuxTestdata.Do(doVMLinuxTestdata)
-
-	if err := vmlinuxTestdata.err; err != nil {
+	td, err := vmlinuxTestdata()
+	if err != nil {
 		tb.Fatal(err)
 	}
 
-	return bytes.NewReader(vmlinuxTestdata.raw)
+	return bytes.NewReader(td.raw)
 }
 
 func vmlinuxTestdataSpec(tb testing.TB) *Spec {
 	tb.Helper()
 
-	vmlinuxTestdata.Do(doVMLinuxTestdata)
-
-	if err := vmlinuxTestdata.err; err != nil {
+	td, err := vmlinuxTestdata()
+	if err != nil {
 		tb.Fatal(err)
 	}
 
-	return vmlinuxTestdata.spec.Copy()
+	return td.spec.Copy()
 }
 
 func parseELFBTF(tb testing.TB, file string) *Spec {
@@ -140,10 +131,7 @@ func TestTypeByNameAmbiguous(t *testing.T) {
 }
 
 func TestTypeByName(t *testing.T) {
-	spec, err := LoadSpecFromReader(vmlinuxTestdataReader(t))
-	if err != nil {
-		t.Fatal(err)
-	}
+	spec := vmlinuxTestdataSpec(t)
 
 	for _, typ := range []interface{}{
 		nil,
@@ -218,6 +206,46 @@ func TestTypeByName(t *testing.T) {
 	}
 }
 
+func TestSpecAdd(t *testing.T) {
+	i := &Int{
+		Name:     "foo",
+		Size:     2,
+		Encoding: Signed | Char,
+	}
+	pi := &Pointer{i}
+
+	s := NewSpec()
+	id, err := s.Add(pi)
+	qt.Assert(t, err, qt.IsNil)
+	qt.Assert(t, id, qt.Equals, TypeID(1), qt.Commentf("First non-void type doesn't get id 1"))
+
+	id, err = s.Add(pi)
+	qt.Assert(t, err, qt.IsNil)
+	qt.Assert(t, id, qt.Equals, TypeID(1))
+
+	_, err = s.TypeID(i)
+	qt.Assert(t, err, qt.IsNotNil, qt.Commentf("Children mustn't be added"))
+
+	id, err = s.Add(i)
+	qt.Assert(t, err, qt.IsNil)
+	qt.Assert(t, id, qt.Equals, TypeID(2), qt.Commentf("Second type doesn't get id 2"))
+
+	id, err = s.Add(i)
+	qt.Assert(t, err, qt.IsNil)
+	qt.Assert(t, id, qt.Equals, TypeID(2), qt.Commentf("Adding a type twice returns different ids"))
+
+	typ, err := s.AnyTypeByName("foo")
+	qt.Assert(t, err, qt.IsNil, qt.Commentf("Add doesn't make named type queryable"))
+	qt.Assert(t, typ, qt.Equals, i)
+
+	id, err = s.Add(&Typedef{"baz", i})
+	qt.Assert(t, err, qt.IsNil)
+	qt.Assert(t, id, qt.Equals, TypeID(3))
+
+	_, err = s.AnyTypeByName("baz")
+	qt.Assert(t, err, qt.IsNil)
+}
+
 func BenchmarkParseVmlinux(b *testing.B) {
 	rd := vmlinuxTestdataReader(b)
 	b.ReportAllocs()
@@ -329,8 +357,12 @@ func TestLoadSpecFromElf(t *testing.T) {
 }
 
 func TestVerifierError(t *testing.T) {
-	btf, _ := newEncoder(kernelEncoderOptions, nil).Encode()
-	_, err := newHandleFromRawBTF(btf)
+	var buf bytes.Buffer
+	if err := marshalTypes(&buf, []Type{&Void{}}, nil, nil); err != nil {
+		t.Fatal(err)
+	}
+
+	_, err := newHandleFromRawBTF(buf.Bytes())
 	testutils.SkipIfNotSupported(t, err)
 	var ve *internal.VerifierError
 	if !errors.As(err, &ve) {
@@ -415,17 +447,14 @@ func ExampleSpec_TypeByName() {
 }
 
 func TestTypesIterator(t *testing.T) {
-	spec, err := LoadSpecFromReader(vmlinuxTestdataReader(t))
-	if err != nil {
-		t.Fatal(err)
-	}
+	spec := vmlinuxTestdataSpec(t)
 
 	if len(spec.types) < 1 {
 		t.Fatal("Not enough types")
 	}
 
 	// Assertion that 'iphdr' type exists within the spec
-	_, err = spec.AnyTypeByName("iphdr")
+	_, err := spec.AnyTypeByName("iphdr")
 	if err != nil {
 		t.Fatalf("Failed to find 'iphdr' type by name: %s", err)
 	}
@@ -450,10 +479,7 @@ func TestTypesIterator(t *testing.T) {
 }
 
 func TestLoadSplitSpecFromReader(t *testing.T) {
-	spec, err := LoadSpecFromReader(vmlinuxTestdataReader(t))
-	if err != nil {
-		t.Fatal(err)
-	}
+	spec := vmlinuxTestdataSpec(t)
 
 	f, err := os.Open("testdata/btf_testmod.btf")
 	if err != nil {
@@ -512,5 +538,4 @@ func TestLoadSplitSpecFromReader(t *testing.T) {
 		t.Fatalf("'bpf_testmod_init` type ID (%d) does not match copied spec's (%d)",
 			typeID, copyTypeID)
 	}
-
 }
diff --git a/btf/marshal_test.go b/btf/marshal_test.go
index 4dd7f23..6013045 100644
--- a/btf/marshal_test.go
+++ b/btf/marshal_test.go
@@ -3,6 +3,7 @@ package btf
 import (
 	"bytes"
 	"encoding/binary"
+	"math"
 	"math/rand"
 	"testing"
 
@@ -19,61 +20,67 @@ func TestBuild(t *testing.T) {
 		Encoding: Signed | Char,
 	}
 
-	enc := newEncoder(encoderOptions{ByteOrder: internal.NativeEndian}, nil)
-
-	id, err := enc.Add(typ)
-	qt.Assert(t, err, qt.IsNil)
-	qt.Assert(t, id, qt.Equals, TypeID(1), qt.Commentf("First non-void type doesn't get id 1"))
-
-	id, err = enc.Add(typ)
-	qt.Assert(t, err, qt.IsNil)
-	qt.Assert(t, id, qt.Equals, TypeID(1), qt.Commentf("Adding a type twice returns different ids"))
+	want := []Type{
+		(*Void)(nil),
+		typ,
+		&Pointer{typ},
+		&Typedef{"baz", typ},
+	}
 
-	raw, err := enc.Encode()
-	qt.Assert(t, err, qt.IsNil, qt.Commentf("Build returned an error"))
+	var buf bytes.Buffer
+	qt.Assert(t, marshalTypes(&buf, want, nil, nil), qt.IsNil)
 
-	spec, err := loadRawSpec(bytes.NewReader(raw), internal.NativeEndian, nil, nil)
+	have, err := loadRawSpec(bytes.NewReader(buf.Bytes()), internal.NativeEndian, nil, nil)
 	qt.Assert(t, err, qt.IsNil, qt.Commentf("Couldn't parse BTF"))
-
-	have, err := spec.AnyTypeByName("foo")
-	qt.Assert(t, err, qt.IsNil)
-	qt.Assert(t, have, qt.DeepEquals, typ)
+	qt.Assert(t, have.types, qt.DeepEquals, want)
 }
 
 func TestRoundtripVMlinux(t *testing.T) {
 	types := vmlinuxSpec(t).types
 
 	// Randomize the order to force different permutations of walking the type
-	// graph.
-	rand.Shuffle(len(types), func(i, j int) {
-		types[i], types[j] = types[j], types[i]
+	// graph. Keep Void at index 0.
+	rand.Shuffle(len(types[1:]), func(i, j int) {
+		types[i+1], types[j+1] = types[j+1], types[i+1]
 	})
 
-	b := newEncoder(kernelEncoderOptions, nil)
-
+	// Skip per CPU datasec, see https://github.com/cilium/ebpf/issues/921
 	for i, typ := range types {
-		_, err := b.Add(typ)
-		qt.Assert(t, err, qt.IsNil, qt.Commentf("add type #%d: %s", i, typ))
+		if ds, ok := typ.(*Datasec); ok && ds.Name == ".data..percpu" {
+			types[i] = types[len(types)-1]
+			types = types[:len(types)-1]
+			break
+		}
+	}
 
-		if b.nextID >= 65_000 {
+	seen := make(map[Type]bool)
+limitTypes:
+	for i, typ := range types {
+		iter := postorderTraversal(typ, func(t Type) (skip bool) {
+			return seen[t]
+		})
+		for iter.Next() {
+			seen[iter.Type] = true
+		}
+		if len(seen) >= math.MaxInt16 {
 			// IDs exceeding math.MaxUint16 can trigger a bug when loading BTF.
 			// This can be removed once the patch lands.
 			// See https://lore.kernel.org/bpf/20220909092107.3035-1-oss@lmb.io/
-			break
+			types = types[:i]
+			break limitTypes
 		}
 	}
 
-	nStr := len(b.strings.strings)
-	nTypes := len(types)
-	t.Log(nStr, "strings", nTypes, "types")
-	t.Log(float64(nStr)/float64(nTypes), "avg strings per type")
-
-	raw, err := b.Encode()
-	qt.Assert(t, err, qt.IsNil, qt.Commentf("build BTF"))
+	var buf bytes.Buffer
+	qt.Assert(t, marshalTypes(&buf, types, nil, nil), qt.IsNil)
 
-	rebuilt, err := loadRawSpec(bytes.NewReader(raw), binary.LittleEndian, nil, nil)
+	rebuilt, err := loadRawSpec(bytes.NewReader(buf.Bytes()), binary.LittleEndian, nil, nil)
 	qt.Assert(t, err, qt.IsNil, qt.Commentf("round tripping BTF failed"))
 
+	if n := len(rebuilt.types); n > math.MaxUint16 {
+		t.Logf("Rebuilt BTF contains %d types which exceeds uint16, test may fail on older kernels", n)
+	}
+
 	h, err := NewHandle(rebuilt)
 	testutils.SkipIfNotSupported(t, err)
 	qt.Assert(t, err, qt.IsNil, qt.Commentf("loading rebuilt BTF failed"))
@@ -81,25 +88,14 @@ func TestRoundtripVMlinux(t *testing.T) {
 }
 
 func BenchmarkBuildVmlinux(b *testing.B) {
-	spec := vmlinuxTestdataSpec(b)
+	types := vmlinuxTestdataSpec(b).types
 
 	b.ReportAllocs()
 	b.ResetTimer()
 
-	types := spec.types
-	strings := spec.strings
-
 	for i := 0; i < b.N; i++ {
-		enc := newEncoder(encoderOptions{ByteOrder: internal.NativeEndian}, newStringTableBuilderFromTable(strings))
-
-		for _, typ := range types {
-			if _, err := enc.Add(typ); err != nil {
-				b.Fatal(err)
-			}
-		}
-
-		_, err := enc.Encode()
-		if err != nil {
+		var buf bytes.Buffer
+		if err := marshalTypes(&buf, types, nil, nil); err != nil {
 			b.Fatal(err)
 		}
 	}
diff --git a/btf/strings_test.go b/btf/strings_test.go
index b44918d..6f2bc4d 100644
--- a/btf/strings_test.go
+++ b/btf/strings_test.go
@@ -72,9 +72,9 @@ func TestStringTable(t *testing.T) {
 }
 
 func TestStringTableBuilder(t *testing.T) {
-	stb := newStringTableBuilder()
+	stb := newStringTableBuilder(0)
 
-	_, err := readStringTable(bytes.NewReader(stb.Marshal()), nil)
+	_, err := readStringTable(bytes.NewReader(stb.AppendEncoded(nil)), nil)
 	qt.Assert(t, err, qt.IsNil, qt.Commentf("Can't parse string table"))
 
 	_, err = stb.Add("foo\x00bar")
@@ -88,7 +88,7 @@ func TestStringTableBuilder(t *testing.T) {
 	foo2, _ := stb.Add("foo")
 	qt.Assert(t, foo1, qt.Equals, foo2, qt.Commentf("Adding the same string returns different offsets"))
 
-	table := stb.Marshal()
+	table := stb.AppendEncoded(nil)
 	if n := bytes.Count(table, []byte("foo")); n != 1 {
 		t.Fatalf("Marshalled string table contains foo %d times instead of once", n)
 	}
diff --git a/cmd/bpf2go/test/test_bpfeb.go b/cmd/bpf2go/test/test_bpfeb.go
index 8ef6590..8b2cc82 100644
--- a/cmd/bpf2go/test/test_bpfeb.go
+++ b/cmd/bpf2go/test/test_bpfeb.go
@@ -1,6 +1,5 @@
 // Code generated by bpf2go; DO NOT EDIT.
 //go:build arm64be || armbe || mips || mips64 || mips64p32 || ppc64 || s390 || s390x || sparc || sparc64
-// +build arm64be armbe mips mips64 mips64p32 ppc64 s390 s390x sparc sparc64
 
 package test
 
diff --git a/cmd/bpf2go/test/test_bpfel.go b/cmd/bpf2go/test/test_bpfel.go
index 759d368..a817197 100644
--- a/cmd/bpf2go/test/test_bpfel.go
+++ b/cmd/bpf2go/test/test_bpfel.go
@@ -1,6 +1,5 @@
 // Code generated by bpf2go; DO NOT EDIT.
 //go:build 386 || amd64 || amd64p32 || arm || arm64 || mips64le || mips64p32le || mipsle || ppc64le || riscv64
-// +build 386 amd64 amd64p32 arm arm64 mips64le mips64p32le mipsle ppc64le riscv64
 
 package test
 
diff --git a/collection_test.go b/collection_test.go
index 68e888b..4972299 100644
--- a/collection_test.go
+++ b/collection_test.go
@@ -10,6 +10,7 @@ import (
 	"github.com/cilium/ebpf/btf"
 	"github.com/cilium/ebpf/internal"
 	"github.com/cilium/ebpf/internal/testutils"
+	qt "github.com/frankban/quicktest"
 )
 
 func TestCollectionSpecNotModified(t *testing.T) {
@@ -328,6 +329,52 @@ func TestCollectionSpecMapReplacements_SpecMismatch(t *testing.T) {
 	}
 }
 
+func TestCollectionRewriteConstants(t *testing.T) {
+	cs := &CollectionSpec{
+		Maps: map[string]*MapSpec{
+			".rodata": {
+				Type:       Array,
+				KeySize:    4,
+				ValueSize:  4,
+				MaxEntries: 1,
+				Value: &btf.Datasec{
+					Vars: []btf.VarSecinfo{
+						{
+							Type: &btf.Var{
+								Name: "the_constant",
+								Type: &btf.Int{Size: 4},
+							},
+							Offset: 0,
+							Size:   4,
+						},
+					},
+				},
+				Contents: []MapKV{
+					{Key: uint32(0), Value: []byte{1, 1, 1, 1}},
+				},
+			},
+		},
+	}
+
+	err := cs.RewriteConstants(map[string]interface{}{
+		"fake_constant_one": uint32(1),
+		"fake_constant_two": uint32(2),
+	})
+	qt.Assert(t, err, qt.IsNotNil, qt.Commentf("RewriteConstants did not fail"))
+
+	var mErr *MissingConstantsError
+	if !errors.As(err, &mErr) {
+		t.Fatal("Error doesn't wrap MissingConstantsError:", err)
+	}
+	qt.Assert(t, mErr.Constants, qt.ContentEquals, []string{"fake_constant_one", "fake_constant_two"})
+
+	err = cs.RewriteConstants(map[string]interface{}{
+		"the_constant": uint32(0x42424242),
+	})
+	qt.Assert(t, err, qt.IsNil)
+	qt.Assert(t, cs.Maps[".rodata"].Contents[0].Value, qt.ContentEquals, []byte{0x42, 0x42, 0x42, 0x42})
+}
+
 func TestCollectionSpec_LoadAndAssign_LazyLoading(t *testing.T) {
 	spec := &CollectionSpec{
 		Maps: map[string]*MapSpec{
diff --git a/example_sock_elf_test.go b/example_sock_elf_test.go
index 081e1b3..03548d1 100644
--- a/example_sock_elf_test.go
+++ b/example_sock_elf_test.go
@@ -1,5 +1,4 @@
 //go:build linux
-// +build linux
 
 package ebpf_test
 
diff --git a/info_test.go b/info_test.go
index 9655387..c50e18d 100644
--- a/info_test.go
+++ b/info_test.go
@@ -122,8 +122,6 @@ func TestProgramInfo(t *testing.T) {
 }
 
 func TestProgramInfoMapIDs(t *testing.T) {
-	testutils.SkipOnOldKernel(t, "4.10", "reading program info")
-
 	arr, err := NewMap(&MapSpec{
 		Type:       Array,
 		KeySize:    4,
@@ -146,21 +144,52 @@ func TestProgramInfoMapIDs(t *testing.T) {
 	defer prog.Close()
 
 	info, err := prog.Info()
+	testutils.SkipIfNotSupported(t, err)
 	qt.Assert(t, err, qt.IsNil)
 
 	ids, ok := info.MapIDs()
-	if testutils.MustKernelVersion().Less(internal.Version{4, 15, 0}) {
+	switch {
+	case testutils.IsKernelLessThan(t, "4.15"):
 		qt.Assert(t, ok, qt.IsFalse)
 		qt.Assert(t, ids, qt.HasLen, 0)
-	} else {
+
+	default:
 		qt.Assert(t, ok, qt.IsTrue)
-		qt.Assert(t, ids, qt.HasLen, 1)
 
 		mapInfo, err := arr.Info()
 		qt.Assert(t, err, qt.IsNil)
+
 		mapID, ok := mapInfo.ID()
 		qt.Assert(t, ok, qt.IsTrue)
-		qt.Assert(t, ids[0], qt.Equals, mapID)
+		qt.Assert(t, ids, qt.ContentEquals, []MapID{mapID})
+	}
+}
+
+func TestProgramInfoMapIDsNoMaps(t *testing.T) {
+	prog, err := NewProgram(&ProgramSpec{
+		Type: SocketFilter,
+		Instructions: asm.Instructions{
+			asm.LoadImm(asm.R0, 0, asm.DWord),
+			asm.Return(),
+		},
+		License: "MIT",
+	})
+	qt.Assert(t, err, qt.IsNil)
+	defer prog.Close()
+
+	info, err := prog.Info()
+	testutils.SkipIfNotSupported(t, err)
+	qt.Assert(t, err, qt.IsNil)
+
+	ids, ok := info.MapIDs()
+	switch {
+	case testutils.IsKernelLessThan(t, "4.15"):
+		qt.Assert(t, ok, qt.IsFalse)
+		qt.Assert(t, ids, qt.HasLen, 0)
+
+	default:
+		qt.Assert(t, ok, qt.IsTrue)
+		qt.Assert(t, ids, qt.HasLen, 0)
 	}
 }
 
@@ -318,3 +347,7 @@ func testStats(prog *Program) error {
 
 	return nil
 }
+
+func TestHaveProgramInfoMapIDs(t *testing.T) {
+	testutils.CheckFeatureTest(t, haveProgramInfoMapIDs)
+}
diff --git a/internal/deque_test.go b/internal/deque_test.go
index d611c07..88123e7 100644
--- a/internal/deque_test.go
+++ b/internal/deque_test.go
@@ -59,22 +59,24 @@ func TestDeque(t *testing.T) {
 		}
 	})
 
-	t.Run("linearise", func(t *testing.T) {
+	t.Run("grow", func(t *testing.T) {
 		var td Deque[int]
 		td.Push(1)
 		td.Push(2)
+		td.Push(3)
+		td.Shift()
 
-		all := td.linearise(0)
-		if len(all) != 2 {
-			t.Fatal("Expected 2 elements, got", len(all))
+		td.Grow(7)
+		if len(td.elems) < 9 {
+			t.Fatal("Expected at least 9 elements, got", len(td.elems))
 		}
 
-		if cap(all)&(cap(all)-1) != 0 {
-			t.Fatalf("Capacity %d is not a power of two", cap(all))
+		if cap(td.elems)&(cap(td.elems)-1) != 0 {
+			t.Fatalf("Capacity %d is not a power of two", cap(td.elems))
 		}
 
-		if all[0] != 1 || all[1] != 2 {
-			t.Fatal("Elements don't match")
+		if td.Shift() != 2 || td.Shift() != 3 {
+			t.Fatal("Elements don't match after grow")
 		}
 	})
 }
diff --git a/internal/testutils/cgroup.go b/internal/testutils/cgroup.go
index 9b2af5f..b3c6020 100644
--- a/internal/testutils/cgroup.go
+++ b/internal/testutils/cgroup.go
@@ -4,45 +4,29 @@ import (
 	"errors"
 	"os"
 	"strings"
-	"sync"
 	"testing"
 
+	"github.com/cilium/ebpf/internal"
 	"golang.org/x/sys/unix"
 )
 
-var cgroup2 = struct {
-	once sync.Once
-	path string
-	err  error
-}{}
-
-func cgroup2Path() (string, error) {
-	cgroup2.once.Do(func() {
-		mounts, err := os.ReadFile("/proc/mounts")
-		if err != nil {
-			cgroup2.err = err
-			return
-		}
-
-		for _, line := range strings.Split(string(mounts), "\n") {
-			mount := strings.SplitN(line, " ", 3)
-			if mount[0] == "cgroup2" {
-				cgroup2.path = mount[1]
-				return
-			}
+var cgroup2Path = internal.Memoize(func() (string, error) {
+	mounts, err := os.ReadFile("/proc/mounts")
+	if err != nil {
+		return "", err
+	}
 
-			continue
+	for _, line := range strings.Split(string(mounts), "\n") {
+		mount := strings.SplitN(line, " ", 3)
+		if mount[0] == "cgroup2" {
+			return mount[1], nil
 		}
 
-		cgroup2.err = errors.New("cgroup2 not mounted")
-	})
-
-	if cgroup2.err != nil {
-		return "", cgroup2.err
+		continue
 	}
 
-	return cgroup2.path, nil
-}
+	return "", errors.New("cgroup2 not mounted")
+})
 
 func CreateCgroup(tb testing.TB) *os.File {
 	tb.Helper()
diff --git a/internal/testutils/feature.go b/internal/testutils/feature.go
index 0db609e..a8397b2 100644
--- a/internal/testutils/feature.go
+++ b/internal/testutils/feature.go
@@ -8,14 +8,6 @@ import (
 	"github.com/cilium/ebpf/internal"
 )
 
-func MustKernelVersion() internal.Version {
-	v, err := internal.KernelVersion()
-	if err != nil {
-		panic(err)
-	}
-	return v
-}
-
 func CheckFeatureTest(t *testing.T, fn func() error) {
 	checkFeatureTestError(t, fn())
 }
@@ -65,11 +57,10 @@ func checkKernelVersion(tb testing.TB, ufe *internal.UnsupportedFeatureError) {
 		return
 	}
 
-	kernelVersion := MustKernelVersion()
-	if ufe.MinimumVersion.Less(kernelVersion) {
+	if !isKernelLessThan(tb, ufe.MinimumVersion) {
 		tb.Helper()
-		tb.Fatalf("Feature '%s' isn't supported even though kernel %s is newer than %s",
-			ufe.Name, kernelVersion, ufe.MinimumVersion)
+		tb.Fatalf("Feature '%s' isn't supported even though kernel is newer than %s",
+			ufe.Name, ufe.MinimumVersion)
 	}
 }
 
@@ -89,6 +80,12 @@ func IsKernelLessThan(tb testing.TB, minVersion string) bool {
 		tb.Fatalf("Invalid version %s: %s", minVersion, err)
 	}
 
+	return isKernelLessThan(tb, minv)
+}
+
+func isKernelLessThan(tb testing.TB, minv internal.Version) bool {
+	tb.Helper()
+
 	if max := os.Getenv("CI_MAX_KERNEL_VERSION"); max != "" {
 		maxv, err := internal.NewVersion(max)
 		if err != nil {
@@ -100,5 +97,15 @@ func IsKernelLessThan(tb testing.TB, minVersion string) bool {
 		}
 	}
 
-	return MustKernelVersion().Less(minv)
+	return kernelVersion(tb).Less(minv)
+}
+
+func kernelVersion(tb testing.TB) internal.Version {
+	tb.Helper()
+
+	v, err := internal.KernelVersion()
+	if err != nil {
+		tb.Fatal(err)
+	}
+	return v
 }
diff --git a/link/kprobe_multi_test.go b/link/kprobe_multi_test.go
index 57c91a2..e6b64bc 100644
--- a/link/kprobe_multi_test.go
+++ b/link/kprobe_multi_test.go
@@ -2,7 +2,6 @@ package link
 
 import (
 	"errors"
-	"math"
 	"os"
 	"testing"
 
@@ -40,7 +39,7 @@ func TestKprobeMultiInput(t *testing.T) {
 	// Symbols and Addresses are mutually exclusive.
 	_, err = KprobeMulti(prog, KprobeMultiOptions{
 		Symbols:   []string{"foo"},
-		Addresses: []uint64{1},
+		Addresses: []uintptr{1},
 	})
 	if !errors.Is(err, errInvalidInput) {
 		t.Fatalf("expected errInvalidInput, got: %v", err)
@@ -70,7 +69,7 @@ func TestKprobeMultiErrors(t *testing.T) {
 	// Only have a negative test for addresses as it would be hard to maintain a
 	// proper one.
 	if _, err := KprobeMulti(prog, KprobeMultiOptions{
-		Addresses: []uint64{math.MaxUint64},
+		Addresses: []uintptr{^uintptr(0)},
 	}); !errors.Is(err, unix.EINVAL) {
 		t.Fatalf("expected EINVAL, got: %s", err)
 	}
diff --git a/link/kprobe_test.go b/link/kprobe_test.go
index 9c01e43..d6fefa0 100644
--- a/link/kprobe_test.go
+++ b/link/kprobe_test.go
@@ -10,7 +10,6 @@ import (
 
 	"github.com/cilium/ebpf"
 	"github.com/cilium/ebpf/asm"
-	"github.com/cilium/ebpf/internal"
 	"github.com/cilium/ebpf/internal/testutils"
 	"github.com/cilium/ebpf/internal/unix"
 )
@@ -73,25 +72,22 @@ func TestKprobeOffset(t *testing.T) {
 
 func TestKretprobeMaxActive(t *testing.T) {
 	prog := mustLoadProgram(t, ebpf.Kprobe, 0, "")
+	defer prog.Close()
 
-	k, err := Kprobe("do_sys_open", prog, &KprobeOptions{RetprobeMaxActive: 4096})
+	_, err := Kprobe("do_sys_open", prog, &KprobeOptions{RetprobeMaxActive: 4096})
 	if !errors.Is(err, errInvalidMaxActive) {
 		t.Fatal("Expected errInvalidMaxActive, got", err)
 	}
-	if k != nil {
-		k.Close()
-	}
 
-	k, err = Kretprobe("__put_task_struct", prog, &KprobeOptions{RetprobeMaxActive: 4096})
-	if testutils.MustKernelVersion().Less(internal.Version{4, 12, 0}) {
-		if !errors.Is(err, ErrNotSupported) {
-			t.Fatal("Expected ErrNotSupported, got", err)
-		}
-	} else if err != nil {
+	k, err := Kretprobe("__put_task_struct", prog, &KprobeOptions{RetprobeMaxActive: 4096})
+	if testutils.IsKernelLessThan(t, "4.12") && errors.Is(err, ErrNotSupported) {
+		t.Skip("Kernel doesn't support maxactive")
+	}
+	if err != nil {
 		t.Fatal("Kretprobe with maxactive returned an error:", err)
 	}
-	if k != nil {
-		k.Close()
+	if err := k.Close(); err != nil {
+		t.Fatal("Closing kretprobe:", err)
 	}
 }
 
diff --git a/map_test.go b/map_test.go
index 75512a0..a655a45 100644
--- a/map_test.go
+++ b/map_test.go
@@ -621,6 +621,38 @@ func TestMapLoadPinned(t *testing.T) {
 	c.Assert(pinned, qt.IsTrue)
 }
 
+func TestMapLoadReusePinned(t *testing.T) {
+	c := qt.New(t)
+
+	for _, typ := range []MapType{Array, Hash, DevMap, DevMapHash} {
+		t.Run(typ.String(), func(t *testing.T) {
+			if typ == DevMap {
+				testutils.SkipOnOldKernel(t, "4.14", "devmap")
+			}
+			if typ == DevMapHash {
+				testutils.SkipOnOldKernel(t, "5.4", "devmap_hash")
+			}
+			tmp := testutils.TempBPFFS(t)
+			spec := &MapSpec{
+				Name:       "pinmap",
+				Type:       typ,
+				KeySize:    4,
+				ValueSize:  4,
+				MaxEntries: 1,
+				Pinning:    PinByName,
+			}
+
+			m1, err := NewMapWithOptions(spec, MapOptions{PinPath: tmp})
+			c.Assert(err, qt.IsNil)
+			defer m1.Close()
+
+			m2, err := NewMapWithOptions(spec, MapOptions{PinPath: tmp})
+			c.Assert(err, qt.IsNil)
+			defer m2.Close()
+		})
+	}
+}
+
 func TestMapLoadPinnedUnpin(t *testing.T) {
 	tmp := testutils.TempBPFFS(t)
 	c := qt.New(t)
diff --git a/prog_test.go b/prog_test.go
index 583349f..31466df 100644
--- a/prog_test.go
+++ b/prog_test.go
@@ -749,7 +749,7 @@ func TestProgramAttachToKernel(t *testing.T) {
 	testutils.SkipOnOldKernel(t, "5.5", "attach_btf_id")
 
 	haveTestmod := false
-	if !testutils.MustKernelVersion().Less(internal.Version{5, 11}) {
+	if !testutils.IsKernelLessThan(t, "5.11") {
 		// See https://github.com/torvalds/linux/commit/290248a5b7d829871b3ea3c62578613a580a1744
 		testmod, err := btf.FindHandle(func(info *btf.HandleInfo) bool {
 			return info.IsModule() && info.Name == "bpf_testmod"