feat: add import resolution with Xenia-style thunk stubs (M8)

- resolve_imports() rewrites PE import records in-place:
  - Variable slots: 0xD000BEEF | (ordinal & 0xFFF) << 16
  - Thunk stubs: li r3, 0 / li r4, <ordinal> / mtspr CTR, r11 / bctr
- New -r/--resolve-imports flag on the extract command
- Without the flag, extraction is unchanged (byte-for-byte identical)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-03-29 17:35:57 +02:00
parent 956f5e8ba8
commit 2425e8177e
5 changed files with 256 additions and 5 deletions

View File

@@ -109,6 +109,118 @@ pub fn decode_import_records(pe_image: &[u8], xex: &Xex2File) -> Result<Vec<Reso
Ok(resolved)
}
/// Summary of import resolution results.
#[derive(Debug)]
pub struct ImportResolutionSummary {
/// Total number of import records processed.
pub total: usize,
/// Number of variable slots written.
pub variables_written: usize,
/// Number of thunk stubs written.
pub thunks_written: usize,
/// Per-library breakdown: (library_name, count).
pub per_library: Vec<(String, usize)>,
}
impl fmt::Display for ImportResolutionSummary {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"Resolved {} imports ({} variables, {} thunks)",
self.total, self.variables_written, self.thunks_written
)?;
for (lib, count) in &self.per_library {
write!(f, "\n {lib}: {count}")?;
}
Ok(())
}
}
/// Resolves imports in the PE image by writing Xenia-style values.
///
/// For variable imports (type 0): writes `0xD000BEEF | (ordinal & 0xFFF) << 16`.
/// For thunk imports (type 1): rewrites the first 8 bytes to valid PowerPC:
/// - word 0: `li r3, 0` (0x38600000)
/// - word 1: `li r4, <ordinal>` (0x38800000 | ordinal)
/// - words 2-3 are left unchanged (already mtspr CTR, r11 + bctr)
pub fn resolve_imports(
pe_image: &mut [u8],
xex: &Xex2File,
) -> Result<ImportResolutionSummary> {
let imports = xex
.optional_headers
.import_libraries
.as_ref()
.ok_or_else(|| Xex2Error::InvalidPeImage("no import libraries header".into()))?;
let load_address = xex.security_info.load_address;
let mut total = 0;
let mut variables_written = 0;
let mut thunks_written = 0;
let mut per_library: Vec<(String, usize)> = Vec::new();
for lib in &imports.libraries {
let mut lib_count = 0;
for &addr in &lib.import_addresses {
let pe_offset = (addr.wrapping_sub(load_address)) as usize;
let raw = read_u32_be(pe_image, pe_offset).map_err(|_| {
Xex2Error::InvalidPeImage(format!(
"import address 0x{addr:08X} (offset 0x{pe_offset:08X}) out of bounds"
))
})?;
let record_type_byte = (raw >> 24) & 0xFF;
let ordinal = (raw & 0xFFFF) as u16;
match record_type_byte {
0x00 => {
// Variable: write 0xD000BEEF | (ordinal & 0xFFF) << 16
let resolved_val: u32 =
0xD000BEEF | ((ordinal as u32 & 0xFFF) << 16);
pe_image[pe_offset..pe_offset + 4]
.copy_from_slice(&resolved_val.to_be_bytes());
variables_written += 1;
}
0x01 => {
// Thunk: rewrite first 8 bytes to valid PPC
if pe_offset + 16 > pe_image.len() {
return Err(Xex2Error::InvalidPeImage(format!(
"thunk at 0x{addr:08X} (offset 0x{pe_offset:08X}) extends past PE image"
)));
}
// li r3, 0
let word0: u32 = 0x38600000;
// li r4, <ordinal>
let word1: u32 = 0x38800000 | ordinal as u32;
pe_image[pe_offset..pe_offset + 4]
.copy_from_slice(&word0.to_be_bytes());
pe_image[pe_offset + 4..pe_offset + 8]
.copy_from_slice(&word1.to_be_bytes());
// words 2-3 (mtspr CTR, r11 + bctr) left unchanged
thunks_written += 1;
}
_ => {
// Unknown record type — skip
}
}
lib_count += 1;
total += 1;
}
per_library.push((lib.name.clone(), lib_count));
}
Ok(ImportResolutionSummary {
total,
variables_written,
thunks_written,
per_library,
})
}
#[cfg(test)]
mod tests {
use super::*;
@@ -118,4 +230,19 @@ mod tests {
assert_eq!(ImportRecordType::Variable.to_string(), "variable");
assert_eq!(ImportRecordType::Thunk.to_string(), "thunk");
}
#[test]
fn test_resolution_summary_display() {
let summary = ImportResolutionSummary {
total: 10,
variables_written: 5,
thunks_written: 5,
per_library: vec![("xboxkrnl.exe".into(), 10)],
};
let s = summary.to_string();
assert!(s.contains("10 imports"));
assert!(s.contains("5 variables"));
assert!(s.contains("5 thunks"));
assert!(s.contains("xboxkrnl.exe: 10"));
}
}