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#[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 #[arg(short = 'x', long)]
33 extended: bool,
34
35 #[arg(short, long)]
37 device: bool,
38
39 #[arg(short, long)]
41 quiet: bool,
42
43 #[arg(short = 'p', long)]
45 show_path: bool,
46
47 #[arg(short = 'k', long = "use-kernel-name")]
51 use_kernel_name: bool,
52
53 #[arg(short = 'A', long = "range", value_name = "LOW[,HIGH]")]
57 range: Option<String>,
58
59 #[arg(required = true)]
61 pid: Vec<i32>,
62}
63
64fn 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
81fn 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 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}