Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 43 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,30 +1,59 @@
# X360 XEX Loader for Ghidra by Warranty Voider

this is a loader module for ghidra for XBox360 XEX files
This is a Ghidra loader extension for Xbox 360 XEX files.

- supports PDB/XDB files
- In loader import page, click Advanced.
- Tick `Load PDB File` + `Use experimental PDB loader` and untick `Process .pdata`
- Select `MSDIA` parser
- supports XEXP delta patches
## SaveEditors Fork Updates

requires min. JDK 17
This fork carries maintained fixes and publishes ready-to-install release zips.

- SaveEditors fork release: `13.0.0` (`Bug fixes`)
- Fixed PDB enum imports so enums use their actual underlying storage size instead of always importing as 8-byte enums.
- Fixed PDB root stream page counting so PDBs with sub-page root directories parse correctly.
- Fixed CodeView `LF_ARRAY` imports so byte lengths are converted into the correct element counts.
- Fixed `.pdata` handling so imported entries become real Ghidra functions instead of label-only symbols.
- Release zips for the fork are published on the [Releases](https://github.com/SaveEditors/XEXLoaderWV/releases) page.
- The upstream patch was submitted as [zeroKilo/XEXLoaderWV#33](https://github.com/zeroKilo/XEXLoaderWV/pull/33).

## Features

- Supports PDB/XDB files.
- In the loader import page, click Advanced.
- Tick `Load PDB File` and `Use experimental PDB loader`, then untick `Process .pdata`.
- Select `MSDIA` parser.
- Supports XEXP delta patches.

Requires the minimum Java version required by your Ghidra install.

[![Alt text](https://img.youtube.com/vi/coGz0f7hHTM/0.jpg)](https://www.youtube.com/watch?v=coGz0f7hHTM)

<!-- this video is outdated -->
<!-- [![Alt text](https://img.youtube.com/vi/dBoofGgraKM/0.jpg)](https://www.youtube.com/watch?v=dBoofGgraKM) -->

## Build problem with gradle wrapper
## Build

This extension is built using the Gradle support files that ship with Ghidra. The required Java and Gradle versions come from:

```text
<GHIDRA_INSTALL_DIR>\Ghidra\application.properties
```

Do not edit those values in Ghidra just to build this project. Instead, use a JDK and Gradle version that satisfy your installed Ghidra release.

EDIT:2025.04.05
For Ghidra `12.0.4`, the current minimums are:

it seems you have to update
```text
application.java.min=21
application.gradle.min=8.5
```

```(Ghidra Install Dir)\Ghidra\application.properties```
This fork was validated against Ghidra `12.0.4`, JDK `21`, and Gradle `9.3.1`.

and upgrade the gradle version like this
Example:

```application.gradle.min=8.10```
```powershell
$env:GHIDRA_INSTALL_DIR='A:\Tools\ghidra_12.0.4_PUBLIC'
$env:JAVA_HOME='A:\Tools\jdk-21'
gradle buildExtension
```

if you have problems with building from source in eclipse with the gradle wrapper.
If you are building from Eclipse or another IDE, make sure it uses a compatible JDK and Gradle version for the Ghidra version you are targeting.
8 changes: 4 additions & 4 deletions XEXLoaderWV/extension.properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name=@extname@
description=The extension description can be customized by editing the extension.properties file.
author=
createdOn=
version=@extversion@
description=Xbox 360 XEX loader for Ghidra with XEX, XEXP, PDB, and XDB support.
author=Warranty Voider; SaveEditors fork maintenance
createdOn=2026-03-25
version=13.0.0
101 changes: 101 additions & 0 deletions XEXLoaderWV/ghidra_scripts/AuditPdbArrayImport.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// Audits LF_ARRAY import outcomes so skipped arrays can be classified by reason.

import ghidra.app.script.GhidraScript;
import ghidra.program.model.data.DataType;
import xexloaderwv.PDBFile;
import xexloaderwv.TPIStream;
import xexloaderwv.TypeRecord;
import xexloaderwv.TypeRecord.LeafRecordKind;

public class AuditPdbArrayImport extends GhidraScript {

@Override
protected void run() throws Exception {
String[] args = getScriptArgs();
if (args.length < 1) {
throw new IllegalArgumentException("Usage: AuditPdbArrayImport.java <path-to-pdb>");
}

String pdbPath = args[0];
printf("Loading PDB for array audit: %s%n", pdbPath);

PDBFile pdb = new PDBFile(pdbPath, monitor, currentProgram);
pdb.tpi.ImportTypeRecords(currentProgram, monitor);

int imported = 0;
int skippedZeroLength = 0;
int skippedMissingElementType = 0;
int skippedInvalidElementLength = 0;
int skippedNonDivisible = 0;
int skippedUnexpected = 0;
StringBuilder details = new StringBuilder();

for (TypeRecord rec : pdb.tpi.typeRecords) {
if (rec.kind != LeafRecordKind.LF_ARRAY || !(rec.record instanceof TypeRecord.LR_Array)) {
continue;
}

TypeRecord.LR_Array arr = (TypeRecord.LR_Array) rec.record;
if (arr.dataType != null) {
imported++;
continue;
}

long expectedLength = arr.val.val_long;
DataType elementType = null;
try {
elementType = pdb.tpi.GetDataTypeByIndex(arr.elemtype);
}
catch (Exception ex) {
elementType = null;
}

String reason;
if (expectedLength == 0) {
skippedZeroLength++;
reason = "zero-length";
}
else if (elementType == null) {
skippedMissingElementType++;
reason = "missing-element-type";
}
else if (elementType.getLength() <= 0) {
skippedInvalidElementLength++;
reason = "invalid-element-length";
}
else if ((expectedLength % elementType.getLength()) != 0) {
skippedNonDivisible++;
reason = "non-divisible";
}
else {
skippedUnexpected++;
reason = "unexpected";
}

if (details.length() < 6000) {
String typeName = pdb.tpi.GetDataTypeNameByIndex(arr.elemtype);
String elementName = typeName == null ? "<null>" : typeName;
String elementLength = elementType == null ? "<null>" : Integer.toString(elementType.getLength());
details.append(String.format(
"typeID=0x%X name=%s expectedBytes=%d elemType=0x%X elemName=%s elemLen=%s reason=%s%n",
rec.typeID, arr.name, expectedLength, arr.elemtype, elementName, elementLength, reason));
}
}

printf(
"Array audit summary: imported=%d skippedZeroLength=%d skippedMissingElementType=%d skippedInvalidElementLength=%d skippedNonDivisible=%d skippedUnexpected=%d%n",
imported,
skippedZeroLength,
skippedMissingElementType,
skippedInvalidElementLength,
skippedNonDivisible,
skippedUnexpected);
if (details.length() > 0) {
printf("%s", details.toString());
}

if (skippedUnexpected > 0) {
throw new RuntimeException("Unexpected skipped arrays were found.");
}
}
}
60 changes: 60 additions & 0 deletions XEXLoaderWV/ghidra_scripts/ValidatePdbArrayLengths.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Validates that imported LF_ARRAY datatypes use the byte length encoded in the PDB.

import ghidra.app.script.GhidraScript;
import xexloaderwv.PDBFile;
import xexloaderwv.TypeRecord;
import xexloaderwv.TypeRecord.LeafRecordKind;

public class ValidatePdbArrayLengths extends GhidraScript {

@Override
protected void run() throws Exception {
String[] args = getScriptArgs();
if (args.length < 1) {
throw new IllegalArgumentException("Usage: ValidatePdbArrayLengths.java <path-to-pdb>");
}

String pdbPath = args[0];
printf("Loading PDB for array validation: %s%n", pdbPath);

PDBFile pdb = new PDBFile(pdbPath, monitor, currentProgram);
pdb.tpi.ImportTypeRecords(currentProgram, monitor);

int checked = 0;
int skipped = 0;
int mismatches = 0;
StringBuilder details = new StringBuilder();

for (TypeRecord rec : pdb.tpi.typeRecords) {
if (rec.kind != LeafRecordKind.LF_ARRAY || !(rec.record instanceof TypeRecord.LR_Array)) {
continue;
}

TypeRecord.LR_Array arr = (TypeRecord.LR_Array) rec.record;
if (arr.dataType == null) {
skipped++;
continue;
}

long expectedLength = arr.val.val_long;
int actualLength = arr.dataType.getLength();
checked++;

if (actualLength != expectedLength) {
mismatches++;
if (details.length() < 4000) {
details.append(String.format(
"typeID=0x%X name=%s expectedBytes=%d actualBytes=%d elemType=0x%X%n",
rec.typeID, arr.name, expectedLength, actualLength, arr.elemtype));
}
}
}

printf("Array validation summary: checked=%d skipped=%d mismatches=%d%n",
checked, skipped, mismatches);

if (mismatches > 0) {
throw new RuntimeException("Found array length mismatches:\n" + details);
}
}
}
51 changes: 51 additions & 0 deletions XEXLoaderWV/src/main/java/xexloaderwv/CodeViewTypeInfo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package xexloaderwv;

final class CodeViewTypeInfo {

private CodeViewTypeInfo() {
}

static int getPrimitiveStorageSize(long typeIndex) {
switch ((int) typeIndex) {
case 0x0010: // T_CHAR
case 0x0020: // T_UCHAR
case 0x0068: // T_INT1
case 0x0069: // T_UINT1
case 0x0070: // T_RCHAR
case 0x0030: // T_BOOL08
return 1;
case 0x0011: // T_SHORT
case 0x0021: // T_USHORT
case 0x0071: // T_WCHAR
case 0x0072: // T_INT2
case 0x0073: // T_UINT2
case 0x0031: // T_BOOL16
case 0x007a: // T_CHAR16
return 2;
case 0x0008: // T_HRESULT
case 0x0012: // T_LONG
case 0x0022: // T_ULONG
case 0x0040: // T_REAL32
case 0x0074: // T_INT4
case 0x0075: // T_UINT4
case 0x0032: // T_BOOL32
case 0x007b: // T_CHAR32
return 4;
case 0x0013: // T_QUAD
case 0x0023: // T_UQUAD
case 0x0041: // T_REAL64
case 0x0076: // T_INT8
case 0x0077: // T_UINT8
case 0x0033: // T_BOOL64
return 8;
case 0x0014: // T_OCT
case 0x0024: // T_UOCT
case 0x0043: // T_REAL128
case 0x0078: // T_INT16
case 0x0079: // T_UINT16
return 16;
default:
return -1;
}
}
}
18 changes: 12 additions & 6 deletions XEXLoaderWV/src/main/java/xexloaderwv/PDBFile.java
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,7 @@ public PDBFile(String path, TaskMonitor monitor, Program program) throws Excepti
int pos;
pos = pAdIndexPages * dPageBytes;
ArrayList<Integer> pages = new ArrayList<Integer>();
int count = dRootBytes / dPageBytes;
if ((dRootBytes / dPageBytes) != 0)
count++;
int count = GetPageCount(dRootBytes);
for(int i = 0; i < count; i++)
{
int v = b.readInt(pos);
Expand Down Expand Up @@ -150,9 +148,7 @@ private void ReadRootStreams(byte[] data) throws Exception
try
{
RootStream rs = rootStreams.get(i);
int subcount = rs.size / dPageBytes;
if ((rs.size % dPageBytes) != 0)
subcount++;
int subcount = GetPageCount(rs.size);
rs.pages = new int[subcount];
for(int j = 0; j < subcount; j++)
{
Expand All @@ -164,4 +160,14 @@ private void ReadRootStreams(byte[] data) throws Exception
catch(Exception e) {}
}
}

private int GetPageCount(int byteCount)
{
if(byteCount <= 0)
return 0;
int count = byteCount / dPageBytes;
if((byteCount % dPageBytes) != 0)
count++;
return count;
}
}
Loading