Repository: cilium/ebpf
Pull request #1576 (2024-10-03 14:08:37)

info: expose ksym info and func info by ProgramInfo
In [pwru](https://github.com/cilium/pwru), I'm planning to improve the tracing info of bpf prog by mapping ksym addresses to bpf prog info, i.e. the function name of subprograms.

Therefore, it requires more prog info exposed from ProgramInfo, including ksym info and func info.

`bpf_prog_info` supports ksym info since commit torvalds/linux@dbecd7388476 ("bpf: get kernel symbol addresses via syscall"), and supports func info since commit torvalds/linux@838e96904ff3 ("bpf: Introduce bpf_func_info").

ti-mo: (By the way, a common format for referring to a commit in Linux is `dbecd7388476 ("bpf: get kernel symbol addresses via syscall")` or `torvalds/linux@dbecd7388476 ("bpf: get kernel symbol addresses via syscall")` to make it clickable on GH.

Patch:
diff --git a/btf/ext_info.go b/btf/ext_info.go
index 227189f..1a49448 100644
--- a/btf/ext_info.go
+++ b/btf/ext_info.go
@@ -16,7 +16,7 @@ import (
 // ExtInfos contains ELF section metadata.
 type ExtInfos struct {
 	// The slices are sorted by offset in ascending order.
-	funcInfos       map[string]FuncInfos
+	funcInfos       map[string]FuncOffsets
 	lineInfos       map[string]LineInfos
 	relocationInfos map[string]CORERelocationInfos
 }
@@ -58,9 +58,9 @@ func loadExtInfos(r io.ReaderAt, bo binary.ByteOrder, spec *Spec) (*ExtInfos, er
 		return nil, fmt.Errorf("parsing BTF function info: %w", err)
 	}
 
-	funcInfos := make(map[string]FuncInfos, len(btfFuncInfos))
+	funcInfos := make(map[string]FuncOffsets, len(btfFuncInfos))
 	for section, bfis := range btfFuncInfos {
-		funcInfos[section], err = newFuncInfos(bfis, spec)
+		funcInfos[section], err = newFuncOffsets(bfis, spec)
 		if err != nil {
 			return nil, fmt.Errorf("section %s: func infos: %w", section, err)
 		}
@@ -117,15 +117,15 @@ func (ei *ExtInfos) Assign(insns asm.Instructions, section string) {
 // Assign per-instruction metadata to the instructions in insns.
 func AssignMetadataToInstructions(
 	insns asm.Instructions,
-	funcInfos FuncInfos,
+	funcInfos FuncOffsets,
 	lineInfos LineInfos,
 	reloInfos CORERelocationInfos,
 ) {
 	iter := insns.Iterate()
 	for iter.Next() {
-		if len(funcInfos.infos) > 0 && funcInfos.infos[0].offset == iter.Offset {
-			*iter.Ins = WithFuncMetadata(*iter.Ins, funcInfos.infos[0].fn)
-			funcInfos.infos = funcInfos.infos[1:]
+		if len(funcInfos) > 0 && funcInfos[0].Offset == iter.Offset {
+			*iter.Ins = WithFuncMetadata(*iter.Ins, funcInfos[0].Func)
+			funcInfos = funcInfos[1:]
 		}
 
 		if len(lineInfos.infos) > 0 && lineInfos.infos[0].offset == iter.Offset {
@@ -159,9 +159,9 @@ marshal:
 	var fiBuf, liBuf bytes.Buffer
 	for {
 		if fn := FuncMetadata(iter.Ins); fn != nil {
-			fi := &funcInfo{
-				fn:     fn,
-				offset: iter.Offset,
+			fi := &FuncOffset{
+				Func:   fn,
+				Offset: iter.Offset,
 			}
 			if err := fi.marshal(&fiBuf, b); err != nil {
 				return nil, nil, fmt.Errorf("write func info: %w", err)
@@ -333,17 +333,17 @@ func parseExtInfoRecordSize(r io.Reader, bo binary.ByteOrder) (uint32, error) {
 	return recordSize, nil
 }
 
-// FuncInfos contains a sorted list of func infos.
-type FuncInfos struct {
-	infos []funcInfo
-}
+// FuncOffsets is a sorted slice of FuncOffset.
+type FuncOffsets []FuncOffset
 
 // The size of a FuncInfo in BTF wire format.
 var FuncInfoSize = uint32(binary.Size(bpfFuncInfo{}))
 
-type funcInfo struct {
-	fn     *Func
-	offset asm.RawInstructionOffset
+// FuncOffset represents a [btf.Func] and its raw instruction offset within a
+// BPF program.
+type FuncOffset struct {
+	Offset asm.RawInstructionOffset
+	Func   *Func
 }
 
 type bpfFuncInfo struct {
@@ -352,7 +352,7 @@ type bpfFuncInfo struct {
 	TypeID  TypeID
 }
 
-func newFuncInfo(fi bpfFuncInfo, spec *Spec) (*funcInfo, error) {
+func newFuncOffset(fi bpfFuncInfo, spec *Spec) (*FuncOffset, error) {
 	typ, err := spec.TypeByID(fi.TypeID)
 	if err != nil {
 		return nil, err
@@ -368,31 +368,32 @@ func newFuncInfo(fi bpfFuncInfo, spec *Spec) (*funcInfo, error) {
 		return nil, fmt.Errorf("func with type ID %d doesn't have a name", fi.TypeID)
 	}
 
-	return &funcInfo{
-		fn,
+	return &FuncOffset{
 		asm.RawInstructionOffset(fi.InsnOff),
+		fn,
 	}, nil
 }
 
-func newFuncInfos(bfis []bpfFuncInfo, spec *Spec) (FuncInfos, error) {
-	fis := FuncInfos{
-		infos: make([]funcInfo, 0, len(bfis)),
-	}
+func newFuncOffsets(bfis []bpfFuncInfo, spec *Spec) (FuncOffsets, error) {
+	fos := make(FuncOffsets, 0, len(bfis))
+
 	for _, bfi := range bfis {
-		fi, err := newFuncInfo(bfi, spec)
+		fi, err := newFuncOffset(bfi, spec)
 		if err != nil {
-			return FuncInfos{}, fmt.Errorf("offset %d: %w", bfi.InsnOff, err)
+			return FuncOffsets{}, fmt.Errorf("offset %d: %w", bfi.InsnOff, err)
 		}
-		fis.infos = append(fis.infos, *fi)
+		fos = append(fos, *fi)
 	}
-	sort.Slice(fis.infos, func(i, j int) bool {
-		return fis.infos[i].offset <= fis.infos[j].offset
+	sort.Slice(fos, func(i, j int) bool {
+		return fos[i].Offset <= fos[j].Offset
 	})
-	return fis, nil
+	return fos, nil
 }
 
-// LoadFuncInfos parses BTF func info in kernel wire format.
-func LoadFuncInfos(reader io.Reader, bo binary.ByteOrder, recordNum uint32, spec *Spec) (FuncInfos, error) {
+// LoadFuncInfos parses BTF func info from kernel wire format into a
+// [FuncOffsets], a sorted slice of [btf.Func]s of (sub)programs within a BPF
+// program with their corresponding raw instruction offsets.
+func LoadFuncInfos(reader io.Reader, bo binary.ByteOrder, recordNum uint32, spec *Spec) (FuncOffsets, error) {
 	fis, err := parseFuncInfoRecords(
 		reader,
 		bo,
@@ -401,20 +402,20 @@ func LoadFuncInfos(reader io.Reader, bo binary.ByteOrder, recordNum uint32, spec
 		false,
 	)
 	if err != nil {
-		return FuncInfos{}, fmt.Errorf("parsing BTF func info: %w", err)
+		return FuncOffsets{}, fmt.Errorf("parsing BTF func info: %w", err)
 	}
 
-	return newFuncInfos(fis, spec)
+	return newFuncOffsets(fis, spec)
 }
 
 // marshal into the BTF wire format.
-func (fi *funcInfo) marshal(w *bytes.Buffer, b *Builder) error {
-	id, err := b.Add(fi.fn)
+func (fi *FuncOffset) marshal(w *bytes.Buffer, b *Builder) error {
+	id, err := b.Add(fi.Func)
 	if err != nil {
 		return err
 	}
 	bfi := bpfFuncInfo{
-		InsnOff: uint32(fi.offset),
+		InsnOff: uint32(fi.Offset),
 		TypeID:  id,
 	}
 	buf := make([]byte, FuncInfoSize)
diff --git a/info.go b/info.go
index 4318540..9bb67b1 100644
--- a/info.go
+++ b/info.go
@@ -12,7 +12,6 @@ import (
 	"strings"
 	"syscall"
 	"time"
-	"unsafe"
 
 	"github.com/cilium/ebpf/asm"
 	"github.com/cilium/ebpf/btf"
@@ -204,6 +203,8 @@ type ProgramInfo struct {
 	numLineInfos uint32
 	funcInfos    []byte
 	numFuncInfos uint32
+	ksymInfos    []uint64
+	numKsymInfos uint32
 }
 
 func newProgramInfoFromFd(fd *sys.FD) (*ProgramInfo, error) {
@@ -240,7 +241,7 @@ func newProgramInfoFromFd(fd *sys.FD) (*ProgramInfo, error) {
 	if info.NrMapIds > 0 {
 		pi.maps = make([]MapID, info.NrMapIds)
 		info2.NrMapIds = info.NrMapIds
-		info2.MapIds = sys.NewPointer(unsafe.Pointer(&pi.maps[0]))
+		info2.MapIds = sys.NewSlicePointer(pi.maps)
 		makeSecondCall = true
 	} else if haveProgramInfoMapIDs() == nil {
 		// This program really has no associated maps.
@@ -281,6 +282,14 @@ func newProgramInfoFromFd(fd *sys.FD) (*ProgramInfo, error) {
 		makeSecondCall = true
 	}
 
+	if info.NrJitedKsyms > 0 {
+		pi.ksymInfos = make([]uint64, info.NrJitedKsyms)
+		info2.JitedKsyms = sys.NewSlicePointer(pi.ksymInfos)
+		info2.NrJitedKsyms = info.NrJitedKsyms
+		pi.numKsymInfos = info.NrJitedKsyms
+		makeSecondCall = true
+	}
+
 	if makeSecondCall {
 		if err := sys.ObjInfo(fd, &info2); err != nil {
 			return nil, err
@@ -508,6 +517,53 @@ func (pi *ProgramInfo) VerifiedInstructions() (uint32, bool) {
 	return pi.verifiedInstructions, pi.verifiedInstructions > 0
 }
 
+// KsymAddrs returns the ksym addresses of the BPF program, including its
+// subprograms. The addresses correspond to their symbols in /proc/kallsyms.
+//
+// Available from 4.18.
+//
+// The bool return value indicates whether this optional field is available.
+func (pi *ProgramInfo) KsymAddrs() ([]uintptr, bool) {
+	addrs := make([]uintptr, 0, len(pi.ksymInfos))
+	for _, addr := range pi.ksymInfos {
+		addrs = append(addrs, uintptr(addr))
+	}
+	return addrs, pi.numKsymInfos > 0
+}
+
+// FuncInfos returns the offset and function information of all (sub)programs in
+// a BPF program.
+//
+// Available from 5.0.
+//
+// Requires CAP_SYS_ADMIN or equivalent for reading BTF information. Returns
+// ErrNotSupported if the program was created without BTF or if the kernel
+// doesn't support the field.
+func (pi *ProgramInfo) FuncInfos() (btf.FuncOffsets, error) {
+	id, ok := pi.BTFID()
+	if pi.numFuncInfos == 0 || !ok {
+		return nil, fmt.Errorf("program created without BTF or unsupported kernel: %w", ErrNotSupported)
+	}
+
+	h, err := btf.NewHandleFromID(id)
+	if err != nil {
+		return nil, fmt.Errorf("get BTF handle: %w", err)
+	}
+	defer h.Close()
+
+	spec, err := h.Spec(nil)
+	if err != nil {
+		return nil, fmt.Errorf("get BTF spec: %w", err)
+	}
+
+	return btf.LoadFuncInfos(
+		bytes.NewReader(pi.funcInfos),
+		internal.NativeEndian,
+		pi.numFuncInfos,
+		spec,
+	)
+}
+
 func scanFdInfo(fd *sys.FD, fields map[string]interface{}) error {
 	fh, err := os.Open(fmt.Sprintf("/proc/self/fdinfo/%d", fd.Int()))
 	if err != nil {
diff --git a/internal/cmd/gentypes/main.go b/internal/cmd/gentypes/main.go
index 7b693ca..11f2859 100644
--- a/internal/cmd/gentypes/main.go
+++ b/internal/cmd/gentypes/main.go
@@ -200,6 +200,7 @@ import (
 				replace(pointer, "xlated_prog_insns"),
 				replace(pointer, "map_ids"),
 				replace(pointer, "line_info"),
+				replace(pointer, "jited_ksyms"),
 				replace(pointer, "func_info"),
 				replace(btfID, "btf_id", "attach_btf_obj_id"),
 				replace(typeID, "attach_btf_id"),
diff --git a/internal/sys/ptr.go b/internal/sys/ptr.go
index e9bb590..af0c014 100644
--- a/internal/sys/ptr.go
+++ b/internal/sys/ptr.go
@@ -11,13 +11,13 @@ func NewPointer(ptr unsafe.Pointer) Pointer {
 	return Pointer{ptr: ptr}
 }
 
-// NewSlicePointer creates a 64-bit pointer from a byte slice.
-func NewSlicePointer(buf []byte) Pointer {
+// NewSlicePointer creates a 64-bit pointer from a slice.
+func NewSlicePointer[T comparable](buf []T) Pointer {
 	if len(buf) == 0 {
 		return Pointer{}
 	}
 
-	return Pointer{ptr: unsafe.Pointer(&buf[0])}
+	return Pointer{ptr: unsafe.Pointer(unsafe.SliceData(buf))}
 }
 
 // NewSlicePointerLen creates a 64-bit pointer from a byte slice.
diff --git a/internal/sys/types.go b/internal/sys/types.go
index d46164a..19b287a 100644
--- a/internal/sys/types.go
+++ b/internal/sys/types.go
@@ -733,7 +733,7 @@ type ProgInfo struct {
 	NetnsIno             uint64
 	NrJitedKsyms         uint32
 	NrJitedFuncLens      uint32
-	JitedKsyms           uint64
+	JitedKsyms           Pointer
 	JitedFuncLens        uint64
 	BtfId                BTFID
 	FuncInfoRecSize      uint32


Test patch:
diff --git a/info_test.go b/info_test.go
index a3c31d3..ede2a1e 100644
--- a/info_test.go
+++ b/info_test.go
@@ -510,3 +510,55 @@ func TestZero(t *testing.T) {
 	qt.Assert(t, qt.IsTrue(zero(&inul)))
 	qt.Assert(t, qt.IsFalse(zero(&ione)))
 }
+
+func TestProgInfoKsym(t *testing.T) {
+	testutils.SkipOnOldKernel(t, "4.18", "Program ksym addresses")
+
+	spec, err := LoadCollectionSpec(testutils.NativeFile(t, "testdata/loader-%s.elf"))
+	qt.Assert(t, qt.IsNil(err))
+
+	var obj struct {
+		Prog *Program `ebpf:"xdp_prog"`
+	}
+	err = spec.LoadAndAssign(&obj, nil)
+	testutils.SkipIfNotSupported(t, err)
+	qt.Assert(t, qt.IsNil(err))
+	defer obj.Prog.Close()
+
+	info, err := obj.Prog.Info()
+	qt.Assert(t, qt.IsNil(err))
+
+	addrs, ok := info.KsymAddrs()
+	qt.Assert(t, qt.IsTrue(ok))
+	qt.Assert(t, qt.HasLen(addrs, 5))
+	for _, addr := range addrs {
+		qt.Assert(t, qt.Not(qt.Equals(addr, 0)))
+	}
+}
+
+func TestProgInfoFuncInfos(t *testing.T) {
+	testutils.SkipOnOldKernel(t, "5.0", "Program func info")
+
+	spec, err := LoadCollectionSpec(testutils.NativeFile(t, "testdata/loader-%s.elf"))
+	qt.Assert(t, qt.IsNil(err))
+
+	var obj struct {
+		Prog *Program `ebpf:"xdp_prog"`
+	}
+	err = spec.LoadAndAssign(&obj, nil)
+	testutils.SkipIfNotSupported(t, err)
+	qt.Assert(t, qt.IsNil(err))
+	defer obj.Prog.Close()
+
+	info, err := obj.Prog.Info()
+	qt.Assert(t, qt.IsNil(err))
+
+	funcs, err := info.FuncInfos()
+	qt.Assert(t, qt.IsNil(err))
+
+	qt.Assert(t, qt.HasLen(funcs, 5))
+	for _, fo := range funcs {
+		qt.Assert(t, qt.IsNotNil(fo.Func))
+		qt.Assert(t, qt.Not(qt.Equals(fo.Func.Name, "")))
+	}
+}