From 321e418f7bb9a331eda6d89196bfefc26d14552e Mon Sep 17 00:00:00 2001
From: Dave Tucker <dave@dtucker.co.uk>
Date: Fri, 24 Jun 2022 15:20:27 +0100
Subject: [PATCH] aya: Implement USDT probes

Signed-off-by: Dave Tucker <dave@dtucker.co.uk>
---
 .github/workflows/build-aya.yml      |   2 +-
 .vscode/settings.json                |   3 +-
 aya-common/src/lib.rs                |   5 +-
 aya/Cargo.toml                       |   1 +
 aya/src/bpf.rs                       |  10 +-
 aya/src/obj/mod.rs                   |   5 +
 aya/src/programs/cgroup_skb.rs       |   2 +-
 aya/src/programs/cgroup_sock.rs      |   2 +-
 aya/src/programs/cgroup_sock_addr.rs |   2 +-
 aya/src/programs/cgroup_sockopt.rs   |   2 +-
 aya/src/programs/cgroup_sysctl.rs    |   7 +-
 aya/src/programs/extension.rs        |  34 +-
 aya/src/programs/kprobe.rs           |   6 +-
 aya/src/programs/mod.rs              |  15 +
 aya/src/programs/perf_attach.rs      |  86 +++--
 aya/src/programs/perf_event.rs       |  27 +-
 aya/src/programs/probe.rs            |  22 +-
 aya/src/programs/sk_lookup.rs        |   2 +-
 aya/src/programs/trace_point.rs      |   9 +-
 aya/src/programs/uprobe.rs           | 282 +---------------
 aya/src/programs/usdt.rs             | 475 +++++++++++++++++++++++++++
 aya/src/programs/utils.rs            | 286 +++++++++++++++-
 aya/src/programs/xdp.rs              |   7 +-
 aya/src/sys/bpf.rs                   |  15 +-
 aya/src/sys/perf_event.rs            |   5 +
 bpf/aya-bpf-bindings/Cargo.toml      |   2 +-
 bpf/aya-bpf/Cargo.toml               |   2 +-
 27 files changed, 963 insertions(+), 353 deletions(-)
 create mode 100644 aya/src/programs/usdt.rs

diff --git a/.github/workflows/build-aya.yml b/.github/workflows/build-aya.yml
index 90a3b6ff..04fa4099 100644
--- a/.github/workflows/build-aya.yml
+++ b/.github/workflows/build-aya.yml
@@ -33,7 +33,7 @@ jobs:
         run: cross build --verbose
 
       - name: Run test
-        run: RUST_BACKTRACE=full cross test --verbose
+        run: RUST_BACKTRACE=full cross test --verbose --all-features
 
   test:
     runs-on: ubuntu-20.04
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 60fe4ee3..6dadfc07 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,3 +1,4 @@
 {
-  "rust-analyzer.linkedProjects": ["Cargo.toml", "bpf/Cargo.toml"]
+  "rust-analyzer.linkedProjects": ["Cargo.toml", "bpf/Cargo.toml"],
+  "rust-analyzer.cargo.allFeatures": true,
 }
diff --git a/aya-common/src/lib.rs b/aya-common/src/lib.rs
index e7752049..d9da0a23 100644
--- a/aya-common/src/lib.rs
+++ b/aya-common/src/lib.rs
@@ -177,7 +177,10 @@ pub mod with_std {
             // This needs testing and potentially updating
             r if r.starts_with('x') => {
                 let n: usize = r.strip_prefix('x').unwrap().parse().unwrap();
-                Ok((offset_of!(pt_regs, uregs) + (mem::size_of::<std::os::raw::c_long>() * n)) as i16)
+                Ok(
+                    (offset_of!(pt_regs, uregs) + (mem::size_of::<std::os::raw::c_long>() * n))
+                        as i16,
+                )
             }
             _ => Err(ParseError::UsdtArgSpecError(format!(
                 "unknown register: {}",
diff --git a/aya/Cargo.toml b/aya/Cargo.toml
index 1b15cace..46563493 100644
--- a/aya/Cargo.toml
+++ b/aya/Cargo.toml
@@ -23,6 +23,7 @@ tokio = { version = "1.2.0", features = ["macros", "rt", "rt-multi-thread", "net
 async-std = { version = "1.9.0", optional = true }
 async-io = { version = "1.3", optional = true }
 log = "0.4"
+aya-common = { version = "0.1.0", path = "../aya-common", features = ["user"] }
 
 [dev-dependencies]
 matches = "0.1.8"
diff --git a/aya/src/bpf.rs b/aya/src/bpf.rs
index 7617ea9d..93f7f3e2 100644
--- a/aya/src/bpf.rs
+++ b/aya/src/bpf.rs
@@ -25,7 +25,7 @@ use crate::{
         BtfTracePoint, CgroupSkb, CgroupSkbAttachType, CgroupSock, CgroupSockAddr, CgroupSockopt,
         CgroupSysctl, Extension, FEntry, FExit, KProbe, LircMode2, Lsm, PerfEvent, ProbeKind,
         Program, ProgramData, ProgramError, RawTracePoint, SchedClassifier, SkLookup, SkMsg, SkSkb,
-        SkSkbKind, SockOps, SocketFilter, TracePoint, UProbe, Xdp,
+        SkSkbKind, SockOps, SocketFilter, TracePoint, UProbe, Usdt, Xdp,
     },
     sys::{
         bpf_load_btf, bpf_map_freeze, bpf_map_update_elem_ptr, is_bpf_cookie_supported,
@@ -540,6 +540,9 @@ impl<'a> BpfLoader<'a> {
                                 attach_type: *attach_type,
                             })
                         }
+                        ProgramSection::Usdt { .. } => Program::Usdt(Usdt {
+                            data: ProgramData::new(prog_name, obj, btf_fd),
+                        }),
                     }
                 };
                 (name, program)
@@ -821,6 +824,11 @@ pub enum BpfError {
     #[error("program error")]
     /// A program error
     ProgramError(#[from] ProgramError),
+
+    /// Required map not found
+    #[error("required map {0} not found. did you enable the usdt feature (aya) or include usdt.bpf.h (libbpf)?")]
+    /// A program error
+    MissingMap(String),
 }
 
 fn load_btf(raw_btf: Vec<u8>) -> Result<RawFd, BtfError> {
diff --git a/aya/src/obj/mod.rs b/aya/src/obj/mod.rs
index c7852e45..6761eb2d 100644
--- a/aya/src/obj/mod.rs
+++ b/aya/src/obj/mod.rs
@@ -192,6 +192,9 @@ pub enum ProgramSection {
         name: String,
         attach_type: CgroupSockAttachType,
     },
+    Usdt {
+        name: String,
+    },
 }
 
 impl ProgramSection {
@@ -225,6 +228,7 @@ impl ProgramSection {
             ProgramSection::Extension { name } => name,
             ProgramSection::SkLookup { name } => name,
             ProgramSection::CgroupSock { name, .. } => name,
+            ProgramSection::Usdt { name } => name,
         }
     }
 }
@@ -248,6 +252,7 @@ impl FromStr for ProgramSection {
             "kprobe" => KProbe { name },
             "kretprobe" => KRetProbe { name },
             "uprobe" => UProbe { name },
+            "usdt" => Usdt { name },
             "uretprobe" => URetProbe { name },
             "xdp" => Xdp { name },
             "tp_btf" => BtfTracePoint { name },
diff --git a/aya/src/programs/cgroup_skb.rs b/aya/src/programs/cgroup_skb.rs
index 08d01069..ecdc68f1 100644
--- a/aya/src/programs/cgroup_skb.rs
+++ b/aya/src/programs/cgroup_skb.rs
@@ -99,7 +99,7 @@ impl CgroupSkb {
         };
         let k_ver = kernel_version().unwrap();
         if k_ver >= (5, 7, 0) {
-            let link_fd = bpf_link_create(prog_fd, cgroup_fd, attach_type, None, 0).map_err(
+            let link_fd = bpf_link_create(prog_fd, cgroup_fd, attach_type, None, None, 0).map_err(
                 |(_, io_error)| ProgramError::SyscallError {
                     call: "bpf_link_create".to_owned(),
                     io_error,
diff --git a/aya/src/programs/cgroup_sock.rs b/aya/src/programs/cgroup_sock.rs
index 08b8bf3c..a81a5797 100644
--- a/aya/src/programs/cgroup_sock.rs
+++ b/aya/src/programs/cgroup_sock.rs
@@ -75,7 +75,7 @@ impl CgroupSock {
         let attach_type = self.data.expected_attach_type.unwrap();
         let k_ver = kernel_version().unwrap();
         if k_ver >= (5, 7, 0) {
-            let link_fd = bpf_link_create(prog_fd, cgroup_fd, attach_type, None, 0).map_err(
+            let link_fd = bpf_link_create(prog_fd, cgroup_fd, attach_type, None, None, 0).map_err(
                 |(_, io_error)| ProgramError::SyscallError {
                     call: "bpf_link_create".to_owned(),
                     io_error,
diff --git a/aya/src/programs/cgroup_sock_addr.rs b/aya/src/programs/cgroup_sock_addr.rs
index e810b756..5466daff 100644
--- a/aya/src/programs/cgroup_sock_addr.rs
+++ b/aya/src/programs/cgroup_sock_addr.rs
@@ -76,7 +76,7 @@ impl CgroupSockAddr {
         let attach_type = self.data.expected_attach_type.unwrap();
         let k_ver = kernel_version().unwrap();
         if k_ver >= (5, 7, 0) {
-            let link_fd = bpf_link_create(prog_fd, cgroup_fd, attach_type, None, 0).map_err(
+            let link_fd = bpf_link_create(prog_fd, cgroup_fd, attach_type, None, None, 0).map_err(
                 |(_, io_error)| ProgramError::SyscallError {
                     call: "bpf_link_create".to_owned(),
                     io_error,
diff --git a/aya/src/programs/cgroup_sockopt.rs b/aya/src/programs/cgroup_sockopt.rs
index b680b3f6..1b511d95 100644
--- a/aya/src/programs/cgroup_sockopt.rs
+++ b/aya/src/programs/cgroup_sockopt.rs
@@ -72,7 +72,7 @@ impl CgroupSockopt {
         let attach_type = self.data.expected_attach_type.unwrap();
         let k_ver = kernel_version().unwrap();
         if k_ver >= (5, 7, 0) {
-            let link_fd = bpf_link_create(prog_fd, cgroup_fd, attach_type, None, 0).map_err(
+            let link_fd = bpf_link_create(prog_fd, cgroup_fd, attach_type, None, None, 0).map_err(
                 |(_, io_error)| ProgramError::SyscallError {
                     call: "bpf_link_create".to_owned(),
                     io_error,
diff --git a/aya/src/programs/cgroup_sysctl.rs b/aya/src/programs/cgroup_sysctl.rs
index 94c0412c..e7972eca 100644
--- a/aya/src/programs/cgroup_sysctl.rs
+++ b/aya/src/programs/cgroup_sysctl.rs
@@ -68,12 +68,11 @@ impl CgroupSysctl {
 
         let k_ver = kernel_version().unwrap();
         if k_ver >= (5, 7, 0) {
-            let link_fd = bpf_link_create(prog_fd, cgroup_fd, BPF_CGROUP_SYSCTL, None, 0).map_err(
-                |(_, io_error)| ProgramError::SyscallError {
+            let link_fd = bpf_link_create(prog_fd, cgroup_fd, BPF_CGROUP_SYSCTL, None, None, 0)
+                .map_err(|(_, io_error)| ProgramError::SyscallError {
                     call: "bpf_link_create".to_owned(),
                     io_error,
-                },
-            )? as RawFd;
+                })? as RawFd;
             self.data
                 .links
                 .insert(CgroupSysctlLink(CgroupSysctlLinkInner::Fd(FdLink::new(
diff --git a/aya/src/programs/extension.rs b/aya/src/programs/extension.rs
index 9ebc2d8e..56c3c86b 100644
--- a/aya/src/programs/extension.rs
+++ b/aya/src/programs/extension.rs
@@ -92,11 +92,18 @@ impl Extension {
         let target_fd = self.data.attach_prog_fd.ok_or(ProgramError::NotLoaded)?;
         let btf_id = self.data.attach_btf_id.ok_or(ProgramError::NotLoaded)?;
         // the attach type must be set as 0, which is bpf_attach_type::BPF_CGROUP_INET_INGRESS
-        let link_fd = bpf_link_create(prog_fd, target_fd, BPF_CGROUP_INET_INGRESS, Some(btf_id), 0)
-            .map_err(|(_, io_error)| ProgramError::SyscallError {
-                call: "bpf_link_create".to_owned(),
-                io_error,
-            })? as RawFd;
+        let link_fd = bpf_link_create(
+            prog_fd,
+            target_fd,
+            BPF_CGROUP_INET_INGRESS,
+            Some(btf_id),
+            None,
+            0,
+        )
+        .map_err(|(_, io_error)| ProgramError::SyscallError {
+            call: "bpf_link_create".to_owned(),
+            io_error,
+        })? as RawFd;
         self.data.links.insert(ExtensionLink(FdLink::new(link_fd)))
     }
 
@@ -120,11 +127,18 @@ impl Extension {
         let (_, btf_id) = get_btf_info(target_fd, func_name)?;
         let prog_fd = self.data.fd_or_err()?;
         // the attach type must be set as 0, which is bpf_attach_type::BPF_CGROUP_INET_INGRESS
-        let link_fd = bpf_link_create(prog_fd, target_fd, BPF_CGROUP_INET_INGRESS, Some(btf_id), 0)
-            .map_err(|(_, io_error)| ProgramError::SyscallError {
-                call: "bpf_link_create".to_owned(),
-                io_error,
-            })? as RawFd;
+        let link_fd = bpf_link_create(
+            prog_fd,
+            target_fd,
+            BPF_CGROUP_INET_INGRESS,
+            Some(btf_id),
+            None,
+            0,
+        )
+        .map_err(|(_, io_error)| ProgramError::SyscallError {
+            call: "bpf_link_create".to_owned(),
+            io_error,
+        })? as RawFd;
         self.data.links.insert(ExtensionLink(FdLink::new(link_fd)))
     }
 
diff --git a/aya/src/programs/kprobe.rs b/aya/src/programs/kprobe.rs
index 71212990..8136734a 100644
--- a/aya/src/programs/kprobe.rs
+++ b/aya/src/programs/kprobe.rs
@@ -6,7 +6,7 @@ use crate::{
     generated::bpf_prog_type::BPF_PROG_TYPE_KPROBE,
     programs::{
         define_link_wrapper, load_program,
-        perf_attach::{PerfLink, PerfLinkId},
+        perf_attach::{PerfLinkIdInner, PerfLinkInner},
         probe::{attach, ProbeKind},
         OwnedLink, ProgramData, ProgramError,
     },
@@ -94,8 +94,8 @@ define_link_wrapper!(
     KProbeLink,
     /// The type returned by [KProbe::attach]. Can be passed to [KProbe::detach].
     KProbeLinkId,
-    PerfLink,
-    PerfLinkId
+    PerfLinkInner,
+    PerfLinkIdInner
 );
 
 /// The type returned when attaching a [`KProbe`] fails.
diff --git a/aya/src/programs/mod.rs b/aya/src/programs/mod.rs
index b2f3fe7a..e2d151d8 100644
--- a/aya/src/programs/mod.rs
+++ b/aya/src/programs/mod.rs
@@ -61,6 +61,7 @@ pub mod tc;
 pub mod tp_btf;
 pub mod trace_point;
 pub mod uprobe;
+pub mod usdt;
 mod utils;
 pub mod xdp;
 
@@ -100,6 +101,7 @@ pub use tc::{SchedClassifier, TcAttachType, TcError};
 pub use tp_btf::BtfTracePoint;
 pub use trace_point::{TracePoint, TracePointError};
 pub use uprobe::{UProbe, UProbeError};
+pub use usdt::{Usdt, UsdtError};
 pub use xdp::{Xdp, XdpError, XdpFlags};
 
 use crate::{
@@ -206,6 +208,10 @@ pub enum ProgramError {
     #[error(transparent)]
     Btf(#[from] BtfError),
 
+    /// An error occurred while working with a Usdt program.
+    #[error(transparent)]
+    Usdt(#[from] UsdtError),
+
     /// The program is not attached.
     #[error("the program name `{name}` is invalid")]
     InvalidName {
@@ -273,6 +279,8 @@ pub enum Program {
     SkLookup(SkLookup),
     /// A [`CgroupSock`] program
     CgroupSock(CgroupSock),
+    /// A [`Usdt`] program
+    Usdt(Usdt),
 }
 
 impl Program {
@@ -303,6 +311,7 @@ impl Program {
             Program::CgroupSockAddr(_) => BPF_PROG_TYPE_CGROUP_SOCK_ADDR,
             Program::SkLookup(_) => BPF_PROG_TYPE_SK_LOOKUP,
             Program::CgroupSock(_) => BPF_PROG_TYPE_CGROUP_SOCK,
+            Program::Usdt(_) => BPF_PROG_TYPE_KPROBE,
         }
     }
 
@@ -332,6 +341,7 @@ impl Program {
             Program::CgroupSockAddr(p) => p.data.pin(path),
             Program::SkLookup(p) => p.data.pin(path),
             Program::CgroupSock(p) => p.data.pin(path),
+            Program::Usdt(p) => p.data.pin(path),
         }
     }
 
@@ -361,6 +371,7 @@ impl Program {
             Program::CgroupSockAddr(p) => p.unload(),
             Program::SkLookup(p) => p.unload(),
             Program::CgroupSock(p) => p.unload(),
+            Program::Usdt(p) => p.unload(),
         }
     }
 
@@ -393,6 +404,7 @@ impl Program {
             Program::CgroupSockAddr(p) => p.fd(),
             Program::SkLookup(p) => p.fd(),
             Program::CgroupSock(p) => p.fd(),
+            Program::Usdt(p) => p.fd(),
         }
     }
 }
@@ -632,6 +644,7 @@ impl_program_unload!(
     SkLookup,
     SockOps,
     CgroupSock,
+    Usdt,
 );
 
 macro_rules! impl_fd {
@@ -671,6 +684,7 @@ impl_fd!(
     SkLookup,
     SockOps,
     CgroupSock,
+    Usdt,
 );
 
 macro_rules! impl_try_from_program {
@@ -725,6 +739,7 @@ impl_try_from_program!(
     CgroupSockAddr,
     SkLookup,
     CgroupSock,
+    Usdt,
 );
 
 /// Provides information about a loaded program, like name, id and statistics
diff --git a/aya/src/programs/perf_attach.rs b/aya/src/programs/perf_attach.rs
index 7bd15f23..8445603e 100644
--- a/aya/src/programs/perf_attach.rs
+++ b/aya/src/programs/perf_attach.rs
@@ -3,11 +3,44 @@ use libc::close;
 use std::os::unix::io::RawFd;
 
 use crate::{
-    programs::{probe::detach_debug_fs, Link, ProbeKind, ProgramData, ProgramError},
-    sys::perf_event_ioctl,
-    PERF_EVENT_IOC_DISABLE, PERF_EVENT_IOC_ENABLE, PERF_EVENT_IOC_SET_BPF,
+    generated::bpf_attach_type::BPF_PERF_EVENT,
+    programs::{probe::detach_debug_fs, Link, ProbeKind, ProgramError},
+    sys::{bpf_link_create, perf_event_ioctl},
+    FEATURES, PERF_EVENT_IOC_DISABLE, PERF_EVENT_IOC_ENABLE, PERF_EVENT_IOC_SET_BPF,
 };
 
+use crate::programs::links::FdLink;
+
+#[derive(Debug, Hash, Eq, PartialEq)]
+pub(crate) enum PerfLinkIdInner {
+    FdLinkId(<FdLink as Link>::Id),
+    PerfLinkId(<PerfLink as Link>::Id),
+}
+
+#[derive(Debug)]
+pub(crate) enum PerfLinkInner {
+    FdLink(FdLink),
+    PerfLink(PerfLink),
+}
+
+impl Link for PerfLinkInner {
+    type Id = PerfLinkIdInner;
+
+    fn id(&self) -> Self::Id {
+        match self {
+            PerfLinkInner::FdLink(link) => PerfLinkIdInner::FdLinkId(link.id()),
+            PerfLinkInner::PerfLink(link) => PerfLinkIdInner::PerfLinkId(link.id()),
+        }
+    }
+
+    fn detach(self) -> Result<(), ProgramError> {
+        match self {
+            PerfLinkInner::FdLink(link) => link.detach(),
+            PerfLinkInner::PerfLink(link) => link.detach(),
+        }
+    }
+}
+
 /// The identifer of a PerfLink.
 #[derive(Debug, Hash, Eq, PartialEq)]
 pub struct PerfLinkId(RawFd);
@@ -41,29 +74,39 @@ impl Link for PerfLink {
     }
 }
 
-pub(crate) fn perf_attach<T: Link + From<PerfLink>>(
-    data: &mut ProgramData<T>,
+pub(crate) fn perf_attach(
+    prog_fd: RawFd,
     fd: RawFd,
-) -> Result<T::Id, ProgramError> {
-    perf_attach_either(data, fd, None, None)
+    cookie: Option<u64>,
+) -> Result<PerfLinkInner, ProgramError> {
+    if FEATURES.bpf_perf_link {
+        let link_fd = bpf_link_create(prog_fd, fd, BPF_PERF_EVENT, None, cookie, 0).map_err(
+            |(_, io_error)| ProgramError::SyscallError {
+                call: "bpf_link_create".to_owned(),
+                io_error,
+            },
+        )? as RawFd;
+        Ok(PerfLinkInner::FdLink(FdLink::new(link_fd)))
+    } else {
+        perf_attach_either(prog_fd, fd, None, None)
+    }
 }
 
-pub(crate) fn perf_attach_debugfs<T: Link + From<PerfLink>>(
-    data: &mut ProgramData<T>,
+pub(crate) fn perf_attach_debugfs(
+    prog_fd: RawFd,
     fd: RawFd,
     probe_kind: ProbeKind,
     event_alias: String,
-) -> Result<T::Id, ProgramError> {
-    perf_attach_either(data, fd, Some(probe_kind), Some(event_alias))
+) -> Result<PerfLinkInner, ProgramError> {
+    perf_attach_either(prog_fd, fd, Some(probe_kind), Some(event_alias))
 }
 
-fn perf_attach_either<T: Link + From<PerfLink>>(
-    data: &mut ProgramData<T>,
+fn perf_attach_either(
+    prog_fd: RawFd,
     fd: RawFd,
     probe_kind: Option<ProbeKind>,
     event_alias: Option<String>,
-) -> Result<T::Id, ProgramError> {
-    let prog_fd = data.fd_or_err()?;
+) -> Result<PerfLinkInner, ProgramError> {
     perf_event_ioctl(fd, PERF_EVENT_IOC_SET_BPF, prog_fd).map_err(|(_, io_error)| {
         ProgramError::SyscallError {
             call: "PERF_EVENT_IOC_SET_BPF".to_owned(),
@@ -77,12 +120,9 @@ fn perf_attach_either<T: Link + From<PerfLink>>(
         }
     })?;
 
-    data.links.insert(
-        PerfLink {
-            perf_fd: fd,
-            probe_kind,
-            event_alias,
-        }
-        .into(),
-    )
+    Ok(PerfLinkInner::PerfLink(PerfLink {
+        perf_fd: fd,
+        probe_kind,
+        event_alias,
+    }))
 }
diff --git a/aya/src/programs/perf_event.rs b/aya/src/programs/perf_event.rs
index 183e1e51..a60d9498 100644
--- a/aya/src/programs/perf_event.rs
+++ b/aya/src/programs/perf_event.rs
@@ -13,12 +13,14 @@ use crate::{
     },
     programs::{
         load_program, perf_attach,
-        perf_attach::{PerfLink, PerfLinkId},
+        perf_attach::{PerfLinkIdInner, PerfLinkInner},
         OwnedLink, ProgramData, ProgramError,
     },
     sys::perf_event_open,
 };
 
+use super::links::define_link_wrapper;
+
 /// The type of perf event
 #[repr(u32)]
 #[derive(Debug, Clone)]
@@ -119,7 +121,7 @@ pub enum PerfEventScope {
 #[derive(Debug)]
 #[doc(alias = "BPF_PROG_TYPE_PERF_EVENT")]
 pub struct PerfEvent {
-    pub(crate) data: ProgramData<PerfLink>,
+    pub(crate) data: ProgramData<PerfEventLink>,
 }
 
 impl PerfEvent {
@@ -141,7 +143,7 @@ impl PerfEvent {
         config: u64,
         scope: PerfEventScope,
         sample_policy: SamplePolicy,
-    ) -> Result<PerfLinkId, ProgramError> {
+    ) -> Result<PerfEventLinkId, ProgramError> {
         let (sample_period, sample_frequency) = match sample_policy {
             SamplePolicy::Period(period) => (period, None),
             SamplePolicy::Frequency(frequency) => (0, Some(frequency)),
@@ -168,13 +170,14 @@ impl PerfEvent {
             io_error,
         })? as i32;
 
-        perf_attach(&mut self.data, fd)
+        let link = perf_attach(self.data.fd_or_err()?, fd, None)?;
+        self.data.links.insert(PerfEventLink(link))
     }
 
     /// Detaches the program.
     ///
     /// See [PerfEvent::attach].
-    pub fn detach(&mut self, link_id: PerfLinkId) -> Result<(), ProgramError> {
+    pub fn detach(&mut self, link_id: PerfEventLinkId) -> Result<(), ProgramError> {
         self.data.links.remove(link_id)
     }
 
@@ -182,7 +185,19 @@ impl PerfEvent {
     ///
     /// The link will be detached on `Drop` and the caller is now responsible
     /// for managing its lifetime.
-    pub fn take_link(&mut self, link_id: PerfLinkId) -> Result<OwnedLink<PerfLink>, ProgramError> {
+    pub fn take_link(
+        &mut self,
+        link_id: PerfEventLinkId,
+    ) -> Result<OwnedLink<PerfEventLink>, ProgramError> {
         Ok(OwnedLink::new(self.data.take_link(link_id)?))
     }
 }
+
+define_link_wrapper!(
+    /// The link used by [PerfEvent] programs.
+    PerfEventLink,
+    /// The type returned by [PerfEvent::attach]. Can be passed to [PerfEvent::detach].
+    PerfEventLinkId,
+    PerfLinkInner,
+    PerfLinkIdInner
+);
diff --git a/aya/src/programs/probe.rs b/aya/src/programs/probe.rs
index 55d9da93..b33f0cbe 100644
--- a/aya/src/programs/probe.rs
+++ b/aya/src/programs/probe.rs
@@ -7,7 +7,7 @@ use std::{
 
 use crate::{
     programs::{
-        kprobe::KProbeError, perf_attach, perf_attach::PerfLink, perf_attach_debugfs,
+        kprobe::KProbeError, perf_attach, perf_attach::PerfLinkInner, perf_attach_debugfs,
         trace_point::read_sys_fs_trace_point_id, uprobe::UProbeError, Link, ProgramData,
         ProgramError,
     },
@@ -36,7 +36,7 @@ impl ProbeKind {
     }
 }
 
-pub(crate) fn attach<T: Link + From<PerfLink>>(
+pub(crate) fn attach<T: Link + From<PerfLinkInner>>(
     program_data: &mut ProgramData<T>,
     kind: ProbeKind,
     fn_name: &str,
@@ -49,12 +49,19 @@ pub(crate) fn attach<T: Link + From<PerfLink>>(
     if k_ver < (4, 17, 0) {
         let (fd, event_alias) = create_as_trace_point(kind, fn_name, offset, pid)?;
 
-        return perf_attach_debugfs(program_data, fd, kind, event_alias);
+        let link = T::from(perf_attach_debugfs(
+            program_data.fd_or_err()?,
+            fd,
+            kind,
+            event_alias,
+        )?);
+        return program_data.links.insert(link);
     };
 
-    let fd = create_as_probe(kind, fn_name, offset, pid)?;
+    let fd = create_as_probe(kind, fn_name, offset, pid, None)?;
 
-    perf_attach(program_data, fd)
+    let link = T::from(perf_attach(program_data.fd_or_err()?, fd, None)?);
+    program_data.links.insert(link)
 }
 
 pub(crate) fn detach_debug_fs(kind: ProbeKind, event_alias: &str) -> Result<(), ProgramError> {
@@ -70,11 +77,12 @@ pub(crate) fn detach_debug_fs(kind: ProbeKind, event_alias: &str) -> Result<(),
     Ok(())
 }
 
-fn create_as_probe(
+pub(crate) fn create_as_probe(
     kind: ProbeKind,
     fn_name: &str,
     offset: u64,
     pid: Option<pid_t>,
+    ref_cnt_offset: Option<u64>,
 ) -> Result<i32, ProgramError> {
     use ProbeKind::*;
 
@@ -97,7 +105,7 @@ fn create_as_probe(
         _ => None,
     };
 
-    let fd = perf_event_open_probe(perf_ty, ret_bit, fn_name, offset, pid).map_err(
+    let fd = perf_event_open_probe(perf_ty, ret_bit, fn_name, offset, pid, ref_cnt_offset).map_err(
         |(_code, io_error)| ProgramError::SyscallError {
             call: "perf_event_open".to_owned(),
             io_error,
diff --git a/aya/src/programs/sk_lookup.rs b/aya/src/programs/sk_lookup.rs
index cd347f99..b8c5099c 100644
--- a/aya/src/programs/sk_lookup.rs
+++ b/aya/src/programs/sk_lookup.rs
@@ -65,7 +65,7 @@ impl SkLookup {
         let prog_fd = self.data.fd_or_err()?;
         let netns_fd = netns.as_raw_fd();
 
-        let link_fd = bpf_link_create(prog_fd, netns_fd, BPF_SK_LOOKUP, None, 0).map_err(
+        let link_fd = bpf_link_create(prog_fd, netns_fd, BPF_SK_LOOKUP, None, None, 0).map_err(
             |(_, io_error)| ProgramError::SyscallError {
                 call: "bpf_link_create".to_owned(),
                 io_error,
diff --git a/aya/src/programs/trace_point.rs b/aya/src/programs/trace_point.rs
index e4f88bff..8a74a901 100644
--- a/aya/src/programs/trace_point.rs
+++ b/aya/src/programs/trace_point.rs
@@ -6,7 +6,7 @@ use crate::{
     generated::bpf_prog_type::BPF_PROG_TYPE_TRACEPOINT,
     programs::{
         define_link_wrapper, load_program,
-        perf_attach::{perf_attach, PerfLink, PerfLinkId},
+        perf_attach::{perf_attach, PerfLinkIdInner, PerfLinkInner},
         OwnedLink, ProgramData, ProgramError,
     },
     sys::perf_event_open_trace_point,
@@ -86,7 +86,8 @@ impl TracePoint {
             }
         })? as i32;
 
-        perf_attach(&mut self.data, fd)
+        let link = TracePointLink(perf_attach(self.data.fd_or_err()?, fd, None)?);
+        self.data.links.insert(link)
     }
 
     /// Detaches from a trace point.
@@ -113,8 +114,8 @@ define_link_wrapper!(
     TracePointLink,
     /// The type returned by [TracePoint::attach]. Can be passed to [TracePoint::detach].
     TracePointLinkId,
-    PerfLink,
-    PerfLinkId
+    PerfLinkInner,
+    PerfLinkIdInner
 );
 
 pub(crate) fn read_sys_fs_trace_point_id(
diff --git a/aya/src/programs/uprobe.rs b/aya/src/programs/uprobe.rs
index 7994ca7b..9c4a9500 100644
--- a/aya/src/programs/uprobe.rs
+++ b/aya/src/programs/uprobe.rs
@@ -1,14 +1,8 @@
 //! User space probes.
 use libc::pid_t;
-use object::{Object, ObjectSymbol};
 use std::{
-    collections::HashMap,
     error::Error,
-    ffi::CStr,
-    fs,
-    io::{self, BufRead, Cursor, Read},
-    mem,
-    os::raw::c_char,
+    io,
     path::{Path, PathBuf},
     sync::Arc,
 };
@@ -18,20 +12,13 @@ use crate::{
     generated::bpf_prog_type::BPF_PROG_TYPE_KPROBE,
     programs::{
         define_link_wrapper, load_program,
-        perf_attach::{PerfLink, PerfLinkId},
+        perf_attach::{PerfLinkIdInner, PerfLinkInner},
         probe::{attach, ProbeKind},
+        utils::{resolve_symbol, ProcMap, ProcMapError, LD_SO_CACHE, LD_SO_CACHE_FILE},
         OwnedLink, ProgramData, ProgramError,
     },
 };
 
-const LD_SO_CACHE_FILE: &str = "/etc/ld.so.cache";
-
-lazy_static! {
-    static ref LD_SO_CACHE: Result<LdSoCache, Arc<io::Error>> =
-        LdSoCache::load(LD_SO_CACHE_FILE).map_err(Arc::new);
-}
-const LD_SO_CACHE_HEADER: &str = "glibc-ld.so.cache1.1";
-
 /// An user space probe.
 ///
 /// User probes are eBPF programs that can be attached to any userspace
@@ -151,8 +138,8 @@ define_link_wrapper!(
     UProbeLink,
     /// The type returned by [UProbe::attach]. Can be passed to [UProbe::detach].
     UProbeLinkId,
-    PerfLink,
-    PerfLinkId
+    PerfLinkInner,
+    PerfLinkIdInner
 );
 
 /// The type returned when attaching an [`UProbe`] fails.
@@ -203,262 +190,3 @@ pub enum UProbeError {
         source: ProcMapError,
     },
 }
-#[derive(Debug)]
-pub(crate) struct CacheEntry {
-    key: String,
-    value: String,
-    _flags: i32,
-}
-
-#[derive(Debug)]
-pub(crate) struct LdSoCache {
-    entries: Vec<CacheEntry>,
-}
-
-impl LdSoCache {
-    pub fn load<T: AsRef<Path>>(path: T) -> Result<Self, io::Error> {
-        let data = fs::read(path)?;
-        Self::parse(&data)
-    }
-
-    fn parse(data: &[u8]) -> Result<Self, io::Error> {
-        let mut cursor = Cursor::new(data);
-
-        let read_u32 = |cursor: &mut Cursor<_>| -> Result<u32, io::Error> {
-            let mut buf = [0u8; mem::size_of::<u32>()];
-            cursor.read_exact(&mut buf)?;
-
-            Ok(u32::from_ne_bytes(buf))
-        };
-
-        let read_i32 = |cursor: &mut Cursor<_>| -> Result<i32, io::Error> {
-            let mut buf = [0u8; mem::size_of::<i32>()];
-            cursor.read_exact(&mut buf)?;
-
-            Ok(i32::from_ne_bytes(buf))
-        };
-
-        let mut buf = [0u8; LD_SO_CACHE_HEADER.len()];
-        cursor.read_exact(&mut buf)?;
-        let header = std::str::from_utf8(&buf).map_err(|_| {
-            io::Error::new(io::ErrorKind::InvalidData, "invalid ld.so.cache header")
-        })?;
-        if header != LD_SO_CACHE_HEADER {
-            return Err(io::Error::new(
-                io::ErrorKind::InvalidData,
-                "invalid ld.so.cache header",
-            ));
-        }
-
-        let num_entries = read_u32(&mut cursor)?;
-        let _str_tab_len = read_u32(&mut cursor)?;
-        cursor.consume(5 * mem::size_of::<u32>());
-
-        let mut entries = Vec::new();
-        for _ in 0..num_entries {
-            let flags = read_i32(&mut cursor)?;
-            let k_pos = read_u32(&mut cursor)? as usize;
-            let v_pos = read_u32(&mut cursor)? as usize;
-            cursor.consume(12);
-            let key =
-                unsafe { CStr::from_ptr(cursor.get_ref()[k_pos..].as_ptr() as *const c_char) }
-                    .to_string_lossy()
-                    .into_owned();
-            let value =
-                unsafe { CStr::from_ptr(cursor.get_ref()[v_pos..].as_ptr() as *const c_char) }
-                    .to_string_lossy()
-                    .into_owned();
-            entries.push(CacheEntry {
-                key,
-                value,
-                _flags: flags,
-            });
-        }
-
-        Ok(LdSoCache { entries })
-    }
-
-    pub fn resolve(&self, lib: &str) -> Option<&str> {
-        let lib = if !lib.contains(".so") {
-            lib.to_string() + ".so"
-        } else {
-            lib.to_string()
-        };
-        self.entries
-            .iter()
-            .find(|entry| entry.key.starts_with(&lib))
-            .map(|entry| entry.value.as_str())
-    }
-}
-
-#[derive(Error, Debug)]
-enum ResolveSymbolError {
-    #[error(transparent)]
-    Io(#[from] io::Error),
-
-    #[error("error parsing ELF")]
-    Object(#[from] object::Error),
-
-    #[error("unknown symbol `{0}`")]
-    Unknown(String),
-}
-
-fn resolve_symbol(path: &str, symbol: &str) -> Result<u64, ResolveSymbolError> {
-    let data = fs::read(path)?;
-    let obj = object::read::File::parse(&*data)?;
-
-    obj.dynamic_symbols()
-        .chain(obj.symbols())
-        .find(|sym| sym.name().map(|name| name == symbol).unwrap_or(false))
-        .map(|s| s.address())
-        .ok_or_else(|| ResolveSymbolError::Unknown(symbol.to_string()))
-}
-
-/// Error reading from /proc/pid/maps
-#[derive(Debug, Error)]
-pub enum ProcMapError {
-    /// An [`io::Error`]
-    #[error(transparent)]
-    IoError(io::Error),
-
-    /// Error parsing a line of /proc/pid/maps
-    #[error("proc map entry parse error")]
-    ParseError,
-}
-
-pub(crate) struct ProcMap {
-    _entries: Vec<ProcMapEntry>,
-    paths: HashMap<String, String>,
-}
-
-impl ProcMap {
-    fn new(pid: pid_t) -> Result<Self, ProcMapError> {
-        let maps_file = format!("/proc/{}/maps", pid);
-        let data = fs::read_to_string(maps_file).map_err(ProcMapError::IoError)?;
-        let mut entries = vec![];
-        let mut paths = HashMap::new();
-        for line in data.lines() {
-            let entry = ProcMapEntry::parse(line)?;
-            if let Some(path) = &entry.path {
-                let p = PathBuf::from(path);
-                let key = p.file_name().unwrap().to_string_lossy().into_owned();
-                let value = p.to_string_lossy().to_string();
-                paths.insert(key, value);
-            }
-            entries.push(entry);
-        }
-        Ok(ProcMap {
-            _entries: entries,
-            paths,
-        })
-    }
-
-    fn find_by_name(&self, lib: &str) -> Result<Option<String>, io::Error> {
-        let ret = if lib.contains(".so") {
-            self.paths.iter().find(|(k, _)| k.as_str().starts_with(lib))
-        } else {
-            let lib = lib.to_string();
-            let lib1 = lib.clone() + ".so";
-            let lib2 = lib + "-";
-            self.paths
-                .iter()
-                .find(|(k, _)| k.starts_with(&lib1) || k.starts_with(&lib2))
-        };
-
-        Ok(ret.map(|(_, v)| v.clone()))
-    }
-}
-
-pub(crate) struct ProcMapEntry {
-    _address: u64,
-    _address_end: u64,
-    _perms: String,
-    _offset: u64,
-    _dev: String,
-    _inode: u32,
-    path: Option<String>,
-}
-
-impl ProcMapEntry {
-    fn parse(line: &str) -> Result<Self, ProcMapError> {
-        let parts: Vec<&str> = line.split_whitespace().collect();
-        if parts.len() < 5 {
-            return Err(ProcMapError::ParseError);
-        }
-        let addr_parts: Vec<&str> = parts[0].split('-').collect();
-        let address =
-            u64::from_str_radix(addr_parts[0], 16).map_err(|_| ProcMapError::ParseError)?;
-        let address_end =
-            u64::from_str_radix(addr_parts[1], 16).map_err(|_| ProcMapError::ParseError)?;
-        let perms = parts[1];
-        let offset = u64::from_str_radix(parts[2], 16).map_err(|_| ProcMapError::ParseError)?;
-        let dev = parts[3];
-        let inode = parts[4].parse().map_err(|_| ProcMapError::ParseError)?;
-        let path = if parts.len() == 6 {
-            if parts[5].starts_with('/') {
-                Some(parts[5].to_string())
-            } else {
-                None
-            }
-        } else {
-            None
-        };
-
-        Ok(ProcMapEntry {
-            _address: address,
-            _address_end: address_end,
-            _perms: perms.to_string(),
-            _offset: offset,
-            _dev: dev.to_string(),
-            _inode: inode,
-            path,
-        })
-    }
-}
-
-#[cfg(test)]
-mod test {
-    use super::*;
-
-    #[test]
-    fn test_parse_proc_map_entry_from_str_1() {
-        let s = "7ffd6fbea000-7ffd6fbec000 r-xp 00000000 00:00 0                          [vdso]";
-        let proc_map = ProcMapEntry::parse(s).unwrap();
-        assert_eq!(proc_map._address, 0x7ffd6fbea000);
-        assert_eq!(proc_map._address_end, 0x7ffd6fbec000);
-        assert_eq!(proc_map._perms, "r-xp");
-        assert_eq!(proc_map._offset, 0x0);
-        assert_eq!(proc_map._dev, "00:00");
-        assert_eq!(proc_map._inode, 0);
-        assert_eq!(proc_map.path, None);
-    }
-
-    #[test]
-    fn test_parse_proc_map_entry_from_str_2() {
-        let s = "7f1bca83a000-7f1bca83c000 rw-p 00036000 fd:01 2895508                    /usr/lib64/ld-linux-x86-64.so.2";
-        let proc_map = ProcMapEntry::parse(s).unwrap();
-        assert_eq!(proc_map._address, 0x7f1bca83a000);
-        assert_eq!(proc_map._address_end, 0x7f1bca83c000);
-        assert_eq!(proc_map._perms, "rw-p");
-        assert_eq!(proc_map._offset, 0x00036000);
-        assert_eq!(proc_map._dev, "fd:01");
-        assert_eq!(proc_map._inode, 2895508);
-        assert_eq!(
-            proc_map.path,
-            Some("/usr/lib64/ld-linux-x86-64.so.2".to_string())
-        );
-    }
-
-    #[test]
-    fn test_parse_proc_map_entry_from_str_3() {
-        let s = "7f1bca5f9000-7f1bca601000 rw-p 00000000 00:00 0";
-        let proc_map = ProcMapEntry::parse(s).unwrap();
-        assert_eq!(proc_map._address, 0x7f1bca5f9000);
-        assert_eq!(proc_map._address_end, 0x7f1bca601000);
-        assert_eq!(proc_map._perms, "rw-p");
-        assert_eq!(proc_map._offset, 0x0);
-        assert_eq!(proc_map._dev, "00:00");
-        assert_eq!(proc_map._inode, 0);
-        assert_eq!(proc_map.path, None);
-    }
-}
diff --git a/aya/src/programs/usdt.rs b/aya/src/programs/usdt.rs
new file mode 100644
index 00000000..70520cbe
--- /dev/null
+++ b/aya/src/programs/usdt.rs
@@ -0,0 +1,475 @@
+//! User statically-defined tracepoints.
+use aya_common::{UsdtSpec, USDT_MAX_SPEC_COUNT};
+use libc::pid_t;
+use object::{elf::*, read::elf::*, Endianness};
+use std::{
+    collections::{HashMap, VecDeque},
+    convert::TryInto,
+    ffi::CStr,
+    fs,
+    io::{self, BufRead, Cursor, Read},
+    mem,
+    path::{Path, PathBuf},
+    sync::Arc,
+};
+use thiserror::Error;
+
+use crate::{
+    generated::{bpf_prog_type::BPF_PROG_TYPE_KPROBE, BPF_NOEXIST},
+    maps::{MapError, MapRefMut},
+    programs::{
+        define_link_wrapper, load_program,
+        perf_attach::{perf_attach, PerfLinkIdInner, PerfLinkInner},
+        probe::create_as_probe,
+        utils::{ProcMap, ProcMapError, LD_SO_CACHE, LD_SO_CACHE_FILE},
+        Link, OwnedLink, ProbeKind, ProgramData, ProgramError,
+    },
+    Pod, FEATURES,
+};
+
+unsafe impl Pod for UsdtSpec {}
+
+/// Name of the map used for USDT specs.
+pub const USDT_SPEC_MAP: &str = "__bpf_usdt_specs";
+/// Name of the map used for USDT to IP mappings.
+pub const USDT_IP_TO_SPEC_MAP: &str = "__bpf_usdt_ip_to_spec_id";
+
+/// A user statically-defined tracepoint
+#[derive(Debug)]
+#[doc(alias = "BPF_PROG_TYPE_KPROBE")]
+pub struct Usdt {
+    pub(crate) data: ProgramData<UsdtLink>,
+}
+
+impl Usdt {
+    /// Loads the program inside the kernel.
+    pub fn load(&mut self) -> Result<(), ProgramError> {
+        load_program(BPF_PROG_TYPE_KPROBE, &mut self.data)
+    }
+
+    /// Attaches the program.
+    ///
+    /// Attaches the uprobe to the tracepoint `tp_provider`/`tp_name` defined in the `target`.
+    /// If `pid` is not `None`, the program executes only when the target
+    /// function is executed by the given `pid`.
+    ///
+    /// The `target` argument can be an absolute path to a binary or library, or
+    /// a library name (eg: `"libc"`).
+    ///
+    /// The returned value can be used to detach, see [Usdt::detach].
+    pub fn attach<T: AsRef<Path>>(
+        &mut self,
+        mut spec_map: crate::maps::Array<MapRefMut, UsdtSpec>,
+        mut ip_to_spec_map: crate::maps::HashMap<MapRefMut, i64, u32>,
+        tp_provider: &str,
+        tp_name: &str,
+        target: T,
+        pid: Option<pid_t>,
+    ) -> Result<UsdtLinkId, ProgramError> {
+        let target = target.as_ref();
+        let target_str = &*target.as_os_str().to_string_lossy();
+
+        let mut path = if let Some(pid) = pid {
+            let proc_map_libs =
+                ProcMap::new(pid).map_err(|e| UsdtError::ProcMapError { pid, source: e })?;
+            proc_map_libs
+                .find_by_name(target_str)
+                .map_err(|io_error| UsdtError::FileError {
+                    filename: format!("/proc/{}/maps", pid),
+                    io_error,
+                })?
+        } else {
+            None
+        };
+
+        if path.is_none() {
+            path = if target.is_absolute() {
+                Some(target_str)
+            } else {
+                let cache = LD_SO_CACHE
+                    .as_ref()
+                    .map_err(|error| UsdtError::InvalidLdSoCache {
+                        io_error: error.clone(),
+                    })?;
+                cache.resolve(target_str)
+            }
+            .map(String::from)
+        };
+
+        let path = path.ok_or(UsdtError::InvalidTarget {
+            path: target.to_owned(),
+        })?;
+
+        let tracepoints = collect_usdts(&path, tp_provider, tp_name, pid)?;
+        let mut perf_links = vec![];
+        let mut spec_ids = VecDeque::with_capacity(USDT_MAX_SPEC_COUNT as usize);
+        for i in 0..USDT_MAX_SPEC_COUNT {
+            spec_ids.push_back(i)
+        }
+        let mut spec_id_map = HashMap::new();
+        for t in tracepoints {
+            let id = if spec_id_map.contains_key(&t.args) {
+                *(spec_id_map.get(&t.args).unwrap())
+            } else {
+                let id = spec_ids.pop_front().unwrap();
+                spec_id_map.insert(t.args.clone(), id);
+                spec_map.set(id, t.spec, 0)?;
+                id
+            };
+            let mut cookie = Some(id as u64);
+            if !FEATURES.bpf_cookie {
+                cookie.take();
+                if let Err(e) =
+                    ip_to_spec_map.insert(t.abs_ip.try_into().unwrap(), id, BPF_NOEXIST.into())
+                {
+                    if let MapError::SyscallError { code, .. } = e {
+                        if code != (-libc::EEXIST).into() {
+                            return Err(ProgramError::MapError(e));
+                        }
+                    }
+                }
+            }
+            let fd = create_as_probe(ProbeKind::UProbe, &path, t.rel_ip, pid, Some(t.sem_off))?;
+            let link = perf_attach(self.data.fd_or_err()?, fd, cookie)?;
+            perf_links.push(link);
+        }
+        let link = UsdtLink(MultiPerfLink { perf_links });
+        self.data.links.insert(link)
+    }
+
+    /// Detaches the program.
+    ///
+    /// See [UProbe::attach].
+    pub fn detach(&mut self, link_id: UsdtLinkId) -> Result<(), ProgramError> {
+        self.data.links.remove(link_id)
+    }
+
+    /// Takes ownership of the link referenced by the provided link_id.
+    ///
+    /// The link will be detached on `Drop` and the caller is now responsible
+    /// for managing its lifetime.
+    pub fn take_link(&mut self, link_id: UsdtLinkId) -> Result<OwnedLink<UsdtLink>, ProgramError> {
+        Ok(OwnedLink::new(self.data.take_link(link_id)?))
+    }
+}
+
+/// The identifer of a MultiPerfLink.
+#[derive(Debug, Hash, Eq, PartialEq)]
+pub struct MultiPerfLinkId(Vec<PerfLinkIdInner>);
+
+/// The attachment type of USDT programs.
+#[derive(Debug)]
+pub struct MultiPerfLink {
+    perf_links: Vec<PerfLinkInner>,
+}
+
+impl Link for MultiPerfLink {
+    type Id = MultiPerfLinkId;
+
+    fn id(&self) -> Self::Id {
+        let ids = self.perf_links.iter().map(|p| p.id()).collect();
+        MultiPerfLinkId(ids)
+    }
+
+    fn detach(self) -> Result<(), ProgramError> {
+        for l in self.perf_links {
+            l.detach()?;
+        }
+        Ok(())
+    }
+}
+
+define_link_wrapper!(
+    /// The link used by [Usdt] programs.
+    UsdtLink,
+    /// The type returned by [Usdt::attach]. Can be passed to [Usdt::detach].
+    UsdtLinkId,
+    MultiPerfLink,
+    MultiPerfLinkId
+);
+
+/// The type returned when attaching an [`UProbe`] fails.
+#[derive(Debug, Error)]
+pub enum UsdtError {
+    /// There was an error parsing `/etc/ld.so.cache`.
+    #[error("error reading `{}` file", LD_SO_CACHE_FILE)]
+    InvalidLdSoCache {
+        /// the original [`io::Error`]
+        #[source]
+        io_error: Arc<io::Error>,
+    },
+
+    /// The target program could not be found.
+    #[error("could not resolve uprobe target `{path}`")]
+    InvalidTarget {
+        /// path to target
+        path: PathBuf,
+    },
+
+    /// There was an error resolving the target symbol.
+    #[error("error resolving symbol")]
+    SymbolError {
+        /// symbol name
+        symbol: String,
+        /// the original error
+        #[source]
+        error: Box<dyn std::error::Error + Send + Sync>,
+    },
+
+    /// There was an error accessing `filename`.
+    #[error("`{filename}`")]
+    FileError {
+        /// The file name
+        filename: String,
+        /// The [`io::Error`] returned from the file operation
+        #[source]
+        io_error: io::Error,
+    },
+
+    /// There was en error resolving a path
+    #[error("error fetching libs for {pid}")]
+    ProcMapError {
+        /// The pid
+        pid: i32,
+        /// The [`ProcMapError`] that caused the error
+        #[source]
+        source: ProcMapError,
+    },
+
+    /// Unsupported file type
+    #[error("unsupported file type")]
+    Unsupported,
+
+    /// An [`io::Error`]
+    #[error("io error")]
+    Io(#[from] io::Error),
+
+    /// An [`object::Error`]
+    #[error("error parsing ELF")]
+    Object(#[from] object::Error),
+
+    /// Can't find matching offset in shard libs
+    #[error("can't find matching offset in shared libs")]
+    OffsetError,
+
+    /// Section is not executable
+    #[error("section is not executable")]
+    NoExec,
+
+    /// Segment is not found
+    #[error("segment not found")]
+    SegmentNotFound,
+
+    /// BPF Cookies are not supported
+    #[error("bpf cookies are required to support attachment without a pid")]
+    NoCookie,
+}
+
+fn collect_usdts(
+    path: &str,
+    provider: &str,
+    name: &str,
+    pid: Option<pid_t>,
+) -> Result<Vec<UsdtTarget>, UsdtError> {
+    let file = fs::read(path)?;
+    let data = &*file;
+    if let Ok(elf) = object::elf::FileHeader32::parse(data) {
+        if mem::size_of::<usize>() != 4 {
+            return Err(UsdtError::Unsupported);
+        }
+        return collect_usdts_from_elf(elf, data, provider, name, pid);
+    } else if let Ok(elf) = object::elf::FileHeader64::parse(data) {
+        if mem::size_of::<usize>() != 8 {
+            return Err(UsdtError::Unsupported);
+        }
+        return collect_usdts_from_elf(elf, data, provider, name, pid);
+    }
+    Err(UsdtError::Unsupported)
+}
+
+fn collect_usdts_from_elf<Elf: FileHeader<Endian = Endianness>>(
+    elf: &Elf,
+    data: &[u8],
+    provider: &str,
+    name: &str,
+    pid: Option<pid_t>,
+) -> Result<Vec<UsdtTarget>, UsdtError> {
+    let endian = elf.endian()?;
+    let sections = elf.sections(endian, data)?;
+    let program_headers = elf.program_headers(endian, data)?;
+    let mut results = vec![];
+    let mut base_addr: Option<u64> = None;
+    if let Some((_, base_section)) = sections.section_by_name(endian, b".stapsdt.base") {
+        base_addr = Some(base_section.sh_addr(endian).into())
+    };
+    if let Some((_, notes_section)) = sections.section_by_name(endian, b".note.stapsdt") {
+        if let Some(mut notes) = notes_section.notes(endian, data)? {
+            while let Ok(Some(note)) = notes.next() {
+                if note.name() != b"stapsdt" {
+                    continue;
+                }
+                if note.n_type(endian) != 3 {
+                    continue;
+                }
+                let note_data = note.desc();
+                let n = UsdtNote::parse(endian, note_data)?;
+                if n.provider != provider || n.name != name {
+                    continue;
+                }
+
+                let mut abs_ip = n.loc_addr;
+                if let Some(addr) = base_addr {
+                    abs_ip += addr - n.base_addr;
+                }
+
+                let seg = find_segment_by_address::<Elf>(program_headers, endian, abs_ip)
+                    .ok_or(UsdtError::SegmentNotFound)?;
+                if seg.p_flags(endian) & PF_X == 0 {
+                    return Err(UsdtError::NoExec);
+                }
+                let rel_ip = abs_ip - seg.p_vaddr(endian).into() + seg.p_offset(endian).into();
+
+                // If attaching to a sharef library and bpf cookies are not supported.
+                // Abs address of attach points are required
+                if elf.e_type(endian) == ET_DYN && !FEATURES.bpf_cookie {
+                    if pid.is_none() {
+                        return Err(UsdtError::NoCookie);
+                    }
+                    let proc_map_libs =
+                        ProcMap::new(pid.unwrap()).map_err(|e| UsdtError::ProcMapError {
+                            pid: pid.unwrap(),
+                            source: e,
+                        })?;
+                    let res = proc_map_libs
+                        .find_by_offset(rel_ip)
+                        .ok_or(UsdtError::OffsetError)?;
+                    abs_ip = res.address - res.offset + rel_ip;
+                }
+
+                let mut sem_off = 0;
+                if n.sem_addr != 0x0 {
+                    // semaphore refcnt support was in 4.20, which is min supported version so we assume its supported
+                    let seg = find_segment_by_address::<Elf>(program_headers, endian, n.sem_addr)
+                        .ok_or(UsdtError::SegmentNotFound)?;
+                    if seg.p_flags(endian) & PF_X == 0 {
+                        return Err(UsdtError::NoExec);
+                    }
+                    sem_off = n.sem_addr - seg.p_vaddr(endian).into() + seg.p_offset(endian).into();
+                }
+                let spec = n.args.parse().unwrap();
+                results.push(UsdtTarget {
+                    abs_ip,
+                    rel_ip,
+                    sem_off,
+                    args: n.args,
+                    spec,
+                })
+            }
+        }
+    }
+    Ok(results)
+}
+
+fn find_segment_by_address<Elf: FileHeader<Endian = Endianness>>(
+    program_headers: &[Elf::ProgramHeader],
+    endian: Endianness,
+    addr: u64,
+) -> Option<&Elf::ProgramHeader> {
+    for header in program_headers {
+        if header.p_vaddr(endian).into() < addr
+            && addr < (header.p_vaddr(endian).into() + header.p_memsz(endian).into())
+        {
+            return Some(header);
+        }
+    }
+    None
+}
+
+#[derive(Debug)]
+pub(crate) struct UsdtTarget {
+    abs_ip: u64,
+    rel_ip: u64,
+    sem_off: u64,
+    args: String,
+    spec: UsdtSpec,
+}
+
+#[derive(Debug)]
+pub(crate) struct UsdtNote {
+    loc_addr: u64,
+    base_addr: u64,
+    sem_addr: u64,
+    provider: String,
+    name: String,
+    args: String,
+}
+
+impl UsdtNote {
+    pub(crate) fn parse(endianness: Endianness, data: &[u8]) -> Result<UsdtNote, UsdtError> {
+        let mut cursor = Cursor::new(data);
+        let read_u64 = |cursor: &mut Cursor<_>| -> Result<u64, io::Error> {
+            let mut buf = [0u8; mem::size_of::<u64>()];
+            cursor.read_exact(&mut buf)?;
+            match endianness {
+                Endianness::Big => Ok(u64::from_be_bytes(buf)),
+                Endianness::Little => Ok(u64::from_le_bytes(buf)),
+            }
+        };
+        let read_string = |cursor: &mut Cursor<_>| -> Result<String, io::Error> {
+            let mut buf = vec![];
+            cursor.read_until(b'\0', &mut buf)?;
+            Ok(CStr::from_bytes_with_nul(&buf)
+                .unwrap()
+                .to_string_lossy()
+                .to_string())
+        };
+        let loc_addr = read_u64(&mut cursor)?;
+        let base_addr = read_u64(&mut cursor)?;
+        let sem_addr = read_u64(&mut cursor)?;
+        let provider = read_string(&mut cursor)?;
+        let name = read_string(&mut cursor)?;
+        let args = read_string(&mut cursor)?;
+
+        let res = UsdtNote {
+            loc_addr,
+            base_addr,
+            sem_addr,
+            provider,
+            name,
+            args,
+        };
+        Ok(res)
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use super::*;
+
+    #[test]
+    fn test_parse_stapsdt() {
+        /*
+        /usr/bin/mariadb:     file format elf64-x86-64
+
+        Contents of section .note.stapsdt:
+         0000 08000000 34000000 03000000 73746170  ....4.......stap
+         0010 73647400 34a10d00 00000000 382e3600  sdt.4.......8.6.
+         0020 00000000 00000000 00000000 6c696267  ............libg
+         0030 63630075 6e77696e 64003840 25726469  cc.unwind.8@%rdi
+         0040 20384025 72736900                     8@%rsi
+        */
+        let data: &[u8] = &[
+            0x34, 0xa1, 0x0d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x2e, 0x36, 0x00, 0x00, 0x00,
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6c, 0x69, 0x62, 0x67,
+            0x63, 0x63, 0x00, 0x75, 0x6e, 0x77, 0x69, 0x6e, 0x64, 0x00, 0x38, 0x40, 0x25, 0x72,
+            0x64, 0x69, 0x20, 0x38, 0x40, 0x25, 0x72, 0x73, 0x69, 0x00,
+        ];
+        let n = UsdtNote::parse(Endianness::Little, data).unwrap();
+        assert_eq!(n.loc_addr, 0xda134);
+        assert_eq!(n.base_addr, 0x362e38);
+        assert_eq!(n.sem_addr, 0x0);
+        assert_eq!(n.provider, "libgcc");
+        assert_eq!(n.name, "unwind");
+        assert_eq!(n.args, "8@%rdi 8@%rsi");
+    }
+}
diff --git a/aya/src/programs/utils.rs b/aya/src/programs/utils.rs
index 56af2a0f..6efe487b 100644
--- a/aya/src/programs/utils.rs
+++ b/aya/src/programs/utils.rs
@@ -1,5 +1,17 @@
 //! Common functions shared between multiple eBPF program types.
-use std::{ffi::CStr, os::unix::io::RawFd};
+use libc::pid_t;
+use object::{Object, ObjectSymbol};
+use std::{
+    collections::HashMap,
+    ffi::CStr,
+    fs,
+    io::{self, BufRead, Cursor, Read},
+    mem,
+    os::{raw::c_char, unix::prelude::RawFd},
+    path::{Path, PathBuf},
+    sync::Arc,
+};
+use thiserror::Error;
 
 use crate::{
     programs::{FdLink, Link, ProgramData, ProgramError},
@@ -22,3 +34,275 @@ pub(crate) fn attach_raw_tracepoint<T: Link + From<FdLink>>(
 
     program_data.links.insert(FdLink::new(pfd).into())
 }
+
+pub(crate) const LD_SO_CACHE_FILE: &str = "/etc/ld.so.cache";
+
+lazy_static! {
+    pub(crate) static ref LD_SO_CACHE: Result<LdSoCache, Arc<io::Error>> =
+        LdSoCache::load(LD_SO_CACHE_FILE).map_err(Arc::new);
+}
+const LD_SO_CACHE_HEADER: &str = "glibc-ld.so.cache1.1";
+#[derive(Debug)]
+pub(crate) struct CacheEntry {
+    key: String,
+    value: String,
+    _flags: i32,
+}
+
+#[derive(Debug)]
+pub(crate) struct LdSoCache {
+    entries: Vec<CacheEntry>,
+}
+
+impl LdSoCache {
+    pub fn load<T: AsRef<Path>>(path: T) -> Result<Self, io::Error> {
+        let data = fs::read(path)?;
+        Self::parse(&data)
+    }
+
+    fn parse(data: &[u8]) -> Result<Self, io::Error> {
+        let mut cursor = Cursor::new(data);
+
+        let read_u32 = |cursor: &mut Cursor<_>| -> Result<u32, io::Error> {
+            let mut buf = [0u8; mem::size_of::<u32>()];
+            cursor.read_exact(&mut buf)?;
+
+            Ok(u32::from_ne_bytes(buf))
+        };
+
+        let read_i32 = |cursor: &mut Cursor<_>| -> Result<i32, io::Error> {
+            let mut buf = [0u8; mem::size_of::<i32>()];
+            cursor.read_exact(&mut buf)?;
+
+            Ok(i32::from_ne_bytes(buf))
+        };
+
+        let mut buf = [0u8; LD_SO_CACHE_HEADER.len()];
+        cursor.read_exact(&mut buf)?;
+        let header = std::str::from_utf8(&buf).map_err(|_| {
+            io::Error::new(io::ErrorKind::InvalidData, "invalid ld.so.cache header")
+        })?;
+        if header != LD_SO_CACHE_HEADER {
+            return Err(io::Error::new(
+                io::ErrorKind::InvalidData,
+                "invalid ld.so.cache header",
+            ));
+        }
+
+        let num_entries = read_u32(&mut cursor)?;
+        let _str_tab_len = read_u32(&mut cursor)?;
+        cursor.consume(5 * mem::size_of::<u32>());
+
+        let mut entries = Vec::new();
+        for _ in 0..num_entries {
+            let flags = read_i32(&mut cursor)?;
+            let k_pos = read_u32(&mut cursor)? as usize;
+            let v_pos = read_u32(&mut cursor)? as usize;
+            cursor.consume(12);
+            let key =
+                unsafe { CStr::from_ptr(cursor.get_ref()[k_pos..].as_ptr() as *const c_char) }
+                    .to_string_lossy()
+                    .into_owned();
+            let value =
+                unsafe { CStr::from_ptr(cursor.get_ref()[v_pos..].as_ptr() as *const c_char) }
+                    .to_string_lossy()
+                    .into_owned();
+            entries.push(CacheEntry {
+                key,
+                value,
+                _flags: flags,
+            });
+        }
+
+        Ok(LdSoCache { entries })
+    }
+
+    pub fn resolve(&self, lib: &str) -> Option<&str> {
+        let lib = if !lib.contains(".so") {
+            lib.to_string() + ".so"
+        } else {
+            lib.to_string()
+        };
+        self.entries
+            .iter()
+            .find(|entry| entry.key.starts_with(&lib))
+            .map(|entry| entry.value.as_str())
+    }
+}
+
+#[derive(Error, Debug)]
+pub(crate) enum ResolveSymbolError {
+    #[error(transparent)]
+    Io(#[from] io::Error),
+
+    #[error("error parsing ELF")]
+    Object(#[from] object::Error),
+
+    #[error("unknown symbol `{0}`")]
+    Unknown(String),
+}
+
+pub(crate) fn resolve_symbol(path: &str, symbol: &str) -> Result<u64, ResolveSymbolError> {
+    let data = fs::read(path)?;
+    let obj = object::read::File::parse(&*data)?;
+
+    obj.dynamic_symbols()
+        .chain(obj.symbols())
+        .find(|sym| sym.name().map(|name| name == symbol).unwrap_or(false))
+        .map(|s| s.address())
+        .ok_or_else(|| ResolveSymbolError::Unknown(symbol.to_string()))
+}
+
+/// Error reading from /proc/pid/maps
+#[derive(Debug, Error)]
+pub enum ProcMapError {
+    /// An [`io::Error`]
+    #[error(transparent)]
+    IoError(io::Error),
+
+    /// Error parsing a line of /proc/pid/maps
+    #[error("proc map entry parse error")]
+    ParseError,
+}
+
+pub(crate) struct ProcMap {
+    entries: Vec<ProcMapEntry>,
+    paths: HashMap<String, String>,
+}
+
+impl ProcMap {
+    pub(crate) fn new(pid: pid_t) -> Result<Self, ProcMapError> {
+        let maps_file = format!("/proc/{}/maps", pid);
+        let data = fs::read_to_string(maps_file).map_err(ProcMapError::IoError)?;
+        let mut entries = vec![];
+        let mut paths = HashMap::new();
+        for line in data.lines() {
+            let entry = ProcMapEntry::parse(line)?;
+            if let Some(path) = &entry.path {
+                let p = PathBuf::from(path);
+                let key = p.file_name().unwrap().to_string_lossy().into_owned();
+                let value = p.to_string_lossy().to_string();
+                paths.insert(key, value);
+            }
+            entries.push(entry);
+        }
+        Ok(ProcMap { entries, paths })
+    }
+
+    pub(crate) fn find_by_name(&self, lib: &str) -> Result<Option<String>, io::Error> {
+        let ret = if lib.contains(".so") {
+            self.paths.iter().find(|(k, _)| k.as_str().starts_with(lib))
+        } else {
+            let lib = lib.to_string();
+            let lib1 = lib.clone() + ".so";
+            let lib2 = lib + "-";
+            self.paths
+                .iter()
+                .find(|(k, _)| k.starts_with(&lib1) || k.starts_with(&lib2))
+        };
+
+        Ok(ret.map(|(_, v)| v.clone()))
+    }
+
+    pub(crate) fn find_by_offset(&self, offset: u64) -> Option<&ProcMapEntry> {
+        for e in &self.entries {
+            if e.offset <= offset && offset < e.offset + (e.address_end - e.address) {
+                return Some(e);
+            }
+        }
+        None
+    }
+}
+
+pub(crate) struct ProcMapEntry {
+    pub address: u64,
+    pub address_end: u64,
+    _perms: String,
+    pub offset: u64,
+    _dev: String,
+    _inode: u32,
+    pub path: Option<String>,
+}
+
+impl ProcMapEntry {
+    fn parse(line: &str) -> Result<Self, ProcMapError> {
+        let parts: Vec<&str> = line.split_whitespace().collect();
+        if parts.len() < 5 {
+            return Err(ProcMapError::ParseError);
+        }
+        let addr_parts: Vec<&str> = parts[0].split('-').collect();
+        let address =
+            u64::from_str_radix(addr_parts[0], 16).map_err(|_| ProcMapError::ParseError)?;
+        let address_end =
+            u64::from_str_radix(addr_parts[1], 16).map_err(|_| ProcMapError::ParseError)?;
+        let perms = parts[1];
+        let offset = u64::from_str_radix(parts[2], 16).map_err(|_| ProcMapError::ParseError)?;
+        let dev = parts[3];
+        let inode = parts[4].parse().map_err(|_| ProcMapError::ParseError)?;
+        let path = if parts.len() == 6 {
+            if parts[5].starts_with('/') {
+                Some(parts[5].to_string())
+            } else {
+                None
+            }
+        } else {
+            None
+        };
+
+        Ok(ProcMapEntry {
+            address,
+            address_end,
+            _perms: perms.to_string(),
+            offset,
+            _dev: dev.to_string(),
+            _inode: inode,
+            path,
+        })
+    }
+}
+#[cfg(test)]
+mod test {
+    use super::*;
+
+    #[test]
+    fn test_parse_proc_map_entry_from_str_1() {
+        let s = "7ffd6fbea000-7ffd6fbec000 r-xp 00000000 00:00 0                          [vdso]";
+        let proc_map = ProcMapEntry::parse(s).unwrap();
+        assert_eq!(proc_map.address, 0x7ffd6fbea000);
+        assert_eq!(proc_map.address_end, 0x7ffd6fbec000);
+        assert_eq!(proc_map._perms, "r-xp");
+        assert_eq!(proc_map.offset, 0x0);
+        assert_eq!(proc_map._dev, "00:00");
+        assert_eq!(proc_map._inode, 0);
+        assert_eq!(proc_map.path, None);
+    }
+
+    #[test]
+    fn test_parse_proc_map_entry_from_str_2() {
+        let s = "7f1bca83a000-7f1bca83c000 rw-p 00036000 fd:01 2895508                    /usr/lib64/ld-linux-x86-64.so.2";
+        let proc_map = ProcMapEntry::parse(s).unwrap();
+        assert_eq!(proc_map.address, 0x7f1bca83a000);
+        assert_eq!(proc_map.address_end, 0x7f1bca83c000);
+        assert_eq!(proc_map._perms, "rw-p");
+        assert_eq!(proc_map.offset, 0x00036000);
+        assert_eq!(proc_map._dev, "fd:01");
+        assert_eq!(proc_map._inode, 2895508);
+        assert_eq!(
+            proc_map.path,
+            Some("/usr/lib64/ld-linux-x86-64.so.2".to_string())
+        );
+    }
+
+    #[test]
+    fn test_parse_proc_map_entry_from_str_3() {
+        let s = "7f1bca5f9000-7f1bca601000 rw-p 00000000 00:00 0";
+        let proc_map = ProcMapEntry::parse(s).unwrap();
+        assert_eq!(proc_map.address, 0x7f1bca5f9000);
+        assert_eq!(proc_map.address_end, 0x7f1bca601000);
+        assert_eq!(proc_map._perms, "rw-p");
+        assert_eq!(proc_map.offset, 0x0);
+        assert_eq!(proc_map._dev, "00:00");
+        assert_eq!(proc_map._inode, 0);
+        assert_eq!(proc_map.path, None);
+    }
+}
diff --git a/aya/src/programs/xdp.rs b/aya/src/programs/xdp.rs
index 226c0caf..b1fef4dd 100644
--- a/aya/src/programs/xdp.rs
+++ b/aya/src/programs/xdp.rs
@@ -106,12 +106,11 @@ impl Xdp {
 
         let k_ver = kernel_version().unwrap();
         if k_ver >= (5, 9, 0) {
-            let link_fd = bpf_link_create(prog_fd, if_index, BPF_XDP, None, flags.bits).map_err(
-                |(_, io_error)| ProgramError::SyscallError {
+            let link_fd = bpf_link_create(prog_fd, if_index, BPF_XDP, None, None, flags.bits)
+                .map_err(|(_, io_error)| ProgramError::SyscallError {
                     call: "bpf_link_create".to_owned(),
                     io_error,
-                },
-            )? as RawFd;
+                })? as RawFd;
             self.data
                 .links
                 .insert(XdpLink(XdpLinkInner::FdLink(FdLink::new(link_fd))))
diff --git a/aya/src/sys/bpf.rs b/aya/src/sys/bpf.rs
index f9f2bbd6..4d8101ad 100644
--- a/aya/src/sys/bpf.rs
+++ b/aya/src/sys/bpf.rs
@@ -327,6 +327,7 @@ pub(crate) fn bpf_link_create(
     target_fd: RawFd,
     attach_type: bpf_attach_type,
     btf_id: Option<u32>,
+    cookie: Option<u64>,
     flags: u32,
 ) -> SysResult {
     let mut attr = unsafe { mem::zeroed::<bpf_attr>() };
@@ -338,6 +339,9 @@ pub(crate) fn bpf_link_create(
     if let Some(btf_id) = btf_id {
         attr.link_create.__bindgen_anon_2.target_btf_id = btf_id;
     }
+    if let Some(cookie) = cookie {
+        attr.link_create.__bindgen_anon_2.perf_event.bpf_cookie = cookie;
+    }
 
     sys_bpf(bpf_cmd::BPF_LINK_CREATE, &attr)
 }
@@ -552,9 +556,14 @@ pub(crate) fn is_perf_link_supported() -> bool {
     u.prog_type = bpf_prog_type::BPF_PROG_TYPE_TRACEPOINT as u32;
 
     if let Ok(fd) = sys_bpf(bpf_cmd::BPF_PROG_LOAD, &attr) {
-        if let Err((code, _)) =
-            bpf_link_create(fd as i32, -1, bpf_attach_type::BPF_PERF_EVENT, None, 0)
-        {
+        if let Err((code, _)) = bpf_link_create(
+            fd as i32,
+            -1,
+            bpf_attach_type::BPF_PERF_EVENT,
+            None,
+            None,
+            0,
+        ) {
             if code == (-libc::EBADF).into() {
                 unsafe { libc::close(fd as i32) };
                 return true;
diff --git a/aya/src/sys/perf_event.rs b/aya/src/sys/perf_event.rs
index 346fea2b..c6691269 100644
--- a/aya/src/sys/perf_event.rs
+++ b/aya/src/sys/perf_event.rs
@@ -67,6 +67,7 @@ pub(crate) fn perf_event_open_probe(
     name: &str,
     offset: u64,
     pid: Option<pid_t>,
+    ref_cnt_offset: Option<u64>,
 ) -> SysResult {
     let mut attr = unsafe { mem::zeroed::<perf_event_attr>() };
 
@@ -74,6 +75,10 @@ pub(crate) fn perf_event_open_probe(
         attr.config = 1 << ret_bit;
     }
 
+    if let Some(ref_cnt_offset) = ref_cnt_offset {
+        attr.config |= ref_cnt_offset << 32;
+    }
+
     let c_name = CString::new(name).unwrap();
 
     attr.size = mem::size_of::<perf_event_attr>() as u32;
diff --git a/bpf/aya-bpf-bindings/Cargo.toml b/bpf/aya-bpf-bindings/Cargo.toml
index f853cb3c..4588e895 100644
--- a/bpf/aya-bpf-bindings/Cargo.toml
+++ b/bpf/aya-bpf-bindings/Cargo.toml
@@ -2,7 +2,7 @@
 name = "aya-bpf-bindings"
 version = "0.1.0"
 authors = ["Alessandro Decina <alessandro.d@gmail.com>"]
-edition = "2018"
+edition = "2021"
 
 [dependencies]
 aya-bpf-cty = { path = "../aya-bpf-cty" }
diff --git a/bpf/aya-bpf/Cargo.toml b/bpf/aya-bpf/Cargo.toml
index c28551c2..76efa22d 100644
--- a/bpf/aya-bpf/Cargo.toml
+++ b/bpf/aya-bpf/Cargo.toml
@@ -2,7 +2,7 @@
 name = "aya-bpf"
 version = "0.1.0"
 authors = ["Alessandro Decina <alessandro.d@gmail.com>"]
-edition = "2018"
+edition = "2021"
 
 [dependencies]
 aya-bpf-cty = { path = "../aya-bpf-cty" }