Skip to content

Commit

Permalink
Merge pull request #249 from cakebaker/pmap_device_format
Browse files Browse the repository at this point in the history
pmap: implement `--device`
  • Loading branch information
sylvestre authored Nov 8, 2024
2 parents 087d37c + cc6d948 commit bc66596
Show file tree
Hide file tree
Showing 3 changed files with 212 additions and 35 deletions.
122 changes: 98 additions & 24 deletions src/uu/pmap/src/maps_format_parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,59 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.

use std::fmt;
use std::io::{Error, ErrorKind};

// Represents a parsed single line from /proc/<PID>/maps.
// Represents a parsed single line from /proc/<PID>/maps for the default and device formats. It
// omits the inode information because it's not used by those formats.
#[derive(Debug, PartialEq)]
pub struct MapLine {
pub address: String,
pub size_in_kb: u64,
pub perms: String,
pub perms: Perms,
pub offset: String,
pub device: String,
pub mapping: String,
}

// Represents a set of permissions from the "perms" column of /proc/<PID>/maps.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Perms {
pub readable: bool,
pub writable: bool,
pub executable: bool,
pub shared: bool,
}

impl From<&str> for Perms {
fn from(s: &str) -> Self {
let mut chars = s.chars();

Self {
readable: chars.next() == Some('r'),
writable: chars.next() == Some('w'),
executable: chars.next() == Some('x'),
shared: chars.next() == Some('s'),
}
}
}

// Please note: While `Perms` has four boolean fields, it's string representation has five
// characters because pmap's default and device formats use five characters for the perms,
// with the last character always being '-'.
impl fmt::Display for Perms {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}{}{}{}-",
if self.readable { 'r' } else { '-' },
if self.writable { 'w' } else { '-' },
if self.executable { 'x' } else { '-' },
if self.shared { 's' } else { '-' },
)
}
}

// Parses a single line from /proc/<PID>/maps. See
// https://www.kernel.org/doc/html/latest/filesystems/proc.html for details about the expected
// format.
Expand All @@ -30,16 +72,29 @@ pub fn parse_map_line(line: &str) -> Result<MapLine, Error> {
let (perms, rest) = rest
.split_once(' ')
.ok_or_else(|| Error::from(ErrorKind::InvalidData))?;
let perms = parse_perms(perms);
let perms = Perms::from(perms);

let (offset, rest) = rest
.split_once(' ')
.ok_or_else(|| Error::from(ErrorKind::InvalidData))?;
let offset = format!("{offset:0>16}");

let (device, rest) = rest
.split_once(' ')
.ok_or_else(|| Error::from(ErrorKind::InvalidData))?;
let device = parse_device(device)?;

let mapping: String = rest.splitn(4, ' ').skip(3).collect();
// skip the "inode" column
let mapping: String = rest.splitn(2, ' ').skip(1).collect();
let mapping = mapping.trim_ascii_start();
let mapping = parse_mapping(mapping);

Ok(MapLine {
address,
size_in_kb,
perms,
offset,
device,
mapping,
})
}
Expand All @@ -58,13 +113,12 @@ fn parse_address(memory_range: &str) -> Result<(String, u64), Error> {
Ok((format!("{start:0>16}"), size_in_kb))
}

// Turns a 4-char perms string from /proc/<PID>/maps into a 5-char perms string. The first three
// chars are left untouched.
fn parse_perms(perms: &str) -> String {
let perms = perms.replace("p", "-");

// the fifth char seems to be always '-' in the original pmap
format!("{perms}-")
// Pads the device info from /proc/<PID>/maps with zeros and turns AB:CD into 0AB:000CD.
fn parse_device(device: &str) -> Result<String, Error> {
let (major, minor) = device
.split_once(':')
.ok_or_else(|| Error::from(ErrorKind::InvalidData))?;
Ok(format!("{major:0>3}:{minor:0>5}"))
}

fn parse_mapping(mapping: &str) -> String {
Expand All @@ -86,44 +140,60 @@ fn parse_mapping(mapping: &str) -> String {
mod test {
use super::*;

fn create_map_line(address: &str, size_in_kb: u64, perms: &str, mapping: &str) -> MapLine {
fn create_map_line(
address: &str,
size_in_kb: u64,
perms: Perms,
offset: &str,
device: &str,
mapping: &str,
) -> MapLine {
MapLine {
address: address.to_string(),
size_in_kb,
perms: perms.to_string(),
perms,
offset: offset.to_string(),
device: device.to_string(),
mapping: mapping.to_string(),
}
}

#[test]
fn test_perms_to_string() {
assert_eq!("-----", Perms::from("---p").to_string());
assert_eq!("---s-", Perms::from("---s").to_string());
assert_eq!("rwx--", Perms::from("rwxp").to_string());
}

#[test]
fn test_parse_map_line() {
let data = [
(
create_map_line("000062442eb9e000", 16, "r----", "konsole"),
create_map_line("000062442eb9e000", 16, Perms::from("r--p"), "0000000000000000", "008:00008", "konsole"),
"62442eb9e000-62442eba2000 r--p 00000000 08:08 10813151 /usr/bin/konsole"
),
(
create_map_line("000071af50000000", 132, "rw---", " [ anon ]"),
create_map_line("000071af50000000", 132, Perms::from("rw-p"), "0000000000000000", "000:00000", " [ anon ]"),
"71af50000000-71af50021000 rw-p 00000000 00:00 0 "
),
(
create_map_line("00007ffc3f8df000", 132, "rw---", " [ stack ]"),
create_map_line("00007ffc3f8df000", 132, Perms::from("rw-p"), "0000000000000000", "000:00000", " [ stack ]"),
"7ffc3f8df000-7ffc3f900000 rw-p 00000000 00:00 0 [stack]"
),
(
create_map_line("000071af8c9e6000", 16, "rw-s-", " [ anon ]"),
create_map_line("000071af8c9e6000", 16, Perms::from("rw-s"), "0000000105830000", "000:00010", " [ anon ]"),
"71af8c9e6000-71af8c9ea000 rw-s 105830000 00:10 1075 anon_inode:i915.gem"
),
(
create_map_line("000071af6cf0c000", 3560, "rw-s-", "memfd:wayland-shm (deleted)"),
create_map_line("000071af6cf0c000", 3560, Perms::from("rw-s"), "0000000000000000", "000:00001", "memfd:wayland-shm (deleted)"),
"71af6cf0c000-71af6d286000 rw-s 00000000 00:01 256481 /memfd:wayland-shm (deleted)"
),
(
create_map_line("ffffffffff600000", 4, "--x--", " [ anon ]"),
create_map_line("ffffffffff600000", 4, Perms::from("--xp"), "0000000000000000", "000:00000", " [ anon ]"),
"ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0 [vsyscall]"
),
(
create_map_line("00005e8187da8000", 24, "r----", "hello world"),
create_map_line("00005e8187da8000", 24, Perms::from("r--p"), "0000000000000000", "008:00008", "hello world"),
"5e8187da8000-5e8187dae000 r--p 00000000 08:08 9524160 /usr/bin/hello world"
),
];
Expand Down Expand Up @@ -161,10 +231,14 @@ mod test {
}

#[test]
fn test_parse_perms() {
assert_eq!("-----", parse_perms("---p"));
assert_eq!("---s-", parse_perms("---s"));
assert_eq!("rwx--", parse_perms("rwxp"));
fn test_parse_device() {
assert_eq!("012:00034", parse_device("12:34").unwrap());
assert_eq!("000:00000", parse_device("00:00").unwrap());
}

#[test]
fn test_parse_device_without_colon() {
assert!(parse_device("1234").is_err());
}

#[test]
Expand Down
70 changes: 60 additions & 10 deletions src/uu/pmap/src/pmap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
// file that was distributed with this source code.

use clap::{crate_version, Arg, ArgAction, Command};
use maps_format_parser::parse_map_line;
use maps_format_parser::{parse_map_line, MapLine};
use std::env;
use std::fs;
use std::io::Error;
Expand Down Expand Up @@ -49,11 +49,12 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
}
}

match parse_maps(pid) {
Ok(total) => println!(" total {total:>16}K"),
Err(_) => {
set_exit_code(1);
}
if matches.get_flag(options::DEVICE) {
output_device_format(pid).map_err(|_| set_exit_code(1)).ok();
} else {
output_default_format(pid)
.map_err(|_| set_exit_code(1))
.ok();
}
}

Expand All @@ -74,21 +75,70 @@ fn parse_cmdline(pid: &str) -> Result<String, Error> {
Ok(cmdline.into())
}

fn parse_maps(pid: &str) -> Result<u64, Error> {
fn process_maps<F>(pid: &str, mut process_line: F) -> Result<(), Error>
where
F: FnMut(&MapLine),
{
let path = format!("/proc/{pid}/maps");
let contents = fs::read_to_string(path)?;
let mut total = 0;

for line in contents.lines() {
let map_line = parse_map_line(line)?;
process_line(&map_line);
}

Ok(())
}

fn output_default_format(pid: &str) -> Result<(), Error> {
let mut total = 0;

process_maps(pid, |map_line| {
println!(
"{} {:>6}K {} {}",
map_line.address, map_line.size_in_kb, map_line.perms, map_line.mapping
);
total += map_line.size_in_kb;
}
})?;

println!(" total {total:>16}K");

Ok(())
}

fn output_device_format(pid: &str) -> Result<(), Error> {
let mut total_mapped = 0;
let mut total_writeable_private = 0;
let mut total_shared = 0;

println!("Address Kbytes Mode Offset Device Mapping");

process_maps(pid, |map_line| {
println!(
"{} {:>7} {} {} {} {}",
map_line.address,
map_line.size_in_kb,
map_line.perms,
map_line.offset,
map_line.device,
map_line.mapping
);
total_mapped += map_line.size_in_kb;

Ok(total)
if map_line.perms.writable && !map_line.perms.shared {
total_writeable_private += map_line.size_in_kb;
}

if map_line.perms.shared {
total_shared += map_line.size_in_kb;
}
})?;

println!(
"mapped: {total_mapped}K writeable/private: {total_writeable_private}K shared: {total_shared}K"
);

Ok(())
}

pub fn uu_app() -> Command {
Expand Down
55 changes: 54 additions & 1 deletion tests/by-util/test_pmap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,22 @@ fn test_non_existing_pid() {
.no_output();
}

#[test]
#[cfg(target_os = "linux")]
fn test_device() {
let pid = process::id();

for arg in ["-d", "--device"] {
let result = new_ucmd!()
.arg(arg)
.arg(pid.to_string())
.succeeds()
.stdout_move_str();

assert_device_format(pid, &result);
}
}

#[test]
fn test_invalid_arg() {
new_ucmd!().arg("--definitely-invalid").fails().code_is(1);
Expand All @@ -94,9 +110,46 @@ fn assert_format(pid: u32, s: &str) {

let rest = rest.trim_end();
let (memory_map, last_line) = rest.rsplit_once('\n').unwrap();
let re = Regex::new("(?m)^[0-9a-f]{16} +[1-9][0-9]*K (-|r)(-|w)(-|x)(-|s)- ( $$[ (anon|stack) $$]|[a-zA-Z0-9._-]+)$").unwrap();
let re = Regex::new(r"(?m)^[0-9a-f]{16} +[1-9][0-9]*K (-|r)(-|w)(-|x)(-|s)- ( \[ (anon|stack) \]|[a-zA-Z0-9._-]+)$").unwrap();
assert!(re.is_match(memory_map));

let re = Regex::new("^ total +[1-9][0-9]*K$").unwrap();
assert!(re.is_match(last_line));
}

// Ensure `s` has the following device format (--device):
//
// 1234: /some/path
// Address Kbytes Mode Offset Device Mapping
// 000073eb5f4c7000 8 rw--- 0000000000036000 008:00008 ld-linux-x86-64.so.2
// 00007ffd588fc000 132 rw--- 0000000000000000 000:00000 [ stack ]
// ffffffffff600000 4 --x-- 0000000000000000 000:00000 [ anon ]
// ...
// mapped: 3060K writeable/private: 348K shared: 0K
#[cfg(target_os = "linux")]
fn assert_device_format(pid: u32, s: &str) {
let lines: Vec<_> = s.lines().collect();
let line_count = lines.len();

let re = Regex::new(&format!("^{pid}: .+[^ ]$")).unwrap();
assert!(re.is_match(lines[0]));

let expected_header = "Address Kbytes Mode Offset Device Mapping";
assert_eq!(expected_header, lines[1]);

let re = Regex::new(
r"^[0-9a-f]{16} +[1-9][0-9]* (-|r)(-|w)(-|x)(-|s)- [0-9a-f]{16} [0-9a-f]{3}:[0-9a-f]{5} ( \[ (anon|stack) \]|[a-zA-Z0-9._-]+)$",
)
.unwrap();

for line in lines.iter().take(line_count - 1).skip(2) {
assert!(re.is_match(line), "failing line: {line}");
}

let re = Regex::new(r"^mapped: \d+K\s{4}writeable/private: \d+K\s{4}shared: \d+K$").unwrap();
assert!(
re.is_match(lines[line_count - 1]),
"failing line: {}",
lines[line_count - 1]
);
}

0 comments on commit bc66596

Please sign in to comment.