From ae863bc663bde69eb71d5c1ec265b0d4c205f7ff Mon Sep 17 00:00:00 2001 From: Alessandro Decina Date: Sun, 21 Mar 2021 07:21:16 +0000 Subject: [PATCH] aya: split aya::programs::probe into ::kprobe and ::uprobe & add docs --- aya/src/bpf.rs | 6 +- aya/src/obj/btf/btf.rs | 10 ++ aya/src/programs/kprobe.rs | 54 +++++++ aya/src/programs/mod.rs | 85 +++++++++-- aya/src/programs/probe.rs | 300 +------------------------------------ aya/src/programs/uprobe.rs | 266 ++++++++++++++++++++++++++++++++ 6 files changed, 413 insertions(+), 308 deletions(-) create mode 100644 aya/src/programs/kprobe.rs create mode 100644 aya/src/programs/uprobe.rs diff --git a/aya/src/bpf.rs b/aya/src/bpf.rs index 91ccb2d9..0005f32a 100644 --- a/aya/src/bpf.rs +++ b/aya/src/bpf.rs @@ -19,8 +19,8 @@ use crate::{ Object, ParseError, ProgramKind, }, programs::{ - probe::ProbeKind, KProbe, Program, ProgramData, ProgramError, SocketFilter, TracePoint, - UProbe, Xdp, + KProbe, ProbeKind, Program, ProgramData, ProgramError, SocketFilter, TracePoint, UProbe, + Xdp, }, sys::bpf_map_update_elem_ptr, util::{possible_cpus, POSSIBLE_CPUS}, @@ -55,7 +55,7 @@ pub(crate) struct bpf_map_def { pub(crate) map_flags: u32, } -/// Used to work with eBPF programs and maps. +/// The main entry point into the library, used to work with eBPF programs and maps. #[derive(Debug)] pub struct Bpf { maps: HashMap, diff --git a/aya/src/obj/btf/btf.rs b/aya/src/obj/btf/btf.rs index 059aca45..02e6cbde 100644 --- a/aya/src/obj/btf/btf.rs +++ b/aya/src/obj/btf/btf.rs @@ -69,6 +69,14 @@ pub enum BtfError { MaximumTypeDepthReached { type_id: u32 }, } +/// Bpf Type Format (BTF) metadata. +/// +/// BTF is a kind of debug metadata that allows eBPF programs compiled against one kernel version +/// to be loaded into different kernel versions. +/// +/// Aya automatically loads BTF metadata if you use [`Bpf::load_file`](crate::Bpf::load_file). You +/// only need to explicitly use this type if you want to load BTF from a non-standard +/// location or if you are using [`Bpf::load`](crate::Bpf::load). #[derive(Clone, Debug)] pub struct Btf { header: btf_header, @@ -78,10 +86,12 @@ pub struct Btf { } impl Btf { + /// Loads BTF metadata from `/sys/kernel/btf/vmlinux`. pub fn from_sys_fs() -> Result { Btf::parse_file("/sys/kernel/btf/vmlinux", Endianness::default()) } + /// Loads BTF metadata from the given `path`. pub fn parse_file>(path: P, endianness: Endianness) -> Result { let path = path.as_ref(); Btf::parse( diff --git a/aya/src/programs/kprobe.rs b/aya/src/programs/kprobe.rs new file mode 100644 index 00000000..7a6709e5 --- /dev/null +++ b/aya/src/programs/kprobe.rs @@ -0,0 +1,54 @@ +//! Kernel space probes. +//! +//! Kernel probes are eBPF programs that can be attached to almost any function inside the kernel. +use libc::pid_t; +use std::io; +use thiserror::Error; + +use crate::{ + generated::bpf_prog_type::BPF_PROG_TYPE_KPROBE, + programs::{ + load_program, + probe::{attach, ProbeKind}, + LinkRef, ProgramData, ProgramError, + }, +}; + +/// A `kprobe` program. +#[derive(Debug)] +pub struct KProbe { + pub(crate) data: ProgramData, + pub(crate) kind: ProbeKind, +} + +impl KProbe { + /// Loads the program inside the kernel. + /// + /// See also [`Program::load`](crate::programs::Program::load). + pub fn load(&mut self) -> Result<(), ProgramError> { + load_program(BPF_PROG_TYPE_KPROBE, &mut self.data) + } + + pub fn name(&self) -> String { + self.data.name.to_string() + } + + pub fn attach( + &mut self, + fn_name: &str, + offset: u64, + pid: Option, + ) -> Result { + attach(&mut self.data, self.kind, fn_name, offset, pid) + } +} + +#[derive(Debug, Error)] +pub enum KProbeError { + #[error("`{filename}`")] + FileError { + filename: String, + #[source] + io_error: io::Error, + }, +} diff --git a/aya/src/programs/mod.rs b/aya/src/programs/mod.rs index 74c97df5..eb583495 100644 --- a/aya/src/programs/mod.rs +++ b/aya/src/programs/mod.rs @@ -1,18 +1,68 @@ +//! eBPF program types. +//! +//! eBPF programs are loaded inside the kernel and attached to one or more hook points. Whenever +//! the kernel or an application reaches those hook points, the programs are executed. +//! +//! # Loading programs +//! +//! When you call [`Bpf::load_file`] or [`Bpf::load`], all the programs present in the code are +//! parsed and can be retrieved using the [`Bpf::program`] and [`Bpf::program_mut`] methods. In +//! order to load a program, you need to get a handle to it and call the `load()` method, for +//! example: +//! +//! ```no_run +//! use aya::{Bpf, programs::KProbe}; +//! use std::convert::TryInto; +//! +//! let mut bpf = Bpf::load_file("ebpf_programs.o")?; +//! // intercept_wakeups is the name of the program we want to load +//! let program: &mut KProbe = bpf.program_mut("intercept_wakeups")?.try_into()?; +//! program.load()?; +//! # Ok::<(), aya::BpfError>(()) +//! ``` +//! +//! # Attaching programs +//! +//! After being loaded, programs must be attached to their target hook points to be executed. The +//! eBPF platform supports many different program types, with each type providing different +//! attachment options. For example when attaching a [`KProbe`], you must provide the name of the +//! kernel function you want instrument; when loading an [`Xdp`] program, you need to specify the +//! network card name you want to hook into, and so forth. +//! +//! Currently aya supports [`KProbe`], [`UProbe`], [`SocketFilter`], [`TracePoint`] and [`Xdp`] +//! programs. To see how to attach them, see the documentation of the respective `attach()` method. +//! +//! # Interacting with programs +//! +//! eBPF programs are event-driven and execute when the hook points they are attached to are hit. +//! To communicate with user-space, programs use data structures provided by the eBPF platform, +//! which can be found in the [maps] module. +//! +//! [`Bpf::load_file`]: crate::Bpf::load_file +//! [`Bpf::load`]: crate::Bpf::load +//! [`Bpf::programs`]: crate::Bpf::programs +//! [`Bpf::program`]: crate::Bpf::program +//! [`Bpf::program_mut`]: crate::Bpf::program_mut +//! [maps]: crate::maps +mod kprobe; mod perf_attach; -pub mod probe; -pub mod socket_filter; -pub mod trace_point; -pub mod xdp; +mod probe; +mod socket_filter; +mod trace_point; +mod uprobe; +mod xdp; use libc::{close, ENOSPC}; use std::{cell::RefCell, cmp, convert::TryFrom, ffi::CStr, io, os::unix::io::RawFd, rc::Rc}; use thiserror::Error; +pub use kprobe::{KProbe, KProbeError}; use perf_attach::*; -pub use probe::{KProbe, KProbeError, UProbe, UProbeError}; +pub use probe::ProbeKind; pub use socket_filter::{SocketFilter, SocketFilterError}; pub use trace_point::{TracePoint, TracePointError}; -pub use xdp::{Xdp, XdpError}; +pub use uprobe::{UProbe, UProbeError}; +pub use xdp::{Xdp, XdpError, XdpFlags}; use crate::{ generated::bpf_prog_type, @@ -87,6 +137,7 @@ pub trait ProgramFd { fn fd(&self) -> Option; } +/// eBPF program type. #[derive(Debug)] pub enum Program { KProbe(KProbe), @@ -97,10 +148,21 @@ pub enum Program { } impl Program { + /// Loads the program in the kernel. + /// + /// # Errors + /// + /// If the load operation fails, the method returns + /// [`ProgramError::LoadError`] and the error's `verifier_log` field + /// contains the output from the kernel verifier. + /// + /// If the program is already loaded, [`ProgramError::AlreadyLoaded`] is + /// returned. pub fn load(&mut self) -> Result<(), ProgramError> { load_program(self.prog_type(), self.data_mut()) } + /// Returns the low level program type. pub fn prog_type(&self) -> bpf_prog_type { use crate::generated::bpf_prog_type::*; match self { @@ -112,7 +174,12 @@ impl Program { } } - pub(crate) fn data(&self) -> &ProgramData { + /// Returns the name of the program. + pub fn name(&self) -> &str { + &self.data().name + } + + fn data(&self) -> &ProgramData { match self { Program::KProbe(p) => &p.data, Program::UProbe(p) => &p.data, @@ -131,10 +198,6 @@ impl Program { Program::Xdp(p) => &mut p.data, } } - - pub fn name(&self) -> &str { - &self.data().name - } } #[derive(Debug)] diff --git a/aya/src/programs/probe.rs b/aya/src/programs/probe.rs index 9988683e..f973a422 100644 --- a/aya/src/programs/probe.rs +++ b/aya/src/programs/probe.rs @@ -1,167 +1,22 @@ use libc::pid_t; -use object::{Object, ObjectSymbol}; -use std::{ - error::Error, - ffi::CStr, - fs, - io::{self, BufRead, Cursor, Read}, - mem, - os::raw::c_char, - path::{Path, PathBuf}, - sync::Arc, -}; -use thiserror::Error; +use std::{fs, io}; use crate::{ - generated::bpf_prog_type::BPF_PROG_TYPE_KPROBE, - programs::{load_program, perf_attach, LinkRef, ProgramData, ProgramError}, + programs::{ + kprobe::KProbeError, perf_attach, uprobe::UProbeError, LinkRef, ProgramData, ProgramError, + }, sys::perf_event_open_probe, }; -const LD_SO_CACHE_FILE: &str = "/etc/ld.so.cache"; - -lazy_static! { - static ref LD_SO_CACHE: Result> = - LdSoCache::load(LD_SO_CACHE_FILE).map_err(Arc::new); -} -const LD_SO_CACHE_HEADER: &str = "glibc-ld.so.cache1.1"; - -#[derive(Debug, Error)] -pub enum KProbeError { - #[error("`{filename}`")] - FileError { - filename: String, - #[source] - io_error: io::Error, - }, -} - -#[derive(Debug, Error)] -pub enum UProbeError { - #[error("error reading `{}` file", LD_SO_CACHE_FILE)] - InvalidLdSoCache { - #[source] - io_error: Arc, - }, - - #[error("could not resolve uprobe target `{path}`")] - InvalidTarget { path: PathBuf }, - - #[error("error resolving symbol")] - SymbolError { - symbol: String, - #[source] - error: Box, - }, - - #[error("`{filename}`")] - FileError { - filename: String, - #[source] - io_error: io::Error, - }, -} - -#[derive(Debug)] -pub struct KProbe { - pub(crate) data: ProgramData, - pub(crate) kind: ProbeKind, -} - -#[derive(Debug)] -pub struct UProbe { - pub(crate) data: ProgramData, - pub(crate) kind: ProbeKind, -} - -impl KProbe { - pub fn load(&mut self) -> Result<(), ProgramError> { - load_program(BPF_PROG_TYPE_KPROBE, &mut self.data) - } - - pub fn name(&self) -> String { - self.data.name.to_string() - } - - pub fn attach( - &mut self, - fn_name: &str, - offset: u64, - pid: Option, - ) -> Result { - attach(&mut self.data, self.kind, fn_name, offset, pid) - } -} - -impl UProbe { - pub fn load(&mut self) -> Result<(), ProgramError> { - load_program(BPF_PROG_TYPE_KPROBE, &mut self.data) - } - - pub fn name(&self) -> String { - self.data.name.to_string() - } - - pub fn attach>( - &mut self, - fn_name: Option<&str>, - offset: u64, - target: T, - pid: Option, - ) -> Result { - let target = target.as_ref(); - let target_str = &*target.as_os_str().to_string_lossy(); - - let mut path = if let Some(pid) = pid { - find_lib_in_proc_maps(pid, &target_str).map_err(|io_error| UProbeError::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| UProbeError::InvalidLdSoCache { - io_error: error.clone(), - })?; - cache.resolve(target_str) - } - .map(String::from) - }; - - let path = path.ok_or(UProbeError::InvalidTarget { - path: target.to_owned(), - })?; - - let sym_offset = if let Some(fn_name) = fn_name { - resolve_symbol(&path, fn_name).map_err(|error| UProbeError::SymbolError { - symbol: fn_name.to_string(), - error: Box::new(error), - })? - } else { - 0 - }; - - attach(&mut self.data, self.kind, &path, sym_offset + offset, pid) - } -} - #[derive(Debug, Copy, Clone)] -pub(crate) enum ProbeKind { +pub enum ProbeKind { KProbe, KRetProbe, UProbe, URetProbe, } -fn attach( +pub(crate) fn attach( program_data: &mut ProgramData, kind: ProbeKind, name: &str, @@ -196,149 +51,6 @@ fn attach( perf_attach(program_data, fd) } -fn proc_maps_libs(pid: pid_t) -> Result, io::Error> { - let maps_file = format!("/proc/{}/maps", pid); - let data = fs::read_to_string(maps_file)?; - - Ok(data - .lines() - .filter_map(|line| { - let line = line.split_whitespace().last()?; - if line.starts_with('/') { - let path = PathBuf::from(line); - let key = path.file_name().unwrap().to_string_lossy().into_owned(); - Some((key, path.to_string_lossy().to_string())) - } else { - None - } - }) - .collect()) -} - -fn find_lib_in_proc_maps(pid: pid_t, lib: &str) -> Result, io::Error> { - let libs = proc_maps_libs(pid)?; - - let ret = if lib.contains(".so") { - libs.iter().find(|(k, _)| k.as_str().starts_with(lib)) - } else { - let lib = lib.to_string(); - let lib1 = lib.clone() + ".so"; - let lib2 = lib + "-"; - libs.iter() - .find(|(k, _)| k.starts_with(&lib1) || k.starts_with(&lib2)) - }; - - Ok(ret.map(|(_, v)| v.clone())) -} - -#[derive(Debug)] -pub(crate) struct CacheEntry { - key: String, - value: String, - flags: i32, -} - -#[derive(Debug)] -pub(crate) struct LdSoCache { - entries: Vec, -} - -impl LdSoCache { - pub fn load>(path: T) -> Result { - let data = fs::read(path)?; - Self::parse(&data) - } - - fn parse(data: &[u8]) -> Result { - let mut cursor = Cursor::new(data); - - let read_u32 = |cursor: &mut Cursor<_>| -> Result { - let mut buf = [0u8; mem::size_of::()]; - cursor.read_exact(&mut buf)?; - - Ok(u32::from_ne_bytes(buf)) - }; - - let read_i32 = |cursor: &mut Cursor<_>| -> Result { - let mut buf = [0u8; mem::size_of::()]; - 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).or(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::()); - - 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 }); - } - - 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 { - 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())) -} - fn read_sys_fs_perf_type(pmu: &str) -> Result { let file = format!("/sys/bus/event_source/devices/{}/type", pmu); diff --git a/aya/src/programs/uprobe.rs b/aya/src/programs/uprobe.rs new file mode 100644 index 00000000..6e51907e --- /dev/null +++ b/aya/src/programs/uprobe.rs @@ -0,0 +1,266 @@ +//! User space probes. +use libc::pid_t; +use object::{Object, ObjectSymbol}; +use std::{ + error::Error, + ffi::CStr, + fs, + io::{self, BufRead, Cursor, Read}, + mem, + os::raw::c_char, + path::{Path, PathBuf}, + sync::Arc, +}; +use thiserror::Error; + +use crate::{ + generated::bpf_prog_type::BPF_PROG_TYPE_KPROBE, + programs::{ + load_program, + probe::{attach, ProbeKind}, + LinkRef, ProgramData, ProgramError, + }, +}; + +const LD_SO_CACHE_FILE: &str = "/etc/ld.so.cache"; + +lazy_static! { + static ref LD_SO_CACHE: Result> = + LdSoCache::load(LD_SO_CACHE_FILE).map_err(Arc::new); +} +const LD_SO_CACHE_HEADER: &str = "glibc-ld.so.cache1.1"; + +#[derive(Debug)] +pub struct UProbe { + pub(crate) data: ProgramData, + pub(crate) kind: ProbeKind, +} + +impl UProbe { + pub fn load(&mut self) -> Result<(), ProgramError> { + load_program(BPF_PROG_TYPE_KPROBE, &mut self.data) + } + + pub fn name(&self) -> String { + self.data.name.to_string() + } + + pub fn attach>( + &mut self, + fn_name: Option<&str>, + offset: u64, + target: T, + pid: Option, + ) -> Result { + let target = target.as_ref(); + let target_str = &*target.as_os_str().to_string_lossy(); + + let mut path = if let Some(pid) = pid { + find_lib_in_proc_maps(pid, &target_str).map_err(|io_error| UProbeError::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| UProbeError::InvalidLdSoCache { + io_error: error.clone(), + })?; + cache.resolve(target_str) + } + .map(String::from) + }; + + let path = path.ok_or(UProbeError::InvalidTarget { + path: target.to_owned(), + })?; + + let sym_offset = if let Some(fn_name) = fn_name { + resolve_symbol(&path, fn_name).map_err(|error| UProbeError::SymbolError { + symbol: fn_name.to_string(), + error: Box::new(error), + })? + } else { + 0 + }; + + attach(&mut self.data, self.kind, &path, sym_offset + offset, pid) + } +} + +#[derive(Debug, Error)] +pub enum UProbeError { + #[error("error reading `{}` file", LD_SO_CACHE_FILE)] + InvalidLdSoCache { + #[source] + io_error: Arc, + }, + + #[error("could not resolve uprobe target `{path}`")] + InvalidTarget { path: PathBuf }, + + #[error("error resolving symbol")] + SymbolError { + symbol: String, + #[source] + error: Box, + }, + + #[error("`{filename}`")] + FileError { + filename: String, + #[source] + io_error: io::Error, + }, +} + +fn proc_maps_libs(pid: pid_t) -> Result, io::Error> { + let maps_file = format!("/proc/{}/maps", pid); + let data = fs::read_to_string(maps_file)?; + + Ok(data + .lines() + .filter_map(|line| { + let line = line.split_whitespace().last()?; + if line.starts_with('/') { + let path = PathBuf::from(line); + let key = path.file_name().unwrap().to_string_lossy().into_owned(); + Some((key, path.to_string_lossy().to_string())) + } else { + None + } + }) + .collect()) +} + +fn find_lib_in_proc_maps(pid: pid_t, lib: &str) -> Result, io::Error> { + let libs = proc_maps_libs(pid)?; + + let ret = if lib.contains(".so") { + libs.iter().find(|(k, _)| k.as_str().starts_with(lib)) + } else { + let lib = lib.to_string(); + let lib1 = lib.clone() + ".so"; + let lib2 = lib + "-"; + libs.iter() + .find(|(k, _)| k.starts_with(&lib1) || k.starts_with(&lib2)) + }; + + Ok(ret.map(|(_, v)| v.clone())) +} + +#[derive(Debug)] +pub(crate) struct CacheEntry { + key: String, + value: String, + flags: i32, +} + +#[derive(Debug)] +pub(crate) struct LdSoCache { + entries: Vec, +} + +impl LdSoCache { + pub fn load>(path: T) -> Result { + let data = fs::read(path)?; + Self::parse(&data) + } + + fn parse(data: &[u8]) -> Result { + let mut cursor = Cursor::new(data); + + let read_u32 = |cursor: &mut Cursor<_>| -> Result { + let mut buf = [0u8; mem::size_of::()]; + cursor.read_exact(&mut buf)?; + + Ok(u32::from_ne_bytes(buf)) + }; + + let read_i32 = |cursor: &mut Cursor<_>| -> Result { + let mut buf = [0u8; mem::size_of::()]; + 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).or(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::()); + + 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 }); + } + + 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 { + 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())) +}