Skip to main content

procutils_pmap/
lib.rs

1use clap::Parser;
2use cols::Cols;
3use procfs::process::{MMPermissions, MMapPath, MemoryMap, Process};
4use procutils_common::{MAX_TERM_WIDTH, man::ManContent};
5use std::process::ExitCode;
6
7pub const MAN: ManContent = ManContent {
8    description: Some(include_str!("../man/description.man")),
9    extra_sections: &[
10        (
11            "FIELD DESCRIPTIONS",
12            include_str!("../man/field_descriptions.man"),
13        ),
14        ("EXAMPLES", include_str!("../man/examples.man")),
15        ("NOTES", include_str!("../man/notes.man")),
16        ("DIVERGENCES", include_str!("../man/divergences.man")),
17        ("SEE ALSO", include_str!("../man/see_also.man")),
18    ],
19};
20
21/// Report memory map of a process.
22#[derive(Parser)]
23#[command(
24    name = "pmap",
25    version,
26    about,
27    max_term_width = MAX_TERM_WIDTH,
28    override_usage = "pmap [options] PID [PID ...]"
29)]
30pub struct Args {
31    /// Show the extended format.
32    #[arg(short = 'x', long)]
33    extended: bool,
34
35    /// Show the device format.
36    #[arg(short, long)]
37    device: bool,
38
39    /// Do not display some header or footer lines.
40    #[arg(short, long)]
41    quiet: bool,
42
43    /// Show full path to files in the mapping column.
44    #[arg(short = 'p', long)]
45    show_path: bool,
46
47    /// Show kernel-supplied mapping names (`[vdso]`, `[vsyscall]`,
48    /// `[stack]`, `[heap]`, `[vvar]`) verbatim, without our default
49    /// padded form (`[ vdso ]` etc).
50    #[arg(short = 'k', long = "use-kernel-name")]
51    use_kernel_name: bool,
52
53    /// Limit results to the given address range. Accepts a single
54    /// address or a comma-separated `LOW,HIGH` pair. Hex with or
55    /// without `0x` prefix.
56    #[arg(short = 'A', long = "range", value_name = "LOW[,HIGH]")]
57    range: Option<String>,
58
59    /// Process IDs.
60    #[arg(required = true)]
61    pid: Vec<i32>,
62}
63
64/// Parse `-A LOW` or `-A LOW,HIGH`. A single value sets HIGH = LOW
65/// (interpret as "any mapping containing this address").
66fn parse_range(s: &str) -> Result<(u64, u64), String> {
67    fn parse_addr(a: &str) -> Result<u64, String> {
68        let stripped = a.trim_start_matches("0x").trim_start_matches("0X");
69        u64::from_str_radix(stripped, 16)
70            .map_err(|_| format!("invalid address: {a}"))
71    }
72    match s.split_once(',') {
73        Some((lo, hi)) => Ok((parse_addr(lo)?, parse_addr(hi)?)),
74        None => {
75            let v = parse_addr(s)?;
76            Ok((v, v))
77        }
78    }
79}
80
81/// True if `[map_start, map_end)` overlaps the inclusive range `[low, high]`.
82fn in_range(map_start: u64, map_end: u64, low: u64, high: u64) -> bool {
83    map_start <= high && map_end > low
84}
85
86#[derive(Cols)]
87struct DefaultRow {
88    #[column(header = "Address")]
89    address: String,
90    #[column(right, header = "Kbytes")]
91    kbytes: String,
92    #[column(header = "Mode")]
93    mode: String,
94    #[column(header = "Mapping")]
95    mapping: String,
96}
97
98#[derive(Cols)]
99struct ExtendedRow {
100    #[column(header = "Address")]
101    address: String,
102    #[column(right, header = "Kbytes")]
103    kbytes: String,
104    #[column(right, header = "RSS")]
105    rss: String,
106    #[column(right, header = "Dirty")]
107    dirty: String,
108    #[column(header = "Mode")]
109    mode: String,
110    #[column(header = "Mapping")]
111    mapping: String,
112}
113
114#[derive(Cols)]
115struct DeviceRow {
116    #[column(header = "Address")]
117    address: String,
118    #[column(right, header = "Kbytes")]
119    kbytes: String,
120    #[column(header = "Mode")]
121    mode: String,
122    #[column(header = "Offset")]
123    offset: String,
124    #[column(header = "Device")]
125    device: String,
126    #[column(header = "Mapping")]
127    mapping: String,
128}
129
130fn format_perms_pmap(perms: MMPermissions) -> String {
131    let r = if perms.contains(MMPermissions::READ) {
132        'r'
133    } else {
134        '-'
135    };
136    let w = if perms.contains(MMPermissions::WRITE) {
137        'w'
138    } else {
139        '-'
140    };
141    let x = if perms.contains(MMPermissions::EXECUTE) {
142        'x'
143    } else {
144        '-'
145    };
146    let s = if perms.contains(MMPermissions::SHARED) {
147        's'
148    } else {
149        '-'
150    };
151    // pmap uses 5-char format: "rwx-s" or "rwx--"
152    format!("{r}{w}{x}{s}-")
153}
154
155fn format_mapping(
156    path: &MMapPath,
157    show_path: bool,
158    kernel_name: bool,
159) -> String {
160    match path {
161        MMapPath::Path(p) => {
162            if show_path {
163                p.display().to_string()
164            } else {
165                p.file_name()
166                    .map(|f| f.to_string_lossy().into_owned())
167                    .unwrap_or_else(|| p.display().to_string())
168            }
169        }
170        MMapPath::Heap if kernel_name => "[heap]".into(),
171        MMapPath::Stack if kernel_name => "[stack]".into(),
172        MMapPath::TStack(tid) if kernel_name => format!("[stack:{tid}]"),
173        MMapPath::Vdso if kernel_name => "[vdso]".into(),
174        MMapPath::Vvar if kernel_name => "[vvar]".into(),
175        MMapPath::Vsyscall if kernel_name => "[vsyscall]".into(),
176        MMapPath::Anonymous if kernel_name => String::new(),
177        MMapPath::Vsys(id) if kernel_name => format!("[sysv:{id}]"),
178        MMapPath::Rollup if kernel_name => "[rollup]".into(),
179        MMapPath::Other(s) if kernel_name => format!("[{s}]"),
180
181        MMapPath::Heap => "[ anon ]".into(),
182        MMapPath::Stack => "[ stack ]".into(),
183        MMapPath::TStack(tid) => format!("[ stack:{tid} ]"),
184        MMapPath::Vdso => "[ vdso ]".into(),
185        MMapPath::Vvar => "[ vvar ]".into(),
186        MMapPath::Vsyscall => "[ anon ]".into(),
187        MMapPath::Anonymous => "[ anon ]".into(),
188        MMapPath::Vsys(_) => "[ sysv ]".into(),
189        MMapPath::Rollup => "[ rollup ]".into(),
190        MMapPath::Other(s) => format!("[ {s} ]"),
191    }
192}
193
194fn kbytes(map: &MemoryMap) -> u64 {
195    (map.address.1 - map.address.0) / 1024
196}
197
198pub fn run(args: Args) -> ExitCode {
199    let range = match args.range.as_deref().map(parse_range) {
200        Some(Ok(r)) => Some(r),
201        Some(Err(e)) => {
202            eprintln!("pmap: {e}");
203            return ExitCode::from(2);
204        }
205        None => None,
206    };
207
208    let mut not_found = false;
209
210    for (i, &pid) in args.pid.iter().enumerate() {
211        if i > 0 {
212            println!();
213        }
214
215        let proc = match Process::new(pid) {
216            Ok(p) => p,
217            Err(e) => {
218                eprintln!("pmap: {pid}: {e}");
219                not_found = true;
220                continue;
221            }
222        };
223
224        let cmdline = proc.cmdline().unwrap_or_default().join(" ");
225
226        println!("{pid}:   {cmdline}");
227
228        if args.extended {
229            show_extended(&proc, &args, range, pid, &mut not_found);
230        } else if args.device {
231            show_device(&proc, &args, range, pid, &mut not_found);
232        } else {
233            show_default(&proc, &args, range, pid, &mut not_found);
234        }
235    }
236
237    if not_found {
238        ExitCode::from(42)
239    } else {
240        ExitCode::SUCCESS
241    }
242}
243
244fn show_default(
245    proc: &Process,
246    args: &Args,
247    range: Option<(u64, u64)>,
248    pid: i32,
249    not_found: &mut bool,
250) {
251    let maps = match proc.maps() {
252        Ok(m) => m,
253        Err(e) => {
254            eprintln!("pmap: {pid}: {e}");
255            *not_found = true;
256            return;
257        }
258    };
259
260    let filtered: Vec<&MemoryMap> = maps
261        .0
262        .iter()
263        .filter(|m| match range {
264            Some((lo, hi)) => in_range(m.address.0, m.address.1, lo, hi),
265            None => true,
266        })
267        .collect();
268
269    let rows: Vec<DefaultRow> = filtered
270        .iter()
271        .map(|m| DefaultRow {
272            address: format!("{:016x}", m.address.0),
273            kbytes: format!("{}K", kbytes(m)),
274            mode: format_perms_pmap(m.perms),
275            mapping: format_mapping(
276                &m.pathname,
277                args.show_path,
278                args.use_kernel_name,
279            ),
280        })
281        .collect();
282
283    let total_kb: u64 = filtered.iter().copied().map(kbytes).sum();
284
285    print_rows(&rows);
286
287    if !args.quiet {
288        println!(" total          {total_kb:>5}K");
289    }
290}
291
292fn show_extended(
293    proc: &Process,
294    args: &Args,
295    range: Option<(u64, u64)>,
296    pid: i32,
297    not_found: &mut bool,
298) {
299    let maps = match proc.smaps() {
300        Ok(m) => m,
301        Err(e) => {
302            eprintln!("pmap: {pid}: {e}");
303            *not_found = true;
304            return;
305        }
306    };
307
308    let mut rows: Vec<ExtendedRow> = Vec::new();
309    let mut total_kb = 0u64;
310    let mut total_rss = 0u64;
311    let mut total_dirty = 0u64;
312
313    for m in &maps.0 {
314        if let Some((lo, hi)) = range
315            && !in_range(m.address.0, m.address.1, lo, hi)
316        {
317            continue;
318        }
319        let kb = kbytes(m);
320        let rss = m.extension.map.get("Rss").copied().unwrap_or(0) / 1024;
321        let dirty =
322            (m.extension.map.get("Private_Dirty").copied().unwrap_or(0)
323                + m.extension.map.get("Shared_Dirty").copied().unwrap_or(0))
324                / 1024;
325
326        total_kb += kb;
327        total_rss += rss;
328        total_dirty += dirty;
329
330        rows.push(ExtendedRow {
331            address: format!("{:016x}", m.address.0),
332            kbytes: format!("{kb}"),
333            rss: format!("{rss}"),
334            dirty: format!("{dirty}"),
335            mode: format_perms_pmap(m.perms),
336            mapping: format_mapping(
337                &m.pathname,
338                args.show_path,
339                args.use_kernel_name,
340            ),
341        });
342    }
343
344    print_rows(&rows);
345
346    if !args.quiet {
347        println!("{} ------- ------- ------- ", "-".repeat(16),);
348        println!(
349            "total kB {:>14} {:>7} {:>7}",
350            total_kb, total_rss, total_dirty,
351        );
352    }
353}
354
355fn show_device(
356    proc: &Process,
357    args: &Args,
358    range: Option<(u64, u64)>,
359    pid: i32,
360    not_found: &mut bool,
361) {
362    let maps = match proc.maps() {
363        Ok(m) => m,
364        Err(e) => {
365            eprintln!("pmap: {pid}: {e}");
366            *not_found = true;
367            return;
368        }
369    };
370
371    let mut rows: Vec<DeviceRow> = Vec::new();
372    let mut total_kb = 0u64;
373    let mut total_writable_private = 0u64;
374    let mut total_shared = 0u64;
375
376    for m in &maps.0 {
377        if let Some((lo, hi)) = range
378            && !in_range(m.address.0, m.address.1, lo, hi)
379        {
380            continue;
381        }
382        let kb = kbytes(m);
383        total_kb += kb;
384
385        if m.perms.contains(MMPermissions::SHARED) {
386            total_shared += kb;
387        }
388        if m.perms.contains(MMPermissions::WRITE)
389            && m.perms.contains(MMPermissions::PRIVATE)
390        {
391            total_writable_private += kb;
392        }
393
394        rows.push(DeviceRow {
395            address: format!("{:016x}", m.address.0),
396            kbytes: format!("{kb}"),
397            mode: format_perms_pmap(m.perms),
398            offset: format!("{:016x}", m.offset),
399            device: format!("{:03}:{:05}", m.dev.0, m.dev.1),
400            mapping: format_mapping(
401                &m.pathname,
402                args.show_path,
403                args.use_kernel_name,
404            ),
405        });
406    }
407
408    print_rows(&rows);
409
410    if !args.quiet {
411        println!(
412            "mapped: {total_kb}K    writeable/private: {total_writable_private}K    shared: {total_shared}K",
413        );
414    }
415}
416
417fn print_rows<T: Cols>(rows: &[T]) {
418    let table = T::to_table(rows);
419    cols::print_table(&table, &mut std::io::stdout().lock()).unwrap();
420}