155 lines
6.2 KiB
Rust
155 lines
6.2 KiB
Rust
//! Analysis-side enrichment over [`sylpheed_ppc::disasm::iter_disasm`].
|
|
//!
|
|
//! Turns a stream of decoder-only [`sylpheed_ppc::disasm::DisasmItem`]s into a
|
|
//! stream of [`RichDisasmItem`]s carrying section name + enclosing function +
|
|
//! label name. The three sinks in [`crate::sinks`] (text, JSON, DuckDB) all
|
|
//! consume `RichDisasmItem`.
|
|
|
|
use std::collections::{BTreeSet, HashMap};
|
|
|
|
use sylpheed_ppc::disasm::DisasmItem;
|
|
|
|
use crate::func::FuncAnalysis;
|
|
|
|
/// `DisasmItem` plus the analysis context (section/function/label).
|
|
#[derive(Debug, Clone)]
|
|
pub struct RichDisasmItem<'a> {
|
|
pub item: DisasmItem,
|
|
pub section: &'a str,
|
|
pub function: Option<u32>,
|
|
pub label: Option<&'a str>,
|
|
/// True when this word is data embedded in a code section (a recovered
|
|
/// jump table or its index map), so its decoded text is meaningless.
|
|
pub is_data: bool,
|
|
}
|
|
|
|
/// Walk one code section, yielding rich items annotated with section name,
|
|
/// enclosing function, and label-at-address.
|
|
///
|
|
/// `function` is the function that actually *contains* the address: it is set
|
|
/// on crossing a function start and cleared again at that function's
|
|
/// `end_address`. It is deliberately `None` in the gaps between functions.
|
|
///
|
|
/// It used to be a pure rolling window — set at each start and never cleared —
|
|
/// which silently attributed every gap word to whichever function happened to
|
|
/// precede it. On the reference title that mislabelled 55,227 instructions,
|
|
/// so `WHERE function = X` returned code that is not part of X, and the
|
|
/// resulting 100% attribution rate hid the fact that `.pdata` leaves ~450 KB
|
|
/// of `.text` unclaimed.
|
|
///
|
|
/// `data_words` is the set of 4-byte-aligned addresses inside code sections
|
|
/// that are known to hold data (see [`crate::jumptables::data_word_addresses`]).
|
|
/// Rows at those addresses are still emitted — their `raw` value is the table
|
|
/// entry a consumer wants — but flagged so nothing mistakes the decoded text
|
|
/// for a real instruction.
|
|
pub fn enrich_section<'a>(
|
|
image: &'a [u8],
|
|
image_base: u32,
|
|
section_name: &'a str,
|
|
va_start: u32,
|
|
va_end: u32,
|
|
func_analysis: &'a FuncAnalysis,
|
|
labels: &'a HashMap<u32, String>,
|
|
data_words: &'a BTreeSet<u32>,
|
|
) -> impl Iterator<Item = RichDisasmItem<'a>> + 'a {
|
|
// (start, end) of the function currently being walked.
|
|
let mut current: Option<(u32, u32)> = None;
|
|
sylpheed_ppc::disasm::iter_disasm(image, image_base, va_start, va_end).map(move |item| {
|
|
// Leaving the current function must be handled before entering the
|
|
// next: a function often starts exactly at its predecessor's end.
|
|
if let Some((_, end)) = current
|
|
&& item.addr >= end
|
|
{
|
|
current = None;
|
|
}
|
|
if let Some(fi) = func_analysis.functions.get(&item.addr) {
|
|
current = Some((item.addr, fi.end));
|
|
}
|
|
let current_func = current.map(|(start, _)| start);
|
|
let label = labels.get(&item.addr).map(|s| s.as_str());
|
|
let is_data = data_words.contains(&item.addr);
|
|
RichDisasmItem {
|
|
item,
|
|
section: section_name,
|
|
function: current_func,
|
|
label,
|
|
is_data,
|
|
}
|
|
})
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::func::{FuncAnalysis, FuncInfo};
|
|
use std::collections::BTreeMap;
|
|
|
|
fn fi(start: u32, end: u32) -> FuncInfo {
|
|
FuncInfo {
|
|
start, end,
|
|
frame_size: 0, saved_gprs: 0, is_leaf: true, is_saverestore: false,
|
|
pdata_validated: true, pdata_length: Some(end - start),
|
|
pdata_prolog_length: None, has_eh: false,
|
|
}
|
|
}
|
|
|
|
/// A word in the gap between two functions belongs to neither. Before the
|
|
/// containment check this walker carried the *preceding* function forward
|
|
/// across the gap, so `WHERE function = X` returned code outside X.
|
|
#[test]
|
|
fn gap_between_functions_is_unattributed() {
|
|
let image_base = 0x82000000u32;
|
|
// 6 words: [f0 f0] [gap gap] [f1 f1]
|
|
let image = vec![0x60u8; 0x40]; // `ori` — decodes cleanly, value irrelevant
|
|
let mut functions = BTreeMap::new();
|
|
functions.insert(image_base, fi(image_base, image_base + 8));
|
|
functions.insert(image_base + 16, fi(image_base + 16, image_base + 24));
|
|
let fa = FuncAnalysis {
|
|
functions,
|
|
save_gpr_base: None,
|
|
restore_gpr_base: None,
|
|
pdata_entries: Vec::new(),
|
|
};
|
|
let labels = HashMap::new();
|
|
let data_words = BTreeSet::new();
|
|
let got: Vec<(u32, Option<u32>)> = enrich_section(
|
|
&image, image_base, ".text", image_base, image_base + 24,
|
|
&fa, &labels, &data_words,
|
|
).map(|r| (r.item.addr, r.function)).collect();
|
|
|
|
assert_eq!(got, vec![
|
|
(image_base, Some(image_base)), // inside f0
|
|
(image_base + 4, Some(image_base)), // inside f0
|
|
(image_base + 8, None), // gap — was wrongly f0
|
|
(image_base + 12, None), // gap — was wrongly f0
|
|
(image_base + 16, Some(image_base + 16)), // f1 starts
|
|
(image_base + 20, Some(image_base + 16)),
|
|
]);
|
|
}
|
|
|
|
/// A function starting exactly at its predecessor's `end_address` must be
|
|
/// entered, not dropped: the leave check runs before the enter check.
|
|
#[test]
|
|
fn adjacent_functions_hand_over_cleanly() {
|
|
let image_base = 0x82000000u32;
|
|
let image = vec![0x60u8; 0x40];
|
|
let mut functions = BTreeMap::new();
|
|
functions.insert(image_base, fi(image_base, image_base + 8));
|
|
functions.insert(image_base + 8, fi(image_base + 8, image_base + 16));
|
|
let fa = FuncAnalysis {
|
|
functions, save_gpr_base: None, restore_gpr_base: None,
|
|
pdata_entries: Vec::new(),
|
|
};
|
|
let labels = HashMap::new();
|
|
let data_words = BTreeSet::new();
|
|
let got: Vec<Option<u32>> = enrich_section(
|
|
&image, image_base, ".text", image_base, image_base + 16,
|
|
&fa, &labels, &data_words,
|
|
).map(|r| r.function).collect();
|
|
assert_eq!(got, vec![
|
|
Some(image_base), Some(image_base),
|
|
Some(image_base + 8), Some(image_base + 8),
|
|
]);
|
|
}
|
|
}
|