diff --git a/Cargo.toml b/Cargo.toml index 19a0182..221c3d5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,8 @@ libc = "0.2.80" lazy_static = "1.4.0" tempfile = "3.1.0" phdrs = { git = "https://github.com/softdevteam/phdrs" } +strum = { version = "0.24.1", features = ["derive", "strum_macros"] } +strum_macros = "0.24.3" [build-dependencies] cc = "1.0.62" diff --git a/build.rs b/build.rs index 5e3af7e..7f0919d 100644 --- a/build.rs +++ b/build.rs @@ -97,14 +97,19 @@ fn main() { let c_deps_dir = make_c_deps_dir(); let c_deps_dir_s = c_deps_dir.display(); + c_build.file("src/util.c"); - // Check if we should build the perf_pt backend. + // Check if we should build the perf collector. if cfg!(all(target_os = "linux", target_arch = "x86_64")) - && feature_check("check_perf_pt.c", "check_perf_pt") + && feature_check("check_perf.c", "check_perf") { - c_build.file("src/backends/perf_pt/collect.c"); - c_build.file("src/backends/perf_pt/decode.c"); - c_build.file("src/backends/perf_pt/util.c"); + c_build.file("src/collect/perf/collect.c"); + println!("cargo:rustc-cfg=collector_perf"); + } + + // FIXME: libipt support is unconditionally built-in for now. + if cfg!(all(target_os = "linux", target_arch = "x86_64")) { + c_build.file("src/decode/libipt/decode.c"); // Decide whether to build our own libipt. if let Ok(val) = env::var("IPT_PATH") { @@ -133,13 +138,14 @@ fn main() { c_deps_dir_s )); - println!("cargo:rustc-cfg=perf_pt"); + println!("cargo:rustc-cfg=decoder_libipt"); if cpu_supports_pt() { - println!("cargo:rustc-cfg=perf_pt_test"); + println!("cargo:rustc-cfg=decoder_libipt_test"); } println!("cargo:rustc-link-lib=static=ipt"); } c_build.include("src/util"); + c_build.include("src"); // to find `hwtracer_private.h`. c_build.compile("hwtracer_c"); // Additional circumstances under which to re-run this build.rs. diff --git a/deny.toml b/deny.toml index 02dda1b..85a95a6 100644 --- a/deny.toml +++ b/deny.toml @@ -3,7 +3,8 @@ unlicensed = "deny" confidence-threshold = 1.0 allow = [ "Apache-2.0", + "ISC", "MIT", "BSD-3-Clause", - "ISC", + "Unicode-DFS-2016", ] diff --git a/examples/simple_example.rs b/examples/simple_example.rs index a3116b2..94d63cf 100644 --- a/examples/simple_example.rs +++ b/examples/simple_example.rs @@ -1,14 +1,15 @@ -use hwtracer::backends::TracerBuilder; use hwtracer::Trace; +use hwtracer::{collect::TraceCollectorBuilder, decode::TraceDecoderBuilder}; use std::time::SystemTime; /// Prints the addresses of the first `qty` blocks in a trace along with it's name and /// computation result. -fn print_trace(trace: &Box, name: &str, result: u32, qty: usize) { - let count = trace.iter_blocks().count(); +fn print_trace(trace: Box, name: &str, result: u32, qty: usize) { + let dec = TraceDecoderBuilder::new().build().unwrap(); + let count = dec.iter_blocks(&*trace).count(); println!("{}: num_blocks={}, result={}", name, count, result); - for (i, blk) in trace.iter_blocks().take(qty).enumerate() { + for (i, blk) in dec.iter_blocks(&*trace).take(qty).enumerate() { println!(" block {}: 0x{:x}", i, blk.unwrap().first_instr()); } if count > qty { @@ -29,21 +30,20 @@ fn work() -> u32 { res } -/// Trace a simple computation loop. +/// Collect and decode a trace for a simple computation loop. /// /// The results are printed to discourage the compiler from optimising the computation out. fn main() { - let mut bldr = TracerBuilder::new(); - println!("Backend configuration: {:?}", bldr.config()); - let mut thr_tracer = bldr.build().unwrap().thread_tracer(); + let bldr = TraceCollectorBuilder::new(); + let mut thr_col = bldr.build().unwrap().thread_collector(); for i in 1..4 { - thr_tracer.start_tracing().unwrap_or_else(|e| { - panic!("Failed to start tracer: {}", e); + thr_col.start_collector().unwrap_or_else(|e| { + panic!("Failed to start collector: {}", e); }); let res = work(); - let trace = thr_tracer.stop_tracing().unwrap(); + let trace = thr_col.stop_collector().unwrap(); let name = format!("trace{}", i); - print_trace(&trace, &name, res, 10); + print_trace(trace, &name, res, 10); } } diff --git a/feature_checks/check_perf.c b/feature_checks/check_perf.c new file mode 100644 index 0000000..630388a --- /dev/null +++ b/feature_checks/check_perf.c @@ -0,0 +1,8 @@ +#include + +int +check(void) +{ + // The perf configuration struct version that first supported Intel PT. + return PERF_ATTR_SIZE_VER5; +} diff --git a/src/backends/dummy.rs b/src/backends/dummy.rs deleted file mode 100644 index c611a7c..0000000 --- a/src/backends/dummy.rs +++ /dev/null @@ -1,121 +0,0 @@ -use crate::errors::HWTracerError; -use crate::{Block, ThreadTracer, Trace, Tracer}; -#[cfg(test)] -use std::fs::File; -use std::iter::Iterator; - -/// An empty dummy trace. -#[derive(Debug)] -struct DummyTrace {} - -impl Trace for DummyTrace { - #[cfg(test)] - fn to_file(&self, _: &mut File) {} - - fn iter_blocks<'t: 'i, 'i>( - &'t self, - ) -> Box> + 'i> { - Box::new(DummyBlockIterator {}) - } - - #[cfg(test)] - fn capacity(&self) -> usize { - 0 - } -} - -#[derive(Debug)] -pub struct DummyTracer {} - -impl DummyTracer { - pub(super) fn new() -> Self { - DummyTracer {} - } -} - -impl Tracer for DummyTracer { - fn thread_tracer(&self) -> Box { - Box::new(DummyThreadTracer::new()) - } -} - -/// A tracer which doesn't really do anything. -pub struct DummyThreadTracer { - // Keeps track of the state of the tracer. - is_tracing: bool, -} - -impl DummyThreadTracer { - /// Create a dummy tracer. - fn new() -> Self { - Self { is_tracing: false } - } -} - -impl ThreadTracer for DummyThreadTracer { - fn start_tracing(&mut self) -> Result<(), HWTracerError> { - if self.is_tracing { - return Err(HWTracerError::AlreadyTracing); - } - self.is_tracing = true; - Ok(()) - } - - fn stop_tracing(&mut self) -> Result, HWTracerError> { - if !self.is_tracing { - return Err(HWTracerError::AlreadyStopped); - } - self.is_tracing = false; - Ok(Box::new(DummyTrace {})) - } -} - -// Iterate over the blocks of a DummyTrace. -// -// Note: there will never be any blocks, but we have to implement the interface. -struct DummyBlockIterator {} - -impl Iterator for DummyBlockIterator { - type Item = Result; - - fn next(&mut self) -> Option { - None - } -} - -#[cfg(test)] -mod tests { - use super::DummyThreadTracer; - use crate::{test_helpers, ThreadTracer}; - - #[test] - fn test_basic_usage() { - test_helpers::test_basic_usage(DummyThreadTracer::new()); - } - - #[test] - fn test_repeated_tracing() { - test_helpers::test_repeated_tracing(DummyThreadTracer::new()); - } - - #[test] - fn test_already_started() { - test_helpers::test_already_started(DummyThreadTracer::new()); - } - - #[test] - fn test_not_started() { - test_helpers::test_not_started(DummyThreadTracer::new()); - } - - #[test] - fn test_block_iterator() { - let mut tracer = DummyThreadTracer::new(); - tracer.start_tracing().unwrap(); - let trace = tracer.stop_tracing().unwrap(); - - // We expect exactly 0 blocks. - let expects = Vec::new(); - test_helpers::test_expected_blocks(trace, expects.iter()); - } -} diff --git a/src/backends/mod.rs b/src/backends/mod.rs deleted file mode 100644 index b465bdf..0000000 --- a/src/backends/mod.rs +++ /dev/null @@ -1,230 +0,0 @@ -use crate::backends::dummy::DummyTracer; -use crate::errors::HWTracerError; -use crate::Tracer; - -#[cfg(perf_pt)] -pub mod perf_pt; -#[cfg(perf_pt)] -use crate::backends::perf_pt::PerfPTTracer; -#[cfg(perf_pt)] -use core::arch::x86_64::__cpuid_count; -use libc::size_t; -pub mod dummy; - -#[derive(Debug)] -pub enum BackendKind { - Dummy, - PerfPT, -} - -const PERF_PT_DFLT_DATA_BUFSIZE: size_t = 64; -const PERF_PT_DFLT_AUX_BUFSIZE: size_t = 1024; -const PERF_PT_DFLT_INITIAL_TRACE_BUFSIZE: size_t = 1024 * 1024; // 1MiB - -impl BackendKind { - // Finds a suitable `BackendKind` for the current hardware/OS. - fn default_platform_backend() -> BackendKind { - let tr_kinds = vec![BackendKind::PerfPT, BackendKind::Dummy]; - for kind in tr_kinds { - if Self::match_platform(&kind).is_ok() { - return kind; - } - } - // The Dummy backend should always be usable. - unreachable!(); - } - - /// Returns `Ok` if the this backend is appropriate for the current platform. - fn match_platform(&self) -> Result<(), HWTracerError> { - match self { - BackendKind::Dummy => Ok(()), - BackendKind::PerfPT => { - #[cfg(not(perf_pt))] - return Err(HWTracerError::BackendUnavailable(BackendKind::PerfPT)); - #[cfg(perf_pt)] - { - if !Self::pt_supported() { - return Err(HWTracerError::NoHWSupport( - "Intel PT not supported by CPU".into(), - )); - } - Ok(()) - } - } - } - } - - /// Checks if the CPU supports Intel Processor Trace. - #[cfg(perf_pt)] - fn pt_supported() -> bool { - let res = unsafe { __cpuid_count(0x7, 0x0) }; - (res.ebx & (1 << 25)) != 0 - } -} - -/// Generic configuration interface for all backends. -/// If a field is `None` at `build()` time then the backend will select a default value. Ant -/// attributes which don't apply to a given backend are also checked. -#[derive(Debug)] -pub enum BackendConfig { - Dummy, - PerfPT(PerfPTConfig), -} - -/// Configures the PerfPT backend. -/// -// Must stay in sync with the C code. -#[derive(Clone, Debug)] -#[repr(C)] -pub struct PerfPTConfig { - /// Data buffer size, in pages. Must be a power of 2. - pub data_bufsize: size_t, - /// AUX buffer size, in pages. Must be a power of 2. - pub aux_bufsize: size_t, - /// The initial trace storage buffer size (in bytes) of new traces. - pub initial_trace_bufsize: size_t, -} - -impl Default for PerfPTConfig { - fn default() -> Self { - Self { - data_bufsize: PERF_PT_DFLT_DATA_BUFSIZE, - aux_bufsize: PERF_PT_DFLT_AUX_BUFSIZE, - initial_trace_bufsize: PERF_PT_DFLT_INITIAL_TRACE_BUFSIZE, - } - } -} - -impl BackendConfig { - fn backend_kind(&self) -> BackendKind { - match self { - BackendConfig::Dummy => BackendKind::Dummy, - BackendConfig::PerfPT { .. } => BackendKind::PerfPT, - } - } -} - -/// A builder interface for instantiating `Tracer`s. -/// -/// # Make a tracer with an appropriate default backend and using default backend options. -/// ``` -/// use hwtracer::backends::TracerBuilder; -/// TracerBuilder::new().build().unwrap(); -/// ``` -/// -/// # Make a tracer using the PerfPT backend and using the default backend options. -/// ``` -/// use hwtracer::backends::TracerBuilder; -/// -/// let res = TracerBuilder::new().perf_pt().build(); -/// if let Ok(tracer) = res { -/// // Use the tracer... -/// } else { -/// // CPU doesn't support Intel Processor Trace. -/// } -/// ``` -/// -/// # Make a tracer with an appropriate default backend and using custom backend options if the PerfPT backend was chosen. -/// ``` -/// use hwtracer::backends::{TracerBuilder, BackendConfig}; -/// let mut bldr = TracerBuilder::new(); -/// if let BackendConfig::PerfPT(ref mut ppt_config) = bldr.config() { -/// ppt_config.aux_bufsize = 8192; -/// } -/// bldr.build().unwrap(); -/// ``` -pub struct TracerBuilder { - config: BackendConfig, -} - -impl TracerBuilder { - /// Create a new TracerBuilder using an appropriate default backend and configuration. - pub fn new() -> Self { - let config = match BackendKind::default_platform_backend() { - BackendKind::Dummy => BackendConfig::Dummy, - BackendKind::PerfPT => BackendConfig::PerfPT(PerfPTConfig::default()), - }; - Self { config } - } - - /// Choose to use the PerfPT backend wth default options. - pub fn perf_pt(mut self) -> Self { - self.config = BackendConfig::PerfPT(PerfPTConfig::default()); - self - } - - /// Choose to use the Dummy backend. - pub fn dummy(mut self) -> Self { - self.config = BackendConfig::Dummy; - self - } - - /// Get a mutable reference to the configuraion. - pub fn config(&mut self) -> &mut BackendConfig { - &mut self.config - } - - /// Build a tracer from the specified configuration. - /// An error is returned if the requested backend is inappropriate for the platform or the - /// requested backend was not compiled in to hwtracer. - pub fn build(self) -> Result, HWTracerError> { - let backend_kind = self.config.backend_kind(); - backend_kind.match_platform()?; - match self.config { - BackendConfig::PerfPT(_pt_conf) => { - // _pt_conf will be unused if perf_pt wasn't built in. - #[cfg(perf_pt)] - return Ok(Box::new(PerfPTTracer::new(_pt_conf)?)); - #[cfg(not(perf_pt))] - unreachable!(); - } - BackendConfig::Dummy => Ok(Box::new(DummyTracer::new())), - } - } -} - -#[cfg(test)] -mod tests { - use super::{BackendConfig, TracerBuilder}; - - // Check that building a default Tracer works. - #[test] - fn test_builder_default_backend() { - assert!(TracerBuilder::new().build().is_ok()); - } - - // Check we can conditionally configure an automatically chosen backend. - #[test] - fn test_builder_configure_default_backend() { - let mut bldr = TracerBuilder::new(); - if let BackendConfig::PerfPT(ref mut ppt_config) = bldr.config() { - ppt_config.aux_bufsize = 8192; - } - assert!(bldr.build().is_ok()); - } - - // Check the `TracerBuilder` correctly reports an unavailable backend. - #[cfg(not(perf_pt))] - #[test] - fn test_backend_unavailable() { - match TracerBuilder::new().perf_pt().build() { - Ok(_) => panic!("backend should be unavailable"), - Err(e) => assert_eq!(e.to_string(), "Backend unavailble: PerfPT"), - } - } - - // Ensure we can share `Tracer`s between threads. - #[test] - fn test_shared_tracers_betwen_threads() { - use std::sync::Arc; - use std::thread; - let arc1 = Arc::new(TracerBuilder::new().build().unwrap()); - let arc2 = Arc::clone(&arc1); - - thread::spawn(move || { - let _ = arc2; - }) - .join() - .unwrap(); - } -} diff --git a/src/backends/perf_pt/mod.rs b/src/backends/perf_pt/mod.rs deleted file mode 100644 index c2d671a..0000000 --- a/src/backends/perf_pt/mod.rs +++ /dev/null @@ -1,827 +0,0 @@ -use super::PerfPTConfig; -use crate::errors::HWTracerError; -use crate::{Block, ThreadTracer, Trace, Tracer}; -use libc::{c_char, c_int, c_void, free, geteuid, malloc, size_t}; -use std::error::Error; -use std::fmt::{self, Display, Formatter}; -use std::fs::File; -use std::io::{self, Read}; -use std::iter::Iterator; -use std::num::ParseIntError; -#[cfg(debug_assertions)] -use std::ops::Drop; -use std::os::unix::io::AsRawFd; -use std::ptr; -use std::{ - env, - ffi::{self, CStr, CString}, -}; -use tempfile::NamedTempFile; - -// The sysfs path used to set perf permissions. -const PERF_PERMS_PATH: &str = "/proc/sys/kernel/perf_event_paranoid"; - -/// An error indicated by a C-level libipt error code. -#[derive(Debug)] -struct LibIPTError(c_int); - -impl Display for LibIPTError { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - // Ask libipt for a string representation of the error code. - let err_str = unsafe { CStr::from_ptr(pt_errstr(self.0)) }; - write!(f, "libipt error: {}", err_str.to_str().unwrap()) - } -} - -impl Error for LibIPTError { - fn description(&self) -> &str { - "libipt error" - } - - fn cause(&self) -> Option<&dyn Error> { - None - } -} - -#[repr(C)] -#[allow(dead_code)] // Only C constructs these. -#[derive(Eq, PartialEq)] -enum PerfPTCErrorKind { - Unused, - Unknown, - Errno, - IPT, -} - -/// Represents an error occurring in the C code in this backend. -/// Rust code calling C inspects one of these if the return value of a call indicates error. -#[repr(C)] -struct PerfPTCError { - typ: PerfPTCErrorKind, - code: c_int, -} - -impl PerfPTCError { - // Creates a new error struct defaulting to an unknown error. - fn new() -> Self { - Self { - typ: PerfPTCErrorKind::Unused, - code: 0, - } - } -} - -impl From for HWTracerError { - fn from(err: PerfPTCError) -> HWTracerError { - // If this assert crashes out, then we forgot a perf_pt_set_err() somewhere in C code. - debug_assert!(err.typ != PerfPTCErrorKind::Unused); - match err.typ { - PerfPTCErrorKind::Unused => HWTracerError::Unknown, - PerfPTCErrorKind::Unknown => HWTracerError::Unknown, - PerfPTCErrorKind::Errno => HWTracerError::Errno(err.code), - PerfPTCErrorKind::IPT => { - // Overflow is a special case with its own error type. - match unsafe { perf_pt_is_overflow_err(err.code) } { - true => HWTracerError::HWBufferOverflow, - false => HWTracerError::Custom(Box::new(LibIPTError(err.code))), - } - } - } - } -} - -// FFI prototypes. -extern "C" { - // collect.c - fn perf_pt_init_tracer(conf: *const PerfPTConfig, err: *mut PerfPTCError) -> *mut c_void; - fn perf_pt_start_tracer( - tr_ctx: *mut c_void, - trace: *mut PerfPTTrace, - err: *mut PerfPTCError, - ) -> bool; - fn perf_pt_stop_tracer(tr_ctx: *mut c_void, err: *mut PerfPTCError) -> bool; - fn perf_pt_free_tracer(tr_ctx: *mut c_void, err: *mut PerfPTCError) -> bool; - // decode.c - fn perf_pt_init_block_decoder( - buf: *const c_void, - len: u64, - vdso_fd: c_int, - vdso_filename: *const c_char, - decoder_status: *mut c_int, - err: *mut PerfPTCError, - current_exe: *const c_char, - ) -> *mut c_void; - fn perf_pt_next_block( - decoder: *mut c_void, - decoder_status: *mut c_int, - addr: *mut u64, - len: *mut u64, - err: *mut PerfPTCError, - ) -> bool; - fn perf_pt_free_block_decoder(decoder: *mut c_void); - // util.c - fn perf_pt_is_overflow_err(err: c_int) -> bool; - // libipt - fn pt_errstr(error_code: c_int) -> *const c_char; -} - -// Iterate over the blocks of a PerfPTTrace. -struct PerfPTBlockIterator<'t> { - decoder: *mut c_void, // C-level libipt block decoder. - decoder_status: c_int, // Stores the current libipt-level status of the above decoder. - #[allow(dead_code)] // Rust doesn't know that this exists only to keep the file long enough. - vdso_tempfile: Option, // VDSO code stored temporarily. - trace: &'t PerfPTTrace, // The trace we are iterating. - errored: bool, // Set to true when an error occurs, thus invalidating the iterator. -} - -impl From for HWTracerError { - fn from(err: io::Error) -> Self { - HWTracerError::Custom(Box::new(err)) - } -} - -impl From for HWTracerError { - fn from(err: ffi::NulError) -> Self { - HWTracerError::Custom(Box::new(err)) - } -} - -impl From for HWTracerError { - fn from(err: ParseIntError) -> Self { - HWTracerError::Custom(Box::new(err)) - } -} - -impl<'t> PerfPTBlockIterator<'t> { - // Initialise the block decoder. - fn init_decoder(&mut self) -> Result<(), HWTracerError> { - // Make a temp file for the C code to write the VDSO code into. - // - // We have to do this because libipt lazily reads the code from the files you load into the - // image. We store it into `self` to ensure the file lives as long as the iterator. - let vdso_tempfile = NamedTempFile::new()?; - // File name of a NamedTempFile should always be valid UTF-8, unwrap() below can't fail. - let vdso_filename = CString::new(vdso_tempfile.path().to_str().unwrap())?; - let mut cerr = PerfPTCError::new(); - let decoder = unsafe { - perf_pt_init_block_decoder( - self.trace.buf.0 as *const c_void, - self.trace.len, - vdso_tempfile.as_raw_fd(), - vdso_filename.as_ptr(), - &mut self.decoder_status, - &mut cerr, - // FIXME: current_exe() isn't reliable. We should find another way to do this. - CString::new(env::current_exe().unwrap().to_str().unwrap()) - .unwrap() - .as_c_str() - .as_ptr() as *const c_char, - ) - }; - if decoder.is_null() { - return Err(cerr.into()); - } - - vdso_tempfile.as_file().sync_all()?; - self.decoder = decoder; - self.vdso_tempfile = Some(vdso_tempfile); - Ok(()) - } -} - -impl<'t> Drop for PerfPTBlockIterator<'t> { - fn drop(&mut self) { - unsafe { perf_pt_free_block_decoder(self.decoder) }; - } -} - -impl<'t> Iterator for PerfPTBlockIterator<'t> { - type Item = Result; - - fn next(&mut self) -> Option { - // There was an error in a previous iteration. - if self.errored { - return None; - } - - // Lazily initialise the block decoder. - if self.decoder.is_null() { - if let Err(e) = self.init_decoder() { - self.errored = true; - return Some(Err(e)); - } - } - - let mut first_instr = 0; - let mut last_instr = 0; - let mut cerr = PerfPTCError::new(); - let rv = unsafe { - perf_pt_next_block( - self.decoder, - &mut self.decoder_status, - &mut first_instr, - &mut last_instr, - &mut cerr, - ) - }; - if !rv { - self.errored = true; // This iterator is unusable now. - return Some(Err(HWTracerError::from(cerr))); - } - if first_instr == 0 { - None // End of packet stream. - } else { - Some(Ok(Block::new(first_instr, last_instr))) - } - } -} - -/// A wrapper around a manually malloc/free'd buffer for holding an Intel PT trace. We've split -/// this out from PerfPTTrace so that we can mark just this raw pointer as `unsafe Send`. -#[repr(C)] -#[derive(Debug)] -struct PerfPTTraceBuf(*mut u8); - -/// We need to be able to transfer `PerfPTTraceBuf`s between threads to allow background -/// compilation. However, `PerfPTTraceBuf` wraps a raw pointer, which is not `Send`, so nor is -/// `PerfPTTraceBuf`. As long as we take great care to never: a) give out copies of the pointer to -/// the wider world, or b) dereference the pointer when we shouldn't, then we can manually (and -/// unsafely) mark the struct as being Send. -unsafe impl Send for PerfPTTrace {} - -/// An Intel PT trace, obtained via Linux perf. -#[repr(C)] -#[derive(Debug)] -pub struct PerfPTTrace { - // The trace buffer. - buf: PerfPTTraceBuf, - // The length of the trace (in bytes). - len: u64, - // `buf`'s allocation size (in bytes), <= `len`. - capacity: u64, -} - -impl PerfPTTrace { - /// Makes a new trace, initially allocating the specified number of bytes for the PT trace - /// packet buffer. - /// - /// The allocation is automatically freed by Rust when the struct falls out of scope. - fn new(capacity: size_t) -> Result { - let buf = unsafe { malloc(capacity) as *mut u8 }; - if buf.is_null() { - return Err(HWTracerError::Unknown); - } - Ok(Self { - buf: PerfPTTraceBuf(buf), - len: 0, - capacity: capacity as u64, - }) - } -} - -impl Trace for PerfPTTrace { - /// Write the raw trace packets into the specified file. - #[cfg(test)] - fn to_file(&self, file: &mut File) { - use std::io::prelude::*; - use std::slice; - - let slice = unsafe { slice::from_raw_parts(self.buf.0 as *const u8, self.len as usize) }; - file.write_all(slice).unwrap(); - } - - fn iter_blocks<'t: 'i, 'i>( - &'t self, - ) -> Box> + 'i> { - let itr = PerfPTBlockIterator { - decoder: ptr::null_mut(), - decoder_status: 0, - vdso_tempfile: None, - trace: self, - errored: false, - }; - Box::new(itr) - } - - #[cfg(test)] - fn capacity(&self) -> usize { - self.capacity as usize - } -} - -impl Drop for PerfPTTrace { - fn drop(&mut self) { - if !self.buf.0.is_null() { - unsafe { free(self.buf.0 as *mut c_void) }; - } - } -} - -#[derive(Debug)] -pub struct PerfPTTracer { - config: PerfPTConfig, -} - -impl PerfPTTracer { - pub(super) fn new(config: PerfPTConfig) -> Result - where - Self: Sized, - { - // Check for inavlid configuration. - fn power_of_2(v: size_t) -> bool { - (v & (v - 1)) == 0 - } - if !power_of_2(config.data_bufsize) { - return Err(HWTracerError::BadConfig(String::from( - "data_bufsize must be a positive power of 2", - ))); - } - if !power_of_2(config.aux_bufsize) { - return Err(HWTracerError::BadConfig(String::from( - "aux_bufsize must be a positive power of 2", - ))); - } - - Self::check_perf_perms()?; - Ok(Self { config }) - } - - fn check_perf_perms() -> Result<(), HWTracerError> { - if unsafe { geteuid() } == 0 { - // Root can always trace. - return Ok(()); - } - - let mut f = File::open(&PERF_PERMS_PATH)?; - let mut buf = String::new(); - f.read_to_string(&mut buf)?; - let perm = buf.trim().parse::()?; - if perm != -1 { - let msg = format!( - "Tracing not permitted: you must be root or {} must contain -1", - PERF_PERMS_PATH - ); - return Err(HWTracerError::Permissions(msg)); - } - - Ok(()) - } -} - -impl Tracer for PerfPTTracer { - fn thread_tracer(&self) -> Box { - Box::new(PerfPTThreadTracer::new(self.config.clone())) - } -} - -/// A tracer that uses the Linux Perf interface to Intel Processor Trace. -pub struct PerfPTThreadTracer { - // The configuration for this tracer. - config: PerfPTConfig, - // Opaque C pointer representing the tracer context. - tracer_ctx: *mut c_void, - // The state of the tracer. - is_tracing: bool, - // The trace currently being collected, or `None`. - trace: Option>, -} - -impl PerfPTThreadTracer { - fn new(config: PerfPTConfig) -> Self { - Self { - config, - tracer_ctx: ptr::null_mut(), - is_tracing: false, - trace: None, - } - } -} - -impl Default for PerfPTThreadTracer { - fn default() -> Self { - PerfPTThreadTracer::new(PerfPTConfig::default()) - } -} - -impl Drop for PerfPTThreadTracer { - fn drop(&mut self) { - if self.is_tracing { - // If we haven't stopped the tracer already, stop it now. - self.stop_tracing().unwrap(); - } - } -} - -impl ThreadTracer for PerfPTThreadTracer { - fn start_tracing(&mut self) -> Result<(), HWTracerError> { - if self.is_tracing { - return Err(HWTracerError::AlreadyTracing); - } - - // At the time of writing, we have to use a fresh Perf file descriptor to ensure traces - // start with a `PSB+` packet sequence. This is required for correct instruction-level and - // block-level decoding. Therefore we have to re-initialise for each new tracing session. - let mut cerr = PerfPTCError::new(); - self.tracer_ctx = - unsafe { perf_pt_init_tracer(&self.config as *const PerfPTConfig, &mut cerr) }; - if self.tracer_ctx.is_null() { - return Err(cerr.into()); - } - - // It is essential we box the trace now to stop it from moving. If it were to move, then - // the reference which we pass to C here would become invalid. The interface to - // `stop_tracing` needs to return a Box anyway, so it's no big deal. - // - // Note that the C code will mutate the trace's members directly. - let mut trace = Box::new(PerfPTTrace::new(self.config.initial_trace_bufsize)?); - let mut cerr = PerfPTCError::new(); - if !unsafe { perf_pt_start_tracer(self.tracer_ctx, &mut *trace, &mut cerr) } { - return Err(cerr.into()); - } - self.is_tracing = true; - self.trace = Some(trace); - Ok(()) - } - - fn stop_tracing(&mut self) -> Result, HWTracerError> { - if !self.is_tracing { - return Err(HWTracerError::AlreadyStopped); - } - let mut cerr = PerfPTCError::new(); - let rc = unsafe { perf_pt_stop_tracer(self.tracer_ctx, &mut cerr) }; - self.is_tracing = false; - if !rc { - return Err(cerr.into()); - } - - let mut cerr = PerfPTCError::new(); - if !unsafe { perf_pt_free_tracer(self.tracer_ctx, &mut cerr) } { - return Err(cerr.into()); - } - self.tracer_ctx = ptr::null_mut(); - - let ret = self.trace.take().unwrap(); - self.trace = None; - Ok(ret as Box) - } -} - -// Called by C to store a ptxed argument into a Rust Vec. -#[cfg(test)] -#[no_mangle] -pub unsafe extern "C" fn push_ptxed_arg(args: &mut Vec, new_arg: *const c_char) { - let new_arg = CStr::from_ptr(new_arg) - .to_owned() - .to_str() - .unwrap() - .to_owned(); - args.push(new_arg); -} - -#[cfg(all(perf_pt_test, test))] -mod tests { - use super::PerfPTCError; - use super::{ - c_int, ptr, size_t, AsRawFd, HWTracerError, NamedTempFile, PerfPTBlockIterator, - PerfPTConfig, PerfPTThreadTracer, PerfPTTrace, ThreadTracer, Trace, - }; - use crate::backends::{BackendConfig, TracerBuilder}; - use crate::{test_helpers, Block}; - use phdrs::{PF_X, PT_LOAD}; - use std::convert::TryFrom; - use std::env; - use std::process::Command; - - extern "C" { - fn dump_vdso(fd: c_int, vaddr: u64, len: size_t, err: &PerfPTCError) -> bool; - } - - const VDSO_FILENAME: &str = "linux-vdso.so.1"; - - // Gets the ptxed arguments required to decode a trace for the current process. - // - // Returns a vector of arguments and a handle to a temproary file containing the VDSO code. The - // caller must make sure that this file lives long enough for ptxed to run (temp files are - // removed when they fall out of scope). - fn self_ptxed_args(trace_filename: &str) -> (Vec, NamedTempFile) { - let ptxed_args = vec![ - "--cpu", - "auto", - "--block-decoder", - "--block:end-on-call", - "--block:end-on-jump", - "--block:show-blocks", - "--pt", - trace_filename, - ]; - let mut ptxed_args: Vec = ptxed_args.into_iter().map(|e| String::from(e)).collect(); - - // Make a temp file to dump the VDSO code into. This is necessary because ptxed cannot read - // code from a virtual address: it can only load from file. - let vdso_tempfile = NamedTempFile::new().unwrap(); - - let exe = env::current_exe().unwrap(); - for obj in phdrs::objects() { - let obj_name = obj.name().to_str().unwrap(); - let mut filename = if cfg!(target_os = "linux") && obj_name == "" { - exe.to_str().unwrap() - } else { - obj_name - }; - - for hdr in obj.iter_phdrs() { - if hdr.type_() != PT_LOAD || hdr.flags() & PF_X.0 == 0 { - continue; // Only look at loadable and executable segments. - } - - let vaddr = obj.addr() + hdr.vaddr(); - let offset; - - if filename == VDSO_FILENAME { - let cerr = PerfPTCError::new(); - if !unsafe { - dump_vdso( - vdso_tempfile.as_raw_fd(), - vaddr, - size_t::try_from(hdr.memsz()).unwrap(), - &cerr, - ) - } { - panic!("failed to dump vdso"); - } - filename = vdso_tempfile.path().to_str().unwrap(); - offset = 0; - } else { - offset = hdr.offset(); - } - - let raw_arg = format!( - "{}:0x{:x}-0x{:x}:0x{:x}", - filename, - offset, - hdr.offset() + hdr.filesz(), - vaddr - ); - ptxed_args.push("--raw".to_owned()); - ptxed_args.push(raw_arg); - } - } - - (ptxed_args, vdso_tempfile) - } - - /* - * Determine if the given x86_64 assembler mnemonic should terminate a block. - * - * Mnemonic assumed to be lower case. - */ - fn instr_terminates_block(instr: &str) -> bool { - assert!(instr.find(|c: char| !c.is_lowercase()).is_none()); - match instr { - // JMP or Jcc are the only instructions beginning with 'j'. - m if m.starts_with("j") => true, - "call" | "ret" | "loop" | "loope" | "loopne" | "syscall" | "sysenter" | "sysexit" - | "sysret" | "xabort" => true, - _ => false, - } - } - - // Given a trace, use ptxed to get a vector of block start vaddrs. - fn get_expected_blocks(trace: &Box) -> Vec { - // Write the trace out to a temp file so ptxed can decode it. - let mut tmpf = NamedTempFile::new().unwrap(); - trace.to_file(&mut tmpf.as_file_mut()); - let (args, _vdso_tempfile) = self_ptxed_args(tmpf.path().to_str().unwrap()); - - let out = Command::new(env!("PTXED")).args(&args).output().unwrap(); - let outstr = String::from_utf8(out.stdout).unwrap(); - if !out.status.success() { - let errstr = String::from_utf8(out.stderr).unwrap(); - panic!( - "ptxed failed:\nInvocation----------\n{:?}\n \ - Stdout\n------\n{}Stderr\n------\n{}", - args, outstr, errstr - ); - } - - let mut block_start = false; - let mut block_vaddrs = Vec::new(); - let mut last_instr: Option<&str> = None; - for line in outstr.lines() { - let line = line.trim(); - if line.contains("error") { - panic!("error line in ptxed output:\n{}", line); - } else if line.starts_with("[") { - // It's a special line, e.g. [enabled], [disabled], [block]... - if line == "[block]" - && (last_instr.is_none() || instr_terminates_block(last_instr.unwrap())) - { - // The next insruction we see will be the start of a block. - block_start = true; - } - } else { - // It's a regular instruction line. - if block_start { - // This instruction is the start of a block. - let vaddr_s = line.split_whitespace().next().unwrap(); - let vaddr = u64::from_str_radix(vaddr_s, 16).unwrap(); - block_vaddrs.push(Block::new(vaddr, 0)); - block_start = false; - } - last_instr = Some(line.split_whitespace().nth(1).unwrap()); - } - } - - block_vaddrs - } - - // Trace a closure and then decode it and check the block iterator agrees with ptxed. - fn trace_and_check_blocks(mut tracer: T, f: F) - where - T: ThreadTracer, - F: FnOnce() -> u64, - { - let trace = test_helpers::trace_closure(&mut tracer, f); - let expects = get_expected_blocks(&trace); - test_helpers::test_expected_blocks(trace, expects.iter()); - } - - #[test] - fn test_basic_usage() { - test_helpers::test_basic_usage(PerfPTThreadTracer::default()); - } - - #[test] - fn test_repeated_tracing() { - test_helpers::test_repeated_tracing(PerfPTThreadTracer::default()); - } - - #[test] - fn test_already_started() { - test_helpers::test_already_started(PerfPTThreadTracer::default()); - } - - #[test] - fn test_not_started() { - test_helpers::test_not_started(PerfPTThreadTracer::default()); - } - - // Test writing a trace to file. - #[cfg(debug_assertions)] - #[test] - fn test_to_file() { - use std::fs::File; - use std::io::prelude::*; - use std::slice; - use Trace; - - // Allocate and fill a buffer to make a "trace" from. - let capacity = 1024; - let mut trace = PerfPTTrace::new(capacity).unwrap(); - trace.len = capacity as u64; - let sl = unsafe { slice::from_raw_parts_mut(trace.buf.0 as *mut u8, capacity) }; - for (i, byte) in sl.iter_mut().enumerate() { - *byte = i as u8; - } - - // Make the trace and write it to a file. - let mut tmpf = NamedTempFile::new().unwrap(); - trace.to_file(&mut tmpf.as_file_mut()); - tmpf.as_file().sync_all().unwrap(); - - // Check the resulting file makes sense. - let file = File::open(tmpf.path().to_str().unwrap()).unwrap(); - let mut total_bytes = 0; - for (i, byte) in file.bytes().enumerate() { - assert_eq!(i as u8, byte.unwrap()); - total_bytes += 1; - } - assert_eq!(total_bytes, capacity); - } - - // Check that our block decoder agrees with the reference implementation in ptxed. - #[test] - fn test_block_iterator1() { - let tracer = PerfPTThreadTracer::default(); - trace_and_check_blocks(tracer, || test_helpers::work_loop(10)); - } - - // Check that our block decoder agrees ptxed on a (likely) empty trace; - #[test] - fn test_block_iterator2() { - let tracer = PerfPTThreadTracer::default(); - trace_and_check_blocks(tracer, || test_helpers::work_loop(0)); - } - - // Check that our block decoder deals with traces involving the VDSO correctly. - #[test] - fn test_block_iterator3() { - use libc::{clock_gettime, timespec, CLOCK_MONOTONIC}; - - let tracer = PerfPTThreadTracer::default(); - trace_and_check_blocks(tracer, || { - let mut res = 0; - let mut tv = timespec { - tv_sec: 0, - tv_nsec: 0, - }; - for _ in 1..100 { - // clock_gettime(2) is in the Linux VDSO. - let rv = unsafe { clock_gettime(CLOCK_MONOTONIC, &mut tv) }; - assert_eq!(rv, 0); - res += tv.tv_sec; - } - res as u64 - }); - } - - // Check that a shorter trace yields fewer blocks. - #[test] - fn test_block_iterator4() { - let tracer1 = PerfPTThreadTracer::default(); - let tracer2 = PerfPTThreadTracer::default(); - test_helpers::test_ten_times_as_many_blocks(tracer1, tracer2); - } - - // Check that our block decoder agrees ptxed on long trace. - // XXX We use an even higher iteration count once our decoder uses a libipt image cache. - #[ignore] // Decoding long traces is slow. - #[test] - fn test_block_iterator5() { - let tracer = PerfPTThreadTracer::default(); - trace_and_check_blocks(tracer, || test_helpers::work_loop(3000)); - } - - // Check that a long trace causes the trace buffer to reallocate. - #[test] - fn test_relloc_trace_buf1() { - let start_bufsize = 512; - let mut config = PerfPTConfig::default(); - config.initial_trace_bufsize = start_bufsize; - let mut tracer = PerfPTThreadTracer::new(config); - - tracer.start_tracing().unwrap(); - let res = test_helpers::work_loop(10000); - let trace = tracer.stop_tracing().unwrap(); - - println!("res: {}", res); // Stop over-optimisation. - assert!(trace.capacity() > start_bufsize); - } - - // Check that a block iterator returns none after an error. - #[test] - fn test_error_stops_block_iter1() { - // A zero-sized trace will lead to an error. - let trace = PerfPTTrace::new(0).unwrap(); - let mut itr = PerfPTBlockIterator { - decoder: ptr::null_mut(), - decoder_status: 0, - vdso_tempfile: None, - trace: &trace, - errored: false, - }; - - // First we expect a libipt error. - match itr.next() { - Some(Err(HWTracerError::Custom(e))) => { - assert!(e.to_string().starts_with("libipt error: ")) - } - _ => panic!(), - } - // And now the iterator is invalid, and should return None. - for _ in 0..128 { - assert!(itr.next().is_none()); - } - } - - #[test] - fn test_config_bad_data_bufsize() { - let mut bldr = TracerBuilder::new().perf_pt(); - match bldr.config() { - BackendConfig::PerfPT(ref mut ppt_conf) => ppt_conf.data_bufsize = 3, - _ => panic!(), - } - match bldr.build() { - Err(HWTracerError::BadConfig(s)) => { - assert_eq!(s, "data_bufsize must be a positive power of 2"); - } - _ => panic!(), - } - } - - #[test] - fn test_config_bad_aux_bufsize() { - let mut bldr = TracerBuilder::new().perf_pt(); - match bldr.config() { - BackendConfig::PerfPT(ref mut ppt_conf) => ppt_conf.aux_bufsize = 3, - _ => panic!(), - } - match bldr.build() { - Err(HWTracerError::BadConfig(s)) => { - assert_eq!(s, "aux_bufsize must be a positive power of 2"); - } - _ => panic!(), - } - } -} diff --git a/src/c_errors.rs b/src/c_errors.rs new file mode 100644 index 0000000..9ab1fb4 --- /dev/null +++ b/src/c_errors.rs @@ -0,0 +1,79 @@ +use crate::{ + decode::libipt::{hwt_ipt_is_overflow_err, pt_errstr}, + HWTracerError, +}; +use libc::c_int; +use std::{ + error::Error, + ffi::CStr, + fmt::{self, Display, Formatter}, +}; + +/// An error indicated by a C-level libipt error code. +#[derive(Debug)] +struct LibIPTError(c_int); + +impl Display for LibIPTError { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + // Ask libipt for a string representation of the error code. + let err_str = unsafe { CStr::from_ptr(pt_errstr(self.0)) }; + write!(f, "libipt error: {}", err_str.to_str().unwrap()) + } +} + +impl Error for LibIPTError { + fn description(&self) -> &str { + "libipt error" + } + + fn cause(&self) -> Option<&dyn Error> { + None + } +} + +#[repr(C)] +#[allow(dead_code)] // Only C constructs these. +#[derive(Eq, PartialEq)] +enum PerfPTCErrorKind { + Unused, + Unknown, + Errno, + IPT, +} + +/// Represents an error occurring in C code. +/// Rust code calling C inspects one of these if the return value of a call indicates error. +#[repr(C)] +pub(crate) struct PerfPTCError { + typ: PerfPTCErrorKind, + code: c_int, +} + +impl PerfPTCError { + // Creates a new error struct defaulting to an unknown error. + pub(crate) fn new() -> Self { + Self { + typ: PerfPTCErrorKind::Unused, + code: 0, + } + } +} + +impl From for HWTracerError { + fn from(err: PerfPTCError) -> HWTracerError { + // If this assert crashes out, then we forgot a hwt_set_cerr() somewhere in C code. + debug_assert!(err.typ != PerfPTCErrorKind::Unused); + match err.typ { + PerfPTCErrorKind::Unused => HWTracerError::Unknown, + PerfPTCErrorKind::Unknown => HWTracerError::Unknown, + PerfPTCErrorKind::Errno => HWTracerError::Errno(err.code), + PerfPTCErrorKind::IPT => { + // Overflow is a special case with its own error type. + match unsafe { hwt_ipt_is_overflow_err(err.code) } { + true => HWTracerError::HWBufferOverflow, + false => HWTracerError::Custom(Box::new(LibIPTError(err.code))), + } + } + } + } +} diff --git a/src/collect/mod.rs b/src/collect/mod.rs new file mode 100644 index 0000000..5e370ab --- /dev/null +++ b/src/collect/mod.rs @@ -0,0 +1,252 @@ +//! Trace collectors. + +use crate::{errors::HWTracerError, Trace}; +use core::arch::x86_64::__cpuid_count; +use libc::size_t; +use strum::IntoEnumIterator; +use strum_macros::EnumIter; + +#[cfg(collector_perf)] +pub(crate) mod perf; +#[cfg(collector_perf)] +pub(crate) use perf::PerfTraceCollector; + +const PERF_DFLT_DATA_BUFSIZE: size_t = 64; +const PERF_DFLT_AUX_BUFSIZE: size_t = 1024; +const PERF_DFLT_INITIAL_TRACE_BUFSIZE: size_t = 1024 * 1024; // 1MiB + +/// The interface offered by all trace collectors. +pub trait TraceCollector: Send + Sync { + /// Return a `ThreadTraceCollector` for tracing the current thread. + fn thread_collector(&self) -> Box; +} + +pub trait ThreadTraceCollector { + /// Start recording a trace. + /// + /// Tracing continues until [stop_collector](trait.ThreadTraceCollector.html#method.stop_collector) is called. + fn start_collector(&mut self) -> Result<(), HWTracerError>; + /// Turns off the tracer. + /// + /// [start_collector](trait.ThreadTraceCollector.html#method.start_collector) must have been called prior. + fn stop_collector(&mut self) -> Result, HWTracerError>; +} + +/// Kinds of collector that hwtracer supports (in order of "auto-selection preference"). +#[derive(Debug, EnumIter)] +pub enum TraceCollectorKind { + /// The `perf` subsystem, as found on Linux. + Perf, +} + +impl TraceCollectorKind { + // Finds a suitable `TraceCollectorKind` for the current hardware/OS. + fn default_for_platform() -> Option { + for kind in TraceCollectorKind::iter() { + if Self::match_platform(&kind).is_ok() { + return Some(kind); + } + } + None + } + + /// Returns `Ok` if the this collector is appropriate for the current platform. + fn match_platform(&self) -> Result<(), HWTracerError> { + match self { + Self::Perf => { + #[cfg(not(collector_perf))] + return Err(HWTracerError::CollectorUnavailable(Self::Perf)); + #[cfg(collector_perf)] + { + if !Self::pt_supported() { + return Err(HWTracerError::NoHWSupport( + "Intel PT not supported by CPU".into(), + )); + } + Ok(()) + } + } + } + } + + /// Checks if the CPU supports Intel Processor Trace. + fn pt_supported() -> bool { + let res = unsafe { __cpuid_count(0x7, 0x0) }; + (res.ebx & (1 << 25)) != 0 + } +} + +/// Configuration for trace collectors. +#[derive(Debug)] +pub enum TraceCollectorConfig { + Perf(PerfCollectorConfig), +} + +/// Configures the Perf collector. +/// +// Must stay in sync with the C code. +#[derive(Clone, Debug)] +#[repr(C)] +pub struct PerfCollectorConfig { + /// Data buffer size, in pages. Must be a power of 2. + pub data_bufsize: size_t, + /// AUX buffer size, in pages. Must be a power of 2. + pub aux_bufsize: size_t, + /// The initial trace storage buffer size (in bytes) of new traces. + pub initial_trace_bufsize: size_t, +} + +impl Default for PerfCollectorConfig { + fn default() -> Self { + Self { + data_bufsize: PERF_DFLT_DATA_BUFSIZE, + aux_bufsize: PERF_DFLT_AUX_BUFSIZE, + initial_trace_bufsize: PERF_DFLT_INITIAL_TRACE_BUFSIZE, + } + } +} + +impl TraceCollectorConfig { + fn kind(&self) -> TraceCollectorKind { + match self { + TraceCollectorConfig::Perf { .. } => TraceCollectorKind::Perf, + } + } +} + +/// A builder interface for instantiating trace collectors. +/// +/// # Make a trace collector using the appropriate defaults. +/// ``` +/// use hwtracer::collect::TraceCollectorBuilder; +/// TraceCollectorBuilder::new().build().unwrap(); +/// ``` +/// +/// # Make a trace collector that uses Perf for collection with default options. +/// ``` +/// use hwtracer::collect::{TraceCollectorBuilder, TraceCollectorKind}; +/// +/// let res = TraceCollectorBuilder::new().kind(TraceCollectorKind::Perf).build(); +/// if let Ok(tracer) = res { +/// // Use the collector. +/// } else { +/// // Platform doesn't support Linux Perf or CPU doesn't support Intel Processor Trace. +/// } +/// ``` +/// +/// # Make a collector appropriate for the current platform, using custom Perf collector options if +/// the Perf collector was chosen. +/// ``` +/// use hwtracer::collect::{TraceCollectorBuilder, TraceCollectorConfig}; +/// let mut bldr = TraceCollectorBuilder::new(); +/// if let TraceCollectorConfig::Perf(ref mut ppt_config) = bldr.config() { +/// ppt_config.aux_bufsize = 8192; +/// } +/// bldr.build().unwrap(); +/// ``` +pub struct TraceCollectorBuilder { + config: TraceCollectorConfig, +} + +impl TraceCollectorBuilder { + /// Create a new `TraceCollectorBuilder` using sensible defaults. + pub fn new() -> Self { + let config = match TraceCollectorKind::default_for_platform().unwrap() { + TraceCollectorKind::Perf => TraceCollectorConfig::Perf(PerfCollectorConfig::default()), + }; + Self { config } + } + + /// Select the kind of trace collector. + pub fn kind(mut self, kind: TraceCollectorKind) -> Self { + self.config = match kind { + TraceCollectorKind::Perf => TraceCollectorConfig::Perf(PerfCollectorConfig::default()), + }; + self + } + + /// Get a mutable reference to the collector configuraion. + pub fn config(&mut self) -> &mut TraceCollectorConfig { + &mut self.config + } + + /// Build the trace collector + /// + /// An error is returned if the requested collector is inappropriate for the platform or not + /// compiled in to hwtracer. + pub fn build(self) -> Result, HWTracerError> { + let kind = self.config.kind(); + kind.match_platform()?; + match self.config { + TraceCollectorConfig::Perf(_pt_conf) => { + #[cfg(collector_perf)] + return Ok(Box::new(PerfTraceCollector::new(_pt_conf)?)); + #[cfg(not(collector_perf))] + unreachable!(); + } + } + } +} + +#[cfg(test)] +pub(crate) mod test_helpers { + use crate::{ + collect::ThreadTraceCollector, errors::HWTracerError, test_helpers::work_loop, Trace, + }; + + // Trace a closure that returns a u64. + pub fn trace_closure(tc: &mut dyn ThreadTraceCollector, f: F) -> Box + where + F: FnOnce() -> u64, + { + tc.start_collector().unwrap(); + let res = f(); + let trace = tc.stop_collector().unwrap(); + println!("traced closure with result: {}", res); // To avoid over-optimisation. + trace + } + + // Check that starting and stopping a trace collector works. + pub fn basic_collection(mut tracer: T) + where + T: ThreadTraceCollector, + { + let trace = trace_closure(&mut tracer, || work_loop(500)); + assert_ne!(trace.len(), 0); + } + + // Check that repeated usage of the same trace collector works. + pub fn repeated_collection(mut tracer: T) + where + T: ThreadTraceCollector, + { + for _ in 0..10 { + trace_closure(&mut tracer, || work_loop(500)); + } + } + + // Check that starting a trace collector twice (without stopping maktracing inbetween) makes an + // appropriate error. + pub fn already_started(mut tc: T) + where + T: ThreadTraceCollector, + { + tc.start_collector().unwrap(); + match tc.start_collector() { + Err(HWTracerError::AlreadyCollecting) => (), + _ => panic!(), + }; + tc.stop_collector().unwrap(); + } + + // Check that stopping an unstarted trace collector makes an appropriate error. + pub fn not_started(mut tc: T) + where + T: ThreadTraceCollector, + { + match tc.stop_collector() { + Err(HWTracerError::AlreadyStopped) => (), + _ => panic!(), + }; + } +} diff --git a/src/backends/perf_pt/collect.c b/src/collect/perf/collect.c similarity index 79% rename from src/backends/perf_pt/collect.c rename to src/collect/perf/collect.c index f342c3d..108aa53 100644 --- a/src/backends/perf_pt/collect.c +++ b/src/collect/perf/collect.c @@ -61,7 +61,7 @@ #include #include -#include "perf_pt_private.h" +#include "hwtracer_private.h" #define SYSFS_PT_TYPE "/sys/bus/event_source/devices/intel_pt/type" #define MAX_PT_TYPE_STR 8 @@ -76,13 +76,12 @@ #endif /* - * Stores all information about the tracer. + * Stores all information about the collector. * Exposed to Rust only as an opaque pointer. */ -struct tracer_ctx { - pthread_t tracer_thread; // Tracer thread handle. - struct perf_pt_cerror - tracer_thread_err; // Errors from inside the tracer thread. +struct hwt_perf_ctx { + pthread_t collector_thread; // Collector thread handle. + struct hwt_cerror collector_thread_err; // Errors from inside the tracer thread. int stop_fds[2]; // Pipe used to stop the poll loop. int perf_fd; // FD used to talk to the perf API. void *aux_buf; // Ptr to the start of the the AUX buffer. @@ -95,7 +94,7 @@ struct tracer_ctx { * Passed from Rust to C to configure tracing. * Must stay in sync with the Rust-side. */ -struct perf_pt_config { +struct hwt_perf_collector_config { size_t data_bufsize; // Data buf size (in pages). size_t aux_bufsize; // AUX buf size (in pages). size_t initial_trace_bufsize; // Initial capacity (in bytes) of a @@ -104,10 +103,10 @@ struct perf_pt_config { /* * The manually malloc/free'd buffer managed by the Rust side. - * To understand why this is split out from `struct perf_pt_trace`, see the + * To understand why this is split out from `struct hwt_perf_trace`, see the * corresponding struct in the Rust side. */ -struct perf_pt_trace_buf { +struct hwt_perf_trace_buf { void *p; }; @@ -116,25 +115,25 @@ struct perf_pt_trace_buf { * * Shared with Rust code. Must stay in sync. */ -struct perf_pt_trace { - struct perf_pt_trace_buf buf; +struct hwt_perf_trace { + struct hwt_perf_trace_buf buf; __u64 len; __u64 capacity; }; /* - * Stuff used in the tracer thread + * Stuff used in the collector thread */ -struct tracer_thread_args { +struct collector_thread_args { int perf_fd; // Perf notification fd. int stop_fd_rd; // Polled for "stop" event. - sem_t *tracer_init_sem; // Tracer init sync. - struct perf_pt_trace + sem_t *collector_init_sem; // Tracer init sync. + struct hwt_perf_trace *trace; // Pointer to trace storage. void *aux_buf; // The AUX buffer itself; struct perf_event_mmap_page *base_header; // Pointer to the header in the base buffer. - struct perf_pt_cerror + struct hwt_cerror *err; // Errors generated inside the thread. }; @@ -159,19 +158,19 @@ struct read_format { // Private prototypes. static bool handle_sample(void *, struct perf_event_mmap_page *, struct - perf_pt_trace *, void *, struct perf_pt_cerror *); + hwt_perf_trace *, void *, struct hwt_cerror *); static bool read_aux(void *, struct perf_event_mmap_page *, - struct perf_pt_trace *, struct perf_pt_cerror *); + struct hwt_perf_trace *, struct hwt_cerror *); static bool poll_loop(int, int, struct perf_event_mmap_page *, void *, - struct perf_pt_trace *, struct perf_pt_cerror *); -static void *tracer_thread(void *); -static int open_perf(size_t, struct perf_pt_cerror *); + struct hwt_perf_trace *, struct hwt_cerror *); +static void *collector_thread(void *); +static int open_perf(size_t, struct hwt_cerror *); // Exposed Prototypes. -struct tracer_ctx *perf_pt_init_tracer(struct perf_pt_config *, struct perf_pt_cerror *); -bool perf_pt_start_tracer(struct tracer_ctx *, struct perf_pt_trace *, struct perf_pt_cerror *); -bool perf_pt_stop_tracer(struct tracer_ctx *tr_ctx, struct perf_pt_cerror *); -bool perf_pt_free_tracer(struct tracer_ctx *tr_ctx, struct perf_pt_cerror *); +struct hwt_perf_ctx *hwt_perf_init_collector(struct hwt_perf_collector_config *, struct hwt_cerror *); +bool hwt_perf_start_collector(struct hwt_perf_ctx *, struct hwt_perf_trace *, struct hwt_cerror *); +bool hwt_perf_stop_collector(struct hwt_perf_ctx *tr_ctx, struct hwt_cerror *); +bool hwt_perf_free_collector(struct hwt_perf_ctx *tr_ctx, struct hwt_cerror *); /* @@ -183,8 +182,8 @@ bool perf_pt_free_tracer(struct tracer_ctx *tr_ctx, struct perf_pt_cerror *); */ static bool handle_sample(void *aux_buf, struct perf_event_mmap_page *hdr, - struct perf_pt_trace *trace, void *data_tmp, - struct perf_pt_cerror *err) + struct hwt_perf_trace *trace, void *data_tmp, + struct hwt_cerror *err) { // We need to use atomics with orderings to protect against 2 cases. // @@ -237,7 +236,7 @@ handle_sample(void *aux_buf, struct perf_event_mmap_page *hdr, // truncated. If it was, then we didn't read out of the data buffer // quickly/frequently enough. if (rec_aux_sample->flags & PERF_AUX_FLAG_TRUNCATED) { - perf_pt_set_err(err, perf_pt_cerror_ipt, pte_overflow); + hwt_set_cerr(err, hwt_cerror_ipt, pte_overflow); return false; } if (read_aux(aux_buf, hdr, trace, err) == false) { @@ -245,7 +244,7 @@ handle_sample(void *aux_buf, struct perf_event_mmap_page *hdr, } break; case PERF_RECORD_LOST: - perf_pt_set_err(err, perf_pt_cerror_ipt, pte_overflow); + hwt_set_cerr(err, hwt_cerror_ipt, pte_overflow); return false; break; case PERF_RECORD_LOST_SAMPLES: @@ -266,7 +265,7 @@ handle_sample(void *aux_buf, struct perf_event_mmap_page *hdr, */ bool read_aux(void *aux_buf, struct perf_event_mmap_page *hdr, - struct perf_pt_trace *trace, struct perf_pt_cerror *err) + struct hwt_perf_trace *trace, struct hwt_cerror *err) { // Use of atomics here for the same reasons as for handle_sample(). __u64 head_monotonic = @@ -294,13 +293,13 @@ read_aux(void *aux_buf, struct perf_event_mmap_page *hdr, // the size_t argument of realloc(3). if (required_capacity >= SIZE_MAX / 2) { // We would overflow the size_t argument of realloc(3). - perf_pt_set_err(err, perf_pt_cerror_errno, ENOMEM); + hwt_set_cerr(err, hwt_cerror_errno, ENOMEM); return false; } size_t new_capacity = required_capacity * 2; void *new_buf = realloc(trace->buf.p, new_capacity); if (new_buf == NULL) { - perf_pt_set_err(err, perf_pt_cerror_errno, errno); + hwt_set_cerr(err, hwt_cerror_errno, errno); return false; } trace->capacity = new_capacity; @@ -328,7 +327,7 @@ read_aux(void *aux_buf, struct perf_event_mmap_page *hdr, */ static bool poll_loop(int perf_fd, int stop_fd, struct perf_event_mmap_page *mmap_hdr, - void *aux, struct perf_pt_trace *trace, struct perf_pt_cerror *err) + void *aux, struct hwt_perf_trace *trace, struct hwt_cerror *err) { int n_events = 0; bool ret = true; @@ -340,7 +339,7 @@ poll_loop(int perf_fd, int stop_fd, struct perf_event_mmap_page *mmap_hdr, // Temporary space for new samples in the data buffer. void *data_tmp = malloc(mmap_hdr->data_size); if (data_tmp == NULL) { - perf_pt_set_err(err, perf_pt_cerror_errno, errno); + hwt_set_cerr(err, hwt_cerror_errno, errno); ret = false; goto done; } @@ -348,13 +347,13 @@ poll_loop(int perf_fd, int stop_fd, struct perf_event_mmap_page *mmap_hdr, while (1) { n_events = poll(pfds, 2, INFTIM); if (n_events == -1) { - perf_pt_set_err(err, perf_pt_cerror_errno, errno); + hwt_set_cerr(err, hwt_cerror_errno, errno); ret = false; goto done; } // POLLIN on pfds[0]: Overflow event on either the Perf AUX or data buffer. - // POLLHUP on pfds[1]: Tracer stopped by parent. + // POLLHUP on pfds[1]: Trace collection stopped by parent. if ((pfds[0].revents & POLLIN) || (pfds[1].revents & POLLHUP)) { // Read from the Perf file descriptor. // We don't actually use any of what we read, but it's probably @@ -362,7 +361,7 @@ poll_loop(int perf_fd, int stop_fd, struct perf_event_mmap_page *mmap_hdr, struct read_format fd_data; if (pfds[0].revents & POLLIN) { if (read(perf_fd, &fd_data, sizeof(fd_data)) == -1) { - perf_pt_set_err(err, perf_pt_cerror_errno, errno); + hwt_set_cerr(err, hwt_cerror_errno, errno); ret = false; break; } @@ -398,7 +397,7 @@ poll_loop(int perf_fd, int stop_fd, struct perf_event_mmap_page *mmap_hdr, * Returns a file descriptor, or -1 on error. */ static int -open_perf(size_t aux_bufsize, struct perf_pt_cerror *err) { +open_perf(size_t aux_bufsize, struct hwt_cerror *err) { struct perf_event_attr attr; memset(&attr, 0, sizeof(attr)); attr.size = sizeof(attr); @@ -409,13 +408,13 @@ open_perf(size_t aux_bufsize, struct perf_pt_cerror *err) { // Get the perf "type" for Intel PT. FILE *pt_type_file = fopen(SYSFS_PT_TYPE, "r"); if (pt_type_file == NULL) { - perf_pt_set_err(err, perf_pt_cerror_errno, errno); + hwt_set_cerr(err, hwt_cerror_errno, errno); ret = -1; goto clean; } char pt_type_str[MAX_PT_TYPE_STR]; if (fgets(pt_type_str, sizeof(pt_type_str), pt_type_file) == NULL) { - perf_pt_set_err(err, perf_pt_cerror_errno, errno); + hwt_set_cerr(err, hwt_cerror_errno, errno); ret = -1; goto clean; } @@ -455,12 +454,12 @@ open_perf(size_t aux_bufsize, struct perf_pt_cerror *err) { } if (ret == -1) { - perf_pt_set_err(err, perf_pt_cerror_errno, errno); + hwt_set_cerr(err, hwt_cerror_errno, errno); } clean: if ((pt_type_file != NULL) && (fclose(pt_type_file) == -1)) { - perf_pt_set_err(err, perf_pt_cerror_errno, errno); + hwt_set_cerr(err, hwt_cerror_errno, errno); ret = -1; } @@ -473,9 +472,9 @@ open_perf(size_t aux_bufsize, struct perf_pt_cerror *err) { * Returns true on success and false otherwise. */ static void * -tracer_thread(void *arg) +collector_thread(void *arg) { - struct tracer_thread_args *thr_args = (struct tracer_thread_args *) arg; + struct collector_thread_args *thr_args = (struct collector_thread_args *) arg; int sem_posted = false; bool ret = true; @@ -483,14 +482,14 @@ tracer_thread(void *arg) // `thr_args', which is on the parent thread's stack, will become unusable. int perf_fd = thr_args->perf_fd; int stop_fd_rd = thr_args->stop_fd_rd; - struct perf_pt_trace *trace = thr_args->trace; + struct hwt_perf_trace *trace = thr_args->trace; void *aux_buf = thr_args->aux_buf; struct perf_event_mmap_page *base_header = thr_args->base_header; - struct perf_pt_cerror *err = thr_args->err; + struct hwt_cerror *err = thr_args->err; // Resume the interpreter loop. - if (sem_post(thr_args->tracer_init_sem) != 0) { - perf_pt_set_err(err, perf_pt_cerror_errno, errno); + if (sem_post(thr_args->collector_init_sem) != 0) { + hwt_set_cerr(err, hwt_cerror_errno, errno); ret = false; goto clean; } @@ -504,7 +503,7 @@ tracer_thread(void *arg) clean: if (!sem_posted) { - sem_post(thr_args->tracer_init_sem); + sem_post(thr_args->collector_init_sem); } return (void *) ret; @@ -517,18 +516,18 @@ tracer_thread(void *arg) */ /* - * Initialise a tracer context. + * Initialise a collector context. */ -struct tracer_ctx * -perf_pt_init_tracer(struct perf_pt_config *tr_conf, struct perf_pt_cerror *err) +struct hwt_perf_ctx * +hwt_perf_init_collector(struct hwt_perf_collector_config *tr_conf, struct hwt_cerror *err) { - struct tracer_ctx *tr_ctx = NULL; + struct hwt_perf_ctx *tr_ctx = NULL; bool failing = false; - // Allocate and initialise tracer context. + // Allocate and initialise collector context. tr_ctx = malloc(sizeof(*tr_ctx)); if (tr_ctx == NULL) { - perf_pt_set_err(err, perf_pt_cerror_errno, errno); + hwt_set_cerr(err, hwt_cerror_errno, errno); failing = true; goto clean; } @@ -541,7 +540,7 @@ perf_pt_init_tracer(struct perf_pt_config *tr_conf, struct perf_pt_cerror *err) // Obtain a file descriptor through which to speak to perf. tr_ctx->perf_fd = open_perf(tr_conf->aux_bufsize, err); if (tr_ctx->perf_fd == -1) { - perf_pt_set_err(err, perf_pt_cerror_errno, errno); + hwt_set_cerr(err, hwt_cerror_errno, errno); failing = true; goto clean; } @@ -571,7 +570,7 @@ perf_pt_init_tracer(struct perf_pt_config *tr_conf, struct perf_pt_cerror *err) tr_ctx->base_bufsize = (1 + tr_conf->data_bufsize) * page_size; tr_ctx->base_buf = mmap(NULL, tr_ctx->base_bufsize, PROT_WRITE, MAP_SHARED, tr_ctx->perf_fd, 0); if (tr_ctx->base_buf == MAP_FAILED) { - perf_pt_set_err(err, perf_pt_cerror_errno, errno); + hwt_set_cerr(err, hwt_cerror_errno, errno); failing = true; goto clean; } @@ -588,14 +587,14 @@ perf_pt_init_tracer(struct perf_pt_config *tr_conf, struct perf_pt_cerror *err) tr_ctx->aux_buf = mmap(NULL, base_header->aux_size, PROT_READ | PROT_WRITE, MAP_SHARED, tr_ctx->perf_fd, base_header->aux_offset); if (tr_ctx->aux_buf == MAP_FAILED) { - perf_pt_set_err(err, perf_pt_cerror_errno, errno); + hwt_set_cerr(err, hwt_cerror_errno, errno); failing = true; goto clean; } clean: if (failing && (tr_ctx != NULL)) { - perf_pt_free_tracer(tr_ctx, err); + hwt_perf_free_collector(tr_ctx, err); return NULL; } return tr_ctx; @@ -612,7 +611,7 @@ perf_pt_init_tracer(struct perf_pt_config *tr_conf, struct perf_pt_cerror *err) * Returns true on success or false otherwise. */ bool -perf_pt_start_tracer(struct tracer_ctx *tr_ctx, struct perf_pt_trace *trace, struct perf_pt_cerror *err) +hwt_perf_start_collector(struct hwt_perf_ctx *tr_ctx, struct hwt_perf_trace *trace, struct hwt_cerror *err) { int clean_sem = 0, clean_thread = 0; int ret = true; @@ -621,51 +620,51 @@ perf_pt_start_tracer(struct tracer_ctx *tr_ctx, struct perf_pt_trace *trace, str // // It has to be a pipe becuase it needs to be used in a poll(6) loop later. if (pipe(tr_ctx->stop_fds) != 0) { - perf_pt_set_err(err, perf_pt_cerror_errno, errno); + hwt_set_cerr(err, hwt_cerror_errno, errno); ret = false; goto clean; } - // Use a semaphore to wait for the tracer to be ready. - sem_t tracer_init_sem; - if (sem_init(&tracer_init_sem, 0, 0) == -1) { - perf_pt_set_err(err, perf_pt_cerror_errno, errno); + // Use a semaphore to wait for the collector to be ready. + sem_t collector_init_sem; + if (sem_init(&collector_init_sem, 0, 0) == -1) { + hwt_set_cerr(err, hwt_cerror_errno, errno); ret = false; goto clean; } clean_sem = 1; - // The tracer context contains an error struct for tracking any errors + // The collector context contains an error struct for tracking any errors // coming from inside the thread. We initialise it to "no errors". - tr_ctx->tracer_thread_err.kind = perf_pt_cerror_unused; - tr_ctx->tracer_thread_err.code = 0; + tr_ctx->collector_thread_err.kind = hwt_cerror_unused; + tr_ctx->collector_thread_err.code = 0; - // Build the arguments struct for the tracer thread. - struct tracer_thread_args thr_args = { + // Build the arguments struct for the collector thread. + struct collector_thread_args thr_args = { tr_ctx->perf_fd, tr_ctx->stop_fds[0], - &tracer_init_sem, + &collector_init_sem, trace, tr_ctx->aux_buf, tr_ctx->base_buf, // The header is the first region in the base buf. - &tr_ctx->tracer_thread_err, + &tr_ctx->collector_thread_err, }; // Spawn a thread to deal with copying out of the PT AUX buffer. - int rc = pthread_create(&tr_ctx->tracer_thread, NULL, tracer_thread, &thr_args); + int rc = pthread_create(&tr_ctx->collector_thread, NULL, collector_thread, &thr_args); if (rc) { - perf_pt_set_err(err, perf_pt_cerror_errno, errno); + hwt_set_cerr(err, hwt_cerror_errno, errno); ret = false; goto clean; } clean_thread = 1; - // Wait for the tracer to initialise, and check it didn't fail. + // Wait for the collector to initialise, and check it didn't fail. rc = -1; while (rc == -1) { - rc = sem_wait(&tracer_init_sem); + rc = sem_wait(&collector_init_sem); if ((rc == -1) && (errno != EINTR)) { - perf_pt_set_err(err, perf_pt_cerror_errno, errno); + hwt_set_cerr(err, hwt_cerror_errno, errno); ret = false; goto clean; } @@ -673,14 +672,14 @@ perf_pt_start_tracer(struct tracer_ctx *tr_ctx, struct perf_pt_trace *trace, str // Turn on tracing hardware. if (ioctl(tr_ctx->perf_fd, PERF_EVENT_IOC_ENABLE, 0) < 0) { - perf_pt_set_err(err, perf_pt_cerror_errno, errno); + hwt_set_cerr(err, hwt_cerror_errno, errno); ret = false; goto clean; } clean: - if ((clean_sem) && (sem_destroy(&tracer_init_sem) == -1)) { - perf_pt_set_err(err, perf_pt_cerror_errno, errno); + if ((clean_sem) && (sem_destroy(&collector_init_sem) == -1)) { + hwt_set_cerr(err, hwt_cerror_errno, errno); ret = false; } @@ -688,7 +687,7 @@ perf_pt_start_tracer(struct tracer_ctx *tr_ctx, struct perf_pt_trace *trace, str if (clean_thread) { close(tr_ctx->stop_fds[1]); // signals thread to stop. tr_ctx->stop_fds[1] = -1; - pthread_join(tr_ctx->tracer_thread, NULL); + pthread_join(tr_ctx->collector_thread, NULL); close(tr_ctx->stop_fds[0]); tr_ctx->stop_fds[0] = -1; } @@ -698,45 +697,45 @@ perf_pt_start_tracer(struct tracer_ctx *tr_ctx, struct perf_pt_trace *trace, str } /* - * Turn off the tracer. + * Turn off trace collection. * * Arguments: - * tr_ctx: The tracer context returned by perf_pt_start_tracer. + * tr_ctx: The tracer context returned by hwt_perf_start_collector. * * Returns true on success or false otherwise. */ bool -perf_pt_stop_tracer(struct tracer_ctx *tr_ctx, struct perf_pt_cerror *err) +hwt_perf_stop_collector(struct hwt_perf_ctx *tr_ctx, struct hwt_cerror *err) { int ret = true; // Turn off tracer hardware. if (ioctl(tr_ctx->perf_fd, PERF_EVENT_IOC_DISABLE, 0) < 0) { - perf_pt_set_err(err, perf_pt_cerror_errno, errno); + hwt_set_cerr(err, hwt_cerror_errno, errno); ret = false; } // Signal poll loop to end. if (close(tr_ctx->stop_fds[1]) == -1) { - perf_pt_set_err(err, perf_pt_cerror_errno, errno); + hwt_set_cerr(err, hwt_cerror_errno, errno); ret = false; } tr_ctx->stop_fds[1] = -1; // Wait for poll loop to exit. void *thr_exit; - if (pthread_join(tr_ctx->tracer_thread, &thr_exit) != 0) { - perf_pt_set_err(err, perf_pt_cerror_errno, errno); + if (pthread_join(tr_ctx->collector_thread, &thr_exit) != 0) { + hwt_set_cerr(err, hwt_cerror_errno, errno); ret = false; } if ((bool) thr_exit != true) { - perf_pt_set_err(err, tr_ctx->tracer_thread_err.kind, tr_ctx->tracer_thread_err.code); + hwt_set_cerr(err, tr_ctx->collector_thread_err.kind, tr_ctx->collector_thread_err.code); ret = false; } // Clean up if (close(tr_ctx->stop_fds[0]) == -1) { - perf_pt_set_err(err, perf_pt_cerror_errno, errno); + hwt_set_cerr(err, hwt_cerror_errno, errno); ret = false; } tr_ctx->stop_fds[0] = -1; @@ -745,29 +744,29 @@ perf_pt_stop_tracer(struct tracer_ctx *tr_ctx, struct perf_pt_cerror *err) } /* - * Clean up and free a tracer_ctx and its contents. + * Clean up and free a hwt_perf_ctx and its contents. * * Returns true on success or false otherwise. */ bool -perf_pt_free_tracer(struct tracer_ctx *tr_ctx, struct perf_pt_cerror *err) { +hwt_perf_free_collector(struct hwt_perf_ctx *tr_ctx, struct hwt_cerror *err) { int ret = true; if ((tr_ctx->aux_buf) && (munmap(tr_ctx->aux_buf, tr_ctx->aux_bufsize) == -1)) { - perf_pt_set_err(err, perf_pt_cerror_errno, errno); + hwt_set_cerr(err, hwt_cerror_errno, errno); ret = false; } if ((tr_ctx->base_buf) && (munmap(tr_ctx->base_buf, tr_ctx->base_bufsize) == -1)) { - perf_pt_set_err(err, perf_pt_cerror_errno, errno); + hwt_set_cerr(err, hwt_cerror_errno, errno); ret = false; } if (tr_ctx->stop_fds[1] != -1) { // If the write end of the pipe is still open, the thread is still running. close(tr_ctx->stop_fds[1]); // signals thread to stop. - if (pthread_join(tr_ctx->tracer_thread, NULL) != 0) { - perf_pt_set_err(err, perf_pt_cerror_errno, errno); + if (pthread_join(tr_ctx->collector_thread, NULL) != 0) { + hwt_set_cerr(err, hwt_cerror_errno, errno); ret = false; } } diff --git a/src/collect/perf/mod.rs b/src/collect/perf/mod.rs new file mode 100644 index 0000000..8793aec --- /dev/null +++ b/src/collect/perf/mod.rs @@ -0,0 +1,328 @@ +//! The Linux Perf trace collector. + +use super::PerfCollectorConfig; +use crate::{ + c_errors::PerfPTCError, + collect::{ThreadTraceCollector, TraceCollector}, + errors::HWTracerError, + Trace, +}; +use libc::{c_void, free, geteuid, malloc, size_t}; +use std::{convert::TryFrom, fs::File, io::Read, ptr, slice}; + +extern "C" { + // collect.c + fn hwt_perf_init_collector( + conf: *const PerfCollectorConfig, + err: *mut PerfPTCError, + ) -> *mut c_void; + fn hwt_perf_start_collector( + tr_ctx: *mut c_void, + trace: *mut PerfTrace, + err: *mut PerfPTCError, + ) -> bool; + fn hwt_perf_stop_collector(tr_ctx: *mut c_void, err: *mut PerfPTCError) -> bool; + fn hwt_perf_free_collector(tr_ctx: *mut c_void, err: *mut PerfPTCError) -> bool; +} + +const PERF_PERMS_PATH: &str = "/proc/sys/kernel/perf_event_paranoid"; + +#[derive(Debug)] +pub(crate) struct PerfTraceCollector { + config: PerfCollectorConfig, +} + +impl PerfTraceCollector { + pub(super) fn new(config: PerfCollectorConfig) -> Result + where + Self: Sized, + { + // Check for inavlid configuration. + fn power_of_2(v: size_t) -> bool { + (v & (v - 1)) == 0 + } + if !power_of_2(config.data_bufsize) { + return Err(HWTracerError::BadConfig(String::from( + "data_bufsize must be a positive power of 2", + ))); + } + if !power_of_2(config.aux_bufsize) { + return Err(HWTracerError::BadConfig(String::from( + "aux_bufsize must be a positive power of 2", + ))); + } + + // Check we have permissions to collect a PT trace using perf. + // + // Note that root always has permission. + // + // FIXME: We just assume that we are collecting a PT trace. + // https://github.com/ykjit/hwtracer/issues/100 + if !unsafe { geteuid() } == 0 { + let mut f = File::open(&PERF_PERMS_PATH)?; + let mut buf = String::new(); + f.read_to_string(&mut buf)?; + let perm = buf.trim().parse::()?; + if perm != -1 { + let msg = format!( + "Tracing not permitted: you must be root or {} must contain -1", + PERF_PERMS_PATH + ); + return Err(HWTracerError::Permissions(msg)); + } + } + + Ok(Self { config }) + } +} + +impl TraceCollector for PerfTraceCollector { + fn thread_collector(&self) -> Box { + Box::new(PerfThreadTraceCollector::new(self.config.clone())) + } +} + +/// A collector that uses the Linux Perf interface to Intel Processor Trace. +pub struct PerfThreadTraceCollector { + // The configuration for this collector. + config: PerfCollectorConfig, + // Opaque C pointer representing the collector context. + ctx: *mut c_void, + // The state of the collector. + is_tracing: bool, + // The trace currently being collected, or `None`. + trace: Option>, +} + +impl PerfThreadTraceCollector { + fn new(config: PerfCollectorConfig) -> Self { + Self { + config, + ctx: ptr::null_mut(), + is_tracing: false, + trace: None, + } + } +} + +impl Default for PerfThreadTraceCollector { + fn default() -> Self { + PerfThreadTraceCollector::new(PerfCollectorConfig::default()) + } +} + +impl Drop for PerfThreadTraceCollector { + fn drop(&mut self) { + if self.is_tracing { + // If we haven't stopped the collector already, stop it now. + self.stop_collector().unwrap(); + } + } +} + +impl ThreadTraceCollector for PerfThreadTraceCollector { + fn start_collector(&mut self) -> Result<(), HWTracerError> { + if self.is_tracing { + return Err(HWTracerError::AlreadyCollecting); + } + + // At the time of writing, we have to use a fresh Perf file descriptor to ensure traces + // start with a `PSB+` packet sequence. This is required for correct instruction-level and + // block-level decoding. Therefore we have to re-initialise for each new tracing session. + let mut cerr = PerfPTCError::new(); + self.ctx = unsafe { + hwt_perf_init_collector(&self.config as *const PerfCollectorConfig, &mut cerr) + }; + if self.ctx.is_null() { + return Err(cerr.into()); + } + + // It is essential we box the trace now to stop it from moving. If it were to move, then + // the reference which we pass to C here would become invalid. The interface to + // `stop_collector` needs to return a Box anyway, so it's no big deal. + // + // Note that the C code will mutate the trace's members directly. + let mut trace = Box::new(PerfTrace::new(self.config.initial_trace_bufsize)?); + let mut cerr = PerfPTCError::new(); + if !unsafe { hwt_perf_start_collector(self.ctx, &mut *trace, &mut cerr) } { + return Err(cerr.into()); + } + self.is_tracing = true; + self.trace = Some(trace); + Ok(()) + } + + fn stop_collector(&mut self) -> Result, HWTracerError> { + if !self.is_tracing { + return Err(HWTracerError::AlreadyStopped); + } + let mut cerr = PerfPTCError::new(); + let rc = unsafe { hwt_perf_stop_collector(self.ctx, &mut cerr) }; + self.is_tracing = false; + if !rc { + return Err(cerr.into()); + } + + let mut cerr = PerfPTCError::new(); + if !unsafe { hwt_perf_free_collector(self.ctx, &mut cerr) } { + return Err(cerr.into()); + } + self.ctx = ptr::null_mut(); + + let ret = self.trace.take().unwrap(); + self.trace = None; + Ok(ret as Box) + } +} + +/// A wrapper around a manually malloc/free'd buffer for holding an Intel PT trace. We've split +/// this out from PerfTrace so that we can mark just this raw pointer as `unsafe Send`. +#[repr(C)] +#[derive(Debug)] +struct PerfTraceBuf(*mut u8); + +/// We need to be able to transfer `PerfTraceBuf`s between threads to allow background +/// compilation. However, `PerfTraceBuf` wraps a raw pointer, which is not `Send`, so nor is +/// `PerfTraceBuf`. As long as we take great care to never: a) give out copies of the pointer to +/// the wider world, or b) dereference the pointer when we shouldn't, then we can manually (and +/// unsafely) mark the struct as being Send. +unsafe impl Send for PerfTrace {} + +/// An Intel PT trace, obtained via Linux perf. +#[repr(C)] +#[derive(Debug)] +pub struct PerfTrace { + // The trace buffer. + buf: PerfTraceBuf, + // The length of the trace (in bytes). + len: u64, + // `buf`'s allocation size (in bytes), <= `len`. + capacity: u64, +} + +impl PerfTrace { + /// Makes a new trace, initially allocating the specified number of bytes for the PT trace + /// packet buffer. + /// + /// The allocation is automatically freed by Rust when the struct falls out of scope. + pub(crate) fn new(capacity: size_t) -> Result { + let buf = unsafe { malloc(capacity) as *mut u8 }; + if buf.is_null() { + return Err(HWTracerError::Unknown); + } + Ok(Self { + buf: PerfTraceBuf(buf), + len: 0, + capacity: capacity as u64, + }) + } +} + +impl Trace for PerfTrace { + /// Write the raw trace packets into the specified file. + #[cfg(test)] + fn to_file(&self, file: &mut File) { + use std::io::prelude::*; + + let slice = unsafe { slice::from_raw_parts(self.buf.0 as *const u8, self.len as usize) }; + file.write_all(slice).unwrap(); + } + + fn bytes(&self) -> &[u8] { + unsafe { slice::from_raw_parts(self.buf.0, usize::try_from(self.len).unwrap()) } + } + + fn len(&self) -> usize { + usize::try_from(self.len).unwrap() + } + + #[cfg(test)] + fn capacity(&self) -> usize { + self.capacity as usize + } +} + +impl Drop for PerfTrace { + fn drop(&mut self) { + if !self.buf.0.is_null() { + unsafe { free(self.buf.0 as *mut c_void) }; + } + } +} + +#[cfg(test)] +mod tests { + use super::{PerfCollectorConfig, PerfThreadTraceCollector}; + use crate::{ + collect::{ + test_helpers, ThreadTraceCollector, TraceCollectorBuilder, TraceCollectorConfig, + TraceCollectorKind, + }, + errors::HWTracerError, + test_helpers::work_loop, + }; + + #[test] + fn basic_collection() { + test_helpers::basic_collection(PerfThreadTraceCollector::default()); + } + + #[test] + pub fn repeated_collection() { + test_helpers::repeated_collection(PerfThreadTraceCollector::default()); + } + + #[test] + pub fn already_started() { + test_helpers::already_started(PerfThreadTraceCollector::default()); + } + + #[test] + pub fn not_started() { + test_helpers::not_started(PerfThreadTraceCollector::default()); + } + + // Check that a long trace causes the trace buffer to reallocate. + #[test] + fn relloc_trace_buf() { + let start_bufsize = 512; + let mut config = PerfCollectorConfig::default(); + config.initial_trace_bufsize = start_bufsize; + let mut tracer = PerfThreadTraceCollector::new(config); + + tracer.start_collector().unwrap(); + let res = work_loop(10000); + let trace = tracer.stop_collector().unwrap(); + + println!("res: {}", res); // Stop over-optimisation. + assert!(trace.capacity() > start_bufsize); + } + + #[test] + fn test_config_bad_data_bufsize() { + let mut bldr = TraceCollectorBuilder::new().kind(TraceCollectorKind::Perf); + match bldr.config() { + TraceCollectorConfig::Perf(ref mut ppt_conf) => ppt_conf.data_bufsize = 3, + } + match bldr.build() { + Err(HWTracerError::BadConfig(s)) => { + assert_eq!(s, "data_bufsize must be a positive power of 2"); + } + _ => panic!(), + } + } + + #[test] + fn test_config_bad_aux_bufsize() { + let mut bldr = TraceCollectorBuilder::new().kind(TraceCollectorKind::Perf); + match bldr.config() { + TraceCollectorConfig::Perf(ref mut ppt_conf) => ppt_conf.aux_bufsize = 3, + } + match bldr.build() { + Err(HWTracerError::BadConfig(s)) => { + assert_eq!(s, "aux_bufsize must be a positive power of 2"); + } + _ => panic!(), + } + } +} diff --git a/src/backends/perf_pt/decode.c b/src/decode/libipt/decode.c similarity index 88% rename from src/backends/perf_pt/decode.c rename to src/decode/libipt/decode.c index e078599..a8fb05c 100644 --- a/src/backends/perf_pt/decode.c +++ b/src/decode/libipt/decode.c @@ -49,29 +49,53 @@ #include #include -#include "perf_pt_private.h" +#include "hwtracer_private.h" + +#define VDSO_NAME "linux-vdso.so.1" struct load_self_image_args { struct pt_image *image; int vdso_fd; char *vdso_filename; - struct perf_pt_cerror *err; + struct hwt_cerror *err; const char *current_exe; struct pt_image_section_cache *iscache; }; // Private prototypes. -static bool handle_events(struct pt_block_decoder *, int *, struct perf_pt_cerror *); +static bool handle_events(struct pt_block_decoder *, int *, struct hwt_cerror *); static bool load_self_image(struct load_self_image_args *); static int load_self_image_cb(struct dl_phdr_info *, size_t, void *); static bool block_is_terminated(struct pt_block *); // Public prototypes. -void *perf_pt_init_block_decoder(void *, uint64_t, int, char *, int *, - struct perf_pt_cerror *, const char *); -bool perf_pt_next_block(struct pt_block_decoder *, int *, uint64_t *, - uint64_t *, struct perf_pt_cerror *); -void perf_pt_free_block_decoder(struct pt_block_decoder *); +void *hwt_ipt_init_block_decoder(void *, uint64_t, int, char *, int *, + struct hwt_cerror *, const char *); +bool hwt_ipt_next_block(struct pt_block_decoder *, int *, uint64_t *, + uint64_t *, struct hwt_cerror *); +void hwt_ipt_free_block_decoder(struct pt_block_decoder *); + +/* + * Dump the VDSO code into the open file descriptor `fd`, starting at `vaddr` + * and of size `len` into a temp file. + * + * Returns true on success or false otherwise. + */ +bool +hwt_ipt_dump_vdso(int fd, uint64_t vaddr, size_t len, struct hwt_cerror *err) +{ + size_t written = 0; + while (written != len) { + int wrote = write(fd, (void *) vaddr + written, len - written); + if (wrote == -1) { + hwt_set_cerr(err, hwt_cerror_errno, errno); + return false; + } + written += wrote; + } + + return true; +} /* * Get ready to retrieve the basic blocks from a PT trace using the code of the @@ -93,8 +117,8 @@ void perf_pt_free_block_decoder(struct pt_block_decoder *); * Returns a pointer to a configured libipt block decoder or NULL on error. */ void * -perf_pt_init_block_decoder(void *buf, uint64_t len, int vdso_fd, char *vdso_filename, - int *decoder_status, struct perf_pt_cerror *err, +hwt_ipt_init_block_decoder(void *buf, uint64_t len, int vdso_fd, char *vdso_filename, + int *decoder_status, struct hwt_cerror *err, const char *current_exe) { bool failing = false; @@ -111,7 +135,7 @@ perf_pt_init_block_decoder(void *buf, uint64_t len, int vdso_fd, char *vdso_file struct pt_block_decoder *decoder = NULL; int rv = pt_cpu_read(&config.cpu); if (rv != pte_ok) { - perf_pt_set_err(err, perf_pt_cerror_ipt, -rv); + hwt_set_cerr(err, hwt_cerror_ipt, -rv); failing = true; goto clean; } @@ -120,7 +144,7 @@ perf_pt_init_block_decoder(void *buf, uint64_t len, int vdso_fd, char *vdso_file if (config.cpu.vendor) { rv = pt_cpu_errata(&config.errata, &config.cpu); if (rv < 0) { - perf_pt_set_err(err, perf_pt_cerror_ipt, -rv); + hwt_set_cerr(err, hwt_cerror_ipt, -rv); failing = true; goto clean; } @@ -129,7 +153,7 @@ perf_pt_init_block_decoder(void *buf, uint64_t len, int vdso_fd, char *vdso_file // Instantiate a decoder. decoder = pt_blk_alloc_decoder(&config); if (decoder == NULL) { - perf_pt_set_err(err, perf_pt_cerror_unknown, 0); + hwt_set_cerr(err, hwt_cerror_unknown, 0); failing = true; goto clean; } @@ -138,10 +162,10 @@ perf_pt_init_block_decoder(void *buf, uint64_t len, int vdso_fd, char *vdso_file *decoder_status = pt_blk_sync_forward(decoder); if (*decoder_status == -pte_eos) { // There were no blocks in the stream. The user will find out on next - // call to perf_pt_next_block(). + // call to hwt_ipt_next_block(). goto clean; } else if (*decoder_status < 0) { - perf_pt_set_err(err, perf_pt_cerror_ipt, -*decoder_status); + hwt_set_cerr(err, hwt_cerror_ipt, -*decoder_status); failing = true; goto clean; } @@ -149,7 +173,7 @@ perf_pt_init_block_decoder(void *buf, uint64_t len, int vdso_fd, char *vdso_file // Build and load a memory image from which to recover control flow. struct pt_image *image = pt_image_alloc(NULL); if (image == NULL) { - perf_pt_set_err(err, perf_pt_cerror_unknown, 0); + hwt_set_cerr(err, hwt_cerror_unknown, 0); failing = true; goto clean; } @@ -157,7 +181,7 @@ perf_pt_init_block_decoder(void *buf, uint64_t len, int vdso_fd, char *vdso_file // Use image cache to speed up decoding. struct pt_image_section_cache *iscache = pt_iscache_alloc(NULL); if(iscache == NULL) { - perf_pt_set_err(err, perf_pt_cerror_unknown, 0); + hwt_set_cerr(err, hwt_cerror_unknown, 0); failing = true; goto clean; } @@ -171,7 +195,7 @@ perf_pt_init_block_decoder(void *buf, uint64_t len, int vdso_fd, char *vdso_file rv = pt_blk_set_image(decoder, image); if (rv < 0) { - perf_pt_set_err(err, perf_pt_cerror_ipt, -rv); + hwt_set_cerr(err, hwt_cerror_ipt, -rv); failing = true; goto clean; } @@ -197,11 +221,11 @@ perf_pt_init_block_decoder(void *buf, uint64_t len, int vdso_fd, char *vdso_file * `*last_instr` are undefined. */ bool -perf_pt_next_block(struct pt_block_decoder *decoder, int *decoder_status, - uint64_t *first_instr, uint64_t *last_instr, struct perf_pt_cerror *err) { +hwt_ipt_next_block(struct pt_block_decoder *decoder, int *decoder_status, + uint64_t *first_instr, uint64_t *last_instr, struct hwt_cerror *err) { // If there are events pending, look at those first. if (handle_events(decoder, decoder_status, err) != true) { - // handle_events will have already called perf_pt_set_err(). + // handle_events will have already called hwt_set_cerr(). return false; } else if (*decoder_status & pts_eos) { // End of stream. @@ -222,7 +246,7 @@ perf_pt_next_block(struct pt_block_decoder *decoder, int *decoder_status, *last_instr = 0; while (!block_is_terminated(&block)) { if (handle_events(decoder, decoder_status, err) != true) { - // handle_events will have already called perf_pt_set_err(). + // handle_events will have already called hwt_set_cerr(). return false; } else if (*decoder_status & pts_eos) { // End of stream. @@ -230,7 +254,7 @@ perf_pt_next_block(struct pt_block_decoder *decoder, int *decoder_status, return true; } // It's possible at this point that we get notified of an event in the - // stream. This will be handled in the next call to `perf_pt_next_block`. + // stream. This will be handled in the next call to `hwt_ipt_next_block`. if ((*decoder_status != 0) && (*decoder_status != pts_event_pending)) { panic("Unexpected decoder status: %d", *decoder_status); } @@ -245,7 +269,7 @@ perf_pt_next_block(struct pt_block_decoder *decoder, int *decoder_status, return true; } else if (*decoder_status < 0) { // A real error. - perf_pt_set_err(err, perf_pt_cerror_ipt, -*decoder_status); + hwt_set_cerr(err, hwt_cerror_ipt, -*decoder_status); return false; } @@ -282,14 +306,14 @@ perf_pt_next_block(struct pt_block_decoder *decoder, int *decoder_status, * overflow. */ static bool -handle_events(struct pt_block_decoder *decoder, int *decoder_status, struct perf_pt_cerror *err) { +handle_events(struct pt_block_decoder *decoder, int *decoder_status, struct hwt_cerror *err) { bool ret = true; while(*decoder_status & pts_event_pending) { struct pt_event event; *decoder_status = pt_blk_event(decoder, &event, sizeof(event)); if (*decoder_status < 0) { - perf_pt_set_err(err, perf_pt_cerror_ipt, -*decoder_status); + hwt_set_cerr(err, hwt_cerror_ipt, -*decoder_status); return false; } @@ -311,7 +335,7 @@ handle_events(struct pt_block_decoder *decoder, int *decoder_status, struct perf case ptev_overflow: // We translate the overflow event to an overflow error for // Rust to detect later. - perf_pt_set_err(err, perf_pt_cerror_ipt, pte_overflow); + hwt_set_cerr(err, hwt_cerror_ipt, pte_overflow); ret = false; break; // Execution mode packet (MODE.Exec). @@ -412,7 +436,7 @@ load_self_image(struct load_self_image_args *args) } if (fsync(args->vdso_fd) == -1) { - perf_pt_set_err(args->err, perf_pt_cerror_errno, errno); + hwt_set_cerr(args->err, hwt_cerror_errno, errno); return false; } @@ -434,7 +458,7 @@ load_self_image_cb(struct dl_phdr_info *info, size_t size, void *data) (void) size; // Unused. Silence warning. struct load_self_image_args *args = data; - struct perf_pt_cerror *err = args->err; + struct hwt_cerror *err = args->err; const char *filename = info->dlpi_name; bool vdso = false; @@ -466,7 +490,7 @@ load_self_image_cb(struct dl_phdr_info *info, size_t size, void *data) // Discussion on adding libipt support for loading from memory here: // https://github.com/01org/processor-trace/issues/37 if (vdso) { - int rv = dump_vdso(args->vdso_fd, vaddr, phdr.p_filesz, err); + int rv = hwt_ipt_dump_vdso(args->vdso_fd, vaddr, phdr.p_filesz, err); if (!rv) { return 1; } @@ -483,7 +507,7 @@ load_self_image_cb(struct dl_phdr_info *info, size_t size, void *data) int rv = pt_image_add_cached(args->image, args->iscache, isid, NULL); if (rv < 0) { - perf_pt_set_err(err, perf_pt_cerror_ipt, -rv); + hwt_set_cerr(err, hwt_cerror_ipt, -rv); return 1; } } @@ -491,34 +515,22 @@ load_self_image_cb(struct dl_phdr_info *info, size_t size, void *data) return 0; } -/* - * Dump the VDSO code into the open file descriptor `fd`, starting at `vaddr` - * and of size `len` into a temp file. - * - * Returns true on success or false otherwise. - */ -bool -dump_vdso(int fd, uint64_t vaddr, size_t len, struct perf_pt_cerror *err) -{ - size_t written = 0; - while (written != len) { - int wrote = write(fd, (void *) vaddr + written, len - written); - if (wrote == -1) { - perf_pt_set_err(err, perf_pt_cerror_errno, errno); - return false; - } - written += wrote; - } - - return true; -} - /* * Free a block decoder and its image. */ void -perf_pt_free_block_decoder(struct pt_block_decoder *decoder) { +hwt_ipt_free_block_decoder(struct pt_block_decoder *decoder) { if (decoder != NULL) { pt_blk_free_decoder(decoder); } } + +/* + * Indicates if the specified error code is the overflow code. + * This exists to avoid copying (and keeping in sync) the ipt error code on the + * Rust side. + */ +bool +hwt_ipt_is_overflow_err(int err) { + return err == pte_overflow; +} diff --git a/src/decode/libipt/mod.rs b/src/decode/libipt/mod.rs new file mode 100644 index 0000000..d9fc573 --- /dev/null +++ b/src/decode/libipt/mod.rs @@ -0,0 +1,399 @@ +//! The libipt trace decoder. + +use crate::{c_errors::PerfPTCError, decode::TraceDecoder, errors::HWTracerError, Block, Trace}; +use libc::{c_char, c_int, c_void}; +use std::{convert::TryFrom, env, ffi::CString, os::fd::AsRawFd, ptr}; +use tempfile::NamedTempFile; + +extern "C" { + // decode.c + fn hwt_ipt_init_block_decoder( + buf: *const c_void, + len: u64, + vdso_fd: c_int, + vdso_filename: *const c_char, + decoder_status: *mut c_int, + err: *mut PerfPTCError, + current_exe: *const c_char, + ) -> *mut c_void; + fn hwt_ipt_next_block( + decoder: *mut c_void, + decoder_status: *mut c_int, + addr: *mut u64, + len: *mut u64, + err: *mut PerfPTCError, + ) -> bool; + fn hwt_ipt_free_block_decoder(decoder: *mut c_void); + // util.c + pub(crate) fn hwt_ipt_is_overflow_err(err: c_int) -> bool; + // libipt + pub(crate) fn pt_errstr(error_code: c_int) -> *const c_char; +} + +pub(crate) struct LibIPTTraceDecoder {} + +impl TraceDecoder for LibIPTTraceDecoder { + fn new() -> Self { + Self {} + } + + fn iter_blocks<'t>( + &'t self, + trace: &'t dyn Trace, + ) -> Box> + '_> { + let itr = LibIPTBlockIterator { + decoder: ptr::null_mut(), + decoder_status: 0, + vdso_tempfile: None, + trace, + errored: false, + }; + Box::new(itr) + } +} + +// Iterate over the blocks of an Intel PT trace using libipt. +struct LibIPTBlockIterator<'t> { + decoder: *mut c_void, // C-level libipt block decoder. + decoder_status: c_int, // Stores the current libipt-level status of the above decoder. + #[allow(dead_code)] // Rust doesn't know that this exists only to keep the file long enough. + vdso_tempfile: Option, // VDSO code stored temporarily. + trace: &'t dyn Trace, // The trace we are iterating over. + errored: bool, // Set to true when an error occurs, thus invalidating the iterator. +} + +impl<'t> LibIPTBlockIterator<'t> { + // Initialise the block decoder. + fn init_decoder(&mut self) -> Result<(), HWTracerError> { + // Make a temp file for the C code to write the VDSO code into. + // + // We have to do this because libipt lazily reads the code from the files you load into the + // image. We store it into `self` to ensure the file lives as long as the iterator. + let vdso_tempfile = NamedTempFile::new()?; + // File name of a NamedTempFile should always be valid UTF-8, unwrap() below can't fail. + let vdso_filename = CString::new(vdso_tempfile.path().to_str().unwrap())?; + let mut cerr = PerfPTCError::new(); + let decoder = unsafe { + hwt_ipt_init_block_decoder( + self.trace.bytes().as_ptr() as *const c_void, + u64::try_from(self.trace.len()).unwrap(), + vdso_tempfile.as_raw_fd(), + vdso_filename.as_ptr(), + &mut self.decoder_status, + &mut cerr, + // FIXME: current_exe() isn't reliable. We should find another way to do this. + CString::new(env::current_exe().unwrap().to_str().unwrap()) + .unwrap() + .as_c_str() + .as_ptr() as *const c_char, + ) + }; + if decoder.is_null() { + return Err(cerr.into()); + } + + vdso_tempfile.as_file().sync_all()?; + self.decoder = decoder; + self.vdso_tempfile = Some(vdso_tempfile); + Ok(()) + } +} + +impl<'t> Drop for LibIPTBlockIterator<'t> { + fn drop(&mut self) { + unsafe { hwt_ipt_free_block_decoder(self.decoder) }; + } +} + +impl<'t> Iterator for LibIPTBlockIterator<'t> { + type Item = Result; + + fn next(&mut self) -> Option { + // There was an error in a previous iteration. + if self.errored { + return None; + } + + // Lazily initialise the block decoder. + if self.decoder.is_null() { + if let Err(e) = self.init_decoder() { + self.errored = true; + return Some(Err(e)); + } + } + + let mut first_instr = 0; + let mut last_instr = 0; + let mut cerr = PerfPTCError::new(); + let rv = unsafe { + hwt_ipt_next_block( + self.decoder, + &mut self.decoder_status, + &mut first_instr, + &mut last_instr, + &mut cerr, + ) + }; + if !rv { + self.errored = true; // This iterator is unusable now. + return Some(Err(HWTracerError::from(cerr))); + } + if first_instr == 0 { + None // End of packet stream. + } else { + Some(Ok(Block::new(first_instr, last_instr))) + } + } +} + +#[cfg(test)] +mod tests { + use super::{LibIPTBlockIterator, PerfPTCError}; + use crate::{ + collect::{ + perf::PerfTrace, test_helpers::trace_closure, ThreadTraceCollector, + TraceCollectorBuilder, + }, + decode::{test_helpers, TraceDecoderKind}, + errors::HWTracerError, + test_helpers::work_loop, + Block, Trace, + }; + use libc::{c_int, size_t, PF_X, PT_LOAD}; + use std::{convert::TryFrom, env, os::fd::AsRawFd, process::Command, ptr}; + use tempfile::NamedTempFile; + + extern "C" { + fn hwt_ipt_dump_vdso(fd: c_int, vaddr: u64, len: size_t, err: &PerfPTCError) -> bool; + } + + const VDSO_FILENAME: &str = "linux-vdso.so.1"; + + // Gets the ptxed arguments required to decode a trace for the current process. + // + // Returns a vector of arguments and a handle to a temproary file containing the VDSO code. The + // caller must make sure that this file lives long enough for ptxed to run (temp files are + // removed when they fall out of scope). + fn self_ptxed_args(trace_filename: &str) -> (Vec, NamedTempFile) { + let ptxed_args = vec![ + "--cpu", + "auto", + "--block-decoder", + "--block:end-on-call", + "--block:end-on-jump", + "--block:show-blocks", + "--pt", + trace_filename, + ]; + let mut ptxed_args: Vec = ptxed_args.into_iter().map(|e| String::from(e)).collect(); + + // Make a temp file to dump the VDSO code into. This is necessary because ptxed cannot read + // code from a virtual address: it can only load from file. + let vdso_tempfile = NamedTempFile::new().unwrap(); + + let exe = env::current_exe().unwrap(); + for obj in phdrs::objects() { + let obj_name = obj.name().to_str().unwrap(); + let mut filename = if cfg!(target_os = "linux") && obj_name == "" { + exe.to_str().unwrap() + } else { + obj_name + }; + + for hdr in obj.iter_phdrs() { + if hdr.type_() != PT_LOAD || hdr.flags() & PF_X == 0 { + continue; // Only look at loadable and executable segments. + } + + let vaddr = obj.addr() + hdr.vaddr(); + let offset; + + if filename == VDSO_FILENAME { + let cerr = PerfPTCError::new(); + if !unsafe { + hwt_ipt_dump_vdso( + vdso_tempfile.as_raw_fd(), + vaddr, + size_t::try_from(hdr.memsz()).unwrap(), + &cerr, + ) + } { + panic!("failed to dump vdso"); + } + filename = vdso_tempfile.path().to_str().unwrap(); + offset = 0; + } else { + offset = hdr.offset(); + } + + let raw_arg = format!( + "{}:0x{:x}-0x{:x}:0x{:x}", + filename, + offset, + hdr.offset() + hdr.filesz(), + vaddr + ); + ptxed_args.push("--raw".to_owned()); + ptxed_args.push(raw_arg); + } + } + + (ptxed_args, vdso_tempfile) + } + + /* + * Determine if the given x86_64 assembler mnemonic should terminate a block. + * + * Mnemonic assumed to be lower case. + */ + fn instr_terminates_block(instr: &str) -> bool { + assert!(instr.find(|c: char| !c.is_lowercase()).is_none()); + match instr { + // JMP or Jcc are the only instructions beginning with 'j'. + m if m.starts_with("j") => true, + "call" | "ret" | "loop" | "loope" | "loopne" | "syscall" | "sysenter" | "sysexit" + | "sysret" | "xabort" => true, + _ => false, + } + } + + // Given a trace, use ptxed to get a vector of block start vaddrs. + fn get_expected_blocks(trace: &Box) -> Vec { + // Write the trace out to a temp file so ptxed can decode it. + let mut tmpf = NamedTempFile::new().unwrap(); + trace.to_file(&mut tmpf.as_file_mut()); + let (args, _vdso_tempfile) = self_ptxed_args(tmpf.path().to_str().unwrap()); + + let out = Command::new(env!("PTXED")).args(&args).output().unwrap(); + let outstr = String::from_utf8(out.stdout).unwrap(); + if !out.status.success() { + let errstr = String::from_utf8(out.stderr).unwrap(); + panic!( + "ptxed failed:\nInvocation----------\n{:?}\n \ + Stdout\n------\n{}Stderr\n------\n{}", + args, outstr, errstr + ); + } + + let mut block_start = false; + let mut block_vaddrs = Vec::new(); + let mut last_instr: Option<&str> = None; + for line in outstr.lines() { + let line = line.trim(); + if line.contains("error") { + panic!("error line in ptxed output:\n{}", line); + } else if line.starts_with("[") { + // It's a special line, e.g. [enabled], [disabled], [block]... + if line == "[block]" + && (last_instr.is_none() || instr_terminates_block(last_instr.unwrap())) + { + // The next insruction we see will be the start of a block. + block_start = true; + } + } else { + // It's a regular instruction line. + if block_start { + // This instruction is the start of a block. + let vaddr_s = line.split_whitespace().next().unwrap(); + let vaddr = u64::from_str_radix(vaddr_s, 16).unwrap(); + block_vaddrs.push(Block::new(vaddr, 0)); + block_start = false; + } + last_instr = Some(line.split_whitespace().nth(1).unwrap()); + } + } + + block_vaddrs + } + + // Trace a closure and then decode it and check the block iterator agrees with ptxed. + fn trace_and_check_blocks(tracer: &mut dyn ThreadTraceCollector, f: F) + where + F: FnOnce() -> u64, + { + let trace = trace_closure(tracer, f); + let expects = get_expected_blocks(&trace); + test_helpers::test_expected_blocks(trace, TraceDecoderKind::LibIPT, expects.iter()); + } + + // Check that the block decoder agrees with the reference implementation in ptxed. + #[test] + fn versus_ptxed_short_trace() { + let tracer = TraceCollectorBuilder::new().build().unwrap(); + trace_and_check_blocks(&mut *tracer.thread_collector(), || work_loop(10)); + } + + // Check that the block decoder agrees ptxed on a (likely) empty trace; + #[test] + fn versus_ptxed_empty_trace() { + let tracer = TraceCollectorBuilder::new().build().unwrap(); + trace_and_check_blocks(&mut *tracer.thread_collector(), || work_loop(0)); + } + + // Check that our block decoder deals with traces involving the VDSO correctly. + #[test] + fn versus_ptxed_vdso() { + use libc::{clock_gettime, timespec, CLOCK_MONOTONIC}; + + let tracer = TraceCollectorBuilder::new().build().unwrap(); + trace_and_check_blocks(&mut *tracer.thread_collector(), || { + let mut res = 0; + let mut tv = timespec { + tv_sec: 0, + tv_nsec: 0, + }; + for _ in 1..100 { + // clock_gettime(2) is in the Linux VDSO. + let rv = unsafe { clock_gettime(CLOCK_MONOTONIC, &mut tv) }; + assert_eq!(rv, 0); + res += tv.tv_sec; + } + res as u64 + }); + } + + // Check that the block decoder agrees with ptxed on long trace. + //#[ignore] // Decoding long traces is slow. + #[test] + fn versus_ptxed_long_trace() { + let tracer = TraceCollectorBuilder::new().build().unwrap(); + trace_and_check_blocks(&mut *tracer.thread_collector(), || work_loop(3000)); + } + + // Check that a block iterator returns none after an error. + #[test] + fn error_stops_block_iter() { + // A zero-sized trace will lead to an error. + let trace = PerfTrace::new(0).unwrap(); + let mut itr = LibIPTBlockIterator { + decoder: ptr::null_mut(), + decoder_status: 0, + vdso_tempfile: None, + trace: &trace, + errored: false, + }; + + // First we expect a libipt error. + match itr.next() { + Some(Err(HWTracerError::Custom(e))) => { + assert!(e.to_string().starts_with("libipt error: ")) + } + _ => panic!(), + } + // And now the iterator is invalid, and should return None. + for _ in 0..128 { + assert!(itr.next().is_none()); + } + } + + #[test] + fn ten_times_as_many_blocks() { + let tr1 = TraceCollectorBuilder::new().build().unwrap(); + let tr2 = TraceCollectorBuilder::new().build().unwrap(); + test_helpers::ten_times_as_many_blocks( + &mut *tr1.thread_collector(), + &mut *tr2.thread_collector(), + TraceDecoderKind::LibIPT, + ); + } +} diff --git a/src/decode/mod.rs b/src/decode/mod.rs new file mode 100644 index 0000000..efe31d0 --- /dev/null +++ b/src/decode/mod.rs @@ -0,0 +1,143 @@ +//! Trace decoders. + +use crate::{errors::HWTracerError, Block, Trace}; +use strum::IntoEnumIterator; +use strum_macros::EnumIter; + +#[cfg(decoder_libipt)] +pub(crate) mod libipt; +#[cfg(decoder_libipt)] +use libipt::LibIPTTraceDecoder; + +#[derive(Clone, Copy, Debug, EnumIter)] +pub enum TraceDecoderKind { + LibIPT, +} + +impl TraceDecoderKind { + fn default_for_platform() -> Option { + for kind in Self::iter() { + if Self::match_platform(&kind).is_ok() { + return Some(kind); + } + } + None + } + + /// Returns `Ok` if the this decoder kind is appropriate for the current platform. + fn match_platform(&self) -> Result<(), HWTracerError> { + match self { + Self::LibIPT => { + #[cfg(not(decoder_libipt))] + return Err(HWTracerError::DecoderUnavailable(Self::LibIPT)); + #[cfg(decoder_libipt)] + return Ok(()); + } + } + } +} + +pub trait TraceDecoder { + // Create the trace decoder. + fn new() -> Self + where + Self: Sized; + + /// Iterate over the blocks of the trace. + fn iter_blocks<'t>( + &'t self, + trace: &'t dyn Trace, + ) -> Box> + '_>; +} + +pub struct TraceDecoderBuilder { + kind: TraceDecoderKind, +} + +impl TraceDecoderBuilder { + /// Create a new TraceDecoderBuilder using an appropriate defaults. + pub fn new() -> Self { + Self { + kind: TraceDecoderKind::default_for_platform().unwrap(), + } + } + + /// Select the kind of trace decoder. + pub fn kind(mut self, kind: TraceDecoderKind) -> Self { + self.kind = kind; + self + } + + /// Build the trace decoder. + /// + /// An error is returned if the requested decoder is inappropriate for the platform or the + /// requested decoder was not compiled in to hwtracer. + pub fn build(self) -> Result, HWTracerError> { + self.kind.match_platform()?; + match self.kind { + TraceDecoderKind::LibIPT => Ok(Box::new(LibIPTTraceDecoder::new())), + } + } +} + +/// Decoder agnostic tests and helper routines live here. +#[cfg(test)] +mod test_helpers { + use super::{TraceDecoder, TraceDecoderBuilder, TraceDecoderKind}; + use crate::{ + collect::{test_helpers::trace_closure, ThreadTraceCollector}, + test_helpers::work_loop, + Block, Trace, + }; + use std::slice::Iter; + + // Helper to check an expected list of blocks matches what we actually got. + pub fn test_expected_blocks( + trace: Box, + decoder_kind: TraceDecoderKind, + mut expect_iter: Iter, + ) { + let dec = TraceDecoderBuilder::new() + .kind(decoder_kind) + .build() + .unwrap(); + let mut got_iter = dec.iter_blocks(&*trace); + loop { + let expect = expect_iter.next(); + let got = got_iter.next(); + if expect.is_none() || got.is_none() { + break; + } + assert_eq!( + got.unwrap().unwrap().first_instr(), + expect.unwrap().first_instr() + ); + } + // Check that both iterators were the same length. + assert!(expect_iter.next().is_none()); + assert!(got_iter.next().is_none()); + } + + // Trace two loops, one 10x larger than the other, then check the proportions match the number + // of block the trace passes through. + pub fn ten_times_as_many_blocks( + tracer1: &mut dyn ThreadTraceCollector, + tracer2: &mut dyn ThreadTraceCollector, + decoder_kind: TraceDecoderKind, + ) { + let trace1 = trace_closure(tracer1, || work_loop(10)); + let trace2 = trace_closure(tracer2, || work_loop(100)); + + let dec: Box = TraceDecoderBuilder::new() + .kind(decoder_kind) + .build() + .unwrap(); + + let ct1 = dec.iter_blocks(&*trace1).count(); + let ct2 = dec.iter_blocks(&*trace2).count(); + + // Should be roughly 10x more blocks in trace2. It won't be exactly 10x, due to the stuff + // we trace either side of the loop itself. On a smallish trace, that will be significant. + assert!(ct2 > ct1 * 9); + } +} diff --git a/src/errors.rs b/src/errors.rs index c9a3bf4..bb602d5 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,8 +1,10 @@ -use crate::backends::BackendKind; +use crate::{collect::TraceCollectorKind, decode::TraceDecoderKind}; use libc::{c_int, strerror}; use std::error::Error; -use std::ffi::CStr; +use std::ffi::{self, CStr}; use std::fmt::{self, Display, Formatter}; +use std::io; +use std::num::ParseIntError; #[derive(Debug)] pub enum HWTracerError { @@ -11,13 +13,14 @@ pub enum HWTracerError { // may succeed. NoHWSupport(String), // The hardware doesn't support a required feature. Not fatal for the // same reason as `Permissions`. This may be non-fatal depending - // upon whether the consumer could (e.g.) try a different backend. - BackendUnavailable(BackendKind), // This backend was not compiled in to hwtracer. - Permissions(String), // Tracing is not permitted using this backend. - Errno(c_int), // Something went wrong in C code. - AlreadyTracing, // Trying to start a tracer that's already tracing. - AlreadyStopped, // Trying to stop a tracer that's not tracing. - BadConfig(String), // The tracer configuration was invalid. + // upon whether the consumer could (e.g.) try a different collector. + CollectorUnavailable(TraceCollectorKind), // This collector was not compiled in to hwtracer. + DecoderUnavailable(TraceDecoderKind), // This decoder was not compiled in to hwtracer. + Permissions(String), // Permission denied. + Errno(c_int), // Something went wrong in C code. + AlreadyCollecting, // Trying to start an already collecting collector. + AlreadyStopped, // Trying to stop a not-currently-active collector. + BadConfig(String), // Configuration was invalid. Custom(Box), // All other errors can be nested here, however, don't rely on this // for performance since the `Box` incurs a runtime cost. Unknown, // An unknown error. Used sparingly in C code which doesn't set errno. @@ -27,7 +30,12 @@ impl Display for HWTracerError { fn fmt(&self, f: &mut Formatter) -> fmt::Result { match *self { HWTracerError::HWBufferOverflow => write!(f, "Hardware trace buffer overflow"), - HWTracerError::BackendUnavailable(ref s) => write!(f, "Backend unavailble: {:?}", s), + HWTracerError::CollectorUnavailable(ref s) => { + write!(f, "Trace collector unavailble: {:?}", s) + } + HWTracerError::DecoderUnavailable(ref s) => { + write!(f, "Trace decoder unavailble: {:?}", s) + } HWTracerError::NoHWSupport(ref s) => write!(f, "{}", s), HWTracerError::Permissions(ref s) => write!(f, "{}", s), HWTracerError::Errno(n) => { @@ -35,10 +43,10 @@ impl Display for HWTracerError { let err_str = unsafe { CStr::from_ptr(strerror(n)) }; write!(f, "{}", err_str.to_str().unwrap()) } - HWTracerError::AlreadyTracing => { - write!(f, "Can't start a tracer that's already tracing") + HWTracerError::AlreadyCollecting => { + write!(f, "Can't start a collector that's already collecting") } - HWTracerError::AlreadyStopped => write!(f, "Can't stop a tracer that's not tracing"), + HWTracerError::AlreadyStopped => write!(f, "Can't stop an inactice collector"), HWTracerError::BadConfig(ref s) => write!(f, "{}", s), HWTracerError::Custom(ref bx) => write!(f, "{}", bx), HWTracerError::Unknown => write!(f, "Unknown error"), @@ -54,10 +62,11 @@ impl Error for HWTracerError { fn cause(&self) -> Option<&dyn Error> { match *self { HWTracerError::HWBufferOverflow => None, - HWTracerError::BackendUnavailable(_) => None, + HWTracerError::CollectorUnavailable(_) => None, + HWTracerError::DecoderUnavailable(_) => None, HWTracerError::NoHWSupport(_) => None, HWTracerError::Permissions(_) => None, - HWTracerError::AlreadyTracing => None, + HWTracerError::AlreadyCollecting => None, HWTracerError::AlreadyStopped => None, HWTracerError::BadConfig(_) => None, HWTracerError::Errno(_) => None, @@ -66,3 +75,21 @@ impl Error for HWTracerError { } } } + +impl From for HWTracerError { + fn from(err: io::Error) -> Self { + HWTracerError::Custom(Box::new(err)) + } +} + +impl From for HWTracerError { + fn from(err: ffi::NulError) -> Self { + HWTracerError::Custom(Box::new(err)) + } +} + +impl From for HWTracerError { + fn from(err: ParseIntError) -> Self { + HWTracerError::Custom(Box::new(err)) + } +} diff --git a/src/backends/perf_pt/perf_pt_private.h b/src/hwtracer_private.h similarity index 77% rename from src/backends/perf_pt/perf_pt_private.h rename to src/hwtracer_private.h index dc1d500..1774d6c 100644 --- a/src/backends/perf_pt/perf_pt_private.h +++ b/src/hwtracer_private.h @@ -35,33 +35,25 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -/* - * This header contains items which are shared amongst multiple C files in the - * perf_pt backend. - */ - -#ifndef __PERF_PT_PRIVATE_H -#define __PERF_PT_PRIVATE_H +#ifndef __HWT_PRIVATE_H +#define __HWT_PRIVATE_H #include #include #include -enum perf_pt_cerror_kind { - perf_pt_cerror_unused, - perf_pt_cerror_unknown, - perf_pt_cerror_errno, - perf_pt_cerror_ipt, +enum hwt_cerror_kind { + hwt_cerror_unused, + hwt_cerror_unknown, + hwt_cerror_errno, + hwt_cerror_ipt, }; -struct perf_pt_cerror { - enum perf_pt_cerror_kind kind; // What sort of error is this? - int code; // The error code itself. +struct hwt_cerror { + enum hwt_cerror_kind kind; // What sort of error is this? + int code; // The error code itself. }; -bool dump_vdso(int, uint64_t, size_t, struct perf_pt_cerror *); -void perf_pt_set_err(struct perf_pt_cerror *, int, int); - -#define VDSO_NAME "linux-vdso.so.1" +void hwt_set_cerr(struct hwt_cerror *, int, int); #endif diff --git a/src/lib.rs b/src/lib.rs index 76923c2..25a9ddf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,50 +3,48 @@ mod block; pub use block::Block; -pub mod backends; +mod c_errors; +pub mod collect; +pub mod decode; pub mod errors; -#[cfg(test)] -mod test_helpers; pub use errors::HWTracerError; use std::fmt::Debug; #[cfg(test)] use std::fs::File; -use std::iter::Iterator; /// Represents a generic trace. /// -/// Each backend has its own concrete implementation. +/// Each trace decoder has its own concrete implementation. pub trait Trace: Debug + Send { - /// Iterate over the blocks of the trace. - fn iter_blocks<'t: 'i, 'i>( - &'t self, - ) -> Box> + 'i>; + fn bytes(&self) -> &[u8]; /// Get the capacity of the trace in bytes. #[cfg(test)] fn capacity(&self) -> usize; + // Get the size of the trace in bytes. + fn len(&self) -> usize; + /// Dump the trace to the specified filename. /// - /// The exact format varies per-backend. + /// The exact format varies depending on what kind of trace it is. #[cfg(test)] fn to_file(&self, file: &mut File); } -/// The interface offered by all tracer types. -pub trait Tracer: Send + Sync { - /// Return a `ThreadTracer` for tracing the current thread. - fn thread_tracer(&self) -> Box; -} +#[cfg(test)] +mod test_helpers { + use std::time::SystemTime; -pub trait ThreadTracer { - /// Start recording a trace. - /// - /// Tracing continues until [stop_tracing](trait.ThreadTracer.html#method.stop_tracing) is called. - fn start_tracing(&mut self) -> Result<(), HWTracerError>; - /// Turns off the tracer. - /// - /// [start_tracing](trait.ThreadTracer.html#method.start_tracing) must have been called prior. - fn stop_tracing(&mut self) -> Result, HWTracerError>; + // A loop that does some work that we can use to build a trace. + #[inline(never)] + pub fn work_loop(iters: u64) -> u64 { + let mut res = 0; + for _ in 0..iters { + // Computation which stops the compiler from eliminating the loop. + res += SystemTime::now().elapsed().unwrap().subsec_nanos() as u64; + } + res + } } diff --git a/src/test_helpers.rs b/src/test_helpers.rs deleted file mode 100644 index 8724491..0000000 --- a/src/test_helpers.rs +++ /dev/null @@ -1,111 +0,0 @@ -//! Test helpers. -//! -//! Each struct implementing the [ThreadTracer](trait.ThreadTracer.html) trait should include tests -//! calling the following helpers. - -#![cfg(test)] - -use super::{Block, HWTracerError, ThreadTracer}; -use crate::Trace; -use std::slice::Iter; -use std::time::SystemTime; - -// A loop that does some work that we can use to build a trace. -#[inline(never)] -pub fn work_loop(iters: u64) -> u64 { - let mut res = 0; - for _ in 0..iters { - // Computation which stops the compiler from eliminating the loop. - res += SystemTime::now().elapsed().unwrap().subsec_nanos() as u64; - } - res -} - -// Trace a closure that returns a u64. -pub fn trace_closure(tracer: &mut dyn ThreadTracer, f: F) -> Box -where - F: FnOnce() -> u64, -{ - tracer.start_tracing().unwrap(); - let res = f(); - let trace = tracer.stop_tracing().unwrap(); - println!("traced closure with result: {}", res); // To avoid over-optimisation. - trace -} - -// Check that starting and stopping a tracer works. -pub fn test_basic_usage(mut tracer: T) -where - T: ThreadTracer, -{ - trace_closure(&mut tracer, || work_loop(500)); -} - -// Check that repeated usage of the same tracer works. -pub fn test_repeated_tracing(mut tracer: T) -where - T: ThreadTracer, -{ - for _ in 0..10 { - trace_closure(&mut tracer, || work_loop(500)); - } -} - -// Check that starting a tracer twice makes an appropriate error. -pub fn test_already_started(mut tracer: T) -where - T: ThreadTracer, -{ - tracer.start_tracing().unwrap(); - match tracer.start_tracing() { - Err(HWTracerError::AlreadyTracing) => (), - _ => panic!(), - }; - tracer.stop_tracing().unwrap(); -} - -// Check that stopping an unstarted tracer makes an appropriate error. -pub fn test_not_started(mut tracer: T) -where - T: ThreadTracer, -{ - match tracer.stop_tracing() { - Err(HWTracerError::AlreadyStopped) => (), - _ => panic!(), - }; -} - -// Helper to check an expected list of blocks matches what we actually got. -pub fn test_expected_blocks(trace: Box, mut expect_iter: Iter) { - let mut got_iter = trace.iter_blocks(); - loop { - let expect = expect_iter.next(); - let got = got_iter.next(); - if expect.is_none() || got.is_none() { - break; - } - assert_eq!( - got.unwrap().unwrap().first_instr(), - expect.unwrap().first_instr() - ); - } - // Check that both iterators were the same length. - assert!(expect_iter.next().is_none()); - assert!(got_iter.next().is_none()); -} - -// Trace two loops, one 10x larger than the other, then check the proportions match the number -// of block the trace passes through. -#[cfg(perf_pt_test)] -pub fn test_ten_times_as_many_blocks(mut tracer1: T, mut tracer2: T) -where - T: ThreadTracer, -{ - let trace1 = trace_closure(&mut tracer1, || work_loop(10)); - let trace2 = trace_closure(&mut tracer2, || work_loop(100)); - - // Should be roughly 10x more blocks in trace2. It won't be exactly 10x, due to the stuff - // we trace either side of the loop itself. On a smallish trace, that will be significant. - let (ct1, ct2) = (trace1.iter_blocks().count(), trace2.iter_blocks().count()); - assert!(ct2 > ct1 * 9); -} diff --git a/src/backends/perf_pt/util.c b/src/util.c similarity index 85% rename from src/backends/perf_pt/util.c rename to src/util.c index 07cd9ec..1b5991d 100644 --- a/src/backends/perf_pt/util.c +++ b/src/util.c @@ -37,26 +37,16 @@ #include #include -#include "perf_pt_private.h" +#include "hwtracer_private.h" /* * Sets the error information (if not already set). */ void -perf_pt_set_err(struct perf_pt_cerror *err, int kind, int code) { +hwt_set_cerr(struct hwt_cerror *err, int kind, int code) { // Only set the error info if we would not be overwriting an earlier error. - if (err->kind == perf_pt_cerror_unused) { + if (err->kind == hwt_cerror_unused) { err->kind = kind; err->code = code; } } - -/* - * Indicates if the specified error code is the overflow code. - * This exists to avoid copying (and keeping in sync) the ipt error code on the - * Rust side. - */ -bool -perf_pt_is_overflow_err(int err) { - return err == pte_overflow; -} diff --git a/tests/pt_chdir_rel.rs b/tests/pt_chdir_rel.rs index fc0f062..5984df8 100644 --- a/tests/pt_chdir_rel.rs +++ b/tests/pt_chdir_rel.rs @@ -2,8 +2,9 @@ /// a relative path. /// /// This may seem like a rather arbitrary thing to check, but this test was derived from a real -/// bug: the PT backend was trying to create a libipt image using a (stale) relative path for the -/// main binary's object. +/// bug: the LibIPT decoder was trying to create a libipt image using a (stale) relative path for +/// the main binary's object. +use hwtracer::{collect::TraceCollectorBuilder, decode::TraceDecoderBuilder}; use std::{env, path::PathBuf, time::SystemTime}; #[inline(never)] @@ -16,8 +17,6 @@ pub fn work_loop(iters: u64) -> u64 { res } -// FIXME: check if the chip actually supports PT. -#[cfg(perf_pt)] #[test] fn pt_chdir_rel() { let arg0 = env::args().next().unwrap(); @@ -43,18 +42,17 @@ fn pt_chdir_rel() { // When we get here, we have a process that was invoked with a relative path. - use hwtracer::backends::perf_pt::PerfPTThreadTracer; - use hwtracer::ThreadTracer; + let tcol = TraceCollectorBuilder::new().build().unwrap(); + let mut thr_col = tcol.thread_collector(); - let mut tracer = PerfPTThreadTracer::default(); - - tracer.start_tracing().unwrap(); + thr_col.start_collector().unwrap(); println!("{}", work_loop(env::args().len() as u64)); - let trace = tracer.stop_tracing().unwrap(); + let trace = thr_col.stop_collector().unwrap(); // Now check that the trace decoder can still find its objects after we change dir. + let tdec = TraceDecoderBuilder::new().build().unwrap(); env::set_current_dir("/").unwrap(); - for b in trace.iter_blocks() { + for b in tdec.iter_blocks(&*trace) { b.unwrap(); // this would error if the decoder was confused by changing dir. } }