fix(xexdb): clear the lint gate on the imported crates

rustfmt, then clippy -D warnings across the three new crates. Mechanical,
except three decisions that are stated rather than silently allowed:

  * lzx.rs gets file-scoped needless_range_loop/explicit_counter_loop allows.
    Index arithmetic IS the algorithm -- LZX is defined over symbol indices,
    Huffman slots and window positions, and a decompressor that is merely
    idiomatic is worth nothing if it is not bit-exact.
  * sylpheed-xexdb gets crate-scoped allows for needless_range_loop (nine
    sites index reg[r] where r is the PowerPC register number -- the index is
    the meaning), too_many_arguments and type_complexity. This code arrived
    whole from a retired repository; a refactor here would be an unreviewed
    edit dressed as a lint fix.
  * Everything else clippy asked for is FIXED, including all 14 doc-indent
    sites, the let-else, and a Prepared type alias in the binary.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-09-13 20:25:44 +02:00
parent 62b0f79590
commit c9dd2cb705
37 changed files with 5308 additions and 2198 deletions

View File

@@ -12,13 +12,17 @@
use std::io::BufRead;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let path = std::env::args().nth(1).ok_or("usage: decode_table_check <table>")?;
let path = std::env::args()
.nth(1)
.ok_or("usage: decode_table_check <table>")?;
let f = std::io::BufReader::new(std::fs::File::open(path)?);
let (mut ok, mut bad, mut invalid) = (0u32, 0u32, 0u32);
for line in f.lines() {
let line = line?;
let mut it = line.split_whitespace();
let (Some(w), Some(name)) = (it.next(), it.next()) else { continue };
let (Some(w), Some(name)) = (it.next(), it.next()) else {
continue;
};
let word = u32::from_str_radix(w.trim_start_matches("0x"), 16)?;
let d = sylpheed_ppc::decoder::decode(word, 0x8200_0000);
let got = format!("{:?}", d.opcode);

View File

@@ -18,34 +18,68 @@ impl DecodedInstr {
// Common field extractors (PPC bit numbering)
/// Primary opcode (bits 0-5)
#[inline] pub fn op(&self) -> u32 { extract_bits(self.raw, 0, 5) }
#[inline]
pub fn op(&self) -> u32 {
extract_bits(self.raw, 0, 5)
}
/// rD/rS/rT (bits 6-10) - destination/source register
#[inline] pub fn rd(&self) -> usize { extract_bits(self.raw, 6, 10) as usize }
#[inline] pub fn rs(&self) -> usize { self.rd() }
#[inline] pub fn rt(&self) -> usize { self.rd() }
#[inline]
pub fn rd(&self) -> usize {
extract_bits(self.raw, 6, 10) as usize
}
#[inline]
pub fn rs(&self) -> usize {
self.rd()
}
#[inline]
pub fn rt(&self) -> usize {
self.rd()
}
/// rA (bits 11-15)
#[inline] pub fn ra(&self) -> usize { extract_bits(self.raw, 11, 15) as usize }
#[inline]
pub fn ra(&self) -> usize {
extract_bits(self.raw, 11, 15) as usize
}
/// rB (bits 16-20)
#[inline] pub fn rb(&self) -> usize { extract_bits(self.raw, 16, 20) as usize }
#[inline]
pub fn rb(&self) -> usize {
extract_bits(self.raw, 16, 20) as usize
}
/// rC (bits 21-25) - for 4-operand instructions
#[inline] pub fn rc(&self) -> usize { extract_bits(self.raw, 21, 25) as usize }
#[inline]
pub fn rc(&self) -> usize {
extract_bits(self.raw, 21, 25) as usize
}
/// SIMM/UIMM (bits 16-31) - signed/unsigned immediate
#[inline] pub fn simm16(&self) -> i16 { (self.raw & 0xFFFF) as i16 }
#[inline] pub fn uimm16(&self) -> u16 { (self.raw & 0xFFFF) as u16 }
#[inline]
pub fn simm16(&self) -> i16 {
(self.raw & 0xFFFF) as i16
}
#[inline]
pub fn uimm16(&self) -> u16 {
(self.raw & 0xFFFF) as u16
}
/// D-form displacement (signed, bits 16-31)
#[inline] pub fn d(&self) -> i32 { self.simm16() as i32 }
#[inline]
pub fn d(&self) -> i32 {
self.simm16() as i32
}
/// DS-form displacement (signed, bits 16-29, shifted left 2)
#[inline] pub fn ds(&self) -> i32 { (self.raw & 0xFFFC) as i16 as i32 }
#[inline]
pub fn ds(&self) -> i32 {
(self.raw & 0xFFFC) as i16 as i32
}
/// LI field for branch (bits 6-29, sign-extended, shifted left 2)
#[inline] pub fn li(&self) -> i32 {
#[inline]
pub fn li(&self) -> i32 {
let li = extract_bits(self.raw, 6, 29);
// Sign-extend from 24 bits, then shift left 2
let sign_extended = ((li as i32) << 8) >> 8;
@@ -53,87 +87,154 @@ impl DecodedInstr {
}
/// BD field for conditional branch (bits 16-29, sign-extended, shifted left 2)
#[inline] pub fn bd(&self) -> i32 {
#[inline]
pub fn bd(&self) -> i32 {
let bd = extract_bits(self.raw, 16, 29);
let sign_extended = ((bd as i32) << 18) >> 18;
sign_extended << 2
}
/// BO field (bits 6-10) - branch options
#[inline] pub fn bo(&self) -> u32 { extract_bits(self.raw, 6, 10) }
#[inline]
pub fn bo(&self) -> u32 {
extract_bits(self.raw, 6, 10)
}
/// BI field (bits 11-15) - branch condition
#[inline] pub fn bi(&self) -> u32 { extract_bits(self.raw, 11, 15) }
#[inline]
pub fn bi(&self) -> u32 {
extract_bits(self.raw, 11, 15)
}
/// AA bit (bit 30) - absolute address
#[inline] pub fn aa(&self) -> bool { (self.raw >> 1) & 1 != 0 }
#[inline]
pub fn aa(&self) -> bool {
(self.raw >> 1) & 1 != 0
}
/// LK bit (bit 31) - link (update LR)
#[inline] pub fn lk(&self) -> bool { self.raw & 1 != 0 }
#[inline]
pub fn lk(&self) -> bool {
self.raw & 1 != 0
}
/// Rc bit (bit 31) - record CR0
#[inline] pub fn rc_bit(&self) -> bool { self.raw & 1 != 0 }
#[inline]
pub fn rc_bit(&self) -> bool {
self.raw & 1 != 0
}
/// Rc for VC-form vector compare instructions — PPC bit 21 = host bit 10.
#[inline] pub fn vc_rc_bit(&self) -> bool { (self.raw >> 10) & 1 != 0 }
#[inline]
pub fn vc_rc_bit(&self) -> bool {
(self.raw >> 10) & 1 != 0
}
/// Rc for VX128_R-form vector compare instructions — PPC bit 27 = host bit 4.
/// VX128_R Rc bit — PPC bit 25 (host bit 6) per canary's FormatVX128_R
/// bitfield layout. PPCBUG-700.
#[inline] pub fn vx128r_rc_bit(&self) -> bool { (self.raw >> 6) & 1 != 0 }
#[inline]
pub fn vx128r_rc_bit(&self) -> bool {
(self.raw >> 6) & 1 != 0
}
/// IMM field for VX128_4-form instructions (vrlimi128) — 5-bit blend mask at PPC bits 11-15.
#[inline] pub fn vx128_4_imm(&self) -> u32 { extract_bits(self.raw, 11, 15) }
#[inline]
pub fn vx128_4_imm(&self) -> u32 {
extract_bits(self.raw, 11, 15)
}
/// z field for VX128_4-form instructions (vrlimi128) — 2-bit rotation index at PPC bits 24-25.
#[inline] pub fn vx128_4_z(&self) -> u32 { extract_bits(self.raw, 24, 25) }
#[inline]
pub fn vx128_4_z(&self) -> u32 {
extract_bits(self.raw, 24, 25)
}
/// OE bit (bit 21) - overflow enable
#[inline] pub fn oe(&self) -> bool { extract_bits(self.raw, 21, 21) != 0 }
#[inline]
pub fn oe(&self) -> bool {
extract_bits(self.raw, 21, 21) != 0
}
/// TO field (bits 6-10) for tw/twi/td/tdi trap instructions.
#[inline] pub fn to(&self) -> u32 { extract_bits(self.raw, 6, 10) }
#[inline]
pub fn to(&self) -> u32 {
extract_bits(self.raw, 6, 10)
}
/// MB, ME fields for rotate instructions
#[inline] pub fn mb(&self) -> u32 { extract_bits(self.raw, 21, 25) }
#[inline] pub fn me(&self) -> u32 { extract_bits(self.raw, 26, 30) }
#[inline]
pub fn mb(&self) -> u32 {
extract_bits(self.raw, 21, 25)
}
#[inline]
pub fn me(&self) -> u32 {
extract_bits(self.raw, 26, 30)
}
/// SH field (bits 16-20) for shift instructions
#[inline] pub fn sh(&self) -> u32 { extract_bits(self.raw, 16, 20) }
#[inline]
pub fn sh(&self) -> u32 {
extract_bits(self.raw, 16, 20)
}
/// SH field for 64-bit shifts (bits 16-20 + bit 30)
#[inline] pub fn sh64(&self) -> u32 {
#[inline]
pub fn sh64(&self) -> u32 {
(extract_bits(self.raw, 30, 30) << 5) | extract_bits(self.raw, 16, 20)
}
/// MB/ME field for MD-form and MDS-form instructions (6-bit field, split encoding).
/// MB[4:0] at PPC bits 21-25; MB[5] at PPC bit 26.
#[inline] pub fn mb_md(&self) -> u32 {
#[inline]
pub fn mb_md(&self) -> u32 {
extract_bits(self.raw, 21, 25) | (extract_bits(self.raw, 26, 26) << 5)
}
/// SPR field (bits 11-20, swapped halves)
#[inline] pub fn spr(&self) -> u32 {
#[inline]
pub fn spr(&self) -> u32 {
let spr_raw = extract_bits(self.raw, 11, 20);
((spr_raw & 0x1F) << 5) | ((spr_raw >> 5) & 0x1F)
}
/// CRM field (bits 12-19) for mtcrf
#[inline] pub fn crm(&self) -> u32 { extract_bits(self.raw, 12, 19) }
#[inline]
pub fn crm(&self) -> u32 {
extract_bits(self.raw, 12, 19)
}
/// crfD (bits 6-8) - condition register field destination
#[inline] pub fn crfd(&self) -> usize { extract_bits(self.raw, 6, 8) as usize }
#[inline]
pub fn crfd(&self) -> usize {
extract_bits(self.raw, 6, 8) as usize
}
/// crfS (bits 11-13)
#[inline] pub fn crfs(&self) -> usize { extract_bits(self.raw, 11, 13) as usize }
#[inline]
pub fn crfs(&self) -> usize {
extract_bits(self.raw, 11, 13) as usize
}
/// L bit (bit 10) - 64-bit compare
#[inline] pub fn l(&self) -> bool { extract_bits(self.raw, 10, 10) != 0 }
#[inline]
pub fn l(&self) -> bool {
extract_bits(self.raw, 10, 10) != 0
}
/// crbD (bits 6-10)
#[inline] pub fn crbd(&self) -> u32 { extract_bits(self.raw, 6, 10) }
#[inline]
pub fn crbd(&self) -> u32 {
extract_bits(self.raw, 6, 10)
}
/// crbA (bits 11-15)
#[inline] pub fn crba(&self) -> u32 { extract_bits(self.raw, 11, 15) }
#[inline]
pub fn crba(&self) -> u32 {
extract_bits(self.raw, 11, 15)
}
/// crbB (bits 16-20)
#[inline] pub fn crbb(&self) -> u32 { extract_bits(self.raw, 16, 20) }
#[inline]
pub fn crbb(&self) -> u32 {
extract_bits(self.raw, 16, 20)
}
// VMX128 field extractors — bit positions match canary's
// FormatVX128/VX128_2/VX128_4/VX128_5/VX128_R bitfield layout
@@ -141,7 +242,8 @@ impl DecodedInstr {
/// VA128 = VA128l(5) | VA128h(1) << 5 | VA128H(1) << 6.
/// Canonical 7-bit register selector: PPC 11-15 (low), PPC 26 (mid), PPC 21 (high).
#[inline] pub fn va128(&self) -> usize {
#[inline]
pub fn va128(&self) -> usize {
(extract_bits(self.raw, 11, 15)
| (extract_bits(self.raw, 26, 26) << 5)
| (extract_bits(self.raw, 21, 21) << 6)) as usize
@@ -149,35 +251,48 @@ impl DecodedInstr {
/// VB128 = VB128l(5) | VB128h(2) << 5. Canary's VB128h is a 2-bit
/// contiguous field at PPC 30-31 (host bits 0-1).
#[inline] pub fn vb128(&self) -> usize {
(extract_bits(self.raw, 16, 20)
| (extract_bits(self.raw, 30, 31) << 5)) as usize
#[inline]
pub fn vb128(&self) -> usize {
(extract_bits(self.raw, 16, 20) | (extract_bits(self.raw, 30, 31) << 5)) as usize
}
/// VD128 = VD128l(5) | VD128h(2) << 5. Canary's VD128h is a 2-bit
/// contiguous field at PPC 28-29 (host bits 2-3).
#[inline] pub fn vd128(&self) -> usize {
(extract_bits(self.raw, 6, 10)
| (extract_bits(self.raw, 28, 29) << 5)) as usize
#[inline]
pub fn vd128(&self) -> usize {
(extract_bits(self.raw, 6, 10) | (extract_bits(self.raw, 28, 29) << 5)) as usize
}
/// VS128 - same encoding as VD128
#[inline] pub fn vs128(&self) -> usize { self.vd128() }
#[inline]
pub fn vs128(&self) -> usize {
self.vd128()
}
/// VC register for VX128_2-form instructions (vperm128) — 3-bit at PPC bits 23-25.
#[inline] pub fn vc128_2(&self) -> usize { extract_bits(self.raw, 23, 25) as usize }
#[inline]
pub fn vc128_2(&self) -> usize {
extract_bits(self.raw, 23, 25) as usize
}
/// NB field (bits 16-20) for lswi/stswi
#[inline] pub fn nb(&self) -> u32 { extract_bits(self.raw, 16, 20) }
#[inline]
pub fn nb(&self) -> u32 {
extract_bits(self.raw, 16, 20)
}
/// PERM field for VX128_P-form instructions (vpermwi128) — 8-bit split encoding.
/// PERMl (5 bits) at PPC bits 11-15; PERMh (3 bits) at PPC bits 23-25.
#[inline] pub fn vx128_p_perm(&self) -> u32 {
#[inline]
pub fn vx128_p_perm(&self) -> u32 {
extract_bits(self.raw, 11, 15) | (extract_bits(self.raw, 23, 25) << 5)
}
/// SH field for VX128_5-form instructions (vsldoi128) — 4-bit shift at PPC bits 22-25.
#[inline] pub fn vx128_5_sh(&self) -> u32 { extract_bits(self.raw, 22, 25) }
#[inline]
pub fn vx128_5_sh(&self) -> u32 {
extract_bits(self.raw, 22, 25)
}
}
/// Extract the 5-bit `UIMM` (`VX128_3`) / `IMM` (`VX128_4`) field. Canary
@@ -1057,8 +1172,15 @@ mod tests {
/// vd_hi is 2 bits (PPC 28-29). Same shape for vb128 (vb_lo at PPC 16-20,
/// vb_hi 2 bits at PPC 30-31). va128 = va_lo | (va_h26<<5) | (va_h21<<6)
/// per canary's 7-bit VA selector.
fn vmx128_test_word(vd_lo: u32, vd_hi: u32, va_lo: u32, va_h26: u32, va_h21: u32,
vb_lo: u32, vb_hi: u32) -> u32 {
fn vmx128_test_word(
vd_lo: u32,
vd_hi: u32,
va_lo: u32,
va_h26: u32,
va_h21: u32,
vb_lo: u32,
vb_hi: u32,
) -> u32 {
// PPC bit i -> host bit (31-i).
(vd_lo << (31 - 10)) // VD128l: PPC 6-10 = host 21-25
| (vd_hi << (31 - 29)) // VD128h: PPC 28-29 = host 2-3 (LSB at host 2)
@@ -1066,15 +1188,19 @@ mod tests {
| (va_h26 << (31 - 26)) // VA128h: PPC 26 = host 5
| (va_h21 << (31 - 21)) // VA128H: PPC 21 = host 10
| (vb_lo << (31 - 20)) // VB128l: PPC 16-20 = host 11-15
| (vb_hi << (31 - 31)) // VB128h: PPC 30-31 = host 0-1 (LSB at host 0)
| vb_hi // VB128h: PPC 30-31 = host 0-1 (LSB at host 0)
}
#[test]
fn vmx128_vd128_low_5_bits_only() {
// vd_lo = 0..31, vd_hi = 0 → vd128 = vd_lo
for r in 0..32u32 {
let raw = (r as u32) << (31 - 10);
let d = DecodedInstr { opcode: PpcOpcode::Invalid, raw, addr: 0 };
let raw = r << (31 - 10);
let d = DecodedInstr {
opcode: PpcOpcode::Invalid,
raw,
addr: 0,
};
assert_eq!(d.vd128(), r as usize, "vd_lo={r}");
}
}
@@ -1082,26 +1208,36 @@ mod tests {
#[test]
fn vmx128_vd128_high_low_bit_adds_32() {
// vd_lo = 0, VD128h = 0b01 (LSB only at host bit 2 = PPC 29) → vd128 = 32
let raw = (1u32 << (31 - 29));
let d = DecodedInstr { opcode: PpcOpcode::Invalid, raw, addr: 0 };
let raw = 1u32 << (31 - 29);
let d = DecodedInstr {
opcode: PpcOpcode::Invalid,
raw,
addr: 0,
};
assert_eq!(d.vd128(), 32);
}
#[test]
fn vmx128_vd128_high_high_bit_adds_64() {
// vd_lo = 0, VD128h = 0b10 (MSB only at host bit 3 = PPC 28) → vd128 = 64
let raw = (1u32 << (31 - 28));
let d = DecodedInstr { opcode: PpcOpcode::Invalid, raw, addr: 0 };
let raw = 1u32 << (31 - 28);
let d = DecodedInstr {
opcode: PpcOpcode::Invalid,
raw,
addr: 0,
};
assert_eq!(d.vd128(), 64);
}
#[test]
fn vmx128_vd128_full_127() {
// vd_lo = 31, VD128h = 0b11 → vd128 = 127
let raw = (31u32 << (31 - 10))
| (1u32 << (31 - 28))
| (1u32 << (31 - 29));
let d = DecodedInstr { opcode: PpcOpcode::Invalid, raw, addr: 0 };
let raw = (31u32 << (31 - 10)) | (1u32 << (31 - 28)) | (1u32 << (31 - 29));
let d = DecodedInstr {
opcode: PpcOpcode::Invalid,
raw,
addr: 0,
};
assert_eq!(d.vd128(), 127);
}
@@ -1109,11 +1245,19 @@ mod tests {
fn vmx128_va128_canary_layout() {
// va_lo = 7 at PPC 11-15, VA128h = 1 at PPC 26 → va128 = 7 | 32 = 39
let raw = (7u32 << (31 - 15)) | (1u32 << (31 - 26));
let d = DecodedInstr { opcode: PpcOpcode::Invalid, raw, addr: 0 };
let d = DecodedInstr {
opcode: PpcOpcode::Invalid,
raw,
addr: 0,
};
assert_eq!(d.va128(), 39);
// VA128H = 1 at PPC 21 → va128 += 64 = 103
let raw = raw | (1u32 << (31 - 21));
let d = DecodedInstr { opcode: PpcOpcode::Invalid, raw, addr: 0 };
let d = DecodedInstr {
opcode: PpcOpcode::Invalid,
raw,
addr: 0,
};
assert_eq!(d.va128(), 7 | 32 | 64);
}
@@ -1122,10 +1266,18 @@ mod tests {
// vb_lo = 5 at PPC 16-20. VB128h = 0b01 (LSB at PPC 31 = host 0) → +32.
// VB128h = 0b11 → +96.
let raw = (5u32 << (31 - 20)) | (1u32 << (31 - 31));
let d = DecodedInstr { opcode: PpcOpcode::Invalid, raw, addr: 0 };
let d = DecodedInstr {
opcode: PpcOpcode::Invalid,
raw,
addr: 0,
};
assert_eq!(d.vb128(), 5 | 32);
let raw = raw | (1u32 << (31 - 30));
let d = DecodedInstr { opcode: PpcOpcode::Invalid, raw, addr: 0 };
let d = DecodedInstr {
opcode: PpcOpcode::Invalid,
raw,
addr: 0,
};
assert_eq!(d.vb128(), 5 | 32 | 64);
}
@@ -1135,9 +1287,12 @@ mod tests {
for r in [0u32, 31, 32, 64, 96, 127] {
let lo = r & 0x1F;
let hi = (r >> 5) & 0x3;
let raw = (lo << (31 - 10))
| (hi << (31 - 29));
let d = DecodedInstr { opcode: PpcOpcode::Invalid, raw, addr: 0 };
let raw = (lo << (31 - 10)) | (hi << (31 - 29));
let d = DecodedInstr {
opcode: PpcOpcode::Invalid,
raw,
addr: 0,
};
assert_eq!(d.vd128(), r as usize, "vd128 mismatch for r={r}");
assert_eq!(d.vs128(), r as usize, "vs128 mismatch for r={r}");
assert_eq!(d.vd128(), d.vs128());
@@ -1150,7 +1305,11 @@ mod tests {
// Keep the helper validated against the real accessor.
// vd_lo=5, vd_hi=0b11 → vd128 = 5 | 96 = 101
let raw = vmx128_test_word(5, 3, 0, 0, 0, 0, 0);
let d = DecodedInstr { opcode: PpcOpcode::Invalid, raw, addr: 0 };
let d = DecodedInstr {
opcode: PpcOpcode::Invalid,
raw,
addr: 0,
};
assert_eq!(d.vd128(), 5 | 32 | 64);
}
@@ -1160,21 +1319,37 @@ mod tests {
// Host bit 9 = 1 (PPC bit 22), host bits 6-8 = 0.
// So raw bit 9 set = raw |= 1 << 9 = 0x200
let raw = 0x200u32; // host bit 9 set only
let d = DecodedInstr { opcode: PpcOpcode::Invalid, raw, addr: 0 };
let d = DecodedInstr {
opcode: PpcOpcode::Invalid,
raw,
addr: 0,
};
assert_eq!(d.vx128_5_sh(), 8, "SH=8: MSB at PPC bit 22");
// SH=1 (binary 0001): host bit 6 set = raw |= 1 << 6 = 0x40
let raw = 0x40u32;
let d = DecodedInstr { opcode: PpcOpcode::Invalid, raw, addr: 0 };
let d = DecodedInstr {
opcode: PpcOpcode::Invalid,
raw,
addr: 0,
};
assert_eq!(d.vx128_5_sh(), 1, "SH=1: LSB at PPC bit 25");
// SH=15 (binary 1111): host bits 6-9 all set = raw |= 0xF << 6 = 0x3C0
let raw = 0x3C0u32;
let d = DecodedInstr { opcode: PpcOpcode::Invalid, raw, addr: 0 };
let d = DecodedInstr {
opcode: PpcOpcode::Invalid,
raw,
addr: 0,
};
assert_eq!(d.vx128_5_sh(), 15, "SH=15: all 4 bits set");
// SH=0: raw=0
let d = DecodedInstr { opcode: PpcOpcode::Invalid, raw: 0, addr: 0 };
let d = DecodedInstr {
opcode: PpcOpcode::Invalid,
raw: 0,
addr: 0,
};
assert_eq!(d.vx128_5_sh(), 0, "SH=0");
}
@@ -1182,23 +1357,39 @@ mod tests {
fn vx128_4_accessors_correct_bit_positions() {
// z=3 (binary 11) at PPC bits 24-25 = host bits 6-7
let raw = 0b11u32 << 6;
let d = DecodedInstr { opcode: PpcOpcode::Invalid, raw, addr: 0 };
let d = DecodedInstr {
opcode: PpcOpcode::Invalid,
raw,
addr: 0,
};
assert_eq!(d.vx128_4_z(), 3, "z=3 from host bits 6-7");
// IMM=0x15 (binary 10101) at PPC bits 11-15 = host bits 16-20
let raw2 = 0x15u32 << 16;
let d2 = DecodedInstr { opcode: PpcOpcode::Invalid, raw: raw2, addr: 0 };
let d2 = DecodedInstr {
opcode: PpcOpcode::Invalid,
raw: raw2,
addr: 0,
};
assert_eq!(d2.vx128_4_imm(), 0x15, "IMM=0x15 from host bits 16-20");
// Combined: z=1, IMM=0xA — fields must not bleed into each other
let raw3 = (0x1u32 << 6) | (0xAu32 << 16);
let d3 = DecodedInstr { opcode: PpcOpcode::Invalid, raw: raw3, addr: 0 };
let d3 = DecodedInstr {
opcode: PpcOpcode::Invalid,
raw: raw3,
addr: 0,
};
assert_eq!(d3.vx128_4_z(), 1, "z=1 combined");
assert_eq!(d3.vx128_4_imm(), 0xA, "IMM=0xA combined");
// z=2, IMM=0xF — max 4-bit blend mask, exercises the full lower nibble
let raw4 = (0b10u32 << 6) | (0xFu32 << 16);
let d4 = DecodedInstr { opcode: PpcOpcode::Invalid, raw: raw4, addr: 0 };
let d4 = DecodedInstr {
opcode: PpcOpcode::Invalid,
raw: raw4,
addr: 0,
};
assert_eq!(d4.vx128_4_z(), 2, "z=2 from binary 10");
assert_eq!(d4.vx128_4_imm(), 0xF, "IMM=0xF all-ones nibble");
}
@@ -1208,16 +1399,32 @@ mod tests {
// VC=5 (binary 101) at PPC bits 23-25 = host bits 6-8
// extract_bits(raw, 23, 25) = (raw >> (31-25)) & 0x7 = (raw >> 6) & 0x7
let raw = 5u32 << 6; // host bits 6-8 = 5
let d = DecodedInstr { opcode: PpcOpcode::Invalid, raw, addr: 0 };
let d = DecodedInstr {
opcode: PpcOpcode::Invalid,
raw,
addr: 0,
};
assert_eq!(d.vc128_2(), 5);
let d0 = DecodedInstr { opcode: PpcOpcode::Invalid, raw: 0, addr: 0 };
let d0 = DecodedInstr {
opcode: PpcOpcode::Invalid,
raw: 0,
addr: 0,
};
assert_eq!(d0.vc128_2(), 0);
let d7 = DecodedInstr { opcode: PpcOpcode::Invalid, raw: 7u32 << 6, addr: 0 };
let d7 = DecodedInstr {
opcode: PpcOpcode::Invalid,
raw: 7u32 << 6,
addr: 0,
};
assert_eq!(d7.vc128_2(), 7);
let d1 = DecodedInstr { opcode: PpcOpcode::Invalid, raw: 1u32 << 6, addr: 0 };
let d1 = DecodedInstr {
opcode: PpcOpcode::Invalid,
raw: 1u32 << 6,
addr: 0,
};
assert_eq!(d1.vc128_2(), 1);
}
@@ -1225,21 +1432,37 @@ mod tests {
fn vx128_p_perm_assembles_correctly() {
// PERMl=0x1F (all 5 bits set) at host bits 16-20: raw = 0x1F << 16
let raw = 0x1Fu32 << 16;
let d = DecodedInstr { opcode: PpcOpcode::Invalid, raw, addr: 0 };
let d = DecodedInstr {
opcode: PpcOpcode::Invalid,
raw,
addr: 0,
};
assert_eq!(d.vx128_p_perm(), 0x1F, "PERMl only");
// PERMh=0x7 (all 3 bits set) at host bits 6-8: raw = 0x7 << 6 = 0x1C0
let raw = 0x7u32 << 6;
let d = DecodedInstr { opcode: PpcOpcode::Invalid, raw, addr: 0 };
let d = DecodedInstr {
opcode: PpcOpcode::Invalid,
raw,
addr: 0,
};
assert_eq!(d.vx128_p_perm(), 0x7 << 5, "PERMh only: bits 5-7");
// PERMl=0xA, PERMh=0x5: raw = (0xA << 16) | (0x5 << 6)
let raw = (0xAu32 << 16) | (0x5u32 << 6);
let d = DecodedInstr { opcode: PpcOpcode::Invalid, raw, addr: 0 };
let d = DecodedInstr {
opcode: PpcOpcode::Invalid,
raw,
addr: 0,
};
assert_eq!(d.vx128_p_perm(), 0xA | (0x5 << 5));
// PERMl and PERMh bits must not bleed into each other
let d = DecodedInstr { opcode: PpcOpcode::Invalid, raw: 0, addr: 0 };
let d = DecodedInstr {
opcode: PpcOpcode::Invalid,
raw: 0,
addr: 0,
};
assert_eq!(d.vx128_p_perm(), 0);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -5,131 +5,498 @@
#[allow(non_camel_case_types)]
pub enum PpcOpcode {
// ALU
addcx, addex, addi, addic, addicx, addis, addmex, addx, addzex,
andcx, andisx, andix, andx,
addcx,
addex,
addi,
addic,
addicx,
addis,
addmex,
addx,
addzex,
andcx,
andisx,
andix,
andx,
// Branch
bcctrx, bclrx, bcx, bx,
bcctrx,
bclrx,
bcx,
bx,
// Compare
cmp, cmpi, cmpl, cmpli,
cmp,
cmpi,
cmpl,
cmpli,
// Count leading zeros
cntlzdx, cntlzwx,
cntlzdx,
cntlzwx,
// Condition register
crand, crandc, creqv, crnand, crnor, cror, crorc, crxor,
crand,
crandc,
creqv,
crnand,
crnor,
cror,
crorc,
crxor,
// Data cache
dcbf, dcbi, dcbst, dcbt, dcbtst, dcbz, dcbz128,
dcbf,
dcbi,
dcbst,
dcbt,
dcbtst,
dcbz,
dcbz128,
// Division
divdux, divdx, divwux, divwx,
divdux,
divdx,
divwux,
divwx,
// Sync/barrier
eieio,
// Logical
eqvx, extsbx, extshx, extswx,
eqvx,
extsbx,
extshx,
extswx,
// FPU
fabsx, faddsx, faddx, fcfidx, fcmpo, fcmpu, fctidx, fctidzx, fctiwx, fctiwzx,
fdivsx, fdivx, fmaddsx, fmaddx, fmrx, fmsubsx, fmsubx, fmulsx, fmulx,
fnabsx, fnegx, fnmaddsx, fnmaddx, fnmsubsx, fnmsubx, fresx, frspx, frsqrtex,
fselx, fsqrtsx, fsqrtx, fsubsx, fsubx,
fabsx,
faddsx,
faddx,
fcfidx,
fcmpo,
fcmpu,
fctidx,
fctidzx,
fctiwx,
fctiwzx,
fdivsx,
fdivx,
fmaddsx,
fmaddx,
fmrx,
fmsubsx,
fmsubx,
fmulsx,
fmulx,
fnabsx,
fnegx,
fnmaddsx,
fnmaddx,
fnmsubsx,
fnmsubx,
fresx,
frspx,
frsqrtex,
fselx,
fsqrtsx,
fsqrtx,
fsubsx,
fsubx,
// Instruction cache
icbi, isync,
icbi,
isync,
// Load byte
lbz, lbzu, lbzux, lbzx,
lbz,
lbzu,
lbzux,
lbzx,
// Load doubleword
ld, ldarx, ldbrx, ldu, ldux, ldx,
ld,
ldarx,
ldbrx,
ldu,
ldux,
ldx,
// Load float
lfd, lfdu, lfdux, lfdx, lfs, lfsu, lfsux, lfsx,
lfd,
lfdu,
lfdux,
lfdx,
lfs,
lfsu,
lfsux,
lfsx,
// Load halfword
lha, lhau, lhaux, lhax, lhbrx, lhz, lhzu, lhzux, lhzx,
lha,
lhau,
lhaux,
lhax,
lhbrx,
lhz,
lhzu,
lhzux,
lhzx,
// Load multiple/string
lmw, lswi, lswx,
lmw,
lswi,
lswx,
// Load vector
lvebx, lvehx, lvewx, lvewx128, lvlx, lvlx128, lvlxl, lvlxl128,
lvrx, lvrx128, lvrxl, lvrxl128,
lvsl, lvsl128, lvsr, lvsr128,
lvx, lvx128, lvxl, lvxl128,
lvebx,
lvehx,
lvewx,
lvewx128,
lvlx,
lvlx128,
lvlxl,
lvlxl128,
lvrx,
lvrx128,
lvrxl,
lvrxl128,
lvsl,
lvsl128,
lvsr,
lvsr128,
lvx,
lvx128,
lvxl,
lvxl128,
// Load word
lwa, lwarx, lwaux, lwax, lwbrx, lwz, lwzu, lwzux, lwzx,
lwa,
lwarx,
lwaux,
lwax,
lwbrx,
lwz,
lwzu,
lwzux,
lwzx,
// Move CR
mcrf, mcrfs, mcrxr,
mcrf,
mcrfs,
mcrxr,
// Move from special
mfcr, mffsx, mfmsr, mfspr, mftb, mfvscr,
mfcr,
mffsx,
mfmsr,
mfspr,
mftb,
mfvscr,
// Move to special
mtcrf, mtfsb0x, mtfsb1x, mtfsfix, mtfsfx, mtmsr, mtmsrd, mtspr, mtvscr,
mtcrf,
mtfsb0x,
mtfsb1x,
mtfsfix,
mtfsfx,
mtmsr,
mtmsrd,
mtspr,
mtvscr,
// Multiply
mulhdux, mulhdx, mulhwux, mulhwx, mulldx, mulli, mullwx,
mulhdux,
mulhdx,
mulhwux,
mulhwx,
mulldx,
mulli,
mullwx,
// Logical
nandx, negx, norx, orcx, ori, oris, orx,
nandx,
negx,
norx,
orcx,
ori,
oris,
orx,
// Rotate
rldclx, rldcrx, rldiclx, rldicrx, rldicx, rldimix, rlwimix, rlwinmx, rlwnmx,
rldclx,
rldcrx,
rldiclx,
rldicrx,
rldicx,
rldimix,
rlwimix,
rlwinmx,
rlwnmx,
// System call
sc,
// Shift
sldx, slwx, sradix, sradx, srawix, srawx, srdx, srwx,
sldx,
slwx,
sradix,
sradx,
srawix,
srawx,
srdx,
srwx,
// Store byte
stb, stbu, stbux, stbx,
stb,
stbu,
stbux,
stbx,
// Store doubleword
std, stdbrx, stdcx, stdu, stdux, stdx,
std,
stdbrx,
stdcx,
stdu,
stdux,
stdx,
// Store float
stfd, stfdu, stfdux, stfdx, stfiwx, stfs, stfsu, stfsux, stfsx,
stfd,
stfdu,
stfdux,
stfdx,
stfiwx,
stfs,
stfsu,
stfsux,
stfsx,
// Store halfword
sth, sthbrx, sthu, sthux, sthx,
sth,
sthbrx,
sthu,
sthux,
sthx,
// Store multiple/string
stmw, stswi, stswx,
stmw,
stswi,
stswx,
// Store vector
stvebx, stvehx, stvewx, stvewx128, stvlx, stvlx128, stvlxl, stvlxl128,
stvrx, stvrx128, stvrxl, stvrxl128,
stvx, stvx128, stvxl, stvxl128,
stvebx,
stvehx,
stvewx,
stvewx128,
stvlx,
stvlx128,
stvlxl,
stvlxl128,
stvrx,
stvrx128,
stvrxl,
stvrxl128,
stvx,
stvx128,
stvxl,
stvxl128,
// Store word
stw, stwbrx, stwcx, stwu, stwux, stwx,
stw,
stwbrx,
stwcx,
stwu,
stwux,
stwx,
// Subtract
subfcx, subfex, subficx, subfmex, subfx, subfzex,
subfcx,
subfex,
subficx,
subfmex,
subfx,
subfzex,
// Sync
sync,
// Trap
td, tdi, tw, twi,
td,
tdi,
tw,
twi,
// VMX integer
vaddcuw, vaddfp, vaddfp128, vaddsbs, vaddshs, vaddsws,
vaddubm, vaddubs, vadduhm, vadduhs, vadduwm, vadduws,
vand, vand128, vandc, vandc128,
vavgsb, vavgsh, vavgsw, vavgub, vavguh, vavguw,
vcfpsxws128, vcfpuxws128, vcfsx, vcfux,
vcmpbfp, vcmpbfp128, vcmpeqfp, vcmpeqfp128,
vcmpequb, vcmpequh, vcmpequw, vcmpequw128,
vcmpgefp, vcmpgefp128, vcmpgtfp, vcmpgtfp128,
vcmpgtsb, vcmpgtsh, vcmpgtsw, vcmpgtub, vcmpgtuh, vcmpgtuw,
vcsxwfp128, vctsxs, vctuxs, vcuxwfp128,
vexptefp, vexptefp128, vlogefp, vlogefp128,
vmaddcfp128, vmaddfp, vmaddfp128,
vmaxfp, vmaxfp128, vmaxsb, vmaxsh, vmaxsw, vmaxub, vmaxuh, vmaxuw,
vmhaddshs, vmhraddshs,
vminfp, vminfp128, vminsb, vminsh, vminsw, vminub, vminuh, vminuw,
vaddcuw,
vaddfp,
vaddfp128,
vaddsbs,
vaddshs,
vaddsws,
vaddubm,
vaddubs,
vadduhm,
vadduhs,
vadduwm,
vadduws,
vand,
vand128,
vandc,
vandc128,
vavgsb,
vavgsh,
vavgsw,
vavgub,
vavguh,
vavguw,
vcfpsxws128,
vcfpuxws128,
vcfsx,
vcfux,
vcmpbfp,
vcmpbfp128,
vcmpeqfp,
vcmpeqfp128,
vcmpequb,
vcmpequh,
vcmpequw,
vcmpequw128,
vcmpgefp,
vcmpgefp128,
vcmpgtfp,
vcmpgtfp128,
vcmpgtsb,
vcmpgtsh,
vcmpgtsw,
vcmpgtub,
vcmpgtuh,
vcmpgtuw,
vcsxwfp128,
vctsxs,
vctuxs,
vcuxwfp128,
vexptefp,
vexptefp128,
vlogefp,
vlogefp128,
vmaddcfp128,
vmaddfp,
vmaddfp128,
vmaxfp,
vmaxfp128,
vmaxsb,
vmaxsh,
vmaxsw,
vmaxub,
vmaxuh,
vmaxuw,
vmhaddshs,
vmhraddshs,
vminfp,
vminfp128,
vminsb,
vminsh,
vminsw,
vminub,
vminuh,
vminuw,
vmladduhm,
vmrghb, vmrghh, vmrghw, vmrghw128, vmrglb, vmrglh, vmrglw, vmrglw128,
vmsum3fp128, vmsum4fp128,
vmsummbm, vmsumshm, vmsumshs, vmsumubm, vmsumuhm, vmsumuhs,
vmulesb, vmulesh, vmuleub, vmuleuh, vmulfp128,
vmulosb, vmulosh, vmuloub, vmulouh,
vnmsubfp, vnmsubfp128, vnor, vnor128,
vor, vor128,
vperm, vperm128, vpermwi128, vpkd3d128,
vpkpx, vpkshss, vpkshss128, vpkshus, vpkshus128,
vpkswss, vpkswss128, vpkswus, vpkswus128,
vpkuhum, vpkuhum128, vpkuhus, vpkuhus128,
vpkuwum, vpkuwum128, vpkuwus, vpkuwus128,
vrefp, vrefp128,
vrfim, vrfim128, vrfin, vrfin128, vrfip, vrfip128, vrfiz, vrfiz128,
vrlb, vrlh, vrlimi128, vrlw, vrlw128,
vrsqrtefp, vrsqrtefp128,
vsel, vsel128,
vsl, vslb, vsldoi, vsldoi128, vslh, vslo, vslo128, vslw, vslw128,
vspltb, vsplth, vspltisb, vspltish, vspltisw, vspltisw128, vspltw, vspltw128,
vsr, vsrab, vsrah, vsraw, vsraw128, vsrb, vsrh, vsro, vsro128, vsrw, vsrw128,
vsubcuw, vsubfp, vsubfp128, vsubsbs, vsubshs, vsubsws,
vsububm, vsububs, vsubuhm, vsubuhs, vsubuwm, vsubuws,
vsum2sws, vsum4sbs, vsum4shs, vsum4ubs, vsumsws,
vupkd3d128, vupkhpx, vupkhsb, vupkhsb128, vupkhsh,
vupklpx, vupklsb, vupklsb128, vupklsh,
vxor, vxor128,
vmrghb,
vmrghh,
vmrghw,
vmrghw128,
vmrglb,
vmrglh,
vmrglw,
vmrglw128,
vmsum3fp128,
vmsum4fp128,
vmsummbm,
vmsumshm,
vmsumshs,
vmsumubm,
vmsumuhm,
vmsumuhs,
vmulesb,
vmulesh,
vmuleub,
vmuleuh,
vmulfp128,
vmulosb,
vmulosh,
vmuloub,
vmulouh,
vnmsubfp,
vnmsubfp128,
vnor,
vnor128,
vor,
vor128,
vperm,
vperm128,
vpermwi128,
vpkd3d128,
vpkpx,
vpkshss,
vpkshss128,
vpkshus,
vpkshus128,
vpkswss,
vpkswss128,
vpkswus,
vpkswus128,
vpkuhum,
vpkuhum128,
vpkuhus,
vpkuhus128,
vpkuwum,
vpkuwum128,
vpkuwus,
vpkuwus128,
vrefp,
vrefp128,
vrfim,
vrfim128,
vrfin,
vrfin128,
vrfip,
vrfip128,
vrfiz,
vrfiz128,
vrlb,
vrlh,
vrlimi128,
vrlw,
vrlw128,
vrsqrtefp,
vrsqrtefp128,
vsel,
vsel128,
vsl,
vslb,
vsldoi,
vsldoi128,
vslh,
vslo,
vslo128,
vslw,
vslw128,
vspltb,
vsplth,
vspltisb,
vspltish,
vspltisw,
vspltisw128,
vspltw,
vspltw128,
vsr,
vsrab,
vsrah,
vsraw,
vsraw128,
vsrb,
vsrh,
vsro,
vsro128,
vsrw,
vsrw128,
vsubcuw,
vsubfp,
vsubfp128,
vsubsbs,
vsubshs,
vsubsws,
vsububm,
vsububs,
vsubuhm,
vsubuhs,
vsubuwm,
vsubuws,
vsum2sws,
vsum4sbs,
vsum4shs,
vsum4ubs,
vsumsws,
vupkd3d128,
vupkhpx,
vupkhsb,
vupkhsb128,
vupkhsh,
vupklpx,
vupklsb,
vupklsb128,
vupklsh,
vxor,
vxor128,
// XOR immediate
xori, xoris, xorx,
xori,
xoris,
xorx,
// Invalid
Invalid,
}
@@ -165,42 +532,102 @@ impl PpcOpcode {
pub fn terminates_block(&self) -> bool {
matches!(
self,
Self::bx | Self::bcx | Self::bclrx | Self::bcctrx
Self::bx
| Self::bcx
| Self::bclrx
| Self::bcctrx
| Self::sc
| Self::td | Self::tdi | Self::tw | Self::twi
| Self::td
| Self::tdi
| Self::tw
| Self::twi
| Self::Invalid
)
}
/// Returns true if this is a load instruction.
pub fn is_load(&self) -> bool {
matches!(self,
Self::lbz | Self::lbzu | Self::lbzux | Self::lbzx |
Self::lhz | Self::lhzu | Self::lhzux | Self::lhzx |
Self::lha | Self::lhau | Self::lhaux | Self::lhax |
Self::lwz | Self::lwzu | Self::lwzux | Self::lwzx |
Self::lwa | Self::lwax | Self::lwaux |
Self::ld | Self::ldu | Self::ldux | Self::ldx |
Self::lfs | Self::lfsu | Self::lfsux | Self::lfsx |
Self::lfd | Self::lfdu | Self::lfdux | Self::lfdx |
Self::lhbrx | Self::lwbrx | Self::ldbrx |
Self::lmw | Self::lswi | Self::lswx |
Self::lwarx | Self::ldarx
matches!(
self,
Self::lbz
| Self::lbzu
| Self::lbzux
| Self::lbzx
| Self::lhz
| Self::lhzu
| Self::lhzux
| Self::lhzx
| Self::lha
| Self::lhau
| Self::lhaux
| Self::lhax
| Self::lwz
| Self::lwzu
| Self::lwzux
| Self::lwzx
| Self::lwa
| Self::lwax
| Self::lwaux
| Self::ld
| Self::ldu
| Self::ldux
| Self::ldx
| Self::lfs
| Self::lfsu
| Self::lfsux
| Self::lfsx
| Self::lfd
| Self::lfdu
| Self::lfdux
| Self::lfdx
| Self::lhbrx
| Self::lwbrx
| Self::ldbrx
| Self::lmw
| Self::lswi
| Self::lswx
| Self::lwarx
| Self::ldarx
)
}
/// Returns true if this is a store instruction.
pub fn is_store(&self) -> bool {
matches!(self,
Self::stb | Self::stbu | Self::stbux | Self::stbx |
Self::sth | Self::sthu | Self::sthux | Self::sthx |
Self::stw | Self::stwu | Self::stwux | Self::stwx |
Self::std | Self::stdu | Self::stdux | Self::stdx |
Self::stfs | Self::stfsu | Self::stfsux | Self::stfsx |
Self::stfd | Self::stfdu | Self::stfdux | Self::stfdx |
Self::sthbrx | Self::stwbrx | Self::stdbrx |
Self::stmw | Self::stswi | Self::stswx |
Self::stwcx | Self::stdcx | Self::stfiwx
matches!(
self,
Self::stb
| Self::stbu
| Self::stbux
| Self::stbx
| Self::sth
| Self::sthu
| Self::sthux
| Self::sthx
| Self::stw
| Self::stwu
| Self::stwux
| Self::stwx
| Self::std
| Self::stdu
| Self::stdux
| Self::stdx
| Self::stfs
| Self::stfsu
| Self::stfsux
| Self::stfsx
| Self::stfd
| Self::stfdu
| Self::stfdux
| Self::stfdx
| Self::sthbrx
| Self::stwbrx
| Self::stdbrx
| Self::stmw
| Self::stswi
| Self::stswx
| Self::stwcx
| Self::stdcx
| Self::stfiwx
)
}
@@ -227,8 +654,13 @@ impl PpcOpcode {
pub fn is_sync_sensitive(&self) -> bool {
matches!(
self,
Self::lwarx | Self::ldarx | Self::stwcx | Self::stdcx
| Self::sync | Self::eieio | Self::isync
Self::lwarx
| Self::ldarx
| Self::stwcx
| Self::stdcx
| Self::sync
| Self::eieio
| Self::isync
)
}

View File

@@ -4,13 +4,13 @@
//! From `xenia-rs`'s `xenia-xex` + `xenia-vfs` when that emulator was retired.
//! `docs/agents/CONSOLIDATION.md` Phase 3.
pub mod vfs;
pub mod header;
pub mod loader;
pub mod lzx;
pub mod pe;
pub mod pdata;
pub mod pe;
pub mod resources;
pub mod tls;
pub mod vfs;
pub use header::Xex2Header;

View File

@@ -1,6 +1,6 @@
use crate::header::*;
use aes::cipher::{BlockDecrypt, KeyInit};
use aes::Aes128;
use aes::cipher::{BlockDecrypt, KeyInit};
use byteorder::{BigEndian, ReadBytesExt};
use std::io::{self, Cursor, Read, Seek, SeekFrom};
@@ -12,7 +12,10 @@ pub fn parse_xex2_header(data: &[u8]) -> io::Result<Xex2Header> {
if magic != XEX2_MAGIC {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("Invalid XEX2 magic: {:#010x} (expected {:#010x})", magic, XEX2_MAGIC),
format!(
"Invalid XEX2 magic: {:#010x} (expected {:#010x})",
magic, XEX2_MAGIC
),
));
}
@@ -84,41 +87,41 @@ fn parse_security_info(cursor: &mut Cursor<&[u8]>) -> io::Result<Xex2SecurityInf
// 0x180: page_descriptor_count (u32)
// 0x184: page_descriptors[] (each is 0x18 bytes: u32 value + 0x14 digest)
let _header_size = cursor.read_u32::<BigEndian>()?; // 0x000
let image_size = cursor.read_u32::<BigEndian>()?; // 0x004
let _header_size = cursor.read_u32::<BigEndian>()?; // 0x000
let image_size = cursor.read_u32::<BigEndian>()?; // 0x004
// Skip RSA signature (0x100 bytes)
let mut rsa_sig = [0u8; 0x100];
cursor.read_exact(&mut rsa_sig)?; // 0x008
cursor.read_exact(&mut rsa_sig)?; // 0x008
let _unk_108 = cursor.read_u32::<BigEndian>()?; // 0x108
let image_flags = cursor.read_u32::<BigEndian>()?; // 0x10C
let load_address = cursor.read_u32::<BigEndian>()?; // 0x110
let _unk_108 = cursor.read_u32::<BigEndian>()?; // 0x108
let image_flags = cursor.read_u32::<BigEndian>()?; // 0x10C
let load_address = cursor.read_u32::<BigEndian>()?; // 0x110
// Skip section_digest (0x14 bytes)
let mut digest = [0u8; 0x14];
cursor.read_exact(&mut digest)?; // 0x114
cursor.read_exact(&mut digest)?; // 0x114
let _import_table_count = cursor.read_u32::<BigEndian>()?; // 0x128
// Skip import_table_digest (0x14 bytes)
cursor.read_exact(&mut digest)?; // 0x12C
cursor.read_exact(&mut digest)?; // 0x12C
// Skip xgd2_media_id (0x10 bytes)
let mut media_id = [0u8; 0x10];
cursor.read_exact(&mut media_id)?; // 0x140
cursor.read_exact(&mut media_id)?; // 0x140
// Read aes_key (0x10 bytes)
let mut aes_key = [0u8; 0x10];
cursor.read_exact(&mut aes_key)?; // 0x150
cursor.read_exact(&mut aes_key)?; // 0x150
let export_table_address = cursor.read_u32::<BigEndian>()?; // 0x160
// Skip header_digest (0x14 bytes)
cursor.read_exact(&mut digest)?; // 0x164
cursor.read_exact(&mut digest)?; // 0x164
let _region = cursor.read_u32::<BigEndian>()?; // 0x178
let _allowed_media = cursor.read_u32::<BigEndian>()?; // 0x17C
let _region = cursor.read_u32::<BigEndian>()?; // 0x178
let _allowed_media = cursor.read_u32::<BigEndian>()?; // 0x17C
let page_descriptor_count = cursor.read_u32::<BigEndian>()?; // 0x180
@@ -144,7 +147,9 @@ fn parse_security_info(cursor: &mut Cursor<&[u8]>) -> io::Result<Xex2SecurityInf
fn parse_file_format_info(data: &[u8], headers: &[Xex2OptionalHeader]) -> Option<FileFormatInfo> {
// The key format: low 8 bits indicate the data size category
// 0xFF = data offset is a pointer to variable-size data in the header area
let header = headers.iter().find(|h| h.key == header_keys::FILE_FORMAT_INFO)?;
let header = headers
.iter()
.find(|h| h.key == header_keys::FILE_FORMAT_INFO)?;
let offset = header.value as usize;
if offset + 8 > data.len() {
return None;
@@ -166,11 +171,18 @@ fn parse_file_format_info(data: &[u8], headers: &[Xex2OptionalHeader]) -> Option
COMPRESSION_BASIC => {
// Basic compression blocks: (data_size, zero_size) pairs
// Number of blocks = (info_size - 8) / 8
let block_count = if info_size > 8 { (info_size - 8) / 8 } else { 0 };
let block_count = if info_size > 8 {
(info_size - 8) / 8
} else {
0
};
for _ in 0..block_count {
let data_size = cursor.read_u32::<BigEndian>().ok()?;
let zero_size = cursor.read_u32::<BigEndian>().ok()?;
basic_blocks.push(BasicCompressionBlock { data_size, zero_size });
basic_blocks.push(BasicCompressionBlock {
data_size,
zero_size,
});
}
}
COMPRESSION_NORMAL => {
@@ -197,7 +209,10 @@ fn parse_file_format_info(data: &[u8], headers: &[Xex2OptionalHeader]) -> Option
/// At this stage, only record addresses are read; ordinals and record types
/// are resolved later by `resolve_imports` once the PE image is decompressed.
fn parse_import_libraries(data: &[u8], headers: &[Xex2OptionalHeader]) -> Vec<ImportLibrary> {
let header = match headers.iter().find(|h| h.key == header_keys::IMPORT_LIBRARIES) {
let header = match headers
.iter()
.find(|h| h.key == header_keys::IMPORT_LIBRARIES)
{
Some(h) => h,
None => return Vec::new(),
};
@@ -208,10 +223,10 @@ fn parse_import_libraries(data: &[u8], headers: &[Xex2OptionalHeader]) -> Vec<Im
}
fn be_u32(data: &[u8], off: usize) -> u32 {
u32::from_be_bytes([data[off], data[off+1], data[off+2], data[off+3]])
u32::from_be_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]])
}
fn be_u16(data: &[u8], off: usize) -> u16 {
u16::from_be_bytes([data[off], data[off+1]])
u16::from_be_bytes([data[off], data[off + 1]])
}
let total_size = be_u32(data, offset) as usize;
@@ -225,11 +240,17 @@ fn parse_import_libraries(data: &[u8], headers: &[Xex2OptionalHeader]) -> Vec<Im
for _ in 0..string_count {
let start = string_data_start + spos;
let mut end = start;
while end < data.len() && data[end] != 0 { end += 1; }
let name = std::str::from_utf8(&data[start..end]).unwrap_or("???").to_string();
while end < data.len() && data[end] != 0 {
end += 1;
}
let name = std::str::from_utf8(&data[start..end])
.unwrap_or("???")
.to_string();
spos += name.len() + 1;
// 4-byte alignment
if !spos.is_multiple_of(4) { spos += 4 - (spos % 4); }
if !spos.is_multiple_of(4) {
spos += 4 - (spos % 4);
}
strings.push(name);
}
@@ -239,7 +260,9 @@ fn parse_import_libraries(data: &[u8], headers: &[Xex2OptionalHeader]) -> Vec<Im
while lib_off + 0x28 <= data.len() && lib_off < offset + total_size {
let lib_size = be_u32(data, lib_off) as usize;
if lib_size == 0 { break; }
if lib_size == 0 {
break;
}
let id = be_u32(data, lib_off + 0x18);
let version_cur = be_u32(data, lib_off + 0x1C);
@@ -247,7 +270,10 @@ fn parse_import_libraries(data: &[u8], headers: &[Xex2OptionalHeader]) -> Vec<Im
let name_index = (be_u16(data, lib_off + 0x24) & 0xFF) as usize;
let count = be_u16(data, lib_off + 0x26) as usize;
let lib_name = strings.get(name_index).cloned().unwrap_or_else(|| format!("lib_{name_index}"));
let lib_name = strings
.get(name_index)
.cloned()
.unwrap_or_else(|| format!("lib_{name_index}"));
let mut imports = Vec::new();
for i in 0..count {
@@ -283,8 +309,10 @@ pub fn resolve_imports(header: &mut Xex2Header, pe_image: &[u8]) {
if pe_off + 4 <= pe_image.len() {
// PE image values are big-endian (Xbox 360 native)
let val = u32::from_be_bytes([
pe_image[pe_off], pe_image[pe_off+1],
pe_image[pe_off+2], pe_image[pe_off+3],
pe_image[pe_off],
pe_image[pe_off + 1],
pe_image[pe_off + 2],
pe_image[pe_off + 3],
]);
imp.record_type = ((val >> 24) & 0xFF) as u8;
imp.ordinal = (val & 0xFFFF) as u16;
@@ -299,14 +327,21 @@ fn parse_execution_info(data: &[u8], headers: &[Xex2OptionalHeader]) -> Option<E
// is an inline struct of 6 u32 words (24 bytes total).
// Layout: media_id(4), version(4), base_version(4), title_id(4),
// platform(1), exec_type(1), disc_number(1), disc_count(1)
let header = headers.iter().find(|h| h.key == header_keys::EXECUTION_INFO)?;
let header = headers
.iter()
.find(|h| h.key == header_keys::EXECUTION_INFO)?;
let off = header.value as usize;
if off + 20 > data.len() {
return None;
}
let media_id = u32::from_be_bytes([data[off], data[off+1], data[off+2], data[off+3]]);
let title_id = u32::from_be_bytes([data[off+12], data[off+13], data[off+14], data[off+15]]);
let media_id = u32::from_be_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]]);
let title_id = u32::from_be_bytes([
data[off + 12],
data[off + 13],
data[off + 14],
data[off + 15],
]);
let disc_number = data[off + 18];
let disc_count = data[off + 19];
@@ -320,24 +355,33 @@ fn parse_execution_info(data: &[u8], headers: &[Xex2OptionalHeader]) -> Option<E
/// Parse original PE name from optional header data.
fn parse_original_pe_name(data: &[u8], headers: &[Xex2OptionalHeader]) -> Option<String> {
let header = headers.iter().find(|h| h.key == header_keys::ORIGINAL_PE_NAME)?;
let header = headers
.iter()
.find(|h| h.key == header_keys::ORIGINAL_PE_NAME)?;
let off = header.value as usize;
if off + 4 > data.len() {
return None;
}
let size = u32::from_be_bytes([data[off], data[off+1], data[off+2], data[off+3]]) as usize;
let size =
u32::from_be_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]]) as usize;
if off + size > data.len() || size <= 4 {
return None;
}
let name_bytes = &data[off + 4..off + size];
Some(String::from_utf8_lossy(name_bytes).trim_end_matches('\0').to_string())
Some(
String::from_utf8_lossy(name_bytes)
.trim_end_matches('\0')
.to_string(),
)
}
/// Get an optional header value by key.
pub fn get_opt_header(header: &Xex2Header, key: u32) -> Option<u32> {
header.optional_headers.iter()
header
.optional_headers
.iter()
.find(|h| h.key == key)
.map(|h| h.value)
}
@@ -389,15 +433,27 @@ pub fn load_image(data: &[u8], header: &Xex2Header) -> io::Result<Vec<u8>> {
metrics::histogram!("xex.load_image_ms").record(elapsed_ms);
metrics::counter!("xex.bytes_in").increment(bytes_in as u64);
metrics::counter!("xex.bytes_out").increment(output.len() as u64);
let ratio = if bytes_in == 0 { 0.0 } else { output.len() as f64 / bytes_in as f64 };
tracing::info!(bytes_in, bytes_out = output.len(), ratio, elapsed_ms, "image loaded");
let ratio = if bytes_in == 0 {
0.0
} else {
output.len() as f64 / bytes_in as f64
};
tracing::info!(
bytes_in,
bytes_out = output.len(),
ratio,
elapsed_ms,
"image loaded"
);
Ok(output)
}
/// Load basic compressed image data.
fn load_basic_compressed(source: &[u8], info: &FileFormatInfo) -> io::Result<Vec<u8>> {
// Calculate total uncompressed size
let total_size: u64 = info.basic_blocks.iter()
let total_size: u64 = info
.basic_blocks
.iter()
.map(|b| b.data_size as u64 + b.zero_size as u64)
.sum();
@@ -412,8 +468,12 @@ fn load_basic_compressed(source: &[u8], info: &FileFormatInfo) -> io::Result<Vec
if src_offset + data_size > source.len() {
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
format!("Basic compression block data extends past end of file (src_offset={:#x}, data_size={:#x}, source_len={:#x})",
src_offset, data_size, source.len()),
format!(
"Basic compression block data extends past end of file (src_offset={:#x}, data_size={:#x}, source_len={:#x})",
src_offset,
data_size,
source.len()
),
));
}
@@ -434,8 +494,7 @@ fn load_basic_compressed(source: &[u8], info: &FileFormatInfo) -> io::Result<Vec
/// Xbox 360 retail AES key for XEX2 session key decryption.
const XEX2_RETAIL_KEY: [u8; 16] = [
0x20, 0xB1, 0x85, 0xA5, 0x9D, 0x28, 0xFD, 0xC3,
0x40, 0x58, 0x3F, 0xBB, 0x08, 0x96, 0xBF, 0x91,
0x20, 0xB1, 0x85, 0xA5, 0x9D, 0x28, 0xFD, 0xC3, 0x40, 0x58, 0x3F, 0xBB, 0x08, 0x96, 0xBF, 0x91,
];
/// Xbox 360 devkit AES key (all zeros).
@@ -486,8 +545,10 @@ fn derive_session_key(header: &Xex2Header) -> [u8; 16] {
///
/// The first block's size comes from the file format header (first_block_size).
/// Each block in the data starts with a block_info struct for the NEXT block:
/// - block_size: u32 BE (size of the next block)
/// - block_hash: [u8; 20] (SHA1 of the next block)
///
/// - block_size: u32 BE (size of the next block)
/// - block_hash: [u8; 20] (SHA1 of the next block)
///
/// Followed by chunks: { chunk_size: u16 BE, data: [u8; chunk_size] }, terminated by chunk_size=0
fn deblock(input: &[u8], first_block_size: u32) -> io::Result<Vec<u8>> {
let mut output = Vec::new();
@@ -499,9 +560,8 @@ fn deblock(input: &[u8], first_block_size: u32) -> io::Result<Vec<u8>> {
// Read next block's info from start of current block data
let next_block_size = if pos + 4 <= input.len() {
u32::from_be_bytes([
input[pos], input[pos + 1], input[pos + 2], input[pos + 3],
]) as usize
u32::from_be_bytes([input[pos], input[pos + 1], input[pos + 2], input[pos + 3]])
as usize
} else {
0
};
@@ -522,8 +582,12 @@ fn deblock(input: &[u8], first_block_size: u32) -> io::Result<Vec<u8>> {
if p + chunk_size > input.len() {
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
format!("De-block chunk extends past input (pos={:#x}, chunk_size={:#x}, input_len={:#x})",
p, chunk_size, input.len()),
format!(
"De-block chunk extends past input (pos={:#x}, chunk_size={:#x}, input_len={:#x})",
p,
chunk_size,
input.len()
),
));
}
output.extend_from_slice(&input[p..p + chunk_size]);
@@ -543,8 +607,14 @@ fn deblock(input: &[u8], first_block_size: u32) -> io::Result<Vec<u8>> {
/// Load normal (LZX) compressed image data.
/// Pipeline: decrypt → de-block → LZX decompress (pure Rust)
#[tracing::instrument(skip_all, fields(bytes_in = source.len()))]
fn load_normal_compressed(source: &[u8], info: &FileFormatInfo, header: &Xex2Header) -> io::Result<Vec<u8>> {
let uncompressed_size = header.security_info.as_ref()
fn load_normal_compressed(
source: &[u8],
info: &FileFormatInfo,
header: &Xex2Header,
) -> io::Result<Vec<u8>> {
let uncompressed_size = header
.security_info
.as_ref()
.map(|s| s.image_size as usize)
.unwrap_or(0);
@@ -577,15 +647,25 @@ fn load_normal_compressed(source: &[u8], info: &FileFormatInfo, header: &Xex2Hea
// Step 3: LZX decompress using pure Rust decoder
let window_bits = match info.normal_window_size {
s if s == 0 => 15, // default
0 => 15, // default
s => (s as f64).log2() as u32,
};
let mut decoder = crate::lzx::LzxDecoder::new(window_bits);
let output = decoder.decompress(&deblocked, uncompressed_size)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, format!("LZX decompression failed: {e}")))?;
let output = decoder
.decompress(&deblocked, uncompressed_size)
.map_err(|e| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("LZX decompression failed: {e}"),
)
})?;
tracing::info!("LZX decompressed: {} -> {} bytes", deblocked.len(), uncompressed_size);
tracing::info!(
"LZX decompressed: {} -> {} bytes",
deblocked.len(),
uncompressed_size
);
Ok(output)
}

View File

@@ -1,3 +1,10 @@
// 🔴 INDEX ARITHMETIC IS THE ALGORITHM HERE, so the range-loop lints are off for
// this file and this file only. LZX decoding is defined over symbol indices,
// Huffman table slots and window positions; rewriting those loops as iterators
// would hide the correspondence with the format, and a decompressor that is
// merely idiomatic is worth nothing if it is not bit-exact.
#![allow(clippy::needless_range_loop, clippy::explicit_counter_loop)]
//! LZX decompressor for Xbox 360 XEX2 "normal compression".
//! Ported from libmspack lzxd.c (C) 2003-2013 Stuart Caie, LGPL 2.1.
@@ -32,9 +39,8 @@ const BITBUF_WIDTH: u32 = 32;
static POSITION_SLOTS: [u32; 11] = [30, 32, 34, 36, 38, 42, 50, 66, 98, 162, 290];
static EXTRA_BITS: [u8; 36] = [
0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6,
7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13, 14, 14,
15, 15, 16, 16,
0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13,
13, 14, 14, 15, 15, 16, 16,
];
#[rustfmt::skip]
@@ -109,17 +115,30 @@ struct BitReader<'a> {
impl<'a> BitReader<'a> {
fn new(data: &'a [u8]) -> Self {
Self { data, pos: 0, buf: 0, left: 0 }
Self {
data,
pos: 0,
buf: 0,
left: 0,
}
}
/// Inject one 16-bit little-endian pair into MSB bit buffer.
fn fill(&mut self) {
let b0 = if self.pos < self.data.len() {
let b = self.data[self.pos]; self.pos += 1; b as u32
} else { 0 };
let b = self.data[self.pos];
self.pos += 1;
b as u32
} else {
0
};
let b1 = if self.pos < self.data.len() {
let b = self.data[self.pos]; self.pos += 1; b as u32
} else { 0 };
let b = self.data[self.pos];
self.pos += 1;
b as u32
} else {
0
};
let word = (b1 << 8) | b0;
self.buf |= word << (16 - self.left as u32);
self.left += 16;
@@ -127,7 +146,9 @@ impl<'a> BitReader<'a> {
#[inline]
fn ensure(&mut self, n: i32) {
while self.left < n { self.fill(); }
while self.left < n {
self.fill();
}
}
#[inline]
@@ -152,26 +173,29 @@ impl<'a> BitReader<'a> {
/// Read a raw byte directly (for UNCOMPRESSED blocks).
fn raw_byte(&mut self) -> u8 {
if self.pos < self.data.len() {
let b = self.data[self.pos]; self.pos += 1; b
} else { 0 }
let b = self.data[self.pos];
self.pos += 1;
b
} else {
0
}
}
/// Re-align the bitstream at a frame boundary.
fn align_frame(&mut self) {
if self.left > 0 { self.ensure(16); }
if self.left > 0 {
self.ensure(16);
}
let r = self.left & 15;
if r != 0 { self.remove(r as u32); }
if r != 0 {
self.remove(r as u32);
}
}
}
// ── Huffman table builder (MSB order) ───────────────────────────────────────
fn make_decode_table(
nsyms: usize,
nbits: usize,
length: &[u8],
table: &mut [u16],
) -> bool {
fn make_decode_table(nsyms: usize, nbits: usize, length: &[u8], table: &mut [u16]) -> bool {
let mut pos: usize = 0;
let table_mask = 1usize << nbits;
let mut bit_mask = table_mask >> 1;
@@ -179,10 +203,14 @@ fn make_decode_table(
// Short codes: direct mapping
for bit_num in 1..=nbits {
for sym in 0..nsyms {
if length[sym] as usize != bit_num { continue; }
if length[sym] as usize != bit_num {
continue;
}
let leaf = pos;
pos += bit_mask;
if pos > table_mask { return true; }
if pos > table_mask {
return true;
}
for i in leaf..leaf + bit_mask {
table[i] = sym as u16;
}
@@ -190,14 +218,20 @@ fn make_decode_table(
bit_mask >>= 1;
}
if pos == table_mask { return false; }
if pos == table_mask {
return false;
}
// Mark remaining entries as unused
for i in pos..table_mask {
table[i] = 0xFFFF;
}
let mut next_symbol = if (table_mask >> 1) < nsyms { nsyms } else { table_mask >> 1 };
let mut next_symbol = if (table_mask >> 1) < nsyms {
nsyms
} else {
table_mask >> 1
};
let mut pos32 = (pos as u32) << 16;
let table_mask32 = (table_mask as u32) << 16;
@@ -206,8 +240,12 @@ fn make_decode_table(
// Long codes: tree traversal
for bit_num in (nbits + 1)..=HUFF_MAXBITS {
for sym in 0..nsyms {
if length[sym] as usize != bit_num { continue; }
if pos32 >= table_mask32 { return true; }
if length[sym] as usize != bit_num {
continue;
}
if pos32 >= table_mask32 {
return true;
}
let mut leaf = (pos32 >> 16) as usize;
@@ -247,9 +285,13 @@ fn read_huffsym(
let mut i: u32 = 1 << (BITBUF_WIDTH - tablebits as u32);
loop {
i >>= 1;
if i == 0 { return Err(LzxError::BadHuffmanTable); }
if i == 0 {
return Err(LzxError::BadHuffmanTable);
}
sym = table[(sym << 1) | if br.buf & i != 0 { 1 } else { 0 }] as usize;
if sym < maxsyms { break; }
if sym < maxsyms {
break;
}
}
}
br.remove(lens[sym] as u32);
@@ -307,7 +349,9 @@ impl LzxDecoder {
frame_posn: 0,
frame: 0,
num_offsets,
r0: 1, r1: 1, r2: 1,
r0: 1,
r1: 1,
r2: 1,
block_type: 0,
block_length: 0,
block_remaining: 0,
@@ -315,20 +359,23 @@ impl LzxDecoder {
intel_filesize: 0,
intel_curpos: 0,
intel_started: false,
pretree_len: vec![0u8; PRETREE_MAXSYMS + LENTABLE_SAFETY],
maintree_len: vec![0u8; MAINTREE_MAXSYMS + LENTABLE_SAFETY],
length_len: vec![0u8; LENGTH_MAXSYMS + LENTABLE_SAFETY],
aligned_len: vec![0u8; ALIGNED_MAXSYMS + LENTABLE_SAFETY],
pretree_table: vec![0u16; (1 << PRETREE_TABLEBITS) + PRETREE_MAXSYMS * 2],
pretree_len: vec![0u8; PRETREE_MAXSYMS + LENTABLE_SAFETY],
maintree_len: vec![0u8; MAINTREE_MAXSYMS + LENTABLE_SAFETY],
length_len: vec![0u8; LENGTH_MAXSYMS + LENTABLE_SAFETY],
aligned_len: vec![0u8; ALIGNED_MAXSYMS + LENTABLE_SAFETY],
pretree_table: vec![0u16; (1 << PRETREE_TABLEBITS) + PRETREE_MAXSYMS * 2],
maintree_table: vec![0u16; (1 << MAINTREE_TABLEBITS) + MAINTREE_MAXSYMS * 2],
length_table: vec![0u16; (1 << LENGTH_TABLEBITS) + LENGTH_MAXSYMS * 2],
aligned_table: vec![0u16; (1 << ALIGNED_TABLEBITS) + ALIGNED_MAXSYMS * 2],
length_table: vec![0u16; (1 << LENGTH_TABLEBITS) + LENGTH_MAXSYMS * 2],
aligned_table: vec![0u16; (1 << ALIGNED_TABLEBITS) + ALIGNED_MAXSYMS * 2],
length_empty: false,
}
}
fn build_table(
lens: &[u8], table: &mut [u16], maxsyms: usize, tablebits: usize,
lens: &[u8],
table: &mut [u16],
maxsyms: usize,
tablebits: usize,
) -> Result<(), LzxError> {
if make_decode_table(maxsyms, tablebits, lens, table) {
Err(LzxError::BadHuffmanTable)
@@ -338,7 +385,10 @@ impl LzxDecoder {
}
fn build_table_maybe_empty(
lens: &[u8], table: &mut [u16], maxsyms: usize, tablebits: usize,
lens: &[u8],
table: &mut [u16],
maxsyms: usize,
tablebits: usize,
) -> Result<bool, LzxError> {
if make_decode_table(maxsyms, tablebits, lens, table) {
// Check if table is simply empty (all lengths zero)
@@ -366,30 +416,63 @@ impl LzxDecoder {
for i in 0..20 {
pretree_len[i] = br.read(4) as u8;
}
Self::build_table(pretree_len, pretree_table, PRETREE_MAXSYMS, PRETREE_TABLEBITS)?;
Self::build_table(
pretree_len,
pretree_table,
PRETREE_MAXSYMS,
PRETREE_TABLEBITS,
)?;
let mut x = first;
while x < last {
let z = read_huffsym(br, pretree_table, pretree_len, PRETREE_TABLEBITS, PRETREE_MAXSYMS)?;
let z = read_huffsym(
br,
pretree_table,
pretree_len,
PRETREE_TABLEBITS,
PRETREE_MAXSYMS,
)?;
if z == 17 {
// Run of zeros: [read 4 bits] + 4
let mut y = br.read(4) as usize + 4;
while y > 0 && x < last { lens[x] = 0; x += 1; y -= 1; }
while y > 0 && x < last {
lens[x] = 0;
x += 1;
y -= 1;
}
} else if z == 18 {
// Run of zeros: [read 5 bits] + 20
let mut y = br.read(5) as usize + 20;
while y > 0 && x < last { lens[x] = 0; x += 1; y -= 1; }
while y > 0 && x < last {
lens[x] = 0;
x += 1;
y -= 1;
}
} else if z == 19 {
// Run of same: [read 1 bit] + 4, then read symbol
let mut y = br.read(1) as usize + 4;
let z2 = read_huffsym(br, pretree_table, pretree_len, PRETREE_TABLEBITS, PRETREE_MAXSYMS)?;
let z2 = read_huffsym(
br,
pretree_table,
pretree_len,
PRETREE_TABLEBITS,
PRETREE_MAXSYMS,
)?;
let mut val = lens[x] as i32 - z2 as i32;
if val < 0 { val += 17; }
while y > 0 && x < last { lens[x] = val as u8; x += 1; y -= 1; }
if val < 0 {
val += 17;
}
while y > 0 && x < last {
lens[x] = val as u8;
x += 1;
y -= 1;
}
} else {
// Delta: code 0..16
let mut val = lens[x] as i32 - z as i32;
if val < 0 { val += 17; }
if val < 0 {
val += 17;
}
lens[x] = val as u8;
x += 1;
}
@@ -425,13 +508,15 @@ impl LzxDecoder {
LZX_FRAME_SIZE
};
let mut bytes_todo = (self.frame_posn + frame_size).wrapping_sub(self.window_posn) as i32;
let mut bytes_todo =
(self.frame_posn + frame_size).wrapping_sub(self.window_posn) as i32;
while bytes_todo > 0 {
// New block?
if self.block_remaining == 0 {
// Realign after odd UNCOMPRESSED block
if self.block_type == LZX_BLOCKTYPE_UNCOMPRESSED && (self.block_length & 1) != 0 {
if self.block_type == LZX_BLOCKTYPE_UNCOMPRESSED && (self.block_length & 1) != 0
{
br.raw_byte();
}
// Read block type (3 bits) and length (24 bits)
@@ -443,33 +528,110 @@ impl LzxDecoder {
match self.block_type {
LZX_BLOCKTYPE_ALIGNED => {
for i in 0..8 { self.aligned_len[i] = br.read(3) as u8; }
Self::build_table(&self.aligned_len, &mut self.aligned_table, ALIGNED_MAXSYMS, ALIGNED_TABLEBITS)?;
for i in 0..8 {
self.aligned_len[i] = br.read(3) as u8;
}
Self::build_table(
&self.aligned_len,
&mut self.aligned_table,
ALIGNED_MAXSYMS,
ALIGNED_TABLEBITS,
)?;
// Fall through to verbatim tree reading
Self::read_lens(&mut br, &mut self.maintree_len, &mut self.pretree_len, &mut self.pretree_table, 0, 256)?;
Self::read_lens(&mut br, &mut self.maintree_len, &mut self.pretree_len, &mut self.pretree_table, 256, LZX_NUM_CHARS + self.num_offsets)?;
Self::build_table(&self.maintree_len, &mut self.maintree_table, MAINTREE_MAXSYMS, MAINTREE_TABLEBITS)?;
if self.maintree_len[0xE8] != 0 { self.intel_started = true; }
Self::read_lens(&mut br, &mut self.length_len, &mut self.pretree_len, &mut self.pretree_table, 0, LZX_NUM_SECONDARY_LENGTHS)?;
self.length_empty = Self::build_table_maybe_empty(&self.length_len, &mut self.length_table, LENGTH_MAXSYMS, LENGTH_TABLEBITS)?;
Self::read_lens(
&mut br,
&mut self.maintree_len,
&mut self.pretree_len,
&mut self.pretree_table,
0,
256,
)?;
Self::read_lens(
&mut br,
&mut self.maintree_len,
&mut self.pretree_len,
&mut self.pretree_table,
256,
LZX_NUM_CHARS + self.num_offsets,
)?;
Self::build_table(
&self.maintree_len,
&mut self.maintree_table,
MAINTREE_MAXSYMS,
MAINTREE_TABLEBITS,
)?;
if self.maintree_len[0xE8] != 0 {
self.intel_started = true;
}
Self::read_lens(
&mut br,
&mut self.length_len,
&mut self.pretree_len,
&mut self.pretree_table,
0,
LZX_NUM_SECONDARY_LENGTHS,
)?;
self.length_empty = Self::build_table_maybe_empty(
&self.length_len,
&mut self.length_table,
LENGTH_MAXSYMS,
LENGTH_TABLEBITS,
)?;
}
LZX_BLOCKTYPE_VERBATIM => {
Self::read_lens(&mut br, &mut self.maintree_len, &mut self.pretree_len, &mut self.pretree_table, 0, 256)?;
Self::read_lens(&mut br, &mut self.maintree_len, &mut self.pretree_len, &mut self.pretree_table, 256, LZX_NUM_CHARS + self.num_offsets)?;
Self::build_table(&self.maintree_len, &mut self.maintree_table, MAINTREE_MAXSYMS, MAINTREE_TABLEBITS)?;
if self.maintree_len[0xE8] != 0 { self.intel_started = true; }
Self::read_lens(&mut br, &mut self.length_len, &mut self.pretree_len, &mut self.pretree_table, 0, LZX_NUM_SECONDARY_LENGTHS)?;
self.length_empty = Self::build_table_maybe_empty(&self.length_len, &mut self.length_table, LENGTH_MAXSYMS, LENGTH_TABLEBITS)?;
Self::read_lens(
&mut br,
&mut self.maintree_len,
&mut self.pretree_len,
&mut self.pretree_table,
0,
256,
)?;
Self::read_lens(
&mut br,
&mut self.maintree_len,
&mut self.pretree_len,
&mut self.pretree_table,
256,
LZX_NUM_CHARS + self.num_offsets,
)?;
Self::build_table(
&self.maintree_len,
&mut self.maintree_table,
MAINTREE_MAXSYMS,
MAINTREE_TABLEBITS,
)?;
if self.maintree_len[0xE8] != 0 {
self.intel_started = true;
}
Self::read_lens(
&mut br,
&mut self.length_len,
&mut self.pretree_len,
&mut self.pretree_table,
0,
LZX_NUM_SECONDARY_LENGTHS,
)?;
self.length_empty = Self::build_table_maybe_empty(
&self.length_len,
&mut self.length_table,
LENGTH_MAXSYMS,
LENGTH_TABLEBITS,
)?;
}
LZX_BLOCKTYPE_UNCOMPRESSED => {
self.intel_started = true;
// Align to byte boundary
if br.left == 0 { br.ensure(16); }
if br.left == 0 {
br.ensure(16);
}
br.left = 0;
br.buf = 0;
// Read R0, R1, R2 (12 bytes, little-endian u32s)
let mut buf = [0u8; 12];
for b in &mut buf { *b = br.raw_byte(); }
for b in &mut buf {
*b = br.raw_byte();
}
self.r0 = u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]);
self.r1 = u32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]);
self.r2 = u32::from_le_bytes([buf[8], buf[9], buf[10], buf[11]]);
@@ -479,7 +641,9 @@ impl LzxDecoder {
}
let mut this_run = self.block_remaining as i32;
if this_run > bytes_todo { this_run = bytes_todo; }
if this_run > bytes_todo {
this_run = bytes_todo;
}
bytes_todo -= this_run;
self.block_remaining -= this_run as usize;
@@ -488,7 +652,13 @@ impl LzxDecoder {
match self.block_type {
LZX_BLOCKTYPE_VERBATIM => {
while this_run > 0 {
let main_element = read_huffsym(&mut br, &self.maintree_table, &self.maintree_len, MAINTREE_TABLEBITS, MAINTREE_MAXSYMS)?;
let main_element = read_huffsym(
&mut br,
&self.maintree_table,
&self.maintree_len,
MAINTREE_TABLEBITS,
MAINTREE_MAXSYMS,
)?;
if main_element < LZX_NUM_CHARS {
self.window[self.window_posn] = main_element as u8;
self.window_posn += 1;
@@ -497,8 +667,16 @@ impl LzxDecoder {
let me = main_element - LZX_NUM_CHARS;
let mut match_length = me & LZX_NUM_PRIMARY_LENGTHS;
if match_length == LZX_NUM_PRIMARY_LENGTHS {
if self.length_empty { return Err(LzxError::Decrunch("LENGTH tree empty".into())); }
let footer = read_huffsym(&mut br, &self.length_table, &self.length_len, LENGTH_TABLEBITS, LENGTH_MAXSYMS)?;
if self.length_empty {
return Err(LzxError::Decrunch("LENGTH tree empty".into()));
}
let footer = read_huffsym(
&mut br,
&self.length_table,
&self.length_len,
LENGTH_TABLEBITS,
LENGTH_MAXSYMS,
)?;
match_length += footer;
}
match_length += LZX_MIN_MATCH;
@@ -506,14 +684,34 @@ impl LzxDecoder {
let mut match_offset = (me >> 3) as u32;
match match_offset {
0 => match_offset = self.r0,
1 => { match_offset = self.r1; self.r1 = self.r0; self.r0 = match_offset; }
2 => { match_offset = self.r2; self.r2 = self.r0; self.r0 = match_offset; }
3 => { match_offset = 1; self.r2 = self.r1; self.r1 = self.r0; self.r0 = match_offset; }
1 => {
match_offset = self.r1;
self.r1 = self.r0;
self.r0 = match_offset;
}
2 => {
match_offset = self.r2;
self.r2 = self.r0;
self.r0 = match_offset;
}
3 => {
match_offset = 1;
self.r2 = self.r1;
self.r1 = self.r0;
self.r0 = match_offset;
}
_ => {
let extra = if match_offset >= 36 { 17 } else { EXTRA_BITS[match_offset as usize] as u32 };
let extra = if match_offset >= 36 {
17
} else {
EXTRA_BITS[match_offset as usize] as u32
};
let verbatim_bits = br.read(extra);
match_offset = POSITION_BASE[match_offset as usize] - 2 + verbatim_bits;
self.r2 = self.r1; self.r1 = self.r0; self.r0 = match_offset;
match_offset = POSITION_BASE[match_offset as usize] - 2
+ verbatim_bits;
self.r2 = self.r1;
self.r1 = self.r0;
self.r0 = match_offset;
}
}
@@ -527,7 +725,13 @@ impl LzxDecoder {
}
LZX_BLOCKTYPE_ALIGNED => {
while this_run > 0 {
let main_element = read_huffsym(&mut br, &self.maintree_table, &self.maintree_len, MAINTREE_TABLEBITS, MAINTREE_MAXSYMS)?;
let main_element = read_huffsym(
&mut br,
&self.maintree_table,
&self.maintree_len,
MAINTREE_TABLEBITS,
MAINTREE_MAXSYMS,
)?;
if main_element < LZX_NUM_CHARS {
self.window[self.window_posn] = main_element as u8;
self.window_posn += 1;
@@ -536,8 +740,16 @@ impl LzxDecoder {
let me = main_element - LZX_NUM_CHARS;
let mut match_length = me & LZX_NUM_PRIMARY_LENGTHS;
if match_length == LZX_NUM_PRIMARY_LENGTHS {
if self.length_empty { return Err(LzxError::Decrunch("LENGTH tree empty".into())); }
let footer = read_huffsym(&mut br, &self.length_table, &self.length_len, LENGTH_TABLEBITS, LENGTH_MAXSYMS)?;
if self.length_empty {
return Err(LzxError::Decrunch("LENGTH tree empty".into()));
}
let footer = read_huffsym(
&mut br,
&self.length_table,
&self.length_len,
LENGTH_TABLEBITS,
LENGTH_MAXSYMS,
)?;
match_length += footer;
}
match_length += LZX_MIN_MATCH;
@@ -545,18 +757,42 @@ impl LzxDecoder {
let mut match_offset = (me >> 3) as u32;
match match_offset {
0 => match_offset = self.r0,
1 => { match_offset = self.r1; self.r1 = self.r0; self.r0 = match_offset; }
2 => { match_offset = self.r2; self.r2 = self.r0; self.r0 = match_offset; }
1 => {
match_offset = self.r1;
self.r1 = self.r0;
self.r0 = match_offset;
}
2 => {
match_offset = self.r2;
self.r2 = self.r0;
self.r0 = match_offset;
}
_ => {
let extra = if match_offset >= 36 { 17 } else { EXTRA_BITS[match_offset as usize] as u32 };
let extra = if match_offset >= 36 {
17
} else {
EXTRA_BITS[match_offset as usize] as u32
};
match_offset = POSITION_BASE[match_offset as usize] - 2;
if extra > 3 {
let verbatim_bits = br.read(extra - 3);
match_offset += verbatim_bits << 3;
let aligned = read_huffsym(&mut br, &self.aligned_table, &self.aligned_len, ALIGNED_TABLEBITS, ALIGNED_MAXSYMS)?;
let aligned = read_huffsym(
&mut br,
&self.aligned_table,
&self.aligned_len,
ALIGNED_TABLEBITS,
ALIGNED_MAXSYMS,
)?;
match_offset += aligned as u32;
} else if extra == 3 {
let aligned = read_huffsym(&mut br, &self.aligned_table, &self.aligned_len, ALIGNED_TABLEBITS, ALIGNED_MAXSYMS)?;
let aligned = read_huffsym(
&mut br,
&self.aligned_table,
&self.aligned_len,
ALIGNED_TABLEBITS,
ALIGNED_MAXSYMS,
)?;
match_offset += aligned as u32;
} else if extra > 0 {
let verbatim_bits = br.read(extra);
@@ -564,7 +800,9 @@ impl LzxDecoder {
} else {
match_offset = 1;
}
self.r2 = self.r1; self.r1 = self.r0; self.r0 = match_offset;
self.r2 = self.r1;
self.r1 = self.r0;
self.r0 = match_offset;
}
}
@@ -599,7 +837,9 @@ impl LzxDecoder {
// Frame boundary check
if (self.window_posn.wrapping_sub(self.frame_posn)) != frame_size {
return Err(LzxError::Decrunch(format!(
"decode beyond frame: {} != {}", self.window_posn - self.frame_posn, frame_size
"decode beyond frame: {} != {}",
self.window_posn - self.frame_posn,
frame_size
)));
}
@@ -607,8 +847,10 @@ impl LzxDecoder {
br.align_frame();
// Intel E8 postprocessing
if self.intel_started && self.intel_filesize != 0
&& self.frame <= 32768 && frame_size > 10
if self.intel_started
&& self.intel_filesize != 0
&& self.frame <= 32768
&& frame_size > 10
{
let mut e8_buf = vec![0u8; frame_size];
e8_buf.copy_from_slice(&self.window[self.frame_posn..self.frame_posn + frame_size]);
@@ -619,18 +861,26 @@ impl LzxDecoder {
let filesize = self.intel_filesize;
while i < limit {
if e8_buf[i] != 0xE8 { i += 1; curpos += 1; continue; }
let abs_off = e8_buf[i+1] as i32
| (e8_buf[i+2] as i32) << 8
| (e8_buf[i+3] as i32) << 16
| (e8_buf[i+4] as i32) << 24;
if e8_buf[i] != 0xE8 {
i += 1;
curpos += 1;
continue;
}
let abs_off = e8_buf[i + 1] as i32
| (e8_buf[i + 2] as i32) << 8
| (e8_buf[i + 3] as i32) << 16
| (e8_buf[i + 4] as i32) << 24;
if abs_off >= -curpos && abs_off < filesize {
let rel_off = if abs_off >= 0 { abs_off - curpos } else { abs_off + filesize };
e8_buf[i+1] = rel_off as u8;
e8_buf[i+2] = (rel_off >> 8) as u8;
e8_buf[i+3] = (rel_off >> 16) as u8;
e8_buf[i+4] = (rel_off >> 24) as u8;
let rel_off = if abs_off >= 0 {
abs_off - curpos
} else {
abs_off + filesize
};
e8_buf[i + 1] = rel_off as u8;
e8_buf[i + 2] = (rel_off >> 8) as u8;
e8_buf[i + 3] = (rel_off >> 16) as u8;
e8_buf[i + 4] = (rel_off >> 24) as u8;
}
i += 5;
curpos += 5;
@@ -641,7 +891,9 @@ impl LzxDecoder {
output.extend_from_slice(&e8_buf[..to_write]);
offset += to_write;
} else {
if self.intel_filesize != 0 { self.intel_curpos += frame_size as i32; }
if self.intel_filesize != 0 {
self.intel_curpos += frame_size as i32;
}
let to_write = frame_size.min(output_len - offset);
output.extend_from_slice(&self.window[self.frame_posn..self.frame_posn + to_write]);
offset += to_write;
@@ -650,8 +902,12 @@ impl LzxDecoder {
// Advance frame
self.frame_posn += frame_size;
self.frame += 1;
if self.window_posn == self.window_size { self.window_posn = 0; }
if self.frame_posn == self.window_size { self.frame_posn = 0; }
if self.window_posn == self.window_size {
self.window_posn = 0;
}
if self.frame_posn == self.window_size {
self.frame_posn = 0;
}
}
Ok(output)

View File

@@ -91,7 +91,11 @@ pub fn parse_pdata(pe: &[u8], image_base: u32, sections: &[PeSection]) -> Vec<Pd
// Image high water = image_base + the largest virtual_address+virtual_size.
let high = sections
.iter()
.map(|s| image_base.wrapping_add(s.virtual_address).wrapping_add(s.virtual_size))
.map(|s| {
image_base
.wrapping_add(s.virtual_address)
.wrapping_add(s.virtual_size)
})
.max()
.unwrap_or(u32::MAX);
out.retain(|e| e.begin_address >= image_base && e.begin_address < high);
@@ -104,7 +108,12 @@ mod tests {
use super::*;
use crate::pe::PeSection;
fn mk_pe(image_base: u32, text_va: u32, text_size: u32, pdata: &[(u32, u32)]) -> (Vec<u8>, Vec<PeSection>) {
fn mk_pe(
image_base: u32,
text_va: u32,
text_size: u32,
pdata: &[(u32, u32)],
) -> (Vec<u8>, Vec<PeSection>) {
// Build a synthetic PE image with .text and .pdata.
// Layout: pdata at RVA 0x1000, .text at RVA 0x2000.
let pdata_rva = 0x1000u32;

View File

@@ -46,7 +46,9 @@ pub fn parse_sections(pe: &[u8]) -> anyhow::Result<Vec<PeSection>> {
let mut sections = Vec::new();
for i in 0..num_sections {
let s = section_table_off + i * 40;
if s + 40 > pe.len() { break; }
if s + 40 > pe.len() {
break;
}
let name_bytes = &pe[s..s + 8];
let name = std::str::from_utf8(name_bytes)

View File

@@ -53,7 +53,8 @@ pub fn parse_resources(data: &[u8], header: &Xex2Header) -> Vec<XexResource> {
if off + 4 > data.len() {
return Vec::new();
}
let size = u32::from_be_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]]) as usize;
let size =
u32::from_be_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]]) as usize;
// The size field counts itself; anything smaller than one record is junk.
if size < 4 + 16 || off + size > data.len() {
return Vec::new();
@@ -67,7 +68,11 @@ pub fn parse_resources(data: &[u8], header: &Xex2Header) -> Vec<XexResource> {
.to_string();
let address = u32::from_be_bytes([data[p + 8], data[p + 9], data[p + 10], data[p + 11]]);
let rsize = u32::from_be_bytes([data[p + 12], data[p + 13], data[p + 14], data[p + 15]]);
out.push(XexResource { name, address, size: rsize });
out.push(XexResource {
name,
address,
size: rsize,
});
}
out
}
@@ -94,7 +99,10 @@ mod tests {
}
fn with_resource(value: u32) -> Xex2Header {
mk_header(vec![Xex2OptionalHeader { key: header_keys::RESOURCE_INFO, value }])
mk_header(vec![Xex2OptionalHeader {
key: header_keys::RESOURCE_INFO,
value,
}])
}
#[test]

View File

@@ -60,7 +60,9 @@ pub fn parse_tls(pe: &[u8], image_base: u32, sections: &[PeSection]) -> Option<T
// +0x14 Characteristics (4)
let tls_section = sections.iter().find(|s| s.name == ".tls")?;
let off = tls_section.virtual_address as usize;
if off + 24 > pe.len() { return None; }
if off + 24 > pe.len() {
return None;
}
// Xbox 360 PE bodies are big-endian; this is consistent with how we
// parse the PE elsewhere (e.g. xref scanning reads BE u32 from PE).
@@ -69,8 +71,8 @@ pub fn parse_tls(pe: &[u8], image_base: u32, sections: &[PeSection]) -> Option<T
};
let raw_data_start = read_u32(off);
let raw_data_end = read_u32(off + 4);
let index_address = read_u32(off + 8);
let raw_data_end = read_u32(off + 4);
let index_address = read_u32(off + 8);
let callback_array = read_u32(off + 12);
let zero_fill_size = read_u32(off + 16);
let characteristics = read_u32(off + 20);
@@ -86,10 +88,14 @@ pub fn parse_tls(pe: &[u8], image_base: u32, sections: &[PeSection]) -> Option<T
let mut p = callback_array.wrapping_sub(image_base) as usize;
while p + 4 <= pe.len() {
let v = read_u32(p);
if v == 0 { break; }
if v == 0 {
break;
}
callbacks.push(TlsCallback { address: v });
p += 4;
if callbacks.len() >= 64 { break; } // sanity cap
if callbacks.len() >= 64 {
break;
} // sanity cap
}
}
@@ -137,15 +143,22 @@ mod tests {
let cb_va: u32 = 0x200;
// Directory fields:
let raw_start = 0x800u32;
let raw_end = 0x900u32;
let idx = 0x1000u32;
let raw_end = 0x900u32;
let idx = 0x1000u32;
let zero_fill = 0x40u32;
let chars = 0x0u32;
let cb_array = image_base + cb_va;
let chars = 0x0u32;
let cb_array = image_base + cb_va;
for (i, v) in [
image_base + raw_start, image_base + raw_end,
image_base + idx, cb_array, zero_fill, chars,
].iter().enumerate() {
image_base + raw_start,
image_base + raw_end,
image_base + idx,
cb_array,
zero_fill,
chars,
]
.iter()
.enumerate()
{
pe[tls_va as usize + i * 4..tls_va as usize + i * 4 + 4]
.copy_from_slice(&v.to_be_bytes());
}

View File

@@ -121,8 +121,11 @@ impl DiscImageDevice {
let node_l = u16::from_le_bytes([buffer[p], buffer[p + 1]]);
let node_r = u16::from_le_bytes([buffer[p + 2], buffer[p + 3]]);
let sector = u32::from_le_bytes([buffer[p + 4], buffer[p + 5], buffer[p + 6], buffer[p + 7]]) as u64;
let length = u32::from_le_bytes([buffer[p + 8], buffer[p + 9], buffer[p + 10], buffer[p + 11]]) as u64;
let sector =
u32::from_le_bytes([buffer[p + 4], buffer[p + 5], buffer[p + 6], buffer[p + 7]]) as u64;
let length =
u32::from_le_bytes([buffer[p + 8], buffer[p + 9], buffer[p + 10], buffer[p + 11]])
as u64;
let attributes = buffer[p + 12];
let name_length = buffer[p + 13] as usize;

View File

@@ -34,11 +34,16 @@ fn main() {
json_path.display()
)
});
let doc: serde_json::Value = serde_json::from_str(&raw).expect("export table is not valid JSON");
let doc: serde_json::Value =
serde_json::from_str(&raw).expect("export table is not valid JSON");
let out = Path::new(&env::var("OUT_DIR").unwrap()).join("ordinals.rs");
let mut f = fs::File::create(&out).unwrap();
writeln!(f, "/// Auto-generated from `docs/reference/xbox360-exports.json`.").unwrap();
writeln!(
f,
"/// Auto-generated from `docs/reference/xbox360-exports.json`."
)
.unwrap();
writeln!(
f,
"pub fn resolve_ordinal(lib: &str, ordinal: u16) -> Option<&'static str> {{"

View File

@@ -12,10 +12,13 @@ use std::time::Instant;
use anyhow::Result;
use clap::{Parser, Subcommand, ValueEnum};
use tracing::{debug, info, instrument, warn};
use tracing::{info, instrument, warn};
#[derive(Parser)]
#[command(name = "sylph-xexdb", about = "XEX static analysis: extract, disassemble, and build the analysis DB")]
#[command(
name = "sylph-xexdb",
about = "XEX static analysis: extract, disassemble, and build the analysis DB"
)]
struct Cli {
#[command(subcommand)]
command: Commands,
@@ -39,7 +42,6 @@ enum AnalyzeMode {
#[derive(Subcommand)]
enum Commands {
/// Display XEX header information
Info {
/// Path to XEX file
@@ -130,8 +132,11 @@ fn load_xex_data(path: &str) -> Result<Vec<u8>> {
if lower.ends_with(".iso") || lower.ends_with(".xiso") {
use sylpheed_xex::vfs::VfsDevice;
info!("detected disc image, extracting default.xex");
let disc = sylpheed_xex::vfs::disc_image::DiscImageDevice::open("disc", std::path::Path::new(path))
.map_err(|e| anyhow::anyhow!("Failed to open disc image: {}", e))?;
let disc = sylpheed_xex::vfs::disc_image::DiscImageDevice::open(
"disc",
std::path::Path::new(path),
)
.map_err(|e| anyhow::anyhow!("Failed to open disc image: {}", e))?;
disc.read_file("default.xex")
.map_err(|e| anyhow::anyhow!("Failed to extract default.xex from disc image: {}", e))
} else {
@@ -149,9 +154,26 @@ fn main() -> Result<()> {
Commands::Info { path } => cmd_info(&path),
Commands::Disasm { path, count, at } => cmd_disasm(&path, count, at),
Commands::Browse { path } => cmd_browse(&path),
Commands::Extract { path, output, db } => cmd_extract(&path, output.as_deref(), db.as_deref()),
Commands::Dis { path, output, db, json, analyze, max_indirect_candidates, quiet } =>
cmd_dis(&path, output.as_deref(), db.as_deref(), json.as_deref(), analyze, max_indirect_candidates, quiet),
Commands::Extract { path, output, db } => {
cmd_extract(&path, output.as_deref(), db.as_deref())
}
Commands::Dis {
path,
output,
db,
json,
analyze,
max_indirect_candidates,
quiet,
} => cmd_dis(
&path,
output.as_deref(),
db.as_deref(),
json.as_deref(),
analyze,
max_indirect_candidates,
quiet,
),
}
}
@@ -188,12 +210,23 @@ fn cmd_info(path: &str) -> Result<()> {
if let Some(ref ffi) = header.file_format_info {
println!("\n=== File Format ===");
println!("Encryption: {}", match ffi.encryption_type {
0 => "None", 1 => "Normal (AES)", _ => "Unknown"
});
println!("Compression: {}", match ffi.compression_type {
0 => "None", 1 => "Basic", 2 => "Normal (LZX)", _ => "Unknown"
});
println!(
"Encryption: {}",
match ffi.encryption_type {
0 => "None",
1 => "Normal (AES)",
_ => "Unknown",
}
);
println!(
"Compression: {}",
match ffi.compression_type {
0 => "None",
1 => "Basic",
2 => "Normal (LZX)",
_ => "Unknown",
}
);
if !ffi.basic_blocks.is_empty() {
println!("Basic blocks: {}", ffi.basic_blocks.len());
}
@@ -216,18 +249,27 @@ fn cmd_info(path: &str) -> Result<()> {
if !header.import_libraries.is_empty() {
println!("\n=== Import Libraries ===");
for lib in &header.import_libraries {
println!(" {} (v{:#010x}, {} imports)", lib.name, lib.version_cur, lib.imports.len());
println!(
" {} (v{:#010x}, {} imports)",
lib.name,
lib.version_cur,
lib.imports.len()
);
}
}
info!(wall_ms = started.elapsed().as_millis() as u64, "info complete");
info!(
wall_ms = started.elapsed().as_millis() as u64,
"info complete"
);
Ok(())
}
/// Clap parser for `--at` — accepts decimal, 0x-prefixed hex, or bare hex.
fn parse_hex_u32(s: &str) -> Result<u32, String> {
let t = s.trim();
let (digits, radix) = if let Some(rest) = t.strip_prefix("0x").or_else(|| t.strip_prefix("0X")) {
let (digits, radix) = if let Some(rest) = t.strip_prefix("0x").or_else(|| t.strip_prefix("0X"))
{
(rest, 16)
} else if t.chars().all(|c| c.is_ascii_digit()) {
(t, 10)
@@ -250,14 +292,25 @@ fn cmd_disasm(path: &str, count: usize, at: Option<u32>) -> Result<()> {
let base = sylpheed_xex::loader::get_image_base(&header)
.ok_or_else(|| anyhow::anyhow!("No image base found in XEX2 header"))?;
info!(entry = format_args!("{:#010x}", entry), base = format_args!("{:#010x}", base), "XEX entry/base");
info!(
entry = format_args!("{:#010x}", entry),
base = format_args!("{:#010x}", base),
"XEX entry/base"
);
let image_data = sylpheed_xex::loader::load_image(&data, &header)?;
info!(bytes = image_data.len(), "image decompressed");
let start = at.unwrap_or(entry);
let label = if at.is_some() { "requested address" } else { "entry point" };
println!("Disassembly from {} {:#010x} ({} instructions):\n", label, start, count);
let label = if at.is_some() {
"requested address"
} else {
"entry point"
};
println!(
"Disassembly from {} {:#010x} ({} instructions):\n",
label, start, count
);
if start < base {
return Err(anyhow::anyhow!(
@@ -281,7 +334,10 @@ fn cmd_disasm(path: &str, count: usize, at: Option<u32>) -> Result<()> {
println!(" {:#010x}: {}", addr, text);
}
info!(wall_ms = started.elapsed().as_millis() as u64, "disasm complete");
info!(
wall_ms = started.elapsed().as_millis() as u64,
"disasm complete"
);
Ok(())
}
@@ -290,8 +346,9 @@ fn cmd_disasm(path: &str, count: usize, at: Option<u32>) -> Result<()> {
fn cmd_browse(path: &str) -> Result<()> {
use sylpheed_xex::vfs::VfsDevice;
let disc = sylpheed_xex::vfs::disc_image::DiscImageDevice::open("disc", std::path::Path::new(path))
.map_err(|e| anyhow::anyhow!("Failed to open disc image: {}", e))?;
let disc =
sylpheed_xex::vfs::disc_image::DiscImageDevice::open("disc", std::path::Path::new(path))
.map_err(|e| anyhow::anyhow!("Failed to open disc image: {}", e))?;
println!("=== XISO Contents: {} ===", path);
match disc.list_root() {
@@ -307,15 +364,22 @@ fn cmd_browse(path: &str) -> Result<()> {
Ok(())
}
/// Helper: load XEX, parse header, decompress PE, resolve imports, parse sections.
#[instrument(skip_all, fields(path = %path))]
/// Load a XEX and prepare it for analysis.
/// Header, decompressed image, PE sections, raw container bytes.
type Prepared = (
sylpheed_xex::Xex2Header,
Vec<u8>,
Vec<sylpheed_xex::pe::PeSection>,
Vec<u8>,
);
/// Load a XEX and prepare it for analysis: parse the header, decompress the PE,
/// resolve imports, parse sections.
///
/// Returns the parsed header, the decompressed image, its sections, and the
/// **raw XEX bytes**. The raw bytes are needed because optional-header values
/// are file offsets into the container, not image VAs — the resource table
/// (and so the embedded XDBF package) is only reachable through them.
fn load_and_prepare(path: &str) -> Result<(sylpheed_xex::Xex2Header, Vec<u8>, Vec<sylpheed_xex::pe::PeSection>, Vec<u8>)> {
/// The **raw XEX bytes** come back too, because optional-header values are file
/// offsets into the container rather than image VAs — the resource table, and so
/// the embedded XDBF package, is only reachable through them.
#[instrument(skip_all, fields(path = %path))]
fn load_and_prepare(path: &str) -> Result<Prepared> {
let data = load_xex_data(path)?;
let mut header = sylpheed_xex::loader::parse_xex2_header(&data)?;
@@ -352,7 +416,11 @@ fn cmd_extract(path: &str, output_dir: Option<&str>, db_path: Option<&str>) -> R
let entry = sylpheed_xex::loader::get_entry_point(&header).unwrap();
let base = sylpheed_xex::loader::get_image_base(&header).unwrap();
let image_size = header.security_info.as_ref().map(|s| s.image_size).unwrap_or(0);
let image_size = header
.security_info
.as_ref()
.map(|s| s.image_size)
.unwrap_or(0);
// Build JSON-serializable info struct
#[derive(Serialize)]
@@ -382,11 +450,15 @@ fn cmd_extract(path: &str, output_dir: Option<&str>, db_path: Option<&str>) -> R
let input_path = std::path::Path::new(path);
let out_dir = match output_dir {
Some(d) => std::path::PathBuf::from(d),
None => input_path.parent().unwrap_or(std::path::Path::new(".")).to_path_buf(),
None => input_path
.parent()
.unwrap_or(std::path::Path::new("."))
.to_path_buf(),
};
std::fs::create_dir_all(&out_dir)?;
let stem = input_path.file_stem()
let stem = input_path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("output");
@@ -406,11 +478,22 @@ fn cmd_extract(path: &str, output_dir: Option<&str>, db_path: Option<&str>) -> R
info!(path = %json_path.display(), "wrote metadata JSON");
// Print summary
let total_imports: usize = header.import_libraries.iter().map(|l| l.imports.len()).sum();
println!("Extracted: {} sections, {} import libraries ({} imports)",
sections.len(), header.import_libraries.len(), total_imports);
let total_imports: usize = header
.import_libraries
.iter()
.map(|l| l.imports.len())
.sum();
println!(
"Extracted: {} sections, {} import libraries ({} imports)",
sections.len(),
header.import_libraries.len(),
total_imports
);
if let Some(ref ei) = header.execution_info {
println!("Title ID: 0x{:08X} Media ID: 0x{:08X}", ei.title_id, ei.media_id);
println!(
"Title ID: 0x{:08X} Media ID: 0x{:08X}",
ei.title_id, ei.media_id
);
}
// Write base tables to SQLite if requested
@@ -468,27 +551,38 @@ fn cmd_dis(
info!(thunks = import_map.len(), "resolved import thunks");
// Function analysis (with .pdata-validated boundaries when present)
let code_sections: Vec<(u32, u32, u32)> = sections.iter()
let code_sections: Vec<(u32, u32, u32)> = sections
.iter()
.filter(|s| s.is_code())
.map(|s| (s.virtual_address, s.virtual_size, s.flags))
.collect();
let pdata_entries = sylpheed_xex::pdata::parse_pdata(&pe_image, base, &sections);
info!(pdata_entries = pdata_entries.len(), "parsed .pdata RUNTIME_FUNCTION entries");
info!(
pdata_entries = pdata_entries.len(),
"parsed .pdata RUNTIME_FUNCTION entries"
);
let func_analysis = sylpheed_xexdb::func::analyze_with_pdata(
&pe_image, base, entry, &code_sections, &pdata_entries,
&pe_image,
base,
entry,
&code_sections,
&pdata_entries,
);
info!(
functions = func_analysis.functions.len(),
pdata_validated = func_analysis.functions.values().filter(|f| f.pdata_validated).count(),
pdata_validated = func_analysis
.functions
.values()
.filter(|f| f.pdata_validated)
.count(),
"function detection complete",
);
// M12 — switch / jump-table recovery. Emits one `jt` xref per distinct
// case body so the case bodies stop looking unreachable, and reports the
// table extents so the linear disassembler can flag them as data.
let jump_tables = sylpheed_xexdb::jumptables::analyze(
&pe_image, base, &sections, &func_analysis,
);
let jump_tables =
sylpheed_xexdb::jumptables::analyze(&pe_image, base, &sections, &func_analysis);
let jt_data_words = sylpheed_xexdb::jumptables::data_word_addresses(&jump_tables);
info!(
jump_tables = jump_tables.len(),
@@ -499,7 +593,13 @@ fn cmd_dis(
// Cross-reference analysis
let mut xref_result = sylpheed_xexdb::xref::analyze_xrefs_skipping(
&pe_image, base, entry, &sections, &func_analysis, &import_map, &jt_data_words,
&pe_image,
base,
entry,
&sections,
&func_analysis,
&import_map,
&jt_data_words,
);
// Feed the recovered `switch` edges into the xref graph, so case bodies
@@ -507,7 +607,8 @@ fn cmd_dis(
let mut jt_edges = 0usize;
for jt in &jump_tables {
for target in jt.distinct_targets() {
xref_result.xrefs
xref_result
.xrefs
.entry(target)
.or_default()
.push(sylpheed_xexdb::xref::Xref {
@@ -515,12 +616,14 @@ fn cmd_dis(
kind: sylpheed_xexdb::xref::XrefKind::JumpTable,
addr_mode: None,
});
xref_result.labels
xref_result
.labels
.entry(target)
.or_insert_with(|| format!("case_{target:08X}"));
jt_edges += 1;
}
xref_result.labels
xref_result
.labels
.entry(jt.table_address)
.or_insert_with(|| format!("jpt_{:08X}", jt.table_address));
}
@@ -548,9 +651,16 @@ fn cmd_dis(
let vptr_block_boundaries: std::collections::HashSet<u32> =
xref_result.labels.keys().copied().collect();
let mut vtable_anchors = sylpheed_xexdb::vtables::scan_vptr_write_constants(
&pe_image, base, &vptr_anchor_funcs, &sections, &vptr_block_boundaries,
&pe_image,
base,
&vptr_anchor_funcs,
&sections,
&vptr_block_boundaries,
);
info!(
vtable_anchors = vtable_anchors.len(),
"vptr-write anchor scan complete"
);
info!(vtable_anchors = vtable_anchors.len(), "vptr-write anchor scan complete");
// M13 — authoritative MSVC RTTI walk. Every `vftable[-1] -> COL` link the
// linker emitted is an anchor the heuristic scan must not miss, and the
@@ -566,7 +676,11 @@ fn cmd_dis(
);
let mut vtables = sylpheed_xexdb::vtables::analyze_with_anchors(
&pe_image, base, &sections, &function_starts, &vtable_anchors,
&pe_image,
base,
&sections,
&function_starts,
&vtable_anchors,
);
let named = sylpheed_xexdb::vtables::apply_rtti_names(&mut vtables, &rtti);
let vtables = vtables;
@@ -584,11 +698,19 @@ fn cmd_dis(
// emits one xref edge per resolvable site. Inserted into xrefs as
// kind='ind_call'.
let indirect_edges = sylpheed_xexdb::indirect::analyze(
&pe_image, base, &func_analysis, &vtables, &xref_result.labels,
&pe_image,
base,
&func_analysis,
&vtables,
&xref_result.labels,
);
info!(
indirect_edges = indirect_edges.len(),
"indirect-dispatch scan complete"
);
info!(indirect_edges = indirect_edges.len(), "indirect-dispatch scan complete");
for edge in &indirect_edges {
xref_result.xrefs
xref_result
.xrefs
.entry(edge.target)
.or_default()
.push(sylpheed_xexdb::xref::Xref {
@@ -613,14 +735,22 @@ fn cmd_dis(
// Generic function-pointer-array scan (M8 + M11). Re-emits M3 vtables
// plus dispatch tables and static-init tables in `.rdata`.
let mut fparrays = sylpheed_xexdb::funcptr_arrays::analyze(
&pe_image, base, &sections, &function_starts, &vtables,
&pe_image,
base,
&sections,
&function_starts,
&vtables,
);
// M11.5 — static-init driver chain detection. Replaces M11's prologue
// heuristic with a structurally-grounded result where the driver
// function shape matches.
let static_init = sylpheed_xexdb::static_init::analyze(
&pe_image, base, &sections, &func_analysis, &function_starts,
&pe_image,
base,
&sections,
&func_analysis,
&function_starts,
&xref_result.labels,
);
info!(
@@ -639,7 +769,10 @@ fn cmd_dis(
}
info!(
funcptr_arrays = fparrays.len(),
dispatch_tables = fparrays.iter().filter(|a| a.kind == "dispatch_table").count(),
dispatch_tables = fparrays
.iter()
.filter(|a| a.kind == "dispatch_table")
.count(),
static_inits = fparrays.iter().filter(|a| a.kind == "static_init").count(),
"function-pointer array set finalised",
);
@@ -655,12 +788,24 @@ fn cmd_dis(
// M5.5 — typed indirect-dispatch resolution (this->vptr → method).
let typed_ind = sylpheed_xexdb::ind_dispatch_typed::analyze(
&pe_image, base, &func_analysis, &vtables, &xref_result.labels,
&pe_image,
base,
&func_analysis,
&vtables,
&xref_result.labels,
max_indirect_candidates,
);
let single = typed_ind.dispatches.iter().filter(|d| d.total_candidates == 1).count();
let single = typed_ind
.dispatches
.iter()
.filter(|d| d.total_candidates == 1)
.count();
let multi = typed_ind.dispatches.len() - single;
let typed_edges: usize = typed_ind.dispatches.iter().map(|d| d.method_pcs.len()).sum();
let typed_edges: usize = typed_ind
.dispatches
.iter()
.map(|d| d.method_pcs.len())
.sum();
info!(
vptr_writes = typed_ind.vptr_writes.len(),
dispatches = typed_ind.dispatches.len(),
@@ -675,7 +820,8 @@ fn cmd_dis(
// possibilities.
for d in &typed_ind.dispatches {
for &method_pc in &d.method_pcs {
xref_result.xrefs
xref_result
.xrefs
.entry(method_pc)
.or_default()
.push(sylpheed_xexdb::xref::Xref {
@@ -707,7 +853,10 @@ fn cmd_dis(
Some(x)
});
if xdbf.is_none() && !resources.is_empty() {
info!(resources = resources.len(), "resource table present but no XDBF package");
info!(
resources = resources.len(),
"resource table present but no XDBF package"
);
}
// Build DisasmInfo
@@ -728,7 +877,11 @@ fn cmd_dis(
let mut w = sylpheed_xexdb::DbWriter::open_fresh(std::path::Path::new(db))?;
w.write_base(&disasm_info)?;
w.ingest_instructions(
&pe_image, &disasm_info, &func_analysis, &xref_result.labels, &jt_data_words,
&pe_image,
&disasm_info,
&func_analysis,
&xref_result.labels,
&jt_data_words,
)?;
w.write_analysis_results(
&pe_image,
@@ -772,12 +925,20 @@ fn cmd_dis(
let mut out = std::io::BufWriter::new(std::fs::File::create(json)?);
let mut total: u64 = 0;
for section in &sections {
if !section.is_code() { continue; }
if !section.is_code() {
continue;
}
let abs_start = base + section.virtual_address;
let abs_end = abs_start + section.virtual_size;
let items = sylpheed_xexdb::enrich_section(
&pe_image, base, &section.name, abs_start, abs_end,
&func_analysis, &xref_result.labels, &jt_data_words,
&pe_image,
base,
&section.name,
abs_start,
abs_end,
&func_analysis,
&xref_result.labels,
&jt_data_words,
);
total += sylpheed_xexdb::sinks::json::write_jsonl(&mut out, items)?;
}
@@ -808,11 +969,13 @@ fn cmd_dis(
}
}
info!(wall_ms = started.elapsed().as_millis() as u64, "dis complete");
info!(
wall_ms = started.elapsed().as_millis() as u64,
"dis complete"
);
Ok(())
}
#[cfg(test)]
mod tests {
use super::parse_hex_u32;

File diff suppressed because it is too large Load Diff

View File

@@ -345,7 +345,10 @@ mod rtti_name_tests {
#[test]
fn plain_class_in_namespace() {
assert_eq!(demangle_type_descriptor(".?AVSilph@silph@@").as_deref(), Some("silph::Silph"));
assert_eq!(
demangle_type_descriptor(".?AVSilph@silph@@").as_deref(),
Some("silph::Silph")
);
}
#[test]
@@ -358,12 +361,16 @@ mod rtti_name_tests {
#[test]
fn global_scope_class() {
assert_eq!(demangle_type_descriptor(".?AVexception@std@@").as_deref(), Some("std::exception"));
assert_eq!(
demangle_type_descriptor(".?AVexception@std@@").as_deref(),
Some("std::exception")
);
}
#[test]
fn anonymous_namespace_is_named() {
let got = demangle_type_descriptor(".?AVAct_Stop@?A0x5cc05762@unnamed_namespaces@@").unwrap();
let got =
demangle_type_descriptor(".?AVAct_Stop@?A0x5cc05762@unnamed_namespaces@@").unwrap();
assert!(got.ends_with("Act_Stop"), "got {got}");
assert!(got.starts_with("unnamed_namespaces"), "got {got}");
}

View File

@@ -86,10 +86,16 @@ mod tests {
fn fi(start: u32, end: u32) -> FuncInfo {
FuncInfo {
start, end,
frame_size: 0, saved_gprs: 0, is_leaf: true, is_saverestore: false,
pdata_validated: true, pdata_length: Some(end - start),
pdata_prolog_length: None, has_eh: false,
start,
end,
frame_size: 0,
saved_gprs: 0,
is_leaf: true,
is_saverestore: false,
pdata_validated: true,
pdata_length: Some(end - start),
pdata_prolog_length: None,
has_eh: false,
}
}
@@ -113,18 +119,29 @@ mod tests {
let labels = HashMap::new();
let data_words = BTreeSet::new();
let got: Vec<(u32, Option<u32>)> = enrich_section(
&image, image_base, ".text", image_base, image_base + 24,
&fa, &labels, &data_words,
).map(|r| (r.item.addr, r.function)).collect();
&image,
image_base,
".text",
image_base,
image_base + 24,
&fa,
&labels,
&data_words,
)
.map(|r| (r.item.addr, r.function))
.collect();
assert_eq!(got, vec![
(image_base, Some(image_base)), // inside f0
(image_base + 4, Some(image_base)), // inside f0
(image_base + 8, None), // gap — was wrongly f0
(image_base + 12, None), // gap — was wrongly f0
(image_base + 16, Some(image_base + 16)), // f1 starts
(image_base + 20, Some(image_base + 16)),
]);
assert_eq!(
got,
vec![
(image_base, Some(image_base)), // inside f0
(image_base + 4, Some(image_base)), // inside f0
(image_base + 8, None), // gap — was wrongly f0
(image_base + 12, None), // gap — was wrongly f0
(image_base + 16, Some(image_base + 16)), // f1 starts
(image_base + 20, Some(image_base + 16)),
]
);
}
/// A function starting exactly at its predecessor's `end_address` must be
@@ -137,18 +154,33 @@ mod tests {
functions.insert(image_base, fi(image_base, image_base + 8));
functions.insert(image_base + 8, fi(image_base + 8, image_base + 16));
let fa = FuncAnalysis {
functions, save_gpr_base: None, restore_gpr_base: None,
functions,
save_gpr_base: None,
restore_gpr_base: None,
pdata_entries: Vec::new(),
};
let labels = HashMap::new();
let data_words = BTreeSet::new();
let got: Vec<Option<u32>> = enrich_section(
&image, image_base, ".text", image_base, image_base + 16,
&fa, &labels, &data_words,
).map(|r| r.function).collect();
assert_eq!(got, vec![
Some(image_base), Some(image_base),
Some(image_base + 8), Some(image_base + 8),
]);
&image,
image_base,
".text",
image_base,
image_base + 16,
&fa,
&labels,
&data_words,
)
.map(|r| r.function)
.collect();
assert_eq!(
got,
vec![
Some(image_base),
Some(image_base),
Some(image_base + 8),
Some(image_base + 8),
]
);
}
}

View File

@@ -49,9 +49,9 @@
use sylpheed_xex::pe::PeSection;
const MAGIC_OLD: u32 = 0x1993_0520;
const MAGIC_V21: u32 = 0x1993_0521;
const MAGIC_V22: u32 = 0x1993_0522;
const MAGIC_OLD: u32 = 0x1993_0520;
const MAGIC_V21: u32 = 0x1993_0521;
const MAGIC_V22: u32 = 0x1993_0522;
#[derive(Debug, Clone, Copy)]
pub struct UnwindMapEntry {
@@ -85,41 +85,56 @@ pub struct EhFuncInfo {
}
#[tracing::instrument(skip_all, fields(image_base = format_args!("{:#010x}", image_base)))]
pub fn analyze(
pe: &[u8],
image_base: u32,
sections: &[PeSection],
) -> Vec<EhFuncInfo> {
pub fn analyze(pe: &[u8], image_base: u32, sections: &[PeSection]) -> Vec<EhFuncInfo> {
let started = std::time::Instant::now();
let mut out: Vec<EhFuncInfo> = Vec::new();
// Compute the union of valid VA ranges across all sections — used to
// sanity-check internal pointers in the FuncInfo records.
let valid_ranges: Vec<(u32, u32)> = sections.iter()
.map(|s| (image_base + s.virtual_address,
image_base + s.virtual_address + s.virtual_size))
let valid_ranges: Vec<(u32, u32)> = sections
.iter()
.map(|s| {
(
image_base + s.virtual_address,
image_base + s.virtual_address + s.virtual_size,
)
})
.collect();
let in_valid = |va: u32| valid_ranges.iter().any(|(lo, hi)| va >= *lo && va < *hi);
let read_u32 = |abs: u32| -> Option<u32> {
let off = abs.wrapping_sub(image_base) as usize;
if off + 4 > pe.len() { return None; }
Some(u32::from_be_bytes([pe[off], pe[off + 1], pe[off + 2], pe[off + 3]]))
if off + 4 > pe.len() {
return None;
}
Some(u32::from_be_bytes([
pe[off],
pe[off + 1],
pe[off + 2],
pe[off + 3],
]))
};
let read_i32 = |abs: u32| -> Option<i32> { read_u32(abs).map(|u| u as i32) };
for section in sections {
if section.name != ".rdata" { continue; }
if section.name != ".rdata" {
continue;
}
let raw_start = section.virtual_address as usize;
let raw_end = (section.virtual_address + section.virtual_size) as usize;
if raw_end > pe.len() { continue; }
if raw_end > pe.len() {
continue;
}
let bytes = &pe[raw_start..raw_end.min(pe.len())];
let va_base = image_base + section.virtual_address;
// Walk on 4-byte alignment looking for the magic.
let mut i = 0;
while i + 4 <= bytes.len() {
if !i.is_multiple_of(4) { i += 1; continue; }
if !i.is_multiple_of(4) {
i += 1;
continue;
}
let m = u32::from_be_bytes([bytes[i], bytes[i + 1], bytes[i + 2], bytes[i + 3]]);
if m == MAGIC_OLD || m == MAGIC_V21 || m == MAGIC_V22 {
let addr = va_base + i as u32;
@@ -152,23 +167,35 @@ fn parse_funcinfo(
read_i32: &impl Fn(u32) -> Option<i32>,
in_valid: &impl Fn(u32) -> bool,
) -> Option<EhFuncInfo> {
let max_state = read_i32(addr + 0x04)?;
let p_unwind_map = read_u32(addr + 0x08)?;
let n_try_blocks = read_u32(addr + 0x0C)?;
let p_try_block_map = read_u32(addr + 0x10)?;
let n_ip_map_entries = read_u32(addr + 0x14)?;
let max_state = read_i32(addr + 0x04)?;
let p_unwind_map = read_u32(addr + 0x08)?;
let n_try_blocks = read_u32(addr + 0x0C)?;
let p_try_block_map = read_u32(addr + 0x10)?;
let n_ip_map_entries = read_u32(addr + 0x14)?;
let p_ip_to_state_map = read_u32(addr + 0x18)?;
// Sanity caps: real FuncInfo records have max_state ≤ a few thousand,
// n_try_blocks ≤ a few hundred. Reject obviously bogus values that
// happened to alias the magic.
if !(0..=10_000).contains(&max_state) { return None; }
if n_try_blocks > 1_000 { return None; }
if n_ip_map_entries > 100_000 { return None; }
if !(0..=10_000).contains(&max_state) {
return None;
}
if n_try_blocks > 1_000 {
return None;
}
if n_ip_map_entries > 100_000 {
return None;
}
// Pointers must either be NULL or land in a valid section.
if p_unwind_map != 0 && !in_valid(p_unwind_map) { return None; }
if p_try_block_map != 0 && !in_valid(p_try_block_map) { return None; }
if p_ip_to_state_map != 0 && !in_valid(p_ip_to_state_map) { return None; }
if p_unwind_map != 0 && !in_valid(p_unwind_map) {
return None;
}
if p_try_block_map != 0 && !in_valid(p_try_block_map) {
return None;
}
if p_ip_to_state_map != 0 && !in_valid(p_ip_to_state_map) {
return None;
}
let (p_es_type_list, eh_flags) = if magic == MAGIC_V21 {
(read_u32(addr + 0x1C), None)
@@ -185,7 +212,10 @@ fn parse_funcinfo(
let p = p_unwind_map.wrapping_add((i * 8) as u32);
let to_state = read_i32(p)?;
let action_pc = read_u32(p + 4)?;
unwind_map.push(UnwindMapEntry { to_state, action_pc });
unwind_map.push(UnwindMapEntry {
to_state,
action_pc,
});
}
}
@@ -194,13 +224,17 @@ fn parse_funcinfo(
if p_try_block_map != 0 && n_try_blocks > 0 {
for i in 0..n_try_blocks {
let p = p_try_block_map.wrapping_add(i * 20);
let try_low = read_i32(p)?;
let try_high = read_i32(p + 4)?;
let catch_high = read_i32(p + 8)?;
let n_catches = read_u32(p + 12)?;
let p_handler_a = read_u32(p + 16)?;
let try_low = read_i32(p)?;
let try_high = read_i32(p + 4)?;
let catch_high = read_i32(p + 8)?;
let n_catches = read_u32(p + 12)?;
let p_handler_a = read_u32(p + 16)?;
try_blocks.push(TryBlockMapEntry {
try_low, try_high, catch_high, n_catches, p_handler_array: p_handler_a,
try_low,
try_high,
catch_high,
n_catches,
p_handler_array: p_handler_a,
});
}
}
@@ -229,8 +263,10 @@ mod tests {
fn mk_section(name: &str, va: u32, size: u32) -> PeSection {
PeSection {
name: name.into(),
virtual_address: va, virtual_size: size,
raw_offset: va, raw_size: size,
virtual_address: va,
virtual_size: size,
raw_offset: va,
raw_size: size,
flags: 0x4000_0040,
}
}
@@ -254,17 +290,17 @@ mod tests {
let unwind_off = (rdata_va + 0x80) as usize;
let unwind_va = image_base + rdata_va + 0x80;
write_be(&mut pe, fi_off, MAGIC_OLD); // magic
write_be_i32(&mut pe, fi_off + 4, 2); // maxState
write_be(&mut pe, fi_off + 8, unwind_va); // pUnwindMap
write_be(&mut pe, fi_off + 12, 0); // nTryBlocks
write_be(&mut pe, fi_off + 16, 0); // pTryBlockMap
write_be(&mut pe, fi_off + 20, 0); // nIPMapEntries
write_be(&mut pe, fi_off + 24, 0); // pIPtoStateMap
write_be(&mut pe, fi_off, MAGIC_OLD); // magic
write_be_i32(&mut pe, fi_off + 4, 2); // maxState
write_be(&mut pe, fi_off + 8, unwind_va); // pUnwindMap
write_be(&mut pe, fi_off + 12, 0); // nTryBlocks
write_be(&mut pe, fi_off + 16, 0); // pTryBlockMap
write_be(&mut pe, fi_off + 20, 0); // nIPMapEntries
write_be(&mut pe, fi_off + 24, 0); // pIPtoStateMap
// Two unwind entries.
write_be_i32(&mut pe, unwind_off, -1); // to_state
write_be(&mut pe, unwind_off + 4, image_base + 0x500); // action_pc
write_be_i32(&mut pe, unwind_off, -1); // to_state
write_be(&mut pe, unwind_off + 4, image_base + 0x500); // action_pc
write_be_i32(&mut pe, unwind_off + 8, 0);
write_be(&mut pe, unwind_off + 12, image_base + 0x600);
@@ -288,7 +324,7 @@ mod tests {
let mut pe = vec![0u8; 0x4000];
let fi_off = (rdata_va + 0x10) as usize;
write_be(&mut pe, fi_off, MAGIC_OLD);
write_be_i32(&mut pe, fi_off + 4, 0xFFFF); // bogus maxState
write_be_i32(&mut pe, fi_off + 4, 0xFFFF); // bogus maxState
let sections = vec![mk_section(".rdata", rdata_va, 0x100)];
let recs = analyze(&pe, image_base, &sections);
assert_eq!(recs.len(), 0);

View File

@@ -9,7 +9,7 @@ use sylpheed_xex::pe::PeSection;
use crate::disasm::enrich_section;
use crate::func::FuncAnalysis;
use crate::sinks::text::write_instr_line;
use crate::xref::{XrefKind, Xref, XrefMap, resolve_source_label};
use crate::xref::{Xref, XrefKind, XrefMap, resolve_source_label};
/// Metadata passed to the formatter (avoids exposing full Xex2Header internals).
pub struct DisasmInfo<'a> {
@@ -40,29 +40,53 @@ pub fn write_asm(
data_words: &BTreeSet<u32>,
) -> anyhow::Result<()> {
// Header
writeln!(out, "; ============================================================================")?;
writeln!(
out,
"; ============================================================================"
)?;
writeln!(out, "; Xbox 360 Disassembly — generated by xenia-rs")?;
if let Some(name) = info.original_pe_name {
writeln!(out, "; Original PE: {name}")?;
}
if let (Some(title_id), Some(media_id)) = (info.title_id, info.media_id) {
writeln!(out, "; Title ID: 0x{title_id:08X} Media ID: 0x{media_id:08X}")?;
writeln!(
out,
"; Title ID: 0x{title_id:08X} Media ID: 0x{media_id:08X}"
)?;
}
writeln!(out, "; Image base: 0x{:08X} Entry point: 0x{:08X}", info.image_base, info.entry_point)?;
writeln!(out, "; Functions detected: {}", func_analysis.functions.len())?;
writeln!(out, "; ============================================================================")?;
writeln!(
out,
"; Image base: 0x{:08X} Entry point: 0x{:08X}",
info.image_base, info.entry_point
)?;
writeln!(
out,
"; Functions detected: {}",
func_analysis.functions.len()
)?;
writeln!(
out,
"; ============================================================================"
)?;
writeln!(out)?;
// Import declarations
if !info.import_libraries.is_empty() {
writeln!(out, "; ── Imports ─────────────────────────────────────────────────────────────────")?;
writeln!(
out,
"; ── Imports ─────────────────────────────────────────────────────────────────"
)?;
for lib in info.import_libraries {
writeln!(out, "; Library: {}", lib.name)?;
for imp in &lib.imports {
let resolved = crate::resolve_ordinal(&lib.name, imp.ordinal);
let name = resolved.unwrap_or("???");
let kind = if imp.record_type == 1 { "thunk" } else { "var" };
writeln!(out, "; [{kind}] 0x{:08X} ordinal 0x{:04X} = {}", imp.address, imp.ordinal, name)?;
writeln!(
out,
"; [{kind}] 0x{:08X} ordinal 0x{:04X} = {}",
imp.address, imp.ordinal, name
)?;
}
}
writeln!(out)?;
@@ -70,8 +94,11 @@ pub fn write_asm(
// Disassemble each section
for section in info.sections {
writeln!(out, "; ── Section: {:8} VA=0x{:08X} Size=0x{:08X} Flags=0x{:08X} ──",
section.name, section.virtual_address, section.virtual_size, section.flags)?;
writeln!(
out,
"; ── Section: {:8} VA=0x{:08X} Size=0x{:08X} Flags=0x{:08X} ──",
section.name, section.virtual_address, section.virtual_size, section.flags
)?;
let va_start = section.virtual_address;
let va_end = va_start + section.virtual_size;
@@ -81,7 +108,8 @@ pub fn write_asm(
let section_labels_sorted: Vec<u32> = if !section.is_code() {
let sec_start = info.image_base + va_start;
let sec_end = info.image_base + va_end;
let mut addrs: Vec<u32> = labels.keys()
let mut addrs: Vec<u32> = labels
.keys()
.filter(|&&a| a >= sec_start && a < sec_end)
.copied()
.collect();
@@ -100,7 +128,13 @@ pub fn write_asm(
let abs_end = info.image_base + va_end;
let items = enrich_section(
pe, info.image_base, &section.name, abs_start, abs_end, func_analysis, labels,
pe,
info.image_base,
&section.name,
abs_start,
abs_end,
func_analysis,
labels,
data_words,
);
for ri in items {
@@ -112,9 +146,14 @@ pub fn write_asm(
writeln!(out, "; end function")?;
}
writeln!(out)?;
writeln!(out, "; ──────────────────────────────────────────────────────────────────────────")?;
writeln!(
out,
"; ──────────────────────────────────────────────────────────────────────────"
)?;
let lbl = labels.get(&abs_addr).cloned()
let lbl = labels
.get(&abs_addr)
.cloned()
.unwrap_or_else(|| format!("sub_{abs_addr:08X}"));
if fi.is_saverestore {
@@ -144,7 +183,10 @@ pub fn write_asm(
}
}
writeln!(out, "; ──────────────────────────────────────────────────────────────────────────")?;
writeln!(
out,
"; ──────────────────────────────────────────────────────────────────────────"
)?;
in_function = true;
}
@@ -152,7 +194,9 @@ pub fn write_asm(
if let Some(lbl) = labels.get(&abs_addr) {
if !func_analysis.is_function_start(abs_addr) {
writeln!(out)?;
if let Some(xref_lines) = format_xrefs(abs_addr, xrefs, func_analysis, labels) {
if let Some(xref_lines) =
format_xrefs(abs_addr, xrefs, func_analysis, labels)
{
for line in &xref_lines {
writeln!(out, "{line}")?;
}
@@ -204,22 +248,32 @@ pub fn write_asm(
line_end = lbl_va;
break;
}
if lbl_va >= line_end { break; }
if lbl_va >= line_end {
break;
}
}
let byte_count = (line_end - addr) as usize;
if off + byte_count > pe.len() { break; }
if off + byte_count > pe.len() {
break;
}
write!(out, " {:08X}: ", abs_addr)?;
for i in 0..byte_count {
write!(out, "{:02X}", pe[off + i])?;
if i % 4 == 3 { write!(out, " ")?; }
if i % 4 == 3 {
write!(out, " ")?;
}
}
// ASCII representation
let pad = (16 - byte_count) * 2 + (16 - byte_count) / 4;
write!(out, "{:>width$} |", "", width = pad)?;
for i in 0..byte_count {
let b = pe[off + i];
let ch = if b.is_ascii_graphic() || b == b' ' { b as char } else { '.' };
let ch = if b.is_ascii_graphic() || b == b' ' {
b as char
} else {
'.'
};
write!(out, "{ch}")?;
}
writeln!(out, "|")?;
@@ -242,7 +296,9 @@ fn format_xrefs(
labels: &HashMap<u32, String>,
) -> Option<Vec<String>> {
let refs = xrefs.get(&target)?;
if refs.is_empty() { return None; }
if refs.is_empty() {
return None;
}
let mut sorted: Vec<Xref> = refs.clone();
sorted.sort();
@@ -254,17 +310,47 @@ fn format_xrefs(
let calls = sorted.iter().filter(|x| x.kind == XrefKind::Call).count();
let jumps = sorted.iter().filter(|x| x.kind == XrefKind::Jump).count();
let branches = sorted.iter().filter(|x| x.kind == XrefKind::Branch).count();
let reads = sorted.iter().filter(|x| x.kind == XrefKind::DataRead).count();
let writes = sorted.iter().filter(|x| x.kind == XrefKind::DataWrite).count();
let data_refs = sorted.iter().filter(|x| x.kind == XrefKind::DataRef).count();
let reads = sorted
.iter()
.filter(|x| x.kind == XrefKind::DataRead)
.count();
let writes = sorted
.iter()
.filter(|x| x.kind == XrefKind::DataWrite)
.count();
let data_refs = sorted
.iter()
.filter(|x| x.kind == XrefKind::DataRef)
.count();
let mut summary_parts = Vec::new();
if calls > 0 { summary_parts.push(format!("{calls} call{}", if calls != 1 { "s" } else { "" })); }
if jumps > 0 { summary_parts.push(format!("{jumps} jump{}", if jumps != 1 { "s" } else { "" })); }
if branches > 0 { summary_parts.push(format!("{branches} branch{}", if branches != 1 { "es" } else { "" })); }
if reads > 0 { summary_parts.push(format!("{reads} read{}", if reads != 1 { "s" } else { "" })); }
if writes > 0 { summary_parts.push(format!("{writes} write{}", if writes != 1 { "s" } else { "" })); }
if data_refs > 0 { summary_parts.push(format!("{data_refs} ref{}", if data_refs != 1 { "s" } else { "" })); }
if calls > 0 {
summary_parts.push(format!("{calls} call{}", if calls != 1 { "s" } else { "" }));
}
if jumps > 0 {
summary_parts.push(format!("{jumps} jump{}", if jumps != 1 { "s" } else { "" }));
}
if branches > 0 {
summary_parts.push(format!(
"{branches} branch{}",
if branches != 1 { "es" } else { "" }
));
}
if reads > 0 {
summary_parts.push(format!("{reads} read{}", if reads != 1 { "s" } else { "" }));
}
if writes > 0 {
summary_parts.push(format!(
"{writes} write{}",
if writes != 1 { "s" } else { "" }
));
}
if data_refs > 0 {
summary_parts.push(format!(
"{data_refs} ref{}",
if data_refs != 1 { "s" } else { "" }
));
}
lines.push(format!("; XREF: {} ({})", summary_parts.join(", "), total));

View File

@@ -5,17 +5,17 @@
//! hence very likely function entry points.
//! 2. Scan the save/restore GPR helper region and label it.
//! 3. For each candidate entry, look for prologue patterns:
//! a) `mfspr rN, LR` (typically r0 or r12)
//! b) `bl __savegprlr_NN` (call into save stub)
//! c) `stwu r1, -N(r1)` (allocate stack frame)
//! a) `mfspr rN, LR` (typically r0 or r12)
//! b) `bl __savegprlr_NN` (call into save stub)
//! c) `stwu r1, -N(r1)` (allocate stack frame)
//! If a prologue is confirmed, record the function and its stack frame size.
//! 4. Walk forward from each function entry to find the epilogue:
//! a) `blr` (return)
//! b) `b __restgprlr_NN` (tail-branch into restore stub which returns)
//! a) `blr` (return)
//! b) `b __restgprlr_NN` (tail-branch into restore stub which returns)
//! Mark the function's end address.
//! 5. Detect leaf functions: `bl` targets that lack a prologue but eventually `blr`.
use std::collections::{HashMap, HashSet, BTreeMap};
use std::collections::{BTreeMap, HashMap, HashSet};
/// Information about a detected function.
#[derive(Debug, Clone)]
@@ -63,37 +63,53 @@ pub struct FuncAnalysis {
// ── Instruction field helpers ──────────────────────────────────────────────
fn op(instr: u32) -> u32 { (instr >> 26) & 0x3F }
fn op(instr: u32) -> u32 {
(instr >> 26) & 0x3F
}
fn bits(instr: u32, hi: u32, lo: u32) -> u32 {
(instr >> (31 - hi)) & ((1 << (hi - lo + 1)) - 1)
}
fn is_mfspr_lr(instr: u32) -> Option<u32> {
// mfspr rD, LR → opcode 31, xo=339, spr=8
if op(instr) != 31 { return None; }
if op(instr) != 31 {
return None;
}
let xo = bits(instr, 30, 21);
if xo != 339 { return None; }
if xo != 339 {
return None;
}
let spr = (bits(instr, 20, 16) << 5) | bits(instr, 15, 11);
if spr != 8 { return None; }
if spr != 8 {
return None;
}
Some(bits(instr, 10, 6)) // return rD
}
#[allow(dead_code)]
fn is_mtspr_lr(instr: u32) -> bool {
// mtspr LR, rS → opcode 31, xo=467, spr=8
if op(instr) != 31 { return false; }
if op(instr) != 31 {
return false;
}
let xo = bits(instr, 30, 21);
if xo != 467 { return false; }
if xo != 467 {
return false;
}
let spr = (bits(instr, 20, 16) << 5) | bits(instr, 15, 11);
spr == 8
}
fn is_stwu_r1(instr: u32) -> Option<i32> {
// stwu r1, d(r1) → opcode 37, rS=1, rA=1
if op(instr) != 37 { return None; }
if op(instr) != 37 {
return None;
}
let rs = bits(instr, 10, 6);
let ra = bits(instr, 15, 11);
if rs != 1 || ra != 1 { return None; }
if rs != 1 || ra != 1 {
return None;
}
let d = ((instr & 0xFFFF) as i16) as i32;
Some(d) // negative = frame allocation
}
@@ -108,9 +124,15 @@ fn is_bctr(instr: u32) -> bool {
fn is_bl(instr: u32) -> Option<u32> {
// bl target → opcode 18, LK=1, AA=0
if op(instr) != 18 { return None; }
if instr & 1 == 0 { return None; } // must have LK bit
if instr & 2 != 0 { return None; } // not absolute
if op(instr) != 18 {
return None;
}
if instr & 1 == 0 {
return None;
} // must have LK bit
if instr & 2 != 0 {
return None;
} // not absolute
// Return the signed offset
let li = instr & 0x03FFFFFC;
Some(li)
@@ -118,9 +140,15 @@ fn is_bl(instr: u32) -> Option<u32> {
fn is_b(instr: u32) -> Option<u32> {
// b target → opcode 18, LK=0, AA=0
if op(instr) != 18 { return None; }
if instr & 1 != 0 { return None; } // no LK bit
if instr & 2 != 0 { return None; } // not absolute
if op(instr) != 18 {
return None;
}
if instr & 1 != 0 {
return None;
} // no LK bit
if instr & 2 != 0 {
return None;
} // not absolute
Some(instr & 0x03FFFFFC)
}
@@ -140,8 +168,15 @@ fn b_target(instr: u32, addr: u32) -> Option<u32> {
fn read_instr(pe: &[u8], abs_addr: u32, image_base: u32) -> Option<u32> {
let off = abs_addr.wrapping_sub(image_base) as usize;
if off + 4 > pe.len() { return None; }
Some(u32::from_be_bytes([pe[off], pe[off+1], pe[off+2], pe[off+3]]))
if off + 4 > pe.len() {
return None;
}
Some(u32::from_be_bytes([
pe[off],
pe[off + 1],
pe[off + 2],
pe[off + 3],
]))
}
// ── Detect the save/restore GPR helper stubs ───────────────────────────────
@@ -165,12 +200,28 @@ fn find_saverestore_stubs(
let mut addr = start;
while addr + 4 * 18 < end {
// Check if this is `std r14, ...(r1)` — opcode 62 (std), rS=14, rA=1
let instr = match read_instr(pe, addr, image_base) { Some(i) => i, None => { addr += 4; continue; } };
if op(instr) == 62 && bits(instr, 10, 6) == 14 && bits(instr, 15, 11) == 1 && (instr & 3) == 0 {
let instr = match read_instr(pe, addr, image_base) {
Some(i) => i,
None => {
addr += 4;
continue;
}
};
if op(instr) == 62
&& bits(instr, 10, 6) == 14
&& bits(instr, 15, 11) == 1
&& (instr & 3) == 0
{
// Verify it's a cascade: r14, r15, ..., r31
let mut ok = true;
for i in 0u32..18 {
let check = match read_instr(pe, addr + i * 4, image_base) { Some(c) => c, None => { ok = false; break; } };
let check = match read_instr(pe, addr + i * 4, image_base) {
Some(c) => c,
None => {
ok = false;
break;
}
};
if op(check) != 62 || bits(check, 10, 6) != 14 + i || bits(check, 15, 11) != 1 {
ok = false;
break;
@@ -193,7 +244,9 @@ fn find_saverestore_stubs(
}
addr += 4;
}
if save_base.is_some() { break; }
if save_base.is_some() {
break;
}
}
(save_base, restore_base)
@@ -245,7 +298,8 @@ pub fn analyze_with_pdata(
pdata: &[sylpheed_xex::pdata::PdataEntry],
) -> FuncAnalysis {
let started = std::time::Instant::now();
let code_ranges: Vec<(u32, u32)> = code_sections.iter()
let code_ranges: Vec<(u32, u32)> = code_sections
.iter()
.map(|(va, sz, _)| (image_base + va, image_base + va + sz))
.collect();
@@ -262,11 +316,15 @@ pub fn analyze_with_pdata(
let mut saverestore_addrs: HashSet<u32> = HashSet::new();
if let Some(sb) = save_base {
// Save block: 18 std + stw + blr = 20 instructions
for i in 0..20 { saverestore_addrs.insert(sb + i * 4); }
for i in 0..20 {
saverestore_addrs.insert(sb + i * 4);
}
}
if let Some(rb) = restore_base {
// Restore block: 18 ld + lwz + mtspr + blr = 21 instructions
for i in 0..21 { saverestore_addrs.insert(rb + i * 4); }
for i in 0..21 {
saverestore_addrs.insert(rb + i * 4);
}
}
// 2. Collect all bl targets as candidate function entries.
@@ -278,12 +336,13 @@ pub fn analyze_with_pdata(
let mut addr = start;
while addr < end {
if let Some(instr) = read_instr(pe, addr, image_base)
&& let Some(target) = bl_target(instr, addr) {
// Don't count calls into save/restore stubs as function entries
if !saverestore_addrs.contains(&target) {
call_targets.insert(target);
}
&& let Some(target) = bl_target(instr, addr)
{
// Don't count calls into save/restore stubs as function entries
if !saverestore_addrs.contains(&target) {
call_targets.insert(target);
}
}
addr += 4;
}
}
@@ -311,7 +370,10 @@ pub fn analyze_with_pdata(
// within this one. Intra-function jumps and switch arms both stay inside
// the range and are therefore never nominated.
let pdata_sorted: Vec<(u32, u32)> = {
let mut v: Vec<(u32, u32)> = pdata.iter().map(|e| (e.begin_address, e.end_address())).collect();
let mut v: Vec<(u32, u32)> = pdata
.iter()
.map(|e| (e.begin_address, e.end_address()))
.collect();
v.sort_unstable();
v
};
@@ -380,7 +442,12 @@ pub fn analyze_with_pdata(
let pdata_entry = pdata_by_begin.get(&func_addr).copied();
if let Some(mut fi) = analyze_function(
pe, image_base, func_addr, &code_ranges, save_base, restore_base,
pe,
image_base,
func_addr,
&code_ranges,
save_base,
restore_base,
) {
if let Some(p) = pdata_entry {
fi.pdata_validated = true;
@@ -428,33 +495,39 @@ pub fn analyze_with_pdata(
// The save block is one cascade: entry at each rN, falls through to blr
// Treat as a single function with the first entry point
let pe_sb = pdata_by_begin.get(&sb).copied();
functions.insert(sb, FuncInfo {
start: sb,
end: sb + 20 * 4, // 18 std + stw r12 + blr
frame_size: 0,
saved_gprs: 18,
is_leaf: true,
is_saverestore: true,
pdata_validated: pe_sb.is_some(),
pdata_length: pe_sb.map(|p| p.function_length),
pdata_prolog_length: pe_sb.map(|p| p.prolog_length),
has_eh: pe_sb.map(|p| (p.flags & 0x2) != 0).unwrap_or(false),
});
functions.insert(
sb,
FuncInfo {
start: sb,
end: sb + 20 * 4, // 18 std + stw r12 + blr
frame_size: 0,
saved_gprs: 18,
is_leaf: true,
is_saverestore: true,
pdata_validated: pe_sb.is_some(),
pdata_length: pe_sb.map(|p| p.function_length),
pdata_prolog_length: pe_sb.map(|p| p.prolog_length),
has_eh: pe_sb.map(|p| (p.flags & 0x2) != 0).unwrap_or(false),
},
);
}
if let Some(rb) = restore_base {
let pe_rb = pdata_by_begin.get(&rb).copied();
functions.insert(rb, FuncInfo {
start: rb,
end: rb + 21 * 4, // 18 ld + lwz r12 + mtspr LR + blr
frame_size: 0,
saved_gprs: 18,
is_leaf: true,
is_saverestore: true,
pdata_validated: pe_rb.is_some(),
pdata_length: pe_rb.map(|p| p.function_length),
pdata_prolog_length: pe_rb.map(|p| p.prolog_length),
has_eh: pe_rb.map(|p| (p.flags & 0x2) != 0).unwrap_or(false),
});
functions.insert(
rb,
FuncInfo {
start: rb,
end: rb + 21 * 4, // 18 ld + lwz r12 + mtspr LR + blr
frame_size: 0,
saved_gprs: 18,
is_leaf: true,
is_saverestore: true,
pdata_validated: pe_rb.is_some(),
pdata_length: pe_rb.map(|p| p.function_length),
pdata_prolog_length: pe_rb.map(|p| p.prolog_length),
has_eh: pe_rb.map(|p| (p.flags & 0x2) != 0).unwrap_or(false),
},
);
}
// 5. Reconcile candidate starts against the linker's ground truth.
@@ -482,9 +555,13 @@ pub fn analyze_with_pdata(
.filter(|&addr| {
pdata_ranges
.binary_search_by(|&(s, e)| {
if addr < s { std::cmp::Ordering::Greater }
else if addr >= e { std::cmp::Ordering::Less }
else { std::cmp::Ordering::Equal }
if addr < s {
std::cmp::Ordering::Greater
} else if addr >= e {
std::cmp::Ordering::Less
} else {
std::cmp::Ordering::Equal
}
})
.is_ok()
})
@@ -534,7 +611,9 @@ pub fn analyze_with_pdata(
fn range_has_call(pe: &[u8], image_base: u32, start: u32, end: u32) -> bool {
let mut addr = start;
while addr < end {
let Some(instr) = read_instr(pe, addr, image_base) else { return false };
let Some(instr) = read_instr(pe, addr, image_base) else {
return false;
};
let opcode = op(instr);
// I-form / B-form with LK, and XL-form bclrl / bcctrl.
if (opcode == 18 || opcode == 16) && instr & 1 == 1 {
@@ -558,8 +637,12 @@ fn analyze_function(
restore_base: Option<u32>,
) -> Option<FuncInfo> {
// Verify the address is within a code section
let in_code = code_ranges.iter().any(|&(s, e)| func_addr >= s && func_addr < e);
if !in_code { return None; }
let in_code = code_ranges
.iter()
.any(|&(s, e)| func_addr >= s && func_addr < e);
if !in_code {
return None;
}
let instr0 = read_instr(pe, func_addr, image_base)?;
@@ -576,11 +659,13 @@ fn analyze_function(
// Check if next is bl to save stub
if let Some(target) = bl_target(instr1, func_addr + 4)
&& let Some(sb) = save_base
&& target >= sb && target < sb + 18 * 4 {
let idx = (target - sb) / 4;
saved_gprs = 18 - idx;
prologue_len = 8;
}
&& target >= sb
&& target < sb + 18 * 4
{
let idx = (target - sb) / 4;
saved_gprs = 18 - idx;
prologue_len = 8;
}
// Next should be stwu r1, -N(r1)
let stwu_instr = read_instr(pe, func_addr + prologue_len, image_base).unwrap_or(0);
@@ -601,7 +686,8 @@ fn analyze_function(
}
// Walk forward to find the end of the function
let max_range = code_ranges.iter()
let max_range = code_ranges
.iter()
.find(|&&(s, e)| func_addr >= s && func_addr < e)
.map(|&(_, e)| e)
.unwrap_or(func_addr + 0x100000);
@@ -628,10 +714,12 @@ fn analyze_function(
// Epilogue: b __restgprlr_NN (tail branch into restore stub)
if let Some(target) = b_target(instr, addr)
&& let Some(rb) = restore_base
&& target >= rb && target < rb + 18 * 4 {
end_addr = addr + 4;
break;
}
&& target >= rb
&& target < rb + 18 * 4
{
end_addr = addr + 4;
break;
}
// Epilogue: bctr (indirect tail call — end of function)
if is_bctr(instr) {
@@ -680,21 +768,23 @@ impl FuncAnalysis {
if fi.is_saverestore {
// Label the block start, plus individual register entry points
if let Some(sb) = self.save_gpr_base
&& addr == sb {
for i in 0u32..18 {
let reg = 14 + i;
labels.insert(sb + i * 4, format!("__savegprlr_{reg}"));
}
continue;
&& addr == sb
{
for i in 0u32..18 {
let reg = 14 + i;
labels.insert(sb + i * 4, format!("__savegprlr_{reg}"));
}
continue;
}
if let Some(rb) = self.restore_gpr_base
&& addr == rb {
for i in 0u32..18 {
let reg = 14 + i;
labels.insert(rb + i * 4, format!("__restgprlr_{reg}"));
}
continue;
&& addr == rb
{
for i in 0u32..18 {
let reg = 14 + i;
labels.insert(rb + i * 4, format!("__restgprlr_{reg}"));
}
continue;
}
}
labels.insert(addr, format!("sub_{addr:08X}"));
}

View File

@@ -75,16 +75,23 @@ pub fn analyze(
// Scan only .rdata for dispatch tables — .data has too many false
// positives from struct fields aliasing function VAs.
for section in sections {
if section.name != ".rdata" { continue; }
if section.name != ".rdata" {
continue;
}
let raw_start = section.virtual_address as usize;
let raw_end = (section.virtual_address + section.virtual_size) as usize;
if raw_end > pe.len() { continue; }
if raw_end > pe.len() {
continue;
}
let bytes = &pe[raw_start..raw_end.min(pe.len())];
let va_base = image_base + section.virtual_address;
let mut i = 0usize;
while i + 8 <= bytes.len() {
if !i.is_multiple_of(4) { i += 1; continue; }
if !i.is_multiple_of(4) {
i += 1;
continue;
}
let mut entries: Vec<u32> = Vec::new();
let mut j = i;
while j + 4 <= bytes.len() {
@@ -120,7 +127,10 @@ pub fn analyze(
let n_si = out.iter().filter(|a| a.kind == "static_init").count();
metrics::histogram!("analysis.phase_ms", "phase" => "funcptr_arrays").record(elapsed_ms);
tracing::info!(
total = out.len(), vtable = n_vt, dispatch_table = n_dt, static_init = n_si,
total = out.len(),
vtable = n_vt,
dispatch_table = n_dt,
static_init = n_si,
elapsed_ms,
"function-pointer array scan complete",
);
@@ -156,14 +166,18 @@ fn classify_run(image_base: u32, entries: &[u32], pe: &[u8]) -> &'static str {
/// `mfspr r12, LR` immediately followed by `stwu r1, -N(r1)` with `N ≤ 0x80`.
fn is_ctor_like(pe: &[u8], image_base: u32, fn_va: u32) -> bool {
let off = fn_va.wrapping_sub(image_base) as usize;
if off + 8 > pe.len() { return false; }
if off + 8 > pe.len() {
return false;
}
let i0 = u32::from_be_bytes([pe[off], pe[off + 1], pe[off + 2], pe[off + 3]]);
let i1 = u32::from_be_bytes([pe[off + 4], pe[off + 5], pe[off + 6], pe[off + 7]]);
// i0: mfspr rD, LR — opcode 31, xo 339, spr 8.
let op0 = i0 >> 26;
let xo0 = (i0 >> 1) & 0x3FF;
let spr0 = (((i0 >> 11) & 0x1F) << 5) | ((i0 >> 16) & 0x1F);
if !(op0 == 31 && xo0 == 339 && spr0 == 8) { return false; }
if !(op0 == 31 && xo0 == 339 && spr0 == 8) {
return false;
}
// i1 must be stwu r1, -N(r1) with N ≤ 0x80, OR a `bl __savegprlr_*`
// followed eventually by stwu (full prologue). Allow either.
let op1 = i1 >> 26;
@@ -217,7 +231,9 @@ mod tests {
let sections = vec![mk_section(".rdata", rdata_va, 0x100)];
let mut starts = BTreeSet::new();
for &p in &pcs { starts.insert(p); }
for &p in &pcs {
starts.insert(p);
}
let arrs = analyze(&pe, image_base, &sections, &starts, &[]);
assert_eq!(arrs.len(), 1);
@@ -231,13 +247,19 @@ mod tests {
let rdata_va = 0x1000u32;
let mut pe = vec![0u8; 0x4000];
let pcs = [image_base + 0x2000, image_base + 0x2010, image_base + 0x2020];
let pcs = [
image_base + 0x2000,
image_base + 0x2010,
image_base + 0x2020,
];
for (i, p) in pcs.iter().enumerate() {
write_be_u32(&mut pe, rdata_va as usize + i * 4, *p);
}
let sections = vec![mk_section(".rdata", rdata_va, 0x100)];
let mut starts = BTreeSet::new();
for &p in &pcs { starts.insert(p); }
for &p in &pcs {
starts.insert(p);
}
let vt = Vtable {
address: image_base + rdata_va,

View File

@@ -112,12 +112,12 @@ pub struct VptrWrite {
pub writer_function: u32,
}
const OP_ADDI: u32 = 14;
const OP_ADDI: u32 = 14;
const OP_ADDIS: u32 = 15;
const OP_BCCTR: u32 = 19;
const OP_LWZ: u32 = 32;
const OP_ORI: u32 = 24;
const OP_STW: u32 = 36;
const OP_LWZ: u32 = 32;
const OP_ORI: u32 = 24;
const OP_STW: u32 = 36;
const OP_X_FORM: u32 = 31;
/// Run the full M5.5 analysis.
@@ -133,26 +133,36 @@ pub fn analyze(
let started = std::time::Instant::now();
let vtable_addrs: BTreeSet<u32> = vtables.iter().map(|v| v.address).collect();
let vtable_by_addr: BTreeMap<u32, &Vtable> =
vtables.iter().map(|v| (v.address, v)).collect();
let vtable_by_addr: BTreeMap<u32, &Vtable> = vtables.iter().map(|v| (v.address, v)).collect();
let block_boundaries: HashSet<u32> = labels.keys().copied().collect();
// Phase 1: scan for vptr writes.
let vptr_writes = scan_vptr_writes(
pe, image_base, func_analysis, &vtable_addrs, &block_boundaries,
pe,
image_base,
func_analysis,
&vtable_addrs,
&block_boundaries,
);
// Phase 2: invert by offset.
let mut vtables_by_offset: HashMap<u32, HashSet<u32>> = HashMap::new();
for w in &vptr_writes {
vtables_by_offset.entry(w.vptr_offset).or_default().insert(w.vtable_addr);
vtables_by_offset
.entry(w.vptr_offset)
.or_default()
.insert(w.vtable_addr);
}
// Phase 3 + 4: scan dispatches and emit edges.
let mut dispatches = scan_dispatches_and_resolve(
pe, image_base, func_analysis, &block_boundaries,
&vtables_by_offset, &vtable_by_addr,
pe,
image_base,
func_analysis,
&block_boundaries,
&vtables_by_offset,
&vtable_by_addr,
);
// Drop the per-candidate lists for sites the analysis could not narrow.
@@ -180,7 +190,10 @@ pub fn analyze(
}
let elapsed_ms = started.elapsed().as_millis() as f64;
let single_candidate = dispatches.iter().filter(|d| d.total_candidates == 1).count();
let single_candidate = dispatches
.iter()
.filter(|d| d.total_candidates == 1)
.count();
let multi_candidate = dispatches.len() - single_candidate;
let total_edges: usize = dispatches.iter().map(|d| d.method_pcs.len()).sum();
metrics::histogram!("analysis.phase_ms", "phase" => "ind_dispatch_typed").record(elapsed_ms);
@@ -197,13 +210,23 @@ pub fn analyze(
"M5.5 typed indirect-dispatch scan complete",
);
TypedIndirectResult { dispatches, vptr_writes }
TypedIndirectResult {
dispatches,
vptr_writes,
}
}
fn read_instr(pe: &[u8], image_base: u32, addr: u32) -> Option<u32> {
let off = addr.wrapping_sub(image_base) as usize;
if off + 4 > pe.len() { return None; }
Some(u32::from_be_bytes([pe[off], pe[off + 1], pe[off + 2], pe[off + 3]]))
if off + 4 > pe.len() {
return None;
}
Some(u32::from_be_bytes([
pe[off],
pe[off + 1],
pe[off + 2],
pe[off + 3],
]))
}
/// Phase 1 — find every `stw rA, off(rB)` where the lis+addi-tracked
@@ -217,14 +240,18 @@ fn scan_vptr_writes(
) -> Vec<VptrWrite> {
let mut writes: Vec<VptrWrite> = Vec::new();
for (&fn_start, fi) in &func_analysis.functions {
if fi.is_saverestore { continue; }
if fi.is_saverestore {
continue;
}
let mut reg: [Option<u32>; 32] = [None; 32];
let mut pc = fn_start;
while pc < fi.end {
if pc != fn_start && block_boundaries.contains(&pc) {
reg = [None; 32];
}
let Some(instr) = read_instr(pe, image_base, pc) else { break };
let Some(instr) = read_instr(pe, image_base, pc) else {
break;
};
let op = instr >> 26;
let rd = ((instr >> 21) & 0x1F) as usize;
let ra = ((instr >> 16) & 0x1F) as usize;
@@ -264,19 +291,23 @@ fn scan_vptr_writes(
32..=35 | 40..=43 | 48..=51 => reg[rd] = None,
OP_X_FORM => {
let xo = (instr >> 1) & 0x3FF;
if xo != 444 && xo != 467 { reg[rd] = None; }
if xo != 444 && xo != 467 {
reg[rd] = None;
}
}
18 => {
// `bl` (LK=1) clobbers volatile r0..r12 + ctr. Plain
// `b` makes the next instruction unreachable; the
// label-based reset handles join points.
if (instr & 1) != 0 {
for r in 0..=12 { reg[r] = None; }
for r in 0..=12 {
reg[r] = None;
}
}
}
16 => {
if (instr & 1) != 0 {
for r in 0..=12 { reg[r] = None; }
16 if (instr & 1) != 0 => {
for r in 0..=12 {
reg[r] = None;
}
}
_ => {}
@@ -302,18 +333,29 @@ fn scan_dispatches_and_resolve(
) -> Vec<TypedDispatch> {
let mut out: Vec<TypedDispatch> = Vec::new();
for (&fn_start, fi) in &func_analysis.functions {
if fi.is_saverestore { continue; }
if fi.is_saverestore {
continue;
}
let mut pc = fn_start;
while pc < fi.end {
let Some(instr) = read_instr(pe, image_base, pc) else { break };
let Some(instr) = read_instr(pe, image_base, pc) else {
break;
};
let op = instr >> 26;
if op == OP_BCCTR {
let xo = (instr >> 1) & 0x3FF;
let lk = (instr & 1) != 0;
if xo == 528 && lk
if xo == 528
&& lk
&& let Some(d) = try_resolve_dispatch_site(
pe, image_base, fn_start, fi.end, pc,
block_boundaries, vtables_by_offset, vtable_by_addr,
pe,
image_base,
fn_start,
fi.end,
pc,
block_boundaries,
vtables_by_offset,
vtable_by_addr,
)
{
out.push(d);
@@ -346,9 +388,15 @@ fn try_resolve_dispatch_site(
let mut mtctr_pc: Option<u32> = None;
for i in 1..=LOOKBACK {
let p = bcctrl_pc.wrapping_sub(i * 4);
if p < fn_start { break; }
if block_boundaries.contains(&p) { break; }
let Some(instr) = read_instr(pe, image_base, p) else { break };
if p < fn_start {
break;
}
if block_boundaries.contains(&p) {
break;
}
let Some(instr) = read_instr(pe, image_base, p) else {
break;
};
let op = instr >> 26;
if op == OP_X_FORM {
let xo = (instr >> 1) & 0x3FF;
@@ -371,17 +419,27 @@ fn try_resolve_dispatch_site(
let mut fn_lwz_pc: Option<u32> = None;
for i in 1..=LOOKBACK {
let p = mtctr_pc.wrapping_sub(i * 4);
if p < fn_start { break; }
if block_boundaries.contains(&p) { break; }
let Some(instr) = read_instr(pe, image_base, p) else { break };
if p < fn_start {
break;
}
if block_boundaries.contains(&p) {
break;
}
let Some(instr) = read_instr(pe, image_base, p) else {
break;
};
let op = instr >> 26;
let rd = ((instr >> 21) & 0x1F) as usize;
if op == OP_LWZ {
if rd == mtctr_rs {
let ra = ((instr >> 16) & 0x1F) as usize;
if ra == 0 { return None; }
if ra == 0 {
return None;
}
let off = ((instr & 0xFFFF) as i16) as i32;
if off < 0 || (off % 4) != 0 { return None; }
if off < 0 || (off % 4) != 0 {
return None;
}
slot = Some((off as u32) / 4);
vt_reg = Some(ra);
fn_lwz_pc = Some(p);
@@ -402,14 +460,22 @@ fn try_resolve_dispatch_site(
let mut vptr_off: Option<u32> = None;
for i in 1..=LOOKBACK {
let p = fn_lwz_pc.wrapping_sub(i * 4);
if p < fn_start { break; }
if block_boundaries.contains(&p) { break; }
let Some(instr) = read_instr(pe, image_base, p) else { break };
if p < fn_start {
break;
}
if block_boundaries.contains(&p) {
break;
}
let Some(instr) = read_instr(pe, image_base, p) else {
break;
};
let op = instr >> 26;
let rd = ((instr >> 21) & 0x1F) as usize;
if op == OP_LWZ && rd == vt_reg {
let ra = ((instr >> 16) & 0x1F) as usize;
if ra == 0 { return None; }
if ra == 0 {
return None;
}
let off = ((instr & 0xFFFF) as i16) as i32;
// Negative offsets are valid in C++ (multiple inheritance casts
// can produce them in some ABIs); reinterpret as u32 wrap.
@@ -435,7 +501,9 @@ fn try_resolve_dispatch_site(
method_pcs.push(method_pc);
}
}
if method_pcs.is_empty() { return None; }
if method_pcs.is_empty() {
return None;
}
let total_candidates = candidate_vtables.len();
Some(TypedDispatch {
@@ -460,13 +528,16 @@ fn writes_reg(instr: u32, r: u32) -> bool {
// Most arithmetic / load opcodes use bits 21..25 = rD/rT.
14 | 15 | 32..=43 | 46 | 48..=51 => rd == r,
// ori/oris/xor/etc. opcodes 24..29 — rA in bits 16..20 is the dest.
24 | 25 | 26 | 27 | 28 | 29 => ((instr >> 16) & 0x1F) == r,
24..=29 => ((instr >> 16) & 0x1F) == r,
// X-form: most write rD; some write rA. Check both, conservatively.
OP_X_FORM => {
let xo = (instr >> 1) & 0x3FF;
// Logical X-form (and/or/xor/etc.): rA is the dest.
// Logical X-form ops (and/or/xor/etc.) write rA, not rD.
if matches!(xo, 26 | 28 | 60 | 124 | 284 | 316 | 444 | 476 | 536 | 539 | 922 | 954) {
if matches!(
xo,
26 | 28 | 60 | 124 | 284 | 316 | 444 | 476 | 536 | 539 | 922 | 954
) {
((instr >> 16) & 0x1F) == r
} else {
rd == r
@@ -496,19 +567,27 @@ mod tests {
fn mk_func_analysis(start: u32, len: u32) -> FuncAnalysis {
let mut functions: BTreeMap<u32, FuncInfo> = BTreeMap::new();
functions.insert(start, FuncInfo {
functions.insert(
start,
end: start + len,
frame_size: 0,
saved_gprs: 0,
is_leaf: false,
is_saverestore: false,
pdata_validated: false,
pdata_length: None,
pdata_prolog_length: None,
has_eh: false,
});
FuncAnalysis { functions, save_gpr_base: None, restore_gpr_base: None, pdata_entries: Vec::new() }
FuncInfo {
start,
end: start + len,
frame_size: 0,
saved_gprs: 0,
is_leaf: false,
is_saverestore: false,
pdata_validated: false,
pdata_length: None,
pdata_prolog_length: None,
has_eh: false,
},
);
FuncAnalysis {
functions,
save_gpr_base: None,
restore_gpr_base: None,
pdata_entries: Vec::new(),
}
}
fn write_be(pe: &mut [u8], at: usize, v: u32) {
@@ -519,7 +598,7 @@ mod tests {
fn enc_vptr_write(pe: &mut [u8], at: usize, vt: u32, write_off: i16, dest_reg: u32) {
let hi = (vt >> 16) as u16;
let lo = (vt & 0xFFFF) as i16;
let lis = (15u32 << 26) | (3 << 21) | 0 << 16 | (hi as u32);
let lis = ((15u32 << 26) | (3 << 21)) | (hi as u32);
let addi = (14u32 << 26) | (3 << 21) | (3 << 16) | ((lo as u16) as u32);
let stw = (36u32 << 26) | (3 << 21) | (dest_reg << 16) | ((write_off as u16) as u32);
write_be(pe, at, lis);
@@ -560,11 +639,21 @@ mod tests {
// Both functions in func_analysis (synthesise).
let mut fa = mk_func_analysis(ctor_pc, 0x40);
fa.functions.insert(disp_pc, FuncInfo {
start: disp_pc, end: disp_pc + 0x40, frame_size: 0, saved_gprs: 0,
is_leaf: false, is_saverestore: false,
pdata_validated: false, pdata_length: None, pdata_prolog_length: None, has_eh: false,
});
fa.functions.insert(
disp_pc,
FuncInfo {
start: disp_pc,
end: disp_pc + 0x40,
frame_size: 0,
saved_gprs: 0,
is_leaf: false,
is_saverestore: false,
pdata_validated: false,
pdata_length: None,
pdata_prolog_length: None,
has_eh: false,
},
);
let vt = mk_vtable(0x82010000, vec![0xAA, 0xBB, 0xCC, 0xDD]);
let labels: HashMap<u32, String> = HashMap::new();
@@ -585,9 +674,14 @@ mod tests {
/// Two classes installing different vtables at offset 0, and one dispatch
/// at slot 1 that therefore matches both.
fn multi_candidate_fixture(image_base: u32)
-> (Vec<u8>, FuncAnalysis, Vec<crate::vtables::Vtable>, HashMap<u32, String>)
{
fn multi_candidate_fixture(
image_base: u32,
) -> (
Vec<u8>,
FuncAnalysis,
Vec<crate::vtables::Vtable>,
HashMap<u32, String>,
) {
let mut pe = vec![0u8; 0x4000];
// Two ctors, each writing a different vtable at offset 0.
@@ -601,16 +695,36 @@ mod tests {
enc_dispatch(&mut pe, (disp - image_base) as usize, 0, 1);
let mut fa = mk_func_analysis(ctor_a, 0x40);
fa.functions.insert(ctor_b, FuncInfo {
start: ctor_b, end: ctor_b + 0x40, frame_size: 0, saved_gprs: 0,
is_leaf: false, is_saverestore: false,
pdata_validated: false, pdata_length: None, pdata_prolog_length: None, has_eh: false,
});
fa.functions.insert(disp, FuncInfo {
start: disp, end: disp + 0x40, frame_size: 0, saved_gprs: 0,
is_leaf: false, is_saverestore: false,
pdata_validated: false, pdata_length: None, pdata_prolog_length: None, has_eh: false,
});
fa.functions.insert(
ctor_b,
FuncInfo {
start: ctor_b,
end: ctor_b + 0x40,
frame_size: 0,
saved_gprs: 0,
is_leaf: false,
is_saverestore: false,
pdata_validated: false,
pdata_length: None,
pdata_prolog_length: None,
has_eh: false,
},
);
fa.functions.insert(
disp,
FuncInfo {
start: disp,
end: disp + 0x40,
frame_size: 0,
saved_gprs: 0,
is_leaf: false,
is_saverestore: false,
pdata_validated: false,
pdata_length: None,
pdata_prolog_length: None,
has_eh: false,
},
);
let vts = vec![
mk_vtable(0x82010000, vec![0x11, 0x22, 0x33, 0x44]),
@@ -647,11 +761,21 @@ mod tests {
enc_dispatch(&mut pe, (disp - image_base) as usize, 0, 10);
let mut fa = mk_func_analysis(ctor, 0x40);
fa.functions.insert(disp, FuncInfo {
start: disp, end: disp + 0x40, frame_size: 0, saved_gprs: 0,
is_leaf: false, is_saverestore: false,
pdata_validated: false, pdata_length: None, pdata_prolog_length: None, has_eh: false,
});
fa.functions.insert(
disp,
FuncInfo {
start: disp,
end: disp + 0x40,
frame_size: 0,
saved_gprs: 0,
is_leaf: false,
is_saverestore: false,
pdata_validated: false,
pdata_length: None,
pdata_prolog_length: None,
has_eh: false,
},
);
let vt = mk_vtable(0x82010000, vec![0x11, 0x22, 0x33, 0x44]);
let labels: HashMap<u32, String> = HashMap::new();
@@ -673,11 +797,21 @@ mod tests {
enc_dispatch(&mut pe, (disp - image_base) as usize, 8, 1);
let mut fa = mk_func_analysis(ctor, 0x40);
fa.functions.insert(disp, FuncInfo {
start: disp, end: disp + 0x40, frame_size: 0, saved_gprs: 0,
is_leaf: false, is_saverestore: false,
pdata_validated: false, pdata_length: None, pdata_prolog_length: None, has_eh: false,
});
fa.functions.insert(
disp,
FuncInfo {
start: disp,
end: disp + 0x40,
frame_size: 0,
saved_gprs: 0,
is_leaf: false,
is_saverestore: false,
pdata_validated: false,
pdata_length: None,
pdata_prolog_length: None,
has_eh: false,
},
);
let vt = mk_vtable(0x82010000, vec![0x11, 0x22, 0x33, 0x44]);
let labels: HashMap<u32, String> = HashMap::new();
@@ -707,5 +841,4 @@ mod tests {
assert!(d.method_pcs.is_empty(), "no speculative edges");
assert!(d.candidate_vtables.is_empty());
}
}

View File

@@ -57,12 +57,12 @@ enum RegVal {
},
}
const OP_ADDI: u32 = 14;
const OP_ADDI: u32 = 14;
const OP_ADDIS: u32 = 15;
const OP_BCCTR: u32 = 19; // also covers blr — distinguish via XO
const OP_LWZ: u32 = 32;
const OP_ORI: u32 = 24;
const OP_X_FORM: u32 = 31; // mtspr / mr / etc.
const OP_BCCTR: u32 = 19; // also covers blr — distinguish via XO
const OP_LWZ: u32 = 32;
const OP_ORI: u32 = 24;
const OP_X_FORM: u32 = 31; // mtspr / mr / etc.
/// Run the static indirect-dispatch scan. Returns one edge per resolvable
/// `bcctrl` site.
@@ -77,8 +77,7 @@ pub fn analyze(
let started = std::time::Instant::now();
// Index vtables by their start VA so the lwz handler can decide
// whether a given Const(addr) is "really" a vtable.
let vtable_by_addr: BTreeMap<u32, &Vtable> =
vtables.iter().map(|v| (v.address, v)).collect();
let vtable_by_addr: BTreeMap<u32, &Vtable> = vtables.iter().map(|v| (v.address, v)).collect();
// Set of all "label"-bearing PCs in the analyzed binary. We treat each
// label as a basic-block boundary (anything `loc_*` is a jump target,
@@ -91,7 +90,9 @@ pub fn analyze(
let mut edges: Vec<IndirectEdge> = Vec::new();
for (&fn_start, fi) in &func_analysis.functions {
if fi.is_saverestore { continue; }
if fi.is_saverestore {
continue;
}
let mut reg: [Option<RegVal>; 32] = [None; 32];
let mut ctr: Option<RegVal> = None;
let mut pc = fn_start;
@@ -162,7 +163,9 @@ pub fn analyze(
let resolved = resolve_vtable_slot(target, &vtable_by_addr)
.or_else(|| resolve_vtable_slot_via_off(base, simm, &vtable_by_addr));
reg[rd] = resolved.map(|(vt, slot, pc)| RegVal::MethodPtr {
vtable_addr: vt, slot, method_pc: pc,
vtable_addr: vt,
slot,
method_pc: pc,
});
} else {
reg[rd] = None;
@@ -201,7 +204,11 @@ pub fn analyze(
if xo == 528 {
let lk = (instr & 1) != 0;
if lk
&& let Some(RegVal::MethodPtr { vtable_addr, slot, method_pc }) = ctr
&& let Some(RegVal::MethodPtr {
vtable_addr,
slot,
method_pc,
}) = ctr
{
edges.push(IndirectEdge {
source: pc,
@@ -227,7 +234,9 @@ pub fn analyze(
18 => {
let lk = (instr & 1) != 0;
if lk {
for r in 0..=12 { reg[r] = None; }
for r in 0..=12 {
reg[r] = None;
}
ctr = None;
}
// LK=0 (`b`) makes fall-through unreachable; nothing to do —
@@ -239,7 +248,9 @@ pub fn analyze(
16 => {
let lk = (instr & 1) != 0;
if lk {
for r in 0..=12 { reg[r] = None; }
for r in 0..=12 {
reg[r] = None;
}
ctr = None;
}
}
@@ -274,8 +285,15 @@ pub fn analyze(
fn read_instr(pe: &[u8], image_base: u32, addr: u32) -> Option<u32> {
let off = addr.wrapping_sub(image_base) as usize;
if off + 4 > pe.len() { return None; }
Some(u32::from_be_bytes([pe[off], pe[off + 1], pe[off + 2], pe[off + 3]]))
if off + 4 > pe.len() {
return None;
}
Some(u32::from_be_bytes([
pe[off],
pe[off + 1],
pe[off + 2],
pe[off + 3],
]))
}
/// `target = base + simm` where `target` is an exact vtable head (rare,
@@ -303,11 +321,17 @@ fn resolve_vtable_slot(
) -> Option<(u32, u32, u32)> {
// BTreeMap range search for the largest key ≤ target.
let (&vt_addr, vt) = vtable_by_addr.range(..=target).next_back()?;
if target < vt_addr { return None; }
if target < vt_addr {
return None;
}
let off = target - vt_addr;
if !off.is_multiple_of(4) { return None; }
if !off.is_multiple_of(4) {
return None;
}
let slot = off / 4;
if slot >= vt.length { return None; }
if slot >= vt.length {
return None;
}
let method_pc = *vt.methods.get(slot as usize)?;
Some((vt_addr, slot, method_pc))
}
@@ -339,14 +363,14 @@ mod tests {
fn encode_pattern(buf: &mut [u8], offset: usize, vtable_addr: u32, slot_off: i32) {
let hi = (vtable_addr >> 16) as u16;
let lo = (vtable_addr & 0xFFFF) as i16;
let lis = (15u32 << 26) | (3 << 21) | (0 << 16) | (hi as u32);
let lis = ((15u32 << 26) | (3 << 21)) | (hi as u32);
// addi r3, r3, lo (signed) — note: addi is treated as signed
let addi = (14u32 << 26) | (3 << 21) | (3 << 16) | ((lo as u16) as u32);
let lwz = (32u32 << 26) | (4 << 21) | (3 << 16) | ((slot_off as u16) as u32);
// mtctr r4 = mtspr CTR(=9), r4. SPR_low (=9) → Rust bits 16-20;
// SPR_high (=0) → Rust bits 11-15. Rc bit 0.
let mtctr = (31u32 << 26) | (4 << 21) | (9 << 16) | (0 << 11) | (467 << 1);
let bcctrl = (19u32 << 26) | (20 << 21) | (528 << 1) | 1; // bcctrl 20, 0
let mtctr = ((31u32 << 26) | (4 << 21) | (9 << 16)) | (467 << 1);
let bcctrl = (19u32 << 26) | (20 << 21) | (528 << 1) | 1; // bcctrl 20, 0
let words = [lis, addi, lwz, mtctr, bcctrl];
for (i, w) in words.iter().enumerate() {
buf[offset + i * 4..offset + i * 4 + 4].copy_from_slice(&w.to_be_bytes());
@@ -365,18 +389,21 @@ mod tests {
encode_pattern(&mut pe, text_va as usize, vtable_addr, 8); // slot 2
let mut functions: BTreeMap<u32, FuncInfo> = BTreeMap::new();
functions.insert(pc_start, FuncInfo {
start: pc_start,
end: pc_start + 5 * 4,
frame_size: 0,
saved_gprs: 0,
is_leaf: false,
is_saverestore: false,
pdata_validated: false,
pdata_length: None,
pdata_prolog_length: None,
has_eh: false,
});
functions.insert(
pc_start,
FuncInfo {
start: pc_start,
end: pc_start + 5 * 4,
frame_size: 0,
saved_gprs: 0,
is_leaf: false,
is_saverestore: false,
pdata_validated: false,
pdata_length: None,
pdata_prolog_length: None,
has_eh: false,
},
);
let func_analysis = FuncAnalysis {
functions,
save_gpr_base: None,
@@ -407,18 +434,21 @@ mod tests {
encode_pattern(&mut pe, text_va as usize, vtable_addr, 48);
let mut functions: BTreeMap<u32, FuncInfo> = BTreeMap::new();
functions.insert(pc_start, FuncInfo {
start: pc_start,
end: pc_start + 5 * 4,
frame_size: 0,
saved_gprs: 0,
is_leaf: false,
is_saverestore: false,
pdata_validated: false,
pdata_length: None,
pdata_prolog_length: None,
has_eh: false,
});
functions.insert(
pc_start,
FuncInfo {
start: pc_start,
end: pc_start + 5 * 4,
frame_size: 0,
saved_gprs: 0,
is_leaf: false,
is_saverestore: false,
pdata_validated: false,
pdata_length: None,
pdata_prolog_length: None,
has_eh: false,
},
);
let func_analysis = FuncAnalysis {
functions,
save_gpr_base: None,
@@ -443,18 +473,21 @@ mod tests {
encode_pattern(&mut pe, text_va as usize, vtable_addr, 0);
let mut functions: BTreeMap<u32, FuncInfo> = BTreeMap::new();
functions.insert(pc_start, FuncInfo {
start: pc_start,
end: pc_start + 5 * 4,
frame_size: 0,
saved_gprs: 0,
is_leaf: false,
is_saverestore: false,
pdata_validated: false,
pdata_length: None,
pdata_prolog_length: None,
has_eh: false,
});
functions.insert(
pc_start,
FuncInfo {
start: pc_start,
end: pc_start + 5 * 4,
frame_size: 0,
saved_gprs: 0,
is_leaf: false,
is_saverestore: false,
pdata_validated: false,
pdata_length: None,
pdata_prolog_length: None,
has_eh: false,
},
);
let func_analysis = FuncAnalysis {
functions,
save_gpr_base: None,
@@ -469,6 +502,10 @@ mod tests {
labels.insert(pc_start + 8, "loc_mid".to_string());
let edges = analyze(&pe, image_base, &func_analysis, &vtables, &labels);
assert_eq!(edges.len(), 0, "label in middle of pattern must kill register state");
assert_eq!(
edges.len(),
0,
"label in middle of pattern must kill register state"
);
}
}

View File

@@ -135,17 +135,33 @@ impl JumpTable {
const BCTR: u32 = 0x4E80_0420;
fn op(i: u32) -> u32 { i >> 26 }
fn rt(i: u32) -> usize { ((i >> 21) & 0x1F) as usize }
fn ra(i: u32) -> usize { ((i >> 16) & 0x1F) as usize }
fn rb(i: u32) -> usize { ((i >> 11) & 0x1F) as usize }
fn xo(i: u32) -> u32 { (i >> 1) & 0x3FF }
fn simm(i: u32) -> i32 { ((i & 0xFFFF) as i16) as i32 }
fn uimm(i: u32) -> u32 { i & 0xFFFF }
fn op(i: u32) -> u32 {
i >> 26
}
fn rt(i: u32) -> usize {
((i >> 21) & 0x1F) as usize
}
fn ra(i: u32) -> usize {
((i >> 16) & 0x1F) as usize
}
fn rb(i: u32) -> usize {
((i >> 11) & 0x1F) as usize
}
fn xo(i: u32) -> u32 {
(i >> 1) & 0x3FF
}
fn simm(i: u32) -> i32 {
((i & 0xFFFF) as i16) as i32
}
fn uimm(i: u32) -> u32 {
i & 0xFFFF
}
/// `mtctr rS` — `mtspr` (op 31, xo 467) with the split SPR field naming CTR (9).
fn is_mtctr(i: u32) -> bool {
if op(i) != 31 || xo(i) != 467 { return false; }
if op(i) != 31 || xo(i) != 467 {
return false;
}
let spr_field = (i >> 11) & 0x3FF;
(((spr_field & 0x1F) << 5) | (spr_field >> 5)) == 9
}
@@ -159,13 +175,13 @@ fn is_mtctr(i: u32) -> bool {
fn op31_dest(i: u32) -> Option<usize> {
// Logical / shift / sign-extend X-forms: destination is `rA` (bits 16..20).
const WRITES_RA: &[u32] = &[
24, 26, 27, 28, 58, 60, 124, 284, 316, 412, 444, 476,
536, 539, 792, 794, 824, 826, 827, 922, 954, 986,
24, 26, 27, 28, 58, 60, 124, 284, 316, 412, 444, 476, 536, 539, 792, 794, 824, 826, 827,
922, 954, 986,
];
// Stores, compares, traps, cache/sync ops and `mtspr`/`mtcrf`: no GPR write.
const NO_GPR: &[u32] = &[
0, 4, 32, 68, // cmp, tw, cmpl, td
150, 151, 215, 407, 662, 918, 660, 727, 231, // stwcx./stwx/stbx/sthx/stfsx/stfdx/…
0, 4, 32, 68, // cmp, tw, cmpl, td
150, 151, 215, 407, 662, 918, 660, 727, 231, // stwcx./stwx/stbx/sthx/stfsx/stfdx/…
144, 467, 512, 598, 854, 982, 1014, 86, 470, 54, // mtcrf/mtspr/mcrxr/sync/dcb*/icbi
];
// Store-*update* forms write back into `rA`.
@@ -173,8 +189,12 @@ fn op31_dest(i: u32) -> Option<usize> {
return Some(ra(i));
}
let x = xo(i);
if NO_GPR.contains(&x) { return None; }
if WRITES_RA.contains(&x) { return Some(ra(i)); }
if NO_GPR.contains(&x) {
return None;
}
if WRITES_RA.contains(&x) {
return Some(ra(i));
}
Some(rt(i))
}
@@ -204,13 +224,25 @@ pub fn analyze_with_stats(
let code_ranges: Vec<(u32, u32)> = sections
.iter()
.filter(|s| s.is_code())
.map(|s| (image_base + s.virtual_address, image_base + s.virtual_address + s.virtual_size))
.map(|s| {
(
image_base + s.virtual_address,
image_base + s.virtual_address + s.virtual_size,
)
})
.collect();
let read = |va: u32| -> Option<u32> {
let off = va.wrapping_sub(image_base) as usize;
if off.checked_add(4)? > pe.len() { return None; }
Some(u32::from_be_bytes([pe[off], pe[off + 1], pe[off + 2], pe[off + 3]]))
if off.checked_add(4)? > pe.len() {
return None;
}
Some(u32::from_be_bytes([
pe[off],
pe[off + 1],
pe[off + 2],
pe[off + 3],
]))
};
let in_code = |va: u32| code_ranges.iter().any(|&(s, e)| va >= s && va < e);
@@ -285,11 +317,11 @@ fn recover_at(
// Straight-line constant propagation over [window_start, bctr_pc).
let mut regs: [Option<u32>; 32] = [None; 32];
let mut lwzx_dest: Option<usize> = None; // rT of the last lwzx
let mut lwzx_dest: Option<usize> = None; // rT of the last lwzx
let mut lwzx_regs: Option<(Option<u32>, Option<u32>)> = None;
let mut lwzx_index_tainted = false; // did the index come from the lbzx?
let mut lwzx_index_tainted = false; // did the index come from the lbzx?
let mut lbzx_regs: Option<(Option<u32>, Option<u32>)> = None;
let mut ctr_src: Option<usize> = None; // rS of the last mtctr
let mut ctr_src: Option<usize> = None; // rS of the last mtctr
let mut bound: Option<u32> = None;
// Taint: "this register holds a value derived from the byte the `lbzx`
// read". Only a register carrying that taint may serve as the jump table's
@@ -300,7 +332,7 @@ fn recover_at(
let mut pc = window_start;
while pc < bctr_pc {
let Some(i) = read(pc) else { return None };
let i = read(pc)?;
match op(i) {
// addis rT, rA, SIMM (lis when rA == 0)
15 => {
@@ -319,14 +351,16 @@ fn recover_at(
10 | 11 => bound = Some(uimm(i)),
31 => {
match xo(i) {
23 => { // lwzx rT, rA, rB — the table read
23 => {
// lwzx rT, rA, rB — the table read
lwzx_dest = Some(rt(i));
lwzx_regs = Some((regs[ra(i)], regs[rb(i)]));
lwzx_index_tainted = from_lbzx[ra(i)] || from_lbzx[rb(i)];
regs[rt(i)] = None;
from_lbzx[rt(i)] = false;
}
87 => { // lbzx rT, rA, rB — the sparse index-map read
87 => {
// lbzx rT, rA, rB — the sparse index-map read
lbzx_regs = Some((regs[ra(i)], regs[rb(i)]));
regs[rt(i)] = None;
from_lbzx = [false; 32];
@@ -363,14 +397,23 @@ fn recover_at(
}
// D/DS-form GPR loads write rT; the update forms also write rA.
32 | 34 | 40 | 42 => regs[rt(i)] = None,
33 | 35 | 41 | 43 => { regs[rt(i)] = None; regs[ra(i)] = None; }
33 | 35 | 41 | 43 => {
regs[rt(i)] = None;
regs[ra(i)] = None;
}
// DS-form: bits 30..31 pick ld(0) / ldu(1) / lwa(2).
58 => {
regs[rt(i)] = None;
if i & 3 == 1 { regs[ra(i)] = None; }
if i & 3 == 1 {
regs[ra(i)] = None;
}
}
// lmw loads rT..r31.
46 => for r in rt(i)..32 { regs[r] = None; },
46 => {
for r in rt(i)..32 {
regs[r] = None;
}
}
// FP loads touch no GPR — except the update forms, which write rA.
// Plain stores write no register at all (their `rT` field is the
// *source*), so a tracked base that merely gets spilled survives.
@@ -440,7 +483,9 @@ fn recover_at(
let word = read(byte_off & !3)?;
let slot = (word >> (8 * (3 - (byte_off & 3)))) & 0xFF;
let t = read(table_address.wrapping_add(slot * 4))?;
if !valid(t) { break; }
if !valid(t) {
break;
}
max_slot = max_slot.max(slot);
targets.push(t);
}
@@ -464,11 +509,18 @@ fn recover_at(
}
// Dense: read consecutive absolute targets until one leaves the function.
let cap = bound.map(|n| n.saturating_add(1)).unwrap_or(MAX_ENTRIES).min(MAX_ENTRIES);
let cap = bound
.map(|n| n.saturating_add(1))
.unwrap_or(MAX_ENTRIES)
.min(MAX_ENTRIES);
let mut targets = Vec::new();
for i in 0..cap {
let Some(t) = read(table_address.wrapping_add(i * 4)) else { break };
if !valid(t) { break; }
let Some(t) = read(table_address.wrapping_add(i * 4)) else {
break;
};
if !valid(t) {
break;
}
targets.push(t);
}
if targets.len() < 2 {
@@ -554,19 +606,27 @@ mod tests {
fn one_function(start: u32, end: u32) -> FuncAnalysis {
let mut functions = BTreeMap::new();
functions.insert(start, FuncInfo {
functions.insert(
start,
end,
frame_size: 0,
saved_gprs: 0,
is_leaf: false,
is_saverestore: false,
pdata_validated: true,
pdata_length: Some(end - start),
pdata_prolog_length: Some(0),
has_eh: false,
});
FuncAnalysis { functions, save_gpr_base: None, restore_gpr_base: None, pdata_entries: Vec::new() }
FuncInfo {
start,
end,
frame_size: 0,
saved_gprs: 0,
is_leaf: false,
is_saverestore: false,
pdata_validated: true,
pdata_length: Some(end - start),
pdata_prolog_length: Some(0),
has_eh: false,
},
);
FuncAnalysis {
functions,
save_gpr_base: None,
restore_gpr_base: None,
pdata_entries: Vec::new(),
}
}
/// Encode the words of the canonical MSVC dense-switch dispatch, ending at
@@ -575,13 +635,13 @@ mod tests {
/// / mtctr r0 / bctr / <table>
fn dense_switch(table_va: u32, n_cases: u32) -> Vec<u32> {
vec![
0x2800_0000 | (10 << 16) | (n_cases - 1), // cmplwi r10, N
0x4181_0000 | 0x20, // bc (bound check, target irrelevant)
0x3D80_0000 | (table_va >> 16), // lis r12, hi
0x398C_0000 | (table_va & 0xFFFF), // addi r12, r12, lo
0x5540_103A, // slwi r0, r10, 2
0x7C0C_002E, // lwzx r0, r12, r0
0x7C09_03A6, // mtctr r0
0x2800_0000 | (10 << 16) | (n_cases - 1), // cmplwi r10, N
0x4181_0000 | 0x20, // bc (bound check, target irrelevant)
0x3D80_0000 | (table_va >> 16), // lis r12, hi
0x398C_0000 | (table_va & 0xFFFF), // addi r12, r12, lo
0x5540_103A, // slwi r0, r10, 2
0x7C0C_002E, // lwzx r0, r12, r0
0x7C09_03A6, // mtctr r0
BCTR,
]
}
@@ -600,7 +660,12 @@ mod tests {
let table_va = TEXT_VA + 8 * 4;
let mut words = dense_switch(table_va, 4);
// Four case bodies, all inside the function.
let cases = [TEXT_VA + 0x40, TEXT_VA + 0x50, TEXT_VA + 0x60, TEXT_VA + 0x70];
let cases = [
TEXT_VA + 0x40,
TEXT_VA + 0x50,
TEXT_VA + 0x60,
TEXT_VA + 0x70,
];
words.extend_from_slice(&cases);
let pe = assemble(&words, 0x100);
@@ -626,7 +691,9 @@ mod tests {
let table_va = TEXT_VA + 8 * 4;
let mut words = dense_switch(table_va, 8);
words.extend_from_slice(&[
TEXT_VA + 0x40, TEXT_VA + 0x50, TEXT_VA + 0x60,
TEXT_VA + 0x40,
TEXT_VA + 0x50,
TEXT_VA + 0x60,
0x8300_0000, // far outside
TEXT_VA + 0x70,
]);
@@ -659,26 +726,31 @@ mod tests {
fn recovers_sparse_two_level_switch() {
// cmplwi r10,5 / bc / lis+addi r11 = &map / lbzx r0,r11,r10
// / lis+addi r12 = &table / slwi r0,r0,2 / lwzx r0,r12,r0 / mtctr / bctr
let map_va = TEXT_VA + 13 * 4; // 6 bytes, then padding
let table_va = TEXT_VA + 17 * 4; // 3 distinct bodies
let map_va = TEXT_VA + 13 * 4; // 6 bytes, then padding
let table_va = TEXT_VA + 17 * 4; // 3 distinct bodies
let words = vec![
0x2800_0000 | (10 << 16) | 5, // cmplwi r10, 5
0x4181_0000 | 0x20, // bc
0x3D60_0000 | (map_va >> 16), // lis r11, map@h
0x396B_0000 | (map_va & 0xFFFF), // addi r11, r11, map@l
0x7C0B_50AE, // lbzx r0, r11, r10
0x3D80_0000 | (table_va >> 16), // lis r12, tab@h
0x398C_0000 | (table_va & 0xFFFF), // addi r12, r12, tab@l
0x5400_103A, // slwi r0, r0, 2
0x7C0C_002E, // lwzx r0, r12, r0
0x7C09_03A6, // mtctr r0
0x2800_0000 | (10 << 16) | 5, // cmplwi r10, 5
0x4181_0000 | 0x20, // bc
0x3D60_0000 | (map_va >> 16), // lis r11, map@h
0x396B_0000 | (map_va & 0xFFFF), // addi r11, r11, map@l
0x7C0B_50AE, // lbzx r0, r11, r10
0x3D80_0000 | (table_va >> 16), // lis r12, tab@h
0x398C_0000 | (table_va & 0xFFFF), // addi r12, r12, tab@l
0x5400_103A, // slwi r0, r0, 2
0x7C0C_002E, // lwzx r0, r12, r0
0x7C09_03A6, // mtctr r0
BCTR,
0, 0,
0,
0,
// map[0..6] = 0,1,2,2,1,0 packed big-endian, then padding
0x0001_0202, 0x0100_0000,
0, 0,
0x0001_0202,
0x0100_0000,
0,
0,
// table[0..3]
TEXT_VA + 0x80, TEXT_VA + 0x90, TEXT_VA + 0xA0,
TEXT_VA + 0x80,
TEXT_VA + 0x90,
TEXT_VA + 0xA0,
];
let pe = assemble(&words, 0x100);
let sections = [text_section(0x100)];
@@ -691,10 +763,17 @@ mod tests {
assert_eq!(jt.index_map_address, Some(map_va));
assert_eq!(jt.index_map_count, Some(6));
assert_eq!(jt.table_slots, 3);
assert_eq!(jt.targets, vec![
TEXT_VA + 0x80, TEXT_VA + 0x90, TEXT_VA + 0xA0,
TEXT_VA + 0xA0, TEXT_VA + 0x90, TEXT_VA + 0x80,
]);
assert_eq!(
jt.targets,
vec![
TEXT_VA + 0x80,
TEXT_VA + 0x90,
TEXT_VA + 0xA0,
TEXT_VA + 0xA0,
TEXT_VA + 0x90,
TEXT_VA + 0x80,
]
);
}
/// An unrelated `lbzx` in the window must not be mistaken for a case index
@@ -704,19 +783,24 @@ mod tests {
fn unrelated_lbzx_does_not_become_an_index_map() {
let table_va = TEXT_VA + 11 * 4;
let mut words = vec![
0x2800_0000 | (10 << 16) | 3, // cmplwi r10, 3
0x4181_0000 | 0x20, // bc
0x3D60_0000 | (TEXT_VA >> 16), // lis r11, text@h (a code constant)
0x396B_0000 | (TEXT_VA & 0xFFFF), // addi r11, r11, text@l
0x7CEB_44AE, // lbzx r7, r11, r8 — unrelated byte load
0x3D80_0000 | (table_va >> 16), // lis r12, tab@h
0x398C_0000 | (table_va & 0xFFFF), // addi r12, r12, tab@l
0x5540_103A, // slwi r0, r10, 2 — index is r10, NOT r7
0x7C0C_002E, // lwzx r0, r12, r0
0x7C09_03A6, // mtctr r0
0x2800_0000 | (10 << 16) | 3, // cmplwi r10, 3
0x4181_0000 | 0x20, // bc
0x3D60_0000 | (TEXT_VA >> 16), // lis r11, text@h (a code constant)
0x396B_0000 | (TEXT_VA & 0xFFFF), // addi r11, r11, text@l
0x7CEB_44AE, // lbzx r7, r11, r8 — unrelated byte load
0x3D80_0000 | (table_va >> 16), // lis r12, tab@h
0x398C_0000 | (table_va & 0xFFFF), // addi r12, r12, tab@l
0x5540_103A, // slwi r0, r10, 2 — index is r10, NOT r7
0x7C0C_002E, // lwzx r0, r12, r0
0x7C09_03A6, // mtctr r0
];
words.push(BCTR);
words.extend_from_slice(&[TEXT_VA + 0x80, TEXT_VA + 0x90, TEXT_VA + 0xA0, TEXT_VA + 0xB0]);
words.extend_from_slice(&[
TEXT_VA + 0x80,
TEXT_VA + 0x90,
TEXT_VA + 0xA0,
TEXT_VA + 0xB0,
]);
let pe = assemble(&words, 0x100);
let sections = [text_section(0x100)];
@@ -731,12 +815,22 @@ mod tests {
#[test]
fn data_regions_merge_adjacent_tables() {
let a = JumpTable {
bctr_pc: 0x8200_1000, function: None, table_address: 0x8200_2000,
entry_count: 4, table_slots: 4, index_map_address: None,
index_map_count: None, bound: None, kind: "direct",
bctr_pc: 0x8200_1000,
function: None,
table_address: 0x8200_2000,
entry_count: 4,
table_slots: 4,
index_map_address: None,
index_map_count: None,
bound: None,
kind: "direct",
targets: vec![0; 4],
};
let b = JumpTable { table_address: 0x8200_2010, bctr_pc: 0x8200_1004, ..a.clone() };
let b = JumpTable {
table_address: 0x8200_2010,
bctr_pc: 0x8200_1004,
..a.clone()
};
let merged = data_regions(&[a, b]);
assert_eq!(merged, vec![(0x8200_2000, 32)]);
}
@@ -744,9 +838,15 @@ mod tests {
#[test]
fn data_word_addresses_covers_every_slot() {
let jt = JumpTable {
bctr_pc: 0x8200_1000, function: None, table_address: 0x8200_2000,
entry_count: 3, table_slots: 3, index_map_address: Some(0x8200_3000),
index_map_count: Some(5), bound: Some(4), kind: "indexed",
bctr_pc: 0x8200_1000,
function: None,
table_address: 0x8200_2000,
entry_count: 3,
table_slots: 3,
index_map_address: Some(0x8200_3000),
index_map_count: Some(5),
bound: Some(4),
kind: "indexed",
targets: vec![0; 5],
};
let words = data_word_addresses(&[jt]);

View File

@@ -1,26 +1,46 @@
pub mod ppc;
pub mod func;
pub mod xref;
// 🔴 THREE LINTS ARE OFF FOR THIS CRATE, WITH REASONS, RATHER THAN SILENTLY.
//
// * `needless_range_loop` — nine sites index `reg[r]` where **`r` is the
// PowerPC register number**. The index is the meaning; an iterator hides
// which GPR a pattern matched, which is the whole content of these passes.
// * `too_many_arguments` — five analysis passes take the image, its base, the
// section table, the function list and several output sinks. Bundling those
// into a struct moves the list rather than shortening it, and this code
// arrived whole from a retired repository: a refactor here would be an
// unreviewed edit dressed as a lint fix.
// * `type_complexity` — one return type in `vtables.rs`, same argument.
//
// Everything else clippy asked for was fixed, including every doc-indent site.
// See `docs/agents/CONSOLIDATION.md` Phase 3.
#![allow(
clippy::needless_range_loop,
clippy::too_many_arguments,
clippy::type_complexity
)]
pub mod db;
pub mod demangle;
pub mod disasm;
pub mod eh_scope;
pub mod formatter;
pub mod func;
pub mod funcptr_arrays;
pub mod ind_dispatch_typed;
pub mod indirect;
pub mod jumptables;
pub mod lookup;
pub mod ppc;
pub mod rtti;
pub mod sinks;
pub mod sql_views;
pub mod demangle;
pub mod vtables;
pub mod lookup;
pub mod indirect;
pub mod ind_dispatch_typed;
pub mod strings;
pub mod funcptr_arrays;
pub mod eh_scope;
pub mod static_init;
pub mod strings;
pub mod vtables;
pub mod xdbf;
pub mod jumptables;
pub mod rtti;
pub mod xref;
mod ordinals;
pub use ordinals::resolve_ordinal;
pub use xref::{XrefKind, Xref, XrefMap, resolve_source_label};
pub use db::{DbWriter, ExecTraceEntry, ImportCallEntry, BranchTraceEntry};
pub use db::{BranchTraceEntry, DbWriter, ExecTraceEntry, ImportCallEntry};
pub use disasm::{RichDisasmItem, enrich_section};
pub use ordinals::resolve_ordinal;
pub use xref::{Xref, XrefKind, XrefMap, resolve_source_label};

View File

@@ -13,7 +13,7 @@
use std::path::Path;
use anyhow::{anyhow, Result};
use anyhow::{Result, anyhow};
use duckdb::params;
/// Parse one probe token into one or more PCs.
@@ -72,7 +72,10 @@ pub fn resolve_probe_token(db_path: Option<&Path>, token: &str) -> Result<Vec<u3
}
fn parse_numeric(token: &str) -> Option<u32> {
if let Some(hex) = token.strip_prefix("0x").or_else(|| token.strip_prefix("0X")) {
if let Some(hex) = token
.strip_prefix("0x")
.or_else(|| token.strip_prefix("0X"))
{
return u32::from_str_radix(hex, 16).ok();
}
token.parse::<u32>().ok()
@@ -89,7 +92,9 @@ fn resolve_class_method(conn: &duckdb::Connection, class: &str, method: &str) ->
WHERE c.name = ? AND dn.method_name = ?",
)?;
let pcs: Vec<u32> = stmt
.query_map(params![class, method], |r| r.get::<_, i64>(0).map(|x| x as u32))?
.query_map(params![class, method], |r| {
r.get::<_, i64>(0).map(|x| x as u32)
})?
.filter_map(|r| r.ok())
.collect();
Ok(pcs)

View File

@@ -24,5 +24,8 @@ impl Decoded {
pub fn disasm(instr: u32, addr: u32) -> Decoded {
let d = decode(instr, addr);
let t = format(&d);
Decoded { base: t.disasm, ext: t.ext_disasm }
Decoded {
base: t.disasm,
ext: t.ext_disasm,
}
}

View File

@@ -117,8 +117,11 @@ impl RttiResult {
/// `vftable[0]` VA → `(demangled class name, subobject offset)`.
pub fn vtable_class_names(&self) -> BTreeMap<u32, (String, u32)> {
let td: BTreeMap<u32, &TypeDescriptor> =
self.type_descriptors.iter().map(|t| (t.address, t)).collect();
let td: BTreeMap<u32, &TypeDescriptor> = self
.type_descriptors
.iter()
.map(|t| (t.address, t))
.collect();
let mut out = BTreeMap::new();
for col in &self.locators {
if let (Some(vt), Some(t)) = (col.vtable_address, td.get(&col.type_descriptor)) {
@@ -138,8 +141,15 @@ pub fn analyze(pe: &[u8], image_base: u32, sections: &[PeSection]) -> RttiResult
let read = |va: u32| -> Option<u32> {
let off = va.wrapping_sub(image_base) as usize;
if off.checked_add(4)? > pe.len() { return None; }
Some(u32::from_be_bytes([pe[off], pe[off + 1], pe[off + 2], pe[off + 3]]))
if off.checked_add(4)? > pe.len() {
return None;
}
Some(u32::from_be_bytes([
pe[off],
pe[off + 1],
pe[off + 2],
pe[off + 3],
]))
};
// Byte ranges actually backed by file data (a section's tail beyond
@@ -151,10 +161,16 @@ pub fn analyze(pe: &[u8], image_base: u32, sections: &[PeSection]) -> RttiResult
};
let ranges: Vec<(String, u32, u32)> = sections
.iter()
.map(|s| { let (a, b) = backed(s); (s.name.clone(), a, b) })
.map(|s| {
let (a, b) = backed(s);
(s.name.clone(), a, b)
})
.collect();
let range_of = |name: &str| -> Option<(u32, u32)> {
ranges.iter().find(|(n, _, _)| n == name).map(|&(_, a, b)| (a, b))
ranges
.iter()
.find(|(n, _, _)| n == name)
.map(|&(_, a, b)| (a, b))
};
// 1. TypeDescriptors. The decorated name lives at descriptor+8 and always
@@ -162,19 +178,35 @@ pub fn analyze(pe: &[u8], image_base: u32, sections: &[PeSection]) -> RttiResult
let mut type_descriptors: Vec<TypeDescriptor> = Vec::new();
let mut td_addrs: BTreeSet<u32> = BTreeSet::new();
for (name, start, end) in &ranges {
if !matches!(name.as_str(), ".data" | ".rdata") { continue; }
if !matches!(name.as_str(), ".data" | ".rdata") {
continue;
}
let s = (*start).wrapping_sub(image_base) as usize;
let e = (*end).wrapping_sub(image_base) as usize;
if e > pe.len() || s >= e { continue; }
if e > pe.len() || s >= e {
continue;
}
let bytes = &pe[s..e];
let mut i = 0usize;
while i + 3 < bytes.len() {
if &bytes[i..i + 3] != b".?A" { i += 1; continue; }
if &bytes[i..i + 3] != b".?A" {
i += 1;
continue;
}
let name_va = start.wrapping_add(i as u32);
// The descriptor head sits 8 bytes before the name.
let Some(td_va) = name_va.checked_sub(8) else { i += 1; continue };
if td_va < *start { i += 1; continue; }
let Some(decorated) = read_cstr(bytes, i, 512) else { i += 1; continue };
let Some(td_va) = name_va.checked_sub(8) else {
i += 1;
continue;
};
if td_va < *start {
i += 1;
continue;
}
let Some(decorated) = read_cstr(bytes, i, 512) else {
i += 1;
continue;
};
i += decorated.len() + 1;
if td_addrs.insert(td_va) {
type_descriptors.push(TypeDescriptor {
@@ -197,8 +229,14 @@ pub fn analyze(pe: &[u8], image_base: u32, sections: &[PeSection]) -> RttiResult
let mut va = rd_start;
while va + 20 <= rd_end {
let (Some(sig), Some(off), Some(cd), Some(ptd), Some(pchd)) = (
read(va), read(va + 4), read(va + 8), read(va + 12), read(va + 16),
) else { break };
read(va),
read(va + 4),
read(va + 8),
read(va + 12),
read(va + 16),
) else {
break;
};
if sig == 0 && td_addrs.contains(&ptd) && pchd >= rd_start && pchd < rd_end {
col_addrs.insert(va);
locators.push(CompleteObjectLocator {
@@ -217,7 +255,9 @@ pub fn analyze(pe: &[u8], image_base: u32, sections: &[PeSection]) -> RttiResult
// 3. `vftable[-1]` sites: any word in initialised data whose value is a COL.
let mut vtable_to_locator: BTreeMap<u32, u32> = BTreeMap::new();
for (name, start, end) in &ranges {
if !matches!(name.as_str(), ".rdata" | ".data") { continue; }
if !matches!(name.as_str(), ".rdata" | ".data") {
continue;
}
let mut va = *start;
while va + 4 <= *end {
if let Some(w) = read(va)
@@ -228,8 +268,10 @@ pub fn analyze(pe: &[u8], image_base: u32, sections: &[PeSection]) -> RttiResult
va += 4;
}
}
let locator_to_vtable: BTreeMap<u32, u32> =
vtable_to_locator.iter().map(|(&vt, &col)| (col, vt)).collect();
let locator_to_vtable: BTreeMap<u32, u32> = vtable_to_locator
.iter()
.map(|(&vt, &col)| (col, vt))
.collect();
for col in &mut locators {
col.vtable_address = locator_to_vtable.get(&col.address).copied();
}
@@ -241,19 +283,37 @@ pub fn analyze(pe: &[u8], image_base: u32, sections: &[PeSection]) -> RttiResult
let chds: BTreeSet<u32> = locators.iter().map(|c| c.class_hierarchy).collect();
if let Some((rd_start, rd_end)) = rdata {
for chd in chds {
let (Some(n_bases), Some(p_array)) = (read(chd + 8), read(chd + 12)) else { continue };
let (Some(n_bases), Some(p_array)) = (read(chd + 8), read(chd + 12)) else {
continue;
};
// A malformed or misidentified descriptor would blow the scan up;
// real hierarchies are small.
if n_bases == 0 || n_bases > 64 { continue; }
if p_array < rd_start || p_array >= rd_end { continue; }
if n_bases == 0 || n_bases > 64 {
continue;
}
if p_array < rd_start || p_array >= rd_end {
continue;
}
for i in 0..n_bases {
let Some(bcd) = read(p_array + i * 4) else { break };
if bcd < rd_start || bcd >= rd_end { break; }
let Some(bcd) = read(p_array + i * 4) else {
break;
};
if bcd < rd_start || bcd >= rd_end {
break;
}
let (Some(ptd), Some(ncb), Some(md), Some(pd), Some(vd), Some(attr)) = (
read(bcd), read(bcd + 4), read(bcd + 8),
read(bcd + 12), read(bcd + 16), read(bcd + 20),
) else { break };
let Some(td) = td_by_addr.get(&ptd) else { break };
read(bcd),
read(bcd + 4),
read(bcd + 8),
read(bcd + 12),
read(bcd + 16),
read(bcd + 20),
) else {
break;
};
let Some(td) = td_by_addr.get(&ptd) else {
break;
};
base_classes.push(BaseClass {
class_hierarchy: chd,
index: i,
@@ -280,7 +340,12 @@ pub fn analyze(pe: &[u8], image_base: u32, sections: &[PeSection]) -> RttiResult
"RTTI walk complete",
);
RttiResult { type_descriptors, locators, base_classes, vtable_to_locator }
RttiResult {
type_descriptors,
locators,
base_classes,
vtable_to_locator,
}
}
/// Read a NUL-terminated ASCII string starting at `off` in `bytes`.
@@ -308,14 +373,18 @@ mod tests {
vec![
PeSection {
name: ".rdata".into(),
virtual_address: RDATA_RVA, virtual_size: SEC_SIZE,
raw_offset: RDATA_RVA, raw_size: SEC_SIZE,
virtual_address: RDATA_RVA,
virtual_size: SEC_SIZE,
raw_offset: RDATA_RVA,
raw_size: SEC_SIZE,
flags: 0x4000_0040,
},
PeSection {
name: ".data".into(),
virtual_address: DATA_RVA, virtual_size: SEC_SIZE,
raw_offset: DATA_RVA, raw_size: SEC_SIZE,
virtual_address: DATA_RVA,
virtual_size: SEC_SIZE,
raw_offset: DATA_RVA,
raw_size: SEC_SIZE,
flags: 0xC000_0040,
},
]
@@ -323,7 +392,9 @@ mod tests {
struct Image(Vec<u8>);
impl Image {
fn new() -> Self { Image(vec![0u8; (DATA_RVA + SEC_SIZE) as usize]) }
fn new() -> Self {
Image(vec![0u8; (DATA_RVA + SEC_SIZE) as usize])
}
fn put_u32(&mut self, va: u32, v: u32) {
let o = (va - BASE) as usize;
self.0[o..o + 4].copy_from_slice(&v.to_be_bytes());
@@ -339,16 +410,23 @@ mod tests {
/// and the `vftable[-1]` word that points at the COL.
#[allow(clippy::too_many_arguments)]
fn emit_class(
img: &mut Image, td: u32, name: &str,
col: u32, offset: u32, chd: u32, bcd_array: u32, bcd: u32, base_name_td: Option<u32>,
img: &mut Image,
td: u32,
name: &str,
col: u32,
offset: u32,
chd: u32,
bcd_array: u32,
bcd: u32,
base_name_td: Option<u32>,
vtable_minus_one: u32,
) {
img.put_u32(td, 0xDEAD_BEEF); // type_info vftable — value is irrelevant
img.put_str(td + 8, name);
img.put_u32(col, 0); // signature
img.put_u32(col, 0); // signature
img.put_u32(col + 4, offset);
img.put_u32(col + 8, 0); // cdOffset
img.put_u32(col + 8, 0); // cdOffset
img.put_u32(col + 12, td);
img.put_u32(col + 16, chd);
@@ -362,16 +440,16 @@ mod tests {
img.put_u32(bcd_array, bcd);
img.put_u32(bcd, td);
img.put_u32(bcd + 4, n_bases - 1);
img.put_u32(bcd + 8, 0); // mdisp
img.put_u32(bcd + 12, u32::MAX); // pdisp = -1
img.put_u32(bcd + 16, 0); // vdisp
img.put_u32(bcd + 20, 0x40); // attributes
img.put_u32(bcd + 8, 0); // mdisp
img.put_u32(bcd + 12, u32::MAX); // pdisp = -1
img.put_u32(bcd + 16, 0); // vdisp
img.put_u32(bcd + 20, 0x40); // attributes
if let Some(base_td) = base_name_td {
let bcd2 = bcd + 24;
img.put_u32(bcd_array + 4, bcd2);
img.put_u32(bcd2, base_td);
img.put_u32(bcd2 + 4, 0);
img.put_u32(bcd2 + 8, 4); // mdisp = 4
img.put_u32(bcd2 + 8, 4); // mdisp = 4
img.put_u32(bcd2 + 12, u32::MAX);
img.put_u32(bcd2 + 16, 0);
img.put_u32(bcd2 + 20, 0);
@@ -387,18 +465,39 @@ mod tests {
let da = BASE + DATA_RVA;
// Base class Foo, then Derived : Foo.
emit_class(&mut img, da + 0x100, ".?AVFoo@ns@@",
rd + 0x100, 0, rd + 0x200, rd + 0x280, rd + 0x300, None,
rd + 0x000);
emit_class(&mut img, da + 0x200, ".?AVDerived@ns@@",
rd + 0x400, 0, rd + 0x500, rd + 0x580, rd + 0x600, Some(da + 0x100),
rd + 0x040);
emit_class(
&mut img,
da + 0x100,
".?AVFoo@ns@@",
rd + 0x100,
0,
rd + 0x200,
rd + 0x280,
rd + 0x300,
None,
rd,
);
emit_class(
&mut img,
da + 0x200,
".?AVDerived@ns@@",
rd + 0x400,
0,
rd + 0x500,
rd + 0x580,
rd + 0x600,
Some(da + 0x100),
rd + 0x040,
);
let r = analyze(&img.0, BASE, &sections());
assert_eq!(r.type_descriptors.len(), 2);
let derived = r.type_descriptors.iter()
.find(|t| t.mangled_name.contains("Derived")).unwrap();
let derived = r
.type_descriptors
.iter()
.find(|t| t.mangled_name.contains("Derived"))
.unwrap();
assert_eq!(derived.demangled_name, "ns::Derived");
assert_eq!(r.locators.len(), 2);
@@ -407,10 +506,15 @@ mod tests {
assert!(r.vtable_anchors().contains(&(rd + 0x004)));
let names = r.vtable_class_names();
assert_eq!(names.get(&(rd + 0x044)), Some(&("ns::Derived".to_string(), 0)));
assert_eq!(
names.get(&(rd + 0x044)),
Some(&("ns::Derived".to_string(), 0))
);
// Derived's hierarchy lists itself at index 0 and Foo at index 1.
let mut bases: Vec<_> = r.base_classes.iter()
let mut bases: Vec<_> = r
.base_classes
.iter()
.filter(|b| b.class_hierarchy == rd + 0x500)
.collect();
bases.sort_by_key(|b| b.index);
@@ -425,9 +529,18 @@ mod tests {
let mut img = Image::new();
let rd = BASE + RDATA_RVA;
let da = BASE + DATA_RVA;
emit_class(&mut img, da + 0x100, ".?AVMulti@@",
rd + 0x100, 0x8, rd + 0x200, rd + 0x280, rd + 0x300, None,
rd + 0x000);
emit_class(
&mut img,
da + 0x100,
".?AVMulti@@",
rd + 0x100,
0x8,
rd + 0x200,
rd + 0x280,
rd + 0x300,
None,
rd,
);
let r = analyze(&img.0, BASE, &sections());
let names = r.vtable_class_names();

View File

@@ -28,7 +28,8 @@ pub fn write_instr_line<W: Write + ?Sized>(
// A word the analysis proved is data (a recovered jump table or its index
// map) must not be printed as if it decoded to something meaningful.
if item.is_data {
let lbl = labels.get(&item.item.raw)
let lbl = labels
.get(&item.item.raw)
.map(|s| format!(" ; -> {s}"))
.unwrap_or_default();
return writeln!(
@@ -52,12 +53,13 @@ pub fn write_instr_line<W: Write + ?Sized>(
if let Some((data_addr, kind)) = data_annotation {
let tag = match kind {
XrefKind::DataRead => "[R]",
XrefKind::DataRead => "[R]",
XrefKind::DataWrite => "[W]",
_ => "[&]",
_ => "[&]",
};
let sec = section_for_addr(data_addr, sections, image_base).unwrap_or("?");
let data_lbl = labels.get(&data_addr)
let data_lbl = labels
.get(&data_addr)
.map(|s| format!(" = {s}"))
.unwrap_or_default();
if !annotated.contains("; ->") {
@@ -67,5 +69,9 @@ pub fn write_instr_line<W: Write + ?Sized>(
}
}
writeln!(out, " {:08X}: {:08X} {}", item.item.addr, item.item.raw, annotated)
writeln!(
out,
" {:08X}: {:08X} {}",
item.item.addr, item.item.raw, annotated
)
}

View File

@@ -23,7 +23,6 @@
//! 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 = "
@@ -66,7 +65,10 @@ 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_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),

View File

@@ -69,10 +69,10 @@ pub struct StaticInitResult {
pub arrays: Vec<FuncPtrArray>,
}
const OP_ADDI: u32 = 14;
const OP_ADDI: u32 = 14;
const OP_ADDIS: u32 = 15;
const OP_BCCTR: u32 = 19;
const OP_LWZ: u32 = 32;
const OP_LWZ: u32 = 32;
const OP_X_FORM: u32 = 31;
#[derive(Debug, Clone, Copy)]
@@ -95,10 +95,12 @@ pub fn analyze(
let mut drivers: Vec<StaticInitDriver> = Vec::new();
for (&fn_start, fi) in &func_analysis.functions {
if fi.is_saverestore { continue; }
if let Some(d) = scan_function_for_driver(
pe, image_base, fn_start, fi.end, &block_boundaries,
) {
if fi.is_saverestore {
continue;
}
if let Some(d) =
scan_function_for_driver(pe, image_base, fn_start, fi.end, &block_boundaries)
{
drivers.push(d);
}
}
@@ -106,7 +108,14 @@ pub fn analyze(
// Build arrays from the discovered drivers + section data.
let mut arrays: Vec<FuncPtrArray> = Vec::new();
for d in &drivers {
if let Some(entries) = read_array(pe, image_base, sections, d.array_start, d.array_end, function_starts) {
if let Some(entries) = read_array(
pe,
image_base,
sections,
d.array_start,
d.array_end,
function_starts,
) {
arrays.push(FuncPtrArray {
address: d.array_start,
length: entries.len() as u32,
@@ -140,7 +149,9 @@ fn read_array(
end: u32,
function_starts: &BTreeSet<u32>,
) -> Option<Vec<u32>> {
if end <= start || (end - start) > 4096 { return None; }
if end <= start || (end - start) > 4096 {
return None;
}
let _section = sections.iter().find(|s| {
let lo = image_base + s.virtual_address;
let hi = lo + s.virtual_size;
@@ -150,15 +161,21 @@ fn read_array(
let mut p = start;
while p < end {
let off = p.wrapping_sub(image_base) as usize;
if off + 4 > pe.len() { return None; }
if off + 4 > pe.len() {
return None;
}
let v = u32::from_be_bytes([pe[off], pe[off + 1], pe[off + 2], pe[off + 3]]);
if v != 0 {
if !function_starts.contains(&v) { return None; }
if !function_starts.contains(&v) {
return None;
}
entries.push(v);
}
p = p.wrapping_add(4);
}
if entries.is_empty() { return None; }
if entries.is_empty() {
return None;
}
Some(entries)
}
@@ -194,7 +211,9 @@ fn scan_function_for_driver(
reg = [None; 32];
}
let off = pc.wrapping_sub(image_base) as usize;
if off + 4 > pe.len() { break; }
if off + 4 > pe.len() {
break;
}
let instr = u32::from_be_bytes([pe[off], pe[off + 1], pe[off + 2], pe[off + 3]]);
let op = instr >> 26;
let rd = ((instr >> 21) & 0x1F) as usize;
@@ -207,7 +226,9 @@ fn scan_function_for_driver(
OP_ADDIS => {
if let Some(RegVal::Const(b)) = reg[ra] {
reg[rd] = Some(RegVal::Const(b.wrapping_add(uimm << 16)));
} else { reg[rd] = None; }
} else {
reg[rd] = None;
}
}
OP_ADDI if ra != 0 => {
let prev = reg[ra];
@@ -229,7 +250,9 @@ fn scan_function_for_driver(
end_init = Some(v);
end_reg = Some(rd);
}
} else { reg[rd] = None; }
} else {
reg[rd] = None;
}
}
OP_LWZ => {
if ra != 0 && Some(ra) == cursor_reg {
@@ -241,9 +264,13 @@ fn scan_function_for_driver(
let xo = (instr >> 1) & 0x3FF;
if xo == 467 {
let spr = (((instr >> 11) & 0x1F) << 5) | ((instr >> 16) & 0x1F);
if spr == 9 && saw_lwz_through_cursor { saw_mtctr = true; }
if spr == 9 && saw_lwz_through_cursor {
saw_mtctr = true;
}
}
if xo != 444 && xo != 467 {
reg[rd] = None;
}
if xo != 444 && xo != 467 { reg[rd] = None; }
}
OP_BCCTR => {
let xo = (instr >> 1) & 0x3FF;
@@ -254,12 +281,14 @@ fn scan_function_for_driver(
}
18 => {
if (instr & 1) != 0 {
for r in 0..=12 { reg[r] = None; }
for r in 0..=12 {
reg[r] = None;
}
}
}
16 => {
if (instr & 1) != 0 {
for r in 0..=12 { reg[r] = None; }
16 if (instr & 1) != 0 => {
for r in 0..=12 {
reg[r] = None;
}
}
_ => {}
@@ -273,8 +302,12 @@ fn scan_function_for_driver(
}
let cursor_init = cursor_init?;
let end_init = end_init?;
if end_init <= cursor_init { return None; }
if end_init - cursor_init > 4096 { return None; }
if end_init <= cursor_init {
return None;
}
if end_init - cursor_init > 4096 {
return None;
}
Some(StaticInitDriver {
driver_function: fn_start,
@@ -294,8 +327,10 @@ mod tests {
fn mk_section(name: &str, va: u32, size: u32) -> PeSection {
PeSection {
name: name.into(),
virtual_address: va, virtual_size: size,
raw_offset: va, raw_size: size,
virtual_address: va,
virtual_size: size,
raw_offset: va,
raw_size: size,
flags: 0x4000_0040,
}
}
@@ -311,7 +346,11 @@ mod tests {
// Array at .rdata + 0x800: 3 function pointers.
let arr_va_lo = 0x800u32;
let fns = [image_base + 0x2000, image_base + 0x2010, image_base + 0x2020];
let fns = [
image_base + 0x2000,
image_base + 0x2010,
image_base + 0x2020,
];
for (i, p) in fns.iter().enumerate() {
write_be(&mut pe, arr_va_lo as usize + i * 4, *p);
}
@@ -330,32 +369,52 @@ mod tests {
// blr
let driver = 0x82001000u32;
let off = (driver - image_base) as usize;
let lis_r3 = (15u32 << 26) | (3 << 21) | ((array_start >> 16) as u32);
let lis_r3 = (15u32 << 26) | (3 << 21) | (array_start >> 16);
let addi_r3 = (14u32 << 26) | (3 << 21) | (3 << 16) | ((array_start as u16) as u32);
let lis_r4 = (15u32 << 26) | (4 << 21) | ((array_end >> 16) as u32);
let lis_r4 = (15u32 << 26) | (4 << 21) | (array_end >> 16);
let addi_r4 = (14u32 << 26) | (4 << 21) | (4 << 16) | ((array_end as u16) as u32);
let lwz = (32u32 << 26) | (5 << 21) | (3 << 16);
let mtctr = (31u32 << 26) | (5 << 21) | (9 << 16) | (467 << 1);
let bcctrl = (19u32 << 26) | (20 << 21) | (528 << 1) | 1;
let addi_inc = (14u32 << 26) | (3 << 21) | (3 << 16) | 4;
let blr = (19u32 << 26) | (20 << 21) | (16 << 1);
for (i, w) in [lis_r3, addi_r3, lis_r4, addi_r4, lwz, mtctr, bcctrl, addi_inc, blr].iter().enumerate() {
for (i, w) in [
lis_r3, addi_r3, lis_r4, addi_r4, lwz, mtctr, bcctrl, addi_inc, blr,
]
.iter()
.enumerate()
{
write_be(&mut pe, off + i * 4, *w);
}
let mut functions: BTreeMap<u32, FuncInfo> = BTreeMap::new();
functions.insert(driver, FuncInfo {
start: driver, end: driver + 0x40, frame_size: 0, saved_gprs: 0,
is_leaf: false, is_saverestore: false,
pdata_validated: false, pdata_length: None, pdata_prolog_length: None, has_eh: false,
});
functions.insert(
driver,
FuncInfo {
start: driver,
end: driver + 0x40,
frame_size: 0,
saved_gprs: 0,
is_leaf: false,
is_saverestore: false,
pdata_validated: false,
pdata_length: None,
pdata_prolog_length: None,
has_eh: false,
},
);
let fa = FuncAnalysis {
functions, save_gpr_base: None, restore_gpr_base: None, pdata_entries: Vec::new(),
functions,
save_gpr_base: None,
restore_gpr_base: None,
pdata_entries: Vec::new(),
};
let sections = vec![mk_section(".rdata", 0x800, 0x100)];
let mut starts = BTreeSet::new();
for &p in &fns { starts.insert(p); }
for &p in &fns {
starts.insert(p);
}
let labels: HashMap<u32, String> = HashMap::new();
let r = analyze(&pe, image_base, &sections, &fa, &starts, &labels);
@@ -382,13 +441,26 @@ mod tests {
write_be(&mut pe, (driver - image_base) as usize, blr);
let mut functions: BTreeMap<u32, FuncInfo> = BTreeMap::new();
functions.insert(driver, FuncInfo {
start: driver, end: driver + 0x40, frame_size: 0, saved_gprs: 0,
is_leaf: true, is_saverestore: false,
pdata_validated: false, pdata_length: None, pdata_prolog_length: None, has_eh: false,
});
functions.insert(
driver,
FuncInfo {
start: driver,
end: driver + 0x40,
frame_size: 0,
saved_gprs: 0,
is_leaf: true,
is_saverestore: false,
pdata_validated: false,
pdata_length: None,
pdata_prolog_length: None,
has_eh: false,
},
);
let fa = FuncAnalysis {
functions, save_gpr_base: None, restore_gpr_base: None, pdata_entries: Vec::new(),
functions,
save_gpr_base: None,
restore_gpr_base: None,
pdata_entries: Vec::new(),
};
let sections = vec![mk_section(".rdata", 0x800, 0x100)];
let starts: BTreeSet<u32> = BTreeSet::new();

View File

@@ -52,12 +52,16 @@ pub fn analyze(pe: &[u8], image_base: u32, sections: &[PeSection]) -> Vec<Detect
let mut out: Vec<DetectedString> = Vec::new();
for section in sections {
if !matches!(section.name.as_str(), ".rdata" | ".data") { continue; }
if !matches!(section.name.as_str(), ".rdata" | ".data") {
continue;
}
let raw_start = section.virtual_address as usize;
// Clamp to the file-backed extent — everything past `raw_size` is BSS.
let backed = section.virtual_size.min(section.raw_size) as usize;
let raw_end = (raw_start + backed).min(pe.len());
if raw_start >= raw_end { continue; }
if raw_start >= raw_end {
continue;
}
let bytes = &pe[raw_start..raw_end];
let va_base = image_base + section.virtual_address;
@@ -74,8 +78,8 @@ pub fn analyze(pe: &[u8], image_base: u32, sections: &[PeSection]) -> Vec<Detect
let elapsed_ms = started.elapsed().as_millis() as f64;
let n_ascii = out.iter().filter(|s| s.encoding == "ascii").count();
let n_utf16 = out.iter().filter(|s| s.encoding == "utf16le").count();
let n_sjis = out.iter().filter(|s| s.encoding == "shift_jis").count();
let n_utf8 = out.iter().filter(|s| s.encoding == "utf8").count();
let n_sjis = out.iter().filter(|s| s.encoding == "shift_jis").count();
let n_utf8 = out.iter().filter(|s| s.encoding == "utf8").count();
metrics::histogram!("analysis.phase_ms", "phase" => "strings").record(elapsed_ms);
tracing::info!(
ascii = n_ascii,
@@ -104,7 +108,9 @@ fn scan_ascii(bytes: &[u8], va_base: u32, out: &mut Vec<DetectedString>) {
continue;
}
let start = i;
while i < bytes.len() && is_printable_ascii(bytes[i]) { i += 1; }
while i < bytes.len() && is_printable_ascii(bytes[i]) {
i += 1;
}
let run_len = i - start;
// Require NUL termination and minimum length.
if run_len >= MIN_LEN && i < bytes.len() && bytes[i] == 0 {
@@ -118,7 +124,9 @@ fn scan_ascii(bytes: &[u8], va_base: u32, out: &mut Vec<DetectedString>) {
});
}
// Skip the NUL (if any) before continuing.
if i < bytes.len() && bytes[i] == 0 { i += 1; }
if i < bytes.len() && bytes[i] == 0 {
i += 1;
}
}
}
@@ -127,7 +135,10 @@ fn scan_utf16le(bytes: &[u8], va_base: u32, out: &mut Vec<DetectedString>) {
// offsets to avoid misaligned hits.
let mut i = 0;
while i + 2 <= bytes.len() {
if !i.is_multiple_of(2) { i += 1; continue; }
if !i.is_multiple_of(2) {
i += 1;
continue;
}
let lo = bytes[i];
let hi = bytes[i + 1];
// Restrict scan-start to printable ASCII range with a zero high byte —
@@ -141,7 +152,9 @@ fn scan_utf16le(bytes: &[u8], va_base: u32, out: &mut Vec<DetectedString>) {
while i + 2 <= bytes.len() {
let l = bytes[i];
let h = bytes[i + 1];
if h != 0 || !is_printable_ascii(l) { break; }
if h != 0 || !is_printable_ascii(l) {
break;
}
codeunits.push((h as u16) << 8 | l as u16);
i += 2;
}
@@ -158,7 +171,9 @@ fn scan_utf16le(bytes: &[u8], va_base: u32, out: &mut Vec<DetectedString>) {
});
}
// Skip past the terminator.
if nul_terminated { i += 2; }
if nul_terminated {
i += 2;
}
}
}
@@ -183,16 +198,16 @@ fn is_sjis_trail(b: u8) -> bool {
/// ASCII.
fn is_text_like(ch: char) -> bool {
let o = ch as u32;
matches!(o, 0x20..=0x7E)
|| matches!(ch, '\t' | '\n' | '\r')
|| is_wide(ch)
matches!(o, 0x20..=0x7E) || matches!(ch, '\t' | '\n' | '\r') || is_wide(ch)
}
/// A full-width character — kana, CJK punctuation, ideograph, or full-width
/// ASCII. Used to tell "real text" from a lucky byte pair.
fn is_wide(ch: char) -> bool {
let o = ch as u32;
(0x3000..=0x30FF).contains(&o) || (0x4E00..=0x9FFF).contains(&o) || (0xFF01..=0xFF5E).contains(&o)
(0x3000..=0x30FF).contains(&o)
|| (0x4E00..=0x9FFF).contains(&o)
|| (0xFF01..=0xFF5E).contains(&o)
}
/// True when `t` contains a lone ASCII character with a full-width character
@@ -207,9 +222,8 @@ fn is_wide(ch: char) -> bool {
/// character wedged between two wide ones.
fn has_isolated_ascii(t: &str) -> bool {
let chars: Vec<char> = t.chars().collect();
(1..chars.len().saturating_sub(1)).any(|k| {
!is_wide(chars[k]) && is_wide(chars[k - 1]) && is_wide(chars[k + 1])
})
(1..chars.len().saturating_sub(1))
.any(|k| !is_wide(chars[k]) && is_wide(chars[k - 1]) && is_wide(chars[k + 1]))
}
/// Decode `raw` as Shift_JIS, rejecting anything that is not convincingly
@@ -224,7 +238,8 @@ fn decode_sjis(raw: &[u8]) -> Option<String> {
// obscure kanji, but hiragana/katakana (U+3040..U+30FF) essentially never
// appear by accident and are ubiquitous in genuine Japanese.
let has_kana = t.chars().any(|c| ('\u{3040}'..='\u{30FF}').contains(&c));
if t.chars().count() >= 4 && has_kana && t.chars().all(is_text_like) && !has_isolated_ascii(&t) {
if t.chars().count() >= 4 && has_kana && t.chars().all(is_text_like) && !has_isolated_ascii(&t)
{
Some(t)
} else {
None
@@ -274,7 +289,9 @@ fn scan_shift_jis(bytes: &[u8], va_base: u32, out: &mut Vec<DetectedString>) {
i = end + 1; // skip NUL
} else {
i = start + 1;
if i < bytes.len() && bytes[i] == 0 { i += 1; }
if i < bytes.len() && bytes[i] == 0 {
i += 1;
}
}
}
}
@@ -291,12 +308,16 @@ fn scan_utf8(bytes: &[u8], va_base: u32, out: &mut Vec<DetectedString>) {
while i < bytes.len() {
let b = bytes[i];
if b < 0x80 {
if !is_printable_ascii(b) { break; }
if !is_printable_ascii(b) {
break;
}
nbytes += 1;
i += 1;
} else if (b & 0xE0) == 0xC0 {
// 2-byte: 110xxxxx 10xxxxxx
if i + 1 >= bytes.len() || (bytes[i + 1] & 0xC0) != 0x80 { break; }
if i + 1 >= bytes.len() || (bytes[i + 1] & 0xC0) != 0x80 {
break;
}
has_multibyte = true;
nbytes += 2;
i += 2;
@@ -304,7 +325,10 @@ fn scan_utf8(bytes: &[u8], va_base: u32, out: &mut Vec<DetectedString>) {
// 3-byte: 1110xxxx 10xxxxxx 10xxxxxx
if i + 2 >= bytes.len()
|| (bytes[i + 1] & 0xC0) != 0x80
|| (bytes[i + 2] & 0xC0) != 0x80 { break; }
|| (bytes[i + 2] & 0xC0) != 0x80
{
break;
}
has_multibyte = true;
nbytes += 3;
i += 3;
@@ -314,7 +338,8 @@ fn scan_utf8(bytes: &[u8], va_base: u32, out: &mut Vec<DetectedString>) {
}
if has_multibyte
&& nbytes >= MIN_LEN
&& i < bytes.len() && bytes[i] == 0
&& i < bytes.len()
&& bytes[i] == 0
&& let Ok(s) = std::str::from_utf8(&bytes[start..i])
{
out.push(DetectedString {
@@ -327,7 +352,9 @@ fn scan_utf8(bytes: &[u8], va_base: u32, out: &mut Vec<DetectedString>) {
i += 1; // skip NUL
} else {
i = start + 1;
if i < bytes.len() && bytes[i] == 0 { i += 1; }
if i < bytes.len() && bytes[i] == 0 {
i += 1;
}
}
}
}
@@ -402,7 +429,10 @@ mod tests {
pe[off..off + s.len()].copy_from_slice(s);
let sections = vec![mk_section(".rdata", 0x1000, 0x100)];
let strings = analyze(&pe, image_base, &sections);
let sjis: Vec<_> = strings.iter().filter(|s| s.encoding == "shift_jis").collect();
let sjis: Vec<_> = strings
.iter()
.filter(|s| s.encoding == "shift_jis")
.collect();
assert_eq!(sjis.len(), 1);
// Decoded to real UTF-8, not rendered as escaped bytes.
assert_eq!(sjis[0].content, "ABCあい");
@@ -420,8 +450,10 @@ mod tests {
pe[off..off + s.len()].copy_from_slice(s);
let sections = vec![mk_section(".rdata", 0x1000, 0x100)];
let strings = analyze(&pe, image_base, &sections);
assert!(strings.iter().all(|s| s.encoding != "shift_jis"),
"float table must not be reported as Japanese text");
assert!(
strings.iter().all(|s| s.encoding != "shift_jis"),
"float table must not be reported as Japanese text"
);
}
#[test]
@@ -440,7 +472,10 @@ mod tests {
pe[off..off + s.len()].copy_from_slice(s);
let sections = vec![mk_section(".rdata", 0x1000, 0x100)];
let strings = analyze(&pe, image_base, &sections);
let sjis: Vec<_> = strings.iter().filter(|s| s.encoding == "shift_jis").collect();
let sjis: Vec<_> = strings
.iter()
.filter(|s| s.encoding == "shift_jis")
.collect();
assert_eq!(sjis.len(), 1);
assert_eq!(sjis[0].content, "システム");
// Reported at the true start, one byte past the run's beginning.
@@ -471,7 +506,9 @@ mod tests {
let s = b"abcdefghij";
pe[off..off + s.len()].copy_from_slice(s);
// Fill rest of section with 0xFF so the run terminates cleanly without NUL.
for j in off + s.len()..off + 0x100 { pe[j] = 0xFF; }
for j in off + s.len()..off + 0x100 {
pe[j] = 0xFF;
}
let sections = vec![mk_section(".rdata", 0x1000, 0x100)];
let strings = analyze(&pe, image_base, &sections);
assert_eq!(strings.len(), 0);

View File

@@ -65,7 +65,13 @@ pub fn analyze(
sections: &[PeSection],
function_starts: &std::collections::BTreeSet<u32>,
) -> Vec<Vtable> {
analyze_with_anchors(pe, image_base, sections, function_starts, &std::collections::BTreeSet::new())
analyze_with_anchors(
pe,
image_base,
sections,
function_starts,
&std::collections::BTreeSet::new(),
)
}
/// Like [`analyze`], but additionally recovers vtables whose base address is
@@ -106,7 +112,12 @@ pub fn analyze_with_anchors(
let rdata_ranges: Vec<(u32, u32)> = sections
.iter()
.filter(|s| s.name == ".rdata")
.map(|s| (image_base + s.virtual_address, image_base + s.virtual_address + s.virtual_size))
.map(|s| {
(
image_base + s.virtual_address,
image_base + s.virtual_address + s.virtual_size,
)
})
.collect();
// TypeDescriptors are *written at startup* (their first word is
// `type_info`'s vftable), so MSVC emits them into writable `.data`, not
@@ -115,7 +126,12 @@ pub fn analyze_with_anchors(
let typedesc_ranges: Vec<(u32, u32)> = sections
.iter()
.filter(|s| matches!(s.name.as_str(), ".rdata" | ".data"))
.map(|s| (image_base + s.virtual_address, image_base + s.virtual_address + s.virtual_size))
.map(|s| {
(
image_base + s.virtual_address,
image_base + s.virtual_address + s.virtual_size,
)
})
.collect();
let mut candidates: Vec<Vtable> = Vec::new();
@@ -125,13 +141,18 @@ pub fn analyze_with_anchors(
let va_end = va_start + section.virtual_size;
let raw_start = section.virtual_address as usize;
let raw_end = (section.virtual_address + section.virtual_size) as usize;
if raw_end > pe.len() { continue; }
if raw_end > pe.len() {
continue;
}
let bytes = &pe[raw_start..raw_end.min(pe.len())];
let mut i = 0usize;
while i + 12 <= bytes.len() {
// Try to start a run at this 4-aligned offset.
if !i.is_multiple_of(4) { i += 1; continue; }
if !i.is_multiple_of(4) {
i += 1;
continue;
}
let mut run_len = 0usize;
let mut methods: Vec<u32> = Vec::new();
let mut j = i;
@@ -203,12 +224,19 @@ pub fn analyze_with_anchors(
let mut recovered = 0usize;
let mut newly: Vec<Vtable> = Vec::new();
for &anchor in anchors {
if is_covered(anchor, &covered) { continue; }
if is_covered(anchor, &covered) {
continue;
}
// Locate the containing .rdata/.data section.
let Some(&(va_lo, va_hi, raw_lo, raw_hi)) =
scan_targets_va.iter().find(|&&(lo, hi, _, _)| anchor >= lo && anchor < hi)
else { continue };
if anchor % 4 != 0 { continue; }
let Some(&(va_lo, va_hi, raw_lo, raw_hi)) = scan_targets_va
.iter()
.find(|&&(lo, hi, _, _)| anchor >= lo && anchor < hi)
else {
continue;
};
if anchor % 4 != 0 {
continue;
}
let raw_hi = raw_hi.min(pe.len());
// Read the fnptr-array run starting at the anchor. Tolerate small
// gaps of non-function slots (null / pure-virtual / unrecognised),
@@ -221,7 +249,11 @@ pub fn analyze_with_anchors(
let mut off = (anchor - va_lo) as usize + raw_lo;
let mut va = anchor;
while off + 4 <= raw_hi && va < va_hi {
if let Some(nb) = next_base && va >= nb { break; }
if let Some(nb) = next_base
&& va >= nb
{
break;
}
let val = u32::from_be_bytes([pe[off], pe[off + 1], pe[off + 2], pe[off + 3]]);
if function_starts.contains(&val) {
methods.push(val);
@@ -244,10 +276,15 @@ pub fn analyze_with_anchors(
}
// Trim any trailing non-function slots (the table ends at its last
// real method).
while methods.last().is_some_and(|&m| !function_starts.contains(&m)) {
while methods
.last()
.is_some_and(|&m| !function_starts.contains(&m))
{
methods.pop();
}
if real_fns == 0 || methods.is_empty() { continue; }
if real_fns == 0 || methods.is_empty() {
continue;
}
let length = methods.len() as u32;
newly.push(Vtable {
address: anchor,
@@ -266,8 +303,10 @@ pub fn analyze_with_anchors(
// contiguity-scan artifact of the same table. Keep fragments that
// only partially overlap (defensive; shouldn't happen for true
// sub-runs) so we never lose method coverage.
let recovered_spans: Vec<(u32, u32)> =
newly.iter().map(|v| (v.address, v.address + v.length * 4)).collect();
let recovered_spans: Vec<(u32, u32)> = newly
.iter()
.map(|v| (v.address, v.address + v.length * 4))
.collect();
candidates.retain(|v| {
!recovered_spans
.iter()
@@ -281,22 +320,37 @@ pub fn analyze_with_anchors(
// RTTI walk: for each candidate, look at vtable[-1].
let pe_image_base = image_base;
for v in &mut candidates {
if v.address < 4 { continue; }
if v.address < 4 {
continue;
}
let col_off = (v.address - pe_image_base - 4) as usize;
if col_off + 4 > pe.len() { continue; }
let col_ptr = u32::from_be_bytes([pe[col_off], pe[col_off + 1], pe[col_off + 2], pe[col_off + 3]]);
if col_ptr == 0 { continue; }
if !is_in_ranges(col_ptr, &rdata_ranges) { continue; }
if col_off + 4 > pe.len() {
continue;
}
let col_ptr = u32::from_be_bytes([
pe[col_off],
pe[col_off + 1],
pe[col_off + 2],
pe[col_off + 3],
]);
if col_ptr == 0 {
continue;
}
if !is_in_ranges(col_ptr, &rdata_ranges) {
continue;
}
// Try to extract the TypeDescriptor mangled-name string.
if let Some((td_ptr, hierarchy_ptr)) = read_col(pe, image_base, col_ptr)
&& let Some(mangled) = read_typedescriptor_name(pe, image_base, td_ptr, &typedesc_ranges)
&& let Some(mangled) =
read_typedescriptor_name(pe, image_base, td_ptr, &typedesc_ranges)
&& let Some(class) = demangle_rtti_typename(&mangled)
{
v.col_address = Some(col_ptr);
v.class_name = class;
v.rtti_present = true;
v.base_classes_json = read_class_hierarchy(pe, image_base, hierarchy_ptr, &rdata_ranges);
v.base_classes_json =
read_class_hierarchy(pe, image_base, hierarchy_ptr, &rdata_ranges);
}
}
@@ -320,8 +374,15 @@ fn is_in_ranges(addr: u32, ranges: &[(u32, u32)]) -> bool {
/// Read 4 big-endian bytes at absolute VA `addr` from the PE image.
fn read_be_u32(pe: &[u8], image_base: u32, addr: u32) -> Option<u32> {
let off = addr.wrapping_sub(image_base) as usize;
if off + 4 > pe.len() { return None; }
Some(u32::from_be_bytes([pe[off], pe[off + 1], pe[off + 2], pe[off + 3]]))
if off + 4 > pe.len() {
return None;
}
Some(u32::from_be_bytes([
pe[off],
pe[off + 1],
pe[off + 2],
pe[off + 3],
]))
}
/// Parse a `CompleteObjectLocator` at VA `col`. Returns
@@ -338,7 +399,9 @@ fn read_be_u32(pe: &[u8], image_base: u32, addr: u32) -> Option<u32> {
fn read_col(pe: &[u8], image_base: u32, col: u32) -> Option<(u32, u32)> {
let td = read_be_u32(pe, image_base, col + 0x0C)?;
let chd = read_be_u32(pe, image_base, col + 0x10)?;
if td == 0 { return None; }
if td == 0 {
return None;
}
Some((td, chd))
}
@@ -352,17 +415,27 @@ fn read_typedescriptor_name(
td: u32,
rdata_ranges: &[(u32, u32)],
) -> Option<String> {
if !is_in_ranges(td, rdata_ranges) { return None; }
if !is_in_ranges(td, rdata_ranges) {
return None;
}
let name_va = td + 0x08;
let off = name_va.wrapping_sub(image_base) as usize;
if off + 1 > pe.len() { return None; }
if off + 1 > pe.len() {
return None;
}
// Read up to 256 bytes or until NUL.
let mut end = off;
while end < pe.len().min(off + 256) && pe[end] != 0 { end += 1; }
if end == off { return None; }
while end < pe.len().min(off + 256) && pe[end] != 0 {
end += 1;
}
if end == off {
return None;
}
let s = std::str::from_utf8(&pe[off..end]).ok()?;
// Sanity: MSVC RTTI names always start with `.?A`.
if !s.starts_with(".?A") { return None; }
if !s.starts_with(".?A") {
return None;
}
Some(s.to_string())
}
@@ -403,11 +476,17 @@ fn read_class_hierarchy(
chd: u32,
rdata_ranges: &[(u32, u32)],
) -> Option<String> {
if !is_in_ranges(chd, rdata_ranges) { return None; }
if !is_in_ranges(chd, rdata_ranges) {
return None;
}
let num_bases = read_be_u32(pe, image_base, chd + 0x08)?;
if num_bases == 0 || num_bases > 256 { return None; } // sanity cap
if num_bases == 0 || num_bases > 256 {
return None;
} // sanity cap
let bca_ptr = read_be_u32(pe, image_base, chd + 0x0C)?;
if !is_in_ranges(bca_ptr, rdata_ranges) { return None; }
if !is_in_ranges(bca_ptr, rdata_ranges) {
return None;
}
let mut names: Vec<String> = Vec::new();
for i in 0..num_bases {
@@ -419,10 +498,7 @@ fn read_class_hierarchy(
Some(p) if is_in_ranges(p, rdata_ranges) => p,
_ => return None,
};
let mangled = match read_typedescriptor_name(pe, image_base, td_ptr, rdata_ranges) {
Some(s) => s,
None => return None,
};
let mangled = read_typedescriptor_name(pe, image_base, td_ptr, rdata_ranges)?;
let cls = demangle_rtti_typename(&mangled).unwrap_or(mangled);
names.push(cls);
}
@@ -453,7 +529,12 @@ pub fn scan_vptr_write_constants(
let data_ranges: Vec<(u32, u32)> = sections
.iter()
.filter(|s| matches!(s.name.as_str(), ".rdata" | ".data"))
.map(|s| (image_base + s.virtual_address, image_base + s.virtual_address + s.virtual_size))
.map(|s| {
(
image_base + s.virtual_address,
image_base + s.virtual_address + s.virtual_size,
)
})
.collect();
let in_data = |a: u32| data_ranges.iter().any(|&(s, e)| a >= s && a < e);
@@ -465,13 +546,22 @@ pub fn scan_vptr_write_constants(
let read = |addr: u32| -> Option<u32> {
let off = addr.wrapping_sub(image_base) as usize;
if off + 4 > pe.len() { return None; }
Some(u32::from_be_bytes([pe[off], pe[off + 1], pe[off + 2], pe[off + 3]]))
if off + 4 > pe.len() {
return None;
}
Some(u32::from_be_bytes([
pe[off],
pe[off + 1],
pe[off + 2],
pe[off + 3],
]))
};
let mut anchors: std::collections::BTreeSet<u32> = std::collections::BTreeSet::new();
for (&fn_start, &(fn_end, is_saverestore)) in functions {
if is_saverestore { continue; }
if is_saverestore {
continue;
}
let mut reg: [Option<u32>; 32] = [None; 32];
let mut pc = fn_start;
while pc < fn_end {
@@ -506,11 +596,13 @@ pub fn scan_vptr_write_constants(
32..=35 | 40..=43 | 48..=51 => reg[rd] = None,
OP_X_FORM => {
let xo = (instr >> 1) & 0x3FF;
if xo != 444 && xo != 467 { reg[rd] = None; } // keep `or`(444=mr)/`mtspr`-ish
if xo != 444 && xo != 467 {
reg[rd] = None;
} // keep `or`(444=mr)/`mtspr`-ish
}
18 | 16 => {
if (instr & 1) != 0 {
for r in 0..=12 { reg[r] = None; }
18 | 16 if (instr & 1) != 0 => {
for r in 0..=12 {
reg[r] = None;
}
}
_ => {}
@@ -550,7 +642,8 @@ pub fn methods_table(
for v in vtables {
for (slot, &fn_va) in v.methods.iter().enumerate() {
let label = labels.get(&fn_va).cloned();
let demangled = label.as_ref()
let demangled = label
.as_ref()
.and_then(|l| demangle::demangle(l).map(|d| d.raw_demangled));
out.push((v.address, slot as u32, fn_va, label, demangled));
}
@@ -572,6 +665,71 @@ pub fn classes_table(vtables: &[Vtable]) -> Vec<(String, u32, bool, Option<Strin
.collect()
}
// ── RTTI relabelling ───────────────────────────────────────────────────────
/// Overwrite heuristic vtable identity with the authoritative RTTI walk.
///
/// [`analyze_with_anchors`] names a table either from its own inline COL walk
/// or, failing that, with a synthetic `ANON_Class_<hash>`. [`crate::rtti`]
/// resolves the same question top-down from the structures the linker emitted,
/// which is exact — so wherever the two disagree, RTTI wins. Rows RTTI knows
/// nothing about keep their heuristic name.
///
/// `base_classes_json` is rebuilt here as the class's full linearised base list
/// (excluding index 0, which is the class itself), which is strictly more than
/// the first-level list the inline walk produced.
///
/// Returns the number of vtables that gained a real class name.
pub fn apply_rtti_names(vtables: &mut [Vtable], rtti: &crate::rtti::RttiResult) -> usize {
use std::collections::BTreeMap;
let names = rtti.vtable_class_names();
let locator_by_vtable: BTreeMap<u32, &crate::rtti::CompleteObjectLocator> = rtti
.locators
.iter()
.filter_map(|c| c.vtable_address.map(|v| (v, c)))
.collect();
// class-hierarchy VA → base class names, in the linker's order.
let mut bases_by_chd: BTreeMap<u32, Vec<&str>> = BTreeMap::new();
for b in &rtti.base_classes {
if b.index == 0 {
continue;
} // index 0 is the class itself
bases_by_chd
.entry(b.class_hierarchy)
.or_default()
.push(b.name.as_str());
}
let mut named = 0usize;
for vt in vtables.iter_mut() {
let Some((class_name, offset)) = names.get(&vt.address) else {
continue;
};
// A secondary-base vftable belongs to the same class but is a distinct
// table; keep them apart by suffixing the subobject offset.
vt.class_name = if *offset == 0 {
class_name.clone()
} else {
format!("{class_name}#base+0x{offset:X}")
};
vt.rtti_present = true;
if let Some(col) = locator_by_vtable.get(&vt.address) {
vt.col_address = Some(col.address);
vt.base_classes_json = bases_by_chd.get(&col.class_hierarchy).map(|names| {
let items: Vec<String> = names
.iter()
.map(|n| format!("\"{}\"", n.replace('\\', "\\\\").replace('"', "\\\"")))
.collect();
format!("[{}]", items.join(","))
});
}
named += 1;
}
named
}
#[cfg(test)]
mod tests {
use super::*;
@@ -603,7 +761,11 @@ mod tests {
let mut pe = vec![0u8; total];
// Vtable: 3 method PCs at .rdata start, all valid function entries.
let m: [u32; 3] = [image_base + text_va, image_base + text_va + 0x10, image_base + text_va + 0x20];
let m: [u32; 3] = [
image_base + text_va,
image_base + text_va + 0x10,
image_base + text_va + 0x20,
];
for (i, val) in m.iter().enumerate() {
pe[rdata_va as usize + i * 4..rdata_va as usize + (i + 1) * 4]
.copy_from_slice(&val.to_be_bytes());
@@ -628,7 +790,9 @@ mod tests {
},
];
let mut function_starts = std::collections::BTreeSet::new();
for &pc in &m { function_starts.insert(pc); }
for &pc in &m {
function_starts.insert(pc);
}
let vtables = analyze(&pe, image_base, &sections, &function_starts);
assert_eq!(vtables.len(), 1);
@@ -680,7 +844,9 @@ mod tests {
},
];
let mut function_starts = std::collections::BTreeSet::new();
for &pc in &[f0, f1, f2] { function_starts.insert(pc); }
for &pc in &[f0, f1, f2] {
function_starts.insert(pc);
}
// Without an anchor: the head gap (null + nonfn = 2 slots) means the
// contiguous run is only [f0,f1,f2]=3 starting at +0x08, so pass-1
@@ -715,13 +881,13 @@ mod tests {
let mut pe = vec![0u8; 0x4000];
// Lay out a tiny .rdata at 0x...A900 so the constant lands in-range.
let vt_base = 0x8200A908u32; // 0x82010000 - 22264
let addis = (15u32 << 26) | (11 << 21) | (0 << 16) | 0x8201;
let addis = ((15u32 << 26) | (11 << 21)) | 0x8201;
let lo = (vt_base & 0xFFFF) as i16; // -22264
let addi = (14u32 << 26) | (11 << 21) | (0 << 16) | ((lo as u16) as u32);
let addi = ((14u32 << 26) | (11 << 21)) | ((lo as u16) as u32);
// addi r11,r0,lo would set r11=lo (sign-extended); we need addis+addi
// chained. Re-encode addis into r11 from r0, then addi r11,r11,lo.
let addi2 = (14u32 << 26) | (11 << 21) | (11 << 16) | ((lo as u16) as u32);
let stw = (36u32 << 26) | (11 << 21) | (31 << 16) | 0; // stw r11,0(r31)
let stw = (36u32 << 26) | (11 << 21) | (31 << 16); // stw r11,0(r31)
let at = (ctor - image_base) as usize;
pe[at..at + 4].copy_from_slice(&addis.to_be_bytes());
pe[at + 4..at + 8].copy_from_slice(&addi2.to_be_bytes());
@@ -736,12 +902,20 @@ mod tests {
raw_size: 0x200,
flags: 0x4000_0040,
}];
let mut funcs: std::collections::BTreeMap<u32, (u32, bool)> = std::collections::BTreeMap::new();
let mut funcs: std::collections::BTreeMap<u32, (u32, bool)> =
std::collections::BTreeMap::new();
funcs.insert(ctor, (ctor + 0x40, false));
let anchors = scan_vptr_write_constants(
&pe, image_base, &funcs, &sections, &std::collections::HashSet::new(),
&pe,
image_base,
&funcs,
&sections,
&std::collections::HashSet::new(),
);
assert!(
anchors.contains(&vt_base),
"ctor vptr store must yield anchor {vt_base:#x}, got {anchors:?}"
);
assert!(anchors.contains(&vt_base), "ctor vptr store must yield anchor {vt_base:#x}, got {anchors:?}");
}
#[test]
@@ -776,66 +950,14 @@ mod tests {
},
];
let mut function_starts = std::collections::BTreeSet::new();
for &pc in &m { function_starts.insert(pc); }
let vtables = analyze(&pe, image_base, &sections, &function_starts);
assert_eq!(vtables.len(), 0, "runs of 2 must be rejected to keep false-positive rate down");
}
}
// ── RTTI relabelling ───────────────────────────────────────────────────────
/// Overwrite heuristic vtable identity with the authoritative RTTI walk.
///
/// [`analyze_with_anchors`] names a table either from its own inline COL walk
/// or, failing that, with a synthetic `ANON_Class_<hash>`. [`crate::rtti`]
/// resolves the same question top-down from the structures the linker emitted,
/// which is exact — so wherever the two disagree, RTTI wins. Rows RTTI knows
/// nothing about keep their heuristic name.
///
/// `base_classes_json` is rebuilt here as the class's full linearised base list
/// (excluding index 0, which is the class itself), which is strictly more than
/// the first-level list the inline walk produced.
///
/// Returns the number of vtables that gained a real class name.
pub fn apply_rtti_names(vtables: &mut [Vtable], rtti: &crate::rtti::RttiResult) -> usize {
use std::collections::BTreeMap;
let names = rtti.vtable_class_names();
let locator_by_vtable: BTreeMap<u32, &crate::rtti::CompleteObjectLocator> = rtti
.locators
.iter()
.filter_map(|c| c.vtable_address.map(|v| (v, c)))
.collect();
// class-hierarchy VA → base class names, in the linker's order.
let mut bases_by_chd: BTreeMap<u32, Vec<&str>> = BTreeMap::new();
for b in &rtti.base_classes {
if b.index == 0 { continue; } // index 0 is the class itself
bases_by_chd.entry(b.class_hierarchy).or_default().push(b.name.as_str());
}
let mut named = 0usize;
for vt in vtables.iter_mut() {
let Some((class_name, offset)) = names.get(&vt.address) else { continue };
// A secondary-base vftable belongs to the same class but is a distinct
// table; keep them apart by suffixing the subobject offset.
vt.class_name = if *offset == 0 {
class_name.clone()
} else {
format!("{class_name}#base+0x{offset:X}")
};
vt.rtti_present = true;
if let Some(col) = locator_by_vtable.get(&vt.address) {
vt.col_address = Some(col.address);
vt.base_classes_json = bases_by_chd.get(&col.class_hierarchy).map(|names| {
let items: Vec<String> = names
.iter()
.map(|n| format!("\"{}\"", n.replace('\\', "\\\\").replace('"', "\\\"")))
.collect();
format!("[{}]", items.join(","))
});
for &pc in &m {
function_starts.insert(pc);
}
named += 1;
let vtables = analyze(&pe, image_base, &sections, &function_starts);
assert_eq!(
vtables.len(),
0,
"runs of 2 must be rejected to keep false-positive rate down"
);
}
named
}

View File

@@ -119,7 +119,10 @@ fn be16(b: &[u8], o: usize) -> Option<u16> {
}
fn be32(b: &[u8], o: usize) -> Option<u32> {
Some(u32::from_be_bytes([
*b.get(o)?, *b.get(o + 1)?, *b.get(o + 2)?, *b.get(o + 3)?,
*b.get(o)?,
*b.get(o + 1)?,
*b.get(o + 2)?,
*b.get(o + 3)?,
]))
}
fn be64(b: &[u8], o: usize) -> Option<u64> {
@@ -188,9 +191,12 @@ pub fn analyze(image: &[u8], base: usize) -> Option<Xdbf> {
for i in 0..entry_used {
let p = entry_table + i * 18;
let (Some(namespace), Some(id), Some(off), Some(size)) =
(be16(image, p), be64(image, p + 2), be32(image, p + 10), be32(image, p + 14))
else {
let (Some(namespace), Some(id), Some(off), Some(size)) = (
be16(image, p),
be64(image, p + 2),
be32(image, p + 10),
be32(image, p + 14),
) else {
continue;
};
let body = data_start + off as usize;
@@ -212,7 +218,11 @@ pub fn analyze(image: &[u8], base: usize) -> Option<Xdbf> {
id,
offset: body,
size,
format: if image[body..].starts_with(b"\x89PNG") { "png" } else { "unknown" },
format: if image[body..].starts_with(b"\x89PNG") {
"png"
} else {
"unknown"
},
}),
NS_STRING_TABLE => {
if let Some(t) = parse_string_table(image, body, size, id as u32) {
@@ -220,7 +230,9 @@ pub fn analyze(image: &[u8], base: usize) -> Option<Xdbf> {
}
}
NS_METADATA => match magic.as_deref() {
Some("XACH") => out.achievements.extend(parse_achievements(image, body, size)),
Some("XACH") => out
.achievements
.extend(parse_achievements(image, body, size)),
Some("XTHD") => out.title = parse_title_header(image, body),
Some("XSTC") => out.default_language = be32(image, body + 12),
_ => {}
@@ -244,16 +256,21 @@ pub fn analyze(image: &[u8], base: usize) -> Option<Xdbf> {
/// `XACH`: `magic, version, size, count u16`, then 36-byte records.
fn parse_achievements(image: &[u8], body: usize, size: usize) -> Vec<Achievement> {
let Some(count) = be16(image, body + 12) else { return Vec::new() };
let Some(count) = be16(image, body + 12) else {
return Vec::new();
};
let mut out = Vec::with_capacity(count as usize);
for i in 0..count as usize {
let p = body + 14 + i * 36;
if p + 36 > body + size {
break;
}
let (Some(id), Some(label_id), Some(description_id), Some(unachieved_id)) =
(be16(image, p), be16(image, p + 2), be16(image, p + 4), be16(image, p + 6))
else {
let (Some(id), Some(label_id), Some(description_id), Some(unachieved_id)) = (
be16(image, p),
be16(image, p + 2),
be16(image, p + 4),
be16(image, p + 6),
) else {
break;
};
out.push(Achievement {
@@ -273,7 +290,12 @@ fn parse_achievements(image: &[u8], body: usize, size: usize) -> Vec<Achievement
///
/// Bodies are UTF-8 (the ASCII subset for most locales; Japanese uses the full
/// range), decoded lossily so one bad table cannot drop a whole language.
fn parse_string_table(image: &[u8], body: usize, size: usize, language: u32) -> Option<StringTable> {
fn parse_string_table(
image: &[u8],
body: usize,
size: usize,
language: u32,
) -> Option<StringTable> {
if fourcc(be32(image, body)?)? != "XSTR" {
return None;
}
@@ -282,7 +304,9 @@ fn parse_string_table(image: &[u8], body: usize, size: usize, language: u32) ->
let mut p = body + 14;
let mut strings = Vec::with_capacity(count as usize);
for _ in 0..count {
let (Some(id), Some(len)) = (be16(image, p), be16(image, p + 2)) else { break };
let (Some(id), Some(len)) = (be16(image, p), be16(image, p + 2)) else {
break;
};
let s = p + 4;
let e = s + len as usize;
if e > end || e > image.len() {
@@ -328,12 +352,12 @@ mod tests {
xach.extend(0u32.to_be_bytes());
xach.extend(1u16.to_be_bytes()); // count
let mut rec = Vec::new();
rec.extend(7u16.to_be_bytes()); // id
rec.extend(7u16.to_be_bytes()); // id
rec.extend(100u16.to_be_bytes()); // label
rec.extend(101u16.to_be_bytes()); // description
rec.extend(102u16.to_be_bytes()); // unachieved
rec.extend(9u32.to_be_bytes()); // image id
rec.extend(20u16.to_be_bytes()); // gamerscore
rec.extend(9u32.to_be_bytes()); // image id
rec.extend(20u16.to_be_bytes()); // gamerscore
rec.extend(0u16.to_be_bytes());
rec.extend(0x0Cu32.to_be_bytes()); // flags
rec.extend([0u8; 16]);
@@ -358,7 +382,7 @@ mod tests {
xthd.extend(1u32.to_be_bytes());
xthd.extend(0u32.to_be_bytes());
xthd.extend(0x5351_07D4u32.to_be_bytes()); // title id
xthd.extend(1u32.to_be_bytes()); // type = full
xthd.extend(1u32.to_be_bytes()); // type = full
xthd.extend(1u16.to_be_bytes());
xthd.extend(2u16.to_be_bytes());
xthd.extend(3u16.to_be_bytes());
@@ -418,7 +442,11 @@ mod tests {
assert_eq!(t.language, 1);
assert_eq!(t.strings[0], (100, "Space Combat Award".to_string()));
// The achievement's label resolves through the table.
let name = t.strings.iter().find(|(i, _)| *i == a.label_id).map(|(_, s)| s.as_str());
let name = t
.strings
.iter()
.find(|(i, _)| *i == a.label_id)
.map(|(_, s)| s.as_str());
assert_eq!(name, Some("Space Combat Award"));
}

View File

@@ -1,8 +1,8 @@
//! Cross-reference analysis for Xbox 360 PE images.
use crate::func::FuncAnalysis;
use std::collections::HashMap;
use sylpheed_xex::pe::PeSection;
use crate::func::FuncAnalysis;
// ── Cross-reference types ────────────────────────────────────────────────
@@ -21,19 +21,22 @@ pub enum XrefKind {
impl XrefKind {
pub fn tag(self) -> &'static str {
match self {
XrefKind::Call => "call",
XrefKind::Call => "call",
XrefKind::IndirectCall => "ind_call",
XrefKind::JumpTable => "jt",
XrefKind::Jump => "j",
XrefKind::Branch => "br",
XrefKind::DataRead => "read",
XrefKind::DataWrite => "write",
XrefKind::DataRef => "ref",
XrefKind::JumpTable => "jt",
XrefKind::Jump => "j",
XrefKind::Branch => "br",
XrefKind::DataRead => "read",
XrefKind::DataWrite => "write",
XrefKind::DataRef => "ref",
}
}
pub fn is_data(self) -> bool {
matches!(self, XrefKind::DataRead | XrefKind::DataWrite | XrefKind::DataRef)
matches!(
self,
XrefKind::DataRead | XrefKind::DataWrite | XrefKind::DataRef
)
}
pub fn db_tag(self) -> &'static str {
@@ -73,14 +76,14 @@ pub enum AddrMode {
impl AddrMode {
pub fn tag(self) -> &'static str {
match self {
AddrMode::DForm => "d_form",
AddrMode::LisAddi => "lis_addi",
AddrMode::LisOri => "lis_ori",
AddrMode::Multiword => "multiword",
AddrMode::DForm => "d_form",
AddrMode::LisAddi => "lis_addi",
AddrMode::LisOri => "lis_ori",
AddrMode::Multiword => "multiword",
AddrMode::XFormIndexed => "x_form_indexed",
AddrMode::XFormByteRev => "x_form_byterev",
AddrMode::Atomic => "atomic",
AddrMode::DCBZ => "dcbz",
AddrMode::Atomic => "atomic",
AddrMode::DCBZ => "dcbz",
}
}
}
@@ -113,7 +116,12 @@ pub fn analyze_xrefs(
import_map: &HashMap<u32, String>,
) -> XrefResult {
analyze_xrefs_skipping(
pe, image_base, entry_point, sections, func_analysis, import_map,
pe,
image_base,
entry_point,
sections,
func_analysis,
import_map,
&std::collections::BTreeSet::new(),
)
}
@@ -151,7 +159,9 @@ pub fn analyze_xrefs_skipping(
let mut xrefs: XrefMap = HashMap::new();
for section in sections {
if !section.is_code() { continue; }
if !section.is_code() {
continue;
}
let va_start = section.virtual_address;
let va_end = va_start + section.virtual_size;
let file_start = section.virtual_address as usize;
@@ -160,10 +170,10 @@ pub fn analyze_xrefs_skipping(
while addr < va_end {
let abs_addr = image_base + addr;
let off = (addr - va_start) as usize + file_start;
if off + 4 > pe.len() { break; }
let instr = u32::from_be_bytes([
pe[off], pe[off+1], pe[off+2], pe[off+3]
]);
if off + 4 > pe.len() {
break;
}
let instr = u32::from_be_bytes([pe[off], pe[off + 1], pe[off + 2], pe[off + 3]]);
if !data_words.contains(&abs_addr) {
collect_branch_target(instr, abs_addr, &mut labels, &mut xrefs);
@@ -176,13 +186,20 @@ pub fn analyze_xrefs_skipping(
let mut data_annotations: HashMap<u32, (u32, XrefKind)> = HashMap::new();
// Build set of valid data address ranges for filtering false positives
let data_ranges: Vec<(u32, u32)> = sections.iter()
.map(|s| (image_base + s.virtual_address,
image_base + s.virtual_address + s.virtual_size))
let data_ranges: Vec<(u32, u32)> = sections
.iter()
.map(|s| {
(
image_base + s.virtual_address,
image_base + s.virtual_address + s.virtual_size,
)
})
.collect();
for section in sections {
if !section.is_code() { continue; }
if !section.is_code() {
continue;
}
let va_start = section.virtual_address;
let va_end = va_start + section.virtual_size;
let file_start = section.virtual_address as usize;
@@ -194,10 +211,10 @@ pub fn analyze_xrefs_skipping(
while addr < va_end {
let abs_addr = image_base + addr;
let off = (addr - va_start) as usize + file_start;
if off + 4 > pe.len() { break; }
let instr = u32::from_be_bytes([
pe[off], pe[off+1], pe[off+2], pe[off+3]
]);
if off + 4 > pe.len() {
break;
}
let instr = u32::from_be_bytes([pe[off], pe[off + 1], pe[off + 2], pe[off + 3]]);
// A jump-table word is not an instruction. Skip it, and drop the
// tracked constants with it: the words around it belong to
@@ -217,9 +234,11 @@ pub fn analyze_xrefs_skipping(
// Reset tracking on function boundaries (prologue = mfspr rN, LR)
if opcode == 31 {
let xo = (instr >> 1) & 0x3FF;
if xo == 339 { // mfspr
if xo == 339 {
// mfspr
let spr = (((instr >> 16) & 0x1F) << 5) | ((instr >> 11) & 0x1F);
if spr == 8 { // LR
if spr == 8 {
// LR
reg_hi = [None; 32];
}
}
@@ -245,10 +264,13 @@ pub fn analyze_xrefs_skipping(
if is_in_ranges(data_addr, &data_ranges) {
data_annotations.insert(abs_addr, (data_addr, XrefKind::DataRef));
xrefs.entry(data_addr).or_default().push(Xref {
source: abs_addr, kind: XrefKind::DataRef,
source: abs_addr,
kind: XrefKind::DataRef,
addr_mode: Some(AddrMode::LisAddi),
});
labels.entry(data_addr).or_insert_with(|| format!("dat_{data_addr:08X}"));
labels
.entry(data_addr)
.or_insert_with(|| format!("dat_{data_addr:08X}"));
}
reg_hi[rd] = Some(data_addr); // propagate for chained access
} else {
@@ -263,10 +285,13 @@ pub fn analyze_xrefs_skipping(
if is_in_ranges(data_addr, &data_ranges) {
data_annotations.insert(abs_addr, (data_addr, XrefKind::DataRef));
xrefs.entry(data_addr).or_default().push(Xref {
source: abs_addr, kind: XrefKind::DataRef,
source: abs_addr,
kind: XrefKind::DataRef,
addr_mode: Some(AddrMode::LisOri),
});
labels.entry(data_addr).or_insert_with(|| format!("dat_{data_addr:08X}"));
labels
.entry(data_addr)
.or_insert_with(|| format!("dat_{data_addr:08X}"));
}
reg_hi[ra] = Some(data_addr);
} else {
@@ -276,17 +301,21 @@ pub fn analyze_xrefs_skipping(
// Load instructions: lwz, lbz, lhz, lha, lfs, lfd, lwzu, etc.
32 | 33 | 34 | 35 | 40 | 41 | 42 | 43 | 48 | 49 | 50 | 51 => {
if ra != 0
&& let Some(base) = reg_hi[ra] {
let data_addr = base.wrapping_add(simm as u32);
if is_in_ranges(data_addr, &data_ranges) {
data_annotations.insert(abs_addr, (data_addr, XrefKind::DataRead));
xrefs.entry(data_addr).or_default().push(Xref {
source: abs_addr, kind: XrefKind::DataRead,
addr_mode: Some(AddrMode::DForm),
});
labels.entry(data_addr).or_insert_with(|| format!("dat_{data_addr:08X}"));
}
&& let Some(base) = reg_hi[ra]
{
let data_addr = base.wrapping_add(simm as u32);
if is_in_ranges(data_addr, &data_ranges) {
data_annotations.insert(abs_addr, (data_addr, XrefKind::DataRead));
xrefs.entry(data_addr).or_default().push(Xref {
source: abs_addr,
kind: XrefKind::DataRead,
addr_mode: Some(AddrMode::DForm),
});
labels
.entry(data_addr)
.or_insert_with(|| format!("dat_{data_addr:08X}"));
}
}
// Load into rD may clobber the tracked value
reg_hi[rd] = None;
}
@@ -302,10 +331,13 @@ pub fn analyze_xrefs_skipping(
if is_in_ranges(addr_w, &data_ranges) {
data_annotations.insert(abs_addr, (addr_w, XrefKind::DataRead));
xrefs.entry(addr_w).or_default().push(Xref {
source: abs_addr, kind: XrefKind::DataRead,
source: abs_addr,
kind: XrefKind::DataRead,
addr_mode: Some(AddrMode::Multiword),
});
labels.entry(addr_w).or_insert_with(|| format!("dat_{addr_w:08X}"));
labels
.entry(addr_w)
.or_insert_with(|| format!("dat_{addr_w:08X}"));
}
addr_w = addr_w.wrapping_add(4);
}
@@ -315,17 +347,21 @@ pub fn analyze_xrefs_skipping(
// Store instructions: stw, stb, sth, stfs, stfd, stwu, etc.
36 | 37 | 38 | 39 | 44 | 45 | 52 | 53 | 54 | 55 => {
if ra != 0
&& let Some(base) = reg_hi[ra] {
let data_addr = base.wrapping_add(simm as u32);
if is_in_ranges(data_addr, &data_ranges) {
data_annotations.insert(abs_addr, (data_addr, XrefKind::DataWrite));
xrefs.entry(data_addr).or_default().push(Xref {
source: abs_addr, kind: XrefKind::DataWrite,
addr_mode: Some(AddrMode::DForm),
});
labels.entry(data_addr).or_insert_with(|| format!("dat_{data_addr:08X}"));
}
&& let Some(base) = reg_hi[ra]
{
let data_addr = base.wrapping_add(simm as u32);
if is_in_ranges(data_addr, &data_ranges) {
data_annotations.insert(abs_addr, (data_addr, XrefKind::DataWrite));
xrefs.entry(data_addr).or_default().push(Xref {
source: abs_addr,
kind: XrefKind::DataWrite,
addr_mode: Some(AddrMode::DForm),
});
labels
.entry(data_addr)
.or_insert_with(|| format!("dat_{data_addr:08X}"));
}
}
}
// stmw rS, simm(rA) — D-form multi-word store. Writes
// (32-rS) consecutive 4-byte words from rS..r31 to
@@ -339,10 +375,13 @@ pub fn analyze_xrefs_skipping(
if is_in_ranges(addr_w, &data_ranges) {
data_annotations.insert(abs_addr, (addr_w, XrefKind::DataWrite));
xrefs.entry(addr_w).or_default().push(Xref {
source: abs_addr, kind: XrefKind::DataWrite,
source: abs_addr,
kind: XrefKind::DataWrite,
addr_mode: Some(AddrMode::Multiword),
});
labels.entry(addr_w).or_insert_with(|| format!("dat_{addr_w:08X}"));
labels
.entry(addr_w)
.or_insert_with(|| format!("dat_{addr_w:08X}"));
}
addr_w = addr_w.wrapping_add(4);
}
@@ -374,8 +413,8 @@ pub fn analyze_xrefs_skipping(
662 => Some((AddrMode::XFormByteRev, XrefKind::DataWrite)), // stwbrx
918 => Some((AddrMode::XFormByteRev, XrefKind::DataWrite)), // sthbrx
// Byte-reverse loads
534 => Some((AddrMode::XFormByteRev, XrefKind::DataRead)), // lwbrx
790 => Some((AddrMode::XFormByteRev, XrefKind::DataRead)), // lhbrx
534 => Some((AddrMode::XFormByteRev, XrefKind::DataRead)), // lwbrx
790 => Some((AddrMode::XFormByteRev, XrefKind::DataRead)), // lhbrx
// dcbz — cache-line zero (32-byte clear). Treat as a write.
1014 => Some((AddrMode::DCBZ, XrefKind::DataWrite)),
// Plain X-form indexed stores (the common ones)
@@ -388,32 +427,32 @@ pub fn analyze_xrefs_skipping(
149 => Some((AddrMode::XFormIndexed, XrefKind::DataWrite)), // stdx
181 => Some((AddrMode::XFormIndexed, XrefKind::DataWrite)), // stdux
// Plain X-form indexed loads
23 => Some((AddrMode::XFormIndexed, XrefKind::DataRead)), // lwzx
87 => Some((AddrMode::XFormIndexed, XrefKind::DataRead)), // lbzx
279 => Some((AddrMode::XFormIndexed, XrefKind::DataRead)), // lhzx
343 => Some((AddrMode::XFormIndexed, XrefKind::DataRead)), // lhax
55 => Some((AddrMode::XFormIndexed, XrefKind::DataRead)), // lwzux
119 => Some((AddrMode::XFormIndexed, XrefKind::DataRead)), // lbzux
311 => Some((AddrMode::XFormIndexed, XrefKind::DataRead)), // lhzux
375 => Some((AddrMode::XFormIndexed, XrefKind::DataRead)), // lhaux
21 => Some((AddrMode::XFormIndexed, XrefKind::DataRead)), // ldx
53 => Some((AddrMode::XFormIndexed, XrefKind::DataRead)), // ldux
23 => Some((AddrMode::XFormIndexed, XrefKind::DataRead)), // lwzx
87 => Some((AddrMode::XFormIndexed, XrefKind::DataRead)), // lbzx
279 => Some((AddrMode::XFormIndexed, XrefKind::DataRead)), // lhzx
343 => Some((AddrMode::XFormIndexed, XrefKind::DataRead)), // lhax
55 => Some((AddrMode::XFormIndexed, XrefKind::DataRead)), // lwzux
119 => Some((AddrMode::XFormIndexed, XrefKind::DataRead)), // lbzux
311 => Some((AddrMode::XFormIndexed, XrefKind::DataRead)), // lhzux
375 => Some((AddrMode::XFormIndexed, XrefKind::DataRead)), // lhaux
21 => Some((AddrMode::XFormIndexed, XrefKind::DataRead)), // ldx
53 => Some((AddrMode::XFormIndexed, XrefKind::DataRead)), // ldux
// AltiVec/VMX (opcode 31) loads & stores. Element
// variants store one byte/halfword/word; full
// `stvx` stores 16 bytes. Address resolution still
// requires both rA and rB constant — common only
// in static-table setup loops.
231 => Some((AddrMode::XFormIndexed, XrefKind::DataWrite)), // stvx
487 => Some((AddrMode::XFormIndexed, XrefKind::DataWrite)), // stvxl
135 => Some((AddrMode::XFormIndexed, XrefKind::DataWrite)), // stvebx
167 => Some((AddrMode::XFormIndexed, XrefKind::DataWrite)), // stvehx
199 => Some((AddrMode::XFormIndexed, XrefKind::DataWrite)), // stvewx
231 => Some((AddrMode::XFormIndexed, XrefKind::DataWrite)), // stvx
487 => Some((AddrMode::XFormIndexed, XrefKind::DataWrite)), // stvxl
135 => Some((AddrMode::XFormIndexed, XrefKind::DataWrite)), // stvebx
167 => Some((AddrMode::XFormIndexed, XrefKind::DataWrite)), // stvehx
199 => Some((AddrMode::XFormIndexed, XrefKind::DataWrite)), // stvewx
// AltiVec/VMX loads — same XO range, kind=read.
103 => Some((AddrMode::XFormIndexed, XrefKind::DataRead)), // lvx
359 => Some((AddrMode::XFormIndexed, XrefKind::DataRead)), // lvxl
7 => Some((AddrMode::XFormIndexed, XrefKind::DataRead)), // lvebx
39 => Some((AddrMode::XFormIndexed, XrefKind::DataRead)), // lvehx
71 => Some((AddrMode::XFormIndexed, XrefKind::DataRead)), // lvewx
103 => Some((AddrMode::XFormIndexed, XrefKind::DataRead)), // lvx
359 => Some((AddrMode::XFormIndexed, XrefKind::DataRead)), // lvxl
7 => Some((AddrMode::XFormIndexed, XrefKind::DataRead)), // lvebx
39 => Some((AddrMode::XFormIndexed, XrefKind::DataRead)), // lvehx
71 => Some((AddrMode::XFormIndexed, XrefKind::DataRead)), // lvewx
_ => None,
}
};
@@ -423,10 +462,13 @@ pub fn analyze_xrefs_skipping(
{
data_annotations.insert(abs_addr, (data_addr, kind));
xrefs.entry(data_addr).or_default().push(Xref {
source: abs_addr, kind,
source: abs_addr,
kind,
addr_mode: Some(addr_mode),
});
labels.entry(data_addr).or_insert_with(|| format!("dat_{data_addr:08X}"));
labels
.entry(data_addr)
.or_insert_with(|| format!("dat_{data_addr:08X}"));
}
// Fall through: any X-form op may write rD; invalidate.
reg_hi[rd] = None;
@@ -435,7 +477,8 @@ pub fn analyze_xrefs_skipping(
_ => {
// Conservatively invalidate for instructions that modify rD
// (most ALU ops, loads, etc.)
if opcode != 18 && opcode != 16 && opcode != 17 { // skip branch/sc
if opcode != 18 && opcode != 16 && opcode != 17 {
// skip branch/sc
reg_hi[rd] = None;
}
}
@@ -456,10 +499,19 @@ pub fn analyze_xrefs_skipping(
"xref analysis complete"
);
XrefResult { labels, xrefs, data_annotations }
XrefResult {
labels,
xrefs,
data_annotations,
}
}
fn collect_branch_target(instr: u32, addr: u32, labels: &mut HashMap<u32, String>, xrefs: &mut XrefMap) {
fn collect_branch_target(
instr: u32,
addr: u32,
labels: &mut HashMap<u32, String>,
xrefs: &mut XrefMap,
) {
let op = (instr >> 26) & 0x3F;
match op {
18 => {
@@ -467,18 +519,38 @@ fn collect_branch_target(instr: u32, addr: u32, labels: &mut HashMap<u32, String
let li = sign_ext26(instr & 0x03FFFFFC);
let aa = instr & 2 != 0;
let lk = instr & 1 != 0;
let target = if aa { li as u32 } else { addr.wrapping_add(li as u32) };
labels.entry(target).or_insert_with(|| format!("loc_{target:08X}"));
let target = if aa {
li as u32
} else {
addr.wrapping_add(li as u32)
};
labels
.entry(target)
.or_insert_with(|| format!("loc_{target:08X}"));
let kind = if lk { XrefKind::Call } else { XrefKind::Jump };
xrefs.entry(target).or_default().push(Xref { source: addr, kind, addr_mode: None });
xrefs.entry(target).or_default().push(Xref {
source: addr,
kind,
addr_mode: None,
});
}
16 => {
// B-form: bc/bcl
let bd = sign_ext16(instr & 0xFFFC);
let aa = instr & 2 != 0;
let target = if aa { bd as u32 } else { addr.wrapping_add(bd as u32) };
labels.entry(target).or_insert_with(|| format!("loc_{target:08X}"));
xrefs.entry(target).or_default().push(Xref { source: addr, kind: XrefKind::Branch, addr_mode: None });
let target = if aa {
bd as u32
} else {
addr.wrapping_add(bd as u32)
};
labels
.entry(target)
.or_insert_with(|| format!("loc_{target:08X}"));
xrefs.entry(target).or_default().push(Xref {
source: addr,
kind: XrefKind::Branch,
addr_mode: None,
});
}
_ => {}
}
@@ -493,7 +565,9 @@ fn sign_ext26(val: u32) -> i32 {
}
fn is_in_ranges(addr: u32, ranges: &[(u32, u32)]) -> bool {
ranges.iter().any(|&(start, end)| addr >= start && addr < end)
ranges
.iter()
.any(|&(start, end)| addr >= start && addr < end)
}
/// Find which section a data address falls in.
@@ -521,10 +595,11 @@ pub fn resolve_source_label(
// Find the containing function (largest start <= addr)
if let Some((&func_start, _fi)) = func_analysis.functions.range(..=addr).next_back()
&& let Some(func_label) = labels.get(&func_start) {
let offset = addr - func_start;
return format!("{func_label}+0x{offset:X}");
}
&& let Some(func_label) = labels.get(&func_start)
{
let offset = addr - func_start;
return format!("{func_label}+0x{offset:X}");
}
format!("0x{addr:08X}")
}
@@ -546,18 +621,30 @@ mod tests {
AddrMode::DCBZ,
];
let tags: std::collections::HashSet<&str> = modes.iter().map(|m| m.tag()).collect();
assert_eq!(tags.len(), modes.len(), "every AddrMode variant must have a unique tag");
assert_eq!(
tags.len(),
modes.len(),
"every AddrMode variant must have a unique tag"
);
}
#[test]
fn xref_struct_carries_addr_mode_for_data_edges() {
let x = Xref { source: 0x1234, kind: XrefKind::DataWrite, addr_mode: Some(AddrMode::DForm) };
let x = Xref {
source: 0x1234,
kind: XrefKind::DataWrite,
addr_mode: Some(AddrMode::DForm),
};
assert_eq!(x.addr_mode.unwrap().tag(), "d_form");
}
#[test]
fn xref_struct_addr_mode_is_none_for_call_edges() {
let x = Xref { source: 0x1234, kind: XrefKind::Call, addr_mode: None };
let x = Xref {
source: 0x1234,
kind: XrefKind::Call,
addr_mode: None,
};
assert!(x.addr_mode.is_none());
}
}

View File

@@ -13,15 +13,19 @@ use std::io::Write;
use duckdb::Connection;
use sylpheed_xex::pe::PeSection;
use sylpheed_xexdb::DbWriter;
use sylpheed_xexdb::formatter::DisasmInfo;
use sylpheed_xexdb::func::{FuncAnalysis, FuncInfo};
use sylpheed_xexdb::rtti::RttiResult;
use sylpheed_xexdb::xref::XrefMap;
use sylpheed_xex::pe::PeSection;
/// Build a 16-byte `.text` section: 4 instructions (mflr / nop / blr / nop).
fn synthetic_pe() -> (Vec<u8>, Vec<PeSection>, Vec<sylpheed_xex::header::ImportLibrary>) {
fn synthetic_pe() -> (
Vec<u8>,
Vec<PeSection>,
Vec<sylpheed_xex::header::ImportLibrary>,
) {
// VA layout: image_base + 0x1000 = .text start (so RVA = 0x1000).
// The DB writer expects pe[rva] to hold the byte at that RVA, so the
// buffer must be at least 0x1000 + section_size bytes long.
@@ -30,9 +34,9 @@ fn synthetic_pe() -> (Vec<u8>, Vec<PeSection>, Vec<sylpheed_xex::header::ImportL
// mfspr r12, LR (a.k.a. mflr r12) — opcode 31, xo 339, spr 8 (LR).
// Encoded with spr halves swapped per the ISA: spr_field = (8<<5).
(31u32 << 26) | (12 << 21) | ((8 << 5) << 11) | (339 << 1),
0x60000000, // nop (ori r0, r0, 0)
(19u32 << 26) | (20 << 21) | (16 << 1), // blr (bclr 20, 0)
0x60000000, // nop
0x60000000, // nop (ori r0, r0, 0)
(19u32 << 26) | (20 << 21) | (16 << 1), // blr (bclr 20, 0)
0x60000000, // nop
];
let mut pe = vec![0u8; RVA + 16];
@@ -111,10 +115,21 @@ fn db_schema_matches_expected_columns() {
w.ingest_instructions(&pe, &info, &func_analysis, &labels, &BTreeSet::new())
.expect("ingest_instructions");
w.write_analysis_results(
&pe, &info, &func_analysis, &labels, &xrefs,
&[], &[], &[], None, &[], &[], &RttiResult::default(), None,
&pe,
&info,
&func_analysis,
&labels,
&xrefs,
&[],
&[],
&[],
None,
&[],
&[],
&RttiResult::default(),
None,
)
.expect("write_analysis_results");
.expect("write_analysis_results");
w.create_sql_views().expect("create_sql_views");
}
@@ -122,258 +137,348 @@ fn db_schema_matches_expected_columns() {
// Lock the column layout per table. Pairs are (name, type).
let expected: &[(&str, &[(&str, &str)])] = &[
("metadata", &[
("key", "VARCHAR"),
("value", "VARCHAR"),
]),
("sections", &[
("name", "VARCHAR"),
("virtual_address", "BIGINT"),
("virtual_size", "BIGINT"),
("raw_offset", "BIGINT"),
("raw_size", "BIGINT"),
("flags", "BIGINT"),
("is_code", "BOOLEAN"),
]),
("imports", &[
("library", "VARCHAR"),
("ordinal", "BIGINT"),
("name", "VARCHAR"),
("record_type", "BIGINT"),
("address", "BIGINT"),
]),
("instructions", &[
("address", "BIGINT"),
("raw", "BIGINT"),
("mnemonic", "VARCHAR"),
("operands", "VARCHAR"),
("disasm", "VARCHAR"),
("ext_mnemonic", "VARCHAR"),
("ext_operands", "VARCHAR"),
("ext_disasm", "VARCHAR"),
("target_hex", "BIGINT"),
("section", "VARCHAR"),
("function", "BIGINT"),
("label", "VARCHAR"),
("is_data", "BOOLEAN"),
]),
("functions", &[
("address", "BIGINT"),
("name", "VARCHAR"),
("end_address", "BIGINT"),
("frame_size", "BIGINT"),
("saved_gprs", "BIGINT"),
("is_leaf", "BOOLEAN"),
("is_saverestore", "BOOLEAN"),
("pdata_validated", "BOOLEAN"),
("pdata_length", "BIGINT"),
("prolog_length", "BIGINT"),
("has_eh", "BOOLEAN"),
]),
("jump_tables", &[
("bctr_pc", "BIGINT"),
("function", "BIGINT"),
("table_address", "BIGINT"),
("entry_count", "BIGINT"),
("table_slots", "BIGINT"),
("index_map_address", "BIGINT"),
("index_map_count", "BIGINT"),
("case_bound", "BIGINT"),
("kind", "VARCHAR"),
]),
("jump_table_entries", &[
("bctr_pc", "BIGINT"),
("case_index", "BIGINT"),
("target_address", "BIGINT"),
]),
("data_in_code", &[
("address", "BIGINT"),
("length", "BIGINT"),
("kind", "VARCHAR"),
]),
("rtti_type_descriptors", &[
("address", "BIGINT"),
("mangled_name", "VARCHAR"),
("demangled_name", "VARCHAR"),
]),
("rtti_locators", &[
("address", "BIGINT"),
("subobject_offset", "BIGINT"),
("cd_offset", "BIGINT"),
("type_descriptor", "BIGINT"),
("class_hierarchy", "BIGINT"),
("vtable_address", "BIGINT"),
]),
("rtti_base_classes", &[
("class_hierarchy", "BIGINT"),
("base_index", "BIGINT"),
("type_descriptor", "BIGINT"),
("name", "VARCHAR"),
("num_contained_bases", "BIGINT"),
("mdisp", "BIGINT"),
("pdisp", "BIGINT"),
("vdisp", "BIGINT"),
("attributes", "BIGINT"),
]),
("pdata_entries", &[
("begin_address", "BIGINT"),
("end_address", "BIGINT"),
("function_length", "BIGINT"),
("prolog_length", "BIGINT"),
("flags", "BIGINT"),
]),
("labels", &[
("address", "BIGINT"),
("name", "VARCHAR"),
("kind", "VARCHAR"),
]),
("xdbf_entries", &[
("namespace", "BIGINT"),
("namespace_name", "VARCHAR"),
("id", "BIGINT"),
("body_offset", "BIGINT"),
("size", "BIGINT"),
("magic", "VARCHAR"),
]),
("xdbf_achievements", &[
("id", "BIGINT"),
("name", "VARCHAR"),
("unlocked_desc", "VARCHAR"),
("locked_desc", "VARCHAR"),
("label_id", "BIGINT"),
("description_id", "BIGINT"),
("unachieved_id", "BIGINT"),
("image_id", "BIGINT"),
("gamerscore", "BIGINT"),
("flags", "BIGINT"),
]),
("xdbf_strings", &[
("language", "BIGINT"),
("language_name", "VARCHAR"),
("string_id", "BIGINT"),
("value", "VARCHAR"),
]),
("xdbf_images", &[
("id", "BIGINT"),
("is_title_icon", "BOOLEAN"),
("body_offset", "BIGINT"),
("size", "BIGINT"),
("format", "VARCHAR"),
]),
("demangled_names", &[
("address", "BIGINT"),
("mangled", "VARCHAR"),
("raw_demangled", "VARCHAR"),
("namespace_path", "VARCHAR"),
("class_name", "VARCHAR"),
("method_name", "VARCHAR"),
("params_signature", "VARCHAR"),
]),
("vtables", &[
("address", "BIGINT"),
("length", "BIGINT"),
("col_address", "BIGINT"),
("class_name", "VARCHAR"),
("rtti_present", "BOOLEAN"),
("base_classes_json", "VARCHAR"),
]),
("methods", &[
("vtable_address", "BIGINT"),
("slot", "BIGINT"),
("function_address", "BIGINT"),
("mangled_name", "VARCHAR"),
("demangled_name", "VARCHAR"),
]),
("classes", &[
("name", "VARCHAR"),
("vtable_address", "BIGINT"),
("rtti_present", "BOOLEAN"),
("base_classes_json", "VARCHAR"),
]),
("strings", &[
("address", "BIGINT"),
("encoding", "VARCHAR"),
("length", "BIGINT"),
("content", "VARCHAR"),
("section", "VARCHAR"),
]),
("tls_info", &[
("raw_data_start", "BIGINT"),
("raw_data_end", "BIGINT"),
("index_address", "BIGINT"),
("callback_array", "BIGINT"),
("zero_fill_size", "BIGINT"),
("characteristics", "BIGINT"),
]),
("tls_callbacks", &[
("slot", "BIGINT"),
("address", "BIGINT"),
]),
("function_pointer_arrays", &[
("address", "BIGINT"),
("length", "BIGINT"),
("kind", "VARCHAR"),
]),
("function_pointer_array_entries", &[
("array_address", "BIGINT"),
("slot", "BIGINT"),
("function_address", "BIGINT"),
]),
("indirect_dispatch_sites", &[
("dispatch_pc", "BIGINT"),
("vptr_offset", "BIGINT"),
("slot", "BIGINT"),
("candidate_count", "BIGINT"),
("truncated", "BOOLEAN"),
]),
("indirect_dispatch_candidates", &[
("dispatch_pc", "BIGINT"),
("vtable_address", "BIGINT"),
("method_address", "BIGINT"),
]),
("vptr_writes", &[
("writer_pc", "BIGINT"),
("vtable_address", "BIGINT"),
("vptr_offset", "BIGINT"),
("writer_function", "BIGINT"),
]),
("eh_funcinfo", &[
("address", "BIGINT"),
("magic", "BIGINT"),
("max_state", "BIGINT"),
("p_unwind_map", "BIGINT"),
("n_try_blocks", "BIGINT"),
("p_try_block_map", "BIGINT"),
("n_ip_map_entries", "BIGINT"),
("p_ip_to_state_map", "BIGINT"),
("p_es_type_list", "BIGINT"),
("eh_flags", "BIGINT"),
]),
("eh_unwind_map", &[
("funcinfo_address", "BIGINT"),
("state_index", "BIGINT"),
("to_state", "BIGINT"),
("action_pc", "BIGINT"),
]),
("eh_try_blocks", &[
("funcinfo_address", "BIGINT"),
("try_index", "BIGINT"),
("try_low", "BIGINT"),
("try_high", "BIGINT"),
("catch_high", "BIGINT"),
("n_catches", "BIGINT"),
("p_handler_array", "BIGINT"),
]),
("xrefs", &[
("source", "BIGINT"),
("target", "BIGINT"),
("kind", "VARCHAR"),
("addr_mode", "VARCHAR"),
("instruction", "VARCHAR"),
("source_func", "BIGINT"),
("source_label", "VARCHAR"),
("target_label", "VARCHAR"),
]),
("metadata", &[("key", "VARCHAR"), ("value", "VARCHAR")]),
(
"sections",
&[
("name", "VARCHAR"),
("virtual_address", "BIGINT"),
("virtual_size", "BIGINT"),
("raw_offset", "BIGINT"),
("raw_size", "BIGINT"),
("flags", "BIGINT"),
("is_code", "BOOLEAN"),
],
),
(
"imports",
&[
("library", "VARCHAR"),
("ordinal", "BIGINT"),
("name", "VARCHAR"),
("record_type", "BIGINT"),
("address", "BIGINT"),
],
),
(
"instructions",
&[
("address", "BIGINT"),
("raw", "BIGINT"),
("mnemonic", "VARCHAR"),
("operands", "VARCHAR"),
("disasm", "VARCHAR"),
("ext_mnemonic", "VARCHAR"),
("ext_operands", "VARCHAR"),
("ext_disasm", "VARCHAR"),
("target_hex", "BIGINT"),
("section", "VARCHAR"),
("function", "BIGINT"),
("label", "VARCHAR"),
("is_data", "BOOLEAN"),
],
),
(
"functions",
&[
("address", "BIGINT"),
("name", "VARCHAR"),
("end_address", "BIGINT"),
("frame_size", "BIGINT"),
("saved_gprs", "BIGINT"),
("is_leaf", "BOOLEAN"),
("is_saverestore", "BOOLEAN"),
("pdata_validated", "BOOLEAN"),
("pdata_length", "BIGINT"),
("prolog_length", "BIGINT"),
("has_eh", "BOOLEAN"),
],
),
(
"jump_tables",
&[
("bctr_pc", "BIGINT"),
("function", "BIGINT"),
("table_address", "BIGINT"),
("entry_count", "BIGINT"),
("table_slots", "BIGINT"),
("index_map_address", "BIGINT"),
("index_map_count", "BIGINT"),
("case_bound", "BIGINT"),
("kind", "VARCHAR"),
],
),
(
"jump_table_entries",
&[
("bctr_pc", "BIGINT"),
("case_index", "BIGINT"),
("target_address", "BIGINT"),
],
),
(
"data_in_code",
&[
("address", "BIGINT"),
("length", "BIGINT"),
("kind", "VARCHAR"),
],
),
(
"rtti_type_descriptors",
&[
("address", "BIGINT"),
("mangled_name", "VARCHAR"),
("demangled_name", "VARCHAR"),
],
),
(
"rtti_locators",
&[
("address", "BIGINT"),
("subobject_offset", "BIGINT"),
("cd_offset", "BIGINT"),
("type_descriptor", "BIGINT"),
("class_hierarchy", "BIGINT"),
("vtable_address", "BIGINT"),
],
),
(
"rtti_base_classes",
&[
("class_hierarchy", "BIGINT"),
("base_index", "BIGINT"),
("type_descriptor", "BIGINT"),
("name", "VARCHAR"),
("num_contained_bases", "BIGINT"),
("mdisp", "BIGINT"),
("pdisp", "BIGINT"),
("vdisp", "BIGINT"),
("attributes", "BIGINT"),
],
),
(
"pdata_entries",
&[
("begin_address", "BIGINT"),
("end_address", "BIGINT"),
("function_length", "BIGINT"),
("prolog_length", "BIGINT"),
("flags", "BIGINT"),
],
),
(
"labels",
&[
("address", "BIGINT"),
("name", "VARCHAR"),
("kind", "VARCHAR"),
],
),
(
"xdbf_entries",
&[
("namespace", "BIGINT"),
("namespace_name", "VARCHAR"),
("id", "BIGINT"),
("body_offset", "BIGINT"),
("size", "BIGINT"),
("magic", "VARCHAR"),
],
),
(
"xdbf_achievements",
&[
("id", "BIGINT"),
("name", "VARCHAR"),
("unlocked_desc", "VARCHAR"),
("locked_desc", "VARCHAR"),
("label_id", "BIGINT"),
("description_id", "BIGINT"),
("unachieved_id", "BIGINT"),
("image_id", "BIGINT"),
("gamerscore", "BIGINT"),
("flags", "BIGINT"),
],
),
(
"xdbf_strings",
&[
("language", "BIGINT"),
("language_name", "VARCHAR"),
("string_id", "BIGINT"),
("value", "VARCHAR"),
],
),
(
"xdbf_images",
&[
("id", "BIGINT"),
("is_title_icon", "BOOLEAN"),
("body_offset", "BIGINT"),
("size", "BIGINT"),
("format", "VARCHAR"),
],
),
(
"demangled_names",
&[
("address", "BIGINT"),
("mangled", "VARCHAR"),
("raw_demangled", "VARCHAR"),
("namespace_path", "VARCHAR"),
("class_name", "VARCHAR"),
("method_name", "VARCHAR"),
("params_signature", "VARCHAR"),
],
),
(
"vtables",
&[
("address", "BIGINT"),
("length", "BIGINT"),
("col_address", "BIGINT"),
("class_name", "VARCHAR"),
("rtti_present", "BOOLEAN"),
("base_classes_json", "VARCHAR"),
],
),
(
"methods",
&[
("vtable_address", "BIGINT"),
("slot", "BIGINT"),
("function_address", "BIGINT"),
("mangled_name", "VARCHAR"),
("demangled_name", "VARCHAR"),
],
),
(
"classes",
&[
("name", "VARCHAR"),
("vtable_address", "BIGINT"),
("rtti_present", "BOOLEAN"),
("base_classes_json", "VARCHAR"),
],
),
(
"strings",
&[
("address", "BIGINT"),
("encoding", "VARCHAR"),
("length", "BIGINT"),
("content", "VARCHAR"),
("section", "VARCHAR"),
],
),
(
"tls_info",
&[
("raw_data_start", "BIGINT"),
("raw_data_end", "BIGINT"),
("index_address", "BIGINT"),
("callback_array", "BIGINT"),
("zero_fill_size", "BIGINT"),
("characteristics", "BIGINT"),
],
),
(
"tls_callbacks",
&[("slot", "BIGINT"), ("address", "BIGINT")],
),
(
"function_pointer_arrays",
&[
("address", "BIGINT"),
("length", "BIGINT"),
("kind", "VARCHAR"),
],
),
(
"function_pointer_array_entries",
&[
("array_address", "BIGINT"),
("slot", "BIGINT"),
("function_address", "BIGINT"),
],
),
(
"indirect_dispatch_sites",
&[
("dispatch_pc", "BIGINT"),
("vptr_offset", "BIGINT"),
("slot", "BIGINT"),
("candidate_count", "BIGINT"),
("truncated", "BOOLEAN"),
],
),
(
"indirect_dispatch_candidates",
&[
("dispatch_pc", "BIGINT"),
("vtable_address", "BIGINT"),
("method_address", "BIGINT"),
],
),
(
"vptr_writes",
&[
("writer_pc", "BIGINT"),
("vtable_address", "BIGINT"),
("vptr_offset", "BIGINT"),
("writer_function", "BIGINT"),
],
),
(
"eh_funcinfo",
&[
("address", "BIGINT"),
("magic", "BIGINT"),
("max_state", "BIGINT"),
("p_unwind_map", "BIGINT"),
("n_try_blocks", "BIGINT"),
("p_try_block_map", "BIGINT"),
("n_ip_map_entries", "BIGINT"),
("p_ip_to_state_map", "BIGINT"),
("p_es_type_list", "BIGINT"),
("eh_flags", "BIGINT"),
],
),
(
"eh_unwind_map",
&[
("funcinfo_address", "BIGINT"),
("state_index", "BIGINT"),
("to_state", "BIGINT"),
("action_pc", "BIGINT"),
],
),
(
"eh_try_blocks",
&[
("funcinfo_address", "BIGINT"),
("try_index", "BIGINT"),
("try_low", "BIGINT"),
("try_high", "BIGINT"),
("catch_high", "BIGINT"),
("n_catches", "BIGINT"),
("p_handler_array", "BIGINT"),
],
),
(
"xrefs",
&[
("source", "BIGINT"),
("target", "BIGINT"),
("kind", "VARCHAR"),
("addr_mode", "VARCHAR"),
("instruction", "VARCHAR"),
("source_func", "BIGINT"),
("source_label", "VARCHAR"),
("target_label", "VARCHAR"),
],
),
];
let mut errs: Vec<String> = Vec::new();
@@ -397,7 +502,8 @@ fn db_schema_matches_expected_columns() {
"{table}: column count mismatch (got {}, expected {})",
rows.len(),
cols.len()
).ok();
)
.ok();
errs.push(format!("{table}: count {} vs {}", rows.len(), cols.len()));
}
for (i, (got, expected_col)) in rows.iter().zip(cols.iter()).enumerate() {
@@ -410,19 +516,31 @@ fn db_schema_matches_expected_columns() {
}
}
assert!(errs.is_empty(), "schema drift detected:\n {}", errs.join("\n "));
assert!(
errs.is_empty(),
"schema drift detected:\n {}",
errs.join("\n ")
);
// Verify row counts in the populated tables.
let n_instr: i64 = conn
.query_row("SELECT COUNT(*) FROM instructions", [], |r| r.get(0))
.unwrap();
assert_eq!(n_instr, 4, "expected 4 instruction rows from the synthetic PE");
assert_eq!(
n_instr, 4,
"expected 4 instruction rows from the synthetic PE"
);
// The synthetic mflr should produce target_hex = NULL, blr likewise (indirect).
let n_with_target: i64 = conn
.query_row("SELECT COUNT(target_hex) FROM instructions", [], |r| r.get(0))
.query_row("SELECT COUNT(target_hex) FROM instructions", [], |r| {
r.get(0)
})
.unwrap();
assert_eq!(n_with_target, 0, "indirect-only fixture should have no direct branch targets");
assert_eq!(
n_with_target, 0,
"indirect-only fixture should have no direct branch targets"
);
// SQL views must be queryable. The `_` in SQL LIKE is a single-char
// wildcard, so we list the names explicitly rather than `LIKE 'v_%'`

View File

@@ -41,7 +41,10 @@ fn cpu_fixture(name: &str) -> PathBuf {
}
fn parse_hex(s: &str) -> u32 {
let trimmed = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")).unwrap_or(s);
let trimmed = s
.strip_prefix("0x")
.or_else(|| s.strip_prefix("0X"))
.unwrap_or(s);
u32::from_str_radix(trimmed, 16).expect("hex u32")
}
@@ -61,8 +64,7 @@ fn check_fixture(fixture_name: &str) {
let raw = parse_hex(&row.raw);
let addr = parse_hex(&row.addr);
let canonical =
sylpheed_ppc::disasm::format(&sylpheed_ppc::decode(raw, addr));
let canonical = sylpheed_ppc::disasm::format(&sylpheed_ppc::decode(raw, addr));
let shim = sylpheed_xexdb::ppc::disasm(raw, addr);
assert_eq!(
@@ -78,13 +80,33 @@ fn check_fixture(fixture_name: &str) {
// Also pin against the fixture's structured fields — guards against
// someone changing the cpu canon without regenerating the fixture.
assert_eq!(canonical.mnemonic, row.mnemonic, "mnemonic drift: {}", row.label);
assert_eq!(canonical.operands, row.operands, "operands drift: {}", row.label);
assert_eq!(canonical.ext_mnemonic, row.ext_mnemonic, "ext_mnemonic drift: {}", row.label);
assert_eq!(canonical.ext_operands, row.ext_operands, "ext_operands drift: {}", row.label);
assert_eq!(
canonical.mnemonic, row.mnemonic,
"mnemonic drift: {}",
row.label
);
assert_eq!(
canonical.operands, row.operands,
"operands drift: {}",
row.label
);
assert_eq!(
canonical.ext_mnemonic, row.ext_mnemonic,
"ext_mnemonic drift: {}",
row.label
);
assert_eq!(
canonical.ext_operands, row.ext_operands,
"ext_operands drift: {}",
row.label
);
let target_str = canonical.branch_target.map(|t| format!("0x{t:08X}"));
assert_eq!(target_str, row.branch_target, "branch_target drift: {}", row.label);
assert_eq!(
target_str, row.branch_target,
"branch_target drift: {}",
row.label
);
}
}