diff --git a/aya/src/programs/mod.rs b/aya/src/programs/mod.rs
index 14a63f0b..cb2019a3 100644
--- a/aya/src/programs/mod.rs
+++ b/aya/src/programs/mod.rs
@@ -71,6 +71,7 @@ use std::{
     io,
     os::unix::io::{AsRawFd, RawFd},
     path::{Path, PathBuf},
+    time::{Duration, SystemTime, UNIX_EPOCH},
 };
 use thiserror::Error;
 
@@ -108,6 +109,7 @@ use crate::{
     maps::MapError,
     obj::{self, btf::BtfError, Function},
     pin::PinError,
+    programs::utils::{get_fdinfo, time_since_boot, time_since_epoch},
     sys::{
         bpf_btf_get_fd_by_id, bpf_get_object, bpf_load_program, bpf_pin_object,
         bpf_prog_get_fd_by_id, bpf_prog_get_info_by_fd, bpf_prog_get_next_id, bpf_prog_query,
@@ -937,16 +939,97 @@ impl ProgramInfo {
         unsafe { std::slice::from_raw_parts(self.0.name.as_ptr() as *const _, length) }
     }
 
-    /// The name of the program as a &str. If the name was not valid unicode, None is returned
+    /// The name of the program as a &str. If the name was not valid unicode, None is returned.
     pub fn name_as_str(&self) -> Option<&str> {
         std::str::from_utf8(self.name()).ok()
     }
 
-    /// The program id for this program. Each program has a unique id.
+    /// The id for this program. Each program has a unique id.
     pub fn id(&self) -> u32 {
         self.0.id
     }
 
+    /// The program tag is a SHA sum of the program's instructions which can be
+    /// used as an additional program identifier. A program's id can vary every time
+    /// it's loaded or unloaded, but the tag will remain the same.
+    pub fn tag(&self) -> u64 {
+        u64::from_be_bytes(self.0.tag)
+    }
+
+    /// The type of a bpf program expressed as an integer.  To understand the
+    /// integer to type mappings please see the linux kernel enum
+    /// [`bpf_prog_type`](https://elixir.bootlin.com/linux/v6.4.4/source/include/uapi/linux/bpf.h#L948).
+    pub fn program_type(&self) -> u32 {
+        self.0.type_
+    }
+
+    /// Returns true if the program is defined with a GPL-compatible license.
+    pub fn gpl_compatible(&self) -> bool {
+        self.0.gpl_compatible() != 0
+    }
+
+    /// Returns the ids of the maps maps used by the program.
+    pub fn map_ids(&self) -> Result<Vec<u32>, ProgramError> {
+        let fd = self.fd()?;
+
+        let len = self.0.nr_map_ids;
+        let mut map_ids = vec![0u32; len as usize];
+
+        bpf_prog_get_info_by_fd(fd, Some(&mut map_ids)).map_err(|io_error| {
+            ProgramError::SyscallError {
+                call: "bpf_prog_get_info_by_fd",
+                io_error,
+            }
+        })?;
+
+        unsafe { libc::close(fd) };
+
+        Ok(map_ids)
+    }
+
+    /// The btf id for the program.
+    pub fn btf_id(&self) -> u32 {
+        self.0.btf_id
+    }
+
+    /// The size in bytes of the program's translated eBPF bytecode.
+    pub fn bytes_xlated(&self) -> u32 {
+        self.0.xlated_prog_len
+    }
+
+    /// The size in bytes of the program's JIT-compiled machine code.
+    pub fn bytes_jited(&self) -> u32 {
+        self.0.jited_prog_len
+    }
+
+    /// How much memory in bytes has been allocated and locked for the program.
+    pub fn bytes_memlock(&self) -> Result<u32, ProgramError> {
+        let fd = self.fd()?;
+
+        let mem = get_fdinfo(fd, "memlock");
+        unsafe { libc::close(fd) };
+
+        mem
+    }
+
+    /// The number of verified instructions in the program.
+    pub fn verified_insns(&self) -> u32 {
+        self.0.verified_insns
+    }
+
+    /// The time the program was loaded.
+    ///
+    /// The load time is specified by the kernel as nanoseconds since system boot,
+    /// this function converts that u64 value to a [`std::time::SystemTime`]
+    /// for easy consumption.  It is calculated by first finding the realtime of system
+    /// boot (i.e the [`Duration`] since [`std::time::UNIX_EPOCH`] minus the [`Duration`]
+    /// since boot), adding the load_time [`Duration`], and finally converting the
+    /// [`Duration`] to a readable [`SystemTime`] by adding to [`std::time::UNIX_EPOCH`].
+    pub fn loaded_at(&self) -> SystemTime {
+        UNIX_EPOCH
+            + ((time_since_epoch() - time_since_boot()) + Duration::from_nanos(self.0.load_time))
+    }
+
     /// Returns the fd associated with the program.
     ///
     /// The returned fd must be closed when no longer needed.
diff --git a/aya/src/programs/utils.rs b/aya/src/programs/utils.rs
index 33b84095..e088c797 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, io, os::unix::io::RawFd, path::Path};
+use std::{
+    ffi::CStr,
+    fs::File,
+    io,
+    io::{BufRead, BufReader},
+    os::unix::io::RawFd,
+    path::Path,
+    time::Duration,
+};
+
+// for docs link
+#[allow(unused)]
+use std::time::UNIX_EPOCH;
 
 use crate::{
     programs::{FdLink, Link, ProgramData, ProgramError},
@@ -23,7 +35,7 @@ pub(crate) fn attach_raw_tracepoint<T: Link + From<FdLink>>(
     program_data.links.insert(FdLink::new(pfd).into())
 }
 
-/// Find tracefs filesystem path
+/// Find tracefs filesystem path.
 pub(crate) fn find_tracefs_path() -> Result<&'static Path, ProgramError> {
     lazy_static::lazy_static! {
         static ref TRACE_FS: Option<&'static Path> = {
@@ -51,3 +63,49 @@ pub(crate) fn find_tracefs_path() -> Result<&'static Path, ProgramError> {
         .as_deref()
         .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "tracefs not found").into())
 }
+
+/// Get time since boot. The returned duration represents the combined
+/// seconds and nanoseconds since the machine was booted.
+pub(crate) fn time_since_boot() -> Duration {
+    let mut time = unsafe { std::mem::zeroed::<libc::timespec>() };
+
+    let ret = unsafe { libc::clock_gettime(libc::CLOCK_BOOTTIME, &mut time) };
+    assert_eq!(ret, 0, "failed to get system bootime");
+    let tv_sec = time.tv_sec as u64;
+    let tv_nsec = time.tv_nsec as u32;
+    Duration::new(tv_sec, tv_nsec)
+}
+
+/// Get the system-wide real (wall-clock) time. The returned Duration represents
+/// the combined seconds and nanoseconds since [`std::time::UNIX_EPOCH`].
+pub(crate) fn time_since_epoch() -> Duration {
+    let mut time = unsafe { std::mem::zeroed::<libc::timespec>() };
+
+    let ret = unsafe { libc::clock_gettime(libc::CLOCK_REALTIME, &mut time) };
+    assert_eq!(ret, 0, "failed to get system realtime");
+    let tv_sec = time.tv_sec as u64;
+    let tv_nsec = time.tv_nsec as u32;
+    Duration::new(tv_sec, tv_nsec)
+}
+
+/// Get the specified information from a file descriptor's fdinfo.
+pub(crate) fn get_fdinfo(fd: RawFd, key: &str) -> Result<u32, ProgramError> {
+    let info = File::open(format!("/proc/self/fdinfo/{}", fd))?;
+    let reader = BufReader::new(info);
+    for line in reader.lines() {
+        match line {
+            Ok(l) => {
+                if !l.contains(key) {
+                    continue;
+                }
+
+                let val = l.rsplit_once('\t').unwrap().1;
+
+                return Ok(val.parse().unwrap());
+            }
+            Err(e) => return Err(ProgramError::IOError(e)),
+        }
+    }
+
+    Ok(0)
+}