Revision control

Copy as Markdown

Other Tools

//! All of these tests are specific to the MacOS task dumper
#![cfg(target_os = "macos")]
use minidump_writer::{mach::LoadCommand, task_dumper::TaskDumper};
use std::fmt::Write;
fn call_otool(args: &[&str]) -> String {
let mut cmd = std::process::Command::new("otool");
cmd.args(args);
let exe_path = std::env::current_exe().expect("unable to retrieve test executable path");
cmd.arg(exe_path);
let output = cmd.output().expect("failed to spawn otool");
assert!(output.status.success());
String::from_utf8(output.stdout).expect("stdout was invalid utf-8")
}
/// Validates we can iterate the load commands for all of the images in the task
#[test]
fn iterates_load_commands() {
let lc_str = call_otool(&["-l"]);
let mut expected = String::new();
let mut lc_index = 0;
expected.push('\n');
while let Some(nlc) = lc_str[lc_index..].find("Load command ") {
lc_index += nlc;
let block = match lc_str[lc_index + 13..].find("Load command ") {
Some(ind) => &lc_str[lc_index + 13..lc_index + 13 + ind],
None => &lc_str[lc_index..],
};
// otool prints the load command index for each command, but we only
// handle the small subset of the available load commands we care about
// so just ignore that
let block = &block[block.find('\n').unwrap() + 1..];
// otool also prints all the sections for LC_SEGMENT_* commands, but
// we don't care about those, so ignore them
let block = match block.find("Section") {
Some(ind) => &block[..ind],
None => block,
};
lc_index += 13;
let cmd = block
.find("cmd ")
.expect("load commnd didn't specify cmd kind");
let cmd_end = block[cmd..]
.find('\n')
.expect("load cmd didn't end with newline");
if matches!(
&block[cmd + 4..cmd + cmd_end],
"LC_SEGMENT_64" | "LC_UUID" | "LC_ID_DYLIB" | "LC_LOAD_DYLINKER"
) {
expected.push_str(block);
}
}
let task_dumper = TaskDumper::new(
// SAFETY: syscall
unsafe { mach2::traps::mach_task_self() },
);
let mut actual = String::new();
// Unfortunately, Apple decided to move dynamic libs into a shared cache,
// removing them from the file system completely, and unless I'm missing it
// there is no way to get the load commands for the dylibs since otool
// only understands file paths? So we just get the load commands for the main
// executable instead, this means that we miss the `LC_ID_DYLIB` commands
// since they only apply to dylibs, but this test is more that we can
// correctly iterate through the load commands themselves, so this _should_
// be fine...
let exe_img = task_dumper
.read_executable_image()
.expect("failed to read executable image");
{
let lcmds = task_dumper
.read_load_commands(&exe_img)
.expect("failed to read load commands");
for lc in lcmds.iter() {
match lc {
LoadCommand::Segment(seg) => {
let segname = std::str::from_utf8(&seg.segment_name).unwrap();
let segname = &segname[..segname.find('\0').unwrap()];
write!(
&mut actual,
"
cmd LC_SEGMENT_64
cmdsize {}
segname {}
vmaddr 0x{:016x}
vmsize 0x{:016x}
fileoff {}
filesize {}
maxprot 0x{:08x}
initprot 0x{:08x}
nsects {}
flags 0x{:x}",
seg.cmd_size,
segname,
seg.vm_addr,
seg.vm_size,
seg.file_off,
seg.file_size,
seg.max_prot,
seg.init_prot,
seg.num_sections,
seg.flags,
)
.unwrap();
}
LoadCommand::Dylib(_dylib) => {
unreachable!();
}
LoadCommand::Uuid(uuid) => {
let id = uuid::Uuid::from_bytes(uuid.uuid);
let mut uuid_buf = [0u8; uuid::fmt::Hyphenated::LENGTH];
let uuid_str = id.hyphenated().encode_upper(&mut uuid_buf);
write!(
&mut actual,
"
cmd LC_UUID
cmdsize {}
uuid {uuid_str}
",
uuid.cmd_size,
)
.unwrap();
}
LoadCommand::DylinkerCommand(dy_cmd) => {
write!(
&mut actual,
"
cmd LC_LOAD_DYLINKER
cmdsize {}
name {} (offset {})",
dy_cmd.cmd_size, dy_cmd.name, dy_cmd.name_offset,
)
.unwrap();
}
}
}
}
similar_asserts::assert_eq!(expected, actual);
}