Repository: cilium/ebpf
Pull request #660 (2022-05-04 12:22:45)

btf: introduce Bits type and remove Int.Bits and Int.Offset
btf: introduce Bits type

    BTF expresses some lengths in bits, others in bytes. So far we don't have a
    good convention how to distinguish the two units. Add a Bits type to make
    the distinction clearer.

btf: remove Int.Bits and Int.Offset

    Unfortunately BTF has two ways to encode bitfields. You can
    create a Member and set Offset / BitfieldSize or create an Int that
    has the offset and bitfield size baked in. The background for this
    can be found in [1].

    Remove Int.Bits and Int.Offset in favour of Member.BitfieldSize and
    Member.Offset. Also get rid of a bunch of incorrect checks for bitfields,
    which we didn't catch due to the weird encoding. This cleans up the API,
    but may make our lives worse if we end up having to support writing out
    legacy bitfields. Let's hope it doesn't come to that.

    The performance of parsing vmlinux hasn't changed dramatically:

        name            old time/op    new time/op    delta
        ParseVmlinux-4    62.1ms ± 1%    61.8ms ± 2%    ~     (p=0.686 n=4+4)

        name            old alloc/op   new alloc/op   delta
        ParseVmlinux-4    44.2MB ± 0%    44.7MB ± 0%  +1.14%  (p=0.029 n=4+4)

        name            old allocs/op  new allocs/op  delta
        ParseVmlinux-4      718k ± 0%      718k ± 0%  +0.00%  (p=0.029 n=4+4)

    1: https://github.com/torvalds/linux/commit/9d5f9f701b1891466fb3dbb1806ad97716f95cc3


Patch:
diff --git a/elf_reader.go b/elf_reader.go
index 8c0a0a1..d3a0913 100644
--- a/elf_reader.go
+++ b/elf_reader.go
@@ -958,7 +958,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.OffsetBits / 8) + vs.Offset
+	start := member.Offset.Bytes() + 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.
diff --git a/internal/btf/btf_types.go b/internal/btf/btf_types.go
index 948dd91..a62b3c9 100644
--- a/internal/btf/btf_types.go
+++ b/internal/btf/btf_types.go
@@ -177,6 +177,10 @@ func (bt *btfType) Size() uint32 {
 	return bt.SizeType
 }
 
+func (bt *btfType) SetSize(size uint32) {
+	bt.SizeType = size
+}
+
 type rawType struct {
 	btfType
 	data interface{}
@@ -285,6 +289,6 @@ func readTypes(r io.Reader, bo binary.ByteOrder, typeLen uint32) ([]rawType, err
 	}
 }
 
-func intEncoding(raw uint32) (IntEncoding, uint32, byte) {
-	return IntEncoding((raw & 0x0f000000) >> 24), (raw & 0x00ff0000) >> 16, byte(raw & 0x000000ff)
+func intEncoding(raw uint32) (IntEncoding, Bits, Bits) {
+	return IntEncoding((raw & 0x0f000000) >> 24), Bits(raw&0x00ff0000) >> 16, Bits(raw & 0x000000ff)
 }
diff --git a/internal/btf/core.go b/internal/btf/core.go
index 600a896..c487548 100644
--- a/internal/btf/core.go
+++ b/internal/btf/core.go
@@ -433,14 +433,14 @@ func coreCalculateFixup(byteOrder binary.ByteOrder, local Type, localID TypeID,
 					return zero, err
 				}
 
-				target = 64 - targetField.bitfieldOffset - targetSize
+				target = uint32(64 - targetField.bitfieldOffset - targetSize)
 			} else {
 				loadWidth, err := Sizeof(targetField.Type)
 				if err != nil {
 					return zero, err
 				}
 
-				target = 64 - uint32(loadWidth)*8 + targetField.bitfieldOffset
+				target = uint32(64 - Bits(loadWidth*8) + targetField.bitfieldOffset)
 			}
 			return fixupWithoutValidation(0, target)
 
@@ -450,7 +450,7 @@ func coreCalculateFixup(byteOrder binary.ByteOrder, local Type, localID TypeID,
 				return zero, err
 			}
 
-			return fixupWithoutValidation(0, 64-targetSize)
+			return fixupWithoutValidation(0, uint32(64-targetSize))
 		}
 	}
 
@@ -546,12 +546,12 @@ type coreField struct {
 	offset uint32
 
 	// The offset of the bitfield in bits from the start of the field.
-	bitfieldOffset uint32
+	bitfieldOffset Bits
 
 	// The size of the bitfield in bits.
 	//
 	// Zero if the field is not a bitfield.
-	bitfieldSize uint32
+	bitfieldSize Bits
 }
 
 func (cf *coreField) adjustOffsetToNthElement(n int) error {
@@ -564,7 +564,7 @@ func (cf *coreField) adjustOffsetToNthElement(n int) error {
 	return nil
 }
 
-func (cf *coreField) adjustOffsetBits(offset uint32) error {
+func (cf *coreField) adjustOffsetBits(offset Bits) error {
 	align, err := alignof(cf.Type)
 	if err != nil {
 		return err
@@ -574,11 +574,11 @@ func (cf *coreField) adjustOffsetBits(offset uint32) error {
 	// 1) converting the bit offset to bytes with a flooring division.
 	// 2) dividing and multiplying that offset by the alignment, yielding the
 	//    load size aligned offset.
-	offsetBytes := (offset / 8) / uint32(align) * uint32(align)
+	offsetBytes := uint32(offset/8) / uint32(align) * uint32(align)
 
 	// The number of bits remaining is the bit offset less the number of bits
 	// we can "skip" with the aligned offset.
-	cf.bitfieldOffset = offset - offsetBytes*8
+	cf.bitfieldOffset = offset - Bits(offsetBytes*8)
 
 	// We know that cf.offset is aligned at to at least align since we get it
 	// from the compiler via BTF. Adding an aligned offsetBytes preserves the
@@ -587,7 +587,7 @@ func (cf *coreField) adjustOffsetBits(offset uint32) error {
 	return nil
 }
 
-func (cf *coreField) sizeBits() (uint32, error) {
+func (cf *coreField) sizeBits() (Bits, error) {
 	if cf.bitfieldSize > 0 {
 		return cf.bitfieldSize, nil
 	}
@@ -599,7 +599,7 @@ func (cf *coreField) sizeBits() (uint32, error) {
 	if err != nil {
 		return 0, nil
 	}
-	return uint32(size) * 8, nil
+	return Bits(size * 8), nil
 }
 
 // coreFindField descends into the local type using the accessor and tries to
@@ -646,7 +646,7 @@ func coreFindField(localT Type, localAcc coreAccessor, targetT Type) (coreField,
 				// This is an anonymous struct or union, ignore it.
 				local = coreField{
 					Type:   localMember.Type,
-					offset: local.offset + localMember.OffsetBits/8,
+					offset: local.offset + localMember.Offset.Bytes(),
 				}
 				localMaybeFlex = false
 				continue
@@ -677,8 +677,8 @@ func coreFindField(localT Type, localAcc coreAccessor, targetT Type) (coreField,
 			targetMaybeFlex = last
 
 			if local.bitfieldSize == 0 && target.bitfieldSize == 0 {
-				local.offset += localMember.OffsetBits / 8
-				target.offset += targetMember.OffsetBits / 8
+				local.offset += localMember.Offset.Bytes()
+				target.offset += targetMember.Offset.Bytes()
 				break
 			}
 
@@ -688,11 +688,11 @@ func coreFindField(localT Type, localAcc coreAccessor, targetT Type) (coreField,
 				return coreField{}, coreField{}, fmt.Errorf("can't descend into bitfield")
 			}
 
-			if err := local.adjustOffsetBits(localMember.OffsetBits); err != nil {
+			if err := local.adjustOffsetBits(localMember.Offset); err != nil {
 				return coreField{}, coreField{}, err
 			}
 
-			if err := target.adjustOffsetBits(targetMember.OffsetBits); err != nil {
+			if err := target.adjustOffsetBits(targetMember.Offset); err != nil {
 				return coreField{}, coreField{}, err
 			}
 
@@ -758,7 +758,7 @@ func coreFindMember(typ composite, name string) (Member, bool, error) {
 
 	type offsetTarget struct {
 		composite
-		offset uint32
+		offset Bits
 	}
 
 	targets := []offsetTarget{{typ, 0}}
@@ -782,7 +782,7 @@ func coreFindMember(typ composite, name string) (Member, bool, error) {
 		for j, member := range members {
 			if member.Name == name {
 				// NB: This is safe because member is a copy.
-				member.OffsetBits += target.offset
+				member.Offset += target.offset
 				return member, j == len(members)-1, nil
 			}
 
@@ -797,7 +797,7 @@ func coreFindMember(typ composite, name string) (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.OffsetBits})
+			targets = append(targets, offsetTarget{comp, target.offset + member.Offset})
 		}
 	}
 
@@ -871,15 +871,9 @@ func coreAreTypesCompatible(localType Type, targetType Type) error {
 		}
 
 		switch lv := (localType).(type) {
-		case *Void, *Struct, *Union, *Enum, *Fwd:
+		case *Void, *Struct, *Union, *Enum, *Fwd, *Int:
 			// Nothing to do here
 
-		case *Int:
-			tv := targetType.(*Int)
-			if lv.isBitfield() || tv.isBitfield() {
-				return fmt.Errorf("bitfield: %w", errImpossibleRelocation)
-			}
-
 		case *Pointer, *Array:
 			depth++
 			localType.walk(&localTs)
@@ -961,7 +955,7 @@ func coreAreMembersCompatible(localType Type, targetType Type) error {
 	}
 
 	switch lv := localType.(type) {
-	case *Array, *Pointer, *Float:
+	case *Array, *Pointer, *Float, *Int:
 		return nil
 
 	case *Enum:
@@ -972,13 +966,6 @@ func coreAreMembersCompatible(localType Type, targetType Type) error {
 		tv := targetType.(*Fwd)
 		return doNamesMatch(lv.Name, tv.Name)
 
-	case *Int:
-		tv := targetType.(*Int)
-		if lv.isBitfield() || tv.isBitfield() {
-			return fmt.Errorf("bitfield: %w", errImpossibleRelocation)
-		}
-		return nil
-
 	default:
 		return fmt.Errorf("type %s: %w", localType, ErrNotSupported)
 	}
diff --git a/internal/btf/format.go b/internal/btf/format.go
index 3acb9b3..c773312 100644
--- a/internal/btf/format.go
+++ b/internal/btf/format.go
@@ -179,7 +179,7 @@ func (gf *GoFormatter) writeStructLit(size uint32, members []Member, depth int)
 			continue
 		}
 
-		offset := m.OffsetBits / 8
+		offset := m.Offset.Bytes()
 		if n := offset - prevOffset; skippedBitfield && n > 0 {
 			fmt.Fprintf(&gf.w, "_ [%d]byte /* unsupported bitfield */; ", n)
 		} else {
@@ -206,8 +206,8 @@ func (gf *GoFormatter) writeStructField(m Member, depth int) error {
 	if m.BitfieldSize > 0 {
 		return fmt.Errorf("bitfields are not supported")
 	}
-	if m.OffsetBits%8 != 0 {
-		return fmt.Errorf("unsupported offset %d", m.OffsetBits)
+	if m.Offset%8 != 0 {
+		return fmt.Errorf("unsupported offset %d", m.Offset)
 	}
 
 	if m.Name == "" {
diff --git a/internal/btf/types.go b/internal/btf/types.go
index 4c8a3b8..82e7fea 100644
--- a/internal/btf/types.go
+++ b/internal/btf/types.go
@@ -110,27 +110,18 @@ func (ie IntEncoding) String() string {
 }
 
 // Int is an integer of a given length.
+//
+// See https://www.kernel.org/doc/html/latest/bpf/btf.html#btf-kind-int
 type Int struct {
 	Name string
 
 	// The size of the integer in bytes.
 	Size     uint32
 	Encoding IntEncoding
-	// OffsetBits is the starting bit offset. Currently always 0.
-	// See https://www.kernel.org/doc/html/latest/bpf/btf.html#btf-kind-int
-	OffsetBits uint32
-	Bits       byte
 }
 
 func (i *Int) Format(fs fmt.State, verb rune) {
-	extra := []interface{}{
-		i.Encoding,
-		"size=", i.Size * 8,
-	}
-	if i.Bits > 0 {
-		extra = append(extra, "bits=", i.Bits)
-	}
-	formatType(fs, verb, i, extra...)
+	formatType(fs, verb, i, i.Encoding, "size=", i.Size*8)
 }
 
 func (i *Int) TypeName() string { return i.Name }
@@ -141,10 +132,6 @@ func (i *Int) copy() Type {
 	return &cpy
 }
 
-func (i *Int) isBitfield() bool {
-	return i.OffsetBits > 0
-}
-
 // Pointer is a pointer to another type.
 type Pointer struct {
 	Target Type
@@ -259,15 +246,22 @@ var (
 	_ composite = (*Union)(nil)
 )
 
+// A value in bits.
+type Bits uint32
+
+// Bytes converts a bit value into bytes.
+func (b Bits) Bytes() uint32 {
+	return uint32(b / 8)
+}
+
 // Member is part of a Struct or Union.
 //
 // It is not a valid Type.
 type Member struct {
-	Name string
-	Type Type
-	// OffsetBits is the bit offset of this member.
-	OffsetBits   uint32
-	BitfieldSize uint32
+	Name         string
+	Type         Type
+	Offset       Bits
+	BitfieldSize Bits
 }
 
 // Enum lists possible values.
@@ -817,6 +811,15 @@ func inflateRawTypes(rawTypes []rawType, rawStrings *stringTable) ([]Type, error
 		return nil
 	}
 
+	type bitfieldFixupDef struct {
+		id TypeID
+		m  *Member
+	}
+
+	var (
+		legacyBitfields = make(map[TypeID][2]Bits) // offset, size
+		bitfieldFixups  []bitfieldFixupDef
+	)
 	convertMembers := func(raw []btfMember, kindFlag bool) ([]Member, error) {
 		// NB: The fixup below relies on pre-allocating this array to
 		// work, since otherwise append might re-allocate members.
@@ -826,18 +829,48 @@ func inflateRawTypes(rawTypes []rawType, rawStrings *stringTable) ([]Type, error
 			if err != nil {
 				return nil, fmt.Errorf("can't get name for member %d: %w", i, err)
 			}
-			m := Member{
-				Name:       name,
-				OffsetBits: btfMember.Offset,
-			}
+
+			members = append(members, Member{
+				Name:   name,
+				Offset: Bits(btfMember.Offset),
+			})
+
+			m := &members[i]
+			fixup(raw[i].Type, &m.Type)
+
 			if kindFlag {
-				m.BitfieldSize = btfMember.Offset >> 24
-				m.OffsetBits &= 0xffffff
+				m.BitfieldSize = Bits(btfMember.Offset >> 24)
+				m.Offset &= 0xffffff
+				// We ignore legacy bitfield definitions if the current composite
+				// is a new-style bitfield. This is kind of safe since offset and
+				// size on the type of the member must be zero if kindFlat is set
+				// according to spec.
+				continue
 			}
-			members = append(members, m)
-		}
-		for i := range members {
-			fixup(raw[i].Type, &members[i].Type)
+
+			// This may be a legacy bitfield, try to fix it up.
+			data, ok := legacyBitfields[raw[i].Type]
+			if ok {
+				// Bingo!
+				m.Offset += data[0]
+				m.BitfieldSize = data[1]
+				continue
+			}
+
+			if m.Type != nil {
+				// We couldn't find a legacy bitfield, but we know that the member's
+				// type has already been inflated. Hence we know that it can't be
+				// a legacy bitfield and there is nothing left to do.
+				continue
+			}
+
+			// We don't have fixup data, and the type we're pointing
+			// at hasn't been inflated yet. No choice but to defer
+			// the fixup.
+			bitfieldFixups = append(bitfieldFixups, bitfieldFixupDef{
+				raw[i].Type,
+				m,
+			})
 		}
 		return members, nil
 	}
@@ -857,8 +890,12 @@ func inflateRawTypes(rawTypes []rawType, rawStrings *stringTable) ([]Type, error
 
 		switch raw.Kind() {
 		case kindInt:
+			size := raw.Size()
 			encoding, offset, bits := intEncoding(*raw.data.(*uint32))
-			typ = &Int{name, raw.Size(), encoding, offset, bits}
+			if offset > 0 || bits.Bytes() != size {
+				legacyBitfields[id] = [2]Bits{offset, bits}
+			}
+			typ = &Int{name, size, encoding}
 
 		case kindPointer:
 			ptr := &Pointer{nil}
@@ -1000,6 +1037,15 @@ func inflateRawTypes(rawTypes []rawType, rawStrings *stringTable) ([]Type, error
 		*fixup.typ = types[i]
 	}
 
+	for _, bitfieldFixup := range bitfieldFixups {
+		data, ok := legacyBitfields[bitfieldFixup.id]
+		if ok {
+			// This is indeed a legacy bitfield, fix it up.
+			bitfieldFixup.m.Offset += data[0]
+			bitfieldFixup.m.BitfieldSize = data[1]
+		}
+	}
+
 	for _, assertion := range assertions {
 		if reflect.TypeOf(*assertion.typ) != assertion.want {
 			return nil, fmt.Errorf("expected %s, got %T", assertion.want, *assertion.typ)
diff --git a/internal/cmd/gentypes/main.go b/internal/cmd/gentypes/main.go
index 5d91e4b..8ef8ac6 100644
--- a/internal/cmd/gentypes/main.go
+++ b/internal/cmd/gentypes/main.go
@@ -628,7 +628,7 @@ func flattenAnon(s *btf.Struct) error {
 		}
 
 		for j := range cs.Members {
-			cs.Members[j].OffsetBits += m.OffsetBits
+			cs.Members[j].Offset += m.Offset
 		}
 
 		newMembers := make([]btf.Member, 0, len(s.Members)+len(cs.Members)-1)
@@ -655,7 +655,7 @@ func truncateAfter(name string) patch {
 			}
 
 			s.Members = s.Members[:i+1]
-			s.Size = (m.OffsetBits / 8) + uint32(size)
+			s.Size = m.Offset.Bytes() + uint32(size)
 			return nil
 		}
 
@@ -699,7 +699,7 @@ func replaceUnionWithBytes(name string) patch {
 						Type:   &btf.Int{Size: 1},
 						Nelems: u.Size,
 					},
-					OffsetBits:   s.Members[i].OffsetBits,
+					Offset:       s.Members[i].Offset,
 					BitfieldSize: s.Members[i].BitfieldSize,
 				}
 				return nil
diff --git a/perf/reader.go b/perf/reader.go
index 1d40e01..cd249e1 100644
--- a/perf/reader.go
+++ b/perf/reader.go
@@ -20,6 +20,8 @@ var (
 	errEOR    = errors.New("end of ring")
 )
 
+var perfEventHeaderSize = binary.Size(perfEventHeader{})
+
 // perfEventHeader must match 'struct perf_event_header` in <linux/perf_event.h>.
 type perfEventHeader struct {
 	Type uint32
@@ -47,34 +49,39 @@ type Record struct {
 	LostSamples uint64
 }
 
-// NB: Has to be preceded by a call to ring.loadHead.
-func readRecordFromRing(ring *perfEventRing) (Record, error) {
-	defer ring.writeTail()
-	return readRecord(ring, ring.cpu)
-}
-
-func readRecord(rd io.Reader, cpu int) (Record, error) {
-	var header perfEventHeader
-	err := binary.Read(rd, internal.NativeEndian, &header)
-	if err == io.EOF {
-		return Record{}, errEOR
+// Read a record from a reader and tag it as being from the given CPU.
+//
+// buf must be at least perfEventHeaderSize bytes long.
+func readRecord(rd io.Reader, rec *Record, buf []byte) error {
+	// Assert that the buffer is large enough.
+	buf = buf[:perfEventHeaderSize]
+	_, err := io.ReadFull(rd, buf)
+	if errors.Is(err, io.EOF) {
+		return errEOR
+	} else if err != nil {
+		return fmt.Errorf("read perf event header: %v", err)
 	}
 
-	if err != nil {
-		return Record{}, fmt.Errorf("can't read event header: %v", err)
+	header := perfEventHeader{
+		internal.NativeEndian.Uint32(buf[0:4]),
+		internal.NativeEndian.Uint16(buf[4:6]),
+		internal.NativeEndian.Uint16(buf[6:8]),
 	}
 
 	switch header.Type {
 	case unix.PERF_RECORD_LOST:
-		lost, err := readLostRecords(rd)
-		return Record{CPU: cpu, LostSamples: lost}, err
+		rec.RawSample = rec.RawSample[:0]
+		rec.LostSamples, err = readLostRecords(rd)
+		return err
 
 	case unix.PERF_RECORD_SAMPLE:
-		sample, err := readRawSample(rd)
-		return Record{CPU: cpu, RawSample: sample}, err
+		rec.LostSamples = 0
+		// We can reuse buf here because perfEventHeaderSize > perfEventSampleSize.
+		rec.RawSample, err = readRawSample(rd, buf, rec.RawSample)
+		return err
 
 	default:
-		return Record{}, &unknownEventError{header.Type}
+		return &unknownEventError{header.Type}
 	}
 }
 
@@ -93,16 +100,32 @@ func readLostRecords(rd io.Reader) (uint64, error) {
 	return lostHeader.Lost, nil
 }
 
-func readRawSample(rd io.Reader) ([]byte, error) {
-	// This must match 'struct perf_event_sample in kernel sources.
-	var size uint32
-	if err := binary.Read(rd, internal.NativeEndian, &size); err != nil {
-		return nil, fmt.Errorf("can't read sample size: %v", err)
+var perfEventSampleSize = binary.Size(uint32(0))
+
+// This must match 'struct perf_event_sample in kernel sources.
+type perfEventSample struct {
+	Size uint32
+}
+
+func readRawSample(rd io.Reader, buf, sampleBuf []byte) ([]byte, error) {
+	buf = buf[:perfEventSampleSize]
+	if _, err := io.ReadFull(rd, buf); err != nil {
+		return nil, fmt.Errorf("read sample size: %v", err)
+	}
+
+	sample := perfEventSample{
+		internal.NativeEndian.Uint32(buf),
+	}
+
+	var data []byte
+	if size := int(sample.Size); cap(sampleBuf) < size {
+		data = make([]byte, size)
+	} else {
+		data = sampleBuf[:size]
 	}
 
-	data := make([]byte, int(size))
 	if _, err := io.ReadFull(rd, data); err != nil {
-		return nil, fmt.Errorf("can't read sample: %v", err)
+		return nil, fmt.Errorf("read sample: %v", err)
 	}
 	return data, nil
 }
@@ -123,6 +146,7 @@ type Reader struct {
 	rings       []*perfEventRing
 	epollEvents []unix.EpollEvent
 	epollRings  []*perfEventRing
+	eventHeader []byte
 
 	// pauseFds are a copy of the fds in 'rings', protected by 'pauseMu'.
 	// These allow Pause/Resume to be executed independently of any ongoing
@@ -215,6 +239,7 @@ func NewReaderWithOptions(array *ebpf.Map, perCPUBuffer int, opts ReaderOptions)
 		poller:      poller,
 		epollEvents: make([]unix.EpollEvent, len(rings)),
 		epollRings:  make([]*perfEventRing, 0, len(rings)),
+		eventHeader: make([]byte, perfEventHeaderSize),
 		pauseFds:    pauseFds,
 	}
 	if err = pr.Resume(); err != nil {
@@ -266,18 +291,24 @@ func (pr *Reader) Close() error {
 //
 // Calling Close interrupts the function.
 func (pr *Reader) Read() (Record, error) {
+	var r Record
+	return r, pr.ReadInto(&r)
+}
+
+// ReadInto is like Read except that it allows reusing Record and associated buffers.
+func (pr *Reader) ReadInto(rec *Record) error {
 	pr.mu.Lock()
 	defer pr.mu.Unlock()
 
 	if pr.rings == nil {
-		return Record{}, fmt.Errorf("perf ringbuffer: %w", ErrClosed)
+		return fmt.Errorf("perf ringbuffer: %w", ErrClosed)
 	}
 
 	for {
 		if len(pr.epollRings) == 0 {
 			nEvents, err := pr.poller.Wait(pr.epollEvents)
 			if err != nil {
-				return Record{}, err
+				return err
 			}
 
 			for _, event := range pr.epollEvents[:nEvents] {
@@ -294,7 +325,7 @@ func (pr *Reader) Read() (Record, error) {
 		// Start at the last available event. The order in which we
 		// process them doesn't matter, and starting at the back allows
 		// resizing epollRings to keep track of processed rings.
-		record, err := readRecordFromRing(pr.epollRings[len(pr.epollRings)-1])
+		err := pr.readRecordFromRing(rec, pr.epollRings[len(pr.epollRings)-1])
 		if err == errEOR {
 			// We've emptied the current ring buffer, process
 			// the next one.
@@ -302,7 +333,7 @@ func (pr *Reader) Read() (Record, error) {
 			continue
 		}
 
-		return record, err
+		return err
 	}
 }
 
@@ -353,6 +384,14 @@ func (pr *Reader) Resume() error {
 	return nil
 }
 
+// NB: Has to be preceded by a call to ring.loadHead.
+func (pr *Reader) readRecordFromRing(rec *Record, ring *perfEventRing) error {
+	defer ring.writeTail()
+
+	rec.CPU = ring.cpu
+	return readRecord(ring, rec, pr.eventHeader)
+}
+
 type unknownEventError struct {
 	eventType uint32
 }
diff --git a/ringbuf/reader.go b/ringbuf/reader.go
index 64e5fec..3b1f26e 100644
--- a/ringbuf/reader.go
+++ b/ringbuf/reader.go
@@ -20,6 +20,8 @@ var (
 	errBusy    = errors.New("sample not committed yet")
 )
 
+var ringbufHeaderSize = binary.Size(ringbufHeader{})
+
 // ringbufHeader from 'struct bpf_ringbuf_hdr' in kernel/bpf/ringbuf.c
 type ringbufHeader struct {
 	Len   uint32
@@ -42,23 +44,29 @@ type Record struct {
 	RawSample []byte
 }
 
-func readRecord(rd *ringbufEventRing) (r Record, err error) {
+// Read a record from an event ring.
+//
+// buf must be at least ringbufHeaderSize bytes long.
+func readRecord(rd *ringbufEventRing, rec *Record, buf []byte) error {
 	rd.loadConsumer()
-	var header ringbufHeader
-	err = binary.Read(rd, internal.NativeEndian, &header)
-	if err == io.EOF {
-		return Record{}, err
+
+	buf = buf[:ringbufHeaderSize]
+	if _, err := io.ReadFull(rd, buf); err == io.EOF {
+		return err
+	} else if err != nil {
+		return fmt.Errorf("read event header: %w", err)
 	}
 
-	if err != nil {
-		return Record{}, fmt.Errorf("can't read event header: %w", err)
+	header := ringbufHeader{
+		internal.NativeEndian.Uint32(buf[0:4]),
+		internal.NativeEndian.Uint32(buf[4:8]),
 	}
 
 	if header.isBusy() {
 		// the next sample in the ring is not committed yet so we
 		// exit without storing the reader/consumer position
 		// and start again from the same position.
-		return Record{}, fmt.Errorf("%w", errBusy)
+		return fmt.Errorf("%w", errBusy)
 	}
 
 	/* read up to 8 byte alignment */
@@ -73,18 +81,22 @@ func readRecord(rd *ringbufEventRing) (r Record, err error) {
 		rd.skipRead(dataLenAligned)
 		rd.storeConsumer()
 
-		return Record{}, fmt.Errorf("%w", errDiscard)
+		return fmt.Errorf("%w", errDiscard)
 	}
 
-	data := make([]byte, dataLenAligned)
+	if cap(rec.RawSample) < int(dataLenAligned) {
+		rec.RawSample = make([]byte, dataLenAligned)
+	} else {
+		rec.RawSample = rec.RawSample[:dataLenAligned]
+	}
 
-	if _, err := io.ReadFull(rd, data); err != nil {
-		return Record{}, fmt.Errorf("can't read sample: %w", err)
+	if _, err := io.ReadFull(rd, rec.RawSample); err != nil {
+		return fmt.Errorf("read sample: %w", err)
 	}
 
 	rd.storeConsumer()
-
-	return Record{RawSample: data[:header.dataLen()]}, nil
+	rec.RawSample = rec.RawSample[:header.dataLen()]
+	return nil
 }
 
 // Reader allows reading bpf_ringbuf_output
@@ -96,6 +108,7 @@ type Reader struct {
 	mu          sync.Mutex
 	ring        *ringbufEventRing
 	epollEvents []unix.EpollEvent
+	header      []byte
 }
 
 // NewReader creates a new BPF ringbuf reader.
@@ -129,6 +142,7 @@ func NewReader(ringbufMap *ebpf.Map) (*Reader, error) {
 		poller:      poller,
 		ring:        ring,
 		epollEvents: make([]unix.EpollEvent, 1),
+		header:      make([]byte, ringbufHeaderSize),
 	}, nil
 }
 
@@ -159,24 +173,30 @@ func (r *Reader) Close() error {
 //
 // Calling Close interrupts the function.
 func (r *Reader) Read() (Record, error) {
+	var rec Record
+	return rec, r.ReadInto(&rec)
+}
+
+// ReadInto is like Read except that it allows reusing Record and associated buffers.
+func (r *Reader) ReadInto(rec *Record) error {
 	r.mu.Lock()
 	defer r.mu.Unlock()
 
 	if r.ring == nil {
-		return Record{}, fmt.Errorf("ringbuffer: %w", ErrClosed)
+		return fmt.Errorf("ringbuffer: %w", ErrClosed)
 	}
 
 	for {
 		_, err := r.poller.Wait(r.epollEvents)
 		if err != nil {
-			return Record{}, err
+			return err
 		}
 
-		record, err := readRecord(r.ring)
+		err = readRecord(r.ring, rec, r.header)
 		if errors.Is(err, errBusy) || errors.Is(err, errDiscard) {
 			continue
 		}
 
-		return record, err
+		return err
 	}
 }


Test patch:
diff --git a/internal/btf/btf_test.go b/internal/btf/btf_test.go
index 9af5e50..1697d90 100644
--- a/internal/btf/btf_test.go
+++ b/internal/btf/btf_test.go
@@ -134,28 +134,38 @@ func TestTypeByName(t *testing.T) {
 		t.Fatal("multiple TypeByName calls for `iphdr` name do not return the same addresses")
 	}
 
-	for _, m := range iphdr1.Members {
-		if m.Name == "version" {
-			// __u8 is a typedef
-			td, ok := m.Type.(*Typedef)
-			if !ok {
-				t.Fatalf("version member of iphdr should be a __u8 typedef: actual: %T", m.Type)
-			}
-			u8int, ok := td.Type.(*Int)
-			if !ok {
-				t.Fatalf("__u8 typedef should point to an Int type: actual: %T", td.Type)
-			}
-			if u8int.Bits != 8 {
-				t.Fatalf("incorrect bit size of an __u8 int: expected: 8 actual: %d", u8int.Bits)
-			}
-			if u8int.Encoding != 0 {
-				t.Fatalf("incorrect encoding of an __u8 int: expected: 0 actual: %x", u8int.Encoding)
-			}
-			if u8int.OffsetBits != 0 {
-				t.Fatalf("incorrect int offset of an __u8 int: expected: 0 actual: %d", u8int.OffsetBits)
-			}
-			break
-		}
+	// Excerpt from linux/ip.h, https://elixir.bootlin.com/linux/latest/A/ident/iphdr
+	//
+	// struct iphdr {
+	// #if defined(__LITTLE_ENDIAN_BITFIELD)
+	//     __u8 ihl:4, version:4;
+	// #elif defined (__BIG_ENDIAN_BITFIELD)
+	//     __u8 version:4, ihl:4;
+	// #else
+	//     ...
+	// }
+	//
+	// The BTF we test against is for little endian.
+	m := iphdr1.Members[1]
+	if m.Name != "version" {
+		t.Fatal("Expected version as the second member, got", m.Name)
+	}
+	td, ok := m.Type.(*Typedef)
+	if !ok {
+		t.Fatalf("version member of iphdr should be a __u8 typedef: actual: %T", m.Type)
+	}
+	u8, ok := td.Type.(*Int)
+	if !ok {
+		t.Fatalf("__u8 typedef should point to an Int type: actual: %T", td.Type)
+	}
+	if m.BitfieldSize != 4 {
+		t.Fatalf("incorrect bitfield size: expected: 4 actual: %d", m.BitfieldSize)
+	}
+	if u8.Encoding != 0 {
+		t.Fatalf("incorrect encoding of an __u8 int: expected: 0 actual: %x", u8.Encoding)
+	}
+	if m.Offset != 4 {
+		t.Fatalf("incorrect bitfield offset: expected: 4 actual: %d", m.Offset)
 	}
 }
 
diff --git a/internal/btf/core_test.go b/internal/btf/core_test.go
index d777cf2..a6e7e19 100644
--- a/internal/btf/core_test.go
+++ b/internal/btf/core_test.go
@@ -25,7 +25,6 @@ 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{OffsetBits: 1}, &Int{}, false},
 		{&Pointer{Target: &Void{}}, &Pointer{Target: &Void{}}, true},
 		{&Pointer{Target: &Void{}}, &Void{}, false},
 		{&Array{Type: &Void{}}, &Array{Type: &Void{}}, true},
@@ -99,7 +98,6 @@ 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{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},
@@ -232,22 +230,22 @@ func TestCOREFindField(t *testing.T) {
 	u16 := &Int{Size: 2}
 	u32 := &Int{Size: 4}
 	aFields := []Member{
-		{Name: "foo", Type: ptr, OffsetBits: 8},
-		{Name: "bar", Type: u16, OffsetBits: 16},
-		{Name: "baz", Type: u32, OffsetBits: 32, BitfieldSize: 3},
-		{Name: "quux", Type: u32, OffsetBits: 35, BitfieldSize: 10},
-		{Name: "quuz", Type: u32, OffsetBits: 45, BitfieldSize: 8},
+		{Name: "foo", Type: ptr, Offset: 8},
+		{Name: "bar", Type: u16, Offset: 16},
+		{Name: "baz", Type: u32, Offset: 32, BitfieldSize: 3},
+		{Name: "quux", Type: u32, Offset: 35, BitfieldSize: 10},
+		{Name: "quuz", Type: u32, Offset: 45, BitfieldSize: 8},
 	}
 	bFields := []Member{
-		{Name: "foo", Type: ptr, OffsetBits: 16},
-		{Name: "bar", Type: u32, OffsetBits: 8},
-		{Name: "other", OffsetBits: 4},
+		{Name: "foo", Type: ptr, Offset: 16},
+		{Name: "bar", Type: u32, Offset: 8},
+		{Name: "other", Offset: 4},
 		// baz is separated out from the other bitfields
-		{Name: "baz", Type: u32, OffsetBits: 64, BitfieldSize: 3},
+		{Name: "baz", Type: u32, Offset: 64, BitfieldSize: 3},
 		// quux's type changes u32->u16
-		{Name: "quux", Type: u16, OffsetBits: 96, BitfieldSize: 10},
+		{Name: "quux", Type: u16, Offset: 96, BitfieldSize: 10},
 		// quuz becomes a normal field
-		{Name: "quuz", Type: u16, OffsetBits: 112},
+		{Name: "quuz", Type: u16, Offset: 112},
 	}
 
 	aStruct := &Struct{Members: aFields, Size: 48}
@@ -327,8 +325,8 @@ func TestCOREFindField(t *testing.T) {
 		return uint32(sz)
 	}
 
-	anon := func(t Type, offset uint32) []Member {
-		return []Member{{Type: t, OffsetBits: offset}}
+	anon := func(t Type, offset Bits) []Member {
+		return []Member{{Type: t, Offset: offset}}
 	}
 
 	anonStruct := func(m ...Member) Member {
diff --git a/internal/btf/format_test.go b/internal/btf/format_test.go
index 08e0bd8..9669b1e 100644
--- a/internal/btf/format_test.go
+++ b/internal/btf/format_test.go
@@ -41,8 +41,8 @@ func TestGoTypeDeclaration(t *testing.T) {
 				Name: "field padding",
 				Size: 16,
 				Members: []Member{
-					{Name: "frob", Type: &Int{Size: 4}, OffsetBits: 0},
-					{Name: "foo", Type: &Int{Size: 8}, OffsetBits: 8 * 8},
+					{Name: "frob", Type: &Int{Size: 4}, Offset: 0},
+					{Name: "foo", Type: &Int{Size: 8}, Offset: 8 * 8},
 				},
 			},
 			"type t struct { frob uint32; _ [4]byte; foo uint64; }",
@@ -52,8 +52,8 @@ func TestGoTypeDeclaration(t *testing.T) {
 				Name: "end padding",
 				Size: 16,
 				Members: []Member{
-					{Name: "foo", Type: &Int{Size: 8}, OffsetBits: 0},
-					{Name: "frob", Type: &Int{Size: 4}, OffsetBits: 8 * 8},
+					{Name: "foo", Type: &Int{Size: 8}, Offset: 0},
+					{Name: "frob", Type: &Int{Size: 4}, Offset: 8 * 8},
 				},
 			},
 			"type t struct { foo uint64; frob uint32; _ [4]byte; }",
@@ -63,8 +63,8 @@ func TestGoTypeDeclaration(t *testing.T) {
 				Name: "bitfield",
 				Size: 8,
 				Members: []Member{
-					{Name: "foo", Type: &Int{Size: 4}, OffsetBits: 0, BitfieldSize: 1},
-					{Name: "frob", Type: &Int{Size: 4}, OffsetBits: 4 * 8},
+					{Name: "foo", Type: &Int{Size: 4}, Offset: 0, BitfieldSize: 1},
+					{Name: "frob", Type: &Int{Size: 4}, Offset: 4 * 8},
 				},
 			},
 			"type t struct { _ [4]byte /* unsupported bitfield */; frob uint32; }",
@@ -79,11 +79,11 @@ func TestGoTypeDeclaration(t *testing.T) {
 						Type: &Struct{
 							Size: 4,
 							Members: []Member{
-								{Name: "bar", Type: &Int{Size: 4}, OffsetBits: 0},
+								{Name: "bar", Type: &Int{Size: 4}, Offset: 0},
 							},
 						},
 					},
-					{Name: "frob", Type: &Int{Size: 4}, OffsetBits: 4 * 8},
+					{Name: "frob", Type: &Int{Size: 4}, Offset: 4 * 8},
 				},
 			},
 			"type t struct { foo struct { bar uint32; }; frob uint32; }",
@@ -98,8 +98,8 @@ func TestGoTypeDeclaration(t *testing.T) {
 						Type: &Union{
 							Size: 4,
 							Members: []Member{
-								{Name: "foo", Type: &Int{Size: 4}, OffsetBits: 0},
-								{Name: "bar", Type: &Int{Size: 4}, OffsetBits: 0},
+								{Name: "foo", Type: &Int{Size: 4}, Offset: 0},
+								{Name: "bar", Type: &Int{Size: 4}, Offset: 0},
 							},
 						},
 					},
diff --git a/internal/btf/strings_test.go b/internal/btf/strings_test.go
index 9a15ab0..3fb0821 100644
--- a/internal/btf/strings_test.go
+++ b/internal/btf/strings_test.go
@@ -59,3 +59,15 @@ func TestStringTable(t *testing.T) {
 		t.Fatal("Accepted non-empty first item")
 	}
 }
+
+func newStringTable(strings ...string) *stringTable {
+	offsets := make([]uint32, len(strings))
+
+	var offset uint32
+	for i, str := range strings {
+		offsets[i] = offset
+		offset += uint32(len(str)) + 1 // account for NUL
+	}
+
+	return &stringTable{offsets, strings}
+}
diff --git a/internal/btf/types_test.go b/internal/btf/types_test.go
index 2d46bb7..97e868d 100644
--- a/internal/btf/types_test.go
+++ b/internal/btf/types_test.go
@@ -102,7 +102,7 @@ func ExampleType_validTypes() {
 func TestType(t *testing.T) {
 	types := []func() Type{
 		func() Type { return &Void{} },
-		func() Type { return &Int{Size: 2, Bits: 3} },
+		func() Type { return &Int{Size: 2} },
 		func() Type { return &Pointer{Target: &Void{}} },
 		func() Type { return &Array{Type: &Int{}} },
 		func() Type {
@@ -353,6 +353,62 @@ func TestUnderlyingType(t *testing.T) {
 	}
 }
 
+func TestInflateLegacyBitfield(t *testing.T) {
+	const offset = 3
+	const size = 5
+
+	var rawInt rawType
+	rawInt.SetKind(kindInt)
+	rawInt.SetSize(4)
+	offsetSize := uint32(offset<<16 | size)
+	rawInt.data = &offsetSize
+
+	var beforeInt rawType
+	beforeInt.SetKind(kindStruct)
+	beforeInt.SetVlen(1)
+	beforeInt.data = []btfMember{{Type: 2}}
+
+	afterInt := beforeInt
+	afterInt.data = []btfMember{{Type: 1}}
+
+	emptyStrings := newStringTable("")
+
+	for _, test := range []struct {
+		name string
+		raw  []rawType
+	}{
+		{"struct before int", []rawType{beforeInt, rawInt}},
+		{"struct after int", []rawType{rawInt, afterInt}},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			types, err := inflateRawTypes(test.raw, emptyStrings)
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			for _, typ := range types {
+				s, ok := typ.(*Struct)
+				if !ok {
+					continue
+				}
+
+				i := s.Members[0]
+				if i.BitfieldSize != size {
+					t.Errorf("Expected bitfield size %d, got %d", size, i.BitfieldSize)
+				}
+
+				if i.Offset != offset {
+					t.Errorf("Expected offset %d, got %d", offset, i.Offset)
+				}
+
+				return
+			}
+
+			t.Fatal("No Struct returned from inflateRawTypes")
+		})
+	}
+}
+
 func BenchmarkUnderlyingType(b *testing.B) {
 	b.Run("no unwrapping", func(b *testing.B) {
 		v := &Int{}
diff --git a/perf/reader_test.go b/perf/reader_test.go
index 4bf5eca..f784491 100644
--- a/perf/reader_test.go
+++ b/perf/reader_test.go
@@ -294,7 +294,8 @@ func TestReadRecord(t *testing.T) {
 		t.Fatal(err)
 	}
 
-	_, err = readRecord(&buf, 0)
+	var rec Record
+	err = readRecord(&buf, &rec, make([]byte, perfEventHeaderSize))
 	if !IsUnknownEvent(err) {
 		t.Error("readRecord should return unknown event error, got", err)
 	}
@@ -415,6 +416,39 @@ func BenchmarkReader(b *testing.B) {
 	}
 }
 
+func BenchmarkReadInto(b *testing.B) {
+	prog, events := mustOutputSamplesProg(b, 80)
+	defer prog.Close()
+	defer events.Close()
+
+	rd, err := NewReader(events, 4096)
+	if err != nil {
+		b.Fatal(err)
+	}
+	defer rd.Close()
+
+	buf := make([]byte, 14)
+
+	b.ResetTimer()
+	b.ReportAllocs()
+
+	var rec Record
+	for i := 0; i < b.N; i++ {
+		// NB: Submitting samples into the perf event ring dominates
+		// the benchmark time unfortunately.
+		ret, _, err := prog.Test(buf)
+		if err != nil {
+			b.Fatal(err)
+		} else if errno := syscall.Errno(-int32(ret)); errno != 0 {
+			b.Fatal("Expected 0 as return value, got", errno)
+		}
+
+		if err := rd.ReadInto(&rec); err != nil {
+			b.Fatal(err)
+		}
+	}
+}
+
 // This exists just to make the example below nicer.
 func bpfPerfEventOutputProgram() (*ebpf.Program, *ebpf.Map) {
 	prog, events, err := outputSamplesProg(5)
@@ -468,3 +502,33 @@ func ExampleReader() {
 	// Data is padded with 0 for alignment
 	fmt.Println("Sample:", record.RawSample)
 }
+
+// ReadRecord allows reducing memory allocations.
+func ExampleReader_ReadInto() {
+	prog, events := bpfPerfEventOutputProgram()
+	defer prog.Close()
+	defer events.Close()
+
+	rd, err := NewReader(events, 4096)
+	if err != nil {
+		panic(err)
+	}
+	defer rd.Close()
+
+	for i := 0; i < 2; i++ {
+		// Write out two samples
+		ret, _, err := prog.Test(make([]byte, 14))
+		if err != nil || ret != 0 {
+			panic("Can't write sample")
+		}
+	}
+
+	var rec Record
+	for i := 0; i < 2; i++ {
+		if err := rd.ReadInto(&rec); err != nil {
+			panic(err)
+		}
+
+		fmt.Println("Sample:", rec.RawSample[:5])
+	}
+}
diff --git a/ringbuf/reader_test.go b/ringbuf/reader_test.go
index 71278fb..71e1ea0 100644
--- a/ringbuf/reader_test.go
+++ b/ringbuf/reader_test.go
@@ -224,9 +224,6 @@ func BenchmarkReader(b *testing.B) {
 		},
 	}
 
-	b.ResetTimer()
-	b.ReportAllocs()
-
 	for _, bm := range readerBenchmarks {
 		b.Run(bm.name, func(b *testing.B) {
 			prog, events := mustOutputSamplesProg(b, bm.flags, 80)
@@ -237,8 +234,13 @@ func BenchmarkReader(b *testing.B) {
 			}
 			defer rd.Close()
 
+			buf := make([]byte, 14)
+
+			b.ResetTimer()
+			b.ReportAllocs()
+
 			for i := 0; i < b.N; i++ {
-				ret, _, err := prog.Benchmark(make([]byte, 14), 1, nil)
+				ret, _, err := prog.Test(buf)
 				if err != nil {
 					b.Fatal(err)
 				} else if errno := syscall.Errno(-int32(ret)); errno != 0 {
@@ -252,3 +254,34 @@ func BenchmarkReader(b *testing.B) {
 		})
 	}
 }
+
+func BenchmarkReadInto(b *testing.B) {
+	testutils.SkipOnOldKernel(b, "5.8", "BPF ring buffer")
+
+	prog, events := mustOutputSamplesProg(b, 0, 80)
+
+	rd, err := NewReader(events)
+	if err != nil {
+		b.Fatal(err)
+	}
+	defer rd.Close()
+
+	buf := make([]byte, 14)
+
+	b.ResetTimer()
+	b.ReportAllocs()
+
+	var rec Record
+	for i := 0; i < b.N; i++ {
+		ret, _, err := prog.Test(buf)
+		if err != nil {
+			b.Fatal(err)
+		} else if errno := syscall.Errno(-int32(ret)); errno != 0 {
+			b.Fatal("Expected 0 as return value, got", errno)
+		}
+
+		if err := rd.ReadInto(&rec); err != nil {
+			b.Fatal("Can't read samples:", err)
+		}
+	}
+}