Carries `xenia-rs` `harvest/import-thunk-naming` (b4f19f1) across into the lifted crate. That work was found UNCOMMITTED in the retired repo's working tree on 2026-09-14 and exists nowhere else: absent from `iterate-4A`, absent from this crate as lifted. CONSOLIDATION.md Phase 5 ends with "delete the local clone" and Phase 7 drops the emulator, so it had a deletion scheduled against it. Not a cherry-pick. The lift's base is exactly the harvest's parent (8401d4d), so each file was merged three-way -- lifted vs base vs harvest -- which is what made the port reviewable: 10 conflicts, 9 of them pure rustfmt reflow from the lift's formatting pass, and 1 semantic. The semantic one is insertion ORDER. `xdbf_achievements.image_id` is now a foreign key onto `xdbf_images(id)`, so the image rows must be inserted before the achievement rows. The merge moved that block; the conflict was the stale copy left at the old position. What arrives: * **Import-thunk recognition** (`imports.rs`, 338 lines). An XEX import is not a PLT jump: the linker emits a four-word thunk whose first two words are import RECORDS that the loader rewrites at module load. On disc they are still records, so a PowerPC-only decoder prints two meaningless `.long`s in front of an indirect branch. This maps every word of every thunk, and every direct branch into one, back to its `imports` row. Shape-validated rather than trusted: an entry is indexed only when the four words it points at actually have the thunk shape. Adds `instructions.import_address` (FK onto `imports.address`) and `import_role` (`'record'` | `'thunk'` | `'call'`, NULL iff import_address is NULL), plus an index. `tools/zq.py` gains `imp` and `impcalls`, and `dis` now names imports instead of printing `.long 0x01010194`. * **Sixteen schema-wide foreign keys**, declared wherever a column is derived from another table. CREATE TABLE and insertion order become load-bearing. `functions` is deliberately NOT an FK parent and the golden test now asserts zero inbound FKs onto it: DuckDB implements UPDATE as delete+insert, so one inbound FK would make `functions.name` un-updatable and break `apply_re_symbols.sql`, which re-applies RE symbol names after every regeneration. The database path needed no change in the binary: `DbWriter` builds its own index inside `ingest_instructions` from `info.import_libraries`. Only the two output paths (`enrich_section` for JSONL, `write_asm`) take it as an argument. Verified: `cargo test -p sylpheed-xexdb` = 10 passed / 0 failed, including `db_schema_golden` (41s, builds a real DuckDB) which locks the 16-FK set and the no-FK-onto-functions rule. `cargo clippy -p sylpheed-xexdb --all-targets -- -D warnings` clean; `cargo fmt --all --check` clean. Stacked on #32 -- it ports into a crate that only exists there. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
302 lines
12 KiB
Rust
302 lines
12 KiB
Rust
//! Additive SQL views over the Phase-3 ingest tables.
|
|
//!
|
|
//! These views are created when `--analyze=sql` or `--analyze=both` is set.
|
|
//! They are *not* a replacement for the Rust passes ([`crate::xref`],
|
|
//! [`crate::func`]) — those still own data-ref resolution and prologue
|
|
//! pattern matching. The views cover the cleanly-relational parts:
|
|
//!
|
|
//! - branch xrefs (self-join on `instructions.target_hex`)
|
|
//! - call graph + reachability (recursive CTE over `xrefs`)
|
|
//! - convenience joins (function-first-instruction, imports-called)
|
|
//!
|
|
//! All views are read-only and stable across re-creation: dropping and
|
|
//! recreating the database via [`crate::db::DbWriter::open_fresh`] re-runs
|
|
//! these definitions.
|
|
//!
|
|
//! ## Cross-check semantics
|
|
//!
|
|
//! `v_branch_xrefs` is intended to produce *exactly* the same `(source,
|
|
//! target, kind)` tuples as the Rust `xref.rs` first pass — given the same
|
|
//! input image. [`crate::db::DbWriter::cross_check_branch_xrefs`] queries
|
|
//! the symmetric difference and returns the row counts; both should be
|
|
//! zero. A non-zero count means the formatter's `mnemonic` column or the
|
|
//! kind-classification CASE drifted out of agreement with `xref.rs`, and
|
|
//! is worth a one-line warning at log time.
|
|
|
|
/// Every XDBF string side-by-side across the languages the title ships, so a
|
|
/// piece of UI text can be looked up once and read in all locales.
|
|
const V_XDBF_TEXT: &str = "
|
|
CREATE OR REPLACE VIEW v_xdbf_text AS
|
|
SELECT
|
|
s.string_id,
|
|
MAX(CASE WHEN s.language = 1 THEN s.value END) AS english,
|
|
MAX(CASE WHEN s.language = 2 THEN s.value END) AS japanese,
|
|
MAX(CASE WHEN s.language = 3 THEN s.value END) AS german,
|
|
MAX(CASE WHEN s.language = 4 THEN s.value END) AS french,
|
|
MAX(CASE WHEN s.language = 5 THEN s.value END) AS spanish,
|
|
MAX(CASE WHEN s.language = 6 THEN s.value END) AS italian
|
|
FROM xdbf_strings s
|
|
GROUP BY s.string_id;
|
|
";
|
|
|
|
/// Achievements joined to their three strings in every shipped language.
|
|
const V_XDBF_ACHIEVEMENTS: &str = "
|
|
CREATE OR REPLACE VIEW v_xdbf_achievements AS
|
|
SELECT
|
|
a.id,
|
|
a.gamerscore,
|
|
s.language,
|
|
s.language_name,
|
|
n.value AS name,
|
|
u.value AS unlocked_desc,
|
|
l.value AS locked_desc,
|
|
a.image_id
|
|
FROM xdbf_achievements a
|
|
JOIN (SELECT DISTINCT language, language_name FROM xdbf_strings) s ON TRUE
|
|
LEFT JOIN xdbf_strings n ON n.language = s.language AND n.string_id = a.label_id
|
|
LEFT JOIN xdbf_strings u ON u.language = s.language AND u.string_id = a.description_id
|
|
LEFT JOIN xdbf_strings l ON l.language = s.language AND l.string_id = a.unachieved_id;
|
|
";
|
|
|
|
/// `(view_name, CREATE VIEW … SQL)` pairs in the order they must run.
|
|
/// Later views may depend on earlier ones (e.g. `v_call_graph` reads
|
|
/// `xrefs`, which is the Rust-pass table; `v_branch_xrefs` is independent).
|
|
pub const ALL_VIEWS: &[(&str, &str)] = &[
|
|
("v_branch_xrefs", V_BRANCH_XREFS),
|
|
("v_call_graph", V_CALL_GRAPH),
|
|
("v_reachability_from_entry", V_REACHABILITY_FROM_ENTRY),
|
|
(
|
|
"v_indirect_reachability_from_entry",
|
|
V_INDIRECT_REACHABILITY_FROM_ENTRY,
|
|
),
|
|
("v_function_first_instruction", V_FUNCTION_FIRST_INSTRUCTION),
|
|
("v_imports_called", V_IMPORTS_CALLED),
|
|
("v_xdbf_text", V_XDBF_TEXT),
|
|
("v_xdbf_achievements", V_XDBF_ACHIEVEMENTS),
|
|
("v_switch_cases", V_SWITCH_CASES),
|
|
("v_class_hierarchy", V_CLASS_HIERARCHY),
|
|
("v_class_methods", V_CLASS_METHODS),
|
|
("v_function_strings", V_FUNCTION_STRINGS),
|
|
];
|
|
|
|
/// Branch cross-references derived purely from `instructions.target_hex`.
|
|
///
|
|
/// Mirrors the kind classification in [`crate::xref::collect_branch_target`]
|
|
/// and the short tags returned by [`crate::xref::XrefKind::tag`] (which are
|
|
/// what `xrefs.kind` actually stores):
|
|
/// - I-form (`b`/`bl`/`ba`/`bla`): `bl`/`bla` → `"call"`, `b`/`ba` → `"j"`
|
|
/// - B-form (`bc`/`bcl`/`bca`/`bcla`): always → `"br"`
|
|
///
|
|
/// Indirect branches (`bclr`/`bcctr`) leave `target_hex` NULL and are
|
|
/// excluded from this view by design.
|
|
const V_BRANCH_XREFS: &str = "
|
|
CREATE OR REPLACE VIEW v_branch_xrefs AS
|
|
SELECT
|
|
address AS source,
|
|
target_hex AS target,
|
|
CASE
|
|
WHEN mnemonic IN ('bl', 'bla') THEN 'call'
|
|
WHEN mnemonic IN ('b', 'ba') THEN 'j'
|
|
WHEN mnemonic IN ('bc', 'bcl', 'bca', 'bcla') THEN 'br'
|
|
ELSE 'br'
|
|
END AS kind,
|
|
mnemonic AS instruction,
|
|
function AS source_func
|
|
FROM instructions
|
|
WHERE target_hex IS NOT NULL;
|
|
";
|
|
|
|
/// Call-graph edges resolved against function names.
|
|
///
|
|
/// Reads from `xrefs` (the Rust-pass table) — this is the canonical source
|
|
/// for *all* edge kinds, including indirect/data; SQL can't reconstruct the
|
|
/// data-ref edges cleanly because they require register tracking. For pure
|
|
/// branch edges, `v_branch_xrefs` produces equivalent rows directly from
|
|
/// `instructions`.
|
|
const V_CALL_GRAPH: &str = "
|
|
CREATE OR REPLACE VIEW v_call_graph AS
|
|
SELECT
|
|
x.source AS caller_addr,
|
|
cf.name AS caller_name,
|
|
x.target AS callee_addr,
|
|
tf.name AS callee_name,
|
|
x.kind AS edge_kind
|
|
FROM xrefs x
|
|
LEFT JOIN functions cf ON cf.address = x.source_func
|
|
LEFT JOIN functions tf ON tf.address = x.target
|
|
WHERE x.kind = 'call';
|
|
";
|
|
|
|
/// Transitive function-level reachability from the entry point over
|
|
/// call/jump/branch edges. Useful for finding dead code
|
|
/// (`SELECT address FROM functions
|
|
/// WHERE address NOT IN (SELECT addr FROM v_reachability_from_entry)`)
|
|
/// and for scoping analysis to the live subset.
|
|
///
|
|
/// Seeds from the function containing the `entry_point` label and walks
|
|
/// the recursive closure: a reachable function's instructions branch into
|
|
/// the functions enclosing the branch targets, which are then reachable
|
|
/// in turn. `UNION` (not `UNION ALL`) deduplicates to handle call-graph
|
|
/// cycles (recursive functions, mutually-recursive pairs).
|
|
const V_REACHABILITY_FROM_ENTRY: &str = "
|
|
CREATE OR REPLACE VIEW v_reachability_from_entry AS
|
|
WITH RECURSIVE reach(fn) AS (
|
|
SELECT i.function FROM instructions i
|
|
JOIN labels l ON l.address = i.address
|
|
WHERE l.name = 'entry_point' AND i.function IS NOT NULL
|
|
UNION
|
|
SELECT tgt.function FROM xrefs x
|
|
JOIN instructions src ON src.address = x.source
|
|
JOIN instructions tgt ON tgt.address = x.target
|
|
JOIN reach r ON src.function = r.fn
|
|
WHERE x.kind IN ('call', 'j', 'br', 'jt')
|
|
AND tgt.function IS NOT NULL
|
|
)
|
|
SELECT fn AS addr FROM reach;
|
|
";
|
|
|
|
/// Reachability extended over `kind='ind_call'` edges from M5. Strict
|
|
/// superset of `v_reachability_from_entry` — every fn there is also here,
|
|
/// plus any function reached only via a vtable bcctrl whose vtable+slot
|
|
/// the M5 dataflow could resolve. Sample 5 newly-reachable PCs in canary
|
|
/// before trusting widely; the analysis intentionally leaves out alias-
|
|
/// dependent indirect calls (vtable loaded from a `this` field).
|
|
const V_INDIRECT_REACHABILITY_FROM_ENTRY: &str = "
|
|
CREATE OR REPLACE VIEW v_indirect_reachability_from_entry AS
|
|
WITH RECURSIVE reach(fn) AS (
|
|
SELECT i.function FROM instructions i
|
|
JOIN labels l ON l.address = i.address
|
|
WHERE l.name = 'entry_point' AND i.function IS NOT NULL
|
|
UNION
|
|
SELECT tgt.function FROM xrefs x
|
|
JOIN instructions src ON src.address = x.source
|
|
JOIN instructions tgt ON tgt.address = x.target
|
|
JOIN reach r ON src.function = r.fn
|
|
WHERE x.kind IN ('call', 'ind_call', 'j', 'br', 'jt')
|
|
AND tgt.function IS NOT NULL
|
|
)
|
|
SELECT fn AS addr FROM reach;
|
|
";
|
|
|
|
/// Convenience join: each function's first decoded instruction. Useful for
|
|
/// quickly inspecting prologue patterns without computing offsets manually.
|
|
const V_FUNCTION_FIRST_INSTRUCTION: &str = "
|
|
CREATE OR REPLACE VIEW v_function_first_instruction AS
|
|
SELECT
|
|
f.address AS function_addr,
|
|
f.name AS function_name,
|
|
i.raw AS first_raw,
|
|
i.disasm AS first_disasm,
|
|
i.ext_disasm AS first_ext_disasm
|
|
FROM functions f
|
|
JOIN instructions i ON i.address = f.address;
|
|
";
|
|
|
|
/// Every call site of a kernel/library import, one row per branch.
|
|
///
|
|
/// Driven by `instructions.import_address` — the foreign key onto `imports`
|
|
/// laid down by [`crate::imports::ImportSites`] — rather than by matching
|
|
/// `xrefs` against import-kind labels. That change fixes two things:
|
|
///
|
|
/// - a tail-call `b` into a thunk is a call to that import, but `xrefs` tags
|
|
/// it `'j'`, so the label-matching version silently dropped all of them
|
|
/// (64 sites on the reference title, against 3,217 `bl`);
|
|
/// - the library and ordinal are now available without re-parsing the name,
|
|
/// so `WHERE library = 'xboxkrnl.exe'` works directly.
|
|
///
|
|
/// `call_site` is the branch instruction; `import_addr` is the thunk head.
|
|
const V_IMPORTS_CALLED: &str = "
|
|
CREATE OR REPLACE VIEW v_imports_called AS
|
|
SELECT
|
|
i.address AS call_site,
|
|
i.function AS function_addr,
|
|
f.name AS function_name,
|
|
i.mnemonic AS instruction,
|
|
i.import_address AS import_addr,
|
|
im.library AS library,
|
|
im.name AS import_name,
|
|
im.ordinal AS ordinal
|
|
FROM instructions i
|
|
JOIN imports im ON im.address = i.import_address
|
|
LEFT JOIN functions f ON f.address = i.function
|
|
WHERE i.import_role = 'call';
|
|
";
|
|
|
|
/// Every recovered `switch` case, joined to the dispatching function and to
|
|
/// the label on the case body. One row per case *value* — several rows can
|
|
/// share a `target_address` when case values fall through to one body.
|
|
const V_SWITCH_CASES: &str = "
|
|
CREATE OR REPLACE VIEW v_switch_cases AS
|
|
SELECT
|
|
jt.bctr_pc AS dispatch_pc,
|
|
jt.function AS function_addr,
|
|
f.name AS function_name,
|
|
jt.kind AS table_kind,
|
|
jt.table_address AS table_address,
|
|
e.case_index AS case_index,
|
|
e.target_address AS target_address,
|
|
l.name AS target_label
|
|
FROM jump_tables jt
|
|
JOIN jump_table_entries e ON e.bctr_pc = jt.bctr_pc
|
|
LEFT JOIN functions f ON f.address = jt.function
|
|
LEFT JOIN labels l ON l.address = e.target_address;
|
|
";
|
|
|
|
/// The C++ inheritance graph as recovered from RTTI. Index 0 of a base-class
|
|
/// array is the class itself and is excluded, so every row is a genuine
|
|
/// `derived -> base` edge carrying the displacement triple needed to find the
|
|
/// base subobject inside an instance.
|
|
const V_CLASS_HIERARCHY: &str = "
|
|
CREATE OR REPLACE VIEW v_class_hierarchy AS
|
|
SELECT DISTINCT
|
|
dtd.demangled_name AS derived_class,
|
|
b.name AS base_class,
|
|
b.base_index AS base_index,
|
|
b.mdisp AS mdisp,
|
|
b.pdisp AS pdisp,
|
|
b.vdisp AS vdisp,
|
|
c.vtable_address AS derived_vtable
|
|
FROM rtti_base_classes b
|
|
JOIN rtti_locators c ON c.class_hierarchy = b.class_hierarchy
|
|
JOIN rtti_type_descriptors dtd ON dtd.address = c.type_descriptor
|
|
WHERE b.base_index > 0;
|
|
";
|
|
|
|
/// Virtual methods per class, resolved through the RTTI-named vtable. The
|
|
/// authoritative counterpart to querying `methods` by an `ANON_Class_*` name.
|
|
const V_CLASS_METHODS: &str = "
|
|
CREATE OR REPLACE VIEW v_class_methods AS
|
|
SELECT
|
|
td.demangled_name AS class_name,
|
|
c.subobject_offset AS subobject_offset,
|
|
v.address AS vtable_address,
|
|
m.slot AS slot,
|
|
m.function_address AS method_addr,
|
|
f.name AS method_name,
|
|
f.has_eh AS method_has_eh
|
|
FROM rtti_locators c
|
|
JOIN rtti_type_descriptors td ON td.address = c.type_descriptor
|
|
JOIN vtables v ON v.address = c.vtable_address
|
|
JOIN methods m ON m.vtable_address = v.address
|
|
LEFT JOIN functions f ON f.address = m.function_address;
|
|
";
|
|
|
|
/// Which function references which string literal. The single most useful
|
|
/// orientation query in a stripped binary: it is how you find the code behind
|
|
/// a message you can see on screen.
|
|
const V_FUNCTION_STRINGS: &str = "
|
|
CREATE OR REPLACE VIEW v_function_strings AS
|
|
SELECT
|
|
x.source_func AS function_addr,
|
|
f.name AS function_name,
|
|
x.source AS reference_pc,
|
|
x.kind AS reference_kind,
|
|
s.address AS string_addr,
|
|
s.encoding AS encoding,
|
|
s.content AS content
|
|
FROM xrefs x
|
|
JOIN strings s ON s.address = x.target
|
|
LEFT JOIN functions f ON f.address = x.source_func
|
|
WHERE x.kind IN ('ref', 'read');
|
|
";
|