Repository: cilium/ebpf
Pull request #463 (2021-10-26 20:36:58)

link/uprobe: support ref_ctr_offset
This option should simplify implementing `USDT` probes on top of `link.Uprobe`


Patch:
diff --git a/link/kprobe.go b/link/kprobe.go
index b779c3e..6b89636 100644
--- a/link/kprobe.go
+++ b/link/kprobe.go
@@ -28,6 +28,13 @@ var (
 
 type probeType uint8
 
+type probeArgs struct {
+	symbol, group, path  string
+	offset, refCtrOffset uint64
+	pid                  int
+	ret                  bool
+}
+
 const (
 	kprobeType probeType = iota
 	uprobeType
@@ -131,10 +138,17 @@ func kprobe(symbol string, prog *ebpf.Program, ret bool) (*perfEvent, error) {
 		return nil, fmt.Errorf("eBPF program type %s is not a Kprobe: %w", prog.Type(), errInvalidInput)
 	}
 
+	args := probeArgs{
+		pid:    perfAllThreads,
+		symbol: platformPrefix(symbol),
+		ret:    ret,
+	}
+
 	// Use kprobe PMU if the kernel has it available.
-	tp, err := pmuKprobe(platformPrefix(symbol), ret)
+	tp, err := pmuKprobe(args)
 	if errors.Is(err, os.ErrNotExist) {
-		tp, err = pmuKprobe(symbol, ret)
+		args.symbol = symbol
+		tp, err = pmuKprobe(args)
 	}
 	if err == nil {
 		return tp, nil
@@ -144,9 +158,11 @@ func kprobe(symbol string, prog *ebpf.Program, ret bool) (*perfEvent, error) {
 	}
 
 	// Use tracefs if kprobe PMU is missing.
-	tp, err = tracefsKprobe(platformPrefix(symbol), ret)
+	args.symbol = platformPrefix(symbol)
+	tp, err = tracefsKprobe(args)
 	if errors.Is(err, os.ErrNotExist) {
-		tp, err = tracefsKprobe(symbol, ret)
+		args.symbol = symbol
+		tp, err = tracefsKprobe(args)
 	}
 	if err != nil {
 		return nil, fmt.Errorf("creating trace event '%s' in tracefs: %w", symbol, err)
@@ -157,8 +173,8 @@ func kprobe(symbol string, prog *ebpf.Program, ret bool) (*perfEvent, error) {
 
 // pmuKprobe opens a perf event based on the kprobe PMU.
 // Returns os.ErrNotExist if the given symbol does not exist in the kernel.
-func pmuKprobe(symbol string, ret bool) (*perfEvent, error) {
-	return pmuProbe(kprobeType, symbol, "", 0, perfAllThreads, ret)
+func pmuKprobe(args probeArgs) (*perfEvent, error) {
+	return pmuProbe(kprobeType, args)
 }
 
 // pmuProbe opens a perf event based on a Performance Monitoring Unit.
@@ -168,7 +184,7 @@ func pmuKprobe(symbol string, ret bool) (*perfEvent, error) {
 // 33ea4b24277b "perf/core: Implement the 'perf_uprobe' PMU"
 //
 // Returns ErrNotSupported if the kernel doesn't support perf_[k,u]probe PMU
-func pmuProbe(typ probeType, symbol, path string, offset uint64, pid int, ret bool) (*perfEvent, error) {
+func pmuProbe(typ probeType, args probeArgs) (*perfEvent, error) {
 	// Getting the PMU type will fail if the kernel doesn't support
 	// the perf_[k,u]probe PMU.
 	et, err := getPMUEventType(typ)
@@ -177,7 +193,7 @@ func pmuProbe(typ probeType, symbol, path string, offset uint64, pid int, ret bo
 	}
 
 	var config uint64
-	if ret {
+	if args.ret {
 		bit, err := typ.RetprobeBit()
 		if err != nil {
 			return nil, err
@@ -192,7 +208,7 @@ func pmuProbe(typ probeType, symbol, path string, offset uint64, pid int, ret bo
 	switch typ {
 	case kprobeType:
 		// Create a pointer to a NUL-terminated string for the kernel.
-		sp, err = unsafeStringPtr(symbol)
+		sp, err = unsafeStringPtr(args.symbol)
 		if err != nil {
 			return nil, err
 		}
@@ -203,11 +219,15 @@ func pmuProbe(typ probeType, symbol, path string, offset uint64, pid int, ret bo
 			Config: config,              // Retprobe flag
 		}
 	case uprobeType:
-		sp, err = unsafeStringPtr(path)
+		sp, err = unsafeStringPtr(args.path)
 		if err != nil {
 			return nil, err
 		}
 
+		if args.refCtrOffset != 0 {
+			config |= args.refCtrOffset << uprobeRefCtrOffsetShift
+		}
+
 		attr = unix.PerfEventAttr{
 			// The minimum size required for PMU uprobes is PERF_ATTR_SIZE_VER1,
 			// since it added the config2 (Ext2) field. The Size field controls the
@@ -216,23 +236,23 @@ func pmuProbe(typ probeType, symbol, path string, offset uint64, pid int, ret bo
 			Size:   unix.PERF_ATTR_SIZE_VER1,
 			Type:   uint32(et),          // PMU event type read from sysfs
 			Ext1:   uint64(uintptr(sp)), // Uprobe path
-			Ext2:   offset,              // Uprobe offset
-			Config: config,              // Retprobe flag
+			Ext2:   args.offset,         // Uprobe offset
+			Config: config,              // RefCtrOffset, Retprobe flag
 		}
 	}
 
-	rawFd, err := unix.PerfEventOpen(&attr, pid, 0, -1, unix.PERF_FLAG_FD_CLOEXEC)
+	rawFd, err := unix.PerfEventOpen(&attr, args.pid, 0, -1, unix.PERF_FLAG_FD_CLOEXEC)
 
 	// Since commit 97c753e62e6c, ENOENT is correctly returned instead of EINVAL
 	// when trying to create a kretprobe for a missing symbol. Make sure ENOENT
 	// is returned to the caller.
 	if errors.Is(err, os.ErrNotExist) || errors.Is(err, unix.EINVAL) {
-		return nil, fmt.Errorf("symbol '%s' not found: %w", symbol, os.ErrNotExist)
+		return nil, fmt.Errorf("symbol '%s' not found: %w", args.symbol, os.ErrNotExist)
 	}
 	// Since at least commit cb9a19fe4aa51, ENOTSUPP is returned
 	// when attempting to set a uprobe on a trap instruction.
 	if errors.Is(err, unix.ENOTSUPP) {
-		return nil, fmt.Errorf("failed setting uprobe on offset %#x (possible trap insn): %w", offset, err)
+		return nil, fmt.Errorf("failed setting uprobe on offset %#x (possible trap insn): %w", args.offset, err)
 	}
 	if err != nil {
 		return nil, fmt.Errorf("opening perf event: %w", err)
@@ -250,14 +270,14 @@ func pmuProbe(typ probeType, symbol, path string, offset uint64, pid int, ret bo
 	return &perfEvent{
 		fd:    fd,
 		pmuID: et,
-		name:  symbol,
-		typ:   typ.PerfEventType(ret),
+		name:  args.symbol,
+		typ:   typ.PerfEventType(args.ret),
 	}, nil
 }
 
 // tracefsKprobe creates a Kprobe tracefs entry.
-func tracefsKprobe(symbol string, ret bool) (*perfEvent, error) {
-	return tracefsProbe(kprobeType, symbol, "", 0, perfAllThreads, ret)
+func tracefsKprobe(args probeArgs) (*perfEvent, error) {
+	return tracefsProbe(kprobeType, args)
 }
 
 // tracefsProbe creates a trace event by writing an entry to <tracefs>/[k,u]probe_events.
@@ -266,7 +286,7 @@ func tracefsKprobe(symbol string, ret bool) (*perfEvent, error) {
 // Path and offset are only set in the case of uprobe(s) and are used to set
 // the executable/library path on the filesystem and the offset where the probe is inserted.
 // A perf event is then opened on the newly-created trace event and returned to the caller.
-func tracefsProbe(typ probeType, symbol, path string, offset uint64, pid int, ret bool) (*perfEvent, error) {
+func tracefsProbe(typ probeType, args probeArgs) (*perfEvent, error) {
 	// Generate a random string for each trace event we attempt to create.
 	// This value is used as the 'group' token in tracefs to allow creating
 	// multiple kprobe trace events with the same name.
@@ -274,32 +294,33 @@ func tracefsProbe(typ probeType, symbol, path string, offset uint64, pid int, re
 	if err != nil {
 		return nil, fmt.Errorf("randomizing group name: %w", err)
 	}
+	args.group = group
 
 	// Before attempting to create a trace event through tracefs,
 	// check if an event with the same group and name already exists.
 	// Kernels 4.x and earlier don't return os.ErrExist on writing a duplicate
 	// entry, so we need to rely on reads for detecting uniqueness.
-	_, err = getTraceEventID(group, symbol)
+	_, err = getTraceEventID(group, args.symbol)
 	if err == nil {
-		return nil, fmt.Errorf("trace event already exists: %s/%s", group, symbol)
+		return nil, fmt.Errorf("trace event already exists: %s/%s", group, args.symbol)
 	}
 	if err != nil && !errors.Is(err, os.ErrNotExist) {
-		return nil, fmt.Errorf("checking trace event %s/%s: %w", group, symbol, err)
+		return nil, fmt.Errorf("checking trace event %s/%s: %w", group, args.symbol, err)
 	}
 
 	// Create the [k,u]probe trace event using tracefs.
-	if err := createTraceFSProbeEvent(typ, group, symbol, path, offset, ret); err != nil {
+	if err := createTraceFSProbeEvent(typ, args); err != nil {
 		return nil, fmt.Errorf("creating probe entry on tracefs: %w", err)
 	}
 
 	// Get the newly-created trace event's id.
-	tid, err := getTraceEventID(group, symbol)
+	tid, err := getTraceEventID(group, args.symbol)
 	if err != nil {
 		return nil, fmt.Errorf("getting trace event id: %w", err)
 	}
 
 	// Kprobes are ephemeral tracepoints and share the same perf event type.
-	fd, err := openTracepointPerfEvent(tid, pid)
+	fd, err := openTracepointPerfEvent(tid, args.pid)
 	if err != nil {
 		return nil, err
 	}
@@ -307,9 +328,9 @@ func tracefsProbe(typ probeType, symbol, path string, offset uint64, pid int, re
 	return &perfEvent{
 		fd:        fd,
 		group:     group,
-		name:      symbol,
+		name:      args.symbol,
 		tracefsID: tid,
-		typ:       typ.PerfEventType(ret),
+		typ:       typ.PerfEventType(args.ret),
 	}, nil
 }
 
@@ -317,7 +338,7 @@ func tracefsProbe(typ probeType, symbol, path string, offset uint64, pid int, re
 // <tracefs>/[k,u]probe_events. Returns os.ErrNotExist if symbol is not a valid
 // kernel symbol, or if it is not traceable with kprobes. Returns os.ErrExist
 // if a probe with the same group and symbol already exists.
-func createTraceFSProbeEvent(typ probeType, group, symbol, path string, offset uint64, ret bool) error {
+func createTraceFSProbeEvent(typ probeType, args probeArgs) error {
 	// Open the kprobe_events file in tracefs.
 	f, err := os.OpenFile(typ.EventsPath(), os.O_APPEND|os.O_WRONLY, 0666)
 	if err != nil {
@@ -342,7 +363,7 @@ func createTraceFSProbeEvent(typ probeType, group, symbol, path string, offset u
 		// subsampling or rate limiting logic can be more accurately implemented in
 		// the eBPF program itself.
 		// See Documentation/kprobes.txt for more details.
-		pe = fmt.Sprintf("%s:%s/%s %s", probePrefix(ret), group, symbol, symbol)
+		pe = fmt.Sprintf("%s:%s/%s %s", probePrefix(args.ret), args.group, args.symbol, args.symbol)
 	case uprobeType:
 		// The uprobe_events syntax is as follows:
 		// p[:[GRP/]EVENT] PATH:OFFSET [FETCHARGS] : Set a probe
@@ -351,18 +372,17 @@ func createTraceFSProbeEvent(typ probeType, group, symbol, path string, offset u
 		//
 		// Some examples:
 		// r:ebpf_1234/readline /bin/bash:0x12345
-		// p:ebpf_5678/main_mySymbol /bin/mybin:0x12345
+		// p:ebpf_5678/main_mySymbol /bin/mybin:0x12345(0x123)
 		//
 		// See Documentation/trace/uprobetracer.txt for more details.
-		pathOffset := uprobePathOffset(path, offset)
-		pe = fmt.Sprintf("%s:%s/%s %s", probePrefix(ret), group, symbol, pathOffset)
+		pe = fmt.Sprintf("%s:%s/%s %s", probePrefix(args.ret), args.group, args.symbol, uprobeToken(args))
 	}
 	_, err = f.WriteString(pe)
 	// Since commit 97c753e62e6c, ENOENT is correctly returned instead of EINVAL
 	// when trying to create a kretprobe for a missing symbol. Make sure ENOENT
 	// is returned to the caller.
 	if errors.Is(err, os.ErrNotExist) || errors.Is(err, unix.EINVAL) {
-		return fmt.Errorf("symbol %s not found: %w", symbol, os.ErrNotExist)
+		return fmt.Errorf("symbol %s not found: %w", args.symbol, os.ErrNotExist)
 	}
 	if err != nil {
 		return fmt.Errorf("writing '%s' to '%s': %w", pe, typ.EventsPath(), err)
diff --git a/link/uprobe.go b/link/uprobe.go
index 59170ce..d603575 100644
--- a/link/uprobe.go
+++ b/link/uprobe.go
@@ -26,6 +26,17 @@ var (
 		err   error
 	}{}
 
+	uprobeRefCtrOffsetPMUPath = "/sys/bus/event_source/devices/uprobe/format/ref_ctr_offset"
+	// elixir.bootlin.com/linux/v5.15-rc7/source/kernel/events/core.c#L9799
+	uprobeRefCtrOffsetShift = 32
+	haveRefCtrOffsetPMU     = internal.FeatureTest("RefCtrOffsetPMU", "4.20", func() error {
+		_, err := os.Stat(uprobeRefCtrOffsetPMUPath)
+		if err != nil {
+			return internal.ErrNotSupported
+		}
+		return nil
+	})
+
 	// ErrNoSymbol indicates that the given symbol was not found
 	// in the ELF symbols table.
 	ErrNoSymbol = errors.New("not found")
@@ -48,11 +59,22 @@ type UprobeOptions struct {
 	// Only set the uprobe on the given process ID. Useful when tracing
 	// shared library calls or programs that have many running instances.
 	PID int
+	// Automatically manage SDT reference counts (semaphores).
+	//
+	// If this field is set, the Kernel will increment/decrement the
+	// semaphore located in the process memory at the provided address on
+	// probe attach/detach.
+	//
+	// See also:
+	// sourceware.org/systemtap/wiki/UserSpaceProbeImplementation (Semaphore Handling)
+	// github.com/torvalds/linux/commit/1cc33161a83d
+	// github.com/torvalds/linux/commit/a6ca88b241d5
+	RefCtrOffset uint64
 }
 
 // To open a new Executable, use:
 //
-//	OpenExecutable("/bin/bash")
+//  OpenExecutable("/bin/bash")
 //
 // The returned value can then be used to open Uprobe(s).
 func OpenExecutable(path string) (*Executable, error) {
@@ -161,7 +183,7 @@ func (ex *Executable) offset(symbol string) (uint64, error) {
 // When using symbols which belongs to shared libraries,
 // an offset must be provided via options:
 //
-//	up, err := ex.Uprobe("main", prog, &UprobeOptions{Offset: 0x123})
+//  up, err := ex.Uprobe("main", prog, &UprobeOptions{Offset: 0x123})
 //
 // Losing the reference to the resulting Link (up) will close the Uprobe
 // and prevent further execution of prog. The Link must be Closed during
@@ -193,7 +215,7 @@ func (ex *Executable) Uprobe(symbol string, prog *ebpf.Program, opts *UprobeOpti
 // When using symbols which belongs to shared libraries,
 // an offset must be provided via options:
 //
-//	up, err := ex.Uretprobe("main", prog, &UprobeOptions{Offset: 0x123})
+//  up, err := ex.Uretprobe("main", prog, &UprobeOptions{Offset: 0x123})
 //
 // Losing the reference to the resulting Link (up) will close the Uprobe
 // and prevent further execution of prog. The Link must be Closed during
@@ -225,11 +247,12 @@ func (ex *Executable) uprobe(symbol string, prog *ebpf.Program, opts *UprobeOpti
 	if prog.Type() != ebpf.Kprobe {
 		return nil, fmt.Errorf("eBPF program type %s is not Kprobe: %w", prog.Type(), errInvalidInput)
 	}
+	if opts == nil {
+		opts = &UprobeOptions{}
+	}
 
-	var offset uint64
-	if opts != nil && opts.Offset != 0 {
-		offset = opts.Offset
-	} else {
+	offset := opts.Offset
+	if offset == 0 {
 		off, err := ex.offset(symbol)
 		if err != nil {
 			return nil, err
@@ -237,13 +260,28 @@ func (ex *Executable) uprobe(symbol string, prog *ebpf.Program, opts *UprobeOpti
 		offset = off
 	}
 
-	pid := perfAllThreads
-	if opts != nil && opts.PID != 0 {
-		pid = opts.PID
+	pid := opts.PID
+	if pid == 0 {
+		pid = perfAllThreads
+	}
+
+	if opts.RefCtrOffset != 0 {
+		if err := haveRefCtrOffsetPMU(); err != nil {
+			return nil, fmt.Errorf("uprobe ref_ctr_offset: %w", err)
+		}
+	}
+
+	args := probeArgs{
+		symbol:       symbol,
+		path:         ex.path,
+		offset:       offset,
+		pid:          pid,
+		refCtrOffset: opts.RefCtrOffset,
+		ret:          ret,
 	}
 
 	// Use uprobe PMU if the kernel has it available.
-	tp, err := pmuUprobe(symbol, ex.path, offset, pid, ret)
+	tp, err := pmuUprobe(args)
 	if err == nil {
 		return tp, nil
 	}
@@ -252,7 +290,8 @@ func (ex *Executable) uprobe(symbol string, prog *ebpf.Program, opts *UprobeOpti
 	}
 
 	// Use tracefs if uprobe PMU is missing.
-	tp, err = tracefsUprobe(uprobeSanitizedSymbol(symbol), ex.path, offset, pid, ret)
+	args.symbol = uprobeSanitizedSymbol(symbol)
+	tp, err = tracefsUprobe(args)
 	if err != nil {
 		return nil, fmt.Errorf("creating trace event '%s:%s' in tracefs: %w", ex.path, symbol, err)
 	}
@@ -261,13 +300,13 @@ func (ex *Executable) uprobe(symbol string, prog *ebpf.Program, opts *UprobeOpti
 }
 
 // pmuUprobe opens a perf event based on the uprobe PMU.
-func pmuUprobe(symbol, path string, offset uint64, pid int, ret bool) (*perfEvent, error) {
-	return pmuProbe(uprobeType, symbol, path, offset, pid, ret)
+func pmuUprobe(args probeArgs) (*perfEvent, error) {
+	return pmuProbe(uprobeType, args)
 }
 
 // tracefsUprobe creates a Uprobe tracefs entry.
-func tracefsUprobe(symbol, path string, offset uint64, pid int, ret bool) (*perfEvent, error) {
-	return tracefsProbe(uprobeType, symbol, path, offset, pid, ret)
+func tracefsUprobe(args probeArgs) (*perfEvent, error) {
+	return tracefsProbe(uprobeType, args)
 }
 
 // uprobeSanitizedSymbol replaces every invalid characted for the tracefs api with an underscore.
@@ -275,9 +314,17 @@ func uprobeSanitizedSymbol(symbol string) string {
 	return rgxUprobeSymbol.ReplaceAllString(symbol, "_")
 }
 
-// uprobePathOffset creates the PATH:OFFSET token for the tracefs api.
-func uprobePathOffset(path string, offset uint64) string {
-	return fmt.Sprintf("%s:%#x", path, offset)
+// uprobeToken creates the PATH:OFFSET(REF_CTR_OFFSET) token for the tracefs api.
+func uprobeToken(args probeArgs) string {
+	po := fmt.Sprintf("%s:%#x", args.path, args.offset)
+
+	if args.refCtrOffset != 0 {
+		// This is not documented in Documentation/trace/uprobetracer.txt.
+		// elixir.bootlin.com/linux/v5.15-rc7/source/kernel/trace/trace.c#L5564
+		po += fmt.Sprintf("(%#x)", args.refCtrOffset)
+	}
+
+	return po
 }
 
 func uretprobeBit() (uint64, error) {


Test patch:
diff --git a/link/kprobe_test.go b/link/kprobe_test.go
index 38afd83..6b3466b 100644
--- a/link/kprobe_test.go
+++ b/link/kprobe_test.go
@@ -13,17 +13,15 @@ import (
 	"github.com/cilium/ebpf/internal/unix"
 )
 
-var (
-	kprobeSpec = ebpf.ProgramSpec{
-		Type:    ebpf.Kprobe,
-		License: "MIT",
-		Instructions: asm.Instructions{
-			// set exit code to 0
-			asm.Mov.Imm(asm.R0, 0),
-			asm.Return(),
-		},
-	}
-)
+var kprobeSpec = ebpf.ProgramSpec{
+	Type:    ebpf.Kprobe,
+	License: "MIT",
+	Instructions: asm.Instructions{
+		// set exit code to 0
+		asm.Mov.Imm(asm.R0, 0),
+		asm.Return(),
+	},
+}
 
 func TestKprobe(t *testing.T) {
 	c := qt.New(t)
@@ -93,21 +91,20 @@ func TestKprobeErrors(t *testing.T) {
 
 // Test k(ret)probe creation using perf_kprobe PMU.
 func TestKprobeCreatePMU(t *testing.T) {
-
 	// Requires at least 4.17 (e12f03d7031a "perf/core: Implement the 'perf_kprobe' PMU")
 	testutils.SkipOnOldKernel(t, "4.17", "perf_kprobe PMU")
 
 	c := qt.New(t)
 
 	// kprobe happy path. printk is always present.
-	pk, err := pmuKprobe("printk", false)
+	pk, err := pmuKprobe(probeArgs{symbol: "printk"})
 	c.Assert(err, qt.IsNil)
 	defer pk.Close()
 
 	c.Assert(pk.typ, qt.Equals, kprobeEvent)
 
 	// kretprobe happy path.
-	pr, err := pmuKprobe("printk", true)
+	pr, err := pmuKprobe(probeArgs{symbol: "printk", ret: true})
 	c.Assert(err, qt.IsNil)
 	defer pr.Close()
 
@@ -115,12 +112,12 @@ func TestKprobeCreatePMU(t *testing.T) {
 
 	// Expect os.ErrNotExist when specifying a non-existent kernel symbol
 	// on kernels 4.17 and up.
-	_, err = pmuKprobe("bogus", false)
+	_, err = pmuKprobe(probeArgs{symbol: "bogus"})
 	c.Assert(errors.Is(err, os.ErrNotExist), qt.IsTrue, qt.Commentf("got error: %s", err))
 
 	// A kernel bug was fixed in 97c753e62e6c where EINVAL was returned instead
 	// of ENOENT, but only for kretprobes.
-	_, err = pmuKprobe("bogus", true)
+	_, err = pmuKprobe(probeArgs{symbol: "bogus", ret: true})
 	c.Assert(errors.Is(err, os.ErrNotExist), qt.IsTrue, qt.Commentf("got error: %s", err))
 }
 
@@ -128,7 +125,7 @@ func TestKprobeCreatePMU(t *testing.T) {
 func TestKprobePMUUnavailable(t *testing.T) {
 	c := qt.New(t)
 
-	pk, err := pmuKprobe("printk", false)
+	pk, err := pmuKprobe(probeArgs{symbol: "printk"})
 	if err == nil {
 		pk.Close()
 		t.Skipf("Kernel supports perf_kprobe PMU, not asserting error.")
@@ -140,7 +137,7 @@ func TestKprobePMUUnavailable(t *testing.T) {
 
 func BenchmarkKprobeCreatePMU(b *testing.B) {
 	for n := 0; n < b.N; n++ {
-		pr, err := pmuKprobe("printk", false)
+		pr, err := pmuKprobe(probeArgs{symbol: "printk"})
 		if err != nil {
 			b.Error("error creating perf_kprobe PMU:", err)
 		}
@@ -158,23 +155,23 @@ func TestKprobeTraceFS(t *testing.T) {
 	symbol := "printk"
 
 	// Open and close tracefs k(ret)probes, checking all errors.
-	kp, err := tracefsKprobe(symbol, false)
+	kp, err := tracefsKprobe(probeArgs{symbol: symbol})
 	c.Assert(err, qt.IsNil)
 	c.Assert(kp.Close(), qt.IsNil)
 	c.Assert(kp.typ, qt.Equals, kprobeEvent)
 
-	kp, err = tracefsKprobe(symbol, true)
+	kp, err = tracefsKprobe(probeArgs{symbol: symbol, ret: true})
 	c.Assert(err, qt.IsNil)
 	c.Assert(kp.Close(), qt.IsNil)
 	c.Assert(kp.typ, qt.Equals, kretprobeEvent)
 
 	// Create two identical trace events, ensure their IDs differ.
-	k1, err := tracefsKprobe(symbol, false)
+	k1, err := tracefsKprobe(probeArgs{symbol: symbol})
 	c.Assert(err, qt.IsNil)
 	defer k1.Close()
 	c.Assert(k1.tracefsID, qt.Not(qt.Equals), 0)
 
-	k2, err := tracefsKprobe(symbol, false)
+	k2, err := tracefsKprobe(probeArgs{symbol: symbol})
 	c.Assert(err, qt.IsNil)
 	defer k2.Close()
 	c.Assert(k2.tracefsID, qt.Not(qt.Equals), 0)
@@ -182,13 +179,17 @@ func TestKprobeTraceFS(t *testing.T) {
 	// Compare the kprobes' tracefs IDs.
 	c.Assert(k1.tracefsID, qt.Not(qt.CmpEquals()), k2.tracefsID)
 
+	// Prepare probe args.
+	args := probeArgs{group: "testgroup", symbol: "symbol"}
+
 	// Write a k(ret)probe event for a non-existing symbol.
-	err = createTraceFSProbeEvent(kprobeType, "testgroup", "bogus", "", 0, false)
+	err = createTraceFSProbeEvent(kprobeType, args)
 	c.Assert(errors.Is(err, os.ErrNotExist), qt.IsTrue, qt.Commentf("got error: %s", err))
 
 	// A kernel bug was fixed in 97c753e62e6c where EINVAL was returned instead
 	// of ENOENT, but only for kretprobes.
-	err = createTraceFSProbeEvent(kprobeType, "testgroup", "bogus", "", 0, true)
+	args.ret = true
+	err = createTraceFSProbeEvent(kprobeType, args)
 	c.Assert(errors.Is(err, os.ErrNotExist), qt.IsTrue, qt.Commentf("got error: %s", err))
 }
 
@@ -196,7 +197,7 @@ func BenchmarkKprobeCreateTraceFS(b *testing.B) {
 	for n := 0; n < b.N; n++ {
 		// Include <tracefs>/kprobe_events operations in the benchmark loop
 		// because we create one per perf event.
-		pr, err := tracefsKprobe("printk", false)
+		pr, err := tracefsKprobe(probeArgs{symbol: "printk"})
 		if err != nil {
 			b.Error("error creating tracefs perf event:", err)
 		}
@@ -226,24 +227,30 @@ func TestKprobeCreateTraceFS(t *testing.T) {
 		_ = closeTraceFSProbeEvent(kprobeType, rg, symbol)
 	}()
 
+	// Prepare probe args.
+	args := probeArgs{group: pg, symbol: symbol}
+
 	// Create a kprobe.
-	err := createTraceFSProbeEvent(kprobeType, pg, symbol, "", 0, false)
+	err := createTraceFSProbeEvent(kprobeType, args)
 	c.Assert(err, qt.IsNil)
 
 	// Attempt to create an identical kprobe using tracefs,
 	// expect it to fail with os.ErrExist.
-	err = createTraceFSProbeEvent(kprobeType, pg, symbol, "", 0, false)
+	err = createTraceFSProbeEvent(kprobeType, args)
 	c.Assert(errors.Is(err, os.ErrExist), qt.IsTrue,
 		qt.Commentf("expected consecutive kprobe creation to contain os.ErrExist, got: %v", err))
 
 	// Expect a successful close of the kprobe.
 	c.Assert(closeTraceFSProbeEvent(kprobeType, pg, symbol), qt.IsNil)
 
+	args.group = rg
+	args.ret = true
+
 	// Same test for a kretprobe.
-	err = createTraceFSProbeEvent(kprobeType, rg, symbol, "", 0, true)
+	err = createTraceFSProbeEvent(kprobeType, args)
 	c.Assert(err, qt.IsNil)
 
-	err = createTraceFSProbeEvent(kprobeType, rg, symbol, "", 0, true)
+	err = createTraceFSProbeEvent(kprobeType, args)
 	c.Assert(os.IsExist(err), qt.IsFalse,
 		qt.Commentf("expected consecutive kretprobe creation to contain os.ErrExist, got: %v", err))
 
diff --git a/link/uprobe_test.go b/link/uprobe_test.go
index cf0ea07..ef3b6cf 100644
--- a/link/uprobe_test.go
+++ b/link/uprobe_test.go
@@ -147,15 +147,24 @@ func TestUprobeCreatePMU(t *testing.T) {
 	off, err := bashEx.offset(bashSym)
 	c.Assert(err, qt.IsNil)
 
+	// Prepare probe args.
+	args := probeArgs{
+		symbol: bashSym,
+		path:   bashEx.path,
+		offset: off,
+		pid:    perfAllThreads,
+	}
+
 	// uprobe PMU
-	pu, err := pmuUprobe(bashSym, bashEx.path, off, perfAllThreads, false)
+	pu, err := pmuUprobe(args)
 	c.Assert(err, qt.IsNil)
 	defer pu.Close()
 
 	c.Assert(pu.typ, qt.Equals, uprobeEvent)
 
 	// uretprobe PMU
-	pr, err := pmuUprobe(bashSym, bashEx.path, off, perfAllThreads, true)
+	args.ret = true
+	pr, err := pmuUprobe(args)
 	c.Assert(err, qt.IsNil)
 	defer pr.Close()
 
@@ -170,7 +179,15 @@ func TestUprobePMUUnavailable(t *testing.T) {
 	off, err := bashEx.offset(bashSym)
 	c.Assert(err, qt.IsNil)
 
-	pk, err := pmuUprobe(bashSym, bashEx.path, off, perfAllThreads, false)
+	// Prepare probe args.
+	args := probeArgs{
+		symbol: bashSym,
+		path:   bashEx.path,
+		offset: off,
+		pid:    perfAllThreads,
+	}
+
+	pk, err := pmuUprobe(args)
 	if err == nil {
 		pk.Close()
 		t.Skipf("Kernel supports perf_uprobe PMU, not asserting error.")
@@ -188,27 +205,34 @@ func TestUprobeTraceFS(t *testing.T) {
 	off, err := bashEx.offset(bashSym)
 	c.Assert(err, qt.IsNil)
 
-	// Sanitize the symbol in order to be used in tracefs API.
-	ssym := uprobeSanitizedSymbol(bashSym)
+	// Prepare probe args.
+	args := probeArgs{
+		symbol: uprobeSanitizedSymbol(bashSym),
+		path:   bashEx.path,
+		offset: off,
+		pid:    perfAllThreads,
+	}
 
 	// Open and close tracefs u(ret)probes, checking all errors.
-	up, err := tracefsUprobe(ssym, bashEx.path, off, perfAllThreads, false)
+	up, err := tracefsUprobe(args)
 	c.Assert(err, qt.IsNil)
 	c.Assert(up.Close(), qt.IsNil)
 	c.Assert(up.typ, qt.Equals, uprobeEvent)
 
-	up, err = tracefsUprobe(ssym, bashEx.path, off, perfAllThreads, true)
+	args.ret = true
+	up, err = tracefsUprobe(args)
 	c.Assert(err, qt.IsNil)
 	c.Assert(up.Close(), qt.IsNil)
 	c.Assert(up.typ, qt.Equals, uretprobeEvent)
 
 	// Create two identical trace events, ensure their IDs differ.
-	u1, err := tracefsUprobe(ssym, bashEx.path, off, perfAllThreads, false)
+	args.ret = false
+	u1, err := tracefsUprobe(args)
 	c.Assert(err, qt.IsNil)
 	defer u1.Close()
 	c.Assert(u1.tracefsID, qt.Not(qt.Equals), 0)
 
-	u2, err := tracefsUprobe(ssym, bashEx.path, off, perfAllThreads, false)
+	u2, err := tracefsUprobe(args)
 	c.Assert(err, qt.IsNil)
 	defer u2.Close()
 	c.Assert(u2.tracefsID, qt.Not(qt.Equals), 0)
@@ -242,24 +266,35 @@ func TestUprobeCreateTraceFS(t *testing.T) {
 		_ = closeTraceFSProbeEvent(uprobeType, rg, ssym)
 	}()
 
+	// Prepare probe args.
+	args := probeArgs{
+		group:  pg,
+		symbol: ssym,
+		path:   bashEx.path,
+		offset: off,
+	}
+
 	// Create a uprobe.
-	err = createTraceFSProbeEvent(uprobeType, pg, ssym, bashEx.path, off, false)
+	err = createTraceFSProbeEvent(uprobeType, args)
 	c.Assert(err, qt.IsNil)
 
 	// Attempt to create an identical uprobe using tracefs,
 	// expect it to fail with os.ErrExist.
-	err = createTraceFSProbeEvent(uprobeType, pg, ssym, bashEx.path, off, false)
+	err = createTraceFSProbeEvent(uprobeType, args)
 	c.Assert(errors.Is(err, os.ErrExist), qt.IsTrue,
 		qt.Commentf("expected consecutive uprobe creation to contain os.ErrExist, got: %v", err))
 
 	// Expect a successful close of the kprobe.
 	c.Assert(closeTraceFSProbeEvent(uprobeType, pg, ssym), qt.IsNil)
 
+	args.group = rg
+	args.ret = true
+
 	// Same test for a kretprobe.
-	err = createTraceFSProbeEvent(uprobeType, rg, ssym, bashEx.path, off, true)
+	err = createTraceFSProbeEvent(uprobeType, args)
 	c.Assert(err, qt.IsNil)
 
-	err = createTraceFSProbeEvent(uprobeType, rg, ssym, bashEx.path, off, true)
+	err = createTraceFSProbeEvent(uprobeType, args)
 	c.Assert(os.IsExist(err), qt.IsFalse,
 		qt.Commentf("expected consecutive uretprobe creation to contain os.ErrExist, got: %v", err))
 
@@ -288,21 +323,22 @@ func TestUprobeSanitizedSymbol(t *testing.T) {
 	}
 }
 
-func TestUprobePathOffset(t *testing.T) {
+func TestUprobeToken(t *testing.T) {
 	tests := []struct {
-		path     string
-		offset   uint64
+		args     probeArgs
 		expected string
 	}{
-		{"/bin/bash", 0, "/bin/bash:0x0"},
-		{"/bin/bash", 1, "/bin/bash:0x1"},
-		{"/bin/bash", 65535, "/bin/bash:0xffff"},
-		{"/bin/bash", 65536, "/bin/bash:0x10000"},
+		{probeArgs{path: "/bin/bash"}, "/bin/bash:0x0"},
+		{probeArgs{path: "/bin/bash", offset: 1}, "/bin/bash:0x1"},
+		{probeArgs{path: "/bin/bash", offset: 65535}, "/bin/bash:0xffff"},
+		{probeArgs{path: "/bin/bash", offset: 65536}, "/bin/bash:0x10000"},
+		{probeArgs{path: "/bin/bash", offset: 1, refCtrOffset: 1}, "/bin/bash:0x1(0x1)"},
+		{probeArgs{path: "/bin/bash", offset: 1, refCtrOffset: 65535}, "/bin/bash:0x1(0xffff)"},
 	}
 
 	for i, tt := range tests {
 		t.Run(fmt.Sprint(i), func(t *testing.T) {
-			po := uprobePathOffset(tt.path, tt.offset)
+			po := uprobeToken(tt.args)
 			if tt.expected != po {
 				t.Errorf("Expected path:offset to be '%s', got '%s'", tt.expected, po)
 			}
@@ -417,3 +453,7 @@ func TestUprobeProgramWrongPID(t *testing.T) {
 	// Assert that the value at index 0 is still 0.
 	assertMapValue(t, m, 0, 0)
 }
+
+func TestHaveRefCtrOffsetPMU(t *testing.T) {
+	testutils.CheckFeatureTest(t, haveRefCtrOffsetPMU)
+}