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:
@@ -12,13 +12,17 @@
|
||||
use std::io::BufRead;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let path = std::env::args().nth(1).ok_or("usage: decode_table_check <table>")?;
|
||||
let path = std::env::args()
|
||||
.nth(1)
|
||||
.ok_or("usage: decode_table_check <table>")?;
|
||||
let f = std::io::BufReader::new(std::fs::File::open(path)?);
|
||||
let (mut ok, mut bad, mut invalid) = (0u32, 0u32, 0u32);
|
||||
for line in f.lines() {
|
||||
let line = line?;
|
||||
let mut it = line.split_whitespace();
|
||||
let (Some(w), Some(name)) = (it.next(), it.next()) else { continue };
|
||||
let (Some(w), Some(name)) = (it.next(), it.next()) else {
|
||||
continue;
|
||||
};
|
||||
let word = u32::from_str_radix(w.trim_start_matches("0x"), 16)?;
|
||||
let d = sylpheed_ppc::decoder::decode(word, 0x8200_0000);
|
||||
let got = format!("{:?}", d.opcode);
|
||||
|
||||
@@ -18,34 +18,68 @@ impl DecodedInstr {
|
||||
// Common field extractors (PPC bit numbering)
|
||||
|
||||
/// Primary opcode (bits 0-5)
|
||||
#[inline] pub fn op(&self) -> u32 { extract_bits(self.raw, 0, 5) }
|
||||
#[inline]
|
||||
pub fn op(&self) -> u32 {
|
||||
extract_bits(self.raw, 0, 5)
|
||||
}
|
||||
|
||||
/// rD/rS/rT (bits 6-10) - destination/source register
|
||||
#[inline] pub fn rd(&self) -> usize { extract_bits(self.raw, 6, 10) as usize }
|
||||
#[inline] pub fn rs(&self) -> usize { self.rd() }
|
||||
#[inline] pub fn rt(&self) -> usize { self.rd() }
|
||||
#[inline]
|
||||
pub fn rd(&self) -> usize {
|
||||
extract_bits(self.raw, 6, 10) as usize
|
||||
}
|
||||
#[inline]
|
||||
pub fn rs(&self) -> usize {
|
||||
self.rd()
|
||||
}
|
||||
#[inline]
|
||||
pub fn rt(&self) -> usize {
|
||||
self.rd()
|
||||
}
|
||||
|
||||
/// rA (bits 11-15)
|
||||
#[inline] pub fn ra(&self) -> usize { extract_bits(self.raw, 11, 15) as usize }
|
||||
#[inline]
|
||||
pub fn ra(&self) -> usize {
|
||||
extract_bits(self.raw, 11, 15) as usize
|
||||
}
|
||||
|
||||
/// rB (bits 16-20)
|
||||
#[inline] pub fn rb(&self) -> usize { extract_bits(self.raw, 16, 20) as usize }
|
||||
#[inline]
|
||||
pub fn rb(&self) -> usize {
|
||||
extract_bits(self.raw, 16, 20) as usize
|
||||
}
|
||||
|
||||
/// rC (bits 21-25) - for 4-operand instructions
|
||||
#[inline] pub fn rc(&self) -> usize { extract_bits(self.raw, 21, 25) as usize }
|
||||
#[inline]
|
||||
pub fn rc(&self) -> usize {
|
||||
extract_bits(self.raw, 21, 25) as usize
|
||||
}
|
||||
|
||||
/// SIMM/UIMM (bits 16-31) - signed/unsigned immediate
|
||||
#[inline] pub fn simm16(&self) -> i16 { (self.raw & 0xFFFF) as i16 }
|
||||
#[inline] pub fn uimm16(&self) -> u16 { (self.raw & 0xFFFF) as u16 }
|
||||
#[inline]
|
||||
pub fn simm16(&self) -> i16 {
|
||||
(self.raw & 0xFFFF) as i16
|
||||
}
|
||||
#[inline]
|
||||
pub fn uimm16(&self) -> u16 {
|
||||
(self.raw & 0xFFFF) as u16
|
||||
}
|
||||
|
||||
/// D-form displacement (signed, bits 16-31)
|
||||
#[inline] pub fn d(&self) -> i32 { self.simm16() as i32 }
|
||||
#[inline]
|
||||
pub fn d(&self) -> i32 {
|
||||
self.simm16() as i32
|
||||
}
|
||||
|
||||
/// DS-form displacement (signed, bits 16-29, shifted left 2)
|
||||
#[inline] pub fn ds(&self) -> i32 { (self.raw & 0xFFFC) as i16 as i32 }
|
||||
#[inline]
|
||||
pub fn ds(&self) -> i32 {
|
||||
(self.raw & 0xFFFC) as i16 as i32
|
||||
}
|
||||
|
||||
/// LI field for branch (bits 6-29, sign-extended, shifted left 2)
|
||||
#[inline] pub fn li(&self) -> i32 {
|
||||
#[inline]
|
||||
pub fn li(&self) -> i32 {
|
||||
let li = extract_bits(self.raw, 6, 29);
|
||||
// Sign-extend from 24 bits, then shift left 2
|
||||
let sign_extended = ((li as i32) << 8) >> 8;
|
||||
@@ -53,87 +87,154 @@ impl DecodedInstr {
|
||||
}
|
||||
|
||||
/// BD field for conditional branch (bits 16-29, sign-extended, shifted left 2)
|
||||
#[inline] pub fn bd(&self) -> i32 {
|
||||
#[inline]
|
||||
pub fn bd(&self) -> i32 {
|
||||
let bd = extract_bits(self.raw, 16, 29);
|
||||
let sign_extended = ((bd as i32) << 18) >> 18;
|
||||
sign_extended << 2
|
||||
}
|
||||
|
||||
/// BO field (bits 6-10) - branch options
|
||||
#[inline] pub fn bo(&self) -> u32 { extract_bits(self.raw, 6, 10) }
|
||||
#[inline]
|
||||
pub fn bo(&self) -> u32 {
|
||||
extract_bits(self.raw, 6, 10)
|
||||
}
|
||||
|
||||
/// BI field (bits 11-15) - branch condition
|
||||
#[inline] pub fn bi(&self) -> u32 { extract_bits(self.raw, 11, 15) }
|
||||
#[inline]
|
||||
pub fn bi(&self) -> u32 {
|
||||
extract_bits(self.raw, 11, 15)
|
||||
}
|
||||
|
||||
/// AA bit (bit 30) - absolute address
|
||||
#[inline] pub fn aa(&self) -> bool { (self.raw >> 1) & 1 != 0 }
|
||||
#[inline]
|
||||
pub fn aa(&self) -> bool {
|
||||
(self.raw >> 1) & 1 != 0
|
||||
}
|
||||
|
||||
/// LK bit (bit 31) - link (update LR)
|
||||
#[inline] pub fn lk(&self) -> bool { self.raw & 1 != 0 }
|
||||
#[inline]
|
||||
pub fn lk(&self) -> bool {
|
||||
self.raw & 1 != 0
|
||||
}
|
||||
|
||||
/// Rc bit (bit 31) - record CR0
|
||||
#[inline] pub fn rc_bit(&self) -> bool { self.raw & 1 != 0 }
|
||||
#[inline]
|
||||
pub fn rc_bit(&self) -> bool {
|
||||
self.raw & 1 != 0
|
||||
}
|
||||
|
||||
/// Rc for VC-form vector compare instructions — PPC bit 21 = host bit 10.
|
||||
#[inline] pub fn vc_rc_bit(&self) -> bool { (self.raw >> 10) & 1 != 0 }
|
||||
#[inline]
|
||||
pub fn vc_rc_bit(&self) -> bool {
|
||||
(self.raw >> 10) & 1 != 0
|
||||
}
|
||||
/// Rc for VX128_R-form vector compare instructions — PPC bit 27 = host bit 4.
|
||||
/// VX128_R Rc bit — PPC bit 25 (host bit 6) per canary's FormatVX128_R
|
||||
/// bitfield layout. PPCBUG-700.
|
||||
#[inline] pub fn vx128r_rc_bit(&self) -> bool { (self.raw >> 6) & 1 != 0 }
|
||||
#[inline]
|
||||
pub fn vx128r_rc_bit(&self) -> bool {
|
||||
(self.raw >> 6) & 1 != 0
|
||||
}
|
||||
|
||||
/// IMM field for VX128_4-form instructions (vrlimi128) — 5-bit blend mask at PPC bits 11-15.
|
||||
#[inline] pub fn vx128_4_imm(&self) -> u32 { extract_bits(self.raw, 11, 15) }
|
||||
#[inline]
|
||||
pub fn vx128_4_imm(&self) -> u32 {
|
||||
extract_bits(self.raw, 11, 15)
|
||||
}
|
||||
/// z field for VX128_4-form instructions (vrlimi128) — 2-bit rotation index at PPC bits 24-25.
|
||||
#[inline] pub fn vx128_4_z(&self) -> u32 { extract_bits(self.raw, 24, 25) }
|
||||
#[inline]
|
||||
pub fn vx128_4_z(&self) -> u32 {
|
||||
extract_bits(self.raw, 24, 25)
|
||||
}
|
||||
|
||||
/// OE bit (bit 21) - overflow enable
|
||||
#[inline] pub fn oe(&self) -> bool { extract_bits(self.raw, 21, 21) != 0 }
|
||||
#[inline]
|
||||
pub fn oe(&self) -> bool {
|
||||
extract_bits(self.raw, 21, 21) != 0
|
||||
}
|
||||
|
||||
/// TO field (bits 6-10) for tw/twi/td/tdi trap instructions.
|
||||
#[inline] pub fn to(&self) -> u32 { extract_bits(self.raw, 6, 10) }
|
||||
#[inline]
|
||||
pub fn to(&self) -> u32 {
|
||||
extract_bits(self.raw, 6, 10)
|
||||
}
|
||||
|
||||
/// MB, ME fields for rotate instructions
|
||||
#[inline] pub fn mb(&self) -> u32 { extract_bits(self.raw, 21, 25) }
|
||||
#[inline] pub fn me(&self) -> u32 { extract_bits(self.raw, 26, 30) }
|
||||
#[inline]
|
||||
pub fn mb(&self) -> u32 {
|
||||
extract_bits(self.raw, 21, 25)
|
||||
}
|
||||
#[inline]
|
||||
pub fn me(&self) -> u32 {
|
||||
extract_bits(self.raw, 26, 30)
|
||||
}
|
||||
|
||||
/// SH field (bits 16-20) for shift instructions
|
||||
#[inline] pub fn sh(&self) -> u32 { extract_bits(self.raw, 16, 20) }
|
||||
#[inline]
|
||||
pub fn sh(&self) -> u32 {
|
||||
extract_bits(self.raw, 16, 20)
|
||||
}
|
||||
|
||||
/// SH field for 64-bit shifts (bits 16-20 + bit 30)
|
||||
#[inline] pub fn sh64(&self) -> u32 {
|
||||
#[inline]
|
||||
pub fn sh64(&self) -> u32 {
|
||||
(extract_bits(self.raw, 30, 30) << 5) | extract_bits(self.raw, 16, 20)
|
||||
}
|
||||
|
||||
/// MB/ME field for MD-form and MDS-form instructions (6-bit field, split encoding).
|
||||
/// MB[4:0] at PPC bits 21-25; MB[5] at PPC bit 26.
|
||||
#[inline] pub fn mb_md(&self) -> u32 {
|
||||
#[inline]
|
||||
pub fn mb_md(&self) -> u32 {
|
||||
extract_bits(self.raw, 21, 25) | (extract_bits(self.raw, 26, 26) << 5)
|
||||
}
|
||||
|
||||
/// SPR field (bits 11-20, swapped halves)
|
||||
#[inline] pub fn spr(&self) -> u32 {
|
||||
#[inline]
|
||||
pub fn spr(&self) -> u32 {
|
||||
let spr_raw = extract_bits(self.raw, 11, 20);
|
||||
((spr_raw & 0x1F) << 5) | ((spr_raw >> 5) & 0x1F)
|
||||
}
|
||||
|
||||
/// CRM field (bits 12-19) for mtcrf
|
||||
#[inline] pub fn crm(&self) -> u32 { extract_bits(self.raw, 12, 19) }
|
||||
#[inline]
|
||||
pub fn crm(&self) -> u32 {
|
||||
extract_bits(self.raw, 12, 19)
|
||||
}
|
||||
|
||||
/// crfD (bits 6-8) - condition register field destination
|
||||
#[inline] pub fn crfd(&self) -> usize { extract_bits(self.raw, 6, 8) as usize }
|
||||
#[inline]
|
||||
pub fn crfd(&self) -> usize {
|
||||
extract_bits(self.raw, 6, 8) as usize
|
||||
}
|
||||
|
||||
/// crfS (bits 11-13)
|
||||
#[inline] pub fn crfs(&self) -> usize { extract_bits(self.raw, 11, 13) as usize }
|
||||
#[inline]
|
||||
pub fn crfs(&self) -> usize {
|
||||
extract_bits(self.raw, 11, 13) as usize
|
||||
}
|
||||
|
||||
/// L bit (bit 10) - 64-bit compare
|
||||
#[inline] pub fn l(&self) -> bool { extract_bits(self.raw, 10, 10) != 0 }
|
||||
#[inline]
|
||||
pub fn l(&self) -> bool {
|
||||
extract_bits(self.raw, 10, 10) != 0
|
||||
}
|
||||
|
||||
/// crbD (bits 6-10)
|
||||
#[inline] pub fn crbd(&self) -> u32 { extract_bits(self.raw, 6, 10) }
|
||||
#[inline]
|
||||
pub fn crbd(&self) -> u32 {
|
||||
extract_bits(self.raw, 6, 10)
|
||||
}
|
||||
/// crbA (bits 11-15)
|
||||
#[inline] pub fn crba(&self) -> u32 { extract_bits(self.raw, 11, 15) }
|
||||
#[inline]
|
||||
pub fn crba(&self) -> u32 {
|
||||
extract_bits(self.raw, 11, 15)
|
||||
}
|
||||
/// crbB (bits 16-20)
|
||||
#[inline] pub fn crbb(&self) -> u32 { extract_bits(self.raw, 16, 20) }
|
||||
#[inline]
|
||||
pub fn crbb(&self) -> u32 {
|
||||
extract_bits(self.raw, 16, 20)
|
||||
}
|
||||
|
||||
// VMX128 field extractors — bit positions match canary's
|
||||
// FormatVX128/VX128_2/VX128_4/VX128_5/VX128_R bitfield layout
|
||||
@@ -141,7 +242,8 @@ impl DecodedInstr {
|
||||
|
||||
/// VA128 = VA128l(5) | VA128h(1) << 5 | VA128H(1) << 6.
|
||||
/// Canonical 7-bit register selector: PPC 11-15 (low), PPC 26 (mid), PPC 21 (high).
|
||||
#[inline] pub fn va128(&self) -> usize {
|
||||
#[inline]
|
||||
pub fn va128(&self) -> usize {
|
||||
(extract_bits(self.raw, 11, 15)
|
||||
| (extract_bits(self.raw, 26, 26) << 5)
|
||||
| (extract_bits(self.raw, 21, 21) << 6)) as usize
|
||||
@@ -149,35 +251,48 @@ impl DecodedInstr {
|
||||
|
||||
/// VB128 = VB128l(5) | VB128h(2) << 5. Canary's VB128h is a 2-bit
|
||||
/// contiguous field at PPC 30-31 (host bits 0-1).
|
||||
#[inline] pub fn vb128(&self) -> usize {
|
||||
(extract_bits(self.raw, 16, 20)
|
||||
| (extract_bits(self.raw, 30, 31) << 5)) as usize
|
||||
#[inline]
|
||||
pub fn vb128(&self) -> usize {
|
||||
(extract_bits(self.raw, 16, 20) | (extract_bits(self.raw, 30, 31) << 5)) as usize
|
||||
}
|
||||
|
||||
/// VD128 = VD128l(5) | VD128h(2) << 5. Canary's VD128h is a 2-bit
|
||||
/// contiguous field at PPC 28-29 (host bits 2-3).
|
||||
#[inline] pub fn vd128(&self) -> usize {
|
||||
(extract_bits(self.raw, 6, 10)
|
||||
| (extract_bits(self.raw, 28, 29) << 5)) as usize
|
||||
#[inline]
|
||||
pub fn vd128(&self) -> usize {
|
||||
(extract_bits(self.raw, 6, 10) | (extract_bits(self.raw, 28, 29) << 5)) as usize
|
||||
}
|
||||
|
||||
/// VS128 - same encoding as VD128
|
||||
#[inline] pub fn vs128(&self) -> usize { self.vd128() }
|
||||
#[inline]
|
||||
pub fn vs128(&self) -> usize {
|
||||
self.vd128()
|
||||
}
|
||||
|
||||
/// VC register for VX128_2-form instructions (vperm128) — 3-bit at PPC bits 23-25.
|
||||
#[inline] pub fn vc128_2(&self) -> usize { extract_bits(self.raw, 23, 25) as usize }
|
||||
#[inline]
|
||||
pub fn vc128_2(&self) -> usize {
|
||||
extract_bits(self.raw, 23, 25) as usize
|
||||
}
|
||||
|
||||
/// NB field (bits 16-20) for lswi/stswi
|
||||
#[inline] pub fn nb(&self) -> u32 { extract_bits(self.raw, 16, 20) }
|
||||
#[inline]
|
||||
pub fn nb(&self) -> u32 {
|
||||
extract_bits(self.raw, 16, 20)
|
||||
}
|
||||
|
||||
/// PERM field for VX128_P-form instructions (vpermwi128) — 8-bit split encoding.
|
||||
/// PERMl (5 bits) at PPC bits 11-15; PERMh (3 bits) at PPC bits 23-25.
|
||||
#[inline] pub fn vx128_p_perm(&self) -> u32 {
|
||||
#[inline]
|
||||
pub fn vx128_p_perm(&self) -> u32 {
|
||||
extract_bits(self.raw, 11, 15) | (extract_bits(self.raw, 23, 25) << 5)
|
||||
}
|
||||
|
||||
/// SH field for VX128_5-form instructions (vsldoi128) — 4-bit shift at PPC bits 22-25.
|
||||
#[inline] pub fn vx128_5_sh(&self) -> u32 { extract_bits(self.raw, 22, 25) }
|
||||
#[inline]
|
||||
pub fn vx128_5_sh(&self) -> u32 {
|
||||
extract_bits(self.raw, 22, 25)
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract the 5-bit `UIMM` (`VX128_3`) / `IMM` (`VX128_4`) field. Canary
|
||||
@@ -1057,8 +1172,15 @@ mod tests {
|
||||
/// vd_hi is 2 bits (PPC 28-29). Same shape for vb128 (vb_lo at PPC 16-20,
|
||||
/// vb_hi 2 bits at PPC 30-31). va128 = va_lo | (va_h26<<5) | (va_h21<<6)
|
||||
/// per canary's 7-bit VA selector.
|
||||
fn vmx128_test_word(vd_lo: u32, vd_hi: u32, va_lo: u32, va_h26: u32, va_h21: u32,
|
||||
vb_lo: u32, vb_hi: u32) -> u32 {
|
||||
fn vmx128_test_word(
|
||||
vd_lo: u32,
|
||||
vd_hi: u32,
|
||||
va_lo: u32,
|
||||
va_h26: u32,
|
||||
va_h21: u32,
|
||||
vb_lo: u32,
|
||||
vb_hi: u32,
|
||||
) -> u32 {
|
||||
// PPC bit i -> host bit (31-i).
|
||||
(vd_lo << (31 - 10)) // VD128l: PPC 6-10 = host 21-25
|
||||
| (vd_hi << (31 - 29)) // VD128h: PPC 28-29 = host 2-3 (LSB at host 2)
|
||||
@@ -1066,15 +1188,19 @@ mod tests {
|
||||
| (va_h26 << (31 - 26)) // VA128h: PPC 26 = host 5
|
||||
| (va_h21 << (31 - 21)) // VA128H: PPC 21 = host 10
|
||||
| (vb_lo << (31 - 20)) // VB128l: PPC 16-20 = host 11-15
|
||||
| (vb_hi << (31 - 31)) // VB128h: PPC 30-31 = host 0-1 (LSB at host 0)
|
||||
| vb_hi // VB128h: PPC 30-31 = host 0-1 (LSB at host 0)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vmx128_vd128_low_5_bits_only() {
|
||||
// vd_lo = 0..31, vd_hi = 0 → vd128 = vd_lo
|
||||
for r in 0..32u32 {
|
||||
let raw = (r as u32) << (31 - 10);
|
||||
let d = DecodedInstr { opcode: PpcOpcode::Invalid, raw, addr: 0 };
|
||||
let raw = r << (31 - 10);
|
||||
let d = DecodedInstr {
|
||||
opcode: PpcOpcode::Invalid,
|
||||
raw,
|
||||
addr: 0,
|
||||
};
|
||||
assert_eq!(d.vd128(), r as usize, "vd_lo={r}");
|
||||
}
|
||||
}
|
||||
@@ -1082,26 +1208,36 @@ mod tests {
|
||||
#[test]
|
||||
fn vmx128_vd128_high_low_bit_adds_32() {
|
||||
// vd_lo = 0, VD128h = 0b01 (LSB only at host bit 2 = PPC 29) → vd128 = 32
|
||||
let raw = (1u32 << (31 - 29));
|
||||
let d = DecodedInstr { opcode: PpcOpcode::Invalid, raw, addr: 0 };
|
||||
let raw = 1u32 << (31 - 29);
|
||||
let d = DecodedInstr {
|
||||
opcode: PpcOpcode::Invalid,
|
||||
raw,
|
||||
addr: 0,
|
||||
};
|
||||
assert_eq!(d.vd128(), 32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vmx128_vd128_high_high_bit_adds_64() {
|
||||
// vd_lo = 0, VD128h = 0b10 (MSB only at host bit 3 = PPC 28) → vd128 = 64
|
||||
let raw = (1u32 << (31 - 28));
|
||||
let d = DecodedInstr { opcode: PpcOpcode::Invalid, raw, addr: 0 };
|
||||
let raw = 1u32 << (31 - 28);
|
||||
let d = DecodedInstr {
|
||||
opcode: PpcOpcode::Invalid,
|
||||
raw,
|
||||
addr: 0,
|
||||
};
|
||||
assert_eq!(d.vd128(), 64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vmx128_vd128_full_127() {
|
||||
// vd_lo = 31, VD128h = 0b11 → vd128 = 127
|
||||
let raw = (31u32 << (31 - 10))
|
||||
| (1u32 << (31 - 28))
|
||||
| (1u32 << (31 - 29));
|
||||
let d = DecodedInstr { opcode: PpcOpcode::Invalid, raw, addr: 0 };
|
||||
let raw = (31u32 << (31 - 10)) | (1u32 << (31 - 28)) | (1u32 << (31 - 29));
|
||||
let d = DecodedInstr {
|
||||
opcode: PpcOpcode::Invalid,
|
||||
raw,
|
||||
addr: 0,
|
||||
};
|
||||
assert_eq!(d.vd128(), 127);
|
||||
}
|
||||
|
||||
@@ -1109,11 +1245,19 @@ mod tests {
|
||||
fn vmx128_va128_canary_layout() {
|
||||
// va_lo = 7 at PPC 11-15, VA128h = 1 at PPC 26 → va128 = 7 | 32 = 39
|
||||
let raw = (7u32 << (31 - 15)) | (1u32 << (31 - 26));
|
||||
let d = DecodedInstr { opcode: PpcOpcode::Invalid, raw, addr: 0 };
|
||||
let d = DecodedInstr {
|
||||
opcode: PpcOpcode::Invalid,
|
||||
raw,
|
||||
addr: 0,
|
||||
};
|
||||
assert_eq!(d.va128(), 39);
|
||||
// VA128H = 1 at PPC 21 → va128 += 64 = 103
|
||||
let raw = raw | (1u32 << (31 - 21));
|
||||
let d = DecodedInstr { opcode: PpcOpcode::Invalid, raw, addr: 0 };
|
||||
let d = DecodedInstr {
|
||||
opcode: PpcOpcode::Invalid,
|
||||
raw,
|
||||
addr: 0,
|
||||
};
|
||||
assert_eq!(d.va128(), 7 | 32 | 64);
|
||||
}
|
||||
|
||||
@@ -1122,10 +1266,18 @@ mod tests {
|
||||
// vb_lo = 5 at PPC 16-20. VB128h = 0b01 (LSB at PPC 31 = host 0) → +32.
|
||||
// VB128h = 0b11 → +96.
|
||||
let raw = (5u32 << (31 - 20)) | (1u32 << (31 - 31));
|
||||
let d = DecodedInstr { opcode: PpcOpcode::Invalid, raw, addr: 0 };
|
||||
let d = DecodedInstr {
|
||||
opcode: PpcOpcode::Invalid,
|
||||
raw,
|
||||
addr: 0,
|
||||
};
|
||||
assert_eq!(d.vb128(), 5 | 32);
|
||||
let raw = raw | (1u32 << (31 - 30));
|
||||
let d = DecodedInstr { opcode: PpcOpcode::Invalid, raw, addr: 0 };
|
||||
let d = DecodedInstr {
|
||||
opcode: PpcOpcode::Invalid,
|
||||
raw,
|
||||
addr: 0,
|
||||
};
|
||||
assert_eq!(d.vb128(), 5 | 32 | 64);
|
||||
}
|
||||
|
||||
@@ -1135,9 +1287,12 @@ mod tests {
|
||||
for r in [0u32, 31, 32, 64, 96, 127] {
|
||||
let lo = r & 0x1F;
|
||||
let hi = (r >> 5) & 0x3;
|
||||
let raw = (lo << (31 - 10))
|
||||
| (hi << (31 - 29));
|
||||
let d = DecodedInstr { opcode: PpcOpcode::Invalid, raw, addr: 0 };
|
||||
let raw = (lo << (31 - 10)) | (hi << (31 - 29));
|
||||
let d = DecodedInstr {
|
||||
opcode: PpcOpcode::Invalid,
|
||||
raw,
|
||||
addr: 0,
|
||||
};
|
||||
assert_eq!(d.vd128(), r as usize, "vd128 mismatch for r={r}");
|
||||
assert_eq!(d.vs128(), r as usize, "vs128 mismatch for r={r}");
|
||||
assert_eq!(d.vd128(), d.vs128());
|
||||
@@ -1150,7 +1305,11 @@ mod tests {
|
||||
// Keep the helper validated against the real accessor.
|
||||
// vd_lo=5, vd_hi=0b11 → vd128 = 5 | 96 = 101
|
||||
let raw = vmx128_test_word(5, 3, 0, 0, 0, 0, 0);
|
||||
let d = DecodedInstr { opcode: PpcOpcode::Invalid, raw, addr: 0 };
|
||||
let d = DecodedInstr {
|
||||
opcode: PpcOpcode::Invalid,
|
||||
raw,
|
||||
addr: 0,
|
||||
};
|
||||
assert_eq!(d.vd128(), 5 | 32 | 64);
|
||||
}
|
||||
|
||||
@@ -1160,21 +1319,37 @@ mod tests {
|
||||
// Host bit 9 = 1 (PPC bit 22), host bits 6-8 = 0.
|
||||
// So raw bit 9 set = raw |= 1 << 9 = 0x200
|
||||
let raw = 0x200u32; // host bit 9 set only
|
||||
let d = DecodedInstr { opcode: PpcOpcode::Invalid, raw, addr: 0 };
|
||||
let d = DecodedInstr {
|
||||
opcode: PpcOpcode::Invalid,
|
||||
raw,
|
||||
addr: 0,
|
||||
};
|
||||
assert_eq!(d.vx128_5_sh(), 8, "SH=8: MSB at PPC bit 22");
|
||||
|
||||
// SH=1 (binary 0001): host bit 6 set = raw |= 1 << 6 = 0x40
|
||||
let raw = 0x40u32;
|
||||
let d = DecodedInstr { opcode: PpcOpcode::Invalid, raw, addr: 0 };
|
||||
let d = DecodedInstr {
|
||||
opcode: PpcOpcode::Invalid,
|
||||
raw,
|
||||
addr: 0,
|
||||
};
|
||||
assert_eq!(d.vx128_5_sh(), 1, "SH=1: LSB at PPC bit 25");
|
||||
|
||||
// SH=15 (binary 1111): host bits 6-9 all set = raw |= 0xF << 6 = 0x3C0
|
||||
let raw = 0x3C0u32;
|
||||
let d = DecodedInstr { opcode: PpcOpcode::Invalid, raw, addr: 0 };
|
||||
let d = DecodedInstr {
|
||||
opcode: PpcOpcode::Invalid,
|
||||
raw,
|
||||
addr: 0,
|
||||
};
|
||||
assert_eq!(d.vx128_5_sh(), 15, "SH=15: all 4 bits set");
|
||||
|
||||
// SH=0: raw=0
|
||||
let d = DecodedInstr { opcode: PpcOpcode::Invalid, raw: 0, addr: 0 };
|
||||
let d = DecodedInstr {
|
||||
opcode: PpcOpcode::Invalid,
|
||||
raw: 0,
|
||||
addr: 0,
|
||||
};
|
||||
assert_eq!(d.vx128_5_sh(), 0, "SH=0");
|
||||
}
|
||||
|
||||
@@ -1182,23 +1357,39 @@ mod tests {
|
||||
fn vx128_4_accessors_correct_bit_positions() {
|
||||
// z=3 (binary 11) at PPC bits 24-25 = host bits 6-7
|
||||
let raw = 0b11u32 << 6;
|
||||
let d = DecodedInstr { opcode: PpcOpcode::Invalid, raw, addr: 0 };
|
||||
let d = DecodedInstr {
|
||||
opcode: PpcOpcode::Invalid,
|
||||
raw,
|
||||
addr: 0,
|
||||
};
|
||||
assert_eq!(d.vx128_4_z(), 3, "z=3 from host bits 6-7");
|
||||
|
||||
// IMM=0x15 (binary 10101) at PPC bits 11-15 = host bits 16-20
|
||||
let raw2 = 0x15u32 << 16;
|
||||
let d2 = DecodedInstr { opcode: PpcOpcode::Invalid, raw: raw2, addr: 0 };
|
||||
let d2 = DecodedInstr {
|
||||
opcode: PpcOpcode::Invalid,
|
||||
raw: raw2,
|
||||
addr: 0,
|
||||
};
|
||||
assert_eq!(d2.vx128_4_imm(), 0x15, "IMM=0x15 from host bits 16-20");
|
||||
|
||||
// Combined: z=1, IMM=0xA — fields must not bleed into each other
|
||||
let raw3 = (0x1u32 << 6) | (0xAu32 << 16);
|
||||
let d3 = DecodedInstr { opcode: PpcOpcode::Invalid, raw: raw3, addr: 0 };
|
||||
let d3 = DecodedInstr {
|
||||
opcode: PpcOpcode::Invalid,
|
||||
raw: raw3,
|
||||
addr: 0,
|
||||
};
|
||||
assert_eq!(d3.vx128_4_z(), 1, "z=1 combined");
|
||||
assert_eq!(d3.vx128_4_imm(), 0xA, "IMM=0xA combined");
|
||||
|
||||
// z=2, IMM=0xF — max 4-bit blend mask, exercises the full lower nibble
|
||||
let raw4 = (0b10u32 << 6) | (0xFu32 << 16);
|
||||
let d4 = DecodedInstr { opcode: PpcOpcode::Invalid, raw: raw4, addr: 0 };
|
||||
let d4 = DecodedInstr {
|
||||
opcode: PpcOpcode::Invalid,
|
||||
raw: raw4,
|
||||
addr: 0,
|
||||
};
|
||||
assert_eq!(d4.vx128_4_z(), 2, "z=2 from binary 10");
|
||||
assert_eq!(d4.vx128_4_imm(), 0xF, "IMM=0xF all-ones nibble");
|
||||
}
|
||||
@@ -1208,16 +1399,32 @@ mod tests {
|
||||
// VC=5 (binary 101) at PPC bits 23-25 = host bits 6-8
|
||||
// extract_bits(raw, 23, 25) = (raw >> (31-25)) & 0x7 = (raw >> 6) & 0x7
|
||||
let raw = 5u32 << 6; // host bits 6-8 = 5
|
||||
let d = DecodedInstr { opcode: PpcOpcode::Invalid, raw, addr: 0 };
|
||||
let d = DecodedInstr {
|
||||
opcode: PpcOpcode::Invalid,
|
||||
raw,
|
||||
addr: 0,
|
||||
};
|
||||
assert_eq!(d.vc128_2(), 5);
|
||||
|
||||
let d0 = DecodedInstr { opcode: PpcOpcode::Invalid, raw: 0, addr: 0 };
|
||||
let d0 = DecodedInstr {
|
||||
opcode: PpcOpcode::Invalid,
|
||||
raw: 0,
|
||||
addr: 0,
|
||||
};
|
||||
assert_eq!(d0.vc128_2(), 0);
|
||||
|
||||
let d7 = DecodedInstr { opcode: PpcOpcode::Invalid, raw: 7u32 << 6, addr: 0 };
|
||||
let d7 = DecodedInstr {
|
||||
opcode: PpcOpcode::Invalid,
|
||||
raw: 7u32 << 6,
|
||||
addr: 0,
|
||||
};
|
||||
assert_eq!(d7.vc128_2(), 7);
|
||||
|
||||
let d1 = DecodedInstr { opcode: PpcOpcode::Invalid, raw: 1u32 << 6, addr: 0 };
|
||||
let d1 = DecodedInstr {
|
||||
opcode: PpcOpcode::Invalid,
|
||||
raw: 1u32 << 6,
|
||||
addr: 0,
|
||||
};
|
||||
assert_eq!(d1.vc128_2(), 1);
|
||||
}
|
||||
|
||||
@@ -1225,21 +1432,37 @@ mod tests {
|
||||
fn vx128_p_perm_assembles_correctly() {
|
||||
// PERMl=0x1F (all 5 bits set) at host bits 16-20: raw = 0x1F << 16
|
||||
let raw = 0x1Fu32 << 16;
|
||||
let d = DecodedInstr { opcode: PpcOpcode::Invalid, raw, addr: 0 };
|
||||
let d = DecodedInstr {
|
||||
opcode: PpcOpcode::Invalid,
|
||||
raw,
|
||||
addr: 0,
|
||||
};
|
||||
assert_eq!(d.vx128_p_perm(), 0x1F, "PERMl only");
|
||||
|
||||
// PERMh=0x7 (all 3 bits set) at host bits 6-8: raw = 0x7 << 6 = 0x1C0
|
||||
let raw = 0x7u32 << 6;
|
||||
let d = DecodedInstr { opcode: PpcOpcode::Invalid, raw, addr: 0 };
|
||||
let d = DecodedInstr {
|
||||
opcode: PpcOpcode::Invalid,
|
||||
raw,
|
||||
addr: 0,
|
||||
};
|
||||
assert_eq!(d.vx128_p_perm(), 0x7 << 5, "PERMh only: bits 5-7");
|
||||
|
||||
// PERMl=0xA, PERMh=0x5: raw = (0xA << 16) | (0x5 << 6)
|
||||
let raw = (0xAu32 << 16) | (0x5u32 << 6);
|
||||
let d = DecodedInstr { opcode: PpcOpcode::Invalid, raw, addr: 0 };
|
||||
let d = DecodedInstr {
|
||||
opcode: PpcOpcode::Invalid,
|
||||
raw,
|
||||
addr: 0,
|
||||
};
|
||||
assert_eq!(d.vx128_p_perm(), 0xA | (0x5 << 5));
|
||||
|
||||
// PERMl and PERMh bits must not bleed into each other
|
||||
let d = DecodedInstr { opcode: PpcOpcode::Invalid, raw: 0, addr: 0 };
|
||||
let d = DecodedInstr {
|
||||
opcode: PpcOpcode::Invalid,
|
||||
raw: 0,
|
||||
addr: 0,
|
||||
};
|
||||
assert_eq!(d.vx128_p_perm(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,131 +5,498 @@
|
||||
#[allow(non_camel_case_types)]
|
||||
pub enum PpcOpcode {
|
||||
// ALU
|
||||
addcx, addex, addi, addic, addicx, addis, addmex, addx, addzex,
|
||||
andcx, andisx, andix, andx,
|
||||
addcx,
|
||||
addex,
|
||||
addi,
|
||||
addic,
|
||||
addicx,
|
||||
addis,
|
||||
addmex,
|
||||
addx,
|
||||
addzex,
|
||||
andcx,
|
||||
andisx,
|
||||
andix,
|
||||
andx,
|
||||
// Branch
|
||||
bcctrx, bclrx, bcx, bx,
|
||||
bcctrx,
|
||||
bclrx,
|
||||
bcx,
|
||||
bx,
|
||||
// Compare
|
||||
cmp, cmpi, cmpl, cmpli,
|
||||
cmp,
|
||||
cmpi,
|
||||
cmpl,
|
||||
cmpli,
|
||||
// Count leading zeros
|
||||
cntlzdx, cntlzwx,
|
||||
cntlzdx,
|
||||
cntlzwx,
|
||||
// Condition register
|
||||
crand, crandc, creqv, crnand, crnor, cror, crorc, crxor,
|
||||
crand,
|
||||
crandc,
|
||||
creqv,
|
||||
crnand,
|
||||
crnor,
|
||||
cror,
|
||||
crorc,
|
||||
crxor,
|
||||
// Data cache
|
||||
dcbf, dcbi, dcbst, dcbt, dcbtst, dcbz, dcbz128,
|
||||
dcbf,
|
||||
dcbi,
|
||||
dcbst,
|
||||
dcbt,
|
||||
dcbtst,
|
||||
dcbz,
|
||||
dcbz128,
|
||||
// Division
|
||||
divdux, divdx, divwux, divwx,
|
||||
divdux,
|
||||
divdx,
|
||||
divwux,
|
||||
divwx,
|
||||
// Sync/barrier
|
||||
eieio,
|
||||
// Logical
|
||||
eqvx, extsbx, extshx, extswx,
|
||||
eqvx,
|
||||
extsbx,
|
||||
extshx,
|
||||
extswx,
|
||||
// FPU
|
||||
fabsx, faddsx, faddx, fcfidx, fcmpo, fcmpu, fctidx, fctidzx, fctiwx, fctiwzx,
|
||||
fdivsx, fdivx, fmaddsx, fmaddx, fmrx, fmsubsx, fmsubx, fmulsx, fmulx,
|
||||
fnabsx, fnegx, fnmaddsx, fnmaddx, fnmsubsx, fnmsubx, fresx, frspx, frsqrtex,
|
||||
fselx, fsqrtsx, fsqrtx, fsubsx, fsubx,
|
||||
fabsx,
|
||||
faddsx,
|
||||
faddx,
|
||||
fcfidx,
|
||||
fcmpo,
|
||||
fcmpu,
|
||||
fctidx,
|
||||
fctidzx,
|
||||
fctiwx,
|
||||
fctiwzx,
|
||||
fdivsx,
|
||||
fdivx,
|
||||
fmaddsx,
|
||||
fmaddx,
|
||||
fmrx,
|
||||
fmsubsx,
|
||||
fmsubx,
|
||||
fmulsx,
|
||||
fmulx,
|
||||
fnabsx,
|
||||
fnegx,
|
||||
fnmaddsx,
|
||||
fnmaddx,
|
||||
fnmsubsx,
|
||||
fnmsubx,
|
||||
fresx,
|
||||
frspx,
|
||||
frsqrtex,
|
||||
fselx,
|
||||
fsqrtsx,
|
||||
fsqrtx,
|
||||
fsubsx,
|
||||
fsubx,
|
||||
// Instruction cache
|
||||
icbi, isync,
|
||||
icbi,
|
||||
isync,
|
||||
// Load byte
|
||||
lbz, lbzu, lbzux, lbzx,
|
||||
lbz,
|
||||
lbzu,
|
||||
lbzux,
|
||||
lbzx,
|
||||
// Load doubleword
|
||||
ld, ldarx, ldbrx, ldu, ldux, ldx,
|
||||
ld,
|
||||
ldarx,
|
||||
ldbrx,
|
||||
ldu,
|
||||
ldux,
|
||||
ldx,
|
||||
// Load float
|
||||
lfd, lfdu, lfdux, lfdx, lfs, lfsu, lfsux, lfsx,
|
||||
lfd,
|
||||
lfdu,
|
||||
lfdux,
|
||||
lfdx,
|
||||
lfs,
|
||||
lfsu,
|
||||
lfsux,
|
||||
lfsx,
|
||||
// Load halfword
|
||||
lha, lhau, lhaux, lhax, lhbrx, lhz, lhzu, lhzux, lhzx,
|
||||
lha,
|
||||
lhau,
|
||||
lhaux,
|
||||
lhax,
|
||||
lhbrx,
|
||||
lhz,
|
||||
lhzu,
|
||||
lhzux,
|
||||
lhzx,
|
||||
// Load multiple/string
|
||||
lmw, lswi, lswx,
|
||||
lmw,
|
||||
lswi,
|
||||
lswx,
|
||||
// Load vector
|
||||
lvebx, lvehx, lvewx, lvewx128, lvlx, lvlx128, lvlxl, lvlxl128,
|
||||
lvrx, lvrx128, lvrxl, lvrxl128,
|
||||
lvsl, lvsl128, lvsr, lvsr128,
|
||||
lvx, lvx128, lvxl, lvxl128,
|
||||
lvebx,
|
||||
lvehx,
|
||||
lvewx,
|
||||
lvewx128,
|
||||
lvlx,
|
||||
lvlx128,
|
||||
lvlxl,
|
||||
lvlxl128,
|
||||
lvrx,
|
||||
lvrx128,
|
||||
lvrxl,
|
||||
lvrxl128,
|
||||
lvsl,
|
||||
lvsl128,
|
||||
lvsr,
|
||||
lvsr128,
|
||||
lvx,
|
||||
lvx128,
|
||||
lvxl,
|
||||
lvxl128,
|
||||
// Load word
|
||||
lwa, lwarx, lwaux, lwax, lwbrx, lwz, lwzu, lwzux, lwzx,
|
||||
lwa,
|
||||
lwarx,
|
||||
lwaux,
|
||||
lwax,
|
||||
lwbrx,
|
||||
lwz,
|
||||
lwzu,
|
||||
lwzux,
|
||||
lwzx,
|
||||
// Move CR
|
||||
mcrf, mcrfs, mcrxr,
|
||||
mcrf,
|
||||
mcrfs,
|
||||
mcrxr,
|
||||
// Move from special
|
||||
mfcr, mffsx, mfmsr, mfspr, mftb, mfvscr,
|
||||
mfcr,
|
||||
mffsx,
|
||||
mfmsr,
|
||||
mfspr,
|
||||
mftb,
|
||||
mfvscr,
|
||||
// Move to special
|
||||
mtcrf, mtfsb0x, mtfsb1x, mtfsfix, mtfsfx, mtmsr, mtmsrd, mtspr, mtvscr,
|
||||
mtcrf,
|
||||
mtfsb0x,
|
||||
mtfsb1x,
|
||||
mtfsfix,
|
||||
mtfsfx,
|
||||
mtmsr,
|
||||
mtmsrd,
|
||||
mtspr,
|
||||
mtvscr,
|
||||
// Multiply
|
||||
mulhdux, mulhdx, mulhwux, mulhwx, mulldx, mulli, mullwx,
|
||||
mulhdux,
|
||||
mulhdx,
|
||||
mulhwux,
|
||||
mulhwx,
|
||||
mulldx,
|
||||
mulli,
|
||||
mullwx,
|
||||
// Logical
|
||||
nandx, negx, norx, orcx, ori, oris, orx,
|
||||
nandx,
|
||||
negx,
|
||||
norx,
|
||||
orcx,
|
||||
ori,
|
||||
oris,
|
||||
orx,
|
||||
// Rotate
|
||||
rldclx, rldcrx, rldiclx, rldicrx, rldicx, rldimix, rlwimix, rlwinmx, rlwnmx,
|
||||
rldclx,
|
||||
rldcrx,
|
||||
rldiclx,
|
||||
rldicrx,
|
||||
rldicx,
|
||||
rldimix,
|
||||
rlwimix,
|
||||
rlwinmx,
|
||||
rlwnmx,
|
||||
// System call
|
||||
sc,
|
||||
// Shift
|
||||
sldx, slwx, sradix, sradx, srawix, srawx, srdx, srwx,
|
||||
sldx,
|
||||
slwx,
|
||||
sradix,
|
||||
sradx,
|
||||
srawix,
|
||||
srawx,
|
||||
srdx,
|
||||
srwx,
|
||||
// Store byte
|
||||
stb, stbu, stbux, stbx,
|
||||
stb,
|
||||
stbu,
|
||||
stbux,
|
||||
stbx,
|
||||
// Store doubleword
|
||||
std, stdbrx, stdcx, stdu, stdux, stdx,
|
||||
std,
|
||||
stdbrx,
|
||||
stdcx,
|
||||
stdu,
|
||||
stdux,
|
||||
stdx,
|
||||
// Store float
|
||||
stfd, stfdu, stfdux, stfdx, stfiwx, stfs, stfsu, stfsux, stfsx,
|
||||
stfd,
|
||||
stfdu,
|
||||
stfdux,
|
||||
stfdx,
|
||||
stfiwx,
|
||||
stfs,
|
||||
stfsu,
|
||||
stfsux,
|
||||
stfsx,
|
||||
// Store halfword
|
||||
sth, sthbrx, sthu, sthux, sthx,
|
||||
sth,
|
||||
sthbrx,
|
||||
sthu,
|
||||
sthux,
|
||||
sthx,
|
||||
// Store multiple/string
|
||||
stmw, stswi, stswx,
|
||||
stmw,
|
||||
stswi,
|
||||
stswx,
|
||||
// Store vector
|
||||
stvebx, stvehx, stvewx, stvewx128, stvlx, stvlx128, stvlxl, stvlxl128,
|
||||
stvrx, stvrx128, stvrxl, stvrxl128,
|
||||
stvx, stvx128, stvxl, stvxl128,
|
||||
stvebx,
|
||||
stvehx,
|
||||
stvewx,
|
||||
stvewx128,
|
||||
stvlx,
|
||||
stvlx128,
|
||||
stvlxl,
|
||||
stvlxl128,
|
||||
stvrx,
|
||||
stvrx128,
|
||||
stvrxl,
|
||||
stvrxl128,
|
||||
stvx,
|
||||
stvx128,
|
||||
stvxl,
|
||||
stvxl128,
|
||||
// Store word
|
||||
stw, stwbrx, stwcx, stwu, stwux, stwx,
|
||||
stw,
|
||||
stwbrx,
|
||||
stwcx,
|
||||
stwu,
|
||||
stwux,
|
||||
stwx,
|
||||
// Subtract
|
||||
subfcx, subfex, subficx, subfmex, subfx, subfzex,
|
||||
subfcx,
|
||||
subfex,
|
||||
subficx,
|
||||
subfmex,
|
||||
subfx,
|
||||
subfzex,
|
||||
// Sync
|
||||
sync,
|
||||
// Trap
|
||||
td, tdi, tw, twi,
|
||||
td,
|
||||
tdi,
|
||||
tw,
|
||||
twi,
|
||||
// VMX integer
|
||||
vaddcuw, vaddfp, vaddfp128, vaddsbs, vaddshs, vaddsws,
|
||||
vaddubm, vaddubs, vadduhm, vadduhs, vadduwm, vadduws,
|
||||
vand, vand128, vandc, vandc128,
|
||||
vavgsb, vavgsh, vavgsw, vavgub, vavguh, vavguw,
|
||||
vcfpsxws128, vcfpuxws128, vcfsx, vcfux,
|
||||
vcmpbfp, vcmpbfp128, vcmpeqfp, vcmpeqfp128,
|
||||
vcmpequb, vcmpequh, vcmpequw, vcmpequw128,
|
||||
vcmpgefp, vcmpgefp128, vcmpgtfp, vcmpgtfp128,
|
||||
vcmpgtsb, vcmpgtsh, vcmpgtsw, vcmpgtub, vcmpgtuh, vcmpgtuw,
|
||||
vcsxwfp128, vctsxs, vctuxs, vcuxwfp128,
|
||||
vexptefp, vexptefp128, vlogefp, vlogefp128,
|
||||
vmaddcfp128, vmaddfp, vmaddfp128,
|
||||
vmaxfp, vmaxfp128, vmaxsb, vmaxsh, vmaxsw, vmaxub, vmaxuh, vmaxuw,
|
||||
vmhaddshs, vmhraddshs,
|
||||
vminfp, vminfp128, vminsb, vminsh, vminsw, vminub, vminuh, vminuw,
|
||||
vaddcuw,
|
||||
vaddfp,
|
||||
vaddfp128,
|
||||
vaddsbs,
|
||||
vaddshs,
|
||||
vaddsws,
|
||||
vaddubm,
|
||||
vaddubs,
|
||||
vadduhm,
|
||||
vadduhs,
|
||||
vadduwm,
|
||||
vadduws,
|
||||
vand,
|
||||
vand128,
|
||||
vandc,
|
||||
vandc128,
|
||||
vavgsb,
|
||||
vavgsh,
|
||||
vavgsw,
|
||||
vavgub,
|
||||
vavguh,
|
||||
vavguw,
|
||||
vcfpsxws128,
|
||||
vcfpuxws128,
|
||||
vcfsx,
|
||||
vcfux,
|
||||
vcmpbfp,
|
||||
vcmpbfp128,
|
||||
vcmpeqfp,
|
||||
vcmpeqfp128,
|
||||
vcmpequb,
|
||||
vcmpequh,
|
||||
vcmpequw,
|
||||
vcmpequw128,
|
||||
vcmpgefp,
|
||||
vcmpgefp128,
|
||||
vcmpgtfp,
|
||||
vcmpgtfp128,
|
||||
vcmpgtsb,
|
||||
vcmpgtsh,
|
||||
vcmpgtsw,
|
||||
vcmpgtub,
|
||||
vcmpgtuh,
|
||||
vcmpgtuw,
|
||||
vcsxwfp128,
|
||||
vctsxs,
|
||||
vctuxs,
|
||||
vcuxwfp128,
|
||||
vexptefp,
|
||||
vexptefp128,
|
||||
vlogefp,
|
||||
vlogefp128,
|
||||
vmaddcfp128,
|
||||
vmaddfp,
|
||||
vmaddfp128,
|
||||
vmaxfp,
|
||||
vmaxfp128,
|
||||
vmaxsb,
|
||||
vmaxsh,
|
||||
vmaxsw,
|
||||
vmaxub,
|
||||
vmaxuh,
|
||||
vmaxuw,
|
||||
vmhaddshs,
|
||||
vmhraddshs,
|
||||
vminfp,
|
||||
vminfp128,
|
||||
vminsb,
|
||||
vminsh,
|
||||
vminsw,
|
||||
vminub,
|
||||
vminuh,
|
||||
vminuw,
|
||||
vmladduhm,
|
||||
vmrghb, vmrghh, vmrghw, vmrghw128, vmrglb, vmrglh, vmrglw, vmrglw128,
|
||||
vmsum3fp128, vmsum4fp128,
|
||||
vmsummbm, vmsumshm, vmsumshs, vmsumubm, vmsumuhm, vmsumuhs,
|
||||
vmulesb, vmulesh, vmuleub, vmuleuh, vmulfp128,
|
||||
vmulosb, vmulosh, vmuloub, vmulouh,
|
||||
vnmsubfp, vnmsubfp128, vnor, vnor128,
|
||||
vor, vor128,
|
||||
vperm, vperm128, vpermwi128, vpkd3d128,
|
||||
vpkpx, vpkshss, vpkshss128, vpkshus, vpkshus128,
|
||||
vpkswss, vpkswss128, vpkswus, vpkswus128,
|
||||
vpkuhum, vpkuhum128, vpkuhus, vpkuhus128,
|
||||
vpkuwum, vpkuwum128, vpkuwus, vpkuwus128,
|
||||
vrefp, vrefp128,
|
||||
vrfim, vrfim128, vrfin, vrfin128, vrfip, vrfip128, vrfiz, vrfiz128,
|
||||
vrlb, vrlh, vrlimi128, vrlw, vrlw128,
|
||||
vrsqrtefp, vrsqrtefp128,
|
||||
vsel, vsel128,
|
||||
vsl, vslb, vsldoi, vsldoi128, vslh, vslo, vslo128, vslw, vslw128,
|
||||
vspltb, vsplth, vspltisb, vspltish, vspltisw, vspltisw128, vspltw, vspltw128,
|
||||
vsr, vsrab, vsrah, vsraw, vsraw128, vsrb, vsrh, vsro, vsro128, vsrw, vsrw128,
|
||||
vsubcuw, vsubfp, vsubfp128, vsubsbs, vsubshs, vsubsws,
|
||||
vsububm, vsububs, vsubuhm, vsubuhs, vsubuwm, vsubuws,
|
||||
vsum2sws, vsum4sbs, vsum4shs, vsum4ubs, vsumsws,
|
||||
vupkd3d128, vupkhpx, vupkhsb, vupkhsb128, vupkhsh,
|
||||
vupklpx, vupklsb, vupklsb128, vupklsh,
|
||||
vxor, vxor128,
|
||||
vmrghb,
|
||||
vmrghh,
|
||||
vmrghw,
|
||||
vmrghw128,
|
||||
vmrglb,
|
||||
vmrglh,
|
||||
vmrglw,
|
||||
vmrglw128,
|
||||
vmsum3fp128,
|
||||
vmsum4fp128,
|
||||
vmsummbm,
|
||||
vmsumshm,
|
||||
vmsumshs,
|
||||
vmsumubm,
|
||||
vmsumuhm,
|
||||
vmsumuhs,
|
||||
vmulesb,
|
||||
vmulesh,
|
||||
vmuleub,
|
||||
vmuleuh,
|
||||
vmulfp128,
|
||||
vmulosb,
|
||||
vmulosh,
|
||||
vmuloub,
|
||||
vmulouh,
|
||||
vnmsubfp,
|
||||
vnmsubfp128,
|
||||
vnor,
|
||||
vnor128,
|
||||
vor,
|
||||
vor128,
|
||||
vperm,
|
||||
vperm128,
|
||||
vpermwi128,
|
||||
vpkd3d128,
|
||||
vpkpx,
|
||||
vpkshss,
|
||||
vpkshss128,
|
||||
vpkshus,
|
||||
vpkshus128,
|
||||
vpkswss,
|
||||
vpkswss128,
|
||||
vpkswus,
|
||||
vpkswus128,
|
||||
vpkuhum,
|
||||
vpkuhum128,
|
||||
vpkuhus,
|
||||
vpkuhus128,
|
||||
vpkuwum,
|
||||
vpkuwum128,
|
||||
vpkuwus,
|
||||
vpkuwus128,
|
||||
vrefp,
|
||||
vrefp128,
|
||||
vrfim,
|
||||
vrfim128,
|
||||
vrfin,
|
||||
vrfin128,
|
||||
vrfip,
|
||||
vrfip128,
|
||||
vrfiz,
|
||||
vrfiz128,
|
||||
vrlb,
|
||||
vrlh,
|
||||
vrlimi128,
|
||||
vrlw,
|
||||
vrlw128,
|
||||
vrsqrtefp,
|
||||
vrsqrtefp128,
|
||||
vsel,
|
||||
vsel128,
|
||||
vsl,
|
||||
vslb,
|
||||
vsldoi,
|
||||
vsldoi128,
|
||||
vslh,
|
||||
vslo,
|
||||
vslo128,
|
||||
vslw,
|
||||
vslw128,
|
||||
vspltb,
|
||||
vsplth,
|
||||
vspltisb,
|
||||
vspltish,
|
||||
vspltisw,
|
||||
vspltisw128,
|
||||
vspltw,
|
||||
vspltw128,
|
||||
vsr,
|
||||
vsrab,
|
||||
vsrah,
|
||||
vsraw,
|
||||
vsraw128,
|
||||
vsrb,
|
||||
vsrh,
|
||||
vsro,
|
||||
vsro128,
|
||||
vsrw,
|
||||
vsrw128,
|
||||
vsubcuw,
|
||||
vsubfp,
|
||||
vsubfp128,
|
||||
vsubsbs,
|
||||
vsubshs,
|
||||
vsubsws,
|
||||
vsububm,
|
||||
vsububs,
|
||||
vsubuhm,
|
||||
vsubuhs,
|
||||
vsubuwm,
|
||||
vsubuws,
|
||||
vsum2sws,
|
||||
vsum4sbs,
|
||||
vsum4shs,
|
||||
vsum4ubs,
|
||||
vsumsws,
|
||||
vupkd3d128,
|
||||
vupkhpx,
|
||||
vupkhsb,
|
||||
vupkhsb128,
|
||||
vupkhsh,
|
||||
vupklpx,
|
||||
vupklsb,
|
||||
vupklsb128,
|
||||
vupklsh,
|
||||
vxor,
|
||||
vxor128,
|
||||
// XOR immediate
|
||||
xori, xoris, xorx,
|
||||
xori,
|
||||
xoris,
|
||||
xorx,
|
||||
// Invalid
|
||||
Invalid,
|
||||
}
|
||||
@@ -165,42 +532,102 @@ impl PpcOpcode {
|
||||
pub fn terminates_block(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Self::bx | Self::bcx | Self::bclrx | Self::bcctrx
|
||||
Self::bx
|
||||
| Self::bcx
|
||||
| Self::bclrx
|
||||
| Self::bcctrx
|
||||
| Self::sc
|
||||
| Self::td | Self::tdi | Self::tw | Self::twi
|
||||
| Self::td
|
||||
| Self::tdi
|
||||
| Self::tw
|
||||
| Self::twi
|
||||
| Self::Invalid
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns true if this is a load instruction.
|
||||
pub fn is_load(&self) -> bool {
|
||||
matches!(self,
|
||||
Self::lbz | Self::lbzu | Self::lbzux | Self::lbzx |
|
||||
Self::lhz | Self::lhzu | Self::lhzux | Self::lhzx |
|
||||
Self::lha | Self::lhau | Self::lhaux | Self::lhax |
|
||||
Self::lwz | Self::lwzu | Self::lwzux | Self::lwzx |
|
||||
Self::lwa | Self::lwax | Self::lwaux |
|
||||
Self::ld | Self::ldu | Self::ldux | Self::ldx |
|
||||
Self::lfs | Self::lfsu | Self::lfsux | Self::lfsx |
|
||||
Self::lfd | Self::lfdu | Self::lfdux | Self::lfdx |
|
||||
Self::lhbrx | Self::lwbrx | Self::ldbrx |
|
||||
Self::lmw | Self::lswi | Self::lswx |
|
||||
Self::lwarx | Self::ldarx
|
||||
matches!(
|
||||
self,
|
||||
Self::lbz
|
||||
| Self::lbzu
|
||||
| Self::lbzux
|
||||
| Self::lbzx
|
||||
| Self::lhz
|
||||
| Self::lhzu
|
||||
| Self::lhzux
|
||||
| Self::lhzx
|
||||
| Self::lha
|
||||
| Self::lhau
|
||||
| Self::lhaux
|
||||
| Self::lhax
|
||||
| Self::lwz
|
||||
| Self::lwzu
|
||||
| Self::lwzux
|
||||
| Self::lwzx
|
||||
| Self::lwa
|
||||
| Self::lwax
|
||||
| Self::lwaux
|
||||
| Self::ld
|
||||
| Self::ldu
|
||||
| Self::ldux
|
||||
| Self::ldx
|
||||
| Self::lfs
|
||||
| Self::lfsu
|
||||
| Self::lfsux
|
||||
| Self::lfsx
|
||||
| Self::lfd
|
||||
| Self::lfdu
|
||||
| Self::lfdux
|
||||
| Self::lfdx
|
||||
| Self::lhbrx
|
||||
| Self::lwbrx
|
||||
| Self::ldbrx
|
||||
| Self::lmw
|
||||
| Self::lswi
|
||||
| Self::lswx
|
||||
| Self::lwarx
|
||||
| Self::ldarx
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns true if this is a store instruction.
|
||||
pub fn is_store(&self) -> bool {
|
||||
matches!(self,
|
||||
Self::stb | Self::stbu | Self::stbux | Self::stbx |
|
||||
Self::sth | Self::sthu | Self::sthux | Self::sthx |
|
||||
Self::stw | Self::stwu | Self::stwux | Self::stwx |
|
||||
Self::std | Self::stdu | Self::stdux | Self::stdx |
|
||||
Self::stfs | Self::stfsu | Self::stfsux | Self::stfsx |
|
||||
Self::stfd | Self::stfdu | Self::stfdux | Self::stfdx |
|
||||
Self::sthbrx | Self::stwbrx | Self::stdbrx |
|
||||
Self::stmw | Self::stswi | Self::stswx |
|
||||
Self::stwcx | Self::stdcx | Self::stfiwx
|
||||
matches!(
|
||||
self,
|
||||
Self::stb
|
||||
| Self::stbu
|
||||
| Self::stbux
|
||||
| Self::stbx
|
||||
| Self::sth
|
||||
| Self::sthu
|
||||
| Self::sthux
|
||||
| Self::sthx
|
||||
| Self::stw
|
||||
| Self::stwu
|
||||
| Self::stwux
|
||||
| Self::stwx
|
||||
| Self::std
|
||||
| Self::stdu
|
||||
| Self::stdux
|
||||
| Self::stdx
|
||||
| Self::stfs
|
||||
| Self::stfsu
|
||||
| Self::stfsux
|
||||
| Self::stfsx
|
||||
| Self::stfd
|
||||
| Self::stfdu
|
||||
| Self::stfdux
|
||||
| Self::stfdx
|
||||
| Self::sthbrx
|
||||
| Self::stwbrx
|
||||
| Self::stdbrx
|
||||
| Self::stmw
|
||||
| Self::stswi
|
||||
| Self::stswx
|
||||
| Self::stwcx
|
||||
| Self::stdcx
|
||||
| Self::stfiwx
|
||||
)
|
||||
}
|
||||
|
||||
@@ -227,8 +654,13 @@ impl PpcOpcode {
|
||||
pub fn is_sync_sensitive(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Self::lwarx | Self::ldarx | Self::stwcx | Self::stdcx
|
||||
| Self::sync | Self::eieio | Self::isync
|
||||
Self::lwarx
|
||||
| Self::ldarx
|
||||
| Self::stwcx
|
||||
| Self::stdcx
|
||||
| Self::sync
|
||||
| Self::eieio
|
||||
| Self::isync
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
//! From `xenia-rs`'s `xenia-xex` + `xenia-vfs` when that emulator was retired.
|
||||
//! `docs/agents/CONSOLIDATION.md` Phase 3.
|
||||
|
||||
pub mod vfs;
|
||||
pub mod header;
|
||||
pub mod loader;
|
||||
pub mod lzx;
|
||||
pub mod pe;
|
||||
pub mod pdata;
|
||||
pub mod pe;
|
||||
pub mod resources;
|
||||
pub mod tls;
|
||||
pub mod vfs;
|
||||
|
||||
pub use header::Xex2Header;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::header::*;
|
||||
use aes::cipher::{BlockDecrypt, KeyInit};
|
||||
use aes::Aes128;
|
||||
use aes::cipher::{BlockDecrypt, KeyInit};
|
||||
use byteorder::{BigEndian, ReadBytesExt};
|
||||
use std::io::{self, Cursor, Read, Seek, SeekFrom};
|
||||
|
||||
@@ -12,7 +12,10 @@ pub fn parse_xex2_header(data: &[u8]) -> io::Result<Xex2Header> {
|
||||
if magic != XEX2_MAGIC {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("Invalid XEX2 magic: {:#010x} (expected {:#010x})", magic, XEX2_MAGIC),
|
||||
format!(
|
||||
"Invalid XEX2 magic: {:#010x} (expected {:#010x})",
|
||||
magic, XEX2_MAGIC
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -84,41 +87,41 @@ fn parse_security_info(cursor: &mut Cursor<&[u8]>) -> io::Result<Xex2SecurityInf
|
||||
// 0x180: page_descriptor_count (u32)
|
||||
// 0x184: page_descriptors[] (each is 0x18 bytes: u32 value + 0x14 digest)
|
||||
|
||||
let _header_size = cursor.read_u32::<BigEndian>()?; // 0x000
|
||||
let image_size = cursor.read_u32::<BigEndian>()?; // 0x004
|
||||
let _header_size = cursor.read_u32::<BigEndian>()?; // 0x000
|
||||
let image_size = cursor.read_u32::<BigEndian>()?; // 0x004
|
||||
|
||||
// Skip RSA signature (0x100 bytes)
|
||||
let mut rsa_sig = [0u8; 0x100];
|
||||
cursor.read_exact(&mut rsa_sig)?; // 0x008
|
||||
cursor.read_exact(&mut rsa_sig)?; // 0x008
|
||||
|
||||
let _unk_108 = cursor.read_u32::<BigEndian>()?; // 0x108
|
||||
let image_flags = cursor.read_u32::<BigEndian>()?; // 0x10C
|
||||
let load_address = cursor.read_u32::<BigEndian>()?; // 0x110
|
||||
let _unk_108 = cursor.read_u32::<BigEndian>()?; // 0x108
|
||||
let image_flags = cursor.read_u32::<BigEndian>()?; // 0x10C
|
||||
let load_address = cursor.read_u32::<BigEndian>()?; // 0x110
|
||||
|
||||
// Skip section_digest (0x14 bytes)
|
||||
let mut digest = [0u8; 0x14];
|
||||
cursor.read_exact(&mut digest)?; // 0x114
|
||||
cursor.read_exact(&mut digest)?; // 0x114
|
||||
|
||||
let _import_table_count = cursor.read_u32::<BigEndian>()?; // 0x128
|
||||
|
||||
// Skip import_table_digest (0x14 bytes)
|
||||
cursor.read_exact(&mut digest)?; // 0x12C
|
||||
cursor.read_exact(&mut digest)?; // 0x12C
|
||||
|
||||
// Skip xgd2_media_id (0x10 bytes)
|
||||
let mut media_id = [0u8; 0x10];
|
||||
cursor.read_exact(&mut media_id)?; // 0x140
|
||||
cursor.read_exact(&mut media_id)?; // 0x140
|
||||
|
||||
// Read aes_key (0x10 bytes)
|
||||
let mut aes_key = [0u8; 0x10];
|
||||
cursor.read_exact(&mut aes_key)?; // 0x150
|
||||
cursor.read_exact(&mut aes_key)?; // 0x150
|
||||
|
||||
let export_table_address = cursor.read_u32::<BigEndian>()?; // 0x160
|
||||
|
||||
// Skip header_digest (0x14 bytes)
|
||||
cursor.read_exact(&mut digest)?; // 0x164
|
||||
cursor.read_exact(&mut digest)?; // 0x164
|
||||
|
||||
let _region = cursor.read_u32::<BigEndian>()?; // 0x178
|
||||
let _allowed_media = cursor.read_u32::<BigEndian>()?; // 0x17C
|
||||
let _region = cursor.read_u32::<BigEndian>()?; // 0x178
|
||||
let _allowed_media = cursor.read_u32::<BigEndian>()?; // 0x17C
|
||||
|
||||
let page_descriptor_count = cursor.read_u32::<BigEndian>()?; // 0x180
|
||||
|
||||
@@ -144,7 +147,9 @@ fn parse_security_info(cursor: &mut Cursor<&[u8]>) -> io::Result<Xex2SecurityInf
|
||||
fn parse_file_format_info(data: &[u8], headers: &[Xex2OptionalHeader]) -> Option<FileFormatInfo> {
|
||||
// The key format: low 8 bits indicate the data size category
|
||||
// 0xFF = data offset is a pointer to variable-size data in the header area
|
||||
let header = headers.iter().find(|h| h.key == header_keys::FILE_FORMAT_INFO)?;
|
||||
let header = headers
|
||||
.iter()
|
||||
.find(|h| h.key == header_keys::FILE_FORMAT_INFO)?;
|
||||
let offset = header.value as usize;
|
||||
if offset + 8 > data.len() {
|
||||
return None;
|
||||
@@ -166,11 +171,18 @@ fn parse_file_format_info(data: &[u8], headers: &[Xex2OptionalHeader]) -> Option
|
||||
COMPRESSION_BASIC => {
|
||||
// Basic compression blocks: (data_size, zero_size) pairs
|
||||
// Number of blocks = (info_size - 8) / 8
|
||||
let block_count = if info_size > 8 { (info_size - 8) / 8 } else { 0 };
|
||||
let block_count = if info_size > 8 {
|
||||
(info_size - 8) / 8
|
||||
} else {
|
||||
0
|
||||
};
|
||||
for _ in 0..block_count {
|
||||
let data_size = cursor.read_u32::<BigEndian>().ok()?;
|
||||
let zero_size = cursor.read_u32::<BigEndian>().ok()?;
|
||||
basic_blocks.push(BasicCompressionBlock { data_size, zero_size });
|
||||
basic_blocks.push(BasicCompressionBlock {
|
||||
data_size,
|
||||
zero_size,
|
||||
});
|
||||
}
|
||||
}
|
||||
COMPRESSION_NORMAL => {
|
||||
@@ -197,7 +209,10 @@ fn parse_file_format_info(data: &[u8], headers: &[Xex2OptionalHeader]) -> Option
|
||||
/// At this stage, only record addresses are read; ordinals and record types
|
||||
/// are resolved later by `resolve_imports` once the PE image is decompressed.
|
||||
fn parse_import_libraries(data: &[u8], headers: &[Xex2OptionalHeader]) -> Vec<ImportLibrary> {
|
||||
let header = match headers.iter().find(|h| h.key == header_keys::IMPORT_LIBRARIES) {
|
||||
let header = match headers
|
||||
.iter()
|
||||
.find(|h| h.key == header_keys::IMPORT_LIBRARIES)
|
||||
{
|
||||
Some(h) => h,
|
||||
None => return Vec::new(),
|
||||
};
|
||||
@@ -208,10 +223,10 @@ fn parse_import_libraries(data: &[u8], headers: &[Xex2OptionalHeader]) -> Vec<Im
|
||||
}
|
||||
|
||||
fn be_u32(data: &[u8], off: usize) -> u32 {
|
||||
u32::from_be_bytes([data[off], data[off+1], data[off+2], data[off+3]])
|
||||
u32::from_be_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]])
|
||||
}
|
||||
fn be_u16(data: &[u8], off: usize) -> u16 {
|
||||
u16::from_be_bytes([data[off], data[off+1]])
|
||||
u16::from_be_bytes([data[off], data[off + 1]])
|
||||
}
|
||||
|
||||
let total_size = be_u32(data, offset) as usize;
|
||||
@@ -225,11 +240,17 @@ fn parse_import_libraries(data: &[u8], headers: &[Xex2OptionalHeader]) -> Vec<Im
|
||||
for _ in 0..string_count {
|
||||
let start = string_data_start + spos;
|
||||
let mut end = start;
|
||||
while end < data.len() && data[end] != 0 { end += 1; }
|
||||
let name = std::str::from_utf8(&data[start..end]).unwrap_or("???").to_string();
|
||||
while end < data.len() && data[end] != 0 {
|
||||
end += 1;
|
||||
}
|
||||
let name = std::str::from_utf8(&data[start..end])
|
||||
.unwrap_or("???")
|
||||
.to_string();
|
||||
spos += name.len() + 1;
|
||||
// 4-byte alignment
|
||||
if !spos.is_multiple_of(4) { spos += 4 - (spos % 4); }
|
||||
if !spos.is_multiple_of(4) {
|
||||
spos += 4 - (spos % 4);
|
||||
}
|
||||
strings.push(name);
|
||||
}
|
||||
|
||||
@@ -239,7 +260,9 @@ fn parse_import_libraries(data: &[u8], headers: &[Xex2OptionalHeader]) -> Vec<Im
|
||||
|
||||
while lib_off + 0x28 <= data.len() && lib_off < offset + total_size {
|
||||
let lib_size = be_u32(data, lib_off) as usize;
|
||||
if lib_size == 0 { break; }
|
||||
if lib_size == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
let id = be_u32(data, lib_off + 0x18);
|
||||
let version_cur = be_u32(data, lib_off + 0x1C);
|
||||
@@ -247,7 +270,10 @@ fn parse_import_libraries(data: &[u8], headers: &[Xex2OptionalHeader]) -> Vec<Im
|
||||
let name_index = (be_u16(data, lib_off + 0x24) & 0xFF) as usize;
|
||||
let count = be_u16(data, lib_off + 0x26) as usize;
|
||||
|
||||
let lib_name = strings.get(name_index).cloned().unwrap_or_else(|| format!("lib_{name_index}"));
|
||||
let lib_name = strings
|
||||
.get(name_index)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| format!("lib_{name_index}"));
|
||||
|
||||
let mut imports = Vec::new();
|
||||
for i in 0..count {
|
||||
@@ -283,8 +309,10 @@ pub fn resolve_imports(header: &mut Xex2Header, pe_image: &[u8]) {
|
||||
if pe_off + 4 <= pe_image.len() {
|
||||
// PE image values are big-endian (Xbox 360 native)
|
||||
let val = u32::from_be_bytes([
|
||||
pe_image[pe_off], pe_image[pe_off+1],
|
||||
pe_image[pe_off+2], pe_image[pe_off+3],
|
||||
pe_image[pe_off],
|
||||
pe_image[pe_off + 1],
|
||||
pe_image[pe_off + 2],
|
||||
pe_image[pe_off + 3],
|
||||
]);
|
||||
imp.record_type = ((val >> 24) & 0xFF) as u8;
|
||||
imp.ordinal = (val & 0xFFFF) as u16;
|
||||
@@ -299,14 +327,21 @@ fn parse_execution_info(data: &[u8], headers: &[Xex2OptionalHeader]) -> Option<E
|
||||
// is an inline struct of 6 u32 words (24 bytes total).
|
||||
// Layout: media_id(4), version(4), base_version(4), title_id(4),
|
||||
// platform(1), exec_type(1), disc_number(1), disc_count(1)
|
||||
let header = headers.iter().find(|h| h.key == header_keys::EXECUTION_INFO)?;
|
||||
let header = headers
|
||||
.iter()
|
||||
.find(|h| h.key == header_keys::EXECUTION_INFO)?;
|
||||
let off = header.value as usize;
|
||||
if off + 20 > data.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let media_id = u32::from_be_bytes([data[off], data[off+1], data[off+2], data[off+3]]);
|
||||
let title_id = u32::from_be_bytes([data[off+12], data[off+13], data[off+14], data[off+15]]);
|
||||
let media_id = u32::from_be_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]]);
|
||||
let title_id = u32::from_be_bytes([
|
||||
data[off + 12],
|
||||
data[off + 13],
|
||||
data[off + 14],
|
||||
data[off + 15],
|
||||
]);
|
||||
let disc_number = data[off + 18];
|
||||
let disc_count = data[off + 19];
|
||||
|
||||
@@ -320,24 +355,33 @@ fn parse_execution_info(data: &[u8], headers: &[Xex2OptionalHeader]) -> Option<E
|
||||
|
||||
/// Parse original PE name from optional header data.
|
||||
fn parse_original_pe_name(data: &[u8], headers: &[Xex2OptionalHeader]) -> Option<String> {
|
||||
let header = headers.iter().find(|h| h.key == header_keys::ORIGINAL_PE_NAME)?;
|
||||
let header = headers
|
||||
.iter()
|
||||
.find(|h| h.key == header_keys::ORIGINAL_PE_NAME)?;
|
||||
let off = header.value as usize;
|
||||
if off + 4 > data.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let size = u32::from_be_bytes([data[off], data[off+1], data[off+2], data[off+3]]) as usize;
|
||||
let size =
|
||||
u32::from_be_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]]) as usize;
|
||||
if off + size > data.len() || size <= 4 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let name_bytes = &data[off + 4..off + size];
|
||||
Some(String::from_utf8_lossy(name_bytes).trim_end_matches('\0').to_string())
|
||||
Some(
|
||||
String::from_utf8_lossy(name_bytes)
|
||||
.trim_end_matches('\0')
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Get an optional header value by key.
|
||||
pub fn get_opt_header(header: &Xex2Header, key: u32) -> Option<u32> {
|
||||
header.optional_headers.iter()
|
||||
header
|
||||
.optional_headers
|
||||
.iter()
|
||||
.find(|h| h.key == key)
|
||||
.map(|h| h.value)
|
||||
}
|
||||
@@ -389,15 +433,27 @@ pub fn load_image(data: &[u8], header: &Xex2Header) -> io::Result<Vec<u8>> {
|
||||
metrics::histogram!("xex.load_image_ms").record(elapsed_ms);
|
||||
metrics::counter!("xex.bytes_in").increment(bytes_in as u64);
|
||||
metrics::counter!("xex.bytes_out").increment(output.len() as u64);
|
||||
let ratio = if bytes_in == 0 { 0.0 } else { output.len() as f64 / bytes_in as f64 };
|
||||
tracing::info!(bytes_in, bytes_out = output.len(), ratio, elapsed_ms, "image loaded");
|
||||
let ratio = if bytes_in == 0 {
|
||||
0.0
|
||||
} else {
|
||||
output.len() as f64 / bytes_in as f64
|
||||
};
|
||||
tracing::info!(
|
||||
bytes_in,
|
||||
bytes_out = output.len(),
|
||||
ratio,
|
||||
elapsed_ms,
|
||||
"image loaded"
|
||||
);
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
/// Load basic compressed image data.
|
||||
fn load_basic_compressed(source: &[u8], info: &FileFormatInfo) -> io::Result<Vec<u8>> {
|
||||
// Calculate total uncompressed size
|
||||
let total_size: u64 = info.basic_blocks.iter()
|
||||
let total_size: u64 = info
|
||||
.basic_blocks
|
||||
.iter()
|
||||
.map(|b| b.data_size as u64 + b.zero_size as u64)
|
||||
.sum();
|
||||
|
||||
@@ -412,8 +468,12 @@ fn load_basic_compressed(source: &[u8], info: &FileFormatInfo) -> io::Result<Vec
|
||||
if src_offset + data_size > source.len() {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::UnexpectedEof,
|
||||
format!("Basic compression block data extends past end of file (src_offset={:#x}, data_size={:#x}, source_len={:#x})",
|
||||
src_offset, data_size, source.len()),
|
||||
format!(
|
||||
"Basic compression block data extends past end of file (src_offset={:#x}, data_size={:#x}, source_len={:#x})",
|
||||
src_offset,
|
||||
data_size,
|
||||
source.len()
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -434,8 +494,7 @@ fn load_basic_compressed(source: &[u8], info: &FileFormatInfo) -> io::Result<Vec
|
||||
|
||||
/// Xbox 360 retail AES key for XEX2 session key decryption.
|
||||
const XEX2_RETAIL_KEY: [u8; 16] = [
|
||||
0x20, 0xB1, 0x85, 0xA5, 0x9D, 0x28, 0xFD, 0xC3,
|
||||
0x40, 0x58, 0x3F, 0xBB, 0x08, 0x96, 0xBF, 0x91,
|
||||
0x20, 0xB1, 0x85, 0xA5, 0x9D, 0x28, 0xFD, 0xC3, 0x40, 0x58, 0x3F, 0xBB, 0x08, 0x96, 0xBF, 0x91,
|
||||
];
|
||||
|
||||
/// Xbox 360 devkit AES key (all zeros).
|
||||
@@ -486,8 +545,10 @@ fn derive_session_key(header: &Xex2Header) -> [u8; 16] {
|
||||
///
|
||||
/// The first block's size comes from the file format header (first_block_size).
|
||||
/// Each block in the data starts with a block_info struct for the NEXT block:
|
||||
/// - block_size: u32 BE (size of the next block)
|
||||
/// - block_hash: [u8; 20] (SHA1 of the next block)
|
||||
///
|
||||
/// - block_size: u32 BE (size of the next block)
|
||||
/// - block_hash: [u8; 20] (SHA1 of the next block)
|
||||
///
|
||||
/// Followed by chunks: { chunk_size: u16 BE, data: [u8; chunk_size] }, terminated by chunk_size=0
|
||||
fn deblock(input: &[u8], first_block_size: u32) -> io::Result<Vec<u8>> {
|
||||
let mut output = Vec::new();
|
||||
@@ -499,9 +560,8 @@ fn deblock(input: &[u8], first_block_size: u32) -> io::Result<Vec<u8>> {
|
||||
|
||||
// Read next block's info from start of current block data
|
||||
let next_block_size = if pos + 4 <= input.len() {
|
||||
u32::from_be_bytes([
|
||||
input[pos], input[pos + 1], input[pos + 2], input[pos + 3],
|
||||
]) as usize
|
||||
u32::from_be_bytes([input[pos], input[pos + 1], input[pos + 2], input[pos + 3]])
|
||||
as usize
|
||||
} else {
|
||||
0
|
||||
};
|
||||
@@ -522,8 +582,12 @@ fn deblock(input: &[u8], first_block_size: u32) -> io::Result<Vec<u8>> {
|
||||
if p + chunk_size > input.len() {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::UnexpectedEof,
|
||||
format!("De-block chunk extends past input (pos={:#x}, chunk_size={:#x}, input_len={:#x})",
|
||||
p, chunk_size, input.len()),
|
||||
format!(
|
||||
"De-block chunk extends past input (pos={:#x}, chunk_size={:#x}, input_len={:#x})",
|
||||
p,
|
||||
chunk_size,
|
||||
input.len()
|
||||
),
|
||||
));
|
||||
}
|
||||
output.extend_from_slice(&input[p..p + chunk_size]);
|
||||
@@ -543,8 +607,14 @@ fn deblock(input: &[u8], first_block_size: u32) -> io::Result<Vec<u8>> {
|
||||
/// Load normal (LZX) compressed image data.
|
||||
/// Pipeline: decrypt → de-block → LZX decompress (pure Rust)
|
||||
#[tracing::instrument(skip_all, fields(bytes_in = source.len()))]
|
||||
fn load_normal_compressed(source: &[u8], info: &FileFormatInfo, header: &Xex2Header) -> io::Result<Vec<u8>> {
|
||||
let uncompressed_size = header.security_info.as_ref()
|
||||
fn load_normal_compressed(
|
||||
source: &[u8],
|
||||
info: &FileFormatInfo,
|
||||
header: &Xex2Header,
|
||||
) -> io::Result<Vec<u8>> {
|
||||
let uncompressed_size = header
|
||||
.security_info
|
||||
.as_ref()
|
||||
.map(|s| s.image_size as usize)
|
||||
.unwrap_or(0);
|
||||
|
||||
@@ -577,15 +647,25 @@ fn load_normal_compressed(source: &[u8], info: &FileFormatInfo, header: &Xex2Hea
|
||||
|
||||
// Step 3: LZX decompress using pure Rust decoder
|
||||
let window_bits = match info.normal_window_size {
|
||||
s if s == 0 => 15, // default
|
||||
0 => 15, // default
|
||||
s => (s as f64).log2() as u32,
|
||||
};
|
||||
|
||||
let mut decoder = crate::lzx::LzxDecoder::new(window_bits);
|
||||
let output = decoder.decompress(&deblocked, uncompressed_size)
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, format!("LZX decompression failed: {e}")))?;
|
||||
let output = decoder
|
||||
.decompress(&deblocked, uncompressed_size)
|
||||
.map_err(|e| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("LZX decompression failed: {e}"),
|
||||
)
|
||||
})?;
|
||||
|
||||
tracing::info!("LZX decompressed: {} -> {} bytes", deblocked.len(), uncompressed_size);
|
||||
tracing::info!(
|
||||
"LZX decompressed: {} -> {} bytes",
|
||||
deblocked.len(),
|
||||
uncompressed_size
|
||||
);
|
||||
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
// 🔴 INDEX ARITHMETIC IS THE ALGORITHM HERE, so the range-loop lints are off for
|
||||
// this file and this file only. LZX decoding is defined over symbol indices,
|
||||
// Huffman table slots and window positions; rewriting those loops as iterators
|
||||
// would hide the correspondence with the format, and a decompressor that is
|
||||
// merely idiomatic is worth nothing if it is not bit-exact.
|
||||
#![allow(clippy::needless_range_loop, clippy::explicit_counter_loop)]
|
||||
|
||||
//! LZX decompressor for Xbox 360 XEX2 "normal compression".
|
||||
//! Ported from libmspack lzxd.c (C) 2003-2013 Stuart Caie, LGPL 2.1.
|
||||
|
||||
@@ -32,9 +39,8 @@ const BITBUF_WIDTH: u32 = 32;
|
||||
static POSITION_SLOTS: [u32; 11] = [30, 32, 34, 36, 38, 42, 50, 66, 98, 162, 290];
|
||||
|
||||
static EXTRA_BITS: [u8; 36] = [
|
||||
0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6,
|
||||
7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13, 14, 14,
|
||||
15, 15, 16, 16,
|
||||
0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13,
|
||||
13, 14, 14, 15, 15, 16, 16,
|
||||
];
|
||||
|
||||
#[rustfmt::skip]
|
||||
@@ -109,17 +115,30 @@ struct BitReader<'a> {
|
||||
|
||||
impl<'a> BitReader<'a> {
|
||||
fn new(data: &'a [u8]) -> Self {
|
||||
Self { data, pos: 0, buf: 0, left: 0 }
|
||||
Self {
|
||||
data,
|
||||
pos: 0,
|
||||
buf: 0,
|
||||
left: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Inject one 16-bit little-endian pair into MSB bit buffer.
|
||||
fn fill(&mut self) {
|
||||
let b0 = if self.pos < self.data.len() {
|
||||
let b = self.data[self.pos]; self.pos += 1; b as u32
|
||||
} else { 0 };
|
||||
let b = self.data[self.pos];
|
||||
self.pos += 1;
|
||||
b as u32
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let b1 = if self.pos < self.data.len() {
|
||||
let b = self.data[self.pos]; self.pos += 1; b as u32
|
||||
} else { 0 };
|
||||
let b = self.data[self.pos];
|
||||
self.pos += 1;
|
||||
b as u32
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let word = (b1 << 8) | b0;
|
||||
self.buf |= word << (16 - self.left as u32);
|
||||
self.left += 16;
|
||||
@@ -127,7 +146,9 @@ impl<'a> BitReader<'a> {
|
||||
|
||||
#[inline]
|
||||
fn ensure(&mut self, n: i32) {
|
||||
while self.left < n { self.fill(); }
|
||||
while self.left < n {
|
||||
self.fill();
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@@ -152,26 +173,29 @@ impl<'a> BitReader<'a> {
|
||||
/// Read a raw byte directly (for UNCOMPRESSED blocks).
|
||||
fn raw_byte(&mut self) -> u8 {
|
||||
if self.pos < self.data.len() {
|
||||
let b = self.data[self.pos]; self.pos += 1; b
|
||||
} else { 0 }
|
||||
let b = self.data[self.pos];
|
||||
self.pos += 1;
|
||||
b
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
/// Re-align the bitstream at a frame boundary.
|
||||
fn align_frame(&mut self) {
|
||||
if self.left > 0 { self.ensure(16); }
|
||||
if self.left > 0 {
|
||||
self.ensure(16);
|
||||
}
|
||||
let r = self.left & 15;
|
||||
if r != 0 { self.remove(r as u32); }
|
||||
if r != 0 {
|
||||
self.remove(r as u32);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Huffman table builder (MSB order) ───────────────────────────────────────
|
||||
|
||||
fn make_decode_table(
|
||||
nsyms: usize,
|
||||
nbits: usize,
|
||||
length: &[u8],
|
||||
table: &mut [u16],
|
||||
) -> bool {
|
||||
fn make_decode_table(nsyms: usize, nbits: usize, length: &[u8], table: &mut [u16]) -> bool {
|
||||
let mut pos: usize = 0;
|
||||
let table_mask = 1usize << nbits;
|
||||
let mut bit_mask = table_mask >> 1;
|
||||
@@ -179,10 +203,14 @@ fn make_decode_table(
|
||||
// Short codes: direct mapping
|
||||
for bit_num in 1..=nbits {
|
||||
for sym in 0..nsyms {
|
||||
if length[sym] as usize != bit_num { continue; }
|
||||
if length[sym] as usize != bit_num {
|
||||
continue;
|
||||
}
|
||||
let leaf = pos;
|
||||
pos += bit_mask;
|
||||
if pos > table_mask { return true; }
|
||||
if pos > table_mask {
|
||||
return true;
|
||||
}
|
||||
for i in leaf..leaf + bit_mask {
|
||||
table[i] = sym as u16;
|
||||
}
|
||||
@@ -190,14 +218,20 @@ fn make_decode_table(
|
||||
bit_mask >>= 1;
|
||||
}
|
||||
|
||||
if pos == table_mask { return false; }
|
||||
if pos == table_mask {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Mark remaining entries as unused
|
||||
for i in pos..table_mask {
|
||||
table[i] = 0xFFFF;
|
||||
}
|
||||
|
||||
let mut next_symbol = if (table_mask >> 1) < nsyms { nsyms } else { table_mask >> 1 };
|
||||
let mut next_symbol = if (table_mask >> 1) < nsyms {
|
||||
nsyms
|
||||
} else {
|
||||
table_mask >> 1
|
||||
};
|
||||
|
||||
let mut pos32 = (pos as u32) << 16;
|
||||
let table_mask32 = (table_mask as u32) << 16;
|
||||
@@ -206,8 +240,12 @@ fn make_decode_table(
|
||||
// Long codes: tree traversal
|
||||
for bit_num in (nbits + 1)..=HUFF_MAXBITS {
|
||||
for sym in 0..nsyms {
|
||||
if length[sym] as usize != bit_num { continue; }
|
||||
if pos32 >= table_mask32 { return true; }
|
||||
if length[sym] as usize != bit_num {
|
||||
continue;
|
||||
}
|
||||
if pos32 >= table_mask32 {
|
||||
return true;
|
||||
}
|
||||
|
||||
let mut leaf = (pos32 >> 16) as usize;
|
||||
|
||||
@@ -247,9 +285,13 @@ fn read_huffsym(
|
||||
let mut i: u32 = 1 << (BITBUF_WIDTH - tablebits as u32);
|
||||
loop {
|
||||
i >>= 1;
|
||||
if i == 0 { return Err(LzxError::BadHuffmanTable); }
|
||||
if i == 0 {
|
||||
return Err(LzxError::BadHuffmanTable);
|
||||
}
|
||||
sym = table[(sym << 1) | if br.buf & i != 0 { 1 } else { 0 }] as usize;
|
||||
if sym < maxsyms { break; }
|
||||
if sym < maxsyms {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
br.remove(lens[sym] as u32);
|
||||
@@ -307,7 +349,9 @@ impl LzxDecoder {
|
||||
frame_posn: 0,
|
||||
frame: 0,
|
||||
num_offsets,
|
||||
r0: 1, r1: 1, r2: 1,
|
||||
r0: 1,
|
||||
r1: 1,
|
||||
r2: 1,
|
||||
block_type: 0,
|
||||
block_length: 0,
|
||||
block_remaining: 0,
|
||||
@@ -315,20 +359,23 @@ impl LzxDecoder {
|
||||
intel_filesize: 0,
|
||||
intel_curpos: 0,
|
||||
intel_started: false,
|
||||
pretree_len: vec![0u8; PRETREE_MAXSYMS + LENTABLE_SAFETY],
|
||||
maintree_len: vec![0u8; MAINTREE_MAXSYMS + LENTABLE_SAFETY],
|
||||
length_len: vec![0u8; LENGTH_MAXSYMS + LENTABLE_SAFETY],
|
||||
aligned_len: vec![0u8; ALIGNED_MAXSYMS + LENTABLE_SAFETY],
|
||||
pretree_table: vec![0u16; (1 << PRETREE_TABLEBITS) + PRETREE_MAXSYMS * 2],
|
||||
pretree_len: vec![0u8; PRETREE_MAXSYMS + LENTABLE_SAFETY],
|
||||
maintree_len: vec![0u8; MAINTREE_MAXSYMS + LENTABLE_SAFETY],
|
||||
length_len: vec![0u8; LENGTH_MAXSYMS + LENTABLE_SAFETY],
|
||||
aligned_len: vec![0u8; ALIGNED_MAXSYMS + LENTABLE_SAFETY],
|
||||
pretree_table: vec![0u16; (1 << PRETREE_TABLEBITS) + PRETREE_MAXSYMS * 2],
|
||||
maintree_table: vec![0u16; (1 << MAINTREE_TABLEBITS) + MAINTREE_MAXSYMS * 2],
|
||||
length_table: vec![0u16; (1 << LENGTH_TABLEBITS) + LENGTH_MAXSYMS * 2],
|
||||
aligned_table: vec![0u16; (1 << ALIGNED_TABLEBITS) + ALIGNED_MAXSYMS * 2],
|
||||
length_table: vec![0u16; (1 << LENGTH_TABLEBITS) + LENGTH_MAXSYMS * 2],
|
||||
aligned_table: vec![0u16; (1 << ALIGNED_TABLEBITS) + ALIGNED_MAXSYMS * 2],
|
||||
length_empty: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn build_table(
|
||||
lens: &[u8], table: &mut [u16], maxsyms: usize, tablebits: usize,
|
||||
lens: &[u8],
|
||||
table: &mut [u16],
|
||||
maxsyms: usize,
|
||||
tablebits: usize,
|
||||
) -> Result<(), LzxError> {
|
||||
if make_decode_table(maxsyms, tablebits, lens, table) {
|
||||
Err(LzxError::BadHuffmanTable)
|
||||
@@ -338,7 +385,10 @@ impl LzxDecoder {
|
||||
}
|
||||
|
||||
fn build_table_maybe_empty(
|
||||
lens: &[u8], table: &mut [u16], maxsyms: usize, tablebits: usize,
|
||||
lens: &[u8],
|
||||
table: &mut [u16],
|
||||
maxsyms: usize,
|
||||
tablebits: usize,
|
||||
) -> Result<bool, LzxError> {
|
||||
if make_decode_table(maxsyms, tablebits, lens, table) {
|
||||
// Check if table is simply empty (all lengths zero)
|
||||
@@ -366,30 +416,63 @@ impl LzxDecoder {
|
||||
for i in 0..20 {
|
||||
pretree_len[i] = br.read(4) as u8;
|
||||
}
|
||||
Self::build_table(pretree_len, pretree_table, PRETREE_MAXSYMS, PRETREE_TABLEBITS)?;
|
||||
Self::build_table(
|
||||
pretree_len,
|
||||
pretree_table,
|
||||
PRETREE_MAXSYMS,
|
||||
PRETREE_TABLEBITS,
|
||||
)?;
|
||||
|
||||
let mut x = first;
|
||||
while x < last {
|
||||
let z = read_huffsym(br, pretree_table, pretree_len, PRETREE_TABLEBITS, PRETREE_MAXSYMS)?;
|
||||
let z = read_huffsym(
|
||||
br,
|
||||
pretree_table,
|
||||
pretree_len,
|
||||
PRETREE_TABLEBITS,
|
||||
PRETREE_MAXSYMS,
|
||||
)?;
|
||||
if z == 17 {
|
||||
// Run of zeros: [read 4 bits] + 4
|
||||
let mut y = br.read(4) as usize + 4;
|
||||
while y > 0 && x < last { lens[x] = 0; x += 1; y -= 1; }
|
||||
while y > 0 && x < last {
|
||||
lens[x] = 0;
|
||||
x += 1;
|
||||
y -= 1;
|
||||
}
|
||||
} else if z == 18 {
|
||||
// Run of zeros: [read 5 bits] + 20
|
||||
let mut y = br.read(5) as usize + 20;
|
||||
while y > 0 && x < last { lens[x] = 0; x += 1; y -= 1; }
|
||||
while y > 0 && x < last {
|
||||
lens[x] = 0;
|
||||
x += 1;
|
||||
y -= 1;
|
||||
}
|
||||
} else if z == 19 {
|
||||
// Run of same: [read 1 bit] + 4, then read symbol
|
||||
let mut y = br.read(1) as usize + 4;
|
||||
let z2 = read_huffsym(br, pretree_table, pretree_len, PRETREE_TABLEBITS, PRETREE_MAXSYMS)?;
|
||||
let z2 = read_huffsym(
|
||||
br,
|
||||
pretree_table,
|
||||
pretree_len,
|
||||
PRETREE_TABLEBITS,
|
||||
PRETREE_MAXSYMS,
|
||||
)?;
|
||||
let mut val = lens[x] as i32 - z2 as i32;
|
||||
if val < 0 { val += 17; }
|
||||
while y > 0 && x < last { lens[x] = val as u8; x += 1; y -= 1; }
|
||||
if val < 0 {
|
||||
val += 17;
|
||||
}
|
||||
while y > 0 && x < last {
|
||||
lens[x] = val as u8;
|
||||
x += 1;
|
||||
y -= 1;
|
||||
}
|
||||
} else {
|
||||
// Delta: code 0..16
|
||||
let mut val = lens[x] as i32 - z as i32;
|
||||
if val < 0 { val += 17; }
|
||||
if val < 0 {
|
||||
val += 17;
|
||||
}
|
||||
lens[x] = val as u8;
|
||||
x += 1;
|
||||
}
|
||||
@@ -425,13 +508,15 @@ impl LzxDecoder {
|
||||
LZX_FRAME_SIZE
|
||||
};
|
||||
|
||||
let mut bytes_todo = (self.frame_posn + frame_size).wrapping_sub(self.window_posn) as i32;
|
||||
let mut bytes_todo =
|
||||
(self.frame_posn + frame_size).wrapping_sub(self.window_posn) as i32;
|
||||
|
||||
while bytes_todo > 0 {
|
||||
// New block?
|
||||
if self.block_remaining == 0 {
|
||||
// Realign after odd UNCOMPRESSED block
|
||||
if self.block_type == LZX_BLOCKTYPE_UNCOMPRESSED && (self.block_length & 1) != 0 {
|
||||
if self.block_type == LZX_BLOCKTYPE_UNCOMPRESSED && (self.block_length & 1) != 0
|
||||
{
|
||||
br.raw_byte();
|
||||
}
|
||||
// Read block type (3 bits) and length (24 bits)
|
||||
@@ -443,33 +528,110 @@ impl LzxDecoder {
|
||||
|
||||
match self.block_type {
|
||||
LZX_BLOCKTYPE_ALIGNED => {
|
||||
for i in 0..8 { self.aligned_len[i] = br.read(3) as u8; }
|
||||
Self::build_table(&self.aligned_len, &mut self.aligned_table, ALIGNED_MAXSYMS, ALIGNED_TABLEBITS)?;
|
||||
for i in 0..8 {
|
||||
self.aligned_len[i] = br.read(3) as u8;
|
||||
}
|
||||
Self::build_table(
|
||||
&self.aligned_len,
|
||||
&mut self.aligned_table,
|
||||
ALIGNED_MAXSYMS,
|
||||
ALIGNED_TABLEBITS,
|
||||
)?;
|
||||
// Fall through to verbatim tree reading
|
||||
Self::read_lens(&mut br, &mut self.maintree_len, &mut self.pretree_len, &mut self.pretree_table, 0, 256)?;
|
||||
Self::read_lens(&mut br, &mut self.maintree_len, &mut self.pretree_len, &mut self.pretree_table, 256, LZX_NUM_CHARS + self.num_offsets)?;
|
||||
Self::build_table(&self.maintree_len, &mut self.maintree_table, MAINTREE_MAXSYMS, MAINTREE_TABLEBITS)?;
|
||||
if self.maintree_len[0xE8] != 0 { self.intel_started = true; }
|
||||
Self::read_lens(&mut br, &mut self.length_len, &mut self.pretree_len, &mut self.pretree_table, 0, LZX_NUM_SECONDARY_LENGTHS)?;
|
||||
self.length_empty = Self::build_table_maybe_empty(&self.length_len, &mut self.length_table, LENGTH_MAXSYMS, LENGTH_TABLEBITS)?;
|
||||
Self::read_lens(
|
||||
&mut br,
|
||||
&mut self.maintree_len,
|
||||
&mut self.pretree_len,
|
||||
&mut self.pretree_table,
|
||||
0,
|
||||
256,
|
||||
)?;
|
||||
Self::read_lens(
|
||||
&mut br,
|
||||
&mut self.maintree_len,
|
||||
&mut self.pretree_len,
|
||||
&mut self.pretree_table,
|
||||
256,
|
||||
LZX_NUM_CHARS + self.num_offsets,
|
||||
)?;
|
||||
Self::build_table(
|
||||
&self.maintree_len,
|
||||
&mut self.maintree_table,
|
||||
MAINTREE_MAXSYMS,
|
||||
MAINTREE_TABLEBITS,
|
||||
)?;
|
||||
if self.maintree_len[0xE8] != 0 {
|
||||
self.intel_started = true;
|
||||
}
|
||||
Self::read_lens(
|
||||
&mut br,
|
||||
&mut self.length_len,
|
||||
&mut self.pretree_len,
|
||||
&mut self.pretree_table,
|
||||
0,
|
||||
LZX_NUM_SECONDARY_LENGTHS,
|
||||
)?;
|
||||
self.length_empty = Self::build_table_maybe_empty(
|
||||
&self.length_len,
|
||||
&mut self.length_table,
|
||||
LENGTH_MAXSYMS,
|
||||
LENGTH_TABLEBITS,
|
||||
)?;
|
||||
}
|
||||
LZX_BLOCKTYPE_VERBATIM => {
|
||||
Self::read_lens(&mut br, &mut self.maintree_len, &mut self.pretree_len, &mut self.pretree_table, 0, 256)?;
|
||||
Self::read_lens(&mut br, &mut self.maintree_len, &mut self.pretree_len, &mut self.pretree_table, 256, LZX_NUM_CHARS + self.num_offsets)?;
|
||||
Self::build_table(&self.maintree_len, &mut self.maintree_table, MAINTREE_MAXSYMS, MAINTREE_TABLEBITS)?;
|
||||
if self.maintree_len[0xE8] != 0 { self.intel_started = true; }
|
||||
Self::read_lens(&mut br, &mut self.length_len, &mut self.pretree_len, &mut self.pretree_table, 0, LZX_NUM_SECONDARY_LENGTHS)?;
|
||||
self.length_empty = Self::build_table_maybe_empty(&self.length_len, &mut self.length_table, LENGTH_MAXSYMS, LENGTH_TABLEBITS)?;
|
||||
Self::read_lens(
|
||||
&mut br,
|
||||
&mut self.maintree_len,
|
||||
&mut self.pretree_len,
|
||||
&mut self.pretree_table,
|
||||
0,
|
||||
256,
|
||||
)?;
|
||||
Self::read_lens(
|
||||
&mut br,
|
||||
&mut self.maintree_len,
|
||||
&mut self.pretree_len,
|
||||
&mut self.pretree_table,
|
||||
256,
|
||||
LZX_NUM_CHARS + self.num_offsets,
|
||||
)?;
|
||||
Self::build_table(
|
||||
&self.maintree_len,
|
||||
&mut self.maintree_table,
|
||||
MAINTREE_MAXSYMS,
|
||||
MAINTREE_TABLEBITS,
|
||||
)?;
|
||||
if self.maintree_len[0xE8] != 0 {
|
||||
self.intel_started = true;
|
||||
}
|
||||
Self::read_lens(
|
||||
&mut br,
|
||||
&mut self.length_len,
|
||||
&mut self.pretree_len,
|
||||
&mut self.pretree_table,
|
||||
0,
|
||||
LZX_NUM_SECONDARY_LENGTHS,
|
||||
)?;
|
||||
self.length_empty = Self::build_table_maybe_empty(
|
||||
&self.length_len,
|
||||
&mut self.length_table,
|
||||
LENGTH_MAXSYMS,
|
||||
LENGTH_TABLEBITS,
|
||||
)?;
|
||||
}
|
||||
LZX_BLOCKTYPE_UNCOMPRESSED => {
|
||||
self.intel_started = true;
|
||||
// Align to byte boundary
|
||||
if br.left == 0 { br.ensure(16); }
|
||||
if br.left == 0 {
|
||||
br.ensure(16);
|
||||
}
|
||||
br.left = 0;
|
||||
br.buf = 0;
|
||||
// Read R0, R1, R2 (12 bytes, little-endian u32s)
|
||||
let mut buf = [0u8; 12];
|
||||
for b in &mut buf { *b = br.raw_byte(); }
|
||||
for b in &mut buf {
|
||||
*b = br.raw_byte();
|
||||
}
|
||||
self.r0 = u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]);
|
||||
self.r1 = u32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]);
|
||||
self.r2 = u32::from_le_bytes([buf[8], buf[9], buf[10], buf[11]]);
|
||||
@@ -479,7 +641,9 @@ impl LzxDecoder {
|
||||
}
|
||||
|
||||
let mut this_run = self.block_remaining as i32;
|
||||
if this_run > bytes_todo { this_run = bytes_todo; }
|
||||
if this_run > bytes_todo {
|
||||
this_run = bytes_todo;
|
||||
}
|
||||
bytes_todo -= this_run;
|
||||
self.block_remaining -= this_run as usize;
|
||||
|
||||
@@ -488,7 +652,13 @@ impl LzxDecoder {
|
||||
match self.block_type {
|
||||
LZX_BLOCKTYPE_VERBATIM => {
|
||||
while this_run > 0 {
|
||||
let main_element = read_huffsym(&mut br, &self.maintree_table, &self.maintree_len, MAINTREE_TABLEBITS, MAINTREE_MAXSYMS)?;
|
||||
let main_element = read_huffsym(
|
||||
&mut br,
|
||||
&self.maintree_table,
|
||||
&self.maintree_len,
|
||||
MAINTREE_TABLEBITS,
|
||||
MAINTREE_MAXSYMS,
|
||||
)?;
|
||||
if main_element < LZX_NUM_CHARS {
|
||||
self.window[self.window_posn] = main_element as u8;
|
||||
self.window_posn += 1;
|
||||
@@ -497,8 +667,16 @@ impl LzxDecoder {
|
||||
let me = main_element - LZX_NUM_CHARS;
|
||||
let mut match_length = me & LZX_NUM_PRIMARY_LENGTHS;
|
||||
if match_length == LZX_NUM_PRIMARY_LENGTHS {
|
||||
if self.length_empty { return Err(LzxError::Decrunch("LENGTH tree empty".into())); }
|
||||
let footer = read_huffsym(&mut br, &self.length_table, &self.length_len, LENGTH_TABLEBITS, LENGTH_MAXSYMS)?;
|
||||
if self.length_empty {
|
||||
return Err(LzxError::Decrunch("LENGTH tree empty".into()));
|
||||
}
|
||||
let footer = read_huffsym(
|
||||
&mut br,
|
||||
&self.length_table,
|
||||
&self.length_len,
|
||||
LENGTH_TABLEBITS,
|
||||
LENGTH_MAXSYMS,
|
||||
)?;
|
||||
match_length += footer;
|
||||
}
|
||||
match_length += LZX_MIN_MATCH;
|
||||
@@ -506,14 +684,34 @@ impl LzxDecoder {
|
||||
let mut match_offset = (me >> 3) as u32;
|
||||
match match_offset {
|
||||
0 => match_offset = self.r0,
|
||||
1 => { match_offset = self.r1; self.r1 = self.r0; self.r0 = match_offset; }
|
||||
2 => { match_offset = self.r2; self.r2 = self.r0; self.r0 = match_offset; }
|
||||
3 => { match_offset = 1; self.r2 = self.r1; self.r1 = self.r0; self.r0 = match_offset; }
|
||||
1 => {
|
||||
match_offset = self.r1;
|
||||
self.r1 = self.r0;
|
||||
self.r0 = match_offset;
|
||||
}
|
||||
2 => {
|
||||
match_offset = self.r2;
|
||||
self.r2 = self.r0;
|
||||
self.r0 = match_offset;
|
||||
}
|
||||
3 => {
|
||||
match_offset = 1;
|
||||
self.r2 = self.r1;
|
||||
self.r1 = self.r0;
|
||||
self.r0 = match_offset;
|
||||
}
|
||||
_ => {
|
||||
let extra = if match_offset >= 36 { 17 } else { EXTRA_BITS[match_offset as usize] as u32 };
|
||||
let extra = if match_offset >= 36 {
|
||||
17
|
||||
} else {
|
||||
EXTRA_BITS[match_offset as usize] as u32
|
||||
};
|
||||
let verbatim_bits = br.read(extra);
|
||||
match_offset = POSITION_BASE[match_offset as usize] - 2 + verbatim_bits;
|
||||
self.r2 = self.r1; self.r1 = self.r0; self.r0 = match_offset;
|
||||
match_offset = POSITION_BASE[match_offset as usize] - 2
|
||||
+ verbatim_bits;
|
||||
self.r2 = self.r1;
|
||||
self.r1 = self.r0;
|
||||
self.r0 = match_offset;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -527,7 +725,13 @@ impl LzxDecoder {
|
||||
}
|
||||
LZX_BLOCKTYPE_ALIGNED => {
|
||||
while this_run > 0 {
|
||||
let main_element = read_huffsym(&mut br, &self.maintree_table, &self.maintree_len, MAINTREE_TABLEBITS, MAINTREE_MAXSYMS)?;
|
||||
let main_element = read_huffsym(
|
||||
&mut br,
|
||||
&self.maintree_table,
|
||||
&self.maintree_len,
|
||||
MAINTREE_TABLEBITS,
|
||||
MAINTREE_MAXSYMS,
|
||||
)?;
|
||||
if main_element < LZX_NUM_CHARS {
|
||||
self.window[self.window_posn] = main_element as u8;
|
||||
self.window_posn += 1;
|
||||
@@ -536,8 +740,16 @@ impl LzxDecoder {
|
||||
let me = main_element - LZX_NUM_CHARS;
|
||||
let mut match_length = me & LZX_NUM_PRIMARY_LENGTHS;
|
||||
if match_length == LZX_NUM_PRIMARY_LENGTHS {
|
||||
if self.length_empty { return Err(LzxError::Decrunch("LENGTH tree empty".into())); }
|
||||
let footer = read_huffsym(&mut br, &self.length_table, &self.length_len, LENGTH_TABLEBITS, LENGTH_MAXSYMS)?;
|
||||
if self.length_empty {
|
||||
return Err(LzxError::Decrunch("LENGTH tree empty".into()));
|
||||
}
|
||||
let footer = read_huffsym(
|
||||
&mut br,
|
||||
&self.length_table,
|
||||
&self.length_len,
|
||||
LENGTH_TABLEBITS,
|
||||
LENGTH_MAXSYMS,
|
||||
)?;
|
||||
match_length += footer;
|
||||
}
|
||||
match_length += LZX_MIN_MATCH;
|
||||
@@ -545,18 +757,42 @@ impl LzxDecoder {
|
||||
let mut match_offset = (me >> 3) as u32;
|
||||
match match_offset {
|
||||
0 => match_offset = self.r0,
|
||||
1 => { match_offset = self.r1; self.r1 = self.r0; self.r0 = match_offset; }
|
||||
2 => { match_offset = self.r2; self.r2 = self.r0; self.r0 = match_offset; }
|
||||
1 => {
|
||||
match_offset = self.r1;
|
||||
self.r1 = self.r0;
|
||||
self.r0 = match_offset;
|
||||
}
|
||||
2 => {
|
||||
match_offset = self.r2;
|
||||
self.r2 = self.r0;
|
||||
self.r0 = match_offset;
|
||||
}
|
||||
_ => {
|
||||
let extra = if match_offset >= 36 { 17 } else { EXTRA_BITS[match_offset as usize] as u32 };
|
||||
let extra = if match_offset >= 36 {
|
||||
17
|
||||
} else {
|
||||
EXTRA_BITS[match_offset as usize] as u32
|
||||
};
|
||||
match_offset = POSITION_BASE[match_offset as usize] - 2;
|
||||
if extra > 3 {
|
||||
let verbatim_bits = br.read(extra - 3);
|
||||
match_offset += verbatim_bits << 3;
|
||||
let aligned = read_huffsym(&mut br, &self.aligned_table, &self.aligned_len, ALIGNED_TABLEBITS, ALIGNED_MAXSYMS)?;
|
||||
let aligned = read_huffsym(
|
||||
&mut br,
|
||||
&self.aligned_table,
|
||||
&self.aligned_len,
|
||||
ALIGNED_TABLEBITS,
|
||||
ALIGNED_MAXSYMS,
|
||||
)?;
|
||||
match_offset += aligned as u32;
|
||||
} else if extra == 3 {
|
||||
let aligned = read_huffsym(&mut br, &self.aligned_table, &self.aligned_len, ALIGNED_TABLEBITS, ALIGNED_MAXSYMS)?;
|
||||
let aligned = read_huffsym(
|
||||
&mut br,
|
||||
&self.aligned_table,
|
||||
&self.aligned_len,
|
||||
ALIGNED_TABLEBITS,
|
||||
ALIGNED_MAXSYMS,
|
||||
)?;
|
||||
match_offset += aligned as u32;
|
||||
} else if extra > 0 {
|
||||
let verbatim_bits = br.read(extra);
|
||||
@@ -564,7 +800,9 @@ impl LzxDecoder {
|
||||
} else {
|
||||
match_offset = 1;
|
||||
}
|
||||
self.r2 = self.r1; self.r1 = self.r0; self.r0 = match_offset;
|
||||
self.r2 = self.r1;
|
||||
self.r1 = self.r0;
|
||||
self.r0 = match_offset;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -599,7 +837,9 @@ impl LzxDecoder {
|
||||
// Frame boundary check
|
||||
if (self.window_posn.wrapping_sub(self.frame_posn)) != frame_size {
|
||||
return Err(LzxError::Decrunch(format!(
|
||||
"decode beyond frame: {} != {}", self.window_posn - self.frame_posn, frame_size
|
||||
"decode beyond frame: {} != {}",
|
||||
self.window_posn - self.frame_posn,
|
||||
frame_size
|
||||
)));
|
||||
}
|
||||
|
||||
@@ -607,8 +847,10 @@ impl LzxDecoder {
|
||||
br.align_frame();
|
||||
|
||||
// Intel E8 postprocessing
|
||||
if self.intel_started && self.intel_filesize != 0
|
||||
&& self.frame <= 32768 && frame_size > 10
|
||||
if self.intel_started
|
||||
&& self.intel_filesize != 0
|
||||
&& self.frame <= 32768
|
||||
&& frame_size > 10
|
||||
{
|
||||
let mut e8_buf = vec![0u8; frame_size];
|
||||
e8_buf.copy_from_slice(&self.window[self.frame_posn..self.frame_posn + frame_size]);
|
||||
@@ -619,18 +861,26 @@ impl LzxDecoder {
|
||||
let filesize = self.intel_filesize;
|
||||
|
||||
while i < limit {
|
||||
if e8_buf[i] != 0xE8 { i += 1; curpos += 1; continue; }
|
||||
let abs_off = e8_buf[i+1] as i32
|
||||
| (e8_buf[i+2] as i32) << 8
|
||||
| (e8_buf[i+3] as i32) << 16
|
||||
| (e8_buf[i+4] as i32) << 24;
|
||||
if e8_buf[i] != 0xE8 {
|
||||
i += 1;
|
||||
curpos += 1;
|
||||
continue;
|
||||
}
|
||||
let abs_off = e8_buf[i + 1] as i32
|
||||
| (e8_buf[i + 2] as i32) << 8
|
||||
| (e8_buf[i + 3] as i32) << 16
|
||||
| (e8_buf[i + 4] as i32) << 24;
|
||||
|
||||
if abs_off >= -curpos && abs_off < filesize {
|
||||
let rel_off = if abs_off >= 0 { abs_off - curpos } else { abs_off + filesize };
|
||||
e8_buf[i+1] = rel_off as u8;
|
||||
e8_buf[i+2] = (rel_off >> 8) as u8;
|
||||
e8_buf[i+3] = (rel_off >> 16) as u8;
|
||||
e8_buf[i+4] = (rel_off >> 24) as u8;
|
||||
let rel_off = if abs_off >= 0 {
|
||||
abs_off - curpos
|
||||
} else {
|
||||
abs_off + filesize
|
||||
};
|
||||
e8_buf[i + 1] = rel_off as u8;
|
||||
e8_buf[i + 2] = (rel_off >> 8) as u8;
|
||||
e8_buf[i + 3] = (rel_off >> 16) as u8;
|
||||
e8_buf[i + 4] = (rel_off >> 24) as u8;
|
||||
}
|
||||
i += 5;
|
||||
curpos += 5;
|
||||
@@ -641,7 +891,9 @@ impl LzxDecoder {
|
||||
output.extend_from_slice(&e8_buf[..to_write]);
|
||||
offset += to_write;
|
||||
} else {
|
||||
if self.intel_filesize != 0 { self.intel_curpos += frame_size as i32; }
|
||||
if self.intel_filesize != 0 {
|
||||
self.intel_curpos += frame_size as i32;
|
||||
}
|
||||
let to_write = frame_size.min(output_len - offset);
|
||||
output.extend_from_slice(&self.window[self.frame_posn..self.frame_posn + to_write]);
|
||||
offset += to_write;
|
||||
@@ -650,8 +902,12 @@ impl LzxDecoder {
|
||||
// Advance frame
|
||||
self.frame_posn += frame_size;
|
||||
self.frame += 1;
|
||||
if self.window_posn == self.window_size { self.window_posn = 0; }
|
||||
if self.frame_posn == self.window_size { self.frame_posn = 0; }
|
||||
if self.window_posn == self.window_size {
|
||||
self.window_posn = 0;
|
||||
}
|
||||
if self.frame_posn == self.window_size {
|
||||
self.frame_posn = 0;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(output)
|
||||
|
||||
@@ -91,7 +91,11 @@ pub fn parse_pdata(pe: &[u8], image_base: u32, sections: &[PeSection]) -> Vec<Pd
|
||||
// Image high water = image_base + the largest virtual_address+virtual_size.
|
||||
let high = sections
|
||||
.iter()
|
||||
.map(|s| image_base.wrapping_add(s.virtual_address).wrapping_add(s.virtual_size))
|
||||
.map(|s| {
|
||||
image_base
|
||||
.wrapping_add(s.virtual_address)
|
||||
.wrapping_add(s.virtual_size)
|
||||
})
|
||||
.max()
|
||||
.unwrap_or(u32::MAX);
|
||||
out.retain(|e| e.begin_address >= image_base && e.begin_address < high);
|
||||
@@ -104,7 +108,12 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::pe::PeSection;
|
||||
|
||||
fn mk_pe(image_base: u32, text_va: u32, text_size: u32, pdata: &[(u32, u32)]) -> (Vec<u8>, Vec<PeSection>) {
|
||||
fn mk_pe(
|
||||
image_base: u32,
|
||||
text_va: u32,
|
||||
text_size: u32,
|
||||
pdata: &[(u32, u32)],
|
||||
) -> (Vec<u8>, Vec<PeSection>) {
|
||||
// Build a synthetic PE image with .text and .pdata.
|
||||
// Layout: pdata at RVA 0x1000, .text at RVA 0x2000.
|
||||
let pdata_rva = 0x1000u32;
|
||||
|
||||
@@ -46,7 +46,9 @@ pub fn parse_sections(pe: &[u8]) -> anyhow::Result<Vec<PeSection>> {
|
||||
let mut sections = Vec::new();
|
||||
for i in 0..num_sections {
|
||||
let s = section_table_off + i * 40;
|
||||
if s + 40 > pe.len() { break; }
|
||||
if s + 40 > pe.len() {
|
||||
break;
|
||||
}
|
||||
|
||||
let name_bytes = &pe[s..s + 8];
|
||||
let name = std::str::from_utf8(name_bytes)
|
||||
|
||||
@@ -53,7 +53,8 @@ pub fn parse_resources(data: &[u8], header: &Xex2Header) -> Vec<XexResource> {
|
||||
if off + 4 > data.len() {
|
||||
return Vec::new();
|
||||
}
|
||||
let size = u32::from_be_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]]) as usize;
|
||||
let size =
|
||||
u32::from_be_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]]) as usize;
|
||||
// The size field counts itself; anything smaller than one record is junk.
|
||||
if size < 4 + 16 || off + size > data.len() {
|
||||
return Vec::new();
|
||||
@@ -67,7 +68,11 @@ pub fn parse_resources(data: &[u8], header: &Xex2Header) -> Vec<XexResource> {
|
||||
.to_string();
|
||||
let address = u32::from_be_bytes([data[p + 8], data[p + 9], data[p + 10], data[p + 11]]);
|
||||
let rsize = u32::from_be_bytes([data[p + 12], data[p + 13], data[p + 14], data[p + 15]]);
|
||||
out.push(XexResource { name, address, size: rsize });
|
||||
out.push(XexResource {
|
||||
name,
|
||||
address,
|
||||
size: rsize,
|
||||
});
|
||||
}
|
||||
out
|
||||
}
|
||||
@@ -94,7 +99,10 @@ mod tests {
|
||||
}
|
||||
|
||||
fn with_resource(value: u32) -> Xex2Header {
|
||||
mk_header(vec![Xex2OptionalHeader { key: header_keys::RESOURCE_INFO, value }])
|
||||
mk_header(vec![Xex2OptionalHeader {
|
||||
key: header_keys::RESOURCE_INFO,
|
||||
value,
|
||||
}])
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -60,7 +60,9 @@ pub fn parse_tls(pe: &[u8], image_base: u32, sections: &[PeSection]) -> Option<T
|
||||
// +0x14 Characteristics (4)
|
||||
let tls_section = sections.iter().find(|s| s.name == ".tls")?;
|
||||
let off = tls_section.virtual_address as usize;
|
||||
if off + 24 > pe.len() { return None; }
|
||||
if off + 24 > pe.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Xbox 360 PE bodies are big-endian; this is consistent with how we
|
||||
// parse the PE elsewhere (e.g. xref scanning reads BE u32 from PE).
|
||||
@@ -69,8 +71,8 @@ pub fn parse_tls(pe: &[u8], image_base: u32, sections: &[PeSection]) -> Option<T
|
||||
};
|
||||
|
||||
let raw_data_start = read_u32(off);
|
||||
let raw_data_end = read_u32(off + 4);
|
||||
let index_address = read_u32(off + 8);
|
||||
let raw_data_end = read_u32(off + 4);
|
||||
let index_address = read_u32(off + 8);
|
||||
let callback_array = read_u32(off + 12);
|
||||
let zero_fill_size = read_u32(off + 16);
|
||||
let characteristics = read_u32(off + 20);
|
||||
@@ -86,10 +88,14 @@ pub fn parse_tls(pe: &[u8], image_base: u32, sections: &[PeSection]) -> Option<T
|
||||
let mut p = callback_array.wrapping_sub(image_base) as usize;
|
||||
while p + 4 <= pe.len() {
|
||||
let v = read_u32(p);
|
||||
if v == 0 { break; }
|
||||
if v == 0 {
|
||||
break;
|
||||
}
|
||||
callbacks.push(TlsCallback { address: v });
|
||||
p += 4;
|
||||
if callbacks.len() >= 64 { break; } // sanity cap
|
||||
if callbacks.len() >= 64 {
|
||||
break;
|
||||
} // sanity cap
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,15 +143,22 @@ mod tests {
|
||||
let cb_va: u32 = 0x200;
|
||||
// Directory fields:
|
||||
let raw_start = 0x800u32;
|
||||
let raw_end = 0x900u32;
|
||||
let idx = 0x1000u32;
|
||||
let raw_end = 0x900u32;
|
||||
let idx = 0x1000u32;
|
||||
let zero_fill = 0x40u32;
|
||||
let chars = 0x0u32;
|
||||
let cb_array = image_base + cb_va;
|
||||
let chars = 0x0u32;
|
||||
let cb_array = image_base + cb_va;
|
||||
for (i, v) in [
|
||||
image_base + raw_start, image_base + raw_end,
|
||||
image_base + idx, cb_array, zero_fill, chars,
|
||||
].iter().enumerate() {
|
||||
image_base + raw_start,
|
||||
image_base + raw_end,
|
||||
image_base + idx,
|
||||
cb_array,
|
||||
zero_fill,
|
||||
chars,
|
||||
]
|
||||
.iter()
|
||||
.enumerate()
|
||||
{
|
||||
pe[tls_va as usize + i * 4..tls_va as usize + i * 4 + 4]
|
||||
.copy_from_slice(&v.to_be_bytes());
|
||||
}
|
||||
|
||||
@@ -121,8 +121,11 @@ impl DiscImageDevice {
|
||||
|
||||
let node_l = u16::from_le_bytes([buffer[p], buffer[p + 1]]);
|
||||
let node_r = u16::from_le_bytes([buffer[p + 2], buffer[p + 3]]);
|
||||
let sector = u32::from_le_bytes([buffer[p + 4], buffer[p + 5], buffer[p + 6], buffer[p + 7]]) as u64;
|
||||
let length = u32::from_le_bytes([buffer[p + 8], buffer[p + 9], buffer[p + 10], buffer[p + 11]]) as u64;
|
||||
let sector =
|
||||
u32::from_le_bytes([buffer[p + 4], buffer[p + 5], buffer[p + 6], buffer[p + 7]]) as u64;
|
||||
let length =
|
||||
u32::from_le_bytes([buffer[p + 8], buffer[p + 9], buffer[p + 10], buffer[p + 11]])
|
||||
as u64;
|
||||
let attributes = buffer[p + 12];
|
||||
let name_length = buffer[p + 13] as usize;
|
||||
|
||||
|
||||
@@ -34,11 +34,16 @@ fn main() {
|
||||
json_path.display()
|
||||
)
|
||||
});
|
||||
let doc: serde_json::Value = serde_json::from_str(&raw).expect("export table is not valid JSON");
|
||||
let doc: serde_json::Value =
|
||||
serde_json::from_str(&raw).expect("export table is not valid JSON");
|
||||
|
||||
let out = Path::new(&env::var("OUT_DIR").unwrap()).join("ordinals.rs");
|
||||
let mut f = fs::File::create(&out).unwrap();
|
||||
writeln!(f, "/// Auto-generated from `docs/reference/xbox360-exports.json`.").unwrap();
|
||||
writeln!(
|
||||
f,
|
||||
"/// Auto-generated from `docs/reference/xbox360-exports.json`."
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(
|
||||
f,
|
||||
"pub fn resolve_ordinal(lib: &str, ordinal: u16) -> Option<&'static str> {{"
|
||||
|
||||
@@ -12,10 +12,13 @@ use std::time::Instant;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{Parser, Subcommand, ValueEnum};
|
||||
use tracing::{debug, info, instrument, warn};
|
||||
use tracing::{info, instrument, warn};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "sylph-xexdb", about = "XEX static analysis: extract, disassemble, and build the analysis DB")]
|
||||
#[command(
|
||||
name = "sylph-xexdb",
|
||||
about = "XEX static analysis: extract, disassemble, and build the analysis DB"
|
||||
)]
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: Commands,
|
||||
@@ -39,7 +42,6 @@ enum AnalyzeMode {
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Commands {
|
||||
|
||||
/// Display XEX header information
|
||||
Info {
|
||||
/// Path to XEX file
|
||||
@@ -130,8 +132,11 @@ fn load_xex_data(path: &str) -> Result<Vec<u8>> {
|
||||
if lower.ends_with(".iso") || lower.ends_with(".xiso") {
|
||||
use sylpheed_xex::vfs::VfsDevice;
|
||||
info!("detected disc image, extracting default.xex");
|
||||
let disc = sylpheed_xex::vfs::disc_image::DiscImageDevice::open("disc", std::path::Path::new(path))
|
||||
.map_err(|e| anyhow::anyhow!("Failed to open disc image: {}", e))?;
|
||||
let disc = sylpheed_xex::vfs::disc_image::DiscImageDevice::open(
|
||||
"disc",
|
||||
std::path::Path::new(path),
|
||||
)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to open disc image: {}", e))?;
|
||||
disc.read_file("default.xex")
|
||||
.map_err(|e| anyhow::anyhow!("Failed to extract default.xex from disc image: {}", e))
|
||||
} else {
|
||||
@@ -149,9 +154,26 @@ fn main() -> Result<()> {
|
||||
Commands::Info { path } => cmd_info(&path),
|
||||
Commands::Disasm { path, count, at } => cmd_disasm(&path, count, at),
|
||||
Commands::Browse { path } => cmd_browse(&path),
|
||||
Commands::Extract { path, output, db } => cmd_extract(&path, output.as_deref(), db.as_deref()),
|
||||
Commands::Dis { path, output, db, json, analyze, max_indirect_candidates, quiet } =>
|
||||
cmd_dis(&path, output.as_deref(), db.as_deref(), json.as_deref(), analyze, max_indirect_candidates, quiet),
|
||||
Commands::Extract { path, output, db } => {
|
||||
cmd_extract(&path, output.as_deref(), db.as_deref())
|
||||
}
|
||||
Commands::Dis {
|
||||
path,
|
||||
output,
|
||||
db,
|
||||
json,
|
||||
analyze,
|
||||
max_indirect_candidates,
|
||||
quiet,
|
||||
} => cmd_dis(
|
||||
&path,
|
||||
output.as_deref(),
|
||||
db.as_deref(),
|
||||
json.as_deref(),
|
||||
analyze,
|
||||
max_indirect_candidates,
|
||||
quiet,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,12 +210,23 @@ fn cmd_info(path: &str) -> Result<()> {
|
||||
|
||||
if let Some(ref ffi) = header.file_format_info {
|
||||
println!("\n=== File Format ===");
|
||||
println!("Encryption: {}", match ffi.encryption_type {
|
||||
0 => "None", 1 => "Normal (AES)", _ => "Unknown"
|
||||
});
|
||||
println!("Compression: {}", match ffi.compression_type {
|
||||
0 => "None", 1 => "Basic", 2 => "Normal (LZX)", _ => "Unknown"
|
||||
});
|
||||
println!(
|
||||
"Encryption: {}",
|
||||
match ffi.encryption_type {
|
||||
0 => "None",
|
||||
1 => "Normal (AES)",
|
||||
_ => "Unknown",
|
||||
}
|
||||
);
|
||||
println!(
|
||||
"Compression: {}",
|
||||
match ffi.compression_type {
|
||||
0 => "None",
|
||||
1 => "Basic",
|
||||
2 => "Normal (LZX)",
|
||||
_ => "Unknown",
|
||||
}
|
||||
);
|
||||
if !ffi.basic_blocks.is_empty() {
|
||||
println!("Basic blocks: {}", ffi.basic_blocks.len());
|
||||
}
|
||||
@@ -216,18 +249,27 @@ fn cmd_info(path: &str) -> Result<()> {
|
||||
if !header.import_libraries.is_empty() {
|
||||
println!("\n=== Import Libraries ===");
|
||||
for lib in &header.import_libraries {
|
||||
println!(" {} (v{:#010x}, {} imports)", lib.name, lib.version_cur, lib.imports.len());
|
||||
println!(
|
||||
" {} (v{:#010x}, {} imports)",
|
||||
lib.name,
|
||||
lib.version_cur,
|
||||
lib.imports.len()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
info!(wall_ms = started.elapsed().as_millis() as u64, "info complete");
|
||||
info!(
|
||||
wall_ms = started.elapsed().as_millis() as u64,
|
||||
"info complete"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Clap parser for `--at` — accepts decimal, 0x-prefixed hex, or bare hex.
|
||||
fn parse_hex_u32(s: &str) -> Result<u32, String> {
|
||||
let t = s.trim();
|
||||
let (digits, radix) = if let Some(rest) = t.strip_prefix("0x").or_else(|| t.strip_prefix("0X")) {
|
||||
let (digits, radix) = if let Some(rest) = t.strip_prefix("0x").or_else(|| t.strip_prefix("0X"))
|
||||
{
|
||||
(rest, 16)
|
||||
} else if t.chars().all(|c| c.is_ascii_digit()) {
|
||||
(t, 10)
|
||||
@@ -250,14 +292,25 @@ fn cmd_disasm(path: &str, count: usize, at: Option<u32>) -> Result<()> {
|
||||
let base = sylpheed_xex::loader::get_image_base(&header)
|
||||
.ok_or_else(|| anyhow::anyhow!("No image base found in XEX2 header"))?;
|
||||
|
||||
info!(entry = format_args!("{:#010x}", entry), base = format_args!("{:#010x}", base), "XEX entry/base");
|
||||
info!(
|
||||
entry = format_args!("{:#010x}", entry),
|
||||
base = format_args!("{:#010x}", base),
|
||||
"XEX entry/base"
|
||||
);
|
||||
|
||||
let image_data = sylpheed_xex::loader::load_image(&data, &header)?;
|
||||
info!(bytes = image_data.len(), "image decompressed");
|
||||
|
||||
let start = at.unwrap_or(entry);
|
||||
let label = if at.is_some() { "requested address" } else { "entry point" };
|
||||
println!("Disassembly from {} {:#010x} ({} instructions):\n", label, start, count);
|
||||
let label = if at.is_some() {
|
||||
"requested address"
|
||||
} else {
|
||||
"entry point"
|
||||
};
|
||||
println!(
|
||||
"Disassembly from {} {:#010x} ({} instructions):\n",
|
||||
label, start, count
|
||||
);
|
||||
|
||||
if start < base {
|
||||
return Err(anyhow::anyhow!(
|
||||
@@ -281,7 +334,10 @@ fn cmd_disasm(path: &str, count: usize, at: Option<u32>) -> Result<()> {
|
||||
println!(" {:#010x}: {}", addr, text);
|
||||
}
|
||||
|
||||
info!(wall_ms = started.elapsed().as_millis() as u64, "disasm complete");
|
||||
info!(
|
||||
wall_ms = started.elapsed().as_millis() as u64,
|
||||
"disasm complete"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -290,8 +346,9 @@ fn cmd_disasm(path: &str, count: usize, at: Option<u32>) -> Result<()> {
|
||||
fn cmd_browse(path: &str) -> Result<()> {
|
||||
use sylpheed_xex::vfs::VfsDevice;
|
||||
|
||||
let disc = sylpheed_xex::vfs::disc_image::DiscImageDevice::open("disc", std::path::Path::new(path))
|
||||
.map_err(|e| anyhow::anyhow!("Failed to open disc image: {}", e))?;
|
||||
let disc =
|
||||
sylpheed_xex::vfs::disc_image::DiscImageDevice::open("disc", std::path::Path::new(path))
|
||||
.map_err(|e| anyhow::anyhow!("Failed to open disc image: {}", e))?;
|
||||
|
||||
println!("=== XISO Contents: {} ===", path);
|
||||
match disc.list_root() {
|
||||
@@ -307,15 +364,22 @@ fn cmd_browse(path: &str) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Helper: load XEX, parse header, decompress PE, resolve imports, parse sections.
|
||||
#[instrument(skip_all, fields(path = %path))]
|
||||
/// Load a XEX and prepare it for analysis.
|
||||
/// Header, decompressed image, PE sections, raw container bytes.
|
||||
type Prepared = (
|
||||
sylpheed_xex::Xex2Header,
|
||||
Vec<u8>,
|
||||
Vec<sylpheed_xex::pe::PeSection>,
|
||||
Vec<u8>,
|
||||
);
|
||||
|
||||
/// Load a XEX and prepare it for analysis: parse the header, decompress the PE,
|
||||
/// resolve imports, parse sections.
|
||||
///
|
||||
/// Returns the parsed header, the decompressed image, its sections, and the
|
||||
/// **raw XEX bytes**. The raw bytes are needed because optional-header values
|
||||
/// are file offsets into the container, not image VAs — the resource table
|
||||
/// (and so the embedded XDBF package) is only reachable through them.
|
||||
fn load_and_prepare(path: &str) -> Result<(sylpheed_xex::Xex2Header, Vec<u8>, Vec<sylpheed_xex::pe::PeSection>, Vec<u8>)> {
|
||||
/// The **raw XEX bytes** come back too, because optional-header values are file
|
||||
/// offsets into the container rather than image VAs — the resource table, and so
|
||||
/// the embedded XDBF package, is only reachable through them.
|
||||
#[instrument(skip_all, fields(path = %path))]
|
||||
fn load_and_prepare(path: &str) -> Result<Prepared> {
|
||||
let data = load_xex_data(path)?;
|
||||
let mut header = sylpheed_xex::loader::parse_xex2_header(&data)?;
|
||||
|
||||
@@ -352,7 +416,11 @@ fn cmd_extract(path: &str, output_dir: Option<&str>, db_path: Option<&str>) -> R
|
||||
|
||||
let entry = sylpheed_xex::loader::get_entry_point(&header).unwrap();
|
||||
let base = sylpheed_xex::loader::get_image_base(&header).unwrap();
|
||||
let image_size = header.security_info.as_ref().map(|s| s.image_size).unwrap_or(0);
|
||||
let image_size = header
|
||||
.security_info
|
||||
.as_ref()
|
||||
.map(|s| s.image_size)
|
||||
.unwrap_or(0);
|
||||
|
||||
// Build JSON-serializable info struct
|
||||
#[derive(Serialize)]
|
||||
@@ -382,11 +450,15 @@ fn cmd_extract(path: &str, output_dir: Option<&str>, db_path: Option<&str>) -> R
|
||||
let input_path = std::path::Path::new(path);
|
||||
let out_dir = match output_dir {
|
||||
Some(d) => std::path::PathBuf::from(d),
|
||||
None => input_path.parent().unwrap_or(std::path::Path::new(".")).to_path_buf(),
|
||||
None => input_path
|
||||
.parent()
|
||||
.unwrap_or(std::path::Path::new("."))
|
||||
.to_path_buf(),
|
||||
};
|
||||
std::fs::create_dir_all(&out_dir)?;
|
||||
|
||||
let stem = input_path.file_stem()
|
||||
let stem = input_path
|
||||
.file_stem()
|
||||
.and_then(|s| s.to_str())
|
||||
.unwrap_or("output");
|
||||
|
||||
@@ -406,11 +478,22 @@ fn cmd_extract(path: &str, output_dir: Option<&str>, db_path: Option<&str>) -> R
|
||||
info!(path = %json_path.display(), "wrote metadata JSON");
|
||||
|
||||
// Print summary
|
||||
let total_imports: usize = header.import_libraries.iter().map(|l| l.imports.len()).sum();
|
||||
println!("Extracted: {} sections, {} import libraries ({} imports)",
|
||||
sections.len(), header.import_libraries.len(), total_imports);
|
||||
let total_imports: usize = header
|
||||
.import_libraries
|
||||
.iter()
|
||||
.map(|l| l.imports.len())
|
||||
.sum();
|
||||
println!(
|
||||
"Extracted: {} sections, {} import libraries ({} imports)",
|
||||
sections.len(),
|
||||
header.import_libraries.len(),
|
||||
total_imports
|
||||
);
|
||||
if let Some(ref ei) = header.execution_info {
|
||||
println!("Title ID: 0x{:08X} Media ID: 0x{:08X}", ei.title_id, ei.media_id);
|
||||
println!(
|
||||
"Title ID: 0x{:08X} Media ID: 0x{:08X}",
|
||||
ei.title_id, ei.media_id
|
||||
);
|
||||
}
|
||||
|
||||
// Write base tables to SQLite if requested
|
||||
@@ -468,27 +551,38 @@ fn cmd_dis(
|
||||
info!(thunks = import_map.len(), "resolved import thunks");
|
||||
|
||||
// Function analysis (with .pdata-validated boundaries when present)
|
||||
let code_sections: Vec<(u32, u32, u32)> = sections.iter()
|
||||
let code_sections: Vec<(u32, u32, u32)> = sections
|
||||
.iter()
|
||||
.filter(|s| s.is_code())
|
||||
.map(|s| (s.virtual_address, s.virtual_size, s.flags))
|
||||
.collect();
|
||||
let pdata_entries = sylpheed_xex::pdata::parse_pdata(&pe_image, base, §ions);
|
||||
info!(pdata_entries = pdata_entries.len(), "parsed .pdata RUNTIME_FUNCTION entries");
|
||||
info!(
|
||||
pdata_entries = pdata_entries.len(),
|
||||
"parsed .pdata RUNTIME_FUNCTION entries"
|
||||
);
|
||||
let func_analysis = sylpheed_xexdb::func::analyze_with_pdata(
|
||||
&pe_image, base, entry, &code_sections, &pdata_entries,
|
||||
&pe_image,
|
||||
base,
|
||||
entry,
|
||||
&code_sections,
|
||||
&pdata_entries,
|
||||
);
|
||||
info!(
|
||||
functions = func_analysis.functions.len(),
|
||||
pdata_validated = func_analysis.functions.values().filter(|f| f.pdata_validated).count(),
|
||||
pdata_validated = func_analysis
|
||||
.functions
|
||||
.values()
|
||||
.filter(|f| f.pdata_validated)
|
||||
.count(),
|
||||
"function detection complete",
|
||||
);
|
||||
|
||||
// M12 — switch / jump-table recovery. Emits one `jt` xref per distinct
|
||||
// case body so the case bodies stop looking unreachable, and reports the
|
||||
// table extents so the linear disassembler can flag them as data.
|
||||
let jump_tables = sylpheed_xexdb::jumptables::analyze(
|
||||
&pe_image, base, §ions, &func_analysis,
|
||||
);
|
||||
let jump_tables =
|
||||
sylpheed_xexdb::jumptables::analyze(&pe_image, base, §ions, &func_analysis);
|
||||
let jt_data_words = sylpheed_xexdb::jumptables::data_word_addresses(&jump_tables);
|
||||
info!(
|
||||
jump_tables = jump_tables.len(),
|
||||
@@ -499,7 +593,13 @@ fn cmd_dis(
|
||||
|
||||
// Cross-reference analysis
|
||||
let mut xref_result = sylpheed_xexdb::xref::analyze_xrefs_skipping(
|
||||
&pe_image, base, entry, §ions, &func_analysis, &import_map, &jt_data_words,
|
||||
&pe_image,
|
||||
base,
|
||||
entry,
|
||||
§ions,
|
||||
&func_analysis,
|
||||
&import_map,
|
||||
&jt_data_words,
|
||||
);
|
||||
|
||||
// Feed the recovered `switch` edges into the xref graph, so case bodies
|
||||
@@ -507,7 +607,8 @@ fn cmd_dis(
|
||||
let mut jt_edges = 0usize;
|
||||
for jt in &jump_tables {
|
||||
for target in jt.distinct_targets() {
|
||||
xref_result.xrefs
|
||||
xref_result
|
||||
.xrefs
|
||||
.entry(target)
|
||||
.or_default()
|
||||
.push(sylpheed_xexdb::xref::Xref {
|
||||
@@ -515,12 +616,14 @@ fn cmd_dis(
|
||||
kind: sylpheed_xexdb::xref::XrefKind::JumpTable,
|
||||
addr_mode: None,
|
||||
});
|
||||
xref_result.labels
|
||||
xref_result
|
||||
.labels
|
||||
.entry(target)
|
||||
.or_insert_with(|| format!("case_{target:08X}"));
|
||||
jt_edges += 1;
|
||||
}
|
||||
xref_result.labels
|
||||
xref_result
|
||||
.labels
|
||||
.entry(jt.table_address)
|
||||
.or_insert_with(|| format!("jpt_{:08X}", jt.table_address));
|
||||
}
|
||||
@@ -548,9 +651,16 @@ fn cmd_dis(
|
||||
let vptr_block_boundaries: std::collections::HashSet<u32> =
|
||||
xref_result.labels.keys().copied().collect();
|
||||
let mut vtable_anchors = sylpheed_xexdb::vtables::scan_vptr_write_constants(
|
||||
&pe_image, base, &vptr_anchor_funcs, §ions, &vptr_block_boundaries,
|
||||
&pe_image,
|
||||
base,
|
||||
&vptr_anchor_funcs,
|
||||
§ions,
|
||||
&vptr_block_boundaries,
|
||||
);
|
||||
info!(
|
||||
vtable_anchors = vtable_anchors.len(),
|
||||
"vptr-write anchor scan complete"
|
||||
);
|
||||
info!(vtable_anchors = vtable_anchors.len(), "vptr-write anchor scan complete");
|
||||
|
||||
// M13 — authoritative MSVC RTTI walk. Every `vftable[-1] -> COL` link the
|
||||
// linker emitted is an anchor the heuristic scan must not miss, and the
|
||||
@@ -566,7 +676,11 @@ fn cmd_dis(
|
||||
);
|
||||
|
||||
let mut vtables = sylpheed_xexdb::vtables::analyze_with_anchors(
|
||||
&pe_image, base, §ions, &function_starts, &vtable_anchors,
|
||||
&pe_image,
|
||||
base,
|
||||
§ions,
|
||||
&function_starts,
|
||||
&vtable_anchors,
|
||||
);
|
||||
let named = sylpheed_xexdb::vtables::apply_rtti_names(&mut vtables, &rtti);
|
||||
let vtables = vtables;
|
||||
@@ -584,11 +698,19 @@ fn cmd_dis(
|
||||
// emits one xref edge per resolvable site. Inserted into xrefs as
|
||||
// kind='ind_call'.
|
||||
let indirect_edges = sylpheed_xexdb::indirect::analyze(
|
||||
&pe_image, base, &func_analysis, &vtables, &xref_result.labels,
|
||||
&pe_image,
|
||||
base,
|
||||
&func_analysis,
|
||||
&vtables,
|
||||
&xref_result.labels,
|
||||
);
|
||||
info!(
|
||||
indirect_edges = indirect_edges.len(),
|
||||
"indirect-dispatch scan complete"
|
||||
);
|
||||
info!(indirect_edges = indirect_edges.len(), "indirect-dispatch scan complete");
|
||||
for edge in &indirect_edges {
|
||||
xref_result.xrefs
|
||||
xref_result
|
||||
.xrefs
|
||||
.entry(edge.target)
|
||||
.or_default()
|
||||
.push(sylpheed_xexdb::xref::Xref {
|
||||
@@ -613,14 +735,22 @@ fn cmd_dis(
|
||||
// Generic function-pointer-array scan (M8 + M11). Re-emits M3 vtables
|
||||
// plus dispatch tables and static-init tables in `.rdata`.
|
||||
let mut fparrays = sylpheed_xexdb::funcptr_arrays::analyze(
|
||||
&pe_image, base, §ions, &function_starts, &vtables,
|
||||
&pe_image,
|
||||
base,
|
||||
§ions,
|
||||
&function_starts,
|
||||
&vtables,
|
||||
);
|
||||
|
||||
// M11.5 — static-init driver chain detection. Replaces M11's prologue
|
||||
// heuristic with a structurally-grounded result where the driver
|
||||
// function shape matches.
|
||||
let static_init = sylpheed_xexdb::static_init::analyze(
|
||||
&pe_image, base, §ions, &func_analysis, &function_starts,
|
||||
&pe_image,
|
||||
base,
|
||||
§ions,
|
||||
&func_analysis,
|
||||
&function_starts,
|
||||
&xref_result.labels,
|
||||
);
|
||||
info!(
|
||||
@@ -639,7 +769,10 @@ fn cmd_dis(
|
||||
}
|
||||
info!(
|
||||
funcptr_arrays = fparrays.len(),
|
||||
dispatch_tables = fparrays.iter().filter(|a| a.kind == "dispatch_table").count(),
|
||||
dispatch_tables = fparrays
|
||||
.iter()
|
||||
.filter(|a| a.kind == "dispatch_table")
|
||||
.count(),
|
||||
static_inits = fparrays.iter().filter(|a| a.kind == "static_init").count(),
|
||||
"function-pointer array set finalised",
|
||||
);
|
||||
@@ -655,12 +788,24 @@ fn cmd_dis(
|
||||
|
||||
// M5.5 — typed indirect-dispatch resolution (this->vptr → method).
|
||||
let typed_ind = sylpheed_xexdb::ind_dispatch_typed::analyze(
|
||||
&pe_image, base, &func_analysis, &vtables, &xref_result.labels,
|
||||
&pe_image,
|
||||
base,
|
||||
&func_analysis,
|
||||
&vtables,
|
||||
&xref_result.labels,
|
||||
max_indirect_candidates,
|
||||
);
|
||||
let single = typed_ind.dispatches.iter().filter(|d| d.total_candidates == 1).count();
|
||||
let single = typed_ind
|
||||
.dispatches
|
||||
.iter()
|
||||
.filter(|d| d.total_candidates == 1)
|
||||
.count();
|
||||
let multi = typed_ind.dispatches.len() - single;
|
||||
let typed_edges: usize = typed_ind.dispatches.iter().map(|d| d.method_pcs.len()).sum();
|
||||
let typed_edges: usize = typed_ind
|
||||
.dispatches
|
||||
.iter()
|
||||
.map(|d| d.method_pcs.len())
|
||||
.sum();
|
||||
info!(
|
||||
vptr_writes = typed_ind.vptr_writes.len(),
|
||||
dispatches = typed_ind.dispatches.len(),
|
||||
@@ -675,7 +820,8 @@ fn cmd_dis(
|
||||
// possibilities.
|
||||
for d in &typed_ind.dispatches {
|
||||
for &method_pc in &d.method_pcs {
|
||||
xref_result.xrefs
|
||||
xref_result
|
||||
.xrefs
|
||||
.entry(method_pc)
|
||||
.or_default()
|
||||
.push(sylpheed_xexdb::xref::Xref {
|
||||
@@ -707,7 +853,10 @@ fn cmd_dis(
|
||||
Some(x)
|
||||
});
|
||||
if xdbf.is_none() && !resources.is_empty() {
|
||||
info!(resources = resources.len(), "resource table present but no XDBF package");
|
||||
info!(
|
||||
resources = resources.len(),
|
||||
"resource table present but no XDBF package"
|
||||
);
|
||||
}
|
||||
|
||||
// Build DisasmInfo
|
||||
@@ -728,7 +877,11 @@ fn cmd_dis(
|
||||
let mut w = sylpheed_xexdb::DbWriter::open_fresh(std::path::Path::new(db))?;
|
||||
w.write_base(&disasm_info)?;
|
||||
w.ingest_instructions(
|
||||
&pe_image, &disasm_info, &func_analysis, &xref_result.labels, &jt_data_words,
|
||||
&pe_image,
|
||||
&disasm_info,
|
||||
&func_analysis,
|
||||
&xref_result.labels,
|
||||
&jt_data_words,
|
||||
)?;
|
||||
w.write_analysis_results(
|
||||
&pe_image,
|
||||
@@ -772,12 +925,20 @@ fn cmd_dis(
|
||||
let mut out = std::io::BufWriter::new(std::fs::File::create(json)?);
|
||||
let mut total: u64 = 0;
|
||||
for section in §ions {
|
||||
if !section.is_code() { continue; }
|
||||
if !section.is_code() {
|
||||
continue;
|
||||
}
|
||||
let abs_start = base + section.virtual_address;
|
||||
let abs_end = abs_start + section.virtual_size;
|
||||
let items = sylpheed_xexdb::enrich_section(
|
||||
&pe_image, base, §ion.name, abs_start, abs_end,
|
||||
&func_analysis, &xref_result.labels, &jt_data_words,
|
||||
&pe_image,
|
||||
base,
|
||||
§ion.name,
|
||||
abs_start,
|
||||
abs_end,
|
||||
&func_analysis,
|
||||
&xref_result.labels,
|
||||
&jt_data_words,
|
||||
);
|
||||
total += sylpheed_xexdb::sinks::json::write_jsonl(&mut out, items)?;
|
||||
}
|
||||
@@ -808,11 +969,13 @@ fn cmd_dis(
|
||||
}
|
||||
}
|
||||
|
||||
info!(wall_ms = started.elapsed().as_millis() as u64, "dis complete");
|
||||
info!(
|
||||
wall_ms = started.elapsed().as_millis() as u64,
|
||||
"dis complete"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::parse_hex_u32;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -345,7 +345,10 @@ mod rtti_name_tests {
|
||||
|
||||
#[test]
|
||||
fn plain_class_in_namespace() {
|
||||
assert_eq!(demangle_type_descriptor(".?AVSilph@silph@@").as_deref(), Some("silph::Silph"));
|
||||
assert_eq!(
|
||||
demangle_type_descriptor(".?AVSilph@silph@@").as_deref(),
|
||||
Some("silph::Silph")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -358,12 +361,16 @@ mod rtti_name_tests {
|
||||
|
||||
#[test]
|
||||
fn global_scope_class() {
|
||||
assert_eq!(demangle_type_descriptor(".?AVexception@std@@").as_deref(), Some("std::exception"));
|
||||
assert_eq!(
|
||||
demangle_type_descriptor(".?AVexception@std@@").as_deref(),
|
||||
Some("std::exception")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn anonymous_namespace_is_named() {
|
||||
let got = demangle_type_descriptor(".?AVAct_Stop@?A0x5cc05762@unnamed_namespaces@@").unwrap();
|
||||
let got =
|
||||
demangle_type_descriptor(".?AVAct_Stop@?A0x5cc05762@unnamed_namespaces@@").unwrap();
|
||||
assert!(got.ends_with("Act_Stop"), "got {got}");
|
||||
assert!(got.starts_with("unnamed_namespaces"), "got {got}");
|
||||
}
|
||||
|
||||
@@ -86,10 +86,16 @@ mod tests {
|
||||
|
||||
fn fi(start: u32, end: u32) -> FuncInfo {
|
||||
FuncInfo {
|
||||
start, end,
|
||||
frame_size: 0, saved_gprs: 0, is_leaf: true, is_saverestore: false,
|
||||
pdata_validated: true, pdata_length: Some(end - start),
|
||||
pdata_prolog_length: None, has_eh: false,
|
||||
start,
|
||||
end,
|
||||
frame_size: 0,
|
||||
saved_gprs: 0,
|
||||
is_leaf: true,
|
||||
is_saverestore: false,
|
||||
pdata_validated: true,
|
||||
pdata_length: Some(end - start),
|
||||
pdata_prolog_length: None,
|
||||
has_eh: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,18 +119,29 @@ mod tests {
|
||||
let labels = HashMap::new();
|
||||
let data_words = BTreeSet::new();
|
||||
let got: Vec<(u32, Option<u32>)> = enrich_section(
|
||||
&image, image_base, ".text", image_base, image_base + 24,
|
||||
&fa, &labels, &data_words,
|
||||
).map(|r| (r.item.addr, r.function)).collect();
|
||||
&image,
|
||||
image_base,
|
||||
".text",
|
||||
image_base,
|
||||
image_base + 24,
|
||||
&fa,
|
||||
&labels,
|
||||
&data_words,
|
||||
)
|
||||
.map(|r| (r.item.addr, r.function))
|
||||
.collect();
|
||||
|
||||
assert_eq!(got, vec![
|
||||
(image_base, Some(image_base)), // inside f0
|
||||
(image_base + 4, Some(image_base)), // inside f0
|
||||
(image_base + 8, None), // gap — was wrongly f0
|
||||
(image_base + 12, None), // gap — was wrongly f0
|
||||
(image_base + 16, Some(image_base + 16)), // f1 starts
|
||||
(image_base + 20, Some(image_base + 16)),
|
||||
]);
|
||||
assert_eq!(
|
||||
got,
|
||||
vec![
|
||||
(image_base, Some(image_base)), // inside f0
|
||||
(image_base + 4, Some(image_base)), // inside f0
|
||||
(image_base + 8, None), // gap — was wrongly f0
|
||||
(image_base + 12, None), // gap — was wrongly f0
|
||||
(image_base + 16, Some(image_base + 16)), // f1 starts
|
||||
(image_base + 20, Some(image_base + 16)),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/// A function starting exactly at its predecessor's `end_address` must be
|
||||
@@ -137,18 +154,33 @@ mod tests {
|
||||
functions.insert(image_base, fi(image_base, image_base + 8));
|
||||
functions.insert(image_base + 8, fi(image_base + 8, image_base + 16));
|
||||
let fa = FuncAnalysis {
|
||||
functions, save_gpr_base: None, restore_gpr_base: None,
|
||||
functions,
|
||||
save_gpr_base: None,
|
||||
restore_gpr_base: None,
|
||||
pdata_entries: Vec::new(),
|
||||
};
|
||||
let labels = HashMap::new();
|
||||
let data_words = BTreeSet::new();
|
||||
let got: Vec<Option<u32>> = enrich_section(
|
||||
&image, image_base, ".text", image_base, image_base + 16,
|
||||
&fa, &labels, &data_words,
|
||||
).map(|r| r.function).collect();
|
||||
assert_eq!(got, vec![
|
||||
Some(image_base), Some(image_base),
|
||||
Some(image_base + 8), Some(image_base + 8),
|
||||
]);
|
||||
&image,
|
||||
image_base,
|
||||
".text",
|
||||
image_base,
|
||||
image_base + 16,
|
||||
&fa,
|
||||
&labels,
|
||||
&data_words,
|
||||
)
|
||||
.map(|r| r.function)
|
||||
.collect();
|
||||
assert_eq!(
|
||||
got,
|
||||
vec![
|
||||
Some(image_base),
|
||||
Some(image_base),
|
||||
Some(image_base + 8),
|
||||
Some(image_base + 8),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,9 +49,9 @@
|
||||
|
||||
use sylpheed_xex::pe::PeSection;
|
||||
|
||||
const MAGIC_OLD: u32 = 0x1993_0520;
|
||||
const MAGIC_V21: u32 = 0x1993_0521;
|
||||
const MAGIC_V22: u32 = 0x1993_0522;
|
||||
const MAGIC_OLD: u32 = 0x1993_0520;
|
||||
const MAGIC_V21: u32 = 0x1993_0521;
|
||||
const MAGIC_V22: u32 = 0x1993_0522;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct UnwindMapEntry {
|
||||
@@ -85,41 +85,56 @@ pub struct EhFuncInfo {
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all, fields(image_base = format_args!("{:#010x}", image_base)))]
|
||||
pub fn analyze(
|
||||
pe: &[u8],
|
||||
image_base: u32,
|
||||
sections: &[PeSection],
|
||||
) -> Vec<EhFuncInfo> {
|
||||
pub fn analyze(pe: &[u8], image_base: u32, sections: &[PeSection]) -> Vec<EhFuncInfo> {
|
||||
let started = std::time::Instant::now();
|
||||
let mut out: Vec<EhFuncInfo> = Vec::new();
|
||||
|
||||
// Compute the union of valid VA ranges across all sections — used to
|
||||
// sanity-check internal pointers in the FuncInfo records.
|
||||
let valid_ranges: Vec<(u32, u32)> = sections.iter()
|
||||
.map(|s| (image_base + s.virtual_address,
|
||||
image_base + s.virtual_address + s.virtual_size))
|
||||
let valid_ranges: Vec<(u32, u32)> = sections
|
||||
.iter()
|
||||
.map(|s| {
|
||||
(
|
||||
image_base + s.virtual_address,
|
||||
image_base + s.virtual_address + s.virtual_size,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let in_valid = |va: u32| valid_ranges.iter().any(|(lo, hi)| va >= *lo && va < *hi);
|
||||
|
||||
let read_u32 = |abs: u32| -> Option<u32> {
|
||||
let off = abs.wrapping_sub(image_base) as usize;
|
||||
if off + 4 > pe.len() { return None; }
|
||||
Some(u32::from_be_bytes([pe[off], pe[off + 1], pe[off + 2], pe[off + 3]]))
|
||||
if off + 4 > pe.len() {
|
||||
return None;
|
||||
}
|
||||
Some(u32::from_be_bytes([
|
||||
pe[off],
|
||||
pe[off + 1],
|
||||
pe[off + 2],
|
||||
pe[off + 3],
|
||||
]))
|
||||
};
|
||||
let read_i32 = |abs: u32| -> Option<i32> { read_u32(abs).map(|u| u as i32) };
|
||||
|
||||
for section in sections {
|
||||
if section.name != ".rdata" { continue; }
|
||||
if section.name != ".rdata" {
|
||||
continue;
|
||||
}
|
||||
let raw_start = section.virtual_address as usize;
|
||||
let raw_end = (section.virtual_address + section.virtual_size) as usize;
|
||||
if raw_end > pe.len() { continue; }
|
||||
if raw_end > pe.len() {
|
||||
continue;
|
||||
}
|
||||
let bytes = &pe[raw_start..raw_end.min(pe.len())];
|
||||
let va_base = image_base + section.virtual_address;
|
||||
|
||||
// Walk on 4-byte alignment looking for the magic.
|
||||
let mut i = 0;
|
||||
while i + 4 <= bytes.len() {
|
||||
if !i.is_multiple_of(4) { i += 1; continue; }
|
||||
if !i.is_multiple_of(4) {
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
let m = u32::from_be_bytes([bytes[i], bytes[i + 1], bytes[i + 2], bytes[i + 3]]);
|
||||
if m == MAGIC_OLD || m == MAGIC_V21 || m == MAGIC_V22 {
|
||||
let addr = va_base + i as u32;
|
||||
@@ -152,23 +167,35 @@ fn parse_funcinfo(
|
||||
read_i32: &impl Fn(u32) -> Option<i32>,
|
||||
in_valid: &impl Fn(u32) -> bool,
|
||||
) -> Option<EhFuncInfo> {
|
||||
let max_state = read_i32(addr + 0x04)?;
|
||||
let p_unwind_map = read_u32(addr + 0x08)?;
|
||||
let n_try_blocks = read_u32(addr + 0x0C)?;
|
||||
let p_try_block_map = read_u32(addr + 0x10)?;
|
||||
let n_ip_map_entries = read_u32(addr + 0x14)?;
|
||||
let max_state = read_i32(addr + 0x04)?;
|
||||
let p_unwind_map = read_u32(addr + 0x08)?;
|
||||
let n_try_blocks = read_u32(addr + 0x0C)?;
|
||||
let p_try_block_map = read_u32(addr + 0x10)?;
|
||||
let n_ip_map_entries = read_u32(addr + 0x14)?;
|
||||
let p_ip_to_state_map = read_u32(addr + 0x18)?;
|
||||
|
||||
// Sanity caps: real FuncInfo records have max_state ≤ a few thousand,
|
||||
// n_try_blocks ≤ a few hundred. Reject obviously bogus values that
|
||||
// happened to alias the magic.
|
||||
if !(0..=10_000).contains(&max_state) { return None; }
|
||||
if n_try_blocks > 1_000 { return None; }
|
||||
if n_ip_map_entries > 100_000 { return None; }
|
||||
if !(0..=10_000).contains(&max_state) {
|
||||
return None;
|
||||
}
|
||||
if n_try_blocks > 1_000 {
|
||||
return None;
|
||||
}
|
||||
if n_ip_map_entries > 100_000 {
|
||||
return None;
|
||||
}
|
||||
// Pointers must either be NULL or land in a valid section.
|
||||
if p_unwind_map != 0 && !in_valid(p_unwind_map) { return None; }
|
||||
if p_try_block_map != 0 && !in_valid(p_try_block_map) { return None; }
|
||||
if p_ip_to_state_map != 0 && !in_valid(p_ip_to_state_map) { return None; }
|
||||
if p_unwind_map != 0 && !in_valid(p_unwind_map) {
|
||||
return None;
|
||||
}
|
||||
if p_try_block_map != 0 && !in_valid(p_try_block_map) {
|
||||
return None;
|
||||
}
|
||||
if p_ip_to_state_map != 0 && !in_valid(p_ip_to_state_map) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let (p_es_type_list, eh_flags) = if magic == MAGIC_V21 {
|
||||
(read_u32(addr + 0x1C), None)
|
||||
@@ -185,7 +212,10 @@ fn parse_funcinfo(
|
||||
let p = p_unwind_map.wrapping_add((i * 8) as u32);
|
||||
let to_state = read_i32(p)?;
|
||||
let action_pc = read_u32(p + 4)?;
|
||||
unwind_map.push(UnwindMapEntry { to_state, action_pc });
|
||||
unwind_map.push(UnwindMapEntry {
|
||||
to_state,
|
||||
action_pc,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,13 +224,17 @@ fn parse_funcinfo(
|
||||
if p_try_block_map != 0 && n_try_blocks > 0 {
|
||||
for i in 0..n_try_blocks {
|
||||
let p = p_try_block_map.wrapping_add(i * 20);
|
||||
let try_low = read_i32(p)?;
|
||||
let try_high = read_i32(p + 4)?;
|
||||
let catch_high = read_i32(p + 8)?;
|
||||
let n_catches = read_u32(p + 12)?;
|
||||
let p_handler_a = read_u32(p + 16)?;
|
||||
let try_low = read_i32(p)?;
|
||||
let try_high = read_i32(p + 4)?;
|
||||
let catch_high = read_i32(p + 8)?;
|
||||
let n_catches = read_u32(p + 12)?;
|
||||
let p_handler_a = read_u32(p + 16)?;
|
||||
try_blocks.push(TryBlockMapEntry {
|
||||
try_low, try_high, catch_high, n_catches, p_handler_array: p_handler_a,
|
||||
try_low,
|
||||
try_high,
|
||||
catch_high,
|
||||
n_catches,
|
||||
p_handler_array: p_handler_a,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -229,8 +263,10 @@ mod tests {
|
||||
fn mk_section(name: &str, va: u32, size: u32) -> PeSection {
|
||||
PeSection {
|
||||
name: name.into(),
|
||||
virtual_address: va, virtual_size: size,
|
||||
raw_offset: va, raw_size: size,
|
||||
virtual_address: va,
|
||||
virtual_size: size,
|
||||
raw_offset: va,
|
||||
raw_size: size,
|
||||
flags: 0x4000_0040,
|
||||
}
|
||||
}
|
||||
@@ -254,17 +290,17 @@ mod tests {
|
||||
let unwind_off = (rdata_va + 0x80) as usize;
|
||||
let unwind_va = image_base + rdata_va + 0x80;
|
||||
|
||||
write_be(&mut pe, fi_off, MAGIC_OLD); // magic
|
||||
write_be_i32(&mut pe, fi_off + 4, 2); // maxState
|
||||
write_be(&mut pe, fi_off + 8, unwind_va); // pUnwindMap
|
||||
write_be(&mut pe, fi_off + 12, 0); // nTryBlocks
|
||||
write_be(&mut pe, fi_off + 16, 0); // pTryBlockMap
|
||||
write_be(&mut pe, fi_off + 20, 0); // nIPMapEntries
|
||||
write_be(&mut pe, fi_off + 24, 0); // pIPtoStateMap
|
||||
write_be(&mut pe, fi_off, MAGIC_OLD); // magic
|
||||
write_be_i32(&mut pe, fi_off + 4, 2); // maxState
|
||||
write_be(&mut pe, fi_off + 8, unwind_va); // pUnwindMap
|
||||
write_be(&mut pe, fi_off + 12, 0); // nTryBlocks
|
||||
write_be(&mut pe, fi_off + 16, 0); // pTryBlockMap
|
||||
write_be(&mut pe, fi_off + 20, 0); // nIPMapEntries
|
||||
write_be(&mut pe, fi_off + 24, 0); // pIPtoStateMap
|
||||
|
||||
// Two unwind entries.
|
||||
write_be_i32(&mut pe, unwind_off, -1); // to_state
|
||||
write_be(&mut pe, unwind_off + 4, image_base + 0x500); // action_pc
|
||||
write_be_i32(&mut pe, unwind_off, -1); // to_state
|
||||
write_be(&mut pe, unwind_off + 4, image_base + 0x500); // action_pc
|
||||
write_be_i32(&mut pe, unwind_off + 8, 0);
|
||||
write_be(&mut pe, unwind_off + 12, image_base + 0x600);
|
||||
|
||||
@@ -288,7 +324,7 @@ mod tests {
|
||||
let mut pe = vec![0u8; 0x4000];
|
||||
let fi_off = (rdata_va + 0x10) as usize;
|
||||
write_be(&mut pe, fi_off, MAGIC_OLD);
|
||||
write_be_i32(&mut pe, fi_off + 4, 0xFFFF); // bogus maxState
|
||||
write_be_i32(&mut pe, fi_off + 4, 0xFFFF); // bogus maxState
|
||||
let sections = vec![mk_section(".rdata", rdata_va, 0x100)];
|
||||
let recs = analyze(&pe, image_base, §ions);
|
||||
assert_eq!(recs.len(), 0);
|
||||
|
||||
@@ -9,7 +9,7 @@ use sylpheed_xex::pe::PeSection;
|
||||
use crate::disasm::enrich_section;
|
||||
use crate::func::FuncAnalysis;
|
||||
use crate::sinks::text::write_instr_line;
|
||||
use crate::xref::{XrefKind, Xref, XrefMap, resolve_source_label};
|
||||
use crate::xref::{Xref, XrefKind, XrefMap, resolve_source_label};
|
||||
|
||||
/// Metadata passed to the formatter (avoids exposing full Xex2Header internals).
|
||||
pub struct DisasmInfo<'a> {
|
||||
@@ -40,29 +40,53 @@ pub fn write_asm(
|
||||
data_words: &BTreeSet<u32>,
|
||||
) -> anyhow::Result<()> {
|
||||
// Header
|
||||
writeln!(out, "; ============================================================================")?;
|
||||
writeln!(
|
||||
out,
|
||||
"; ============================================================================"
|
||||
)?;
|
||||
writeln!(out, "; Xbox 360 Disassembly — generated by xenia-rs")?;
|
||||
if let Some(name) = info.original_pe_name {
|
||||
writeln!(out, "; Original PE: {name}")?;
|
||||
}
|
||||
if let (Some(title_id), Some(media_id)) = (info.title_id, info.media_id) {
|
||||
writeln!(out, "; Title ID: 0x{title_id:08X} Media ID: 0x{media_id:08X}")?;
|
||||
writeln!(
|
||||
out,
|
||||
"; Title ID: 0x{title_id:08X} Media ID: 0x{media_id:08X}"
|
||||
)?;
|
||||
}
|
||||
writeln!(out, "; Image base: 0x{:08X} Entry point: 0x{:08X}", info.image_base, info.entry_point)?;
|
||||
writeln!(out, "; Functions detected: {}", func_analysis.functions.len())?;
|
||||
writeln!(out, "; ============================================================================")?;
|
||||
writeln!(
|
||||
out,
|
||||
"; Image base: 0x{:08X} Entry point: 0x{:08X}",
|
||||
info.image_base, info.entry_point
|
||||
)?;
|
||||
writeln!(
|
||||
out,
|
||||
"; Functions detected: {}",
|
||||
func_analysis.functions.len()
|
||||
)?;
|
||||
writeln!(
|
||||
out,
|
||||
"; ============================================================================"
|
||||
)?;
|
||||
writeln!(out)?;
|
||||
|
||||
// Import declarations
|
||||
if !info.import_libraries.is_empty() {
|
||||
writeln!(out, "; ── Imports ─────────────────────────────────────────────────────────────────")?;
|
||||
writeln!(
|
||||
out,
|
||||
"; ── Imports ─────────────────────────────────────────────────────────────────"
|
||||
)?;
|
||||
for lib in info.import_libraries {
|
||||
writeln!(out, "; Library: {}", lib.name)?;
|
||||
for imp in &lib.imports {
|
||||
let resolved = crate::resolve_ordinal(&lib.name, imp.ordinal);
|
||||
let name = resolved.unwrap_or("???");
|
||||
let kind = if imp.record_type == 1 { "thunk" } else { "var" };
|
||||
writeln!(out, "; [{kind}] 0x{:08X} ordinal 0x{:04X} = {}", imp.address, imp.ordinal, name)?;
|
||||
writeln!(
|
||||
out,
|
||||
"; [{kind}] 0x{:08X} ordinal 0x{:04X} = {}",
|
||||
imp.address, imp.ordinal, name
|
||||
)?;
|
||||
}
|
||||
}
|
||||
writeln!(out)?;
|
||||
@@ -70,8 +94,11 @@ pub fn write_asm(
|
||||
|
||||
// Disassemble each section
|
||||
for section in info.sections {
|
||||
writeln!(out, "; ── Section: {:8} VA=0x{:08X} Size=0x{:08X} Flags=0x{:08X} ──",
|
||||
section.name, section.virtual_address, section.virtual_size, section.flags)?;
|
||||
writeln!(
|
||||
out,
|
||||
"; ── Section: {:8} VA=0x{:08X} Size=0x{:08X} Flags=0x{:08X} ──",
|
||||
section.name, section.virtual_address, section.virtual_size, section.flags
|
||||
)?;
|
||||
|
||||
let va_start = section.virtual_address;
|
||||
let va_end = va_start + section.virtual_size;
|
||||
@@ -81,7 +108,8 @@ pub fn write_asm(
|
||||
let section_labels_sorted: Vec<u32> = if !section.is_code() {
|
||||
let sec_start = info.image_base + va_start;
|
||||
let sec_end = info.image_base + va_end;
|
||||
let mut addrs: Vec<u32> = labels.keys()
|
||||
let mut addrs: Vec<u32> = labels
|
||||
.keys()
|
||||
.filter(|&&a| a >= sec_start && a < sec_end)
|
||||
.copied()
|
||||
.collect();
|
||||
@@ -100,7 +128,13 @@ pub fn write_asm(
|
||||
let abs_end = info.image_base + va_end;
|
||||
|
||||
let items = enrich_section(
|
||||
pe, info.image_base, §ion.name, abs_start, abs_end, func_analysis, labels,
|
||||
pe,
|
||||
info.image_base,
|
||||
§ion.name,
|
||||
abs_start,
|
||||
abs_end,
|
||||
func_analysis,
|
||||
labels,
|
||||
data_words,
|
||||
);
|
||||
for ri in items {
|
||||
@@ -112,9 +146,14 @@ pub fn write_asm(
|
||||
writeln!(out, "; end function")?;
|
||||
}
|
||||
writeln!(out)?;
|
||||
writeln!(out, "; ──────────────────────────────────────────────────────────────────────────")?;
|
||||
writeln!(
|
||||
out,
|
||||
"; ──────────────────────────────────────────────────────────────────────────"
|
||||
)?;
|
||||
|
||||
let lbl = labels.get(&abs_addr).cloned()
|
||||
let lbl = labels
|
||||
.get(&abs_addr)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| format!("sub_{abs_addr:08X}"));
|
||||
|
||||
if fi.is_saverestore {
|
||||
@@ -144,7 +183,10 @@ pub fn write_asm(
|
||||
}
|
||||
}
|
||||
|
||||
writeln!(out, "; ──────────────────────────────────────────────────────────────────────────")?;
|
||||
writeln!(
|
||||
out,
|
||||
"; ──────────────────────────────────────────────────────────────────────────"
|
||||
)?;
|
||||
in_function = true;
|
||||
}
|
||||
|
||||
@@ -152,7 +194,9 @@ pub fn write_asm(
|
||||
if let Some(lbl) = labels.get(&abs_addr) {
|
||||
if !func_analysis.is_function_start(abs_addr) {
|
||||
writeln!(out)?;
|
||||
if let Some(xref_lines) = format_xrefs(abs_addr, xrefs, func_analysis, labels) {
|
||||
if let Some(xref_lines) =
|
||||
format_xrefs(abs_addr, xrefs, func_analysis, labels)
|
||||
{
|
||||
for line in &xref_lines {
|
||||
writeln!(out, "{line}")?;
|
||||
}
|
||||
@@ -204,22 +248,32 @@ pub fn write_asm(
|
||||
line_end = lbl_va;
|
||||
break;
|
||||
}
|
||||
if lbl_va >= line_end { break; }
|
||||
if lbl_va >= line_end {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let byte_count = (line_end - addr) as usize;
|
||||
if off + byte_count > pe.len() { break; }
|
||||
if off + byte_count > pe.len() {
|
||||
break;
|
||||
}
|
||||
|
||||
write!(out, " {:08X}: ", abs_addr)?;
|
||||
for i in 0..byte_count {
|
||||
write!(out, "{:02X}", pe[off + i])?;
|
||||
if i % 4 == 3 { write!(out, " ")?; }
|
||||
if i % 4 == 3 {
|
||||
write!(out, " ")?;
|
||||
}
|
||||
}
|
||||
// ASCII representation
|
||||
let pad = (16 - byte_count) * 2 + (16 - byte_count) / 4;
|
||||
write!(out, "{:>width$} |", "", width = pad)?;
|
||||
for i in 0..byte_count {
|
||||
let b = pe[off + i];
|
||||
let ch = if b.is_ascii_graphic() || b == b' ' { b as char } else { '.' };
|
||||
let ch = if b.is_ascii_graphic() || b == b' ' {
|
||||
b as char
|
||||
} else {
|
||||
'.'
|
||||
};
|
||||
write!(out, "{ch}")?;
|
||||
}
|
||||
writeln!(out, "|")?;
|
||||
@@ -242,7 +296,9 @@ fn format_xrefs(
|
||||
labels: &HashMap<u32, String>,
|
||||
) -> Option<Vec<String>> {
|
||||
let refs = xrefs.get(&target)?;
|
||||
if refs.is_empty() { return None; }
|
||||
if refs.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut sorted: Vec<Xref> = refs.clone();
|
||||
sorted.sort();
|
||||
@@ -254,17 +310,47 @@ fn format_xrefs(
|
||||
let calls = sorted.iter().filter(|x| x.kind == XrefKind::Call).count();
|
||||
let jumps = sorted.iter().filter(|x| x.kind == XrefKind::Jump).count();
|
||||
let branches = sorted.iter().filter(|x| x.kind == XrefKind::Branch).count();
|
||||
let reads = sorted.iter().filter(|x| x.kind == XrefKind::DataRead).count();
|
||||
let writes = sorted.iter().filter(|x| x.kind == XrefKind::DataWrite).count();
|
||||
let data_refs = sorted.iter().filter(|x| x.kind == XrefKind::DataRef).count();
|
||||
let reads = sorted
|
||||
.iter()
|
||||
.filter(|x| x.kind == XrefKind::DataRead)
|
||||
.count();
|
||||
let writes = sorted
|
||||
.iter()
|
||||
.filter(|x| x.kind == XrefKind::DataWrite)
|
||||
.count();
|
||||
let data_refs = sorted
|
||||
.iter()
|
||||
.filter(|x| x.kind == XrefKind::DataRef)
|
||||
.count();
|
||||
|
||||
let mut summary_parts = Vec::new();
|
||||
if calls > 0 { summary_parts.push(format!("{calls} call{}", if calls != 1 { "s" } else { "" })); }
|
||||
if jumps > 0 { summary_parts.push(format!("{jumps} jump{}", if jumps != 1 { "s" } else { "" })); }
|
||||
if branches > 0 { summary_parts.push(format!("{branches} branch{}", if branches != 1 { "es" } else { "" })); }
|
||||
if reads > 0 { summary_parts.push(format!("{reads} read{}", if reads != 1 { "s" } else { "" })); }
|
||||
if writes > 0 { summary_parts.push(format!("{writes} write{}", if writes != 1 { "s" } else { "" })); }
|
||||
if data_refs > 0 { summary_parts.push(format!("{data_refs} ref{}", if data_refs != 1 { "s" } else { "" })); }
|
||||
if calls > 0 {
|
||||
summary_parts.push(format!("{calls} call{}", if calls != 1 { "s" } else { "" }));
|
||||
}
|
||||
if jumps > 0 {
|
||||
summary_parts.push(format!("{jumps} jump{}", if jumps != 1 { "s" } else { "" }));
|
||||
}
|
||||
if branches > 0 {
|
||||
summary_parts.push(format!(
|
||||
"{branches} branch{}",
|
||||
if branches != 1 { "es" } else { "" }
|
||||
));
|
||||
}
|
||||
if reads > 0 {
|
||||
summary_parts.push(format!("{reads} read{}", if reads != 1 { "s" } else { "" }));
|
||||
}
|
||||
if writes > 0 {
|
||||
summary_parts.push(format!(
|
||||
"{writes} write{}",
|
||||
if writes != 1 { "s" } else { "" }
|
||||
));
|
||||
}
|
||||
if data_refs > 0 {
|
||||
summary_parts.push(format!(
|
||||
"{data_refs} ref{}",
|
||||
if data_refs != 1 { "s" } else { "" }
|
||||
));
|
||||
}
|
||||
|
||||
lines.push(format!("; XREF: {} ({})", summary_parts.join(", "), total));
|
||||
|
||||
|
||||
@@ -5,17 +5,17 @@
|
||||
//! hence very likely function entry points.
|
||||
//! 2. Scan the save/restore GPR helper region and label it.
|
||||
//! 3. For each candidate entry, look for prologue patterns:
|
||||
//! a) `mfspr rN, LR` (typically r0 or r12)
|
||||
//! b) `bl __savegprlr_NN` (call into save stub)
|
||||
//! c) `stwu r1, -N(r1)` (allocate stack frame)
|
||||
//! a) `mfspr rN, LR` (typically r0 or r12)
|
||||
//! b) `bl __savegprlr_NN` (call into save stub)
|
||||
//! c) `stwu r1, -N(r1)` (allocate stack frame)
|
||||
//! If a prologue is confirmed, record the function and its stack frame size.
|
||||
//! 4. Walk forward from each function entry to find the epilogue:
|
||||
//! a) `blr` (return)
|
||||
//! b) `b __restgprlr_NN` (tail-branch into restore stub which returns)
|
||||
//! a) `blr` (return)
|
||||
//! b) `b __restgprlr_NN` (tail-branch into restore stub which returns)
|
||||
//! Mark the function's end address.
|
||||
//! 5. Detect leaf functions: `bl` targets that lack a prologue but eventually `blr`.
|
||||
|
||||
use std::collections::{HashMap, HashSet, BTreeMap};
|
||||
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||
|
||||
/// Information about a detected function.
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -63,37 +63,53 @@ pub struct FuncAnalysis {
|
||||
|
||||
// ── Instruction field helpers ──────────────────────────────────────────────
|
||||
|
||||
fn op(instr: u32) -> u32 { (instr >> 26) & 0x3F }
|
||||
fn op(instr: u32) -> u32 {
|
||||
(instr >> 26) & 0x3F
|
||||
}
|
||||
fn bits(instr: u32, hi: u32, lo: u32) -> u32 {
|
||||
(instr >> (31 - hi)) & ((1 << (hi - lo + 1)) - 1)
|
||||
}
|
||||
|
||||
fn is_mfspr_lr(instr: u32) -> Option<u32> {
|
||||
// mfspr rD, LR → opcode 31, xo=339, spr=8
|
||||
if op(instr) != 31 { return None; }
|
||||
if op(instr) != 31 {
|
||||
return None;
|
||||
}
|
||||
let xo = bits(instr, 30, 21);
|
||||
if xo != 339 { return None; }
|
||||
if xo != 339 {
|
||||
return None;
|
||||
}
|
||||
let spr = (bits(instr, 20, 16) << 5) | bits(instr, 15, 11);
|
||||
if spr != 8 { return None; }
|
||||
if spr != 8 {
|
||||
return None;
|
||||
}
|
||||
Some(bits(instr, 10, 6)) // return rD
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn is_mtspr_lr(instr: u32) -> bool {
|
||||
// mtspr LR, rS → opcode 31, xo=467, spr=8
|
||||
if op(instr) != 31 { return false; }
|
||||
if op(instr) != 31 {
|
||||
return false;
|
||||
}
|
||||
let xo = bits(instr, 30, 21);
|
||||
if xo != 467 { return false; }
|
||||
if xo != 467 {
|
||||
return false;
|
||||
}
|
||||
let spr = (bits(instr, 20, 16) << 5) | bits(instr, 15, 11);
|
||||
spr == 8
|
||||
}
|
||||
|
||||
fn is_stwu_r1(instr: u32) -> Option<i32> {
|
||||
// stwu r1, d(r1) → opcode 37, rS=1, rA=1
|
||||
if op(instr) != 37 { return None; }
|
||||
if op(instr) != 37 {
|
||||
return None;
|
||||
}
|
||||
let rs = bits(instr, 10, 6);
|
||||
let ra = bits(instr, 15, 11);
|
||||
if rs != 1 || ra != 1 { return None; }
|
||||
if rs != 1 || ra != 1 {
|
||||
return None;
|
||||
}
|
||||
let d = ((instr & 0xFFFF) as i16) as i32;
|
||||
Some(d) // negative = frame allocation
|
||||
}
|
||||
@@ -108,9 +124,15 @@ fn is_bctr(instr: u32) -> bool {
|
||||
|
||||
fn is_bl(instr: u32) -> Option<u32> {
|
||||
// bl target → opcode 18, LK=1, AA=0
|
||||
if op(instr) != 18 { return None; }
|
||||
if instr & 1 == 0 { return None; } // must have LK bit
|
||||
if instr & 2 != 0 { return None; } // not absolute
|
||||
if op(instr) != 18 {
|
||||
return None;
|
||||
}
|
||||
if instr & 1 == 0 {
|
||||
return None;
|
||||
} // must have LK bit
|
||||
if instr & 2 != 0 {
|
||||
return None;
|
||||
} // not absolute
|
||||
// Return the signed offset
|
||||
let li = instr & 0x03FFFFFC;
|
||||
Some(li)
|
||||
@@ -118,9 +140,15 @@ fn is_bl(instr: u32) -> Option<u32> {
|
||||
|
||||
fn is_b(instr: u32) -> Option<u32> {
|
||||
// b target → opcode 18, LK=0, AA=0
|
||||
if op(instr) != 18 { return None; }
|
||||
if instr & 1 != 0 { return None; } // no LK bit
|
||||
if instr & 2 != 0 { return None; } // not absolute
|
||||
if op(instr) != 18 {
|
||||
return None;
|
||||
}
|
||||
if instr & 1 != 0 {
|
||||
return None;
|
||||
} // no LK bit
|
||||
if instr & 2 != 0 {
|
||||
return None;
|
||||
} // not absolute
|
||||
Some(instr & 0x03FFFFFC)
|
||||
}
|
||||
|
||||
@@ -140,8 +168,15 @@ fn b_target(instr: u32, addr: u32) -> Option<u32> {
|
||||
|
||||
fn read_instr(pe: &[u8], abs_addr: u32, image_base: u32) -> Option<u32> {
|
||||
let off = abs_addr.wrapping_sub(image_base) as usize;
|
||||
if off + 4 > pe.len() { return None; }
|
||||
Some(u32::from_be_bytes([pe[off], pe[off+1], pe[off+2], pe[off+3]]))
|
||||
if off + 4 > pe.len() {
|
||||
return None;
|
||||
}
|
||||
Some(u32::from_be_bytes([
|
||||
pe[off],
|
||||
pe[off + 1],
|
||||
pe[off + 2],
|
||||
pe[off + 3],
|
||||
]))
|
||||
}
|
||||
|
||||
// ── Detect the save/restore GPR helper stubs ───────────────────────────────
|
||||
@@ -165,12 +200,28 @@ fn find_saverestore_stubs(
|
||||
let mut addr = start;
|
||||
while addr + 4 * 18 < end {
|
||||
// Check if this is `std r14, ...(r1)` — opcode 62 (std), rS=14, rA=1
|
||||
let instr = match read_instr(pe, addr, image_base) { Some(i) => i, None => { addr += 4; continue; } };
|
||||
if op(instr) == 62 && bits(instr, 10, 6) == 14 && bits(instr, 15, 11) == 1 && (instr & 3) == 0 {
|
||||
let instr = match read_instr(pe, addr, image_base) {
|
||||
Some(i) => i,
|
||||
None => {
|
||||
addr += 4;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
if op(instr) == 62
|
||||
&& bits(instr, 10, 6) == 14
|
||||
&& bits(instr, 15, 11) == 1
|
||||
&& (instr & 3) == 0
|
||||
{
|
||||
// Verify it's a cascade: r14, r15, ..., r31
|
||||
let mut ok = true;
|
||||
for i in 0u32..18 {
|
||||
let check = match read_instr(pe, addr + i * 4, image_base) { Some(c) => c, None => { ok = false; break; } };
|
||||
let check = match read_instr(pe, addr + i * 4, image_base) {
|
||||
Some(c) => c,
|
||||
None => {
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
};
|
||||
if op(check) != 62 || bits(check, 10, 6) != 14 + i || bits(check, 15, 11) != 1 {
|
||||
ok = false;
|
||||
break;
|
||||
@@ -193,7 +244,9 @@ fn find_saverestore_stubs(
|
||||
}
|
||||
addr += 4;
|
||||
}
|
||||
if save_base.is_some() { break; }
|
||||
if save_base.is_some() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
(save_base, restore_base)
|
||||
@@ -245,7 +298,8 @@ pub fn analyze_with_pdata(
|
||||
pdata: &[sylpheed_xex::pdata::PdataEntry],
|
||||
) -> FuncAnalysis {
|
||||
let started = std::time::Instant::now();
|
||||
let code_ranges: Vec<(u32, u32)> = code_sections.iter()
|
||||
let code_ranges: Vec<(u32, u32)> = code_sections
|
||||
.iter()
|
||||
.map(|(va, sz, _)| (image_base + va, image_base + va + sz))
|
||||
.collect();
|
||||
|
||||
@@ -262,11 +316,15 @@ pub fn analyze_with_pdata(
|
||||
let mut saverestore_addrs: HashSet<u32> = HashSet::new();
|
||||
if let Some(sb) = save_base {
|
||||
// Save block: 18 std + stw + blr = 20 instructions
|
||||
for i in 0..20 { saverestore_addrs.insert(sb + i * 4); }
|
||||
for i in 0..20 {
|
||||
saverestore_addrs.insert(sb + i * 4);
|
||||
}
|
||||
}
|
||||
if let Some(rb) = restore_base {
|
||||
// Restore block: 18 ld + lwz + mtspr + blr = 21 instructions
|
||||
for i in 0..21 { saverestore_addrs.insert(rb + i * 4); }
|
||||
for i in 0..21 {
|
||||
saverestore_addrs.insert(rb + i * 4);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Collect all bl targets as candidate function entries.
|
||||
@@ -278,12 +336,13 @@ pub fn analyze_with_pdata(
|
||||
let mut addr = start;
|
||||
while addr < end {
|
||||
if let Some(instr) = read_instr(pe, addr, image_base)
|
||||
&& let Some(target) = bl_target(instr, addr) {
|
||||
// Don't count calls into save/restore stubs as function entries
|
||||
if !saverestore_addrs.contains(&target) {
|
||||
call_targets.insert(target);
|
||||
}
|
||||
&& let Some(target) = bl_target(instr, addr)
|
||||
{
|
||||
// Don't count calls into save/restore stubs as function entries
|
||||
if !saverestore_addrs.contains(&target) {
|
||||
call_targets.insert(target);
|
||||
}
|
||||
}
|
||||
addr += 4;
|
||||
}
|
||||
}
|
||||
@@ -311,7 +370,10 @@ pub fn analyze_with_pdata(
|
||||
// within this one. Intra-function jumps and switch arms both stay inside
|
||||
// the range and are therefore never nominated.
|
||||
let pdata_sorted: Vec<(u32, u32)> = {
|
||||
let mut v: Vec<(u32, u32)> = pdata.iter().map(|e| (e.begin_address, e.end_address())).collect();
|
||||
let mut v: Vec<(u32, u32)> = pdata
|
||||
.iter()
|
||||
.map(|e| (e.begin_address, e.end_address()))
|
||||
.collect();
|
||||
v.sort_unstable();
|
||||
v
|
||||
};
|
||||
@@ -380,7 +442,12 @@ pub fn analyze_with_pdata(
|
||||
let pdata_entry = pdata_by_begin.get(&func_addr).copied();
|
||||
|
||||
if let Some(mut fi) = analyze_function(
|
||||
pe, image_base, func_addr, &code_ranges, save_base, restore_base,
|
||||
pe,
|
||||
image_base,
|
||||
func_addr,
|
||||
&code_ranges,
|
||||
save_base,
|
||||
restore_base,
|
||||
) {
|
||||
if let Some(p) = pdata_entry {
|
||||
fi.pdata_validated = true;
|
||||
@@ -428,33 +495,39 @@ pub fn analyze_with_pdata(
|
||||
// The save block is one cascade: entry at each rN, falls through to blr
|
||||
// Treat as a single function with the first entry point
|
||||
let pe_sb = pdata_by_begin.get(&sb).copied();
|
||||
functions.insert(sb, FuncInfo {
|
||||
start: sb,
|
||||
end: sb + 20 * 4, // 18 std + stw r12 + blr
|
||||
frame_size: 0,
|
||||
saved_gprs: 18,
|
||||
is_leaf: true,
|
||||
is_saverestore: true,
|
||||
pdata_validated: pe_sb.is_some(),
|
||||
pdata_length: pe_sb.map(|p| p.function_length),
|
||||
pdata_prolog_length: pe_sb.map(|p| p.prolog_length),
|
||||
has_eh: pe_sb.map(|p| (p.flags & 0x2) != 0).unwrap_or(false),
|
||||
});
|
||||
functions.insert(
|
||||
sb,
|
||||
FuncInfo {
|
||||
start: sb,
|
||||
end: sb + 20 * 4, // 18 std + stw r12 + blr
|
||||
frame_size: 0,
|
||||
saved_gprs: 18,
|
||||
is_leaf: true,
|
||||
is_saverestore: true,
|
||||
pdata_validated: pe_sb.is_some(),
|
||||
pdata_length: pe_sb.map(|p| p.function_length),
|
||||
pdata_prolog_length: pe_sb.map(|p| p.prolog_length),
|
||||
has_eh: pe_sb.map(|p| (p.flags & 0x2) != 0).unwrap_or(false),
|
||||
},
|
||||
);
|
||||
}
|
||||
if let Some(rb) = restore_base {
|
||||
let pe_rb = pdata_by_begin.get(&rb).copied();
|
||||
functions.insert(rb, FuncInfo {
|
||||
start: rb,
|
||||
end: rb + 21 * 4, // 18 ld + lwz r12 + mtspr LR + blr
|
||||
frame_size: 0,
|
||||
saved_gprs: 18,
|
||||
is_leaf: true,
|
||||
is_saverestore: true,
|
||||
pdata_validated: pe_rb.is_some(),
|
||||
pdata_length: pe_rb.map(|p| p.function_length),
|
||||
pdata_prolog_length: pe_rb.map(|p| p.prolog_length),
|
||||
has_eh: pe_rb.map(|p| (p.flags & 0x2) != 0).unwrap_or(false),
|
||||
});
|
||||
functions.insert(
|
||||
rb,
|
||||
FuncInfo {
|
||||
start: rb,
|
||||
end: rb + 21 * 4, // 18 ld + lwz r12 + mtspr LR + blr
|
||||
frame_size: 0,
|
||||
saved_gprs: 18,
|
||||
is_leaf: true,
|
||||
is_saverestore: true,
|
||||
pdata_validated: pe_rb.is_some(),
|
||||
pdata_length: pe_rb.map(|p| p.function_length),
|
||||
pdata_prolog_length: pe_rb.map(|p| p.prolog_length),
|
||||
has_eh: pe_rb.map(|p| (p.flags & 0x2) != 0).unwrap_or(false),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// 5. Reconcile candidate starts against the linker's ground truth.
|
||||
@@ -482,9 +555,13 @@ pub fn analyze_with_pdata(
|
||||
.filter(|&addr| {
|
||||
pdata_ranges
|
||||
.binary_search_by(|&(s, e)| {
|
||||
if addr < s { std::cmp::Ordering::Greater }
|
||||
else if addr >= e { std::cmp::Ordering::Less }
|
||||
else { std::cmp::Ordering::Equal }
|
||||
if addr < s {
|
||||
std::cmp::Ordering::Greater
|
||||
} else if addr >= e {
|
||||
std::cmp::Ordering::Less
|
||||
} else {
|
||||
std::cmp::Ordering::Equal
|
||||
}
|
||||
})
|
||||
.is_ok()
|
||||
})
|
||||
@@ -534,7 +611,9 @@ pub fn analyze_with_pdata(
|
||||
fn range_has_call(pe: &[u8], image_base: u32, start: u32, end: u32) -> bool {
|
||||
let mut addr = start;
|
||||
while addr < end {
|
||||
let Some(instr) = read_instr(pe, addr, image_base) else { return false };
|
||||
let Some(instr) = read_instr(pe, addr, image_base) else {
|
||||
return false;
|
||||
};
|
||||
let opcode = op(instr);
|
||||
// I-form / B-form with LK, and XL-form bclrl / bcctrl.
|
||||
if (opcode == 18 || opcode == 16) && instr & 1 == 1 {
|
||||
@@ -558,8 +637,12 @@ fn analyze_function(
|
||||
restore_base: Option<u32>,
|
||||
) -> Option<FuncInfo> {
|
||||
// Verify the address is within a code section
|
||||
let in_code = code_ranges.iter().any(|&(s, e)| func_addr >= s && func_addr < e);
|
||||
if !in_code { return None; }
|
||||
let in_code = code_ranges
|
||||
.iter()
|
||||
.any(|&(s, e)| func_addr >= s && func_addr < e);
|
||||
if !in_code {
|
||||
return None;
|
||||
}
|
||||
|
||||
let instr0 = read_instr(pe, func_addr, image_base)?;
|
||||
|
||||
@@ -576,11 +659,13 @@ fn analyze_function(
|
||||
// Check if next is bl to save stub
|
||||
if let Some(target) = bl_target(instr1, func_addr + 4)
|
||||
&& let Some(sb) = save_base
|
||||
&& target >= sb && target < sb + 18 * 4 {
|
||||
let idx = (target - sb) / 4;
|
||||
saved_gprs = 18 - idx;
|
||||
prologue_len = 8;
|
||||
}
|
||||
&& target >= sb
|
||||
&& target < sb + 18 * 4
|
||||
{
|
||||
let idx = (target - sb) / 4;
|
||||
saved_gprs = 18 - idx;
|
||||
prologue_len = 8;
|
||||
}
|
||||
|
||||
// Next should be stwu r1, -N(r1)
|
||||
let stwu_instr = read_instr(pe, func_addr + prologue_len, image_base).unwrap_or(0);
|
||||
@@ -601,7 +686,8 @@ fn analyze_function(
|
||||
}
|
||||
|
||||
// Walk forward to find the end of the function
|
||||
let max_range = code_ranges.iter()
|
||||
let max_range = code_ranges
|
||||
.iter()
|
||||
.find(|&&(s, e)| func_addr >= s && func_addr < e)
|
||||
.map(|&(_, e)| e)
|
||||
.unwrap_or(func_addr + 0x100000);
|
||||
@@ -628,10 +714,12 @@ fn analyze_function(
|
||||
// Epilogue: b __restgprlr_NN (tail branch into restore stub)
|
||||
if let Some(target) = b_target(instr, addr)
|
||||
&& let Some(rb) = restore_base
|
||||
&& target >= rb && target < rb + 18 * 4 {
|
||||
end_addr = addr + 4;
|
||||
break;
|
||||
}
|
||||
&& target >= rb
|
||||
&& target < rb + 18 * 4
|
||||
{
|
||||
end_addr = addr + 4;
|
||||
break;
|
||||
}
|
||||
|
||||
// Epilogue: bctr (indirect tail call — end of function)
|
||||
if is_bctr(instr) {
|
||||
@@ -680,21 +768,23 @@ impl FuncAnalysis {
|
||||
if fi.is_saverestore {
|
||||
// Label the block start, plus individual register entry points
|
||||
if let Some(sb) = self.save_gpr_base
|
||||
&& addr == sb {
|
||||
for i in 0u32..18 {
|
||||
let reg = 14 + i;
|
||||
labels.insert(sb + i * 4, format!("__savegprlr_{reg}"));
|
||||
}
|
||||
continue;
|
||||
&& addr == sb
|
||||
{
|
||||
for i in 0u32..18 {
|
||||
let reg = 14 + i;
|
||||
labels.insert(sb + i * 4, format!("__savegprlr_{reg}"));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if let Some(rb) = self.restore_gpr_base
|
||||
&& addr == rb {
|
||||
for i in 0u32..18 {
|
||||
let reg = 14 + i;
|
||||
labels.insert(rb + i * 4, format!("__restgprlr_{reg}"));
|
||||
}
|
||||
continue;
|
||||
&& addr == rb
|
||||
{
|
||||
for i in 0u32..18 {
|
||||
let reg = 14 + i;
|
||||
labels.insert(rb + i * 4, format!("__restgprlr_{reg}"));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
labels.insert(addr, format!("sub_{addr:08X}"));
|
||||
}
|
||||
|
||||
@@ -75,16 +75,23 @@ pub fn analyze(
|
||||
// Scan only .rdata for dispatch tables — .data has too many false
|
||||
// positives from struct fields aliasing function VAs.
|
||||
for section in sections {
|
||||
if section.name != ".rdata" { continue; }
|
||||
if section.name != ".rdata" {
|
||||
continue;
|
||||
}
|
||||
let raw_start = section.virtual_address as usize;
|
||||
let raw_end = (section.virtual_address + section.virtual_size) as usize;
|
||||
if raw_end > pe.len() { continue; }
|
||||
if raw_end > pe.len() {
|
||||
continue;
|
||||
}
|
||||
let bytes = &pe[raw_start..raw_end.min(pe.len())];
|
||||
let va_base = image_base + section.virtual_address;
|
||||
|
||||
let mut i = 0usize;
|
||||
while i + 8 <= bytes.len() {
|
||||
if !i.is_multiple_of(4) { i += 1; continue; }
|
||||
if !i.is_multiple_of(4) {
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
let mut entries: Vec<u32> = Vec::new();
|
||||
let mut j = i;
|
||||
while j + 4 <= bytes.len() {
|
||||
@@ -120,7 +127,10 @@ pub fn analyze(
|
||||
let n_si = out.iter().filter(|a| a.kind == "static_init").count();
|
||||
metrics::histogram!("analysis.phase_ms", "phase" => "funcptr_arrays").record(elapsed_ms);
|
||||
tracing::info!(
|
||||
total = out.len(), vtable = n_vt, dispatch_table = n_dt, static_init = n_si,
|
||||
total = out.len(),
|
||||
vtable = n_vt,
|
||||
dispatch_table = n_dt,
|
||||
static_init = n_si,
|
||||
elapsed_ms,
|
||||
"function-pointer array scan complete",
|
||||
);
|
||||
@@ -156,14 +166,18 @@ fn classify_run(image_base: u32, entries: &[u32], pe: &[u8]) -> &'static str {
|
||||
/// `mfspr r12, LR` immediately followed by `stwu r1, -N(r1)` with `N ≤ 0x80`.
|
||||
fn is_ctor_like(pe: &[u8], image_base: u32, fn_va: u32) -> bool {
|
||||
let off = fn_va.wrapping_sub(image_base) as usize;
|
||||
if off + 8 > pe.len() { return false; }
|
||||
if off + 8 > pe.len() {
|
||||
return false;
|
||||
}
|
||||
let i0 = u32::from_be_bytes([pe[off], pe[off + 1], pe[off + 2], pe[off + 3]]);
|
||||
let i1 = u32::from_be_bytes([pe[off + 4], pe[off + 5], pe[off + 6], pe[off + 7]]);
|
||||
// i0: mfspr rD, LR — opcode 31, xo 339, spr 8.
|
||||
let op0 = i0 >> 26;
|
||||
let xo0 = (i0 >> 1) & 0x3FF;
|
||||
let spr0 = (((i0 >> 11) & 0x1F) << 5) | ((i0 >> 16) & 0x1F);
|
||||
if !(op0 == 31 && xo0 == 339 && spr0 == 8) { return false; }
|
||||
if !(op0 == 31 && xo0 == 339 && spr0 == 8) {
|
||||
return false;
|
||||
}
|
||||
// i1 must be stwu r1, -N(r1) with N ≤ 0x80, OR a `bl __savegprlr_*`
|
||||
// followed eventually by stwu (full prologue). Allow either.
|
||||
let op1 = i1 >> 26;
|
||||
@@ -217,7 +231,9 @@ mod tests {
|
||||
|
||||
let sections = vec![mk_section(".rdata", rdata_va, 0x100)];
|
||||
let mut starts = BTreeSet::new();
|
||||
for &p in &pcs { starts.insert(p); }
|
||||
for &p in &pcs {
|
||||
starts.insert(p);
|
||||
}
|
||||
|
||||
let arrs = analyze(&pe, image_base, §ions, &starts, &[]);
|
||||
assert_eq!(arrs.len(), 1);
|
||||
@@ -231,13 +247,19 @@ mod tests {
|
||||
let rdata_va = 0x1000u32;
|
||||
let mut pe = vec![0u8; 0x4000];
|
||||
|
||||
let pcs = [image_base + 0x2000, image_base + 0x2010, image_base + 0x2020];
|
||||
let pcs = [
|
||||
image_base + 0x2000,
|
||||
image_base + 0x2010,
|
||||
image_base + 0x2020,
|
||||
];
|
||||
for (i, p) in pcs.iter().enumerate() {
|
||||
write_be_u32(&mut pe, rdata_va as usize + i * 4, *p);
|
||||
}
|
||||
let sections = vec![mk_section(".rdata", rdata_va, 0x100)];
|
||||
let mut starts = BTreeSet::new();
|
||||
for &p in &pcs { starts.insert(p); }
|
||||
for &p in &pcs {
|
||||
starts.insert(p);
|
||||
}
|
||||
|
||||
let vt = Vtable {
|
||||
address: image_base + rdata_va,
|
||||
|
||||
@@ -112,12 +112,12 @@ pub struct VptrWrite {
|
||||
pub writer_function: u32,
|
||||
}
|
||||
|
||||
const OP_ADDI: u32 = 14;
|
||||
const OP_ADDI: u32 = 14;
|
||||
const OP_ADDIS: u32 = 15;
|
||||
const OP_BCCTR: u32 = 19;
|
||||
const OP_LWZ: u32 = 32;
|
||||
const OP_ORI: u32 = 24;
|
||||
const OP_STW: u32 = 36;
|
||||
const OP_LWZ: u32 = 32;
|
||||
const OP_ORI: u32 = 24;
|
||||
const OP_STW: u32 = 36;
|
||||
const OP_X_FORM: u32 = 31;
|
||||
|
||||
/// Run the full M5.5 analysis.
|
||||
@@ -133,26 +133,36 @@ pub fn analyze(
|
||||
let started = std::time::Instant::now();
|
||||
|
||||
let vtable_addrs: BTreeSet<u32> = vtables.iter().map(|v| v.address).collect();
|
||||
let vtable_by_addr: BTreeMap<u32, &Vtable> =
|
||||
vtables.iter().map(|v| (v.address, v)).collect();
|
||||
let vtable_by_addr: BTreeMap<u32, &Vtable> = vtables.iter().map(|v| (v.address, v)).collect();
|
||||
|
||||
let block_boundaries: HashSet<u32> = labels.keys().copied().collect();
|
||||
|
||||
// Phase 1: scan for vptr writes.
|
||||
let vptr_writes = scan_vptr_writes(
|
||||
pe, image_base, func_analysis, &vtable_addrs, &block_boundaries,
|
||||
pe,
|
||||
image_base,
|
||||
func_analysis,
|
||||
&vtable_addrs,
|
||||
&block_boundaries,
|
||||
);
|
||||
|
||||
// Phase 2: invert by offset.
|
||||
let mut vtables_by_offset: HashMap<u32, HashSet<u32>> = HashMap::new();
|
||||
for w in &vptr_writes {
|
||||
vtables_by_offset.entry(w.vptr_offset).or_default().insert(w.vtable_addr);
|
||||
vtables_by_offset
|
||||
.entry(w.vptr_offset)
|
||||
.or_default()
|
||||
.insert(w.vtable_addr);
|
||||
}
|
||||
|
||||
// Phase 3 + 4: scan dispatches and emit edges.
|
||||
let mut dispatches = scan_dispatches_and_resolve(
|
||||
pe, image_base, func_analysis, &block_boundaries,
|
||||
&vtables_by_offset, &vtable_by_addr,
|
||||
pe,
|
||||
image_base,
|
||||
func_analysis,
|
||||
&block_boundaries,
|
||||
&vtables_by_offset,
|
||||
&vtable_by_addr,
|
||||
);
|
||||
|
||||
// Drop the per-candidate lists for sites the analysis could not narrow.
|
||||
@@ -180,7 +190,10 @@ pub fn analyze(
|
||||
}
|
||||
|
||||
let elapsed_ms = started.elapsed().as_millis() as f64;
|
||||
let single_candidate = dispatches.iter().filter(|d| d.total_candidates == 1).count();
|
||||
let single_candidate = dispatches
|
||||
.iter()
|
||||
.filter(|d| d.total_candidates == 1)
|
||||
.count();
|
||||
let multi_candidate = dispatches.len() - single_candidate;
|
||||
let total_edges: usize = dispatches.iter().map(|d| d.method_pcs.len()).sum();
|
||||
metrics::histogram!("analysis.phase_ms", "phase" => "ind_dispatch_typed").record(elapsed_ms);
|
||||
@@ -197,13 +210,23 @@ pub fn analyze(
|
||||
"M5.5 typed indirect-dispatch scan complete",
|
||||
);
|
||||
|
||||
TypedIndirectResult { dispatches, vptr_writes }
|
||||
TypedIndirectResult {
|
||||
dispatches,
|
||||
vptr_writes,
|
||||
}
|
||||
}
|
||||
|
||||
fn read_instr(pe: &[u8], image_base: u32, addr: u32) -> Option<u32> {
|
||||
let off = addr.wrapping_sub(image_base) as usize;
|
||||
if off + 4 > pe.len() { return None; }
|
||||
Some(u32::from_be_bytes([pe[off], pe[off + 1], pe[off + 2], pe[off + 3]]))
|
||||
if off + 4 > pe.len() {
|
||||
return None;
|
||||
}
|
||||
Some(u32::from_be_bytes([
|
||||
pe[off],
|
||||
pe[off + 1],
|
||||
pe[off + 2],
|
||||
pe[off + 3],
|
||||
]))
|
||||
}
|
||||
|
||||
/// Phase 1 — find every `stw rA, off(rB)` where the lis+addi-tracked
|
||||
@@ -217,14 +240,18 @@ fn scan_vptr_writes(
|
||||
) -> Vec<VptrWrite> {
|
||||
let mut writes: Vec<VptrWrite> = Vec::new();
|
||||
for (&fn_start, fi) in &func_analysis.functions {
|
||||
if fi.is_saverestore { continue; }
|
||||
if fi.is_saverestore {
|
||||
continue;
|
||||
}
|
||||
let mut reg: [Option<u32>; 32] = [None; 32];
|
||||
let mut pc = fn_start;
|
||||
while pc < fi.end {
|
||||
if pc != fn_start && block_boundaries.contains(&pc) {
|
||||
reg = [None; 32];
|
||||
}
|
||||
let Some(instr) = read_instr(pe, image_base, pc) else { break };
|
||||
let Some(instr) = read_instr(pe, image_base, pc) else {
|
||||
break;
|
||||
};
|
||||
let op = instr >> 26;
|
||||
let rd = ((instr >> 21) & 0x1F) as usize;
|
||||
let ra = ((instr >> 16) & 0x1F) as usize;
|
||||
@@ -264,19 +291,23 @@ fn scan_vptr_writes(
|
||||
32..=35 | 40..=43 | 48..=51 => reg[rd] = None,
|
||||
OP_X_FORM => {
|
||||
let xo = (instr >> 1) & 0x3FF;
|
||||
if xo != 444 && xo != 467 { reg[rd] = None; }
|
||||
if xo != 444 && xo != 467 {
|
||||
reg[rd] = None;
|
||||
}
|
||||
}
|
||||
18 => {
|
||||
// `bl` (LK=1) clobbers volatile r0..r12 + ctr. Plain
|
||||
// `b` makes the next instruction unreachable; the
|
||||
// label-based reset handles join points.
|
||||
if (instr & 1) != 0 {
|
||||
for r in 0..=12 { reg[r] = None; }
|
||||
for r in 0..=12 {
|
||||
reg[r] = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
16 => {
|
||||
if (instr & 1) != 0 {
|
||||
for r in 0..=12 { reg[r] = None; }
|
||||
16 if (instr & 1) != 0 => {
|
||||
for r in 0..=12 {
|
||||
reg[r] = None;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
@@ -302,18 +333,29 @@ fn scan_dispatches_and_resolve(
|
||||
) -> Vec<TypedDispatch> {
|
||||
let mut out: Vec<TypedDispatch> = Vec::new();
|
||||
for (&fn_start, fi) in &func_analysis.functions {
|
||||
if fi.is_saverestore { continue; }
|
||||
if fi.is_saverestore {
|
||||
continue;
|
||||
}
|
||||
let mut pc = fn_start;
|
||||
while pc < fi.end {
|
||||
let Some(instr) = read_instr(pe, image_base, pc) else { break };
|
||||
let Some(instr) = read_instr(pe, image_base, pc) else {
|
||||
break;
|
||||
};
|
||||
let op = instr >> 26;
|
||||
if op == OP_BCCTR {
|
||||
let xo = (instr >> 1) & 0x3FF;
|
||||
let lk = (instr & 1) != 0;
|
||||
if xo == 528 && lk
|
||||
if xo == 528
|
||||
&& lk
|
||||
&& let Some(d) = try_resolve_dispatch_site(
|
||||
pe, image_base, fn_start, fi.end, pc,
|
||||
block_boundaries, vtables_by_offset, vtable_by_addr,
|
||||
pe,
|
||||
image_base,
|
||||
fn_start,
|
||||
fi.end,
|
||||
pc,
|
||||
block_boundaries,
|
||||
vtables_by_offset,
|
||||
vtable_by_addr,
|
||||
)
|
||||
{
|
||||
out.push(d);
|
||||
@@ -346,9 +388,15 @@ fn try_resolve_dispatch_site(
|
||||
let mut mtctr_pc: Option<u32> = None;
|
||||
for i in 1..=LOOKBACK {
|
||||
let p = bcctrl_pc.wrapping_sub(i * 4);
|
||||
if p < fn_start { break; }
|
||||
if block_boundaries.contains(&p) { break; }
|
||||
let Some(instr) = read_instr(pe, image_base, p) else { break };
|
||||
if p < fn_start {
|
||||
break;
|
||||
}
|
||||
if block_boundaries.contains(&p) {
|
||||
break;
|
||||
}
|
||||
let Some(instr) = read_instr(pe, image_base, p) else {
|
||||
break;
|
||||
};
|
||||
let op = instr >> 26;
|
||||
if op == OP_X_FORM {
|
||||
let xo = (instr >> 1) & 0x3FF;
|
||||
@@ -371,17 +419,27 @@ fn try_resolve_dispatch_site(
|
||||
let mut fn_lwz_pc: Option<u32> = None;
|
||||
for i in 1..=LOOKBACK {
|
||||
let p = mtctr_pc.wrapping_sub(i * 4);
|
||||
if p < fn_start { break; }
|
||||
if block_boundaries.contains(&p) { break; }
|
||||
let Some(instr) = read_instr(pe, image_base, p) else { break };
|
||||
if p < fn_start {
|
||||
break;
|
||||
}
|
||||
if block_boundaries.contains(&p) {
|
||||
break;
|
||||
}
|
||||
let Some(instr) = read_instr(pe, image_base, p) else {
|
||||
break;
|
||||
};
|
||||
let op = instr >> 26;
|
||||
let rd = ((instr >> 21) & 0x1F) as usize;
|
||||
if op == OP_LWZ {
|
||||
if rd == mtctr_rs {
|
||||
let ra = ((instr >> 16) & 0x1F) as usize;
|
||||
if ra == 0 { return None; }
|
||||
if ra == 0 {
|
||||
return None;
|
||||
}
|
||||
let off = ((instr & 0xFFFF) as i16) as i32;
|
||||
if off < 0 || (off % 4) != 0 { return None; }
|
||||
if off < 0 || (off % 4) != 0 {
|
||||
return None;
|
||||
}
|
||||
slot = Some((off as u32) / 4);
|
||||
vt_reg = Some(ra);
|
||||
fn_lwz_pc = Some(p);
|
||||
@@ -402,14 +460,22 @@ fn try_resolve_dispatch_site(
|
||||
let mut vptr_off: Option<u32> = None;
|
||||
for i in 1..=LOOKBACK {
|
||||
let p = fn_lwz_pc.wrapping_sub(i * 4);
|
||||
if p < fn_start { break; }
|
||||
if block_boundaries.contains(&p) { break; }
|
||||
let Some(instr) = read_instr(pe, image_base, p) else { break };
|
||||
if p < fn_start {
|
||||
break;
|
||||
}
|
||||
if block_boundaries.contains(&p) {
|
||||
break;
|
||||
}
|
||||
let Some(instr) = read_instr(pe, image_base, p) else {
|
||||
break;
|
||||
};
|
||||
let op = instr >> 26;
|
||||
let rd = ((instr >> 21) & 0x1F) as usize;
|
||||
if op == OP_LWZ && rd == vt_reg {
|
||||
let ra = ((instr >> 16) & 0x1F) as usize;
|
||||
if ra == 0 { return None; }
|
||||
if ra == 0 {
|
||||
return None;
|
||||
}
|
||||
let off = ((instr & 0xFFFF) as i16) as i32;
|
||||
// Negative offsets are valid in C++ (multiple inheritance casts
|
||||
// can produce them in some ABIs); reinterpret as u32 wrap.
|
||||
@@ -435,7 +501,9 @@ fn try_resolve_dispatch_site(
|
||||
method_pcs.push(method_pc);
|
||||
}
|
||||
}
|
||||
if method_pcs.is_empty() { return None; }
|
||||
if method_pcs.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let total_candidates = candidate_vtables.len();
|
||||
|
||||
Some(TypedDispatch {
|
||||
@@ -460,13 +528,16 @@ fn writes_reg(instr: u32, r: u32) -> bool {
|
||||
// Most arithmetic / load opcodes use bits 21..25 = rD/rT.
|
||||
14 | 15 | 32..=43 | 46 | 48..=51 => rd == r,
|
||||
// ori/oris/xor/etc. opcodes 24..29 — rA in bits 16..20 is the dest.
|
||||
24 | 25 | 26 | 27 | 28 | 29 => ((instr >> 16) & 0x1F) == r,
|
||||
24..=29 => ((instr >> 16) & 0x1F) == r,
|
||||
// X-form: most write rD; some write rA. Check both, conservatively.
|
||||
OP_X_FORM => {
|
||||
let xo = (instr >> 1) & 0x3FF;
|
||||
// Logical X-form (and/or/xor/etc.): rA is the dest.
|
||||
// Logical X-form ops (and/or/xor/etc.) write rA, not rD.
|
||||
if matches!(xo, 26 | 28 | 60 | 124 | 284 | 316 | 444 | 476 | 536 | 539 | 922 | 954) {
|
||||
if matches!(
|
||||
xo,
|
||||
26 | 28 | 60 | 124 | 284 | 316 | 444 | 476 | 536 | 539 | 922 | 954
|
||||
) {
|
||||
((instr >> 16) & 0x1F) == r
|
||||
} else {
|
||||
rd == r
|
||||
@@ -496,19 +567,27 @@ mod tests {
|
||||
|
||||
fn mk_func_analysis(start: u32, len: u32) -> FuncAnalysis {
|
||||
let mut functions: BTreeMap<u32, FuncInfo> = BTreeMap::new();
|
||||
functions.insert(start, FuncInfo {
|
||||
functions.insert(
|
||||
start,
|
||||
end: start + len,
|
||||
frame_size: 0,
|
||||
saved_gprs: 0,
|
||||
is_leaf: false,
|
||||
is_saverestore: false,
|
||||
pdata_validated: false,
|
||||
pdata_length: None,
|
||||
pdata_prolog_length: None,
|
||||
has_eh: false,
|
||||
});
|
||||
FuncAnalysis { functions, save_gpr_base: None, restore_gpr_base: None, pdata_entries: Vec::new() }
|
||||
FuncInfo {
|
||||
start,
|
||||
end: start + len,
|
||||
frame_size: 0,
|
||||
saved_gprs: 0,
|
||||
is_leaf: false,
|
||||
is_saverestore: false,
|
||||
pdata_validated: false,
|
||||
pdata_length: None,
|
||||
pdata_prolog_length: None,
|
||||
has_eh: false,
|
||||
},
|
||||
);
|
||||
FuncAnalysis {
|
||||
functions,
|
||||
save_gpr_base: None,
|
||||
restore_gpr_base: None,
|
||||
pdata_entries: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn write_be(pe: &mut [u8], at: usize, v: u32) {
|
||||
@@ -519,7 +598,7 @@ mod tests {
|
||||
fn enc_vptr_write(pe: &mut [u8], at: usize, vt: u32, write_off: i16, dest_reg: u32) {
|
||||
let hi = (vt >> 16) as u16;
|
||||
let lo = (vt & 0xFFFF) as i16;
|
||||
let lis = (15u32 << 26) | (3 << 21) | 0 << 16 | (hi as u32);
|
||||
let lis = ((15u32 << 26) | (3 << 21)) | (hi as u32);
|
||||
let addi = (14u32 << 26) | (3 << 21) | (3 << 16) | ((lo as u16) as u32);
|
||||
let stw = (36u32 << 26) | (3 << 21) | (dest_reg << 16) | ((write_off as u16) as u32);
|
||||
write_be(pe, at, lis);
|
||||
@@ -560,11 +639,21 @@ mod tests {
|
||||
|
||||
// Both functions in func_analysis (synthesise).
|
||||
let mut fa = mk_func_analysis(ctor_pc, 0x40);
|
||||
fa.functions.insert(disp_pc, FuncInfo {
|
||||
start: disp_pc, end: disp_pc + 0x40, frame_size: 0, saved_gprs: 0,
|
||||
is_leaf: false, is_saverestore: false,
|
||||
pdata_validated: false, pdata_length: None, pdata_prolog_length: None, has_eh: false,
|
||||
});
|
||||
fa.functions.insert(
|
||||
disp_pc,
|
||||
FuncInfo {
|
||||
start: disp_pc,
|
||||
end: disp_pc + 0x40,
|
||||
frame_size: 0,
|
||||
saved_gprs: 0,
|
||||
is_leaf: false,
|
||||
is_saverestore: false,
|
||||
pdata_validated: false,
|
||||
pdata_length: None,
|
||||
pdata_prolog_length: None,
|
||||
has_eh: false,
|
||||
},
|
||||
);
|
||||
|
||||
let vt = mk_vtable(0x82010000, vec![0xAA, 0xBB, 0xCC, 0xDD]);
|
||||
let labels: HashMap<u32, String> = HashMap::new();
|
||||
@@ -585,9 +674,14 @@ mod tests {
|
||||
|
||||
/// Two classes installing different vtables at offset 0, and one dispatch
|
||||
/// at slot 1 that therefore matches both.
|
||||
fn multi_candidate_fixture(image_base: u32)
|
||||
-> (Vec<u8>, FuncAnalysis, Vec<crate::vtables::Vtable>, HashMap<u32, String>)
|
||||
{
|
||||
fn multi_candidate_fixture(
|
||||
image_base: u32,
|
||||
) -> (
|
||||
Vec<u8>,
|
||||
FuncAnalysis,
|
||||
Vec<crate::vtables::Vtable>,
|
||||
HashMap<u32, String>,
|
||||
) {
|
||||
let mut pe = vec![0u8; 0x4000];
|
||||
|
||||
// Two ctors, each writing a different vtable at offset 0.
|
||||
@@ -601,16 +695,36 @@ mod tests {
|
||||
enc_dispatch(&mut pe, (disp - image_base) as usize, 0, 1);
|
||||
|
||||
let mut fa = mk_func_analysis(ctor_a, 0x40);
|
||||
fa.functions.insert(ctor_b, FuncInfo {
|
||||
start: ctor_b, end: ctor_b + 0x40, frame_size: 0, saved_gprs: 0,
|
||||
is_leaf: false, is_saverestore: false,
|
||||
pdata_validated: false, pdata_length: None, pdata_prolog_length: None, has_eh: false,
|
||||
});
|
||||
fa.functions.insert(disp, FuncInfo {
|
||||
start: disp, end: disp + 0x40, frame_size: 0, saved_gprs: 0,
|
||||
is_leaf: false, is_saverestore: false,
|
||||
pdata_validated: false, pdata_length: None, pdata_prolog_length: None, has_eh: false,
|
||||
});
|
||||
fa.functions.insert(
|
||||
ctor_b,
|
||||
FuncInfo {
|
||||
start: ctor_b,
|
||||
end: ctor_b + 0x40,
|
||||
frame_size: 0,
|
||||
saved_gprs: 0,
|
||||
is_leaf: false,
|
||||
is_saverestore: false,
|
||||
pdata_validated: false,
|
||||
pdata_length: None,
|
||||
pdata_prolog_length: None,
|
||||
has_eh: false,
|
||||
},
|
||||
);
|
||||
fa.functions.insert(
|
||||
disp,
|
||||
FuncInfo {
|
||||
start: disp,
|
||||
end: disp + 0x40,
|
||||
frame_size: 0,
|
||||
saved_gprs: 0,
|
||||
is_leaf: false,
|
||||
is_saverestore: false,
|
||||
pdata_validated: false,
|
||||
pdata_length: None,
|
||||
pdata_prolog_length: None,
|
||||
has_eh: false,
|
||||
},
|
||||
);
|
||||
|
||||
let vts = vec![
|
||||
mk_vtable(0x82010000, vec![0x11, 0x22, 0x33, 0x44]),
|
||||
@@ -647,11 +761,21 @@ mod tests {
|
||||
enc_dispatch(&mut pe, (disp - image_base) as usize, 0, 10);
|
||||
|
||||
let mut fa = mk_func_analysis(ctor, 0x40);
|
||||
fa.functions.insert(disp, FuncInfo {
|
||||
start: disp, end: disp + 0x40, frame_size: 0, saved_gprs: 0,
|
||||
is_leaf: false, is_saverestore: false,
|
||||
pdata_validated: false, pdata_length: None, pdata_prolog_length: None, has_eh: false,
|
||||
});
|
||||
fa.functions.insert(
|
||||
disp,
|
||||
FuncInfo {
|
||||
start: disp,
|
||||
end: disp + 0x40,
|
||||
frame_size: 0,
|
||||
saved_gprs: 0,
|
||||
is_leaf: false,
|
||||
is_saverestore: false,
|
||||
pdata_validated: false,
|
||||
pdata_length: None,
|
||||
pdata_prolog_length: None,
|
||||
has_eh: false,
|
||||
},
|
||||
);
|
||||
|
||||
let vt = mk_vtable(0x82010000, vec![0x11, 0x22, 0x33, 0x44]);
|
||||
let labels: HashMap<u32, String> = HashMap::new();
|
||||
@@ -673,11 +797,21 @@ mod tests {
|
||||
enc_dispatch(&mut pe, (disp - image_base) as usize, 8, 1);
|
||||
|
||||
let mut fa = mk_func_analysis(ctor, 0x40);
|
||||
fa.functions.insert(disp, FuncInfo {
|
||||
start: disp, end: disp + 0x40, frame_size: 0, saved_gprs: 0,
|
||||
is_leaf: false, is_saverestore: false,
|
||||
pdata_validated: false, pdata_length: None, pdata_prolog_length: None, has_eh: false,
|
||||
});
|
||||
fa.functions.insert(
|
||||
disp,
|
||||
FuncInfo {
|
||||
start: disp,
|
||||
end: disp + 0x40,
|
||||
frame_size: 0,
|
||||
saved_gprs: 0,
|
||||
is_leaf: false,
|
||||
is_saverestore: false,
|
||||
pdata_validated: false,
|
||||
pdata_length: None,
|
||||
pdata_prolog_length: None,
|
||||
has_eh: false,
|
||||
},
|
||||
);
|
||||
|
||||
let vt = mk_vtable(0x82010000, vec![0x11, 0x22, 0x33, 0x44]);
|
||||
let labels: HashMap<u32, String> = HashMap::new();
|
||||
@@ -707,5 +841,4 @@ mod tests {
|
||||
assert!(d.method_pcs.is_empty(), "no speculative edges");
|
||||
assert!(d.candidate_vtables.is_empty());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -57,12 +57,12 @@ enum RegVal {
|
||||
},
|
||||
}
|
||||
|
||||
const OP_ADDI: u32 = 14;
|
||||
const OP_ADDI: u32 = 14;
|
||||
const OP_ADDIS: u32 = 15;
|
||||
const OP_BCCTR: u32 = 19; // also covers blr — distinguish via XO
|
||||
const OP_LWZ: u32 = 32;
|
||||
const OP_ORI: u32 = 24;
|
||||
const OP_X_FORM: u32 = 31; // mtspr / mr / etc.
|
||||
const OP_BCCTR: u32 = 19; // also covers blr — distinguish via XO
|
||||
const OP_LWZ: u32 = 32;
|
||||
const OP_ORI: u32 = 24;
|
||||
const OP_X_FORM: u32 = 31; // mtspr / mr / etc.
|
||||
|
||||
/// Run the static indirect-dispatch scan. Returns one edge per resolvable
|
||||
/// `bcctrl` site.
|
||||
@@ -77,8 +77,7 @@ pub fn analyze(
|
||||
let started = std::time::Instant::now();
|
||||
// Index vtables by their start VA so the lwz handler can decide
|
||||
// whether a given Const(addr) is "really" a vtable.
|
||||
let vtable_by_addr: BTreeMap<u32, &Vtable> =
|
||||
vtables.iter().map(|v| (v.address, v)).collect();
|
||||
let vtable_by_addr: BTreeMap<u32, &Vtable> = vtables.iter().map(|v| (v.address, v)).collect();
|
||||
|
||||
// Set of all "label"-bearing PCs in the analyzed binary. We treat each
|
||||
// label as a basic-block boundary (anything `loc_*` is a jump target,
|
||||
@@ -91,7 +90,9 @@ pub fn analyze(
|
||||
let mut edges: Vec<IndirectEdge> = Vec::new();
|
||||
|
||||
for (&fn_start, fi) in &func_analysis.functions {
|
||||
if fi.is_saverestore { continue; }
|
||||
if fi.is_saverestore {
|
||||
continue;
|
||||
}
|
||||
let mut reg: [Option<RegVal>; 32] = [None; 32];
|
||||
let mut ctr: Option<RegVal> = None;
|
||||
let mut pc = fn_start;
|
||||
@@ -162,7 +163,9 @@ pub fn analyze(
|
||||
let resolved = resolve_vtable_slot(target, &vtable_by_addr)
|
||||
.or_else(|| resolve_vtable_slot_via_off(base, simm, &vtable_by_addr));
|
||||
reg[rd] = resolved.map(|(vt, slot, pc)| RegVal::MethodPtr {
|
||||
vtable_addr: vt, slot, method_pc: pc,
|
||||
vtable_addr: vt,
|
||||
slot,
|
||||
method_pc: pc,
|
||||
});
|
||||
} else {
|
||||
reg[rd] = None;
|
||||
@@ -201,7 +204,11 @@ pub fn analyze(
|
||||
if xo == 528 {
|
||||
let lk = (instr & 1) != 0;
|
||||
if lk
|
||||
&& let Some(RegVal::MethodPtr { vtable_addr, slot, method_pc }) = ctr
|
||||
&& let Some(RegVal::MethodPtr {
|
||||
vtable_addr,
|
||||
slot,
|
||||
method_pc,
|
||||
}) = ctr
|
||||
{
|
||||
edges.push(IndirectEdge {
|
||||
source: pc,
|
||||
@@ -227,7 +234,9 @@ pub fn analyze(
|
||||
18 => {
|
||||
let lk = (instr & 1) != 0;
|
||||
if lk {
|
||||
for r in 0..=12 { reg[r] = None; }
|
||||
for r in 0..=12 {
|
||||
reg[r] = None;
|
||||
}
|
||||
ctr = None;
|
||||
}
|
||||
// LK=0 (`b`) makes fall-through unreachable; nothing to do —
|
||||
@@ -239,7 +248,9 @@ pub fn analyze(
|
||||
16 => {
|
||||
let lk = (instr & 1) != 0;
|
||||
if lk {
|
||||
for r in 0..=12 { reg[r] = None; }
|
||||
for r in 0..=12 {
|
||||
reg[r] = None;
|
||||
}
|
||||
ctr = None;
|
||||
}
|
||||
}
|
||||
@@ -274,8 +285,15 @@ pub fn analyze(
|
||||
|
||||
fn read_instr(pe: &[u8], image_base: u32, addr: u32) -> Option<u32> {
|
||||
let off = addr.wrapping_sub(image_base) as usize;
|
||||
if off + 4 > pe.len() { return None; }
|
||||
Some(u32::from_be_bytes([pe[off], pe[off + 1], pe[off + 2], pe[off + 3]]))
|
||||
if off + 4 > pe.len() {
|
||||
return None;
|
||||
}
|
||||
Some(u32::from_be_bytes([
|
||||
pe[off],
|
||||
pe[off + 1],
|
||||
pe[off + 2],
|
||||
pe[off + 3],
|
||||
]))
|
||||
}
|
||||
|
||||
/// `target = base + simm` where `target` is an exact vtable head (rare,
|
||||
@@ -303,11 +321,17 @@ fn resolve_vtable_slot(
|
||||
) -> Option<(u32, u32, u32)> {
|
||||
// BTreeMap range search for the largest key ≤ target.
|
||||
let (&vt_addr, vt) = vtable_by_addr.range(..=target).next_back()?;
|
||||
if target < vt_addr { return None; }
|
||||
if target < vt_addr {
|
||||
return None;
|
||||
}
|
||||
let off = target - vt_addr;
|
||||
if !off.is_multiple_of(4) { return None; }
|
||||
if !off.is_multiple_of(4) {
|
||||
return None;
|
||||
}
|
||||
let slot = off / 4;
|
||||
if slot >= vt.length { return None; }
|
||||
if slot >= vt.length {
|
||||
return None;
|
||||
}
|
||||
let method_pc = *vt.methods.get(slot as usize)?;
|
||||
Some((vt_addr, slot, method_pc))
|
||||
}
|
||||
@@ -339,14 +363,14 @@ mod tests {
|
||||
fn encode_pattern(buf: &mut [u8], offset: usize, vtable_addr: u32, slot_off: i32) {
|
||||
let hi = (vtable_addr >> 16) as u16;
|
||||
let lo = (vtable_addr & 0xFFFF) as i16;
|
||||
let lis = (15u32 << 26) | (3 << 21) | (0 << 16) | (hi as u32);
|
||||
let lis = ((15u32 << 26) | (3 << 21)) | (hi as u32);
|
||||
// addi r3, r3, lo (signed) — note: addi is treated as signed
|
||||
let addi = (14u32 << 26) | (3 << 21) | (3 << 16) | ((lo as u16) as u32);
|
||||
let lwz = (32u32 << 26) | (4 << 21) | (3 << 16) | ((slot_off as u16) as u32);
|
||||
// mtctr r4 = mtspr CTR(=9), r4. SPR_low (=9) → Rust bits 16-20;
|
||||
// SPR_high (=0) → Rust bits 11-15. Rc bit 0.
|
||||
let mtctr = (31u32 << 26) | (4 << 21) | (9 << 16) | (0 << 11) | (467 << 1);
|
||||
let bcctrl = (19u32 << 26) | (20 << 21) | (528 << 1) | 1; // bcctrl 20, 0
|
||||
let mtctr = ((31u32 << 26) | (4 << 21) | (9 << 16)) | (467 << 1);
|
||||
let bcctrl = (19u32 << 26) | (20 << 21) | (528 << 1) | 1; // bcctrl 20, 0
|
||||
let words = [lis, addi, lwz, mtctr, bcctrl];
|
||||
for (i, w) in words.iter().enumerate() {
|
||||
buf[offset + i * 4..offset + i * 4 + 4].copy_from_slice(&w.to_be_bytes());
|
||||
@@ -365,18 +389,21 @@ mod tests {
|
||||
encode_pattern(&mut pe, text_va as usize, vtable_addr, 8); // slot 2
|
||||
|
||||
let mut functions: BTreeMap<u32, FuncInfo> = BTreeMap::new();
|
||||
functions.insert(pc_start, FuncInfo {
|
||||
start: pc_start,
|
||||
end: pc_start + 5 * 4,
|
||||
frame_size: 0,
|
||||
saved_gprs: 0,
|
||||
is_leaf: false,
|
||||
is_saverestore: false,
|
||||
pdata_validated: false,
|
||||
pdata_length: None,
|
||||
pdata_prolog_length: None,
|
||||
has_eh: false,
|
||||
});
|
||||
functions.insert(
|
||||
pc_start,
|
||||
FuncInfo {
|
||||
start: pc_start,
|
||||
end: pc_start + 5 * 4,
|
||||
frame_size: 0,
|
||||
saved_gprs: 0,
|
||||
is_leaf: false,
|
||||
is_saverestore: false,
|
||||
pdata_validated: false,
|
||||
pdata_length: None,
|
||||
pdata_prolog_length: None,
|
||||
has_eh: false,
|
||||
},
|
||||
);
|
||||
let func_analysis = FuncAnalysis {
|
||||
functions,
|
||||
save_gpr_base: None,
|
||||
@@ -407,18 +434,21 @@ mod tests {
|
||||
encode_pattern(&mut pe, text_va as usize, vtable_addr, 48);
|
||||
|
||||
let mut functions: BTreeMap<u32, FuncInfo> = BTreeMap::new();
|
||||
functions.insert(pc_start, FuncInfo {
|
||||
start: pc_start,
|
||||
end: pc_start + 5 * 4,
|
||||
frame_size: 0,
|
||||
saved_gprs: 0,
|
||||
is_leaf: false,
|
||||
is_saverestore: false,
|
||||
pdata_validated: false,
|
||||
pdata_length: None,
|
||||
pdata_prolog_length: None,
|
||||
has_eh: false,
|
||||
});
|
||||
functions.insert(
|
||||
pc_start,
|
||||
FuncInfo {
|
||||
start: pc_start,
|
||||
end: pc_start + 5 * 4,
|
||||
frame_size: 0,
|
||||
saved_gprs: 0,
|
||||
is_leaf: false,
|
||||
is_saverestore: false,
|
||||
pdata_validated: false,
|
||||
pdata_length: None,
|
||||
pdata_prolog_length: None,
|
||||
has_eh: false,
|
||||
},
|
||||
);
|
||||
let func_analysis = FuncAnalysis {
|
||||
functions,
|
||||
save_gpr_base: None,
|
||||
@@ -443,18 +473,21 @@ mod tests {
|
||||
encode_pattern(&mut pe, text_va as usize, vtable_addr, 0);
|
||||
|
||||
let mut functions: BTreeMap<u32, FuncInfo> = BTreeMap::new();
|
||||
functions.insert(pc_start, FuncInfo {
|
||||
start: pc_start,
|
||||
end: pc_start + 5 * 4,
|
||||
frame_size: 0,
|
||||
saved_gprs: 0,
|
||||
is_leaf: false,
|
||||
is_saverestore: false,
|
||||
pdata_validated: false,
|
||||
pdata_length: None,
|
||||
pdata_prolog_length: None,
|
||||
has_eh: false,
|
||||
});
|
||||
functions.insert(
|
||||
pc_start,
|
||||
FuncInfo {
|
||||
start: pc_start,
|
||||
end: pc_start + 5 * 4,
|
||||
frame_size: 0,
|
||||
saved_gprs: 0,
|
||||
is_leaf: false,
|
||||
is_saverestore: false,
|
||||
pdata_validated: false,
|
||||
pdata_length: None,
|
||||
pdata_prolog_length: None,
|
||||
has_eh: false,
|
||||
},
|
||||
);
|
||||
let func_analysis = FuncAnalysis {
|
||||
functions,
|
||||
save_gpr_base: None,
|
||||
@@ -469,6 +502,10 @@ mod tests {
|
||||
labels.insert(pc_start + 8, "loc_mid".to_string());
|
||||
|
||||
let edges = analyze(&pe, image_base, &func_analysis, &vtables, &labels);
|
||||
assert_eq!(edges.len(), 0, "label in middle of pattern must kill register state");
|
||||
assert_eq!(
|
||||
edges.len(),
|
||||
0,
|
||||
"label in middle of pattern must kill register state"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,17 +135,33 @@ impl JumpTable {
|
||||
|
||||
const BCTR: u32 = 0x4E80_0420;
|
||||
|
||||
fn op(i: u32) -> u32 { i >> 26 }
|
||||
fn rt(i: u32) -> usize { ((i >> 21) & 0x1F) as usize }
|
||||
fn ra(i: u32) -> usize { ((i >> 16) & 0x1F) as usize }
|
||||
fn rb(i: u32) -> usize { ((i >> 11) & 0x1F) as usize }
|
||||
fn xo(i: u32) -> u32 { (i >> 1) & 0x3FF }
|
||||
fn simm(i: u32) -> i32 { ((i & 0xFFFF) as i16) as i32 }
|
||||
fn uimm(i: u32) -> u32 { i & 0xFFFF }
|
||||
fn op(i: u32) -> u32 {
|
||||
i >> 26
|
||||
}
|
||||
fn rt(i: u32) -> usize {
|
||||
((i >> 21) & 0x1F) as usize
|
||||
}
|
||||
fn ra(i: u32) -> usize {
|
||||
((i >> 16) & 0x1F) as usize
|
||||
}
|
||||
fn rb(i: u32) -> usize {
|
||||
((i >> 11) & 0x1F) as usize
|
||||
}
|
||||
fn xo(i: u32) -> u32 {
|
||||
(i >> 1) & 0x3FF
|
||||
}
|
||||
fn simm(i: u32) -> i32 {
|
||||
((i & 0xFFFF) as i16) as i32
|
||||
}
|
||||
fn uimm(i: u32) -> u32 {
|
||||
i & 0xFFFF
|
||||
}
|
||||
|
||||
/// `mtctr rS` — `mtspr` (op 31, xo 467) with the split SPR field naming CTR (9).
|
||||
fn is_mtctr(i: u32) -> bool {
|
||||
if op(i) != 31 || xo(i) != 467 { return false; }
|
||||
if op(i) != 31 || xo(i) != 467 {
|
||||
return false;
|
||||
}
|
||||
let spr_field = (i >> 11) & 0x3FF;
|
||||
(((spr_field & 0x1F) << 5) | (spr_field >> 5)) == 9
|
||||
}
|
||||
@@ -159,13 +175,13 @@ fn is_mtctr(i: u32) -> bool {
|
||||
fn op31_dest(i: u32) -> Option<usize> {
|
||||
// Logical / shift / sign-extend X-forms: destination is `rA` (bits 16..20).
|
||||
const WRITES_RA: &[u32] = &[
|
||||
24, 26, 27, 28, 58, 60, 124, 284, 316, 412, 444, 476,
|
||||
536, 539, 792, 794, 824, 826, 827, 922, 954, 986,
|
||||
24, 26, 27, 28, 58, 60, 124, 284, 316, 412, 444, 476, 536, 539, 792, 794, 824, 826, 827,
|
||||
922, 954, 986,
|
||||
];
|
||||
// Stores, compares, traps, cache/sync ops and `mtspr`/`mtcrf`: no GPR write.
|
||||
const NO_GPR: &[u32] = &[
|
||||
0, 4, 32, 68, // cmp, tw, cmpl, td
|
||||
150, 151, 215, 407, 662, 918, 660, 727, 231, // stwcx./stwx/stbx/sthx/stfsx/stfdx/…
|
||||
0, 4, 32, 68, // cmp, tw, cmpl, td
|
||||
150, 151, 215, 407, 662, 918, 660, 727, 231, // stwcx./stwx/stbx/sthx/stfsx/stfdx/…
|
||||
144, 467, 512, 598, 854, 982, 1014, 86, 470, 54, // mtcrf/mtspr/mcrxr/sync/dcb*/icbi
|
||||
];
|
||||
// Store-*update* forms write back into `rA`.
|
||||
@@ -173,8 +189,12 @@ fn op31_dest(i: u32) -> Option<usize> {
|
||||
return Some(ra(i));
|
||||
}
|
||||
let x = xo(i);
|
||||
if NO_GPR.contains(&x) { return None; }
|
||||
if WRITES_RA.contains(&x) { return Some(ra(i)); }
|
||||
if NO_GPR.contains(&x) {
|
||||
return None;
|
||||
}
|
||||
if WRITES_RA.contains(&x) {
|
||||
return Some(ra(i));
|
||||
}
|
||||
Some(rt(i))
|
||||
}
|
||||
|
||||
@@ -204,13 +224,25 @@ pub fn analyze_with_stats(
|
||||
let code_ranges: Vec<(u32, u32)> = sections
|
||||
.iter()
|
||||
.filter(|s| s.is_code())
|
||||
.map(|s| (image_base + s.virtual_address, image_base + s.virtual_address + s.virtual_size))
|
||||
.map(|s| {
|
||||
(
|
||||
image_base + s.virtual_address,
|
||||
image_base + s.virtual_address + s.virtual_size,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let read = |va: u32| -> Option<u32> {
|
||||
let off = va.wrapping_sub(image_base) as usize;
|
||||
if off.checked_add(4)? > pe.len() { return None; }
|
||||
Some(u32::from_be_bytes([pe[off], pe[off + 1], pe[off + 2], pe[off + 3]]))
|
||||
if off.checked_add(4)? > pe.len() {
|
||||
return None;
|
||||
}
|
||||
Some(u32::from_be_bytes([
|
||||
pe[off],
|
||||
pe[off + 1],
|
||||
pe[off + 2],
|
||||
pe[off + 3],
|
||||
]))
|
||||
};
|
||||
let in_code = |va: u32| code_ranges.iter().any(|&(s, e)| va >= s && va < e);
|
||||
|
||||
@@ -285,11 +317,11 @@ fn recover_at(
|
||||
|
||||
// Straight-line constant propagation over [window_start, bctr_pc).
|
||||
let mut regs: [Option<u32>; 32] = [None; 32];
|
||||
let mut lwzx_dest: Option<usize> = None; // rT of the last lwzx
|
||||
let mut lwzx_dest: Option<usize> = None; // rT of the last lwzx
|
||||
let mut lwzx_regs: Option<(Option<u32>, Option<u32>)> = None;
|
||||
let mut lwzx_index_tainted = false; // did the index come from the lbzx?
|
||||
let mut lwzx_index_tainted = false; // did the index come from the lbzx?
|
||||
let mut lbzx_regs: Option<(Option<u32>, Option<u32>)> = None;
|
||||
let mut ctr_src: Option<usize> = None; // rS of the last mtctr
|
||||
let mut ctr_src: Option<usize> = None; // rS of the last mtctr
|
||||
let mut bound: Option<u32> = None;
|
||||
// Taint: "this register holds a value derived from the byte the `lbzx`
|
||||
// read". Only a register carrying that taint may serve as the jump table's
|
||||
@@ -300,7 +332,7 @@ fn recover_at(
|
||||
|
||||
let mut pc = window_start;
|
||||
while pc < bctr_pc {
|
||||
let Some(i) = read(pc) else { return None };
|
||||
let i = read(pc)?;
|
||||
match op(i) {
|
||||
// addis rT, rA, SIMM (lis when rA == 0)
|
||||
15 => {
|
||||
@@ -319,14 +351,16 @@ fn recover_at(
|
||||
10 | 11 => bound = Some(uimm(i)),
|
||||
31 => {
|
||||
match xo(i) {
|
||||
23 => { // lwzx rT, rA, rB — the table read
|
||||
23 => {
|
||||
// lwzx rT, rA, rB — the table read
|
||||
lwzx_dest = Some(rt(i));
|
||||
lwzx_regs = Some((regs[ra(i)], regs[rb(i)]));
|
||||
lwzx_index_tainted = from_lbzx[ra(i)] || from_lbzx[rb(i)];
|
||||
regs[rt(i)] = None;
|
||||
from_lbzx[rt(i)] = false;
|
||||
}
|
||||
87 => { // lbzx rT, rA, rB — the sparse index-map read
|
||||
87 => {
|
||||
// lbzx rT, rA, rB — the sparse index-map read
|
||||
lbzx_regs = Some((regs[ra(i)], regs[rb(i)]));
|
||||
regs[rt(i)] = None;
|
||||
from_lbzx = [false; 32];
|
||||
@@ -363,14 +397,23 @@ fn recover_at(
|
||||
}
|
||||
// D/DS-form GPR loads write rT; the update forms also write rA.
|
||||
32 | 34 | 40 | 42 => regs[rt(i)] = None,
|
||||
33 | 35 | 41 | 43 => { regs[rt(i)] = None; regs[ra(i)] = None; }
|
||||
33 | 35 | 41 | 43 => {
|
||||
regs[rt(i)] = None;
|
||||
regs[ra(i)] = None;
|
||||
}
|
||||
// DS-form: bits 30..31 pick ld(0) / ldu(1) / lwa(2).
|
||||
58 => {
|
||||
regs[rt(i)] = None;
|
||||
if i & 3 == 1 { regs[ra(i)] = None; }
|
||||
if i & 3 == 1 {
|
||||
regs[ra(i)] = None;
|
||||
}
|
||||
}
|
||||
// lmw loads rT..r31.
|
||||
46 => for r in rt(i)..32 { regs[r] = None; },
|
||||
46 => {
|
||||
for r in rt(i)..32 {
|
||||
regs[r] = None;
|
||||
}
|
||||
}
|
||||
// FP loads touch no GPR — except the update forms, which write rA.
|
||||
// Plain stores write no register at all (their `rT` field is the
|
||||
// *source*), so a tracked base that merely gets spilled survives.
|
||||
@@ -440,7 +483,9 @@ fn recover_at(
|
||||
let word = read(byte_off & !3)?;
|
||||
let slot = (word >> (8 * (3 - (byte_off & 3)))) & 0xFF;
|
||||
let t = read(table_address.wrapping_add(slot * 4))?;
|
||||
if !valid(t) { break; }
|
||||
if !valid(t) {
|
||||
break;
|
||||
}
|
||||
max_slot = max_slot.max(slot);
|
||||
targets.push(t);
|
||||
}
|
||||
@@ -464,11 +509,18 @@ fn recover_at(
|
||||
}
|
||||
|
||||
// Dense: read consecutive absolute targets until one leaves the function.
|
||||
let cap = bound.map(|n| n.saturating_add(1)).unwrap_or(MAX_ENTRIES).min(MAX_ENTRIES);
|
||||
let cap = bound
|
||||
.map(|n| n.saturating_add(1))
|
||||
.unwrap_or(MAX_ENTRIES)
|
||||
.min(MAX_ENTRIES);
|
||||
let mut targets = Vec::new();
|
||||
for i in 0..cap {
|
||||
let Some(t) = read(table_address.wrapping_add(i * 4)) else { break };
|
||||
if !valid(t) { break; }
|
||||
let Some(t) = read(table_address.wrapping_add(i * 4)) else {
|
||||
break;
|
||||
};
|
||||
if !valid(t) {
|
||||
break;
|
||||
}
|
||||
targets.push(t);
|
||||
}
|
||||
if targets.len() < 2 {
|
||||
@@ -554,19 +606,27 @@ mod tests {
|
||||
|
||||
fn one_function(start: u32, end: u32) -> FuncAnalysis {
|
||||
let mut functions = BTreeMap::new();
|
||||
functions.insert(start, FuncInfo {
|
||||
functions.insert(
|
||||
start,
|
||||
end,
|
||||
frame_size: 0,
|
||||
saved_gprs: 0,
|
||||
is_leaf: false,
|
||||
is_saverestore: false,
|
||||
pdata_validated: true,
|
||||
pdata_length: Some(end - start),
|
||||
pdata_prolog_length: Some(0),
|
||||
has_eh: false,
|
||||
});
|
||||
FuncAnalysis { functions, save_gpr_base: None, restore_gpr_base: None, pdata_entries: Vec::new() }
|
||||
FuncInfo {
|
||||
start,
|
||||
end,
|
||||
frame_size: 0,
|
||||
saved_gprs: 0,
|
||||
is_leaf: false,
|
||||
is_saverestore: false,
|
||||
pdata_validated: true,
|
||||
pdata_length: Some(end - start),
|
||||
pdata_prolog_length: Some(0),
|
||||
has_eh: false,
|
||||
},
|
||||
);
|
||||
FuncAnalysis {
|
||||
functions,
|
||||
save_gpr_base: None,
|
||||
restore_gpr_base: None,
|
||||
pdata_entries: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Encode the words of the canonical MSVC dense-switch dispatch, ending at
|
||||
@@ -575,13 +635,13 @@ mod tests {
|
||||
/// / mtctr r0 / bctr / <table>
|
||||
fn dense_switch(table_va: u32, n_cases: u32) -> Vec<u32> {
|
||||
vec![
|
||||
0x2800_0000 | (10 << 16) | (n_cases - 1), // cmplwi r10, N
|
||||
0x4181_0000 | 0x20, // bc (bound check, target irrelevant)
|
||||
0x3D80_0000 | (table_va >> 16), // lis r12, hi
|
||||
0x398C_0000 | (table_va & 0xFFFF), // addi r12, r12, lo
|
||||
0x5540_103A, // slwi r0, r10, 2
|
||||
0x7C0C_002E, // lwzx r0, r12, r0
|
||||
0x7C09_03A6, // mtctr r0
|
||||
0x2800_0000 | (10 << 16) | (n_cases - 1), // cmplwi r10, N
|
||||
0x4181_0000 | 0x20, // bc (bound check, target irrelevant)
|
||||
0x3D80_0000 | (table_va >> 16), // lis r12, hi
|
||||
0x398C_0000 | (table_va & 0xFFFF), // addi r12, r12, lo
|
||||
0x5540_103A, // slwi r0, r10, 2
|
||||
0x7C0C_002E, // lwzx r0, r12, r0
|
||||
0x7C09_03A6, // mtctr r0
|
||||
BCTR,
|
||||
]
|
||||
}
|
||||
@@ -600,7 +660,12 @@ mod tests {
|
||||
let table_va = TEXT_VA + 8 * 4;
|
||||
let mut words = dense_switch(table_va, 4);
|
||||
// Four case bodies, all inside the function.
|
||||
let cases = [TEXT_VA + 0x40, TEXT_VA + 0x50, TEXT_VA + 0x60, TEXT_VA + 0x70];
|
||||
let cases = [
|
||||
TEXT_VA + 0x40,
|
||||
TEXT_VA + 0x50,
|
||||
TEXT_VA + 0x60,
|
||||
TEXT_VA + 0x70,
|
||||
];
|
||||
words.extend_from_slice(&cases);
|
||||
|
||||
let pe = assemble(&words, 0x100);
|
||||
@@ -626,7 +691,9 @@ mod tests {
|
||||
let table_va = TEXT_VA + 8 * 4;
|
||||
let mut words = dense_switch(table_va, 8);
|
||||
words.extend_from_slice(&[
|
||||
TEXT_VA + 0x40, TEXT_VA + 0x50, TEXT_VA + 0x60,
|
||||
TEXT_VA + 0x40,
|
||||
TEXT_VA + 0x50,
|
||||
TEXT_VA + 0x60,
|
||||
0x8300_0000, // far outside
|
||||
TEXT_VA + 0x70,
|
||||
]);
|
||||
@@ -659,26 +726,31 @@ mod tests {
|
||||
fn recovers_sparse_two_level_switch() {
|
||||
// cmplwi r10,5 / bc / lis+addi r11 = &map / lbzx r0,r11,r10
|
||||
// / lis+addi r12 = &table / slwi r0,r0,2 / lwzx r0,r12,r0 / mtctr / bctr
|
||||
let map_va = TEXT_VA + 13 * 4; // 6 bytes, then padding
|
||||
let table_va = TEXT_VA + 17 * 4; // 3 distinct bodies
|
||||
let map_va = TEXT_VA + 13 * 4; // 6 bytes, then padding
|
||||
let table_va = TEXT_VA + 17 * 4; // 3 distinct bodies
|
||||
let words = vec![
|
||||
0x2800_0000 | (10 << 16) | 5, // cmplwi r10, 5
|
||||
0x4181_0000 | 0x20, // bc
|
||||
0x3D60_0000 | (map_va >> 16), // lis r11, map@h
|
||||
0x396B_0000 | (map_va & 0xFFFF), // addi r11, r11, map@l
|
||||
0x7C0B_50AE, // lbzx r0, r11, r10
|
||||
0x3D80_0000 | (table_va >> 16), // lis r12, tab@h
|
||||
0x398C_0000 | (table_va & 0xFFFF), // addi r12, r12, tab@l
|
||||
0x5400_103A, // slwi r0, r0, 2
|
||||
0x7C0C_002E, // lwzx r0, r12, r0
|
||||
0x7C09_03A6, // mtctr r0
|
||||
0x2800_0000 | (10 << 16) | 5, // cmplwi r10, 5
|
||||
0x4181_0000 | 0x20, // bc
|
||||
0x3D60_0000 | (map_va >> 16), // lis r11, map@h
|
||||
0x396B_0000 | (map_va & 0xFFFF), // addi r11, r11, map@l
|
||||
0x7C0B_50AE, // lbzx r0, r11, r10
|
||||
0x3D80_0000 | (table_va >> 16), // lis r12, tab@h
|
||||
0x398C_0000 | (table_va & 0xFFFF), // addi r12, r12, tab@l
|
||||
0x5400_103A, // slwi r0, r0, 2
|
||||
0x7C0C_002E, // lwzx r0, r12, r0
|
||||
0x7C09_03A6, // mtctr r0
|
||||
BCTR,
|
||||
0, 0,
|
||||
0,
|
||||
0,
|
||||
// map[0..6] = 0,1,2,2,1,0 packed big-endian, then padding
|
||||
0x0001_0202, 0x0100_0000,
|
||||
0, 0,
|
||||
0x0001_0202,
|
||||
0x0100_0000,
|
||||
0,
|
||||
0,
|
||||
// table[0..3]
|
||||
TEXT_VA + 0x80, TEXT_VA + 0x90, TEXT_VA + 0xA0,
|
||||
TEXT_VA + 0x80,
|
||||
TEXT_VA + 0x90,
|
||||
TEXT_VA + 0xA0,
|
||||
];
|
||||
let pe = assemble(&words, 0x100);
|
||||
let sections = [text_section(0x100)];
|
||||
@@ -691,10 +763,17 @@ mod tests {
|
||||
assert_eq!(jt.index_map_address, Some(map_va));
|
||||
assert_eq!(jt.index_map_count, Some(6));
|
||||
assert_eq!(jt.table_slots, 3);
|
||||
assert_eq!(jt.targets, vec![
|
||||
TEXT_VA + 0x80, TEXT_VA + 0x90, TEXT_VA + 0xA0,
|
||||
TEXT_VA + 0xA0, TEXT_VA + 0x90, TEXT_VA + 0x80,
|
||||
]);
|
||||
assert_eq!(
|
||||
jt.targets,
|
||||
vec![
|
||||
TEXT_VA + 0x80,
|
||||
TEXT_VA + 0x90,
|
||||
TEXT_VA + 0xA0,
|
||||
TEXT_VA + 0xA0,
|
||||
TEXT_VA + 0x90,
|
||||
TEXT_VA + 0x80,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/// An unrelated `lbzx` in the window must not be mistaken for a case index
|
||||
@@ -704,19 +783,24 @@ mod tests {
|
||||
fn unrelated_lbzx_does_not_become_an_index_map() {
|
||||
let table_va = TEXT_VA + 11 * 4;
|
||||
let mut words = vec![
|
||||
0x2800_0000 | (10 << 16) | 3, // cmplwi r10, 3
|
||||
0x4181_0000 | 0x20, // bc
|
||||
0x3D60_0000 | (TEXT_VA >> 16), // lis r11, text@h (a code constant)
|
||||
0x396B_0000 | (TEXT_VA & 0xFFFF), // addi r11, r11, text@l
|
||||
0x7CEB_44AE, // lbzx r7, r11, r8 — unrelated byte load
|
||||
0x3D80_0000 | (table_va >> 16), // lis r12, tab@h
|
||||
0x398C_0000 | (table_va & 0xFFFF), // addi r12, r12, tab@l
|
||||
0x5540_103A, // slwi r0, r10, 2 — index is r10, NOT r7
|
||||
0x7C0C_002E, // lwzx r0, r12, r0
|
||||
0x7C09_03A6, // mtctr r0
|
||||
0x2800_0000 | (10 << 16) | 3, // cmplwi r10, 3
|
||||
0x4181_0000 | 0x20, // bc
|
||||
0x3D60_0000 | (TEXT_VA >> 16), // lis r11, text@h (a code constant)
|
||||
0x396B_0000 | (TEXT_VA & 0xFFFF), // addi r11, r11, text@l
|
||||
0x7CEB_44AE, // lbzx r7, r11, r8 — unrelated byte load
|
||||
0x3D80_0000 | (table_va >> 16), // lis r12, tab@h
|
||||
0x398C_0000 | (table_va & 0xFFFF), // addi r12, r12, tab@l
|
||||
0x5540_103A, // slwi r0, r10, 2 — index is r10, NOT r7
|
||||
0x7C0C_002E, // lwzx r0, r12, r0
|
||||
0x7C09_03A6, // mtctr r0
|
||||
];
|
||||
words.push(BCTR);
|
||||
words.extend_from_slice(&[TEXT_VA + 0x80, TEXT_VA + 0x90, TEXT_VA + 0xA0, TEXT_VA + 0xB0]);
|
||||
words.extend_from_slice(&[
|
||||
TEXT_VA + 0x80,
|
||||
TEXT_VA + 0x90,
|
||||
TEXT_VA + 0xA0,
|
||||
TEXT_VA + 0xB0,
|
||||
]);
|
||||
|
||||
let pe = assemble(&words, 0x100);
|
||||
let sections = [text_section(0x100)];
|
||||
@@ -731,12 +815,22 @@ mod tests {
|
||||
#[test]
|
||||
fn data_regions_merge_adjacent_tables() {
|
||||
let a = JumpTable {
|
||||
bctr_pc: 0x8200_1000, function: None, table_address: 0x8200_2000,
|
||||
entry_count: 4, table_slots: 4, index_map_address: None,
|
||||
index_map_count: None, bound: None, kind: "direct",
|
||||
bctr_pc: 0x8200_1000,
|
||||
function: None,
|
||||
table_address: 0x8200_2000,
|
||||
entry_count: 4,
|
||||
table_slots: 4,
|
||||
index_map_address: None,
|
||||
index_map_count: None,
|
||||
bound: None,
|
||||
kind: "direct",
|
||||
targets: vec![0; 4],
|
||||
};
|
||||
let b = JumpTable { table_address: 0x8200_2010, bctr_pc: 0x8200_1004, ..a.clone() };
|
||||
let b = JumpTable {
|
||||
table_address: 0x8200_2010,
|
||||
bctr_pc: 0x8200_1004,
|
||||
..a.clone()
|
||||
};
|
||||
let merged = data_regions(&[a, b]);
|
||||
assert_eq!(merged, vec![(0x8200_2000, 32)]);
|
||||
}
|
||||
@@ -744,9 +838,15 @@ mod tests {
|
||||
#[test]
|
||||
fn data_word_addresses_covers_every_slot() {
|
||||
let jt = JumpTable {
|
||||
bctr_pc: 0x8200_1000, function: None, table_address: 0x8200_2000,
|
||||
entry_count: 3, table_slots: 3, index_map_address: Some(0x8200_3000),
|
||||
index_map_count: Some(5), bound: Some(4), kind: "indexed",
|
||||
bctr_pc: 0x8200_1000,
|
||||
function: None,
|
||||
table_address: 0x8200_2000,
|
||||
entry_count: 3,
|
||||
table_slots: 3,
|
||||
index_map_address: Some(0x8200_3000),
|
||||
index_map_count: Some(5),
|
||||
bound: Some(4),
|
||||
kind: "indexed",
|
||||
targets: vec![0; 5],
|
||||
};
|
||||
let words = data_word_addresses(&[jt]);
|
||||
|
||||
@@ -1,26 +1,46 @@
|
||||
pub mod ppc;
|
||||
pub mod func;
|
||||
pub mod xref;
|
||||
// 🔴 THREE LINTS ARE OFF FOR THIS CRATE, WITH REASONS, RATHER THAN SILENTLY.
|
||||
//
|
||||
// * `needless_range_loop` — nine sites index `reg[r]` where **`r` is the
|
||||
// PowerPC register number**. The index is the meaning; an iterator hides
|
||||
// which GPR a pattern matched, which is the whole content of these passes.
|
||||
// * `too_many_arguments` — five analysis passes take the image, its base, the
|
||||
// section table, the function list and several output sinks. Bundling those
|
||||
// into a struct moves the list rather than shortening it, and this code
|
||||
// arrived whole from a retired repository: a refactor here would be an
|
||||
// unreviewed edit dressed as a lint fix.
|
||||
// * `type_complexity` — one return type in `vtables.rs`, same argument.
|
||||
//
|
||||
// Everything else clippy asked for was fixed, including every doc-indent site.
|
||||
// See `docs/agents/CONSOLIDATION.md` Phase 3.
|
||||
#![allow(
|
||||
clippy::needless_range_loop,
|
||||
clippy::too_many_arguments,
|
||||
clippy::type_complexity
|
||||
)]
|
||||
|
||||
pub mod db;
|
||||
pub mod demangle;
|
||||
pub mod disasm;
|
||||
pub mod eh_scope;
|
||||
pub mod formatter;
|
||||
pub mod func;
|
||||
pub mod funcptr_arrays;
|
||||
pub mod ind_dispatch_typed;
|
||||
pub mod indirect;
|
||||
pub mod jumptables;
|
||||
pub mod lookup;
|
||||
pub mod ppc;
|
||||
pub mod rtti;
|
||||
pub mod sinks;
|
||||
pub mod sql_views;
|
||||
pub mod demangle;
|
||||
pub mod vtables;
|
||||
pub mod lookup;
|
||||
pub mod indirect;
|
||||
pub mod ind_dispatch_typed;
|
||||
pub mod strings;
|
||||
pub mod funcptr_arrays;
|
||||
pub mod eh_scope;
|
||||
pub mod static_init;
|
||||
pub mod strings;
|
||||
pub mod vtables;
|
||||
pub mod xdbf;
|
||||
pub mod jumptables;
|
||||
pub mod rtti;
|
||||
pub mod xref;
|
||||
|
||||
mod ordinals;
|
||||
pub use ordinals::resolve_ordinal;
|
||||
pub use xref::{XrefKind, Xref, XrefMap, resolve_source_label};
|
||||
pub use db::{DbWriter, ExecTraceEntry, ImportCallEntry, BranchTraceEntry};
|
||||
pub use db::{BranchTraceEntry, DbWriter, ExecTraceEntry, ImportCallEntry};
|
||||
pub use disasm::{RichDisasmItem, enrich_section};
|
||||
pub use ordinals::resolve_ordinal;
|
||||
pub use xref::{Xref, XrefKind, XrefMap, resolve_source_label};
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use anyhow::{Result, anyhow};
|
||||
use duckdb::params;
|
||||
|
||||
/// Parse one probe token into one or more PCs.
|
||||
@@ -72,7 +72,10 @@ pub fn resolve_probe_token(db_path: Option<&Path>, token: &str) -> Result<Vec<u3
|
||||
}
|
||||
|
||||
fn parse_numeric(token: &str) -> Option<u32> {
|
||||
if let Some(hex) = token.strip_prefix("0x").or_else(|| token.strip_prefix("0X")) {
|
||||
if let Some(hex) = token
|
||||
.strip_prefix("0x")
|
||||
.or_else(|| token.strip_prefix("0X"))
|
||||
{
|
||||
return u32::from_str_radix(hex, 16).ok();
|
||||
}
|
||||
token.parse::<u32>().ok()
|
||||
@@ -89,7 +92,9 @@ fn resolve_class_method(conn: &duckdb::Connection, class: &str, method: &str) ->
|
||||
WHERE c.name = ? AND dn.method_name = ?",
|
||||
)?;
|
||||
let pcs: Vec<u32> = stmt
|
||||
.query_map(params![class, method], |r| r.get::<_, i64>(0).map(|x| x as u32))?
|
||||
.query_map(params![class, method], |r| {
|
||||
r.get::<_, i64>(0).map(|x| x as u32)
|
||||
})?
|
||||
.filter_map(|r| r.ok())
|
||||
.collect();
|
||||
Ok(pcs)
|
||||
|
||||
@@ -24,5 +24,8 @@ impl Decoded {
|
||||
pub fn disasm(instr: u32, addr: u32) -> Decoded {
|
||||
let d = decode(instr, addr);
|
||||
let t = format(&d);
|
||||
Decoded { base: t.disasm, ext: t.ext_disasm }
|
||||
Decoded {
|
||||
base: t.disasm,
|
||||
ext: t.ext_disasm,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,8 +117,11 @@ impl RttiResult {
|
||||
|
||||
/// `vftable[0]` VA → `(demangled class name, subobject offset)`.
|
||||
pub fn vtable_class_names(&self) -> BTreeMap<u32, (String, u32)> {
|
||||
let td: BTreeMap<u32, &TypeDescriptor> =
|
||||
self.type_descriptors.iter().map(|t| (t.address, t)).collect();
|
||||
let td: BTreeMap<u32, &TypeDescriptor> = self
|
||||
.type_descriptors
|
||||
.iter()
|
||||
.map(|t| (t.address, t))
|
||||
.collect();
|
||||
let mut out = BTreeMap::new();
|
||||
for col in &self.locators {
|
||||
if let (Some(vt), Some(t)) = (col.vtable_address, td.get(&col.type_descriptor)) {
|
||||
@@ -138,8 +141,15 @@ pub fn analyze(pe: &[u8], image_base: u32, sections: &[PeSection]) -> RttiResult
|
||||
|
||||
let read = |va: u32| -> Option<u32> {
|
||||
let off = va.wrapping_sub(image_base) as usize;
|
||||
if off.checked_add(4)? > pe.len() { return None; }
|
||||
Some(u32::from_be_bytes([pe[off], pe[off + 1], pe[off + 2], pe[off + 3]]))
|
||||
if off.checked_add(4)? > pe.len() {
|
||||
return None;
|
||||
}
|
||||
Some(u32::from_be_bytes([
|
||||
pe[off],
|
||||
pe[off + 1],
|
||||
pe[off + 2],
|
||||
pe[off + 3],
|
||||
]))
|
||||
};
|
||||
|
||||
// Byte ranges actually backed by file data (a section's tail beyond
|
||||
@@ -151,10 +161,16 @@ pub fn analyze(pe: &[u8], image_base: u32, sections: &[PeSection]) -> RttiResult
|
||||
};
|
||||
let ranges: Vec<(String, u32, u32)> = sections
|
||||
.iter()
|
||||
.map(|s| { let (a, b) = backed(s); (s.name.clone(), a, b) })
|
||||
.map(|s| {
|
||||
let (a, b) = backed(s);
|
||||
(s.name.clone(), a, b)
|
||||
})
|
||||
.collect();
|
||||
let range_of = |name: &str| -> Option<(u32, u32)> {
|
||||
ranges.iter().find(|(n, _, _)| n == name).map(|&(_, a, b)| (a, b))
|
||||
ranges
|
||||
.iter()
|
||||
.find(|(n, _, _)| n == name)
|
||||
.map(|&(_, a, b)| (a, b))
|
||||
};
|
||||
|
||||
// 1. TypeDescriptors. The decorated name lives at descriptor+8 and always
|
||||
@@ -162,19 +178,35 @@ pub fn analyze(pe: &[u8], image_base: u32, sections: &[PeSection]) -> RttiResult
|
||||
let mut type_descriptors: Vec<TypeDescriptor> = Vec::new();
|
||||
let mut td_addrs: BTreeSet<u32> = BTreeSet::new();
|
||||
for (name, start, end) in &ranges {
|
||||
if !matches!(name.as_str(), ".data" | ".rdata") { continue; }
|
||||
if !matches!(name.as_str(), ".data" | ".rdata") {
|
||||
continue;
|
||||
}
|
||||
let s = (*start).wrapping_sub(image_base) as usize;
|
||||
let e = (*end).wrapping_sub(image_base) as usize;
|
||||
if e > pe.len() || s >= e { continue; }
|
||||
if e > pe.len() || s >= e {
|
||||
continue;
|
||||
}
|
||||
let bytes = &pe[s..e];
|
||||
let mut i = 0usize;
|
||||
while i + 3 < bytes.len() {
|
||||
if &bytes[i..i + 3] != b".?A" { i += 1; continue; }
|
||||
if &bytes[i..i + 3] != b".?A" {
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
let name_va = start.wrapping_add(i as u32);
|
||||
// The descriptor head sits 8 bytes before the name.
|
||||
let Some(td_va) = name_va.checked_sub(8) else { i += 1; continue };
|
||||
if td_va < *start { i += 1; continue; }
|
||||
let Some(decorated) = read_cstr(bytes, i, 512) else { i += 1; continue };
|
||||
let Some(td_va) = name_va.checked_sub(8) else {
|
||||
i += 1;
|
||||
continue;
|
||||
};
|
||||
if td_va < *start {
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
let Some(decorated) = read_cstr(bytes, i, 512) else {
|
||||
i += 1;
|
||||
continue;
|
||||
};
|
||||
i += decorated.len() + 1;
|
||||
if td_addrs.insert(td_va) {
|
||||
type_descriptors.push(TypeDescriptor {
|
||||
@@ -197,8 +229,14 @@ pub fn analyze(pe: &[u8], image_base: u32, sections: &[PeSection]) -> RttiResult
|
||||
let mut va = rd_start;
|
||||
while va + 20 <= rd_end {
|
||||
let (Some(sig), Some(off), Some(cd), Some(ptd), Some(pchd)) = (
|
||||
read(va), read(va + 4), read(va + 8), read(va + 12), read(va + 16),
|
||||
) else { break };
|
||||
read(va),
|
||||
read(va + 4),
|
||||
read(va + 8),
|
||||
read(va + 12),
|
||||
read(va + 16),
|
||||
) else {
|
||||
break;
|
||||
};
|
||||
if sig == 0 && td_addrs.contains(&ptd) && pchd >= rd_start && pchd < rd_end {
|
||||
col_addrs.insert(va);
|
||||
locators.push(CompleteObjectLocator {
|
||||
@@ -217,7 +255,9 @@ pub fn analyze(pe: &[u8], image_base: u32, sections: &[PeSection]) -> RttiResult
|
||||
// 3. `vftable[-1]` sites: any word in initialised data whose value is a COL.
|
||||
let mut vtable_to_locator: BTreeMap<u32, u32> = BTreeMap::new();
|
||||
for (name, start, end) in &ranges {
|
||||
if !matches!(name.as_str(), ".rdata" | ".data") { continue; }
|
||||
if !matches!(name.as_str(), ".rdata" | ".data") {
|
||||
continue;
|
||||
}
|
||||
let mut va = *start;
|
||||
while va + 4 <= *end {
|
||||
if let Some(w) = read(va)
|
||||
@@ -228,8 +268,10 @@ pub fn analyze(pe: &[u8], image_base: u32, sections: &[PeSection]) -> RttiResult
|
||||
va += 4;
|
||||
}
|
||||
}
|
||||
let locator_to_vtable: BTreeMap<u32, u32> =
|
||||
vtable_to_locator.iter().map(|(&vt, &col)| (col, vt)).collect();
|
||||
let locator_to_vtable: BTreeMap<u32, u32> = vtable_to_locator
|
||||
.iter()
|
||||
.map(|(&vt, &col)| (col, vt))
|
||||
.collect();
|
||||
for col in &mut locators {
|
||||
col.vtable_address = locator_to_vtable.get(&col.address).copied();
|
||||
}
|
||||
@@ -241,19 +283,37 @@ pub fn analyze(pe: &[u8], image_base: u32, sections: &[PeSection]) -> RttiResult
|
||||
let chds: BTreeSet<u32> = locators.iter().map(|c| c.class_hierarchy).collect();
|
||||
if let Some((rd_start, rd_end)) = rdata {
|
||||
for chd in chds {
|
||||
let (Some(n_bases), Some(p_array)) = (read(chd + 8), read(chd + 12)) else { continue };
|
||||
let (Some(n_bases), Some(p_array)) = (read(chd + 8), read(chd + 12)) else {
|
||||
continue;
|
||||
};
|
||||
// A malformed or misidentified descriptor would blow the scan up;
|
||||
// real hierarchies are small.
|
||||
if n_bases == 0 || n_bases > 64 { continue; }
|
||||
if p_array < rd_start || p_array >= rd_end { continue; }
|
||||
if n_bases == 0 || n_bases > 64 {
|
||||
continue;
|
||||
}
|
||||
if p_array < rd_start || p_array >= rd_end {
|
||||
continue;
|
||||
}
|
||||
for i in 0..n_bases {
|
||||
let Some(bcd) = read(p_array + i * 4) else { break };
|
||||
if bcd < rd_start || bcd >= rd_end { break; }
|
||||
let Some(bcd) = read(p_array + i * 4) else {
|
||||
break;
|
||||
};
|
||||
if bcd < rd_start || bcd >= rd_end {
|
||||
break;
|
||||
}
|
||||
let (Some(ptd), Some(ncb), Some(md), Some(pd), Some(vd), Some(attr)) = (
|
||||
read(bcd), read(bcd + 4), read(bcd + 8),
|
||||
read(bcd + 12), read(bcd + 16), read(bcd + 20),
|
||||
) else { break };
|
||||
let Some(td) = td_by_addr.get(&ptd) else { break };
|
||||
read(bcd),
|
||||
read(bcd + 4),
|
||||
read(bcd + 8),
|
||||
read(bcd + 12),
|
||||
read(bcd + 16),
|
||||
read(bcd + 20),
|
||||
) else {
|
||||
break;
|
||||
};
|
||||
let Some(td) = td_by_addr.get(&ptd) else {
|
||||
break;
|
||||
};
|
||||
base_classes.push(BaseClass {
|
||||
class_hierarchy: chd,
|
||||
index: i,
|
||||
@@ -280,7 +340,12 @@ pub fn analyze(pe: &[u8], image_base: u32, sections: &[PeSection]) -> RttiResult
|
||||
"RTTI walk complete",
|
||||
);
|
||||
|
||||
RttiResult { type_descriptors, locators, base_classes, vtable_to_locator }
|
||||
RttiResult {
|
||||
type_descriptors,
|
||||
locators,
|
||||
base_classes,
|
||||
vtable_to_locator,
|
||||
}
|
||||
}
|
||||
|
||||
/// Read a NUL-terminated ASCII string starting at `off` in `bytes`.
|
||||
@@ -308,14 +373,18 @@ mod tests {
|
||||
vec![
|
||||
PeSection {
|
||||
name: ".rdata".into(),
|
||||
virtual_address: RDATA_RVA, virtual_size: SEC_SIZE,
|
||||
raw_offset: RDATA_RVA, raw_size: SEC_SIZE,
|
||||
virtual_address: RDATA_RVA,
|
||||
virtual_size: SEC_SIZE,
|
||||
raw_offset: RDATA_RVA,
|
||||
raw_size: SEC_SIZE,
|
||||
flags: 0x4000_0040,
|
||||
},
|
||||
PeSection {
|
||||
name: ".data".into(),
|
||||
virtual_address: DATA_RVA, virtual_size: SEC_SIZE,
|
||||
raw_offset: DATA_RVA, raw_size: SEC_SIZE,
|
||||
virtual_address: DATA_RVA,
|
||||
virtual_size: SEC_SIZE,
|
||||
raw_offset: DATA_RVA,
|
||||
raw_size: SEC_SIZE,
|
||||
flags: 0xC000_0040,
|
||||
},
|
||||
]
|
||||
@@ -323,7 +392,9 @@ mod tests {
|
||||
|
||||
struct Image(Vec<u8>);
|
||||
impl Image {
|
||||
fn new() -> Self { Image(vec![0u8; (DATA_RVA + SEC_SIZE) as usize]) }
|
||||
fn new() -> Self {
|
||||
Image(vec![0u8; (DATA_RVA + SEC_SIZE) as usize])
|
||||
}
|
||||
fn put_u32(&mut self, va: u32, v: u32) {
|
||||
let o = (va - BASE) as usize;
|
||||
self.0[o..o + 4].copy_from_slice(&v.to_be_bytes());
|
||||
@@ -339,16 +410,23 @@ mod tests {
|
||||
/// and the `vftable[-1]` word that points at the COL.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn emit_class(
|
||||
img: &mut Image, td: u32, name: &str,
|
||||
col: u32, offset: u32, chd: u32, bcd_array: u32, bcd: u32, base_name_td: Option<u32>,
|
||||
img: &mut Image,
|
||||
td: u32,
|
||||
name: &str,
|
||||
col: u32,
|
||||
offset: u32,
|
||||
chd: u32,
|
||||
bcd_array: u32,
|
||||
bcd: u32,
|
||||
base_name_td: Option<u32>,
|
||||
vtable_minus_one: u32,
|
||||
) {
|
||||
img.put_u32(td, 0xDEAD_BEEF); // type_info vftable — value is irrelevant
|
||||
img.put_str(td + 8, name);
|
||||
|
||||
img.put_u32(col, 0); // signature
|
||||
img.put_u32(col, 0); // signature
|
||||
img.put_u32(col + 4, offset);
|
||||
img.put_u32(col + 8, 0); // cdOffset
|
||||
img.put_u32(col + 8, 0); // cdOffset
|
||||
img.put_u32(col + 12, td);
|
||||
img.put_u32(col + 16, chd);
|
||||
|
||||
@@ -362,16 +440,16 @@ mod tests {
|
||||
img.put_u32(bcd_array, bcd);
|
||||
img.put_u32(bcd, td);
|
||||
img.put_u32(bcd + 4, n_bases - 1);
|
||||
img.put_u32(bcd + 8, 0); // mdisp
|
||||
img.put_u32(bcd + 12, u32::MAX); // pdisp = -1
|
||||
img.put_u32(bcd + 16, 0); // vdisp
|
||||
img.put_u32(bcd + 20, 0x40); // attributes
|
||||
img.put_u32(bcd + 8, 0); // mdisp
|
||||
img.put_u32(bcd + 12, u32::MAX); // pdisp = -1
|
||||
img.put_u32(bcd + 16, 0); // vdisp
|
||||
img.put_u32(bcd + 20, 0x40); // attributes
|
||||
if let Some(base_td) = base_name_td {
|
||||
let bcd2 = bcd + 24;
|
||||
img.put_u32(bcd_array + 4, bcd2);
|
||||
img.put_u32(bcd2, base_td);
|
||||
img.put_u32(bcd2 + 4, 0);
|
||||
img.put_u32(bcd2 + 8, 4); // mdisp = 4
|
||||
img.put_u32(bcd2 + 8, 4); // mdisp = 4
|
||||
img.put_u32(bcd2 + 12, u32::MAX);
|
||||
img.put_u32(bcd2 + 16, 0);
|
||||
img.put_u32(bcd2 + 20, 0);
|
||||
@@ -387,18 +465,39 @@ mod tests {
|
||||
let da = BASE + DATA_RVA;
|
||||
|
||||
// Base class Foo, then Derived : Foo.
|
||||
emit_class(&mut img, da + 0x100, ".?AVFoo@ns@@",
|
||||
rd + 0x100, 0, rd + 0x200, rd + 0x280, rd + 0x300, None,
|
||||
rd + 0x000);
|
||||
emit_class(&mut img, da + 0x200, ".?AVDerived@ns@@",
|
||||
rd + 0x400, 0, rd + 0x500, rd + 0x580, rd + 0x600, Some(da + 0x100),
|
||||
rd + 0x040);
|
||||
emit_class(
|
||||
&mut img,
|
||||
da + 0x100,
|
||||
".?AVFoo@ns@@",
|
||||
rd + 0x100,
|
||||
0,
|
||||
rd + 0x200,
|
||||
rd + 0x280,
|
||||
rd + 0x300,
|
||||
None,
|
||||
rd,
|
||||
);
|
||||
emit_class(
|
||||
&mut img,
|
||||
da + 0x200,
|
||||
".?AVDerived@ns@@",
|
||||
rd + 0x400,
|
||||
0,
|
||||
rd + 0x500,
|
||||
rd + 0x580,
|
||||
rd + 0x600,
|
||||
Some(da + 0x100),
|
||||
rd + 0x040,
|
||||
);
|
||||
|
||||
let r = analyze(&img.0, BASE, §ions());
|
||||
|
||||
assert_eq!(r.type_descriptors.len(), 2);
|
||||
let derived = r.type_descriptors.iter()
|
||||
.find(|t| t.mangled_name.contains("Derived")).unwrap();
|
||||
let derived = r
|
||||
.type_descriptors
|
||||
.iter()
|
||||
.find(|t| t.mangled_name.contains("Derived"))
|
||||
.unwrap();
|
||||
assert_eq!(derived.demangled_name, "ns::Derived");
|
||||
|
||||
assert_eq!(r.locators.len(), 2);
|
||||
@@ -407,10 +506,15 @@ mod tests {
|
||||
assert!(r.vtable_anchors().contains(&(rd + 0x004)));
|
||||
|
||||
let names = r.vtable_class_names();
|
||||
assert_eq!(names.get(&(rd + 0x044)), Some(&("ns::Derived".to_string(), 0)));
|
||||
assert_eq!(
|
||||
names.get(&(rd + 0x044)),
|
||||
Some(&("ns::Derived".to_string(), 0))
|
||||
);
|
||||
|
||||
// Derived's hierarchy lists itself at index 0 and Foo at index 1.
|
||||
let mut bases: Vec<_> = r.base_classes.iter()
|
||||
let mut bases: Vec<_> = r
|
||||
.base_classes
|
||||
.iter()
|
||||
.filter(|b| b.class_hierarchy == rd + 0x500)
|
||||
.collect();
|
||||
bases.sort_by_key(|b| b.index);
|
||||
@@ -425,9 +529,18 @@ mod tests {
|
||||
let mut img = Image::new();
|
||||
let rd = BASE + RDATA_RVA;
|
||||
let da = BASE + DATA_RVA;
|
||||
emit_class(&mut img, da + 0x100, ".?AVMulti@@",
|
||||
rd + 0x100, 0x8, rd + 0x200, rd + 0x280, rd + 0x300, None,
|
||||
rd + 0x000);
|
||||
emit_class(
|
||||
&mut img,
|
||||
da + 0x100,
|
||||
".?AVMulti@@",
|
||||
rd + 0x100,
|
||||
0x8,
|
||||
rd + 0x200,
|
||||
rd + 0x280,
|
||||
rd + 0x300,
|
||||
None,
|
||||
rd,
|
||||
);
|
||||
|
||||
let r = analyze(&img.0, BASE, §ions());
|
||||
let names = r.vtable_class_names();
|
||||
|
||||
@@ -28,7 +28,8 @@ pub fn write_instr_line<W: Write + ?Sized>(
|
||||
// A word the analysis proved is data (a recovered jump table or its index
|
||||
// map) must not be printed as if it decoded to something meaningful.
|
||||
if item.is_data {
|
||||
let lbl = labels.get(&item.item.raw)
|
||||
let lbl = labels
|
||||
.get(&item.item.raw)
|
||||
.map(|s| format!(" ; -> {s}"))
|
||||
.unwrap_or_default();
|
||||
return writeln!(
|
||||
@@ -52,12 +53,13 @@ pub fn write_instr_line<W: Write + ?Sized>(
|
||||
|
||||
if let Some((data_addr, kind)) = data_annotation {
|
||||
let tag = match kind {
|
||||
XrefKind::DataRead => "[R]",
|
||||
XrefKind::DataRead => "[R]",
|
||||
XrefKind::DataWrite => "[W]",
|
||||
_ => "[&]",
|
||||
_ => "[&]",
|
||||
};
|
||||
let sec = section_for_addr(data_addr, sections, image_base).unwrap_or("?");
|
||||
let data_lbl = labels.get(&data_addr)
|
||||
let data_lbl = labels
|
||||
.get(&data_addr)
|
||||
.map(|s| format!(" = {s}"))
|
||||
.unwrap_or_default();
|
||||
if !annotated.contains("; ->") {
|
||||
@@ -67,5 +69,9 @@ pub fn write_instr_line<W: Write + ?Sized>(
|
||||
}
|
||||
}
|
||||
|
||||
writeln!(out, " {:08X}: {:08X} {}", item.item.addr, item.item.raw, annotated)
|
||||
writeln!(
|
||||
out,
|
||||
" {:08X}: {:08X} {}",
|
||||
item.item.addr, item.item.raw, annotated
|
||||
)
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
//! kind-classification CASE drifted out of agreement with `xref.rs`, and
|
||||
//! is worth a one-line warning at log time.
|
||||
|
||||
|
||||
/// Every XDBF string side-by-side across the languages the title ships, so a
|
||||
/// piece of UI text can be looked up once and read in all locales.
|
||||
const V_XDBF_TEXT: &str = "
|
||||
@@ -66,7 +65,10 @@ pub const ALL_VIEWS: &[(&str, &str)] = &[
|
||||
("v_branch_xrefs", V_BRANCH_XREFS),
|
||||
("v_call_graph", V_CALL_GRAPH),
|
||||
("v_reachability_from_entry", V_REACHABILITY_FROM_ENTRY),
|
||||
("v_indirect_reachability_from_entry", V_INDIRECT_REACHABILITY_FROM_ENTRY),
|
||||
(
|
||||
"v_indirect_reachability_from_entry",
|
||||
V_INDIRECT_REACHABILITY_FROM_ENTRY,
|
||||
),
|
||||
("v_function_first_instruction", V_FUNCTION_FIRST_INSTRUCTION),
|
||||
("v_imports_called", V_IMPORTS_CALLED),
|
||||
("v_xdbf_text", V_XDBF_TEXT),
|
||||
|
||||
@@ -69,10 +69,10 @@ pub struct StaticInitResult {
|
||||
pub arrays: Vec<FuncPtrArray>,
|
||||
}
|
||||
|
||||
const OP_ADDI: u32 = 14;
|
||||
const OP_ADDI: u32 = 14;
|
||||
const OP_ADDIS: u32 = 15;
|
||||
const OP_BCCTR: u32 = 19;
|
||||
const OP_LWZ: u32 = 32;
|
||||
const OP_LWZ: u32 = 32;
|
||||
const OP_X_FORM: u32 = 31;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
@@ -95,10 +95,12 @@ pub fn analyze(
|
||||
let mut drivers: Vec<StaticInitDriver> = Vec::new();
|
||||
|
||||
for (&fn_start, fi) in &func_analysis.functions {
|
||||
if fi.is_saverestore { continue; }
|
||||
if let Some(d) = scan_function_for_driver(
|
||||
pe, image_base, fn_start, fi.end, &block_boundaries,
|
||||
) {
|
||||
if fi.is_saverestore {
|
||||
continue;
|
||||
}
|
||||
if let Some(d) =
|
||||
scan_function_for_driver(pe, image_base, fn_start, fi.end, &block_boundaries)
|
||||
{
|
||||
drivers.push(d);
|
||||
}
|
||||
}
|
||||
@@ -106,7 +108,14 @@ pub fn analyze(
|
||||
// Build arrays from the discovered drivers + section data.
|
||||
let mut arrays: Vec<FuncPtrArray> = Vec::new();
|
||||
for d in &drivers {
|
||||
if let Some(entries) = read_array(pe, image_base, sections, d.array_start, d.array_end, function_starts) {
|
||||
if let Some(entries) = read_array(
|
||||
pe,
|
||||
image_base,
|
||||
sections,
|
||||
d.array_start,
|
||||
d.array_end,
|
||||
function_starts,
|
||||
) {
|
||||
arrays.push(FuncPtrArray {
|
||||
address: d.array_start,
|
||||
length: entries.len() as u32,
|
||||
@@ -140,7 +149,9 @@ fn read_array(
|
||||
end: u32,
|
||||
function_starts: &BTreeSet<u32>,
|
||||
) -> Option<Vec<u32>> {
|
||||
if end <= start || (end - start) > 4096 { return None; }
|
||||
if end <= start || (end - start) > 4096 {
|
||||
return None;
|
||||
}
|
||||
let _section = sections.iter().find(|s| {
|
||||
let lo = image_base + s.virtual_address;
|
||||
let hi = lo + s.virtual_size;
|
||||
@@ -150,15 +161,21 @@ fn read_array(
|
||||
let mut p = start;
|
||||
while p < end {
|
||||
let off = p.wrapping_sub(image_base) as usize;
|
||||
if off + 4 > pe.len() { return None; }
|
||||
if off + 4 > pe.len() {
|
||||
return None;
|
||||
}
|
||||
let v = u32::from_be_bytes([pe[off], pe[off + 1], pe[off + 2], pe[off + 3]]);
|
||||
if v != 0 {
|
||||
if !function_starts.contains(&v) { return None; }
|
||||
if !function_starts.contains(&v) {
|
||||
return None;
|
||||
}
|
||||
entries.push(v);
|
||||
}
|
||||
p = p.wrapping_add(4);
|
||||
}
|
||||
if entries.is_empty() { return None; }
|
||||
if entries.is_empty() {
|
||||
return None;
|
||||
}
|
||||
Some(entries)
|
||||
}
|
||||
|
||||
@@ -194,7 +211,9 @@ fn scan_function_for_driver(
|
||||
reg = [None; 32];
|
||||
}
|
||||
let off = pc.wrapping_sub(image_base) as usize;
|
||||
if off + 4 > pe.len() { break; }
|
||||
if off + 4 > pe.len() {
|
||||
break;
|
||||
}
|
||||
let instr = u32::from_be_bytes([pe[off], pe[off + 1], pe[off + 2], pe[off + 3]]);
|
||||
let op = instr >> 26;
|
||||
let rd = ((instr >> 21) & 0x1F) as usize;
|
||||
@@ -207,7 +226,9 @@ fn scan_function_for_driver(
|
||||
OP_ADDIS => {
|
||||
if let Some(RegVal::Const(b)) = reg[ra] {
|
||||
reg[rd] = Some(RegVal::Const(b.wrapping_add(uimm << 16)));
|
||||
} else { reg[rd] = None; }
|
||||
} else {
|
||||
reg[rd] = None;
|
||||
}
|
||||
}
|
||||
OP_ADDI if ra != 0 => {
|
||||
let prev = reg[ra];
|
||||
@@ -229,7 +250,9 @@ fn scan_function_for_driver(
|
||||
end_init = Some(v);
|
||||
end_reg = Some(rd);
|
||||
}
|
||||
} else { reg[rd] = None; }
|
||||
} else {
|
||||
reg[rd] = None;
|
||||
}
|
||||
}
|
||||
OP_LWZ => {
|
||||
if ra != 0 && Some(ra) == cursor_reg {
|
||||
@@ -241,9 +264,13 @@ fn scan_function_for_driver(
|
||||
let xo = (instr >> 1) & 0x3FF;
|
||||
if xo == 467 {
|
||||
let spr = (((instr >> 11) & 0x1F) << 5) | ((instr >> 16) & 0x1F);
|
||||
if spr == 9 && saw_lwz_through_cursor { saw_mtctr = true; }
|
||||
if spr == 9 && saw_lwz_through_cursor {
|
||||
saw_mtctr = true;
|
||||
}
|
||||
}
|
||||
if xo != 444 && xo != 467 {
|
||||
reg[rd] = None;
|
||||
}
|
||||
if xo != 444 && xo != 467 { reg[rd] = None; }
|
||||
}
|
||||
OP_BCCTR => {
|
||||
let xo = (instr >> 1) & 0x3FF;
|
||||
@@ -254,12 +281,14 @@ fn scan_function_for_driver(
|
||||
}
|
||||
18 => {
|
||||
if (instr & 1) != 0 {
|
||||
for r in 0..=12 { reg[r] = None; }
|
||||
for r in 0..=12 {
|
||||
reg[r] = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
16 => {
|
||||
if (instr & 1) != 0 {
|
||||
for r in 0..=12 { reg[r] = None; }
|
||||
16 if (instr & 1) != 0 => {
|
||||
for r in 0..=12 {
|
||||
reg[r] = None;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
@@ -273,8 +302,12 @@ fn scan_function_for_driver(
|
||||
}
|
||||
let cursor_init = cursor_init?;
|
||||
let end_init = end_init?;
|
||||
if end_init <= cursor_init { return None; }
|
||||
if end_init - cursor_init > 4096 { return None; }
|
||||
if end_init <= cursor_init {
|
||||
return None;
|
||||
}
|
||||
if end_init - cursor_init > 4096 {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(StaticInitDriver {
|
||||
driver_function: fn_start,
|
||||
@@ -294,8 +327,10 @@ mod tests {
|
||||
fn mk_section(name: &str, va: u32, size: u32) -> PeSection {
|
||||
PeSection {
|
||||
name: name.into(),
|
||||
virtual_address: va, virtual_size: size,
|
||||
raw_offset: va, raw_size: size,
|
||||
virtual_address: va,
|
||||
virtual_size: size,
|
||||
raw_offset: va,
|
||||
raw_size: size,
|
||||
flags: 0x4000_0040,
|
||||
}
|
||||
}
|
||||
@@ -311,7 +346,11 @@ mod tests {
|
||||
|
||||
// Array at .rdata + 0x800: 3 function pointers.
|
||||
let arr_va_lo = 0x800u32;
|
||||
let fns = [image_base + 0x2000, image_base + 0x2010, image_base + 0x2020];
|
||||
let fns = [
|
||||
image_base + 0x2000,
|
||||
image_base + 0x2010,
|
||||
image_base + 0x2020,
|
||||
];
|
||||
for (i, p) in fns.iter().enumerate() {
|
||||
write_be(&mut pe, arr_va_lo as usize + i * 4, *p);
|
||||
}
|
||||
@@ -330,32 +369,52 @@ mod tests {
|
||||
// blr
|
||||
let driver = 0x82001000u32;
|
||||
let off = (driver - image_base) as usize;
|
||||
let lis_r3 = (15u32 << 26) | (3 << 21) | ((array_start >> 16) as u32);
|
||||
let lis_r3 = (15u32 << 26) | (3 << 21) | (array_start >> 16);
|
||||
let addi_r3 = (14u32 << 26) | (3 << 21) | (3 << 16) | ((array_start as u16) as u32);
|
||||
let lis_r4 = (15u32 << 26) | (4 << 21) | ((array_end >> 16) as u32);
|
||||
let lis_r4 = (15u32 << 26) | (4 << 21) | (array_end >> 16);
|
||||
let addi_r4 = (14u32 << 26) | (4 << 21) | (4 << 16) | ((array_end as u16) as u32);
|
||||
let lwz = (32u32 << 26) | (5 << 21) | (3 << 16);
|
||||
let mtctr = (31u32 << 26) | (5 << 21) | (9 << 16) | (467 << 1);
|
||||
let bcctrl = (19u32 << 26) | (20 << 21) | (528 << 1) | 1;
|
||||
let addi_inc = (14u32 << 26) | (3 << 21) | (3 << 16) | 4;
|
||||
let blr = (19u32 << 26) | (20 << 21) | (16 << 1);
|
||||
for (i, w) in [lis_r3, addi_r3, lis_r4, addi_r4, lwz, mtctr, bcctrl, addi_inc, blr].iter().enumerate() {
|
||||
for (i, w) in [
|
||||
lis_r3, addi_r3, lis_r4, addi_r4, lwz, mtctr, bcctrl, addi_inc, blr,
|
||||
]
|
||||
.iter()
|
||||
.enumerate()
|
||||
{
|
||||
write_be(&mut pe, off + i * 4, *w);
|
||||
}
|
||||
|
||||
let mut functions: BTreeMap<u32, FuncInfo> = BTreeMap::new();
|
||||
functions.insert(driver, FuncInfo {
|
||||
start: driver, end: driver + 0x40, frame_size: 0, saved_gprs: 0,
|
||||
is_leaf: false, is_saverestore: false,
|
||||
pdata_validated: false, pdata_length: None, pdata_prolog_length: None, has_eh: false,
|
||||
});
|
||||
functions.insert(
|
||||
driver,
|
||||
FuncInfo {
|
||||
start: driver,
|
||||
end: driver + 0x40,
|
||||
frame_size: 0,
|
||||
saved_gprs: 0,
|
||||
is_leaf: false,
|
||||
is_saverestore: false,
|
||||
pdata_validated: false,
|
||||
pdata_length: None,
|
||||
pdata_prolog_length: None,
|
||||
has_eh: false,
|
||||
},
|
||||
);
|
||||
let fa = FuncAnalysis {
|
||||
functions, save_gpr_base: None, restore_gpr_base: None, pdata_entries: Vec::new(),
|
||||
functions,
|
||||
save_gpr_base: None,
|
||||
restore_gpr_base: None,
|
||||
pdata_entries: Vec::new(),
|
||||
};
|
||||
|
||||
let sections = vec![mk_section(".rdata", 0x800, 0x100)];
|
||||
let mut starts = BTreeSet::new();
|
||||
for &p in &fns { starts.insert(p); }
|
||||
for &p in &fns {
|
||||
starts.insert(p);
|
||||
}
|
||||
let labels: HashMap<u32, String> = HashMap::new();
|
||||
|
||||
let r = analyze(&pe, image_base, §ions, &fa, &starts, &labels);
|
||||
@@ -382,13 +441,26 @@ mod tests {
|
||||
write_be(&mut pe, (driver - image_base) as usize, blr);
|
||||
|
||||
let mut functions: BTreeMap<u32, FuncInfo> = BTreeMap::new();
|
||||
functions.insert(driver, FuncInfo {
|
||||
start: driver, end: driver + 0x40, frame_size: 0, saved_gprs: 0,
|
||||
is_leaf: true, is_saverestore: false,
|
||||
pdata_validated: false, pdata_length: None, pdata_prolog_length: None, has_eh: false,
|
||||
});
|
||||
functions.insert(
|
||||
driver,
|
||||
FuncInfo {
|
||||
start: driver,
|
||||
end: driver + 0x40,
|
||||
frame_size: 0,
|
||||
saved_gprs: 0,
|
||||
is_leaf: true,
|
||||
is_saverestore: false,
|
||||
pdata_validated: false,
|
||||
pdata_length: None,
|
||||
pdata_prolog_length: None,
|
||||
has_eh: false,
|
||||
},
|
||||
);
|
||||
let fa = FuncAnalysis {
|
||||
functions, save_gpr_base: None, restore_gpr_base: None, pdata_entries: Vec::new(),
|
||||
functions,
|
||||
save_gpr_base: None,
|
||||
restore_gpr_base: None,
|
||||
pdata_entries: Vec::new(),
|
||||
};
|
||||
let sections = vec![mk_section(".rdata", 0x800, 0x100)];
|
||||
let starts: BTreeSet<u32> = BTreeSet::new();
|
||||
|
||||
@@ -52,12 +52,16 @@ pub fn analyze(pe: &[u8], image_base: u32, sections: &[PeSection]) -> Vec<Detect
|
||||
let mut out: Vec<DetectedString> = Vec::new();
|
||||
|
||||
for section in sections {
|
||||
if !matches!(section.name.as_str(), ".rdata" | ".data") { continue; }
|
||||
if !matches!(section.name.as_str(), ".rdata" | ".data") {
|
||||
continue;
|
||||
}
|
||||
let raw_start = section.virtual_address as usize;
|
||||
// Clamp to the file-backed extent — everything past `raw_size` is BSS.
|
||||
let backed = section.virtual_size.min(section.raw_size) as usize;
|
||||
let raw_end = (raw_start + backed).min(pe.len());
|
||||
if raw_start >= raw_end { continue; }
|
||||
if raw_start >= raw_end {
|
||||
continue;
|
||||
}
|
||||
let bytes = &pe[raw_start..raw_end];
|
||||
let va_base = image_base + section.virtual_address;
|
||||
|
||||
@@ -74,8 +78,8 @@ pub fn analyze(pe: &[u8], image_base: u32, sections: &[PeSection]) -> Vec<Detect
|
||||
let elapsed_ms = started.elapsed().as_millis() as f64;
|
||||
let n_ascii = out.iter().filter(|s| s.encoding == "ascii").count();
|
||||
let n_utf16 = out.iter().filter(|s| s.encoding == "utf16le").count();
|
||||
let n_sjis = out.iter().filter(|s| s.encoding == "shift_jis").count();
|
||||
let n_utf8 = out.iter().filter(|s| s.encoding == "utf8").count();
|
||||
let n_sjis = out.iter().filter(|s| s.encoding == "shift_jis").count();
|
||||
let n_utf8 = out.iter().filter(|s| s.encoding == "utf8").count();
|
||||
metrics::histogram!("analysis.phase_ms", "phase" => "strings").record(elapsed_ms);
|
||||
tracing::info!(
|
||||
ascii = n_ascii,
|
||||
@@ -104,7 +108,9 @@ fn scan_ascii(bytes: &[u8], va_base: u32, out: &mut Vec<DetectedString>) {
|
||||
continue;
|
||||
}
|
||||
let start = i;
|
||||
while i < bytes.len() && is_printable_ascii(bytes[i]) { i += 1; }
|
||||
while i < bytes.len() && is_printable_ascii(bytes[i]) {
|
||||
i += 1;
|
||||
}
|
||||
let run_len = i - start;
|
||||
// Require NUL termination and minimum length.
|
||||
if run_len >= MIN_LEN && i < bytes.len() && bytes[i] == 0 {
|
||||
@@ -118,7 +124,9 @@ fn scan_ascii(bytes: &[u8], va_base: u32, out: &mut Vec<DetectedString>) {
|
||||
});
|
||||
}
|
||||
// Skip the NUL (if any) before continuing.
|
||||
if i < bytes.len() && bytes[i] == 0 { i += 1; }
|
||||
if i < bytes.len() && bytes[i] == 0 {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,7 +135,10 @@ fn scan_utf16le(bytes: &[u8], va_base: u32, out: &mut Vec<DetectedString>) {
|
||||
// offsets to avoid misaligned hits.
|
||||
let mut i = 0;
|
||||
while i + 2 <= bytes.len() {
|
||||
if !i.is_multiple_of(2) { i += 1; continue; }
|
||||
if !i.is_multiple_of(2) {
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
let lo = bytes[i];
|
||||
let hi = bytes[i + 1];
|
||||
// Restrict scan-start to printable ASCII range with a zero high byte —
|
||||
@@ -141,7 +152,9 @@ fn scan_utf16le(bytes: &[u8], va_base: u32, out: &mut Vec<DetectedString>) {
|
||||
while i + 2 <= bytes.len() {
|
||||
let l = bytes[i];
|
||||
let h = bytes[i + 1];
|
||||
if h != 0 || !is_printable_ascii(l) { break; }
|
||||
if h != 0 || !is_printable_ascii(l) {
|
||||
break;
|
||||
}
|
||||
codeunits.push((h as u16) << 8 | l as u16);
|
||||
i += 2;
|
||||
}
|
||||
@@ -158,7 +171,9 @@ fn scan_utf16le(bytes: &[u8], va_base: u32, out: &mut Vec<DetectedString>) {
|
||||
});
|
||||
}
|
||||
// Skip past the terminator.
|
||||
if nul_terminated { i += 2; }
|
||||
if nul_terminated {
|
||||
i += 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,16 +198,16 @@ fn is_sjis_trail(b: u8) -> bool {
|
||||
/// ASCII.
|
||||
fn is_text_like(ch: char) -> bool {
|
||||
let o = ch as u32;
|
||||
matches!(o, 0x20..=0x7E)
|
||||
|| matches!(ch, '\t' | '\n' | '\r')
|
||||
|| is_wide(ch)
|
||||
matches!(o, 0x20..=0x7E) || matches!(ch, '\t' | '\n' | '\r') || is_wide(ch)
|
||||
}
|
||||
|
||||
/// A full-width character — kana, CJK punctuation, ideograph, or full-width
|
||||
/// ASCII. Used to tell "real text" from a lucky byte pair.
|
||||
fn is_wide(ch: char) -> bool {
|
||||
let o = ch as u32;
|
||||
(0x3000..=0x30FF).contains(&o) || (0x4E00..=0x9FFF).contains(&o) || (0xFF01..=0xFF5E).contains(&o)
|
||||
(0x3000..=0x30FF).contains(&o)
|
||||
|| (0x4E00..=0x9FFF).contains(&o)
|
||||
|| (0xFF01..=0xFF5E).contains(&o)
|
||||
}
|
||||
|
||||
/// True when `t` contains a lone ASCII character with a full-width character
|
||||
@@ -207,9 +222,8 @@ fn is_wide(ch: char) -> bool {
|
||||
/// character wedged between two wide ones.
|
||||
fn has_isolated_ascii(t: &str) -> bool {
|
||||
let chars: Vec<char> = t.chars().collect();
|
||||
(1..chars.len().saturating_sub(1)).any(|k| {
|
||||
!is_wide(chars[k]) && is_wide(chars[k - 1]) && is_wide(chars[k + 1])
|
||||
})
|
||||
(1..chars.len().saturating_sub(1))
|
||||
.any(|k| !is_wide(chars[k]) && is_wide(chars[k - 1]) && is_wide(chars[k + 1]))
|
||||
}
|
||||
|
||||
/// Decode `raw` as Shift_JIS, rejecting anything that is not convincingly
|
||||
@@ -224,7 +238,8 @@ fn decode_sjis(raw: &[u8]) -> Option<String> {
|
||||
// obscure kanji, but hiragana/katakana (U+3040..U+30FF) essentially never
|
||||
// appear by accident and are ubiquitous in genuine Japanese.
|
||||
let has_kana = t.chars().any(|c| ('\u{3040}'..='\u{30FF}').contains(&c));
|
||||
if t.chars().count() >= 4 && has_kana && t.chars().all(is_text_like) && !has_isolated_ascii(&t) {
|
||||
if t.chars().count() >= 4 && has_kana && t.chars().all(is_text_like) && !has_isolated_ascii(&t)
|
||||
{
|
||||
Some(t)
|
||||
} else {
|
||||
None
|
||||
@@ -274,7 +289,9 @@ fn scan_shift_jis(bytes: &[u8], va_base: u32, out: &mut Vec<DetectedString>) {
|
||||
i = end + 1; // skip NUL
|
||||
} else {
|
||||
i = start + 1;
|
||||
if i < bytes.len() && bytes[i] == 0 { i += 1; }
|
||||
if i < bytes.len() && bytes[i] == 0 {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -291,12 +308,16 @@ fn scan_utf8(bytes: &[u8], va_base: u32, out: &mut Vec<DetectedString>) {
|
||||
while i < bytes.len() {
|
||||
let b = bytes[i];
|
||||
if b < 0x80 {
|
||||
if !is_printable_ascii(b) { break; }
|
||||
if !is_printable_ascii(b) {
|
||||
break;
|
||||
}
|
||||
nbytes += 1;
|
||||
i += 1;
|
||||
} else if (b & 0xE0) == 0xC0 {
|
||||
// 2-byte: 110xxxxx 10xxxxxx
|
||||
if i + 1 >= bytes.len() || (bytes[i + 1] & 0xC0) != 0x80 { break; }
|
||||
if i + 1 >= bytes.len() || (bytes[i + 1] & 0xC0) != 0x80 {
|
||||
break;
|
||||
}
|
||||
has_multibyte = true;
|
||||
nbytes += 2;
|
||||
i += 2;
|
||||
@@ -304,7 +325,10 @@ fn scan_utf8(bytes: &[u8], va_base: u32, out: &mut Vec<DetectedString>) {
|
||||
// 3-byte: 1110xxxx 10xxxxxx 10xxxxxx
|
||||
if i + 2 >= bytes.len()
|
||||
|| (bytes[i + 1] & 0xC0) != 0x80
|
||||
|| (bytes[i + 2] & 0xC0) != 0x80 { break; }
|
||||
|| (bytes[i + 2] & 0xC0) != 0x80
|
||||
{
|
||||
break;
|
||||
}
|
||||
has_multibyte = true;
|
||||
nbytes += 3;
|
||||
i += 3;
|
||||
@@ -314,7 +338,8 @@ fn scan_utf8(bytes: &[u8], va_base: u32, out: &mut Vec<DetectedString>) {
|
||||
}
|
||||
if has_multibyte
|
||||
&& nbytes >= MIN_LEN
|
||||
&& i < bytes.len() && bytes[i] == 0
|
||||
&& i < bytes.len()
|
||||
&& bytes[i] == 0
|
||||
&& let Ok(s) = std::str::from_utf8(&bytes[start..i])
|
||||
{
|
||||
out.push(DetectedString {
|
||||
@@ -327,7 +352,9 @@ fn scan_utf8(bytes: &[u8], va_base: u32, out: &mut Vec<DetectedString>) {
|
||||
i += 1; // skip NUL
|
||||
} else {
|
||||
i = start + 1;
|
||||
if i < bytes.len() && bytes[i] == 0 { i += 1; }
|
||||
if i < bytes.len() && bytes[i] == 0 {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -402,7 +429,10 @@ mod tests {
|
||||
pe[off..off + s.len()].copy_from_slice(s);
|
||||
let sections = vec![mk_section(".rdata", 0x1000, 0x100)];
|
||||
let strings = analyze(&pe, image_base, §ions);
|
||||
let sjis: Vec<_> = strings.iter().filter(|s| s.encoding == "shift_jis").collect();
|
||||
let sjis: Vec<_> = strings
|
||||
.iter()
|
||||
.filter(|s| s.encoding == "shift_jis")
|
||||
.collect();
|
||||
assert_eq!(sjis.len(), 1);
|
||||
// Decoded to real UTF-8, not rendered as escaped bytes.
|
||||
assert_eq!(sjis[0].content, "ABCあい");
|
||||
@@ -420,8 +450,10 @@ mod tests {
|
||||
pe[off..off + s.len()].copy_from_slice(s);
|
||||
let sections = vec![mk_section(".rdata", 0x1000, 0x100)];
|
||||
let strings = analyze(&pe, image_base, §ions);
|
||||
assert!(strings.iter().all(|s| s.encoding != "shift_jis"),
|
||||
"float table must not be reported as Japanese text");
|
||||
assert!(
|
||||
strings.iter().all(|s| s.encoding != "shift_jis"),
|
||||
"float table must not be reported as Japanese text"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -440,7 +472,10 @@ mod tests {
|
||||
pe[off..off + s.len()].copy_from_slice(s);
|
||||
let sections = vec![mk_section(".rdata", 0x1000, 0x100)];
|
||||
let strings = analyze(&pe, image_base, §ions);
|
||||
let sjis: Vec<_> = strings.iter().filter(|s| s.encoding == "shift_jis").collect();
|
||||
let sjis: Vec<_> = strings
|
||||
.iter()
|
||||
.filter(|s| s.encoding == "shift_jis")
|
||||
.collect();
|
||||
assert_eq!(sjis.len(), 1);
|
||||
assert_eq!(sjis[0].content, "システム");
|
||||
// Reported at the true start, one byte past the run's beginning.
|
||||
@@ -471,7 +506,9 @@ mod tests {
|
||||
let s = b"abcdefghij";
|
||||
pe[off..off + s.len()].copy_from_slice(s);
|
||||
// Fill rest of section with 0xFF so the run terminates cleanly without NUL.
|
||||
for j in off + s.len()..off + 0x100 { pe[j] = 0xFF; }
|
||||
for j in off + s.len()..off + 0x100 {
|
||||
pe[j] = 0xFF;
|
||||
}
|
||||
let sections = vec![mk_section(".rdata", 0x1000, 0x100)];
|
||||
let strings = analyze(&pe, image_base, §ions);
|
||||
assert_eq!(strings.len(), 0);
|
||||
|
||||
@@ -65,7 +65,13 @@ pub fn analyze(
|
||||
sections: &[PeSection],
|
||||
function_starts: &std::collections::BTreeSet<u32>,
|
||||
) -> Vec<Vtable> {
|
||||
analyze_with_anchors(pe, image_base, sections, function_starts, &std::collections::BTreeSet::new())
|
||||
analyze_with_anchors(
|
||||
pe,
|
||||
image_base,
|
||||
sections,
|
||||
function_starts,
|
||||
&std::collections::BTreeSet::new(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Like [`analyze`], but additionally recovers vtables whose base address is
|
||||
@@ -106,7 +112,12 @@ pub fn analyze_with_anchors(
|
||||
let rdata_ranges: Vec<(u32, u32)> = sections
|
||||
.iter()
|
||||
.filter(|s| s.name == ".rdata")
|
||||
.map(|s| (image_base + s.virtual_address, image_base + s.virtual_address + s.virtual_size))
|
||||
.map(|s| {
|
||||
(
|
||||
image_base + s.virtual_address,
|
||||
image_base + s.virtual_address + s.virtual_size,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
// TypeDescriptors are *written at startup* (their first word is
|
||||
// `type_info`'s vftable), so MSVC emits them into writable `.data`, not
|
||||
@@ -115,7 +126,12 @@ pub fn analyze_with_anchors(
|
||||
let typedesc_ranges: Vec<(u32, u32)> = sections
|
||||
.iter()
|
||||
.filter(|s| matches!(s.name.as_str(), ".rdata" | ".data"))
|
||||
.map(|s| (image_base + s.virtual_address, image_base + s.virtual_address + s.virtual_size))
|
||||
.map(|s| {
|
||||
(
|
||||
image_base + s.virtual_address,
|
||||
image_base + s.virtual_address + s.virtual_size,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut candidates: Vec<Vtable> = Vec::new();
|
||||
@@ -125,13 +141,18 @@ pub fn analyze_with_anchors(
|
||||
let va_end = va_start + section.virtual_size;
|
||||
let raw_start = section.virtual_address as usize;
|
||||
let raw_end = (section.virtual_address + section.virtual_size) as usize;
|
||||
if raw_end > pe.len() { continue; }
|
||||
if raw_end > pe.len() {
|
||||
continue;
|
||||
}
|
||||
let bytes = &pe[raw_start..raw_end.min(pe.len())];
|
||||
|
||||
let mut i = 0usize;
|
||||
while i + 12 <= bytes.len() {
|
||||
// Try to start a run at this 4-aligned offset.
|
||||
if !i.is_multiple_of(4) { i += 1; continue; }
|
||||
if !i.is_multiple_of(4) {
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
let mut run_len = 0usize;
|
||||
let mut methods: Vec<u32> = Vec::new();
|
||||
let mut j = i;
|
||||
@@ -203,12 +224,19 @@ pub fn analyze_with_anchors(
|
||||
let mut recovered = 0usize;
|
||||
let mut newly: Vec<Vtable> = Vec::new();
|
||||
for &anchor in anchors {
|
||||
if is_covered(anchor, &covered) { continue; }
|
||||
if is_covered(anchor, &covered) {
|
||||
continue;
|
||||
}
|
||||
// Locate the containing .rdata/.data section.
|
||||
let Some(&(va_lo, va_hi, raw_lo, raw_hi)) =
|
||||
scan_targets_va.iter().find(|&&(lo, hi, _, _)| anchor >= lo && anchor < hi)
|
||||
else { continue };
|
||||
if anchor % 4 != 0 { continue; }
|
||||
let Some(&(va_lo, va_hi, raw_lo, raw_hi)) = scan_targets_va
|
||||
.iter()
|
||||
.find(|&&(lo, hi, _, _)| anchor >= lo && anchor < hi)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
if anchor % 4 != 0 {
|
||||
continue;
|
||||
}
|
||||
let raw_hi = raw_hi.min(pe.len());
|
||||
// Read the fnptr-array run starting at the anchor. Tolerate small
|
||||
// gaps of non-function slots (null / pure-virtual / unrecognised),
|
||||
@@ -221,7 +249,11 @@ pub fn analyze_with_anchors(
|
||||
let mut off = (anchor - va_lo) as usize + raw_lo;
|
||||
let mut va = anchor;
|
||||
while off + 4 <= raw_hi && va < va_hi {
|
||||
if let Some(nb) = next_base && va >= nb { break; }
|
||||
if let Some(nb) = next_base
|
||||
&& va >= nb
|
||||
{
|
||||
break;
|
||||
}
|
||||
let val = u32::from_be_bytes([pe[off], pe[off + 1], pe[off + 2], pe[off + 3]]);
|
||||
if function_starts.contains(&val) {
|
||||
methods.push(val);
|
||||
@@ -244,10 +276,15 @@ pub fn analyze_with_anchors(
|
||||
}
|
||||
// Trim any trailing non-function slots (the table ends at its last
|
||||
// real method).
|
||||
while methods.last().is_some_and(|&m| !function_starts.contains(&m)) {
|
||||
while methods
|
||||
.last()
|
||||
.is_some_and(|&m| !function_starts.contains(&m))
|
||||
{
|
||||
methods.pop();
|
||||
}
|
||||
if real_fns == 0 || methods.is_empty() { continue; }
|
||||
if real_fns == 0 || methods.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let length = methods.len() as u32;
|
||||
newly.push(Vtable {
|
||||
address: anchor,
|
||||
@@ -266,8 +303,10 @@ pub fn analyze_with_anchors(
|
||||
// contiguity-scan artifact of the same table. Keep fragments that
|
||||
// only partially overlap (defensive; shouldn't happen for true
|
||||
// sub-runs) so we never lose method coverage.
|
||||
let recovered_spans: Vec<(u32, u32)> =
|
||||
newly.iter().map(|v| (v.address, v.address + v.length * 4)).collect();
|
||||
let recovered_spans: Vec<(u32, u32)> = newly
|
||||
.iter()
|
||||
.map(|v| (v.address, v.address + v.length * 4))
|
||||
.collect();
|
||||
candidates.retain(|v| {
|
||||
!recovered_spans
|
||||
.iter()
|
||||
@@ -281,22 +320,37 @@ pub fn analyze_with_anchors(
|
||||
// RTTI walk: for each candidate, look at vtable[-1].
|
||||
let pe_image_base = image_base;
|
||||
for v in &mut candidates {
|
||||
if v.address < 4 { continue; }
|
||||
if v.address < 4 {
|
||||
continue;
|
||||
}
|
||||
let col_off = (v.address - pe_image_base - 4) as usize;
|
||||
if col_off + 4 > pe.len() { continue; }
|
||||
let col_ptr = u32::from_be_bytes([pe[col_off], pe[col_off + 1], pe[col_off + 2], pe[col_off + 3]]);
|
||||
if col_ptr == 0 { continue; }
|
||||
if !is_in_ranges(col_ptr, &rdata_ranges) { continue; }
|
||||
if col_off + 4 > pe.len() {
|
||||
continue;
|
||||
}
|
||||
let col_ptr = u32::from_be_bytes([
|
||||
pe[col_off],
|
||||
pe[col_off + 1],
|
||||
pe[col_off + 2],
|
||||
pe[col_off + 3],
|
||||
]);
|
||||
if col_ptr == 0 {
|
||||
continue;
|
||||
}
|
||||
if !is_in_ranges(col_ptr, &rdata_ranges) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Try to extract the TypeDescriptor mangled-name string.
|
||||
if let Some((td_ptr, hierarchy_ptr)) = read_col(pe, image_base, col_ptr)
|
||||
&& let Some(mangled) = read_typedescriptor_name(pe, image_base, td_ptr, &typedesc_ranges)
|
||||
&& let Some(mangled) =
|
||||
read_typedescriptor_name(pe, image_base, td_ptr, &typedesc_ranges)
|
||||
&& let Some(class) = demangle_rtti_typename(&mangled)
|
||||
{
|
||||
v.col_address = Some(col_ptr);
|
||||
v.class_name = class;
|
||||
v.rtti_present = true;
|
||||
v.base_classes_json = read_class_hierarchy(pe, image_base, hierarchy_ptr, &rdata_ranges);
|
||||
v.base_classes_json =
|
||||
read_class_hierarchy(pe, image_base, hierarchy_ptr, &rdata_ranges);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -320,8 +374,15 @@ fn is_in_ranges(addr: u32, ranges: &[(u32, u32)]) -> bool {
|
||||
/// Read 4 big-endian bytes at absolute VA `addr` from the PE image.
|
||||
fn read_be_u32(pe: &[u8], image_base: u32, addr: u32) -> Option<u32> {
|
||||
let off = addr.wrapping_sub(image_base) as usize;
|
||||
if off + 4 > pe.len() { return None; }
|
||||
Some(u32::from_be_bytes([pe[off], pe[off + 1], pe[off + 2], pe[off + 3]]))
|
||||
if off + 4 > pe.len() {
|
||||
return None;
|
||||
}
|
||||
Some(u32::from_be_bytes([
|
||||
pe[off],
|
||||
pe[off + 1],
|
||||
pe[off + 2],
|
||||
pe[off + 3],
|
||||
]))
|
||||
}
|
||||
|
||||
/// Parse a `CompleteObjectLocator` at VA `col`. Returns
|
||||
@@ -338,7 +399,9 @@ fn read_be_u32(pe: &[u8], image_base: u32, addr: u32) -> Option<u32> {
|
||||
fn read_col(pe: &[u8], image_base: u32, col: u32) -> Option<(u32, u32)> {
|
||||
let td = read_be_u32(pe, image_base, col + 0x0C)?;
|
||||
let chd = read_be_u32(pe, image_base, col + 0x10)?;
|
||||
if td == 0 { return None; }
|
||||
if td == 0 {
|
||||
return None;
|
||||
}
|
||||
Some((td, chd))
|
||||
}
|
||||
|
||||
@@ -352,17 +415,27 @@ fn read_typedescriptor_name(
|
||||
td: u32,
|
||||
rdata_ranges: &[(u32, u32)],
|
||||
) -> Option<String> {
|
||||
if !is_in_ranges(td, rdata_ranges) { return None; }
|
||||
if !is_in_ranges(td, rdata_ranges) {
|
||||
return None;
|
||||
}
|
||||
let name_va = td + 0x08;
|
||||
let off = name_va.wrapping_sub(image_base) as usize;
|
||||
if off + 1 > pe.len() { return None; }
|
||||
if off + 1 > pe.len() {
|
||||
return None;
|
||||
}
|
||||
// Read up to 256 bytes or until NUL.
|
||||
let mut end = off;
|
||||
while end < pe.len().min(off + 256) && pe[end] != 0 { end += 1; }
|
||||
if end == off { return None; }
|
||||
while end < pe.len().min(off + 256) && pe[end] != 0 {
|
||||
end += 1;
|
||||
}
|
||||
if end == off {
|
||||
return None;
|
||||
}
|
||||
let s = std::str::from_utf8(&pe[off..end]).ok()?;
|
||||
// Sanity: MSVC RTTI names always start with `.?A`.
|
||||
if !s.starts_with(".?A") { return None; }
|
||||
if !s.starts_with(".?A") {
|
||||
return None;
|
||||
}
|
||||
Some(s.to_string())
|
||||
}
|
||||
|
||||
@@ -403,11 +476,17 @@ fn read_class_hierarchy(
|
||||
chd: u32,
|
||||
rdata_ranges: &[(u32, u32)],
|
||||
) -> Option<String> {
|
||||
if !is_in_ranges(chd, rdata_ranges) { return None; }
|
||||
if !is_in_ranges(chd, rdata_ranges) {
|
||||
return None;
|
||||
}
|
||||
let num_bases = read_be_u32(pe, image_base, chd + 0x08)?;
|
||||
if num_bases == 0 || num_bases > 256 { return None; } // sanity cap
|
||||
if num_bases == 0 || num_bases > 256 {
|
||||
return None;
|
||||
} // sanity cap
|
||||
let bca_ptr = read_be_u32(pe, image_base, chd + 0x0C)?;
|
||||
if !is_in_ranges(bca_ptr, rdata_ranges) { return None; }
|
||||
if !is_in_ranges(bca_ptr, rdata_ranges) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut names: Vec<String> = Vec::new();
|
||||
for i in 0..num_bases {
|
||||
@@ -419,10 +498,7 @@ fn read_class_hierarchy(
|
||||
Some(p) if is_in_ranges(p, rdata_ranges) => p,
|
||||
_ => return None,
|
||||
};
|
||||
let mangled = match read_typedescriptor_name(pe, image_base, td_ptr, rdata_ranges) {
|
||||
Some(s) => s,
|
||||
None => return None,
|
||||
};
|
||||
let mangled = read_typedescriptor_name(pe, image_base, td_ptr, rdata_ranges)?;
|
||||
let cls = demangle_rtti_typename(&mangled).unwrap_or(mangled);
|
||||
names.push(cls);
|
||||
}
|
||||
@@ -453,7 +529,12 @@ pub fn scan_vptr_write_constants(
|
||||
let data_ranges: Vec<(u32, u32)> = sections
|
||||
.iter()
|
||||
.filter(|s| matches!(s.name.as_str(), ".rdata" | ".data"))
|
||||
.map(|s| (image_base + s.virtual_address, image_base + s.virtual_address + s.virtual_size))
|
||||
.map(|s| {
|
||||
(
|
||||
image_base + s.virtual_address,
|
||||
image_base + s.virtual_address + s.virtual_size,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let in_data = |a: u32| data_ranges.iter().any(|&(s, e)| a >= s && a < e);
|
||||
|
||||
@@ -465,13 +546,22 @@ pub fn scan_vptr_write_constants(
|
||||
|
||||
let read = |addr: u32| -> Option<u32> {
|
||||
let off = addr.wrapping_sub(image_base) as usize;
|
||||
if off + 4 > pe.len() { return None; }
|
||||
Some(u32::from_be_bytes([pe[off], pe[off + 1], pe[off + 2], pe[off + 3]]))
|
||||
if off + 4 > pe.len() {
|
||||
return None;
|
||||
}
|
||||
Some(u32::from_be_bytes([
|
||||
pe[off],
|
||||
pe[off + 1],
|
||||
pe[off + 2],
|
||||
pe[off + 3],
|
||||
]))
|
||||
};
|
||||
|
||||
let mut anchors: std::collections::BTreeSet<u32> = std::collections::BTreeSet::new();
|
||||
for (&fn_start, &(fn_end, is_saverestore)) in functions {
|
||||
if is_saverestore { continue; }
|
||||
if is_saverestore {
|
||||
continue;
|
||||
}
|
||||
let mut reg: [Option<u32>; 32] = [None; 32];
|
||||
let mut pc = fn_start;
|
||||
while pc < fn_end {
|
||||
@@ -506,11 +596,13 @@ pub fn scan_vptr_write_constants(
|
||||
32..=35 | 40..=43 | 48..=51 => reg[rd] = None,
|
||||
OP_X_FORM => {
|
||||
let xo = (instr >> 1) & 0x3FF;
|
||||
if xo != 444 && xo != 467 { reg[rd] = None; } // keep `or`(444=mr)/`mtspr`-ish
|
||||
if xo != 444 && xo != 467 {
|
||||
reg[rd] = None;
|
||||
} // keep `or`(444=mr)/`mtspr`-ish
|
||||
}
|
||||
18 | 16 => {
|
||||
if (instr & 1) != 0 {
|
||||
for r in 0..=12 { reg[r] = None; }
|
||||
18 | 16 if (instr & 1) != 0 => {
|
||||
for r in 0..=12 {
|
||||
reg[r] = None;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
@@ -550,7 +642,8 @@ pub fn methods_table(
|
||||
for v in vtables {
|
||||
for (slot, &fn_va) in v.methods.iter().enumerate() {
|
||||
let label = labels.get(&fn_va).cloned();
|
||||
let demangled = label.as_ref()
|
||||
let demangled = label
|
||||
.as_ref()
|
||||
.and_then(|l| demangle::demangle(l).map(|d| d.raw_demangled));
|
||||
out.push((v.address, slot as u32, fn_va, label, demangled));
|
||||
}
|
||||
@@ -572,6 +665,71 @@ pub fn classes_table(vtables: &[Vtable]) -> Vec<(String, u32, bool, Option<Strin
|
||||
.collect()
|
||||
}
|
||||
|
||||
// ── RTTI relabelling ───────────────────────────────────────────────────────
|
||||
|
||||
/// Overwrite heuristic vtable identity with the authoritative RTTI walk.
|
||||
///
|
||||
/// [`analyze_with_anchors`] names a table either from its own inline COL walk
|
||||
/// or, failing that, with a synthetic `ANON_Class_<hash>`. [`crate::rtti`]
|
||||
/// resolves the same question top-down from the structures the linker emitted,
|
||||
/// which is exact — so wherever the two disagree, RTTI wins. Rows RTTI knows
|
||||
/// nothing about keep their heuristic name.
|
||||
///
|
||||
/// `base_classes_json` is rebuilt here as the class's full linearised base list
|
||||
/// (excluding index 0, which is the class itself), which is strictly more than
|
||||
/// the first-level list the inline walk produced.
|
||||
///
|
||||
/// Returns the number of vtables that gained a real class name.
|
||||
pub fn apply_rtti_names(vtables: &mut [Vtable], rtti: &crate::rtti::RttiResult) -> usize {
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
let names = rtti.vtable_class_names();
|
||||
let locator_by_vtable: BTreeMap<u32, &crate::rtti::CompleteObjectLocator> = rtti
|
||||
.locators
|
||||
.iter()
|
||||
.filter_map(|c| c.vtable_address.map(|v| (v, c)))
|
||||
.collect();
|
||||
|
||||
// class-hierarchy VA → base class names, in the linker's order.
|
||||
let mut bases_by_chd: BTreeMap<u32, Vec<&str>> = BTreeMap::new();
|
||||
for b in &rtti.base_classes {
|
||||
if b.index == 0 {
|
||||
continue;
|
||||
} // index 0 is the class itself
|
||||
bases_by_chd
|
||||
.entry(b.class_hierarchy)
|
||||
.or_default()
|
||||
.push(b.name.as_str());
|
||||
}
|
||||
|
||||
let mut named = 0usize;
|
||||
for vt in vtables.iter_mut() {
|
||||
let Some((class_name, offset)) = names.get(&vt.address) else {
|
||||
continue;
|
||||
};
|
||||
// A secondary-base vftable belongs to the same class but is a distinct
|
||||
// table; keep them apart by suffixing the subobject offset.
|
||||
vt.class_name = if *offset == 0 {
|
||||
class_name.clone()
|
||||
} else {
|
||||
format!("{class_name}#base+0x{offset:X}")
|
||||
};
|
||||
vt.rtti_present = true;
|
||||
if let Some(col) = locator_by_vtable.get(&vt.address) {
|
||||
vt.col_address = Some(col.address);
|
||||
vt.base_classes_json = bases_by_chd.get(&col.class_hierarchy).map(|names| {
|
||||
let items: Vec<String> = names
|
||||
.iter()
|
||||
.map(|n| format!("\"{}\"", n.replace('\\', "\\\\").replace('"', "\\\"")))
|
||||
.collect();
|
||||
format!("[{}]", items.join(","))
|
||||
});
|
||||
}
|
||||
named += 1;
|
||||
}
|
||||
named
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -603,7 +761,11 @@ mod tests {
|
||||
let mut pe = vec![0u8; total];
|
||||
|
||||
// Vtable: 3 method PCs at .rdata start, all valid function entries.
|
||||
let m: [u32; 3] = [image_base + text_va, image_base + text_va + 0x10, image_base + text_va + 0x20];
|
||||
let m: [u32; 3] = [
|
||||
image_base + text_va,
|
||||
image_base + text_va + 0x10,
|
||||
image_base + text_va + 0x20,
|
||||
];
|
||||
for (i, val) in m.iter().enumerate() {
|
||||
pe[rdata_va as usize + i * 4..rdata_va as usize + (i + 1) * 4]
|
||||
.copy_from_slice(&val.to_be_bytes());
|
||||
@@ -628,7 +790,9 @@ mod tests {
|
||||
},
|
||||
];
|
||||
let mut function_starts = std::collections::BTreeSet::new();
|
||||
for &pc in &m { function_starts.insert(pc); }
|
||||
for &pc in &m {
|
||||
function_starts.insert(pc);
|
||||
}
|
||||
|
||||
let vtables = analyze(&pe, image_base, §ions, &function_starts);
|
||||
assert_eq!(vtables.len(), 1);
|
||||
@@ -680,7 +844,9 @@ mod tests {
|
||||
},
|
||||
];
|
||||
let mut function_starts = std::collections::BTreeSet::new();
|
||||
for &pc in &[f0, f1, f2] { function_starts.insert(pc); }
|
||||
for &pc in &[f0, f1, f2] {
|
||||
function_starts.insert(pc);
|
||||
}
|
||||
|
||||
// Without an anchor: the head gap (null + nonfn = 2 slots) means the
|
||||
// contiguous run is only [f0,f1,f2]=3 starting at +0x08, so pass-1
|
||||
@@ -715,13 +881,13 @@ mod tests {
|
||||
let mut pe = vec![0u8; 0x4000];
|
||||
// Lay out a tiny .rdata at 0x...A900 so the constant lands in-range.
|
||||
let vt_base = 0x8200A908u32; // 0x82010000 - 22264
|
||||
let addis = (15u32 << 26) | (11 << 21) | (0 << 16) | 0x8201;
|
||||
let addis = ((15u32 << 26) | (11 << 21)) | 0x8201;
|
||||
let lo = (vt_base & 0xFFFF) as i16; // -22264
|
||||
let addi = (14u32 << 26) | (11 << 21) | (0 << 16) | ((lo as u16) as u32);
|
||||
let addi = ((14u32 << 26) | (11 << 21)) | ((lo as u16) as u32);
|
||||
// addi r11,r0,lo would set r11=lo (sign-extended); we need addis+addi
|
||||
// chained. Re-encode addis into r11 from r0, then addi r11,r11,lo.
|
||||
let addi2 = (14u32 << 26) | (11 << 21) | (11 << 16) | ((lo as u16) as u32);
|
||||
let stw = (36u32 << 26) | (11 << 21) | (31 << 16) | 0; // stw r11,0(r31)
|
||||
let stw = (36u32 << 26) | (11 << 21) | (31 << 16); // stw r11,0(r31)
|
||||
let at = (ctor - image_base) as usize;
|
||||
pe[at..at + 4].copy_from_slice(&addis.to_be_bytes());
|
||||
pe[at + 4..at + 8].copy_from_slice(&addi2.to_be_bytes());
|
||||
@@ -736,12 +902,20 @@ mod tests {
|
||||
raw_size: 0x200,
|
||||
flags: 0x4000_0040,
|
||||
}];
|
||||
let mut funcs: std::collections::BTreeMap<u32, (u32, bool)> = std::collections::BTreeMap::new();
|
||||
let mut funcs: std::collections::BTreeMap<u32, (u32, bool)> =
|
||||
std::collections::BTreeMap::new();
|
||||
funcs.insert(ctor, (ctor + 0x40, false));
|
||||
let anchors = scan_vptr_write_constants(
|
||||
&pe, image_base, &funcs, §ions, &std::collections::HashSet::new(),
|
||||
&pe,
|
||||
image_base,
|
||||
&funcs,
|
||||
§ions,
|
||||
&std::collections::HashSet::new(),
|
||||
);
|
||||
assert!(
|
||||
anchors.contains(&vt_base),
|
||||
"ctor vptr store must yield anchor {vt_base:#x}, got {anchors:?}"
|
||||
);
|
||||
assert!(anchors.contains(&vt_base), "ctor vptr store must yield anchor {vt_base:#x}, got {anchors:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -776,66 +950,14 @@ mod tests {
|
||||
},
|
||||
];
|
||||
let mut function_starts = std::collections::BTreeSet::new();
|
||||
for &pc in &m { function_starts.insert(pc); }
|
||||
let vtables = analyze(&pe, image_base, §ions, &function_starts);
|
||||
assert_eq!(vtables.len(), 0, "runs of 2 must be rejected to keep false-positive rate down");
|
||||
}
|
||||
}
|
||||
|
||||
// ── RTTI relabelling ───────────────────────────────────────────────────────
|
||||
|
||||
/// Overwrite heuristic vtable identity with the authoritative RTTI walk.
|
||||
///
|
||||
/// [`analyze_with_anchors`] names a table either from its own inline COL walk
|
||||
/// or, failing that, with a synthetic `ANON_Class_<hash>`. [`crate::rtti`]
|
||||
/// resolves the same question top-down from the structures the linker emitted,
|
||||
/// which is exact — so wherever the two disagree, RTTI wins. Rows RTTI knows
|
||||
/// nothing about keep their heuristic name.
|
||||
///
|
||||
/// `base_classes_json` is rebuilt here as the class's full linearised base list
|
||||
/// (excluding index 0, which is the class itself), which is strictly more than
|
||||
/// the first-level list the inline walk produced.
|
||||
///
|
||||
/// Returns the number of vtables that gained a real class name.
|
||||
pub fn apply_rtti_names(vtables: &mut [Vtable], rtti: &crate::rtti::RttiResult) -> usize {
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
let names = rtti.vtable_class_names();
|
||||
let locator_by_vtable: BTreeMap<u32, &crate::rtti::CompleteObjectLocator> = rtti
|
||||
.locators
|
||||
.iter()
|
||||
.filter_map(|c| c.vtable_address.map(|v| (v, c)))
|
||||
.collect();
|
||||
|
||||
// class-hierarchy VA → base class names, in the linker's order.
|
||||
let mut bases_by_chd: BTreeMap<u32, Vec<&str>> = BTreeMap::new();
|
||||
for b in &rtti.base_classes {
|
||||
if b.index == 0 { continue; } // index 0 is the class itself
|
||||
bases_by_chd.entry(b.class_hierarchy).or_default().push(b.name.as_str());
|
||||
}
|
||||
|
||||
let mut named = 0usize;
|
||||
for vt in vtables.iter_mut() {
|
||||
let Some((class_name, offset)) = names.get(&vt.address) else { continue };
|
||||
// A secondary-base vftable belongs to the same class but is a distinct
|
||||
// table; keep them apart by suffixing the subobject offset.
|
||||
vt.class_name = if *offset == 0 {
|
||||
class_name.clone()
|
||||
} else {
|
||||
format!("{class_name}#base+0x{offset:X}")
|
||||
};
|
||||
vt.rtti_present = true;
|
||||
if let Some(col) = locator_by_vtable.get(&vt.address) {
|
||||
vt.col_address = Some(col.address);
|
||||
vt.base_classes_json = bases_by_chd.get(&col.class_hierarchy).map(|names| {
|
||||
let items: Vec<String> = names
|
||||
.iter()
|
||||
.map(|n| format!("\"{}\"", n.replace('\\', "\\\\").replace('"', "\\\"")))
|
||||
.collect();
|
||||
format!("[{}]", items.join(","))
|
||||
});
|
||||
for &pc in &m {
|
||||
function_starts.insert(pc);
|
||||
}
|
||||
named += 1;
|
||||
let vtables = analyze(&pe, image_base, §ions, &function_starts);
|
||||
assert_eq!(
|
||||
vtables.len(),
|
||||
0,
|
||||
"runs of 2 must be rejected to keep false-positive rate down"
|
||||
);
|
||||
}
|
||||
named
|
||||
}
|
||||
|
||||
@@ -119,7 +119,10 @@ fn be16(b: &[u8], o: usize) -> Option<u16> {
|
||||
}
|
||||
fn be32(b: &[u8], o: usize) -> Option<u32> {
|
||||
Some(u32::from_be_bytes([
|
||||
*b.get(o)?, *b.get(o + 1)?, *b.get(o + 2)?, *b.get(o + 3)?,
|
||||
*b.get(o)?,
|
||||
*b.get(o + 1)?,
|
||||
*b.get(o + 2)?,
|
||||
*b.get(o + 3)?,
|
||||
]))
|
||||
}
|
||||
fn be64(b: &[u8], o: usize) -> Option<u64> {
|
||||
@@ -188,9 +191,12 @@ pub fn analyze(image: &[u8], base: usize) -> Option<Xdbf> {
|
||||
|
||||
for i in 0..entry_used {
|
||||
let p = entry_table + i * 18;
|
||||
let (Some(namespace), Some(id), Some(off), Some(size)) =
|
||||
(be16(image, p), be64(image, p + 2), be32(image, p + 10), be32(image, p + 14))
|
||||
else {
|
||||
let (Some(namespace), Some(id), Some(off), Some(size)) = (
|
||||
be16(image, p),
|
||||
be64(image, p + 2),
|
||||
be32(image, p + 10),
|
||||
be32(image, p + 14),
|
||||
) else {
|
||||
continue;
|
||||
};
|
||||
let body = data_start + off as usize;
|
||||
@@ -212,7 +218,11 @@ pub fn analyze(image: &[u8], base: usize) -> Option<Xdbf> {
|
||||
id,
|
||||
offset: body,
|
||||
size,
|
||||
format: if image[body..].starts_with(b"\x89PNG") { "png" } else { "unknown" },
|
||||
format: if image[body..].starts_with(b"\x89PNG") {
|
||||
"png"
|
||||
} else {
|
||||
"unknown"
|
||||
},
|
||||
}),
|
||||
NS_STRING_TABLE => {
|
||||
if let Some(t) = parse_string_table(image, body, size, id as u32) {
|
||||
@@ -220,7 +230,9 @@ pub fn analyze(image: &[u8], base: usize) -> Option<Xdbf> {
|
||||
}
|
||||
}
|
||||
NS_METADATA => match magic.as_deref() {
|
||||
Some("XACH") => out.achievements.extend(parse_achievements(image, body, size)),
|
||||
Some("XACH") => out
|
||||
.achievements
|
||||
.extend(parse_achievements(image, body, size)),
|
||||
Some("XTHD") => out.title = parse_title_header(image, body),
|
||||
Some("XSTC") => out.default_language = be32(image, body + 12),
|
||||
_ => {}
|
||||
@@ -244,16 +256,21 @@ pub fn analyze(image: &[u8], base: usize) -> Option<Xdbf> {
|
||||
|
||||
/// `XACH`: `magic, version, size, count u16`, then 36-byte records.
|
||||
fn parse_achievements(image: &[u8], body: usize, size: usize) -> Vec<Achievement> {
|
||||
let Some(count) = be16(image, body + 12) else { return Vec::new() };
|
||||
let Some(count) = be16(image, body + 12) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let mut out = Vec::with_capacity(count as usize);
|
||||
for i in 0..count as usize {
|
||||
let p = body + 14 + i * 36;
|
||||
if p + 36 > body + size {
|
||||
break;
|
||||
}
|
||||
let (Some(id), Some(label_id), Some(description_id), Some(unachieved_id)) =
|
||||
(be16(image, p), be16(image, p + 2), be16(image, p + 4), be16(image, p + 6))
|
||||
else {
|
||||
let (Some(id), Some(label_id), Some(description_id), Some(unachieved_id)) = (
|
||||
be16(image, p),
|
||||
be16(image, p + 2),
|
||||
be16(image, p + 4),
|
||||
be16(image, p + 6),
|
||||
) else {
|
||||
break;
|
||||
};
|
||||
out.push(Achievement {
|
||||
@@ -273,7 +290,12 @@ fn parse_achievements(image: &[u8], body: usize, size: usize) -> Vec<Achievement
|
||||
///
|
||||
/// Bodies are UTF-8 (the ASCII subset for most locales; Japanese uses the full
|
||||
/// range), decoded lossily so one bad table cannot drop a whole language.
|
||||
fn parse_string_table(image: &[u8], body: usize, size: usize, language: u32) -> Option<StringTable> {
|
||||
fn parse_string_table(
|
||||
image: &[u8],
|
||||
body: usize,
|
||||
size: usize,
|
||||
language: u32,
|
||||
) -> Option<StringTable> {
|
||||
if fourcc(be32(image, body)?)? != "XSTR" {
|
||||
return None;
|
||||
}
|
||||
@@ -282,7 +304,9 @@ fn parse_string_table(image: &[u8], body: usize, size: usize, language: u32) ->
|
||||
let mut p = body + 14;
|
||||
let mut strings = Vec::with_capacity(count as usize);
|
||||
for _ in 0..count {
|
||||
let (Some(id), Some(len)) = (be16(image, p), be16(image, p + 2)) else { break };
|
||||
let (Some(id), Some(len)) = (be16(image, p), be16(image, p + 2)) else {
|
||||
break;
|
||||
};
|
||||
let s = p + 4;
|
||||
let e = s + len as usize;
|
||||
if e > end || e > image.len() {
|
||||
@@ -328,12 +352,12 @@ mod tests {
|
||||
xach.extend(0u32.to_be_bytes());
|
||||
xach.extend(1u16.to_be_bytes()); // count
|
||||
let mut rec = Vec::new();
|
||||
rec.extend(7u16.to_be_bytes()); // id
|
||||
rec.extend(7u16.to_be_bytes()); // id
|
||||
rec.extend(100u16.to_be_bytes()); // label
|
||||
rec.extend(101u16.to_be_bytes()); // description
|
||||
rec.extend(102u16.to_be_bytes()); // unachieved
|
||||
rec.extend(9u32.to_be_bytes()); // image id
|
||||
rec.extend(20u16.to_be_bytes()); // gamerscore
|
||||
rec.extend(9u32.to_be_bytes()); // image id
|
||||
rec.extend(20u16.to_be_bytes()); // gamerscore
|
||||
rec.extend(0u16.to_be_bytes());
|
||||
rec.extend(0x0Cu32.to_be_bytes()); // flags
|
||||
rec.extend([0u8; 16]);
|
||||
@@ -358,7 +382,7 @@ mod tests {
|
||||
xthd.extend(1u32.to_be_bytes());
|
||||
xthd.extend(0u32.to_be_bytes());
|
||||
xthd.extend(0x5351_07D4u32.to_be_bytes()); // title id
|
||||
xthd.extend(1u32.to_be_bytes()); // type = full
|
||||
xthd.extend(1u32.to_be_bytes()); // type = full
|
||||
xthd.extend(1u16.to_be_bytes());
|
||||
xthd.extend(2u16.to_be_bytes());
|
||||
xthd.extend(3u16.to_be_bytes());
|
||||
@@ -418,7 +442,11 @@ mod tests {
|
||||
assert_eq!(t.language, 1);
|
||||
assert_eq!(t.strings[0], (100, "Space Combat Award".to_string()));
|
||||
// The achievement's label resolves through the table.
|
||||
let name = t.strings.iter().find(|(i, _)| *i == a.label_id).map(|(_, s)| s.as_str());
|
||||
let name = t
|
||||
.strings
|
||||
.iter()
|
||||
.find(|(i, _)| *i == a.label_id)
|
||||
.map(|(_, s)| s.as_str());
|
||||
assert_eq!(name, Some("Space Combat Award"));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
//! Cross-reference analysis for Xbox 360 PE images.
|
||||
|
||||
use crate::func::FuncAnalysis;
|
||||
use std::collections::HashMap;
|
||||
use sylpheed_xex::pe::PeSection;
|
||||
use crate::func::FuncAnalysis;
|
||||
|
||||
// ── Cross-reference types ────────────────────────────────────────────────
|
||||
|
||||
@@ -21,19 +21,22 @@ pub enum XrefKind {
|
||||
impl XrefKind {
|
||||
pub fn tag(self) -> &'static str {
|
||||
match self {
|
||||
XrefKind::Call => "call",
|
||||
XrefKind::Call => "call",
|
||||
XrefKind::IndirectCall => "ind_call",
|
||||
XrefKind::JumpTable => "jt",
|
||||
XrefKind::Jump => "j",
|
||||
XrefKind::Branch => "br",
|
||||
XrefKind::DataRead => "read",
|
||||
XrefKind::DataWrite => "write",
|
||||
XrefKind::DataRef => "ref",
|
||||
XrefKind::JumpTable => "jt",
|
||||
XrefKind::Jump => "j",
|
||||
XrefKind::Branch => "br",
|
||||
XrefKind::DataRead => "read",
|
||||
XrefKind::DataWrite => "write",
|
||||
XrefKind::DataRef => "ref",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_data(self) -> bool {
|
||||
matches!(self, XrefKind::DataRead | XrefKind::DataWrite | XrefKind::DataRef)
|
||||
matches!(
|
||||
self,
|
||||
XrefKind::DataRead | XrefKind::DataWrite | XrefKind::DataRef
|
||||
)
|
||||
}
|
||||
|
||||
pub fn db_tag(self) -> &'static str {
|
||||
@@ -73,14 +76,14 @@ pub enum AddrMode {
|
||||
impl AddrMode {
|
||||
pub fn tag(self) -> &'static str {
|
||||
match self {
|
||||
AddrMode::DForm => "d_form",
|
||||
AddrMode::LisAddi => "lis_addi",
|
||||
AddrMode::LisOri => "lis_ori",
|
||||
AddrMode::Multiword => "multiword",
|
||||
AddrMode::DForm => "d_form",
|
||||
AddrMode::LisAddi => "lis_addi",
|
||||
AddrMode::LisOri => "lis_ori",
|
||||
AddrMode::Multiword => "multiword",
|
||||
AddrMode::XFormIndexed => "x_form_indexed",
|
||||
AddrMode::XFormByteRev => "x_form_byterev",
|
||||
AddrMode::Atomic => "atomic",
|
||||
AddrMode::DCBZ => "dcbz",
|
||||
AddrMode::Atomic => "atomic",
|
||||
AddrMode::DCBZ => "dcbz",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -113,7 +116,12 @@ pub fn analyze_xrefs(
|
||||
import_map: &HashMap<u32, String>,
|
||||
) -> XrefResult {
|
||||
analyze_xrefs_skipping(
|
||||
pe, image_base, entry_point, sections, func_analysis, import_map,
|
||||
pe,
|
||||
image_base,
|
||||
entry_point,
|
||||
sections,
|
||||
func_analysis,
|
||||
import_map,
|
||||
&std::collections::BTreeSet::new(),
|
||||
)
|
||||
}
|
||||
@@ -151,7 +159,9 @@ pub fn analyze_xrefs_skipping(
|
||||
let mut xrefs: XrefMap = HashMap::new();
|
||||
|
||||
for section in sections {
|
||||
if !section.is_code() { continue; }
|
||||
if !section.is_code() {
|
||||
continue;
|
||||
}
|
||||
let va_start = section.virtual_address;
|
||||
let va_end = va_start + section.virtual_size;
|
||||
let file_start = section.virtual_address as usize;
|
||||
@@ -160,10 +170,10 @@ pub fn analyze_xrefs_skipping(
|
||||
while addr < va_end {
|
||||
let abs_addr = image_base + addr;
|
||||
let off = (addr - va_start) as usize + file_start;
|
||||
if off + 4 > pe.len() { break; }
|
||||
let instr = u32::from_be_bytes([
|
||||
pe[off], pe[off+1], pe[off+2], pe[off+3]
|
||||
]);
|
||||
if off + 4 > pe.len() {
|
||||
break;
|
||||
}
|
||||
let instr = u32::from_be_bytes([pe[off], pe[off + 1], pe[off + 2], pe[off + 3]]);
|
||||
|
||||
if !data_words.contains(&abs_addr) {
|
||||
collect_branch_target(instr, abs_addr, &mut labels, &mut xrefs);
|
||||
@@ -176,13 +186,20 @@ pub fn analyze_xrefs_skipping(
|
||||
let mut data_annotations: HashMap<u32, (u32, XrefKind)> = HashMap::new();
|
||||
|
||||
// Build set of valid data address ranges for filtering false positives
|
||||
let data_ranges: Vec<(u32, u32)> = sections.iter()
|
||||
.map(|s| (image_base + s.virtual_address,
|
||||
image_base + s.virtual_address + s.virtual_size))
|
||||
let data_ranges: Vec<(u32, u32)> = sections
|
||||
.iter()
|
||||
.map(|s| {
|
||||
(
|
||||
image_base + s.virtual_address,
|
||||
image_base + s.virtual_address + s.virtual_size,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
for section in sections {
|
||||
if !section.is_code() { continue; }
|
||||
if !section.is_code() {
|
||||
continue;
|
||||
}
|
||||
let va_start = section.virtual_address;
|
||||
let va_end = va_start + section.virtual_size;
|
||||
let file_start = section.virtual_address as usize;
|
||||
@@ -194,10 +211,10 @@ pub fn analyze_xrefs_skipping(
|
||||
while addr < va_end {
|
||||
let abs_addr = image_base + addr;
|
||||
let off = (addr - va_start) as usize + file_start;
|
||||
if off + 4 > pe.len() { break; }
|
||||
let instr = u32::from_be_bytes([
|
||||
pe[off], pe[off+1], pe[off+2], pe[off+3]
|
||||
]);
|
||||
if off + 4 > pe.len() {
|
||||
break;
|
||||
}
|
||||
let instr = u32::from_be_bytes([pe[off], pe[off + 1], pe[off + 2], pe[off + 3]]);
|
||||
|
||||
// A jump-table word is not an instruction. Skip it, and drop the
|
||||
// tracked constants with it: the words around it belong to
|
||||
@@ -217,9 +234,11 @@ pub fn analyze_xrefs_skipping(
|
||||
// Reset tracking on function boundaries (prologue = mfspr rN, LR)
|
||||
if opcode == 31 {
|
||||
let xo = (instr >> 1) & 0x3FF;
|
||||
if xo == 339 { // mfspr
|
||||
if xo == 339 {
|
||||
// mfspr
|
||||
let spr = (((instr >> 16) & 0x1F) << 5) | ((instr >> 11) & 0x1F);
|
||||
if spr == 8 { // LR
|
||||
if spr == 8 {
|
||||
// LR
|
||||
reg_hi = [None; 32];
|
||||
}
|
||||
}
|
||||
@@ -245,10 +264,13 @@ pub fn analyze_xrefs_skipping(
|
||||
if is_in_ranges(data_addr, &data_ranges) {
|
||||
data_annotations.insert(abs_addr, (data_addr, XrefKind::DataRef));
|
||||
xrefs.entry(data_addr).or_default().push(Xref {
|
||||
source: abs_addr, kind: XrefKind::DataRef,
|
||||
source: abs_addr,
|
||||
kind: XrefKind::DataRef,
|
||||
addr_mode: Some(AddrMode::LisAddi),
|
||||
});
|
||||
labels.entry(data_addr).or_insert_with(|| format!("dat_{data_addr:08X}"));
|
||||
labels
|
||||
.entry(data_addr)
|
||||
.or_insert_with(|| format!("dat_{data_addr:08X}"));
|
||||
}
|
||||
reg_hi[rd] = Some(data_addr); // propagate for chained access
|
||||
} else {
|
||||
@@ -263,10 +285,13 @@ pub fn analyze_xrefs_skipping(
|
||||
if is_in_ranges(data_addr, &data_ranges) {
|
||||
data_annotations.insert(abs_addr, (data_addr, XrefKind::DataRef));
|
||||
xrefs.entry(data_addr).or_default().push(Xref {
|
||||
source: abs_addr, kind: XrefKind::DataRef,
|
||||
source: abs_addr,
|
||||
kind: XrefKind::DataRef,
|
||||
addr_mode: Some(AddrMode::LisOri),
|
||||
});
|
||||
labels.entry(data_addr).or_insert_with(|| format!("dat_{data_addr:08X}"));
|
||||
labels
|
||||
.entry(data_addr)
|
||||
.or_insert_with(|| format!("dat_{data_addr:08X}"));
|
||||
}
|
||||
reg_hi[ra] = Some(data_addr);
|
||||
} else {
|
||||
@@ -276,17 +301,21 @@ pub fn analyze_xrefs_skipping(
|
||||
// Load instructions: lwz, lbz, lhz, lha, lfs, lfd, lwzu, etc.
|
||||
32 | 33 | 34 | 35 | 40 | 41 | 42 | 43 | 48 | 49 | 50 | 51 => {
|
||||
if ra != 0
|
||||
&& let Some(base) = reg_hi[ra] {
|
||||
let data_addr = base.wrapping_add(simm as u32);
|
||||
if is_in_ranges(data_addr, &data_ranges) {
|
||||
data_annotations.insert(abs_addr, (data_addr, XrefKind::DataRead));
|
||||
xrefs.entry(data_addr).or_default().push(Xref {
|
||||
source: abs_addr, kind: XrefKind::DataRead,
|
||||
addr_mode: Some(AddrMode::DForm),
|
||||
});
|
||||
labels.entry(data_addr).or_insert_with(|| format!("dat_{data_addr:08X}"));
|
||||
}
|
||||
&& let Some(base) = reg_hi[ra]
|
||||
{
|
||||
let data_addr = base.wrapping_add(simm as u32);
|
||||
if is_in_ranges(data_addr, &data_ranges) {
|
||||
data_annotations.insert(abs_addr, (data_addr, XrefKind::DataRead));
|
||||
xrefs.entry(data_addr).or_default().push(Xref {
|
||||
source: abs_addr,
|
||||
kind: XrefKind::DataRead,
|
||||
addr_mode: Some(AddrMode::DForm),
|
||||
});
|
||||
labels
|
||||
.entry(data_addr)
|
||||
.or_insert_with(|| format!("dat_{data_addr:08X}"));
|
||||
}
|
||||
}
|
||||
// Load into rD may clobber the tracked value
|
||||
reg_hi[rd] = None;
|
||||
}
|
||||
@@ -302,10 +331,13 @@ pub fn analyze_xrefs_skipping(
|
||||
if is_in_ranges(addr_w, &data_ranges) {
|
||||
data_annotations.insert(abs_addr, (addr_w, XrefKind::DataRead));
|
||||
xrefs.entry(addr_w).or_default().push(Xref {
|
||||
source: abs_addr, kind: XrefKind::DataRead,
|
||||
source: abs_addr,
|
||||
kind: XrefKind::DataRead,
|
||||
addr_mode: Some(AddrMode::Multiword),
|
||||
});
|
||||
labels.entry(addr_w).or_insert_with(|| format!("dat_{addr_w:08X}"));
|
||||
labels
|
||||
.entry(addr_w)
|
||||
.or_insert_with(|| format!("dat_{addr_w:08X}"));
|
||||
}
|
||||
addr_w = addr_w.wrapping_add(4);
|
||||
}
|
||||
@@ -315,17 +347,21 @@ pub fn analyze_xrefs_skipping(
|
||||
// Store instructions: stw, stb, sth, stfs, stfd, stwu, etc.
|
||||
36 | 37 | 38 | 39 | 44 | 45 | 52 | 53 | 54 | 55 => {
|
||||
if ra != 0
|
||||
&& let Some(base) = reg_hi[ra] {
|
||||
let data_addr = base.wrapping_add(simm as u32);
|
||||
if is_in_ranges(data_addr, &data_ranges) {
|
||||
data_annotations.insert(abs_addr, (data_addr, XrefKind::DataWrite));
|
||||
xrefs.entry(data_addr).or_default().push(Xref {
|
||||
source: abs_addr, kind: XrefKind::DataWrite,
|
||||
addr_mode: Some(AddrMode::DForm),
|
||||
});
|
||||
labels.entry(data_addr).or_insert_with(|| format!("dat_{data_addr:08X}"));
|
||||
}
|
||||
&& let Some(base) = reg_hi[ra]
|
||||
{
|
||||
let data_addr = base.wrapping_add(simm as u32);
|
||||
if is_in_ranges(data_addr, &data_ranges) {
|
||||
data_annotations.insert(abs_addr, (data_addr, XrefKind::DataWrite));
|
||||
xrefs.entry(data_addr).or_default().push(Xref {
|
||||
source: abs_addr,
|
||||
kind: XrefKind::DataWrite,
|
||||
addr_mode: Some(AddrMode::DForm),
|
||||
});
|
||||
labels
|
||||
.entry(data_addr)
|
||||
.or_insert_with(|| format!("dat_{data_addr:08X}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
// stmw rS, simm(rA) — D-form multi-word store. Writes
|
||||
// (32-rS) consecutive 4-byte words from rS..r31 to
|
||||
@@ -339,10 +375,13 @@ pub fn analyze_xrefs_skipping(
|
||||
if is_in_ranges(addr_w, &data_ranges) {
|
||||
data_annotations.insert(abs_addr, (addr_w, XrefKind::DataWrite));
|
||||
xrefs.entry(addr_w).or_default().push(Xref {
|
||||
source: abs_addr, kind: XrefKind::DataWrite,
|
||||
source: abs_addr,
|
||||
kind: XrefKind::DataWrite,
|
||||
addr_mode: Some(AddrMode::Multiword),
|
||||
});
|
||||
labels.entry(addr_w).or_insert_with(|| format!("dat_{addr_w:08X}"));
|
||||
labels
|
||||
.entry(addr_w)
|
||||
.or_insert_with(|| format!("dat_{addr_w:08X}"));
|
||||
}
|
||||
addr_w = addr_w.wrapping_add(4);
|
||||
}
|
||||
@@ -374,8 +413,8 @@ pub fn analyze_xrefs_skipping(
|
||||
662 => Some((AddrMode::XFormByteRev, XrefKind::DataWrite)), // stwbrx
|
||||
918 => Some((AddrMode::XFormByteRev, XrefKind::DataWrite)), // sthbrx
|
||||
// Byte-reverse loads
|
||||
534 => Some((AddrMode::XFormByteRev, XrefKind::DataRead)), // lwbrx
|
||||
790 => Some((AddrMode::XFormByteRev, XrefKind::DataRead)), // lhbrx
|
||||
534 => Some((AddrMode::XFormByteRev, XrefKind::DataRead)), // lwbrx
|
||||
790 => Some((AddrMode::XFormByteRev, XrefKind::DataRead)), // lhbrx
|
||||
// dcbz — cache-line zero (32-byte clear). Treat as a write.
|
||||
1014 => Some((AddrMode::DCBZ, XrefKind::DataWrite)),
|
||||
// Plain X-form indexed stores (the common ones)
|
||||
@@ -388,32 +427,32 @@ pub fn analyze_xrefs_skipping(
|
||||
149 => Some((AddrMode::XFormIndexed, XrefKind::DataWrite)), // stdx
|
||||
181 => Some((AddrMode::XFormIndexed, XrefKind::DataWrite)), // stdux
|
||||
// Plain X-form indexed loads
|
||||
23 => Some((AddrMode::XFormIndexed, XrefKind::DataRead)), // lwzx
|
||||
87 => Some((AddrMode::XFormIndexed, XrefKind::DataRead)), // lbzx
|
||||
279 => Some((AddrMode::XFormIndexed, XrefKind::DataRead)), // lhzx
|
||||
343 => Some((AddrMode::XFormIndexed, XrefKind::DataRead)), // lhax
|
||||
55 => Some((AddrMode::XFormIndexed, XrefKind::DataRead)), // lwzux
|
||||
119 => Some((AddrMode::XFormIndexed, XrefKind::DataRead)), // lbzux
|
||||
311 => Some((AddrMode::XFormIndexed, XrefKind::DataRead)), // lhzux
|
||||
375 => Some((AddrMode::XFormIndexed, XrefKind::DataRead)), // lhaux
|
||||
21 => Some((AddrMode::XFormIndexed, XrefKind::DataRead)), // ldx
|
||||
53 => Some((AddrMode::XFormIndexed, XrefKind::DataRead)), // ldux
|
||||
23 => Some((AddrMode::XFormIndexed, XrefKind::DataRead)), // lwzx
|
||||
87 => Some((AddrMode::XFormIndexed, XrefKind::DataRead)), // lbzx
|
||||
279 => Some((AddrMode::XFormIndexed, XrefKind::DataRead)), // lhzx
|
||||
343 => Some((AddrMode::XFormIndexed, XrefKind::DataRead)), // lhax
|
||||
55 => Some((AddrMode::XFormIndexed, XrefKind::DataRead)), // lwzux
|
||||
119 => Some((AddrMode::XFormIndexed, XrefKind::DataRead)), // lbzux
|
||||
311 => Some((AddrMode::XFormIndexed, XrefKind::DataRead)), // lhzux
|
||||
375 => Some((AddrMode::XFormIndexed, XrefKind::DataRead)), // lhaux
|
||||
21 => Some((AddrMode::XFormIndexed, XrefKind::DataRead)), // ldx
|
||||
53 => Some((AddrMode::XFormIndexed, XrefKind::DataRead)), // ldux
|
||||
// AltiVec/VMX (opcode 31) loads & stores. Element
|
||||
// variants store one byte/halfword/word; full
|
||||
// `stvx` stores 16 bytes. Address resolution still
|
||||
// requires both rA and rB constant — common only
|
||||
// in static-table setup loops.
|
||||
231 => Some((AddrMode::XFormIndexed, XrefKind::DataWrite)), // stvx
|
||||
487 => Some((AddrMode::XFormIndexed, XrefKind::DataWrite)), // stvxl
|
||||
135 => Some((AddrMode::XFormIndexed, XrefKind::DataWrite)), // stvebx
|
||||
167 => Some((AddrMode::XFormIndexed, XrefKind::DataWrite)), // stvehx
|
||||
199 => Some((AddrMode::XFormIndexed, XrefKind::DataWrite)), // stvewx
|
||||
231 => Some((AddrMode::XFormIndexed, XrefKind::DataWrite)), // stvx
|
||||
487 => Some((AddrMode::XFormIndexed, XrefKind::DataWrite)), // stvxl
|
||||
135 => Some((AddrMode::XFormIndexed, XrefKind::DataWrite)), // stvebx
|
||||
167 => Some((AddrMode::XFormIndexed, XrefKind::DataWrite)), // stvehx
|
||||
199 => Some((AddrMode::XFormIndexed, XrefKind::DataWrite)), // stvewx
|
||||
// AltiVec/VMX loads — same XO range, kind=read.
|
||||
103 => Some((AddrMode::XFormIndexed, XrefKind::DataRead)), // lvx
|
||||
359 => Some((AddrMode::XFormIndexed, XrefKind::DataRead)), // lvxl
|
||||
7 => Some((AddrMode::XFormIndexed, XrefKind::DataRead)), // lvebx
|
||||
39 => Some((AddrMode::XFormIndexed, XrefKind::DataRead)), // lvehx
|
||||
71 => Some((AddrMode::XFormIndexed, XrefKind::DataRead)), // lvewx
|
||||
103 => Some((AddrMode::XFormIndexed, XrefKind::DataRead)), // lvx
|
||||
359 => Some((AddrMode::XFormIndexed, XrefKind::DataRead)), // lvxl
|
||||
7 => Some((AddrMode::XFormIndexed, XrefKind::DataRead)), // lvebx
|
||||
39 => Some((AddrMode::XFormIndexed, XrefKind::DataRead)), // lvehx
|
||||
71 => Some((AddrMode::XFormIndexed, XrefKind::DataRead)), // lvewx
|
||||
_ => None,
|
||||
}
|
||||
};
|
||||
@@ -423,10 +462,13 @@ pub fn analyze_xrefs_skipping(
|
||||
{
|
||||
data_annotations.insert(abs_addr, (data_addr, kind));
|
||||
xrefs.entry(data_addr).or_default().push(Xref {
|
||||
source: abs_addr, kind,
|
||||
source: abs_addr,
|
||||
kind,
|
||||
addr_mode: Some(addr_mode),
|
||||
});
|
||||
labels.entry(data_addr).or_insert_with(|| format!("dat_{data_addr:08X}"));
|
||||
labels
|
||||
.entry(data_addr)
|
||||
.or_insert_with(|| format!("dat_{data_addr:08X}"));
|
||||
}
|
||||
// Fall through: any X-form op may write rD; invalidate.
|
||||
reg_hi[rd] = None;
|
||||
@@ -435,7 +477,8 @@ pub fn analyze_xrefs_skipping(
|
||||
_ => {
|
||||
// Conservatively invalidate for instructions that modify rD
|
||||
// (most ALU ops, loads, etc.)
|
||||
if opcode != 18 && opcode != 16 && opcode != 17 { // skip branch/sc
|
||||
if opcode != 18 && opcode != 16 && opcode != 17 {
|
||||
// skip branch/sc
|
||||
reg_hi[rd] = None;
|
||||
}
|
||||
}
|
||||
@@ -456,10 +499,19 @@ pub fn analyze_xrefs_skipping(
|
||||
"xref analysis complete"
|
||||
);
|
||||
|
||||
XrefResult { labels, xrefs, data_annotations }
|
||||
XrefResult {
|
||||
labels,
|
||||
xrefs,
|
||||
data_annotations,
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_branch_target(instr: u32, addr: u32, labels: &mut HashMap<u32, String>, xrefs: &mut XrefMap) {
|
||||
fn collect_branch_target(
|
||||
instr: u32,
|
||||
addr: u32,
|
||||
labels: &mut HashMap<u32, String>,
|
||||
xrefs: &mut XrefMap,
|
||||
) {
|
||||
let op = (instr >> 26) & 0x3F;
|
||||
match op {
|
||||
18 => {
|
||||
@@ -467,18 +519,38 @@ fn collect_branch_target(instr: u32, addr: u32, labels: &mut HashMap<u32, String
|
||||
let li = sign_ext26(instr & 0x03FFFFFC);
|
||||
let aa = instr & 2 != 0;
|
||||
let lk = instr & 1 != 0;
|
||||
let target = if aa { li as u32 } else { addr.wrapping_add(li as u32) };
|
||||
labels.entry(target).or_insert_with(|| format!("loc_{target:08X}"));
|
||||
let target = if aa {
|
||||
li as u32
|
||||
} else {
|
||||
addr.wrapping_add(li as u32)
|
||||
};
|
||||
labels
|
||||
.entry(target)
|
||||
.or_insert_with(|| format!("loc_{target:08X}"));
|
||||
let kind = if lk { XrefKind::Call } else { XrefKind::Jump };
|
||||
xrefs.entry(target).or_default().push(Xref { source: addr, kind, addr_mode: None });
|
||||
xrefs.entry(target).or_default().push(Xref {
|
||||
source: addr,
|
||||
kind,
|
||||
addr_mode: None,
|
||||
});
|
||||
}
|
||||
16 => {
|
||||
// B-form: bc/bcl
|
||||
let bd = sign_ext16(instr & 0xFFFC);
|
||||
let aa = instr & 2 != 0;
|
||||
let target = if aa { bd as u32 } else { addr.wrapping_add(bd as u32) };
|
||||
labels.entry(target).or_insert_with(|| format!("loc_{target:08X}"));
|
||||
xrefs.entry(target).or_default().push(Xref { source: addr, kind: XrefKind::Branch, addr_mode: None });
|
||||
let target = if aa {
|
||||
bd as u32
|
||||
} else {
|
||||
addr.wrapping_add(bd as u32)
|
||||
};
|
||||
labels
|
||||
.entry(target)
|
||||
.or_insert_with(|| format!("loc_{target:08X}"));
|
||||
xrefs.entry(target).or_default().push(Xref {
|
||||
source: addr,
|
||||
kind: XrefKind::Branch,
|
||||
addr_mode: None,
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -493,7 +565,9 @@ fn sign_ext26(val: u32) -> i32 {
|
||||
}
|
||||
|
||||
fn is_in_ranges(addr: u32, ranges: &[(u32, u32)]) -> bool {
|
||||
ranges.iter().any(|&(start, end)| addr >= start && addr < end)
|
||||
ranges
|
||||
.iter()
|
||||
.any(|&(start, end)| addr >= start && addr < end)
|
||||
}
|
||||
|
||||
/// Find which section a data address falls in.
|
||||
@@ -521,10 +595,11 @@ pub fn resolve_source_label(
|
||||
|
||||
// Find the containing function (largest start <= addr)
|
||||
if let Some((&func_start, _fi)) = func_analysis.functions.range(..=addr).next_back()
|
||||
&& let Some(func_label) = labels.get(&func_start) {
|
||||
let offset = addr - func_start;
|
||||
return format!("{func_label}+0x{offset:X}");
|
||||
}
|
||||
&& let Some(func_label) = labels.get(&func_start)
|
||||
{
|
||||
let offset = addr - func_start;
|
||||
return format!("{func_label}+0x{offset:X}");
|
||||
}
|
||||
|
||||
format!("0x{addr:08X}")
|
||||
}
|
||||
@@ -546,18 +621,30 @@ mod tests {
|
||||
AddrMode::DCBZ,
|
||||
];
|
||||
let tags: std::collections::HashSet<&str> = modes.iter().map(|m| m.tag()).collect();
|
||||
assert_eq!(tags.len(), modes.len(), "every AddrMode variant must have a unique tag");
|
||||
assert_eq!(
|
||||
tags.len(),
|
||||
modes.len(),
|
||||
"every AddrMode variant must have a unique tag"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn xref_struct_carries_addr_mode_for_data_edges() {
|
||||
let x = Xref { source: 0x1234, kind: XrefKind::DataWrite, addr_mode: Some(AddrMode::DForm) };
|
||||
let x = Xref {
|
||||
source: 0x1234,
|
||||
kind: XrefKind::DataWrite,
|
||||
addr_mode: Some(AddrMode::DForm),
|
||||
};
|
||||
assert_eq!(x.addr_mode.unwrap().tag(), "d_form");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn xref_struct_addr_mode_is_none_for_call_edges() {
|
||||
let x = Xref { source: 0x1234, kind: XrefKind::Call, addr_mode: None };
|
||||
let x = Xref {
|
||||
source: 0x1234,
|
||||
kind: XrefKind::Call,
|
||||
addr_mode: None,
|
||||
};
|
||||
assert!(x.addr_mode.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,15 +13,19 @@ use std::io::Write;
|
||||
|
||||
use duckdb::Connection;
|
||||
|
||||
use sylpheed_xex::pe::PeSection;
|
||||
use sylpheed_xexdb::DbWriter;
|
||||
use sylpheed_xexdb::formatter::DisasmInfo;
|
||||
use sylpheed_xexdb::func::{FuncAnalysis, FuncInfo};
|
||||
use sylpheed_xexdb::rtti::RttiResult;
|
||||
use sylpheed_xexdb::xref::XrefMap;
|
||||
use sylpheed_xex::pe::PeSection;
|
||||
|
||||
/// Build a 16-byte `.text` section: 4 instructions (mflr / nop / blr / nop).
|
||||
fn synthetic_pe() -> (Vec<u8>, Vec<PeSection>, Vec<sylpheed_xex::header::ImportLibrary>) {
|
||||
fn synthetic_pe() -> (
|
||||
Vec<u8>,
|
||||
Vec<PeSection>,
|
||||
Vec<sylpheed_xex::header::ImportLibrary>,
|
||||
) {
|
||||
// VA layout: image_base + 0x1000 = .text start (so RVA = 0x1000).
|
||||
// The DB writer expects pe[rva] to hold the byte at that RVA, so the
|
||||
// buffer must be at least 0x1000 + section_size bytes long.
|
||||
@@ -30,9 +34,9 @@ fn synthetic_pe() -> (Vec<u8>, Vec<PeSection>, Vec<sylpheed_xex::header::ImportL
|
||||
// mfspr r12, LR (a.k.a. mflr r12) — opcode 31, xo 339, spr 8 (LR).
|
||||
// Encoded with spr halves swapped per the ISA: spr_field = (8<<5).
|
||||
(31u32 << 26) | (12 << 21) | ((8 << 5) << 11) | (339 << 1),
|
||||
0x60000000, // nop (ori r0, r0, 0)
|
||||
(19u32 << 26) | (20 << 21) | (16 << 1), // blr (bclr 20, 0)
|
||||
0x60000000, // nop
|
||||
0x60000000, // nop (ori r0, r0, 0)
|
||||
(19u32 << 26) | (20 << 21) | (16 << 1), // blr (bclr 20, 0)
|
||||
0x60000000, // nop
|
||||
];
|
||||
|
||||
let mut pe = vec![0u8; RVA + 16];
|
||||
@@ -111,10 +115,21 @@ fn db_schema_matches_expected_columns() {
|
||||
w.ingest_instructions(&pe, &info, &func_analysis, &labels, &BTreeSet::new())
|
||||
.expect("ingest_instructions");
|
||||
w.write_analysis_results(
|
||||
&pe, &info, &func_analysis, &labels, &xrefs,
|
||||
&[], &[], &[], None, &[], &[], &RttiResult::default(), None,
|
||||
&pe,
|
||||
&info,
|
||||
&func_analysis,
|
||||
&labels,
|
||||
&xrefs,
|
||||
&[],
|
||||
&[],
|
||||
&[],
|
||||
None,
|
||||
&[],
|
||||
&[],
|
||||
&RttiResult::default(),
|
||||
None,
|
||||
)
|
||||
.expect("write_analysis_results");
|
||||
.expect("write_analysis_results");
|
||||
w.create_sql_views().expect("create_sql_views");
|
||||
}
|
||||
|
||||
@@ -122,258 +137,348 @@ fn db_schema_matches_expected_columns() {
|
||||
|
||||
// Lock the column layout per table. Pairs are (name, type).
|
||||
let expected: &[(&str, &[(&str, &str)])] = &[
|
||||
("metadata", &[
|
||||
("key", "VARCHAR"),
|
||||
("value", "VARCHAR"),
|
||||
]),
|
||||
("sections", &[
|
||||
("name", "VARCHAR"),
|
||||
("virtual_address", "BIGINT"),
|
||||
("virtual_size", "BIGINT"),
|
||||
("raw_offset", "BIGINT"),
|
||||
("raw_size", "BIGINT"),
|
||||
("flags", "BIGINT"),
|
||||
("is_code", "BOOLEAN"),
|
||||
]),
|
||||
("imports", &[
|
||||
("library", "VARCHAR"),
|
||||
("ordinal", "BIGINT"),
|
||||
("name", "VARCHAR"),
|
||||
("record_type", "BIGINT"),
|
||||
("address", "BIGINT"),
|
||||
]),
|
||||
("instructions", &[
|
||||
("address", "BIGINT"),
|
||||
("raw", "BIGINT"),
|
||||
("mnemonic", "VARCHAR"),
|
||||
("operands", "VARCHAR"),
|
||||
("disasm", "VARCHAR"),
|
||||
("ext_mnemonic", "VARCHAR"),
|
||||
("ext_operands", "VARCHAR"),
|
||||
("ext_disasm", "VARCHAR"),
|
||||
("target_hex", "BIGINT"),
|
||||
("section", "VARCHAR"),
|
||||
("function", "BIGINT"),
|
||||
("label", "VARCHAR"),
|
||||
("is_data", "BOOLEAN"),
|
||||
]),
|
||||
("functions", &[
|
||||
("address", "BIGINT"),
|
||||
("name", "VARCHAR"),
|
||||
("end_address", "BIGINT"),
|
||||
("frame_size", "BIGINT"),
|
||||
("saved_gprs", "BIGINT"),
|
||||
("is_leaf", "BOOLEAN"),
|
||||
("is_saverestore", "BOOLEAN"),
|
||||
("pdata_validated", "BOOLEAN"),
|
||||
("pdata_length", "BIGINT"),
|
||||
("prolog_length", "BIGINT"),
|
||||
("has_eh", "BOOLEAN"),
|
||||
]),
|
||||
("jump_tables", &[
|
||||
("bctr_pc", "BIGINT"),
|
||||
("function", "BIGINT"),
|
||||
("table_address", "BIGINT"),
|
||||
("entry_count", "BIGINT"),
|
||||
("table_slots", "BIGINT"),
|
||||
("index_map_address", "BIGINT"),
|
||||
("index_map_count", "BIGINT"),
|
||||
("case_bound", "BIGINT"),
|
||||
("kind", "VARCHAR"),
|
||||
]),
|
||||
("jump_table_entries", &[
|
||||
("bctr_pc", "BIGINT"),
|
||||
("case_index", "BIGINT"),
|
||||
("target_address", "BIGINT"),
|
||||
]),
|
||||
("data_in_code", &[
|
||||
("address", "BIGINT"),
|
||||
("length", "BIGINT"),
|
||||
("kind", "VARCHAR"),
|
||||
]),
|
||||
("rtti_type_descriptors", &[
|
||||
("address", "BIGINT"),
|
||||
("mangled_name", "VARCHAR"),
|
||||
("demangled_name", "VARCHAR"),
|
||||
]),
|
||||
("rtti_locators", &[
|
||||
("address", "BIGINT"),
|
||||
("subobject_offset", "BIGINT"),
|
||||
("cd_offset", "BIGINT"),
|
||||
("type_descriptor", "BIGINT"),
|
||||
("class_hierarchy", "BIGINT"),
|
||||
("vtable_address", "BIGINT"),
|
||||
]),
|
||||
("rtti_base_classes", &[
|
||||
("class_hierarchy", "BIGINT"),
|
||||
("base_index", "BIGINT"),
|
||||
("type_descriptor", "BIGINT"),
|
||||
("name", "VARCHAR"),
|
||||
("num_contained_bases", "BIGINT"),
|
||||
("mdisp", "BIGINT"),
|
||||
("pdisp", "BIGINT"),
|
||||
("vdisp", "BIGINT"),
|
||||
("attributes", "BIGINT"),
|
||||
]),
|
||||
("pdata_entries", &[
|
||||
("begin_address", "BIGINT"),
|
||||
("end_address", "BIGINT"),
|
||||
("function_length", "BIGINT"),
|
||||
("prolog_length", "BIGINT"),
|
||||
("flags", "BIGINT"),
|
||||
]),
|
||||
("labels", &[
|
||||
("address", "BIGINT"),
|
||||
("name", "VARCHAR"),
|
||||
("kind", "VARCHAR"),
|
||||
]),
|
||||
("xdbf_entries", &[
|
||||
("namespace", "BIGINT"),
|
||||
("namespace_name", "VARCHAR"),
|
||||
("id", "BIGINT"),
|
||||
("body_offset", "BIGINT"),
|
||||
("size", "BIGINT"),
|
||||
("magic", "VARCHAR"),
|
||||
]),
|
||||
("xdbf_achievements", &[
|
||||
("id", "BIGINT"),
|
||||
("name", "VARCHAR"),
|
||||
("unlocked_desc", "VARCHAR"),
|
||||
("locked_desc", "VARCHAR"),
|
||||
("label_id", "BIGINT"),
|
||||
("description_id", "BIGINT"),
|
||||
("unachieved_id", "BIGINT"),
|
||||
("image_id", "BIGINT"),
|
||||
("gamerscore", "BIGINT"),
|
||||
("flags", "BIGINT"),
|
||||
]),
|
||||
("xdbf_strings", &[
|
||||
("language", "BIGINT"),
|
||||
("language_name", "VARCHAR"),
|
||||
("string_id", "BIGINT"),
|
||||
("value", "VARCHAR"),
|
||||
]),
|
||||
("xdbf_images", &[
|
||||
("id", "BIGINT"),
|
||||
("is_title_icon", "BOOLEAN"),
|
||||
("body_offset", "BIGINT"),
|
||||
("size", "BIGINT"),
|
||||
("format", "VARCHAR"),
|
||||
]),
|
||||
("demangled_names", &[
|
||||
("address", "BIGINT"),
|
||||
("mangled", "VARCHAR"),
|
||||
("raw_demangled", "VARCHAR"),
|
||||
("namespace_path", "VARCHAR"),
|
||||
("class_name", "VARCHAR"),
|
||||
("method_name", "VARCHAR"),
|
||||
("params_signature", "VARCHAR"),
|
||||
]),
|
||||
("vtables", &[
|
||||
("address", "BIGINT"),
|
||||
("length", "BIGINT"),
|
||||
("col_address", "BIGINT"),
|
||||
("class_name", "VARCHAR"),
|
||||
("rtti_present", "BOOLEAN"),
|
||||
("base_classes_json", "VARCHAR"),
|
||||
]),
|
||||
("methods", &[
|
||||
("vtable_address", "BIGINT"),
|
||||
("slot", "BIGINT"),
|
||||
("function_address", "BIGINT"),
|
||||
("mangled_name", "VARCHAR"),
|
||||
("demangled_name", "VARCHAR"),
|
||||
]),
|
||||
("classes", &[
|
||||
("name", "VARCHAR"),
|
||||
("vtable_address", "BIGINT"),
|
||||
("rtti_present", "BOOLEAN"),
|
||||
("base_classes_json", "VARCHAR"),
|
||||
]),
|
||||
("strings", &[
|
||||
("address", "BIGINT"),
|
||||
("encoding", "VARCHAR"),
|
||||
("length", "BIGINT"),
|
||||
("content", "VARCHAR"),
|
||||
("section", "VARCHAR"),
|
||||
]),
|
||||
("tls_info", &[
|
||||
("raw_data_start", "BIGINT"),
|
||||
("raw_data_end", "BIGINT"),
|
||||
("index_address", "BIGINT"),
|
||||
("callback_array", "BIGINT"),
|
||||
("zero_fill_size", "BIGINT"),
|
||||
("characteristics", "BIGINT"),
|
||||
]),
|
||||
("tls_callbacks", &[
|
||||
("slot", "BIGINT"),
|
||||
("address", "BIGINT"),
|
||||
]),
|
||||
("function_pointer_arrays", &[
|
||||
("address", "BIGINT"),
|
||||
("length", "BIGINT"),
|
||||
("kind", "VARCHAR"),
|
||||
]),
|
||||
("function_pointer_array_entries", &[
|
||||
("array_address", "BIGINT"),
|
||||
("slot", "BIGINT"),
|
||||
("function_address", "BIGINT"),
|
||||
]),
|
||||
("indirect_dispatch_sites", &[
|
||||
("dispatch_pc", "BIGINT"),
|
||||
("vptr_offset", "BIGINT"),
|
||||
("slot", "BIGINT"),
|
||||
("candidate_count", "BIGINT"),
|
||||
("truncated", "BOOLEAN"),
|
||||
]),
|
||||
("indirect_dispatch_candidates", &[
|
||||
("dispatch_pc", "BIGINT"),
|
||||
("vtable_address", "BIGINT"),
|
||||
("method_address", "BIGINT"),
|
||||
]),
|
||||
("vptr_writes", &[
|
||||
("writer_pc", "BIGINT"),
|
||||
("vtable_address", "BIGINT"),
|
||||
("vptr_offset", "BIGINT"),
|
||||
("writer_function", "BIGINT"),
|
||||
]),
|
||||
("eh_funcinfo", &[
|
||||
("address", "BIGINT"),
|
||||
("magic", "BIGINT"),
|
||||
("max_state", "BIGINT"),
|
||||
("p_unwind_map", "BIGINT"),
|
||||
("n_try_blocks", "BIGINT"),
|
||||
("p_try_block_map", "BIGINT"),
|
||||
("n_ip_map_entries", "BIGINT"),
|
||||
("p_ip_to_state_map", "BIGINT"),
|
||||
("p_es_type_list", "BIGINT"),
|
||||
("eh_flags", "BIGINT"),
|
||||
]),
|
||||
("eh_unwind_map", &[
|
||||
("funcinfo_address", "BIGINT"),
|
||||
("state_index", "BIGINT"),
|
||||
("to_state", "BIGINT"),
|
||||
("action_pc", "BIGINT"),
|
||||
]),
|
||||
("eh_try_blocks", &[
|
||||
("funcinfo_address", "BIGINT"),
|
||||
("try_index", "BIGINT"),
|
||||
("try_low", "BIGINT"),
|
||||
("try_high", "BIGINT"),
|
||||
("catch_high", "BIGINT"),
|
||||
("n_catches", "BIGINT"),
|
||||
("p_handler_array", "BIGINT"),
|
||||
]),
|
||||
("xrefs", &[
|
||||
("source", "BIGINT"),
|
||||
("target", "BIGINT"),
|
||||
("kind", "VARCHAR"),
|
||||
("addr_mode", "VARCHAR"),
|
||||
("instruction", "VARCHAR"),
|
||||
("source_func", "BIGINT"),
|
||||
("source_label", "VARCHAR"),
|
||||
("target_label", "VARCHAR"),
|
||||
]),
|
||||
("metadata", &[("key", "VARCHAR"), ("value", "VARCHAR")]),
|
||||
(
|
||||
"sections",
|
||||
&[
|
||||
("name", "VARCHAR"),
|
||||
("virtual_address", "BIGINT"),
|
||||
("virtual_size", "BIGINT"),
|
||||
("raw_offset", "BIGINT"),
|
||||
("raw_size", "BIGINT"),
|
||||
("flags", "BIGINT"),
|
||||
("is_code", "BOOLEAN"),
|
||||
],
|
||||
),
|
||||
(
|
||||
"imports",
|
||||
&[
|
||||
("library", "VARCHAR"),
|
||||
("ordinal", "BIGINT"),
|
||||
("name", "VARCHAR"),
|
||||
("record_type", "BIGINT"),
|
||||
("address", "BIGINT"),
|
||||
],
|
||||
),
|
||||
(
|
||||
"instructions",
|
||||
&[
|
||||
("address", "BIGINT"),
|
||||
("raw", "BIGINT"),
|
||||
("mnemonic", "VARCHAR"),
|
||||
("operands", "VARCHAR"),
|
||||
("disasm", "VARCHAR"),
|
||||
("ext_mnemonic", "VARCHAR"),
|
||||
("ext_operands", "VARCHAR"),
|
||||
("ext_disasm", "VARCHAR"),
|
||||
("target_hex", "BIGINT"),
|
||||
("section", "VARCHAR"),
|
||||
("function", "BIGINT"),
|
||||
("label", "VARCHAR"),
|
||||
("is_data", "BOOLEAN"),
|
||||
],
|
||||
),
|
||||
(
|
||||
"functions",
|
||||
&[
|
||||
("address", "BIGINT"),
|
||||
("name", "VARCHAR"),
|
||||
("end_address", "BIGINT"),
|
||||
("frame_size", "BIGINT"),
|
||||
("saved_gprs", "BIGINT"),
|
||||
("is_leaf", "BOOLEAN"),
|
||||
("is_saverestore", "BOOLEAN"),
|
||||
("pdata_validated", "BOOLEAN"),
|
||||
("pdata_length", "BIGINT"),
|
||||
("prolog_length", "BIGINT"),
|
||||
("has_eh", "BOOLEAN"),
|
||||
],
|
||||
),
|
||||
(
|
||||
"jump_tables",
|
||||
&[
|
||||
("bctr_pc", "BIGINT"),
|
||||
("function", "BIGINT"),
|
||||
("table_address", "BIGINT"),
|
||||
("entry_count", "BIGINT"),
|
||||
("table_slots", "BIGINT"),
|
||||
("index_map_address", "BIGINT"),
|
||||
("index_map_count", "BIGINT"),
|
||||
("case_bound", "BIGINT"),
|
||||
("kind", "VARCHAR"),
|
||||
],
|
||||
),
|
||||
(
|
||||
"jump_table_entries",
|
||||
&[
|
||||
("bctr_pc", "BIGINT"),
|
||||
("case_index", "BIGINT"),
|
||||
("target_address", "BIGINT"),
|
||||
],
|
||||
),
|
||||
(
|
||||
"data_in_code",
|
||||
&[
|
||||
("address", "BIGINT"),
|
||||
("length", "BIGINT"),
|
||||
("kind", "VARCHAR"),
|
||||
],
|
||||
),
|
||||
(
|
||||
"rtti_type_descriptors",
|
||||
&[
|
||||
("address", "BIGINT"),
|
||||
("mangled_name", "VARCHAR"),
|
||||
("demangled_name", "VARCHAR"),
|
||||
],
|
||||
),
|
||||
(
|
||||
"rtti_locators",
|
||||
&[
|
||||
("address", "BIGINT"),
|
||||
("subobject_offset", "BIGINT"),
|
||||
("cd_offset", "BIGINT"),
|
||||
("type_descriptor", "BIGINT"),
|
||||
("class_hierarchy", "BIGINT"),
|
||||
("vtable_address", "BIGINT"),
|
||||
],
|
||||
),
|
||||
(
|
||||
"rtti_base_classes",
|
||||
&[
|
||||
("class_hierarchy", "BIGINT"),
|
||||
("base_index", "BIGINT"),
|
||||
("type_descriptor", "BIGINT"),
|
||||
("name", "VARCHAR"),
|
||||
("num_contained_bases", "BIGINT"),
|
||||
("mdisp", "BIGINT"),
|
||||
("pdisp", "BIGINT"),
|
||||
("vdisp", "BIGINT"),
|
||||
("attributes", "BIGINT"),
|
||||
],
|
||||
),
|
||||
(
|
||||
"pdata_entries",
|
||||
&[
|
||||
("begin_address", "BIGINT"),
|
||||
("end_address", "BIGINT"),
|
||||
("function_length", "BIGINT"),
|
||||
("prolog_length", "BIGINT"),
|
||||
("flags", "BIGINT"),
|
||||
],
|
||||
),
|
||||
(
|
||||
"labels",
|
||||
&[
|
||||
("address", "BIGINT"),
|
||||
("name", "VARCHAR"),
|
||||
("kind", "VARCHAR"),
|
||||
],
|
||||
),
|
||||
(
|
||||
"xdbf_entries",
|
||||
&[
|
||||
("namespace", "BIGINT"),
|
||||
("namespace_name", "VARCHAR"),
|
||||
("id", "BIGINT"),
|
||||
("body_offset", "BIGINT"),
|
||||
("size", "BIGINT"),
|
||||
("magic", "VARCHAR"),
|
||||
],
|
||||
),
|
||||
(
|
||||
"xdbf_achievements",
|
||||
&[
|
||||
("id", "BIGINT"),
|
||||
("name", "VARCHAR"),
|
||||
("unlocked_desc", "VARCHAR"),
|
||||
("locked_desc", "VARCHAR"),
|
||||
("label_id", "BIGINT"),
|
||||
("description_id", "BIGINT"),
|
||||
("unachieved_id", "BIGINT"),
|
||||
("image_id", "BIGINT"),
|
||||
("gamerscore", "BIGINT"),
|
||||
("flags", "BIGINT"),
|
||||
],
|
||||
),
|
||||
(
|
||||
"xdbf_strings",
|
||||
&[
|
||||
("language", "BIGINT"),
|
||||
("language_name", "VARCHAR"),
|
||||
("string_id", "BIGINT"),
|
||||
("value", "VARCHAR"),
|
||||
],
|
||||
),
|
||||
(
|
||||
"xdbf_images",
|
||||
&[
|
||||
("id", "BIGINT"),
|
||||
("is_title_icon", "BOOLEAN"),
|
||||
("body_offset", "BIGINT"),
|
||||
("size", "BIGINT"),
|
||||
("format", "VARCHAR"),
|
||||
],
|
||||
),
|
||||
(
|
||||
"demangled_names",
|
||||
&[
|
||||
("address", "BIGINT"),
|
||||
("mangled", "VARCHAR"),
|
||||
("raw_demangled", "VARCHAR"),
|
||||
("namespace_path", "VARCHAR"),
|
||||
("class_name", "VARCHAR"),
|
||||
("method_name", "VARCHAR"),
|
||||
("params_signature", "VARCHAR"),
|
||||
],
|
||||
),
|
||||
(
|
||||
"vtables",
|
||||
&[
|
||||
("address", "BIGINT"),
|
||||
("length", "BIGINT"),
|
||||
("col_address", "BIGINT"),
|
||||
("class_name", "VARCHAR"),
|
||||
("rtti_present", "BOOLEAN"),
|
||||
("base_classes_json", "VARCHAR"),
|
||||
],
|
||||
),
|
||||
(
|
||||
"methods",
|
||||
&[
|
||||
("vtable_address", "BIGINT"),
|
||||
("slot", "BIGINT"),
|
||||
("function_address", "BIGINT"),
|
||||
("mangled_name", "VARCHAR"),
|
||||
("demangled_name", "VARCHAR"),
|
||||
],
|
||||
),
|
||||
(
|
||||
"classes",
|
||||
&[
|
||||
("name", "VARCHAR"),
|
||||
("vtable_address", "BIGINT"),
|
||||
("rtti_present", "BOOLEAN"),
|
||||
("base_classes_json", "VARCHAR"),
|
||||
],
|
||||
),
|
||||
(
|
||||
"strings",
|
||||
&[
|
||||
("address", "BIGINT"),
|
||||
("encoding", "VARCHAR"),
|
||||
("length", "BIGINT"),
|
||||
("content", "VARCHAR"),
|
||||
("section", "VARCHAR"),
|
||||
],
|
||||
),
|
||||
(
|
||||
"tls_info",
|
||||
&[
|
||||
("raw_data_start", "BIGINT"),
|
||||
("raw_data_end", "BIGINT"),
|
||||
("index_address", "BIGINT"),
|
||||
("callback_array", "BIGINT"),
|
||||
("zero_fill_size", "BIGINT"),
|
||||
("characteristics", "BIGINT"),
|
||||
],
|
||||
),
|
||||
(
|
||||
"tls_callbacks",
|
||||
&[("slot", "BIGINT"), ("address", "BIGINT")],
|
||||
),
|
||||
(
|
||||
"function_pointer_arrays",
|
||||
&[
|
||||
("address", "BIGINT"),
|
||||
("length", "BIGINT"),
|
||||
("kind", "VARCHAR"),
|
||||
],
|
||||
),
|
||||
(
|
||||
"function_pointer_array_entries",
|
||||
&[
|
||||
("array_address", "BIGINT"),
|
||||
("slot", "BIGINT"),
|
||||
("function_address", "BIGINT"),
|
||||
],
|
||||
),
|
||||
(
|
||||
"indirect_dispatch_sites",
|
||||
&[
|
||||
("dispatch_pc", "BIGINT"),
|
||||
("vptr_offset", "BIGINT"),
|
||||
("slot", "BIGINT"),
|
||||
("candidate_count", "BIGINT"),
|
||||
("truncated", "BOOLEAN"),
|
||||
],
|
||||
),
|
||||
(
|
||||
"indirect_dispatch_candidates",
|
||||
&[
|
||||
("dispatch_pc", "BIGINT"),
|
||||
("vtable_address", "BIGINT"),
|
||||
("method_address", "BIGINT"),
|
||||
],
|
||||
),
|
||||
(
|
||||
"vptr_writes",
|
||||
&[
|
||||
("writer_pc", "BIGINT"),
|
||||
("vtable_address", "BIGINT"),
|
||||
("vptr_offset", "BIGINT"),
|
||||
("writer_function", "BIGINT"),
|
||||
],
|
||||
),
|
||||
(
|
||||
"eh_funcinfo",
|
||||
&[
|
||||
("address", "BIGINT"),
|
||||
("magic", "BIGINT"),
|
||||
("max_state", "BIGINT"),
|
||||
("p_unwind_map", "BIGINT"),
|
||||
("n_try_blocks", "BIGINT"),
|
||||
("p_try_block_map", "BIGINT"),
|
||||
("n_ip_map_entries", "BIGINT"),
|
||||
("p_ip_to_state_map", "BIGINT"),
|
||||
("p_es_type_list", "BIGINT"),
|
||||
("eh_flags", "BIGINT"),
|
||||
],
|
||||
),
|
||||
(
|
||||
"eh_unwind_map",
|
||||
&[
|
||||
("funcinfo_address", "BIGINT"),
|
||||
("state_index", "BIGINT"),
|
||||
("to_state", "BIGINT"),
|
||||
("action_pc", "BIGINT"),
|
||||
],
|
||||
),
|
||||
(
|
||||
"eh_try_blocks",
|
||||
&[
|
||||
("funcinfo_address", "BIGINT"),
|
||||
("try_index", "BIGINT"),
|
||||
("try_low", "BIGINT"),
|
||||
("try_high", "BIGINT"),
|
||||
("catch_high", "BIGINT"),
|
||||
("n_catches", "BIGINT"),
|
||||
("p_handler_array", "BIGINT"),
|
||||
],
|
||||
),
|
||||
(
|
||||
"xrefs",
|
||||
&[
|
||||
("source", "BIGINT"),
|
||||
("target", "BIGINT"),
|
||||
("kind", "VARCHAR"),
|
||||
("addr_mode", "VARCHAR"),
|
||||
("instruction", "VARCHAR"),
|
||||
("source_func", "BIGINT"),
|
||||
("source_label", "VARCHAR"),
|
||||
("target_label", "VARCHAR"),
|
||||
],
|
||||
),
|
||||
];
|
||||
|
||||
let mut errs: Vec<String> = Vec::new();
|
||||
@@ -397,7 +502,8 @@ fn db_schema_matches_expected_columns() {
|
||||
"{table}: column count mismatch (got {}, expected {})",
|
||||
rows.len(),
|
||||
cols.len()
|
||||
).ok();
|
||||
)
|
||||
.ok();
|
||||
errs.push(format!("{table}: count {} vs {}", rows.len(), cols.len()));
|
||||
}
|
||||
for (i, (got, expected_col)) in rows.iter().zip(cols.iter()).enumerate() {
|
||||
@@ -410,19 +516,31 @@ fn db_schema_matches_expected_columns() {
|
||||
}
|
||||
}
|
||||
|
||||
assert!(errs.is_empty(), "schema drift detected:\n {}", errs.join("\n "));
|
||||
assert!(
|
||||
errs.is_empty(),
|
||||
"schema drift detected:\n {}",
|
||||
errs.join("\n ")
|
||||
);
|
||||
|
||||
// Verify row counts in the populated tables.
|
||||
let n_instr: i64 = conn
|
||||
.query_row("SELECT COUNT(*) FROM instructions", [], |r| r.get(0))
|
||||
.unwrap();
|
||||
assert_eq!(n_instr, 4, "expected 4 instruction rows from the synthetic PE");
|
||||
assert_eq!(
|
||||
n_instr, 4,
|
||||
"expected 4 instruction rows from the synthetic PE"
|
||||
);
|
||||
|
||||
// The synthetic mflr should produce target_hex = NULL, blr likewise (indirect).
|
||||
let n_with_target: i64 = conn
|
||||
.query_row("SELECT COUNT(target_hex) FROM instructions", [], |r| r.get(0))
|
||||
.query_row("SELECT COUNT(target_hex) FROM instructions", [], |r| {
|
||||
r.get(0)
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(n_with_target, 0, "indirect-only fixture should have no direct branch targets");
|
||||
assert_eq!(
|
||||
n_with_target, 0,
|
||||
"indirect-only fixture should have no direct branch targets"
|
||||
);
|
||||
|
||||
// SQL views must be queryable. The `_` in SQL LIKE is a single-char
|
||||
// wildcard, so we list the names explicitly rather than `LIKE 'v_%'`
|
||||
|
||||
@@ -41,7 +41,10 @@ fn cpu_fixture(name: &str) -> PathBuf {
|
||||
}
|
||||
|
||||
fn parse_hex(s: &str) -> u32 {
|
||||
let trimmed = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")).unwrap_or(s);
|
||||
let trimmed = s
|
||||
.strip_prefix("0x")
|
||||
.or_else(|| s.strip_prefix("0X"))
|
||||
.unwrap_or(s);
|
||||
u32::from_str_radix(trimmed, 16).expect("hex u32")
|
||||
}
|
||||
|
||||
@@ -61,8 +64,7 @@ fn check_fixture(fixture_name: &str) {
|
||||
let raw = parse_hex(&row.raw);
|
||||
let addr = parse_hex(&row.addr);
|
||||
|
||||
let canonical =
|
||||
sylpheed_ppc::disasm::format(&sylpheed_ppc::decode(raw, addr));
|
||||
let canonical = sylpheed_ppc::disasm::format(&sylpheed_ppc::decode(raw, addr));
|
||||
let shim = sylpheed_xexdb::ppc::disasm(raw, addr);
|
||||
|
||||
assert_eq!(
|
||||
@@ -78,13 +80,33 @@ fn check_fixture(fixture_name: &str) {
|
||||
|
||||
// Also pin against the fixture's structured fields — guards against
|
||||
// someone changing the cpu canon without regenerating the fixture.
|
||||
assert_eq!(canonical.mnemonic, row.mnemonic, "mnemonic drift: {}", row.label);
|
||||
assert_eq!(canonical.operands, row.operands, "operands drift: {}", row.label);
|
||||
assert_eq!(canonical.ext_mnemonic, row.ext_mnemonic, "ext_mnemonic drift: {}", row.label);
|
||||
assert_eq!(canonical.ext_operands, row.ext_operands, "ext_operands drift: {}", row.label);
|
||||
assert_eq!(
|
||||
canonical.mnemonic, row.mnemonic,
|
||||
"mnemonic drift: {}",
|
||||
row.label
|
||||
);
|
||||
assert_eq!(
|
||||
canonical.operands, row.operands,
|
||||
"operands drift: {}",
|
||||
row.label
|
||||
);
|
||||
assert_eq!(
|
||||
canonical.ext_mnemonic, row.ext_mnemonic,
|
||||
"ext_mnemonic drift: {}",
|
||||
row.label
|
||||
);
|
||||
assert_eq!(
|
||||
canonical.ext_operands, row.ext_operands,
|
||||
"ext_operands drift: {}",
|
||||
row.label
|
||||
);
|
||||
|
||||
let target_str = canonical.branch_target.map(|t| format!("0x{t:08X}"));
|
||||
assert_eq!(target_str, row.branch_target, "branch_target drift: {}", row.label);
|
||||
assert_eq!(
|
||||
target_str, row.branch_target,
|
||||
"branch_target drift: {}",
|
||||
row.label
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user