Withdraw the collision anchor: it was a byte-order bug in my own search
Last commit claimed exactly one GP-relative occurrence of the collision assert
string's $lo half, at 0x354cc8, and called it an unambiguous anchor. That was
wrong, and building the disassembler is what proved it.
The search compared word bytes 1..3 against the 16-bit immediate. The PS2's MIPS
is little-endian, so an I-type immediate sits in bits 15..0 -- word bytes 0 and
1. Bytes 1..3 are the opcode, $rs and $rt fields. The match was on those.
The instruction at the phantom hit is 0x24a89948:
addiu $t0, $a1, -0x66b8
whose real immediate is 0x9948 -- ten bytes from the 0x99a8 being searched for.
-0x66b8 merely looks like a plausible $gp offset, which is what made it
convincing. With the byte order corrected the count is ZERO, in 2,751,268 bytes.
So the anchor is not established and the assertion is withdrawn. What survives is
still worth having: the functions are named (Camera_CollPrimTest takes (prim,
grid)), $gp = 0x1AEFF0, .lit loads at 0x1A7C80, and the assert string is a unique
copy at overlay+0xd18.
Probably the code reaching that string does not address it through $gp, or
reaches a copy in another section. The decomp's assert-message-names.tsv gives
0x002db350 for level 0 -- inside .lit, but it disassembles to a function epilogue
(jr $ra / addiu $sp, $sp, 0x70), so those are code locations, not string
addresses, and they are not traced to the assert either.
tools/overlay-anchors now does the search with the correct byte order and
carries a comment on exactly the part that was wrong, because that is the part
that will be re-derived otherwise. It also disassembles around an address:
cargo run -q -p gc-overlay-anchors -- <overlay.bin> --disasm 0x2db350 0 64
The useful general lesson, which the roadmap already says and this is a second
demonstration of: a probe that returns a small, clean, narratively satisfying
number is the most dangerous kind of wrong result. "Exactly one hit in 2.75 MB"
read as conclusive. It was an artifact of comparing the wrong two bytes, and it
survived a commit because it fit the story.
This commit is contained in:
@@ -21,6 +21,13 @@ fn main() -> Result<()> {
|
||||
.context("usage: gc-overlay-anchors <overlay.bin> [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<u64> {
|
||||
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<usize> {
|
||||
if needle.is_empty() || needle.len() > h.len() {
|
||||
return Vec::new();
|
||||
|
||||
Reference in New Issue
Block a user