Scaffold xex2tractor Rust project with cargo, add MIT license, README, and XEX2 file format documentation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
38 KiB
XEX2 File Format Documentation
This document describes the XEX2 (Xbox 360 Executable) file format as implemented in the Xenia emulator. All multi-byte values in the XEX2 file are big-endian unless otherwise noted. The contained PE image uses big-endian values as well (PowerPC BE target).
Terminology:
- XEX offset: byte offset from the start of the
.xexfile on disk - PE offset: byte offset from the start of the decompressed/decrypted PE image (which begins at
xex2_header.header_sizein the raw file, but after decryption/decompression is loaded at the base address) - Memory address: Xbox 360 virtual address (typically starting at the
load_addressfrom security info, e.g.0x82000000)
1. Top-Level XEX2 File Layout
+==================================+ XEX offset 0x00
| xex2_header |
| (magic, flags, header_size, |
| security_offset, opt headers) |
+----------------------------------+ XEX offset 0x18
| Optional Headers Array |
| (header_count entries of |
| xex2_opt_header, 8 bytes each) |
+----------------------------------+ XEX offset = security_offset
| xex2_security_info |
| (RSA sig, AES key, pages, ...) |
+----------------------------------+ XEX offset varies
| Optional Header Data |
| (pointed to by opt headers) |
+==================================+ XEX offset = header_size
| Encrypted/Compressed PE Image |
| (the actual executable payload) |
+==================================+ XEX offset = end of file
2. Main XEX2 Header (xex2_header)
Located at XEX offset 0x00.
| Offset | Size | Field | Description |
|---|---|---|---|
| 0x00 | 4 | magic |
Magic bytes: XEX2 (0x58455832) |
| 0x04 | 4 | module_flags |
Bitfield of xex2_module_flags (see below) |
| 0x08 | 4 | header_size |
Total size of all headers in bytes. The PE image data starts at this XEX offset. |
| 0x0C | 4 | reserved |
Reserved (typically 0) |
| 0x10 | 4 | security_offset |
XEX offset to the xex2_security_info structure (from start of file) |
| 0x14 | 4 | header_count |
Number of optional header entries following |
| 0x18 | 8 * N | headers[N] |
Array of xex2_opt_header entries |
Module Flags (xex2_module_flags, bitmask)
| Value | Name | Description |
|---|---|---|
| 0x00000001 | XEX_MODULE_TITLE |
Main game/app executable |
| 0x00000002 | XEX_MODULE_EXPORTS_TO_TITLE |
Module exports functions to titles |
| 0x00000004 | XEX_MODULE_SYSTEM_DEBUGGER |
System debugger module |
| 0x00000008 | XEX_MODULE_DLL_MODULE |
DLL module |
| 0x00000010 | XEX_MODULE_MODULE_PATCH |
Module patch |
| 0x00000020 | XEX_MODULE_PATCH_FULL |
Full patch (replaces entire module) |
| 0x00000040 | XEX_MODULE_PATCH_DELTA |
Delta patch (applies diffs) |
| 0x00000080 | XEX_MODULE_USER_MODE |
User-mode module |
3. Optional Header Entry (xex2_opt_header)
Each entry is 8 bytes, located starting at XEX offset 0x18.
| Offset | Size | Field | Description |
|---|---|---|---|
| 0x00 | 4 | key |
Header key identifier (xex2_header_keys enum) |
| 0x04 | 4 | value / offset |
Interpretation depends on low byte of key |
How the value/offset field is interpreted
The low byte (key & 0xFF) determines the meaning:
| Low byte | Meaning |
|---|---|
0x00 |
The 4-byte value field is the data itself (inline uint32_t). |
0x01 |
The value field is the data itself (stored in-place, pointer to the 4-byte field within the header). |
| Any other | offset is an XEX offset (from start of file) pointing to the actual header data structure. |
Optional Header Keys (xex2_header_keys)
| Key Value | Name | Data Size/Type | Description |
|---|---|---|---|
| 0x000002FF | XEX_HEADER_RESOURCE_INFO |
Variable | Embedded resource descriptors |
| 0x000003FF | XEX_HEADER_FILE_FORMAT_INFO |
Variable | Encryption + compression info |
| 0x000005FF | XEX_HEADER_DELTA_PATCH_DESCRIPTOR |
Variable | Delta patch descriptor |
| 0x00000405 | XEX_HEADER_BASE_REFERENCE |
Variable | Base reference for patches |
| 0x00004304 | XEX_HEADER_DISC_PROFILE_ID |
4 bytes | Disc profile ID |
| 0x000080FF | XEX_HEADER_BOUNDING_PATH |
Variable string | Bounding path |
| 0x00008105 | XEX_HEADER_DEVICE_ID |
20 bytes | Device ID |
| 0x00010001 | XEX_HEADER_ORIGINAL_BASE_ADDRESS |
Inline u32 | Original PE base address |
| 0x00010100 | XEX_HEADER_ENTRY_POINT |
Inline u32 | Program entry point (memory address) |
| 0x00010201 | XEX_HEADER_IMAGE_BASE_ADDRESS |
Inline u32 | Load base address override |
| 0x000103FF | XEX_HEADER_IMPORT_LIBRARIES |
Variable | Import library table |
| 0x00018002 | XEX_HEADER_CHECKSUM_TIMESTAMP |
8 bytes | Checksum + timestamp |
| 0x00018102 | XEX_HEADER_ENABLED_FOR_CALLCAP |
8 bytes | Callcap thunk addresses |
| 0x00018200 | XEX_HEADER_ENABLED_FOR_FASTCAP |
Inline u32 | Fastcap enabled |
| 0x000183FF | XEX_HEADER_ORIGINAL_PE_NAME |
Variable string | Original PE file name |
| 0x000200FF | XEX_HEADER_STATIC_LIBRARIES |
Variable | Linked static library info |
| 0x00020104 | XEX_HEADER_TLS_INFO |
16 bytes | Thread-Local Storage info |
| 0x00020200 | XEX_HEADER_DEFAULT_STACK_SIZE |
Inline u32 | Default stack size |
| 0x00020301 | XEX_HEADER_DEFAULT_FILESYSTEM_CACHE_SIZE |
Inline u32 | FS cache size |
| 0x00020401 | XEX_HEADER_DEFAULT_HEAP_SIZE |
Inline u32 | Default heap size |
| 0x00028002 | XEX_HEADER_PAGE_HEAP_SIZE_AND_FLAGS |
8 bytes | Page heap config |
| 0x00030000 | XEX_HEADER_SYSTEM_FLAGS |
Inline u32 | System privilege flags |
| 0x00030100 | XEX_HEADER_SYSTEM_FLAGS_32 |
Inline u32 | Extended system flags (Kinect, etc.) |
| 0x00030200 | XEX_HEADER_SYSTEM_FLAGS_64 |
Inline u32 | 64-bit privilege flags |
| 0x00040006 | XEX_HEADER_EXECUTION_INFO |
24 bytes | Title ID, media ID, disc info |
| 0x00040201 | XEX_HEADER_TITLE_WORKSPACE_SIZE |
Inline u32 | Title workspace size |
| 0x00040310 | XEX_HEADER_GAME_RATINGS |
64 bytes | Game content ratings |
| 0x00040404 | XEX_HEADER_LAN_KEY |
16 bytes | LAN encryption key |
| 0x000405FF | XEX_HEADER_XBOX360_LOGO |
Variable | Xbox 360 logo bitmap |
| 0x000406FF | XEX_HEADER_MULTIDISC_MEDIA_IDS |
Variable | Multi-disc media IDs |
| 0x000407FF | XEX_HEADER_ALTERNATE_TITLE_IDS |
Variable | Alternate title IDs |
| 0x00040801 | XEX_HEADER_ADDITIONAL_TITLE_MEMORY |
Inline u32 | Extra title memory |
| 0x00E10402 | XEX_HEADER_EXPORTS_BY_NAME |
8 bytes | PE export directory info |
4. Security Info (xex2_security_info)
Located at XEX offset = xex2_header.security_offset (from start of file).
| Offset | Size | Field | Description |
|---|---|---|---|
| 0x000 | 4 | header_size |
Size of this security info structure |
| 0x004 | 4 | image_size |
Size of the decompressed PE image |
| 0x008 | 256 (0x100) | rsa_signature |
RSA-2048 signature over the header |
| 0x108 | 4 | unk_108 |
Unknown (length field?) |
| 0x10C | 4 | image_flags |
xex2_image_flags bitmask |
| 0x110 | 4 | load_address |
Virtual memory address where the PE is loaded (e.g. 0x82000000) |
| 0x114 | 20 (0x14) | section_digest |
SHA-1 digest of section data |
| 0x128 | 4 | import_table_count |
Number of import table entries |
| 0x12C | 20 (0x14) | import_table_digest |
SHA-1 digest of import table |
| 0x140 | 16 (0x10) | xgd2_media_id |
XGD2 media identifier |
| 0x150 | 16 (0x10) | aes_key |
Encrypted AES-128 session key (see Encryption section) |
| 0x160 | 4 | export_table |
Memory address of the XEX export table (0 if none) |
| 0x164 | 20 (0x14) | header_digest |
SHA-1 digest of header |
| 0x178 | 4 | region |
Allowed regions (xex2_region_flags) |
| 0x17C | 4 | allowed_media_types |
Allowed media types (xex2_media_flags) |
| 0x180 | 4 | page_descriptor_count |
Number of page descriptors following |
| 0x184 | 24 * N | page_descriptors[N] |
Array of xex2_page_descriptor entries |
Image Flags (xex2_image_flags, bitmask)
| Value | Name |
|---|---|
| 0x00000002 | Manufacturing utility |
| 0x00000004 | Manufacturing support tools |
| 0x00000008 | XGD2 media only |
| 0x00000100 | Cardea key |
| 0x00000200 | Xeika key |
| 0x00000400 | Usermode title |
| 0x00000800 | Usermode system |
| 0x10000000 | 4KB page size (otherwise 64KB) |
| 0x20000000 | Region free |
| 0x40000000 | Revocation check optional |
| 0x80000000 | Revocation check required |
Region Flags (xex2_region_flags, bitmask)
| Value | Region |
|---|---|
| 0x000000FF | NTSC/U (North America) |
| 0x0000FF00 | NTSC/J (Japan + Asia) |
| 0x00000100 | NTSC/J - Japan |
| 0x00000200 | NTSC/J - China |
| 0x00FF0000 | PAL (Europe) |
| 0x00010000 | PAL - Australia/New Zealand |
| 0xFF000000 | Other regions |
| 0xFFFFFFFF | All regions (region-free) |
Media Flags (xex2_media_flags, bitmask)
| Value | Media Type |
|---|---|
| 0x00000001 | Hard disk |
| 0x00000002 | DVD X2 |
| 0x00000004 | DVD/CD |
| 0x00000008 | DVD-5 |
| 0x00000010 | DVD-9 |
| 0x00000020 | System flash |
| 0x00000080 | Memory unit |
| 0x00000100 | USB mass storage |
| 0x00000200 | Network |
| 0x00000400 | Direct from memory |
| 0x00000800 | RAM drive |
| 0x00001000 | SVOD |
| 0x01000000 | Insecure package |
| 0x02000000 | Savegame package |
| 0x04000000 | Locally signed package |
| 0x08000000 | LIVE signed package |
| 0x10000000 | Xbox package |
5. Page Descriptors (xex2_page_descriptor)
Each page descriptor is 24 bytes and immediately follows page_descriptor_count in the security info (starting at XEX offset = security_offset + 0x184).
+----------------------------------+
| Bits 31-28 | Bits 27-0 | 0x00 (4 bytes, combined bitfield)
| info (4b) | page_count (28b) |
+----------------------------------+
| data_digest (20 bytes, SHA-1) | 0x04
+----------------------------------+
| Offset | Size | Field | Description |
|---|---|---|---|
| 0x00 | 4 | value |
Combined bitfield (big-endian, must be byte-swapped before reading bits) |
.info (bits 31-28) |
Section type: 1=Code, 2=Data, 3=Read-only data | ||
.page_count (bits 27-0) |
Number of pages in this section | ||
| 0x04 | 20 | data_digest |
SHA-1 hash of the page data |
Page size is determined by XEX_IMAGE_PAGE_SIZE_4KB in image flags:
- If set: 4 KB (0x1000) pages
- If not set: 64 KB (0x10000) pages
Memory mapping: Pages are mapped sequentially starting at load_address. For page descriptor i, the memory address is:
address = load_address + (sum of all previous page_counts) * page_size
size = desc.page_count * page_size
Section types determine memory protection:
| Type | Value | Protection |
|---|---|---|
XEX_SECTION_CODE |
1 | Read-only (or Read+Write if writable_code_segments) |
XEX_SECTION_DATA |
2 | Read + Write |
XEX_SECTION_READONLY_DATA |
3 | Read-only |
6. Encryption
Overview
XEX2 uses a two-level AES-128-CBC encryption scheme:
- The session key (per-XEX) is stored encrypted in
xex2_security_info.aes_key(at security info offset 0x150). - This session key is itself encrypted with one of the well-known master keys.
- The session key is then used to decrypt the PE image payload.
Master AES-128 Keys
| Key | Value (hex) | Usage |
|---|---|---|
| XEX2 Retail | 20 B1 85 A5 9D 28 FD C3 40 58 3F BB 08 96 BF 91 |
Production/retail XEX2 files |
| XEX2 DevKit | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
Development kit XEX2 files (null key) |
| XEX1 Retail | A2 6C 10 F7 1F D9 35 E9 8B 99 92 2C E9 32 15 72 |
Legacy XEX1 format |
Key Derivation Process
1. Read encrypted_session_key from xex2_security_info.aes_key[0x10]
2. Decrypt encrypted_session_key using master_key with AES-128-CBC (IV = 0)
→ This yields the session_key[16]
3. Use session_key to decrypt the PE image payload with AES-128-CBC (IV = 0)
AES-128-CBC Decryption Algorithm
The decryption used is standard AES-128 in CBC mode with a zero IV (16 bytes of 0x00):
Input: session_key[16], ciphertext, length
State: IV[16] = {0, 0, ..., 0}
rk[] = rijndaelKeySetupDec(session_key, 128) // 128-bit key
For each 16-byte block:
plaintext_block = rijndaelDecrypt(rk, Nr, ciphertext_block)
plaintext_block ^= IV // XOR with previous ciphertext (or IV for first block)
IV = ciphertext_block // Update IV to current ciphertext
Implementation: Uses the Rijndael reference implementation (rijndael-alg-fst.c), with Nr rounds returned by rijndaelKeySetupDec() (10 rounds for AES-128).
Key Trial Order
The loader tries keys in this order, falling back on failure:
- XEX2 Retail key
- XEX2 DevKit key (all zeros)
- XEX1 Retail key
Success is determined by checking if the decrypted image begins with a valid PE signature (MZ / 0x5A4D).
Encryption Type (xex2_encryption_type)
Stored in xex2_opt_file_format_info.encryption_type:
| Value | Name | Description |
|---|---|---|
| 0 | XEX_ENCRYPTION_NONE |
PE image is not encrypted |
| 1 | XEX_ENCRYPTION_NORMAL |
PE image is AES-128-CBC encrypted |
7. Compression
Compression type is stored in xex2_opt_file_format_info.compression_type.
File Format Info (xex2_opt_file_format_info)
Pointed to by optional header key 0x000003FF (XEX_HEADER_FILE_FORMAT_INFO).
| Offset | Size | Field | Description |
|---|---|---|---|
| 0x00 | 4 | info_size |
Total size of this structure |
| 0x04 | 2 | encryption_type |
0=None, 1=Normal (AES-128-CBC) |
| 0x06 | 2 | compression_type |
0=None, 1=Basic, 2=Normal, 3=Delta |
| 0x08 | ... | compression_info |
Union: basic or normal compression info |
Compression Types
| Value | Name | Description |
|---|---|---|
| 0 | XEX_COMPRESSION_NONE |
No compression; raw PE image data |
| 1 | XEX_COMPRESSION_BASIC |
Block-based zero-fill compression |
| 2 | XEX_COMPRESSION_NORMAL |
LZX (Lempel-Ziv extended) compression with SHA-1 block chaining |
| 3 | XEX_COMPRESSION_DELTA |
Delta patch compression (for update patches) |
7a. No Compression (XEX_COMPRESSION_NONE)
The PE image starts at XEX offset = header_size and extends to end of file. The raw data length is xex_file_size - header_size. If encrypted, the entire payload is decrypted in-place with AES-128-CBC using the session key.
7b. Basic Compression (XEX_COMPRESSION_BASIC)
The compression info contains an array of block descriptors that describe alternating data and zero-filled regions.
Basic Compression Block (xex2_file_basic_compression_block)
Located at xex2_opt_file_format_info offset 0x08. The number of blocks is (info_size - 8) / 8.
| Offset | Size | Field | Description |
|---|---|---|---|
| 0x00 | 4 | data_size |
Bytes of real data to copy from the XEX payload |
| 0x04 | 4 | zero_size |
Bytes of zeros to append after the data |
Decompression process (after AES-128-CBC decryption if encryption_type == NORMAL):
source_ptr = XEX file + header_size // start of PE payload in XEX file
dest_ptr = base_address in memory // Xbox 360 virtual memory
For each block[i]:
Copy block[i].data_size bytes from source_ptr to dest_ptr
Advance source_ptr by data_size
Zero-fill block[i].zero_size bytes at dest_ptr + data_size
Advance dest_ptr by (data_size + zero_size)
The total uncompressed size = sum of all (data_size + zero_size) across all blocks.
Note on encryption with basic compression: When encryption is NORMAL, the AES-128-CBC decryption is performed inline per block — the CBC IV state carries across block boundaries (it is NOT reset per block). The same session_key and continuous CBC state are used.
7c. Normal Compression (XEX_COMPRESSION_NORMAL)
This is a two-stage process: de-blocking, then LZX decompression.
Normal Compression Info (xex2_file_normal_compression_info)
Located at xex2_opt_file_format_info offset 0x08:
| Offset | Size | Field | Description |
|---|---|---|---|
| 0x00 | 4 | window_size |
LZX decompression window size in bytes (must be power of 2) |
| 0x04 | 4 | first_block.block_size |
Size of the first compressed block in bytes |
| 0x08 | 20 | first_block.block_hash |
SHA-1 hash of the first block's data |
Compressed Block Info (xex2_compressed_block_info)
Each block in the compressed stream is described by a chained structure:
| Offset | Size | Field | Description |
|---|---|---|---|
| 0x00 | 4 | block_size |
Total size of this block in bytes (0 = end of chain) |
| 0x04 | 20 | block_hash |
SHA-1 hash of this block's data |
Block chaining: The block_size and block_hash of the next block are stored at the beginning of the current block's data. This creates a hash chain for integrity verification.
Decompression Process
1. DECRYPT (if encrypted):
Decrypt the entire PE payload (XEX file + header_size, length = file_size - header_size)
using AES-128-CBC with session_key and zero IV.
2. DE-BLOCK:
current_block_info = first_block from compression_info header
source_ptr = start of decrypted payload
dest_buffer = temporary buffer
While current_block_info.block_size != 0:
a. Verify SHA-1(source_ptr, current_block_info.block_size) == current_block_info.block_hash
b. Read next_block_info from source_ptr:
next_block_size = bytes [0..3] (4 bytes)
next_block_hash = bytes [4..23] (20 bytes)
c. Skip past block header (4 + 20 = 24 bytes)
d. Read data chunks:
While true:
chunk_size = read 2 bytes (big-endian uint16)
If chunk_size == 0: break (end of block)
Copy chunk_size bytes to dest_buffer
e. Advance source_ptr to: previous source_ptr + current_block_info.block_size
f. current_block_info = next_block_info
3. LZX DECOMPRESS:
Decompress dest_buffer using LZX algorithm:
- Input: de-blocked data
- Output size: image_size (from page descriptors sum)
- Window size: compression_info.normal.window_size
- Reset interval: 0 (no reset)
- Frame size: 0x8000 (32 KB)
Output is written to memory at base_address.
LZX Algorithm Details
- Algorithm: LZX (Lempel-Ziv Extended), the same algorithm used in Microsoft CAB files
- Implementation: mspack library (
lzxd.c) - Window size: Specified per-XEX in
window_sizefield (typically a power of 2; common values include 0x20000 = 128KB) - Window bits:
log2(window_size)— computed via bit scan - Frame size: Fixed at
0x8000(32,768 bytes) - Reset interval: 0 (no periodic state reset)
7d. Delta Compression / Patching (XEX_COMPRESSION_DELTA)
Used for XEX patches (XEXP files). The patch XEX has XEX_MODULE_PATCH_DELTA set in module_flags.
Delta Patch Descriptor (xex2_opt_delta_patch_descriptor)
Pointed to by optional header key 0x000005FF:
| Offset | Size | Field | Description |
|---|---|---|---|
| 0x00 | 4 | size |
Size of the header patch data |
| 0x04 | 4 | target_version_value |
Target version after patch (xex2_version bitfield) |
| 0x08 | 4 | source_version_value |
Source version required (xex2_version bitfield) |
| 0x0C | 20 | digest_source |
SHA-1 digest of source image |
| 0x20 | 16 | image_key_source |
Key verification data |
| 0x30 | 4 | size_of_target_headers |
Size of target XEX headers after patch |
| 0x34 | 4 | delta_headers_source_offset |
Offset within source XEX headers to copy from |
| 0x38 | 4 | delta_headers_source_size |
Size of source header data to copy |
| 0x3C | 4 | delta_headers_target_offset |
Offset within target XEX headers to copy to |
| 0x40 | 4 | delta_image_source_offset |
Offset within source PE image to copy from |
| 0x44 | 4 | delta_image_source_size |
Size of source image data to copy |
| 0x48 | 4 | delta_image_target_offset |
Offset within target PE image to copy to |
| 0x4C | ... | info |
First xex2_delta_patch entry (inline) |
Delta Patch Entry (xex2_delta_patch)
| Offset | Size | Field | Description |
|---|---|---|---|
| 0x00 | 4 | old_addr |
Offset in the existing memory image to read source data from |
| 0x04 | 4 | new_addr |
Offset in the existing memory image to write patched data to |
| 0x08 | 2 | uncompressed_len |
Size of decompressed output |
| 0x0A | 2 | compressed_len |
Size of compressed patch data (special values below) |
| 0x0C | ... | patch_data |
Compressed patch data (variable length) |
Special compressed_len values:
| Value | Action |
|---|---|
| 0 | Zero-fill: memset(dest + new_addr, 0, uncompressed_len) |
| 1 | Copy: memcpy(dest + new_addr, dest + old_addr, uncompressed_len) |
| >= 2 | LZX delta decompress: decompress patch_data using old_addr data as window reference |
Delta Patch Key Handling
Delta patches use a three-level key scheme:
- Base module's session key is decrypted using the master key (as normal)
- The patch's encrypted AES key is then decrypted using the base module's session key (not the master key)
- Verification:
AES_Decrypt(base_session_key, patch_descriptor.image_key_source)must equal the original session key of the base module
8. Import Libraries
Located via optional header key 0x000103FF (XEX_HEADER_IMPORT_LIBRARIES).
Import Libraries Container (xex2_opt_import_libraries)
| Offset | Size | Field | Description |
|---|---|---|---|
| 0x00 | 4 | size |
Total size of the import libraries structure |
| 0x04 | 4 | string_table.size |
Size of the string table in bytes |
| 0x08 | 4 | string_table.count |
Number of strings in the table |
| 0x0C | N | string_table.data |
Null-terminated strings, 4-byte aligned with padding |
Library entries follow immediately after the string table (at offset string_table.size + 12).
Import Library (xex2_import_library)
Each library entry:
| Offset | Size | Field | Description |
|---|---|---|---|
| 0x00 | 4 | size |
Size of this library entry in bytes |
| 0x04 | 20 | next_import_digest |
SHA-1 digest of next import entry |
| 0x18 | 4 | id |
Library ID |
| 0x1C | 4 | version_value |
Library version (xex2_version bitfield) |
| 0x20 | 4 | version_min_value |
Minimum required version |
| 0x24 | 2 | name_index |
Index (low byte) into the string table |
| 0x26 | 2 | count |
Number of import records |
| 0x28 | 4 * N | import_table[N] |
Array of import record memory addresses |
Import Record Format (in memory)
Each entry in import_table is a memory address pointing to a location within the loaded PE image. At that memory address, the value is:
Bits 31-24 (byte 0): record_type
0x00 = Variable import
0x01 = Thunk (function) import
Bits 15-0 (bytes 2-3): ordinal number
Variable imports (record_type == 0): The memory slot is overwritten with:
- For kernel exports (implemented): the variable's address
- For kernel exports (not implemented):
0xD000BEEF | (ordinal & 0xFFF) << 16 - For user module exports: the export address
- For unresolved imports:
0xF00DF00D
Thunk imports (record_type == 1): The 16-byte thunk in memory is originally:
+0x00: li r3, 0 // 0x38600000
+0x04: li r4, <ordinal> // 0x38800000 | ordinal
+0x08: mtspr CTR, r11 // 0x7D6903A6
+0x0C: bctr // 0x4E800420
For user module imports, this is rewritten to:
+0x00: lis r11, <addr_hi> // 0x3D600000 | (addr >> 16)
+0x04: ori r11, r11, <addr_lo>// 0x616B0000 | (addr & 0xFFFF)
+0x08: mtspr CTR, r11 // (unchanged)
+0x0C: bctr // (unchanged)
Import records alternate: variable descriptor, then thunk address, then next variable descriptor, etc.
9. Export Table (xex2_export_table)
Located at the memory address specified in xex2_security_info.export_table. This is a virtual address, NOT a file offset.
| Offset | Size | Field | Description |
|---|---|---|---|
| 0x00 | 12 | magic[3] |
Magic identifier (3 uint32_t values) |
| 0x0C | 8 | modulenumber[2] |
Module number (2 uint32_t values) |
| 0x14 | 12 | version[3] |
Version info (3 uint32_t values) |
| 0x20 | 4 | imagebaseaddr |
Image base address (must be shifted left 16 bits to get actual address) |
| 0x24 | 4 | count |
Number of exports |
| 0x28 | 4 | base |
Base ordinal number |
| 0x2C | 4 * N | ordOffset[N] |
Array of ordinal offsets |
Resolving an export address:
function_address = ordOffset[ordinal - base] + (imagebaseaddr << 16)
PE Export Directory (X_IMAGE_EXPORT_DIRECTORY)
An alternative export mechanism via the PE header (optional header key XEX_HEADER_EXPORTS_BY_NAME). The xex2_opt_data_directory at that key contains:
offset: RVA from PE base to theX_IMAGE_EXPORT_DIRECTORYsize: Size of the export directory
The export directory is standard PE format (little-endian within the Xbox PE):
| Offset | Size | Field |
|---|---|---|
| 0x00 | 4 | Characteristics |
| 0x04 | 4 | TimeDateStamp |
| 0x08 | 2 | MajorVersion |
| 0x0A | 2 | MinorVersion |
| 0x0C | 4 | Name (RVA) |
| 0x10 | 4 | Base ordinal |
| 0x14 | 4 | NumberOfFunctions |
| 0x18 | 4 | NumberOfNames |
| 0x1C | 4 | AddressOfFunctions (RVA from export directory) |
| 0x20 | 4 | AddressOfNames (RVA from export directory) |
| 0x24 | 4 | AddressOfNameOrdinals (RVA from export directory) |
10. Specific Optional Header Structures
Execution Info (xex2_opt_execution_info) — Key 0x00040006
24 bytes (0x18). All offsets relative to start of structure.
| Offset | Size | Field | Description |
|---|---|---|---|
| 0x00 | 4 | media_id |
Media identifier |
| 0x04 | 4 | version_value |
Module version (xex2_version bitfield) |
| 0x08 | 4 | base_version_value |
Base version |
| 0x0C | 4 | title_id |
Title ID (e.g. 0x415607D1) |
| 0x10 | 1 | platform |
Platform identifier |
| 0x11 | 1 | executable_table |
Executable table index |
| 0x12 | 1 | disc_number |
Current disc number |
| 0x13 | 1 | disc_count |
Total disc count |
| 0x14 | 4 | savegame_id |
Savegame identifier |
Version Bitfield (xex2_version)
Packed into a 32-bit big-endian value:
| Bits | Field | Width |
|---|---|---|
| 31-28 | major |
4 bits |
| 27-24 | minor |
4 bits |
| 23-8 | build |
16 bits |
| 7-0 | qfe |
8 bits |
TLS Info (xex2_opt_tls_info) — Key 0x00020104
16 bytes (0x10):
| Offset | Size | Field | Description |
|---|---|---|---|
| 0x00 | 4 | slot_count |
Number of TLS slots |
| 0x04 | 4 | raw_data_address |
Memory address of TLS raw data |
| 0x08 | 4 | data_size |
Total TLS data size |
| 0x0C | 4 | raw_data_size |
Size of initialized TLS data |
Checksum / Timestamp (xex2_opt_checksum_timedatestamp) — Key 0x00018002
8 bytes:
| Offset | Size | Field | Description |
|---|---|---|---|
| 0x00 | 4 | checksum |
Module checksum |
| 0x04 | 4 | timedatestamp |
Unix timestamp of build |
Resource Info (xex2_opt_resource_info) — Key 0x000002FF
Variable size. Resource count = (size - 4) / 16.
| Offset | Size | Field | Description |
|---|---|---|---|
| 0x00 | 4 | size |
Total size of resource info |
| 0x04 | 16 * N | resources[N] |
Array of xex2_resource |
Each xex2_resource (16 bytes):
| Offset | Size | Field | Description |
|---|---|---|---|
| 0x00 | 8 | name |
Resource name (null-padded) |
| 0x08 | 4 | address |
Memory address of resource |
| 0x0C | 4 | size |
Size of resource in bytes |
Static Libraries (xex2_opt_static_libraries) — Key 0x000200FF
| Offset | Size | Field | Description |
|---|---|---|---|
| 0x00 | 4 | size |
Total size. Library count = (size - 4) / 16 |
| 0x04 | 16 * N | libraries[N] |
Array of xex2_opt_static_library |
Each xex2_opt_static_library (16 bytes / 0x10):
| Offset | Size | Field | Description |
|---|---|---|---|
| 0x00 | 8 | name |
Library name (null-padded) |
| 0x08 | 2 | version_major |
Major version |
| 0x0A | 2 | version_minor |
Minor version |
| 0x0C | 2 | version_build |
Build number |
| 0x0E | 1 | approval_type |
0=Unapproved, 1=Possible, 2=Approved, 3=Expired |
| 0x0F | 1 | version_qfe |
QFE version |
LAN Key (xex2_opt_lan_key) — Key 0x00040404
16 bytes: raw AES-128 key used for LAN multiplayer encryption.
Game Ratings (xex2_game_ratings_t) — Key 0x00040310
64 bytes (0x40) containing age ratings for various regional rating boards:
| Offset | Size | Board |
|---|---|---|
| 0x00 | 1 | ESRB (North America) |
| 0x01 | 1 | PEGI (Europe) |
| 0x02 | 1 | PEGI Finland |
| 0x03 | 1 | PEGI Portugal |
| 0x04 | 1 | BBFC (UK/Ireland) |
| 0x05 | 1 | CERO (Japan) |
| 0x06 | 1 | USK (Germany) |
| 0x07 | 1 | OFLC Australia |
| 0x08 | 1 | OFLC New Zealand |
| 0x09 | 1 | KMRB (South Korea) |
| 0x0A | 1 | Brazil |
| 0x0B | 1 | FPB (South Africa) |
| 0x0C | 52 | Reserved / Unknown |
Each rating value is 0xFF for "Unrated".
Callcap Imports (xex2_opt_call_cap_imports) — Key 0x00018102
8 bytes:
| Offset | Size | Field | Description |
|---|---|---|---|
| 0x00 | 4 | start_func_thunk_addr |
Memory address of start function thunk |
| 0x04 | 4 | end_func_thunk_addr |
Memory address of end function thunk |
Data Directory (xex2_opt_data_directory) — Key 0x00E10402
8 bytes, used for PE exports-by-name:
| Offset | Size | Field | Description |
|---|---|---|---|
| 0x00 | 4 | offset |
RVA from PE image base |
| 0x04 | 4 | size |
Size of the directory |
Bound Path (xex2_opt_bound_path) — Key 0x000080FF
Variable length:
| Offset | Size | Field | Description |
|---|---|---|---|
| 0x00 | 4 | size |
Total size including this field |
| 0x04 | N | path |
Null-terminated path string |
Original PE Name (xex2_opt_original_pe_name) — Key 0x000183FF
Variable length:
| Offset | Size | Field | Description |
|---|---|---|---|
| 0x00 | 4 | size |
Total size including this field |
| 0x04 | N | name |
Null-terminated original PE filename |
11. PE Image (After Decryption/Decompression)
After decryption and decompression, the PE image is loaded into Xbox 360 virtual memory at load_address (from security info, or overridden by XEX_HEADER_IMAGE_BASE_ADDRESS).
PE Headers (in memory at load_address)
The PE image is a standard 32-bit PE executable for PowerPC Big-Endian:
DOS Header (at memory base address)
| Offset | Size | Field | Description |
|---|---|---|---|
| 0x00 | 2 | e_magic |
MZ signature (0x5A4D) — note: stored as 0x905A4D with byte swap check |
| ... | ... | ... | Standard DOS header fields |
| 0x3C | 4 | e_lfanew |
Offset to NT headers (from start of PE image) |
NT Headers (at PE offset e_lfanew)
| Offset | Size | Field | Description |
|---|---|---|---|
| 0x00 | 4 | Signature |
PE\0\0 (0x00004550) |
| 0x04 | 20 | FileHeader |
COFF file header |
| 0x18 | 224 | OptionalHeader |
PE32 optional header |
File Header Validation
| Field | Expected Value | Description |
|---|---|---|
Machine |
0x01F2 | IMAGE_FILE_MACHINE_POWERPCBE |
Characteristics |
bit 0x0100 set | IMAGE_FILE_32BIT_MACHINE |
SizeOfOptionalHeader |
224 (0xE0) | Standard PE32 optional header size |
Optional Header Validation
| Field | Expected Value |
|---|---|
Magic |
0x10B (IMAGE_NT_OPTIONAL_HDR32_MAGIC) |
Subsystem |
14 (IMAGE_SUBSYSTEM_XBOX) |
Section Headers
Located immediately after the optional header. Each section header is 40 bytes:
| Offset | Size | Field | Description |
|---|---|---|---|
| 0x00 | 8 | Name |
Section name (e.g. .text, .rdata, .data) |
| 0x08 | 4 | VirtualSize |
Size in memory |
| 0x0C | 4 | VirtualAddress |
RVA from PE image base |
| 0x10 | 4 | SizeOfRawData |
Size of raw data |
| 0x14 | 4 | PointerToRawData |
PE offset to raw data |
| 0x18 | 4 | PointerToRelocations |
(not used) |
| 0x1C | 4 | PointerToLinenumbers |
(not used) |
| 0x20 | 2 | NumberOfRelocations |
(not used) |
| 0x22 | 2 | NumberOfLinenumbers |
(not used) |
| 0x24 | 4 | Characteristics |
Section flags |
Section characteristics relevant to Xbox 360:
| Value | Name |
|---|---|
| 0x00000020 | Contains code |
| 0x00000040 | Contains initialized data |
| 0x00000080 | Contains uninitialized data |
| 0x20000000 | Memory execute |
| 0x40000000 | Memory read |
| 0x80000000 | Memory write |
The in-memory address of a section is: load_address + VirtualAddress
12. System Flags (xex2_system_flags)
Inline u32 at optional header key 0x00030000. Bitmask of system privileges:
| Value | Name | Description |
|---|---|---|
| 0x00000001 | NO_FORCED_REBOOT | |
| 0x00000002 | FOREGROUND_TASKS | |
| 0x00000004 | NO_ODD_MAPPING | |
| 0x00000008 | HANDLE_MCE_INPUT | |
| 0x00000010 | RESTRICTED_HUD_FEATURES | |
| 0x00000020 | HANDLE_GAMEPAD_DISCONNECT | |
| 0x00000040 | INSECURE_SOCKETS | |
| 0x00000080 | XBOX1_INTEROPERABILITY | |
| 0x00000100 | DASH_CONTEXT | |
| 0x00000200 | USES_GAME_VOICE_CHANNEL | |
| 0x00000400 | PAL50_INCOMPATIBLE | |
| 0x00000800 | INSECURE_UTILITY_DRIVE | |
| 0x00001000 | XAM_HOOKS | |
| 0x00002000 | ACCESS_PII | |
| 0x00004000 | CROSS_PLATFORM_SYSTEM_LINK | |
| 0x00008000 | MULTIDISC_SWAP | |
| 0x00010000 | MULTIDISC_INSECURE_MEDIA | |
| 0x00020000 | AP25_MEDIA | Anti-piracy 2.5 media check |
| 0x00040000 | NO_CONFIRM_EXIT | |
| 0x00080000 | ALLOW_BACKGROUND_DOWNLOAD | |
| 0x00100000 | CREATE_PERSISTABLE_RAMDRIVE | |
| 0x00200000 | INHERIT_PERSISTENT_RAMDRIVE | |
| 0x00400000 | ALLOW_HUD_VIBRATION | |
| 0x00800000 | ACCESS_UTILITY_PARTITIONS | |
| 0x01000000 | IPTV_INPUT_SUPPORTED | |
| 0x02000000 | PREFER_BIG_BUTTON_INPUT | |
| 0x04000000 | ALLOW_EXTENDED_SYSTEM_RESERVATION | |
| 0x08000000 | MULTIDISC_CROSS_TITLE | |
| 0x10000000 | INSTALL_INCOMPATIBLE | |
| 0x20000000 | ALLOW_AVATAR_GET_METADATA_BY_XUID | |
| 0x40000000 | ALLOW_CONTROLLER_SWAPPING | |
| 0x80000000 | DASH_EXTENSIBILITY_MODULE |
Extended System Flags (32-bit) — Key 0x00030100
| Value | Name |
|---|---|
| 0x00000001 | ALLOW_NETWORK_READ_CANCEL |
| 0x00000002 | UNINTERRUPTABLE_READS |
| 0x00000004 | REQUIRE_FULL_EXPERIENCE |
| 0x00000008 | GAME_VOICE_REQUIRED_UI |
| 0x00000010 | TITLE_SET_PRESENCE_STRING |
| 0x00000020 | CAMERA_ANGLE_CONTROL |
| 0x00000040 | SKELETAL_TRACKING_REQUIRED |
| 0x00000080 | SKELETAL_TRACKING_SUPPORTED |
13. Complete Loading Sequence
Here is the full loading process as implemented by Xenia:
1. READ HEADER
a. Read xex2_header from offset 0
b. Verify magic == "XEX2" (0x58455832)
c. Copy entire header region (header_size bytes) into memory
2. PARSE SECURITY INFO
a. Navigate to xex2_header.security_offset
b. Extract: RSA signature, encrypted AES key, load_address, image_flags,
export_table address, page_descriptors
c. Determine base_address: use XEX_HEADER_IMAGE_BASE_ADDRESS if present,
otherwise security_info.load_address
3. DECRYPT & DECOMPRESS PE IMAGE
a. Determine encryption/compression from XEX_HEADER_FILE_FORMAT_INFO
b. Derive session key:
session_key = AES-128-CBC-Decrypt(master_key, security_info.aes_key)
Try retail key first, then devkit, then XEX1 key
c. Based on compression_type:
- NONE: decrypt payload directly to base_address
- BASIC: decrypt + zero-fill blocks to base_address
- NORMAL: decrypt → de-block → LZX decompress to base_address
d. For patches: store raw patch data for later application
4. VERIFY PE IMAGE
a. Check for MZ signature (0x5A4D) at base_address
b. If not valid PE and not a patch, loading fails
5. APPLY PATCHES (if applicable)
a. Patch XEX headers using LZX delta
b. Re-derive session keys for patched module
c. Decrypt and apply image delta patches block by block
d. Verify block hashes (SHA-1) at each step
6. PARSE PE HEADERS (LoadContinue)
a. Verify DOS header (MZ), NT headers (PE\0\0)
b. Verify Machine == POWERPCBE, Subsystem == XBOX
c. Extract all PE sections (name, VA, size, flags)
7. SETUP MEMORY PROTECTION
a. For each page_descriptor:
- CODE / READONLY_DATA → Read-only
- DATA → Read + Write
b. Track low_address (first code page) and high_address (last code page)
8. RESOLVE IMPORTS
a. Parse XEX_HEADER_IMPORT_LIBRARIES
b. For each import library:
- Parse string table for library names
- Load dependent user modules if not already loaded
- For each import record:
* Variable (type 0): write resolved address to memory slot
* Thunk (type 1): declare function, optionally rewrite PPC branch code
9. SETUP EXPORTS
a. If security_info.export_table != 0: XEX export table is in memory
b. If XEX_HEADER_EXPORTS_BY_NAME present: PE export directory is available
14. Integrity Verification (SHA-1)
SHA-1 is used throughout the format for data integrity:
| Location | What is Hashed | Hash Location |
|---|---|---|
| Page descriptors | Each page's data in memory | xex2_page_descriptor.data_digest (20 bytes) |
| Security info | Section data | xex2_security_info.section_digest |
| Security info | Import table | xex2_security_info.import_table_digest |
| Security info | Header data | xex2_security_info.header_digest |
| Normal compression | Each compressed block | xex2_compressed_block_info.block_hash (chain) |
| Delta patches | Each patch block | xex2_compressed_block_info.block_hash |
| Import libraries | Next import entry | xex2_import_library.next_import_digest |
| Delta patches | Source image | xex2_opt_delta_patch_descriptor.digest_source |
15. Key Source Files
| File | Purpose |
|---|---|
src/xenia/kernel/util/xex2_info.h |
All XEX2 structure definitions, enums, and flags |
src/xenia/cpu/xex_module.h |
XexModule class, SecurityInfoContext, ImportLibrary structures |
src/xenia/cpu/xex_module.cc |
Main loading logic, AES keys, decryption, decompression dispatch |
src/xenia/cpu/lzx.h / lzx.cc |
LZX decompression and delta patch application |
src/xenia/base/pe_image.h |
PE (DOS/NT/Section) header structures |
third_party/crypto/rijndael-alg-fst.c |
AES (Rijndael) cipher implementation |
third_party/mspack/lzxd.c |
LZX decompression engine |