Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com> Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
5651 lines
219 KiB
Diff
5651 lines
219 KiB
Diff
diff --git a/decompiler/Cargo.lock b/decompiler/Cargo.lock
|
|
index 157e7bd2..6caa4604 100644
|
|
--- a/decompiler/Cargo.lock
|
|
+++ b/decompiler/Cargo.lock
|
|
@@ -23,6 +23,12 @@ version = "0.7.8"
|
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56"
|
|
|
|
+[[package]]
|
|
+name = "base64"
|
|
+version = "0.22.1"
|
|
+source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
|
+
|
|
[[package]]
|
|
name = "bitflags"
|
|
version = "2.13.0"
|
|
@@ -265,6 +271,8 @@ dependencies = [
|
|
name = "kuna-cli"
|
|
version = "0.1.0"
|
|
dependencies = [
|
|
+ "base64",
|
|
+ "flate2",
|
|
"kuna-analysis",
|
|
"kuna-base",
|
|
"kuna-console",
|
|
diff --git a/decompiler/crates/kuna-analysis/src/analyzers/noreturn_disc/flow_tests.rs b/decompiler/crates/kuna-analysis/src/analyzers/noreturn_disc/flow_tests.rs
|
|
new file mode 100644
|
|
index 00000000..34158718
|
|
--- /dev/null
|
|
+++ b/decompiler/crates/kuna-analysis/src/analyzers/noreturn_disc/flow_tests.rs
|
|
@@ -0,0 +1,115 @@
|
|
+use super::{call_site_has_no_fallthrough, discover_noreturn};
|
|
+use crate::listing::{DiscoveredFunction, FlowKind, FlowType, Insn, Listing};
|
|
+
|
|
+fn instruction(at: u64, kind: FlowKind, fall: Option<u64>, targets: &[u64]) -> Insn {
|
|
+ Insn {
|
|
+ addr: at,
|
|
+ len: 4,
|
|
+ fall_through: fall,
|
|
+ flow: FlowType {
|
|
+ kind,
|
|
+ is_call: kind == FlowKind::Call,
|
|
+ is_jump: matches!(
|
|
+ kind,
|
|
+ FlowKind::ConditionalBranch
|
|
+ | FlowKind::UnconditionalBranch
|
|
+ | FlowKind::ComputedJump
|
|
+ ),
|
|
+ is_terminal: kind == FlowKind::Return,
|
|
+ is_conditional: kind == FlowKind::ConditionalBranch,
|
|
+ is_computed: kind == FlowKind::ComputedJump,
|
|
+ is_indirect: kind == FlowKind::ComputedJump,
|
|
+ has_fallthrough: fall.is_some(),
|
|
+ },
|
|
+ flows: targets.to_vec(),
|
|
+ mnemonic: String::new(),
|
|
+ operands: String::new(),
|
|
+ pcode: None,
|
|
+ }
|
|
+}
|
|
+
|
|
+fn listing(insns: Vec<Insn>) -> Listing {
|
|
+ let funcs = [0x100, 0x200, 0x300]
|
|
+ .into_iter()
|
|
+ .map(|entry| DiscoveredFunction {
|
|
+ entry,
|
|
+ name: None,
|
|
+ from_symbol: true,
|
|
+ has_no_return: entry == 0x200,
|
|
+ call_fixup: None,
|
|
+ })
|
|
+ .collect();
|
|
+ Listing::from_model_for_test(insns, funcs, vec![], vec![(0x100, 0x400)])
|
|
+}
|
|
+
|
|
+fn concludes(listing: &Listing) -> bool {
|
|
+ discover_noreturn(listing, true)
|
|
+ .iter()
|
|
+ .any(|f| f.addr == 0x100)
|
|
+}
|
|
+
|
|
+#[test]
|
|
+fn returning_tail_and_fatal_alternative_do_not_make_a_noreturn_function() {
|
|
+ // OpenSSH warning shape: one branch restores LR/SP and tail-transfers to
|
|
+ // free, while an allocation-failure branch calls a known fatal wrapper.
|
|
+ let model = listing(vec![
|
|
+ instruction(0x100, FlowKind::ConditionalBranch, Some(0x104), &[0x110]),
|
|
+ instruction(0x104, FlowKind::UnconditionalBranch, None, &[0x300]),
|
|
+ instruction(0x110, FlowKind::Call, None, &[0x200]),
|
|
+ ]);
|
|
+ assert!(!concludes(&model));
|
|
+}
|
|
+
|
|
+#[test]
|
|
+fn unknown_exits_cannot_be_hidden_by_another_fatal_path() {
|
|
+ for other in [
|
|
+ instruction(0x104, FlowKind::ComputedJump, None, &[]),
|
|
+ instruction(0x104, FlowKind::Call, None, &[0x300]),
|
|
+ instruction(0x104, FlowKind::Fallthrough, Some(0x108), &[]),
|
|
+ instruction(0x104, FlowKind::UnconditionalBranch, None, &[0x500]),
|
|
+ ] {
|
|
+ let model = listing(vec![
|
|
+ instruction(0x100, FlowKind::ConditionalBranch, Some(0x104), &[0x110]),
|
|
+ other,
|
|
+ instruction(0x110, FlowKind::Call, None, &[0x200]),
|
|
+ ]);
|
|
+ assert!(!concludes(&model));
|
|
+ }
|
|
+}
|
|
+
|
|
+#[test]
|
|
+fn genuine_terminal_paths_and_dead_return_remain_discoverable() {
|
|
+ let both = listing(vec![
|
|
+ instruction(0x100, FlowKind::ConditionalBranch, Some(0x104), &[0x110]),
|
|
+ instruction(0x104, FlowKind::UnconditionalBranch, None, &[0x200]),
|
|
+ instruction(0x110, FlowKind::Call, None, &[0x200]),
|
|
+ ]);
|
|
+ assert!(concludes(&both));
|
|
+ let dead_return = listing(vec![
|
|
+ instruction(0x100, FlowKind::Call, Some(0x104), &[0x200]),
|
|
+ instruction(0x104, FlowKind::Return, None, &[]),
|
|
+ ]);
|
|
+ assert!(concludes(&dead_return));
|
|
+}
|
|
+
|
|
+#[test]
|
|
+fn wrapper_fixpoint_keeps_real_noreturn_transitive() {
|
|
+ let model = listing(vec![
|
|
+ instruction(0x100, FlowKind::Call, None, &[0x300]),
|
|
+ instruction(0x300, FlowKind::Call, None, &[0x200]),
|
|
+ ]);
|
|
+ assert!(concludes(&model));
|
|
+}
|
|
+
|
|
+#[test]
|
|
+fn missing_call_successor_is_unknown_rather_than_a_noreturn_vote() {
|
|
+ let model = listing(vec![instruction(0x100, FlowKind::Call, None, &[0x300])]);
|
|
+ assert!(!call_site_has_no_fallthrough(&model, 0x100, 0x300, true));
|
|
+ let positive = listing(vec![instruction(
|
|
+ 0x100,
|
|
+ FlowKind::Call,
|
|
+ Some(0x200),
|
|
+ &[0x300],
|
|
+ )]);
|
|
+ assert!(call_site_has_no_fallthrough(&positive, 0x100, 0x300, true));
|
|
+}
|
|
diff --git a/decompiler/crates/kuna-analysis/src/analyzers/noreturn_disc/kuna_discstrict.rs b/decompiler/crates/kuna-analysis/src/analyzers/noreturn_disc/kuna_discstrict.rs
|
|
index f735bea7..78bee36d 100644
|
|
--- a/decompiler/crates/kuna-analysis/src/analyzers/noreturn_disc/kuna_discstrict.rs
|
|
+++ b/decompiler/crates/kuna-analysis/src/analyzers/noreturn_disc/kuna_discstrict.rs
|
|
@@ -39,9 +39,9 @@
|
|
//! - the successor is **another function's entry** — the compiler placed the next
|
|
//! function immediately after the call, leaving the caller no tail at all.
|
|
//!
|
|
-//! The terminal arm (a call with no fall-through VMA at all — a tail jump lowered
|
|
-//! to a call) stays in the caller, ahead of this test: it is a property of the call
|
|
-//! instruction, not of its successor.
|
|
+//! A call with no recorded successor contributes no new vote: a returning tail
|
|
+//! call also has no local continuation. The caller handles that unknown before
|
|
+//! this positive-successor test.
|
|
//!
|
|
//! Dropping the bare arm is what makes the data arm *live*. `is_data(fall)` implies
|
|
//! `!is_instruction_start(fall)`, so under the legacy order the data arm was
|
|
diff --git a/decompiler/crates/kuna-analysis/src/analyzers/noreturn_disc/mod.rs b/decompiler/crates/kuna-analysis/src/analyzers/noreturn_disc/mod.rs
|
|
index 52134fc3..d8107ba2 100644
|
|
--- a/decompiler/crates/kuna-analysis/src/analyzers/noreturn_disc/mod.rs
|
|
+++ b/decompiler/crates/kuna-analysis/src/analyzers/noreturn_disc/mod.rs
|
|
@@ -17,8 +17,9 @@
|
|
//! the `call` — the compiler knows control never comes back, so it does not emit a
|
|
//! sane fall-through there. When **≥3** call sites of a callee show this signal
|
|
//! (Ghidra's evidence threshold), the callee is concluded no-return. The result is
|
|
-//! iterated to a fixpoint: a function whose last act is a call to an
|
|
-//! already-discovered-no-return function is itself no-return.
|
|
+//! iterated to a fixpoint. Promotion requires every reachable path to end in
|
|
+//! known no-return code, using the same walk as structural propagation. A
|
|
+//! terminal alternative alone cannot erase another returning or unknown exit.
|
|
//!
|
|
//! This was **infeasible at the kuna-analysis tier until the Listing/xref tier**
|
|
//! (`docs/history/listing-tier-design.md`): it needs the program-wide disassembled Listing
|
|
@@ -51,9 +52,8 @@
|
|
//!
|
|
//! # Faithful scope / LOSS
|
|
//!
|
|
-//! - **Evidence threshold = 3**, matching Ghidra's `EVIDENCE_THRESHOLD`. Below the
|
|
-//! threshold a callee is left alone (a custom wrapper called only once or twice
|
|
-//! is not concluded no-return — the heuristic needs corroboration).
|
|
+//! - **Evidence threshold = 3** for the call-site heuristic. Below it, only
|
|
+//! separate all-path promotion can prove a known-terminal wrapper no-return.
|
|
//! - The "no-valid-fall-through" predicate is the Listing-faithful subset Ghidra's
|
|
//! `checkForNoReturn` reduces to at this tier: the fall-through VMA landed in
|
|
//! data (`is_data` — outside every executable range), or it is another
|
|
@@ -71,11 +71,15 @@
|
|
|
|
use std::collections::{BTreeMap, BTreeSet};
|
|
|
|
+use super::noreturn_propagate::known_noreturn_paths;
|
|
use crate::listing::{Listing, RefKind};
|
|
use crate::pass::{AnalysisCtx, AnalysisOutput, AnalysisPass, NoReturnFact, Phase};
|
|
|
|
pub mod kuna_discstrict;
|
|
|
|
+#[cfg(test)]
|
|
+mod flow_tests;
|
|
+
|
|
/// Ghidra's `FindNoReturnFunctionsAnalyzer` evidence threshold: a callee is
|
|
/// concluded no-return once at least this many of its call sites show no valid
|
|
/// fall-through (`EVIDENCE_THRESHOLD` / the operand-vote count).
|
|
@@ -117,14 +121,14 @@ impl AnalysisPass for NoReturnDiscoveredPass {
|
|
/// The instruction after the call (`call_vma + len`) has no valid fall-through
|
|
/// when it is not a decoded instruction start, OR it landed in data, OR it is the
|
|
/// entry of *another* function (the compiler placed the next function immediately
|
|
-/// after the dead call). A call instruction with no recorded fall-through VMA
|
|
-/// (terminal — e.g. a tail-call lowered to a jump) is itself evidence.
|
|
+/// after the dead call). A missing successor alone is unknown: tail calls to
|
|
+/// returning functions also have no local fall-through.
|
|
///
|
|
/// With `strict` (`noreturn_discstrict`, GH-312) the bare "not a decoded
|
|
/// instruction start" arm is dropped: the Listing walk always attempts a call's
|
|
/// successor, so that arm is a decode-failure detector and votes a callee
|
|
-/// no-return on kuna's own spec gaps. Only the terminal arm and the two positive
|
|
-/// arms survive — see [`kuna_discstrict`].
|
|
+/// no-return on kuna's own spec gaps. Only the two positive
|
|
+/// successor arms survive — see [`kuna_discstrict`].
|
|
fn call_site_has_no_fallthrough(
|
|
listing: &Listing,
|
|
call_vma: u64,
|
|
@@ -136,8 +140,10 @@ fn call_site_has_no_fallthrough(
|
|
return false;
|
|
};
|
|
let Some(fall) = insn.fall_through else {
|
|
- // A call with no fall-through at all (a lowered tail jump) is evidence.
|
|
- return true;
|
|
+ // No local successor is compatible with a returning tail call, an
|
|
+ // incomplete flow model, or a known no-return override. None can supply
|
|
+ // new evidence about a callee whose return behaviour is still unknown.
|
|
+ return false;
|
|
};
|
|
if strict {
|
|
return kuna_discstrict::successor_is_positive_evidence(listing, fall, callee);
|
|
@@ -187,48 +193,6 @@ fn skip_callee(listing: &Listing, callee: u64, terminal: &BTreeSet<u64>) -> bool
|
|
}
|
|
}
|
|
|
|
-/// Does function `entry` flow ONLY into an already-terminal (no-return) function
|
|
-/// — the fixpoint promotion rule (a function whose every terminal path is a call
|
|
-/// to an already-discovered-no-return function, with no returning path, is itself
|
|
-/// no-return)?
|
|
-///
|
|
-/// Sound guard against false positives: we require BOTH (a) the function's
|
|
-/// instruction span `[entry, next_entry)` contains a terminal call to a function
|
|
-/// in `terminal` (a no-fall-through call to a no-return callee), AND (b) the
|
|
-/// function contains NO `RETURN` instruction (no path returns). A function with a
|
|
-/// `ret` somewhere returns on that path and must not be promoted by this rule
|
|
-/// (the call-site evidence rule still applies to it independently). This is the
|
|
-/// cheap, Listing-faithful form of Ghidra's worklist feedback (a newly no-return
|
|
-/// function re-flags its tail-calling callers).
|
|
-fn last_act_is_terminal_call(
|
|
- listing: &Listing,
|
|
- entry: u64,
|
|
- terminal: &BTreeSet<u64>,
|
|
- strict: bool,
|
|
-) -> bool {
|
|
- let next = listing.next_function_after(entry).map(|f| f.entry);
|
|
- let mut has_terminal_call = false;
|
|
- for (&vma, insn) in listing.instructions_in_range(entry, next) {
|
|
- // (b) Any returning path disqualifies the function from this rule.
|
|
- if insn.flow.is_terminal {
|
|
- return false;
|
|
- }
|
|
- if !insn.flow.is_call {
|
|
- continue;
|
|
- }
|
|
- // (a) A call whose static callee is already terminal AND which itself has
|
|
- // no valid fall-through (the call is a terminal flow of this function).
|
|
- for &callee in &insn.flows {
|
|
- if terminal.contains(&callee)
|
|
- && call_site_has_no_fallthrough(listing, vma, callee, strict)
|
|
- {
|
|
- has_terminal_call = true;
|
|
- }
|
|
- }
|
|
- }
|
|
- has_terminal_call
|
|
-}
|
|
-
|
|
/// Run the evidence-tally + fixpoint over the Listing and return the discovered
|
|
/// no-return functions as [`NoReturnFact`]s (design §6 + Ghidra's
|
|
/// `FindNoReturnFunctionsAnalyzer`).
|
|
@@ -236,9 +200,9 @@ fn last_act_is_terminal_call(
|
|
/// Two rules, iterated to a fixpoint:
|
|
/// 1. **Call-site evidence**: a callee with `>= EVIDENCE_THRESHOLD` (3) call sites
|
|
/// showing no valid fall-through is no-return.
|
|
-/// 2. **Last-act promotion** (the fixpoint feedback): a function whose terminal
|
|
-/// instruction is a call to an already-discovered-no-return function is itself
|
|
-/// no-return — so a wrapper-of-a-wrapper converges.
|
|
+/// 2. **Reachable-path promotion**: the shared propagation walk proves every
|
|
+/// reachable exit is to an already-no-return callee. Returning tails, missing
|
|
+/// successors, unresolved jumps and escaping branches remain unknown.
|
|
///
|
|
/// The result is keyed by entry VMA (a `BTreeMap` so the emitted facts are
|
|
/// address-ordered and deduped). The `terminal` set seeds with the Known-list
|
|
@@ -246,8 +210,8 @@ fn last_act_is_terminal_call(
|
|
fn discover_noreturn(listing: &Listing, strict: bool) -> Vec<NoReturnFact> {
|
|
let mut discovered: BTreeMap<u64, NoReturnFact> = BTreeMap::new();
|
|
// The "already no-return" anchor set: the Known-seeded functions plus
|
|
- // everything discovered so far. A call to one of these is a no-fall-through
|
|
- // site for its caller (rule 2 reads this).
|
|
+ // everything discovered so far. The shared path walk treats a transfer to
|
|
+ // one of these as ending that path (rule 2 reads this).
|
|
let mut terminal: BTreeSet<u64> = listing
|
|
.functions()
|
|
.filter(|(_, f)| f.has_no_return)
|
|
@@ -272,9 +236,9 @@ fn discover_noreturn(listing: &Listing, strict: bool) -> Vec<NoReturnFact> {
|
|
if skip_callee(listing, entry, &terminal) {
|
|
continue;
|
|
}
|
|
- // Rule 1: direct call-site evidence. Rule 2: terminal-call promotion.
|
|
+ // Rule 1: direct call-site evidence. Rule 2: shared all-path promotion.
|
|
let concluded = no_fallthrough_evidence(listing, entry, strict) >= EVIDENCE_THRESHOLD
|
|
- || last_act_is_terminal_call(listing, entry, &terminal, strict);
|
|
+ || known_noreturn_paths(listing, entry, &terminal);
|
|
if concluded {
|
|
let name = listing
|
|
.function_at(entry)
|
|
@@ -294,12 +258,7 @@ fn discover_noreturn(listing: &Listing, strict: bool) -> Vec<NoReturnFact> {
|
|
mod tests {
|
|
use super::*;
|
|
|
|
- // The fixpoint convergence + threshold logic is exercised end-to-end by the
|
|
- // cross-crate `verify_noreturn_disc.rs` gate (a real x86-64 fixture with a
|
|
- // custom `die()` wrapper called >=3x). A unit test here would need a hand-built
|
|
- // `Listing`, whose private fields are not constructible outside the `listing`
|
|
- // module; the threshold/predicate are pinned by that e2e + the predicate
|
|
- // helpers below stay tiny and obviously-correct by inspection.
|
|
+ // Flow semantics and the actual fixpoint are exercised in flow_tests.
|
|
|
|
#[test]
|
|
fn threshold_is_three() {
|
|
diff --git a/decompiler/crates/kuna-analysis/src/analyzers/noreturn_propagate/mod.rs b/decompiler/crates/kuna-analysis/src/analyzers/noreturn_propagate/mod.rs
|
|
index f334a5a2..5b72c223 100644
|
|
--- a/decompiler/crates/kuna-analysis/src/analyzers/noreturn_propagate/mod.rs
|
|
+++ b/decompiler/crates/kuna-analysis/src/analyzers/noreturn_propagate/mod.rs
|
|
@@ -457,6 +457,18 @@ fn function_reaches_only_noreturn(
|
|
error_recog: &ErrorRecognizer,
|
|
) -> bool {
|
|
let next = listing.next_function_after(entry).map(|f| f.entry);
|
|
+ // A terminal transfer is necessary. Most functions have none: avoid a
|
|
+ // per-function map allocation and CFG walk for them in both consumers.
|
|
+ let has_terminal_transfer = listing.instructions_in_range(entry, next).any(|(_, insn)| {
|
|
+ (insn.flow.is_call || insn.flow.is_jump)
|
|
+ && insn.flows.iter().any(|target| {
|
|
+ terminal.contains(target)
|
|
+ || (insn.flow.is_call && error_recog.is_error(*target))
|
|
+ })
|
|
+ });
|
|
+ if !has_terminal_transfer {
|
|
+ return false;
|
|
+ }
|
|
// Address-indexed body for O(log n) successor lookup.
|
|
let body: BTreeMap<u64, &Insn> = listing
|
|
.instructions_in_range(entry, next)
|
|
@@ -465,6 +477,18 @@ fn function_reaches_only_noreturn(
|
|
reaches_only_noreturn_walk(&body, entry, next, terminal, error_recog)
|
|
}
|
|
|
|
+/// Discovered-no-return promotion shares the conservative CFG owner. The
|
|
+/// discovery pass has no per-call error(status) recognition authority, so only
|
|
+/// its current known-terminal set can end a path. A tail transfer to anything
|
|
+/// else preserves the caller's continuation as possibly returning.
|
|
+pub(super) fn known_noreturn_paths(
|
|
+ listing: &Listing,
|
|
+ entry: u64,
|
|
+ terminal: &BTreeSet<u64>,
|
|
+) -> bool {
|
|
+ function_reaches_only_noreturn(listing, entry, terminal, &ErrorRecognizer::disabled())
|
|
+}
|
|
+
|
|
/// The reachability walk core (extracted for unit testing): from `entry`, walk the
|
|
/// instruction-level reachable graph of `body` (bounded `[entry, next)`), treating
|
|
/// a call/jump to a callee in `terminal` (or a value-conditional `error(nonzero,…)`
|
|
diff --git a/decompiler/crates/kuna-analysis/src/listing/kuna_pc_relative.rs b/decompiler/crates/kuna-analysis/src/listing/kuna_pc_relative.rs
|
|
new file mode 100644
|
|
index 00000000..5385192d
|
|
--- /dev/null
|
|
+++ b/decompiler/crates/kuna-analysis/src/listing/kuna_pc_relative.rs
|
|
@@ -0,0 +1,493 @@
|
|
+//! Compose bounded, native p-code PC-relative address formations for xrefs.
|
|
+//!
|
|
+//! Pool words require a later contribution from the instruction's own PC.
|
|
+//! AArch64 page bases require an ADRP mnemonic from SLEIGH plus its exact
|
|
+//! constant COPY; a later COPY/ADD/SUB can then materialize the full address.
|
|
+//! Unknown writes, overlapping slices, calls, jumps and returns terminate the
|
|
+//! affected knowledge. Space identity is retained. State is function-local,
|
|
+//! follows only fall-through addresses, and retains the existing eight-hop /
|
|
+//! eight-value bounds. This is candidate static address formation, not runtime
|
|
+//! feasibility, alias analysis or a complete data-flow fixed point.
|
|
+
|
|
+use std::collections::HashMap;
|
|
+use std::rc::Rc;
|
|
+
|
|
+use kuna_base::space::{spacetype, AddrSpace};
|
|
+use kuna_num::opcodes::OpCode;
|
|
+use kuna_num::pcoderaw::VarnodeData;
|
|
+
|
|
+use super::kuna_poolref::PoolImage;
|
|
+use super::xrefs::{in_range, looks_like_address, FullOp, XrefKind};
|
|
+
|
|
+/// How many instructions a pool value may travel before the `add` that composes
|
|
+/// it. GCC and Clang emit the pair adjacent (the displacement is measured from
|
|
+/// the `add`'s own label, so the two are one template), but instruction
|
|
+/// scheduling separates them when several references are formed at once. Bounded
|
|
+/// because every hop is one more instruction over which the tracked register is
|
|
+/// believed.
|
|
+const MAX_HOPS: u8 = 8;
|
|
+
|
|
+/// How far past an instruction's own address a constant may lie and still be its
|
|
+/// materialized PC. A32 reads `pc` as `inst_start + 8`, Thumb as `+ 4`, x86 `rip`
|
|
+/// as the fall-through; nothing needs more slack than that.
|
|
+const PC_WINDOW: u64 = 16;
|
|
+
|
|
+/// How many pool values may be tracked into one instruction. A join point can be
|
|
+/// reached from several predecessors, each carrying its own; the cap keeps that
|
|
+/// from accumulating on a pathological CFG.
|
|
+const MAX_TRACKED: usize = 8;
|
|
+
|
|
+/// A provenanced value in processor storage; spaces are pointer identities
|
|
+/// owned by the live architecture, never serialized or compared across models.
|
|
+#[derive(Debug, Clone, Copy)]
|
|
+struct Pending {
|
|
+ key: Key,
|
|
+ known: Known,
|
|
+}
|
|
+
|
|
+/// One instruction and its separately validated architecture-specific seed.
|
|
+pub(super) struct Instruction<'a> {
|
|
+ pub vma: u64,
|
|
+ pub len: u32,
|
|
+ pub ops: &'a [FullOp],
|
|
+ pub pc_page: Option<PcPageCopy>,
|
|
+}
|
|
+
|
|
+/// Shape of an exact constant page COPY, not sufficient authority by itself.
|
|
+/// The caller must check SLEIGH's mnemonic for this same decoded instruction.
|
|
+pub(super) struct PcPageCopy(Pending);
|
|
+impl PcPageCopy {
|
|
+ pub(super) fn candidate(ops: &[FullOp], data: Option<&Rc<AddrSpace>>) -> Option<Self> {
|
|
+ let [op] = ops else { return None };
|
|
+ if op.opcode != OpCode::CPUI_COPY {
|
|
+ return None;
|
|
+ }
|
|
+ let out = op.out.as_ref()?;
|
|
+ let key = key_of(out, data)?;
|
|
+ let [input] = op.ins.as_slice() else {
|
|
+ return None;
|
|
+ };
|
|
+ if !matches!(key, Key::Reg(..))
|
|
+ || out.size != 8
|
|
+ || input.size != 8
|
|
+ || !is_constant(input)
|
|
+ || input.offset & 0xfff != 0
|
|
+ {
|
|
+ return None;
|
|
+ }
|
|
+ Some(Self(Pending {
|
|
+ key,
|
|
+ known: Known::seed(input.offset, true),
|
|
+ }))
|
|
+ }
|
|
+ pub(super) fn for_mnemonic(self, mnemonic: &str) -> Option<Self> {
|
|
+ mnemonic.eq_ignore_ascii_case("ADRP").then_some(self)
|
|
+ }
|
|
+}
|
|
+
|
|
+/// The pool values one function's walk is carrying, keyed by the address of the
|
|
+/// instruction they reach.
|
|
+///
|
|
+/// Keyed by address rather than held as "the previous instruction's state"
|
|
+/// because [`super::xrefs::build_with_focus`] walks a function breadth-first: at
|
|
+/// a conditional branch the queue interleaves the two successors, so "the
|
|
+/// instruction decoded before this one" is not the instruction that precedes it.
|
|
+#[derive(Default)]
|
|
+pub(super) struct PcRelativeValues {
|
|
+ at: HashMap<u64, Vec<Pending>>,
|
|
+}
|
|
+
|
|
+impl PcRelativeValues {
|
|
+ /// Consume one decoded instruction, and answer the addresses it composed.
|
|
+ ///
|
|
+ /// Called for every instruction of the function in whatever order the walk
|
|
+ /// reaches them; the answer is empty for all but the `add` of a PIC pair.
|
|
+ pub(super) fn step(
|
|
+ &mut self,
|
|
+ instruction: Instruction<'_>,
|
|
+ pool: Option<&PoolImage>,
|
|
+ mapped: &[(u64, u64)],
|
|
+ data_space: Option<&Rc<AddrSpace>>,
|
|
+ ) -> Vec<(u64, XrefKind)> {
|
|
+ let Instruction {
|
|
+ vma,
|
|
+ len,
|
|
+ ops,
|
|
+ pc_page,
|
|
+ } = instruction;
|
|
+ // Hashing every instruction's address would be this walk's whole cost on
|
|
+ // an architecture that never forms one of these pairs; nothing is in
|
|
+ // flight until a pool load puts it there.
|
|
+ let pending = if self.at.is_empty() {
|
|
+ Vec::new()
|
|
+ } else {
|
|
+ self.at.remove(&vma).unwrap_or_default()
|
|
+ };
|
|
+ let (mut refs, mut carry) = if pending.is_empty() {
|
|
+ (Vec::new(), Vec::new())
|
|
+ } else {
|
|
+ evaluate(vma, ops, &pending, mapped, data_space)
|
|
+ };
|
|
+
|
|
+ // A load out of the pool starts (or restarts) the value its register
|
|
+ // holds, so the pair `ldr; add` needs no separate entry point.
|
|
+ if let Some(fresh) = pc_page
|
|
+ .map(|p| p.0)
|
|
+ .or_else(|| pool.and_then(|p| seed(ops, p, mapped, data_space)))
|
|
+ {
|
|
+ carry.retain(|p| !overlaps(p, &fresh));
|
|
+ carry.push(fresh);
|
|
+ }
|
|
+
|
|
+ if !carry.is_empty() && !leaves_the_run(ops) {
|
|
+ let next = vma.wrapping_add(u64::from(len));
|
|
+ let slot = self.at.entry(next).or_default();
|
|
+ for mut p in carry {
|
|
+ p.known.hops += 1;
|
|
+ if p.known.hops <= MAX_HOPS && slot.len() < MAX_TRACKED {
|
|
+ slot.push(p);
|
|
+ }
|
|
+ }
|
|
+ if slot.is_empty() {
|
|
+ self.at.remove(&next);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ refs.sort_unstable();
|
|
+ refs.dedup();
|
|
+ refs
|
|
+ }
|
|
+}
|
|
+
|
|
+/// The composed addresses this instruction forms, and the tracked values that
|
|
+/// survive it.
|
|
+fn evaluate(
|
|
+ vma: u64,
|
|
+ ops: &[FullOp],
|
|
+ pending: &[Pending],
|
|
+ mapped: &[(u64, u64)],
|
|
+ data: Option<&Rc<AddrSpace>>,
|
|
+) -> (Vec<(u64, XrefKind)>, Vec<Pending>) {
|
|
+ let mut vals: Vec<(Key, Known)> = pending.iter().map(|p| (p.key, p.known)).collect();
|
|
+ let mut found: Vec<(u64, XrefKind)> = Vec::new();
|
|
+
|
|
+ let predicated = internal_predicate(ops);
|
|
+ for op in ops {
|
|
+ if !predicated {
|
|
+ if let Some(reference) = memory_reference(op, vma, &vals, data, mapped) {
|
|
+ found.push(reference);
|
|
+ }
|
|
+ }
|
|
+ let folded = if predicated {
|
|
+ None
|
|
+ } else {
|
|
+ fold(op, vma, &vals, data)
|
|
+ };
|
|
+ let Some(out) = &op.out else { continue };
|
|
+ let key = key_of(out, data);
|
|
+ invalidate(&mut vals, key);
|
|
+ let (Some(key), Some(mut val)) = (key, folded) else {
|
|
+ continue;
|
|
+ };
|
|
+ val.v &= mask(out.size);
|
|
+ if matches!(key, Key::Reg(..)) && val.seed && val.pc && in_range(mapped, val.v) {
|
|
+ found.push((val.v, XrefKind::Data));
|
|
+ }
|
|
+ vals.push((key, val));
|
|
+ }
|
|
+
|
|
+ // Carry register copies as well as original seeds; instruction-local
|
|
+ // temporaries never survive this boundary. Unknown/partial writes have
|
|
+ // already removed every overlapping value in that same native space.
|
|
+ let survivors = vals
|
|
+ .into_iter()
|
|
+ .filter(|(key, val)| matches!(key, Key::Reg(..)) && val.seed)
|
|
+ .map(|(key, known)| Pending { key, known })
|
|
+ .collect();
|
|
+ (found, survivors)
|
|
+}
|
|
+
|
|
+/// The pool word this instruction loads into a register, when it loads one that
|
|
+/// [`super::kuna_poolref`] declined to follow as a pointer.
|
|
+///
|
|
+/// The first such load, which on every architecture that forms these pairs is
|
|
+/// also the only one: a literal load's address is a decode-time constant, and a
|
|
+/// multi-register load (`ldm`) computes its addresses from a base register.
|
|
+fn seed(
|
|
+ ops: &[FullOp],
|
|
+ pool: &PoolImage,
|
|
+ mapped: &[(u64, u64)],
|
|
+ data: Option<&Rc<AddrSpace>>,
|
|
+) -> Option<Pending> {
|
|
+ let data_index = data.map(|d| d.get_index());
|
|
+ if internal_predicate(ops) {
|
|
+ return None;
|
|
+ }
|
|
+ for (index, op) in ops.iter().enumerate() {
|
|
+ if op.opcode != OpCode::CPUI_LOAD {
|
|
+ continue;
|
|
+ }
|
|
+ let Some(out) = &op.out else { continue };
|
|
+ if !matches!(key_of(out, data), Some(Key::Reg(..))) || out.size != pool.ptr_width() {
|
|
+ continue;
|
|
+ }
|
|
+ // `in0` is the space id: a load through some other space is not a read of
|
|
+ // the image.
|
|
+ let through_image = op
|
|
+ .ins
|
|
+ .first()
|
|
+ .zip(data_index)
|
|
+ .is_some_and(|(vn, idx)| vn.offset == u64::try_from(idx).unwrap_or(u64::MAX));
|
|
+ if !through_image {
|
|
+ continue;
|
|
+ }
|
|
+ let Some(addr) = op.ins.get(1).filter(|vn| is_constant(vn)) else {
|
|
+ continue;
|
|
+ };
|
|
+ let Some(word) = pool.word_at(addr.offset) else {
|
|
+ continue;
|
|
+ };
|
|
+ if looks_like_address(word) && in_range(mapped, word) {
|
|
+ continue; // a pointer, which is `poolref`'s to follow
|
|
+ }
|
|
+ let key = key_of(out, data)?;
|
|
+ let pending = Pending {
|
|
+ key,
|
|
+ known: Known::seed(word, false),
|
|
+ };
|
|
+ if ops[index + 1..]
|
|
+ .iter()
|
|
+ .filter_map(|op| op.out.as_ref())
|
|
+ .filter_map(|out| key_of(out, data))
|
|
+ .any(|key| {
|
|
+ overlaps(
|
|
+ &pending,
|
|
+ &Pending {
|
|
+ key,
|
|
+ known: pending.known,
|
|
+ },
|
|
+ )
|
|
+ })
|
|
+ {
|
|
+ continue;
|
|
+ }
|
|
+ return Some(pending);
|
|
+ }
|
|
+ None
|
|
+}
|
|
+
|
|
+/// An instruction-local conditional may skip its writes. Do not pretend all
|
|
+/// p-code paths execute in sequence; invalidate outputs and retain only values
|
|
+/// it cannot change. Machine conditional branches target code space instead.
|
|
+fn internal_predicate(ops: &[FullOp]) -> bool {
|
|
+ ops.iter()
|
|
+ .any(|op| op.opcode == OpCode::CPUI_CBRANCH && op.ins.first().is_some_and(is_constant))
|
|
+}
|
|
+
|
|
+/// Does control leave the straight-line run here, so the fall-through is not the
|
|
+/// next thing executed (or a call has clobbered the registers in between)?
|
|
+///
|
|
+/// A `CBRANCH` is deliberately not in the set: its fall-through IS a successor,
|
|
+/// it writes nothing, and ARM spells a predicated instruction that way.
|
|
+fn leaves_the_run(ops: &[FullOp]) -> bool {
|
|
+ ops.iter().any(|op| {
|
|
+ matches!(
|
|
+ op.opcode,
|
|
+ OpCode::CPUI_BRANCH
|
|
+ | OpCode::CPUI_BRANCHIND
|
|
+ | OpCode::CPUI_CALL
|
|
+ | OpCode::CPUI_CALLIND
|
|
+ | OpCode::CPUI_CALLOTHER
|
|
+ | OpCode::CPUI_RETURN
|
|
+ )
|
|
+ })
|
|
+}
|
|
+
|
|
+// --- the one-instruction constant fold ---------------------------------------
|
|
+
|
|
+/// A value the fold knows, and where it came from. Both provenance bits must be
|
|
+/// set for the result to be a PIC address: `seed` alone is the pool word being
|
|
+/// used as a number, `pc` alone is any PC-relative arithmetic.
|
|
+#[derive(Debug, Clone, Copy)]
|
|
+struct Known {
|
|
+ v: u64,
|
|
+ /// A validated image displacement or PC-page COPY contributed to this value.
|
|
+ seed: bool,
|
|
+ /// A PC contribution is known (possibly from an earlier ADRP).
|
|
+ pc: bool,
|
|
+ hops: u8,
|
|
+}
|
|
+
|
|
+impl Known {
|
|
+ fn seed(v: u64, pc: bool) -> Self {
|
|
+ Known {
|
|
+ v,
|
|
+ seed: true,
|
|
+ pc,
|
|
+ hops: 0,
|
|
+ }
|
|
+ }
|
|
+}
|
|
+
|
|
+/// A varnode's identity, in the two classes the fold models. `register` and `ram`
|
|
+/// are both processor spaces, so the space type alone cannot tell them apart —
|
|
+/// the default data space is what separates them.
|
|
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
+enum Key {
|
|
+ Reg(usize, u64, u32),
|
|
+ Tmp(usize, u64, u32),
|
|
+}
|
|
+
|
|
+fn key_of(vn: &VarnodeData, data: Option<&Rc<AddrSpace>>) -> Option<Key> {
|
|
+ let space = vn.space.as_ref()?;
|
|
+ match space.get_type() {
|
|
+ spacetype::IPTR_INTERNAL => Some(Key::Tmp(Rc::as_ptr(space) as usize, vn.offset, vn.size)),
|
|
+ spacetype::IPTR_PROCESSOR if !matches!(data, Some(d) if Rc::ptr_eq(space, d)) => {
|
|
+ Some(Key::Reg(Rc::as_ptr(space) as usize, vn.offset, vn.size))
|
|
+ }
|
|
+ _ => None,
|
|
+ }
|
|
+}
|
|
+
|
|
+fn is_constant(vn: &VarnodeData) -> bool {
|
|
+ vn.space
|
|
+ .as_ref()
|
|
+ .is_some_and(|s| s.get_type() == spacetype::IPTR_CONSTANT)
|
|
+}
|
|
+
|
|
+/// The value this op produces, or `None` when the fold does not model it.
|
|
+fn fold(
|
|
+ op: &FullOp,
|
|
+ vma: u64,
|
|
+ vals: &[(Key, Known)],
|
|
+ data: Option<&Rc<AddrSpace>>,
|
|
+) -> Option<Known> {
|
|
+ if !(1..=8).contains(&op.out.as_ref()?.size) {
|
|
+ return None;
|
|
+ }
|
|
+ let get = |i: usize| known_value(op.ins.get(i)?, vma, vals, data);
|
|
+ let a = get(0)?;
|
|
+ match op.opcode {
|
|
+ OpCode::CPUI_COPY => Some(a),
|
|
+ OpCode::CPUI_INT_ADD => {
|
|
+ let b = get(1)?;
|
|
+ Some(Known {
|
|
+ v: a.v.wrapping_add(b.v),
|
|
+ seed: a.seed || b.seed,
|
|
+ pc: a.pc || b.pc,
|
|
+ hops: a.hops.max(b.hops),
|
|
+ })
|
|
+ }
|
|
+ OpCode::CPUI_INT_SUB => {
|
|
+ let b = get(1)?;
|
|
+ Some(Known {
|
|
+ v: a.v.wrapping_sub(b.v),
|
|
+ seed: a.seed || b.seed,
|
|
+ pc: a.pc || b.pc,
|
|
+ hops: a.hops.max(b.hops),
|
|
+ })
|
|
+ }
|
|
+ _ => None,
|
|
+ }
|
|
+}
|
|
+
|
|
+fn known_value(
|
|
+ vn: &VarnodeData,
|
|
+ vma: u64,
|
|
+ vals: &[(Key, Known)],
|
|
+ data: Option<&Rc<AddrSpace>>,
|
|
+) -> Option<Known> {
|
|
+ if !(1..=8).contains(&vn.size) {
|
|
+ return None;
|
|
+ }
|
|
+ if is_constant(vn) {
|
|
+ return Some(Known {
|
|
+ v: vn.offset & mask(vn.size),
|
|
+ seed: false,
|
|
+ pc: is_own_pc(vn.offset, vma),
|
|
+ hops: 0,
|
|
+ });
|
|
+ }
|
|
+ let key = key_of(vn, data)?;
|
|
+ vals.iter().rev().find(|(k, _)| *k == key).map(|(_, v)| *v)
|
|
+}
|
|
+
|
|
+/// Only the native default-data space's byte-addressed LOAD/STORE operand
|
|
+/// is an access. Space selectors and stored integer values are not pointers.
|
|
+/// The cell is referenced; no mutable cell/GOT content is propagated as live.
|
|
+fn memory_reference(
|
|
+ op: &FullOp,
|
|
+ vma: u64,
|
|
+ vals: &[(Key, Known)],
|
|
+ data: Option<&Rc<AddrSpace>>,
|
|
+ mapped: &[(u64, u64)],
|
|
+) -> Option<(u64, XrefKind)> {
|
|
+ let kind = match op.opcode {
|
|
+ OpCode::CPUI_LOAD if op.ins.len() == 2 && op.out.as_ref().is_some_and(|v| v.size > 0) => {
|
|
+ XrefKind::Read
|
|
+ }
|
|
+ OpCode::CPUI_STORE if op.ins.len() == 3 && op.out.is_none() && op.ins[2].size > 0 => {
|
|
+ XrefKind::Write
|
|
+ }
|
|
+ _ => return None,
|
|
+ };
|
|
+ let data = data?;
|
|
+ if data.get_word_size() != 1 {
|
|
+ return None;
|
|
+ }
|
|
+ let selector = op.ins.first()?;
|
|
+ if !is_constant(selector) || u64::try_from(data.get_index()).ok()? != selector.offset {
|
|
+ return None;
|
|
+ }
|
|
+ let pointer = known_value(op.ins.get(1)?, vma, vals, Some(data))?;
|
|
+ (pointer.seed && pointer.pc && in_range(mapped, pointer.v)).then_some((pointer.v, kind))
|
|
+}
|
|
+
|
|
+/// Is `c` this instruction materializing its own program counter?
|
|
+fn is_own_pc(c: u64, vma: u64) -> bool {
|
|
+ c >= vma && c <= vma.wrapping_add(PC_WINDOW)
|
|
+}
|
|
+
|
|
+/// Drop every tracked value whose bytes overlap the write to `key`. Keying on the
|
|
+/// exact triple would let a write to `r0` leave a stale two-byte view of it
|
|
+/// behind, which is the one way this fold could hand out a value the program does
|
|
+/// not hold.
|
|
+fn invalidate(vals: &mut Vec<(Key, Known)>, key: Option<Key>) {
|
|
+ let Some(key) = key else { return };
|
|
+ let (cls, off, size) = split(key);
|
|
+ vals.retain(|(k, _)| {
|
|
+ let (c, o, s) = split(*k);
|
|
+ c != cls || !spans_overlap(o, s, off, size)
|
|
+ });
|
|
+}
|
|
+
|
|
+fn split(key: Key) -> ((u8, usize), u64, u32) {
|
|
+ match key {
|
|
+ Key::Reg(space, off, size) => ((0, space), off, size),
|
|
+ Key::Tmp(space, off, size) => ((1, space), off, size),
|
|
+ }
|
|
+}
|
|
+
|
|
+fn spans_overlap(a_off: u64, a_size: u32, b_off: u64, b_size: u32) -> bool {
|
|
+ let (a, b) = (u128::from(a_off), u128::from(b_off));
|
|
+ a < b + u128::from(b_size) && b < a + u128::from(a_size)
|
|
+}
|
|
+
|
|
+fn overlaps(a: &Pending, b: &Pending) -> bool {
|
|
+ let (ac, ao, az) = split(a.key);
|
|
+ let (bc, bo, bz) = split(b.key);
|
|
+ ac == bc && spans_overlap(ao, az, bo, bz)
|
|
+}
|
|
+
|
|
+/// The byte mask of an `n`-byte value.
|
|
+fn mask(size: u32) -> u64 {
|
|
+ if size == 0 || size >= 8 {
|
|
+ u64::MAX
|
|
+ } else {
|
|
+ (1u64 << (8 * size)) - 1
|
|
+ }
|
|
+}
|
|
+
|
|
+#[cfg(test)]
|
|
+#[path = "kuna_pc_relative/tests.rs"]
|
|
+mod tests;
|
|
diff --git a/decompiler/crates/kuna-analysis/src/listing/kuna_pc_relative/tests.rs b/decompiler/crates/kuna-analysis/src/listing/kuna_pc_relative/tests.rs
|
|
new file mode 100644
|
|
index 00000000..d52c80c8
|
|
--- /dev/null
|
|
+++ b/decompiler/crates/kuna-analysis/src/listing/kuna_pc_relative/tests.rs
|
|
@@ -0,0 +1,698 @@
|
|
+use super::*;
|
|
+
|
|
+/// The reduction of the filing image: a read-only pool at 0x6a0 holding the
|
|
+/// displacement -0x1c1, and `.rodata` at 0x400.
|
|
+const POOL: [u8; 8] = [
|
|
+ 0x3f, 0xfe, 0xff, 0xff, // 0x6a0 -> 0xfffffe3f (= -0x1c1)
|
|
+ 0x00, 0x05, 0x00, 0x00, // 0x6a4 -> 0x500 (a mapped address)
|
|
+];
|
|
+const MAPPED: [(u64, u64); 2] = [(0x400, 0x4c3), (0x4c4, 0x6b0)];
|
|
+
|
|
+fn image() -> PoolImage<'static> {
|
|
+ PoolImage::from_ranges(vec![(0x6a0, 0x6a8, &POOL[..])], 4, true).unwrap()
|
|
+}
|
|
+
|
|
+/// A throwaway space set: `ram` stands in for the default data space,
|
|
+/// `register` for any other processor space, as the two are told apart in
|
|
+/// the engine.
|
|
+struct Spaces {
|
|
+ konst: Rc<AddrSpace>,
|
|
+ ram: Rc<AddrSpace>,
|
|
+ reg: Rc<AddrSpace>,
|
|
+ uniq: Rc<AddrSpace>,
|
|
+}
|
|
+
|
|
+fn spaces() -> Spaces {
|
|
+ Spaces {
|
|
+ konst: Rc::new(AddrSpace::new_for_decode(spacetype::IPTR_CONSTANT)),
|
|
+ ram: Rc::new(AddrSpace::new_for_decode(spacetype::IPTR_PROCESSOR)),
|
|
+ reg: Rc::new(AddrSpace::new_for_decode(spacetype::IPTR_PROCESSOR)),
|
|
+ uniq: Rc::new(AddrSpace::new_for_decode(spacetype::IPTR_INTERNAL)),
|
|
+ }
|
|
+}
|
|
+
|
|
+/// The space-id constant a `LOAD` through `ram` carries in `in0`.
|
|
+fn ram_id(s: &Spaces) -> u64 {
|
|
+ u64::try_from(s.ram.get_index()).unwrap()
|
|
+}
|
|
+
|
|
+fn vn(space: &Rc<AddrSpace>, offset: u64, size: u32) -> VarnodeData {
|
|
+ VarnodeData {
|
|
+ space: Some(Rc::clone(space)),
|
|
+ offset,
|
|
+ size,
|
|
+ }
|
|
+}
|
|
+
|
|
+fn op(opcode: OpCode, out: Option<VarnodeData>, ins: Vec<VarnodeData>) -> FullOp {
|
|
+ FullOp { opcode, out, ins }
|
|
+}
|
|
+
|
|
+/// `ldr r0,[pc,#imm]` — SLEIGH resolves the PC-relative address at decode
|
|
+/// time, so the load reads a constant location.
|
|
+fn ldr(s: &Spaces, reg: u64, at: u64) -> Vec<FullOp> {
|
|
+ vec![op(
|
|
+ OpCode::CPUI_LOAD,
|
|
+ Some(vn(&s.reg, reg, 4)),
|
|
+ vec![vn(&s.konst, ram_id(s), 8), vn(&s.konst, at, 4)],
|
|
+ )]
|
|
+}
|
|
+
|
|
+/// `add rN,pc,rN` at `vma`, exactly as the ARM SLEIGH lifts it: the PC is
|
|
+/// `inst_start + 8`, formed in a temporary first.
|
|
+fn add_pc(s: &Spaces, reg: u64, vma: u64) -> Vec<FullOp> {
|
|
+ vec![
|
|
+ op(
|
|
+ OpCode::CPUI_INT_ADD,
|
|
+ Some(vn(&s.uniq, 0x2f00, 4)),
|
|
+ vec![vn(&s.konst, vma, 4), vn(&s.konst, 8, 4)],
|
|
+ ),
|
|
+ op(
|
|
+ OpCode::CPUI_INT_ADD,
|
|
+ Some(vn(&s.reg, reg, 4)),
|
|
+ vec![vn(&s.uniq, 0x2f00, 4), vn(&s.reg, reg, 4)],
|
|
+ ),
|
|
+ ]
|
|
+}
|
|
+
|
|
+/// The defect: the address of the literal is in no instruction and in no pool
|
|
+/// word — only in the two put together.
|
|
+#[test]
|
|
+fn a_pool_displacement_added_to_the_pc_is_the_address_it_forms() {
|
|
+ let s = spaces();
|
|
+ let mut pp = PcRelativeValues::default();
|
|
+ assert!(pp
|
|
+ .step(
|
|
+ Instruction {
|
|
+ vma: 0x660,
|
|
+ len: 4,
|
|
+ ops: &ldr(&s, 0x20, 0x6a0),
|
|
+ pc_page: None
|
|
+ },
|
|
+ Some(&image()),
|
|
+ &MAPPED,
|
|
+ Some(&s.ram)
|
|
+ )
|
|
+ .is_empty());
|
|
+ assert_eq!(
|
|
+ pp.step(
|
|
+ Instruction {
|
|
+ vma: 0x664,
|
|
+ len: 4,
|
|
+ ops: &add_pc(&s, 0x20, 0x664),
|
|
+ pc_page: None
|
|
+ },
|
|
+ Some(&image()),
|
|
+ &MAPPED,
|
|
+ Some(&s.ram)
|
|
+ ),
|
|
+ vec![(0x4ab, XrefKind::Data)],
|
|
+ "0x66c + (-0x1c1)"
|
|
+ );
|
|
+}
|
|
+
|
|
+/// The pair need not be adjacent: instruction scheduling separates the load
|
|
+/// from the `add` when a function forms several references at once.
|
|
+#[test]
|
|
+fn the_add_may_sit_a_few_instructions_past_the_load() {
|
|
+ let s = spaces();
|
|
+ let mut pp = PcRelativeValues::default();
|
|
+ let nop = vec![op(
|
|
+ OpCode::CPUI_INT_ADD,
|
|
+ Some(vn(&s.reg, 0x30, 4)),
|
|
+ vec![vn(&s.reg, 0x30, 4), vn(&s.konst, 4, 4)],
|
|
+ )];
|
|
+ pp.step(
|
|
+ Instruction {
|
|
+ vma: 0x660,
|
|
+ len: 4,
|
|
+ ops: &ldr(&s, 0x20, 0x6a0),
|
|
+ pc_page: None,
|
|
+ },
|
|
+ Some(&image()),
|
|
+ &MAPPED,
|
|
+ Some(&s.ram),
|
|
+ );
|
|
+ pp.step(
|
|
+ Instruction {
|
|
+ vma: 0x664,
|
|
+ len: 4,
|
|
+ ops: &nop,
|
|
+ pc_page: None,
|
|
+ },
|
|
+ Some(&image()),
|
|
+ &MAPPED,
|
|
+ Some(&s.ram),
|
|
+ );
|
|
+ assert_eq!(
|
|
+ pp.step(
|
|
+ Instruction {
|
|
+ vma: 0x668,
|
|
+ len: 4,
|
|
+ ops: &add_pc(&s, 0x20, 0x668),
|
|
+ pc_page: None
|
|
+ },
|
|
+ Some(&image()),
|
|
+ &MAPPED,
|
|
+ Some(&s.ram)
|
|
+ ),
|
|
+ vec![(0x4af, XrefKind::Data)]
|
|
+ );
|
|
+}
|
|
+
|
|
+/// Overwriting the register between the load and the `add` ends the value:
|
|
+/// what the `add` composes is whatever was written last.
|
|
+#[test]
|
|
+fn a_write_to_the_register_ends_the_tracked_word() {
|
|
+ let s = spaces();
|
|
+ let mut pp = PcRelativeValues::default();
|
|
+ let clobber = vec![op(
|
|
+ OpCode::CPUI_COPY,
|
|
+ Some(vn(&s.reg, 0x20, 4)),
|
|
+ vec![vn(&s.konst, 0, 4)],
|
|
+ )];
|
|
+ pp.step(
|
|
+ Instruction {
|
|
+ vma: 0x660,
|
|
+ len: 4,
|
|
+ ops: &ldr(&s, 0x20, 0x6a0),
|
|
+ pc_page: None,
|
|
+ },
|
|
+ Some(&image()),
|
|
+ &MAPPED,
|
|
+ Some(&s.ram),
|
|
+ );
|
|
+ pp.step(
|
|
+ Instruction {
|
|
+ vma: 0x664,
|
|
+ len: 4,
|
|
+ ops: &clobber,
|
|
+ pc_page: None,
|
|
+ },
|
|
+ Some(&image()),
|
|
+ &MAPPED,
|
|
+ Some(&s.ram),
|
|
+ );
|
|
+ assert!(pp
|
|
+ .step(
|
|
+ Instruction {
|
|
+ vma: 0x668,
|
|
+ len: 4,
|
|
+ ops: &add_pc(&s, 0x20, 0x668),
|
|
+ pc_page: None
|
|
+ },
|
|
+ Some(&image()),
|
|
+ &MAPPED,
|
|
+ Some(&s.ram)
|
|
+ )
|
|
+ .is_empty());
|
|
+}
|
|
+
|
|
+/// A call clobbers the caller-saved registers, so nothing survives it.
|
|
+#[test]
|
|
+fn a_call_ends_the_tracked_word() {
|
|
+ let s = spaces();
|
|
+ let mut pp = PcRelativeValues::default();
|
|
+ let mut call = ldr(&s, 0x20, 0x6a0);
|
|
+ call.push(op(OpCode::CPUI_CALL, None, vec![vn(&s.ram, 0x700, 4)]));
|
|
+ pp.step(
|
|
+ Instruction {
|
|
+ vma: 0x660,
|
|
+ len: 4,
|
|
+ ops: &call,
|
|
+ pc_page: None,
|
|
+ },
|
|
+ Some(&image()),
|
|
+ &MAPPED,
|
|
+ Some(&s.ram),
|
|
+ );
|
|
+ assert!(pp
|
|
+ .step(
|
|
+ Instruction {
|
|
+ vma: 0x664,
|
|
+ len: 4,
|
|
+ ops: &add_pc(&s, 0x20, 0x664),
|
|
+ pc_page: None
|
|
+ },
|
|
+ Some(&image()),
|
|
+ &MAPPED,
|
|
+ Some(&s.ram)
|
|
+ )
|
|
+ .is_empty());
|
|
+}
|
|
+
|
|
+/// The three refusals, each of which would be a fabricated reference.
|
|
+#[test]
|
|
+fn arithmetic_without_the_pc_a_pointer_word_and_an_unmapped_sum_are_declined() {
|
|
+ let s = spaces();
|
|
+
|
|
+ // `add r0,r0,#8` — the pool word used as a number, not as a base.
|
|
+ let mut pp = PcRelativeValues::default();
|
|
+ pp.step(
|
|
+ Instruction {
|
|
+ vma: 0x660,
|
|
+ len: 4,
|
|
+ ops: &ldr(&s, 0x20, 0x6a0),
|
|
+ pc_page: None,
|
|
+ },
|
|
+ Some(&image()),
|
|
+ &MAPPED,
|
|
+ Some(&s.ram),
|
|
+ );
|
|
+ let no_pc = vec![op(
|
|
+ OpCode::CPUI_INT_ADD,
|
|
+ Some(vn(&s.reg, 0x20, 4)),
|
|
+ vec![vn(&s.reg, 0x20, 4), vn(&s.konst, 8, 4)],
|
|
+ )];
|
|
+ assert!(pp
|
|
+ .step(
|
|
+ Instruction {
|
|
+ vma: 0x664,
|
|
+ len: 4,
|
|
+ ops: &no_pc,
|
|
+ pc_page: None
|
|
+ },
|
|
+ Some(&image()),
|
|
+ &MAPPED,
|
|
+ Some(&s.ram)
|
|
+ )
|
|
+ .is_empty());
|
|
+
|
|
+ // The word at 0x6a4 IS a mapped address, so it is `poolref`'s pointer to
|
|
+ // follow and never a displacement.
|
|
+ let mut pp = PcRelativeValues::default();
|
|
+ pp.step(
|
|
+ Instruction {
|
|
+ vma: 0x660,
|
|
+ len: 4,
|
|
+ ops: &ldr(&s, 0x20, 0x6a4),
|
|
+ pc_page: None,
|
|
+ },
|
|
+ Some(&image()),
|
|
+ &MAPPED,
|
|
+ Some(&s.ram),
|
|
+ );
|
|
+ assert!(pp
|
|
+ .step(
|
|
+ Instruction {
|
|
+ vma: 0x664,
|
|
+ len: 4,
|
|
+ ops: &add_pc(&s, 0x20, 0x664),
|
|
+ pc_page: None
|
|
+ },
|
|
+ Some(&image()),
|
|
+ &MAPPED,
|
|
+ Some(&s.ram)
|
|
+ )
|
|
+ .is_empty());
|
|
+
|
|
+ // A sum landing in no section is not an address, whatever formed it.
|
|
+ let mut pp = PcRelativeValues::default();
|
|
+ pp.step(
|
|
+ Instruction {
|
|
+ vma: 0x660,
|
|
+ len: 4,
|
|
+ ops: &ldr(&s, 0x20, 0x6a0),
|
|
+ pc_page: None,
|
|
+ },
|
|
+ Some(&image()),
|
|
+ &MAPPED,
|
|
+ Some(&s.ram),
|
|
+ );
|
|
+ assert!(pp
|
|
+ .step(
|
|
+ Instruction {
|
|
+ vma: 0x664,
|
|
+ len: 4,
|
|
+ ops: &add_pc(&s, 0x20, 0x664),
|
|
+ pc_page: None
|
|
+ },
|
|
+ Some(&image()),
|
|
+ &[(0x9000, 0x9100)],
|
|
+ Some(&s.ram)
|
|
+ )
|
|
+ .is_empty());
|
|
+}
|
|
+
|
|
+/// The GOT idiom `ldr r1,[pc,#imm]; ldr r1,[pc,r1]` composes its address into
|
|
+/// a temporary and dereferences it. The address is never held in a register,
|
|
+/// so it is a cell read, not an address-taken relation or a claim about the
|
|
+/// value loaded out of that cell.
|
|
+#[test]
|
|
+fn a_composed_temporary_load_references_the_cell_without_an_address_taken_claim() {
|
|
+ let s = spaces();
|
|
+ let mut pp = PcRelativeValues::default();
|
|
+ pp.step(
|
|
+ Instruction {
|
|
+ vma: 0x660,
|
|
+ len: 4,
|
|
+ ops: &ldr(&s, 0x24, 0x6a0),
|
|
+ pc_page: None,
|
|
+ },
|
|
+ Some(&image()),
|
|
+ &MAPPED,
|
|
+ Some(&s.ram),
|
|
+ );
|
|
+ let indirect = vec![
|
|
+ op(
|
|
+ OpCode::CPUI_INT_ADD,
|
|
+ Some(vn(&s.uniq, 0x2f00, 4)),
|
|
+ vec![vn(&s.konst, 0x664, 4), vn(&s.konst, 8, 4)],
|
|
+ ),
|
|
+ op(
|
|
+ OpCode::CPUI_INT_ADD,
|
|
+ Some(vn(&s.uniq, 0x10900, 4)),
|
|
+ vec![vn(&s.uniq, 0x2f00, 4), vn(&s.reg, 0x24, 4)],
|
|
+ ),
|
|
+ op(
|
|
+ OpCode::CPUI_LOAD,
|
|
+ Some(vn(&s.reg, 0x24, 4)),
|
|
+ vec![vn(&s.konst, ram_id(&s), 8), vn(&s.uniq, 0x10900, 4)],
|
|
+ ),
|
|
+ ];
|
|
+ assert_eq!(
|
|
+ pp.step(
|
|
+ Instruction {
|
|
+ vma: 0x664,
|
|
+ len: 4,
|
|
+ ops: &indirect,
|
|
+ pc_page: None
|
|
+ },
|
|
+ Some(&image()),
|
|
+ &MAPPED,
|
|
+ Some(&s.ram)
|
|
+ ),
|
|
+ vec![(0x4ab, XrefKind::Read)]
|
|
+ );
|
|
+}
|
|
+
|
|
+/// A narrow or unaligned pool read is not a slot at all, which is
|
|
+/// [`PoolImage`]'s rule and is inherited here.
|
|
+#[test]
|
|
+fn a_narrow_pool_read_is_not_a_displacement() {
|
|
+ let s = spaces();
|
|
+ let mut pp = PcRelativeValues::default();
|
|
+ let narrow = vec![op(
|
|
+ OpCode::CPUI_LOAD,
|
|
+ Some(vn(&s.reg, 0x20, 2)),
|
|
+ vec![vn(&s.konst, ram_id(&s), 8), vn(&s.konst, 0x6a0, 4)],
|
|
+ )];
|
|
+ pp.step(
|
|
+ Instruction {
|
|
+ vma: 0x660,
|
|
+ len: 4,
|
|
+ ops: &narrow,
|
|
+ pc_page: None,
|
|
+ },
|
|
+ Some(&image()),
|
|
+ &MAPPED,
|
|
+ Some(&s.ram),
|
|
+ );
|
|
+ assert!(pp
|
|
+ .step(
|
|
+ Instruction {
|
|
+ vma: 0x664,
|
|
+ len: 4,
|
|
+ ops: &add_pc(&s, 0x20, 0x664),
|
|
+ pc_page: None
|
|
+ },
|
|
+ Some(&image()),
|
|
+ &MAPPED,
|
|
+ Some(&s.ram)
|
|
+ )
|
|
+ .is_empty());
|
|
+}
|
|
+
|
|
+/// Nothing is tracked past the hop bound.
|
|
+#[test]
|
|
+fn a_word_is_not_believed_forever() {
|
|
+ let s = spaces();
|
|
+ let mut pp = PcRelativeValues::default();
|
|
+ let nothing: Vec<FullOp> = Vec::new();
|
|
+ pp.step(
|
|
+ Instruction {
|
|
+ vma: 0x600,
|
|
+ len: 4,
|
|
+ ops: &ldr(&s, 0x20, 0x6a0),
|
|
+ pc_page: None,
|
|
+ },
|
|
+ Some(&image()),
|
|
+ &MAPPED,
|
|
+ Some(&s.ram),
|
|
+ );
|
|
+ let mut vma = 0x604;
|
|
+ for _ in 0..MAX_HOPS {
|
|
+ pp.step(
|
|
+ Instruction {
|
|
+ vma: vma,
|
|
+ len: 4,
|
|
+ ops: ¬hing,
|
|
+ pc_page: None,
|
|
+ },
|
|
+ Some(&image()),
|
|
+ &MAPPED,
|
|
+ Some(&s.ram),
|
|
+ );
|
|
+ vma += 4;
|
|
+ }
|
|
+ assert!(pp
|
|
+ .step(
|
|
+ Instruction {
|
|
+ vma: vma,
|
|
+ len: 4,
|
|
+ ops: &add_pc(&s, 0x20, vma),
|
|
+ pc_page: None
|
|
+ },
|
|
+ Some(&image()),
|
|
+ &MAPPED,
|
|
+ Some(&s.ram)
|
|
+ )
|
|
+ .is_empty());
|
|
+}
|
|
+
|
|
+fn page_copy(s: &Spaces, reg: u64) -> Vec<FullOp> {
|
|
+ vec![op(
|
|
+ OpCode::CPUI_COPY,
|
|
+ Some(vn(&s.reg, reg, 8)),
|
|
+ vec![vn(&s.konst, 0xb0000, 8)],
|
|
+ )]
|
|
+}
|
|
+fn page_add(s: &Spaces, source: u64, output: u64, width: u32) -> Vec<FullOp> {
|
|
+ vec![op(
|
|
+ OpCode::CPUI_INT_ADD,
|
|
+ Some(vn(&s.reg, output, width)),
|
|
+ vec![vn(&s.reg, source, 8), vn(&s.konst, 0x468, 8)],
|
|
+ )]
|
|
+}
|
|
+fn begin_page(pp: &mut PcRelativeValues, s: &Spaces) {
|
|
+ let ops = page_copy(s, 0x4030);
|
|
+ let seed = PcPageCopy::candidate(&ops, Some(&s.ram))
|
|
+ .unwrap()
|
|
+ .for_mnemonic("ADRP");
|
|
+ assert!(pp
|
|
+ .step(
|
|
+ Instruction {
|
|
+ vma: 0x2574c,
|
|
+ len: 4,
|
|
+ ops: &ops,
|
|
+ pc_page: seed
|
|
+ },
|
|
+ None,
|
|
+ &[(0xb0000, 0xb2000)],
|
|
+ Some(&s.ram)
|
|
+ )
|
|
+ .is_empty());
|
|
+}
|
|
+fn advance(
|
|
+ pp: &mut PcRelativeValues,
|
|
+ s: &Spaces,
|
|
+ vma: u64,
|
|
+ ops: &[FullOp],
|
|
+) -> Vec<(u64, XrefKind)> {
|
|
+ pp.step(
|
|
+ Instruction {
|
|
+ vma,
|
|
+ len: 4,
|
|
+ ops,
|
|
+ pc_page: None,
|
|
+ },
|
|
+ None,
|
|
+ &[(0xb0000, 0xb2000)],
|
|
+ Some(&s.ram),
|
|
+ )
|
|
+}
|
|
+#[test]
|
|
+fn adrp_add_composes_without_a_literal_pool_or_a_register_offset_table() {
|
|
+ let s = spaces();
|
|
+ let mut pp = PcRelativeValues::default();
|
|
+ begin_page(&mut pp, &s);
|
|
+ assert_eq!(
|
|
+ advance(&mut pp, &s, 0x25750, &page_add(&s, 0x4030, 0x4030, 8)),
|
|
+ vec![(0xb0468, XrefKind::Data)]
|
|
+ );
|
|
+}
|
|
+#[test]
|
|
+fn a_plain_constant_copy_is_not_a_pc_page_authority() {
|
|
+ let s = spaces();
|
|
+ let ops = page_copy(&s, 0x4030);
|
|
+ assert!(PcPageCopy::candidate(&ops, Some(&s.ram))
|
|
+ .unwrap()
|
|
+ .for_mnemonic("MOV")
|
|
+ .is_none());
|
|
+ let mut pp = PcRelativeValues::default();
|
|
+ advance(&mut pp, &s, 0x2574c, &ops);
|
|
+ assert!(advance(&mut pp, &s, 0x25750, &page_add(&s, 0x4030, 0x4030, 8)).is_empty());
|
|
+ let mut narrow = page_copy(&s, 0x4030);
|
|
+ narrow[0].out.as_mut().unwrap().size = 4;
|
|
+ assert!(PcPageCopy::candidate(&narrow, Some(&s.ram)).is_none());
|
|
+}
|
|
+#[test]
|
|
+fn scheduled_register_copies_carry_identity_and_width() {
|
|
+ let s = spaces();
|
|
+ let mut pp = PcRelativeValues::default();
|
|
+ begin_page(&mut pp, &s);
|
|
+ advance(
|
|
+ &mut pp,
|
|
+ &s,
|
|
+ 0x25750,
|
|
+ &[op(
|
|
+ OpCode::CPUI_COPY,
|
|
+ Some(vn(&s.reg, 0x4040, 8)),
|
|
+ vec![vn(&s.reg, 0x4030, 8)],
|
|
+ )],
|
|
+ );
|
|
+ assert_eq!(
|
|
+ advance(&mut pp, &s, 0x25754, &page_add(&s, 0x4040, 0x4040, 8)),
|
|
+ vec![(0xb0468, XrefKind::Data)]
|
|
+ );
|
|
+}
|
|
+#[test]
|
|
+fn narrow_overwrites_end_the_page_but_other_spaces_do_not_clobber_it() {
|
|
+ let s = spaces();
|
|
+ for foreign in [false, true] {
|
|
+ let mut pp = PcRelativeValues::default();
|
|
+ begin_page(&mut pp, &s);
|
|
+ let other = Rc::new(AddrSpace::new_for_decode(spacetype::IPTR_PROCESSOR));
|
|
+ let space = if foreign { &other } else { &s.reg };
|
|
+ advance(
|
|
+ &mut pp,
|
|
+ &s,
|
|
+ 0x25750,
|
|
+ &[op(
|
|
+ OpCode::CPUI_COPY,
|
|
+ Some(vn(space, 0x4034, 4)),
|
|
+ vec![vn(&s.konst, 0, 4)],
|
|
+ )],
|
|
+ );
|
|
+ let refs = advance(&mut pp, &s, 0x25754, &page_add(&s, 0x4030, 0x4030, 8));
|
|
+ assert_eq!(
|
|
+ refs,
|
|
+ if foreign {
|
|
+ vec![(0xb0468, XrefKind::Data)]
|
|
+ } else {
|
|
+ vec![]
|
|
+ }
|
|
+ );
|
|
+ }
|
|
+}
|
|
+#[test]
|
|
+fn control_barriers_and_unmodelled_widths_do_not_invent_addresses() {
|
|
+ let s = spaces();
|
|
+ for opcode in [
|
|
+ OpCode::CPUI_CALL,
|
|
+ OpCode::CPUI_CALLIND,
|
|
+ OpCode::CPUI_BRANCH,
|
|
+ OpCode::CPUI_BRANCHIND,
|
|
+ OpCode::CPUI_RETURN,
|
|
+ ] {
|
|
+ let mut pp = PcRelativeValues::default();
|
|
+ begin_page(&mut pp, &s);
|
|
+ advance(&mut pp, &s, 0x25750, &[op(opcode, None, vec![])]);
|
|
+ assert!(advance(&mut pp, &s, 0x25754, &page_add(&s, 0x4030, 0x4030, 8)).is_empty());
|
|
+ }
|
|
+ for width in [0, 16] {
|
|
+ let mut pp = PcRelativeValues::default();
|
|
+ begin_page(&mut pp, &s);
|
|
+ assert!(advance(&mut pp, &s, 0x25750, &page_add(&s, 0x4030, 0x4030, width)).is_empty());
|
|
+ }
|
|
+}
|
|
+#[test]
|
|
+fn internal_predicates_and_same_instruction_pool_clobbers_are_unknown() {
|
|
+ let s = spaces();
|
|
+ let mut pp = PcRelativeValues::default();
|
|
+ begin_page(&mut pp, &s);
|
|
+ let mut conditional = vec![op(
|
|
+ OpCode::CPUI_CBRANCH,
|
|
+ None,
|
|
+ vec![vn(&s.konst, 2, 8), vn(&s.reg, 0, 1)],
|
|
+ )];
|
|
+ conditional.extend(page_add(&s, 0x4030, 0x4030, 8));
|
|
+ assert!(advance(&mut pp, &s, 0x25750, &conditional).is_empty());
|
|
+ assert!(advance(&mut pp, &s, 0x25754, &page_add(&s, 0x4030, 0x4030, 8)).is_empty());
|
|
+ let mut load = ldr(&s, 0x20, 0x6a0);
|
|
+ load.push(op(
|
|
+ OpCode::CPUI_COPY,
|
|
+ Some(vn(&s.reg, 0x20, 4)),
|
|
+ vec![vn(&s.konst, 0, 4)],
|
|
+ ));
|
|
+ assert!(seed(&load, &image(), &MAPPED, Some(&s.ram)).is_none());
|
|
+}
|
|
+#[test]
|
|
+fn the_last_u64_byte_is_still_a_real_overlapping_storage_byte() {
|
|
+ assert!(spans_overlap(u64::MAX, 1, u64::MAX, 1));
|
|
+ assert!(spans_overlap(u64::MAX - 1, 2, u64::MAX, 1));
|
|
+ assert!(!spans_overlap(u64::MAX - 1, 1, u64::MAX, 1));
|
|
+}
|
|
+
|
|
+#[test]
|
|
+fn page_based_load_and_store_reference_the_cell_without_claiming_its_contents() {
|
|
+ let s = spaces();
|
|
+ for opcode in [OpCode::CPUI_LOAD, OpCode::CPUI_STORE] {
|
|
+ let mut pp = PcRelativeValues::default();
|
|
+ begin_page(&mut pp, &s);
|
|
+ let mut ops = vec![op(
|
|
+ OpCode::CPUI_INT_ADD,
|
|
+ Some(vn(&s.uniq, 0x10, 8)),
|
|
+ vec![vn(&s.reg, 0x4030, 8), vn(&s.konst, 0x468, 8)],
|
|
+ )];
|
|
+ ops.push(op(
|
|
+ opcode,
|
|
+ if opcode == OpCode::CPUI_LOAD {
|
|
+ Some(vn(&s.reg, 0x4040, 8))
|
|
+ } else {
|
|
+ None
|
|
+ },
|
|
+ if opcode == OpCode::CPUI_LOAD {
|
|
+ vec![vn(&s.konst, ram_id(&s), 8), vn(&s.uniq, 0x10, 8)]
|
|
+ } else {
|
|
+ vec![
|
|
+ vn(&s.konst, ram_id(&s), 8),
|
|
+ vn(&s.uniq, 0x10, 8),
|
|
+ vn(&s.reg, 0, 8),
|
|
+ ]
|
|
+ },
|
|
+ ));
|
|
+ let kind = if opcode == OpCode::CPUI_LOAD {
|
|
+ XrefKind::Read
|
|
+ } else {
|
|
+ XrefKind::Write
|
|
+ };
|
|
+ assert_eq!(advance(&mut pp, &s, 0x25750, &ops), vec![(0xb0468, kind)]);
|
|
+ // LOAD content stays unknown even when its address was proven.
|
|
+ assert!(advance(&mut pp, &s, 0x25754, &page_add(&s, 0x4040, 0x4040, 8)).is_empty());
|
|
+ }
|
|
+}
|
|
+#[test]
|
|
+fn foreign_or_nonconstant_space_selectors_do_not_create_data_accesses() {
|
|
+ let s = spaces();
|
|
+ for selector in [vn(&s.konst, ram_id(&s) + 1, 8), vn(&s.reg, ram_id(&s), 8)] {
|
|
+ let mut pp = PcRelativeValues::default();
|
|
+ begin_page(&mut pp, &s);
|
|
+ let ops = vec![op(
|
|
+ OpCode::CPUI_LOAD,
|
|
+ Some(vn(&s.reg, 0x4040, 8)),
|
|
+ vec![selector, vn(&s.reg, 0x4030, 8)],
|
|
+ )];
|
|
+ assert!(advance(&mut pp, &s, 0x25750, &ops).is_empty());
|
|
+ }
|
|
+}
|
|
diff --git a/decompiler/crates/kuna-analysis/src/listing/kuna_picpool.rs b/decompiler/crates/kuna-analysis/src/listing/kuna_picpool.rs
|
|
deleted file mode 100644
|
|
index 053f04d8..00000000
|
|
--- a/decompiler/crates/kuna-analysis/src/listing/kuna_picpool.rs
|
|
+++ /dev/null
|
|
@@ -1,618 +0,0 @@
|
|
-//! (kuna) `picpool` — compose a literal-pool word with the PC that adds it, so a
|
|
-//! position-independent string reference has an owning function.
|
|
-//!
|
|
-//! # The gap
|
|
-//!
|
|
-//! [`super::kuna_poolref`] follows a pool word as an **absolute** pointer. In a
|
|
-//! position-independent ARM image the pool holds no pointer at all: it holds the
|
|
-//! signed distance from the `add` that consumes it to the datum, and the address
|
|
-//! only exists once the two are put together.
|
|
-//!
|
|
-//! ```text
|
|
-//! 0x660 ldr r0,[0x6a0] ; r0 = the word at 0x6a0 = 0xfffffe3f
|
|
-//! 0x664 add r0,pc,r0 ; r0 = 0x66c + (-0x1c1) = 0x4ab, "Benar! Flag: ..."
|
|
-//! 0x668 bl printf
|
|
-//! ...
|
|
-//! 0x6a0 3f fe ff ff
|
|
-//! ```
|
|
-//!
|
|
-//! `poolref` correctly declines 0xfffffe3f — it is not an address and lands in no
|
|
-//! section — so nothing in the program references 0x4ab, `kuna xrefs --to` it
|
|
-//! answers zero and `kuna strings --json` reports it with no owning function, on
|
|
-//! a binary whose disassembly plainly shows which function prints it. The idiom
|
|
-//! is not one site either: the filing image forms every one of its four string
|
|
-//! references this way.
|
|
-//!
|
|
-//! This is [`super::kuna_picbase`]'s defect with a per-site base instead of a
|
|
-//! module-wide one. There the PIC base is a register a prologue establishes once
|
|
-//! and the whole function inherits; here each reference carries its own base —
|
|
-//! the PC of its own `add` — so there is nothing to detect once and nothing to
|
|
-//! scope.
|
|
-//!
|
|
-//! # The rule
|
|
-//!
|
|
-//! Two decode-time constants, composed, and nothing else. A pool word is admitted
|
|
-//! as a *displacement* only when
|
|
-//!
|
|
-//! * it comes out of the same read-only, pointer-sized, pointer-aligned slot
|
|
-//! [`PoolImage`] already vouches for — the image's copy of it is what runs;
|
|
-//! * `poolref` **declined** it, so the two mechanisms never claim one word: a
|
|
-//! value that is already a mapped address is a pointer, not a displacement;
|
|
-//! * a later instruction in the same straight-line run folds it, through
|
|
-//! `COPY`/`INT_ADD`/`INT_SUB` alone, together with a constant this
|
|
-//! instruction materialized from its **own address** (SLEIGH spells a PC read
|
|
-//! that way: `inst_start + 8` on A32, `+ 4` on Thumb, the fall-through on
|
|
-//! x86);
|
|
-//! * and the folded value lands in a **register** and in a mapped section.
|
|
-//!
|
|
-//! Then the composed address is filed as [`XrefKind::Data`](super::xrefs::XrefKind::Data)
|
|
-//! from the instruction that completed it — the address-taken case, which is
|
|
-//! exactly what the `add` did, and attributing it to an instruction rather than
|
|
-//! to the pool word is what gives the reference an owning function.
|
|
-//!
|
|
-//! `ScalarOperandAnalyzer.checkOperands`' "below 4096 could be a number" floor
|
|
-//! is deliberately **not** applied to the composed value. That floor asks whether
|
|
-//! an *immediate* is an address or an integer; here the question is already
|
|
-//! settled by the PC, and applying it would discard every reference in an image
|
|
-//! mapped low — which the filing Android PIE is, its whole layout living under
|
|
-//! 0x2828.
|
|
-
|
|
-use std::collections::HashMap;
|
|
-use std::rc::Rc;
|
|
-
|
|
-use kuna_base::space::{spacetype, AddrSpace};
|
|
-use kuna_num::opcodes::OpCode;
|
|
-use kuna_num::pcoderaw::VarnodeData;
|
|
-
|
|
-use super::kuna_poolref::PoolImage;
|
|
-use super::xrefs::{in_range, looks_like_address, FullOp};
|
|
-
|
|
-/// How many instructions a pool value may travel before the `add` that composes
|
|
-/// it. GCC and Clang emit the pair adjacent (the displacement is measured from
|
|
-/// the `add`'s own label, so the two are one template), but instruction
|
|
-/// scheduling separates them when several references are formed at once. Bounded
|
|
-/// because every hop is one more instruction over which the tracked register is
|
|
-/// believed.
|
|
-const MAX_HOPS: u8 = 8;
|
|
-
|
|
-/// How far past an instruction's own address a constant may lie and still be its
|
|
-/// materialized PC. A32 reads `pc` as `inst_start + 8`, Thumb as `+ 4`, x86 `rip`
|
|
-/// as the fall-through; nothing needs more slack than that.
|
|
-const PC_WINDOW: u64 = 16;
|
|
-
|
|
-/// How many pool values may be tracked into one instruction. A join point can be
|
|
-/// reached from several predecessors, each carrying its own; the cap keeps that
|
|
-/// from accumulating on a pathological CFG.
|
|
-const MAX_TRACKED: usize = 8;
|
|
-
|
|
-/// A pool word in flight: the register holding it, its value, and how far it has
|
|
-/// travelled from the load.
|
|
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
-struct Pending {
|
|
- /// Register-space offset and size of the varnode holding the word.
|
|
- off: u64,
|
|
- size: u32,
|
|
- /// The word, zero-extended out of the image. The composition is a wrapping
|
|
- /// add masked to the register width, so a negative displacement needs no
|
|
- /// separate sign extension.
|
|
- word: u64,
|
|
- hops: u8,
|
|
-}
|
|
-
|
|
-/// The pool values one function's walk is carrying, keyed by the address of the
|
|
-/// instruction they reach.
|
|
-///
|
|
-/// Keyed by address rather than held as "the previous instruction's state"
|
|
-/// because [`super::xrefs::build_with_focus`] walks a function breadth-first: at
|
|
-/// a conditional branch the queue interleaves the two successors, so "the
|
|
-/// instruction decoded before this one" is not the instruction that precedes it.
|
|
-#[derive(Default)]
|
|
-pub(super) struct PicPool {
|
|
- at: HashMap<u64, Vec<Pending>>,
|
|
-}
|
|
-
|
|
-impl PicPool {
|
|
- /// Consume one decoded instruction, and answer the addresses it composed.
|
|
- ///
|
|
- /// Called for every instruction of the function in whatever order the walk
|
|
- /// reaches them; the answer is empty for all but the `add` of a PIC pair.
|
|
- pub(super) fn step(
|
|
- &mut self,
|
|
- vma: u64,
|
|
- len: u32,
|
|
- ops: &[FullOp],
|
|
- pool: &PoolImage,
|
|
- mapped: &[(u64, u64)],
|
|
- data_space: Option<&Rc<AddrSpace>>,
|
|
- ) -> Vec<u64> {
|
|
- // Hashing every instruction's address would be this walk's whole cost on
|
|
- // an architecture that never forms one of these pairs; nothing is in
|
|
- // flight until a pool load puts it there.
|
|
- let pending =
|
|
- if self.at.is_empty() { Vec::new() } else { self.at.remove(&vma).unwrap_or_default() };
|
|
- let (mut refs, mut carry) = if pending.is_empty() {
|
|
- (Vec::new(), Vec::new())
|
|
- } else {
|
|
- evaluate(vma, ops, &pending, mapped, data_space)
|
|
- };
|
|
-
|
|
- // A load out of the pool starts (or restarts) the value its register
|
|
- // holds, so the pair `ldr; add` needs no separate entry point.
|
|
- if let Some(fresh) = seed(ops, pool, mapped, data_space) {
|
|
- carry.retain(|p| !overlaps(p, &fresh));
|
|
- carry.push(fresh);
|
|
- }
|
|
-
|
|
- if !carry.is_empty() && !leaves_the_run(ops) {
|
|
- let next = vma.wrapping_add(u64::from(len));
|
|
- let slot = self.at.entry(next).or_default();
|
|
- for mut p in carry {
|
|
- p.hops += 1;
|
|
- if p.hops <= MAX_HOPS && slot.len() < MAX_TRACKED {
|
|
- slot.push(p);
|
|
- }
|
|
- }
|
|
- if slot.is_empty() {
|
|
- self.at.remove(&next);
|
|
- }
|
|
- }
|
|
-
|
|
- refs.sort_unstable();
|
|
- refs.dedup();
|
|
- refs
|
|
- }
|
|
-}
|
|
-
|
|
-/// The composed addresses this instruction forms, and the tracked values that
|
|
-/// survive it.
|
|
-fn evaluate(
|
|
- vma: u64,
|
|
- ops: &[FullOp],
|
|
- pending: &[Pending],
|
|
- mapped: &[(u64, u64)],
|
|
- data: Option<&Rc<AddrSpace>>,
|
|
-) -> (Vec<u64>, Vec<Pending>) {
|
|
- let mut vals: Vec<(Key, Known)> =
|
|
- pending.iter().map(|p| (Key::Reg(p.off, p.size), Known::seed(p.word))).collect();
|
|
- let mut written: Vec<(u64, u32)> = Vec::new();
|
|
- let mut found: Vec<u64> = Vec::new();
|
|
-
|
|
- for op in ops {
|
|
- let folded = fold(op, vma, &vals, data);
|
|
- let Some(out) = &op.out else { continue };
|
|
- let key = key_of(out, data);
|
|
- if let Some(Key::Reg(off, size)) = key {
|
|
- written.push((off, size));
|
|
- }
|
|
- invalidate(&mut vals, key);
|
|
- let (Some(key), Some(mut val)) = (key, folded) else { continue };
|
|
- val.v &= mask(out.size);
|
|
- if matches!(key, Key::Reg(..)) && val.seed && val.pc && in_range(mapped, val.v) {
|
|
- found.push(val.v);
|
|
- }
|
|
- vals.push((key, val));
|
|
- }
|
|
-
|
|
- let survivors = pending
|
|
- .iter()
|
|
- .filter(|p| {
|
|
- !written.iter().any(|&(off, size)| {
|
|
- spans_overlap(p.off, p.size, off, size)
|
|
- })
|
|
- })
|
|
- .copied()
|
|
- .collect();
|
|
- (found, survivors)
|
|
-}
|
|
-
|
|
-/// The pool word this instruction loads into a register, when it loads one that
|
|
-/// [`super::kuna_poolref`] declined to follow as a pointer.
|
|
-///
|
|
-/// The first such load, which on every architecture that forms these pairs is
|
|
-/// also the only one: a literal load's address is a decode-time constant, and a
|
|
-/// multi-register load (`ldm`) computes its addresses from a base register.
|
|
-fn seed(
|
|
- ops: &[FullOp],
|
|
- pool: &PoolImage,
|
|
- mapped: &[(u64, u64)],
|
|
- data: Option<&Rc<AddrSpace>>,
|
|
-) -> Option<Pending> {
|
|
- let data_index = data.map(|d| d.get_index());
|
|
- for op in ops {
|
|
- if op.opcode != OpCode::CPUI_LOAD {
|
|
- continue;
|
|
- }
|
|
- let Some(out) = &op.out else { continue };
|
|
- if !matches!(key_of(out, data), Some(Key::Reg(..))) || out.size != pool.ptr_width() {
|
|
- continue;
|
|
- }
|
|
- // `in0` is the space id: a load through some other space is not a read of
|
|
- // the image.
|
|
- let through_image = op
|
|
- .ins
|
|
- .first()
|
|
- .zip(data_index)
|
|
- .is_some_and(|(vn, idx)| vn.offset == u64::try_from(idx).unwrap_or(u64::MAX));
|
|
- if !through_image {
|
|
- continue;
|
|
- }
|
|
- let Some(addr) = op.ins.get(1).filter(|vn| is_constant(vn)) else { continue };
|
|
- let Some(word) = pool.word_at(addr.offset) else { continue };
|
|
- if looks_like_address(word) && in_range(mapped, word) {
|
|
- continue; // a pointer, which is `poolref`'s to follow
|
|
- }
|
|
- return Some(Pending { off: out.offset, size: out.size, word, hops: 0 });
|
|
- }
|
|
- None
|
|
-}
|
|
-
|
|
-/// Does control leave the straight-line run here, so the fall-through is not the
|
|
-/// next thing executed (or a call has clobbered the registers in between)?
|
|
-///
|
|
-/// A `CBRANCH` is deliberately not in the set: its fall-through IS a successor,
|
|
-/// it writes nothing, and ARM spells a predicated instruction that way.
|
|
-fn leaves_the_run(ops: &[FullOp]) -> bool {
|
|
- ops.iter().any(|op| {
|
|
- matches!(
|
|
- op.opcode,
|
|
- OpCode::CPUI_BRANCH
|
|
- | OpCode::CPUI_BRANCHIND
|
|
- | OpCode::CPUI_CALL
|
|
- | OpCode::CPUI_CALLIND
|
|
- | OpCode::CPUI_CALLOTHER
|
|
- | OpCode::CPUI_RETURN
|
|
- )
|
|
- })
|
|
-}
|
|
-
|
|
-// --- the one-instruction constant fold ---------------------------------------
|
|
-
|
|
-/// A value the fold knows, and where it came from. Both provenance bits must be
|
|
-/// set for the result to be a PIC address: `seed` alone is the pool word being
|
|
-/// used as a number, `pc` alone is any PC-relative arithmetic.
|
|
-#[derive(Debug, Clone, Copy)]
|
|
-struct Known {
|
|
- v: u64,
|
|
- /// A pool word contributed to this value.
|
|
- seed: bool,
|
|
- /// This instruction's own PC contributed to this value.
|
|
- pc: bool,
|
|
-}
|
|
-
|
|
-impl Known {
|
|
- fn seed(v: u64) -> Self {
|
|
- Known { v, seed: true, pc: false }
|
|
- }
|
|
-}
|
|
-
|
|
-/// A varnode's identity, in the two classes the fold models. `register` and `ram`
|
|
-/// are both processor spaces, so the space type alone cannot tell them apart —
|
|
-/// the default data space is what separates them.
|
|
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
-enum Key {
|
|
- Reg(u64, u32),
|
|
- Tmp(u64, u32),
|
|
-}
|
|
-
|
|
-fn key_of(vn: &VarnodeData, data: Option<&Rc<AddrSpace>>) -> Option<Key> {
|
|
- let space = vn.space.as_ref()?;
|
|
- match space.get_type() {
|
|
- spacetype::IPTR_INTERNAL => Some(Key::Tmp(vn.offset, vn.size)),
|
|
- spacetype::IPTR_PROCESSOR if !matches!(data, Some(d) if Rc::ptr_eq(space, d)) => {
|
|
- Some(Key::Reg(vn.offset, vn.size))
|
|
- }
|
|
- _ => None,
|
|
- }
|
|
-}
|
|
-
|
|
-fn is_constant(vn: &VarnodeData) -> bool {
|
|
- vn.space.as_ref().is_some_and(|s| s.get_type() == spacetype::IPTR_CONSTANT)
|
|
-}
|
|
-
|
|
-/// The value this op produces, or `None` when the fold does not model it.
|
|
-fn fold(
|
|
- op: &FullOp,
|
|
- vma: u64,
|
|
- vals: &[(Key, Known)],
|
|
- data: Option<&Rc<AddrSpace>>,
|
|
-) -> Option<Known> {
|
|
- let get = |i: usize| -> Option<Known> {
|
|
- let vn = op.ins.get(i)?;
|
|
- if is_constant(vn) {
|
|
- return Some(Known {
|
|
- v: vn.offset & mask(vn.size),
|
|
- seed: false,
|
|
- pc: is_own_pc(vn.offset, vma),
|
|
- });
|
|
- }
|
|
- let key = key_of(vn, data)?;
|
|
- vals.iter().rev().find(|(k, _)| *k == key).map(|(_, v)| *v)
|
|
- };
|
|
- let a = get(0)?;
|
|
- match op.opcode {
|
|
- OpCode::CPUI_COPY => Some(a),
|
|
- OpCode::CPUI_INT_ADD => {
|
|
- let b = get(1)?;
|
|
- Some(Known { v: a.v.wrapping_add(b.v), seed: a.seed || b.seed, pc: a.pc || b.pc })
|
|
- }
|
|
- OpCode::CPUI_INT_SUB => {
|
|
- let b = get(1)?;
|
|
- Some(Known { v: a.v.wrapping_sub(b.v), seed: a.seed || b.seed, pc: a.pc || b.pc })
|
|
- }
|
|
- _ => None,
|
|
- }
|
|
-}
|
|
-
|
|
-/// Is `c` this instruction materializing its own program counter?
|
|
-fn is_own_pc(c: u64, vma: u64) -> bool {
|
|
- c >= vma && c <= vma.wrapping_add(PC_WINDOW)
|
|
-}
|
|
-
|
|
-/// Drop every tracked value whose bytes overlap the write to `key`. Keying on the
|
|
-/// exact triple would let a write to `r0` leave a stale two-byte view of it
|
|
-/// behind, which is the one way this fold could hand out a value the program does
|
|
-/// not hold.
|
|
-fn invalidate(vals: &mut Vec<(Key, Known)>, key: Option<Key>) {
|
|
- let Some(key) = key else { return };
|
|
- let (cls, off, size) = split(key);
|
|
- vals.retain(|(k, _)| {
|
|
- let (c, o, s) = split(*k);
|
|
- c != cls || !spans_overlap(o, s, off, size)
|
|
- });
|
|
-}
|
|
-
|
|
-fn split(key: Key) -> (u8, u64, u32) {
|
|
- match key {
|
|
- Key::Reg(off, size) => (0, off, size),
|
|
- Key::Tmp(off, size) => (1, off, size),
|
|
- }
|
|
-}
|
|
-
|
|
-fn spans_overlap(a_off: u64, a_size: u32, b_off: u64, b_size: u32) -> bool {
|
|
- let a_end = a_off.saturating_add(u64::from(a_size));
|
|
- let b_end = b_off.saturating_add(u64::from(b_size));
|
|
- a_off < b_end && b_off < a_end
|
|
-}
|
|
-
|
|
-fn overlaps(a: &Pending, b: &Pending) -> bool {
|
|
- spans_overlap(a.off, a.size, b.off, b.size)
|
|
-}
|
|
-
|
|
-/// The byte mask of an `n`-byte value.
|
|
-fn mask(size: u32) -> u64 {
|
|
- if size == 0 || size >= 8 {
|
|
- u64::MAX
|
|
- } else {
|
|
- (1u64 << (8 * size)) - 1
|
|
- }
|
|
-}
|
|
-
|
|
-#[cfg(test)]
|
|
-mod tests {
|
|
- use super::*;
|
|
-
|
|
- /// The reduction of the filing image: a read-only pool at 0x6a0 holding the
|
|
- /// displacement -0x1c1, and `.rodata` at 0x400.
|
|
- const POOL: [u8; 8] = [
|
|
- 0x3f, 0xfe, 0xff, 0xff, // 0x6a0 -> 0xfffffe3f (= -0x1c1)
|
|
- 0x00, 0x05, 0x00, 0x00, // 0x6a4 -> 0x500 (a mapped address)
|
|
- ];
|
|
- const MAPPED: [(u64, u64); 2] = [(0x400, 0x4c3), (0x4c4, 0x6b0)];
|
|
-
|
|
- fn image() -> PoolImage<'static> {
|
|
- PoolImage::from_ranges(vec![(0x6a0, 0x6a8, &POOL[..])], 4, true).unwrap()
|
|
- }
|
|
-
|
|
- /// A throwaway space set: `ram` stands in for the default data space,
|
|
- /// `register` for any other processor space, as the two are told apart in
|
|
- /// the engine.
|
|
- struct Spaces {
|
|
- konst: Rc<AddrSpace>,
|
|
- ram: Rc<AddrSpace>,
|
|
- reg: Rc<AddrSpace>,
|
|
- uniq: Rc<AddrSpace>,
|
|
- }
|
|
-
|
|
- fn spaces() -> Spaces {
|
|
- Spaces {
|
|
- konst: Rc::new(AddrSpace::new_for_decode(spacetype::IPTR_CONSTANT)),
|
|
- ram: Rc::new(AddrSpace::new_for_decode(spacetype::IPTR_PROCESSOR)),
|
|
- reg: Rc::new(AddrSpace::new_for_decode(spacetype::IPTR_PROCESSOR)),
|
|
- uniq: Rc::new(AddrSpace::new_for_decode(spacetype::IPTR_INTERNAL)),
|
|
- }
|
|
- }
|
|
-
|
|
- /// The space-id constant a `LOAD` through `ram` carries in `in0`.
|
|
- fn ram_id(s: &Spaces) -> u64 {
|
|
- u64::try_from(s.ram.get_index()).unwrap()
|
|
- }
|
|
-
|
|
- fn vn(space: &Rc<AddrSpace>, offset: u64, size: u32) -> VarnodeData {
|
|
- VarnodeData { space: Some(Rc::clone(space)), offset, size }
|
|
- }
|
|
-
|
|
- fn op(opcode: OpCode, out: Option<VarnodeData>, ins: Vec<VarnodeData>) -> FullOp {
|
|
- FullOp { opcode, out, ins }
|
|
- }
|
|
-
|
|
- /// `ldr r0,[pc,#imm]` — SLEIGH resolves the PC-relative address at decode
|
|
- /// time, so the load reads a constant location.
|
|
- fn ldr(s: &Spaces, reg: u64, at: u64) -> Vec<FullOp> {
|
|
- vec![op(
|
|
- OpCode::CPUI_LOAD,
|
|
- Some(vn(&s.reg, reg, 4)),
|
|
- vec![vn(&s.konst, ram_id(s), 8), vn(&s.konst, at, 4)],
|
|
- )]
|
|
- }
|
|
-
|
|
- /// `add rN,pc,rN` at `vma`, exactly as the ARM SLEIGH lifts it: the PC is
|
|
- /// `inst_start + 8`, formed in a temporary first.
|
|
- fn add_pc(s: &Spaces, reg: u64, vma: u64) -> Vec<FullOp> {
|
|
- vec![
|
|
- op(
|
|
- OpCode::CPUI_INT_ADD,
|
|
- Some(vn(&s.uniq, 0x2f00, 4)),
|
|
- vec![vn(&s.konst, vma, 4), vn(&s.konst, 8, 4)],
|
|
- ),
|
|
- op(
|
|
- OpCode::CPUI_INT_ADD,
|
|
- Some(vn(&s.reg, reg, 4)),
|
|
- vec![vn(&s.uniq, 0x2f00, 4), vn(&s.reg, reg, 4)],
|
|
- ),
|
|
- ]
|
|
- }
|
|
-
|
|
- /// The defect: the address of the literal is in no instruction and in no pool
|
|
- /// word — only in the two put together.
|
|
- #[test]
|
|
- fn a_pool_displacement_added_to_the_pc_is_the_address_it_forms() {
|
|
- let s = spaces();
|
|
- let mut pp = PicPool::default();
|
|
- assert!(pp.step(0x660, 4, &ldr(&s, 0x20, 0x6a0), &image(), &MAPPED, Some(&s.ram)).is_empty());
|
|
- assert_eq!(
|
|
- pp.step(0x664, 4, &add_pc(&s, 0x20, 0x664), &image(), &MAPPED, Some(&s.ram)),
|
|
- vec![0x4ab],
|
|
- "0x66c + (-0x1c1)"
|
|
- );
|
|
- }
|
|
-
|
|
- /// The pair need not be adjacent: instruction scheduling separates the load
|
|
- /// from the `add` when a function forms several references at once.
|
|
- #[test]
|
|
- fn the_add_may_sit_a_few_instructions_past_the_load() {
|
|
- let s = spaces();
|
|
- let mut pp = PicPool::default();
|
|
- let nop = vec![op(
|
|
- OpCode::CPUI_INT_ADD,
|
|
- Some(vn(&s.reg, 0x30, 4)),
|
|
- vec![vn(&s.reg, 0x30, 4), vn(&s.konst, 4, 4)],
|
|
- )];
|
|
- pp.step(0x660, 4, &ldr(&s, 0x20, 0x6a0), &image(), &MAPPED, Some(&s.ram));
|
|
- pp.step(0x664, 4, &nop, &image(), &MAPPED, Some(&s.ram));
|
|
- assert_eq!(
|
|
- pp.step(0x668, 4, &add_pc(&s, 0x20, 0x668), &image(), &MAPPED, Some(&s.ram)),
|
|
- vec![0x4af]
|
|
- );
|
|
- }
|
|
-
|
|
- /// Overwriting the register between the load and the `add` ends the value:
|
|
- /// what the `add` composes is whatever was written last.
|
|
- #[test]
|
|
- fn a_write_to_the_register_ends_the_tracked_word() {
|
|
- let s = spaces();
|
|
- let mut pp = PicPool::default();
|
|
- let clobber =
|
|
- vec![op(OpCode::CPUI_COPY, Some(vn(&s.reg, 0x20, 4)), vec![vn(&s.konst, 0, 4)])];
|
|
- pp.step(0x660, 4, &ldr(&s, 0x20, 0x6a0), &image(), &MAPPED, Some(&s.ram));
|
|
- pp.step(0x664, 4, &clobber, &image(), &MAPPED, Some(&s.ram));
|
|
- assert!(pp
|
|
- .step(0x668, 4, &add_pc(&s, 0x20, 0x668), &image(), &MAPPED, Some(&s.ram))
|
|
- .is_empty());
|
|
- }
|
|
-
|
|
- /// A call clobbers the caller-saved registers, so nothing survives it.
|
|
- #[test]
|
|
- fn a_call_ends_the_tracked_word() {
|
|
- let s = spaces();
|
|
- let mut pp = PicPool::default();
|
|
- let mut call = ldr(&s, 0x20, 0x6a0);
|
|
- call.push(op(OpCode::CPUI_CALL, None, vec![vn(&s.ram, 0x700, 4)]));
|
|
- pp.step(0x660, 4, &call, &image(), &MAPPED, Some(&s.ram));
|
|
- assert!(pp
|
|
- .step(0x664, 4, &add_pc(&s, 0x20, 0x664), &image(), &MAPPED, Some(&s.ram))
|
|
- .is_empty());
|
|
- }
|
|
-
|
|
- /// The three refusals, each of which would be a fabricated reference.
|
|
- #[test]
|
|
- fn arithmetic_without_the_pc_a_pointer_word_and_an_unmapped_sum_are_declined() {
|
|
- let s = spaces();
|
|
-
|
|
- // `add r0,r0,#8` — the pool word used as a number, not as a base.
|
|
- let mut pp = PicPool::default();
|
|
- pp.step(0x660, 4, &ldr(&s, 0x20, 0x6a0), &image(), &MAPPED, Some(&s.ram));
|
|
- let no_pc = vec![op(
|
|
- OpCode::CPUI_INT_ADD,
|
|
- Some(vn(&s.reg, 0x20, 4)),
|
|
- vec![vn(&s.reg, 0x20, 4), vn(&s.konst, 8, 4)],
|
|
- )];
|
|
- assert!(pp.step(0x664, 4, &no_pc, &image(), &MAPPED, Some(&s.ram)).is_empty());
|
|
-
|
|
- // The word at 0x6a4 IS a mapped address, so it is `poolref`'s pointer to
|
|
- // follow and never a displacement.
|
|
- let mut pp = PicPool::default();
|
|
- pp.step(0x660, 4, &ldr(&s, 0x20, 0x6a4), &image(), &MAPPED, Some(&s.ram));
|
|
- assert!(pp
|
|
- .step(0x664, 4, &add_pc(&s, 0x20, 0x664), &image(), &MAPPED, Some(&s.ram))
|
|
- .is_empty());
|
|
-
|
|
- // A sum landing in no section is not an address, whatever formed it.
|
|
- let mut pp = PicPool::default();
|
|
- pp.step(0x660, 4, &ldr(&s, 0x20, 0x6a0), &image(), &MAPPED, Some(&s.ram));
|
|
- assert!(pp
|
|
- .step(0x664, 4, &add_pc(&s, 0x20, 0x664), &image(), &[(0x9000, 0x9100)], Some(&s.ram))
|
|
- .is_empty());
|
|
- }
|
|
-
|
|
- /// The GOT idiom `ldr r1,[pc,#imm]; ldr r1,[pc,r1]` composes its address into
|
|
- /// a temporary and dereferences it. The address is never held in a register,
|
|
- /// so nothing is filed — the double indirection is not this mechanism's.
|
|
- #[test]
|
|
- fn a_composed_address_that_stays_in_a_temporary_files_nothing() {
|
|
- let s = spaces();
|
|
- let mut pp = PicPool::default();
|
|
- pp.step(0x660, 4, &ldr(&s, 0x24, 0x6a0), &image(), &MAPPED, Some(&s.ram));
|
|
- let indirect = vec![
|
|
- op(
|
|
- OpCode::CPUI_INT_ADD,
|
|
- Some(vn(&s.uniq, 0x2f00, 4)),
|
|
- vec![vn(&s.konst, 0x664, 4), vn(&s.konst, 8, 4)],
|
|
- ),
|
|
- op(
|
|
- OpCode::CPUI_INT_ADD,
|
|
- Some(vn(&s.uniq, 0x10900, 4)),
|
|
- vec![vn(&s.uniq, 0x2f00, 4), vn(&s.reg, 0x24, 4)],
|
|
- ),
|
|
- op(
|
|
- OpCode::CPUI_LOAD,
|
|
- Some(vn(&s.reg, 0x24, 4)),
|
|
- vec![vn(&s.konst, ram_id(&s), 8), vn(&s.uniq, 0x10900, 4)],
|
|
- ),
|
|
- ];
|
|
- assert!(pp.step(0x664, 4, &indirect, &image(), &MAPPED, Some(&s.ram)).is_empty());
|
|
- }
|
|
-
|
|
- /// A narrow or unaligned pool read is not a slot at all, which is
|
|
- /// [`PoolImage`]'s rule and is inherited here.
|
|
- #[test]
|
|
- fn a_narrow_pool_read_is_not_a_displacement() {
|
|
- let s = spaces();
|
|
- let mut pp = PicPool::default();
|
|
- let narrow = vec![op(
|
|
- OpCode::CPUI_LOAD,
|
|
- Some(vn(&s.reg, 0x20, 2)),
|
|
- vec![vn(&s.konst, ram_id(&s), 8), vn(&s.konst, 0x6a0, 4)],
|
|
- )];
|
|
- pp.step(0x660, 4, &narrow, &image(), &MAPPED, Some(&s.ram));
|
|
- assert!(pp
|
|
- .step(0x664, 4, &add_pc(&s, 0x20, 0x664), &image(), &MAPPED, Some(&s.ram))
|
|
- .is_empty());
|
|
- }
|
|
-
|
|
- /// Nothing is tracked past the hop bound.
|
|
- #[test]
|
|
- fn a_word_is_not_believed_forever() {
|
|
- let s = spaces();
|
|
- let mut pp = PicPool::default();
|
|
- let nothing: Vec<FullOp> = Vec::new();
|
|
- pp.step(0x600, 4, &ldr(&s, 0x20, 0x6a0), &image(), &MAPPED, Some(&s.ram));
|
|
- let mut vma = 0x604;
|
|
- for _ in 0..MAX_HOPS {
|
|
- pp.step(vma, 4, ¬hing, &image(), &MAPPED, Some(&s.ram));
|
|
- vma += 4;
|
|
- }
|
|
- assert!(pp
|
|
- .step(vma, 4, &add_pc(&s, 0x20, vma), &image(), &MAPPED, Some(&s.ram))
|
|
- .is_empty());
|
|
- }
|
|
-}
|
|
diff --git a/decompiler/crates/kuna-analysis/src/listing/kuna_switchtable.rs b/decompiler/crates/kuna-analysis/src/listing/kuna_switchtable.rs
|
|
index 1b2ef57d..0691dca7 100644
|
|
--- a/decompiler/crates/kuna-analysis/src/listing/kuna_switchtable.rs
|
|
+++ b/decompiler/crates/kuna-analysis/src/listing/kuna_switchtable.rs
|
|
@@ -428,7 +428,7 @@ pub(super) enum Val {
|
|
}
|
|
|
|
/// A varnode's identity, in the two classes the fold models — the same split
|
|
-/// [`super::kuna_picpool`] makes, since `register` and `ram` are both processor
|
|
+/// [`super::kuna_pc_relative`] makes, since `register` and `ram` are both processor
|
|
/// spaces and only the default data space tells them apart.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub(super) enum Key {
|
|
diff --git a/decompiler/crates/kuna-analysis/src/listing/mod.rs b/decompiler/crates/kuna-analysis/src/listing/mod.rs
|
|
index e0f09af5..f69ee298 100644
|
|
--- a/decompiler/crates/kuna-analysis/src/listing/mod.rs
|
|
+++ b/decompiler/crates/kuna-analysis/src/listing/mod.rs
|
|
@@ -48,7 +48,7 @@ mod kuna_armwalkmode;
|
|
pub(crate) mod kuna_framemode;
|
|
pub(crate) mod kuna_decodereuse;
|
|
mod kuna_picbase;
|
|
-mod kuna_picpool;
|
|
+mod kuna_pc_relative;
|
|
mod kuna_poolref;
|
|
mod kuna_ppclocalentry;
|
|
mod kuna_switchtable;
|
|
diff --git a/decompiler/crates/kuna-analysis/src/listing/xrefs.rs b/decompiler/crates/kuna-analysis/src/listing/xrefs.rs
|
|
index b39058b1..9dc7ec0f 100644
|
|
--- a/decompiler/crates/kuna-analysis/src/listing/xrefs.rs
|
|
+++ b/decompiler/crates/kuna-analysis/src/listing/xrefs.rs
|
|
@@ -78,7 +78,7 @@ use object::SectionKind;
|
|
use super::classify::classify;
|
|
use super::context::ContextPainter;
|
|
use super::kuna_picbase::{self, Ctx as PicCtx, PicBase};
|
|
-use super::kuna_picpool::PicPool;
|
|
+use super::kuna_pc_relative::{Instruction, PcPageCopy, PcRelativeValues};
|
|
use super::kuna_poolref::PoolImage;
|
|
use super::kuna_switchtable;
|
|
use super::model::{FlowKind, RawOp};
|
|
@@ -554,10 +554,13 @@ impl PcodeEmit for FullCapture {
|
|
#[derive(Default)]
|
|
struct AsmCapture {
|
|
text: String,
|
|
+ mnemonic: String,
|
|
}
|
|
|
|
impl AssemblyEmit for AsmCapture {
|
|
fn dump(&mut self, _addr: &Address, mnem: &str, body: &str) {
|
|
+ self.mnemonic.clear();
|
|
+ self.mnemonic.push_str(mnem);
|
|
self.text.clear();
|
|
self.text.push_str(mnem);
|
|
if !body.is_empty() {
|
|
@@ -698,6 +701,7 @@ fn descend(
|
|
// processor). `None` when the program has no data space: no varnode can
|
|
// match it, so the direct-access projection simply contributes nothing.
|
|
let data_space = arch.manage().get_default_data_space().cloned();
|
|
+ let aarch64 = arch.get_description().starts_with("AARCH64:");
|
|
|
|
// (kuna) `picbase`: the module's PIC base register, when the program has one
|
|
// this can prove. Detected before the walk because the base a function
|
|
@@ -962,10 +966,10 @@ fn descend(
|
|
// body before any of it can be attributed (`kuna_picbase::scope`), and
|
|
// buffering it costs nothing on the overwhelmingly common `None` path.
|
|
let mut body: Vec<kuna_picbase::BaseCandidate> = Vec::new();
|
|
- // (kuna) `picpool`: the pool words this body is carrying towards the
|
|
+ // Bounded PC-relative values this body is carrying towards the
|
|
// `add` that turns each into an address. Per function, because the values
|
|
// it tracks are live only inside one straight-line run.
|
|
- let mut picpool = PicPool::default();
|
|
+ let mut pc_relative = PcRelativeValues::default();
|
|
while let Some((vma, mode)) = insn_queue.pop_front() {
|
|
if st.decoded.contains(&vma) {
|
|
continue; // already decoded (the VisitStat dedup)
|
|
@@ -1031,26 +1035,26 @@ fn descend(
|
|
let fall_through = vma.wrapping_add(len as u64);
|
|
data_refs(cap.ops(), data_space.as_ref(), &mapped, fall_through, pool.as_ref())
|
|
};
|
|
- // (kuna) `picpool`: the address a PC-relative literal-pool pair forms,
|
|
- // which is in neither of its two instructions on its own. See
|
|
- // [`super::kuna_picpool`].
|
|
- let picrefs = match pool.as_ref() {
|
|
- Some(p) => picpool.step(
|
|
- vma,
|
|
- len,
|
|
- cap.ops(),
|
|
- p,
|
|
- &mapped,
|
|
- data_space.as_ref(),
|
|
- ),
|
|
- None => Vec::new(),
|
|
- };
|
|
- // Every row this instruction produces carries the same render, and an
|
|
- // instruction that produces none needs no render at all.
|
|
+ // Reuse the bounded PC-relative owner for pool displacements and
|
|
+ // AArch64 split page addresses. SLEIGH supplies the mnemonic; no
|
|
+ // assembly operand parsing or register-offset table is duplicated.
|
|
+ let mut rendered = None;
|
|
+ let pc_page = if aarch64 && len == 4 {
|
|
+ PcPageCopy::candidate(cap.ops(), data_space.as_ref()).and_then(|candidate| {
|
|
+ let decoded = assembly(translate, vma, &code_space);
|
|
+ let seed = candidate.for_mnemonic(&decoded.mnemonic);
|
|
+ rendered = Some(decoded.text);
|
|
+ seed
|
|
+ })
|
|
+ } else { None };
|
|
+ let picrefs = pc_relative.step(
|
|
+ Instruction {vma, len, ops: cap.ops(), pc_page},
|
|
+ pool.as_ref(), &mapped, data_space.as_ref(),
|
|
+ );
|
|
let mut text = if c.flows.is_empty() && drefs.is_empty() && picrefs.is_empty() {
|
|
String::new()
|
|
} else {
|
|
- assembly(translate, vma, &code_space)
|
|
+ rendered.unwrap_or_else(|| assembly(translate, vma, &code_space).text)
|
|
};
|
|
|
|
if cap.ops().iter().any(|op| op.opcode == OpCode::CPUI_CALLIND) {
|
|
@@ -1150,7 +1154,7 @@ fn descend(
|
|
// and a row whose instruction column is blank does not say
|
|
// which jump reached the case body.
|
|
if !cases.is_empty() && text.is_empty() {
|
|
- text = assembly(translate, vma, &code_space);
|
|
+ text = assembly(translate, vma, &code_space).text;
|
|
}
|
|
for target in cases {
|
|
st.file(vma, target, XrefKind::Jump, &text);
|
|
@@ -1162,8 +1166,8 @@ fn descend(
|
|
for &(to, kind) in &drefs {
|
|
st.file(vma, to, kind, &text);
|
|
}
|
|
- for &to in &picrefs {
|
|
- st.file(vma, to, XrefKind::Data, &text);
|
|
+ for &(to, kind) in &picrefs {
|
|
+ st.file(vma, to, kind, &text);
|
|
}
|
|
// Both halves the deferred base-relative pass needs are pure
|
|
// functions of this instruction's ops, so they are computed here
|
|
@@ -1201,7 +1205,7 @@ fn descend(
|
|
// parse of the whole program on any image with a PIC base
|
|
// (an i386 `__x86.get_pc_thunk` binary: 154,608 renders to
|
|
// file 151 references).
|
|
- let text = assembly(translate, cand.vma, &code_space);
|
|
+ let text = assembly(translate, cand.vma, &code_space).text;
|
|
for &(to, kind) in &cand.refs {
|
|
st.file(cand.vma, to, kind, &text);
|
|
}
|
|
@@ -1336,7 +1340,7 @@ fn render_fingerprints(
|
|
let mut vma = entry;
|
|
for _ in 0..2 {
|
|
let Some(insn) = listing.insns.get_mut(&vma) else { break };
|
|
- let text = assembly(translate, vma, code_space);
|
|
+ let text = assembly(translate, vma, code_space).text;
|
|
insn.mnemonic = text.split_whitespace().next().unwrap_or_default().to_string();
|
|
vma = vma.wrapping_add(u64::from(insn.len));
|
|
}
|
|
@@ -1606,14 +1610,14 @@ pub(super) fn decode(
|
|
/// will actually carry the text. On a 466 KB obfuscated i386 image (154,638
|
|
/// instructions) rendering every decode cost 0.22 s of a 1.3 s walk, and the
|
|
/// large majority of instructions file no reference at all.
|
|
-fn assembly(translate: &dyn Translate, vma: u64, code_space: &Rc<AddrSpace>) -> String {
|
|
+fn assembly(translate: &dyn Translate, vma: u64, code_space: &Rc<AddrSpace>) -> AsmCapture {
|
|
|
|
let addr = Address::new(Rc::clone(code_space), vma);
|
|
let mut asm = AsmCapture::default();
|
|
- let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
|
- let _ = translate.print_assembly(&mut asm, &addr);
|
|
+ let decoded = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
|
+ translate.print_assembly(&mut asm, &addr)
|
|
}));
|
|
- asm.text
|
|
+ if matches!(decoded, Ok(Ok(len)) if len > 0) { asm } else { AsmCapture::default() }
|
|
}
|
|
|
|
/// The data references one instruction's p-code carries.
|
|
diff --git a/decompiler/crates/kuna-analysis/src/loader/elf_plt.rs b/decompiler/crates/kuna-analysis/src/loader/elf_plt.rs
|
|
index 4ab6cbca..abcf8c5b 100644
|
|
--- a/decompiler/crates/kuna-analysis/src/loader/elf_plt.rs
|
|
+++ b/decompiler/crates/kuna-analysis/src/loader/elf_plt.rs
|
|
@@ -43,6 +43,8 @@ use std::collections::{HashMap, HashSet};
|
|
use object::read::{Object, ObjectSection, ObjectSymbol};
|
|
use object::{Architecture, RelocationTarget};
|
|
|
|
+mod aarch64;
|
|
+
|
|
/// One resolved PLT-stub import: the stub entry address (the CALL target the
|
|
/// decompiler sees) and the clean imported name.
|
|
pub(crate) struct PltSym {
|
|
@@ -395,7 +397,7 @@ fn decode_plt_section(
|
|
}
|
|
Architecture::I386 => decode_i386(vma, data, i386_got_base, map, out, named_got),
|
|
Architecture::Aarch64 | Architecture::Aarch64_Ilp32 => {
|
|
- decode_aarch64(vma, data, map, out, named_got)
|
|
+ aarch64::decode(vma, data, map, out, named_got)
|
|
}
|
|
Architecture::Arm => decode_arm(vma, data, map, out, named_got),
|
|
Architecture::Riscv64 | Architecture::Riscv32 => {
|
|
@@ -470,7 +472,7 @@ fn decode_x86_64(
|
|
/// `jmp *disp32(%ebx)`, GOT-relative, where `%ebx` holds the GOT base (the PIC
|
|
/// prologue's `call __x86.get_pc_thunk.bx; add $_,%ebx` sets it to
|
|
/// `_GLOBAL_OFFSET_TABLE_`). The slot is `base + sign(disp32)`. This is the
|
|
-/// i386-PIE analog of [`decode_x86_64`] (RIP-relative) / [`decode_aarch64`]
|
|
+/// i386-PIE analog of [`decode_x86_64`] (RIP-relative) / [`aarch64::decode`]
|
|
/// (adrp/ldr veneer); naming these stubs lets the no-return pass flag `exit`
|
|
/// and collapses the spurious fall-through loop (angr
|
|
/// `test_decompiling_nl_i386_pie`). `i386_got_base` is `None` when the GOT
|
|
@@ -564,62 +566,6 @@ fn x86_stub_start(data: &[u8], i: usize, endbr: &[u8; 4]) -> usize {
|
|
// no-op, so those arches keep the pre-fix behavior.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
-/// AArch64 standard GNU `ld` import veneer (16-byte entry):
|
|
-/// ```text
|
|
-/// adrp x16, GOT_page ; PC-relative page of the GOT slot
|
|
-/// ldr x17, [x16, #lo12] ; 64-bit unsigned-offset load (scaled by 8)
|
|
-/// add x16, x16, #lo12
|
|
-/// br x17
|
|
-/// ```
|
|
-/// `got = (adrp_addr & !0xFFF) + (sign_extend_21(immhi:immlo) << 12) + imm12*8`.
|
|
-/// A64 is fixed 32-bit little-endian, so we scan at 4-byte alignment matching the
|
|
-/// `adrp x16` + `ldr x17,[x16]` pair; the 32-byte PLT0 header and any leading
|
|
-/// `bti c` fall out of the pattern, and the GOT-vs-map cross-check drops the rest.
|
|
-fn decode_aarch64(
|
|
- vma: u64,
|
|
- data: &[u8],
|
|
- map: &HashMap<u64, Vec<u8>>,
|
|
- out: &mut Vec<PltSym>,
|
|
- named_got: &mut HashSet<u64>,
|
|
-) {
|
|
- let mut off = 0usize;
|
|
- while off + 8 <= data.len() {
|
|
- let adrp = u32::from_le_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]]);
|
|
- let ldr =
|
|
- u32::from_le_bytes([data[off + 4], data[off + 5], data[off + 6], data[off + 7]]);
|
|
-
|
|
- // adrp Xd, imm: op=1 (bit31), bits[28:24]=10000; immlo=bits[30:29],
|
|
- // immhi=bits[23:5], Rd=bits[4:0].
|
|
- let is_adrp = (adrp >> 31) & 1 == 1 && (adrp >> 24) & 0x1f == 0b1_0000;
|
|
- let adrp_rd = adrp & 0x1f;
|
|
- // ldr Xt,[Xn,#imm] 64-bit unsigned offset: size=11(bits[31:30]),
|
|
- // bits[29:24]=111001, opc=01(bits[23:22]=LDR), imm12=bits[21:10],
|
|
- // Rn=bits[9:5], Rt=bits[4:0].
|
|
- let is_ldr = (ldr >> 30) & 0x3 == 0b11
|
|
- && (ldr >> 24) & 0x3f == 0b11_1001
|
|
- && (ldr >> 22) & 0x3 == 0b01;
|
|
- let ldr_rn = (ldr >> 5) & 0x1f;
|
|
- let ldr_rt = ldr & 0x1f;
|
|
-
|
|
- if is_adrp && adrp_rd == 16 && is_ldr && ldr_rn == 16 && ldr_rt == 17 {
|
|
- let immlo = ((adrp >> 29) & 0x3) as i64;
|
|
- let immhi = ((adrp >> 5) & 0x7_ffff) as i64; // 19 bits
|
|
- let mut imm21 = (immhi << 2) | immlo;
|
|
- if imm21 & (1 << 20) != 0 {
|
|
- imm21 -= 1 << 21; // sign-extend the 21-bit page offset
|
|
- }
|
|
- let page_off = imm21 << 12;
|
|
- let ldr_off = (((ldr >> 10) & 0xfff) as u64) * 8; // 64-bit scale
|
|
- let adrp_addr = vma.wrapping_add(off as u64);
|
|
- let got = (adrp_addr & !0xFFF)
|
|
- .wrapping_add(page_off as u64)
|
|
- .wrapping_add(ldr_off);
|
|
- record(adrp_addr, got, map, out, named_got);
|
|
- }
|
|
- off += 4;
|
|
- }
|
|
-}
|
|
-
|
|
/// ARM A32 "modified immediate" (data-processing operand2): an 8-bit value
|
|
/// `imm8` rotated right by `2*rot`. `op2` is the low 12 bits of the instruction.
|
|
fn arm_modified_immediate(op2: u32) -> u32 {
|
|
@@ -1159,32 +1105,6 @@ mod tests {
|
|
//! in `loadimage_object`'s tests.
|
|
use super::*;
|
|
|
|
- #[test]
|
|
- fn aarch64_plt_decode() {
|
|
- // clang bytes: `adrp x16,0x3000; ldr x17,[x16,#0xff8]; add x16,x16,#0xff8; br x17`.
|
|
- let mut data: Vec<u8> = vec![0u8; 32]; // dummy PLT0 header
|
|
- data.extend_from_slice(&0xf0000010u32.to_le_bytes()); // adrp x16, +0x3000
|
|
- data.extend_from_slice(&0xf947fe11u32.to_le_bytes()); // ldr x17,[x16,#0xff8]
|
|
- data.extend_from_slice(&0x913fe210u32.to_le_bytes()); // add x16,x16,#0xff8
|
|
- data.extend_from_slice(&0xd61f0220u32.to_le_bytes()); // br x17
|
|
-
|
|
- let vma = 0x10000u64;
|
|
- let adrp_addr = vma + 32;
|
|
- // got = (adrp_addr & !0xFFF) + page(0x3000) + ldr_off(0xff8)
|
|
- let expected_got = (adrp_addr & !0xFFF) + 0x3000 + 0xff8;
|
|
- assert_eq!(expected_got, 0x13ff8);
|
|
-
|
|
- let mut map: HashMap<u64, Vec<u8>> = HashMap::new();
|
|
- map.insert(expected_got, b"read".to_vec());
|
|
- let mut out: Vec<PltSym> = Vec::new();
|
|
- let mut named: HashSet<u64> = HashSet::new();
|
|
- decode_aarch64(vma, &data, &map, &mut out, &mut named);
|
|
-
|
|
- assert_eq!(out.len(), 1);
|
|
- assert_eq!(out[0].addr, adrp_addr);
|
|
- assert_eq!(out[0].name, b"read".to_vec());
|
|
- }
|
|
-
|
|
#[test]
|
|
fn arm_plt_decode() {
|
|
// clang bytes: `add ip,pc,#0x600000; add ip,ip,#0x3000; ldr pc,[ip,#0xff8]!`.
|
|
diff --git a/decompiler/crates/kuna-analysis/src/loader/elf_plt/aarch64.rs b/decompiler/crates/kuna-analysis/src/loader/elf_plt/aarch64.rs
|
|
new file mode 100644
|
|
index 00000000..ee493912
|
|
--- /dev/null
|
|
+++ b/decompiler/crates/kuna-analysis/src/loader/elf_plt/aarch64.rs
|
|
@@ -0,0 +1,237 @@
|
|
+//! AArch64 PLT entry identity, shared by section and PT_DYNAMIC scans.
|
|
+//!
|
|
+//! Recognize the complete standard GNU/LLVM veneer, with optional BTI c and
|
|
+//! AUTIA1716 as described in Arm's System V ABI, "Sample PLT sequences":
|
|
+//! https://github.com/ARM-software/abi-aa/blob/main/sysvabi64/sysvabi64.rst#sample-plt-sequences
|
|
+//! Correlate the decoded GOT slot with the relocation owner in the parent.
|
|
+//! Prefixes belong to the entry; the ADRP's own PC supplies the page. An
|
|
+//! ordinary preceding instruction is never absorbed into the entry. Other
|
|
+//! veneer forms remain unrecognized rather than inheriting a guessed name.
|
|
+
|
|
+use std::collections::{HashMap, HashSet};
|
|
+
|
|
+use super::{record, PltSym};
|
|
+
|
|
+const BTI_C: u32 = 0xd503245f;
|
|
+const AUTIA1716: u32 = 0xd503219f;
|
|
+const BR_X17: u32 = 0xd61f0220;
|
|
+
|
|
+fn word(data: &[u8], off: usize) -> Option<u32> {
|
|
+ let bytes = data.get(off..)?.get(..4)?;
|
|
+ Some(u32::from_le_bytes(bytes.try_into().ok()?))
|
|
+}
|
|
+
|
|
+pub(super) fn decode(
|
|
+ vma: u64,
|
|
+ data: &[u8],
|
|
+ map: &HashMap<u64, Vec<u8>>,
|
|
+ out: &mut Vec<PltSym>,
|
|
+ named_got: &mut HashSet<u64>,
|
|
+) {
|
|
+ if vma & 3 != 0 {
|
|
+ return;
|
|
+ }
|
|
+ for off in (0..data.len()).step_by(4) {
|
|
+ let Some(adrp) = word(data, off) else { break };
|
|
+ // ADRP X16, page; opcode plus destination, with both immediate fields free.
|
|
+ if adrp & 0x9f00001f != 0x90000010 {
|
|
+ continue;
|
|
+ }
|
|
+ let body = &data[off..];
|
|
+ let Some(ldr) = word(body, 4) else { continue };
|
|
+ // LDR X17,[X16,#imm12*8], not a 32-bit load, another register or a call.
|
|
+ if ldr & 0xffc003ff != 0xf9400211 {
|
|
+ continue;
|
|
+ }
|
|
+ let lo = ((ldr >> 10) & 0xfff) * 8;
|
|
+ // The ADD must form exactly the same GOT slot for the lazy resolver
|
|
+ // (and for PAC's modifier); neither an unrelated load nor an incomplete
|
|
+ // ADRP/LDR pair establishes an imported function.
|
|
+ if lo > 0xfff || word(body, 8) != Some(0x91000210 | (lo << 10)) {
|
|
+ continue;
|
|
+ }
|
|
+ let transfer = if word(body, 12) == Some(AUTIA1716) {
|
|
+ 16
|
|
+ } else {
|
|
+ 12
|
|
+ };
|
|
+ if word(body, transfer) != Some(BR_X17) {
|
|
+ continue;
|
|
+ }
|
|
+ let Some(adrp_addr) = vma.checked_add(off as u64) else {
|
|
+ break;
|
|
+ };
|
|
+ if adrp_addr.checked_add(transfer as u64 + 3).is_none() {
|
|
+ continue;
|
|
+ }
|
|
+ let imm21 = (((adrp >> 5) & 0x7ffff) << 2) | ((adrp >> 29) & 3);
|
|
+ let pages = ((imm21 as i32) << 11 >> 11) as i64;
|
|
+ let got = (adrp_addr & !0xfff)
|
|
+ .wrapping_add((pages << 12) as u64)
|
|
+ .wrapping_add(lo as u64);
|
|
+ let prefix = off.checked_sub(4).and_then(|at| word(data, at)) == Some(BTI_C);
|
|
+ let entry = if prefix { adrp_addr - 4 } else { adrp_addr };
|
|
+ record(entry, got, map, out, named_got);
|
|
+ }
|
|
+}
|
|
+
|
|
+#[cfg(test)]
|
|
+mod tests {
|
|
+ use super::{decode, PltSym};
|
|
+ use std::collections::{HashMap, HashSet};
|
|
+
|
|
+ // Actual Debian arm64 OpenSSH entry: BTI; ADRP 0x10f000; LDR #0x390;
|
|
+ // ADD #0x390; BR x17. Relocation 0x10f390 is free, not the neighbouring
|
|
+ // name printed by GNU objdump's synthetic fixed-stride PLT labels.
|
|
+ const BTI_C: u32 = 0xd503245f;
|
|
+ const AUTIA1716: u32 = 0xd503219f;
|
|
+ const BODY: [u32; 4] = [0x90000830, 0xf941ca11, 0x910e4210, 0xd61f0220];
|
|
+
|
|
+ fn words(words: &[u32]) -> Vec<u8> {
|
|
+ words.iter().flat_map(|word| word.to_le_bytes()).collect()
|
|
+ }
|
|
+
|
|
+ fn names(vma: u64, data: &[u8]) -> Vec<(u64, Vec<u8>)> {
|
|
+ let map = HashMap::from([(0x10f390, b"free".to_vec())]);
|
|
+ let mut out: Vec<PltSym> = Vec::new();
|
|
+ decode(vma, data, &map, &mut out, &mut HashSet::new());
|
|
+ out.into_iter().map(|s| (s.addr, s.name)).collect()
|
|
+ }
|
|
+
|
|
+ #[test]
|
|
+ fn aarch64_plt_decode() {
|
|
+ // clang bytes: `adrp x16,0x3000; ldr x17,[x16,#0xff8]; add x16,x16,#0xff8; br x17`.
|
|
+ let mut data: Vec<u8> = vec![0u8; 32]; // dummy PLT0 header
|
|
+ data.extend_from_slice(&0xf0000010u32.to_le_bytes()); // adrp x16, +0x3000
|
|
+ data.extend_from_slice(&0xf947fe11u32.to_le_bytes()); // ldr x17,[x16,#0xff8]
|
|
+ data.extend_from_slice(&0x913fe210u32.to_le_bytes()); // add x16,x16,#0xff8
|
|
+ data.extend_from_slice(&0xd61f0220u32.to_le_bytes()); // br x17
|
|
+
|
|
+ let vma = 0x10000u64;
|
|
+ let adrp_addr = vma + 32;
|
|
+ // got = (adrp_addr & !0xFFF) + page(0x3000) + ldr_off(0xff8)
|
|
+ let expected_got = (adrp_addr & !0xFFF) + 0x3000 + 0xff8;
|
|
+ assert_eq!(expected_got, 0x13ff8);
|
|
+
|
|
+ let mut map: HashMap<u64, Vec<u8>> = HashMap::new();
|
|
+ map.insert(expected_got, b"read".to_vec());
|
|
+ let mut out: Vec<PltSym> = Vec::new();
|
|
+ let mut named: HashSet<u64> = HashSet::new();
|
|
+ decode(vma, &data, &map, &mut out, &mut named);
|
|
+
|
|
+ assert_eq!(out.len(), 1);
|
|
+ assert_eq!(out[0].addr, adrp_addr);
|
|
+ assert_eq!(out[0].name, b"read".to_vec());
|
|
+ }
|
|
+
|
|
+ #[test]
|
|
+ fn bti_and_plain_entries_share_the_relocation_name_not_the_body_identity() {
|
|
+ let mut entry = vec![BTI_C];
|
|
+ entry.extend_from_slice(&BODY);
|
|
+ assert_eq!(names(0xbad0, &words(&entry)), [(0xbad0, b"free".to_vec())]);
|
|
+ assert_eq!(names(0xbad4, &words(&BODY)), [(0xbad4, b"free".to_vec())]);
|
|
+ }
|
|
+
|
|
+ #[test]
|
|
+ fn pac_and_bti_pac_entries_keep_their_entry_address() {
|
|
+ let mut entry = BODY[..3].to_vec();
|
|
+ entry.extend([AUTIA1716, BODY[3]]);
|
|
+ assert_eq!(names(0xbad4, &words(&entry)), [(0xbad4, b"free".to_vec())]);
|
|
+ entry.insert(0, BTI_C);
|
|
+ assert_eq!(names(0xbad0, &words(&entry)), [(0xbad0, b"free".to_vec())]);
|
|
+ }
|
|
+
|
|
+ #[test]
|
|
+ fn only_the_exact_immediate_bti_call_prefix_extends_the_entry() {
|
|
+ for prefix in [0xd503201f, 0xd503249f, 0xd50324df, 0xd503245e, 0] {
|
|
+ let mut entry = vec![prefix];
|
|
+ entry.extend_from_slice(&BODY);
|
|
+ assert_eq!(names(0xbad0, &words(&entry)), [(0xbad4, b"free".to_vec())]);
|
|
+ }
|
|
+ let mut entry = vec![BTI_C, 0xd503201f];
|
|
+ entry.extend_from_slice(&BODY);
|
|
+ assert_eq!(names(0xbacc, &words(&entry)), [(0xbad4, b"free".to_vec())]);
|
|
+ }
|
|
+
|
|
+ #[test]
|
|
+ fn incomplete_or_corrupt_veneer_never_consumes_a_relocation() {
|
|
+ for pac in [false, true] {
|
|
+ let mut entry = vec![BTI_C];
|
|
+ entry.extend_from_slice(&BODY[..3]);
|
|
+ if pac {
|
|
+ entry.push(AUTIA1716);
|
|
+ }
|
|
+ entry.push(BODY[3]);
|
|
+ let data = words(&entry);
|
|
+ for len in 0..data.len() {
|
|
+ assert!(
|
|
+ names(0xbad0, &data[..len]).is_empty(),
|
|
+ "truncated at {len}, PAC={pac}"
|
|
+ );
|
|
+ }
|
|
+ }
|
|
+ for (index, word) in [
|
|
+ (0, BODY[0] ^ 1), // wrong page register
|
|
+ (1, BODY[1] ^ 1), // wrong load register
|
|
+ (1, BODY[1] ^ (1 << 30)), // 32-bit load
|
|
+ (2, 0xd503201f), // no ADD
|
|
+ (2, BODY[2] ^ (1 << 10)), // inconsistent GOT modifier
|
|
+ (2, BODY[2] | (1 << 22)), // ADD's shifted immediate
|
|
+ (3, 0xd63f0220), // BLR is a call, not the veneer transfer
|
|
+ (3, 0xd61f0200), // BR x16 is not the loaded x17
|
|
+ ] {
|
|
+ let mut body = BODY;
|
|
+ body[index] = word;
|
|
+ assert!(
|
|
+ names(0xbad4, &words(&body)).is_empty(),
|
|
+ "corrupt index {index}"
|
|
+ );
|
|
+ }
|
|
+ let mut body = BODY[..3].to_vec();
|
|
+ body.extend([0xd503219e, BODY[3]]); // not AUTIA1716
|
|
+ assert!(names(0xbad4, &words(&body)).is_empty());
|
|
+ }
|
|
+
|
|
+ #[test]
|
|
+ fn adjacent_entries_use_adrp_pc_and_exact_slots_without_stride_assumptions() {
|
|
+ // First ADRP sits on the next page after its BTI, with a negative page
|
|
+ // delta. The second plain entry is adjacent: no fixed 16/24-byte stride.
|
|
+ let first = [BTI_C, 0xf0fffff0, 0xf947fe11, 0x913fe210, 0xd61f0220];
|
|
+ let second = [0xf0fffff0, 0xf947fa11, 0x913fc210, 0xd61f0220];
|
|
+ let mut data = words(&first);
|
|
+ data.extend(words(&second));
|
|
+ let map = HashMap::from([(0x30ff8, b"one".to_vec()), (0x30ff0, b"two".to_vec())]);
|
|
+ let mut out = Vec::new();
|
|
+ let mut named = HashSet::new();
|
|
+ decode(0x30ffc, &data, &map, &mut out, &mut named);
|
|
+ assert_eq!(
|
|
+ out.iter()
|
|
+ .map(|s| (s.addr, s.name.as_slice()))
|
|
+ .collect::<Vec<_>>(),
|
|
+ [(0x30ffc, b"one".as_slice()), (0x31010, b"two".as_slice())]
|
|
+ );
|
|
+ decode(0x30ffc, &data, &map, &mut out, &mut named);
|
|
+ assert_eq!(out.len(), 2, "shared GOT deduplication remains intact");
|
|
+ }
|
|
+
|
|
+ #[test]
|
|
+ fn absent_slot_misalignment_and_overflow_cannot_create_entries() {
|
|
+ let data = words(&BODY);
|
|
+ assert!(names(0xbad5, &data).is_empty());
|
|
+ assert!(names(0xbbad4, &data).is_empty());
|
|
+ let mut out = Vec::new();
|
|
+ let map = HashMap::from([(0x104390, b"wrapped".to_vec())]);
|
|
+ decode(
|
|
+ 0xbad4,
|
|
+ &data,
|
|
+ &HashMap::new(),
|
|
+ &mut out,
|
|
+ &mut HashSet::new(),
|
|
+ );
|
|
+ assert!(out.is_empty());
|
|
+ let mut overflow = words(&[BTI_C, BTI_C]);
|
|
+ overflow.extend_from_slice(&data);
|
|
+ decode(u64::MAX - 3, &overflow, &map, &mut out, &mut HashSet::new());
|
|
+ assert!(out.is_empty());
|
|
+ }
|
|
+}
|
|
diff --git a/decompiler/crates/kuna-cli/Cargo.toml b/decompiler/crates/kuna-cli/Cargo.toml
|
|
index 06231a77..db79599c 100644
|
|
--- a/decompiler/crates/kuna-cli/Cargo.toml
|
|
+++ b/decompiler/crates/kuna-cli/Cargo.toml
|
|
@@ -9,6 +9,8 @@ license.workspace = true
|
|
publish.workspace = true
|
|
|
|
[dependencies]
|
|
+base64 = "=0.22.1"
|
|
+flate2 = "=1.1.9"
|
|
kuna-decomp.workspace = true
|
|
kuna-base.workspace = true
|
|
kuna-console.workspace = true
|
|
diff --git a/decompiler/crates/kuna-cli/src/atomic_compression.rs b/decompiler/crates/kuna-cli/src/atomic_compression.rs
|
|
new file mode 100644
|
|
index 00000000..48631d39
|
|
--- /dev/null
|
|
+++ b/decompiler/crates/kuna-cli/src/atomic_compression.rs
|
|
@@ -0,0 +1,122 @@
|
|
+//! Bounded lossless wire encoding of logical schema-3 atomic graphs.
|
|
+use crate::jsonfmt::{dumps_compact_bounded, Json};
|
|
+use base64::{engine::general_purpose::STANDARD, Engine as _};
|
|
+use flate2::{write::GzEncoder, Compression};
|
|
+use std::io::{self, Write};
|
|
+
|
|
+const MAX_RAW_BYTES: usize = 32 * 1024 * 1024;
|
|
+struct BoundedBytes {
|
|
+ data: Vec<u8>,
|
|
+ limit: usize,
|
|
+}
|
|
+impl Write for BoundedBytes {
|
|
+ fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
|
|
+ let next = self
|
|
+ .data
|
|
+ .len()
|
|
+ .checked_add(bytes.len())
|
|
+ .filter(|n| *n <= self.limit)
|
|
+ .ok_or_else(|| io::Error::other("atomic compression capacity exceeded"))?;
|
|
+ if next > self.data.capacity() {
|
|
+ let capacity = next
|
|
+ .max(self.data.capacity().saturating_mul(2))
|
|
+ .min(self.limit);
|
|
+ self.data
|
|
+ .try_reserve_exact(capacity - self.data.len())
|
|
+ .map_err(|_| io::Error::other("atomic compression allocation failed"))?;
|
|
+ }
|
|
+ self.data.extend_from_slice(bytes);
|
|
+ Ok(bytes.len())
|
|
+ }
|
|
+ fn flush(&mut self) -> io::Result<()> {
|
|
+ Ok(())
|
|
+ }
|
|
+}
|
|
+
|
|
+pub(crate) fn encode(graph: &Json, limit: usize) -> Option<(Json, usize)> {
|
|
+ let Json::Object(fields) = graph else {
|
|
+ return None;
|
|
+ };
|
|
+ if !fields
|
|
+ .iter()
|
|
+ .any(|(key, v)| key == "schema_version" && *v == Json::Number("3".into()))
|
|
+ {
|
|
+ return None;
|
|
+ }
|
|
+ let raw = dumps_compact_bounded(graph, MAX_RAW_BYTES)?;
|
|
+ let mut envelope = Json::Object(vec![
|
|
+ ("schema_version".into(), Json::Number("4".into())),
|
|
+ ("encoding".into(), Json::Str("gzip-base64".into())),
|
|
+ ("raw_bytes".into(), Json::Number(raw.len().to_string())),
|
|
+ ("data".into(), Json::Str(String::new())),
|
|
+ ]);
|
|
+ let overhead = dumps_compact_bounded(&envelope, limit)?.len();
|
|
+ let payload_limit = limit.checked_sub(overhead)?;
|
|
+ let compressed_limit = (payload_limit / 4).checked_mul(3)?;
|
|
+ let mut gzip = GzEncoder::new(
|
|
+ BoundedBytes {
|
|
+ data: Vec::new(),
|
|
+ limit: compressed_limit,
|
|
+ },
|
|
+ Compression::fast(),
|
|
+ );
|
|
+ gzip.write_all(raw.as_bytes()).ok()?;
|
|
+ let compressed = gzip.finish().ok()?.data;
|
|
+ let encoded_len = compressed
|
|
+ .len()
|
|
+ .checked_add(2)?
|
|
+ .checked_div(3)?
|
|
+ .checked_mul(4)?;
|
|
+ if encoded_len > payload_limit {
|
|
+ return None;
|
|
+ }
|
|
+ let encoded = STANDARD.encode(compressed);
|
|
+ if let Json::Object(fields) = &mut envelope {
|
|
+ let (_, data) = fields.iter_mut().find(|(key, _)| key == "data")?;
|
|
+ *data = Json::Str(encoded);
|
|
+ }
|
|
+ Some((envelope, overhead + encoded_len))
|
|
+}
|
|
+
|
|
+#[cfg(test)]
|
|
+mod tests {
|
|
+ use super::*;
|
|
+ use std::io::Read;
|
|
+ #[test]
|
|
+ fn lossless_frame_accounts_for_escaping_and_base64_expansion() {
|
|
+ let graph = Json::Object(vec![
|
|
+ ("schema_version".into(), Json::Number("3".into())),
|
|
+ ("statement".into(), Json::Str("中文😀\0".repeat(1000))),
|
|
+ ]);
|
|
+ let (encoded, size) = encode(&graph, 2000).unwrap();
|
|
+ let text = dumps_compact_bounded(&encoded, 2000).unwrap();
|
|
+ assert_eq!(text.len(), size);
|
|
+ let frame: serde_json::Value = serde_json::from_str(&text).unwrap();
|
|
+ let bytes = STANDARD.decode(frame["data"].as_str().unwrap()).unwrap();
|
|
+ let mut restored = String::new();
|
|
+ flate2::read::GzDecoder::new(bytes.as_slice())
|
|
+ .read_to_string(&mut restored)
|
|
+ .unwrap();
|
|
+ assert_eq!(
|
|
+ restored,
|
|
+ dumps_compact_bounded(&graph, MAX_RAW_BYTES).unwrap()
|
|
+ );
|
|
+ assert_eq!(frame["raw_bytes"].as_u64().unwrap(), restored.len() as u64);
|
|
+ assert!(encode(&graph, 1).is_none());
|
|
+ assert!(encode(&graph, size - 1).is_none());
|
|
+ }
|
|
+ #[test]
|
|
+ fn archive_is_single_layer_and_raw_allocation_has_an_independent_bound() {
|
|
+ assert!(encode(&Json::Str("not a graph".into()), 1000).is_none());
|
|
+ let nested = Json::Object(vec![("schema_version".into(), Json::Number("4".into()))]);
|
|
+ assert!(encode(&nested, 1000).is_none());
|
|
+ let oversized = Json::Object(vec![
|
|
+ ("schema_version".into(), Json::Number("3".into())),
|
|
+ (
|
|
+ "statement".into(),
|
|
+ Json::Str("\0".repeat(MAX_RAW_BYTES / 6 + 1)),
|
|
+ ),
|
|
+ ]);
|
|
+ assert!(encode(&oversized, 1000).is_none());
|
|
+ }
|
|
+}
|
|
diff --git a/decompiler/crates/kuna-cli/src/atomic_json.rs b/decompiler/crates/kuna-cli/src/atomic_json.rs
|
|
new file mode 100644
|
|
index 00000000..08deb2e5
|
|
--- /dev/null
|
|
+++ b/decompiler/crates/kuna-cli/src/atomic_json.rs
|
|
@@ -0,0 +1,497 @@
|
|
+//! Opt-in atomic documents share the ordinary record producer and JSON writer.
|
|
+//! A graph that cannot fit must not discard otherwise readable reconstructed C.
|
|
+
|
|
+use crate::jsonfmt::{dumps_compact_bounded, Json};
|
|
+
|
|
+// Ownership is moved out while trying bounded alternatives, never cloned.
|
|
+struct RemovedFacet {
|
|
+ function: usize,
|
|
+ value: (String, Json),
|
|
+ added_error: bool,
|
|
+}
|
|
+// JSON object member order is not facet identity. Restore owned entries by key,
|
|
+// never by an ordinal invalidated when another independent facet was removed.
|
|
+#[derive(Clone, Copy)]
|
|
+enum Omission {
|
|
+ Capacity,
|
|
+ RedundantWithGraph,
|
|
+}
|
|
+fn function_records(document: &mut Json) -> Option<&mut Vec<Json>> {
|
|
+ let Json::Object(fields) = document else {
|
|
+ return None;
|
|
+ };
|
|
+ let (_, value) = fields.iter_mut().find(|(key, _)| key == "functions")?;
|
|
+ let Json::Array(functions) = value else {
|
|
+ return None;
|
|
+ };
|
|
+ Some(functions)
|
|
+}
|
|
+fn take_facet(document: &mut Json, name: &str, limit: usize, why: Omission) -> Vec<RemovedFacet> {
|
|
+ let mut removed = Vec::new();
|
|
+ if let Some(functions) = function_records(document) {
|
|
+ for (function, record) in functions.iter_mut().enumerate() {
|
|
+ if let Json::Object(fields) = record {
|
|
+ if matches!(why, Omission::RedundantWithGraph)
|
|
+ && !fields.iter().any(|(key, _)| key == "atomic_facts")
|
|
+ {
|
|
+ continue;
|
|
+ }
|
|
+ if let Some(field) = fields.iter().position(|(key, _)| key == name) {
|
|
+ let value = fields.remove(field);
|
|
+ let error = format!("{name}_error");
|
|
+ let added_error = matches!(why, Omission::Capacity)
|
|
+ && !fields.iter().any(|(key, _)| key == &error);
|
|
+ if added_error {
|
|
+ fields.push((error, Json::Str(format!("atomic encoded document exceeds {limit} byte capacity; {name} omitted, code retained"))));
|
|
+ }
|
|
+ removed.push(RemovedFacet {
|
|
+ function,
|
|
+ value,
|
|
+ added_error,
|
|
+ });
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ removed
|
|
+}
|
|
+fn restore_facet(document: &mut Json, removed: Vec<RemovedFacet>) {
|
|
+ if let Some(functions) = function_records(document) {
|
|
+ for facet in removed {
|
|
+ if let Json::Object(fields) = &mut functions[facet.function] {
|
|
+ if facet.added_error {
|
|
+ let error = format!("{}_error", facet.value.0);
|
|
+ fields.retain(|(key, _)| key != &error);
|
|
+ }
|
|
+ fields.push(facet.value);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+}
|
|
+struct GraphAlternative {
|
|
+ function: usize,
|
|
+ value: Json,
|
|
+}
|
|
+fn compress_graphs(document: &mut Json, budget: usize) -> Vec<GraphAlternative> {
|
|
+ let mut originals = Vec::new();
|
|
+ let mut remaining = budget;
|
|
+ if let Some(functions) = function_records(document) {
|
|
+ for (function, record) in functions.iter_mut().enumerate() {
|
|
+ if let Json::Object(fields) = record {
|
|
+ if let Some((_, graph)) = fields.iter_mut().find(|(key, _)| key == "atomic_facts") {
|
|
+ if let Some((encoded, size)) =
|
|
+ crate::atomic_compression::encode(graph, remaining)
|
|
+ {
|
|
+ remaining -= size;
|
|
+ originals.push(GraphAlternative {
|
|
+ function,
|
|
+ value: std::mem::replace(graph, encoded),
|
|
+ });
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ originals
|
|
+}
|
|
+fn exchange_graphs(
|
|
+ document: &mut Json,
|
|
+ alternatives: Vec<GraphAlternative>,
|
|
+) -> Vec<GraphAlternative> {
|
|
+ let mut previous = Vec::new();
|
|
+ if let Some(functions) = function_records(document) {
|
|
+ for alternative in alternatives {
|
|
+ if let Json::Object(fields) = &mut functions[alternative.function] {
|
|
+ if let Some((_, graph)) = fields.iter_mut().find(|(key, _)| key == "atomic_facts") {
|
|
+ previous.push(GraphAlternative {
|
|
+ function: alternative.function,
|
|
+ value: std::mem::replace(graph, alternative.value),
|
|
+ });
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ previous
|
|
+}
|
|
+fn bounded(document: &Json, budget: usize) -> Option<String> {
|
|
+ let mut text = dumps_compact_bounded(document, budget)?;
|
|
+ text.push('\n');
|
|
+ Some(text)
|
|
+}
|
|
+// Reclaim a saved independent facet when other omissions have made room.
|
|
+// Only bounded encoded strings coexist here, never cloned graph/facet trees.
|
|
+fn reclaim(document: &mut Json, budget: usize, name: &str, saved: &mut Vec<RemovedFacet>, base: String) -> String {
|
|
+ if saved.is_empty() { return base; }
|
|
+ restore_facet(document, std::mem::take(saved));
|
|
+ if let Some(complete) = bounded(document, budget) { return complete; }
|
|
+ *saved = take_facet(document, name, budget + 1, Omission::Capacity);
|
|
+ base
|
|
+}
|
|
+fn bounded_reclaiming(document: &mut Json, budget: usize, name: &str, saved: &mut Vec<RemovedFacet>) -> Option<String> {
|
|
+ let base = bounded(document, budget)?;
|
|
+ Some(reclaim(document, budget, name, saved, base))
|
|
+}
|
|
+pub(crate) fn render(mut document: Json, limit: usize) -> Result<String, String> {
|
|
+ let budget = limit
|
|
+ .checked_sub(1)
|
|
+ .ok_or("atomic JSON output budget is zero")?;
|
|
+ // A newly added independent facet must not displace established graph/call
|
|
+ // coverage. Reclaim it only after the existing owner selects a fitting result.
|
|
+ let mut parameters = take_facet(&mut document, "atomic_parameters", limit, Omission::Capacity);
|
|
+ // A full graph already owns these operation/value facts. Keep its bounded
|
|
+ // projection aside in case encoded size later makes that graph unavailable.
|
|
+ let projection = take_facet(
|
|
+ &mut document,
|
|
+ "atomic_projection",
|
|
+ limit,
|
|
+ Omission::RedundantWithGraph,
|
|
+ );
|
|
+ if let Some(text) = bounded(&document, budget) {
|
|
+ return Ok(reclaim(&mut document, budget, "atomic_parameters", &mut parameters, text));
|
|
+ }
|
|
+ let originals = compress_graphs(&mut document, budget);
|
|
+ let mut alternatives = if originals.is_empty() {
|
|
+ Vec::new()
|
|
+ } else {
|
|
+ if let Some(text) = bounded(&document, budget) {
|
|
+ return Ok(reclaim(&mut document, budget, "atomic_parameters", &mut parameters, text));
|
|
+ }
|
|
+ exchange_graphs(&mut document, originals)
|
|
+ };
|
|
+ // New switch rows must not displace previously available graph/call facts.
|
|
+ // Retry without that independent facet before weakening existing coverage.
|
|
+ let mut switches = take_facet(&mut document, "atomic_switches", limit, Omission::Capacity);
|
|
+ if !switches.is_empty() {
|
|
+ if let Some(text) = bounded(&document, budget) {
|
|
+ return Ok(reclaim(&mut document, budget, "atomic_parameters", &mut parameters, text));
|
|
+ }
|
|
+ if !alternatives.is_empty() {
|
|
+ let originals = exchange_graphs(&mut document, alternatives);
|
|
+ if let Some(text) = bounded(&document, budget) {
|
|
+ return Ok(reclaim(&mut document, budget, "atomic_parameters", &mut parameters, text));
|
|
+ }
|
|
+ alternatives = exchange_graphs(&mut document, originals);
|
|
+ }
|
|
+ }
|
|
+ let calls = take_facet(&mut document, "atomic_calls", limit, Omission::Capacity);
|
|
+ if !calls.is_empty() {
|
|
+ if let Some(text) =
|
|
+ bounded_reclaiming(&mut document, budget, "atomic_switches", &mut switches)
|
|
+ {
|
|
+ return Ok(reclaim(&mut document, budget, "atomic_parameters", &mut parameters, text));
|
|
+ }
|
|
+ if !alternatives.is_empty() {
|
|
+ drop(exchange_graphs(&mut document, alternatives));
|
|
+ if let Some(text) =
|
|
+ bounded_reclaiming(&mut document, budget, "atomic_switches", &mut switches)
|
|
+ {
|
|
+ return Ok(reclaim(&mut document, budget, "atomic_parameters", &mut parameters, text));
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ restore_facet(&mut document, calls);
|
|
+ take_facet(&mut document, "atomic_facts", limit, Omission::Capacity);
|
|
+ restore_facet(&mut document, projection);
|
|
+ restore_facet(&mut document, std::mem::take(&mut switches));
|
|
+ if let Some(text) = bounded(&document, budget) {
|
|
+ return Ok(reclaim(&mut document, budget, "atomic_parameters", &mut parameters, text));
|
|
+ }
|
|
+ switches = take_facet(&mut document, "atomic_switches", limit, Omission::Capacity);
|
|
+ if !switches.is_empty() {
|
|
+ if let Some(text) = bounded(&document, budget) {
|
|
+ return Ok(reclaim(&mut document, budget, "atomic_parameters", &mut parameters, text));
|
|
+ }
|
|
+ }
|
|
+ // Independent calls retain priority when the combined fallback is too big.
|
|
+ let projection = take_facet(
|
|
+ &mut document,
|
|
+ "atomic_projection",
|
|
+ limit,
|
|
+ Omission::Capacity,
|
|
+ );
|
|
+ if let Some(text) = bounded_reclaiming(&mut document, budget, "atomic_switches", &mut switches)
|
|
+ {
|
|
+ return Ok(reclaim(&mut document, budget, "atomic_parameters", &mut parameters, text));
|
|
+ }
|
|
+ restore_facet(&mut document, projection);
|
|
+ take_facet(&mut document, "atomic_calls", limit, Omission::Capacity);
|
|
+ if let Some(text) = bounded_reclaiming(&mut document, budget, "atomic_switches", &mut switches)
|
|
+ {
|
|
+ return Ok(reclaim(&mut document, budget, "atomic_parameters", &mut parameters, text));
|
|
+ }
|
|
+ take_facet(
|
|
+ &mut document,
|
|
+ "atomic_projection",
|
|
+ limit,
|
|
+ Omission::Capacity,
|
|
+ );
|
|
+ bounded_reclaiming(&mut document, budget, "atomic_switches", &mut switches)
|
|
+ .map(|text| reclaim(&mut document, budget, "atomic_parameters", &mut parameters, text)).ok_or_else(|| {
|
|
+ format!("atomic JSON exceeds {limit} byte capacity even without recovered facets")
|
|
+ })
|
|
+}
|
|
+
|
|
+#[cfg(test)]
|
|
+mod switch_facet_tests {
|
|
+ use super::*;
|
|
+ fn document(graph: usize, calls: usize, projection: usize, switches: usize) -> Json {
|
|
+ Json::Object(vec![(
|
|
+ "functions".into(),
|
|
+ Json::Array(vec![Json::Object(vec![
|
|
+ ("code".into(), Json::Str("retained C".into())),
|
|
+ ("atomic_facts".into(), Json::Str("g".repeat(graph))),
|
|
+ ("atomic_calls".into(), Json::Str("c".repeat(calls))),
|
|
+ (
|
|
+ "atomic_projection".into(),
|
|
+ Json::Str("p".repeat(projection)),
|
|
+ ),
|
|
+ ("atomic_switches".into(), Json::Str("s".repeat(switches))),
|
|
+ ])]),
|
|
+ )])
|
|
+ }
|
|
+ #[test]
|
|
+ fn oversized_switches_do_not_displace_existing_graph_and_calls() {
|
|
+ let text = render(document(600, 20, 10, 5000), 1500).unwrap();
|
|
+ let d: serde_json::Value = serde_json::from_str(&text).unwrap();
|
|
+ let f = &d["functions"][0];
|
|
+ assert_eq!(f["code"], "retained C");
|
|
+ assert_eq!(f["atomic_facts"].as_str().unwrap().len(), 600);
|
|
+ assert_eq!(f["atomic_calls"].as_str().unwrap().len(), 20);
|
|
+ assert!(f.get("atomic_switches").is_none());
|
|
+ assert!(f["atomic_switches_error"].is_string());
|
|
+ }
|
|
+ #[test]
|
|
+ fn reclaim_switches_after_a_large_projection_is_omitted() {
|
|
+ let text = render(document(5000, 100, 3000, 100), 2000).unwrap();
|
|
+ let d: serde_json::Value = serde_json::from_str(&text).unwrap();
|
|
+ let f = &d["functions"][0];
|
|
+ assert_eq!(f["atomic_calls"].as_str().unwrap().len(), 100);
|
|
+ assert_eq!(f["atomic_switches"].as_str().unwrap().len(), 100);
|
|
+ assert!(f.get("atomic_projection").is_none());
|
|
+ assert!(f.get("atomic_switches_error").is_none());
|
|
+ }
|
|
+ #[test]
|
|
+ fn independent_switch_rows_survive_when_all_other_facets_overflow() {
|
|
+ let text = render(document(5000, 5000, 5000, 1200), 2000).unwrap();
|
|
+ let d: serde_json::Value = serde_json::from_str(&text).unwrap();
|
|
+ let f = &d["functions"][0];
|
|
+ assert_eq!(f["code"], "retained C");
|
|
+ assert_eq!(f["atomic_switches"].as_str().unwrap().len(), 1200);
|
|
+ assert!(f.get("atomic_switches_error").is_none());
|
|
+ }
|
|
+}
|
|
+
|
|
+#[cfg(test)]
|
|
+mod tests {
|
|
+ use super::*;
|
|
+
|
|
+ #[test]
|
|
+ fn encoded_overflow_preserves_code_and_replaces_only_the_graph() {
|
|
+ let original = Json::Object(vec![(
|
|
+ "functions".into(),
|
|
+ Json::Array(vec![Json::Object(vec![
|
|
+ ("code".into(), Json::Str("retained C".into())),
|
|
+ ("error".into(), Json::Null),
|
|
+ ("atomic_facts".into(), Json::Str("😀".repeat(1000))),
|
|
+ ])]),
|
|
+ )]);
|
|
+ let full = render(original.clone(), 20000).unwrap();
|
|
+ assert!(full.len() > 12000); // ASCII surrogate escaping is accounted for.
|
|
+ let reduced = render(original.clone(), 256).unwrap();
|
|
+ let document: serde_json::Value = serde_json::from_str(&reduced).unwrap();
|
|
+ let function = &document["functions"][0];
|
|
+ assert_eq!(function["code"], "retained C");
|
|
+ assert_eq!(function["error"], serde_json::Value::Null);
|
|
+ assert!(function.get("atomic_facts").is_none());
|
|
+ assert!(function["atomic_facts_error"]
|
|
+ .as_str()
|
|
+ .unwrap()
|
|
+ .contains("256"));
|
|
+ assert!(reduced.len() <= 256);
|
|
+ assert!(render(original, 1).is_err());
|
|
+ }
|
|
+}
|
|
+
|
|
+#[cfg(test)]
|
|
+mod compressed_graph_tests {
|
|
+ use super::*;
|
|
+ use base64::{engine::general_purpose::STANDARD, Engine as _};
|
|
+ use std::io::Read;
|
|
+ #[test]
|
|
+ fn compressed_graph_retains_calls_and_code_before_losing_any_facet() {
|
|
+ let graph = Json::Object(vec![
|
|
+ ("schema_version".into(), Json::Number("3".into())),
|
|
+ ("payload".into(), Json::Str("x".repeat(100_000))),
|
|
+ ]);
|
|
+ let original = Json::Object(vec![(
|
|
+ "functions".into(),
|
|
+ Json::Array(vec![Json::Object(vec![
|
|
+ ("code".into(), Json::Str("retained C".into())),
|
|
+ ("atomic_facts".into(), graph.clone()),
|
|
+ ("atomic_calls".into(), Json::Str("independent calls".into())),
|
|
+ ("atomic_projection".into(), Json::Str("fallback".into())),
|
|
+ ])]),
|
|
+ )]);
|
|
+ let uncompressed: serde_json::Value =
|
|
+ serde_json::from_str(&render(original.clone(), 200_000).unwrap()).unwrap();
|
|
+ assert_eq!(
|
|
+ uncompressed["functions"][0]["atomic_facts"]["schema_version"],
|
|
+ 3
|
|
+ );
|
|
+ let text = render(original.clone(), 4096).unwrap();
|
|
+ assert!(text.len() <= 4096);
|
|
+ let document: serde_json::Value = serde_json::from_str(&text).unwrap();
|
|
+ let function = &document["functions"][0];
|
|
+ assert_eq!(function["code"], "retained C");
|
|
+ assert_eq!(function["atomic_calls"], "independent calls");
|
|
+ assert!(function.get("atomic_projection").is_none());
|
|
+ assert!(function.get("atomic_facts_error").is_none());
|
|
+ let frame = &function["atomic_facts"];
|
|
+ assert_eq!(frame["schema_version"], 4);
|
|
+ let compressed = STANDARD.decode(frame["data"].as_str().unwrap()).unwrap();
|
|
+ let mut restored = String::new();
|
|
+ flate2::read::GzDecoder::new(compressed.as_slice())
|
|
+ .read_to_string(&mut restored)
|
|
+ .unwrap();
|
|
+ assert_eq!(restored, dumps_compact_bounded(&graph, 200_000).unwrap());
|
|
+ let tiny: serde_json::Value =
|
|
+ serde_json::from_str(&render(original, 512).unwrap()).unwrap();
|
|
+ assert!(tiny["functions"][0].get("atomic_facts").is_none());
|
|
+ assert_eq!(tiny["functions"][0]["atomic_projection"], "fallback");
|
|
+ assert_eq!(tiny["functions"][0]["atomic_calls"], "independent calls");
|
|
+ }
|
|
+}
|
|
+
|
|
+#[cfg(test)]
|
|
+mod independent_facet_tests {
|
|
+ use super::*;
|
|
+ fn document(graph: usize, calls: usize) -> Json {
|
|
+ Json::Object(vec![(
|
|
+ "functions".into(),
|
|
+ Json::Array(vec![Json::Object(vec![
|
|
+ ("code".into(), Json::Str("retained C".into())),
|
|
+ ("ordinary".into(), Json::Number("42".into())),
|
|
+ ("atomic_facts".into(), Json::Str("x".repeat(graph))),
|
|
+ ("atomic_calls".into(), Json::Str("y".repeat(calls))),
|
|
+ ])]),
|
|
+ )])
|
|
+ }
|
|
+ #[test]
|
|
+ fn each_facet_survives_the_others_encoded_overflow() {
|
|
+ for (graph, calls, keep) in [(2000, 1, "atomic_calls"), (1, 2000, "atomic_facts")] {
|
|
+ let rendered = render(document(graph, calls), 512).unwrap();
|
|
+ assert!(rendered.len() <= 512);
|
|
+ let d: serde_json::Value = serde_json::from_str(&rendered).unwrap();
|
|
+ let f = &d["functions"][0];
|
|
+ assert!(f.get(keep).is_some());
|
|
+ assert_eq!(f["ordinary"], 42);
|
|
+ assert_eq!(f["code"], "retained C");
|
|
+ }
|
|
+ }
|
|
+ #[test]
|
|
+ fn jointly_oversized_facets_preserve_the_existing_graph_when_it_fits() {
|
|
+ let rendered = render(document(1000, 1000), 1600).unwrap();
|
|
+ let d: serde_json::Value = serde_json::from_str(&rendered).unwrap();
|
|
+ assert!(d["functions"][0].get("atomic_facts").is_some());
|
|
+ assert!(d["functions"][0].get("atomic_calls").is_none());
|
|
+ }
|
|
+ #[test]
|
|
+ fn base_record_survives_both_overflows_or_fails_explicitly() {
|
|
+ let rendered = render(document(2000, 2000), 512).unwrap();
|
|
+ let d: serde_json::Value = serde_json::from_str(&rendered).unwrap();
|
|
+ let f = &d["functions"][0];
|
|
+ assert!(f.get("atomic_facts").is_none() && f.get("atomic_calls").is_none());
|
|
+ assert!(f["atomic_facts_error"].is_string() && f["atomic_calls_error"].is_string());
|
|
+ assert_eq!(f["code"], "retained C");
|
|
+ assert!(render(document(1, 1), 1).is_err());
|
|
+ }
|
|
+}
|
|
+
|
|
+#[cfg(test)]
|
|
+mod projection_tests {
|
|
+ use super::*;
|
|
+ fn document(graph: usize, calls: usize, projection: usize) -> Json {
|
|
+ Json::Object(vec![(
|
|
+ "functions".into(),
|
|
+ Json::Array(vec![Json::Object(vec![
|
|
+ ("code".into(), Json::Str("retained C".into())),
|
|
+ ("atomic_facts".into(), Json::Str("g".repeat(graph))),
|
|
+ ("atomic_calls".into(), Json::Str("c".repeat(calls))),
|
|
+ (
|
|
+ "atomic_projection".into(),
|
|
+ Json::Str("p".repeat(projection)),
|
|
+ ),
|
|
+ ])]),
|
|
+ )])
|
|
+ }
|
|
+ #[test]
|
|
+ fn full_graph_avoids_duplicate_projection_and_encoded_failure_restores_it() {
|
|
+ for (graph, retained) in [(10, "atomic_facts"), (3000, "atomic_projection")] {
|
|
+ let rendered = render(document(graph, 10, 10), 1024).unwrap();
|
|
+ let d: serde_json::Value = serde_json::from_str(&rendered).unwrap();
|
|
+ let f = &d["functions"][0];
|
|
+ assert!(f.get(retained).is_some());
|
|
+ assert!(f.get("atomic_calls").is_some());
|
|
+ assert_eq!(f["code"], "retained C");
|
|
+ if retained == "atomic_facts" {
|
|
+ assert!(f.get("atomic_projection").is_none());
|
|
+ assert!(f.get("atomic_projection_error").is_none());
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ #[test]
|
|
+ fn fallback_facets_survive_independently_without_losing_code() {
|
|
+ for (calls, projection, retained) in
|
|
+ [(3000, 10, "atomic_projection"), (10, 3000, "atomic_calls")]
|
|
+ {
|
|
+ let rendered = render(document(3000, calls, projection), 1024).unwrap();
|
|
+ let d: serde_json::Value = serde_json::from_str(&rendered).unwrap();
|
|
+ assert!(d["functions"][0].get(retained).is_some());
|
|
+ assert_eq!(d["functions"][0]["code"], "retained C");
|
|
+ assert!(rendered.len() <= 1024);
|
|
+ }
|
|
+ }
|
|
+}
|
|
+
|
|
+#[cfg(test)]
|
|
+mod parameter_facet_tests {
|
|
+ use super::*;
|
|
+ fn document(graph: usize, calls: usize, parameters: usize) -> Json {
|
|
+ Json::Object(vec![("functions".into(),Json::Array(vec![Json::Object(vec![
|
|
+ ("code".into(),Json::Str("readable C".into())),
|
|
+ ("atomic_facts".into(),Json::Str("g".repeat(graph))),
|
|
+ ("atomic_calls".into(),Json::Str("c".repeat(calls))),
|
|
+ ("atomic_parameters".into(),Json::Str("p".repeat(parameters))),
|
|
+ ])]))])
|
|
+ }
|
|
+ #[test]
|
|
+ fn oversized_parameters_do_not_displace_existing_graph_and_calls() {
|
|
+ let text=render(document(400,20,3000),900).unwrap();
|
|
+ let value:serde_json::Value=serde_json::from_str(&text).unwrap();let f=&value["functions"][0];
|
|
+ assert_eq!(f["atomic_facts"].as_str().unwrap().len(),400);
|
|
+ assert_eq!(f["atomic_calls"].as_str().unwrap().len(),20);
|
|
+ assert!(f.get("atomic_parameters").is_none());
|
|
+ assert!(f["atomic_parameters_error"].is_string());
|
|
+ }
|
|
+ #[test]
|
|
+ fn independent_small_parameters_survive_when_graph_and_calls_do_not_fit() {
|
|
+ let text=render(document(3000,3000,12),900).unwrap();
|
|
+ assert!(text.len()<=900);
|
|
+ let value:serde_json::Value=serde_json::from_str(&text).unwrap();let f=&value["functions"][0];
|
|
+ assert_eq!(f["code"],"readable C");
|
|
+ assert_eq!(f["atomic_parameters"].as_str().unwrap().len(),12);
|
|
+ assert!(f.get("atomic_parameters_error").is_none());
|
|
+ }
|
|
+ #[test]
|
|
+ fn restore_is_owned_by_field_identity_after_other_fields_are_removed() {
|
|
+ let mut doc=document(10,10,10);
|
|
+ let parameters=take_facet(&mut doc,"atomic_parameters",1000,Omission::Capacity);
|
|
+ take_facet(&mut doc,"atomic_facts",1000,Omission::Capacity);
|
|
+ take_facet(&mut doc,"atomic_calls",1000,Omission::Capacity);
|
|
+ restore_facet(&mut doc,parameters);
|
|
+ let text=bounded(&doc,1000).unwrap();
|
|
+ let value:serde_json::Value=serde_json::from_str(&text).unwrap();
|
|
+ assert_eq!(value["functions"][0]["atomic_parameters"],"p".repeat(10));
|
|
+ assert!(value["functions"][0].get("atomic_parameters_error").is_none());
|
|
+ }
|
|
+}
|
|
diff --git a/decompiler/crates/kuna-cli/src/decompile.rs b/decompiler/crates/kuna-cli/src/decompile.rs
|
|
index ad249b04..62074996 100644
|
|
--- a/decompiler/crates/kuna-cli/src/decompile.rs
|
|
+++ b/decompiler/crates/kuna-cli/src/decompile.rs
|
|
@@ -841,6 +841,8 @@ pub fn main(argv: &[String]) -> Result<i32, String> {
|
|
let mut target: Option<String> = None;
|
|
let mut addr = false;
|
|
let mut json = false;
|
|
+ let mut atomic_facts = false;
|
|
+ let mut atomic_output_bytes = None;
|
|
let mut bfd_target: Option<String> = None;
|
|
let mut raw = false;
|
|
let mut regions = false;
|
|
@@ -871,6 +873,13 @@ pub fn main(argv: &[String]) -> Result<i32, String> {
|
|
match a {
|
|
"--addr" => addr = true,
|
|
"--json" => json = true,
|
|
+ "--atomic-facts" => atomic_facts = true,
|
|
+ "--atomic-output-bytes" => {
|
|
+ let value = take_value(argv, &mut i, "--atomic-output-bytes")?;
|
|
+ atomic_output_bytes = Some(value.parse::<usize>()
|
|
+ .ok().filter(|bytes| *bytes > 0)
|
|
+ .ok_or("--atomic-output-bytes requires a positive byte count")?);
|
|
+ }
|
|
"--raw" => raw = true,
|
|
"--raw-image" => raw_image = true,
|
|
"--base" => base = Some(parse_cli_address(&take_value(argv, &mut i, "--base")?)?),
|
|
@@ -979,7 +988,13 @@ pub fn main(argv: &[String]) -> Result<i32, String> {
|
|
return Err("--base requires --raw-image".into());
|
|
}
|
|
|
|
+ if atomic_output_bytes.is_some() && !atomic_facts {
|
|
+ return Err("--atomic-output-bytes requires --atomic-facts".into());
|
|
+ }
|
|
if json {
|
|
+ if atomic_facts && (!cfg!(feature = "embed-specs") || sleighpath.is_some()) {
|
|
+ return Err("--atomic-facts requires embedded specs and does not accept --sleighpath".into());
|
|
+ }
|
|
// Refused, not ignored: each of these is a `decomp_dbg` transcript the
|
|
// in-process JSON path never produces, and silently dropping half a
|
|
// request is the failure mode `--json` exists to end.
|
|
@@ -997,6 +1012,8 @@ pub fn main(argv: &[String]) -> Result<i32, String> {
|
|
binary: &binary,
|
|
target: &target,
|
|
by_address: addr,
|
|
+ atomic_facts,
|
|
+ atomic_output_bytes: atomic_output_bytes.unwrap_or(8 * 1024 * 1024),
|
|
forwarded: &forwarded,
|
|
mode: mode.as_deref(),
|
|
bfd_target: bfd_target.as_deref(),
|
|
@@ -1007,6 +1024,10 @@ pub fn main(argv: &[String]) -> Result<i32, String> {
|
|
}));
|
|
}
|
|
|
|
+ if atomic_facts {
|
|
+ return Err("--atomic-facts requires --json".into());
|
|
+ }
|
|
+
|
|
// (kuna outlang, DIV-80) The auto policy -- follow the binary when the caller
|
|
// named no language. See `decompile_all::detected_output_language`.
|
|
if !saw_language && !options.iter().any(|(n, _)| n == "setlanguage") {
|
|
@@ -1106,6 +1127,8 @@ struct JsonRequest<'a> {
|
|
binary: &'a str,
|
|
target: &'a str,
|
|
by_address: bool,
|
|
+ atomic_facts: bool,
|
|
+ atomic_output_bytes: usize,
|
|
/// The `--option` / `--language` tokens, in argv order.
|
|
forwarded: &'a [String],
|
|
mode: Option<&'a str>,
|
|
@@ -1127,6 +1150,18 @@ struct JsonRequest<'a> {
|
|
/// drift.
|
|
fn run_json(req: &JsonRequest) -> i32 {
|
|
let mut argv: Vec<String> = vec![req.binary.to_string(), "--json".into()];
|
|
+ if req.atomic_facts {
|
|
+ match crate::embedded_specs::tree() {
|
|
+ Some(Ok(root)) => {
|
|
+ argv.push("--sleighpath".into());
|
|
+ argv.push(root.to_string_lossy().into_owned());
|
|
+ }
|
|
+ _ => {
|
|
+ eprintln!("error: atomic facts require readable embedded specs");
|
|
+ return 2;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
if req.by_address {
|
|
argv.push("--addr".into());
|
|
argv.push(if looks_like_addr(req.target) {
|
|
@@ -1168,7 +1203,7 @@ fn run_json(req: &JsonRequest) -> i32 {
|
|
// it has to reconcile against a prose stderr line — the missing error channel
|
|
// is half of why this flag exists. The verdict follows the document, matching
|
|
// `run`'s stdout-then-stderr ordering.
|
|
- let (text, failure) = match decompile_json(&args, req.target) {
|
|
+ let (text, failure) = match decompile_json(&args, req.target, req.atomic_facts.then_some(req.atomic_output_bytes)) {
|
|
Ok(answered) => answered,
|
|
Err(message) => (
|
|
decompile_all::render_result_json(
|
|
@@ -1197,7 +1232,7 @@ fn run_json(req: &JsonRequest) -> i32 {
|
|
/// was asked about exactly that function — so `--json` cannot turn the text
|
|
/// surface's non-zero verdict into a success; the reason is in the document too,
|
|
/// as the record's `error` and as the run-level one.
|
|
-fn decompile_json(args: &AllArgs, target: &str) -> Result<(String, Option<String>), String> {
|
|
+fn decompile_json(args: &AllArgs, target: &str, atomic_limit: Option<usize>) -> Result<(String, Option<String>), String> {
|
|
let mut prog = decompile_all::load_program(args, DriverDefaults::Decompile)?;
|
|
let targets = decompile_all::resolve_targets(&prog, args)?;
|
|
let interior = crate::interior::selections(&prog, args, &targets);
|
|
@@ -1217,7 +1252,7 @@ fn decompile_json(args: &AllArgs, target: &str) -> Result<(String, Option<String
|
|
});
|
|
}
|
|
|
|
- let funcs = decompile_all::decompile_entries(&mut prog, args, targets, false);
|
|
+ let funcs = decompile_all::decompile_entries(&mut prog, args, targets, false, atomic_limit.is_some());
|
|
let assertions = prog.assertion_outcomes();
|
|
let mut failure = funcs.iter().find_map(|f| {
|
|
f.error.as_ref().map(|reason| {
|
|
@@ -1235,14 +1270,18 @@ fn decompile_json(args: &AllArgs, target: &str) -> Result<(String, Option<String
|
|
if failure.is_none() && decompile_all::any_refused_assertion(&assertions) {
|
|
failure = Some(format!("--assert directive refused by the pipeline in {}", args.binary));
|
|
}
|
|
- let text = decompile_all::render_result_json(
|
|
+ let text = if let Some(limit) = atomic_limit {
|
|
+ decompile_all::render_atomic_result_json(
|
|
+ &args.binary, &funcs, &args.options, failure.as_deref(), &assertions, &interior, limit,
|
|
+ )?
|
|
+ } else { decompile_all::render_result_json(
|
|
&args.binary,
|
|
&funcs,
|
|
&args.options,
|
|
failure.as_deref(),
|
|
&assertions,
|
|
&interior,
|
|
- );
|
|
+ ) };
|
|
if failure.is_none() && args.assert_strict && rejected {
|
|
failure = Some(format!("--assert directive rejected in {}", args.binary));
|
|
}
|
|
diff --git a/decompiler/crates/kuna-cli/src/decompile_all.rs b/decompiler/crates/kuna-cli/src/decompile_all.rs
|
|
index 698c7243..c1dceab2 100644
|
|
--- a/decompiler/crates/kuna-cli/src/decompile_all.rs
|
|
+++ b/decompiler/crates/kuna-cli/src/decompile_all.rs
|
|
@@ -744,6 +744,7 @@ fn run_jobs_worker(args: &Args) -> Result<(), String> {
|
|
park_recovered_proto: false,
|
|
single_target: false,
|
|
want_tokens: false,
|
|
+ want_atomic_facts: false,
|
|
want_pointer_arguments: args.jobs_pointerargs,
|
|
};
|
|
let mut pending = entries.into_iter();
|
|
@@ -1264,7 +1265,7 @@ fn decompile_all(args: &Args, filters: &Filters) -> Result<AllRun, String> {
|
|
let funcs = if callee_first {
|
|
decompile_entries_callee_first(&mut prog, args, targets, explicit)
|
|
} else {
|
|
- decompile_entries(&mut prog, args, targets, /* pointer_arguments= */ true)
|
|
+ decompile_entries(&mut prog, args, targets, /* pointer_arguments= */ true, false)
|
|
};
|
|
Ok(AllRun { funcs, discovered, assertions: prog.assertion_outcomes(), interior })
|
|
}
|
|
@@ -1319,6 +1320,7 @@ pub(crate) fn decompile_entries(
|
|
args: &Args,
|
|
targets: Vec<FunctionEntry>,
|
|
pointer_arguments: bool,
|
|
+ want_atomic_facts: bool,
|
|
) -> Vec<FuncResult> {
|
|
arm_fn_budgets(prog, args);
|
|
let opts = DecompileOptions {
|
|
@@ -1326,6 +1328,7 @@ pub(crate) fn decompile_entries(
|
|
want_provenance: args.json,
|
|
single_target: targets.len() == 1,
|
|
want_pointer_arguments: pointer_arguments,
|
|
+ want_atomic_facts,
|
|
..DecompileOptions::default()
|
|
};
|
|
kuna_console::project::decompile_targets_with(prog, targets, &opts)
|
|
@@ -2003,6 +2006,19 @@ pub(crate) fn render_result_json(
|
|
render_selected_json(binary, funcs, options, error, None, assertions, interior)
|
|
}
|
|
|
|
+pub(crate) fn render_atomic_result_json(
|
|
+ binary: &str,
|
|
+ funcs: &[FuncResult],
|
|
+ options: &[(String, String)],
|
|
+ error: Option<&str>,
|
|
+ assertions: &[kuna_console::assertions::Outcome],
|
|
+ interior: &crate::interior::Interiors,
|
|
+ limit: usize,
|
|
+) -> Result<String, String> {
|
|
+ let language = last_option_value(options, "setlanguage").unwrap_or("c-language");
|
|
+ crate::atomic_json::render(result_json(binary, funcs, language, error, None, assertions, interior), limit)
|
|
+}
|
|
+
|
|
/// [`render_result_json`] plus the inventory `total` a NARROWED whole-binary run
|
|
/// answers with.
|
|
///
|
|
@@ -2152,6 +2168,13 @@ fn result_json(
|
|
// Always present, `[]` unless the option is on.
|
|
("types".into(), types),
|
|
];
|
|
+ if let Some(detail) = f.detail.as_ref() {
|
|
+ append_atomic_facet(&mut record, "atomic_facts", detail.atomic_facts.as_ref());
|
|
+ append_atomic_facet(&mut record, "atomic_calls", detail.atomic_calls.as_ref());
|
|
+ append_atomic_facet(&mut record, "atomic_parameters", detail.atomic_parameters.as_ref());
|
|
+ append_atomic_facet(&mut record, "atomic_switches", detail.atomic_switches.as_ref());
|
|
+ append_atomic_facet(&mut record, "atomic_projection", detail.atomic_projection.as_ref());
|
|
+ }
|
|
if !f.stack_objects.is_empty() {
|
|
record.push(("stack_objects".into(), Json::Array(
|
|
f.stack_objects.iter().map(stack_object_json).collect(),
|
|
@@ -3519,3 +3542,16 @@ mod triage_tests {
|
|
}
|
|
|
|
}
|
|
+
|
|
+// Named independent facets share serialization/failure semantics; a bad facet
|
|
+// cannot erase the ordinary C record or another valid facet.
|
|
+fn append_atomic_facet<T: serde::Serialize>(record: &mut Vec<(String, Json)>, name: &str, facts: Option<&Result<T, String>>) {
|
|
+ if let Some(facts) = facts {
|
|
+ let encoded = facts.as_ref().map_err(Clone::clone)
|
|
+ .and_then(|facts| serde_json::to_value(facts).map_err(|error| format!("atomic facet serialization failed: {error}")));
|
|
+ match encoded {
|
|
+ Ok(value) => record.push((name.into(), Json::from(value))),
|
|
+ Err(reason) => record.push((format!("{name}_error"), Json::Str(reason))),
|
|
+ }
|
|
+ }
|
|
+}
|
|
diff --git a/decompiler/crates/kuna-cli/src/decompile_all/callee_first.rs b/decompiler/crates/kuna-cli/src/decompile_all/callee_first.rs
|
|
index 3eb2a6e4..2b951ba2 100644
|
|
--- a/decompiler/crates/kuna-cli/src/decompile_all/callee_first.rs
|
|
+++ b/decompiler/crates/kuna-cli/src/decompile_all/callee_first.rs
|
|
@@ -28,6 +28,7 @@ pub(super) fn decompile_entries_callee_first(
|
|
park_recovered_proto: false,
|
|
single_target: targets.len() == 1,
|
|
want_tokens: false,
|
|
+ want_atomic_facts: false,
|
|
want_pointer_arguments: true,
|
|
};
|
|
decompile_callee_first(prog, args, targets, explicit, base)
|
|
diff --git a/decompiler/crates/kuna-cli/src/jsonfmt.rs b/decompiler/crates/kuna-cli/src/jsonfmt.rs
|
|
index 198b85a6..71bd9675 100644
|
|
--- a/decompiler/crates/kuna-cli/src/jsonfmt.rs
|
|
+++ b/decompiler/crates/kuna-cli/src/jsonfmt.rs
|
|
@@ -28,6 +28,19 @@ pub enum Json {
|
|
Object(Vec<(String, Json)>),
|
|
}
|
|
|
|
+impl From<serde_json::Value> for Json {
|
|
+ fn from(value: serde_json::Value) -> Self {
|
|
+ match value {
|
|
+ serde_json::Value::Null => Json::Null,
|
|
+ serde_json::Value::Bool(value) => Json::Bool(value),
|
|
+ serde_json::Value::Number(value) => Json::Number(value.to_string()),
|
|
+ serde_json::Value::String(value) => Json::Str(value),
|
|
+ serde_json::Value::Array(values) => Json::Array(values.into_iter().map(Json::from).collect()),
|
|
+ serde_json::Value::Object(values) => Json::Object(values.into_iter().map(|(key, value)| (key, Json::from(value))).collect()),
|
|
+ }
|
|
+ }
|
|
+}
|
|
+
|
|
/// Render `v` exactly as CPython's `json.dumps(v, indent=2)` would (ASCII-safe
|
|
/// string escaping, `", "`/`": "` item/key separators collapsed to the indent
|
|
/// form: items separated by `,\n`, keys by `": "`).
|
|
@@ -50,6 +63,12 @@ pub fn dumps_compact(v: &Json) -> String {
|
|
writer::render(v, Format::Compact)
|
|
}
|
|
|
|
+/// Enforce the caller's encoded-byte budget before growing the output buffer.
|
|
+/// Uses the same writer and escaping as the other modes; no length estimator.
|
|
+pub fn dumps_compact_bounded(v: &Json, limit: usize) -> Option<String> {
|
|
+ writer::render_bounded(v, Format::Compact, limit)
|
|
+}
|
|
+
|
|
/// Extract the JSON array/object span out of a `decomp_dbg` console transcript.
|
|
///
|
|
/// Port of `kuna/catalog.py::_extract_json`: drop the fixed `[decomp]>` prompt
|
|
diff --git a/decompiler/crates/kuna-cli/src/jsonfmt/writer.rs b/decompiler/crates/kuna-cli/src/jsonfmt/writer.rs
|
|
index c4eca1db..06b9d024 100644
|
|
--- a/decompiler/crates/kuna-cli/src/jsonfmt/writer.rs
|
|
+++ b/decompiler/crates/kuna-cli/src/jsonfmt/writer.rs
|
|
@@ -1,7 +1,7 @@
|
|
//! JSON layout and ASCII-safe escaping shared by all CLI output modes.
|
|
|
|
use super::Json;
|
|
-use std::fmt::Write as _;
|
|
+use std::fmt::{self, Write};
|
|
|
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
|
pub(super) enum Format {
|
|
@@ -12,108 +12,142 @@ pub(super) enum Format {
|
|
|
|
pub(super) fn render(value: &Json, format: Format) -> String {
|
|
let mut out = String::new();
|
|
- write_value(&mut out, value, 0, format);
|
|
+ write_value(&mut out, value, 0, format).expect("String writer is infallible");
|
|
out
|
|
}
|
|
|
|
+struct BoundedString {
|
|
+ text: String,
|
|
+ limit: usize,
|
|
+}
|
|
+
|
|
+impl Write for BoundedString {
|
|
+ fn write_str(&mut self, value: &str) -> fmt::Result {
|
|
+ let next = self.text.len().checked_add(value.len()).ok_or(fmt::Error)?;
|
|
+ if next > self.limit {
|
|
+ return Err(fmt::Error);
|
|
+ }
|
|
+ if next > self.text.capacity() {
|
|
+ let capacity = next
|
|
+ .max(self.text.capacity().saturating_mul(2))
|
|
+ .min(self.limit);
|
|
+ self.text
|
|
+ .try_reserve_exact(capacity - self.text.len())
|
|
+ .map_err(|_| fmt::Error)?;
|
|
+ }
|
|
+ self.text.push_str(value);
|
|
+ Ok(())
|
|
+ }
|
|
+}
|
|
+
|
|
+pub(super) fn render_bounded(value: &Json, format: Format, limit: usize) -> Option<String> {
|
|
+ let mut out = BoundedString {
|
|
+ text: String::new(),
|
|
+ limit,
|
|
+ };
|
|
+ write_value(&mut out, value, 0, format).ok()?;
|
|
+ Some(out.text)
|
|
+}
|
|
+
|
|
impl Format {
|
|
- fn line(self, out: &mut String, indent: usize) {
|
|
+ fn line(self, out: &mut impl Write, indent: usize) -> fmt::Result {
|
|
if self != Self::Compact {
|
|
- out.push('\n');
|
|
+ out.write_char('\n')?;
|
|
for _ in 0..indent {
|
|
- out.push(' ');
|
|
+ out.write_char(' ')?;
|
|
}
|
|
}
|
|
+ Ok(())
|
|
}
|
|
}
|
|
|
|
-fn write_value(out: &mut String, value: &Json, indent: usize, format: Format) {
|
|
+fn write_value(out: &mut impl Write, value: &Json, indent: usize, format: Format) -> fmt::Result {
|
|
match value {
|
|
- Json::Null => out.push_str("null"),
|
|
- Json::Bool(true) => out.push_str("true"),
|
|
- Json::Bool(false) => out.push_str("false"),
|
|
- Json::Number(number) => out.push_str(number),
|
|
+ Json::Null => out.write_str("null"),
|
|
+ Json::Bool(true) => out.write_str("true"),
|
|
+ Json::Bool(false) => out.write_str("false"),
|
|
+ Json::Number(number) => out.write_str(number),
|
|
Json::Str(text) => write_string(out, text),
|
|
Json::Array(items) => {
|
|
- out.push('[');
|
|
+ out.write_char('[')?;
|
|
for (i, item) in items.iter().enumerate() {
|
|
if i > 0 {
|
|
- out.push(',');
|
|
+ out.write_char(',')?;
|
|
}
|
|
- format.line(out, indent + 2);
|
|
- write_value(out, item, indent + 2, format);
|
|
+ format.line(out, indent + 2)?;
|
|
+ write_value(out, item, indent + 2, format)?;
|
|
}
|
|
if !items.is_empty() {
|
|
- format.line(out, indent);
|
|
+ format.line(out, indent)?;
|
|
}
|
|
- out.push(']');
|
|
+ out.write_char(']')
|
|
}
|
|
Json::Object(pairs) => {
|
|
if format == Format::Sorted && pairs.len() > 1 {
|
|
let mut ordered: Vec<_> = pairs.iter().collect();
|
|
ordered.sort_by(|a, b| a.0.cmp(&b.0));
|
|
- write_pairs(out, ordered.into_iter(), indent, format);
|
|
+ write_pairs(out, ordered.into_iter(), indent, format)
|
|
} else {
|
|
- write_pairs(out, pairs.iter(), indent, format);
|
|
+ write_pairs(out, pairs.iter(), indent, format)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn write_pairs<'a>(
|
|
- out: &mut String,
|
|
+ out: &mut impl Write,
|
|
pairs: impl ExactSizeIterator<Item = &'a (String, Json)>,
|
|
indent: usize,
|
|
format: Format,
|
|
-) {
|
|
+) -> fmt::Result {
|
|
let nonempty = pairs.len() != 0;
|
|
- out.push('{');
|
|
+ out.write_char('{')?;
|
|
for (i, (key, value)) in pairs.enumerate() {
|
|
if i > 0 {
|
|
- out.push(',');
|
|
+ out.write_char(',')?;
|
|
}
|
|
- format.line(out, indent + 2);
|
|
- write_string(out, key);
|
|
- out.push(':');
|
|
+ format.line(out, indent + 2)?;
|
|
+ write_string(out, key)?;
|
|
+ out.write_char(':')?;
|
|
if format != Format::Compact {
|
|
- out.push(' ');
|
|
+ out.write_char(' ')?;
|
|
}
|
|
- write_value(out, value, indent + 2, format);
|
|
+ write_value(out, value, indent + 2, format)?;
|
|
}
|
|
if nonempty {
|
|
- format.line(out, indent);
|
|
+ format.line(out, indent)?;
|
|
}
|
|
- out.push('}');
|
|
+ out.write_char('}')
|
|
}
|
|
|
|
-fn write_string(out: &mut String, s: &str) {
|
|
- out.push('"');
|
|
+fn write_string(out: &mut impl Write, s: &str) -> fmt::Result {
|
|
+ out.write_char('"')?;
|
|
for ch in s.chars() {
|
|
match ch {
|
|
- '"' => out.push_str("\\\""),
|
|
- '\\' => out.push_str("\\\\"),
|
|
- '\n' => out.push_str("\\n"),
|
|
- '\r' => out.push_str("\\r"),
|
|
- '\t' => out.push_str("\\t"),
|
|
- '\u{0008}' => out.push_str("\\b"),
|
|
- '\u{000C}' => out.push_str("\\f"),
|
|
+ '"' => out.write_str("\\\""),
|
|
+ '\\' => out.write_str("\\\\"),
|
|
+ '\n' => out.write_str("\\n"),
|
|
+ '\r' => out.write_str("\\r"),
|
|
+ '\t' => out.write_str("\\t"),
|
|
+ '\u{0008}' => out.write_str("\\b"),
|
|
+ '\u{000C}' => out.write_str("\\f"),
|
|
c if (c as u32) < 0x20 => {
|
|
- let _ = write!(out, "\\u{:04x}", c as u32);
|
|
+ write!(out, "\\u{:04x}", c as u32)
|
|
}
|
|
- c if (c as u32) < 0x7F => out.push(c),
|
|
+ c if (c as u32) < 0x7F => out.write_char(c),
|
|
c => {
|
|
// ensure_ascii: emit \uXXXX (surrogate pair for astral planes).
|
|
let cp = c as u32;
|
|
if cp <= 0xFFFF {
|
|
- let _ = write!(out, "\\u{:04x}", cp);
|
|
+ write!(out, "\\u{:04x}", cp)
|
|
} else {
|
|
let v = cp - 0x10000;
|
|
let hi = 0xD800 + (v >> 10);
|
|
let lo = 0xDC00 + (v & 0x3FF);
|
|
- let _ = write!(out, "\\u{:04x}\\u{:04x}", hi, lo);
|
|
+ write!(out, "\\u{:04x}\\u{:04x}", hi, lo)
|
|
}
|
|
}
|
|
- }
|
|
+ }?;
|
|
}
|
|
- out.push('"');
|
|
+ out.write_char('"')
|
|
}
|
|
diff --git a/decompiler/crates/kuna-cli/src/jsonfmt/writer_tests.rs b/decompiler/crates/kuna-cli/src/jsonfmt/writer_tests.rs
|
|
index 6710e12a..ef90468c 100644
|
|
--- a/decompiler/crates/kuna-cli/src/jsonfmt/writer_tests.rs
|
|
+++ b/decompiler/crates/kuna-cli/src/jsonfmt/writer_tests.rs
|
|
@@ -94,3 +94,21 @@ fn every_unicode_scalar_round_trips_through_ascii_output() {
|
|
assert_eq!(serde_json::from_str::<String>(&output).unwrap(), text);
|
|
}
|
|
}
|
|
+
|
|
+#[test]
|
|
+fn bounded_writer_counts_actual_escaped_bytes_at_every_boundary() {
|
|
+ let value = Json::Object(vec![("escaped".into(), Json::Str("\0\"\\é😀".into()))]);
|
|
+ let complete = dumps_compact(&value);
|
|
+ assert!(complete.is_ascii());
|
|
+ for limit in 0..complete.len() {
|
|
+ assert!(dumps_compact_bounded(&value, limit).is_none());
|
|
+ }
|
|
+ assert_eq!(
|
|
+ dumps_compact_bounded(&value, complete.len()),
|
|
+ Some(complete.clone())
|
|
+ );
|
|
+ assert_eq!(
|
|
+ dumps_compact_bounded(&value, complete.len() + 1),
|
|
+ Some(complete)
|
|
+ );
|
|
+}
|
|
diff --git a/decompiler/crates/kuna-cli/src/main.rs b/decompiler/crates/kuna-cli/src/main.rs
|
|
index 2345743c..a16abb71 100644
|
|
--- a/decompiler/crates/kuna-cli/src/main.rs
|
|
+++ b/decompiler/crates/kuna-cli/src/main.rs
|
|
@@ -1,6 +1,8 @@
|
|
//! Command dispatch for the kuna CLI.
|
|
|
|
mod args;
|
|
+mod atomic_json;
|
|
+mod atomic_compression;
|
|
mod callgraph;
|
|
mod catalog;
|
|
mod crypto;
|
|
@@ -31,6 +33,7 @@ mod runtime_hints;
|
|
mod skill;
|
|
mod specs;
|
|
mod strings;
|
|
+mod storage_map;
|
|
mod test;
|
|
mod unpack;
|
|
mod xrefs;
|
|
@@ -52,6 +55,7 @@ const COMMANDS: &[(&str, CommandHandler)] = &[
|
|
("read", disassemble::run_read),
|
|
("xrefs", xrefs::run),
|
|
("strings", strings::run),
|
|
+ ("storage-map", storage_map::run),
|
|
("crypto", crypto::run),
|
|
("unpack", unpack::run),
|
|
("docs", docs::run),
|
|
diff --git a/decompiler/crates/kuna-cli/src/project_stream.rs b/decompiler/crates/kuna-cli/src/project_stream.rs
|
|
index e40b18c9..b8cae233 100644
|
|
--- a/decompiler/crates/kuna-cli/src/project_stream.rs
|
|
+++ b/decompiler/crates/kuna-cli/src/project_stream.rs
|
|
@@ -858,6 +858,7 @@ fn export(
|
|
// every function of the export.
|
|
single_target: false,
|
|
want_tokens: false,
|
|
+ want_atomic_facts: false,
|
|
want_pointer_arguments: false,
|
|
};
|
|
|
|
diff --git a/decompiler/crates/kuna-cli/src/storage_map.rs b/decompiler/crates/kuna-cli/src/storage_map.rs
|
|
new file mode 100644
|
|
index 00000000..b2a30345
|
|
--- /dev/null
|
|
+++ b/decompiler/crates/kuna-cli/src/storage_map.rs
|
|
@@ -0,0 +1,232 @@
|
|
+//! Read-only architecture storage metadata without discovery or decompilation.
|
|
+use crate::decompile_all::{load_program, Args, DriverDefaults};
|
|
+use kuna_base::space::{AddrSpaceManager, VarnodeStorage};
|
|
+use serde::{
|
|
+ ser::{SerializeSeq, SerializeStruct},
|
|
+ Serialize, Serializer,
|
|
+};
|
|
+use std::{
|
|
+ collections::BTreeMap,
|
|
+ io::{self, Write},
|
|
+};
|
|
+
|
|
+const OUTPUT_BYTES: usize = 8 * 1024 * 1024;
|
|
+struct BoundedOutput {
|
|
+ bytes: Vec<u8>,
|
|
+ limit: usize,
|
|
+}
|
|
+impl Write for BoundedOutput {
|
|
+ fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
|
|
+ let next = self
|
|
+ .bytes
|
|
+ .len()
|
|
+ .checked_add(bytes.len())
|
|
+ .filter(|n| *n <= self.limit)
|
|
+ .ok_or_else(|| io::Error::other("storage map encoded-byte capacity exceeded"))?;
|
|
+ if next > self.bytes.capacity() {
|
|
+ let capacity = next
|
|
+ .checked_next_power_of_two()
|
|
+ .unwrap_or(self.limit)
|
|
+ .min(self.limit);
|
|
+ self.bytes
|
|
+ .try_reserve_exact(capacity - self.bytes.len())
|
|
+ .map_err(io::Error::other)?;
|
|
+ }
|
|
+ self.bytes.extend_from_slice(bytes);
|
|
+ Ok(bytes.len())
|
|
+ }
|
|
+ fn flush(&mut self) -> io::Result<()> {
|
|
+ Ok(())
|
|
+ }
|
|
+}
|
|
+#[derive(Serialize)]
|
|
+struct Space<'a> {
|
|
+ native_index: i32,
|
|
+ name: &'a str,
|
|
+ kind: String,
|
|
+ address_size_bytes: u32,
|
|
+ addressable_unit_bytes: u32,
|
|
+ endianness: &'static str,
|
|
+ highest_offset: String,
|
|
+}
|
|
+struct Spaces<'a>(&'a AddrSpaceManager);
|
|
+impl Serialize for Spaces<'_> {
|
|
+ fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
|
+ let mut rows = serializer.serialize_seq(None)?;
|
|
+ for index in 0..self.0.num_spaces() {
|
|
+ if let Some(space) = self.0.get_space(index) {
|
|
+ rows.serialize_element(&Space {
|
|
+ native_index: space.get_index(),
|
|
+ name: space.get_name(),
|
|
+ kind: format!("{:?}", space.get_type()),
|
|
+ address_size_bytes: space.get_addr_size(),
|
|
+ addressable_unit_bytes: space.get_word_size(),
|
|
+ endianness: if space.is_big_endian() {
|
|
+ "big"
|
|
+ } else {
|
|
+ "little"
|
|
+ },
|
|
+ highest_offset: format!("0x{:x}", space.get_highest()),
|
|
+ })?;
|
|
+ }
|
|
+ }
|
|
+ rows.end()
|
|
+ }
|
|
+}
|
|
+#[derive(Serialize)]
|
|
+struct Register<'a> {
|
|
+ native_row: usize,
|
|
+ space: Option<&'a str>,
|
|
+ offset: String,
|
|
+ size_bytes: u32,
|
|
+ name: Option<&'a str>,
|
|
+ name_bytes_hex: Option<String>,
|
|
+}
|
|
+struct Registers<'a>(&'a BTreeMap<VarnodeStorage, Vec<u8>>);
|
|
+impl Serialize for Registers<'_> {
|
|
+ fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
|
+ let mut rows = serializer.serialize_seq(Some(self.0.len()))?;
|
|
+ for (native_row, (location, bytes)) in self.0.iter().enumerate() {
|
|
+ let name = std::str::from_utf8(bytes).ok();
|
|
+ let name_bytes_hex = if name.is_some() {
|
|
+ None
|
|
+ } else {
|
|
+ if bytes.len() > OUTPUT_BYTES / 2 {
|
|
+ return Err(serde::ser::Error::custom(
|
|
+ "native register name exceeds encoded budget",
|
|
+ ));
|
|
+ }
|
|
+ Some(bytes.iter().map(|b| format!("{b:02x}")).collect())
|
|
+ };
|
|
+ rows.serialize_element(&Register {
|
|
+ native_row,
|
|
+ space: location.space.as_ref().map(|s| s.get_name()),
|
|
+ offset: format!("0x{:x}", location.offset),
|
|
+ size_bytes: location.size,
|
|
+ name,
|
|
+ name_bytes_hex,
|
|
+ })?;
|
|
+ }
|
|
+ rows.end()
|
|
+ }
|
|
+}
|
|
+struct Catalog<'a> {
|
|
+ binary: &'a str,
|
|
+ program: &'a kuna_console::engine::ConsoleProgram,
|
|
+}
|
|
+impl Serialize for Catalog<'_> {
|
|
+ fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
|
+ let arch = self.program.arch();
|
|
+ let sleigh = arch
|
|
+ .translate()
|
|
+ .as_sleigh()
|
|
+ .ok_or_else(|| serde::ser::Error::custom("native Sleigh register map unavailable"))?;
|
|
+ let registers = sleigh.base().get_all_registers();
|
|
+ let manager = arch.manage();
|
|
+ let mut doc = serializer.serialize_struct("StorageMap", 8)?;
|
|
+ doc.serialize_field("schema_version", &1)?;
|
|
+ doc.serialize_field("binary", self.binary)?;
|
|
+ doc.serialize_field(
|
|
+ "architecture",
|
|
+ &serde_json::json!({
|
|
+ "native_id": arch.get_description(),
|
|
+ "default_code_space": manager.get_default_code_space().map(|s|s.get_name()),
|
|
+ }),
|
|
+ )?;
|
|
+ doc.serialize_field("space_index_capacity", &manager.num_spaces())?;
|
|
+ doc.serialize_field("register_count", ®isters.len())?;
|
|
+ doc.serialize_field("spaces", &Spaces(manager))?;
|
|
+ doc.serialize_field("registers", &Registers(registers))?;
|
|
+ doc.serialize_field("coverage", &serde_json::json!({
|
|
+ "scope":"bootstrapped native architecture spaces and exact varnode_xref register-name map",
|
|
+ "native_exact_register_map":"complete", "alias_relationships":"unknown",
|
|
+ "dynamic_join_records":"not-collected", "memory_values":"not-returned",
|
|
+ "listing_discovery":false, "functions_decompiled":0,
|
|
+ }))?;
|
|
+ doc.end()
|
|
+ }
|
|
+}
|
|
+pub(crate) fn run(argv: &[String]) -> i32 {
|
|
+ if argv.len() != 2 || argv[1] != "--json" {
|
|
+ eprintln!("usage: kuna storage-map <binary> --json");
|
|
+ return 2;
|
|
+ }
|
|
+ let embedded = match crate::embedded_specs::tree() {
|
|
+ Some(Ok(root)) => root.to_string_lossy().into_owned(),
|
|
+ Some(Err(error)) => {
|
|
+ eprintln!("error: embedded specs: {error}");
|
|
+ return 1;
|
|
+ }
|
|
+ None => {
|
|
+ eprintln!("error: storage-map requires embedded specs");
|
|
+ return 1;
|
|
+ }
|
|
+ };
|
|
+ let args = Args {
|
|
+ binary: argv[0].clone(),
|
|
+ json: true,
|
|
+ sleighpath: Some(embedded),
|
|
+ options: vec![
|
|
+ ("listing".into(), "off".into()),
|
|
+ ("fast_funcdisc".into(), "off".into()),
|
|
+ ],
|
|
+ ..Args::serial()
|
|
+ };
|
|
+ let result = (|| {
|
|
+ let program = load_program(&args, DriverDefaults::Query)?;
|
|
+ let mut output = BoundedOutput {
|
|
+ bytes: Vec::new(),
|
|
+ limit: OUTPUT_BYTES,
|
|
+ };
|
|
+ serde_json::to_writer(
|
|
+ &mut output,
|
|
+ &Catalog {
|
|
+ binary: &args.binary,
|
|
+ program: &program,
|
|
+ },
|
|
+ )
|
|
+ .map_err(|e| e.to_string())?;
|
|
+ output.write_all(b"\n").map_err(|e| e.to_string())?;
|
|
+ String::from_utf8(output.bytes).map_err(|e| e.to_string())
|
|
+ })();
|
|
+ match result {
|
|
+ Ok(text) => crate::output::emit_with_status(&text, 0),
|
|
+ Err(error) => {
|
|
+ eprintln!("error: {error}");
|
|
+ 1
|
|
+ }
|
|
+ }
|
|
+}
|
|
+#[cfg(test)]
|
|
+mod tests {
|
|
+ use super::*;
|
|
+ #[test]
|
|
+ fn output_admission_precedes_allocation_and_failed_writes_preserve_prefix() {
|
|
+ let mut output = BoundedOutput {
|
|
+ bytes: Vec::new(),
|
|
+ limit: 5,
|
|
+ };
|
|
+ output.write_all(b"123").unwrap();
|
|
+ assert!(output.write_all(b"456").is_err());
|
|
+ assert_eq!(output.bytes, b"123");
|
|
+ assert!(output.bytes.capacity() <= 5);
|
|
+ output.write_all(b"45").unwrap();
|
|
+ assert_eq!(output.bytes, b"12345");
|
|
+ }
|
|
+ #[test]
|
|
+ fn invalid_utf8_names_keep_exact_bytes_instead_of_lossy_text() {
|
|
+ let mut map = BTreeMap::new();
|
|
+ map.insert(
|
|
+ VarnodeStorage {
|
|
+ space: None,
|
|
+ offset: u64::MAX,
|
|
+ size: 1,
|
|
+ },
|
|
+ vec![0xff, 0x00],
|
|
+ );
|
|
+ let value = serde_json::to_value(Registers(&map)).unwrap();
|
|
+ assert!(value[0]["name"].is_null());
|
|
+ assert_eq!(value[0]["name_bytes_hex"], "ff00");
|
|
+ assert_eq!(value[0]["offset"], "0xffffffffffffffff");
|
|
+ }
|
|
+}
|
|
diff --git a/decompiler/crates/kuna-console/src/inspect.rs b/decompiler/crates/kuna-console/src/inspect.rs
|
|
index 74024b10..9e36d55f 100644
|
|
--- a/decompiler/crates/kuna-console/src/inspect.rs
|
|
+++ b/decompiler/crates/kuna-console/src/inspect.rs
|
|
@@ -24,6 +24,11 @@ pub const FUNCTION_ROW_CAP: usize = 4096;
|
|
pub struct FuncDetail {
|
|
pub tokens: Vec<CodeToken>,
|
|
pub tokens_error: Option<String>,
|
|
+ pub atomic_facts: Option<Result<kuna_decomp::atomic_facts::FunctionFacts, String>>,
|
|
+ pub atomic_calls: Option<Result<kuna_decomp::atomic_calls::CallFacts, String>>,
|
|
+ pub atomic_parameters: Option<Result<kuna_decomp::atomic_parameters::Parameters, String>>,
|
|
+ pub atomic_switches: Option<Result<kuna_decomp::atomic_switches::SwitchFacts, String>>,
|
|
+ pub atomic_projection: Option<Result<kuna_decomp::atomic_projection::Projection, String>>,
|
|
}
|
|
|
|
impl FuncDetail {
|
|
@@ -40,13 +45,13 @@ impl FuncDetail {
|
|
) -> Self {
|
|
let mut tokens = kuna_srcmap::resolve(fd, var_refs, raw, untrimmed, variables);
|
|
if let Err(error) = kuna_srcmap::verify(&tokens, code) {
|
|
- return FuncDetail { tokens: Vec::new(), tokens_error: Some(error) };
|
|
+ return FuncDetail { tokens: Vec::new(), tokens_error: Some(error), ..Default::default() };
|
|
}
|
|
for t in &mut tokens {
|
|
t.address = t.address.map(|a| prog.output_code_offset(a));
|
|
t.callee = t.callee.map(|a| prog.output_code_offset(a));
|
|
}
|
|
- FuncDetail { tokens, tokens_error: None }
|
|
+ FuncDetail { tokens, tokens_error: None, ..Default::default() }
|
|
}
|
|
}
|
|
|
|
diff --git a/decompiler/crates/kuna-console/src/project.rs b/decompiler/crates/kuna-console/src/project.rs
|
|
index bc401db2..2ef18e27 100644
|
|
--- a/decompiler/crates/kuna-console/src/project.rs
|
|
+++ b/decompiler/crates/kuna-console/src/project.rs
|
|
@@ -245,6 +245,8 @@ pub struct DecompileOptions {
|
|
/// Capture the token source map ([`FuncResult::detail`]); implies the
|
|
/// provenance render.
|
|
pub want_tokens: bool,
|
|
+ /// Retain the read-only recovered IR from the same function instance.
|
|
+ pub want_atomic_facts: bool,
|
|
/// Record [`FuncResult::pointerargs`] for a buffered driver. A batch that
|
|
/// returns its results ([`decompile_targets_with`]) reconciles them itself;
|
|
/// a pulled caller owns that step.
|
|
@@ -278,6 +280,7 @@ pub fn decompile_targets(
|
|
park_recovered_proto: false,
|
|
single_target: targets.len() == 1,
|
|
want_tokens: false,
|
|
+ want_atomic_facts: false,
|
|
want_pointer_arguments: false,
|
|
};
|
|
decompile_batch(prog, targets, &opts)
|
|
@@ -321,6 +324,7 @@ pub fn export_options(single_target: bool) -> DecompileOptions {
|
|
park_recovered_proto: false,
|
|
single_target,
|
|
want_tokens: false,
|
|
+ want_atomic_facts: false,
|
|
want_pointer_arguments: true,
|
|
}
|
|
}
|
|
@@ -509,6 +513,7 @@ pub fn decompile_pulled(
|
|
header_carries_types,
|
|
single_target,
|
|
want_tokens,
|
|
+ want_atomic_facts,
|
|
want_pointer_arguments,
|
|
..
|
|
} = *opts;
|
|
@@ -838,11 +843,24 @@ pub fn decompile_pulled(
|
|
}
|
|
}
|
|
let var_refs = provenance.apply_to_variables_with_refs(&fd, &mut variables);
|
|
- let detail = want_tokens.then(|| {
|
|
+ let mut detail = want_tokens.then(|| {
|
|
Box::new(crate::inspect::FuncDetail::resolve(
|
|
prog, &fd, &var_refs, &raw_tokens, &untrimmed, &code, &variables,
|
|
))
|
|
});
|
|
+ if want_atomic_facts {
|
|
+ let detail = detail.get_or_insert_with(Default::default);
|
|
+ detail.atomic_parameters = Some(kuna_decomp::atomic_parameters::collect(fd.get_func_proto()));
|
|
+ let membership = kuna_decomp::atomic_membership::inspect(&fd);
|
|
+ detail.atomic_calls = Some(membership.as_ref().map_err(Clone::clone)
|
|
+ .and_then(|m| kuna_decomp::atomic_calls::collect_all(&fd, m)));
|
|
+ detail.atomic_switches = Some(membership.as_ref().map_err(Clone::clone)
|
|
+ .and_then(|m| kuna_decomp::atomic_switches::collect(&fd, m)));
|
|
+ detail.atomic_facts = Some(membership.as_ref().map_err(Clone::clone)
|
|
+ .and_then(|m| kuna_decomp::atomic_facts::collect(prog.arch(), &fd, m)));
|
|
+ detail.atomic_projection = Some(membership.as_ref().map_err(Clone::clone)
|
|
+ .and_then(|m| kuna_decomp::atomic_projection::collect(prog.arch(), &fd, m)));
|
|
+ }
|
|
for variable in &mut variables {
|
|
for address in &mut variable.addresses {
|
|
*address = prog.output_code_offset(*address);
|
|
diff --git a/decompiler/crates/kuna-decomp/Cargo.toml b/decompiler/crates/kuna-decomp/Cargo.toml
|
|
index 40b093b6..8f03b654 100644
|
|
--- a/decompiler/crates/kuna-decomp/Cargo.toml
|
|
+++ b/decompiler/crates/kuna-decomp/Cargo.toml
|
|
@@ -15,11 +15,11 @@ kuna-sleigh.workspace = true
|
|
thiserror.workspace = true
|
|
smallvec.workspace = true
|
|
slotmap.workspace = true
|
|
+serde = { version = "1", features = ["derive"] }
|
|
|
|
[dev-dependencies]
|
|
# Verifier-only: the datatest <stringmatch> engine (item w10-structure-printbody).
|
|
regex = { workspace = true }
|
|
-serde = { version = "1", features = ["derive"] }
|
|
toml = { version = "1.1", default-features = false, features = ["parse", "serde", "std"] }
|
|
|
|
[build-dependencies]
|
|
diff --git a/decompiler/crates/kuna-decomp/src/p2_lift/jumptable.rs b/decompiler/crates/kuna-decomp/src/p2_lift/jumptable.rs
|
|
index b2677f6c..5e913130 100644
|
|
--- a/decompiler/crates/kuna-decomp/src/p2_lift/jumptable.rs
|
|
+++ b/decompiler/crates/kuna-decomp/src/p2_lift/jumptable.rs
|
|
@@ -5481,6 +5481,22 @@ impl JumpTable {
|
|
self.label[index as usize]
|
|
}
|
|
|
|
+ /// Borrow observed rows for read-only analysis without indexing absent labels.
|
|
+ /// Row order, labels and recovered out-edge positions are separate identities.
|
|
+ pub fn case_rows(&self) -> KunaResult<impl ExactSizeIterator<Item = (usize, &Address, Option<uintb>)>> {
|
|
+ if self.label.len() > self.addresstable.len() {
|
|
+ return Err(KunaError::lowlevel("jump-table labels exceed address rows"));
|
|
+ }
|
|
+ Ok(self.addresstable.iter().enumerate().map(|(index, address)| {
|
|
+ (index, address, self.label.get(index).copied())
|
|
+ }))
|
|
+ }
|
|
+
|
|
+ /// Borrow the native row-to-out-edge mapping; never infer it from addresses.
|
|
+ pub fn case_edge_rows(&self) -> impl ExactSizeIterator<Item = (int4, int4)> + '_ {
|
|
+ self.block2addr.iter().map(|pair| (pair.address_index, pair.block_position))
|
|
+ }
|
|
+
|
|
/// Force a given basic-block to be a switch destination
|
|
/// (C++ `JumpTable::addBlockToSwitch`, `jumptable.cc:2663`).
|
|
pub fn add_block_to_switch(&mut self, fd: &Funcdata, bl: BlockId, lab: uintb) -> KunaResult<()> {
|
|
diff --git a/decompiler/crates/kuna-decomp/src/p2_lift/jumptable/tests.rs b/decompiler/crates/kuna-decomp/src/p2_lift/jumptable/tests.rs
|
|
index 8aad0172..2f69df2d 100644
|
|
--- a/decompiler/crates/kuna-decomp/src/p2_lift/jumptable/tests.rs
|
|
+++ b/decompiler/crates/kuna-decomp/src/p2_lift/jumptable/tests.rs
|
|
@@ -725,3 +725,21 @@ fn guard_range_retry_keeps_both_wrapped_bounds() {
|
|
model.calc_range(&fd, input, &mut range);
|
|
assert_eq!((range.get_min(), range.get_end(), range.get_size()), (58, 62, 4));
|
|
}
|
|
+
|
|
+#[test]
|
|
+fn atomic_case_rows_keep_ordinals_labels_and_edges_separate() {
|
|
+ let manage = build_manager();
|
|
+ let ram = Rc::clone(manage.get_space_by_name("ram").unwrap());
|
|
+ let mut table = JumpTable::new(Address::new(Rc::clone(&ram), 0x1000));
|
|
+ table.kuna_push_entry(Address::new(Rc::clone(&ram), 0x3000), 2, 0x77);
|
|
+ table.kuna_push_entry(Address::new(Rc::clone(&ram), 0x4000), 0, NO_LABEL);
|
|
+ table.kuna_finalize(0);
|
|
+ let rows: Vec<_> = table.case_rows().unwrap()
|
|
+ .map(|(index, address, label)| (index, address.get_offset(), label)).collect();
|
|
+ assert_eq!(rows, vec![(0, 0x3000, Some(0x77)), (1, 0x4000, Some(NO_LABEL))]);
|
|
+ assert_eq!(table.case_edge_rows().collect::<Vec<_>>(), vec![(1, 0), (0, 2)]);
|
|
+ table.label.pop();
|
|
+ assert_eq!(table.case_rows().unwrap().nth(1).unwrap().2, None);
|
|
+ table.label.extend([1, 2]);
|
|
+ assert!(table.case_rows().is_err());
|
|
+}
|
|
diff --git a/decompiler/crates/kuna-decomp/src/substrate/atomic_calls.rs b/decompiler/crates/kuna-decomp/src/substrate/atomic_calls.rs
|
|
new file mode 100644
|
|
index 00000000..fa04cafc
|
|
--- /dev/null
|
|
+++ b/decompiler/crates/kuna-decomp/src/substrate/atomic_calls.rs
|
|
@@ -0,0 +1,189 @@
|
|
+//! Recovered call relations. Argument values and passing storage are separate.
|
|
+use crate::{
|
|
+ atomic_membership::{Membership, MAX_CALLS},
|
|
+ atomic_values::{self, operation_id, value_id, ValueFact},
|
|
+ context::OpId,
|
|
+ atomic_storage::{location, storage, Location, Storage},
|
|
+ fspec::{FuncCallSpecs, OFFSET_UNKNOWN},
|
|
+ funcdata::Funcdata,
|
|
+};
|
|
+use serde::Serialize;
|
|
+use std::collections::{BTreeSet, HashMap};
|
|
+
|
|
+#[derive(Debug, Clone, Serialize)]
|
|
+pub struct CallFacts {
|
|
+ pub schema_version: u32,
|
|
+ pub items: Vec<CallFact>,
|
|
+ pub values: Vec<ValueFact>,
|
|
+ pub coverage: CallCoverage,
|
|
+}
|
|
+#[derive(Debug, Clone, Serialize)]
|
|
+pub struct CallCoverage {
|
|
+ pub scope: &'static str,
|
|
+ pub blocks_examined: i32,
|
|
+ pub operations_examined: usize,
|
|
+ pub value_scope: &'static str,
|
|
+ pub machine_call_equivalence: &'static str,
|
|
+}
|
|
+
|
|
+pub fn collect_all(fd: &Funcdata, membership: &Membership) -> Result<CallFacts, String> {
|
|
+ const MAX_VALUES: usize = 36_000;
|
|
+ const MAX_INPUTS: usize = 50_000;
|
|
+ if fd.bblocks_get_size() <= 0 {
|
|
+ return Err("recovered block membership is unavailable".into());
|
|
+ }
|
|
+ if fd.num_calls() < 0 || fd.num_calls() as usize > MAX_CALLS {
|
|
+ return Err("atomic call-spec capacity exceeded".into());
|
|
+ }
|
|
+ let mut specs = HashMap::new();
|
|
+ for i in 0..fd.num_calls() {
|
|
+ let spec = fd.get_call_specs(i);
|
|
+ if specs.insert(spec.get_op(), spec).is_some() {
|
|
+ return Err("duplicate native call-spec operation".into());
|
|
+ }
|
|
+ }
|
|
+ let mut items = Vec::new();
|
|
+ let mut keys = BTreeSet::new();
|
|
+ let mut inputs = 0usize;
|
|
+ let mut text = atomic_values::TextBudget::new(2 * 1024 * 1024);
|
|
+ for &key in &membership.calls {
|
|
+ let op = fd.obank().get(key).ok_or("stale call operation")?;
|
|
+ inputs = inputs
|
|
+ .checked_add(op.num_input() as usize)
|
|
+ .ok_or("atomic call input count overflow")?;
|
|
+ if inputs > MAX_INPUTS {
|
|
+ return Err("atomic call input capacity exceeded".into());
|
|
+ }
|
|
+ let spec = specs.get(&key).copied();
|
|
+ if let Some(spec) = spec {
|
|
+ text.charge(spec.get_name().len())?;
|
|
+ if spec.proto().has_model() {
|
|
+ text.charge(spec.proto().get_model_name().len())?;
|
|
+ }
|
|
+ }
|
|
+ for index in 0..op.num_input() {
|
|
+ if let Some(v) = op.get_in(index) {
|
|
+ keys.insert(v);
|
|
+ }
|
|
+ }
|
|
+ if let Some(v) = op.get_out() {
|
|
+ keys.insert(v);
|
|
+ }
|
|
+ if keys.len() > MAX_VALUES {
|
|
+ return Err("atomic call value capacity exceeded".into());
|
|
+ }
|
|
+ items.push(collect(fd, key, spec)?);
|
|
+ }
|
|
+ let values = atomic_values::collect(fd, keys, &mut text)?;
|
|
+ Ok(CallFacts {
|
|
+ schema_version: 1, items, values,
|
|
+ coverage: CallCoverage {
|
|
+ scope: "all recovered block-member CALL/CALLIND operations",
|
|
+ blocks_examined: membership.blocks_examined, operations_examined: membership.operations_examined,
|
|
+ value_scope: "call inputs including target and outputs only; definitions may be outside this facet",
|
|
+ machine_call_equivalence: "unknown",
|
|
+ },
|
|
+ })
|
|
+}
|
|
+
|
|
+#[derive(Debug, Clone, Serialize)]
|
|
+pub struct Argument {
|
|
+ pub input_index: i32,
|
|
+ pub value_id: Option<String>,
|
|
+ pub storage: Option<Storage>,
|
|
+ pub storage_authority: Option<&'static str>,
|
|
+}
|
|
+#[derive(Debug, Clone, Serialize)]
|
|
+pub struct Prototype {
|
|
+ pub model: Option<String>,
|
|
+ pub model_unknown: Option<bool>,
|
|
+ pub inputs_locked: bool,
|
|
+ pub variadic: bool,
|
|
+ pub parameter_shift: i32,
|
|
+}
|
|
+#[derive(Debug, Clone, Serialize)]
|
|
+pub struct CallFact {
|
|
+ pub id: String,
|
|
+ pub origin_address: String,
|
|
+ pub block: i32,
|
|
+ pub opcode: String,
|
|
+ pub is_dead: bool,
|
|
+ pub target_value_id: Option<String>,
|
|
+ pub spec_available: bool,
|
|
+ pub target: Option<Location>,
|
|
+ pub target_name: Option<String>,
|
|
+ pub arguments: Vec<Argument>,
|
|
+ pub output: Option<String>,
|
|
+ pub prototype: Option<Prototype>,
|
|
+ pub stack_offset: Option<String>,
|
|
+}
|
|
+fn collect(fd: &Funcdata, key: OpId, spec: Option<&FuncCallSpecs>) -> Result<CallFact, String> {
|
|
+ let op = fd.obank().get(key).ok_or("stale call operation")?;
|
|
+ let arguments = (1..op.num_input())
|
|
+ .map(|input_index| {
|
|
+ // The native owner stores finalized passing locations separately
|
|
+ // from SSA values. Admit that mapping only with matching arity.
|
|
+ // Placeholder/shifted inputs are not source parameter ordinals.
|
|
+ let binding = spec.and_then(|spec| {
|
|
+ if spec.get_paramshift() != 0 || spec.get_stack_placeholder_slot() == input_index {
|
|
+ return None;
|
|
+ }
|
|
+ if spec.final_input_storage().len() + 1 == op.num_input() as usize {
|
|
+ let (address, size) = &spec.final_input_storage()[(input_index - 1) as usize];
|
|
+ return storage(address, *size).map(|s| (s, "final-input-storage"));
|
|
+ }
|
|
+ let proto = spec.proto();
|
|
+ if proto.has_store() && proto.is_input_locked() {
|
|
+ return proto.get_param(input_index - 1).and_then(|param| {
|
|
+ storage(¶m.get_address(), param.get_size())
|
|
+ .map(|s| (s, "locked-prototype"))
|
|
+ });
|
|
+ }
|
|
+ None
|
|
+ });
|
|
+ let (storage, storage_authority) = match binding {
|
|
+ Some((s, authority)) => (Some(s), Some(authority)),
|
|
+ None => (None, None),
|
|
+ };
|
|
+ Argument {
|
|
+ input_index,
|
|
+ value_id: op.get_in(input_index).map(value_id),
|
|
+ storage,
|
|
+ storage_authority,
|
|
+ }
|
|
+ })
|
|
+ .collect();
|
|
+ Ok(CallFact {
|
|
+ id: operation_id(fd, key)?,
|
|
+ origin_address: format!("0x{:x}", op.get_seq_num().get_addr().get_offset()),
|
|
+ block: fd
|
|
+ .bblocks_ref()
|
|
+ .block(op.get_parent().ok_or("call has no recovered block")?)
|
|
+ .get_index(),
|
|
+ opcode: format!("{:?}", op.code()),
|
|
+ is_dead: op.is_dead(),
|
|
+ target_value_id: op.get_in(0).map(value_id),
|
|
+ spec_available: spec.is_some(),
|
|
+ target: spec.and_then(|s| location(s.get_entry_address())),
|
|
+ target_name: spec
|
|
+ .and_then(|s| (!s.get_name().is_empty()).then(|| s.get_name().to_string())),
|
|
+ arguments,
|
|
+ output: op.get_out().map(value_id),
|
|
+ prototype: spec.map(|s| {
|
|
+ let proto = s.proto();
|
|
+ Prototype {
|
|
+ model: proto
|
|
+ .has_model()
|
|
+ .then(|| proto.get_model_name().to_string()),
|
|
+ model_unknown: proto.has_model().then(|| proto.is_model_unknown()),
|
|
+ inputs_locked: proto.is_input_locked(),
|
|
+ variadic: proto.is_dotdotdot(),
|
|
+ parameter_shift: s.get_paramshift(),
|
|
+ }
|
|
+ }),
|
|
+ stack_offset: spec.and_then(|s| {
|
|
+ (s.get_spacebase_offset() != OFFSET_UNKNOWN)
|
|
+ .then(|| format!("0x{:x}", s.get_spacebase_offset()))
|
|
+ }),
|
|
+ })
|
|
+}
|
|
diff --git a/decompiler/crates/kuna-decomp/src/substrate/atomic_facts.rs b/decompiler/crates/kuna-decomp/src/substrate/atomic_facts.rs
|
|
new file mode 100644
|
|
index 00000000..6b9afe69
|
|
--- /dev/null
|
|
+++ b/decompiler/crates/kuna-decomp/src/substrate/atomic_facts.rs
|
|
@@ -0,0 +1,156 @@
|
|
+//! Opt-in, read-only export of the recovered function IR for the REA experiment.
|
|
+//! IDs are local to this captured model; addresses alone are not SSA identities.
|
|
+use crate::{
|
|
+ architecture::Architecture,
|
|
+ atomic_membership::Membership,
|
|
+ atomic_operations::{OperationBudget, OperationFact},
|
|
+ atomic_values::{operation_id, value_id, ValueFact},
|
|
+ funcdata::Funcdata,
|
|
+};
|
|
+use kuna_num::opcodes::OpCode;
|
|
+use serde::Serialize;
|
|
+use std::collections::BTreeSet;
|
|
+
|
|
+#[derive(Debug, Clone, Serialize)]
|
|
+pub struct FunctionFacts {
|
|
+ pub schema_version: u32,
|
|
+ pub blocks: Vec<BlockFact>,
|
|
+ pub operations: Vec<OperationFact>,
|
|
+ pub values: Vec<ValueFact>,
|
|
+ pub coverage: Coverage,
|
|
+}
|
|
+#[derive(Debug, Clone, Serialize)]
|
|
+pub struct Coverage {
|
|
+ pub scope: &'static str,
|
|
+ pub machine_semantic_equivalence: &'static str,
|
|
+ pub indirect_branch_operations: Vec<String>,
|
|
+ pub omitted_definition_ids: Vec<String>,
|
|
+ pub issues: Vec<String>,
|
|
+}
|
|
+#[derive(Debug, Clone, Serialize)]
|
|
+pub struct BlockFact {
|
|
+ pub id: i32,
|
|
+ pub start: String,
|
|
+ pub operations: Vec<String>,
|
|
+ pub predecessors: Vec<i32>,
|
|
+ pub successors: Vec<EdgeFact>,
|
|
+}
|
|
+#[derive(Debug, Clone, Serialize)]
|
|
+pub struct EdgeFact {
|
|
+ pub id: String,
|
|
+ pub target: i32,
|
|
+ pub index: i32,
|
|
+ pub polarity: Option<&'static str>,
|
|
+ pub condition_value: Option<String>,
|
|
+ pub condition_test: Option<&'static str>,
|
|
+}
|
|
+pub fn collect(
|
|
+ arch: &Architecture,
|
|
+ fd: &Funcdata,
|
|
+ membership: &Membership,
|
|
+) -> Result<FunctionFacts, String> {
|
|
+ // Bounds cover allocations before JSON serialization, independently of the
|
|
+ // transport byte budget. Overflow is an unavailable facet, not clipped IR.
|
|
+ const MAX_OPERATIONS: usize = 32_000;
|
|
+ const MAX_VALUES: usize = 36_000;
|
|
+ const MAX_INPUTS: usize = 100_000;
|
|
+ const MAX_DISPLAY_BYTES: usize = 4 * 1024 * 1024;
|
|
+ const MAX_CFG_EDGES: usize = 50_000;
|
|
+ if fd.bblocks_get_size() <= 0 {
|
|
+ return Err("recovered basic-block graph is unavailable".into());
|
|
+ }
|
|
+ if fd.bblocks_get_size() > 12_000 {
|
|
+ return Err("atomic block capacity exceeded".into());
|
|
+ }
|
|
+ if membership.operations_examined > MAX_OPERATIONS {
|
|
+ return Err("atomic operation capacity exceeded".into());
|
|
+ }
|
|
+ let mut blocks = Vec::new();
|
|
+ let mut operations = Vec::new();
|
|
+ let mut retained_ops = BTreeSet::new();
|
|
+ let mut edge_count = 0usize;
|
|
+ let mut budget = OperationBudget::new(MAX_VALUES, MAX_INPUTS, MAX_DISPLAY_BYTES);
|
|
+ let mut coverage = Coverage {
|
|
+ scope: "complete native recovered basic-block graph; block-member operations",
|
|
+ machine_semantic_equivalence: "unknown",
|
|
+ indirect_branch_operations: Vec::new(),
|
|
+ omitted_definition_ids: Vec::new(),
|
|
+ issues: Vec::new(),
|
|
+ };
|
|
+ for block_index in 0..fd.bblocks_get_size() {
|
|
+ let block_key = fd.bblocks_get_block(block_index);
|
|
+ let block = fd.bblocks_ref().block(block_key);
|
|
+ let members = fd.bb_ops(block_key);
|
|
+ if members.len() > MAX_OPERATIONS - operations.len() {
|
|
+ return Err("atomic operation capacity exceeded".into());
|
|
+ }
|
|
+ edge_count = edge_count
|
|
+ .checked_add(block.size_out() as usize)
|
|
+ .ok_or("atomic CFG edge count overflow")?;
|
|
+ if edge_count > MAX_CFG_EDGES {
|
|
+ return Err("atomic CFG edge capacity exceeded".into());
|
|
+ }
|
|
+ let tail = members.last().and_then(|id| fd.obank().get(*id));
|
|
+ let conditional =
|
|
+ tail.filter(|op| op.code() == OpCode::CPUI_CBRANCH && block.size_out() == 2);
|
|
+ let condition_value = conditional.and_then(|op| op.get_in(1)).map(value_id);
|
|
+ let condition_test = conditional.map(|op| {
|
|
+ if op.is_boolean_flip() {
|
|
+ "eq-zero"
|
|
+ } else {
|
|
+ "ne-zero"
|
|
+ }
|
|
+ });
|
|
+ let successors = (0..block.size_out())
|
|
+ .map(|i| EdgeFact {
|
|
+ id: format!("{}:{i}", block.get_index()),
|
|
+ target: fd.bblocks_ref().block(block.get_out(i)).get_index(),
|
|
+ index: i,
|
|
+ polarity: conditional.map(|_| if i == 0 { "false" } else { "true" }),
|
|
+ condition_value: condition_value.clone(),
|
|
+ condition_test,
|
|
+ })
|
|
+ .collect();
|
|
+ blocks.push(BlockFact {
|
|
+ id: block.get_index(),
|
|
+ start: format!("0x{:x}", fd.bblocks_block_start(block_key).get_offset()),
|
|
+ operations: members
|
|
+ .iter()
|
|
+ .map(|id| operation_id(fd, *id))
|
|
+ .collect::<Result<_, _>>()?,
|
|
+ predecessors: (0..block.size_in())
|
|
+ .map(|i| fd.bblocks_ref().block(block.get_in(i)).get_index())
|
|
+ .collect(),
|
|
+ successors,
|
|
+ });
|
|
+ for key in members {
|
|
+ if operations.len() >= MAX_OPERATIONS {
|
|
+ return Err("atomic operation capacity exceeded".into());
|
|
+ }
|
|
+ let op = budget.collect(arch, fd, key)?;
|
|
+ if !retained_ops.insert(op.id.clone()) {
|
|
+ return Err("operation belongs to multiple recovered blocks".into());
|
|
+ }
|
|
+ if op.opcode == "CPUI_BRANCHIND" {
|
|
+ coverage.indirect_branch_operations.push(op.id.clone());
|
|
+ }
|
|
+ operations.push(op);
|
|
+ }
|
|
+ }
|
|
+ let values = budget.values(fd)?;
|
|
+ coverage.issues = budget.issues;
|
|
+ let missing: BTreeSet<_> = values
|
|
+ .iter()
|
|
+ .filter_map(|v| v.definition.as_ref())
|
|
+ .filter(|id| !retained_ops.contains(*id))
|
|
+ .cloned()
|
|
+ .collect();
|
|
+ coverage.omitted_definition_ids = missing.into_iter().collect();
|
|
+ Ok(FunctionFacts {
|
|
+ schema_version: 3,
|
|
+ blocks,
|
|
+ operations,
|
|
+ values,
|
|
+ coverage,
|
|
+ })
|
|
+}
|
|
diff --git a/decompiler/crates/kuna-decomp/src/substrate/atomic_membership.rs b/decompiler/crates/kuna-decomp/src/substrate/atomic_membership.rs
|
|
new file mode 100644
|
|
index 00000000..be11e472
|
|
--- /dev/null
|
|
+++ b/decompiler/crates/kuna-decomp/src/substrate/atomic_membership.rs
|
|
@@ -0,0 +1,76 @@
|
|
+//! One validated membership scan for independent recovered fact facets.
|
|
+use crate::{context::OpId, funcdata::Funcdata};
|
|
+use kuna_num::opcodes::OpCode;
|
|
+use std::collections::HashSet;
|
|
+
|
|
+pub const MAX_CALLS: usize = 12_000;
|
|
+const MAX_SWITCHES: usize = 12_000;
|
|
+// Bound the optional membership index separately from call discovery. Crossing
|
|
+// this resource budget disables projections, without discarding known calls.
|
|
+const MAX_INDEXED_OPERATIONS: usize = 65_536;
|
|
+pub struct Membership {
|
|
+ pub calls: Vec<OpId>,
|
|
+ pub switches: Option<Vec<OpId>>,
|
|
+ pub members: Option<HashSet<OpId>>,
|
|
+ pub blocks_examined: i32,
|
|
+ pub operations_examined: usize,
|
|
+}
|
|
+pub fn inspect(fd: &Funcdata) -> Result<Membership, String> {
|
|
+ if fd.bblocks_get_size() <= 0 {
|
|
+ return Err("recovered block membership is unavailable".into());
|
|
+ }
|
|
+ let mut result = Membership {
|
|
+ calls: Vec::new(),
|
|
+ switches: Some(Vec::new()),
|
|
+ members: Some(HashSet::new()),
|
|
+ blocks_examined: fd.bblocks_get_size(),
|
|
+ operations_examined: 0,
|
|
+ };
|
|
+ for i in 0..fd.bblocks_get_size() {
|
|
+ let block = fd.bblocks_get_block(i);
|
|
+ let expected = fd.bb_op_len(block);
|
|
+ let mut examined = 0;
|
|
+ let mut cursor = fd.bb_op_head(block);
|
|
+ while let Some(key) = cursor {
|
|
+ if examined >= expected {
|
|
+ return Err("cyclic native block operation list".into());
|
|
+ }
|
|
+ examined += 1;
|
|
+ result.operations_examined = result
|
|
+ .operations_examined
|
|
+ .checked_add(1)
|
|
+ .ok_or("native operation count overflow")?;
|
|
+ let op = fd.obank().get(key).ok_or("stale block operation")?;
|
|
+ if op.get_parent() != Some(block) {
|
|
+ return Err("native block ownership mismatch".into());
|
|
+ }
|
|
+ cursor = op.basic_neighbours().1;
|
|
+ if let Some(members) = result.members.as_mut() {
|
|
+ if members.len() == MAX_INDEXED_OPERATIONS {
|
|
+ result.members = None;
|
|
+ } else if !members.insert(key) {
|
|
+ return Err("operation belongs to multiple recovered blocks".into());
|
|
+ }
|
|
+ }
|
|
+ if matches!(op.code(), OpCode::CPUI_CALL | OpCode::CPUI_CALLIND) {
|
|
+ if result.calls.len() >= MAX_CALLS {
|
|
+ return Err("atomic call capacity exceeded".into());
|
|
+ }
|
|
+ result.calls.push(key);
|
|
+ }
|
|
+ if op.code() == OpCode::CPUI_BRANCHIND {
|
|
+ if let Some(switches) = result.switches.as_mut() {
|
|
+ if switches.len() == MAX_SWITCHES {
|
|
+ result.switches = None;
|
|
+ } else {
|
|
+ switches.push(key);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ if examined != expected {
|
|
+ return Err("native block operation count mismatch".into());
|
|
+ }
|
|
+ }
|
|
+ Ok(result)
|
|
+}
|
|
diff --git a/decompiler/crates/kuna-decomp/src/substrate/atomic_operations.rs b/decompiler/crates/kuna-decomp/src/substrate/atomic_operations.rs
|
|
new file mode 100644
|
|
index 00000000..30d6be39
|
|
--- /dev/null
|
|
+++ b/decompiler/crates/kuna-decomp/src/substrate/atomic_operations.rs
|
|
@@ -0,0 +1,119 @@
|
|
+//! Shared read-only operation/value normalization for full and selected facts.
|
|
+use crate::{
|
|
+ architecture::Architecture,
|
|
+ atomic_values::{self, operation_id, value_id, ValueFact},
|
|
+ context::{OpId, VarnodeId},
|
|
+ funcdata::Funcdata,
|
|
+};
|
|
+use kuna_num::opcodes::OpCode;
|
|
+use serde::Serialize;
|
|
+use std::collections::BTreeSet;
|
|
+
|
|
+#[derive(Debug, Clone, Serialize)]
|
|
+pub struct OperationFact {
|
|
+ pub id: String,
|
|
+ pub address: String,
|
|
+ pub block: i32,
|
|
+ pub opcode: String,
|
|
+ pub statement: String,
|
|
+ pub is_dead: bool,
|
|
+ pub inputs: Vec<InputFact>,
|
|
+ pub output: Option<String>,
|
|
+}
|
|
+#[derive(Debug, Clone, Serialize)]
|
|
+pub struct InputFact {
|
|
+ pub input_index: i32,
|
|
+ pub value_id: Option<String>,
|
|
+ pub phi_predecessor: Option<i32>,
|
|
+ pub phi_predecessor_edge: Option<String>,
|
|
+}
|
|
+pub struct OperationBudget {
|
|
+ max_values: usize,
|
|
+ max_inputs: usize,
|
|
+ keys: BTreeSet<VarnodeId>,
|
|
+ input_count: usize,
|
|
+ text: atomic_values::TextBudget,
|
|
+ pub issues: Vec<String>,
|
|
+}
|
|
+impl OperationBudget {
|
|
+ pub fn new(max_values: usize, max_inputs: usize, max_display: usize) -> Self {
|
|
+ Self {
|
|
+ max_values,
|
|
+ max_inputs,
|
|
+ keys: BTreeSet::new(),
|
|
+ input_count: 0,
|
|
+ text: atomic_values::TextBudget::new(max_display),
|
|
+ issues: Vec::new(),
|
|
+ }
|
|
+ }
|
|
+ pub fn collect(
|
|
+ &mut self,
|
|
+ arch: &Architecture,
|
|
+ fd: &Funcdata,
|
|
+ key: OpId,
|
|
+ ) -> Result<OperationFact, String> {
|
|
+ let op = fd.obank().get(key).ok_or("stale operation")?;
|
|
+ let parent = op.get_parent().ok_or("operation has no recovered block")?;
|
|
+ let block = fd.bblocks_ref().block(parent);
|
|
+ let id = operation_id(fd, key)?;
|
|
+ if op.num_input() < 0 {
|
|
+ return Err("negative native input count".into());
|
|
+ }
|
|
+ self.input_count = self
|
|
+ .input_count
|
|
+ .checked_add(op.num_input() as usize)
|
|
+ .ok_or("atomic input count overflow")?;
|
|
+ if self.input_count > self.max_inputs {
|
|
+ return Err("atomic input-edge capacity exceeded".into());
|
|
+ }
|
|
+ let phi = op.code() == OpCode::CPUI_MULTIEQUAL;
|
|
+ let phi_bound = phi && block.size_in() == op.num_input();
|
|
+ if phi && !phi_bound {
|
|
+ self.issues
|
|
+ .push(format!("{id}: phi/predecessor count mismatch"));
|
|
+ }
|
|
+ let inputs = (0..op.num_input())
|
|
+ .map(|index| {
|
|
+ let value = op.get_in(index);
|
|
+ if let Some(v) = value {
|
|
+ self.keys.insert(v);
|
|
+ }
|
|
+ InputFact {
|
|
+ input_index: index,
|
|
+ value_id: value.map(value_id),
|
|
+ phi_predecessor: phi_bound
|
|
+ .then(|| fd.bblocks_ref().block(block.get_in(index)).get_index()),
|
|
+ phi_predecessor_edge: phi_bound.then(|| {
|
|
+ format!(
|
|
+ "{}:{}",
|
|
+ fd.bblocks_ref().block(block.get_in(index)).get_index(),
|
|
+ block.get_in_rev_index(index)
|
|
+ )
|
|
+ }),
|
|
+ }
|
|
+ })
|
|
+ .collect();
|
|
+ if let Some(v) = op.get_out() {
|
|
+ self.keys.insert(v);
|
|
+ }
|
|
+ if self.keys.len() > self.max_values {
|
|
+ return Err("atomic value capacity exceeded".into());
|
|
+ }
|
|
+ let mut statement = String::new();
|
|
+ crate::funcdata_printraw::render_op(&mut statement, arch, fd, key)?;
|
|
+ self.text.charge(statement.len())?;
|
|
+ Ok(OperationFact {
|
|
+ id,
|
|
+ address: format!("0x{:x}", op.get_seq_num().get_addr().get_offset()),
|
|
+ block: block.get_index(),
|
|
+ opcode: format!("{:?}", op.code()),
|
|
+ statement,
|
|
+ is_dead: op.is_dead(),
|
|
+ inputs,
|
|
+ output: op.get_out().map(value_id),
|
|
+ })
|
|
+ }
|
|
+ pub fn values(&mut self, fd: &Funcdata) -> Result<Vec<ValueFact>, String> {
|
|
+ atomic_values::collect(fd, std::mem::take(&mut self.keys), &mut self.text)
|
|
+ }
|
|
+}
|
|
diff --git a/decompiler/crates/kuna-decomp/src/substrate/atomic_parameters.rs b/decompiler/crates/kuna-decomp/src/substrate/atomic_parameters.rs
|
|
new file mode 100644
|
|
index 00000000..82a34a49
|
|
--- /dev/null
|
|
+++ b/decompiler/crates/kuna-decomp/src/substrate/atomic_parameters.rs
|
|
@@ -0,0 +1,155 @@
|
|
+//! Native function prototype facts, independent of raw input varnodes/call count.
|
|
+use crate::{
|
|
+ atomic_storage::{storage, Storage},
|
|
+ atomic_values::TextBudget,
|
|
+ dtype::type_metatype,
|
|
+ fspec::{FuncProto, ProtoParameter},
|
|
+};
|
|
+use serde::Serialize;
|
|
+
|
|
+// Bound row expansion independently of the encoded document/text budgets.
|
|
+pub const MAX_PARAMETERS: usize = 12_000;
|
|
+#[derive(Debug, Clone, Serialize)]
|
|
+pub struct Parameter {
|
|
+ pub id: String,
|
|
+ pub native_index: Option<i32>,
|
|
+ pub name: String,
|
|
+ pub type_name: Option<String>,
|
|
+ pub size_bytes: i32,
|
|
+ pub is_void: Option<bool>,
|
|
+ pub storage: Option<Storage>,
|
|
+ pub type_locked: bool,
|
|
+ pub name_locked: bool,
|
|
+ pub size_locked: bool,
|
|
+ pub this_pointer: bool,
|
|
+ pub indirect_storage: bool,
|
|
+ pub hidden_return: bool,
|
|
+}
|
|
+#[derive(Debug, Clone, Serialize)]
|
|
+pub struct Parameters {
|
|
+ pub schema_version: u32,
|
|
+ pub model: Option<String>,
|
|
+ pub model_unknown: Option<bool>,
|
|
+ pub inputs_locked: bool,
|
|
+ pub output_locked: bool,
|
|
+ pub variadic: bool,
|
|
+ pub input_errors: bool,
|
|
+ pub output_errors: bool,
|
|
+ pub items: Vec<Parameter>,
|
|
+ pub output: Parameter,
|
|
+}
|
|
+fn parameter(
|
|
+ p: &dyn ProtoParameter,
|
|
+ index: Option<i32>,
|
|
+ text: &mut TextBudget,
|
|
+) -> Result<Parameter, String> {
|
|
+ text.charge(p.get_name().len())?;
|
|
+ if let Some(t) = p.get_type() {
|
|
+ text.charge(t.get_name().len())?;
|
|
+ }
|
|
+ Ok(Parameter {
|
|
+ id: index.map_or_else(|| "return".into(), |i| format!("parameter:{i}")),
|
|
+ native_index: index,
|
|
+ name: p.get_name().into(),
|
|
+ type_name: p.get_type().map(|t| t.get_name().into()),
|
|
+ size_bytes: p.get_size(),
|
|
+ is_void: p
|
|
+ .get_type()
|
|
+ .map(|t| t.get_metatype() == type_metatype::TYPE_VOID),
|
|
+ storage: storage(&p.get_address(), p.get_size()),
|
|
+ type_locked: p.is_type_locked(),
|
|
+ name_locked: p.is_name_locked(),
|
|
+ size_locked: p.is_size_type_locked(),
|
|
+ this_pointer: p.is_this_pointer(),
|
|
+ indirect_storage: p.is_indirect_storage(),
|
|
+ hidden_return: p.is_hidden_return(),
|
|
+ })
|
|
+}
|
|
+pub fn collect(proto: &FuncProto) -> Result<Parameters, String> {
|
|
+ if !proto.has_store() {
|
|
+ return Err("native function prototype store is unavailable".into());
|
|
+ }
|
|
+ let count =
|
|
+ usize::try_from(proto.num_params()).map_err(|_| "invalid native parameter count")?;
|
|
+ if count > MAX_PARAMETERS {
|
|
+ return Err("atomic parameter row capacity exceeded".into());
|
|
+ }
|
|
+ let mut text = TextBudget::new(2 * 1024 * 1024);
|
|
+ if proto.has_model() {
|
|
+ text.charge(proto.get_model_name().len())?;
|
|
+ }
|
|
+ let mut items = Vec::with_capacity(count);
|
|
+ for i in 0..count {
|
|
+ let p = proto
|
|
+ .get_param(i as i32)
|
|
+ .ok_or("missing native parameter slot")?;
|
|
+ items.push(parameter(p, Some(i as i32), &mut text)?);
|
|
+ }
|
|
+ Ok(Parameters {
|
|
+ schema_version: 1,
|
|
+ model: proto.has_model().then(|| proto.get_model_name().into()),
|
|
+ model_unknown: proto.has_model().then(|| proto.is_model_unknown()),
|
|
+ inputs_locked: proto.is_input_locked(),
|
|
+ output_locked: proto.is_output_locked(),
|
|
+ variadic: proto.is_dotdotdot(),
|
|
+ input_errors: proto.has_input_errors(),
|
|
+ output_errors: proto.has_output_errors(),
|
|
+ items,
|
|
+ output: parameter(proto.get_output(), None, &mut text)?,
|
|
+ })
|
|
+}
|
|
+
|
|
+#[cfg(test)]
|
|
+mod tests {
|
|
+ use super::*;
|
|
+ use crate::{dtype::Datatype, fspec::ParameterPieces};
|
|
+ use kuna_base::{
|
|
+ address::Address,
|
|
+ space::{spacetype, AddrSpace},
|
|
+ };
|
|
+ use std::rc::Rc;
|
|
+ #[test]
|
|
+ fn absent_store_is_unknown_not_an_empty_signature() {
|
|
+ assert!(collect(&FuncProto::new())
|
|
+ .unwrap_err()
|
|
+ .contains("unavailable"));
|
|
+ }
|
|
+ #[test]
|
|
+ fn native_slots_return_storage_void_and_locks_remain_distinct() {
|
|
+ let mut proto = FuncProto::new();
|
|
+ proto.attach_internal_store(Rc::new(Datatype::new(0, type_metatype::TYPE_VOID)));
|
|
+ let empty = collect(&proto).unwrap();
|
|
+ assert!(empty.items.is_empty());
|
|
+ assert_eq!(empty.output.is_void, Some(true));
|
|
+ assert!(empty.output.storage.is_none());
|
|
+ let reg = Rc::new(AddrSpace::new(
|
|
+ spacetype::IPTR_PROCESSOR,
|
|
+ "register",
|
|
+ false,
|
|
+ 8,
|
|
+ 1,
|
|
+ 3,
|
|
+ 0,
|
|
+ 0,
|
|
+ 0,
|
|
+ ));
|
|
+ let pieces = ParameterPieces {
|
|
+ addr: Address::new(reg, 0x38),
|
|
+ type_: Some(Rc::new(Datatype::new(8, type_metatype::TYPE_UNKNOWN))),
|
|
+ flags: 0,
|
|
+ };
|
|
+ proto.set_param(0, "display name", &pieces);
|
|
+ proto.set_input_lock(true);
|
|
+ proto.set_output(&pieces);
|
|
+ let facts = collect(&proto).unwrap();
|
|
+ assert_eq!(facts.items.len(), 1);
|
|
+ assert_eq!(facts.items[0].id, "parameter:0");
|
|
+ assert_eq!(facts.items[0].native_index, Some(0));
|
|
+ assert!(facts.inputs_locked && facts.items[0].type_locked);
|
|
+ assert_eq!(facts.output.is_void, Some(false));
|
|
+ let storage = facts.output.storage.unwrap();
|
|
+ assert_eq!(storage.location.space, "register");
|
|
+ assert_eq!(storage.location.offset, "0x38");
|
|
+ assert_eq!(storage.size_bytes, 8);
|
|
+ }
|
|
+}
|
|
diff --git a/decompiler/crates/kuna-decomp/src/substrate/atomic_projection.rs b/decompiler/crates/kuna-decomp/src/substrate/atomic_projection.rs
|
|
new file mode 100644
|
|
index 00000000..097b153a
|
|
--- /dev/null
|
|
+++ b/decompiler/crates/kuna-decomp/src/substrate/atomic_projection.rs
|
|
@@ -0,0 +1,117 @@
|
|
+//! Bounded backward SSA facts, independent of full CFG export availability.
|
|
+use crate::{
|
|
+ architecture::Architecture,
|
|
+ atomic_membership::Membership,
|
|
+ atomic_operations::{OperationBudget, OperationFact},
|
|
+ atomic_values::{operation_id, ValueFact},
|
|
+ funcdata::Funcdata,
|
|
+};
|
|
+use serde::Serialize;
|
|
+use std::collections::{BTreeMap, HashSet, VecDeque};
|
|
+
|
|
+#[derive(Debug, Clone, Serialize)]
|
|
+pub struct Projection {
|
|
+ pub schema_version: u32,
|
|
+ pub roots: Vec<String>,
|
|
+ pub operations: Vec<OperationFact>,
|
|
+ pub values: Vec<ValueFact>,
|
|
+ pub coverage: Coverage,
|
|
+}
|
|
+#[derive(Debug, Clone, Serialize)]
|
|
+pub struct Frontier {
|
|
+ pub definition: String,
|
|
+ pub reason: &'static str,
|
|
+}
|
|
+#[derive(Debug, Clone, Serialize)]
|
|
+pub struct Coverage {
|
|
+ pub scope: &'static str,
|
|
+ pub operation_budget: usize,
|
|
+ pub frontier: Vec<Frontier>,
|
|
+ pub issues: Vec<String>,
|
|
+ pub machine_semantic_equivalence: &'static str,
|
|
+}
|
|
+pub fn collect(
|
|
+ arch: &Architecture,
|
|
+ fd: &Funcdata,
|
|
+ membership: &Membership,
|
|
+) -> Result<Projection, String> {
|
|
+ const MAX_OPERATIONS: usize = 4096;
|
|
+ let members = membership
|
|
+ .members
|
|
+ .as_ref()
|
|
+ .ok_or("projection membership-index capacity exceeded")?;
|
|
+ if membership.calls.len() > MAX_OPERATIONS {
|
|
+ return Err("projection call-root capacity exceeded".into());
|
|
+ }
|
|
+ let roots = membership
|
|
+ .calls
|
|
+ .iter()
|
|
+ .map(|key| operation_id(fd, *key))
|
|
+ .collect::<Result<Vec<_>, _>>()?;
|
|
+ let mut queue: VecDeque<_> = membership.calls.iter().copied().collect();
|
|
+ let mut queued: HashSet<_> = membership.calls.iter().copied().collect();
|
|
+ let mut retained = HashSet::new();
|
|
+ let mut operations = Vec::new();
|
|
+ let mut budget = OperationBudget::new(12_288, 16_384, 2 * 1024 * 1024);
|
|
+ while let Some(key) = queue.pop_front() {
|
|
+ if !members.contains(&key) {
|
|
+ continue;
|
|
+ }
|
|
+ if retained.len() == MAX_OPERATIONS {
|
|
+ break;
|
|
+ }
|
|
+ if !retained.insert(key) {
|
|
+ continue;
|
|
+ }
|
|
+ operations.push(budget.collect(arch, fd, key)?);
|
|
+ let op = fd.obank().get(key).ok_or("stale projection operation")?;
|
|
+ for i in 0..op.num_input() {
|
|
+ if let Some(value) = op.get_in(i) {
|
|
+ let value = fd.vbank().get(value).ok_or("stale projection operand")?;
|
|
+ if let Some(definition) = value.get_def() {
|
|
+ if queued.insert(definition) {
|
|
+ queue.push_back(definition);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ let values = budget.values(fd)?;
|
|
+ let retained_ids: HashSet<_> = operations.iter().map(|op| op.id.as_str()).collect();
|
|
+ let mut reasons = BTreeMap::new();
|
|
+ for key in queued.difference(&retained) {
|
|
+ reasons.insert(
|
|
+ operation_id(fd, *key)?,
|
|
+ if members.contains(key) {
|
|
+ "operation-budget"
|
|
+ } else {
|
|
+ "outside-recovered-membership"
|
|
+ },
|
|
+ );
|
|
+ }
|
|
+ let missing: HashSet<_> = values
|
|
+ .iter()
|
|
+ .filter_map(|v| v.definition.as_ref())
|
|
+ .filter(|id| !retained_ids.contains(id.as_str()))
|
|
+ .cloned()
|
|
+ .collect();
|
|
+ let frontier = reasons
|
|
+ .into_iter()
|
|
+ .filter(|(id, _)| missing.contains(id))
|
|
+ .map(|(definition, reason)| Frontier { definition, reason })
|
|
+ .collect();
|
|
+ Ok(Projection {
|
|
+ schema_version: 1,
|
|
+ roots,
|
|
+ operations,
|
|
+ values,
|
|
+ coverage: Coverage {
|
|
+ scope:
|
|
+ "call-rooted bounded backward SSA definitions; not a CFG or complete use inventory",
|
|
+ operation_budget: MAX_OPERATIONS,
|
|
+ frontier,
|
|
+ issues: budget.issues,
|
|
+ machine_semantic_equivalence: "unknown",
|
|
+ },
|
|
+ })
|
|
+}
|
|
diff --git a/decompiler/crates/kuna-decomp/src/substrate/atomic_storage.rs b/decompiler/crates/kuna-decomp/src/substrate/atomic_storage.rs
|
|
new file mode 100644
|
|
index 00000000..9d64d4c2
|
|
--- /dev/null
|
|
+++ b/decompiler/crates/kuna-decomp/src/substrate/atomic_storage.rs
|
|
@@ -0,0 +1,30 @@
|
|
+//! Shared native storage representation; ABI/frame interpretation is not inferred.
|
|
+use kuna_base::address::Address;
|
|
+use serde::Serialize;
|
|
+
|
|
+#[derive(Debug, Clone, Serialize)]
|
|
+pub struct Location {
|
|
+ pub space: String,
|
|
+ pub offset: String,
|
|
+}
|
|
+#[derive(Debug, Clone, Serialize)]
|
|
+pub struct Storage {
|
|
+ #[serde(flatten)]
|
|
+ pub location: Location,
|
|
+ pub size_bytes: i32,
|
|
+}
|
|
+pub(crate) fn location(address: &Address) -> Option<Location> {
|
|
+ address.get_space().map(|space| Location {
|
|
+ space: space.get_name().to_string(),
|
|
+ offset: format!("0x{:x}", address.get_offset()),
|
|
+ })
|
|
+}
|
|
+pub(crate) fn storage(address: &Address, size_bytes: i32) -> Option<Storage> {
|
|
+ if size_bytes <= 0 {
|
|
+ return None;
|
|
+ }
|
|
+ location(address).map(|location| Storage {
|
|
+ location,
|
|
+ size_bytes,
|
|
+ })
|
|
+}
|
|
diff --git a/decompiler/crates/kuna-decomp/src/substrate/atomic_switches.rs b/decompiler/crates/kuna-decomp/src/substrate/atomic_switches.rs
|
|
new file mode 100644
|
|
index 00000000..e8992d5d
|
|
--- /dev/null
|
|
+++ b/decompiler/crates/kuna-decomp/src/substrate/atomic_switches.rs
|
|
@@ -0,0 +1,198 @@
|
|
+//! Read-only native jump-table facts; row ordinals are not case values.
|
|
+use crate::{
|
|
+ atomic_membership::Membership,
|
|
+ atomic_values::{self, operation_id, ValueFact},
|
|
+ funcdata::Funcdata,
|
|
+ jumptable::NO_LABEL,
|
|
+};
|
|
+use kuna_base::address::Address;
|
|
+use serde::Serialize;
|
|
+use std::collections::{BTreeMap, BTreeSet};
|
|
+
|
|
+const MAX_ROWS: usize = 100_000;
|
|
+const MAX_MAPPINGS: usize = 100_000;
|
|
+#[derive(Debug, Clone, Serialize)]
|
|
+pub struct SwitchFacts {
|
|
+ pub schema_version: u32,
|
|
+ pub items: Vec<SwitchFact>,
|
|
+ pub coverage: Coverage,
|
|
+}
|
|
+#[derive(Debug, Clone, Serialize)]
|
|
+pub struct Coverage {
|
|
+ pub scope: &'static str,
|
|
+ pub blocks_examined: i32,
|
|
+ pub operations_examined: usize,
|
|
+ pub machine_semantic_equivalence: &'static str,
|
|
+}
|
|
+#[derive(Debug, Clone, Serialize)]
|
|
+pub struct SwitchFact {
|
|
+ pub id: String,
|
|
+ pub origin_address: String,
|
|
+ pub block: i32,
|
|
+ pub selector: Option<ValueFact>,
|
|
+ pub table: Option<Table>,
|
|
+}
|
|
+#[derive(Debug, Clone, Serialize)]
|
|
+pub struct Table {
|
|
+ pub partial: bool,
|
|
+ pub labelled: bool,
|
|
+ pub consumed_mask: String,
|
|
+ pub signed_render_hint: bool,
|
|
+ pub default_raw_index: i32,
|
|
+ pub default_edge: Option<Edge>,
|
|
+ pub default_is_folded: bool,
|
|
+ pub rows: Vec<Row>,
|
|
+}
|
|
+#[derive(Debug, Clone, Serialize)]
|
|
+pub struct Edge {
|
|
+ pub id: String,
|
|
+ pub index: i32,
|
|
+ pub target: i32,
|
|
+}
|
|
+#[derive(Debug, Clone, Serialize)]
|
|
+pub struct Row {
|
|
+ pub index: usize,
|
|
+ pub label: Option<Label>,
|
|
+ pub recovered_target: Option<Location>,
|
|
+ pub edges: Vec<Edge>,
|
|
+}
|
|
+#[derive(Debug, Clone, Serialize)]
|
|
+pub struct Label {
|
|
+ pub status: &'static str,
|
|
+ pub raw_bits: String,
|
|
+}
|
|
+#[derive(Debug, Clone, Serialize)]
|
|
+pub struct Location {
|
|
+ pub space: String,
|
|
+ pub offset: String,
|
|
+}
|
|
+fn location(
|
|
+ address: &Address,
|
|
+ text: &mut atomic_values::TextBudget,
|
|
+) -> Result<Option<Location>, String> {
|
|
+ let Some(space) = address.get_space() else {
|
|
+ return Ok(None);
|
|
+ };
|
|
+ text.charge(space.get_name().len())?;
|
|
+ Ok(Some(Location {
|
|
+ space: space.get_name().to_string(),
|
|
+ offset: format!("0x{:x}", address.get_offset()),
|
|
+ }))
|
|
+}
|
|
+fn edge(fd: &Funcdata, parent: crate::context::BlockId, index: i32) -> Result<Edge, String> {
|
|
+ let block = fd.bblocks_ref().block(parent);
|
|
+ if index < 0 || index >= block.size_out() {
|
|
+ return Err("native switch out-edge index is outside its block".into());
|
|
+ }
|
|
+ Ok(Edge {
|
|
+ id: format!("{}:{index}", block.get_index()),
|
|
+ index,
|
|
+ target: fd.bblocks_ref().block(block.get_out(index)).get_index(),
|
|
+ })
|
|
+}
|
|
+pub fn collect(fd: &Funcdata, membership: &Membership) -> Result<SwitchFacts, String> {
|
|
+ let switches = membership
|
|
+ .switches
|
|
+ .as_ref()
|
|
+ .ok_or("atomic switch capacity exceeded")?;
|
|
+ let mut items = Vec::with_capacity(switches.len());
|
|
+ let mut row_count = 0usize;
|
|
+ let mut mapping_count = 0usize;
|
|
+ let mut text = atomic_values::TextBudget::new(2 * 1024 * 1024);
|
|
+ for &key in switches {
|
|
+ let op = fd.obank().get(key).ok_or("stale switch operation")?;
|
|
+ let parent = op.get_parent().ok_or("native switch has no block")?;
|
|
+ let mut values = BTreeSet::new();
|
|
+ if let Some(value) = op.get_in(0) {
|
|
+ values.insert(value);
|
|
+ }
|
|
+ let selector = atomic_values::collect(fd, values, &mut text)?.pop();
|
|
+ if let Some(space) = selector.as_ref().and_then(|value| value.space.as_ref()) {
|
|
+ text.charge(space.len())?;
|
|
+ }
|
|
+ let table = if let Some(table) = fd.find_jump_table(key) {
|
|
+ if table.get_indirect_op() != Some(key) {
|
|
+ return Err("native jump-table operation identity is unresolved".into());
|
|
+ }
|
|
+ let observed = table.case_rows().map_err(|e| e.to_string())?;
|
|
+ row_count = row_count
|
|
+ .checked_add(observed.len())
|
|
+ .ok_or("atomic case count overflow")?;
|
|
+ let mappings = table.case_edge_rows();
|
|
+ mapping_count = mapping_count
|
|
+ .checked_add(mappings.len())
|
|
+ .ok_or("atomic case edge count overflow")?;
|
|
+ if row_count > MAX_ROWS || mapping_count > MAX_MAPPINGS {
|
|
+ return Err("atomic switch row/mapping capacity exceeded".into());
|
|
+ }
|
|
+ let mut by_row: BTreeMap<usize, Vec<Edge>> = BTreeMap::new();
|
|
+ let mut seen = BTreeSet::new();
|
|
+ for (row, position) in mappings {
|
|
+ if row < 0 || row as usize >= observed.len() {
|
|
+ return Err("native switch mapping names an absent address row".into());
|
|
+ }
|
|
+ if !seen.insert((row, position)) {
|
|
+ return Err("duplicate native switch row/out-edge mapping".into());
|
|
+ }
|
|
+ by_row
|
|
+ .entry(row as usize)
|
|
+ .or_default()
|
|
+ .push(edge(fd, parent, position)?);
|
|
+ }
|
|
+ let rows = observed
|
|
+ .map(|(index, address, label)| {
|
|
+ Ok(Row {
|
|
+ index,
|
|
+ label: label.map(|bits| Label {
|
|
+ status: if bits == NO_LABEL {
|
|
+ "no-label"
|
|
+ } else {
|
|
+ "value"
|
|
+ },
|
|
+ raw_bits: format!("0x{bits:x}"),
|
|
+ }),
|
|
+ recovered_target: location(address, &mut text)?,
|
|
+ edges: by_row.remove(&index).unwrap_or_default(),
|
|
+ })
|
|
+ })
|
|
+ .collect::<Result<Vec<_>, String>>()?;
|
|
+ let raw_default = table.get_default_block();
|
|
+ if raw_default < -1 {
|
|
+ return Err("invalid native default out-edge index".into());
|
|
+ }
|
|
+ Some(Table {
|
|
+ partial: table.is_partial(),
|
|
+ labelled: table.is_labelled(),
|
|
+ consumed_mask: format!("0x{:x}", table.get_switch_var_consume()),
|
|
+ signed_render_hint: table.kuna_has_signed_labels(),
|
|
+ default_raw_index: raw_default,
|
|
+ default_edge: if raw_default == -1 {
|
|
+ None
|
|
+ } else {
|
|
+ Some(edge(fd, parent, raw_default)?)
|
|
+ },
|
|
+ default_is_folded: table.has_folded_default(),
|
|
+ rows,
|
|
+ })
|
|
+ } else {
|
|
+ None
|
|
+ };
|
|
+ items.push(SwitchFact {
|
|
+ id: operation_id(fd, key)?,
|
|
+ origin_address: format!("0x{:x}", op.get_addr().get_offset()),
|
|
+ block: fd.bblocks_ref().block(parent).get_index(),
|
|
+ selector,
|
|
+ table,
|
|
+ });
|
|
+ }
|
|
+ Ok(SwitchFacts {
|
|
+ schema_version: 1,
|
|
+ items,
|
|
+ coverage: Coverage {
|
|
+ scope: "all recovered block-member BRANCHIND operations and native jump-table rows",
|
|
+ blocks_examined: membership.blocks_examined,
|
|
+ operations_examined: membership.operations_examined,
|
|
+ machine_semantic_equivalence: "unknown",
|
|
+ },
|
|
+ })
|
|
+}
|
|
diff --git a/decompiler/crates/kuna-decomp/src/substrate/atomic_values.rs b/decompiler/crates/kuna-decomp/src/substrate/atomic_values.rs
|
|
new file mode 100644
|
|
index 00000000..6d23954c
|
|
--- /dev/null
|
|
+++ b/decompiler/crates/kuna-decomp/src/substrate/atomic_values.rs
|
|
@@ -0,0 +1,80 @@
|
|
+//! One read-only interpretation of native value and operation identities.
|
|
+use crate::{
|
|
+ context::{OpId, VarnodeId},
|
|
+ funcdata::Funcdata,
|
|
+};
|
|
+use serde::Serialize;
|
|
+use slotmap::Key;
|
|
+use std::collections::BTreeSet;
|
|
+
|
|
+pub struct TextBudget {
|
|
+ used: usize,
|
|
+ limit: usize,
|
|
+}
|
|
+impl TextBudget {
|
|
+ pub fn new(limit: usize) -> Self {
|
|
+ Self { used: 0, limit }
|
|
+ }
|
|
+ pub fn charge(&mut self, bytes: usize) -> Result<(), String> {
|
|
+ let next = self
|
|
+ .used
|
|
+ .checked_add(bytes)
|
|
+ .ok_or("atomic text count overflow")?;
|
|
+ if next > self.limit {
|
|
+ return Err("atomic text capacity exceeded".into());
|
|
+ }
|
|
+ self.used = next;
|
|
+ Ok(())
|
|
+ }
|
|
+}
|
|
+#[derive(Debug, Clone, Serialize)]
|
|
+pub struct ValueFact {
|
|
+ pub id: String,
|
|
+ pub space: Option<String>,
|
|
+ pub offset: String,
|
|
+ pub size_bytes: i32,
|
|
+ pub is_constant: bool,
|
|
+ pub is_input: bool,
|
|
+ pub is_annotation: bool,
|
|
+ pub definition: Option<String>,
|
|
+ pub type_name: String,
|
|
+}
|
|
+pub fn value_id(id: VarnodeId) -> String {
|
|
+ format!("v{:x}", id.data().as_ffi())
|
|
+}
|
|
+pub fn operation_id(fd: &Funcdata, id: OpId) -> Result<String, String> {
|
|
+ let operation = fd.obank().get(id).ok_or("stale operation identity")?;
|
|
+ let seq = operation.get_seq_num();
|
|
+ Ok(format!(
|
|
+ "0x{:x}:{:x}",
|
|
+ seq.get_addr().get_offset(),
|
|
+ seq.get_time()
|
|
+ ))
|
|
+}
|
|
+pub fn collect(
|
|
+ fd: &Funcdata,
|
|
+ keys: BTreeSet<VarnodeId>,
|
|
+ text: &mut TextBudget,
|
|
+) -> Result<Vec<ValueFact>, String> {
|
|
+ let mut values = Vec::with_capacity(keys.len());
|
|
+ for key in keys {
|
|
+ let value = fd.vbank().get(key).ok_or("stale input/output value")?;
|
|
+ let type_name = value.get_type().get_name();
|
|
+ text.charge(type_name.len())?;
|
|
+ values.push(ValueFact {
|
|
+ id: value_id(key),
|
|
+ space: value
|
|
+ .get_addr()
|
|
+ .get_space()
|
|
+ .map(|s| s.get_name().to_string()),
|
|
+ offset: format!("0x{:x}", value.get_offset()),
|
|
+ size_bytes: value.get_size(),
|
|
+ is_constant: value.is_constant(),
|
|
+ is_input: value.is_input(),
|
|
+ is_annotation: value.is_annotation(),
|
|
+ definition: value.get_def().map(|id| operation_id(fd, id)).transpose()?,
|
|
+ type_name: type_name.to_string(),
|
|
+ });
|
|
+ }
|
|
+ Ok(values)
|
|
+}
|
|
diff --git a/decompiler/crates/kuna-decomp/src/substrate/funcdata_printraw.rs b/decompiler/crates/kuna-decomp/src/substrate/funcdata_printraw.rs
|
|
index 39927ceb..844950a6 100644
|
|
--- a/decompiler/crates/kuna-decomp/src/substrate/funcdata_printraw.rs
|
|
+++ b/decompiler/crates/kuna-decomp/src/substrate/funcdata_printraw.rs
|
|
@@ -376,7 +376,7 @@ fn callother_operator_name(
|
|
/// dispatch. The special op-codes (LOAD/STORE/branches/calls/RETURN/marker)
|
|
/// match their `TypeOp*::printRaw` override; everything else falls to the
|
|
/// generic Binary/Unary/Func shape keyed by the [`TypeOpClass`].
|
|
-fn render_op(s: &mut String, arch: &Architecture, fd: &Funcdata, op: OpId) -> Result<(), String> {
|
|
+pub(crate) fn render_op(s: &mut String, arch: &Architecture, fd: &Funcdata, op: OpId) -> Result<(), String> {
|
|
let opc = fd.obank().get(op).ok_or_else(|| "render_op: stale op".to_string())?.code();
|
|
let num_input = fd.obank().get(op).ok_or_else(|| "render_op: stale op".to_string())?.num_input();
|
|
let in_at = |i: int4| -> Option<VarnodeId> { fd.obank().get(op).and_then(|o| o.get_in(i)) };
|
|
diff --git a/decompiler/crates/kuna-decomp/src/substrate/mod.rs b/decompiler/crates/kuna-decomp/src/substrate/mod.rs
|
|
index 00528cb5..4330ff1c 100644
|
|
--- a/decompiler/crates/kuna-decomp/src/substrate/mod.rs
|
|
+++ b/decompiler/crates/kuna-decomp/src/substrate/mod.rs
|
|
@@ -11,6 +11,15 @@ pub mod funcdata_block;
|
|
pub mod funcdata_encode;
|
|
pub mod funcdata_op;
|
|
pub mod funcdata_printraw;
|
|
+pub mod atomic_facts;
|
|
+pub mod atomic_parameters;
|
|
+pub mod atomic_storage;
|
|
+pub mod atomic_calls;
|
|
+pub mod atomic_membership;
|
|
+pub mod atomic_operations;
|
|
+pub mod atomic_projection;
|
|
+pub mod atomic_values;
|
|
+pub mod atomic_switches;
|
|
pub mod funcdata_varnode;
|
|
pub mod dtype;
|
|
pub mod context;
|
|
diff --git a/docs/spec/00-overview.md b/docs/spec/00-overview.md
|
|
index ce499c4f..9d927aa5 100644
|
|
--- a/docs/spec/00-overview.md
|
|
+++ b/docs/spec/00-overview.md
|
|
@@ -3431,3 +3431,98 @@ Reserving it creates no analysis varnode and changes no physical mapping or
|
|
emitted C. Ghidra receives temporary storage for the logical local, rather than
|
|
a persistable ordinary stack-variable location; ordinary symbol storage and
|
|
register-plus-stack parameter JOINs keep their existing encoding.
|
|
+
|
|
+## Experimental read-only atomic facts
|
|
+
|
|
+The same opt-in capture also exports an independent version-1 `atomic_switches`
|
|
+facet. One membership scan supplies recovered BRANCHIND identities. Native
|
|
+jump-table rows retain their original index, optional label/status, original
|
|
+target address and native row-to-out-edge mapping. Indexes are never treated as
|
|
+case values or mapped by comparing optimized block start addresses. Missing
|
|
+tables/labels/mappings, the reserved no-label marker and partial tables remain
|
|
+explicit. A native model default out-edge is separate from any machine
|
|
+out-of-range guard destination. Selector values and consumed-bit masks do not establish runtime
|
|
+feasibility or machine semantic equivalence; the native signed-label flag is a
|
|
+render hint, not a general signedness proof.
|
|
+
|
|
+Construction admits 12,000 switches, 100,000 rows and row/edge mappings, and a
|
|
+2 MiB shared selector/type/address-space text budget. Mapping is indexed once
|
|
+instead of scanning every edge for every case. New switch rows cannot displace
|
|
+otherwise fitting existing graph/call facts: the bounded document writer retries
|
|
+without this facet first, retains explicit capacity errors, and can still keep
|
|
+the independent rows if other facets overflow. Ordinary non-opt-in analysis and
|
|
+executable bytes are unchanged.
|
|
+
|
|
+The opt-in `decompile --json --atomic-facts` surface captures the same live
|
|
+function after C rendering (`decompiler/crates/kuna-decomp/src/substrate/atomic_facts.rs`).
|
|
+It does not change the analysis phases or C output. The default path does not
|
|
+collect these records. This experiment requires embedded processor specs and
|
|
+selects those assets explicitly; a caller-provided `--sleighpath` is rejected.
|
|
+
|
|
+The export retains ordered basic-block predecessors and successors, slotmap
|
|
+value identities, operation sequence identities, operand indexes, outputs and
|
|
+phi input-to-predecessor-edge bindings. IDs belong to this captured function
|
|
+model; instruction addresses are origins, not SSA value identities. Native
|
|
+recovered graph completeness does not establish runtime successor coverage,
|
|
+memory alias dependencies or machine-semantic equivalence. Indirect-branch
|
|
+operations and definitions outside block membership remain explicit. Capacity
|
|
+failures make this facet unavailable with a reason and preserve the C record;
|
|
+they do not emit a clipped graph labeled complete.
|
|
+
|
|
+`--atomic-output-bytes N` sets the encoded document budget (default 8 MiB).
|
|
+Atomic JSON uses the ordinary record producer and the shared compact ASCII-safe
|
|
+writer. That writer checks each encoded write before growing its bounded output
|
|
+buffer, including control/surrogate escaping and the final newline. A document
|
|
+that cannot fit omits unavailable recovered facets with named errors and
|
|
+retains C, locations and ordinary metadata. If that base record also cannot fit,
|
|
+the command reports failure instead of truncating it or analyzing the target
|
|
+again. This flag requires `--atomic-facts --json`; ordinary output is unchanged.
|
|
+
|
|
+Graph schema version 3 and an independent `atomic_calls` version-1 facet are
|
|
+captured from the same live function after C rendering. The call facet scans
|
|
+all native block members without allocating a copy of every block's operations;
|
|
+it has its own call/input/value/text limits. Full-graph capacity failure leaves
|
|
+calls and their boundary values available. Shared value extraction owns the
|
|
+native value identities, flags, widths and definitions; boundary values do not
|
|
+claim a complete function value/definition graph.
|
|
+
|
|
+`FuncCallSpecs` supplies recovered targets, names, prototype flags and passing
|
|
+storage. Target input zero remains an SSA value (often an fspec annotation),
|
|
+never a callee code address. CALLIND targets can remain unknown. Storage uses
|
|
+matching-arity finalized locations or locked prototype storage; shifted inputs
|
|
+and placeholders stay unknown. Input slots are not source parameter ordinals.
|
|
+
|
|
+The encoded writer attempts bounded facet alternatives without cloning their
|
|
+contents. A raw schema-3 graph that fits keeps its existing format. Before
|
|
+dropping useful facts, an oversized graph can use a schema-4 envelope with
|
|
+`encoding: "gzip-base64"`, the exact `raw_bytes` and canonical padded `data`.
|
|
+Its decompressed JSON is the unchanged logical schema-3 graph. Raw encoding
|
|
+stops at 32 MiB; compressed output, base64 expansion and envelope overhead are
|
|
+bounded before allocation against the caller's encoded budget. All graphs in a
|
|
+document share that encoded budget. Optional compression does not analyze again.
|
|
+The independent full-graph row limits are 32,000 operations, 36,000 values,
|
|
+100,000 inputs, 4 MiB display/type text, 12,000 blocks and 50,000 CFG edges.
|
|
+They bound construction separately from wire size; exceeding them is unavailable,
|
|
+not a complete clipped graph. C and independent calls keep their own contracts.
|
|
+A full graph omits its redundant operation projection; if encoded
|
|
+capacity makes that graph unavailable, the independent projection is restored.
|
|
+A fitting graph has priority, then fitting calls, then the projection. Failed
|
|
+facets retain named errors. If C itself cannot fit, output fails explicitly
|
|
+without reanalysis. Facet errors do not fabricate empty collections or change C.
|
|
+
|
|
+`atomic_membership.rs` owns one checked scan of recovered block membership and
|
|
+call roots. Its optional index is bounded at 65,536 native operation keys; calls
|
|
+remain readable beyond that index budget. `atomic_operations.rs` normalizes
|
|
+operands, phi bindings, origins, outputs, display and values for both full and
|
|
+selected exports. Full-graph operation capacity is checked before materializing
|
|
+its rows, after the common scan.
|
|
+
|
|
+The independent version-1 `atomic_projection` retains all recovered calls as
|
|
+roots, then follows backward operand definitions within a 4,096-operation,
|
|
+12,288-value, 16,384-input and 2 MiB text budget. Too many roots or unavailable
|
|
+membership make this facet unavailable. Omitted defining operations have exact
|
|
+IDs and `operation-budget` or `outside-recovered-membership` frontier reasons.
|
|
+This does not collect CFG or complete use lists, infer memory aliases, or prove
|
|
+runtime reachability. Collection uses the same live function after C rendering;
|
|
+no second decompilation is performed. Complete source Evidence keeps the native
|
|
+frontier while callers may select a compact summary and individual facts.
|