linux_memutils/
iomem.rs

1// SPDX-FileCopyrightText: Benedikt Vollmerhaus <benedikt@vollmerhaus.org>
2// SPDX-License-Identifier: MIT
3/*!
4Parsing of the physical memory map provided by `/proc/iomem`.
5
6The `/proc/iomem` file exposes the kernel's resource tree and thus a map of
7physical memory to user space, making it very useful for gracefully reading
8specific regions of memory from [`/dev/mem`].
9
10# Background
11
12The Linux kernel maintains a _resource tree_ with the memory address ranges
13allocated to every resource (RAM, devices, and so on).
14
15The first additions to this tree are made during early boot when the system
16firmware supplies its initial memory map to the kernel via [E820] (BIOS) or
17[GetMemoryMap()] (UEFI). The kernel will practically always modify this map
18further (based on known quirks or custom `memmap` overrides, for instance)
19before registering its specified memory regions in the tree.
20
21Additional address ranges are allocated for device [MMIO], so the tree will
22contain not just entries for the above memory map (with [ACPI Address Range
23Types] such as `Reserved`) but also more arbitrarily named devices (such as
24`IOAPIC 0` or `PCI Bus <ID>`).
25
26## Excerpt of `/proc/iomem`
27
28> Notice how the address range names include both human-readable ACPI types
29> and MMIO devices.
30
31```text
3200000000-00000fff : Reserved            // Real-Mode Address Space (< 1 MiB)
3300001000-0009efff : System RAM
340009f000-0009ffff : Reserved
35000e0000-000fffff : Reserved
36  000a0000-000effff : PCI Bus 0000:00
37  000f0000-000fffff : System ROM
3800100000-09bfffff : System RAM          // Extended Memory (> 1 MiB)
39[~]
40cad7e000-cbd7dfff : ACPI Non-volatile Storage
41  cbc37000-cbc37fff : USBC000:00
42cbd7e000-cbdfdfff : ACPI Tables
43[~]
44fc000000-fdffffff : PCI Bus 0000:00
45  [~ Other devices on the PCIe bus ~]
46  fd900000-fd9fffff : PCI Bus 0000:01
47    fd900000-fd903fff : 0000:01:00.0
48      fd900000-fd903fff : nvme
49  fdf00000-fdf7ffff : amd_iommu
50feb00000-feb00007 : SB800 TCO
51fec00000-fec003ff : IOAPIC 0
52[~]
53100000000-72e2fffff : System RAM
54  38a400000-38b7fffff : Kernel code
55  38b800000-38c532fff : Kernel rodata
56  38c600000-38c88cf7f : Kernel data
57  38d20e000-38d5fffff : Kernel bss
58```
59
60[`/dev/mem`]: https://man7.org/linux/man-pages/man4/mem.4.html
61[E820]: https://uefi.org/specs/ACPI/6.5/15_System_Address_Map_Interfaces.html#int-15h-e820h-query-system-address-map
62[GetMemoryMap()]: https://uefi.org/specs/ACPI/6.5/15_System_Address_Map_Interfaces.html#uefi-getmemorymap-boot-services-function
63[MMIO]: https://docs.kernel.org/driver-api/device-io.html#memory-mapped-io
64[ACPI Address Range Types]: https://uefi.org/specs/ACPI/6.5/15_System_Address_Map_Interfaces.html#address-range-types
65*/
66use std::fmt::{self, Debug, Display, Formatter};
67use std::sync::LazyLock;
68use std::{fs, io};
69
70use regex_lite::Regex;
71use thiserror::Error;
72
73/// A region in physical memory as indicated in the memory map.
74#[derive(Debug, PartialEq)]
75pub struct MemoryRegion {
76    pub start_address: usize,
77    pub end_address: usize,
78    pub region_type: MemoryRegionType,
79}
80
81impl MemoryRegion {
82    /// Return the length of this memory region in bytes.
83    #[must_use]
84    pub fn size(&self) -> usize {
85        self.end_address - self.start_address
86    }
87}
88
89impl Display for MemoryRegion {
90    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
91        write!(
92            f,
93            "[{:#018x}-{:#018x}] ({:?})",
94            self.start_address, self.end_address, self.region_type
95        )
96    }
97}
98
99/// The types of memory address ranges distinguished by the kernel.
100///
101/// These largely correspond to the _ACPI Address Range Types_ as defined in
102/// the kernel's [`e820_type`] enum (with some minor changes).
103///
104/// UEFI provides even more fine-grained _Memory Types_, but the kernel maps
105/// those to the basic ACPI types in [`do_add_efi_memmap`] (according to the
106/// specified [UEFI–ACPI Mapping]).
107///
108/// [`e820_type`]: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/arch/x86/include/asm/e820/types.h?h=v6.12#n10
109/// [`do_add_efi_memmap`]: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/arch/x86/platform/efi/efi.c?h=v6.12#n121
110/// [UEFI–ACPI Mapping]: https://uefi.org/specs/ACPI/6.5/15_System_Address_Map_Interfaces.html#uefi-memory-types-and-mapping-to-acpi-address-range-types
111#[derive(Debug, PartialEq)]
112pub enum MemoryRegionType {
113    Usable,
114    Reserved,
115    SoftReserved,
116    AcpiData,
117    AcpiNvs,
118    Unusable,
119    Persistent,
120    /// An E820 type not known to the kernel.
121    Unknown,
122
123    /// Any type not part of the ACPI specification, such as an MMIO range.
124    NonAcpi(String),
125}
126
127impl From<&str> for MemoryRegionType {
128    /// Return the enum variant for a given address range type as printed
129    /// by the kernel.
130    ///
131    /// This mapping is derived from the [`e820_type_to_string`] function.
132    ///
133    /// [`e820_type_to_string`]: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/arch/x86/kernel/e820.c?h=v6.12#n1063
134    fn from(s: &str) -> Self {
135        match s {
136            "System RAM" => Self::Usable,
137            "Reserved" => Self::Reserved,
138            "Soft Reserved" => Self::SoftReserved,
139            "ACPI Tables" => Self::AcpiData,
140            "ACPI Non-volatile Storage" => Self::AcpiNvs,
141            "Unusable memory" => Self::Unusable,
142            "Persistent Memory" | "Persistent Memory (legacy)" => Self::Persistent,
143            "Unknown E820 type" => Self::Unknown,
144
145            _ => Self::NonAcpi(s.into()),
146        }
147    }
148}
149
150impl Display for MemoryRegionType {
151    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
152        write!(f, "{self:?}")
153    }
154}
155
156#[derive(Error, Debug, PartialEq)]
157pub enum ParseError {
158    #[error("line of memory map is not in iomem format: '{0}'")]
159    InvalidFormat(String),
160    #[error("line of memory map has invalid start address: {0}")]
161    InvalidStartAddress(String),
162    #[error("line of memory map has invalid end address: {0}")]
163    InvalidEndAddress(String),
164}
165
166/// Directly read and parse `/proc/iomem` to a vector of [`MemoryRegion`]s.
167///
168/// # Errors
169///
170/// This function will return an error if `/proc/iomem` could not be read.
171///
172/// # Panics
173///
174/// This function panics if the file contains unexpected lines or ones with
175/// invalid memory addresses. As this should **never** happen on a standard
176/// Linux kernel, it may indicate a somewhat corrupt system state.
177#[allow(clippy::module_name_repetitions)]
178pub fn parse_proc_iomem() -> io::Result<Vec<MemoryRegion>> {
179    let contents = fs::read_to_string("/proc/iomem")?;
180    let memory_regions =
181        parse_iomem_map(&contents).expect("/proc/iomem should contain only valid lines");
182    Ok(memory_regions)
183}
184
185/// Parse the given `iomem`-style memory map to a vector of [`MemoryRegion`]s.
186///
187/// # Errors
188///
189/// This function will return an error if the memory map could not be parsed.
190pub fn parse_iomem_map(content: &str) -> Result<Vec<MemoryRegion>, ParseError> {
191    let mut memory_regions = Vec::new();
192
193    for line in content.lines().filter(|l| !l.is_empty()) {
194        // This parsing cannot yet represent the resource hierarchy, so
195        // ignore sub-resources rather than potentially misrepresenting
196        // the memory map (e.g. for a "Reserved" within an MMIO region)
197        if line.starts_with(' ') {
198            continue;
199        }
200
201        let region = parse_iomem_map_line(line)?;
202        memory_regions.push(region);
203    }
204
205    Ok(memory_regions)
206}
207
208/// A regex for lines of an `iomem`-style memory map.
209static IOMEM_MAP_LINE_REGEX: LazyLock<Regex> =
210    LazyLock::new(|| Regex::new(r"(\w+)-(\w+) : (.+)").unwrap());
211
212/// Parse the given line of an `iomem`-style memory map to a [`MemoryRegion`].
213fn parse_iomem_map_line(line: &str) -> Result<MemoryRegion, ParseError> {
214    let Some((_full, [start_address, end_address, region_type])) = IOMEM_MAP_LINE_REGEX
215        .captures(line)
216        .map(|caps| caps.extract())
217    else {
218        return Err(ParseError::InvalidFormat(line.into()));
219    };
220
221    let Ok(start_address) = usize::from_str_radix(start_address, 16) else {
222        return Err(ParseError::InvalidStartAddress(start_address.into()));
223    };
224    let Ok(end_address) = usize::from_str_radix(end_address, 16) else {
225        return Err(ParseError::InvalidEndAddress(end_address.into()));
226    };
227
228    let memory_region = MemoryRegion {
229        start_address,
230        end_address,
231        region_type: MemoryRegionType::from(region_type),
232    };
233
234    Ok(memory_region)
235}
236
237#[cfg(test)]
238#[allow(clippy::unreadable_literal)]
239mod tests {
240    use super::*;
241    use indoc::indoc;
242
243    mod memory_region {
244        use super::*;
245
246        #[test]
247        fn size_returns_correct_size_of_region_in_bytes() {
248            let region = MemoryRegion {
249                start_address: 0x00100000,
250                end_address: 0x09bfffff,
251                region_type: MemoryRegionType::Usable,
252            };
253            assert_eq!(region.size(), 162_529_279);
254        }
255
256        #[test]
257        fn is_formatted_as_expected_human_readable_string() {
258            let region = MemoryRegion {
259                start_address: 0x00100000,
260                end_address: 0x09bfffff,
261                region_type: MemoryRegionType::Usable,
262            };
263            assert_eq!(
264                region.to_string(),
265                "[0x0000000000100000-0x0000000009bfffff] (Usable)"
266            );
267        }
268    }
269
270    mod parse_iomem_map {
271        use super::*;
272
273        /// A dummy `iomem`-style memory map comprising all ACPI types.
274        const PROC_IOMEM_MAP: &str = indoc! {"
275            00000080-000000ff : System RAM
276            00000100-000001ff : Reserved
277              00000180-000001bf : PCI Bus 0000:00
278              000001c0-000001ff : System ROM
279            00000200-000003ff : Soft Reserved
280            00000400-000007ff : ACPI Tables
281            00000800-00000fff : ACPI Non-volatile Storage
282            00001000-00001fff : Unusable memory
283            00002000-00003fff : Persistent Memory
284            00004000-00007fff : Persistent Memory (legacy)
285            00008000-0000ffff : Unknown E820 type
286            00010000-0001ffff : PCI ECAM 0000
287              00010000-0001ffff : Reserved
288                00010000-0001ffff : pnp 00:00
289        "};
290
291        #[test]
292        fn returns_vector_of_expected_memory_regions() {
293            let regions = parse_iomem_map(PROC_IOMEM_MAP);
294            let expected_regions = vec![
295                (0x000080, MemoryRegionType::Usable),
296                (0x000100, MemoryRegionType::Reserved),
297                (0x000200, MemoryRegionType::SoftReserved),
298                (0x000400, MemoryRegionType::AcpiData),
299                (0x000800, MemoryRegionType::AcpiNvs),
300                (0x001000, MemoryRegionType::Unusable),
301                (0x002000, MemoryRegionType::Persistent),
302                (0x004000, MemoryRegionType::Persistent),
303                (0x008000, MemoryRegionType::Unknown),
304                (0x010000, MemoryRegionType::NonAcpi("PCI ECAM 0000".into())),
305            ]
306            .into_iter()
307            .map(|(start_address, region_type)| MemoryRegion {
308                start_address,
309                end_address: start_address * 2 - 1,
310                region_type,
311            })
312            .collect();
313
314            assert_eq!(regions, Ok(expected_regions));
315        }
316    }
317
318    mod parse_iomem_map_line {
319        use super::*;
320
321        #[test]
322        fn returns_expected_memory_region_for_line_with_acpi_type() {
323            let result = parse_iomem_map_line("00100000-09bfffff : System RAM");
324            assert_eq!(
325                result,
326                Ok(MemoryRegion {
327                    start_address: 0x00100000,
328                    end_address: 0x09bfffff,
329                    region_type: MemoryRegionType::Usable,
330                })
331            );
332        }
333
334        #[test]
335        fn returns_expected_memory_region_for_line_with_non_acpi_type() {
336            let result = parse_iomem_map_line("d0000000-f7ffffff : PCI Bus 0000:00");
337            assert_eq!(
338                result,
339                Ok(MemoryRegion {
340                    start_address: 0xd0000000,
341                    end_address: 0xf7ffffff,
342                    region_type: MemoryRegionType::NonAcpi("PCI Bus 0000:00".into()),
343                })
344            );
345        }
346
347        #[test]
348        fn returns_invalid_format_error_if_line_is_not_in_iomem_format() {
349            let invalid_line = "This is not an iomem memory map line.";
350            assert_eq!(
351                parse_iomem_map_line(invalid_line),
352                Err(ParseError::InvalidFormat(invalid_line.into()))
353            );
354        }
355
356        #[test]
357        fn returns_invalid_start_address_error_if_start_address_is_not_hex() {
358            assert_eq!(
359                parse_iomem_map_line("0000yyyy-00000fff : Reserved"),
360                Err(ParseError::InvalidStartAddress("0000yyyy".into()))
361            );
362        }
363
364        #[test]
365        fn returns_invalid_end_address_error_if_end_address_is_not_hex() {
366            assert_eq!(
367                parse_iomem_map_line("00000000-0000zzzz : Reserved"),
368                Err(ParseError::InvalidEndAddress("0000zzzz".into()))
369            );
370        }
371    }
372}