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; use std::io::BufRead;
fn main() -> Result<(), Box<dyn std::error::Error>> { 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 f = std::io::BufReader::new(std::fs::File::open(path)?);
let (mut ok, mut bad, mut invalid) = (0u32, 0u32, 0u32); let (mut ok, mut bad, mut invalid) = (0u32, 0u32, 0u32);
for line in f.lines() { for line in f.lines() {
let line = line?; let line = line?;
let mut it = line.split_whitespace(); 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 word = u32::from_str_radix(w.trim_start_matches("0x"), 16)?;
let d = sylpheed_ppc::decoder::decode(word, 0x8200_0000); let d = sylpheed_ppc::decoder::decode(word, 0x8200_0000);
let got = format!("{:?}", d.opcode); let got = format!("{:?}", d.opcode);

View File

@@ -18,34 +18,68 @@ impl DecodedInstr {
// Common field extractors (PPC bit numbering) // Common field extractors (PPC bit numbering)
/// Primary opcode (bits 0-5) /// 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 /// rD/rS/rT (bits 6-10) - destination/source register
#[inline] pub fn rd(&self) -> usize { extract_bits(self.raw, 6, 10) as usize } #[inline]
#[inline] pub fn rs(&self) -> usize { self.rd() } pub fn rd(&self) -> usize {
#[inline] pub fn rt(&self) -> usize { self.rd() } 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) /// 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) /// 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 /// 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 /// SIMM/UIMM (bits 16-31) - signed/unsigned immediate
#[inline] pub fn simm16(&self) -> i16 { (self.raw & 0xFFFF) as i16 } #[inline]
#[inline] pub fn uimm16(&self) -> u16 { (self.raw & 0xFFFF) as u16 } 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) /// 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) /// 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) /// 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); let li = extract_bits(self.raw, 6, 29);
// Sign-extend from 24 bits, then shift left 2 // Sign-extend from 24 bits, then shift left 2
let sign_extended = ((li as i32) << 8) >> 8; 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) /// 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 bd = extract_bits(self.raw, 16, 29);
let sign_extended = ((bd as i32) << 18) >> 18; let sign_extended = ((bd as i32) << 18) >> 18;
sign_extended << 2 sign_extended << 2
} }
/// BO field (bits 6-10) - branch options /// 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 /// 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 /// 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) /// 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 /// 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. /// 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. /// 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 /// VX128_R Rc bit — PPC bit 25 (host bit 6) per canary's FormatVX128_R
/// bitfield layout. PPCBUG-700. /// 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. /// 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. /// 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 /// 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. /// 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 /// MB, ME fields for rotate instructions
#[inline] pub fn mb(&self) -> u32 { extract_bits(self.raw, 21, 25) } #[inline]
#[inline] pub fn me(&self) -> u32 { extract_bits(self.raw, 26, 30) } 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 /// 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) /// 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) (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/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. /// 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) extract_bits(self.raw, 21, 25) | (extract_bits(self.raw, 26, 26) << 5)
} }
/// SPR field (bits 11-20, swapped halves) /// 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); let spr_raw = extract_bits(self.raw, 11, 20);
((spr_raw & 0x1F) << 5) | ((spr_raw >> 5) & 0x1F) ((spr_raw & 0x1F) << 5) | ((spr_raw >> 5) & 0x1F)
} }
/// CRM field (bits 12-19) for mtcrf /// 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 /// 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) /// 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 /// 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) /// 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) /// 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) /// 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 // VMX128 field extractors — bit positions match canary's
// FormatVX128/VX128_2/VX128_4/VX128_5/VX128_R bitfield layout // 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. /// VA128 = VA128l(5) | VA128h(1) << 5 | VA128H(1) << 6.
/// Canonical 7-bit register selector: PPC 11-15 (low), PPC 26 (mid), PPC 21 (high). /// 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, 11, 15)
| (extract_bits(self.raw, 26, 26) << 5) | (extract_bits(self.raw, 26, 26) << 5)
| (extract_bits(self.raw, 21, 21) << 6)) as usize | (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 /// VB128 = VB128l(5) | VB128h(2) << 5. Canary's VB128h is a 2-bit
/// contiguous field at PPC 30-31 (host bits 0-1). /// contiguous field at PPC 30-31 (host bits 0-1).
#[inline] pub fn vb128(&self) -> usize { #[inline]
(extract_bits(self.raw, 16, 20) pub fn vb128(&self) -> usize {
| (extract_bits(self.raw, 30, 31) << 5)) as 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 /// VD128 = VD128l(5) | VD128h(2) << 5. Canary's VD128h is a 2-bit
/// contiguous field at PPC 28-29 (host bits 2-3). /// contiguous field at PPC 28-29 (host bits 2-3).
#[inline] pub fn vd128(&self) -> usize { #[inline]
(extract_bits(self.raw, 6, 10) pub fn vd128(&self) -> usize {
| (extract_bits(self.raw, 28, 29) << 5)) as usize (extract_bits(self.raw, 6, 10) | (extract_bits(self.raw, 28, 29) << 5)) as usize
} }
/// VS128 - same encoding as VD128 /// 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. /// 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 /// 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. /// 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. /// 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) 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. /// 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 /// 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, /// 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) /// vb_hi 2 bits at PPC 30-31). va128 = va_lo | (va_h26<<5) | (va_h21<<6)
/// per canary's 7-bit VA selector. /// 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, fn vmx128_test_word(
vb_lo: u32, vb_hi: u32) -> u32 { 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). // PPC bit i -> host bit (31-i).
(vd_lo << (31 - 10)) // VD128l: PPC 6-10 = host 21-25 (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) | (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_h26 << (31 - 26)) // VA128h: PPC 26 = host 5
| (va_h21 << (31 - 21)) // VA128H: PPC 21 = host 10 | (va_h21 << (31 - 21)) // VA128H: PPC 21 = host 10
| (vb_lo << (31 - 20)) // VB128l: PPC 16-20 = host 11-15 | (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] #[test]
fn vmx128_vd128_low_5_bits_only() { fn vmx128_vd128_low_5_bits_only() {
// vd_lo = 0..31, vd_hi = 0 → vd128 = vd_lo // vd_lo = 0..31, vd_hi = 0 → vd128 = vd_lo
for r in 0..32u32 { for r in 0..32u32 {
let raw = (r as u32) << (31 - 10); let raw = r << (31 - 10);
let d = DecodedInstr { opcode: PpcOpcode::Invalid, raw, addr: 0 }; let d = DecodedInstr {
opcode: PpcOpcode::Invalid,
raw,
addr: 0,
};
assert_eq!(d.vd128(), r as usize, "vd_lo={r}"); assert_eq!(d.vd128(), r as usize, "vd_lo={r}");
} }
} }
@@ -1082,26 +1208,36 @@ mod tests {
#[test] #[test]
fn vmx128_vd128_high_low_bit_adds_32() { fn vmx128_vd128_high_low_bit_adds_32() {
// vd_lo = 0, VD128h = 0b01 (LSB only at host bit 2 = PPC 29) → vd128 = 32 // vd_lo = 0, VD128h = 0b01 (LSB only at host bit 2 = PPC 29) → vd128 = 32
let raw = (1u32 << (31 - 29)); let raw = 1u32 << (31 - 29);
let d = DecodedInstr { opcode: PpcOpcode::Invalid, raw, addr: 0 }; let d = DecodedInstr {
opcode: PpcOpcode::Invalid,
raw,
addr: 0,
};
assert_eq!(d.vd128(), 32); assert_eq!(d.vd128(), 32);
} }
#[test] #[test]
fn vmx128_vd128_high_high_bit_adds_64() { fn vmx128_vd128_high_high_bit_adds_64() {
// vd_lo = 0, VD128h = 0b10 (MSB only at host bit 3 = PPC 28) → vd128 = 64 // vd_lo = 0, VD128h = 0b10 (MSB only at host bit 3 = PPC 28) → vd128 = 64
let raw = (1u32 << (31 - 28)); let raw = 1u32 << (31 - 28);
let d = DecodedInstr { opcode: PpcOpcode::Invalid, raw, addr: 0 }; let d = DecodedInstr {
opcode: PpcOpcode::Invalid,
raw,
addr: 0,
};
assert_eq!(d.vd128(), 64); assert_eq!(d.vd128(), 64);
} }
#[test] #[test]
fn vmx128_vd128_full_127() { fn vmx128_vd128_full_127() {
// vd_lo = 31, VD128h = 0b11 → vd128 = 127 // vd_lo = 31, VD128h = 0b11 → vd128 = 127
let raw = (31u32 << (31 - 10)) let raw = (31u32 << (31 - 10)) | (1u32 << (31 - 28)) | (1u32 << (31 - 29));
| (1u32 << (31 - 28)) let d = DecodedInstr {
| (1u32 << (31 - 29)); opcode: PpcOpcode::Invalid,
let d = DecodedInstr { opcode: PpcOpcode::Invalid, raw, addr: 0 }; raw,
addr: 0,
};
assert_eq!(d.vd128(), 127); assert_eq!(d.vd128(), 127);
} }
@@ -1109,11 +1245,19 @@ mod tests {
fn vmx128_va128_canary_layout() { fn vmx128_va128_canary_layout() {
// va_lo = 7 at PPC 11-15, VA128h = 1 at PPC 26 → va128 = 7 | 32 = 39 // 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 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); assert_eq!(d.va128(), 39);
// VA128H = 1 at PPC 21 → va128 += 64 = 103 // VA128H = 1 at PPC 21 → va128 += 64 = 103
let raw = raw | (1u32 << (31 - 21)); 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); 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. // vb_lo = 5 at PPC 16-20. VB128h = 0b01 (LSB at PPC 31 = host 0) → +32.
// VB128h = 0b11 → +96. // VB128h = 0b11 → +96.
let raw = (5u32 << (31 - 20)) | (1u32 << (31 - 31)); 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); assert_eq!(d.vb128(), 5 | 32);
let raw = raw | (1u32 << (31 - 30)); 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); assert_eq!(d.vb128(), 5 | 32 | 64);
} }
@@ -1135,9 +1287,12 @@ mod tests {
for r in [0u32, 31, 32, 64, 96, 127] { for r in [0u32, 31, 32, 64, 96, 127] {
let lo = r & 0x1F; let lo = r & 0x1F;
let hi = (r >> 5) & 0x3; let hi = (r >> 5) & 0x3;
let raw = (lo << (31 - 10)) let raw = (lo << (31 - 10)) | (hi << (31 - 29));
| (hi << (31 - 29)); let d = DecodedInstr {
let d = DecodedInstr { opcode: PpcOpcode::Invalid, raw, addr: 0 }; opcode: PpcOpcode::Invalid,
raw,
addr: 0,
};
assert_eq!(d.vd128(), r as usize, "vd128 mismatch for r={r}"); 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.vs128(), r as usize, "vs128 mismatch for r={r}");
assert_eq!(d.vd128(), d.vs128()); assert_eq!(d.vd128(), d.vs128());
@@ -1150,7 +1305,11 @@ mod tests {
// Keep the helper validated against the real accessor. // Keep the helper validated against the real accessor.
// vd_lo=5, vd_hi=0b11 → vd128 = 5 | 96 = 101 // vd_lo=5, vd_hi=0b11 → vd128 = 5 | 96 = 101
let raw = vmx128_test_word(5, 3, 0, 0, 0, 0, 0); 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); 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. // Host bit 9 = 1 (PPC bit 22), host bits 6-8 = 0.
// So raw bit 9 set = raw |= 1 << 9 = 0x200 // So raw bit 9 set = raw |= 1 << 9 = 0x200
let raw = 0x200u32; // host bit 9 set only 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"); 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 // SH=1 (binary 0001): host bit 6 set = raw |= 1 << 6 = 0x40
let raw = 0x40u32; 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"); 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 // SH=15 (binary 1111): host bits 6-9 all set = raw |= 0xF << 6 = 0x3C0
let raw = 0x3C0u32; 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"); assert_eq!(d.vx128_5_sh(), 15, "SH=15: all 4 bits set");
// SH=0: raw=0 // 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"); assert_eq!(d.vx128_5_sh(), 0, "SH=0");
} }
@@ -1182,23 +1357,39 @@ mod tests {
fn vx128_4_accessors_correct_bit_positions() { fn vx128_4_accessors_correct_bit_positions() {
// z=3 (binary 11) at PPC bits 24-25 = host bits 6-7 // z=3 (binary 11) at PPC bits 24-25 = host bits 6-7
let raw = 0b11u32 << 6; 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"); 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 // IMM=0x15 (binary 10101) at PPC bits 11-15 = host bits 16-20
let raw2 = 0x15u32 << 16; 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"); 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 // Combined: z=1, IMM=0xA — fields must not bleed into each other
let raw3 = (0x1u32 << 6) | (0xAu32 << 16); 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_z(), 1, "z=1 combined");
assert_eq!(d3.vx128_4_imm(), 0xA, "IMM=0xA 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 // z=2, IMM=0xF — max 4-bit blend mask, exercises the full lower nibble
let raw4 = (0b10u32 << 6) | (0xFu32 << 16); 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_z(), 2, "z=2 from binary 10");
assert_eq!(d4.vx128_4_imm(), 0xF, "IMM=0xF all-ones nibble"); 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 // 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 // extract_bits(raw, 23, 25) = (raw >> (31-25)) & 0x7 = (raw >> 6) & 0x7
let raw = 5u32 << 6; // host bits 6-8 = 5 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); 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); 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); 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); assert_eq!(d1.vc128_2(), 1);
} }
@@ -1225,21 +1432,37 @@ mod tests {
fn vx128_p_perm_assembles_correctly() { fn vx128_p_perm_assembles_correctly() {
// PERMl=0x1F (all 5 bits set) at host bits 16-20: raw = 0x1F << 16 // PERMl=0x1F (all 5 bits set) at host bits 16-20: raw = 0x1F << 16
let raw = 0x1Fu32 << 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"); assert_eq!(d.vx128_p_perm(), 0x1F, "PERMl only");
// PERMh=0x7 (all 3 bits set) at host bits 6-8: raw = 0x7 << 6 = 0x1C0 // PERMh=0x7 (all 3 bits set) at host bits 6-8: raw = 0x7 << 6 = 0x1C0
let raw = 0x7u32 << 6; 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"); assert_eq!(d.vx128_p_perm(), 0x7 << 5, "PERMh only: bits 5-7");
// PERMl=0xA, PERMh=0x5: raw = (0xA << 16) | (0x5 << 6) // PERMl=0xA, PERMh=0x5: raw = (0xA << 16) | (0x5 << 6)
let raw = (0xAu32 << 16) | (0x5u32 << 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)); assert_eq!(d.vx128_p_perm(), 0xA | (0x5 << 5));
// PERMl and PERMh bits must not bleed into each other // 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); 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)] #[allow(non_camel_case_types)]
pub enum PpcOpcode { pub enum PpcOpcode {
// ALU // ALU
addcx, addex, addi, addic, addicx, addis, addmex, addx, addzex, addcx,
andcx, andisx, andix, andx, addex,
addi,
addic,
addicx,
addis,
addmex,
addx,
addzex,
andcx,
andisx,
andix,
andx,
// Branch // Branch
bcctrx, bclrx, bcx, bx, bcctrx,
bclrx,
bcx,
bx,
// Compare // Compare
cmp, cmpi, cmpl, cmpli, cmp,
cmpi,
cmpl,
cmpli,
// Count leading zeros // Count leading zeros
cntlzdx, cntlzwx, cntlzdx,
cntlzwx,
// Condition register // Condition register
crand, crandc, creqv, crnand, crnor, cror, crorc, crxor, crand,
crandc,
creqv,
crnand,
crnor,
cror,
crorc,
crxor,
// Data cache // Data cache
dcbf, dcbi, dcbst, dcbt, dcbtst, dcbz, dcbz128, dcbf,
dcbi,
dcbst,
dcbt,
dcbtst,
dcbz,
dcbz128,
// Division // Division
divdux, divdx, divwux, divwx, divdux,
divdx,
divwux,
divwx,
// Sync/barrier // Sync/barrier
eieio, eieio,
// Logical // Logical
eqvx, extsbx, extshx, extswx, eqvx,
extsbx,
extshx,
extswx,
// FPU // FPU
fabsx, faddsx, faddx, fcfidx, fcmpo, fcmpu, fctidx, fctidzx, fctiwx, fctiwzx, fabsx,
fdivsx, fdivx, fmaddsx, fmaddx, fmrx, fmsubsx, fmsubx, fmulsx, fmulx, faddsx,
fnabsx, fnegx, fnmaddsx, fnmaddx, fnmsubsx, fnmsubx, fresx, frspx, frsqrtex, faddx,
fselx, fsqrtsx, fsqrtx, fsubsx, fsubx, 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 // Instruction cache
icbi, isync, icbi,
isync,
// Load byte // Load byte
lbz, lbzu, lbzux, lbzx, lbz,
lbzu,
lbzux,
lbzx,
// Load doubleword // Load doubleword
ld, ldarx, ldbrx, ldu, ldux, ldx, ld,
ldarx,
ldbrx,
ldu,
ldux,
ldx,
// Load float // Load float
lfd, lfdu, lfdux, lfdx, lfs, lfsu, lfsux, lfsx, lfd,
lfdu,
lfdux,
lfdx,
lfs,
lfsu,
lfsux,
lfsx,
// Load halfword // Load halfword
lha, lhau, lhaux, lhax, lhbrx, lhz, lhzu, lhzux, lhzx, lha,
lhau,
lhaux,
lhax,
lhbrx,
lhz,
lhzu,
lhzux,
lhzx,
// Load multiple/string // Load multiple/string
lmw, lswi, lswx, lmw,
lswi,
lswx,
// Load vector // Load vector
lvebx, lvehx, lvewx, lvewx128, lvlx, lvlx128, lvlxl, lvlxl128, lvebx,
lvrx, lvrx128, lvrxl, lvrxl128, lvehx,
lvsl, lvsl128, lvsr, lvsr128, lvewx,
lvx, lvx128, lvxl, lvxl128, lvewx128,
lvlx,
lvlx128,
lvlxl,
lvlxl128,
lvrx,
lvrx128,
lvrxl,
lvrxl128,
lvsl,
lvsl128,
lvsr,
lvsr128,
lvx,
lvx128,
lvxl,
lvxl128,
// Load word // Load word
lwa, lwarx, lwaux, lwax, lwbrx, lwz, lwzu, lwzux, lwzx, lwa,
lwarx,
lwaux,
lwax,
lwbrx,
lwz,
lwzu,
lwzux,
lwzx,
// Move CR // Move CR
mcrf, mcrfs, mcrxr, mcrf,
mcrfs,
mcrxr,
// Move from special // Move from special
mfcr, mffsx, mfmsr, mfspr, mftb, mfvscr, mfcr,
mffsx,
mfmsr,
mfspr,
mftb,
mfvscr,
// Move to special // Move to special
mtcrf, mtfsb0x, mtfsb1x, mtfsfix, mtfsfx, mtmsr, mtmsrd, mtspr, mtvscr, mtcrf,
mtfsb0x,
mtfsb1x,
mtfsfix,
mtfsfx,
mtmsr,
mtmsrd,
mtspr,
mtvscr,
// Multiply // Multiply
mulhdux, mulhdx, mulhwux, mulhwx, mulldx, mulli, mullwx, mulhdux,
mulhdx,
mulhwux,
mulhwx,
mulldx,
mulli,
mullwx,
// Logical // Logical
nandx, negx, norx, orcx, ori, oris, orx, nandx,
negx,
norx,
orcx,
ori,
oris,
orx,
// Rotate // Rotate
rldclx, rldcrx, rldiclx, rldicrx, rldicx, rldimix, rlwimix, rlwinmx, rlwnmx, rldclx,
rldcrx,
rldiclx,
rldicrx,
rldicx,
rldimix,
rlwimix,
rlwinmx,
rlwnmx,
// System call // System call
sc, sc,
// Shift // Shift
sldx, slwx, sradix, sradx, srawix, srawx, srdx, srwx, sldx,
slwx,
sradix,
sradx,
srawix,
srawx,
srdx,
srwx,
// Store byte // Store byte
stb, stbu, stbux, stbx, stb,
stbu,
stbux,
stbx,
// Store doubleword // Store doubleword
std, stdbrx, stdcx, stdu, stdux, stdx, std,
stdbrx,
stdcx,
stdu,
stdux,
stdx,
// Store float // Store float
stfd, stfdu, stfdux, stfdx, stfiwx, stfs, stfsu, stfsux, stfsx, stfd,
stfdu,
stfdux,
stfdx,
stfiwx,
stfs,
stfsu,
stfsux,
stfsx,
// Store halfword // Store halfword
sth, sthbrx, sthu, sthux, sthx, sth,
sthbrx,
sthu,
sthux,
sthx,
// Store multiple/string // Store multiple/string
stmw, stswi, stswx, stmw,
stswi,
stswx,
// Store vector // Store vector
stvebx, stvehx, stvewx, stvewx128, stvlx, stvlx128, stvlxl, stvlxl128, stvebx,
stvrx, stvrx128, stvrxl, stvrxl128, stvehx,
stvx, stvx128, stvxl, stvxl128, stvewx,
stvewx128,
stvlx,
stvlx128,
stvlxl,
stvlxl128,
stvrx,
stvrx128,
stvrxl,
stvrxl128,
stvx,
stvx128,
stvxl,
stvxl128,
// Store word // Store word
stw, stwbrx, stwcx, stwu, stwux, stwx, stw,
stwbrx,
stwcx,
stwu,
stwux,
stwx,
// Subtract // Subtract
subfcx, subfex, subficx, subfmex, subfx, subfzex, subfcx,
subfex,
subficx,
subfmex,
subfx,
subfzex,
// Sync // Sync
sync, sync,
// Trap // Trap
td, tdi, tw, twi, td,
tdi,
tw,
twi,
// VMX integer // VMX integer
vaddcuw, vaddfp, vaddfp128, vaddsbs, vaddshs, vaddsws, vaddcuw,
vaddubm, vaddubs, vadduhm, vadduhs, vadduwm, vadduws, vaddfp,
vand, vand128, vandc, vandc128, vaddfp128,
vavgsb, vavgsh, vavgsw, vavgub, vavguh, vavguw, vaddsbs,
vcfpsxws128, vcfpuxws128, vcfsx, vcfux, vaddshs,
vcmpbfp, vcmpbfp128, vcmpeqfp, vcmpeqfp128, vaddsws,
vcmpequb, vcmpequh, vcmpequw, vcmpequw128, vaddubm,
vcmpgefp, vcmpgefp128, vcmpgtfp, vcmpgtfp128, vaddubs,
vcmpgtsb, vcmpgtsh, vcmpgtsw, vcmpgtub, vcmpgtuh, vcmpgtuw, vadduhm,
vcsxwfp128, vctsxs, vctuxs, vcuxwfp128, vadduhs,
vexptefp, vexptefp128, vlogefp, vlogefp128, vadduwm,
vmaddcfp128, vmaddfp, vmaddfp128, vadduws,
vmaxfp, vmaxfp128, vmaxsb, vmaxsh, vmaxsw, vmaxub, vmaxuh, vmaxuw, vand,
vmhaddshs, vmhraddshs, vand128,
vminfp, vminfp128, vminsb, vminsh, vminsw, vminub, vminuh, vminuw, 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, vmladduhm,
vmrghb, vmrghh, vmrghw, vmrghw128, vmrglb, vmrglh, vmrglw, vmrglw128, vmrghb,
vmsum3fp128, vmsum4fp128, vmrghh,
vmsummbm, vmsumshm, vmsumshs, vmsumubm, vmsumuhm, vmsumuhs, vmrghw,
vmulesb, vmulesh, vmuleub, vmuleuh, vmulfp128, vmrghw128,
vmulosb, vmulosh, vmuloub, vmulouh, vmrglb,
vnmsubfp, vnmsubfp128, vnor, vnor128, vmrglh,
vor, vor128, vmrglw,
vperm, vperm128, vpermwi128, vpkd3d128, vmrglw128,
vpkpx, vpkshss, vpkshss128, vpkshus, vpkshus128, vmsum3fp128,
vpkswss, vpkswss128, vpkswus, vpkswus128, vmsum4fp128,
vpkuhum, vpkuhum128, vpkuhus, vpkuhus128, vmsummbm,
vpkuwum, vpkuwum128, vpkuwus, vpkuwus128, vmsumshm,
vrefp, vrefp128, vmsumshs,
vrfim, vrfim128, vrfin, vrfin128, vrfip, vrfip128, vrfiz, vrfiz128, vmsumubm,
vrlb, vrlh, vrlimi128, vrlw, vrlw128, vmsumuhm,
vrsqrtefp, vrsqrtefp128, vmsumuhs,
vsel, vsel128, vmulesb,
vsl, vslb, vsldoi, vsldoi128, vslh, vslo, vslo128, vslw, vslw128, vmulesh,
vspltb, vsplth, vspltisb, vspltish, vspltisw, vspltisw128, vspltw, vspltw128, vmuleub,
vsr, vsrab, vsrah, vsraw, vsraw128, vsrb, vsrh, vsro, vsro128, vsrw, vsrw128, vmuleuh,
vsubcuw, vsubfp, vsubfp128, vsubsbs, vsubshs, vsubsws, vmulfp128,
vsububm, vsububs, vsubuhm, vsubuhs, vsubuwm, vsubuws, vmulosb,
vsum2sws, vsum4sbs, vsum4shs, vsum4ubs, vsumsws, vmulosh,
vupkd3d128, vupkhpx, vupkhsb, vupkhsb128, vupkhsh, vmuloub,
vupklpx, vupklsb, vupklsb128, vupklsh, vmulouh,
vxor, vxor128, 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 // XOR immediate
xori, xoris, xorx, xori,
xoris,
xorx,
// Invalid // Invalid
Invalid, Invalid,
} }
@@ -165,42 +532,102 @@ impl PpcOpcode {
pub fn terminates_block(&self) -> bool { pub fn terminates_block(&self) -> bool {
matches!( matches!(
self, self,
Self::bx | Self::bcx | Self::bclrx | Self::bcctrx Self::bx
| Self::bcx
| Self::bclrx
| Self::bcctrx
| Self::sc | Self::sc
| Self::td | Self::tdi | Self::tw | Self::twi | Self::td
| Self::tdi
| Self::tw
| Self::twi
| Self::Invalid | Self::Invalid
) )
} }
/// Returns true if this is a load instruction. /// Returns true if this is a load instruction.
pub fn is_load(&self) -> bool { pub fn is_load(&self) -> bool {
matches!(self, matches!(
Self::lbz | Self::lbzu | Self::lbzux | Self::lbzx | self,
Self::lhz | Self::lhzu | Self::lhzux | Self::lhzx | Self::lbz
Self::lha | Self::lhau | Self::lhaux | Self::lhax | | Self::lbzu
Self::lwz | Self::lwzu | Self::lwzux | Self::lwzx | | Self::lbzux
Self::lwa | Self::lwax | Self::lwaux | | Self::lbzx
Self::ld | Self::ldu | Self::ldux | Self::ldx | | Self::lhz
Self::lfs | Self::lfsu | Self::lfsux | Self::lfsx | | Self::lhzu
Self::lfd | Self::lfdu | Self::lfdux | Self::lfdx | | Self::lhzux
Self::lhbrx | Self::lwbrx | Self::ldbrx | | Self::lhzx
Self::lmw | Self::lswi | Self::lswx | | Self::lha
Self::lwarx | Self::ldarx | 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. /// Returns true if this is a store instruction.
pub fn is_store(&self) -> bool { pub fn is_store(&self) -> bool {
matches!(self, matches!(
Self::stb | Self::stbu | Self::stbux | Self::stbx | self,
Self::sth | Self::sthu | Self::sthux | Self::sthx | Self::stb
Self::stw | Self::stwu | Self::stwux | Self::stwx | | Self::stbu
Self::std | Self::stdu | Self::stdux | Self::stdx | | Self::stbux
Self::stfs | Self::stfsu | Self::stfsux | Self::stfsx | | Self::stbx
Self::stfd | Self::stfdu | Self::stfdux | Self::stfdx | | Self::sth
Self::sthbrx | Self::stwbrx | Self::stdbrx | | Self::sthu
Self::stmw | Self::stswi | Self::stswx | | Self::sthux
Self::stwcx | Self::stdcx | Self::stfiwx | 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 { pub fn is_sync_sensitive(&self) -> bool {
matches!( matches!(
self, self,
Self::lwarx | Self::ldarx | Self::stwcx | Self::stdcx Self::lwarx
| Self::sync | Self::eieio | Self::isync | 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. //! From `xenia-rs`'s `xenia-xex` + `xenia-vfs` when that emulator was retired.
//! `docs/agents/CONSOLIDATION.md` Phase 3. //! `docs/agents/CONSOLIDATION.md` Phase 3.
pub mod vfs;
pub mod header; pub mod header;
pub mod loader; pub mod loader;
pub mod lzx; pub mod lzx;
pub mod pe;
pub mod pdata; pub mod pdata;
pub mod pe;
pub mod resources; pub mod resources;
pub mod tls; pub mod tls;
pub mod vfs;
pub use header::Xex2Header; pub use header::Xex2Header;

View File

@@ -1,6 +1,6 @@
use crate::header::*; use crate::header::*;
use aes::cipher::{BlockDecrypt, KeyInit};
use aes::Aes128; use aes::Aes128;
use aes::cipher::{BlockDecrypt, KeyInit};
use byteorder::{BigEndian, ReadBytesExt}; use byteorder::{BigEndian, ReadBytesExt};
use std::io::{self, Cursor, Read, Seek, SeekFrom}; 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 { if magic != XEX2_MAGIC {
return Err(io::Error::new( return Err(io::Error::new(
io::ErrorKind::InvalidData, 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) // 0x180: page_descriptor_count (u32)
// 0x184: page_descriptors[] (each is 0x18 bytes: u32 value + 0x14 digest) // 0x184: page_descriptors[] (each is 0x18 bytes: u32 value + 0x14 digest)
let _header_size = cursor.read_u32::<BigEndian>()?; // 0x000 let _header_size = cursor.read_u32::<BigEndian>()?; // 0x000
let image_size = cursor.read_u32::<BigEndian>()?; // 0x004 let image_size = cursor.read_u32::<BigEndian>()?; // 0x004
// Skip RSA signature (0x100 bytes) // Skip RSA signature (0x100 bytes)
let mut rsa_sig = [0u8; 0x100]; 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 _unk_108 = cursor.read_u32::<BigEndian>()?; // 0x108
let image_flags = cursor.read_u32::<BigEndian>()?; // 0x10C let image_flags = cursor.read_u32::<BigEndian>()?; // 0x10C
let load_address = cursor.read_u32::<BigEndian>()?; // 0x110 let load_address = cursor.read_u32::<BigEndian>()?; // 0x110
// Skip section_digest (0x14 bytes) // Skip section_digest (0x14 bytes)
let mut digest = [0u8; 0x14]; 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 let _import_table_count = cursor.read_u32::<BigEndian>()?; // 0x128
// Skip import_table_digest (0x14 bytes) // Skip import_table_digest (0x14 bytes)
cursor.read_exact(&mut digest)?; // 0x12C cursor.read_exact(&mut digest)?; // 0x12C
// Skip xgd2_media_id (0x10 bytes) // Skip xgd2_media_id (0x10 bytes)
let mut media_id = [0u8; 0x10]; 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) // Read aes_key (0x10 bytes)
let mut aes_key = [0u8; 0x10]; 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 let export_table_address = cursor.read_u32::<BigEndian>()?; // 0x160
// Skip header_digest (0x14 bytes) // 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 _region = cursor.read_u32::<BigEndian>()?; // 0x178
let _allowed_media = cursor.read_u32::<BigEndian>()?; // 0x17C let _allowed_media = cursor.read_u32::<BigEndian>()?; // 0x17C
let page_descriptor_count = cursor.read_u32::<BigEndian>()?; // 0x180 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> { fn parse_file_format_info(data: &[u8], headers: &[Xex2OptionalHeader]) -> Option<FileFormatInfo> {
// The key format: low 8 bits indicate the data size category // 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 // 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; let offset = header.value as usize;
if offset + 8 > data.len() { if offset + 8 > data.len() {
return None; return None;
@@ -166,11 +171,18 @@ fn parse_file_format_info(data: &[u8], headers: &[Xex2OptionalHeader]) -> Option
COMPRESSION_BASIC => { COMPRESSION_BASIC => {
// Basic compression blocks: (data_size, zero_size) pairs // Basic compression blocks: (data_size, zero_size) pairs
// Number of blocks = (info_size - 8) / 8 // 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 { for _ in 0..block_count {
let data_size = cursor.read_u32::<BigEndian>().ok()?; let data_size = cursor.read_u32::<BigEndian>().ok()?;
let zero_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 => { 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 /// At this stage, only record addresses are read; ordinals and record types
/// are resolved later by `resolve_imports` once the PE image is decompressed. /// are resolved later by `resolve_imports` once the PE image is decompressed.
fn parse_import_libraries(data: &[u8], headers: &[Xex2OptionalHeader]) -> Vec<ImportLibrary> { 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, Some(h) => h,
None => return Vec::new(), 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 { 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 { 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; 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 { for _ in 0..string_count {
let start = string_data_start + spos; let start = string_data_start + spos;
let mut end = start; let mut end = start;
while end < data.len() && data[end] != 0 { end += 1; } while end < data.len() && data[end] != 0 {
let name = std::str::from_utf8(&data[start..end]).unwrap_or("???").to_string(); end += 1;
}
let name = std::str::from_utf8(&data[start..end])
.unwrap_or("???")
.to_string();
spos += name.len() + 1; spos += name.len() + 1;
// 4-byte alignment // 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); 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 { while lib_off + 0x28 <= data.len() && lib_off < offset + total_size {
let lib_size = be_u32(data, lib_off) as usize; 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 id = be_u32(data, lib_off + 0x18);
let version_cur = be_u32(data, lib_off + 0x1C); 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 name_index = (be_u16(data, lib_off + 0x24) & 0xFF) as usize;
let count = be_u16(data, lib_off + 0x26) 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(); let mut imports = Vec::new();
for i in 0..count { 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() { if pe_off + 4 <= pe_image.len() {
// PE image values are big-endian (Xbox 360 native) // PE image values are big-endian (Xbox 360 native)
let val = u32::from_be_bytes([ let val = u32::from_be_bytes([
pe_image[pe_off], pe_image[pe_off+1], pe_image[pe_off],
pe_image[pe_off+2], pe_image[pe_off+3], pe_image[pe_off + 1],
pe_image[pe_off + 2],
pe_image[pe_off + 3],
]); ]);
imp.record_type = ((val >> 24) & 0xFF) as u8; imp.record_type = ((val >> 24) & 0xFF) as u8;
imp.ordinal = (val & 0xFFFF) as u16; 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). // is an inline struct of 6 u32 words (24 bytes total).
// Layout: media_id(4), version(4), base_version(4), title_id(4), // Layout: media_id(4), version(4), base_version(4), title_id(4),
// platform(1), exec_type(1), disc_number(1), disc_count(1) // 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; let off = header.value as usize;
if off + 20 > data.len() { if off + 20 > data.len() {
return None; return None;
} }
let media_id = u32::from_be_bytes([data[off], data[off+1], data[off+2], data[off+3]]); 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 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_number = data[off + 18];
let disc_count = data[off + 19]; 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. /// Parse original PE name from optional header data.
fn parse_original_pe_name(data: &[u8], headers: &[Xex2OptionalHeader]) -> Option<String> { 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; let off = header.value as usize;
if off + 4 > data.len() { if off + 4 > data.len() {
return None; 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 { if off + size > data.len() || size <= 4 {
return None; return None;
} }
let name_bytes = &data[off + 4..off + size]; 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. /// Get an optional header value by key.
pub fn get_opt_header(header: &Xex2Header, key: u32) -> Option<u32> { pub fn get_opt_header(header: &Xex2Header, key: u32) -> Option<u32> {
header.optional_headers.iter() header
.optional_headers
.iter()
.find(|h| h.key == key) .find(|h| h.key == key)
.map(|h| h.value) .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::histogram!("xex.load_image_ms").record(elapsed_ms);
metrics::counter!("xex.bytes_in").increment(bytes_in as u64); metrics::counter!("xex.bytes_in").increment(bytes_in as u64);
metrics::counter!("xex.bytes_out").increment(output.len() 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 }; let ratio = if bytes_in == 0 {
tracing::info!(bytes_in, bytes_out = output.len(), ratio, elapsed_ms, "image loaded"); 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) Ok(output)
} }
/// Load basic compressed image data. /// Load basic compressed image data.
fn load_basic_compressed(source: &[u8], info: &FileFormatInfo) -> io::Result<Vec<u8>> { fn load_basic_compressed(source: &[u8], info: &FileFormatInfo) -> io::Result<Vec<u8>> {
// Calculate total uncompressed size // 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) .map(|b| b.data_size as u64 + b.zero_size as u64)
.sum(); .sum();
@@ -412,8 +468,12 @@ fn load_basic_compressed(source: &[u8], info: &FileFormatInfo) -> io::Result<Vec
if src_offset + data_size > source.len() { if src_offset + data_size > source.len() {
return Err(io::Error::new( return Err(io::Error::new(
io::ErrorKind::UnexpectedEof, io::ErrorKind::UnexpectedEof,
format!("Basic compression block data extends past end of file (src_offset={:#x}, data_size={:#x}, source_len={:#x})", format!(
src_offset, data_size, source.len()), "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. /// Xbox 360 retail AES key for XEX2 session key decryption.
const XEX2_RETAIL_KEY: [u8; 16] = [ const XEX2_RETAIL_KEY: [u8; 16] = [
0x20, 0xB1, 0x85, 0xA5, 0x9D, 0x28, 0xFD, 0xC3, 0x20, 0xB1, 0x85, 0xA5, 0x9D, 0x28, 0xFD, 0xC3, 0x40, 0x58, 0x3F, 0xBB, 0x08, 0x96, 0xBF, 0x91,
0x40, 0x58, 0x3F, 0xBB, 0x08, 0x96, 0xBF, 0x91,
]; ];
/// Xbox 360 devkit AES key (all zeros). /// 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). /// 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: /// 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 /// 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>> { fn deblock(input: &[u8], first_block_size: u32) -> io::Result<Vec<u8>> {
let mut output = Vec::new(); 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 // Read next block's info from start of current block data
let next_block_size = if pos + 4 <= input.len() { let next_block_size = if pos + 4 <= input.len() {
u32::from_be_bytes([ u32::from_be_bytes([input[pos], input[pos + 1], input[pos + 2], input[pos + 3]])
input[pos], input[pos + 1], input[pos + 2], input[pos + 3], as usize
]) as usize
} else { } else {
0 0
}; };
@@ -522,8 +582,12 @@ fn deblock(input: &[u8], first_block_size: u32) -> io::Result<Vec<u8>> {
if p + chunk_size > input.len() { if p + chunk_size > input.len() {
return Err(io::Error::new( return Err(io::Error::new(
io::ErrorKind::UnexpectedEof, io::ErrorKind::UnexpectedEof,
format!("De-block chunk extends past input (pos={:#x}, chunk_size={:#x}, input_len={:#x})", format!(
p, chunk_size, input.len()), "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]); 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. /// Load normal (LZX) compressed image data.
/// Pipeline: decrypt → de-block → LZX decompress (pure Rust) /// Pipeline: decrypt → de-block → LZX decompress (pure Rust)
#[tracing::instrument(skip_all, fields(bytes_in = source.len()))] #[tracing::instrument(skip_all, fields(bytes_in = source.len()))]
fn load_normal_compressed(source: &[u8], info: &FileFormatInfo, header: &Xex2Header) -> io::Result<Vec<u8>> { fn load_normal_compressed(
let uncompressed_size = header.security_info.as_ref() 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) .map(|s| s.image_size as usize)
.unwrap_or(0); .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 // Step 3: LZX decompress using pure Rust decoder
let window_bits = match info.normal_window_size { let window_bits = match info.normal_window_size {
s if s == 0 => 15, // default 0 => 15, // default
s => (s as f64).log2() as u32, s => (s as f64).log2() as u32,
}; };
let mut decoder = crate::lzx::LzxDecoder::new(window_bits); let mut decoder = crate::lzx::LzxDecoder::new(window_bits);
let output = decoder.decompress(&deblocked, uncompressed_size) let output = decoder
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, format!("LZX decompression failed: {e}")))?; .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) 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". //! LZX decompressor for Xbox 360 XEX2 "normal compression".
//! Ported from libmspack lzxd.c (C) 2003-2013 Stuart Caie, LGPL 2.1. //! 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 POSITION_SLOTS: [u32; 11] = [30, 32, 34, 36, 38, 42, 50, 66, 98, 162, 290];
static EXTRA_BITS: [u8; 36] = [ static EXTRA_BITS: [u8; 36] = [
0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 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,
7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13, 14, 14, 13, 14, 14, 15, 15, 16, 16,
15, 15, 16, 16,
]; ];
#[rustfmt::skip] #[rustfmt::skip]
@@ -109,17 +115,30 @@ struct BitReader<'a> {
impl<'a> BitReader<'a> { impl<'a> BitReader<'a> {
fn new(data: &'a [u8]) -> Self { 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. /// Inject one 16-bit little-endian pair into MSB bit buffer.
fn fill(&mut self) { fn fill(&mut self) {
let b0 = if self.pos < self.data.len() { let b0 = if self.pos < self.data.len() {
let b = self.data[self.pos]; self.pos += 1; b as u32 let b = self.data[self.pos];
} else { 0 }; self.pos += 1;
b as u32
} else {
0
};
let b1 = if self.pos < self.data.len() { let b1 = if self.pos < self.data.len() {
let b = self.data[self.pos]; self.pos += 1; b as u32 let b = self.data[self.pos];
} else { 0 }; self.pos += 1;
b as u32
} else {
0
};
let word = (b1 << 8) | b0; let word = (b1 << 8) | b0;
self.buf |= word << (16 - self.left as u32); self.buf |= word << (16 - self.left as u32);
self.left += 16; self.left += 16;
@@ -127,7 +146,9 @@ impl<'a> BitReader<'a> {
#[inline] #[inline]
fn ensure(&mut self, n: i32) { fn ensure(&mut self, n: i32) {
while self.left < n { self.fill(); } while self.left < n {
self.fill();
}
} }
#[inline] #[inline]
@@ -152,26 +173,29 @@ impl<'a> BitReader<'a> {
/// Read a raw byte directly (for UNCOMPRESSED blocks). /// Read a raw byte directly (for UNCOMPRESSED blocks).
fn raw_byte(&mut self) -> u8 { fn raw_byte(&mut self) -> u8 {
if self.pos < self.data.len() { if self.pos < self.data.len() {
let b = self.data[self.pos]; self.pos += 1; b let b = self.data[self.pos];
} else { 0 } self.pos += 1;
b
} else {
0
}
} }
/// Re-align the bitstream at a frame boundary. /// Re-align the bitstream at a frame boundary.
fn align_frame(&mut self) { fn align_frame(&mut self) {
if self.left > 0 { self.ensure(16); } if self.left > 0 {
self.ensure(16);
}
let r = self.left & 15; 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) ─────────────────────────────────────── // ── Huffman table builder (MSB order) ───────────────────────────────────────
fn make_decode_table( fn make_decode_table(nsyms: usize, nbits: usize, length: &[u8], table: &mut [u16]) -> bool {
nsyms: usize,
nbits: usize,
length: &[u8],
table: &mut [u16],
) -> bool {
let mut pos: usize = 0; let mut pos: usize = 0;
let table_mask = 1usize << nbits; let table_mask = 1usize << nbits;
let mut bit_mask = table_mask >> 1; let mut bit_mask = table_mask >> 1;
@@ -179,10 +203,14 @@ fn make_decode_table(
// Short codes: direct mapping // Short codes: direct mapping
for bit_num in 1..=nbits { for bit_num in 1..=nbits {
for sym in 0..nsyms { for sym in 0..nsyms {
if length[sym] as usize != bit_num { continue; } if length[sym] as usize != bit_num {
continue;
}
let leaf = pos; let leaf = pos;
pos += bit_mask; pos += bit_mask;
if pos > table_mask { return true; } if pos > table_mask {
return true;
}
for i in leaf..leaf + bit_mask { for i in leaf..leaf + bit_mask {
table[i] = sym as u16; table[i] = sym as u16;
} }
@@ -190,14 +218,20 @@ fn make_decode_table(
bit_mask >>= 1; bit_mask >>= 1;
} }
if pos == table_mask { return false; } if pos == table_mask {
return false;
}
// Mark remaining entries as unused // Mark remaining entries as unused
for i in pos..table_mask { for i in pos..table_mask {
table[i] = 0xFFFF; 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 mut pos32 = (pos as u32) << 16;
let table_mask32 = (table_mask as u32) << 16; let table_mask32 = (table_mask as u32) << 16;
@@ -206,8 +240,12 @@ fn make_decode_table(
// Long codes: tree traversal // Long codes: tree traversal
for bit_num in (nbits + 1)..=HUFF_MAXBITS { for bit_num in (nbits + 1)..=HUFF_MAXBITS {
for sym in 0..nsyms { for sym in 0..nsyms {
if length[sym] as usize != bit_num { continue; } if length[sym] as usize != bit_num {
if pos32 >= table_mask32 { return true; } continue;
}
if pos32 >= table_mask32 {
return true;
}
let mut leaf = (pos32 >> 16) as usize; let mut leaf = (pos32 >> 16) as usize;
@@ -247,9 +285,13 @@ fn read_huffsym(
let mut i: u32 = 1 << (BITBUF_WIDTH - tablebits as u32); let mut i: u32 = 1 << (BITBUF_WIDTH - tablebits as u32);
loop { loop {
i >>= 1; 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; 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); br.remove(lens[sym] as u32);
@@ -307,7 +349,9 @@ impl LzxDecoder {
frame_posn: 0, frame_posn: 0,
frame: 0, frame: 0,
num_offsets, num_offsets,
r0: 1, r1: 1, r2: 1, r0: 1,
r1: 1,
r2: 1,
block_type: 0, block_type: 0,
block_length: 0, block_length: 0,
block_remaining: 0, block_remaining: 0,
@@ -315,20 +359,23 @@ impl LzxDecoder {
intel_filesize: 0, intel_filesize: 0,
intel_curpos: 0, intel_curpos: 0,
intel_started: false, intel_started: false,
pretree_len: vec![0u8; PRETREE_MAXSYMS + LENTABLE_SAFETY], pretree_len: vec![0u8; PRETREE_MAXSYMS + LENTABLE_SAFETY],
maintree_len: vec![0u8; MAINTREE_MAXSYMS + LENTABLE_SAFETY], maintree_len: vec![0u8; MAINTREE_MAXSYMS + LENTABLE_SAFETY],
length_len: vec![0u8; LENGTH_MAXSYMS + LENTABLE_SAFETY], length_len: vec![0u8; LENGTH_MAXSYMS + LENTABLE_SAFETY],
aligned_len: vec![0u8; ALIGNED_MAXSYMS + LENTABLE_SAFETY], aligned_len: vec![0u8; ALIGNED_MAXSYMS + LENTABLE_SAFETY],
pretree_table: vec![0u16; (1 << PRETREE_TABLEBITS) + PRETREE_MAXSYMS * 2], pretree_table: vec![0u16; (1 << PRETREE_TABLEBITS) + PRETREE_MAXSYMS * 2],
maintree_table: vec![0u16; (1 << MAINTREE_TABLEBITS) + MAINTREE_MAXSYMS * 2], maintree_table: vec![0u16; (1 << MAINTREE_TABLEBITS) + MAINTREE_MAXSYMS * 2],
length_table: vec![0u16; (1 << LENGTH_TABLEBITS) + LENGTH_MAXSYMS * 2], length_table: vec![0u16; (1 << LENGTH_TABLEBITS) + LENGTH_MAXSYMS * 2],
aligned_table: vec![0u16; (1 << ALIGNED_TABLEBITS) + ALIGNED_MAXSYMS * 2], aligned_table: vec![0u16; (1 << ALIGNED_TABLEBITS) + ALIGNED_MAXSYMS * 2],
length_empty: false, length_empty: false,
} }
} }
fn build_table( fn build_table(
lens: &[u8], table: &mut [u16], maxsyms: usize, tablebits: usize, lens: &[u8],
table: &mut [u16],
maxsyms: usize,
tablebits: usize,
) -> Result<(), LzxError> { ) -> Result<(), LzxError> {
if make_decode_table(maxsyms, tablebits, lens, table) { if make_decode_table(maxsyms, tablebits, lens, table) {
Err(LzxError::BadHuffmanTable) Err(LzxError::BadHuffmanTable)
@@ -338,7 +385,10 @@ impl LzxDecoder {
} }
fn build_table_maybe_empty( 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> { ) -> Result<bool, LzxError> {
if make_decode_table(maxsyms, tablebits, lens, table) { if make_decode_table(maxsyms, tablebits, lens, table) {
// Check if table is simply empty (all lengths zero) // Check if table is simply empty (all lengths zero)
@@ -366,30 +416,63 @@ impl LzxDecoder {
for i in 0..20 { for i in 0..20 {
pretree_len[i] = br.read(4) as u8; 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; let mut x = first;
while x < last { 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 { if z == 17 {
// Run of zeros: [read 4 bits] + 4 // Run of zeros: [read 4 bits] + 4
let mut y = br.read(4) as usize + 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 { } else if z == 18 {
// Run of zeros: [read 5 bits] + 20 // Run of zeros: [read 5 bits] + 20
let mut y = br.read(5) as usize + 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 { } else if z == 19 {
// Run of same: [read 1 bit] + 4, then read symbol // Run of same: [read 1 bit] + 4, then read symbol
let mut y = br.read(1) as usize + 4; 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; let mut val = lens[x] as i32 - z2 as i32;
if val < 0 { val += 17; } if val < 0 {
while y > 0 && x < last { lens[x] = val as u8; x += 1; y -= 1; } val += 17;
}
while y > 0 && x < last {
lens[x] = val as u8;
x += 1;
y -= 1;
}
} else { } else {
// Delta: code 0..16 // Delta: code 0..16
let mut val = lens[x] as i32 - z as i32; 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; lens[x] = val as u8;
x += 1; x += 1;
} }
@@ -425,13 +508,15 @@ impl LzxDecoder {
LZX_FRAME_SIZE 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 { while bytes_todo > 0 {
// New block? // New block?
if self.block_remaining == 0 { if self.block_remaining == 0 {
// Realign after odd UNCOMPRESSED block // 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(); br.raw_byte();
} }
// Read block type (3 bits) and length (24 bits) // Read block type (3 bits) and length (24 bits)
@@ -443,33 +528,110 @@ impl LzxDecoder {
match self.block_type { match self.block_type {
LZX_BLOCKTYPE_ALIGNED => { LZX_BLOCKTYPE_ALIGNED => {
for i in 0..8 { self.aligned_len[i] = br.read(3) as u8; } for i in 0..8 {
Self::build_table(&self.aligned_len, &mut self.aligned_table, ALIGNED_MAXSYMS, ALIGNED_TABLEBITS)?; 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 // 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(
Self::read_lens(&mut br, &mut self.maintree_len, &mut self.pretree_len, &mut self.pretree_table, 256, LZX_NUM_CHARS + self.num_offsets)?; &mut br,
Self::build_table(&self.maintree_len, &mut self.maintree_table, MAINTREE_MAXSYMS, MAINTREE_TABLEBITS)?; &mut self.maintree_len,
if self.maintree_len[0xE8] != 0 { self.intel_started = true; } &mut self.pretree_len,
Self::read_lens(&mut br, &mut self.length_len, &mut self.pretree_len, &mut self.pretree_table, 0, LZX_NUM_SECONDARY_LENGTHS)?; &mut self.pretree_table,
self.length_empty = Self::build_table_maybe_empty(&self.length_len, &mut self.length_table, LENGTH_MAXSYMS, LENGTH_TABLEBITS)?; 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 => { 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(
Self::read_lens(&mut br, &mut self.maintree_len, &mut self.pretree_len, &mut self.pretree_table, 256, LZX_NUM_CHARS + self.num_offsets)?; &mut br,
Self::build_table(&self.maintree_len, &mut self.maintree_table, MAINTREE_MAXSYMS, MAINTREE_TABLEBITS)?; &mut self.maintree_len,
if self.maintree_len[0xE8] != 0 { self.intel_started = true; } &mut self.pretree_len,
Self::read_lens(&mut br, &mut self.length_len, &mut self.pretree_len, &mut self.pretree_table, 0, LZX_NUM_SECONDARY_LENGTHS)?; &mut self.pretree_table,
self.length_empty = Self::build_table_maybe_empty(&self.length_len, &mut self.length_table, LENGTH_MAXSYMS, LENGTH_TABLEBITS)?; 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 => { LZX_BLOCKTYPE_UNCOMPRESSED => {
self.intel_started = true; self.intel_started = true;
// Align to byte boundary // Align to byte boundary
if br.left == 0 { br.ensure(16); } if br.left == 0 {
br.ensure(16);
}
br.left = 0; br.left = 0;
br.buf = 0; br.buf = 0;
// Read R0, R1, R2 (12 bytes, little-endian u32s) // Read R0, R1, R2 (12 bytes, little-endian u32s)
let mut buf = [0u8; 12]; 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.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.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]]); 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; 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; bytes_todo -= this_run;
self.block_remaining -= this_run as usize; self.block_remaining -= this_run as usize;
@@ -488,7 +652,13 @@ impl LzxDecoder {
match self.block_type { match self.block_type {
LZX_BLOCKTYPE_VERBATIM => { LZX_BLOCKTYPE_VERBATIM => {
while this_run > 0 { 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 { if main_element < LZX_NUM_CHARS {
self.window[self.window_posn] = main_element as u8; self.window[self.window_posn] = main_element as u8;
self.window_posn += 1; self.window_posn += 1;
@@ -497,8 +667,16 @@ impl LzxDecoder {
let me = main_element - LZX_NUM_CHARS; let me = main_element - LZX_NUM_CHARS;
let mut match_length = me & LZX_NUM_PRIMARY_LENGTHS; let mut match_length = me & LZX_NUM_PRIMARY_LENGTHS;
if match_length == LZX_NUM_PRIMARY_LENGTHS { if match_length == LZX_NUM_PRIMARY_LENGTHS {
if self.length_empty { return Err(LzxError::Decrunch("LENGTH tree empty".into())); } if self.length_empty {
let footer = read_huffsym(&mut br, &self.length_table, &self.length_len, LENGTH_TABLEBITS, LENGTH_MAXSYMS)?; 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 += footer;
} }
match_length += LZX_MIN_MATCH; match_length += LZX_MIN_MATCH;
@@ -506,14 +684,34 @@ impl LzxDecoder {
let mut match_offset = (me >> 3) as u32; let mut match_offset = (me >> 3) as u32;
match match_offset { match match_offset {
0 => match_offset = self.r0, 0 => match_offset = self.r0,
1 => { match_offset = self.r1; self.r1 = self.r0; self.r0 = match_offset; } 1 => {
2 => { match_offset = self.r2; self.r2 = self.r0; self.r0 = match_offset; } match_offset = self.r1;
3 => { match_offset = 1; self.r2 = self.r1; self.r1 = self.r0; self.r0 = match_offset; } 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); let verbatim_bits = br.read(extra);
match_offset = POSITION_BASE[match_offset as usize] - 2 + verbatim_bits; match_offset = POSITION_BASE[match_offset as usize] - 2
self.r2 = self.r1; self.r1 = self.r0; self.r0 = match_offset; + verbatim_bits;
self.r2 = self.r1;
self.r1 = self.r0;
self.r0 = match_offset;
} }
} }
@@ -527,7 +725,13 @@ impl LzxDecoder {
} }
LZX_BLOCKTYPE_ALIGNED => { LZX_BLOCKTYPE_ALIGNED => {
while this_run > 0 { 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 { if main_element < LZX_NUM_CHARS {
self.window[self.window_posn] = main_element as u8; self.window[self.window_posn] = main_element as u8;
self.window_posn += 1; self.window_posn += 1;
@@ -536,8 +740,16 @@ impl LzxDecoder {
let me = main_element - LZX_NUM_CHARS; let me = main_element - LZX_NUM_CHARS;
let mut match_length = me & LZX_NUM_PRIMARY_LENGTHS; let mut match_length = me & LZX_NUM_PRIMARY_LENGTHS;
if match_length == LZX_NUM_PRIMARY_LENGTHS { if match_length == LZX_NUM_PRIMARY_LENGTHS {
if self.length_empty { return Err(LzxError::Decrunch("LENGTH tree empty".into())); } if self.length_empty {
let footer = read_huffsym(&mut br, &self.length_table, &self.length_len, LENGTH_TABLEBITS, LENGTH_MAXSYMS)?; 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 += footer;
} }
match_length += LZX_MIN_MATCH; match_length += LZX_MIN_MATCH;
@@ -545,18 +757,42 @@ impl LzxDecoder {
let mut match_offset = (me >> 3) as u32; let mut match_offset = (me >> 3) as u32;
match match_offset { match match_offset {
0 => match_offset = self.r0, 0 => match_offset = self.r0,
1 => { match_offset = self.r1; self.r1 = self.r0; self.r0 = match_offset; } 1 => {
2 => { match_offset = self.r2; self.r2 = self.r0; self.r0 = match_offset; } 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; match_offset = POSITION_BASE[match_offset as usize] - 2;
if extra > 3 { if extra > 3 {
let verbatim_bits = br.read(extra - 3); let verbatim_bits = br.read(extra - 3);
match_offset += verbatim_bits << 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; match_offset += aligned as u32;
} else if extra == 3 { } 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; match_offset += aligned as u32;
} else if extra > 0 { } else if extra > 0 {
let verbatim_bits = br.read(extra); let verbatim_bits = br.read(extra);
@@ -564,7 +800,9 @@ impl LzxDecoder {
} else { } else {
match_offset = 1; 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 // Frame boundary check
if (self.window_posn.wrapping_sub(self.frame_posn)) != frame_size { if (self.window_posn.wrapping_sub(self.frame_posn)) != frame_size {
return Err(LzxError::Decrunch(format!( 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(); br.align_frame();
// Intel E8 postprocessing // Intel E8 postprocessing
if self.intel_started && self.intel_filesize != 0 if self.intel_started
&& self.frame <= 32768 && frame_size > 10 && self.intel_filesize != 0
&& self.frame <= 32768
&& frame_size > 10
{ {
let mut e8_buf = vec![0u8; frame_size]; let mut e8_buf = vec![0u8; frame_size];
e8_buf.copy_from_slice(&self.window[self.frame_posn..self.frame_posn + 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; let filesize = self.intel_filesize;
while i < limit { while i < limit {
if e8_buf[i] != 0xE8 { i += 1; curpos += 1; continue; } if e8_buf[i] != 0xE8 {
let abs_off = e8_buf[i+1] as i32 i += 1;
| (e8_buf[i+2] as i32) << 8 curpos += 1;
| (e8_buf[i+3] as i32) << 16 continue;
| (e8_buf[i+4] as i32) << 24; }
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 { if abs_off >= -curpos && abs_off < filesize {
let rel_off = if abs_off >= 0 { abs_off - curpos } else { abs_off + filesize }; let rel_off = if abs_off >= 0 {
e8_buf[i+1] = rel_off as u8; abs_off - curpos
e8_buf[i+2] = (rel_off >> 8) as u8; } else {
e8_buf[i+3] = (rel_off >> 16) as u8; abs_off + filesize
e8_buf[i+4] = (rel_off >> 24) as u8; };
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; i += 5;
curpos += 5; curpos += 5;
@@ -641,7 +891,9 @@ impl LzxDecoder {
output.extend_from_slice(&e8_buf[..to_write]); output.extend_from_slice(&e8_buf[..to_write]);
offset += to_write; offset += to_write;
} else { } 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); let to_write = frame_size.min(output_len - offset);
output.extend_from_slice(&self.window[self.frame_posn..self.frame_posn + to_write]); output.extend_from_slice(&self.window[self.frame_posn..self.frame_posn + to_write]);
offset += to_write; offset += to_write;
@@ -650,8 +902,12 @@ impl LzxDecoder {
// Advance frame // Advance frame
self.frame_posn += frame_size; self.frame_posn += frame_size;
self.frame += 1; self.frame += 1;
if self.window_posn == self.window_size { self.window_posn = 0; } if self.window_posn == self.window_size {
if self.frame_posn == self.window_size { self.frame_posn = 0; } self.window_posn = 0;
}
if self.frame_posn == self.window_size {
self.frame_posn = 0;
}
} }
Ok(output) 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. // Image high water = image_base + the largest virtual_address+virtual_size.
let high = sections let high = sections
.iter() .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() .max()
.unwrap_or(u32::MAX); .unwrap_or(u32::MAX);
out.retain(|e| e.begin_address >= image_base && e.begin_address < high); out.retain(|e| e.begin_address >= image_base && e.begin_address < high);
@@ -104,7 +108,12 @@ mod tests {
use super::*; use super::*;
use crate::pe::PeSection; 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. // Build a synthetic PE image with .text and .pdata.
// Layout: pdata at RVA 0x1000, .text at RVA 0x2000. // Layout: pdata at RVA 0x1000, .text at RVA 0x2000.
let pdata_rva = 0x1000u32; 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(); let mut sections = Vec::new();
for i in 0..num_sections { for i in 0..num_sections {
let s = section_table_off + i * 40; 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_bytes = &pe[s..s + 8];
let name = std::str::from_utf8(name_bytes) 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() { if off + 4 > data.len() {
return Vec::new(); 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. // The size field counts itself; anything smaller than one record is junk.
if size < 4 + 16 || off + size > data.len() { if size < 4 + 16 || off + size > data.len() {
return Vec::new(); return Vec::new();
@@ -67,7 +68,11 @@ pub fn parse_resources(data: &[u8], header: &Xex2Header) -> Vec<XexResource> {
.to_string(); .to_string();
let address = u32::from_be_bytes([data[p + 8], data[p + 9], data[p + 10], data[p + 11]]); 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]]); 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 out
} }
@@ -94,7 +99,10 @@ mod tests {
} }
fn with_resource(value: u32) -> Xex2Header { 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] #[test]

View File

@@ -60,7 +60,9 @@ pub fn parse_tls(pe: &[u8], image_base: u32, sections: &[PeSection]) -> Option<T
// +0x14 Characteristics (4) // +0x14 Characteristics (4)
let tls_section = sections.iter().find(|s| s.name == ".tls")?; let tls_section = sections.iter().find(|s| s.name == ".tls")?;
let off = tls_section.virtual_address as usize; 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 // 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). // 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_start = read_u32(off);
let raw_data_end = read_u32(off + 4); let raw_data_end = read_u32(off + 4);
let index_address = read_u32(off + 8); let index_address = read_u32(off + 8);
let callback_array = read_u32(off + 12); let callback_array = read_u32(off + 12);
let zero_fill_size = read_u32(off + 16); let zero_fill_size = read_u32(off + 16);
let characteristics = read_u32(off + 20); 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; let mut p = callback_array.wrapping_sub(image_base) as usize;
while p + 4 <= pe.len() { while p + 4 <= pe.len() {
let v = read_u32(p); let v = read_u32(p);
if v == 0 { break; } if v == 0 {
break;
}
callbacks.push(TlsCallback { address: v }); callbacks.push(TlsCallback { address: v });
p += 4; 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; let cb_va: u32 = 0x200;
// Directory fields: // Directory fields:
let raw_start = 0x800u32; let raw_start = 0x800u32;
let raw_end = 0x900u32; let raw_end = 0x900u32;
let idx = 0x1000u32; let idx = 0x1000u32;
let zero_fill = 0x40u32; let zero_fill = 0x40u32;
let chars = 0x0u32; let chars = 0x0u32;
let cb_array = image_base + cb_va; let cb_array = image_base + cb_va;
for (i, v) in [ for (i, v) in [
image_base + raw_start, image_base + raw_end, image_base + raw_start,
image_base + idx, cb_array, zero_fill, chars, image_base + raw_end,
].iter().enumerate() { image_base + idx,
cb_array,
zero_fill,
chars,
]
.iter()
.enumerate()
{
pe[tls_va as usize + i * 4..tls_va as usize + i * 4 + 4] pe[tls_va as usize + i * 4..tls_va as usize + i * 4 + 4]
.copy_from_slice(&v.to_be_bytes()); .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_l = u16::from_le_bytes([buffer[p], buffer[p + 1]]);
let node_r = u16::from_le_bytes([buffer[p + 2], buffer[p + 3]]); 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 sector =
let length = u32::from_le_bytes([buffer[p + 8], buffer[p + 9], buffer[p + 10], buffer[p + 11]]) as u64; 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 attributes = buffer[p + 12];
let name_length = buffer[p + 13] as usize; let name_length = buffer[p + 13] as usize;

View File

@@ -34,11 +34,16 @@ fn main() {
json_path.display() 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 out = Path::new(&env::var("OUT_DIR").unwrap()).join("ordinals.rs");
let mut f = fs::File::create(&out).unwrap(); 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!( writeln!(
f, f,
"pub fn resolve_ordinal(lib: &str, ordinal: u16) -> Option<&'static str> {{" "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 anyhow::Result;
use clap::{Parser, Subcommand, ValueEnum}; use clap::{Parser, Subcommand, ValueEnum};
use tracing::{debug, info, instrument, warn}; use tracing::{info, instrument, warn};
#[derive(Parser)] #[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 { struct Cli {
#[command(subcommand)] #[command(subcommand)]
command: Commands, command: Commands,
@@ -39,7 +42,6 @@ enum AnalyzeMode {
#[derive(Subcommand)] #[derive(Subcommand)]
enum Commands { enum Commands {
/// Display XEX header information /// Display XEX header information
Info { Info {
/// Path to XEX file /// 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") { if lower.ends_with(".iso") || lower.ends_with(".xiso") {
use sylpheed_xex::vfs::VfsDevice; use sylpheed_xex::vfs::VfsDevice;
info!("detected disc image, extracting default.xex"); info!("detected disc image, extracting default.xex");
let disc = sylpheed_xex::vfs::disc_image::DiscImageDevice::open("disc", std::path::Path::new(path)) let disc = sylpheed_xex::vfs::disc_image::DiscImageDevice::open(
.map_err(|e| anyhow::anyhow!("Failed to open disc image: {}", e))?; "disc",
std::path::Path::new(path),
)
.map_err(|e| anyhow::anyhow!("Failed to open disc image: {}", e))?;
disc.read_file("default.xex") disc.read_file("default.xex")
.map_err(|e| anyhow::anyhow!("Failed to extract default.xex from disc image: {}", e)) .map_err(|e| anyhow::anyhow!("Failed to extract default.xex from disc image: {}", e))
} else { } else {
@@ -149,9 +154,26 @@ fn main() -> Result<()> {
Commands::Info { path } => cmd_info(&path), Commands::Info { path } => cmd_info(&path),
Commands::Disasm { path, count, at } => cmd_disasm(&path, count, at), Commands::Disasm { path, count, at } => cmd_disasm(&path, count, at),
Commands::Browse { path } => cmd_browse(&path), Commands::Browse { path } => cmd_browse(&path),
Commands::Extract { path, output, db } => cmd_extract(&path, output.as_deref(), db.as_deref()), Commands::Extract { path, output, db } => {
Commands::Dis { path, output, db, json, analyze, max_indirect_candidates, quiet } => cmd_extract(&path, output.as_deref(), db.as_deref())
cmd_dis(&path, output.as_deref(), db.as_deref(), json.as_deref(), analyze, max_indirect_candidates, quiet), }
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 { if let Some(ref ffi) = header.file_format_info {
println!("\n=== File Format ==="); println!("\n=== File Format ===");
println!("Encryption: {}", match ffi.encryption_type { println!(
0 => "None", 1 => "Normal (AES)", _ => "Unknown" "Encryption: {}",
}); match ffi.encryption_type {
println!("Compression: {}", match ffi.compression_type { 0 => "None",
0 => "None", 1 => "Basic", 2 => "Normal (LZX)", _ => "Unknown" 1 => "Normal (AES)",
}); _ => "Unknown",
}
);
println!(
"Compression: {}",
match ffi.compression_type {
0 => "None",
1 => "Basic",
2 => "Normal (LZX)",
_ => "Unknown",
}
);
if !ffi.basic_blocks.is_empty() { if !ffi.basic_blocks.is_empty() {
println!("Basic blocks: {}", ffi.basic_blocks.len()); println!("Basic blocks: {}", ffi.basic_blocks.len());
} }
@@ -216,18 +249,27 @@ fn cmd_info(path: &str) -> Result<()> {
if !header.import_libraries.is_empty() { if !header.import_libraries.is_empty() {
println!("\n=== Import Libraries ==="); println!("\n=== Import Libraries ===");
for lib in &header.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(()) Ok(())
} }
/// Clap parser for `--at` — accepts decimal, 0x-prefixed hex, or bare hex. /// Clap parser for `--at` — accepts decimal, 0x-prefixed hex, or bare hex.
fn parse_hex_u32(s: &str) -> Result<u32, String> { fn parse_hex_u32(s: &str) -> Result<u32, String> {
let t = s.trim(); 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) (rest, 16)
} else if t.chars().all(|c| c.is_ascii_digit()) { } else if t.chars().all(|c| c.is_ascii_digit()) {
(t, 10) (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) let base = sylpheed_xex::loader::get_image_base(&header)
.ok_or_else(|| anyhow::anyhow!("No image base found in XEX2 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)?; let image_data = sylpheed_xex::loader::load_image(&data, &header)?;
info!(bytes = image_data.len(), "image decompressed"); info!(bytes = image_data.len(), "image decompressed");
let start = at.unwrap_or(entry); let start = at.unwrap_or(entry);
let label = if at.is_some() { "requested address" } else { "entry point" }; let label = if at.is_some() {
println!("Disassembly from {} {:#010x} ({} instructions):\n", label, start, count); "requested address"
} else {
"entry point"
};
println!(
"Disassembly from {} {:#010x} ({} instructions):\n",
label, start, count
);
if start < base { if start < base {
return Err(anyhow::anyhow!( return Err(anyhow::anyhow!(
@@ -281,7 +334,10 @@ fn cmd_disasm(path: &str, count: usize, at: Option<u32>) -> Result<()> {
println!(" {:#010x}: {}", addr, text); 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(()) Ok(())
} }
@@ -290,8 +346,9 @@ fn cmd_disasm(path: &str, count: usize, at: Option<u32>) -> Result<()> {
fn cmd_browse(path: &str) -> Result<()> { fn cmd_browse(path: &str) -> Result<()> {
use sylpheed_xex::vfs::VfsDevice; use sylpheed_xex::vfs::VfsDevice;
let disc = sylpheed_xex::vfs::disc_image::DiscImageDevice::open("disc", std::path::Path::new(path)) let disc =
.map_err(|e| anyhow::anyhow!("Failed to open disc image: {}", e))?; 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); println!("=== XISO Contents: {} ===", path);
match disc.list_root() { match disc.list_root() {
@@ -307,15 +364,22 @@ fn cmd_browse(path: &str) -> Result<()> {
Ok(()) Ok(())
} }
/// Helper: load XEX, parse header, decompress PE, resolve imports, parse sections. /// Header, decompressed image, PE sections, raw container bytes.
#[instrument(skip_all, fields(path = %path))] type Prepared = (
/// Load a XEX and prepare it for analysis. 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 /// The **raw XEX bytes** come back too, because optional-header values are file
/// **raw XEX bytes**. The raw bytes are needed because optional-header values /// offsets into the container rather than image VAs — the resource table, and so
/// are file offsets into the container, not image VAs — the resource table /// the embedded XDBF package, is only reachable through them.
/// (and so the embedded XDBF package) is only reachable through them. #[instrument(skip_all, fields(path = %path))]
fn load_and_prepare(path: &str) -> Result<(sylpheed_xex::Xex2Header, Vec<u8>, Vec<sylpheed_xex::pe::PeSection>, Vec<u8>)> { fn load_and_prepare(path: &str) -> Result<Prepared> {
let data = load_xex_data(path)?; let data = load_xex_data(path)?;
let mut header = sylpheed_xex::loader::parse_xex2_header(&data)?; 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 entry = sylpheed_xex::loader::get_entry_point(&header).unwrap();
let base = sylpheed_xex::loader::get_image_base(&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 // Build JSON-serializable info struct
#[derive(Serialize)] #[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 input_path = std::path::Path::new(path);
let out_dir = match output_dir { let out_dir = match output_dir {
Some(d) => std::path::PathBuf::from(d), 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)?; 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()) .and_then(|s| s.to_str())
.unwrap_or("output"); .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"); info!(path = %json_path.display(), "wrote metadata JSON");
// Print summary // Print summary
let total_imports: usize = header.import_libraries.iter().map(|l| l.imports.len()).sum(); let total_imports: usize = header
println!("Extracted: {} sections, {} import libraries ({} imports)", .import_libraries
sections.len(), header.import_libraries.len(), total_imports); .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 { 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 // Write base tables to SQLite if requested
@@ -468,27 +551,38 @@ fn cmd_dis(
info!(thunks = import_map.len(), "resolved import thunks"); info!(thunks = import_map.len(), "resolved import thunks");
// Function analysis (with .pdata-validated boundaries when present) // 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()) .filter(|s| s.is_code())
.map(|s| (s.virtual_address, s.virtual_size, s.flags)) .map(|s| (s.virtual_address, s.virtual_size, s.flags))
.collect(); .collect();
let pdata_entries = sylpheed_xex::pdata::parse_pdata(&pe_image, base, &sections); 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( 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!( info!(
functions = func_analysis.functions.len(), 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", "function detection complete",
); );
// M12 — switch / jump-table recovery. Emits one `jt` xref per distinct // M12 — switch / jump-table recovery. Emits one `jt` xref per distinct
// case body so the case bodies stop looking unreachable, and reports the // case body so the case bodies stop looking unreachable, and reports the
// table extents so the linear disassembler can flag them as data. // table extents so the linear disassembler can flag them as data.
let jump_tables = sylpheed_xexdb::jumptables::analyze( let jump_tables =
&pe_image, base, &sections, &func_analysis, sylpheed_xexdb::jumptables::analyze(&pe_image, base, &sections, &func_analysis);
);
let jt_data_words = sylpheed_xexdb::jumptables::data_word_addresses(&jump_tables); let jt_data_words = sylpheed_xexdb::jumptables::data_word_addresses(&jump_tables);
info!( info!(
jump_tables = jump_tables.len(), jump_tables = jump_tables.len(),
@@ -499,7 +593,13 @@ fn cmd_dis(
// Cross-reference analysis // Cross-reference analysis
let mut xref_result = sylpheed_xexdb::xref::analyze_xrefs_skipping( 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 // Feed the recovered `switch` edges into the xref graph, so case bodies
@@ -507,7 +607,8 @@ fn cmd_dis(
let mut jt_edges = 0usize; let mut jt_edges = 0usize;
for jt in &jump_tables { for jt in &jump_tables {
for target in jt.distinct_targets() { for target in jt.distinct_targets() {
xref_result.xrefs xref_result
.xrefs
.entry(target) .entry(target)
.or_default() .or_default()
.push(sylpheed_xexdb::xref::Xref { .push(sylpheed_xexdb::xref::Xref {
@@ -515,12 +616,14 @@ fn cmd_dis(
kind: sylpheed_xexdb::xref::XrefKind::JumpTable, kind: sylpheed_xexdb::xref::XrefKind::JumpTable,
addr_mode: None, addr_mode: None,
}); });
xref_result.labels xref_result
.labels
.entry(target) .entry(target)
.or_insert_with(|| format!("case_{target:08X}")); .or_insert_with(|| format!("case_{target:08X}"));
jt_edges += 1; jt_edges += 1;
} }
xref_result.labels xref_result
.labels
.entry(jt.table_address) .entry(jt.table_address)
.or_insert_with(|| format!("jpt_{:08X}", 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> = let vptr_block_boundaries: std::collections::HashSet<u32> =
xref_result.labels.keys().copied().collect(); xref_result.labels.keys().copied().collect();
let mut vtable_anchors = sylpheed_xexdb::vtables::scan_vptr_write_constants( 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 // M13 — authoritative MSVC RTTI walk. Every `vftable[-1] -> COL` link the
// linker emitted is an anchor the heuristic scan must not miss, and 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( 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 named = sylpheed_xexdb::vtables::apply_rtti_names(&mut vtables, &rtti);
let vtables = vtables; let vtables = vtables;
@@ -584,11 +698,19 @@ fn cmd_dis(
// emits one xref edge per resolvable site. Inserted into xrefs as // emits one xref edge per resolvable site. Inserted into xrefs as
// kind='ind_call'. // kind='ind_call'.
let indirect_edges = sylpheed_xexdb::indirect::analyze( 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 { for edge in &indirect_edges {
xref_result.xrefs xref_result
.xrefs
.entry(edge.target) .entry(edge.target)
.or_default() .or_default()
.push(sylpheed_xexdb::xref::Xref { .push(sylpheed_xexdb::xref::Xref {
@@ -613,14 +735,22 @@ fn cmd_dis(
// Generic function-pointer-array scan (M8 + M11). Re-emits M3 vtables // Generic function-pointer-array scan (M8 + M11). Re-emits M3 vtables
// plus dispatch tables and static-init tables in `.rdata`. // plus dispatch tables and static-init tables in `.rdata`.
let mut fparrays = sylpheed_xexdb::funcptr_arrays::analyze( 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 // M11.5 — static-init driver chain detection. Replaces M11's prologue
// heuristic with a structurally-grounded result where the driver // heuristic with a structurally-grounded result where the driver
// function shape matches. // function shape matches.
let static_init = sylpheed_xexdb::static_init::analyze( 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, &xref_result.labels,
); );
info!( info!(
@@ -639,7 +769,10 @@ fn cmd_dis(
} }
info!( info!(
funcptr_arrays = fparrays.len(), 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(), static_inits = fparrays.iter().filter(|a| a.kind == "static_init").count(),
"function-pointer array set finalised", "function-pointer array set finalised",
); );
@@ -655,12 +788,24 @@ fn cmd_dis(
// M5.5 — typed indirect-dispatch resolution (this->vptr → method). // M5.5 — typed indirect-dispatch resolution (this->vptr → method).
let typed_ind = sylpheed_xexdb::ind_dispatch_typed::analyze( 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, 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 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!( info!(
vptr_writes = typed_ind.vptr_writes.len(), vptr_writes = typed_ind.vptr_writes.len(),
dispatches = typed_ind.dispatches.len(), dispatches = typed_ind.dispatches.len(),
@@ -675,7 +820,8 @@ fn cmd_dis(
// possibilities. // possibilities.
for d in &typed_ind.dispatches { for d in &typed_ind.dispatches {
for &method_pc in &d.method_pcs { for &method_pc in &d.method_pcs {
xref_result.xrefs xref_result
.xrefs
.entry(method_pc) .entry(method_pc)
.or_default() .or_default()
.push(sylpheed_xexdb::xref::Xref { .push(sylpheed_xexdb::xref::Xref {
@@ -707,7 +853,10 @@ fn cmd_dis(
Some(x) Some(x)
}); });
if xdbf.is_none() && !resources.is_empty() { 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 // Build DisasmInfo
@@ -728,7 +877,11 @@ fn cmd_dis(
let mut w = sylpheed_xexdb::DbWriter::open_fresh(std::path::Path::new(db))?; let mut w = sylpheed_xexdb::DbWriter::open_fresh(std::path::Path::new(db))?;
w.write_base(&disasm_info)?; w.write_base(&disasm_info)?;
w.ingest_instructions( 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( w.write_analysis_results(
&pe_image, &pe_image,
@@ -772,12 +925,20 @@ fn cmd_dis(
let mut out = std::io::BufWriter::new(std::fs::File::create(json)?); let mut out = std::io::BufWriter::new(std::fs::File::create(json)?);
let mut total: u64 = 0; let mut total: u64 = 0;
for section in &sections { for section in &sections {
if !section.is_code() { continue; } if !section.is_code() {
continue;
}
let abs_start = base + section.virtual_address; let abs_start = base + section.virtual_address;
let abs_end = abs_start + section.virtual_size; let abs_end = abs_start + section.virtual_size;
let items = sylpheed_xexdb::enrich_section( let items = sylpheed_xexdb::enrich_section(
&pe_image, base, &section.name, abs_start, abs_end, &pe_image,
&func_analysis, &xref_result.labels, &jt_data_words, 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)?; 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(()) Ok(())
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::parse_hex_u32; 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] #[test]
fn plain_class_in_namespace() { 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] #[test]
@@ -358,12 +361,16 @@ mod rtti_name_tests {
#[test] #[test]
fn global_scope_class() { 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] #[test]
fn anonymous_namespace_is_named() { 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.ends_with("Act_Stop"), "got {got}");
assert!(got.starts_with("unnamed_namespaces"), "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 { fn fi(start: u32, end: u32) -> FuncInfo {
FuncInfo { FuncInfo {
start, end, start,
frame_size: 0, saved_gprs: 0, is_leaf: true, is_saverestore: false, end,
pdata_validated: true, pdata_length: Some(end - start), frame_size: 0,
pdata_prolog_length: None, has_eh: false, 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 labels = HashMap::new();
let data_words = BTreeSet::new(); let data_words = BTreeSet::new();
let got: Vec<(u32, Option<u32>)> = enrich_section( let got: Vec<(u32, Option<u32>)> = enrich_section(
&image, image_base, ".text", image_base, image_base + 24, &image,
&fa, &labels, &data_words, image_base,
).map(|r| (r.item.addr, r.function)).collect(); ".text",
image_base,
image_base + 24,
&fa,
&labels,
&data_words,
)
.map(|r| (r.item.addr, r.function))
.collect();
assert_eq!(got, vec![ assert_eq!(
(image_base, Some(image_base)), // inside f0 got,
(image_base + 4, Some(image_base)), // inside f0 vec![
(image_base + 8, None), // gap — was wrongly f0 (image_base, Some(image_base)), // inside f0
(image_base + 12, None), // gap — was wrongly f0 (image_base + 4, Some(image_base)), // inside f0
(image_base + 16, Some(image_base + 16)), // f1 starts (image_base + 8, None), // gap — was wrongly f0
(image_base + 20, Some(image_base + 16)), (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 /// 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, fi(image_base, image_base + 8));
functions.insert(image_base + 8, fi(image_base + 8, image_base + 16)); functions.insert(image_base + 8, fi(image_base + 8, image_base + 16));
let fa = FuncAnalysis { 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(), pdata_entries: Vec::new(),
}; };
let labels = HashMap::new(); let labels = HashMap::new();
let data_words = BTreeSet::new(); let data_words = BTreeSet::new();
let got: Vec<Option<u32>> = enrich_section( let got: Vec<Option<u32>> = enrich_section(
&image, image_base, ".text", image_base, image_base + 16, &image,
&fa, &labels, &data_words, image_base,
).map(|r| r.function).collect(); ".text",
assert_eq!(got, vec![ image_base,
Some(image_base), Some(image_base), image_base + 16,
Some(image_base + 8), Some(image_base + 8), &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; use sylpheed_xex::pe::PeSection;
const MAGIC_OLD: u32 = 0x1993_0520; const MAGIC_OLD: u32 = 0x1993_0520;
const MAGIC_V21: u32 = 0x1993_0521; const MAGIC_V21: u32 = 0x1993_0521;
const MAGIC_V22: u32 = 0x1993_0522; const MAGIC_V22: u32 = 0x1993_0522;
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub struct UnwindMapEntry { pub struct UnwindMapEntry {
@@ -85,41 +85,56 @@ pub struct EhFuncInfo {
} }
#[tracing::instrument(skip_all, fields(image_base = format_args!("{:#010x}", image_base)))] #[tracing::instrument(skip_all, fields(image_base = format_args!("{:#010x}", image_base)))]
pub fn analyze( pub fn analyze(pe: &[u8], image_base: u32, sections: &[PeSection]) -> Vec<EhFuncInfo> {
pe: &[u8],
image_base: u32,
sections: &[PeSection],
) -> Vec<EhFuncInfo> {
let started = std::time::Instant::now(); let started = std::time::Instant::now();
let mut out: Vec<EhFuncInfo> = Vec::new(); let mut out: Vec<EhFuncInfo> = Vec::new();
// Compute the union of valid VA ranges across all sections — used to // Compute the union of valid VA ranges across all sections — used to
// sanity-check internal pointers in the FuncInfo records. // sanity-check internal pointers in the FuncInfo records.
let valid_ranges: Vec<(u32, u32)> = sections.iter() let valid_ranges: Vec<(u32, u32)> = sections
.map(|s| (image_base + s.virtual_address, .iter()
image_base + s.virtual_address + s.virtual_size)) .map(|s| {
(
image_base + s.virtual_address,
image_base + s.virtual_address + s.virtual_size,
)
})
.collect(); .collect();
let in_valid = |va: u32| valid_ranges.iter().any(|(lo, hi)| va >= *lo && va < *hi); let in_valid = |va: u32| valid_ranges.iter().any(|(lo, hi)| va >= *lo && va < *hi);
let read_u32 = |abs: u32| -> Option<u32> { let read_u32 = |abs: u32| -> Option<u32> {
let off = abs.wrapping_sub(image_base) as usize; let off = abs.wrapping_sub(image_base) as usize;
if off + 4 > pe.len() { return None; } if off + 4 > pe.len() {
Some(u32::from_be_bytes([pe[off], pe[off + 1], pe[off + 2], pe[off + 3]])) 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) }; let read_i32 = |abs: u32| -> Option<i32> { read_u32(abs).map(|u| u as i32) };
for section in sections { for section in sections {
if section.name != ".rdata" { continue; } if section.name != ".rdata" {
continue;
}
let raw_start = section.virtual_address as usize; let raw_start = section.virtual_address as usize;
let raw_end = (section.virtual_address + section.virtual_size) 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 bytes = &pe[raw_start..raw_end.min(pe.len())];
let va_base = image_base + section.virtual_address; let va_base = image_base + section.virtual_address;
// Walk on 4-byte alignment looking for the magic. // Walk on 4-byte alignment looking for the magic.
let mut i = 0; let mut i = 0;
while i + 4 <= bytes.len() { 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]]); 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 { if m == MAGIC_OLD || m == MAGIC_V21 || m == MAGIC_V22 {
let addr = va_base + i as u32; let addr = va_base + i as u32;
@@ -152,23 +167,35 @@ fn parse_funcinfo(
read_i32: &impl Fn(u32) -> Option<i32>, read_i32: &impl Fn(u32) -> Option<i32>,
in_valid: &impl Fn(u32) -> bool, in_valid: &impl Fn(u32) -> bool,
) -> Option<EhFuncInfo> { ) -> Option<EhFuncInfo> {
let max_state = read_i32(addr + 0x04)?; let max_state = read_i32(addr + 0x04)?;
let p_unwind_map = read_u32(addr + 0x08)?; let p_unwind_map = read_u32(addr + 0x08)?;
let n_try_blocks = read_u32(addr + 0x0C)?; let n_try_blocks = read_u32(addr + 0x0C)?;
let p_try_block_map = read_u32(addr + 0x10)?; let p_try_block_map = read_u32(addr + 0x10)?;
let n_ip_map_entries = read_u32(addr + 0x14)?; let n_ip_map_entries = read_u32(addr + 0x14)?;
let p_ip_to_state_map = read_u32(addr + 0x18)?; let p_ip_to_state_map = read_u32(addr + 0x18)?;
// Sanity caps: real FuncInfo records have max_state ≤ a few thousand, // Sanity caps: real FuncInfo records have max_state ≤ a few thousand,
// n_try_blocks ≤ a few hundred. Reject obviously bogus values that // n_try_blocks ≤ a few hundred. Reject obviously bogus values that
// happened to alias the magic. // happened to alias the magic.
if !(0..=10_000).contains(&max_state) { return None; } if !(0..=10_000).contains(&max_state) {
if n_try_blocks > 1_000 { return None; } return None;
if n_ip_map_entries > 100_000 { 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. // 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_unwind_map != 0 && !in_valid(p_unwind_map) {
if p_try_block_map != 0 && !in_valid(p_try_block_map) { return None; } return None;
if p_ip_to_state_map != 0 && !in_valid(p_ip_to_state_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 { let (p_es_type_list, eh_flags) = if magic == MAGIC_V21 {
(read_u32(addr + 0x1C), None) (read_u32(addr + 0x1C), None)
@@ -185,7 +212,10 @@ fn parse_funcinfo(
let p = p_unwind_map.wrapping_add((i * 8) as u32); let p = p_unwind_map.wrapping_add((i * 8) as u32);
let to_state = read_i32(p)?; let to_state = read_i32(p)?;
let action_pc = read_u32(p + 4)?; 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 { if p_try_block_map != 0 && n_try_blocks > 0 {
for i in 0..n_try_blocks { for i in 0..n_try_blocks {
let p = p_try_block_map.wrapping_add(i * 20); let p = p_try_block_map.wrapping_add(i * 20);
let try_low = read_i32(p)?; let try_low = read_i32(p)?;
let try_high = read_i32(p + 4)?; let try_high = read_i32(p + 4)?;
let catch_high = read_i32(p + 8)?; let catch_high = read_i32(p + 8)?;
let n_catches = read_u32(p + 12)?; let n_catches = read_u32(p + 12)?;
let p_handler_a = read_u32(p + 16)?; let p_handler_a = read_u32(p + 16)?;
try_blocks.push(TryBlockMapEntry { 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 { fn mk_section(name: &str, va: u32, size: u32) -> PeSection {
PeSection { PeSection {
name: name.into(), name: name.into(),
virtual_address: va, virtual_size: size, virtual_address: va,
raw_offset: va, raw_size: size, virtual_size: size,
raw_offset: va,
raw_size: size,
flags: 0x4000_0040, flags: 0x4000_0040,
} }
} }
@@ -254,17 +290,17 @@ mod tests {
let unwind_off = (rdata_va + 0x80) as usize; let unwind_off = (rdata_va + 0x80) as usize;
let unwind_va = image_base + rdata_va + 0x80; let unwind_va = image_base + rdata_va + 0x80;
write_be(&mut pe, fi_off, MAGIC_OLD); // magic write_be(&mut pe, fi_off, MAGIC_OLD); // magic
write_be_i32(&mut pe, fi_off + 4, 2); // maxState 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 + 8, unwind_va); // pUnwindMap
write_be(&mut pe, fi_off + 12, 0); // nTryBlocks write_be(&mut pe, fi_off + 12, 0); // nTryBlocks
write_be(&mut pe, fi_off + 16, 0); // pTryBlockMap write_be(&mut pe, fi_off + 16, 0); // pTryBlockMap
write_be(&mut pe, fi_off + 20, 0); // nIPMapEntries write_be(&mut pe, fi_off + 20, 0); // nIPMapEntries
write_be(&mut pe, fi_off + 24, 0); // pIPtoStateMap write_be(&mut pe, fi_off + 24, 0); // pIPtoStateMap
// Two unwind entries. // Two unwind entries.
write_be_i32(&mut pe, unwind_off, -1); // to_state write_be_i32(&mut pe, unwind_off, -1); // to_state
write_be(&mut pe, unwind_off + 4, image_base + 0x500); // action_pc write_be(&mut pe, unwind_off + 4, image_base + 0x500); // action_pc
write_be_i32(&mut pe, unwind_off + 8, 0); write_be_i32(&mut pe, unwind_off + 8, 0);
write_be(&mut pe, unwind_off + 12, image_base + 0x600); write_be(&mut pe, unwind_off + 12, image_base + 0x600);
@@ -288,7 +324,7 @@ mod tests {
let mut pe = vec![0u8; 0x4000]; let mut pe = vec![0u8; 0x4000];
let fi_off = (rdata_va + 0x10) as usize; let fi_off = (rdata_va + 0x10) as usize;
write_be(&mut pe, fi_off, MAGIC_OLD); 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 sections = vec![mk_section(".rdata", rdata_va, 0x100)];
let recs = analyze(&pe, image_base, &sections); let recs = analyze(&pe, image_base, &sections);
assert_eq!(recs.len(), 0); assert_eq!(recs.len(), 0);

View File

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

View File

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

View File

@@ -112,12 +112,12 @@ pub struct VptrWrite {
pub writer_function: u32, pub writer_function: u32,
} }
const OP_ADDI: u32 = 14; const OP_ADDI: u32 = 14;
const OP_ADDIS: u32 = 15; const OP_ADDIS: u32 = 15;
const OP_BCCTR: u32 = 19; const OP_BCCTR: u32 = 19;
const OP_LWZ: u32 = 32; const OP_LWZ: u32 = 32;
const OP_ORI: u32 = 24; const OP_ORI: u32 = 24;
const OP_STW: u32 = 36; const OP_STW: u32 = 36;
const OP_X_FORM: u32 = 31; const OP_X_FORM: u32 = 31;
/// Run the full M5.5 analysis. /// Run the full M5.5 analysis.
@@ -133,26 +133,36 @@ pub fn analyze(
let started = std::time::Instant::now(); let started = std::time::Instant::now();
let vtable_addrs: BTreeSet<u32> = vtables.iter().map(|v| v.address).collect(); let vtable_addrs: BTreeSet<u32> = vtables.iter().map(|v| v.address).collect();
let vtable_by_addr: BTreeMap<u32, &Vtable> = let vtable_by_addr: BTreeMap<u32, &Vtable> = vtables.iter().map(|v| (v.address, v)).collect();
vtables.iter().map(|v| (v.address, v)).collect();
let block_boundaries: HashSet<u32> = labels.keys().copied().collect(); let block_boundaries: HashSet<u32> = labels.keys().copied().collect();
// Phase 1: scan for vptr writes. // Phase 1: scan for vptr writes.
let vptr_writes = scan_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. // Phase 2: invert by offset.
let mut vtables_by_offset: HashMap<u32, HashSet<u32>> = HashMap::new(); let mut vtables_by_offset: HashMap<u32, HashSet<u32>> = HashMap::new();
for w in &vptr_writes { 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. // Phase 3 + 4: scan dispatches and emit edges.
let mut dispatches = scan_dispatches_and_resolve( let mut dispatches = scan_dispatches_and_resolve(
pe, image_base, func_analysis, &block_boundaries, pe,
&vtables_by_offset, &vtable_by_addr, image_base,
func_analysis,
&block_boundaries,
&vtables_by_offset,
&vtable_by_addr,
); );
// Drop the per-candidate lists for sites the analysis could not narrow. // 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 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 multi_candidate = dispatches.len() - single_candidate;
let total_edges: usize = dispatches.iter().map(|d| d.method_pcs.len()).sum(); 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); 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", "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> { fn read_instr(pe: &[u8], image_base: u32, addr: u32) -> Option<u32> {
let off = addr.wrapping_sub(image_base) as usize; let off = addr.wrapping_sub(image_base) as usize;
if off + 4 > pe.len() { return None; } if off + 4 > pe.len() {
Some(u32::from_be_bytes([pe[off], pe[off + 1], pe[off + 2], pe[off + 3]])) 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 /// Phase 1 — find every `stw rA, off(rB)` where the lis+addi-tracked
@@ -217,14 +240,18 @@ fn scan_vptr_writes(
) -> Vec<VptrWrite> { ) -> Vec<VptrWrite> {
let mut writes: Vec<VptrWrite> = Vec::new(); let mut writes: Vec<VptrWrite> = Vec::new();
for (&fn_start, fi) in &func_analysis.functions { 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 reg: [Option<u32>; 32] = [None; 32];
let mut pc = fn_start; let mut pc = fn_start;
while pc < fi.end { while pc < fi.end {
if pc != fn_start && block_boundaries.contains(&pc) { if pc != fn_start && block_boundaries.contains(&pc) {
reg = [None; 32]; 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 op = instr >> 26;
let rd = ((instr >> 21) & 0x1F) as usize; let rd = ((instr >> 21) & 0x1F) as usize;
let ra = ((instr >> 16) & 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, 32..=35 | 40..=43 | 48..=51 => reg[rd] = None,
OP_X_FORM => { OP_X_FORM => {
let xo = (instr >> 1) & 0x3FF; let xo = (instr >> 1) & 0x3FF;
if xo != 444 && xo != 467 { reg[rd] = None; } if xo != 444 && xo != 467 {
reg[rd] = None;
}
} }
18 => { 18 => {
// `bl` (LK=1) clobbers volatile r0..r12 + ctr. Plain // `bl` (LK=1) clobbers volatile r0..r12 + ctr. Plain
// `b` makes the next instruction unreachable; the // `b` makes the next instruction unreachable; the
// label-based reset handles join points. // label-based reset handles join points.
if (instr & 1) != 0 { if (instr & 1) != 0 {
for r in 0..=12 { reg[r] = None; } for r in 0..=12 {
reg[r] = None;
}
} }
} }
16 => { 16 if (instr & 1) != 0 => {
if (instr & 1) != 0 { for r in 0..=12 {
for r in 0..=12 { reg[r] = None; } reg[r] = None;
} }
} }
_ => {} _ => {}
@@ -302,18 +333,29 @@ fn scan_dispatches_and_resolve(
) -> Vec<TypedDispatch> { ) -> Vec<TypedDispatch> {
let mut out: Vec<TypedDispatch> = Vec::new(); let mut out: Vec<TypedDispatch> = Vec::new();
for (&fn_start, fi) in &func_analysis.functions { for (&fn_start, fi) in &func_analysis.functions {
if fi.is_saverestore { continue; } if fi.is_saverestore {
continue;
}
let mut pc = fn_start; let mut pc = fn_start;
while pc < fi.end { 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; let op = instr >> 26;
if op == OP_BCCTR { if op == OP_BCCTR {
let xo = (instr >> 1) & 0x3FF; let xo = (instr >> 1) & 0x3FF;
let lk = (instr & 1) != 0; let lk = (instr & 1) != 0;
if xo == 528 && lk if xo == 528
&& lk
&& let Some(d) = try_resolve_dispatch_site( && let Some(d) = try_resolve_dispatch_site(
pe, image_base, fn_start, fi.end, pc, pe,
block_boundaries, vtables_by_offset, vtable_by_addr, image_base,
fn_start,
fi.end,
pc,
block_boundaries,
vtables_by_offset,
vtable_by_addr,
) )
{ {
out.push(d); out.push(d);
@@ -346,9 +388,15 @@ fn try_resolve_dispatch_site(
let mut mtctr_pc: Option<u32> = None; let mut mtctr_pc: Option<u32> = None;
for i in 1..=LOOKBACK { for i in 1..=LOOKBACK {
let p = bcctrl_pc.wrapping_sub(i * 4); let p = bcctrl_pc.wrapping_sub(i * 4);
if p < fn_start { break; } if p < fn_start {
if block_boundaries.contains(&p) { break; } break;
let Some(instr) = read_instr(pe, image_base, p) else { break }; }
if block_boundaries.contains(&p) {
break;
}
let Some(instr) = read_instr(pe, image_base, p) else {
break;
};
let op = instr >> 26; let op = instr >> 26;
if op == OP_X_FORM { if op == OP_X_FORM {
let xo = (instr >> 1) & 0x3FF; let xo = (instr >> 1) & 0x3FF;
@@ -371,17 +419,27 @@ fn try_resolve_dispatch_site(
let mut fn_lwz_pc: Option<u32> = None; let mut fn_lwz_pc: Option<u32> = None;
for i in 1..=LOOKBACK { for i in 1..=LOOKBACK {
let p = mtctr_pc.wrapping_sub(i * 4); let p = mtctr_pc.wrapping_sub(i * 4);
if p < fn_start { break; } if p < fn_start {
if block_boundaries.contains(&p) { break; } break;
let Some(instr) = read_instr(pe, image_base, p) else { break }; }
if block_boundaries.contains(&p) {
break;
}
let Some(instr) = read_instr(pe, image_base, p) else {
break;
};
let op = instr >> 26; let op = instr >> 26;
let rd = ((instr >> 21) & 0x1F) as usize; let rd = ((instr >> 21) & 0x1F) as usize;
if op == OP_LWZ { if op == OP_LWZ {
if rd == mtctr_rs { if rd == mtctr_rs {
let ra = ((instr >> 16) & 0x1F) as usize; 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; 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); slot = Some((off as u32) / 4);
vt_reg = Some(ra); vt_reg = Some(ra);
fn_lwz_pc = Some(p); fn_lwz_pc = Some(p);
@@ -402,14 +460,22 @@ fn try_resolve_dispatch_site(
let mut vptr_off: Option<u32> = None; let mut vptr_off: Option<u32> = None;
for i in 1..=LOOKBACK { for i in 1..=LOOKBACK {
let p = fn_lwz_pc.wrapping_sub(i * 4); let p = fn_lwz_pc.wrapping_sub(i * 4);
if p < fn_start { break; } if p < fn_start {
if block_boundaries.contains(&p) { break; } break;
let Some(instr) = read_instr(pe, image_base, p) else { break }; }
if block_boundaries.contains(&p) {
break;
}
let Some(instr) = read_instr(pe, image_base, p) else {
break;
};
let op = instr >> 26; let op = instr >> 26;
let rd = ((instr >> 21) & 0x1F) as usize; let rd = ((instr >> 21) & 0x1F) as usize;
if op == OP_LWZ && rd == vt_reg { if op == OP_LWZ && rd == vt_reg {
let ra = ((instr >> 16) & 0x1F) as usize; 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; let off = ((instr & 0xFFFF) as i16) as i32;
// Negative offsets are valid in C++ (multiple inheritance casts // Negative offsets are valid in C++ (multiple inheritance casts
// can produce them in some ABIs); reinterpret as u32 wrap. // can produce them in some ABIs); reinterpret as u32 wrap.
@@ -435,7 +501,9 @@ fn try_resolve_dispatch_site(
method_pcs.push(method_pc); 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(); let total_candidates = candidate_vtables.len();
Some(TypedDispatch { Some(TypedDispatch {
@@ -460,13 +528,16 @@ fn writes_reg(instr: u32, r: u32) -> bool {
// Most arithmetic / load opcodes use bits 21..25 = rD/rT. // Most arithmetic / load opcodes use bits 21..25 = rD/rT.
14 | 15 | 32..=43 | 46 | 48..=51 => rd == r, 14 | 15 | 32..=43 | 46 | 48..=51 => rd == r,
// ori/oris/xor/etc. opcodes 24..29 — rA in bits 16..20 is the dest. // 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. // X-form: most write rD; some write rA. Check both, conservatively.
OP_X_FORM => { OP_X_FORM => {
let xo = (instr >> 1) & 0x3FF; let xo = (instr >> 1) & 0x3FF;
// Logical X-form (and/or/xor/etc.): rA is the dest. // Logical X-form (and/or/xor/etc.): rA is the dest.
// Logical X-form ops (and/or/xor/etc.) write rA, not rD. // 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 ((instr >> 16) & 0x1F) == r
} else { } else {
rd == r rd == r
@@ -496,19 +567,27 @@ mod tests {
fn mk_func_analysis(start: u32, len: u32) -> FuncAnalysis { fn mk_func_analysis(start: u32, len: u32) -> FuncAnalysis {
let mut functions: BTreeMap<u32, FuncInfo> = BTreeMap::new(); let mut functions: BTreeMap<u32, FuncInfo> = BTreeMap::new();
functions.insert(start, FuncInfo { functions.insert(
start, start,
end: start + len, FuncInfo {
frame_size: 0, start,
saved_gprs: 0, end: start + len,
is_leaf: false, frame_size: 0,
is_saverestore: false, saved_gprs: 0,
pdata_validated: false, is_leaf: false,
pdata_length: None, is_saverestore: false,
pdata_prolog_length: None, pdata_validated: false,
has_eh: false, pdata_length: None,
}); pdata_prolog_length: None,
FuncAnalysis { functions, save_gpr_base: None, restore_gpr_base: None, pdata_entries: Vec::new() } 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) { 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) { fn enc_vptr_write(pe: &mut [u8], at: usize, vt: u32, write_off: i16, dest_reg: u32) {
let hi = (vt >> 16) as u16; let hi = (vt >> 16) as u16;
let lo = (vt & 0xFFFF) as i16; 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 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); let stw = (36u32 << 26) | (3 << 21) | (dest_reg << 16) | ((write_off as u16) as u32);
write_be(pe, at, lis); write_be(pe, at, lis);
@@ -560,11 +639,21 @@ mod tests {
// Both functions in func_analysis (synthesise). // Both functions in func_analysis (synthesise).
let mut fa = mk_func_analysis(ctor_pc, 0x40); let mut fa = mk_func_analysis(ctor_pc, 0x40);
fa.functions.insert(disp_pc, FuncInfo { fa.functions.insert(
start: disp_pc, end: disp_pc + 0x40, frame_size: 0, saved_gprs: 0, disp_pc,
is_leaf: false, is_saverestore: false, FuncInfo {
pdata_validated: false, pdata_length: None, pdata_prolog_length: None, has_eh: false, 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 vt = mk_vtable(0x82010000, vec![0xAA, 0xBB, 0xCC, 0xDD]);
let labels: HashMap<u32, String> = HashMap::new(); let labels: HashMap<u32, String> = HashMap::new();
@@ -585,9 +674,14 @@ mod tests {
/// Two classes installing different vtables at offset 0, and one dispatch /// Two classes installing different vtables at offset 0, and one dispatch
/// at slot 1 that therefore matches both. /// at slot 1 that therefore matches both.
fn multi_candidate_fixture(image_base: u32) fn multi_candidate_fixture(
-> (Vec<u8>, FuncAnalysis, Vec<crate::vtables::Vtable>, HashMap<u32, String>) image_base: u32,
{ ) -> (
Vec<u8>,
FuncAnalysis,
Vec<crate::vtables::Vtable>,
HashMap<u32, String>,
) {
let mut pe = vec![0u8; 0x4000]; let mut pe = vec![0u8; 0x4000];
// Two ctors, each writing a different vtable at offset 0. // 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); enc_dispatch(&mut pe, (disp - image_base) as usize, 0, 1);
let mut fa = mk_func_analysis(ctor_a, 0x40); let mut fa = mk_func_analysis(ctor_a, 0x40);
fa.functions.insert(ctor_b, FuncInfo { fa.functions.insert(
start: ctor_b, end: ctor_b + 0x40, frame_size: 0, saved_gprs: 0, ctor_b,
is_leaf: false, is_saverestore: false, FuncInfo {
pdata_validated: false, pdata_length: None, pdata_prolog_length: None, has_eh: false, start: ctor_b,
}); end: ctor_b + 0x40,
fa.functions.insert(disp, FuncInfo { frame_size: 0,
start: disp, end: disp + 0x40, frame_size: 0, saved_gprs: 0, saved_gprs: 0,
is_leaf: false, is_saverestore: false, is_leaf: false,
pdata_validated: false, pdata_length: None, pdata_prolog_length: None, has_eh: 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![ let vts = vec![
mk_vtable(0x82010000, vec![0x11, 0x22, 0x33, 0x44]), 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); enc_dispatch(&mut pe, (disp - image_base) as usize, 0, 10);
let mut fa = mk_func_analysis(ctor, 0x40); let mut fa = mk_func_analysis(ctor, 0x40);
fa.functions.insert(disp, FuncInfo { fa.functions.insert(
start: disp, end: disp + 0x40, frame_size: 0, saved_gprs: 0, disp,
is_leaf: false, is_saverestore: false, FuncInfo {
pdata_validated: false, pdata_length: None, pdata_prolog_length: None, has_eh: false, 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 vt = mk_vtable(0x82010000, vec![0x11, 0x22, 0x33, 0x44]);
let labels: HashMap<u32, String> = HashMap::new(); let labels: HashMap<u32, String> = HashMap::new();
@@ -673,11 +797,21 @@ mod tests {
enc_dispatch(&mut pe, (disp - image_base) as usize, 8, 1); enc_dispatch(&mut pe, (disp - image_base) as usize, 8, 1);
let mut fa = mk_func_analysis(ctor, 0x40); let mut fa = mk_func_analysis(ctor, 0x40);
fa.functions.insert(disp, FuncInfo { fa.functions.insert(
start: disp, end: disp + 0x40, frame_size: 0, saved_gprs: 0, disp,
is_leaf: false, is_saverestore: false, FuncInfo {
pdata_validated: false, pdata_length: None, pdata_prolog_length: None, has_eh: false, 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 vt = mk_vtable(0x82010000, vec![0x11, 0x22, 0x33, 0x44]);
let labels: HashMap<u32, String> = HashMap::new(); let labels: HashMap<u32, String> = HashMap::new();
@@ -707,5 +841,4 @@ mod tests {
assert!(d.method_pcs.is_empty(), "no speculative edges"); assert!(d.method_pcs.is_empty(), "no speculative edges");
assert!(d.candidate_vtables.is_empty()); 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_ADDIS: u32 = 15;
const OP_BCCTR: u32 = 19; // also covers blr — distinguish via XO const OP_BCCTR: u32 = 19; // also covers blr — distinguish via XO
const OP_LWZ: u32 = 32; const OP_LWZ: u32 = 32;
const OP_ORI: u32 = 24; const OP_ORI: u32 = 24;
const OP_X_FORM: u32 = 31; // mtspr / mr / etc. const OP_X_FORM: u32 = 31; // mtspr / mr / etc.
/// Run the static indirect-dispatch scan. Returns one edge per resolvable /// Run the static indirect-dispatch scan. Returns one edge per resolvable
/// `bcctrl` site. /// `bcctrl` site.
@@ -77,8 +77,7 @@ pub fn analyze(
let started = std::time::Instant::now(); let started = std::time::Instant::now();
// Index vtables by their start VA so the lwz handler can decide // Index vtables by their start VA so the lwz handler can decide
// whether a given Const(addr) is "really" a vtable. // whether a given Const(addr) is "really" a vtable.
let vtable_by_addr: BTreeMap<u32, &Vtable> = let vtable_by_addr: BTreeMap<u32, &Vtable> = vtables.iter().map(|v| (v.address, v)).collect();
vtables.iter().map(|v| (v.address, v)).collect();
// Set of all "label"-bearing PCs in the analyzed binary. We treat each // 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, // 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(); let mut edges: Vec<IndirectEdge> = Vec::new();
for (&fn_start, fi) in &func_analysis.functions { 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 reg: [Option<RegVal>; 32] = [None; 32];
let mut ctr: Option<RegVal> = None; let mut ctr: Option<RegVal> = None;
let mut pc = fn_start; let mut pc = fn_start;
@@ -162,7 +163,9 @@ pub fn analyze(
let resolved = resolve_vtable_slot(target, &vtable_by_addr) let resolved = resolve_vtable_slot(target, &vtable_by_addr)
.or_else(|| resolve_vtable_slot_via_off(base, simm, &vtable_by_addr)); .or_else(|| resolve_vtable_slot_via_off(base, simm, &vtable_by_addr));
reg[rd] = resolved.map(|(vt, slot, pc)| RegVal::MethodPtr { reg[rd] = resolved.map(|(vt, slot, pc)| RegVal::MethodPtr {
vtable_addr: vt, slot, method_pc: pc, vtable_addr: vt,
slot,
method_pc: pc,
}); });
} else { } else {
reg[rd] = None; reg[rd] = None;
@@ -201,7 +204,11 @@ pub fn analyze(
if xo == 528 { if xo == 528 {
let lk = (instr & 1) != 0; let lk = (instr & 1) != 0;
if lk 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 { edges.push(IndirectEdge {
source: pc, source: pc,
@@ -227,7 +234,9 @@ pub fn analyze(
18 => { 18 => {
let lk = (instr & 1) != 0; let lk = (instr & 1) != 0;
if lk { if lk {
for r in 0..=12 { reg[r] = None; } for r in 0..=12 {
reg[r] = None;
}
ctr = None; ctr = None;
} }
// LK=0 (`b`) makes fall-through unreachable; nothing to do — // LK=0 (`b`) makes fall-through unreachable; nothing to do —
@@ -239,7 +248,9 @@ pub fn analyze(
16 => { 16 => {
let lk = (instr & 1) != 0; let lk = (instr & 1) != 0;
if lk { if lk {
for r in 0..=12 { reg[r] = None; } for r in 0..=12 {
reg[r] = None;
}
ctr = None; ctr = None;
} }
} }
@@ -274,8 +285,15 @@ pub fn analyze(
fn read_instr(pe: &[u8], image_base: u32, addr: u32) -> Option<u32> { fn read_instr(pe: &[u8], image_base: u32, addr: u32) -> Option<u32> {
let off = addr.wrapping_sub(image_base) as usize; let off = addr.wrapping_sub(image_base) as usize;
if off + 4 > pe.len() { return None; } if off + 4 > pe.len() {
Some(u32::from_be_bytes([pe[off], pe[off + 1], pe[off + 2], pe[off + 3]])) 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, /// `target = base + simm` where `target` is an exact vtable head (rare,
@@ -303,11 +321,17 @@ fn resolve_vtable_slot(
) -> Option<(u32, u32, u32)> { ) -> Option<(u32, u32, u32)> {
// BTreeMap range search for the largest key ≤ target. // BTreeMap range search for the largest key ≤ target.
let (&vt_addr, vt) = vtable_by_addr.range(..=target).next_back()?; 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; 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; 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)?; let method_pc = *vt.methods.get(slot as usize)?;
Some((vt_addr, slot, method_pc)) 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) { fn encode_pattern(buf: &mut [u8], offset: usize, vtable_addr: u32, slot_off: i32) {
let hi = (vtable_addr >> 16) as u16; let hi = (vtable_addr >> 16) as u16;
let lo = (vtable_addr & 0xFFFF) as i16; 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 // 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 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); 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; // mtctr r4 = mtspr CTR(=9), r4. SPR_low (=9) → Rust bits 16-20;
// SPR_high (=0) → Rust bits 11-15. Rc bit 0. // SPR_high (=0) → Rust bits 11-15. Rc bit 0.
let mtctr = (31u32 << 26) | (4 << 21) | (9 << 16) | (0 << 11) | (467 << 1); let mtctr = ((31u32 << 26) | (4 << 21) | (9 << 16)) | (467 << 1);
let bcctrl = (19u32 << 26) | (20 << 21) | (528 << 1) | 1; // bcctrl 20, 0 let bcctrl = (19u32 << 26) | (20 << 21) | (528 << 1) | 1; // bcctrl 20, 0
let words = [lis, addi, lwz, mtctr, bcctrl]; let words = [lis, addi, lwz, mtctr, bcctrl];
for (i, w) in words.iter().enumerate() { for (i, w) in words.iter().enumerate() {
buf[offset + i * 4..offset + i * 4 + 4].copy_from_slice(&w.to_be_bytes()); 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 encode_pattern(&mut pe, text_va as usize, vtable_addr, 8); // slot 2
let mut functions: BTreeMap<u32, FuncInfo> = BTreeMap::new(); let mut functions: BTreeMap<u32, FuncInfo> = BTreeMap::new();
functions.insert(pc_start, FuncInfo { functions.insert(
start: pc_start, pc_start,
end: pc_start + 5 * 4, FuncInfo {
frame_size: 0, start: pc_start,
saved_gprs: 0, end: pc_start + 5 * 4,
is_leaf: false, frame_size: 0,
is_saverestore: false, saved_gprs: 0,
pdata_validated: false, is_leaf: false,
pdata_length: None, is_saverestore: false,
pdata_prolog_length: None, pdata_validated: false,
has_eh: false, pdata_length: None,
}); pdata_prolog_length: None,
has_eh: false,
},
);
let func_analysis = FuncAnalysis { let func_analysis = FuncAnalysis {
functions, functions,
save_gpr_base: None, save_gpr_base: None,
@@ -407,18 +434,21 @@ mod tests {
encode_pattern(&mut pe, text_va as usize, vtable_addr, 48); encode_pattern(&mut pe, text_va as usize, vtable_addr, 48);
let mut functions: BTreeMap<u32, FuncInfo> = BTreeMap::new(); let mut functions: BTreeMap<u32, FuncInfo> = BTreeMap::new();
functions.insert(pc_start, FuncInfo { functions.insert(
start: pc_start, pc_start,
end: pc_start + 5 * 4, FuncInfo {
frame_size: 0, start: pc_start,
saved_gprs: 0, end: pc_start + 5 * 4,
is_leaf: false, frame_size: 0,
is_saverestore: false, saved_gprs: 0,
pdata_validated: false, is_leaf: false,
pdata_length: None, is_saverestore: false,
pdata_prolog_length: None, pdata_validated: false,
has_eh: false, pdata_length: None,
}); pdata_prolog_length: None,
has_eh: false,
},
);
let func_analysis = FuncAnalysis { let func_analysis = FuncAnalysis {
functions, functions,
save_gpr_base: None, save_gpr_base: None,
@@ -443,18 +473,21 @@ mod tests {
encode_pattern(&mut pe, text_va as usize, vtable_addr, 0); encode_pattern(&mut pe, text_va as usize, vtable_addr, 0);
let mut functions: BTreeMap<u32, FuncInfo> = BTreeMap::new(); let mut functions: BTreeMap<u32, FuncInfo> = BTreeMap::new();
functions.insert(pc_start, FuncInfo { functions.insert(
start: pc_start, pc_start,
end: pc_start + 5 * 4, FuncInfo {
frame_size: 0, start: pc_start,
saved_gprs: 0, end: pc_start + 5 * 4,
is_leaf: false, frame_size: 0,
is_saverestore: false, saved_gprs: 0,
pdata_validated: false, is_leaf: false,
pdata_length: None, is_saverestore: false,
pdata_prolog_length: None, pdata_validated: false,
has_eh: false, pdata_length: None,
}); pdata_prolog_length: None,
has_eh: false,
},
);
let func_analysis = FuncAnalysis { let func_analysis = FuncAnalysis {
functions, functions,
save_gpr_base: None, save_gpr_base: None,
@@ -469,6 +502,10 @@ mod tests {
labels.insert(pc_start + 8, "loc_mid".to_string()); labels.insert(pc_start + 8, "loc_mid".to_string());
let edges = analyze(&pe, image_base, &func_analysis, &vtables, &labels); 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; const BCTR: u32 = 0x4E80_0420;
fn op(i: u32) -> u32 { i >> 26 } fn op(i: u32) -> u32 {
fn rt(i: u32) -> usize { ((i >> 21) & 0x1F) as usize } i >> 26
fn ra(i: u32) -> usize { ((i >> 16) & 0x1F) as usize } }
fn rb(i: u32) -> usize { ((i >> 11) & 0x1F) as usize } fn rt(i: u32) -> usize {
fn xo(i: u32) -> u32 { (i >> 1) & 0x3FF } ((i >> 21) & 0x1F) as usize
fn simm(i: u32) -> i32 { ((i & 0xFFFF) as i16) as i32 } }
fn uimm(i: u32) -> u32 { i & 0xFFFF } 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). /// `mtctr rS` — `mtspr` (op 31, xo 467) with the split SPR field naming CTR (9).
fn is_mtctr(i: u32) -> bool { 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; let spr_field = (i >> 11) & 0x3FF;
(((spr_field & 0x1F) << 5) | (spr_field >> 5)) == 9 (((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> { fn op31_dest(i: u32) -> Option<usize> {
// Logical / shift / sign-extend X-forms: destination is `rA` (bits 16..20). // Logical / shift / sign-extend X-forms: destination is `rA` (bits 16..20).
const WRITES_RA: &[u32] = &[ const WRITES_RA: &[u32] = &[
24, 26, 27, 28, 58, 60, 124, 284, 316, 412, 444, 476, 24, 26, 27, 28, 58, 60, 124, 284, 316, 412, 444, 476, 536, 539, 792, 794, 824, 826, 827,
536, 539, 792, 794, 824, 826, 827, 922, 954, 986, 922, 954, 986,
]; ];
// Stores, compares, traps, cache/sync ops and `mtspr`/`mtcrf`: no GPR write. // Stores, compares, traps, cache/sync ops and `mtspr`/`mtcrf`: no GPR write.
const NO_GPR: &[u32] = &[ const NO_GPR: &[u32] = &[
0, 4, 32, 68, // cmp, tw, cmpl, td 0, 4, 32, 68, // cmp, tw, cmpl, td
150, 151, 215, 407, 662, 918, 660, 727, 231, // stwcx./stwx/stbx/sthx/stfsx/stfdx/… 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 144, 467, 512, 598, 854, 982, 1014, 86, 470, 54, // mtcrf/mtspr/mcrxr/sync/dcb*/icbi
]; ];
// Store-*update* forms write back into `rA`. // Store-*update* forms write back into `rA`.
@@ -173,8 +189,12 @@ fn op31_dest(i: u32) -> Option<usize> {
return Some(ra(i)); return Some(ra(i));
} }
let x = xo(i); let x = xo(i);
if NO_GPR.contains(&x) { return None; } if NO_GPR.contains(&x) {
if WRITES_RA.contains(&x) { return Some(ra(i)); } return None;
}
if WRITES_RA.contains(&x) {
return Some(ra(i));
}
Some(rt(i)) Some(rt(i))
} }
@@ -204,13 +224,25 @@ pub fn analyze_with_stats(
let code_ranges: Vec<(u32, u32)> = sections let code_ranges: Vec<(u32, u32)> = sections
.iter() .iter()
.filter(|s| s.is_code()) .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(); .collect();
let read = |va: u32| -> Option<u32> { let read = |va: u32| -> Option<u32> {
let off = va.wrapping_sub(image_base) as usize; let off = va.wrapping_sub(image_base) as usize;
if off.checked_add(4)? > pe.len() { return None; } if off.checked_add(4)? > pe.len() {
Some(u32::from_be_bytes([pe[off], pe[off + 1], pe[off + 2], pe[off + 3]])) 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); 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). // Straight-line constant propagation over [window_start, bctr_pc).
let mut regs: [Option<u32>; 32] = [None; 32]; 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_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 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; let mut bound: Option<u32> = None;
// Taint: "this register holds a value derived from the byte the `lbzx` // 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 // 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; let mut pc = window_start;
while pc < bctr_pc { while pc < bctr_pc {
let Some(i) = read(pc) else { return None }; let i = read(pc)?;
match op(i) { match op(i) {
// addis rT, rA, SIMM (lis when rA == 0) // addis rT, rA, SIMM (lis when rA == 0)
15 => { 15 => {
@@ -319,14 +351,16 @@ fn recover_at(
10 | 11 => bound = Some(uimm(i)), 10 | 11 => bound = Some(uimm(i)),
31 => { 31 => {
match xo(i) { 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_dest = Some(rt(i));
lwzx_regs = Some((regs[ra(i)], regs[rb(i)])); lwzx_regs = Some((regs[ra(i)], regs[rb(i)]));
lwzx_index_tainted = from_lbzx[ra(i)] || from_lbzx[rb(i)]; lwzx_index_tainted = from_lbzx[ra(i)] || from_lbzx[rb(i)];
regs[rt(i)] = None; regs[rt(i)] = None;
from_lbzx[rt(i)] = false; 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)])); lbzx_regs = Some((regs[ra(i)], regs[rb(i)]));
regs[rt(i)] = None; regs[rt(i)] = None;
from_lbzx = [false; 32]; from_lbzx = [false; 32];
@@ -363,14 +397,23 @@ fn recover_at(
} }
// D/DS-form GPR loads write rT; the update forms also write rA. // D/DS-form GPR loads write rT; the update forms also write rA.
32 | 34 | 40 | 42 => regs[rt(i)] = None, 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). // DS-form: bits 30..31 pick ld(0) / ldu(1) / lwa(2).
58 => { 58 => {
regs[rt(i)] = None; regs[rt(i)] = None;
if i & 3 == 1 { regs[ra(i)] = None; } if i & 3 == 1 {
regs[ra(i)] = None;
}
} }
// lmw loads rT..r31. // 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. // FP loads touch no GPR — except the update forms, which write rA.
// Plain stores write no register at all (their `rT` field is the // Plain stores write no register at all (their `rT` field is the
// *source*), so a tracked base that merely gets spilled survives. // *source*), so a tracked base that merely gets spilled survives.
@@ -440,7 +483,9 @@ fn recover_at(
let word = read(byte_off & !3)?; let word = read(byte_off & !3)?;
let slot = (word >> (8 * (3 - (byte_off & 3)))) & 0xFF; let slot = (word >> (8 * (3 - (byte_off & 3)))) & 0xFF;
let t = read(table_address.wrapping_add(slot * 4))?; let t = read(table_address.wrapping_add(slot * 4))?;
if !valid(t) { break; } if !valid(t) {
break;
}
max_slot = max_slot.max(slot); max_slot = max_slot.max(slot);
targets.push(t); targets.push(t);
} }
@@ -464,11 +509,18 @@ fn recover_at(
} }
// Dense: read consecutive absolute targets until one leaves the function. // 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(); let mut targets = Vec::new();
for i in 0..cap { for i in 0..cap {
let Some(t) = read(table_address.wrapping_add(i * 4)) else { break }; let Some(t) = read(table_address.wrapping_add(i * 4)) else {
if !valid(t) { break; } break;
};
if !valid(t) {
break;
}
targets.push(t); targets.push(t);
} }
if targets.len() < 2 { if targets.len() < 2 {
@@ -554,19 +606,27 @@ mod tests {
fn one_function(start: u32, end: u32) -> FuncAnalysis { fn one_function(start: u32, end: u32) -> FuncAnalysis {
let mut functions = BTreeMap::new(); let mut functions = BTreeMap::new();
functions.insert(start, FuncInfo { functions.insert(
start, start,
end, FuncInfo {
frame_size: 0, start,
saved_gprs: 0, end,
is_leaf: false, frame_size: 0,
is_saverestore: false, saved_gprs: 0,
pdata_validated: true, is_leaf: false,
pdata_length: Some(end - start), is_saverestore: false,
pdata_prolog_length: Some(0), pdata_validated: true,
has_eh: false, pdata_length: Some(end - start),
}); pdata_prolog_length: Some(0),
FuncAnalysis { functions, save_gpr_base: None, restore_gpr_base: None, pdata_entries: Vec::new() } 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 /// Encode the words of the canonical MSVC dense-switch dispatch, ending at
@@ -575,13 +635,13 @@ mod tests {
/// / mtctr r0 / bctr / <table> /// / mtctr r0 / bctr / <table>
fn dense_switch(table_va: u32, n_cases: u32) -> Vec<u32> { fn dense_switch(table_va: u32, n_cases: u32) -> Vec<u32> {
vec![ vec![
0x2800_0000 | (10 << 16) | (n_cases - 1), // cmplwi r10, N 0x2800_0000 | (10 << 16) | (n_cases - 1), // cmplwi r10, N
0x4181_0000 | 0x20, // bc (bound check, target irrelevant) 0x4181_0000 | 0x20, // bc (bound check, target irrelevant)
0x3D80_0000 | (table_va >> 16), // lis r12, hi 0x3D80_0000 | (table_va >> 16), // lis r12, hi
0x398C_0000 | (table_va & 0xFFFF), // addi r12, r12, lo 0x398C_0000 | (table_va & 0xFFFF), // addi r12, r12, lo
0x5540_103A, // slwi r0, r10, 2 0x5540_103A, // slwi r0, r10, 2
0x7C0C_002E, // lwzx r0, r12, r0 0x7C0C_002E, // lwzx r0, r12, r0
0x7C09_03A6, // mtctr r0 0x7C09_03A6, // mtctr r0
BCTR, BCTR,
] ]
} }
@@ -600,7 +660,12 @@ mod tests {
let table_va = TEXT_VA + 8 * 4; let table_va = TEXT_VA + 8 * 4;
let mut words = dense_switch(table_va, 4); let mut words = dense_switch(table_va, 4);
// Four case bodies, all inside the function. // 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); words.extend_from_slice(&cases);
let pe = assemble(&words, 0x100); let pe = assemble(&words, 0x100);
@@ -626,7 +691,9 @@ mod tests {
let table_va = TEXT_VA + 8 * 4; let table_va = TEXT_VA + 8 * 4;
let mut words = dense_switch(table_va, 8); let mut words = dense_switch(table_va, 8);
words.extend_from_slice(&[ 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 0x8300_0000, // far outside
TEXT_VA + 0x70, TEXT_VA + 0x70,
]); ]);
@@ -659,26 +726,31 @@ mod tests {
fn recovers_sparse_two_level_switch() { fn recovers_sparse_two_level_switch() {
// cmplwi r10,5 / bc / lis+addi r11 = &map / lbzx r0,r11,r10 // 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 // / 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 map_va = TEXT_VA + 13 * 4; // 6 bytes, then padding
let table_va = TEXT_VA + 17 * 4; // 3 distinct bodies let table_va = TEXT_VA + 17 * 4; // 3 distinct bodies
let words = vec![ let words = vec![
0x2800_0000 | (10 << 16) | 5, // cmplwi r10, 5 0x2800_0000 | (10 << 16) | 5, // cmplwi r10, 5
0x4181_0000 | 0x20, // bc 0x4181_0000 | 0x20, // bc
0x3D60_0000 | (map_va >> 16), // lis r11, map@h 0x3D60_0000 | (map_va >> 16), // lis r11, map@h
0x396B_0000 | (map_va & 0xFFFF), // addi r11, r11, map@l 0x396B_0000 | (map_va & 0xFFFF), // addi r11, r11, map@l
0x7C0B_50AE, // lbzx r0, r11, r10 0x7C0B_50AE, // lbzx r0, r11, r10
0x3D80_0000 | (table_va >> 16), // lis r12, tab@h 0x3D80_0000 | (table_va >> 16), // lis r12, tab@h
0x398C_0000 | (table_va & 0xFFFF), // addi r12, r12, tab@l 0x398C_0000 | (table_va & 0xFFFF), // addi r12, r12, tab@l
0x5400_103A, // slwi r0, r0, 2 0x5400_103A, // slwi r0, r0, 2
0x7C0C_002E, // lwzx r0, r12, r0 0x7C0C_002E, // lwzx r0, r12, r0
0x7C09_03A6, // mtctr r0 0x7C09_03A6, // mtctr r0
BCTR, BCTR,
0, 0, 0,
0,
// map[0..6] = 0,1,2,2,1,0 packed big-endian, then padding // map[0..6] = 0,1,2,2,1,0 packed big-endian, then padding
0x0001_0202, 0x0100_0000, 0x0001_0202,
0, 0, 0x0100_0000,
0,
0,
// table[0..3] // 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 pe = assemble(&words, 0x100);
let sections = [text_section(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_address, Some(map_va));
assert_eq!(jt.index_map_count, Some(6)); assert_eq!(jt.index_map_count, Some(6));
assert_eq!(jt.table_slots, 3); assert_eq!(jt.table_slots, 3);
assert_eq!(jt.targets, vec![ assert_eq!(
TEXT_VA + 0x80, TEXT_VA + 0x90, TEXT_VA + 0xA0, jt.targets,
TEXT_VA + 0xA0, TEXT_VA + 0x90, TEXT_VA + 0x80, 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 /// 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() { fn unrelated_lbzx_does_not_become_an_index_map() {
let table_va = TEXT_VA + 11 * 4; let table_va = TEXT_VA + 11 * 4;
let mut words = vec![ let mut words = vec![
0x2800_0000 | (10 << 16) | 3, // cmplwi r10, 3 0x2800_0000 | (10 << 16) | 3, // cmplwi r10, 3
0x4181_0000 | 0x20, // bc 0x4181_0000 | 0x20, // bc
0x3D60_0000 | (TEXT_VA >> 16), // lis r11, text@h (a code constant) 0x3D60_0000 | (TEXT_VA >> 16), // lis r11, text@h (a code constant)
0x396B_0000 | (TEXT_VA & 0xFFFF), // addi r11, r11, text@l 0x396B_0000 | (TEXT_VA & 0xFFFF), // addi r11, r11, text@l
0x7CEB_44AE, // lbzx r7, r11, r8 — unrelated byte load 0x7CEB_44AE, // lbzx r7, r11, r8 — unrelated byte load
0x3D80_0000 | (table_va >> 16), // lis r12, tab@h 0x3D80_0000 | (table_va >> 16), // lis r12, tab@h
0x398C_0000 | (table_va & 0xFFFF), // addi r12, r12, tab@l 0x398C_0000 | (table_va & 0xFFFF), // addi r12, r12, tab@l
0x5540_103A, // slwi r0, r10, 2 — index is r10, NOT r7 0x5540_103A, // slwi r0, r10, 2 — index is r10, NOT r7
0x7C0C_002E, // lwzx r0, r12, r0 0x7C0C_002E, // lwzx r0, r12, r0
0x7C09_03A6, // mtctr r0 0x7C09_03A6, // mtctr r0
]; ];
words.push(BCTR); 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 pe = assemble(&words, 0x100);
let sections = [text_section(0x100)]; let sections = [text_section(0x100)];
@@ -731,12 +815,22 @@ mod tests {
#[test] #[test]
fn data_regions_merge_adjacent_tables() { fn data_regions_merge_adjacent_tables() {
let a = JumpTable { let a = JumpTable {
bctr_pc: 0x8200_1000, function: None, table_address: 0x8200_2000, bctr_pc: 0x8200_1000,
entry_count: 4, table_slots: 4, index_map_address: None, function: None,
index_map_count: None, bound: None, kind: "direct", 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], 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]); let merged = data_regions(&[a, b]);
assert_eq!(merged, vec![(0x8200_2000, 32)]); assert_eq!(merged, vec![(0x8200_2000, 32)]);
} }
@@ -744,9 +838,15 @@ mod tests {
#[test] #[test]
fn data_word_addresses_covers_every_slot() { fn data_word_addresses_covers_every_slot() {
let jt = JumpTable { let jt = JumpTable {
bctr_pc: 0x8200_1000, function: None, table_address: 0x8200_2000, bctr_pc: 0x8200_1000,
entry_count: 3, table_slots: 3, index_map_address: Some(0x8200_3000), function: None,
index_map_count: Some(5), bound: Some(4), kind: "indexed", 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], targets: vec![0; 5],
}; };
let words = data_word_addresses(&[jt]); let words = data_word_addresses(&[jt]);

View File

@@ -1,26 +1,46 @@
pub mod ppc; // 🔴 THREE LINTS ARE OFF FOR THIS CRATE, WITH REASONS, RATHER THAN SILENTLY.
pub mod func; //
pub mod xref; // * `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 db;
pub mod demangle;
pub mod disasm; pub mod disasm;
pub mod eh_scope;
pub mod formatter; 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 sinks;
pub mod sql_views; 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 static_init;
pub mod strings;
pub mod vtables;
pub mod xdbf; pub mod xdbf;
pub mod jumptables; pub mod xref;
pub mod rtti;
mod ordinals; mod ordinals;
pub use ordinals::resolve_ordinal; pub use db::{BranchTraceEntry, DbWriter, ExecTraceEntry, ImportCallEntry};
pub use xref::{XrefKind, Xref, XrefMap, resolve_source_label};
pub use db::{DbWriter, ExecTraceEntry, ImportCallEntry, BranchTraceEntry};
pub use disasm::{RichDisasmItem, enrich_section}; 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 std::path::Path;
use anyhow::{anyhow, Result}; use anyhow::{Result, anyhow};
use duckdb::params; use duckdb::params;
/// Parse one probe token into one or more PCs. /// 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> { 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(); return u32::from_str_radix(hex, 16).ok();
} }
token.parse::<u32>().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 = ?", WHERE c.name = ? AND dn.method_name = ?",
)?; )?;
let pcs: Vec<u32> = stmt 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()) .filter_map(|r| r.ok())
.collect(); .collect();
Ok(pcs) Ok(pcs)

View File

@@ -24,5 +24,8 @@ impl Decoded {
pub fn disasm(instr: u32, addr: u32) -> Decoded { pub fn disasm(instr: u32, addr: u32) -> Decoded {
let d = decode(instr, addr); let d = decode(instr, addr);
let t = format(&d); 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)`. /// `vftable[0]` VA → `(demangled class name, subobject offset)`.
pub fn vtable_class_names(&self) -> BTreeMap<u32, (String, u32)> { pub fn vtable_class_names(&self) -> BTreeMap<u32, (String, u32)> {
let td: BTreeMap<u32, &TypeDescriptor> = let td: BTreeMap<u32, &TypeDescriptor> = self
self.type_descriptors.iter().map(|t| (t.address, t)).collect(); .type_descriptors
.iter()
.map(|t| (t.address, t))
.collect();
let mut out = BTreeMap::new(); let mut out = BTreeMap::new();
for col in &self.locators { for col in &self.locators {
if let (Some(vt), Some(t)) = (col.vtable_address, td.get(&col.type_descriptor)) { 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 read = |va: u32| -> Option<u32> {
let off = va.wrapping_sub(image_base) as usize; let off = va.wrapping_sub(image_base) as usize;
if off.checked_add(4)? > pe.len() { return None; } if off.checked_add(4)? > pe.len() {
Some(u32::from_be_bytes([pe[off], pe[off + 1], pe[off + 2], pe[off + 3]])) 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 // 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 let ranges: Vec<(String, u32, u32)> = sections
.iter() .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(); .collect();
let range_of = |name: &str| -> Option<(u32, u32)> { 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 // 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 type_descriptors: Vec<TypeDescriptor> = Vec::new();
let mut td_addrs: BTreeSet<u32> = BTreeSet::new(); let mut td_addrs: BTreeSet<u32> = BTreeSet::new();
for (name, start, end) in &ranges { 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 s = (*start).wrapping_sub(image_base) as usize;
let e = (*end).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 bytes = &pe[s..e];
let mut i = 0usize; let mut i = 0usize;
while i + 3 < bytes.len() { 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); let name_va = start.wrapping_add(i as u32);
// The descriptor head sits 8 bytes before the name. // The descriptor head sits 8 bytes before the name.
let Some(td_va) = name_va.checked_sub(8) else { i += 1; continue }; let Some(td_va) = name_va.checked_sub(8) else {
if td_va < *start { i += 1; continue; } i += 1;
let Some(decorated) = read_cstr(bytes, i, 512) else { i += 1; continue }; continue;
};
if td_va < *start {
i += 1;
continue;
}
let Some(decorated) = read_cstr(bytes, i, 512) else {
i += 1;
continue;
};
i += decorated.len() + 1; i += decorated.len() + 1;
if td_addrs.insert(td_va) { if td_addrs.insert(td_va) {
type_descriptors.push(TypeDescriptor { type_descriptors.push(TypeDescriptor {
@@ -197,8 +229,14 @@ pub fn analyze(pe: &[u8], image_base: u32, sections: &[PeSection]) -> RttiResult
let mut va = rd_start; let mut va = rd_start;
while va + 20 <= rd_end { while va + 20 <= rd_end {
let (Some(sig), Some(off), Some(cd), Some(ptd), Some(pchd)) = ( 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), read(va),
) else { break }; 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 { if sig == 0 && td_addrs.contains(&ptd) && pchd >= rd_start && pchd < rd_end {
col_addrs.insert(va); col_addrs.insert(va);
locators.push(CompleteObjectLocator { 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. // 3. `vftable[-1]` sites: any word in initialised data whose value is a COL.
let mut vtable_to_locator: BTreeMap<u32, u32> = BTreeMap::new(); let mut vtable_to_locator: BTreeMap<u32, u32> = BTreeMap::new();
for (name, start, end) in &ranges { 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; let mut va = *start;
while va + 4 <= *end { while va + 4 <= *end {
if let Some(w) = read(va) if let Some(w) = read(va)
@@ -228,8 +268,10 @@ pub fn analyze(pe: &[u8], image_base: u32, sections: &[PeSection]) -> RttiResult
va += 4; va += 4;
} }
} }
let locator_to_vtable: BTreeMap<u32, u32> = let locator_to_vtable: BTreeMap<u32, u32> = vtable_to_locator
vtable_to_locator.iter().map(|(&vt, &col)| (col, vt)).collect(); .iter()
.map(|(&vt, &col)| (col, vt))
.collect();
for col in &mut locators { for col in &mut locators {
col.vtable_address = locator_to_vtable.get(&col.address).copied(); 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(); let chds: BTreeSet<u32> = locators.iter().map(|c| c.class_hierarchy).collect();
if let Some((rd_start, rd_end)) = rdata { if let Some((rd_start, rd_end)) = rdata {
for chd in chds { 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; // A malformed or misidentified descriptor would blow the scan up;
// real hierarchies are small. // real hierarchies are small.
if n_bases == 0 || n_bases > 64 { continue; } if n_bases == 0 || n_bases > 64 {
if p_array < rd_start || p_array >= rd_end { continue; } continue;
}
if p_array < rd_start || p_array >= rd_end {
continue;
}
for i in 0..n_bases { for i in 0..n_bases {
let Some(bcd) = read(p_array + i * 4) else { break }; let Some(bcd) = read(p_array + i * 4) else {
if bcd < rd_start || bcd >= rd_end { break; } break;
};
if bcd < rd_start || bcd >= rd_end {
break;
}
let (Some(ptd), Some(ncb), Some(md), Some(pd), Some(vd), Some(attr)) = ( let (Some(ptd), Some(ncb), Some(md), Some(pd), Some(vd), Some(attr)) = (
read(bcd), read(bcd + 4), read(bcd + 8), read(bcd),
read(bcd + 12), read(bcd + 16), read(bcd + 20), read(bcd + 4),
) else { break }; read(bcd + 8),
let Some(td) = td_by_addr.get(&ptd) else { break }; 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 { base_classes.push(BaseClass {
class_hierarchy: chd, class_hierarchy: chd,
index: i, index: i,
@@ -280,7 +340,12 @@ pub fn analyze(pe: &[u8], image_base: u32, sections: &[PeSection]) -> RttiResult
"RTTI walk complete", "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`. /// Read a NUL-terminated ASCII string starting at `off` in `bytes`.
@@ -308,14 +373,18 @@ mod tests {
vec![ vec![
PeSection { PeSection {
name: ".rdata".into(), name: ".rdata".into(),
virtual_address: RDATA_RVA, virtual_size: SEC_SIZE, virtual_address: RDATA_RVA,
raw_offset: RDATA_RVA, raw_size: SEC_SIZE, virtual_size: SEC_SIZE,
raw_offset: RDATA_RVA,
raw_size: SEC_SIZE,
flags: 0x4000_0040, flags: 0x4000_0040,
}, },
PeSection { PeSection {
name: ".data".into(), name: ".data".into(),
virtual_address: DATA_RVA, virtual_size: SEC_SIZE, virtual_address: DATA_RVA,
raw_offset: DATA_RVA, raw_size: SEC_SIZE, virtual_size: SEC_SIZE,
raw_offset: DATA_RVA,
raw_size: SEC_SIZE,
flags: 0xC000_0040, flags: 0xC000_0040,
}, },
] ]
@@ -323,7 +392,9 @@ mod tests {
struct Image(Vec<u8>); struct Image(Vec<u8>);
impl Image { 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) { fn put_u32(&mut self, va: u32, v: u32) {
let o = (va - BASE) as usize; let o = (va - BASE) as usize;
self.0[o..o + 4].copy_from_slice(&v.to_be_bytes()); 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. /// and the `vftable[-1]` word that points at the COL.
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
fn emit_class( fn emit_class(
img: &mut Image, td: u32, name: &str, img: &mut Image,
col: u32, offset: u32, chd: u32, bcd_array: u32, bcd: u32, base_name_td: Option<u32>, td: u32,
name: &str,
col: u32,
offset: u32,
chd: u32,
bcd_array: u32,
bcd: u32,
base_name_td: Option<u32>,
vtable_minus_one: u32, vtable_minus_one: u32,
) { ) {
img.put_u32(td, 0xDEAD_BEEF); // type_info vftable — value is irrelevant img.put_u32(td, 0xDEAD_BEEF); // type_info vftable — value is irrelevant
img.put_str(td + 8, name); 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 + 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 + 12, td);
img.put_u32(col + 16, chd); img.put_u32(col + 16, chd);
@@ -362,16 +440,16 @@ mod tests {
img.put_u32(bcd_array, bcd); img.put_u32(bcd_array, bcd);
img.put_u32(bcd, td); img.put_u32(bcd, td);
img.put_u32(bcd + 4, n_bases - 1); img.put_u32(bcd + 4, n_bases - 1);
img.put_u32(bcd + 8, 0); // mdisp img.put_u32(bcd + 8, 0); // mdisp
img.put_u32(bcd + 12, u32::MAX); // pdisp = -1 img.put_u32(bcd + 12, u32::MAX); // pdisp = -1
img.put_u32(bcd + 16, 0); // vdisp img.put_u32(bcd + 16, 0); // vdisp
img.put_u32(bcd + 20, 0x40); // attributes img.put_u32(bcd + 20, 0x40); // attributes
if let Some(base_td) = base_name_td { if let Some(base_td) = base_name_td {
let bcd2 = bcd + 24; let bcd2 = bcd + 24;
img.put_u32(bcd_array + 4, bcd2); img.put_u32(bcd_array + 4, bcd2);
img.put_u32(bcd2, base_td); img.put_u32(bcd2, base_td);
img.put_u32(bcd2 + 4, 0); 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 + 12, u32::MAX);
img.put_u32(bcd2 + 16, 0); img.put_u32(bcd2 + 16, 0);
img.put_u32(bcd2 + 20, 0); img.put_u32(bcd2 + 20, 0);
@@ -387,18 +465,39 @@ mod tests {
let da = BASE + DATA_RVA; let da = BASE + DATA_RVA;
// Base class Foo, then Derived : Foo. // Base class Foo, then Derived : Foo.
emit_class(&mut img, da + 0x100, ".?AVFoo@ns@@", emit_class(
rd + 0x100, 0, rd + 0x200, rd + 0x280, rd + 0x300, None, &mut img,
rd + 0x000); da + 0x100,
emit_class(&mut img, da + 0x200, ".?AVDerived@ns@@", ".?AVFoo@ns@@",
rd + 0x400, 0, rd + 0x500, rd + 0x580, rd + 0x600, Some(da + 0x100), rd + 0x100,
rd + 0x040); 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()); let r = analyze(&img.0, BASE, &sections());
assert_eq!(r.type_descriptors.len(), 2); assert_eq!(r.type_descriptors.len(), 2);
let derived = r.type_descriptors.iter() let derived = r
.find(|t| t.mangled_name.contains("Derived")).unwrap(); .type_descriptors
.iter()
.find(|t| t.mangled_name.contains("Derived"))
.unwrap();
assert_eq!(derived.demangled_name, "ns::Derived"); assert_eq!(derived.demangled_name, "ns::Derived");
assert_eq!(r.locators.len(), 2); assert_eq!(r.locators.len(), 2);
@@ -407,10 +506,15 @@ mod tests {
assert!(r.vtable_anchors().contains(&(rd + 0x004))); assert!(r.vtable_anchors().contains(&(rd + 0x004)));
let names = r.vtable_class_names(); 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. // 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) .filter(|b| b.class_hierarchy == rd + 0x500)
.collect(); .collect();
bases.sort_by_key(|b| b.index); bases.sort_by_key(|b| b.index);
@@ -425,9 +529,18 @@ mod tests {
let mut img = Image::new(); let mut img = Image::new();
let rd = BASE + RDATA_RVA; let rd = BASE + RDATA_RVA;
let da = BASE + DATA_RVA; let da = BASE + DATA_RVA;
emit_class(&mut img, da + 0x100, ".?AVMulti@@", emit_class(
rd + 0x100, 0x8, rd + 0x200, rd + 0x280, rd + 0x300, None, &mut img,
rd + 0x000); da + 0x100,
".?AVMulti@@",
rd + 0x100,
0x8,
rd + 0x200,
rd + 0x280,
rd + 0x300,
None,
rd,
);
let r = analyze(&img.0, BASE, &sections()); let r = analyze(&img.0, BASE, &sections());
let names = r.vtable_class_names(); 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 // 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. // map) must not be printed as if it decoded to something meaningful.
if item.is_data { if item.is_data {
let lbl = labels.get(&item.item.raw) let lbl = labels
.get(&item.item.raw)
.map(|s| format!(" ; -> {s}")) .map(|s| format!(" ; -> {s}"))
.unwrap_or_default(); .unwrap_or_default();
return writeln!( return writeln!(
@@ -52,12 +53,13 @@ pub fn write_instr_line<W: Write + ?Sized>(
if let Some((data_addr, kind)) = data_annotation { if let Some((data_addr, kind)) = data_annotation {
let tag = match kind { let tag = match kind {
XrefKind::DataRead => "[R]", XrefKind::DataRead => "[R]",
XrefKind::DataWrite => "[W]", XrefKind::DataWrite => "[W]",
_ => "[&]", _ => "[&]",
}; };
let sec = section_for_addr(data_addr, sections, image_base).unwrap_or("?"); 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}")) .map(|s| format!(" = {s}"))
.unwrap_or_default(); .unwrap_or_default();
if !annotated.contains("; ->") { 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 //! kind-classification CASE drifted out of agreement with `xref.rs`, and
//! is worth a one-line warning at log time. //! is worth a one-line warning at log time.
/// Every XDBF string side-by-side across the languages the title ships, so a /// 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. /// piece of UI text can be looked up once and read in all locales.
const V_XDBF_TEXT: &str = " const V_XDBF_TEXT: &str = "
@@ -66,7 +65,10 @@ pub const ALL_VIEWS: &[(&str, &str)] = &[
("v_branch_xrefs", V_BRANCH_XREFS), ("v_branch_xrefs", V_BRANCH_XREFS),
("v_call_graph", V_CALL_GRAPH), ("v_call_graph", V_CALL_GRAPH),
("v_reachability_from_entry", V_REACHABILITY_FROM_ENTRY), ("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_function_first_instruction", V_FUNCTION_FIRST_INSTRUCTION),
("v_imports_called", V_IMPORTS_CALLED), ("v_imports_called", V_IMPORTS_CALLED),
("v_xdbf_text", V_XDBF_TEXT), ("v_xdbf_text", V_XDBF_TEXT),

View File

@@ -69,10 +69,10 @@ pub struct StaticInitResult {
pub arrays: Vec<FuncPtrArray>, pub arrays: Vec<FuncPtrArray>,
} }
const OP_ADDI: u32 = 14; const OP_ADDI: u32 = 14;
const OP_ADDIS: u32 = 15; const OP_ADDIS: u32 = 15;
const OP_BCCTR: u32 = 19; const OP_BCCTR: u32 = 19;
const OP_LWZ: u32 = 32; const OP_LWZ: u32 = 32;
const OP_X_FORM: u32 = 31; const OP_X_FORM: u32 = 31;
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
@@ -95,10 +95,12 @@ pub fn analyze(
let mut drivers: Vec<StaticInitDriver> = Vec::new(); let mut drivers: Vec<StaticInitDriver> = Vec::new();
for (&fn_start, fi) in &func_analysis.functions { for (&fn_start, fi) in &func_analysis.functions {
if fi.is_saverestore { continue; } if fi.is_saverestore {
if let Some(d) = scan_function_for_driver( continue;
pe, image_base, fn_start, fi.end, &block_boundaries, }
) { if let Some(d) =
scan_function_for_driver(pe, image_base, fn_start, fi.end, &block_boundaries)
{
drivers.push(d); drivers.push(d);
} }
} }
@@ -106,7 +108,14 @@ pub fn analyze(
// Build arrays from the discovered drivers + section data. // Build arrays from the discovered drivers + section data.
let mut arrays: Vec<FuncPtrArray> = Vec::new(); let mut arrays: Vec<FuncPtrArray> = Vec::new();
for d in &drivers { 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 { arrays.push(FuncPtrArray {
address: d.array_start, address: d.array_start,
length: entries.len() as u32, length: entries.len() as u32,
@@ -140,7 +149,9 @@ fn read_array(
end: u32, end: u32,
function_starts: &BTreeSet<u32>, function_starts: &BTreeSet<u32>,
) -> Option<Vec<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 _section = sections.iter().find(|s| {
let lo = image_base + s.virtual_address; let lo = image_base + s.virtual_address;
let hi = lo + s.virtual_size; let hi = lo + s.virtual_size;
@@ -150,15 +161,21 @@ fn read_array(
let mut p = start; let mut p = start;
while p < end { while p < end {
let off = p.wrapping_sub(image_base) as usize; 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]]); let v = u32::from_be_bytes([pe[off], pe[off + 1], pe[off + 2], pe[off + 3]]);
if v != 0 { if v != 0 {
if !function_starts.contains(&v) { return None; } if !function_starts.contains(&v) {
return None;
}
entries.push(v); entries.push(v);
} }
p = p.wrapping_add(4); p = p.wrapping_add(4);
} }
if entries.is_empty() { return None; } if entries.is_empty() {
return None;
}
Some(entries) Some(entries)
} }
@@ -194,7 +211,9 @@ fn scan_function_for_driver(
reg = [None; 32]; reg = [None; 32];
} }
let off = pc.wrapping_sub(image_base) as usize; 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 instr = u32::from_be_bytes([pe[off], pe[off + 1], pe[off + 2], pe[off + 3]]);
let op = instr >> 26; let op = instr >> 26;
let rd = ((instr >> 21) & 0x1F) as usize; let rd = ((instr >> 21) & 0x1F) as usize;
@@ -207,7 +226,9 @@ fn scan_function_for_driver(
OP_ADDIS => { OP_ADDIS => {
if let Some(RegVal::Const(b)) = reg[ra] { if let Some(RegVal::Const(b)) = reg[ra] {
reg[rd] = Some(RegVal::Const(b.wrapping_add(uimm << 16))); reg[rd] = Some(RegVal::Const(b.wrapping_add(uimm << 16)));
} else { reg[rd] = None; } } else {
reg[rd] = None;
}
} }
OP_ADDI if ra != 0 => { OP_ADDI if ra != 0 => {
let prev = reg[ra]; let prev = reg[ra];
@@ -229,7 +250,9 @@ fn scan_function_for_driver(
end_init = Some(v); end_init = Some(v);
end_reg = Some(rd); end_reg = Some(rd);
} }
} else { reg[rd] = None; } } else {
reg[rd] = None;
}
} }
OP_LWZ => { OP_LWZ => {
if ra != 0 && Some(ra) == cursor_reg { if ra != 0 && Some(ra) == cursor_reg {
@@ -241,9 +264,13 @@ fn scan_function_for_driver(
let xo = (instr >> 1) & 0x3FF; let xo = (instr >> 1) & 0x3FF;
if xo == 467 { if xo == 467 {
let spr = (((instr >> 11) & 0x1F) << 5) | ((instr >> 16) & 0x1F); 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 => { OP_BCCTR => {
let xo = (instr >> 1) & 0x3FF; let xo = (instr >> 1) & 0x3FF;
@@ -254,12 +281,14 @@ fn scan_function_for_driver(
} }
18 => { 18 => {
if (instr & 1) != 0 { if (instr & 1) != 0 {
for r in 0..=12 { reg[r] = None; } for r in 0..=12 {
reg[r] = None;
}
} }
} }
16 => { 16 if (instr & 1) != 0 => {
if (instr & 1) != 0 { for r in 0..=12 {
for r in 0..=12 { reg[r] = None; } reg[r] = None;
} }
} }
_ => {} _ => {}
@@ -273,8 +302,12 @@ fn scan_function_for_driver(
} }
let cursor_init = cursor_init?; let cursor_init = cursor_init?;
let end_init = end_init?; let end_init = end_init?;
if end_init <= cursor_init { return None; } if end_init <= cursor_init {
if end_init - cursor_init > 4096 { return None; } return None;
}
if end_init - cursor_init > 4096 {
return None;
}
Some(StaticInitDriver { Some(StaticInitDriver {
driver_function: fn_start, driver_function: fn_start,
@@ -294,8 +327,10 @@ mod tests {
fn mk_section(name: &str, va: u32, size: u32) -> PeSection { fn mk_section(name: &str, va: u32, size: u32) -> PeSection {
PeSection { PeSection {
name: name.into(), name: name.into(),
virtual_address: va, virtual_size: size, virtual_address: va,
raw_offset: va, raw_size: size, virtual_size: size,
raw_offset: va,
raw_size: size,
flags: 0x4000_0040, flags: 0x4000_0040,
} }
} }
@@ -311,7 +346,11 @@ mod tests {
// Array at .rdata + 0x800: 3 function pointers. // Array at .rdata + 0x800: 3 function pointers.
let arr_va_lo = 0x800u32; 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() { for (i, p) in fns.iter().enumerate() {
write_be(&mut pe, arr_va_lo as usize + i * 4, *p); write_be(&mut pe, arr_va_lo as usize + i * 4, *p);
} }
@@ -330,32 +369,52 @@ mod tests {
// blr // blr
let driver = 0x82001000u32; let driver = 0x82001000u32;
let off = (driver - image_base) as usize; 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 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 addi_r4 = (14u32 << 26) | (4 << 21) | (4 << 16) | ((array_end as u16) as u32);
let lwz = (32u32 << 26) | (5 << 21) | (3 << 16); let lwz = (32u32 << 26) | (5 << 21) | (3 << 16);
let mtctr = (31u32 << 26) | (5 << 21) | (9 << 16) | (467 << 1); let mtctr = (31u32 << 26) | (5 << 21) | (9 << 16) | (467 << 1);
let bcctrl = (19u32 << 26) | (20 << 21) | (528 << 1) | 1; let bcctrl = (19u32 << 26) | (20 << 21) | (528 << 1) | 1;
let addi_inc = (14u32 << 26) | (3 << 21) | (3 << 16) | 4; let addi_inc = (14u32 << 26) | (3 << 21) | (3 << 16) | 4;
let blr = (19u32 << 26) | (20 << 21) | (16 << 1); 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); write_be(&mut pe, off + i * 4, *w);
} }
let mut functions: BTreeMap<u32, FuncInfo> = BTreeMap::new(); let mut functions: BTreeMap<u32, FuncInfo> = BTreeMap::new();
functions.insert(driver, FuncInfo { functions.insert(
start: driver, end: driver + 0x40, frame_size: 0, saved_gprs: 0, driver,
is_leaf: false, is_saverestore: false, FuncInfo {
pdata_validated: false, pdata_length: None, pdata_prolog_length: None, has_eh: false, 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 { 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 sections = vec![mk_section(".rdata", 0x800, 0x100)];
let mut starts = BTreeSet::new(); 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 labels: HashMap<u32, String> = HashMap::new();
let r = analyze(&pe, image_base, &sections, &fa, &starts, &labels); 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); write_be(&mut pe, (driver - image_base) as usize, blr);
let mut functions: BTreeMap<u32, FuncInfo> = BTreeMap::new(); let mut functions: BTreeMap<u32, FuncInfo> = BTreeMap::new();
functions.insert(driver, FuncInfo { functions.insert(
start: driver, end: driver + 0x40, frame_size: 0, saved_gprs: 0, driver,
is_leaf: true, is_saverestore: false, FuncInfo {
pdata_validated: false, pdata_length: None, pdata_prolog_length: None, has_eh: false, 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 { 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 sections = vec![mk_section(".rdata", 0x800, 0x100)];
let starts: BTreeSet<u32> = BTreeSet::new(); 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(); let mut out: Vec<DetectedString> = Vec::new();
for section in sections { 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; let raw_start = section.virtual_address as usize;
// Clamp to the file-backed extent — everything past `raw_size` is BSS. // Clamp to the file-backed extent — everything past `raw_size` is BSS.
let backed = section.virtual_size.min(section.raw_size) as usize; let backed = section.virtual_size.min(section.raw_size) as usize;
let raw_end = (raw_start + backed).min(pe.len()); 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 bytes = &pe[raw_start..raw_end];
let va_base = image_base + section.virtual_address; 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 elapsed_ms = started.elapsed().as_millis() as f64;
let n_ascii = out.iter().filter(|s| s.encoding == "ascii").count(); let n_ascii = out.iter().filter(|s| s.encoding == "ascii").count();
let n_utf16 = out.iter().filter(|s| s.encoding == "utf16le").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_sjis = out.iter().filter(|s| s.encoding == "shift_jis").count();
let n_utf8 = out.iter().filter(|s| s.encoding == "utf8").count(); let n_utf8 = out.iter().filter(|s| s.encoding == "utf8").count();
metrics::histogram!("analysis.phase_ms", "phase" => "strings").record(elapsed_ms); metrics::histogram!("analysis.phase_ms", "phase" => "strings").record(elapsed_ms);
tracing::info!( tracing::info!(
ascii = n_ascii, ascii = n_ascii,
@@ -104,7 +108,9 @@ fn scan_ascii(bytes: &[u8], va_base: u32, out: &mut Vec<DetectedString>) {
continue; continue;
} }
let start = i; 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; let run_len = i - start;
// Require NUL termination and minimum length. // Require NUL termination and minimum length.
if run_len >= MIN_LEN && i < bytes.len() && bytes[i] == 0 { 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. // 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. // offsets to avoid misaligned hits.
let mut i = 0; let mut i = 0;
while i + 2 <= bytes.len() { 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 lo = bytes[i];
let hi = bytes[i + 1]; let hi = bytes[i + 1];
// Restrict scan-start to printable ASCII range with a zero high byte — // 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() { while i + 2 <= bytes.len() {
let l = bytes[i]; let l = bytes[i];
let h = bytes[i + 1]; 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); codeunits.push((h as u16) << 8 | l as u16);
i += 2; i += 2;
} }
@@ -158,7 +171,9 @@ fn scan_utf16le(bytes: &[u8], va_base: u32, out: &mut Vec<DetectedString>) {
}); });
} }
// Skip past the terminator. // 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. /// ASCII.
fn is_text_like(ch: char) -> bool { fn is_text_like(ch: char) -> bool {
let o = ch as u32; let o = ch as u32;
matches!(o, 0x20..=0x7E) matches!(o, 0x20..=0x7E) || matches!(ch, '\t' | '\n' | '\r') || is_wide(ch)
|| matches!(ch, '\t' | '\n' | '\r')
|| is_wide(ch)
} }
/// A full-width character — kana, CJK punctuation, ideograph, or full-width /// A full-width character — kana, CJK punctuation, ideograph, or full-width
/// ASCII. Used to tell "real text" from a lucky byte pair. /// ASCII. Used to tell "real text" from a lucky byte pair.
fn is_wide(ch: char) -> bool { fn is_wide(ch: char) -> bool {
let o = ch as u32; 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 /// 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. /// character wedged between two wide ones.
fn has_isolated_ascii(t: &str) -> bool { fn has_isolated_ascii(t: &str) -> bool {
let chars: Vec<char> = t.chars().collect(); let chars: Vec<char> = t.chars().collect();
(1..chars.len().saturating_sub(1)).any(|k| { (1..chars.len().saturating_sub(1))
!is_wide(chars[k]) && is_wide(chars[k - 1]) && is_wide(chars[k + 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 /// 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 // obscure kanji, but hiragana/katakana (U+3040..U+30FF) essentially never
// appear by accident and are ubiquitous in genuine Japanese. // appear by accident and are ubiquitous in genuine Japanese.
let has_kana = t.chars().any(|c| ('\u{3040}'..='\u{30FF}').contains(&c)); 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) Some(t)
} else { } else {
None None
@@ -274,7 +289,9 @@ fn scan_shift_jis(bytes: &[u8], va_base: u32, out: &mut Vec<DetectedString>) {
i = end + 1; // skip NUL i = end + 1; // skip NUL
} else { } else {
i = start + 1; 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() { while i < bytes.len() {
let b = bytes[i]; let b = bytes[i];
if b < 0x80 { if b < 0x80 {
if !is_printable_ascii(b) { break; } if !is_printable_ascii(b) {
break;
}
nbytes += 1; nbytes += 1;
i += 1; i += 1;
} else if (b & 0xE0) == 0xC0 { } else if (b & 0xE0) == 0xC0 {
// 2-byte: 110xxxxx 10xxxxxx // 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; has_multibyte = true;
nbytes += 2; nbytes += 2;
i += 2; i += 2;
@@ -304,7 +325,10 @@ fn scan_utf8(bytes: &[u8], va_base: u32, out: &mut Vec<DetectedString>) {
// 3-byte: 1110xxxx 10xxxxxx 10xxxxxx // 3-byte: 1110xxxx 10xxxxxx 10xxxxxx
if i + 2 >= bytes.len() if i + 2 >= bytes.len()
|| (bytes[i + 1] & 0xC0) != 0x80 || (bytes[i + 1] & 0xC0) != 0x80
|| (bytes[i + 2] & 0xC0) != 0x80 { break; } || (bytes[i + 2] & 0xC0) != 0x80
{
break;
}
has_multibyte = true; has_multibyte = true;
nbytes += 3; nbytes += 3;
i += 3; i += 3;
@@ -314,7 +338,8 @@ fn scan_utf8(bytes: &[u8], va_base: u32, out: &mut Vec<DetectedString>) {
} }
if has_multibyte if has_multibyte
&& nbytes >= MIN_LEN && 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]) && let Ok(s) = std::str::from_utf8(&bytes[start..i])
{ {
out.push(DetectedString { out.push(DetectedString {
@@ -327,7 +352,9 @@ fn scan_utf8(bytes: &[u8], va_base: u32, out: &mut Vec<DetectedString>) {
i += 1; // skip NUL i += 1; // skip NUL
} else { } else {
i = start + 1; 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); pe[off..off + s.len()].copy_from_slice(s);
let sections = vec![mk_section(".rdata", 0x1000, 0x100)]; let sections = vec![mk_section(".rdata", 0x1000, 0x100)];
let strings = analyze(&pe, image_base, &sections); 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.len(), 1);
// Decoded to real UTF-8, not rendered as escaped bytes. // Decoded to real UTF-8, not rendered as escaped bytes.
assert_eq!(sjis[0].content, "ABCあい"); assert_eq!(sjis[0].content, "ABCあい");
@@ -420,8 +450,10 @@ mod tests {
pe[off..off + s.len()].copy_from_slice(s); pe[off..off + s.len()].copy_from_slice(s);
let sections = vec![mk_section(".rdata", 0x1000, 0x100)]; let sections = vec![mk_section(".rdata", 0x1000, 0x100)];
let strings = analyze(&pe, image_base, &sections); let strings = analyze(&pe, image_base, &sections);
assert!(strings.iter().all(|s| s.encoding != "shift_jis"), assert!(
"float table must not be reported as Japanese text"); strings.iter().all(|s| s.encoding != "shift_jis"),
"float table must not be reported as Japanese text"
);
} }
#[test] #[test]
@@ -440,7 +472,10 @@ mod tests {
pe[off..off + s.len()].copy_from_slice(s); pe[off..off + s.len()].copy_from_slice(s);
let sections = vec![mk_section(".rdata", 0x1000, 0x100)]; let sections = vec![mk_section(".rdata", 0x1000, 0x100)];
let strings = analyze(&pe, image_base, &sections); 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.len(), 1);
assert_eq!(sjis[0].content, "システム"); assert_eq!(sjis[0].content, "システム");
// Reported at the true start, one byte past the run's beginning. // Reported at the true start, one byte past the run's beginning.
@@ -471,7 +506,9 @@ mod tests {
let s = b"abcdefghij"; let s = b"abcdefghij";
pe[off..off + s.len()].copy_from_slice(s); pe[off..off + s.len()].copy_from_slice(s);
// Fill rest of section with 0xFF so the run terminates cleanly without NUL. // 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 sections = vec![mk_section(".rdata", 0x1000, 0x100)];
let strings = analyze(&pe, image_base, &sections); let strings = analyze(&pe, image_base, &sections);
assert_eq!(strings.len(), 0); assert_eq!(strings.len(), 0);

View File

@@ -65,7 +65,13 @@ pub fn analyze(
sections: &[PeSection], sections: &[PeSection],
function_starts: &std::collections::BTreeSet<u32>, function_starts: &std::collections::BTreeSet<u32>,
) -> Vec<Vtable> { ) -> 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 /// 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 let rdata_ranges: Vec<(u32, u32)> = sections
.iter() .iter()
.filter(|s| s.name == ".rdata") .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(); .collect();
// TypeDescriptors are *written at startup* (their first word is // TypeDescriptors are *written at startup* (their first word is
// `type_info`'s vftable), so MSVC emits them into writable `.data`, not // `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 let typedesc_ranges: Vec<(u32, u32)> = sections
.iter() .iter()
.filter(|s| matches!(s.name.as_str(), ".rdata" | ".data")) .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(); .collect();
let mut candidates: Vec<Vtable> = Vec::new(); 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 va_end = va_start + section.virtual_size;
let raw_start = section.virtual_address as usize; let raw_start = section.virtual_address as usize;
let raw_end = (section.virtual_address + section.virtual_size) 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 bytes = &pe[raw_start..raw_end.min(pe.len())];
let mut i = 0usize; let mut i = 0usize;
while i + 12 <= bytes.len() { while i + 12 <= bytes.len() {
// Try to start a run at this 4-aligned offset. // 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 run_len = 0usize;
let mut methods: Vec<u32> = Vec::new(); let mut methods: Vec<u32> = Vec::new();
let mut j = i; let mut j = i;
@@ -203,12 +224,19 @@ pub fn analyze_with_anchors(
let mut recovered = 0usize; let mut recovered = 0usize;
let mut newly: Vec<Vtable> = Vec::new(); let mut newly: Vec<Vtable> = Vec::new();
for &anchor in anchors { for &anchor in anchors {
if is_covered(anchor, &covered) { continue; } if is_covered(anchor, &covered) {
continue;
}
// Locate the containing .rdata/.data section. // Locate the containing .rdata/.data section.
let Some(&(va_lo, va_hi, raw_lo, raw_hi)) = let Some(&(va_lo, va_hi, raw_lo, raw_hi)) = scan_targets_va
scan_targets_va.iter().find(|&&(lo, hi, _, _)| anchor >= lo && anchor < hi) .iter()
else { continue }; .find(|&&(lo, hi, _, _)| anchor >= lo && anchor < hi)
if anchor % 4 != 0 { continue; } else {
continue;
};
if anchor % 4 != 0 {
continue;
}
let raw_hi = raw_hi.min(pe.len()); let raw_hi = raw_hi.min(pe.len());
// Read the fnptr-array run starting at the anchor. Tolerate small // Read the fnptr-array run starting at the anchor. Tolerate small
// gaps of non-function slots (null / pure-virtual / unrecognised), // 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 off = (anchor - va_lo) as usize + raw_lo;
let mut va = anchor; let mut va = anchor;
while off + 4 <= raw_hi && va < va_hi { 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]]); let val = u32::from_be_bytes([pe[off], pe[off + 1], pe[off + 2], pe[off + 3]]);
if function_starts.contains(&val) { if function_starts.contains(&val) {
methods.push(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 // Trim any trailing non-function slots (the table ends at its last
// real method). // 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(); methods.pop();
} }
if real_fns == 0 || methods.is_empty() { continue; } if real_fns == 0 || methods.is_empty() {
continue;
}
let length = methods.len() as u32; let length = methods.len() as u32;
newly.push(Vtable { newly.push(Vtable {
address: anchor, address: anchor,
@@ -266,8 +303,10 @@ pub fn analyze_with_anchors(
// contiguity-scan artifact of the same table. Keep fragments that // contiguity-scan artifact of the same table. Keep fragments that
// only partially overlap (defensive; shouldn't happen for true // only partially overlap (defensive; shouldn't happen for true
// sub-runs) so we never lose method coverage. // sub-runs) so we never lose method coverage.
let recovered_spans: Vec<(u32, u32)> = let recovered_spans: Vec<(u32, u32)> = newly
newly.iter().map(|v| (v.address, v.address + v.length * 4)).collect(); .iter()
.map(|v| (v.address, v.address + v.length * 4))
.collect();
candidates.retain(|v| { candidates.retain(|v| {
!recovered_spans !recovered_spans
.iter() .iter()
@@ -281,22 +320,37 @@ pub fn analyze_with_anchors(
// RTTI walk: for each candidate, look at vtable[-1]. // RTTI walk: for each candidate, look at vtable[-1].
let pe_image_base = image_base; let pe_image_base = image_base;
for v in &mut candidates { 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; let col_off = (v.address - pe_image_base - 4) as usize;
if col_off + 4 > pe.len() { continue; } if col_off + 4 > pe.len() {
let col_ptr = u32::from_be_bytes([pe[col_off], pe[col_off + 1], pe[col_off + 2], pe[col_off + 3]]); continue;
if col_ptr == 0 { continue; } }
if !is_in_ranges(col_ptr, &rdata_ranges) { 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. // Try to extract the TypeDescriptor mangled-name string.
if let Some((td_ptr, hierarchy_ptr)) = read_col(pe, image_base, col_ptr) 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) && let Some(class) = demangle_rtti_typename(&mangled)
{ {
v.col_address = Some(col_ptr); v.col_address = Some(col_ptr);
v.class_name = class; v.class_name = class;
v.rtti_present = true; 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. /// 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> { fn read_be_u32(pe: &[u8], image_base: u32, addr: u32) -> Option<u32> {
let off = addr.wrapping_sub(image_base) as usize; let off = addr.wrapping_sub(image_base) as usize;
if off + 4 > pe.len() { return None; } if off + 4 > pe.len() {
Some(u32::from_be_bytes([pe[off], pe[off + 1], pe[off + 2], pe[off + 3]])) 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 /// 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)> { fn read_col(pe: &[u8], image_base: u32, col: u32) -> Option<(u32, u32)> {
let td = read_be_u32(pe, image_base, col + 0x0C)?; let td = read_be_u32(pe, image_base, col + 0x0C)?;
let chd = read_be_u32(pe, image_base, col + 0x10)?; let chd = read_be_u32(pe, image_base, col + 0x10)?;
if td == 0 { return None; } if td == 0 {
return None;
}
Some((td, chd)) Some((td, chd))
} }
@@ -352,17 +415,27 @@ fn read_typedescriptor_name(
td: u32, td: u32,
rdata_ranges: &[(u32, u32)], rdata_ranges: &[(u32, u32)],
) -> Option<String> { ) -> 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 name_va = td + 0x08;
let off = name_va.wrapping_sub(image_base) as usize; 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. // Read up to 256 bytes or until NUL.
let mut end = off; let mut end = off;
while end < pe.len().min(off + 256) && pe[end] != 0 { end += 1; } while end < pe.len().min(off + 256) && pe[end] != 0 {
if end == off { return None; } end += 1;
}
if end == off {
return None;
}
let s = std::str::from_utf8(&pe[off..end]).ok()?; let s = std::str::from_utf8(&pe[off..end]).ok()?;
// Sanity: MSVC RTTI names always start with `.?A`. // 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()) Some(s.to_string())
} }
@@ -403,11 +476,17 @@ fn read_class_hierarchy(
chd: u32, chd: u32,
rdata_ranges: &[(u32, u32)], rdata_ranges: &[(u32, u32)],
) -> Option<String> { ) -> 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)?; 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)?; 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(); let mut names: Vec<String> = Vec::new();
for i in 0..num_bases { for i in 0..num_bases {
@@ -419,10 +498,7 @@ fn read_class_hierarchy(
Some(p) if is_in_ranges(p, rdata_ranges) => p, Some(p) if is_in_ranges(p, rdata_ranges) => p,
_ => return None, _ => return None,
}; };
let mangled = match read_typedescriptor_name(pe, image_base, td_ptr, rdata_ranges) { let mangled = read_typedescriptor_name(pe, image_base, td_ptr, rdata_ranges)?;
Some(s) => s,
None => return None,
};
let cls = demangle_rtti_typename(&mangled).unwrap_or(mangled); let cls = demangle_rtti_typename(&mangled).unwrap_or(mangled);
names.push(cls); names.push(cls);
} }
@@ -453,7 +529,12 @@ pub fn scan_vptr_write_constants(
let data_ranges: Vec<(u32, u32)> = sections let data_ranges: Vec<(u32, u32)> = sections
.iter() .iter()
.filter(|s| matches!(s.name.as_str(), ".rdata" | ".data")) .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(); .collect();
let in_data = |a: u32| data_ranges.iter().any(|&(s, e)| a >= s && a < e); 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 read = |addr: u32| -> Option<u32> {
let off = addr.wrapping_sub(image_base) as usize; let off = addr.wrapping_sub(image_base) as usize;
if off + 4 > pe.len() { return None; } if off + 4 > pe.len() {
Some(u32::from_be_bytes([pe[off], pe[off + 1], pe[off + 2], pe[off + 3]])) 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(); let mut anchors: std::collections::BTreeSet<u32> = std::collections::BTreeSet::new();
for (&fn_start, &(fn_end, is_saverestore)) in functions { 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 reg: [Option<u32>; 32] = [None; 32];
let mut pc = fn_start; let mut pc = fn_start;
while pc < fn_end { while pc < fn_end {
@@ -506,11 +596,13 @@ pub fn scan_vptr_write_constants(
32..=35 | 40..=43 | 48..=51 => reg[rd] = None, 32..=35 | 40..=43 | 48..=51 => reg[rd] = None,
OP_X_FORM => { OP_X_FORM => {
let xo = (instr >> 1) & 0x3FF; 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 => { 18 | 16 if (instr & 1) != 0 => {
if (instr & 1) != 0 { for r in 0..=12 {
for r in 0..=12 { reg[r] = None; } reg[r] = None;
} }
} }
_ => {} _ => {}
@@ -550,7 +642,8 @@ pub fn methods_table(
for v in vtables { for v in vtables {
for (slot, &fn_va) in v.methods.iter().enumerate() { for (slot, &fn_va) in v.methods.iter().enumerate() {
let label = labels.get(&fn_va).cloned(); 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)); .and_then(|l| demangle::demangle(l).map(|d| d.raw_demangled));
out.push((v.address, slot as u32, fn_va, label, 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() .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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@@ -603,7 +761,11 @@ mod tests {
let mut pe = vec![0u8; total]; let mut pe = vec![0u8; total];
// Vtable: 3 method PCs at .rdata start, all valid function entries. // 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() { for (i, val) in m.iter().enumerate() {
pe[rdata_va as usize + i * 4..rdata_va as usize + (i + 1) * 4] pe[rdata_va as usize + i * 4..rdata_va as usize + (i + 1) * 4]
.copy_from_slice(&val.to_be_bytes()); .copy_from_slice(&val.to_be_bytes());
@@ -628,7 +790,9 @@ mod tests {
}, },
]; ];
let mut function_starts = std::collections::BTreeSet::new(); 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); let vtables = analyze(&pe, image_base, &sections, &function_starts);
assert_eq!(vtables.len(), 1); assert_eq!(vtables.len(), 1);
@@ -680,7 +844,9 @@ mod tests {
}, },
]; ];
let mut function_starts = std::collections::BTreeSet::new(); 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 // 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 // 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]; let mut pe = vec![0u8; 0x4000];
// Lay out a tiny .rdata at 0x...A900 so the constant lands in-range. // Lay out a tiny .rdata at 0x...A900 so the constant lands in-range.
let vt_base = 0x8200A908u32; // 0x82010000 - 22264 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 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 // 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. // 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 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; let at = (ctor - image_base) as usize;
pe[at..at + 4].copy_from_slice(&addis.to_be_bytes()); pe[at..at + 4].copy_from_slice(&addis.to_be_bytes());
pe[at + 4..at + 8].copy_from_slice(&addi2.to_be_bytes()); pe[at + 4..at + 8].copy_from_slice(&addi2.to_be_bytes());
@@ -736,12 +902,20 @@ mod tests {
raw_size: 0x200, raw_size: 0x200,
flags: 0x4000_0040, 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)); funcs.insert(ctor, (ctor + 0x40, false));
let anchors = scan_vptr_write_constants( 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] #[test]
@@ -776,66 +950,14 @@ mod tests {
}, },
]; ];
let mut function_starts = std::collections::BTreeSet::new(); let mut function_starts = std::collections::BTreeSet::new();
for &pc in &m { function_starts.insert(pc); } for &pc in &m {
let vtables = analyze(&pe, image_base, &sections, &function_starts); function_starts.insert(pc);
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(","))
});
} }
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> { fn be32(b: &[u8], o: usize) -> Option<u32> {
Some(u32::from_be_bytes([ 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> { 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 { for i in 0..entry_used {
let p = entry_table + i * 18; let p = entry_table + i * 18;
let (Some(namespace), Some(id), Some(off), Some(size)) = let (Some(namespace), Some(id), Some(off), Some(size)) = (
(be16(image, p), be64(image, p + 2), be32(image, p + 10), be32(image, p + 14)) be16(image, p),
else { be64(image, p + 2),
be32(image, p + 10),
be32(image, p + 14),
) else {
continue; continue;
}; };
let body = data_start + off as usize; let body = data_start + off as usize;
@@ -212,7 +218,11 @@ pub fn analyze(image: &[u8], base: usize) -> Option<Xdbf> {
id, id,
offset: body, offset: body,
size, 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 => { NS_STRING_TABLE => {
if let Some(t) = parse_string_table(image, body, size, id as u32) { 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() { 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("XTHD") => out.title = parse_title_header(image, body),
Some("XSTC") => out.default_language = be32(image, body + 12), 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. /// `XACH`: `magic, version, size, count u16`, then 36-byte records.
fn parse_achievements(image: &[u8], body: usize, size: usize) -> Vec<Achievement> { 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); let mut out = Vec::with_capacity(count as usize);
for i in 0..count as usize { for i in 0..count as usize {
let p = body + 14 + i * 36; let p = body + 14 + i * 36;
if p + 36 > body + size { if p + 36 > body + size {
break; break;
} }
let (Some(id), Some(label_id), Some(description_id), Some(unachieved_id)) = 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)) be16(image, p),
else { be16(image, p + 2),
be16(image, p + 4),
be16(image, p + 6),
) else {
break; break;
}; };
out.push(Achievement { 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 /// 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. /// 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" { if fourcc(be32(image, body)?)? != "XSTR" {
return None; 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 p = body + 14;
let mut strings = Vec::with_capacity(count as usize); let mut strings = Vec::with_capacity(count as usize);
for _ in 0..count { 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 s = p + 4;
let e = s + len as usize; let e = s + len as usize;
if e > end || e > image.len() { if e > end || e > image.len() {
@@ -328,12 +352,12 @@ mod tests {
xach.extend(0u32.to_be_bytes()); xach.extend(0u32.to_be_bytes());
xach.extend(1u16.to_be_bytes()); // count xach.extend(1u16.to_be_bytes()); // count
let mut rec = Vec::new(); 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(100u16.to_be_bytes()); // label
rec.extend(101u16.to_be_bytes()); // description rec.extend(101u16.to_be_bytes()); // description
rec.extend(102u16.to_be_bytes()); // unachieved rec.extend(102u16.to_be_bytes()); // unachieved
rec.extend(9u32.to_be_bytes()); // image id rec.extend(9u32.to_be_bytes()); // image id
rec.extend(20u16.to_be_bytes()); // gamerscore rec.extend(20u16.to_be_bytes()); // gamerscore
rec.extend(0u16.to_be_bytes()); rec.extend(0u16.to_be_bytes());
rec.extend(0x0Cu32.to_be_bytes()); // flags rec.extend(0x0Cu32.to_be_bytes()); // flags
rec.extend([0u8; 16]); rec.extend([0u8; 16]);
@@ -358,7 +382,7 @@ mod tests {
xthd.extend(1u32.to_be_bytes()); xthd.extend(1u32.to_be_bytes());
xthd.extend(0u32.to_be_bytes()); xthd.extend(0u32.to_be_bytes());
xthd.extend(0x5351_07D4u32.to_be_bytes()); // title id 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(1u16.to_be_bytes());
xthd.extend(2u16.to_be_bytes()); xthd.extend(2u16.to_be_bytes());
xthd.extend(3u16.to_be_bytes()); xthd.extend(3u16.to_be_bytes());
@@ -418,7 +442,11 @@ mod tests {
assert_eq!(t.language, 1); assert_eq!(t.language, 1);
assert_eq!(t.strings[0], (100, "Space Combat Award".to_string())); assert_eq!(t.strings[0], (100, "Space Combat Award".to_string()));
// The achievement's label resolves through the table. // 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")); assert_eq!(name, Some("Space Combat Award"));
} }

View File

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

View File

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