#![allow(clippy::missing_errors_doc)]
use crate::asm::statements::Label;
use crate::cached_lines::CachedLines;
use crate::demangle::LabelKind;
use crate::{
color, demangle, esafeprintln, get_context_for, safeprintln, Dumpable, Item, RawLines, URange,
};
use crate::opts::{Format, NameDisplay, RedundantLabels, SourcesFrom};
mod statements;
use nom::Parser as _;
use owo_colors::OwoColorize;
use statements::{parse_statement, Loc};
pub use statements::{Directive, GenericDirective, Instruction, Statement};
use std::borrow::Cow;
use std::cell::RefCell;
use std::collections::{BTreeMap, BTreeSet, HashMap};
use std::ops::Range;
use std::path::{Display, Path, PathBuf};
type SourceFile = (String, Option<(Source, CachedLines)>);
pub fn parse_file(input: &str) -> anyhow::Result<Vec<Statement<'_>>> {
match nom::multi::many0(parse_statement).parse(input) {
Ok(("", stmts)) => Ok(stmts),
Ok((leftovers, _)) =>
{
#[allow(clippy::redundant_else)]
if leftovers.len() < 1000 {
anyhow::bail!("Didn't consume everything, leftovers: {leftovers:?}")
} else {
let head = &leftovers[..leftovers
.char_indices()
.nth(200)
.expect("Shouldn't have that much unicode here...")
.0];
anyhow::bail!("Didn't consume everything, leftovers prefix: {head:?}");
}
}
Err(err) => anyhow::bail!("Couldn't parse the .s file: {err}"),
}
}
#[must_use]
pub fn find_items(lines: &[Statement]) -> BTreeMap<Item, Range<usize>> {
let mut res = BTreeMap::new();
let mut sec_start = 0;
let mut item: Option<Item> = None;
let mut names = BTreeMap::new();
for (ix, line) in lines.iter().enumerate() {
if line.is_section_start() {
if item.is_none() {
sec_start = ix;
} else {
}
} else if line.is_global() && sec_start + 3 < ix {
sec_start = ix;
} else if line.is_end_of_fn() {
let sec_end = ix;
let range = sec_start..sec_end;
if let Some(mut item) = item.take() {
item.len = ix - item.len;
item.non_blank_len = item.len;
res.insert(item, range);
}
} else if let Statement::Label(label) = line {
if let Some(dem) = demangle::demangled(label.id) {
let hashed = format!("{dem:?}");
let name = format!("{dem:#?}");
let name_entry = names.entry(name.clone()).or_insert(0);
item = Some(Item {
mangled_name: label.id.to_owned(),
name,
hashed,
index: *name_entry,
len: ix,
non_blank_len: 0,
});
*name_entry += 1;
} else if matches!(label.kind, LabelKind::Unknown | LabelKind::Global) {
if let Some(mut i) = handle_non_mangled_labels(lines, ix, label, sec_start) {
let name_entry = names.entry(i.name.clone()).or_insert(0);
i.index = *name_entry;
item = Some(i);
*name_entry += 1;
}
}
}
}
let globals = lines
.iter()
.enumerate()
.filter_map(|(ix, line)| {
if let Statement::Directive(Directive::Global(name)) = line {
Some((name, ix))
} else {
None
}
})
.collect::<HashMap<_, _>>();
for (end, line) in lines.iter().enumerate() {
let name = match line {
Statement::Directive(Directive::SetValue(name, _)) => name,
Statement::Assignment(name, _) => name,
_ => continue,
};
let Some(start) = globals.get(name).copied() else {
continue;
};
let range = start..end + 1;
if range.len() > 10 {
continue;
}
let sym = name;
if let Some(dem) = demangle::demangled(sym) {
let hashed = format!("{dem:?}");
let name = format!("{dem:#?}");
let name_entry = names.entry(name.clone()).or_insert(0);
res.insert(
Item {
mangled_name: (*sym).to_string(),
name,
hashed,
index: *name_entry,
len: range.len(),
non_blank_len: range.len(),
},
range,
);
*name_entry += 1;
}
}
res
}
fn handle_non_mangled_labels(
lines: &[Statement],
ix: usize,
label: &Label,
sec_start: usize,
) -> Option<Item> {
match lines.get(sec_start) {
Some(Statement::Directive(Directive::SectionStart(ss))) => {
const MACOS_TEXT_SECTION: &str = "__TEXT,__text,regular,pure_instructions";
const WINDOWS_TEXT_SECTION_PREFIX: &str = ".text,\"xr\",one_only,";
let is_mac = *ss == MACOS_TEXT_SECTION;
let is_windows = ss.starts_with(WINDOWS_TEXT_SECTION_PREFIX);
if is_windows || is_mac {
for line in &lines[sec_start..ix] {
if let Statement::Directive(Directive::Global(g)) = line {
if let Some(item) = get_item_in_section(ix, label, g, is_mac) {
return Some(item);
}
}
}
None
} else {
get_item_in_section(ix, label, ss.strip_prefix(".text.")?, false)
}
}
Some(Statement::Directive(Directive::Global(g))) => get_item_in_section(ix, label, g, true),
_ => None,
}
}
fn get_item_in_section(ix: usize, label: &Label, ss: &str, strip_underscore: bool) -> Option<Item> {
if !ss.starts_with(label.id) {
return None;
}
let name = if strip_underscore && label.id.starts_with('_') {
String::from(&label.id[1..])
} else {
String::from(label.id)
};
Some(Item {
mangled_name: label.id.to_owned(),
name: name.clone(),
hashed: name,
index: 0, len: ix,
non_blank_len: 0,
})
}
fn used_labels<'a>(stmts: &'_ [Statement<'a>]) -> BTreeSet<&'a str> {
stmts
.iter()
.filter_map(|stmt| match stmt {
Statement::Label(_) | Statement::Nothing => None,
Statement::Directive(dir) => match dir {
Directive::File(_)
| Directive::Loc(_)
| Directive::Global(_)
| Directive::SubsectionsViaSym
| Directive::SymIsFun(_) => None,
Directive::Data(_, val) | Directive::SetValue(_, val) => Some(*val),
Directive::Generic(g) => Some(g.0),
Directive::SectionStart(ss) => Some(*ss),
},
Statement::Instruction(i) => i.args,
Statement::Assignment(_, _) => None,
Statement::Dunno(s) => Some(s),
})
.flat_map(demangle::local_labels)
.collect::<BTreeSet<_>>()
}
fn scan_constant(
name: &str,
sections: &BTreeMap<&str, usize>,
body: &[Statement],
) -> Option<URange> {
let start = *sections.get(name)?;
let end = start
+ body[start + 1..]
.iter()
.take_while(|s| matches!(s, Statement::Directive(Directive::Data(_, _))))
.count()
+ 1;
Some(URange { start, end })
}
fn dump_range(
files: &BTreeMap<u64, SourceFile>,
fmt: &Format,
print_range: Range<usize>,
body: &[Statement], ) -> anyhow::Result<()> {
let print_range = URange::from(print_range);
let mut prev_loc = Loc::default();
let stmts = &body[print_range];
let used = if fmt.redundant_labels == RedundantLabels::Keep {
BTreeSet::new()
} else {
used_labels(stmts)
};
let mut empty_line = false;
for (ix, line) in stmts.iter().enumerate() {
if fmt.verbosity > 3 {
safeprintln!("{line:?}");
}
if let Statement::Directive(Directive::File(_)) = &line {
} else if let Statement::Directive(Directive::Loc(loc)) = &line {
if !fmt.rust {
continue;
}
if loc.line == 0 {
continue;
}
if loc == &prev_loc {
continue;
}
prev_loc = *loc;
match files.get(&loc.file) {
Some((fname, Some((source, file)))) => {
if source.show_for(fmt.sources_from) {
let pos = format!("\t\t// {fname}:{}", loc.line);
safeprintln!("{}", color!(pos, OwoColorize::cyan));
if let Some(rust_line) = &file.get(loc.line as usize - 1) {
safeprintln!(
"\t\t{}",
color!(rust_line.trim_start(), OwoColorize::bright_red)
);
} else {
safeprintln!("\t\t{}",
color!("Corrupted rust-src installation? Try re-adding rust-src component.", OwoColorize::red)
);
}
}
}
Some((fname, None)) => {
if fmt.verbosity > 1 {
safeprintln!(
"\t\t{} {}",
color!("//", OwoColorize::cyan),
color!(
"Can't locate the file, please open a ticket with cargo-show-asm",
OwoColorize::red
),
);
}
let pos = format!("\t\t// {fname}:{}", loc.line);
safeprintln!("{}", color!(pos, OwoColorize::cyan));
}
None => {
panic!("DWARF file refers to an undefined location {loc:?}");
}
}
empty_line = false;
} else if let Statement::Label(Label {
kind: kind @ (LabelKind::Local | LabelKind::Temp),
id,
}) = line
{
match fmt.redundant_labels {
_ if ix == 0 || used.contains(id) => {
safeprintln!("{line}");
}
RedundantLabels::Keep => {
safeprintln!("{line}");
}
RedundantLabels::Blanks => {
if !empty_line && *kind != LabelKind::Temp {
safeprintln!();
empty_line = true;
}
}
RedundantLabels::Strip => {}
}
} else {
if fmt.simplify && line.boring() {
continue;
}
empty_line = false;
match fmt.name_display {
NameDisplay::Full => safeprintln!("{line:#}"),
NameDisplay::Short => safeprintln!("{line}"),
NameDisplay::Mangled => safeprintln!("{line:-}"),
}
}
}
Ok(())
}
fn path_formatter() -> impl for<'p> Fn(&'p Path, &'p mut PathBuf) -> Display<'p> {
let current_dir = std::env::current_dir().unwrap_or_default();
let home_dir = std::env::home_dir();
let home = if std::path::MAIN_SEPARATOR == '/' {
"~"
} else {
"%userprofile%"
};
move |path, tmp| {
if path.is_absolute() {
if let Ok(rel) = path.strip_prefix(¤t_dir) {
rel
} else if let Some(path_in_home) = home_dir
.as_ref()
.and_then(|home| path.strip_prefix(home).ok())
{
tmp.clear();
tmp.push(home);
tmp.push(path_in_home);
&*tmp
} else {
path
}
} else {
path
}
.display()
}
}
#[derive(Debug, Clone)]
pub enum Source {
Crate,
External,
Stdlib,
Rustc,
}
impl Source {
fn show_for(&self, from: SourcesFrom) -> bool {
match self {
Source::Crate => true,
Source::External => match from {
SourcesFrom::ThisWorkspace => false,
SourcesFrom::AllCrates | SourcesFrom::AllSources => true,
},
Source::Rustc | Source::Stdlib => match from {
SourcesFrom::ThisWorkspace | SourcesFrom::AllCrates => false,
SourcesFrom::AllSources => true,
},
}
}
}
fn locate_sources(sysroot: &Path, workspace: &Path, path: &Path) -> Option<(Source, PathBuf)> {
let mut path = Cow::Borrowed(path);
if path.exists() {
let source = if path.starts_with(workspace) {
Source::Crate
} else {
Source::External
};
return Some((source, path.into()));
}
let no_rust_src = || {
esafeprintln!(
"You need to install rustc sources to be able to see the rust annotations, try\n\
\trustup component add rust-src"
);
std::process::exit(1);
};
if (path.starts_with("/rustc/") || path.starts_with("/private/tmp"))
&& path
.as_os_str()
.to_str()
.is_some_and(|s| s.contains('\\') && s.contains('/'))
{
let cursed_path = path
.as_os_str()
.to_str()
.expect("They are coming from a text file");
path = Cow::Owned(PathBuf::from(cursed_path.replace('\\', "/")));
}
if path.starts_with("/rustc") && path.iter().any(|c| c == "compiler") {
let mut source = sysroot.join("lib/rustlib/rustc-src/rust");
for part in path.components().skip(3) {
source.push(part);
}
if source.exists() {
return Some((Source::Rustc, source));
} else {
no_rust_src();
}
}
if path.starts_with("/rustc/") {
let mut source = sysroot.join("lib/rustlib/src/rust");
for part in path.components().skip(3) {
source.push(part);
}
if source.exists() {
return Some((Source::Stdlib, source));
} else {
no_rust_src();
}
}
if path.starts_with("/private/tmp") && path.components().any(|c| c.as_os_str() == "library") {
let mut source = sysroot.join("lib/rustlib/src/rust");
for part in path.components().skip(5) {
source.push(part);
}
if source.exists() {
return Some((Source::Stdlib, source));
} else {
no_rust_src();
}
}
if let Some(ix) = path
.components()
.position(|c| c.as_os_str() == "cargo" || c.as_os_str() == ".cargo")
.and_then(|ix| path.components().nth(ix).zip(Some(ix)))
.and_then(|(c, ix)| (c.as_os_str() == "registry").then_some(ix))
{
#[allow(deprecated)]
let mut source = std::env::home_dir().expect("No home dir?");
source.push(".cargo");
for part in path.components().skip(ix) {
source.push(part);
}
if source.exists() {
return Some((Source::External, source));
} else {
panic!(
"{path:?} looks like it can be a cargo registry reference but we failed to get it"
);
}
}
None
}
fn load_rust_sources(
sysroot: &Path,
workspace: &Path,
statements: &[Statement],
fmt: &Format,
files: &mut BTreeMap<u64, SourceFile>,
) {
let home_dir = std::env::home_dir();
let format_path = path_formatter();
let mut tmp = PathBuf::new();
for line in statements {
if let Statement::Directive(Directive::File(f)) = line {
files.entry(f.index).or_insert_with(|| {
let path = f.path.as_full_path_with_home_dir(home_dir.as_deref());
let pretty_path = format_path(&path, &mut tmp).to_string();
if fmt.verbosity > 2 {
safeprintln!("Reading file #{} {}", f.index, path.display());
}
if let Some((source, filepath)) = locate_sources(sysroot, workspace, &path) {
if fmt.verbosity > 3 {
safeprintln!("Resolved name is {filepath:?}");
}
let sources = std::fs::read_to_string(&filepath).expect("Can't read a file");
if sources.is_empty() {
if fmt.verbosity > 0 {
safeprintln!("Ignoring empty file {filepath:?}!");
}
(pretty_path, None)
} else {
if fmt.verbosity > 3 {
safeprintln!("Got {} bytes", sources.len());
}
let lines = CachedLines::without_ending(sources);
(pretty_path, Some((source, lines)))
}
} else {
if fmt.verbosity > 1 {
safeprintln!("File not found {}", path.display());
}
(pretty_path, None)
}
});
}
}
}
impl RawLines for Statement<'_> {
fn lines(&self) -> Option<&str> {
match self {
Statement::Instruction(i) => i.args,
Statement::Directive(Directive::SetValue(_, i)) => Some(i),
_ => None,
}
}
}
pub struct Asm<'a> {
workspace: &'a Path,
sysroot: &'a Path,
sources: RefCell<BTreeMap<u64, SourceFile>>,
}
impl<'a> Asm<'a> {
pub fn new(workspace: &'a Path, sysroot: &'a Path) -> Self {
Self {
workspace,
sysroot,
sources: Default::default(),
}
}
}
impl Dumpable for Asm<'_> {
type Line<'l> = Statement<'l>;
fn split_lines(contents: &str) -> anyhow::Result<Vec<Self::Line<'_>>> {
parse_file(contents)
}
fn find_items(lines: &[Self::Line<'_>]) -> BTreeMap<Item, Range<usize>> {
find_items(lines)
}
fn dump_range(&self, fmt: &Format, lines: &[Self::Line<'_>]) -> anyhow::Result<()> {
dump_range(&self.sources.borrow(), fmt, 0..lines.len(), lines)
}
fn extra_context(
&self,
fmt: &Format,
lines: &[Self::Line<'_>],
range: Range<usize>,
items: &BTreeMap<Item, Range<usize>>,
) -> Vec<Range<usize>> {
let mut res = get_context_for(fmt.context, lines, range.clone(), items);
if fmt.rust {
load_rust_sources(
self.sysroot,
self.workspace,
lines,
fmt,
&mut self.sources.borrow_mut(),
);
}
if fmt.include_constants {
let print_range = URange::from(range);
let mut pending = vec![print_range];
let mut seen: BTreeSet<URange> = BTreeSet::new();
let constants = lines
.iter()
.enumerate()
.filter_map(|(ix, stmt)| {
let Statement::Label(Label { id, .. }) = stmt else {
return None;
};
matches!(
lines.get(ix + 1),
Some(Statement::Directive(Directive::Data(_, _)))
)
.then_some((*id, ix))
})
.collect::<BTreeMap<_, _>>();
while let Some(subset) = pending.pop() {
seen.insert(subset);
for s in &lines[subset] {
if let Statement::Instruction(Instruction {
args: Some(arg), ..
})
| Statement::Directive(Directive::Generic(GenericDirective(arg))) = s
{
for label in crate::demangle::local_labels(arg) {
if let Some(constant_range) = scan_constant(label, &constants, lines) {
if !seen.contains(&constant_range)
&& !print_range.fully_contains(constant_range)
{
pending.push(constant_range);
}
}
}
}
}
}
seen.remove(&print_range);
for range in &seen {
res.push(range.start..range.end);
}
}
if fmt.simplify {
res.retain(|range| {
lines[range.start..range.end]
.iter()
.any(|s| !(s.boring() || matches!(s, Statement::Nothing | Statement::Label(_))))
});
}
res
}
}