diff --git a/Cargo.lock b/Cargo.lock index 3c2d920..2cd60a1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1668,6 +1668,25 @@ dependencies = [ "wayland-client", ] +[[package]] +name = "capstone" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f442ae0f2f3f1b923334b4a5386c95c69c1cfa072bafa23d6fae6d9682eb1dd4" +dependencies = [ + "capstone-sys", + "static_assertions", +] + +[[package]] +name = "capstone-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e8087cab6731295f5a2a2bd82989ba4f41d3a428aab2e7c98d8f4db38aac05" +dependencies = [ + "cc", +] + [[package]] name = "cc" version = "1.6.0" @@ -2366,6 +2385,7 @@ name = "gc-overlay-anchors" version = "0.1.0" dependencies = [ "anyhow", + "capstone", ] [[package]] diff --git a/docs/formats/core-data-regions.md b/docs/formats/core-data-regions.md index 994b3e3..d70259a 100644 --- a/docs/formats/core-data-regions.md +++ b/docs/formats/core-data-regions.md @@ -1903,25 +1903,54 @@ What it does supply is `$gp`, and that is enough to start. delta -0x6658 ($lo half 0x99a8) MIPS materialises a GP-relative address as a `%hi`/`%lo` pair, and the `$lo` half -is a 16-bit immediate sitting in bytes 1..3 of a 4-byte instruction. Searching -the whole overlay for that half, instruction-aligned, gives: +is a 16-bit immediate in the instruction stream. So the anchor search is well +defined -- and it returns **nothing**: - 0x354cc8 (overlay+0x1ad048) -- exactly ONE occurrence + $lo half 0x99a8 (low byte 0xa8, high byte 0x99) + occurrences 0, instruction-aligned, in 2,751,268 bytes -One occurrence in 2.75 MB of code is an unambiguous anchor: `0x354cc8` is the -instruction that materialises the assert string, and the function containing it -is `Camera_CollPrimTest`. That is the route in, and it needs no symbols. +**An earlier version of this file claimed exactly one hit, at `0x354cc8`, and +called it an unambiguous anchor. That was wrong, and the mistake was worth +recording.** The search compared word bytes 1..3 against the immediate. The PS2's +MIPS is little-endian, so an I-type immediate is in bits 15..0, which is word +bytes **0** and **1** -- bytes 1..3 are the opcode, `$rs` and `$rt` fields. The +match was on those, not on an address. -`tools/overlay-anchors` does this step and is dependency-free; it prints strings, -GP-relative deltas and occurrence counts. Asserting the uniqueness: +The instruction at the phantom hit is `0x24a89948`: + + addiu $t0, $a1, -0x66b8 + +whose real immediate is `0x9948`, ten bytes away from the `0x99a8` being +searched for, and whose `-0x66b8` merely looks like a plausible `$gp` offset. +With the byte order corrected, the count is zero. + +So the collision anchor is **not established**, and the assertion that it was has +been withdrawn. What survives is weaker and still useful: + + * `Camera_CollPrimTest` and `MB_CheckCollPill` are named, with + `Camera_CollPrimTest(void *prim, void *grid)`. + * `$gp = 0x1AEFF0`, and `.lit` loads at `0x1A7C80`. + * The assert string is at `overlay+0xd18`, a unique copy. + +The likely reason the search comes back empty is that the code reaching that +string does not go through `$gp` -- or reaches a copy in another section. The +decomp's `assert-message-names.tsv` gives `0x002db350` for level 0, which is +inside `.lit` but disassembles to a function epilogue (`jr $ra` / +`addiu $sp, $sp, 0x70`), so those are code locations, not string addresses, and +they are not yet traced to the assert either. + +`tools/overlay-anchors` does the anchor search and is dependency-free. Its +correctly-ordered result for the collision string is zero, and it now carries a +comment explaining the byte order, since that is precisely the part that was +wrong: cargo run -q -p gc-overlay-anchors -- \ /home/reaper/Documents/gc-extracted/level0/overlay.bin \ "Camera_CollPrimTest WARNING!" -Note the decomp's `docs/assert-message-names.tsv` holds **code** addresses, not -string addresses (level 0's `0x002db350` disassembles to instructions). They are -function-level anchors and are a second, independent way in. +Next step, if collision is still worth it: find how the assert string is actually +addressed -- a `%hi`/`%lo` split that does not straddle zero, a pointer table, or +simply the right section -- before assuming a `$gp` relative load. ### What this project did *not* give up diff --git a/tools/overlay-anchors/Cargo.toml b/tools/overlay-anchors/Cargo.toml index f3c0b56..bdafaf4 100644 --- a/tools/overlay-anchors/Cargo.toml +++ b/tools/overlay-anchors/Cargo.toml @@ -8,3 +8,4 @@ publish = false [dependencies] anyhow = "1" +capstone = { version = "0.14", features = ["arch_mips"] } diff --git a/tools/overlay-anchors/src/main.rs b/tools/overlay-anchors/src/main.rs index 29e12ab..def00f3 100644 --- a/tools/overlay-anchors/src/main.rs +++ b/tools/overlay-anchors/src/main.rs @@ -21,6 +21,13 @@ fn main() -> Result<()> { .context("usage: gc-overlay-anchors [needle] [--level N]")?; let overlay = std::fs::read(path).with_context(|| format!("read {path}"))?; + if args.get(1).map(String::as_str) == Some("--disasm") { + let ram = parse_ram(args.get(2).context("--disasm needs a RAM address")?)?; + let before: usize = args.get(3).and_then(|v| v.parse().ok()).unwrap_or(64); + let after: usize = args.get(4).and_then(|v| v.parse().ok()).unwrap_or(64); + disasm(&overlay, ram, before, after)?; + return Ok(()); + } match args.get(1) { None => { list_strings(&overlay); @@ -54,11 +61,18 @@ fn report(overlay: &[u8], needle: &[u8], off: usize) { u16::from_be_bytes(lo) ); - // Count instruction-aligned occurrences of the $lo immediate. MIPS puts it in - // bytes 1..3 of a 4-byte instruction, big-endian regardless of file order. + // Count instruction-aligned occurrences of the $lo immediate. + // + // The PS2's MIPS is little-endian, and an I-type immediate occupies bits + // 15..0 of the word, so the low byte of the immediate is word BYTE 0 and the + // high byte is word BYTE 1. Reading bytes 1..3 instead -- which an earlier + // version of this file did -- matches the opcode and $rs fields instead and + // manufactures hits out of nothing. + let lo_byte = (delta as u16) as u8; + let hi_byte = ((delta as u16) >> 8) as u8; let mut hits = BTreeSet::new(); for i in (0..overlay.len().saturating_sub(3)).step_by(4) { - if overlay[i + 1] == lo[0] && overlay[i + 2] == lo[1] { + if overlay[i] == lo_byte && overlay[i + 1] == hi_byte { hits.insert(i); } } @@ -73,6 +87,47 @@ fn report(overlay: &[u8], needle: &[u8], off: usize) { } } +fn parse_ram(s: &str) -> Result { + let t = s.trim_start_matches("0x").replace('_', ""); + u64::from_str_radix(&t, 16).with_context(|| format!("parse address {s}")) +} + +/// Disassemble `before` bytes before and `after` bytes after `ram`. +fn disasm(overlay: &[u8], ram: u64, before: usize, after: usize) -> Result<()> { + use capstone::prelude::*; + use capstone::Endian; + let off = usize::try_from( + ram.checked_sub(LIT_BASE) + .context("address below .lit base")?, + )?; + if off + after > overlay.len() { + anyhow::bail!("{ram:#x}+{after} runs past the end of the section"); + } + let cs = capstone::Capstone::new() + .mips() + .mode(capstone::arch::mips::ArchMode::Mips32) + .endian(Endian::Little) + .detail(true) + .build() + .context("capstone mips32")?; + + let start = off.saturating_sub(before); + let chunk = &overlay[start..off + after]; + let insns = cs + .disasm_all(chunk, LIT_BASE + start as u64) + .context("disassemble")?; + for i in insns.iter() { + let at = start + usize::try_from(i.address() - LIT_BASE).unwrap(); + eprintln!( + "{} {:#010x} {}", + if at == off { ">>" } else { " " }, + i.address(), + i + ); + } + Ok(()) +} + fn find_all(h: &[u8], needle: &[u8]) -> Vec { if needle.is_empty() || needle.len() > h.len() { return Vec::new();