Skip to main content

iptr_edge_analyzer/memory_reader/
perf_mmap.rs

1//! This module contains a memory reader that re-construct memory content
2//! from `perf.data` files.
3
4use std::{
5    fs::File,
6    path::{Path, PathBuf},
7};
8
9use super::ReadMemory;
10use iptr_perf_pt_reader::PerfMmap2Header;
11use memmap2::{Mmap, MmapOptions};
12use thiserror::Error;
13
14/// Memory reader that re-construct memory content from `perf.data` files.
15///
16/// To create a memory reader from perf.data, you should make sure
17/// that all binary images involved in the process that be recorded
18/// into perf.data are not modified and still in their original paths
19/// (perf.data only records the mmap operation for the target process,
20/// we use the arguments of mmap to reconstruct the target memory)
21///
22/// You should not use this struct if your `perf.data` also records kernel
23/// traces, since the kernel memory information would not be recorded in
24/// the `perf.data` file.
25pub struct PerfMmapBasedMemoryReader {
26    /// Recorded mmapped contents
27    entries: Vec<MmappedEntry>,
28}
29
30/// Information of mmapped entries.
31///
32/// This struct can be retrieved by [`PerfMmapBasedMemoryReader::mmapped_entries`]
33pub struct MmappedEntry {
34    mmap: Mmap,
35    virtual_address: u64,
36}
37
38impl MmappedEntry {
39    /// Get the content of mmapped entry
40    #[must_use]
41    pub fn content(&self) -> &[u8] {
42        &self.mmap
43    }
44
45    /// Get the virtual address of mmapped entry when
46    /// Intel PT trace is recorded
47    #[must_use]
48    pub fn virtual_address(&self) -> u64 {
49        self.virtual_address
50    }
51}
52
53/// Error type for [`PerfMmapBasedMemoryReader`] in the
54/// implementation of [`ReadMemory`]
55#[derive(Debug, Error)]
56pub enum PerfMmapBasedMemoryReaderError {
57    /// The queried address is not mmapped
58    #[error("Not mmapped area {0:#x} accessed")]
59    NotMmapped(u64),
60}
61
62/// Error type for [`PerfMmapBasedMemoryReader`], only used in
63/// [`PerfMmapBasedMemoryReader::new`].
64#[derive(Debug, Error)]
65pub enum PerfMmapBasedMemoryReaderCreateError {
66    /// Failed to open mmapped file
67    #[error("Failed to open mmapped file {}: {source}", path.display())]
68    FileIo {
69        /// Path of target file
70        path: PathBuf,
71        /// Source of error
72        #[source]
73        source: std::io::Error,
74    },
75    /// The mmapped file is not long enough to match the length
76    /// recorded in the `perf.data`.
77    #[error("Target file {} is shorter than mapped moment: expected {expect_length} bytes, but got {real_length} bytes.", path.display())]
78    FileTooShort {
79        /// Path of target file
80        path: PathBuf,
81        /// Length recorded in `perf.data`
82        expect_length: u64,
83        /// Real length of target file
84        real_length: u64,
85    },
86}
87
88impl PerfMmapBasedMemoryReader {
89    /// Create a memory reader from mmap2 headers in perf.data.
90    ///
91    /// Some special mmapped regions (e.g. VDSO pages) will be skipped
92    /// since we cannot get its content.
93    #[expect(clippy::cast_possible_truncation)]
94    pub fn new(
95        mmap2_headers: &[PerfMmap2Header],
96    ) -> Result<Self, PerfMmapBasedMemoryReaderCreateError> {
97        let mut entries = Vec::with_capacity(mmap2_headers.len());
98
99        for mmap2_header in mmap2_headers {
100            let filename_path = Path::new(&mmap2_header.filename);
101            if !filename_path.is_absolute() {
102                // For example, VDSO
103                log::warn!(
104                    "Mmapped filename {} is not absolute path, skip.",
105                    mmap2_header.filename
106                );
107                continue;
108            }
109            let file = File::open(filename_path).map_err(|io_err| {
110                PerfMmapBasedMemoryReaderCreateError::FileIo {
111                    path: filename_path.to_path_buf(),
112                    source: io_err,
113                }
114            })?;
115            // SAFETY: check the safety requirements of memmap2 documentation
116            let mmap_res = unsafe {
117                MmapOptions::default()
118                    .len(mmap2_header.len as usize)
119                    .offset(mmap2_header.pgoff)
120                    .map(&file)
121            };
122            let mmap = mmap_res.map_err(|io_err| PerfMmapBasedMemoryReaderCreateError::FileIo {
123                path: filename_path.to_path_buf(),
124                source: io_err,
125            })?;
126            if mmap.len() as u64 != mmap2_header.len {
127                return Err(PerfMmapBasedMemoryReaderCreateError::FileTooShort {
128                    path: filename_path.to_path_buf(),
129                    expect_length: mmap2_header.len,
130                    real_length: mmap.len() as u64,
131                });
132            }
133            log::trace!(
134                "Mmapped {:016x}--{:016x}\t{}",
135                mmap2_header.addr,
136                mmap2_header.addr.saturating_add(mmap2_header.len),
137                mmap2_header.filename
138            );
139            entries.push(MmappedEntry {
140                mmap,
141                virtual_address: mmap2_header.addr,
142            });
143        }
144
145        // Sort entries so that we can binary search it
146        entries.sort_by_key(|entry| entry.virtual_address);
147
148        Ok(Self { entries })
149    }
150
151    /// Get mmapped entries.
152    ///
153    /// The entries are guaranteed to be sorted by virtual addresses
154    #[must_use]
155    pub fn mmapped_entries(&self) -> &[MmappedEntry] {
156        &self.entries
157    }
158}
159
160impl ReadMemory for PerfMmapBasedMemoryReader {
161    type Error = PerfMmapBasedMemoryReaderError;
162
163    fn at_decode_begin(&mut self) -> Result<(), Self::Error> {
164        Ok(())
165    }
166
167    #[expect(clippy::cast_possible_truncation)]
168    fn read_memory<T>(
169        &mut self,
170        address: u64,
171        size: usize,
172        callback: impl FnOnce(&[u8]) -> T,
173    ) -> std::result::Result<T, Self::Error> {
174        let pos = match self
175            .entries
176            .binary_search_by_key(&address, |entry| entry.virtual_address)
177        {
178            Ok(pos) => pos,
179            Err(pos) => {
180                if pos == 0 {
181                    return Err(PerfMmapBasedMemoryReaderError::NotMmapped(address));
182                }
183                pos - 1
184            }
185        };
186        // SAFETY: pos is generated by binary search, no possibility to out of bounds
187        debug_assert!(pos < self.entries.len(), "Unexpected pos out of bounds!");
188        let entry = unsafe { self.entries.get_unchecked(pos) };
189        let start_offset = address - entry.virtual_address;
190        let read_size = std::cmp::min(size, entry.mmap.len().saturating_sub(start_offset as usize));
191        if read_size == 0 {
192            return Err(PerfMmapBasedMemoryReaderError::NotMmapped(address));
193        }
194        let Some(mem) = entry
195            .mmap
196            .get((start_offset as usize)..((start_offset as usize).saturating_add(read_size)))
197        else {
198            return Err(PerfMmapBasedMemoryReaderError::NotMmapped(
199                address.saturating_add(read_size as u64) - 1,
200            ));
201        };
202        Ok(callback(mem))
203    }
204}