Initial commit: xenia-rs workspace for Xbox 360 RE
Rust reimplementation of the xenia Xbox 360 emulator targeting reverse- engineering and preservation, initially scoped to Project Sylpheed. Includes: - XEX2 loader (LZX decompression, AES decryption, PE parsing) - XISO / XGD2 disc image VFS - PPC interpreter with 200+ opcodes and VMX128 decoding - Static analyzer: functions, cross-references, labels, asm + SQLite output - HLE kernel covering the xboxkrnl/xam subset used by Sylpheed init - Debugger with in-memory and SQLite-backed execution tracing - `xenia-rs` CLI with extract/dis/exec commands that produce cumulative, superset SQLite databases and opt-in instruction/import/branch traces Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
/target/
|
||||
*.iso
|
||||
*.xiso
|
||||
*.db
|
||||
861
Cargo.lock
generated
Normal file
861
Cargo.lock
generated
Normal file
@@ -0,0 +1,861 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "aes"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cipher",
|
||||
"cpufeatures",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.8.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"version_check",
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
"anstyle-query",
|
||||
"anstyle-wincon",
|
||||
"colorchoice",
|
||||
"is_terminal_polyfill",
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"once_cell_polyfill",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.102"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.60"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "cipher"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
|
||||
dependencies = [
|
||||
"crypto-common",
|
||||
"inout",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
"strsim",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fallible-iterator"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
|
||||
|
||||
[[package]]
|
||||
name = "fallible-streaming-iterator"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.14.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashlink"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af"
|
||||
dependencies = [
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "inout"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.184"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af"
|
||||
|
||||
[[package]]
|
||||
name = "libsqlite3-sys"
|
||||
version = "0.28.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c10584274047cb335c23d3e61bcef8e323adae7c5c8c760540f73610177fc3f"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||
|
||||
[[package]]
|
||||
name = "matchers"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
|
||||
dependencies = [
|
||||
"regex-automata",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||
|
||||
[[package]]
|
||||
name = "nu-ansi-term"
|
||||
version = "0.50.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell_polyfill"
|
||||
version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
|
||||
|
||||
[[package]]
|
||||
name = "rusqlite"
|
||||
version = "0.31.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b838eba278d213a8beaf485bd313fd580ca4505a00d5871caeb1457c55322cae"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"fallible-iterator",
|
||||
"fallible-streaming-iterator",
|
||||
"hashlink",
|
||||
"libsqlite3-sys",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.149"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sharded-slab"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thread_local"
|
||||
version = "1.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
version = "0.1.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
||||
dependencies = [
|
||||
"pin-project-lite",
|
||||
"tracing-attributes",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-attributes"
|
||||
version = "0.1.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-core"
|
||||
version = "0.1.36"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"valuable",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-log"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
|
||||
dependencies = [
|
||||
"log",
|
||||
"once_cell",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-subscriber"
|
||||
version = "0.3.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
|
||||
dependencies = [
|
||||
"matchers",
|
||||
"nu-ansi-term",
|
||||
"once_cell",
|
||||
"regex-automata",
|
||||
"sharded-slab",
|
||||
"smallvec",
|
||||
"thread_local",
|
||||
"tracing",
|
||||
"tracing-core",
|
||||
"tracing-log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "valuable"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.59.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "xenia-analysis"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"rusqlite",
|
||||
"serde",
|
||||
"tracing",
|
||||
"xenia-xex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xenia-app"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"xenia-analysis",
|
||||
"xenia-apu",
|
||||
"xenia-cpu",
|
||||
"xenia-debugger",
|
||||
"xenia-gpu",
|
||||
"xenia-hid",
|
||||
"xenia-kernel",
|
||||
"xenia-memory",
|
||||
"xenia-types",
|
||||
"xenia-vfs",
|
||||
"xenia-xex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xenia-apu"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"thiserror",
|
||||
"tracing",
|
||||
"xenia-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xenia-cpu"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"thiserror",
|
||||
"tracing",
|
||||
"xenia-memory",
|
||||
"xenia-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xenia-debugger"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"thiserror",
|
||||
"tracing",
|
||||
"xenia-cpu",
|
||||
"xenia-memory",
|
||||
"xenia-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xenia-gpu"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"byteorder",
|
||||
"thiserror",
|
||||
"tracing",
|
||||
"xenia-memory",
|
||||
"xenia-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xenia-hid"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"thiserror",
|
||||
"tracing",
|
||||
"xenia-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xenia-kernel"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"thiserror",
|
||||
"tracing",
|
||||
"xenia-cpu",
|
||||
"xenia-memory",
|
||||
"xenia-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xenia-memory"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"libc",
|
||||
"thiserror",
|
||||
"tracing",
|
||||
"windows-sys 0.59.0",
|
||||
"xenia-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xenia-types"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"byteorder",
|
||||
"serde",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xenia-vfs"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"byteorder",
|
||||
"thiserror",
|
||||
"tracing",
|
||||
"xenia-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xenia-xex"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"anyhow",
|
||||
"byteorder",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
"tracing",
|
||||
"xenia-memory",
|
||||
"xenia-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.48"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.8.48"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||
47
Cargo.toml
Normal file
47
Cargo.toml
Normal file
@@ -0,0 +1,47 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"crates/xenia-types",
|
||||
"crates/xenia-memory",
|
||||
"crates/xenia-cpu",
|
||||
"crates/xenia-xex",
|
||||
"crates/xenia-vfs",
|
||||
"crates/xenia-kernel",
|
||||
"crates/xenia-gpu",
|
||||
"crates/xenia-apu",
|
||||
"crates/xenia-hid",
|
||||
"crates/xenia-debugger",
|
||||
"crates/xenia-analysis",
|
||||
"crates/xenia-app",
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
license = "BSD-3-Clause"
|
||||
|
||||
[workspace.dependencies]
|
||||
# Shared types
|
||||
xenia-types = { path = "crates/xenia-types" }
|
||||
xenia-memory = { path = "crates/xenia-memory" }
|
||||
xenia-cpu = { path = "crates/xenia-cpu" }
|
||||
xenia-xex = { path = "crates/xenia-xex" }
|
||||
xenia-vfs = { path = "crates/xenia-vfs" }
|
||||
xenia-kernel = { path = "crates/xenia-kernel" }
|
||||
xenia-gpu = { path = "crates/xenia-gpu" }
|
||||
xenia-apu = { path = "crates/xenia-apu" }
|
||||
xenia-hid = { path = "crates/xenia-hid" }
|
||||
xenia-debugger = { path = "crates/xenia-debugger" }
|
||||
xenia-analysis = { path = "crates/xenia-analysis" }
|
||||
|
||||
# External dependencies
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
bitflags = "2"
|
||||
byteorder = "1"
|
||||
thiserror = "2"
|
||||
anyhow = "1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
aes = "0.8"
|
||||
rusqlite = { version = "0.31", features = ["bundled"] }
|
||||
132
README.md
Normal file
132
README.md
Normal file
@@ -0,0 +1,132 @@
|
||||
# xenia-rs
|
||||
|
||||
Rust reimplementation of the Xbox 360 emulator [xenia](https://github.com/xenia-project/xenia),
|
||||
focused on **reverse-engineering and preservation** rather than full-speed play.
|
||||
The initial target is *Project Sylpheed — Arc of Deception*; getting the title
|
||||
disassembled, traced, and far enough into its init path to understand its engine.
|
||||
|
||||
Heavy cross-reference to [xenia-canary](https://github.com/xenia-canary/xenia-canary)
|
||||
for CPU context setup, kernel export behavior, and XEX loading semantics.
|
||||
|
||||
## Status
|
||||
|
||||
- **XEX loader** — XEX2 header parsing, LZX decompression, AES decryption, PE section parsing.
|
||||
- **VFS / XISO** — XGD2 dual-layer disc images (with the 0x0FD90000 partition offset).
|
||||
- **PPC interpreter** — 200+ opcodes, PowerPC 32/64-bit GPR/FPR, VMX128 decoding.
|
||||
- **Static analyzer** — function discovery (prolog/epilog heuristics), cross-references, labels,
|
||||
save/restore helper detection, assembly text + SQLite database output.
|
||||
- **Kernel HLE** — minimal subset driving Project Sylpheed: ~170 xboxkrnl + xam exports
|
||||
(critical sections, events, TLS, virtual memory, Vd stubs, XAM input/user/content).
|
||||
- **Debugger** — in-memory step/break, SQLite execution + import-call + branch tracing.
|
||||
|
||||
Not yet: GPU (xenos/xe-shader), APU audio, HID, kernel scheduler, full threading,
|
||||
exception delivery.
|
||||
|
||||
## Workspace
|
||||
|
||||
```
|
||||
crates/
|
||||
xenia-types # shared primitive types, bitflags
|
||||
xenia-memory # guest memory, paged allocator, page table
|
||||
xenia-cpu # PPC decoder, interpreter, context
|
||||
xenia-xex # XEX2 loader, PE parser, LZX, AES
|
||||
xenia-vfs # XISO / disc-image reader
|
||||
xenia-kernel # HLE kernel state, exports, XAM
|
||||
xenia-gpu # (stub) Xenos command processor
|
||||
xenia-apu # (stub) XAudio
|
||||
xenia-hid # (stub) XInput
|
||||
xenia-debugger # in-memory trace, breakpoints, step modes
|
||||
xenia-analysis # function/xref analysis, assembly formatter, SQLite DbWriter
|
||||
xenia-app # `xenia-rs` CLI binary
|
||||
```
|
||||
|
||||
## CLI
|
||||
|
||||
Build:
|
||||
```sh
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
The binary `xenia-rs` accepts XEX2 files or ISO / XISO disc images as input
|
||||
(the loader auto-detects discs and extracts `default.xex`).
|
||||
|
||||
### `info` / `browse` / `disasm`
|
||||
|
||||
Quick header / disc / first-N-instructions inspection. See `--help`.
|
||||
|
||||
### `extract` — unpack PE + metadata
|
||||
|
||||
```sh
|
||||
xenia-rs extract <xex-or-iso> [-o <out-dir>] [--db <sqlite-path>]
|
||||
```
|
||||
|
||||
Writes `<name>.pe` (decompressed/decrypted PE image) and `<name>.xex.json`
|
||||
(header metadata). With `--db`, also emits a SQLite database containing the
|
||||
**base tables**: `metadata`, `sections`, `imports`.
|
||||
|
||||
### `dis` — full disassembly
|
||||
|
||||
```sh
|
||||
xenia-rs dis <xex-or-iso> [-o <asm-file>] [--db <sqlite-path>] [--quiet]
|
||||
```
|
||||
|
||||
Runs function + cross-reference analysis and produces:
|
||||
- assembly text to stdout or `-o <file>` (unless `--quiet`)
|
||||
- optional SQLite DB with the **base tables + disasm tables**:
|
||||
`functions`, `labels`, `instructions`, `xrefs`
|
||||
|
||||
### `exec` — interpret with tracing
|
||||
|
||||
```sh
|
||||
xenia-rs exec <xex-or-iso> [-n <max-instrs>] [--db <sqlite-path>]
|
||||
[--trace-instructions] [--trace-imports] [--trace-branches]
|
||||
```
|
||||
|
||||
Loads the title, initializes CPU state per xenia-canary, intercepts import
|
||||
thunks with HLE kernel calls, and interprets from the entry point. Without
|
||||
`-n`, runs until halt/fault. With `--db`, produces a DB that is a **superset
|
||||
of `dis --db`** plus opt-in trace tables:
|
||||
|
||||
| flag | table | rows |
|
||||
|-------------------------|----------------|---------------------------------------------------------|
|
||||
| `--trace-instructions` | `exec_trace` | one row per interpreted instruction (PC, r3/r4, LR, SP) |
|
||||
| `--trace-imports` | `import_calls` | one row per kernel/XAM call (module, ordinal, args) |
|
||||
| `--trace-branches` | `branch_trace` | taken branches classified as `call`/`return`/`jump`/`branch` |
|
||||
|
||||
### Cumulative DB layering
|
||||
|
||||
Each command's DB is a superset of the previous. A single
|
||||
`xenia-rs exec <iso> --db full.db --trace-instructions --trace-imports --trace-branches`
|
||||
produces the full picture in one pass — base tables, complete static
|
||||
disassembly, and runtime traces correlatable by address/cycle.
|
||||
|
||||
## Performance knobs
|
||||
|
||||
- **`XENIA_DB_BATCH_SIZE`** — rows per streaming commit / trace-buffer flush
|
||||
(default `100_000`). Lower values reduce memory use; higher values reduce
|
||||
fsync overhead on slow disks.
|
||||
|
||||
The DB writer uses `journal_mode=OFF`, `synchronous=OFF`, `locking_mode=EXCLUSIVE`
|
||||
and commits in batches; no `ANALYZE` is run at finalize. Indices are created
|
||||
after bulk insertion with progress messages.
|
||||
|
||||
## Example queries
|
||||
|
||||
```sql
|
||||
-- Top 20 kernel functions called during early init
|
||||
SELECT name, COUNT(*) FROM import_calls GROUP BY name ORDER BY 2 DESC LIMIT 20;
|
||||
|
||||
-- All basic-block leaders (targets of taken branches) not already labelled
|
||||
SELECT DISTINCT bt.target
|
||||
FROM branch_trace bt LEFT JOIN labels l ON l.address = bt.target
|
||||
WHERE l.address IS NULL;
|
||||
|
||||
-- Correlate a traced call site with its static disassembly
|
||||
SELECT et.cycle, i.disasm, i.ext_disasm
|
||||
FROM exec_trace et JOIN instructions i ON i.address = et.address
|
||||
WHERE et.address = 0x824AB748 ORDER BY et.cycle;
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
BSD-3-Clause, matching upstream xenia.
|
||||
13
crates/xenia-analysis/Cargo.toml
Normal file
13
crates/xenia-analysis/Cargo.toml
Normal file
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "xenia-analysis"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
build = "build.rs"
|
||||
|
||||
[dependencies]
|
||||
xenia-xex = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
rusqlite = { workspace = true }
|
||||
87
crates/xenia-analysis/build.rs
Normal file
87
crates/xenia-analysis/build.rs
Normal file
@@ -0,0 +1,87 @@
|
||||
//! Build script: parse xenia's xboxkrnl_table.inc and xam_table.inc to generate
|
||||
//! ordinal->name lookup tables at compile time.
|
||||
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
|
||||
fn parse_table(path: &Path) -> Vec<(u32, String, String)> {
|
||||
let content = match fs::read_to_string(path) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
eprintln!("cargo:warning=could not read {}: {}", path.display(), e);
|
||||
return Vec::new();
|
||||
}
|
||||
};
|
||||
|
||||
let mut entries = Vec::new();
|
||||
for line in content.lines() {
|
||||
let line = line.trim();
|
||||
// XE_EXPORT(module, 0xNNNNNNNN, Name, kType),
|
||||
if !line.starts_with("XE_EXPORT(") { continue; }
|
||||
let inner = match line.strip_prefix("XE_EXPORT(").and_then(|s| s.strip_suffix("),")) {
|
||||
Some(s) => s,
|
||||
None => continue,
|
||||
};
|
||||
let parts: Vec<&str> = inner.splitn(4, ',').map(|s| s.trim()).collect();
|
||||
if parts.len() < 4 { continue; }
|
||||
let module = parts[0].to_string();
|
||||
let ordinal = match u32::from_str_radix(parts[1].trim_start_matches("0x").trim_start_matches("0X"), 16) {
|
||||
Ok(n) => n,
|
||||
Err(_) => continue,
|
||||
};
|
||||
let name = parts[2].to_string();
|
||||
entries.push((ordinal, name, module));
|
||||
}
|
||||
entries
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let out_dir = env::var("OUT_DIR").unwrap();
|
||||
let dest = Path::new(&out_dir).join("ordinals.rs");
|
||||
let mut f = fs::File::create(&dest).unwrap();
|
||||
|
||||
// Locate xenia tables relative to the workspace root
|
||||
// crates/xenia-analysis/ -> ../../ -> workspace root -> ../xenia-canary/
|
||||
let manifest = env::var("CARGO_MANIFEST_DIR").unwrap();
|
||||
let workspace_root = Path::new(&manifest).parent().unwrap().parent().unwrap();
|
||||
let project_root = workspace_root.parent().unwrap();
|
||||
|
||||
let krnl_path = project_root
|
||||
.join("xenia-canary/src/xenia/kernel/xboxkrnl/xboxkrnl_table.inc");
|
||||
let xam_path = project_root
|
||||
.join("xenia-canary/src/xenia/kernel/xam/xam_table.inc");
|
||||
|
||||
println!("cargo:rerun-if-changed={}", krnl_path.display());
|
||||
println!("cargo:rerun-if-changed={}", xam_path.display());
|
||||
|
||||
let krnl = parse_table(&krnl_path);
|
||||
let xam = parse_table(&xam_path);
|
||||
|
||||
writeln!(f, "/// Auto-generated from xenia's export tables.").unwrap();
|
||||
writeln!(f, "pub fn resolve_ordinal(lib: &str, ordinal: u16) -> Option<&'static str> {{").unwrap();
|
||||
writeln!(f, " match lib {{").unwrap();
|
||||
|
||||
// xboxkrnl.exe
|
||||
writeln!(f, " \"xboxkrnl.exe\" => match ordinal {{").unwrap();
|
||||
for (ord, name, _) in &krnl {
|
||||
writeln!(f, " 0x{ord:04X} => Some(\"{name}\"),").unwrap();
|
||||
}
|
||||
writeln!(f, " _ => None,").unwrap();
|
||||
writeln!(f, " }},").unwrap();
|
||||
|
||||
// xam.xex
|
||||
writeln!(f, " \"xam.xex\" => match ordinal {{").unwrap();
|
||||
for (ord, name, _) in &xam {
|
||||
writeln!(f, " 0x{ord:04X} => Some(\"{name}\"),").unwrap();
|
||||
}
|
||||
writeln!(f, " _ => None,").unwrap();
|
||||
writeln!(f, " }},").unwrap();
|
||||
|
||||
writeln!(f, " _ => None,").unwrap();
|
||||
writeln!(f, " }}").unwrap();
|
||||
writeln!(f, "}}").unwrap();
|
||||
|
||||
eprintln!("ordinals.rs: {} xboxkrnl + {} xam entries", krnl.len(), xam.len());
|
||||
}
|
||||
727
crates/xenia-analysis/src/db.rs
Normal file
727
crates/xenia-analysis/src/db.rs
Normal file
@@ -0,0 +1,727 @@
|
||||
//! SQLite database writer for xenia-rs.
|
||||
//!
|
||||
//! Layered, streaming writes shared by `extract`, `dis`, and `exec`.
|
||||
//! Each command's output is a superset of the previous:
|
||||
//! - `extract --db` -> base tables (metadata, sections, imports)
|
||||
//! - `dis --db` -> base + disasm tables (functions, labels, instructions, xrefs)
|
||||
//! - `exec --db` -> base + disasm + opt-in trace tables (exec_trace, import_calls, branch_trace)
|
||||
//!
|
||||
//! Performance: streaming commits every 100k rows, no end-of-run ANALYZE,
|
||||
//! progress messages before each index build.
|
||||
//!
|
||||
//! Trace kind values for `branch_trace.kind`:
|
||||
//! - "call" : any branch with LK set (raw & 1 == 1)
|
||||
//! - "return" : bclrx without LK
|
||||
//! - "jump" : bcctrx without LK
|
||||
//! - "branch" : bx/bcx without LK
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
|
||||
use rusqlite::{Connection, params};
|
||||
|
||||
use crate::func::FuncAnalysis;
|
||||
use crate::xref::{XrefMap, resolve_source_label};
|
||||
use crate::formatter::DisasmInfo;
|
||||
|
||||
const DEFAULT_BATCH_SIZE: u64 = 100_000;
|
||||
|
||||
/// Number of rows per DB commit / trace buffer flush.
|
||||
/// Configurable via the `XENIA_DB_BATCH_SIZE` env var (default 100_000).
|
||||
/// Used for:
|
||||
/// - `instructions` and `xrefs` streaming commits in `write_disasm`
|
||||
/// - `exec_trace` and `branch_trace` buffer thresholds during exec
|
||||
/// (`import_calls` always flushes at 1000 — low volume, not worth scaling.)
|
||||
fn batch_size() -> u64 {
|
||||
use std::sync::OnceLock;
|
||||
static CACHED: OnceLock<u64> = OnceLock::new();
|
||||
*CACHED.get_or_init(|| {
|
||||
std::env::var("XENIA_DB_BATCH_SIZE")
|
||||
.ok()
|
||||
.and_then(|s| s.parse::<u64>().ok())
|
||||
.filter(|&n| n > 0)
|
||||
.unwrap_or(DEFAULT_BATCH_SIZE)
|
||||
})
|
||||
}
|
||||
|
||||
pub struct ExecTraceEntry {
|
||||
pub address: u32,
|
||||
pub cycle: u64,
|
||||
pub r3: u64,
|
||||
pub r4: u64,
|
||||
pub lr: u64,
|
||||
pub sp: u64,
|
||||
}
|
||||
|
||||
pub struct ImportCallEntry {
|
||||
pub address: u32,
|
||||
pub cycle: u64,
|
||||
pub module: String,
|
||||
pub ordinal: u16,
|
||||
pub name: String,
|
||||
pub arg_r3: u64,
|
||||
pub arg_r4: u64,
|
||||
pub arg_r5: u64,
|
||||
pub arg_r6: u64,
|
||||
pub return_value: u64,
|
||||
}
|
||||
|
||||
pub struct BranchTraceEntry {
|
||||
pub source: u32,
|
||||
pub target: u32,
|
||||
pub cycle: u64,
|
||||
pub kind: &'static str,
|
||||
pub lr: u64,
|
||||
}
|
||||
|
||||
pub struct DbWriter {
|
||||
conn: Connection,
|
||||
exec_buffer: Vec<ExecTraceEntry>,
|
||||
import_buffer: Vec<ImportCallEntry>,
|
||||
branch_buffer: Vec<BranchTraceEntry>,
|
||||
exec_count: u64,
|
||||
import_count: u64,
|
||||
branch_count: u64,
|
||||
trace_instructions: bool,
|
||||
trace_imports: bool,
|
||||
trace_branches: bool,
|
||||
}
|
||||
|
||||
impl DbWriter {
|
||||
/// Open a fresh database at `path`, removing any existing file first.
|
||||
pub fn open_fresh(path: &Path) -> anyhow::Result<Self> {
|
||||
if path.exists() {
|
||||
std::fs::remove_file(path)?;
|
||||
}
|
||||
let conn = Connection::open(path)?;
|
||||
conn.execute_batch("
|
||||
PRAGMA journal_mode = OFF;
|
||||
PRAGMA synchronous = OFF;
|
||||
PRAGMA locking_mode = EXCLUSIVE;
|
||||
PRAGMA temp_store = MEMORY;
|
||||
")?;
|
||||
let cap = batch_size() as usize;
|
||||
Ok(Self {
|
||||
conn,
|
||||
exec_buffer: Vec::with_capacity(cap),
|
||||
import_buffer: Vec::with_capacity(1024),
|
||||
branch_buffer: Vec::with_capacity(cap),
|
||||
exec_count: 0,
|
||||
import_count: 0,
|
||||
branch_count: 0,
|
||||
trace_instructions: false,
|
||||
trace_imports: false,
|
||||
trace_branches: false,
|
||||
})
|
||||
}
|
||||
|
||||
// ── Base layer (written by extract/dis/exec) ─────────────────────────────
|
||||
|
||||
/// Write metadata, sections, imports tables and their indices.
|
||||
pub fn write_base(&mut self, info: &DisasmInfo) -> anyhow::Result<()> {
|
||||
self.conn.execute_batch("
|
||||
CREATE TABLE metadata (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE sections (
|
||||
name TEXT NOT NULL,
|
||||
virtual_address INTEGER NOT NULL,
|
||||
virtual_size INTEGER NOT NULL,
|
||||
raw_offset INTEGER NOT NULL,
|
||||
raw_size INTEGER NOT NULL,
|
||||
flags INTEGER NOT NULL,
|
||||
is_code BOOLEAN NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE imports (
|
||||
library TEXT NOT NULL,
|
||||
ordinal INTEGER NOT NULL,
|
||||
name TEXT,
|
||||
record_type INTEGER NOT NULL,
|
||||
address INTEGER NOT NULL
|
||||
);
|
||||
")?;
|
||||
|
||||
insert_metadata(&self.conn, info)?;
|
||||
insert_sections(&self.conn, info.sections)?;
|
||||
insert_imports(&self.conn, info)?;
|
||||
|
||||
self.conn.execute_batch("
|
||||
CREATE INDEX idx_imports_library ON imports(library);
|
||||
CREATE INDEX idx_imports_name ON imports(name);
|
||||
")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ── Disasm layer (written by dis/exec) ───────────────────────────────────
|
||||
|
||||
/// Write functions, labels, instructions, xrefs tables and indices.
|
||||
pub fn write_disasm(
|
||||
&mut self,
|
||||
pe: &[u8],
|
||||
info: &DisasmInfo,
|
||||
func_analysis: &FuncAnalysis,
|
||||
labels: &HashMap<u32, String>,
|
||||
xrefs: &XrefMap,
|
||||
) -> anyhow::Result<()> {
|
||||
self.conn.execute_batch("
|
||||
CREATE TABLE functions (
|
||||
address INTEGER PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
end_address INTEGER NOT NULL,
|
||||
frame_size INTEGER NOT NULL,
|
||||
saved_gprs INTEGER NOT NULL,
|
||||
is_leaf BOOLEAN NOT NULL,
|
||||
is_saverestore BOOLEAN NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE labels (
|
||||
address INTEGER PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
kind TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE instructions (
|
||||
address INTEGER PRIMARY KEY,
|
||||
raw INTEGER NOT NULL,
|
||||
mnemonic TEXT NOT NULL,
|
||||
operands TEXT NOT NULL,
|
||||
disasm TEXT NOT NULL,
|
||||
ext_mnemonic TEXT,
|
||||
ext_operands TEXT,
|
||||
ext_disasm TEXT,
|
||||
section TEXT NOT NULL,
|
||||
function INTEGER,
|
||||
label TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE xrefs (
|
||||
source INTEGER NOT NULL,
|
||||
target INTEGER NOT NULL,
|
||||
kind TEXT NOT NULL,
|
||||
instruction TEXT,
|
||||
source_func INTEGER,
|
||||
source_label TEXT,
|
||||
target_label TEXT
|
||||
);
|
||||
")?;
|
||||
|
||||
insert_functions(&self.conn, func_analysis, labels)?;
|
||||
insert_labels(&self.conn, labels)?;
|
||||
insert_instructions_streaming(&self.conn, pe, info, func_analysis, labels)?;
|
||||
insert_xrefs_streaming(&self.conn, xrefs, pe, info.image_base, func_analysis, labels)?;
|
||||
|
||||
let indices = [
|
||||
("idx_functions_name", "CREATE INDEX idx_functions_name ON functions(name)"),
|
||||
("idx_labels_kind", "CREATE INDEX idx_labels_kind ON labels(kind)"),
|
||||
("idx_labels_name", "CREATE INDEX idx_labels_name ON labels(name)"),
|
||||
("idx_instructions_function", "CREATE INDEX idx_instructions_function ON instructions(function)"),
|
||||
("idx_instructions_mnemonic", "CREATE INDEX idx_instructions_mnemonic ON instructions(mnemonic)"),
|
||||
("idx_instructions_ext_mnemonic","CREATE INDEX idx_instructions_ext_mnemonic ON instructions(ext_mnemonic)"),
|
||||
("idx_instructions_section", "CREATE INDEX idx_instructions_section ON instructions(section)"),
|
||||
("idx_instructions_label", "CREATE INDEX idx_instructions_label ON instructions(label)"),
|
||||
("idx_xrefs_target", "CREATE INDEX idx_xrefs_target ON xrefs(target)"),
|
||||
("idx_xrefs_source", "CREATE INDEX idx_xrefs_source ON xrefs(source)"),
|
||||
("idx_xrefs_source_func", "CREATE INDEX idx_xrefs_source_func ON xrefs(source_func)"),
|
||||
("idx_xrefs_kind", "CREATE INDEX idx_xrefs_kind ON xrefs(kind)"),
|
||||
("idx_xrefs_instruction", "CREATE INDEX idx_xrefs_instruction ON xrefs(instruction)"),
|
||||
("idx_xrefs_target_label", "CREATE INDEX idx_xrefs_target_label ON xrefs(target_label)"),
|
||||
];
|
||||
for (name, sql) in indices {
|
||||
eprintln!("[db] creating {name}...");
|
||||
self.conn.execute_batch(sql)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ── Trace layer (written by exec when flags enabled) ─────────────────────
|
||||
|
||||
/// Create the opt-in trace tables. No-op if all flags are false.
|
||||
pub fn prepare_trace_tables(
|
||||
&mut self,
|
||||
trace_instructions: bool,
|
||||
trace_imports: bool,
|
||||
trace_branches: bool,
|
||||
) -> anyhow::Result<()> {
|
||||
self.trace_instructions = trace_instructions;
|
||||
self.trace_imports = trace_imports;
|
||||
self.trace_branches = trace_branches;
|
||||
|
||||
if trace_instructions {
|
||||
self.conn.execute_batch("
|
||||
CREATE TABLE IF NOT EXISTS exec_trace (
|
||||
id INTEGER PRIMARY KEY,
|
||||
address INTEGER NOT NULL,
|
||||
cycle INTEGER NOT NULL,
|
||||
r3 INTEGER NOT NULL,
|
||||
r4 INTEGER NOT NULL,
|
||||
lr INTEGER NOT NULL,
|
||||
sp INTEGER NOT NULL
|
||||
);
|
||||
DELETE FROM exec_trace;
|
||||
")?;
|
||||
}
|
||||
|
||||
if trace_imports {
|
||||
self.conn.execute_batch("
|
||||
CREATE TABLE IF NOT EXISTS import_calls (
|
||||
id INTEGER PRIMARY KEY,
|
||||
address INTEGER NOT NULL,
|
||||
cycle INTEGER NOT NULL,
|
||||
module TEXT NOT NULL,
|
||||
ordinal INTEGER NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
arg_r3 INTEGER NOT NULL,
|
||||
arg_r4 INTEGER NOT NULL,
|
||||
arg_r5 INTEGER NOT NULL,
|
||||
arg_r6 INTEGER NOT NULL,
|
||||
return_value INTEGER NOT NULL
|
||||
);
|
||||
DELETE FROM import_calls;
|
||||
")?;
|
||||
}
|
||||
|
||||
if trace_branches {
|
||||
self.conn.execute_batch("
|
||||
CREATE TABLE IF NOT EXISTS branch_trace (
|
||||
id INTEGER PRIMARY KEY,
|
||||
cycle INTEGER NOT NULL,
|
||||
source INTEGER NOT NULL,
|
||||
target INTEGER NOT NULL,
|
||||
kind TEXT NOT NULL,
|
||||
lr INTEGER NOT NULL
|
||||
);
|
||||
DELETE FROM branch_trace;
|
||||
")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn log_instruction(&mut self, entry: ExecTraceEntry) {
|
||||
if !self.trace_instructions { return; }
|
||||
self.exec_buffer.push(entry);
|
||||
if self.exec_buffer.len() as u64 >= batch_size() {
|
||||
self.flush_exec();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn log_import_call(&mut self, entry: ImportCallEntry) {
|
||||
if !self.trace_imports { return; }
|
||||
self.import_buffer.push(entry);
|
||||
if self.import_buffer.len() >= 1000 {
|
||||
self.flush_imports();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn log_branch(&mut self, entry: BranchTraceEntry) {
|
||||
if !self.trace_branches { return; }
|
||||
self.branch_buffer.push(entry);
|
||||
if self.branch_buffer.len() as u64 >= batch_size() {
|
||||
self.flush_branches();
|
||||
}
|
||||
}
|
||||
|
||||
fn flush_exec(&mut self) {
|
||||
if self.exec_buffer.is_empty() { return; }
|
||||
let tx = self.conn.unchecked_transaction().unwrap();
|
||||
{
|
||||
let mut stmt = tx.prepare_cached(
|
||||
"INSERT INTO exec_trace (address, cycle, r3, r4, lr, sp) VALUES (?1, ?2, ?3, ?4, ?5, ?6)"
|
||||
).unwrap();
|
||||
for e in &self.exec_buffer {
|
||||
stmt.execute(params![
|
||||
e.address as i64,
|
||||
e.cycle as i64,
|
||||
e.r3 as i64,
|
||||
e.r4 as i64,
|
||||
e.lr as i64,
|
||||
e.sp as i64,
|
||||
]).ok();
|
||||
}
|
||||
}
|
||||
tx.commit().ok();
|
||||
self.exec_count += self.exec_buffer.len() as u64;
|
||||
self.exec_buffer.clear();
|
||||
}
|
||||
|
||||
fn flush_imports(&mut self) {
|
||||
if self.import_buffer.is_empty() { return; }
|
||||
let tx = self.conn.unchecked_transaction().unwrap();
|
||||
{
|
||||
let mut stmt = tx.prepare_cached(
|
||||
"INSERT INTO import_calls (address, cycle, module, ordinal, name, arg_r3, arg_r4, arg_r5, arg_r6, return_value)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)"
|
||||
).unwrap();
|
||||
for e in &self.import_buffer {
|
||||
stmt.execute(params![
|
||||
e.address as i64,
|
||||
e.cycle as i64,
|
||||
e.module,
|
||||
e.ordinal as i64,
|
||||
e.name,
|
||||
e.arg_r3 as i64,
|
||||
e.arg_r4 as i64,
|
||||
e.arg_r5 as i64,
|
||||
e.arg_r6 as i64,
|
||||
e.return_value as i64,
|
||||
]).ok();
|
||||
}
|
||||
}
|
||||
tx.commit().ok();
|
||||
self.import_count += self.import_buffer.len() as u64;
|
||||
self.import_buffer.clear();
|
||||
}
|
||||
|
||||
fn flush_branches(&mut self) {
|
||||
if self.branch_buffer.is_empty() { return; }
|
||||
let tx = self.conn.unchecked_transaction().unwrap();
|
||||
{
|
||||
let mut stmt = tx.prepare_cached(
|
||||
"INSERT INTO branch_trace (cycle, source, target, kind, lr) VALUES (?1, ?2, ?3, ?4, ?5)"
|
||||
).unwrap();
|
||||
for e in &self.branch_buffer {
|
||||
stmt.execute(params![
|
||||
e.cycle as i64,
|
||||
e.source as i64,
|
||||
e.target as i64,
|
||||
e.kind,
|
||||
e.lr as i64,
|
||||
]).ok();
|
||||
}
|
||||
}
|
||||
tx.commit().ok();
|
||||
self.branch_count += self.branch_buffer.len() as u64;
|
||||
self.branch_buffer.clear();
|
||||
}
|
||||
|
||||
/// Flush remaining trace buffers and create their indices.
|
||||
pub fn finalize_traces(&mut self) -> anyhow::Result<()> {
|
||||
self.flush_exec();
|
||||
self.flush_imports();
|
||||
self.flush_branches();
|
||||
|
||||
if self.trace_instructions {
|
||||
eprintln!("[db] creating idx_exec_trace_address...");
|
||||
self.conn.execute_batch("CREATE INDEX IF NOT EXISTS idx_exec_trace_address ON exec_trace(address);")?;
|
||||
eprintln!("[db] creating idx_exec_trace_cycle...");
|
||||
self.conn.execute_batch("CREATE INDEX IF NOT EXISTS idx_exec_trace_cycle ON exec_trace(cycle);")?;
|
||||
}
|
||||
if self.trace_imports {
|
||||
eprintln!("[db] creating idx_import_calls_name...");
|
||||
self.conn.execute_batch("CREATE INDEX IF NOT EXISTS idx_import_calls_name ON import_calls(name);")?;
|
||||
eprintln!("[db] creating idx_import_calls_cycle...");
|
||||
self.conn.execute_batch("CREATE INDEX IF NOT EXISTS idx_import_calls_cycle ON import_calls(cycle);")?;
|
||||
}
|
||||
if self.trace_branches {
|
||||
eprintln!("[db] creating idx_branch_trace_source...");
|
||||
self.conn.execute_batch("CREATE INDEX IF NOT EXISTS idx_branch_trace_source ON branch_trace(source);")?;
|
||||
eprintln!("[db] creating idx_branch_trace_target...");
|
||||
self.conn.execute_batch("CREATE INDEX IF NOT EXISTS idx_branch_trace_target ON branch_trace(target);")?;
|
||||
eprintln!("[db] creating idx_branch_trace_kind...");
|
||||
self.conn.execute_batch("CREATE INDEX IF NOT EXISTS idx_branch_trace_kind ON branch_trace(kind);")?;
|
||||
eprintln!("[db] creating idx_branch_trace_cycle...");
|
||||
self.conn.execute_batch("CREATE INDEX IF NOT EXISTS idx_branch_trace_cycle ON branch_trace(cycle);")?;
|
||||
}
|
||||
|
||||
eprintln!(
|
||||
"[db] trace totals: {} instructions, {} imports, {} branches",
|
||||
self.exec_count, self.import_count, self.branch_count
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Backwards-compatible wrapper that writes the full base + disasm layers.
|
||||
pub fn write_db(
|
||||
path: &Path,
|
||||
pe: &[u8],
|
||||
info: &DisasmInfo,
|
||||
func_analysis: &FuncAnalysis,
|
||||
labels: &HashMap<u32, String>,
|
||||
_import_map: &HashMap<u32, String>,
|
||||
xrefs: &XrefMap,
|
||||
) -> anyhow::Result<()> {
|
||||
let mut w = DbWriter::open_fresh(path)?;
|
||||
w.write_base(info)?;
|
||||
w.write_disasm(pe, info, func_analysis, labels, xrefs)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ── Helpers ────────────────────────────────────────────────────────────────
|
||||
|
||||
fn insert_metadata(conn: &Connection, info: &DisasmInfo) -> anyhow::Result<()> {
|
||||
let mut stmt = conn.prepare("INSERT INTO metadata (key, value) VALUES (?1, ?2)")?;
|
||||
stmt.execute(params!["image_base", format!("0x{:08X}", info.image_base)])?;
|
||||
stmt.execute(params!["entry_point", format!("0x{:08X}", info.entry_point)])?;
|
||||
if let Some(name) = info.original_pe_name {
|
||||
stmt.execute(params!["original_pe_name", name])?;
|
||||
}
|
||||
if let Some(title_id) = info.title_id {
|
||||
stmt.execute(params!["title_id", format!("0x{:08X}", title_id)])?;
|
||||
}
|
||||
if let Some(media_id) = info.media_id {
|
||||
stmt.execute(params!["media_id", format!("0x{:08X}", media_id)])?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn insert_sections(conn: &Connection, sections: &[xenia_xex::pe::PeSection]) -> anyhow::Result<()> {
|
||||
let mut stmt = conn.prepare(
|
||||
"INSERT INTO sections (name, virtual_address, virtual_size, raw_offset, raw_size, flags, is_code)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)"
|
||||
)?;
|
||||
for s in sections {
|
||||
stmt.execute(params![
|
||||
s.name,
|
||||
s.virtual_address as i64,
|
||||
s.virtual_size as i64,
|
||||
s.raw_offset as i64,
|
||||
s.raw_size as i64,
|
||||
s.flags as i64,
|
||||
s.is_code() as i32,
|
||||
])?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn insert_imports(conn: &Connection, info: &DisasmInfo) -> anyhow::Result<()> {
|
||||
let mut stmt = conn.prepare(
|
||||
"INSERT INTO imports (library, ordinal, name, record_type, address)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5)"
|
||||
)?;
|
||||
for lib in info.import_libraries {
|
||||
for imp in &lib.imports {
|
||||
let resolved = crate::resolve_ordinal(&lib.name, imp.ordinal);
|
||||
stmt.execute(params![
|
||||
lib.name,
|
||||
imp.ordinal as i64,
|
||||
resolved,
|
||||
imp.record_type as i64,
|
||||
imp.address as i64,
|
||||
])?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn insert_functions(
|
||||
conn: &Connection,
|
||||
func_analysis: &FuncAnalysis,
|
||||
labels: &HashMap<u32, String>,
|
||||
) -> anyhow::Result<()> {
|
||||
let mut stmt = conn.prepare(
|
||||
"INSERT INTO functions (address, name, end_address, frame_size, saved_gprs, is_leaf, is_saverestore)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)"
|
||||
)?;
|
||||
for (&addr, fi) in &func_analysis.functions {
|
||||
let name = labels.get(&addr)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| format!("sub_{addr:08X}"));
|
||||
stmt.execute(params![
|
||||
addr as i64,
|
||||
name,
|
||||
fi.end as i64,
|
||||
fi.frame_size as i64,
|
||||
fi.saved_gprs as i64,
|
||||
fi.is_leaf as i32,
|
||||
fi.is_saverestore as i32,
|
||||
])?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn insert_labels(
|
||||
conn: &Connection,
|
||||
labels: &HashMap<u32, String>,
|
||||
) -> anyhow::Result<()> {
|
||||
let mut stmt = conn.prepare(
|
||||
"INSERT OR IGNORE INTO labels (address, name, kind) VALUES (?1, ?2, ?3)"
|
||||
)?;
|
||||
for (&addr, name) in labels {
|
||||
let kind = if name.starts_with("sub_") || name == "entry_point" {
|
||||
"function"
|
||||
} else if name.starts_with("__imp_") {
|
||||
"import"
|
||||
} else if name.starts_with("__savegprlr_") || name.starts_with("__restgprlr_") {
|
||||
"saverestore"
|
||||
} else if name.starts_with("loc_") {
|
||||
"local"
|
||||
} else if name.starts_with("dat_") {
|
||||
"data"
|
||||
} else {
|
||||
"other"
|
||||
};
|
||||
stmt.execute(params![addr as i64, name, kind])?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn insert_instructions_streaming(
|
||||
conn: &Connection,
|
||||
pe: &[u8],
|
||||
info: &DisasmInfo,
|
||||
func_analysis: &FuncAnalysis,
|
||||
labels: &HashMap<u32, String>,
|
||||
) -> anyhow::Result<()> {
|
||||
let mut tx = conn.unchecked_transaction()?;
|
||||
let mut count: u64 = 0;
|
||||
let mut since_commit: u64 = 0;
|
||||
|
||||
for section in info.sections {
|
||||
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;
|
||||
|
||||
let mut current_func: Option<u32> = None;
|
||||
let mut addr = va_start;
|
||||
|
||||
while addr < va_end {
|
||||
let abs_addr = info.image_base + addr;
|
||||
let off = (addr - va_start) as usize + file_start;
|
||||
if off + 4 > pe.len() { break; }
|
||||
|
||||
if func_analysis.is_function_start(abs_addr) {
|
||||
current_func = Some(abs_addr);
|
||||
}
|
||||
|
||||
let instr = u32::from_be_bytes([pe[off], pe[off+1], pe[off+2], pe[off+3]]);
|
||||
let decoded = crate::ppc::disasm(instr, abs_addr);
|
||||
let (mnemonic, operands) = split_disasm(&decoded.base);
|
||||
|
||||
let (ext_mnemonic, ext_operands, ext_disasm): (Option<&str>, Option<&str>, Option<&str>) =
|
||||
match &decoded.ext {
|
||||
Some(ext) => {
|
||||
let (em, eo) = split_disasm(ext);
|
||||
(Some(em), Some(eo), Some(ext.as_str()))
|
||||
}
|
||||
None => (None, None, None),
|
||||
};
|
||||
let label = labels.get(&abs_addr).map(|s| s.as_str());
|
||||
|
||||
{
|
||||
let mut stmt = tx.prepare_cached(
|
||||
"INSERT INTO instructions (address, raw, mnemonic, operands, disasm, ext_mnemonic, ext_operands, ext_disasm, section, function, label)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)"
|
||||
)?;
|
||||
stmt.execute(params![
|
||||
abs_addr as i64,
|
||||
instr as i64,
|
||||
mnemonic,
|
||||
operands,
|
||||
decoded.base,
|
||||
ext_mnemonic,
|
||||
ext_operands,
|
||||
ext_disasm,
|
||||
section.name,
|
||||
current_func.map(|a| a as i64),
|
||||
label,
|
||||
])?;
|
||||
}
|
||||
|
||||
count += 1;
|
||||
since_commit += 1;
|
||||
addr += 4;
|
||||
|
||||
if since_commit >= batch_size() {
|
||||
tx.commit()?;
|
||||
eprintln!("[db] instructions: {count} committed");
|
||||
tx = conn.unchecked_transaction()?;
|
||||
since_commit = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tx.commit()?;
|
||||
eprintln!("[db] inserted {count} instructions");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn insert_xrefs_streaming(
|
||||
conn: &Connection,
|
||||
xrefs: &XrefMap,
|
||||
pe: &[u8],
|
||||
image_base: u32,
|
||||
func_analysis: &FuncAnalysis,
|
||||
labels: &HashMap<u32, String>,
|
||||
) -> anyhow::Result<()> {
|
||||
let mut tx = conn.unchecked_transaction()?;
|
||||
let mut count: u64 = 0;
|
||||
let mut since_commit: u64 = 0;
|
||||
|
||||
for (&target, refs) in xrefs {
|
||||
let target_label = labels.get(&target).map(|s| s.as_str());
|
||||
|
||||
for xref in refs {
|
||||
let kind = xref.kind.db_tag();
|
||||
|
||||
let instruction: Option<String> = {
|
||||
let off = xref.source.wrapping_sub(image_base) as usize;
|
||||
if off + 4 <= pe.len() {
|
||||
let raw = u32::from_be_bytes([pe[off], pe[off+1], pe[off+2], pe[off+3]]);
|
||||
let decoded = crate::ppc::disasm(raw, xref.source);
|
||||
let display = decoded.display().to_string();
|
||||
let (mnem, _) = split_disasm(&display);
|
||||
Some(mnem.to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
let source_func = func_analysis.functions
|
||||
.range(..=xref.source)
|
||||
.next_back()
|
||||
.map(|(&a, _)| a as i64);
|
||||
|
||||
let source_label = resolve_source_label(
|
||||
xref.source, func_analysis, labels,
|
||||
);
|
||||
|
||||
{
|
||||
let mut stmt = tx.prepare_cached(
|
||||
"INSERT INTO xrefs (source, target, kind, instruction, source_func, source_label, target_label)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)"
|
||||
)?;
|
||||
stmt.execute(params![
|
||||
xref.source as i64,
|
||||
target as i64,
|
||||
kind,
|
||||
instruction,
|
||||
source_func,
|
||||
source_label,
|
||||
target_label,
|
||||
])?;
|
||||
}
|
||||
|
||||
count += 1;
|
||||
since_commit += 1;
|
||||
|
||||
if since_commit >= batch_size() {
|
||||
tx.commit()?;
|
||||
eprintln!("[db] xrefs: {count} committed");
|
||||
tx = conn.unchecked_transaction()?;
|
||||
since_commit = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tx.commit()?;
|
||||
eprintln!("[db] inserted {count} xrefs");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Split "mnemonic operands" into (mnemonic, operands).
|
||||
fn split_disasm(disasm: &str) -> (&str, &str) {
|
||||
let trimmed = disasm.trim();
|
||||
if let Some(pos) = trimmed.find(|c: char| c.is_whitespace()) {
|
||||
let mnemonic = &trimmed[..pos];
|
||||
let operands = trimmed[pos..].trim_start();
|
||||
(mnemonic, operands)
|
||||
} else {
|
||||
(trimmed, "")
|
||||
}
|
||||
}
|
||||
318
crates/xenia-analysis/src/formatter.rs
Normal file
318
crates/xenia-analysis/src/formatter.rs
Normal file
@@ -0,0 +1,318 @@
|
||||
//! Assembly text output formatter for Xbox 360 disassembly.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::io::Write;
|
||||
|
||||
use xenia_xex::header::ImportLibrary;
|
||||
use xenia_xex::pe::PeSection;
|
||||
|
||||
use crate::func::FuncAnalysis;
|
||||
use crate::xref::{XrefKind, Xref, XrefMap, section_for_addr, resolve_source_label};
|
||||
|
||||
/// Metadata passed to the formatter (avoids exposing full Xex2Header internals).
|
||||
pub struct DisasmInfo<'a> {
|
||||
pub image_base: u32,
|
||||
pub entry_point: u32,
|
||||
pub original_pe_name: Option<&'a str>,
|
||||
pub title_id: Option<u32>,
|
||||
pub media_id: Option<u32>,
|
||||
pub sections: &'a [PeSection],
|
||||
pub import_libraries: &'a [ImportLibrary],
|
||||
}
|
||||
|
||||
/// Write full disassembly to the output stream.
|
||||
pub fn write_asm(
|
||||
out: &mut dyn Write,
|
||||
pe: &[u8],
|
||||
info: &DisasmInfo,
|
||||
func_analysis: &FuncAnalysis,
|
||||
labels: &HashMap<u32, String>,
|
||||
import_map: &HashMap<u32, String>,
|
||||
xrefs: &XrefMap,
|
||||
data_annotations: &HashMap<u32, (u32, XrefKind)>,
|
||||
) -> anyhow::Result<()> {
|
||||
// Header
|
||||
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, "; 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 ─────────────────────────────────────────────────────────────────")?;
|
||||
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)?;
|
||||
}
|
||||
|
||||
// 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)?;
|
||||
|
||||
let va_start = section.virtual_address;
|
||||
let va_end = va_start + section.virtual_size;
|
||||
let file_start = section.virtual_address as usize;
|
||||
|
||||
// Pre-sort data labels in this section for break-at-label hex dump
|
||||
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()
|
||||
.filter(|&&a| a >= sec_start && a < sec_end)
|
||||
.copied()
|
||||
.collect();
|
||||
addrs.sort();
|
||||
addrs
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
if section.is_code() {
|
||||
writeln!(out, ".text")?;
|
||||
writeln!(out)?;
|
||||
|
||||
let mut in_function = false;
|
||||
let mut addr = va_start;
|
||||
while addr < va_end {
|
||||
let abs_addr = info.image_base + addr;
|
||||
let off = (addr - va_start) as usize + file_start;
|
||||
if off + 4 > pe.len() { break; }
|
||||
|
||||
// Function start? Emit separator + header
|
||||
if let Some(fi) = func_analysis.get(abs_addr) {
|
||||
if in_function {
|
||||
writeln!(out, "; end function")?;
|
||||
}
|
||||
writeln!(out)?;
|
||||
writeln!(out, "; ──────────────────────────────────────────────────────────────────────────")?;
|
||||
|
||||
let lbl = labels.get(&abs_addr).cloned()
|
||||
.unwrap_or_else(|| format!("sub_{abs_addr:08X}"));
|
||||
|
||||
if fi.is_saverestore {
|
||||
writeln!(out, "; FUNCTION: {lbl} (save/restore GPR helper)")?;
|
||||
} else if fi.is_leaf {
|
||||
writeln!(out, "; FUNCTION: {lbl} (leaf)")?;
|
||||
} else {
|
||||
let mut details = Vec::new();
|
||||
if fi.frame_size > 0 {
|
||||
details.push(format!("frame={}", fi.frame_size));
|
||||
}
|
||||
if fi.saved_gprs > 0 {
|
||||
let first_reg = 32 - fi.saved_gprs;
|
||||
details.push(format!("saves r{first_reg}-r31"));
|
||||
}
|
||||
let detail_str = if details.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!(" ({})", details.join(", "))
|
||||
};
|
||||
writeln!(out, "; FUNCTION: {lbl}{detail_str}")?;
|
||||
}
|
||||
|
||||
// Xrefs for function entry
|
||||
if let Some(xref_lines) = format_xrefs(abs_addr, xrefs, func_analysis, labels) {
|
||||
for line in &xref_lines {
|
||||
writeln!(out, "{line}")?;
|
||||
}
|
||||
}
|
||||
|
||||
writeln!(out, "; ──────────────────────────────────────────────────────────────────────────")?;
|
||||
in_function = true;
|
||||
}
|
||||
|
||||
// Label
|
||||
if let Some(lbl) = labels.get(&abs_addr) {
|
||||
if !func_analysis.is_function_start(abs_addr) {
|
||||
writeln!(out)?;
|
||||
// Xrefs for local labels
|
||||
if let Some(xref_lines) = format_xrefs(abs_addr, xrefs, func_analysis, labels) {
|
||||
for line in &xref_lines {
|
||||
writeln!(out, "{line}")?;
|
||||
}
|
||||
}
|
||||
writeln!(out, "{lbl}:")?;
|
||||
} else {
|
||||
writeln!(out)?;
|
||||
writeln!(out, "{lbl}:")?;
|
||||
}
|
||||
}
|
||||
|
||||
// Import thunk annotation
|
||||
if let Some(imp_name) = import_map.get(&abs_addr) {
|
||||
writeln!(out, " ; IMPORT: {imp_name}")?;
|
||||
}
|
||||
|
||||
let instr = u32::from_be_bytes([
|
||||
pe[off], pe[off+1], pe[off+2], pe[off+3]
|
||||
]);
|
||||
|
||||
let decoded = crate::ppc::disasm(instr, abs_addr);
|
||||
let disasm_text = decoded.display().to_string();
|
||||
|
||||
// Annotate branch targets with label names
|
||||
let mut annotated = annotate_branch(&disasm_text, labels);
|
||||
|
||||
// Annotate data references
|
||||
if let Some(&(data_addr, kind)) = data_annotations.get(&abs_addr) {
|
||||
let tag = match kind {
|
||||
XrefKind::DataRead => "[R]",
|
||||
XrefKind::DataWrite => "[W]",
|
||||
_ => "[&]",
|
||||
};
|
||||
let sec = section_for_addr(data_addr, info.sections, info.image_base)
|
||||
.unwrap_or("?");
|
||||
let data_lbl = labels.get(&data_addr)
|
||||
.map(|s| format!(" = {s}"))
|
||||
.unwrap_or_default();
|
||||
if !annotated.contains("; ->") {
|
||||
annotated = format!("{annotated:<40} ; {tag} 0x{data_addr:08X} ({sec}){data_lbl}");
|
||||
} else {
|
||||
annotated = format!("{annotated} {tag} 0x{data_addr:08X} ({sec}){data_lbl}");
|
||||
}
|
||||
}
|
||||
|
||||
writeln!(out, " {:08X}: {:08X} {}", abs_addr, instr, annotated)?;
|
||||
addr += 4;
|
||||
}
|
||||
if in_function {
|
||||
writeln!(out, "; end function")?;
|
||||
}
|
||||
} else {
|
||||
// Data section: hex dump
|
||||
writeln!(out, ".data")?;
|
||||
writeln!(out)?;
|
||||
|
||||
let mut addr = va_start;
|
||||
while addr < va_end {
|
||||
let abs_addr = info.image_base + addr;
|
||||
let off = (addr - va_start) as usize + file_start;
|
||||
|
||||
if let Some(lbl) = labels.get(&abs_addr) {
|
||||
writeln!(out)?;
|
||||
// Xrefs for data labels
|
||||
if let Some(xref_lines) = format_xrefs(abs_addr, xrefs, func_analysis, labels) {
|
||||
for line in &xref_lines {
|
||||
writeln!(out, "{line}")?;
|
||||
}
|
||||
}
|
||||
writeln!(out, "{lbl}:")?;
|
||||
}
|
||||
|
||||
// Emit up to 16 bytes per line, but break at label boundaries
|
||||
let mut line_end = std::cmp::min(addr + 16, va_end);
|
||||
for &lbl_addr in §ion_labels_sorted {
|
||||
let lbl_va = lbl_addr - info.image_base;
|
||||
if lbl_va > addr && lbl_va < line_end {
|
||||
line_end = lbl_va;
|
||||
break;
|
||||
}
|
||||
if lbl_va >= line_end { break; }
|
||||
}
|
||||
let byte_count = (line_end - addr) as usize;
|
||||
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, " ")?; }
|
||||
}
|
||||
// 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 { '.' };
|
||||
write!(out, "{ch}")?;
|
||||
}
|
||||
writeln!(out, "|")?;
|
||||
|
||||
addr = line_end;
|
||||
}
|
||||
}
|
||||
writeln!(out)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
const XREF_DISPLAY_LIMIT: usize = 8;
|
||||
|
||||
fn format_xrefs(
|
||||
target: u32,
|
||||
xrefs: &XrefMap,
|
||||
func_analysis: &FuncAnalysis,
|
||||
labels: &HashMap<u32, String>,
|
||||
) -> Option<Vec<String>> {
|
||||
let refs = xrefs.get(&target)?;
|
||||
if refs.is_empty() { return None; }
|
||||
|
||||
let mut sorted: Vec<Xref> = refs.clone();
|
||||
sorted.sort();
|
||||
sorted.dedup();
|
||||
|
||||
let total = sorted.len();
|
||||
let mut lines = Vec::new();
|
||||
|
||||
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 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 { "" })); }
|
||||
|
||||
lines.push(format!("; XREF: {} ({})", summary_parts.join(", "), total));
|
||||
|
||||
for (i, xref) in sorted.iter().enumerate() {
|
||||
if i >= XREF_DISPLAY_LIMIT {
|
||||
lines.push(format!("; ... and {} more", total - XREF_DISPLAY_LIMIT));
|
||||
break;
|
||||
}
|
||||
let source_label = resolve_source_label(xref.source, func_analysis, labels);
|
||||
lines.push(format!("; {} from {}", xref.kind.tag(), source_label));
|
||||
}
|
||||
|
||||
Some(lines)
|
||||
}
|
||||
|
||||
fn annotate_branch(disasm: &str, labels: &HashMap<u32, String>) -> String {
|
||||
if let Some(pos) = disasm.find("0x") {
|
||||
let hex_start = pos + 2;
|
||||
let hex_end = disasm[hex_start..].find(|c: char| !c.is_ascii_hexdigit())
|
||||
.map(|i| hex_start + i)
|
||||
.unwrap_or(disasm.len());
|
||||
let hex_str = &disasm[hex_start..hex_end];
|
||||
if hex_str.len() == 8 {
|
||||
if let Ok(addr) = u32::from_str_radix(hex_str, 16) {
|
||||
if let Some(lbl) = labels.get(&addr) {
|
||||
return format!("{disasm:<40} ; -> {lbl}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
disasm.to_string()
|
||||
}
|
||||
444
crates/xenia-analysis/src/func.rs
Normal file
444
crates/xenia-analysis/src/func.rs
Normal file
@@ -0,0 +1,444 @@
|
||||
//! Function boundary detection via PPC prologue/epilogue pattern matching.
|
||||
//!
|
||||
//! Strategy (multi-pass):
|
||||
//! 1. Identify all `bl` (branch-and-link) targets — these are call sites,
|
||||
//! 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)
|
||||
//! 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)
|
||||
//! 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};
|
||||
|
||||
/// Information about a detected function.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FuncInfo {
|
||||
/// Absolute start address.
|
||||
pub start: u32,
|
||||
/// Absolute end address (exclusive — one past last instruction).
|
||||
pub end: u32,
|
||||
/// Stack frame size (0 if unknown / leaf).
|
||||
pub frame_size: u32,
|
||||
/// Number of saved GPRs (via __savegprlr helper), 0 if unknown.
|
||||
pub saved_gprs: u32,
|
||||
/// True if this is a leaf function (no bl, no frame setup).
|
||||
pub is_leaf: bool,
|
||||
/// True if this is a save/restore GPR helper stub.
|
||||
pub is_saverestore: bool,
|
||||
}
|
||||
|
||||
/// Result of the function analysis pass.
|
||||
pub struct FuncAnalysis {
|
||||
/// address → FuncInfo for every detected function, sorted by address.
|
||||
pub functions: BTreeMap<u32, FuncInfo>,
|
||||
/// Addresses in the save-GPR region (start of __savegprlr block).
|
||||
pub save_gpr_base: Option<u32>,
|
||||
/// Addresses in the restore-GPR region (start of __restgprlr block).
|
||||
pub restore_gpr_base: Option<u32>,
|
||||
}
|
||||
|
||||
// ── Instruction field helpers ──────────────────────────────────────────────
|
||||
|
||||
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; }
|
||||
let xo = bits(instr, 30, 21);
|
||||
if xo != 339 { return None; }
|
||||
let spr = (bits(instr, 20, 16) << 5) | bits(instr, 15, 11);
|
||||
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; }
|
||||
let xo = bits(instr, 30, 21);
|
||||
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; }
|
||||
let rs = bits(instr, 10, 6);
|
||||
let ra = bits(instr, 15, 11);
|
||||
if rs != 1 || ra != 1 { return None; }
|
||||
let d = ((instr & 0xFFFF) as i16) as i32;
|
||||
Some(d) // negative = frame allocation
|
||||
}
|
||||
|
||||
fn is_blr(instr: u32) -> bool {
|
||||
instr == 0x4E800020
|
||||
}
|
||||
|
||||
fn is_bctr(instr: u32) -> bool {
|
||||
instr == 0x4E800420
|
||||
}
|
||||
|
||||
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
|
||||
// Return the signed offset
|
||||
let li = instr & 0x03FFFFFC;
|
||||
Some(li)
|
||||
}
|
||||
|
||||
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
|
||||
Some(instr & 0x03FFFFFC)
|
||||
}
|
||||
|
||||
fn sign_ext26(val: u32) -> i32 {
|
||||
((val << 6) as i32) >> 6
|
||||
}
|
||||
|
||||
fn bl_target(instr: u32, addr: u32) -> Option<u32> {
|
||||
is_bl(instr).map(|off| addr.wrapping_add(sign_ext26(off) as u32))
|
||||
}
|
||||
|
||||
fn b_target(instr: u32, addr: u32) -> Option<u32> {
|
||||
is_b(instr).map(|off| addr.wrapping_add(sign_ext26(off) as u32))
|
||||
}
|
||||
|
||||
// ── Read instruction from PE ───────────────────────────────────────────────
|
||||
|
||||
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]]))
|
||||
}
|
||||
|
||||
// ── Detect the save/restore GPR helper stubs ───────────────────────────────
|
||||
//
|
||||
// These are a well-known pattern emitted by the Xbox 360 linker.
|
||||
// Save block: a cascade of `std rN, offset(r1)` for r14..r31 + `stw r12, -8(r1)` + `blr`
|
||||
// Restore: a cascade of `ld rN, offset(r1)` for r14..r31 + `lwz r12, -8(r1)` + `mtspr LR, r12` + `blr`
|
||||
//
|
||||
// We detect the save block by finding 18 consecutive `std rN, ...(r1)` instructions
|
||||
// for r14 through r31.
|
||||
|
||||
fn find_saverestore_stubs(
|
||||
pe: &[u8],
|
||||
image_base: u32,
|
||||
code_ranges: &[(u32, u32)], // (abs_start, abs_end)
|
||||
) -> (Option<u32>, Option<u32>) {
|
||||
let mut save_base = None;
|
||||
let mut restore_base = None;
|
||||
|
||||
for &(start, end) in code_ranges {
|
||||
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 {
|
||||
// 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; } };
|
||||
if op(check) != 62 || bits(check, 10, 6) != 14 + i || bits(check, 15, 11) != 1 {
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ok {
|
||||
save_base = Some(addr);
|
||||
// Restore block typically follows the save block
|
||||
// After save: stw r12, -8(r1) + blr, then restore starts
|
||||
let after_save = addr + 18 * 4 + 8; // skip stw r12 + blr
|
||||
let check = read_instr(pe, after_save, image_base);
|
||||
if let Some(c) = check {
|
||||
// Should be `ld r14, ...(r1)` — opcode 58, rT=14, rA=1
|
||||
if op(c) == 58 && bits(c, 10, 6) == 14 && bits(c, 15, 11) == 1 {
|
||||
restore_base = Some(after_save);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
addr += 4;
|
||||
}
|
||||
if save_base.is_some() { break; }
|
||||
}
|
||||
|
||||
(save_base, restore_base)
|
||||
}
|
||||
|
||||
// ── Main analysis ──────────────────────────────────────────────────────────
|
||||
|
||||
pub fn analyze(
|
||||
pe: &[u8],
|
||||
image_base: u32,
|
||||
entry_point: u32,
|
||||
code_sections: &[(u32, u32, u32)], // (va_start, va_size, flags)
|
||||
) -> FuncAnalysis {
|
||||
let code_ranges: Vec<(u32, u32)> = code_sections.iter()
|
||||
.map(|(va, sz, _)| (image_base + va, image_base + va + sz))
|
||||
.collect();
|
||||
|
||||
// 1. Find save/restore stubs
|
||||
let (save_base, restore_base) = find_saverestore_stubs(pe, image_base, &code_ranges);
|
||||
if let Some(sb) = save_base {
|
||||
eprintln!("[func] __savegprlr stub at 0x{sb:08X}");
|
||||
}
|
||||
if let Some(rb) = restore_base {
|
||||
eprintln!("[func] __restgprlr stub at 0x{rb:08X}");
|
||||
}
|
||||
|
||||
// Set of addresses in the save/restore region (to exclude from function detection)
|
||||
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); }
|
||||
}
|
||||
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); }
|
||||
}
|
||||
|
||||
// 2. Collect all bl targets as candidate function entries
|
||||
let mut call_targets: HashSet<u32> = HashSet::new();
|
||||
call_targets.insert(entry_point);
|
||||
|
||||
for &(start, end) in &code_ranges {
|
||||
let mut addr = start;
|
||||
while addr < end {
|
||||
if let Some(instr) = read_instr(pe, addr, image_base) {
|
||||
if 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;
|
||||
}
|
||||
}
|
||||
eprintln!("[func] {} bl targets (candidate functions)", call_targets.len());
|
||||
|
||||
// 3. For each candidate, detect prologue and walk to epilogue
|
||||
let mut functions: BTreeMap<u32, FuncInfo> = BTreeMap::new();
|
||||
|
||||
for &func_addr in &call_targets {
|
||||
if let Some(fi) = analyze_function(pe, image_base, func_addr, &code_ranges, save_base, restore_base) {
|
||||
functions.insert(func_addr, fi);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Label save/restore stubs as special functions — one entry for the whole block
|
||||
if let Some(sb) = save_base {
|
||||
// The save block is one cascade: entry at each rN, falls through to blr
|
||||
// Treat as a single function with the first entry point
|
||||
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,
|
||||
});
|
||||
}
|
||||
if let Some(rb) = restore_base {
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
eprintln!("[func] {} functions detected", functions.len());
|
||||
|
||||
FuncAnalysis {
|
||||
functions,
|
||||
save_gpr_base: save_base,
|
||||
restore_gpr_base: restore_base,
|
||||
}
|
||||
}
|
||||
|
||||
/// Analyze a single function starting at `func_addr`.
|
||||
fn analyze_function(
|
||||
pe: &[u8],
|
||||
image_base: u32,
|
||||
func_addr: u32,
|
||||
code_ranges: &[(u32, u32)],
|
||||
save_base: Option<u32>,
|
||||
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 instr0 = read_instr(pe, func_addr, image_base)?;
|
||||
|
||||
let mut frame_size: u32 = 0;
|
||||
let mut saved_gprs: u32 = 0;
|
||||
let mut is_leaf = false;
|
||||
let mut prologue_len: u32 = 0;
|
||||
|
||||
// Pattern A: mfspr rN, LR [+ bl __savegprlr_NN] + stwu r1, -N(r1)
|
||||
if let Some(_lr_reg) = is_mfspr_lr(instr0) {
|
||||
prologue_len = 4;
|
||||
let instr1 = read_instr(pe, func_addr + 4, image_base).unwrap_or(0);
|
||||
|
||||
// Check if next is bl to save stub
|
||||
if let Some(target) = bl_target(instr1, func_addr + 4) {
|
||||
if let Some(sb) = save_base {
|
||||
if 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);
|
||||
if let Some(d) = is_stwu_r1(stwu_instr) {
|
||||
frame_size = (-d) as u32;
|
||||
prologue_len += 4;
|
||||
}
|
||||
}
|
||||
// Pattern B: stwu r1, -N(r1) without mfspr (rare but possible for leaf-ish functions)
|
||||
else if let Some(d) = is_stwu_r1(instr0) {
|
||||
frame_size = (-d) as u32;
|
||||
prologue_len = 4;
|
||||
is_leaf = true; // no LR save = likely leaf (or uses CTR)
|
||||
}
|
||||
// Pattern C: no prologue — leaf function, just code until blr
|
||||
else {
|
||||
is_leaf = true;
|
||||
}
|
||||
|
||||
// Walk forward to find the end of the function
|
||||
let max_range = code_ranges.iter()
|
||||
.find(|&&(s, e)| func_addr >= s && func_addr < e)
|
||||
.map(|&(_, e)| e)
|
||||
.unwrap_or(func_addr + 0x100000);
|
||||
|
||||
let mut end_addr = func_addr + 4;
|
||||
let mut addr = func_addr + prologue_len;
|
||||
let scan_limit = std::cmp::min(addr + 0x100000, max_range); // 1MB max function
|
||||
|
||||
while addr < scan_limit {
|
||||
let instr = match read_instr(pe, addr, image_base) {
|
||||
Some(i) => i,
|
||||
None => break,
|
||||
};
|
||||
|
||||
// Epilogue: blr
|
||||
if is_blr(instr) {
|
||||
end_addr = addr + 4;
|
||||
// Check if the instruction after blr looks like padding or another function
|
||||
// Sometimes there's trailing data after blr; we stop at the first blr
|
||||
// that isn't inside a branch-over pattern
|
||||
break;
|
||||
}
|
||||
|
||||
// Epilogue: b __restgprlr_NN (tail branch into restore stub)
|
||||
if let Some(target) = b_target(instr, addr) {
|
||||
if let Some(rb) = restore_base {
|
||||
if target >= rb && target < rb + 18 * 4 {
|
||||
end_addr = addr + 4;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Epilogue: bctr (indirect tail call — end of function)
|
||||
if is_bctr(instr) {
|
||||
end_addr = addr + 4;
|
||||
break;
|
||||
}
|
||||
|
||||
addr += 4;
|
||||
}
|
||||
|
||||
// If we didn't find any epilogue within a reasonable range, still emit
|
||||
// the function but mark end at the scan point
|
||||
if end_addr <= func_addr + 4 && prologue_len > 0 {
|
||||
end_addr = addr;
|
||||
}
|
||||
|
||||
// Don't emit zero-size "functions" for addresses that are just data
|
||||
if end_addr <= func_addr + 4 && prologue_len == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(FuncInfo {
|
||||
start: func_addr,
|
||||
end: end_addr,
|
||||
frame_size,
|
||||
saved_gprs,
|
||||
is_leaf,
|
||||
is_saverestore: false,
|
||||
})
|
||||
}
|
||||
|
||||
// ── Label generation ───────────────────────────────────────────────────────
|
||||
|
||||
impl FuncAnalysis {
|
||||
/// Generate labels for all detected functions.
|
||||
/// Call targets with confirmed prologues get `sub_XXXXXXXX`.
|
||||
/// Save/restore entries get `__savegprlr_NN` / `__restgprlr_NN`.
|
||||
pub fn generate_labels(&self) -> HashMap<u32, String> {
|
||||
let mut labels = HashMap::new();
|
||||
|
||||
for (&addr, fi) in &self.functions {
|
||||
if fi.is_saverestore {
|
||||
// Label the block start, plus individual register entry points
|
||||
if let Some(sb) = self.save_gpr_base {
|
||||
if 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 {
|
||||
if 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}"));
|
||||
}
|
||||
|
||||
labels
|
||||
}
|
||||
|
||||
/// Returns true if `addr` is the start of a detected function.
|
||||
pub fn is_function_start(&self, addr: u32) -> bool {
|
||||
self.functions.contains_key(&addr)
|
||||
}
|
||||
|
||||
/// Get info for the function starting at `addr`.
|
||||
pub fn get(&self, addr: u32) -> Option<&FuncInfo> {
|
||||
self.functions.get(&addr)
|
||||
}
|
||||
}
|
||||
10
crates/xenia-analysis/src/lib.rs
Normal file
10
crates/xenia-analysis/src/lib.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
pub mod ppc;
|
||||
pub mod func;
|
||||
pub mod xref;
|
||||
pub mod db;
|
||||
pub mod formatter;
|
||||
|
||||
mod ordinals;
|
||||
pub use ordinals::resolve_ordinal;
|
||||
pub use xref::{XrefKind, Xref, XrefMap, resolve_source_label};
|
||||
pub use db::{DbWriter, ExecTraceEntry, ImportCallEntry, BranchTraceEntry};
|
||||
1
crates/xenia-analysis/src/ordinals.rs
Normal file
1
crates/xenia-analysis/src/ordinals.rs
Normal file
@@ -0,0 +1 @@
|
||||
include!(concat!(env!("OUT_DIR"), "/ordinals.rs"));
|
||||
1376
crates/xenia-analysis/src/ppc.rs
Normal file
1376
crates/xenia-analysis/src/ppc.rs
Normal file
File diff suppressed because it is too large
Load Diff
296
crates/xenia-analysis/src/xref.rs
Normal file
296
crates/xenia-analysis/src/xref.rs
Normal file
@@ -0,0 +1,296 @@
|
||||
//! Cross-reference analysis for Xbox 360 PE images.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use xenia_xex::pe::PeSection;
|
||||
use crate::func::FuncAnalysis;
|
||||
|
||||
// ── Cross-reference types ────────────────────────────────────────────────
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum XrefKind {
|
||||
Call, // bl
|
||||
Jump, // b (unconditional)
|
||||
Branch, // bc / bXX (conditional)
|
||||
DataRead, // lwz, lbz, lhz, lha, lfs, lfd, etc. from resolved address
|
||||
DataWrite, // stw, stb, sth, stfs, stfd, etc. to resolved address
|
||||
DataRef, // address computed via lis+addi/ori but not directly loaded/stored
|
||||
}
|
||||
|
||||
impl XrefKind {
|
||||
pub fn tag(self) -> &'static str {
|
||||
match self {
|
||||
XrefKind::Call => "call",
|
||||
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)
|
||||
}
|
||||
|
||||
pub fn db_tag(self) -> &'static str {
|
||||
self.tag()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct Xref {
|
||||
pub source: u32,
|
||||
pub kind: XrefKind,
|
||||
}
|
||||
|
||||
pub type XrefMap = HashMap<u32, Vec<Xref>>;
|
||||
|
||||
/// Result of cross-reference analysis.
|
||||
pub struct XrefResult {
|
||||
pub labels: HashMap<u32, String>,
|
||||
pub xrefs: XrefMap,
|
||||
pub data_annotations: HashMap<u32, (u32, XrefKind)>,
|
||||
}
|
||||
|
||||
/// Perform full cross-reference analysis on a PE image.
|
||||
pub fn analyze_xrefs(
|
||||
pe: &[u8],
|
||||
image_base: u32,
|
||||
entry_point: u32,
|
||||
sections: &[PeSection],
|
||||
func_analysis: &FuncAnalysis,
|
||||
import_map: &HashMap<u32, String>,
|
||||
) -> XrefResult {
|
||||
let func_labels = func_analysis.generate_labels();
|
||||
let mut labels: HashMap<u32, String> = func_labels;
|
||||
labels.insert(entry_point, "entry_point".to_string());
|
||||
|
||||
// Add import thunks as labels
|
||||
for (addr, name) in import_map {
|
||||
labels.insert(*addr, format!("__imp_{}", name.replace("::", "_")));
|
||||
}
|
||||
|
||||
// First pass: collect branch targets + cross-references from code sections
|
||||
let mut xrefs: XrefMap = HashMap::new();
|
||||
|
||||
for section in sections {
|
||||
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;
|
||||
|
||||
let mut addr = va_start;
|
||||
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]
|
||||
]);
|
||||
|
||||
collect_branch_target(instr, abs_addr, &mut labels, &mut xrefs);
|
||||
addr += 4;
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: resolve data references via lis+load/store pattern matching
|
||||
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))
|
||||
.collect();
|
||||
|
||||
for section in sections {
|
||||
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;
|
||||
|
||||
// Register state: track lis results. reg_hi[r] = Some(high_16_bits << 16)
|
||||
let mut reg_hi: [Option<u32>; 32] = [None; 32];
|
||||
|
||||
let mut addr = va_start;
|
||||
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]
|
||||
]);
|
||||
|
||||
let opcode = (instr >> 26) & 0x3F;
|
||||
let rd = ((instr >> 21) & 0x1F) as usize;
|
||||
let ra = ((instr >> 16) & 0x1F) as usize;
|
||||
let simm = ((instr & 0xFFFF) as i16) as i32;
|
||||
let uimm = (instr & 0xFFFF) as u32;
|
||||
|
||||
// Reset tracking on function boundaries (prologue = mfspr rN, LR)
|
||||
if opcode == 31 {
|
||||
let xo = (instr >> 1) & 0x3FF;
|
||||
if xo == 339 { // mfspr
|
||||
let spr = (((instr >> 16) & 0x1F) << 5) | ((instr >> 11) & 0x1F);
|
||||
if spr == 8 { // LR
|
||||
reg_hi = [None; 32];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match opcode {
|
||||
// lis rD, IMM (encoded as addis rD, r0, IMM)
|
||||
15 if ra == 0 => {
|
||||
reg_hi[rd] = Some(uimm << 16);
|
||||
}
|
||||
// addis rD, rA, IMM (rA != 0) — if rA has known lis, update
|
||||
15 if ra != 0 => {
|
||||
if let Some(base) = reg_hi[ra] {
|
||||
reg_hi[rd] = Some(base.wrapping_add(uimm << 16));
|
||||
} else {
|
||||
reg_hi[rd] = None;
|
||||
}
|
||||
}
|
||||
// addi rD, rA, IMM — compute full address if rA has known lis
|
||||
14 if ra != 0 => {
|
||||
if 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::DataRef));
|
||||
xrefs.entry(data_addr).or_default().push(Xref { source: abs_addr, kind: XrefKind::DataRef });
|
||||
labels.entry(data_addr).or_insert_with(|| format!("dat_{data_addr:08X}"));
|
||||
}
|
||||
reg_hi[rd] = Some(data_addr); // propagate for chained access
|
||||
} else {
|
||||
reg_hi[rd] = None;
|
||||
}
|
||||
}
|
||||
// ori rA, rS, UIMM — compute full address
|
||||
24 => {
|
||||
let rs = rd; // source is bits 21-25 for ori
|
||||
if let Some(base) = reg_hi[rs] {
|
||||
let data_addr = base | uimm;
|
||||
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 });
|
||||
labels.entry(data_addr).or_insert_with(|| format!("dat_{data_addr:08X}"));
|
||||
}
|
||||
reg_hi[ra] = Some(data_addr);
|
||||
} else {
|
||||
reg_hi[ra] = None;
|
||||
}
|
||||
}
|
||||
// Load instructions: lwz, lbz, lhz, lha, lfs, lfd, lwzu, etc.
|
||||
32 | 33 | 34 | 35 | 40 | 41 | 42 | 43 | 46 | 48 | 49 | 50 | 51 => {
|
||||
if ra != 0 {
|
||||
if 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 });
|
||||
labels.entry(data_addr).or_insert_with(|| format!("dat_{data_addr:08X}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Load into rD may clobber the tracked value
|
||||
reg_hi[rd] = None;
|
||||
}
|
||||
// Store instructions: stw, stb, sth, stfs, stfd, stwu, etc.
|
||||
36 | 37 | 38 | 39 | 44 | 45 | 47 | 52 | 53 | 54 | 55 => {
|
||||
if ra != 0 {
|
||||
if 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 });
|
||||
labels.entry(data_addr).or_insert_with(|| format!("dat_{data_addr:08X}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Any other instruction writing to rD: invalidate
|
||||
_ => {
|
||||
// Conservatively invalidate for instructions that modify rD
|
||||
// (most ALU ops, loads, etc.)
|
||||
if opcode != 18 && opcode != 16 && opcode != 17 { // skip branch/sc
|
||||
reg_hi[rd] = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addr += 4;
|
||||
}
|
||||
}
|
||||
|
||||
XrefResult { labels, xrefs, data_annotations }
|
||||
}
|
||||
|
||||
fn collect_branch_target(instr: u32, addr: u32, labels: &mut HashMap<u32, String>, xrefs: &mut XrefMap) {
|
||||
let op = (instr >> 26) & 0x3F;
|
||||
match op {
|
||||
18 => {
|
||||
// I-form: b/bl/ba/bla
|
||||
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 kind = if lk { XrefKind::Call } else { XrefKind::Jump };
|
||||
xrefs.entry(target).or_default().push(Xref { source: addr, kind });
|
||||
}
|
||||
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 });
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn sign_ext16(val: u32) -> i32 {
|
||||
((val << 16) as i32) >> 16
|
||||
}
|
||||
|
||||
fn sign_ext26(val: u32) -> i32 {
|
||||
((val << 6) as i32) >> 6
|
||||
}
|
||||
|
||||
fn is_in_ranges(addr: u32, ranges: &[(u32, u32)]) -> bool {
|
||||
ranges.iter().any(|&(start, end)| addr >= start && addr < end)
|
||||
}
|
||||
|
||||
/// Find which section a data address falls in.
|
||||
pub fn section_for_addr<'a>(addr: u32, sections: &'a [PeSection], image_base: u32) -> Option<&'a str> {
|
||||
for s in sections {
|
||||
let start = image_base + s.virtual_address;
|
||||
let end = start + s.virtual_size;
|
||||
if addr >= start && addr < end {
|
||||
return Some(&s.name);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Resolve a source address to "function_name+0xNN" or just "0xADDR".
|
||||
pub fn resolve_source_label(
|
||||
addr: u32,
|
||||
func_analysis: &FuncAnalysis,
|
||||
labels: &HashMap<u32, String>,
|
||||
) -> String {
|
||||
// Direct label hit?
|
||||
if let Some(lbl) = labels.get(&addr) {
|
||||
return lbl.clone();
|
||||
}
|
||||
|
||||
// Find the containing function (largest start <= addr)
|
||||
if let Some((&func_start, _fi)) = func_analysis.functions.range(..=addr).next_back() {
|
||||
if let Some(func_label) = labels.get(&func_start) {
|
||||
let offset = addr - func_start;
|
||||
return format!("{func_label}+0x{offset:X}");
|
||||
}
|
||||
}
|
||||
|
||||
format!("0x{addr:08X}")
|
||||
}
|
||||
28
crates/xenia-app/Cargo.toml
Normal file
28
crates/xenia-app/Cargo.toml
Normal file
@@ -0,0 +1,28 @@
|
||||
[package]
|
||||
name = "xenia-app"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[[bin]]
|
||||
name = "xenia-rs"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
xenia-types = { workspace = true }
|
||||
xenia-memory = { workspace = true }
|
||||
xenia-cpu = { workspace = true }
|
||||
xenia-xex = { workspace = true }
|
||||
xenia-vfs = { workspace = true }
|
||||
xenia-kernel = { workspace = true }
|
||||
xenia-gpu = { workspace = true }
|
||||
xenia-apu = { workspace = true }
|
||||
xenia-hid = { workspace = true }
|
||||
xenia-debugger = { workspace = true }
|
||||
xenia-analysis = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
812
crates/xenia-app/src/main.rs
Normal file
812
crates/xenia-app/src/main.rs
Normal file
@@ -0,0 +1,812 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{Parser, Subcommand};
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
use xenia_kernel::ModuleId;
|
||||
use xenia_memory::MemoryAccess;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "xenia-rs")]
|
||||
#[command(about = "Xbox 360 emulator for reverse engineering and preservation")]
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: Commands,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Commands {
|
||||
/// Disassemble a XEX file from its entry point
|
||||
Disasm {
|
||||
/// Path to XEX file
|
||||
path: String,
|
||||
/// Number of instructions to disassemble
|
||||
#[arg(short = 'n', default_value = "64")]
|
||||
count: usize,
|
||||
},
|
||||
/// Load and execute a XEX file with tracing
|
||||
Exec {
|
||||
/// Path to XEX file
|
||||
path: String,
|
||||
/// Maximum instructions to execute before stopping (unlimited if omitted)
|
||||
#[arg(short = 'n')]
|
||||
max_instructions: Option<u64>,
|
||||
/// SQLite database to write to. Includes the full static analysis
|
||||
/// that `dis --db` would produce, plus any opt-in trace tables.
|
||||
#[arg(long)]
|
||||
db: Option<String>,
|
||||
/// Log each executed instruction to the `exec_trace` table
|
||||
#[arg(long)]
|
||||
trace_instructions: bool,
|
||||
/// Log kernel/import calls to the `import_calls` table
|
||||
#[arg(long)]
|
||||
trace_imports: bool,
|
||||
/// Log taken branches (calls, returns, jumps) to the `branch_trace` table
|
||||
#[arg(long)]
|
||||
trace_branches: bool,
|
||||
/// Suppress banners, kernel-call logs, and final register dump
|
||||
/// (only errors, faults, halts, and the summary line are printed)
|
||||
#[arg(long)]
|
||||
quiet: bool,
|
||||
},
|
||||
/// Browse XISO disc image contents
|
||||
Browse {
|
||||
/// Path to XISO file
|
||||
path: String,
|
||||
},
|
||||
/// Display XEX header information
|
||||
Info {
|
||||
/// Path to XEX file
|
||||
path: String,
|
||||
},
|
||||
/// Extract PE image and metadata from a XEX file
|
||||
Extract {
|
||||
/// Path to XEX or ISO file
|
||||
path: String,
|
||||
/// Output directory (default: same directory as input)
|
||||
#[arg(short, long)]
|
||||
output: Option<String>,
|
||||
/// Write base tables (metadata, sections, imports) to a SQLite database
|
||||
#[arg(long)]
|
||||
db: Option<String>,
|
||||
},
|
||||
/// Full disassembly with function detection, cross-references, and optional database
|
||||
Dis {
|
||||
/// Path to XEX or ISO file
|
||||
path: String,
|
||||
/// Output .asm file (default: stdout)
|
||||
#[arg(short, long)]
|
||||
output: Option<String>,
|
||||
/// Output SQLite database (also includes the base extract tables)
|
||||
#[arg(long)]
|
||||
db: Option<String>,
|
||||
/// Suppress assembly text output (DB-only mode)
|
||||
#[arg(long)]
|
||||
quiet: bool,
|
||||
},
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
// Bump default log level to `warn` for quiet exec runs so kernel-call
|
||||
// tracing::info! spam is filtered out. RUST_LOG still wins if set.
|
||||
let exec_quiet = matches!(&cli.command, Commands::Exec { quiet: true, .. });
|
||||
let default_level = if exec_quiet { "warn" } else { "info" };
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(EnvFilter::from_default_env().add_directive(default_level.parse()?))
|
||||
.init();
|
||||
|
||||
match cli.command {
|
||||
Commands::Disasm { path, count } => cmd_disasm(&path, count),
|
||||
Commands::Exec {
|
||||
path,
|
||||
max_instructions,
|
||||
db,
|
||||
trace_instructions,
|
||||
trace_imports,
|
||||
trace_branches,
|
||||
quiet,
|
||||
} => cmd_exec(
|
||||
&path,
|
||||
max_instructions,
|
||||
db.as_deref(),
|
||||
trace_instructions,
|
||||
trace_imports,
|
||||
trace_branches,
|
||||
quiet,
|
||||
),
|
||||
Commands::Browse { path } => cmd_browse(&path),
|
||||
Commands::Info { path } => cmd_info(&path),
|
||||
Commands::Extract { path, output, db } => cmd_extract(&path, output.as_deref(), db.as_deref()),
|
||||
Commands::Dis { path, output, db, quiet } => cmd_dis(&path, output.as_deref(), db.as_deref(), quiet),
|
||||
}
|
||||
}
|
||||
|
||||
/// Load XEX data from a path. If the path is an ISO, extract default.xex from it.
|
||||
fn load_xex_data(path: &str) -> Result<Vec<u8>> {
|
||||
let lower = path.to_lowercase();
|
||||
if lower.ends_with(".iso") || lower.ends_with(".xiso") {
|
||||
use xenia_vfs::VfsDevice;
|
||||
println!("Detected disc image, extracting default.xex...");
|
||||
let disc = xenia_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 {
|
||||
Ok(std::fs::read(path)?)
|
||||
}
|
||||
}
|
||||
|
||||
fn cmd_info(path: &str) -> Result<()> {
|
||||
let data = load_xex_data(path)?;
|
||||
let header = xenia_xex::loader::parse_xex2_header(&data)?;
|
||||
|
||||
println!("=== XEX2 Header ===");
|
||||
println!("Magic: {:#010x}", header.magic);
|
||||
println!("Module Flags: {:#010x}", header.module_flags);
|
||||
println!("Header Size: {:#x}", header.header_size);
|
||||
println!("Headers: {}", header.header_count);
|
||||
|
||||
if let Some(entry) = xenia_xex::loader::get_entry_point(&header) {
|
||||
println!("Entry Point: {:#010x}", entry);
|
||||
}
|
||||
if let Some(base) = xenia_xex::loader::get_image_base(&header) {
|
||||
println!("Image Base: {:#010x}", base);
|
||||
}
|
||||
|
||||
println!("\n=== Optional Headers ===");
|
||||
for h in &header.optional_headers {
|
||||
println!(" Key: {:#010x} Value: {:#010x}", h.key, h.value);
|
||||
}
|
||||
|
||||
if let Some(ref sec) = header.security_info {
|
||||
println!("\n=== Security Info ===");
|
||||
println!("Image Size: {:#x}", sec.image_size);
|
||||
println!("Load Address: {:#010x}", sec.load_address);
|
||||
println!("Image Flags: {:#010x}", sec.image_flags);
|
||||
println!("Page Descs: {}", sec.page_descriptors.len());
|
||||
}
|
||||
|
||||
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"
|
||||
});
|
||||
if !ffi.basic_blocks.is_empty() {
|
||||
println!("Basic blocks: {}", ffi.basic_blocks.len());
|
||||
}
|
||||
if ffi.normal_window_size != 0 {
|
||||
println!("LZX Window: {:#x}", ffi.normal_window_size);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref name) = header.original_pe_name {
|
||||
println!("\nOriginal PE: {}", name);
|
||||
}
|
||||
|
||||
if let Some(ref ei) = header.execution_info {
|
||||
println!("\n=== Execution Info ===");
|
||||
println!("Title ID: {:#010x}", ei.title_id);
|
||||
println!("Media ID: {:#010x}", ei.media_id);
|
||||
println!("Disc: {} of {}", ei.disc_number, ei.disc_count);
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cmd_disasm(path: &str, count: usize) -> Result<()> {
|
||||
let data = load_xex_data(path)?;
|
||||
let header = xenia_xex::loader::parse_xex2_header(&data)?;
|
||||
|
||||
let entry = xenia_xex::loader::get_entry_point(&header)
|
||||
.ok_or_else(|| anyhow::anyhow!("No entry point found in XEX2 header"))?;
|
||||
let base = xenia_xex::loader::get_image_base(&header)
|
||||
.ok_or_else(|| anyhow::anyhow!("No image base found in XEX2 header"))?;
|
||||
|
||||
println!("Entry point: {:#010x}, Image base: {:#010x}", entry, base);
|
||||
|
||||
// Load and decompress the image
|
||||
let image_data = xenia_xex::loader::load_image(&data, &header)?;
|
||||
println!("Image loaded: {} bytes decompressed", image_data.len());
|
||||
println!("Disassembly from entry point ({} instructions):\n", count);
|
||||
|
||||
let entry_offset = (entry - base) as usize;
|
||||
if entry_offset + count * 4 <= image_data.len() {
|
||||
let block = xenia_cpu::disasm::disassemble_block(&image_data[entry_offset..], entry, count);
|
||||
for (addr, text) in block {
|
||||
println!(" {:#010x}: {}", addr, text);
|
||||
}
|
||||
} else {
|
||||
println!(" (entry point offset {:#x} is outside image bounds, image is {:#x} bytes)", entry_offset, image_data.len());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cmd_exec(
|
||||
path: &str,
|
||||
max_instructions: Option<u64>,
|
||||
db_path: Option<&str>,
|
||||
trace_instructions: bool,
|
||||
trace_imports: bool,
|
||||
trace_branches: bool,
|
||||
quiet: bool,
|
||||
) -> Result<()> {
|
||||
let data = load_xex_data(path)?;
|
||||
let mut header = xenia_xex::loader::parse_xex2_header(&data)?;
|
||||
|
||||
let entry = xenia_xex::loader::get_entry_point(&header)
|
||||
.ok_or_else(|| anyhow::anyhow!("No entry point found"))?;
|
||||
let base = xenia_xex::loader::get_image_base(&header)
|
||||
.ok_or_else(|| anyhow::anyhow!("No image base found"))?;
|
||||
|
||||
if !quiet {
|
||||
if let Some(ref ffi) = header.file_format_info {
|
||||
println!("Compression: {} (encryption: {})",
|
||||
match ffi.compression_type {
|
||||
0 => "none", 1 => "basic", 2 => "normal (LZX)", _ => "unknown"
|
||||
},
|
||||
match ffi.encryption_type {
|
||||
0 => "none", 1 => "normal (AES)", _ => "unknown"
|
||||
});
|
||||
}
|
||||
if !header.import_libraries.is_empty() {
|
||||
println!("Import libraries:");
|
||||
for lib in &header.import_libraries {
|
||||
println!(" {} ({} imports)", lib.name, lib.imports.len());
|
||||
}
|
||||
}
|
||||
println!("Loading XEX: entry={:#010x} base={:#010x}", entry, base);
|
||||
}
|
||||
|
||||
// Allocate guest memory
|
||||
let mut mem = xenia_memory::GuestMemory::new()
|
||||
.map_err(|e| anyhow::anyhow!("Failed to allocate guest memory: {}", e))?;
|
||||
|
||||
// Load and decompress the XEX image
|
||||
let image_data = xenia_xex::loader::load_image(&data, &header)?;
|
||||
|
||||
// Resolve import ordinals from PE image
|
||||
xenia_xex::loader::resolve_imports(&mut header, &image_data);
|
||||
|
||||
let alloc_size = ((image_data.len() + 4095) & !4095) as u32;
|
||||
let rw = xenia_memory::page_table::MemoryProtect::READ
|
||||
| xenia_memory::page_table::MemoryProtect::WRITE;
|
||||
mem.alloc(base, alloc_size, rw)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to allocate guest memory region: {}", e))?;
|
||||
mem.write_bulk(base, &image_data);
|
||||
|
||||
// ── Phase 1: Build import thunk map ──────────────────────────────────
|
||||
let mut thunk_map: HashMap<u32, (ModuleId, u16, String)> = HashMap::new();
|
||||
for lib in &header.import_libraries {
|
||||
let module = match lib.name.as_str() {
|
||||
"xboxkrnl.exe" => ModuleId::Xboxkrnl,
|
||||
"xam.xex" => ModuleId::Xam,
|
||||
_ => continue,
|
||||
};
|
||||
for imp in &lib.imports {
|
||||
if imp.record_type == 1 {
|
||||
let name = xenia_analysis::resolve_ordinal(&lib.name, imp.ordinal)
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_else(|| format!("ordinal_{:#06X}", imp.ordinal));
|
||||
thunk_map.insert(imp.address, (module, imp.ordinal, name));
|
||||
}
|
||||
}
|
||||
}
|
||||
if !quiet {
|
||||
println!("Import thunks mapped: {}", thunk_map.len());
|
||||
}
|
||||
|
||||
// ── Phase 2: CPU initialization per xenia-canary ─────────────────────
|
||||
// Allocate stack (1MB at 0x70000000)
|
||||
let stack_base = 0x7000_0000u32;
|
||||
let stack_size = 0x10_0000u32;
|
||||
mem.alloc(stack_base, stack_size, rw)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to allocate stack: {}", e))?;
|
||||
|
||||
// Allocate PCR (Processor Control Region) and TLS
|
||||
let pcr_addr = 0x7FFF_0000u32;
|
||||
let tls_addr = 0x7FFE_0000u32;
|
||||
mem.alloc(pcr_addr, 0x1000, rw)?;
|
||||
mem.alloc(tls_addr, 0x1000, rw)?;
|
||||
|
||||
// Initialize PCR structure
|
||||
mem.write_u32(pcr_addr, tls_addr); // PCR->tls_ptr
|
||||
mem.write_u32(pcr_addr + 0x100, 0x1000); // PCR->current_thread (fake)
|
||||
mem.write_u32(pcr_addr + 0x150, 0); // PCR->dpc_active
|
||||
|
||||
// Set up CPU context per xenia-canary/cpu/thread_state.cc
|
||||
let mut ctx = xenia_cpu::PpcContext::new();
|
||||
ctx.pc = entry;
|
||||
ctx.gpr[1] = (stack_base + stack_size - 0x80) as u64; // Stack pointer
|
||||
ctx.gpr[2] = 0x2000_0000; // RTOC
|
||||
ctx.gpr[13] = pcr_addr as u64; // PCR/TLS pointer
|
||||
ctx.msr = 0x9030; // Hardware-dumped MSR
|
||||
|
||||
// ── Phase 3: Data export patching (variable imports) ─────────────────
|
||||
for lib in &header.import_libraries {
|
||||
for imp in &lib.imports {
|
||||
if imp.record_type != 0 { continue; } // Only variable entries
|
||||
let addr = imp.address;
|
||||
match (lib.name.as_str(), imp.ordinal) {
|
||||
("xboxkrnl.exe", 0x0158) => {
|
||||
// XboxKrnlVersion: {major=2, minor=0, build=20000, qfe=0}
|
||||
mem.write_u16(addr, 2);
|
||||
mem.write_u16(addr + 2, 0);
|
||||
mem.write_u16(addr + 4, 20000);
|
||||
mem.write_u16(addr + 6, 0);
|
||||
}
|
||||
("xboxkrnl.exe", 0x0193) => {
|
||||
// XexExecutableModuleHandle -> image base
|
||||
mem.write_u32(addr, base);
|
||||
}
|
||||
("xboxkrnl.exe", 0x01C0) => {
|
||||
// VdGpuClockInMHz
|
||||
mem.write_u32(addr, 500);
|
||||
}
|
||||
_ => {
|
||||
// All other variable exports: write 0
|
||||
mem.write_u32(addr, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Phase 4: Set up kernel ───────────────────────────────────────────
|
||||
let mut kernel = xenia_kernel::KernelState::new();
|
||||
kernel.image_base = base;
|
||||
|
||||
// ── Phase 5: Set up SQLite DB with full static analysis + opt-in traces ──
|
||||
let mut db_writer: Option<xenia_analysis::DbWriter> = None;
|
||||
if let Some(db) = db_path {
|
||||
use std::collections::HashMap;
|
||||
|
||||
let sections = xenia_xex::pe::parse_sections(&image_data)?;
|
||||
|
||||
// Build import address -> name map (for xref analysis)
|
||||
let mut import_map: HashMap<u32, String> = HashMap::new();
|
||||
for lib in &header.import_libraries {
|
||||
for imp in &lib.imports {
|
||||
let resolved = xenia_analysis::resolve_ordinal(&lib.name, imp.ordinal);
|
||||
let name = match resolved {
|
||||
Some(n) => format!("{}::{}", lib.name, n),
|
||||
None => format!("{}::ordinal_{:#06X}", lib.name, imp.ordinal),
|
||||
};
|
||||
import_map.insert(imp.address, name);
|
||||
}
|
||||
}
|
||||
|
||||
// Function + xref analysis
|
||||
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 func_analysis = xenia_analysis::func::analyze(&image_data, base, entry, &code_sections);
|
||||
eprintln!("Functions detected: {}", func_analysis.functions.len());
|
||||
|
||||
let xref_result = xenia_analysis::xref::analyze_xrefs(
|
||||
&image_data, base, entry, §ions, &func_analysis, &import_map,
|
||||
);
|
||||
let total_xrefs: usize = xref_result.xrefs.values().map(|v| v.len()).sum();
|
||||
eprintln!("Labels: {}, Cross-references: {}", xref_result.labels.len(), total_xrefs);
|
||||
|
||||
let disasm_info = xenia_analysis::formatter::DisasmInfo {
|
||||
image_base: base,
|
||||
entry_point: entry,
|
||||
original_pe_name: header.original_pe_name.as_deref(),
|
||||
title_id: header.execution_info.as_ref().map(|e| e.title_id),
|
||||
media_id: header.execution_info.as_ref().map(|e| e.media_id),
|
||||
sections: §ions,
|
||||
import_libraries: &header.import_libraries,
|
||||
};
|
||||
|
||||
eprintln!("Writing database to {db}...");
|
||||
let mut w = xenia_analysis::DbWriter::open_fresh(std::path::Path::new(db))?;
|
||||
w.write_base(&disasm_info)?;
|
||||
w.write_disasm(&image_data, &disasm_info, &func_analysis, &xref_result.labels, &xref_result.xrefs)?;
|
||||
w.prepare_trace_tables(trace_instructions, trace_imports, trace_branches)?;
|
||||
db_writer = Some(w);
|
||||
}
|
||||
|
||||
// Set up debugger (in-memory trace disabled when any file-based trace is on)
|
||||
let mut debugger = xenia_debugger::Debugger::new();
|
||||
debugger.paused = false;
|
||||
debugger.step_mode = xenia_debugger::StepMode::Run;
|
||||
debugger.trace_enabled = !trace_instructions;
|
||||
|
||||
if !quiet {
|
||||
match max_instructions {
|
||||
Some(n) => println!("Starting execution (max {n} instructions)...\n"),
|
||||
None => println!("Starting execution (unlimited)...\n"),
|
||||
}
|
||||
}
|
||||
|
||||
// ── Phase 6: Execution loop with thunk interception ──────────────────
|
||||
use xenia_cpu::interpreter::{step, StepResult};
|
||||
use xenia_cpu::PpcOpcode;
|
||||
|
||||
let mut instruction_count: u64 = 0;
|
||||
let mut unimpl_count: u64 = 0;
|
||||
let mut import_count: u64 = 0;
|
||||
|
||||
loop {
|
||||
if let Some(limit) = max_instructions {
|
||||
if instruction_count >= limit {
|
||||
println!("\nReached max instruction count ({limit})");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Import thunk interception ──
|
||||
if let Some((module, ordinal, name)) = thunk_map.get(&ctx.pc) {
|
||||
let module = *module;
|
||||
let ordinal_u32 = *ordinal as u32;
|
||||
|
||||
let thunk_pc = ctx.pc;
|
||||
let args = [ctx.gpr[3], ctx.gpr[4], ctx.gpr[5], ctx.gpr[6]];
|
||||
|
||||
kernel.call_export(module, ordinal_u32, &mut ctx, &mut mem);
|
||||
|
||||
if let Some(ref mut db) = db_writer {
|
||||
db.log_import_call(xenia_analysis::ImportCallEntry {
|
||||
address: thunk_pc,
|
||||
cycle: ctx.cycle_count,
|
||||
module: match module {
|
||||
ModuleId::Xboxkrnl => "xboxkrnl.exe".to_string(),
|
||||
ModuleId::Xam => "xam.xex".to_string(),
|
||||
ModuleId::Xbdm => "xbdm.xex".to_string(),
|
||||
},
|
||||
ordinal: *ordinal,
|
||||
name: name.clone(),
|
||||
arg_r3: args[0],
|
||||
arg_r4: args[1],
|
||||
arg_r5: args[2],
|
||||
arg_r6: args[3],
|
||||
return_value: ctx.gpr[3],
|
||||
});
|
||||
}
|
||||
|
||||
// Simulate blr (return to caller)
|
||||
ctx.pc = ctx.lr as u32;
|
||||
ctx.cycle_count += 1;
|
||||
instruction_count += 1;
|
||||
import_count += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if PC is in mapped memory
|
||||
if !mem.is_mapped(ctx.pc) {
|
||||
println!("[{:>8}] FAULT: PC {:#010x} is in unmapped memory", instruction_count, ctx.pc);
|
||||
break;
|
||||
}
|
||||
|
||||
// Pre-step debugger
|
||||
debugger.pre_step(&ctx, &mem);
|
||||
|
||||
let pc_before = ctx.pc;
|
||||
// Decode the instruction word before step() so we can classify branches
|
||||
let raw_before = mem.read_u32(pc_before);
|
||||
let opcode_before = xenia_cpu::decode(raw_before, pc_before).opcode;
|
||||
|
||||
let result = step(&mut ctx, &mut mem);
|
||||
instruction_count += 1;
|
||||
|
||||
if let Some(ref mut db) = db_writer {
|
||||
db.log_instruction(xenia_analysis::ExecTraceEntry {
|
||||
address: pc_before,
|
||||
cycle: ctx.cycle_count,
|
||||
r3: ctx.gpr[3],
|
||||
r4: ctx.gpr[4],
|
||||
lr: ctx.lr,
|
||||
sp: ctx.gpr[1],
|
||||
});
|
||||
|
||||
// Branch detection — fallthrough (pc_before + 4) means untaken conditional
|
||||
if opcode_before.is_branch() && ctx.pc != pc_before.wrapping_add(4) {
|
||||
let lk = (raw_before & 1) == 1;
|
||||
let kind: &'static str = if lk {
|
||||
"call"
|
||||
} else if opcode_before == PpcOpcode::bclrx {
|
||||
"return"
|
||||
} else if opcode_before == PpcOpcode::bcctrx {
|
||||
"jump"
|
||||
} else {
|
||||
"branch"
|
||||
};
|
||||
db.log_branch(xenia_analysis::BranchTraceEntry {
|
||||
cycle: ctx.cycle_count,
|
||||
source: pc_before,
|
||||
target: ctx.pc,
|
||||
kind,
|
||||
lr: ctx.lr,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Post-step debugger
|
||||
debugger.post_step(&ctx, &mem);
|
||||
|
||||
match result {
|
||||
StepResult::Continue => {}
|
||||
StepResult::SystemCall => {
|
||||
tracing::warn!("SYSCALL at {:#010x}", pc_before);
|
||||
}
|
||||
StepResult::Unimplemented(op) => {
|
||||
unimpl_count += 1;
|
||||
if unimpl_count <= 50 {
|
||||
println!("[{:>8}] UNIMPL: {:?} at {:#010x}", instruction_count, op, pc_before);
|
||||
} else if unimpl_count == 51 {
|
||||
println!(" (suppressing further UNIMPL messages)");
|
||||
}
|
||||
}
|
||||
StepResult::Trap => {
|
||||
tracing::warn!("TRAP at {:#010x}", pc_before);
|
||||
}
|
||||
StepResult::Halted => {
|
||||
println!("[{:>8}] HALTED", instruction_count);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if debugger.should_break() {
|
||||
println!("[{:>8}] BREAK at {:#010x}", instruction_count, ctx.pc);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Finalize trace DB (flush buffers + create indices)
|
||||
if let Some(ref mut db) = db_writer {
|
||||
db.finalize_traces()?;
|
||||
}
|
||||
|
||||
if !quiet {
|
||||
println!("\n=== Final State ===");
|
||||
println!("PC: {:#010x}", ctx.pc);
|
||||
println!("LR: {:#010x}", ctx.lr as u32);
|
||||
println!("CTR: {:#010x}", ctx.ctr as u32);
|
||||
println!("CR: {:#010x}", ctx.cr());
|
||||
println!("XER: CA={} OV={} SO={}", ctx.xer_ca, ctx.xer_ov, ctx.xer_so);
|
||||
for i in 0..32 {
|
||||
if ctx.gpr[i] != 0 {
|
||||
println!("r{:<2}: {:#018x}", i, ctx.gpr[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("Executed {} instructions ({} import calls, {} unimplemented)",
|
||||
instruction_count, import_count, unimpl_count);
|
||||
if !quiet && db_writer.is_none() {
|
||||
println!("Trace log: {} entries", debugger.trace_log.len());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cmd_browse(path: &str) -> Result<()> {
|
||||
use xenia_vfs::VfsDevice;
|
||||
|
||||
let disc = xenia_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() {
|
||||
Ok(entries) => {
|
||||
for entry in entries {
|
||||
let kind = if entry.is_directory { "DIR " } else { "FILE" };
|
||||
println!(" {} {:>10} {}", kind, entry.size, entry.name);
|
||||
}
|
||||
}
|
||||
Err(e) => println!(" Error listing contents: {}", e),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Helper: load XEX, parse header, decompress PE, resolve imports, parse sections.
|
||||
fn load_and_prepare(path: &str) -> Result<(xenia_xex::Xex2Header, Vec<u8>, Vec<xenia_xex::pe::PeSection>)> {
|
||||
let data = load_xex_data(path)?;
|
||||
let mut header = xenia_xex::loader::parse_xex2_header(&data)?;
|
||||
|
||||
let entry = xenia_xex::loader::get_entry_point(&header)
|
||||
.ok_or_else(|| anyhow::anyhow!("No entry point found in XEX2 header"))?;
|
||||
let base = xenia_xex::loader::get_image_base(&header)
|
||||
.ok_or_else(|| anyhow::anyhow!("No image base found in XEX2 header"))?;
|
||||
|
||||
eprintln!("Entry point: {:#010x}, Image base: {:#010x}", entry, base);
|
||||
|
||||
let pe_image = xenia_xex::loader::load_image(&data, &header)?;
|
||||
eprintln!("Image loaded: {} bytes decompressed", pe_image.len());
|
||||
|
||||
// Resolve import ordinals and record types from the PE image
|
||||
xenia_xex::loader::resolve_imports(&mut header, &pe_image);
|
||||
|
||||
// Parse PE sections
|
||||
let sections = xenia_xex::pe::parse_sections(&pe_image)?;
|
||||
eprintln!("PE sections: {}", sections.len());
|
||||
|
||||
Ok((header, pe_image, sections))
|
||||
}
|
||||
|
||||
fn cmd_extract(path: &str, output_dir: Option<&str>, db_path: Option<&str>) -> Result<()> {
|
||||
use serde::Serialize;
|
||||
|
||||
let (header, pe_image, sections) = load_and_prepare(path)?;
|
||||
|
||||
let entry = xenia_xex::loader::get_entry_point(&header).unwrap();
|
||||
let base = xenia_xex::loader::get_image_base(&header).unwrap();
|
||||
let image_size = header.security_info.as_ref().map(|s| s.image_size).unwrap_or(0);
|
||||
|
||||
// Build JSON-serializable info struct
|
||||
#[derive(Serialize)]
|
||||
struct Xex2Info<'a> {
|
||||
module_flags: u32,
|
||||
image_base: u32,
|
||||
entry_point: u32,
|
||||
image_size: u32,
|
||||
original_pe_name: Option<&'a str>,
|
||||
execution_info: &'a Option<xenia_xex::header::ExecutionInfo>,
|
||||
import_libraries: &'a [xenia_xex::header::ImportLibrary],
|
||||
sections: &'a [xenia_xex::pe::PeSection],
|
||||
}
|
||||
|
||||
let info = Xex2Info {
|
||||
module_flags: header.module_flags,
|
||||
image_base: base,
|
||||
entry_point: entry,
|
||||
image_size,
|
||||
original_pe_name: header.original_pe_name.as_deref(),
|
||||
execution_info: &header.execution_info,
|
||||
import_libraries: &header.import_libraries,
|
||||
sections: §ions,
|
||||
};
|
||||
|
||||
// Determine output directory
|
||||
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(),
|
||||
};
|
||||
std::fs::create_dir_all(&out_dir)?;
|
||||
|
||||
let stem = input_path.file_stem()
|
||||
.and_then(|s| s.to_str())
|
||||
.unwrap_or("output");
|
||||
|
||||
// Write PE image
|
||||
let pe_path = out_dir.join(format!("{stem}.pe"));
|
||||
std::fs::write(&pe_path, &pe_image)?;
|
||||
eprintln!("Wrote PE image: {} ({} bytes)", pe_path.display(), pe_image.len());
|
||||
|
||||
// Write JSON metadata
|
||||
let json_path = out_dir.join(format!("{stem}.xex.json"));
|
||||
let json = serde_json::to_string_pretty(&info)?;
|
||||
std::fs::write(&json_path, &json)?;
|
||||
eprintln!("Wrote metadata: {}", json_path.display());
|
||||
|
||||
// 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);
|
||||
if let Some(ref ei) = header.execution_info {
|
||||
println!("Title ID: 0x{:08X} Media ID: 0x{:08X}", ei.title_id, ei.media_id);
|
||||
}
|
||||
|
||||
// Write base tables to SQLite if requested
|
||||
if let Some(db) = db_path {
|
||||
let disasm_info = xenia_analysis::formatter::DisasmInfo {
|
||||
image_base: base,
|
||||
entry_point: entry,
|
||||
original_pe_name: header.original_pe_name.as_deref(),
|
||||
title_id: header.execution_info.as_ref().map(|e| e.title_id),
|
||||
media_id: header.execution_info.as_ref().map(|e| e.media_id),
|
||||
sections: §ions,
|
||||
import_libraries: &header.import_libraries,
|
||||
};
|
||||
eprintln!("Writing base tables to {db}...");
|
||||
let mut w = xenia_analysis::DbWriter::open_fresh(std::path::Path::new(db))?;
|
||||
w.write_base(&disasm_info)?;
|
||||
eprintln!("Database written: {db}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cmd_dis(path: &str, output: Option<&str>, db_path: Option<&str>, quiet: bool) -> Result<()> {
|
||||
use std::collections::HashMap;
|
||||
|
||||
let (header, pe_image, sections) = load_and_prepare(path)?;
|
||||
|
||||
let entry = xenia_xex::loader::get_entry_point(&header).unwrap();
|
||||
let base = xenia_xex::loader::get_image_base(&header).unwrap();
|
||||
|
||||
// Build import address -> name map
|
||||
let mut import_map: HashMap<u32, String> = HashMap::new();
|
||||
for lib in &header.import_libraries {
|
||||
for imp in &lib.imports {
|
||||
let resolved = xenia_analysis::resolve_ordinal(&lib.name, imp.ordinal);
|
||||
let name = match resolved {
|
||||
Some(n) => format!("{}::{}", lib.name, n),
|
||||
None => format!("{}::ordinal_{:#06X}", lib.name, imp.ordinal),
|
||||
};
|
||||
import_map.insert(imp.address, name);
|
||||
}
|
||||
}
|
||||
eprintln!("Resolved {} import thunks", import_map.len());
|
||||
|
||||
// Function analysis
|
||||
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 func_analysis = xenia_analysis::func::analyze(&pe_image, base, entry, &code_sections);
|
||||
eprintln!("Functions detected: {}", func_analysis.functions.len());
|
||||
|
||||
// Cross-reference analysis
|
||||
let xref_result = xenia_analysis::xref::analyze_xrefs(
|
||||
&pe_image, base, entry, §ions, &func_analysis, &import_map,
|
||||
);
|
||||
let total_xrefs: usize = xref_result.xrefs.values().map(|v| v.len()).sum();
|
||||
eprintln!("Labels: {}, Cross-references: {}", xref_result.labels.len(), total_xrefs);
|
||||
|
||||
// Build DisasmInfo
|
||||
let disasm_info = xenia_analysis::formatter::DisasmInfo {
|
||||
image_base: base,
|
||||
entry_point: entry,
|
||||
original_pe_name: header.original_pe_name.as_deref(),
|
||||
title_id: header.execution_info.as_ref().map(|e| e.title_id),
|
||||
media_id: header.execution_info.as_ref().map(|e| e.media_id),
|
||||
sections: §ions,
|
||||
import_libraries: &header.import_libraries,
|
||||
};
|
||||
|
||||
// SQLite database output (base + disasm layers)
|
||||
if let Some(db) = db_path {
|
||||
eprintln!("Writing database to {db}...");
|
||||
let mut w = xenia_analysis::DbWriter::open_fresh(std::path::Path::new(db))?;
|
||||
w.write_base(&disasm_info)?;
|
||||
w.write_disasm(
|
||||
&pe_image,
|
||||
&disasm_info,
|
||||
&func_analysis,
|
||||
&xref_result.labels,
|
||||
&xref_result.xrefs,
|
||||
)?;
|
||||
eprintln!("Database written: {db}");
|
||||
}
|
||||
|
||||
// Assembly output (skipped when --quiet and no --output specified)
|
||||
if !quiet || output.is_some() {
|
||||
let mut out: Box<dyn std::io::Write> = match output {
|
||||
Some(path) => Box::new(std::io::BufWriter::new(std::fs::File::create(path)?)),
|
||||
None => Box::new(std::io::BufWriter::new(std::io::stdout().lock())),
|
||||
};
|
||||
|
||||
xenia_analysis::formatter::write_asm(
|
||||
&mut *out,
|
||||
&pe_image,
|
||||
&disasm_info,
|
||||
&func_analysis,
|
||||
&xref_result.labels,
|
||||
&import_map,
|
||||
&xref_result.xrefs,
|
||||
&xref_result.data_annotations,
|
||||
)?;
|
||||
|
||||
if let Some(path) = output {
|
||||
eprintln!("Wrote disassembly: {path}");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
10
crates/xenia-apu/Cargo.toml
Normal file
10
crates/xenia-apu/Cargo.toml
Normal file
@@ -0,0 +1,10 @@
|
||||
[package]
|
||||
name = "xenia-apu"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
xenia-types = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
16
crates/xenia-apu/src/lib.rs
Normal file
16
crates/xenia-apu/src/lib.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
/// Audio processing unit stub. Logging only for now.
|
||||
pub struct AudioSystem {
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
impl AudioSystem {
|
||||
pub fn new() -> Self {
|
||||
Self { enabled: false }
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AudioSystem {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
12
crates/xenia-cpu/Cargo.toml
Normal file
12
crates/xenia-cpu/Cargo.toml
Normal file
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "xenia-cpu"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
xenia-types = { workspace = true }
|
||||
xenia-memory = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
bitflags = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
191
crates/xenia-cpu/src/context.rs
Normal file
191
crates/xenia-cpu/src/context.rs
Normal file
@@ -0,0 +1,191 @@
|
||||
use xenia_types::Vec128;
|
||||
|
||||
/// Condition register field (one of CR0-CR7).
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct CrField {
|
||||
pub lt: bool,
|
||||
pub gt: bool,
|
||||
pub eq: bool,
|
||||
pub so: bool,
|
||||
}
|
||||
|
||||
impl CrField {
|
||||
pub fn as_u8(&self) -> u8 {
|
||||
((self.lt as u8) << 3) | ((self.gt as u8) << 2) | ((self.eq as u8) << 1) | (self.so as u8)
|
||||
}
|
||||
|
||||
pub fn from_u8(val: u8) -> Self {
|
||||
Self {
|
||||
lt: val & 8 != 0,
|
||||
gt: val & 4 != 0,
|
||||
eq: val & 2 != 0,
|
||||
so: val & 1 != 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// SPR (Special Purpose Register) numbers used by mfspr/mtspr.
|
||||
pub mod spr {
|
||||
pub const XER: u32 = 1;
|
||||
pub const LR: u32 = 8;
|
||||
pub const CTR: u32 = 9;
|
||||
pub const TBL: u32 = 268;
|
||||
pub const TBU: u32 = 269;
|
||||
pub const SPRG0: u32 = 272;
|
||||
pub const SPRG1: u32 = 273;
|
||||
pub const SPRG2: u32 = 274;
|
||||
pub const SPRG3: u32 = 275;
|
||||
pub const PVR: u32 = 287;
|
||||
pub const PIR: u32 = 1023;
|
||||
}
|
||||
|
||||
/// PowerPC processor context. Holds all register state for one guest thread.
|
||||
/// Mirrors PPCContext from ppc_context.h, minus JIT-specific fields.
|
||||
#[repr(C, align(64))]
|
||||
pub struct PpcContext {
|
||||
// General purpose registers (R0-R31)
|
||||
pub gpr: [u64; 32],
|
||||
// Count register
|
||||
pub ctr: u64,
|
||||
// Link register
|
||||
pub lr: u64,
|
||||
// Machine state register
|
||||
pub msr: u64,
|
||||
// Floating-point registers (F0-F31)
|
||||
pub fpr: [f64; 32],
|
||||
// VMX128 vector registers (V0-V127, Xbox 360 extended set)
|
||||
pub vr: [Vec128; 128],
|
||||
|
||||
// Condition register fields (CR0-CR7)
|
||||
pub cr: [CrField; 8],
|
||||
// Floating-point status and control register
|
||||
pub fpscr: u32,
|
||||
// XER register (split for easy individual updates)
|
||||
pub xer_ca: u8,
|
||||
pub xer_ov: u8,
|
||||
pub xer_so: u8,
|
||||
// Altivec VSCR saturation bit
|
||||
pub vscr_sat: u8,
|
||||
|
||||
// Program counter
|
||||
pub pc: u32,
|
||||
// Reservation address/value for lwarx/stwcx
|
||||
pub reserved_addr: u32,
|
||||
pub reserved_val: u64,
|
||||
pub has_reservation: bool,
|
||||
|
||||
// Thread ID (for kernel use)
|
||||
pub thread_id: u32,
|
||||
|
||||
// Cycle counter for timing
|
||||
pub cycle_count: u64,
|
||||
|
||||
// Time base (incremented each instruction for debugging)
|
||||
pub timebase: u64,
|
||||
}
|
||||
|
||||
impl PpcContext {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
gpr: [0; 32],
|
||||
ctr: 0,
|
||||
lr: 0,
|
||||
msr: 0,
|
||||
fpr: [0.0; 32],
|
||||
vr: [Vec128::ZERO; 128],
|
||||
cr: [CrField::default(); 8],
|
||||
fpscr: 0,
|
||||
xer_ca: 0,
|
||||
xer_ov: 0,
|
||||
xer_so: 0,
|
||||
vscr_sat: 0,
|
||||
pc: 0,
|
||||
reserved_addr: 0,
|
||||
reserved_val: 0,
|
||||
has_reservation: false,
|
||||
thread_id: 0,
|
||||
cycle_count: 0,
|
||||
timebase: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the full 32-bit condition register.
|
||||
pub fn cr(&self) -> u32 {
|
||||
let mut val = 0u32;
|
||||
for (i, field) in self.cr.iter().enumerate() {
|
||||
val |= (field.as_u8() as u32) << (28 - i * 4);
|
||||
}
|
||||
val
|
||||
}
|
||||
|
||||
/// Set the full 32-bit condition register.
|
||||
pub fn set_cr(&mut self, val: u32) {
|
||||
for i in 0..8 {
|
||||
self.cr[i] = CrField::from_u8(((val >> (28 - i * 4)) & 0xF) as u8);
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a single CR bit by absolute bit number (0-31).
|
||||
pub fn get_cr_bit(&self, bit: u32) -> bool {
|
||||
let field = (bit / 4) as usize;
|
||||
let sub = bit % 4;
|
||||
match sub {
|
||||
0 => self.cr[field].lt,
|
||||
1 => self.cr[field].gt,
|
||||
2 => self.cr[field].eq,
|
||||
3 => self.cr[field].so,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set a single CR bit by absolute bit number (0-31).
|
||||
pub fn set_cr_bit(&mut self, bit: u32, val: bool) {
|
||||
let field = (bit / 4) as usize;
|
||||
let sub = bit % 4;
|
||||
match sub {
|
||||
0 => self.cr[field].lt = val,
|
||||
1 => self.cr[field].gt = val,
|
||||
2 => self.cr[field].eq = val,
|
||||
3 => self.cr[field].so = val,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Update a condition register field based on a comparison result (signed).
|
||||
pub fn update_cr_signed(&mut self, field: usize, val: i64) {
|
||||
self.cr[field] = CrField {
|
||||
lt: val < 0,
|
||||
gt: val > 0,
|
||||
eq: val == 0,
|
||||
so: self.xer_so != 0,
|
||||
};
|
||||
}
|
||||
|
||||
/// Update a condition register field based on a comparison result (unsigned).
|
||||
pub fn update_cr_unsigned(&mut self, field: usize, a: u64, b: u64) {
|
||||
self.cr[field] = CrField {
|
||||
lt: a < b,
|
||||
gt: a > b,
|
||||
eq: a == b,
|
||||
so: self.xer_so != 0,
|
||||
};
|
||||
}
|
||||
|
||||
/// Get the full XER register value.
|
||||
pub fn xer(&self) -> u32 {
|
||||
((self.xer_so as u32) << 31) | ((self.xer_ov as u32) << 30) | ((self.xer_ca as u32) << 29)
|
||||
}
|
||||
|
||||
/// Set XER from a full 32-bit value.
|
||||
pub fn set_xer(&mut self, val: u32) {
|
||||
self.xer_so = ((val >> 31) & 1) as u8;
|
||||
self.xer_ov = ((val >> 30) & 1) as u8;
|
||||
self.xer_ca = ((val >> 29) & 1) as u8;
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PpcContext {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
819
crates/xenia-cpu/src/decoder.rs
Normal file
819
crates/xenia-cpu/src/decoder.rs
Normal file
@@ -0,0 +1,819 @@
|
||||
use crate::opcode::PpcOpcode;
|
||||
|
||||
/// Extract bits [a..=b] from a 32-bit value (PPC bit numbering: 0 = MSB).
|
||||
#[inline(always)]
|
||||
const fn extract_bits(v: u32, a: u32, b: u32) -> u32 {
|
||||
(v >> (32 - 1 - b)) & ((1 << (b - a + 1)) - 1)
|
||||
}
|
||||
|
||||
/// Decoded PPC instruction with extracted operand fields.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct DecodedInstr {
|
||||
pub opcode: PpcOpcode,
|
||||
pub raw: u32,
|
||||
pub addr: u32,
|
||||
}
|
||||
|
||||
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) }
|
||||
|
||||
/// 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() }
|
||||
|
||||
/// rA (bits 11-15)
|
||||
#[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 }
|
||||
|
||||
/// rC (bits 21-25) - for 4-operand instructions
|
||||
#[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 }
|
||||
|
||||
/// D-form displacement (signed, bits 16-31)
|
||||
#[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 }
|
||||
|
||||
/// LI field for branch (bits 6-29, sign-extended, shifted left 2)
|
||||
#[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;
|
||||
sign_extended << 2
|
||||
}
|
||||
|
||||
/// BD field for conditional branch (bits 16-29, sign-extended, shifted left 2)
|
||||
#[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) }
|
||||
|
||||
/// BI field (bits 11-15) - branch condition
|
||||
#[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 }
|
||||
|
||||
/// LK bit (bit 31) - link (update LR)
|
||||
#[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 }
|
||||
|
||||
/// OE bit (bit 21) - overflow enable
|
||||
#[inline] pub fn oe(&self) -> bool { extract_bits(self.raw, 21, 21) != 0 }
|
||||
|
||||
/// 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) }
|
||||
|
||||
/// SH field (bits 16-20) for shift instructions
|
||||
#[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 {
|
||||
(extract_bits(self.raw, 16, 20) << 1) | extract_bits(self.raw, 30, 30)
|
||||
}
|
||||
|
||||
/// SPR field (bits 11-20, swapped halves)
|
||||
#[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) }
|
||||
|
||||
/// crfD (bits 6-8) - condition register field destination
|
||||
#[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 }
|
||||
|
||||
/// L bit (bit 10) - 64-bit compare
|
||||
#[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) }
|
||||
/// crbA (bits 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) }
|
||||
|
||||
// VMX128 field extractors
|
||||
|
||||
/// VA128 (bits 6-10, plus bit from 29)
|
||||
#[inline] pub fn va128(&self) -> usize {
|
||||
(extract_bits(self.raw, 6, 10) | (extract_bits(self.raw, 29, 29) << 5)) as usize
|
||||
}
|
||||
|
||||
/// VB128 (bits 16-20, plus bits from 28, 30)
|
||||
#[inline] pub fn vb128(&self) -> usize {
|
||||
(extract_bits(self.raw, 16, 20)
|
||||
| (extract_bits(self.raw, 28, 28) << 5)
|
||||
| (extract_bits(self.raw, 30, 30) << 6)) as usize
|
||||
}
|
||||
|
||||
/// VD128 (bits 6-10, plus bits from 21, 22)
|
||||
#[inline] pub fn vd128(&self) -> usize {
|
||||
(extract_bits(self.raw, 6, 10)
|
||||
| (extract_bits(self.raw, 21, 21) << 5)
|
||||
| (extract_bits(self.raw, 22, 22) << 6)) as usize
|
||||
}
|
||||
|
||||
/// VS128 - same encoding as VD128
|
||||
#[inline] pub fn vs128(&self) -> usize { self.vd128() }
|
||||
|
||||
/// NB field (bits 16-20) for lswi/stswi
|
||||
#[inline] pub fn nb(&self) -> u32 { extract_bits(self.raw, 16, 20) }
|
||||
}
|
||||
|
||||
/// Decode a 32-bit PPC instruction into its opcode.
|
||||
/// Direct translation of the C++ LookupOpcode from ppc_opcode_lookup_gen.cc.
|
||||
pub fn decode(raw: u32, addr: u32) -> DecodedInstr {
|
||||
let opcode = lookup_opcode(raw);
|
||||
DecodedInstr { opcode, raw, addr }
|
||||
}
|
||||
|
||||
fn lookup_opcode(code: u32) -> PpcOpcode {
|
||||
match extract_bits(code, 0, 5) {
|
||||
2 => PpcOpcode::tdi,
|
||||
3 => PpcOpcode::twi,
|
||||
4 => decode_op4(code),
|
||||
5 => decode_op5(code),
|
||||
6 => decode_op6(code),
|
||||
7 => PpcOpcode::mulli,
|
||||
8 => PpcOpcode::subficx,
|
||||
10 => PpcOpcode::cmpli,
|
||||
11 => PpcOpcode::cmpi,
|
||||
12 => PpcOpcode::addic,
|
||||
13 => PpcOpcode::addicx,
|
||||
14 => PpcOpcode::addi,
|
||||
15 => PpcOpcode::addis,
|
||||
16 => PpcOpcode::bcx,
|
||||
17 => PpcOpcode::sc,
|
||||
18 => PpcOpcode::bx,
|
||||
19 => decode_op19(code),
|
||||
20 => PpcOpcode::rlwimix,
|
||||
21 => PpcOpcode::rlwinmx,
|
||||
23 => PpcOpcode::rlwnmx,
|
||||
24 => PpcOpcode::ori,
|
||||
25 => PpcOpcode::oris,
|
||||
26 => PpcOpcode::xori,
|
||||
27 => PpcOpcode::xoris,
|
||||
28 => PpcOpcode::andix,
|
||||
29 => PpcOpcode::andisx,
|
||||
30 => decode_op30(code),
|
||||
31 => decode_op31(code),
|
||||
32 => PpcOpcode::lwz,
|
||||
33 => PpcOpcode::lwzu,
|
||||
34 => PpcOpcode::lbz,
|
||||
35 => PpcOpcode::lbzu,
|
||||
36 => PpcOpcode::stw,
|
||||
37 => PpcOpcode::stwu,
|
||||
38 => PpcOpcode::stb,
|
||||
39 => PpcOpcode::stbu,
|
||||
40 => PpcOpcode::lhz,
|
||||
41 => PpcOpcode::lhzu,
|
||||
42 => PpcOpcode::lha,
|
||||
43 => PpcOpcode::lhau,
|
||||
44 => PpcOpcode::sth,
|
||||
45 => PpcOpcode::sthu,
|
||||
46 => PpcOpcode::lmw,
|
||||
47 => PpcOpcode::stmw,
|
||||
48 => PpcOpcode::lfs,
|
||||
49 => PpcOpcode::lfsu,
|
||||
50 => PpcOpcode::lfd,
|
||||
51 => PpcOpcode::lfdu,
|
||||
52 => PpcOpcode::stfs,
|
||||
53 => PpcOpcode::stfsu,
|
||||
54 => PpcOpcode::stfd,
|
||||
55 => PpcOpcode::stfdu,
|
||||
58 => match extract_bits(code, 30, 31) {
|
||||
0b00 => PpcOpcode::ld,
|
||||
0b01 => PpcOpcode::ldu,
|
||||
0b10 => PpcOpcode::lwa,
|
||||
_ => PpcOpcode::Invalid,
|
||||
},
|
||||
59 => match extract_bits(code, 26, 30) {
|
||||
0b10010 => PpcOpcode::fdivsx,
|
||||
0b10100 => PpcOpcode::fsubsx,
|
||||
0b10101 => PpcOpcode::faddsx,
|
||||
0b10110 => PpcOpcode::fsqrtsx,
|
||||
0b11000 => PpcOpcode::fresx,
|
||||
0b11001 => PpcOpcode::fmulsx,
|
||||
0b11100 => PpcOpcode::fmsubsx,
|
||||
0b11101 => PpcOpcode::fmaddsx,
|
||||
0b11110 => PpcOpcode::fnmsubsx,
|
||||
0b11111 => PpcOpcode::fnmaddsx,
|
||||
_ => PpcOpcode::Invalid,
|
||||
},
|
||||
62 => match extract_bits(code, 30, 31) {
|
||||
0b00 => PpcOpcode::std,
|
||||
0b01 => PpcOpcode::stdu,
|
||||
_ => PpcOpcode::Invalid,
|
||||
},
|
||||
63 => decode_op63(code),
|
||||
_ => PpcOpcode::Invalid,
|
||||
}
|
||||
}
|
||||
|
||||
fn decode_op4(code: u32) -> PpcOpcode {
|
||||
// VMX128 load/store (op=4, bits 21-27 << 4 | bits 30-31)
|
||||
let key1 = (extract_bits(code, 21, 27) << 4) | extract_bits(code, 30, 31);
|
||||
match key1 {
|
||||
0b00000000011 => return PpcOpcode::lvsl128,
|
||||
0b00001000011 => return PpcOpcode::lvsr128,
|
||||
0b00010000011 => return PpcOpcode::lvewx128,
|
||||
0b00011000011 => return PpcOpcode::lvx128,
|
||||
0b00110000011 => return PpcOpcode::stvewx128,
|
||||
0b00111000011 => return PpcOpcode::stvx128,
|
||||
0b01011000011 => return PpcOpcode::lvxl128,
|
||||
0b01111000011 => return PpcOpcode::stvxl128,
|
||||
0b10000000011 => return PpcOpcode::lvlx128,
|
||||
0b10001000011 => return PpcOpcode::lvrx128,
|
||||
0b10100000011 => return PpcOpcode::stvlx128,
|
||||
0b10101000011 => return PpcOpcode::stvrx128,
|
||||
0b11000000011 => return PpcOpcode::lvlxl128,
|
||||
0b11001000011 => return PpcOpcode::lvrxl128,
|
||||
0b11100000011 => return PpcOpcode::stvlxl128,
|
||||
0b11101000011 => return PpcOpcode::stvrxl128,
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// Standard VMX (op=4, bits 21-31)
|
||||
let key2 = extract_bits(code, 21, 31);
|
||||
match key2 {
|
||||
0b00000000000 => return PpcOpcode::vaddubm,
|
||||
0b00000000010 => return PpcOpcode::vmaxub,
|
||||
0b00000000100 => return PpcOpcode::vrlb,
|
||||
0b00000001000 => return PpcOpcode::vmuloub,
|
||||
0b00000001010 => return PpcOpcode::vaddfp,
|
||||
0b00000001100 => return PpcOpcode::vmrghb,
|
||||
0b00000001110 => return PpcOpcode::vpkuhum,
|
||||
0b00001000000 => return PpcOpcode::vadduhm,
|
||||
0b00001000010 => return PpcOpcode::vmaxuh,
|
||||
0b00001000100 => return PpcOpcode::vrlh,
|
||||
0b00001001000 => return PpcOpcode::vmulouh,
|
||||
0b00001001010 => return PpcOpcode::vsubfp,
|
||||
0b00001001100 => return PpcOpcode::vmrghh,
|
||||
0b00001001110 => return PpcOpcode::vpkuwum,
|
||||
0b00010000000 => return PpcOpcode::vadduwm,
|
||||
0b00010000010 => return PpcOpcode::vmaxuw,
|
||||
0b00010000100 => return PpcOpcode::vrlw,
|
||||
0b00010001100 => return PpcOpcode::vmrghw,
|
||||
0b00010001110 => return PpcOpcode::vpkuhus,
|
||||
0b00011001110 => return PpcOpcode::vpkuwus,
|
||||
0b00100000010 => return PpcOpcode::vmaxsb,
|
||||
0b00100000100 => return PpcOpcode::vslb,
|
||||
0b00100001000 => return PpcOpcode::vmulosb,
|
||||
0b00100001010 => return PpcOpcode::vrefp,
|
||||
0b00100001100 => return PpcOpcode::vmrglb,
|
||||
0b00100001110 => return PpcOpcode::vpkshus,
|
||||
0b00101000010 => return PpcOpcode::vmaxsh,
|
||||
0b00101000100 => return PpcOpcode::vslh,
|
||||
0b00101001000 => return PpcOpcode::vmulosh,
|
||||
0b00101001010 => return PpcOpcode::vrsqrtefp,
|
||||
0b00101001100 => return PpcOpcode::vmrglh,
|
||||
0b00101001110 => return PpcOpcode::vpkswus,
|
||||
0b00110000000 => return PpcOpcode::vaddcuw,
|
||||
0b00110000010 => return PpcOpcode::vmaxsw,
|
||||
0b00110000100 => return PpcOpcode::vslw,
|
||||
0b00110001010 => return PpcOpcode::vexptefp,
|
||||
0b00110001100 => return PpcOpcode::vmrglw,
|
||||
0b00110001110 => return PpcOpcode::vpkshss,
|
||||
0b00111000100 => return PpcOpcode::vsl,
|
||||
0b00111001010 => return PpcOpcode::vlogefp,
|
||||
0b00111001110 => return PpcOpcode::vpkswss,
|
||||
0b01000000000 => return PpcOpcode::vaddubs,
|
||||
0b01000000010 => return PpcOpcode::vminub,
|
||||
0b01000000100 => return PpcOpcode::vsrb,
|
||||
0b01000001000 => return PpcOpcode::vmuleub,
|
||||
0b01000001010 => return PpcOpcode::vrfin,
|
||||
0b01000001100 => return PpcOpcode::vspltb,
|
||||
0b01000001110 => return PpcOpcode::vupkhsb,
|
||||
0b01001000000 => return PpcOpcode::vadduhs,
|
||||
0b01001000010 => return PpcOpcode::vminuh,
|
||||
0b01001000100 => return PpcOpcode::vsrh,
|
||||
0b01001001000 => return PpcOpcode::vmuleuh,
|
||||
0b01001001010 => return PpcOpcode::vrfiz,
|
||||
0b01001001100 => return PpcOpcode::vsplth,
|
||||
0b01001001110 => return PpcOpcode::vupkhsh,
|
||||
0b01010000000 => return PpcOpcode::vadduws,
|
||||
0b01010000010 => return PpcOpcode::vminuw,
|
||||
0b01010000100 => return PpcOpcode::vsrw,
|
||||
0b01010001010 => return PpcOpcode::vrfip,
|
||||
0b01010001100 => return PpcOpcode::vspltw,
|
||||
0b01010001110 => return PpcOpcode::vupklsb,
|
||||
0b01011000100 => return PpcOpcode::vsr,
|
||||
0b01011001010 => return PpcOpcode::vrfim,
|
||||
0b01011001110 => return PpcOpcode::vupklsh,
|
||||
0b01100000000 => return PpcOpcode::vaddsbs,
|
||||
0b01100000010 => return PpcOpcode::vminsb,
|
||||
0b01100000100 => return PpcOpcode::vsrab,
|
||||
0b01100001000 => return PpcOpcode::vmulesb,
|
||||
0b01100001010 => return PpcOpcode::vcfux,
|
||||
0b01100001100 => return PpcOpcode::vspltisb,
|
||||
0b01100001110 => return PpcOpcode::vpkpx,
|
||||
0b01101000000 => return PpcOpcode::vaddshs,
|
||||
0b01101000010 => return PpcOpcode::vminsh,
|
||||
0b01101000100 => return PpcOpcode::vsrah,
|
||||
0b01101001000 => return PpcOpcode::vmulesh,
|
||||
0b01101001010 => return PpcOpcode::vcfsx,
|
||||
0b01101001100 => return PpcOpcode::vspltish,
|
||||
0b01101001110 => return PpcOpcode::vupkhpx,
|
||||
0b01110000000 => return PpcOpcode::vaddsws,
|
||||
0b01110000010 => return PpcOpcode::vminsw,
|
||||
0b01110000100 => return PpcOpcode::vsraw,
|
||||
0b01110001010 => return PpcOpcode::vctuxs,
|
||||
0b01110001100 => return PpcOpcode::vspltisw,
|
||||
0b01111001010 => return PpcOpcode::vctsxs,
|
||||
0b01111001110 => return PpcOpcode::vupklpx,
|
||||
0b10000000000 => return PpcOpcode::vsububm,
|
||||
0b10000000010 => return PpcOpcode::vavgub,
|
||||
0b10000000100 => return PpcOpcode::vand,
|
||||
0b10000001010 => return PpcOpcode::vmaxfp,
|
||||
0b10000001100 => return PpcOpcode::vslo,
|
||||
0b10001000000 => return PpcOpcode::vsubuhm,
|
||||
0b10001000010 => return PpcOpcode::vavguh,
|
||||
0b10001000100 => return PpcOpcode::vandc,
|
||||
0b10001001010 => return PpcOpcode::vminfp,
|
||||
0b10001001100 => return PpcOpcode::vsro,
|
||||
0b10010000000 => return PpcOpcode::vsubuwm,
|
||||
0b10010000010 => return PpcOpcode::vavguw,
|
||||
0b10010000100 => return PpcOpcode::vor,
|
||||
0b10011000100 => return PpcOpcode::vxor,
|
||||
0b10100000010 => return PpcOpcode::vavgsb,
|
||||
0b10100000100 => return PpcOpcode::vnor,
|
||||
0b10101000010 => return PpcOpcode::vavgsh,
|
||||
0b10110000000 => return PpcOpcode::vsubcuw,
|
||||
0b10110000010 => return PpcOpcode::vavgsw,
|
||||
0b11000000000 => return PpcOpcode::vsububs,
|
||||
0b11000000100 => return PpcOpcode::mfvscr,
|
||||
0b11000001000 => return PpcOpcode::vsum4ubs,
|
||||
0b11001000000 => return PpcOpcode::vsubuhs,
|
||||
0b11001000100 => return PpcOpcode::mtvscr,
|
||||
0b11001001000 => return PpcOpcode::vsum4shs,
|
||||
0b11010000000 => return PpcOpcode::vsubuws,
|
||||
0b11010001000 => return PpcOpcode::vsum2sws,
|
||||
0b11100000000 => return PpcOpcode::vsubsbs,
|
||||
0b11100001000 => return PpcOpcode::vsum4sbs,
|
||||
0b11101000000 => return PpcOpcode::vsubshs,
|
||||
0b11110000000 => return PpcOpcode::vsubsws,
|
||||
0b11110001000 => return PpcOpcode::vsumsws,
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// VMX compare (op=4, bits 22-31)
|
||||
let key3 = extract_bits(code, 22, 31);
|
||||
match key3 {
|
||||
0b0000000110 => return PpcOpcode::vcmpequb,
|
||||
0b0001000110 => return PpcOpcode::vcmpequh,
|
||||
0b0010000110 => return PpcOpcode::vcmpequw,
|
||||
0b0011000110 => return PpcOpcode::vcmpeqfp,
|
||||
0b0111000110 => return PpcOpcode::vcmpgefp,
|
||||
0b1000000110 => return PpcOpcode::vcmpgtub,
|
||||
0b1001000110 => return PpcOpcode::vcmpgtuh,
|
||||
0b1010000110 => return PpcOpcode::vcmpgtuw,
|
||||
0b1011000110 => return PpcOpcode::vcmpgtfp,
|
||||
0b1100000110 => return PpcOpcode::vcmpgtsb,
|
||||
0b1101000110 => return PpcOpcode::vcmpgtsh,
|
||||
0b1110000110 => return PpcOpcode::vcmpgtsw,
|
||||
0b1111000110 => return PpcOpcode::vcmpbfp,
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// VMX 4-operand (op=4, bits 26-31)
|
||||
let key4 = extract_bits(code, 26, 31);
|
||||
match key4 {
|
||||
0b100000 => return PpcOpcode::vmhaddshs,
|
||||
0b100001 => return PpcOpcode::vmhraddshs,
|
||||
0b100010 => return PpcOpcode::vmladduhm,
|
||||
0b100100 => return PpcOpcode::vmsumubm,
|
||||
0b100101 => return PpcOpcode::vmsummbm,
|
||||
0b100110 => return PpcOpcode::vmsumuhm,
|
||||
0b100111 => return PpcOpcode::vmsumuhs,
|
||||
0b101000 => return PpcOpcode::vmsumshm,
|
||||
0b101001 => return PpcOpcode::vmsumshs,
|
||||
0b101010 => return PpcOpcode::vsel,
|
||||
0b101011 => return PpcOpcode::vperm,
|
||||
0b101100 => return PpcOpcode::vsldoi,
|
||||
0b101110 => return PpcOpcode::vmaddfp,
|
||||
0b101111 => return PpcOpcode::vnmsubfp,
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// vsldoi128 (op=4, bit 27)
|
||||
if extract_bits(code, 27, 27) == 1 {
|
||||
return PpcOpcode::vsldoi128;
|
||||
}
|
||||
|
||||
PpcOpcode::Invalid
|
||||
}
|
||||
|
||||
fn decode_op5(code: u32) -> PpcOpcode {
|
||||
// vperm128 (op=5, bits 22,27)
|
||||
let key1 = (extract_bits(code, 22, 22) << 5) | extract_bits(code, 27, 27);
|
||||
if key1 == 0b000000 {
|
||||
return PpcOpcode::vperm128;
|
||||
}
|
||||
|
||||
let key2 = (extract_bits(code, 22, 25) << 2) | extract_bits(code, 27, 27);
|
||||
match key2 {
|
||||
0b000001 => PpcOpcode::vaddfp128,
|
||||
0b000101 => PpcOpcode::vsubfp128,
|
||||
0b001001 => PpcOpcode::vmulfp128,
|
||||
0b001101 => PpcOpcode::vmaddfp128,
|
||||
0b010001 => PpcOpcode::vmaddcfp128,
|
||||
0b010101 => PpcOpcode::vnmsubfp128,
|
||||
0b011001 => PpcOpcode::vmsum3fp128,
|
||||
0b011101 => PpcOpcode::vmsum4fp128,
|
||||
0b100000 => PpcOpcode::vpkshss128,
|
||||
0b100001 => PpcOpcode::vand128,
|
||||
0b100100 => PpcOpcode::vpkshus128,
|
||||
0b100101 => PpcOpcode::vandc128,
|
||||
0b101000 => PpcOpcode::vpkswss128,
|
||||
0b101001 => PpcOpcode::vnor128,
|
||||
0b101100 => PpcOpcode::vpkswus128,
|
||||
0b101101 => PpcOpcode::vor128,
|
||||
0b110000 => PpcOpcode::vpkuhum128,
|
||||
0b110001 => PpcOpcode::vxor128,
|
||||
0b110100 => PpcOpcode::vpkuhus128,
|
||||
0b110101 => PpcOpcode::vsel128,
|
||||
0b111000 => PpcOpcode::vpkuwum128,
|
||||
0b111001 => PpcOpcode::vslo128,
|
||||
0b111100 => PpcOpcode::vpkuwus128,
|
||||
0b111101 => PpcOpcode::vsro128,
|
||||
_ => PpcOpcode::Invalid,
|
||||
}
|
||||
}
|
||||
|
||||
fn decode_op6(code: u32) -> PpcOpcode {
|
||||
// vpermwi128
|
||||
let key1 = (extract_bits(code, 21, 22) << 5) | extract_bits(code, 26, 27);
|
||||
if key1 == 0b0100001 {
|
||||
return PpcOpcode::vpermwi128;
|
||||
}
|
||||
|
||||
// vpkd3d128, vrlimi128
|
||||
let key2 = (extract_bits(code, 21, 23) << 4) | extract_bits(code, 26, 27);
|
||||
match key2 {
|
||||
0b1100001 => return PpcOpcode::vpkd3d128,
|
||||
0b1110001 => return PpcOpcode::vrlimi128,
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// Unary VMX128 ops
|
||||
let key3 = extract_bits(code, 21, 27);
|
||||
match key3 {
|
||||
0b0100011 => return PpcOpcode::vcfpsxws128,
|
||||
0b0100111 => return PpcOpcode::vcfpuxws128,
|
||||
0b0101011 => return PpcOpcode::vcsxwfp128,
|
||||
0b0101111 => return PpcOpcode::vcuxwfp128,
|
||||
0b0110011 => return PpcOpcode::vrfim128,
|
||||
0b0110111 => return PpcOpcode::vrfin128,
|
||||
0b0111011 => return PpcOpcode::vrfip128,
|
||||
0b0111111 => return PpcOpcode::vrfiz128,
|
||||
0b1100011 => return PpcOpcode::vrefp128,
|
||||
0b1100111 => return PpcOpcode::vrsqrtefp128,
|
||||
0b1101011 => return PpcOpcode::vexptefp128,
|
||||
0b1101111 => return PpcOpcode::vlogefp128,
|
||||
0b1110011 => return PpcOpcode::vspltw128,
|
||||
0b1110111 => return PpcOpcode::vspltisw128,
|
||||
0b1111111 => return PpcOpcode::vupkd3d128,
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// VMX128 compare
|
||||
let key4 = (extract_bits(code, 22, 24) << 3) | extract_bits(code, 27, 27);
|
||||
match key4 {
|
||||
0b000000 => return PpcOpcode::vcmpeqfp128,
|
||||
0b001000 => return PpcOpcode::vcmpgefp128,
|
||||
0b010000 => return PpcOpcode::vcmpgtfp128,
|
||||
0b011000 => return PpcOpcode::vcmpbfp128,
|
||||
0b100000 => return PpcOpcode::vcmpequw128,
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// VMX128 shift/merge
|
||||
let key5 = (extract_bits(code, 22, 25) << 2) | extract_bits(code, 27, 27);
|
||||
match key5 {
|
||||
0b000101 => return PpcOpcode::vrlw128,
|
||||
0b001101 => return PpcOpcode::vslw128,
|
||||
0b010101 => return PpcOpcode::vsraw128,
|
||||
0b011101 => return PpcOpcode::vsrw128,
|
||||
0b101000 => return PpcOpcode::vmaxfp128,
|
||||
0b101100 => return PpcOpcode::vminfp128,
|
||||
0b110000 => return PpcOpcode::vmrghw128,
|
||||
0b110100 => return PpcOpcode::vmrglw128,
|
||||
0b111000 => return PpcOpcode::vupkhsb128,
|
||||
0b111100 => return PpcOpcode::vupklsb128,
|
||||
_ => {}
|
||||
}
|
||||
|
||||
PpcOpcode::Invalid
|
||||
}
|
||||
|
||||
fn decode_op19(code: u32) -> PpcOpcode {
|
||||
match extract_bits(code, 21, 30) {
|
||||
0b0000000000 => PpcOpcode::mcrf,
|
||||
0b0000010000 => PpcOpcode::bclrx,
|
||||
0b0000100001 => PpcOpcode::crnor,
|
||||
0b0010000001 => PpcOpcode::crandc,
|
||||
0b0010010110 => PpcOpcode::isync,
|
||||
0b0011000001 => PpcOpcode::crxor,
|
||||
0b0011100001 => PpcOpcode::crnand,
|
||||
0b0100000001 => PpcOpcode::crand,
|
||||
0b0100100001 => PpcOpcode::creqv,
|
||||
0b0110100001 => PpcOpcode::crorc,
|
||||
0b0111000001 => PpcOpcode::cror,
|
||||
0b1000010000 => PpcOpcode::bcctrx,
|
||||
_ => PpcOpcode::Invalid,
|
||||
}
|
||||
}
|
||||
|
||||
fn decode_op30(code: u32) -> PpcOpcode {
|
||||
match extract_bits(code, 27, 29) {
|
||||
0b000 => PpcOpcode::rldiclx,
|
||||
0b001 => PpcOpcode::rldicrx,
|
||||
0b010 => PpcOpcode::rldicx,
|
||||
0b011 => PpcOpcode::rldimix,
|
||||
_ => match extract_bits(code, 27, 30) {
|
||||
0b1000 => PpcOpcode::rldclx,
|
||||
0b1001 => PpcOpcode::rldcrx,
|
||||
_ => PpcOpcode::Invalid,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn decode_op31(code: u32) -> PpcOpcode {
|
||||
// sradix has a unique 10-bit key (bits 21-29)
|
||||
if extract_bits(code, 21, 29) == 0b110011101 {
|
||||
return PpcOpcode::sradix;
|
||||
}
|
||||
|
||||
// Main op31 table (bits 21-30)
|
||||
let key = extract_bits(code, 21, 30);
|
||||
match key {
|
||||
0b0000000000 => return PpcOpcode::cmp,
|
||||
0b0000000100 => return PpcOpcode::tw,
|
||||
0b0000000110 => return PpcOpcode::lvsl,
|
||||
0b0000000111 => return PpcOpcode::lvebx,
|
||||
0b0000010011 => return PpcOpcode::mfcr,
|
||||
0b0000010100 => return PpcOpcode::lwarx,
|
||||
0b0000010101 => return PpcOpcode::ldx,
|
||||
0b0000010111 => return PpcOpcode::lwzx,
|
||||
0b0000011000 => return PpcOpcode::slwx,
|
||||
0b0000011010 => return PpcOpcode::cntlzwx,
|
||||
0b0000011011 => return PpcOpcode::sldx,
|
||||
0b0000011100 => return PpcOpcode::andx,
|
||||
0b0000100000 => return PpcOpcode::cmpl,
|
||||
0b0000100110 => return PpcOpcode::lvsr,
|
||||
0b0000100111 => return PpcOpcode::lvehx,
|
||||
0b0000110101 => return PpcOpcode::ldux,
|
||||
0b0000110110 => return PpcOpcode::dcbst,
|
||||
0b0000110111 => return PpcOpcode::lwzux,
|
||||
0b0000111010 => return PpcOpcode::cntlzdx,
|
||||
0b0000111100 => return PpcOpcode::andcx,
|
||||
0b0001000100 => return PpcOpcode::td,
|
||||
0b0001000111 => return PpcOpcode::lvewx,
|
||||
0b0001010011 => return PpcOpcode::mfmsr,
|
||||
0b0001010100 => return PpcOpcode::ldarx,
|
||||
0b0001010110 => return PpcOpcode::dcbf,
|
||||
0b0001010111 => return PpcOpcode::lbzx,
|
||||
0b0001100111 => return PpcOpcode::lvx,
|
||||
0b0001110111 => return PpcOpcode::lbzux,
|
||||
0b0001111100 => return PpcOpcode::norx,
|
||||
0b0010000111 => return PpcOpcode::stvebx,
|
||||
0b0010010000 => return PpcOpcode::mtcrf,
|
||||
0b0010010010 => return PpcOpcode::mtmsr,
|
||||
0b0010010101 => return PpcOpcode::stdx,
|
||||
0b0010010110 => return PpcOpcode::stwcx,
|
||||
0b0010010111 => return PpcOpcode::stwx,
|
||||
0b0010100111 => return PpcOpcode::stvehx,
|
||||
0b0010110010 => return PpcOpcode::mtmsrd,
|
||||
0b0010110101 => return PpcOpcode::stdux,
|
||||
0b0010110111 => return PpcOpcode::stwux,
|
||||
0b0011000111 => return PpcOpcode::stvewx,
|
||||
0b0011010110 => return PpcOpcode::stdcx,
|
||||
0b0011010111 => return PpcOpcode::stbx,
|
||||
0b0011100111 => return PpcOpcode::stvx,
|
||||
0b0011110110 => return PpcOpcode::dcbtst,
|
||||
0b0011110111 => return PpcOpcode::stbux,
|
||||
0b0100010110 => return PpcOpcode::dcbt,
|
||||
0b0100010111 => return PpcOpcode::lhzx,
|
||||
0b0100011100 => return PpcOpcode::eqvx,
|
||||
0b0100110111 => return PpcOpcode::lhzux,
|
||||
0b0100111100 => return PpcOpcode::xorx,
|
||||
0b0101010011 => return PpcOpcode::mfspr,
|
||||
0b0101010101 => return PpcOpcode::lwax,
|
||||
0b0101010111 => return PpcOpcode::lhax,
|
||||
0b0101100111 => return PpcOpcode::lvxl,
|
||||
0b0101110011 => return PpcOpcode::mftb,
|
||||
0b0101110101 => return PpcOpcode::lwaux,
|
||||
0b0101110111 => return PpcOpcode::lhaux,
|
||||
0b0110010111 => return PpcOpcode::sthx,
|
||||
0b0110011100 => return PpcOpcode::orcx,
|
||||
0b0110110111 => return PpcOpcode::sthux,
|
||||
0b0110111100 => return PpcOpcode::orx,
|
||||
0b0111010011 => return PpcOpcode::mtspr,
|
||||
0b0111010110 => return PpcOpcode::dcbi,
|
||||
0b0111011100 => return PpcOpcode::nandx,
|
||||
0b0111100111 => return PpcOpcode::stvxl,
|
||||
0b1000000000 => return PpcOpcode::mcrxr,
|
||||
0b1000000111 => return PpcOpcode::lvlx,
|
||||
0b1000010100 => return PpcOpcode::ldbrx,
|
||||
0b1000010101 => return PpcOpcode::lswx,
|
||||
0b1000010110 => return PpcOpcode::lwbrx,
|
||||
0b1000010111 => return PpcOpcode::lfsx,
|
||||
0b1000011000 => return PpcOpcode::srwx,
|
||||
0b1000011011 => return PpcOpcode::srdx,
|
||||
0b1000100111 => return PpcOpcode::lvrx,
|
||||
0b1000110111 => return PpcOpcode::lfsux,
|
||||
0b1001010101 => return PpcOpcode::lswi,
|
||||
0b1001010110 => return PpcOpcode::sync,
|
||||
0b1001010111 => return PpcOpcode::lfdx,
|
||||
0b1001110111 => return PpcOpcode::lfdux,
|
||||
0b1010000111 => return PpcOpcode::stvlx,
|
||||
0b1010010100 => return PpcOpcode::stdbrx,
|
||||
0b1010010101 => return PpcOpcode::stswx,
|
||||
0b1010010110 => return PpcOpcode::stwbrx,
|
||||
0b1010010111 => return PpcOpcode::stfsx,
|
||||
0b1010100111 => return PpcOpcode::stvrx,
|
||||
0b1010110111 => return PpcOpcode::stfsux,
|
||||
0b1011010101 => return PpcOpcode::stswi,
|
||||
0b1011010111 => return PpcOpcode::stfdx,
|
||||
0b1011110111 => return PpcOpcode::stfdux,
|
||||
0b1100000111 => return PpcOpcode::lvlxl,
|
||||
0b1100010110 => return PpcOpcode::lhbrx,
|
||||
0b1100011000 => return PpcOpcode::srawx,
|
||||
0b1100011010 => return PpcOpcode::sradx,
|
||||
0b1100100111 => return PpcOpcode::lvrxl,
|
||||
0b1100111000 => return PpcOpcode::srawix,
|
||||
0b1101010110 => return PpcOpcode::eieio,
|
||||
0b1110000111 => return PpcOpcode::stvlxl,
|
||||
0b1110010110 => return PpcOpcode::sthbrx,
|
||||
0b1110011010 => return PpcOpcode::extshx,
|
||||
0b1110100111 => return PpcOpcode::stvrxl,
|
||||
0b1110111010 => return PpcOpcode::extsbx,
|
||||
0b1111010110 => return PpcOpcode::icbi,
|
||||
0b1111010111 => return PpcOpcode::stfiwx,
|
||||
0b1111011010 => return PpcOpcode::extswx,
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// Arithmetic op31 (bits 22-30)
|
||||
let key2 = extract_bits(code, 22, 30);
|
||||
match key2 {
|
||||
0b000001000 => return PpcOpcode::subfcx,
|
||||
0b000001001 => return PpcOpcode::mulhdux,
|
||||
0b000001010 => return PpcOpcode::addcx,
|
||||
0b000001011 => return PpcOpcode::mulhwux,
|
||||
0b000101000 => return PpcOpcode::subfx,
|
||||
0b001001001 => return PpcOpcode::mulhdx,
|
||||
0b001001011 => return PpcOpcode::mulhwx,
|
||||
0b001101000 => return PpcOpcode::negx,
|
||||
0b010001000 => return PpcOpcode::subfex,
|
||||
0b010001010 => return PpcOpcode::addex,
|
||||
0b011001000 => return PpcOpcode::subfzex,
|
||||
0b011001010 => return PpcOpcode::addzex,
|
||||
0b011101000 => return PpcOpcode::subfmex,
|
||||
0b011101001 => return PpcOpcode::mulldx,
|
||||
0b011101010 => return PpcOpcode::addmex,
|
||||
0b011101011 => return PpcOpcode::mullwx,
|
||||
0b100001010 => return PpcOpcode::addx,
|
||||
0b111001001 => return PpcOpcode::divdux,
|
||||
0b111001011 => return PpcOpcode::divwux,
|
||||
0b111101001 => return PpcOpcode::divdx,
|
||||
0b111101011 => return PpcOpcode::divwx,
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// dcbz/dcbz128 special case
|
||||
let key3 = (extract_bits(code, 6, 10) << 20) | (extract_bits(code, 21, 30));
|
||||
match key3 {
|
||||
0b0000000000000001111110110 => return PpcOpcode::dcbz,
|
||||
0b0000100000000001111110110 => return PpcOpcode::dcbz128,
|
||||
_ => {}
|
||||
}
|
||||
|
||||
PpcOpcode::Invalid
|
||||
}
|
||||
|
||||
fn decode_op63(code: u32) -> PpcOpcode {
|
||||
// Primary op63 table (bits 21-30)
|
||||
match extract_bits(code, 21, 30) {
|
||||
0b0000000000 => return PpcOpcode::fcmpu,
|
||||
0b0000001100 => return PpcOpcode::frspx,
|
||||
0b0000001110 => return PpcOpcode::fctiwx,
|
||||
0b0000001111 => return PpcOpcode::fctiwzx,
|
||||
0b0000100000 => return PpcOpcode::fcmpo,
|
||||
0b0000100110 => return PpcOpcode::mtfsb1x,
|
||||
0b0000101000 => return PpcOpcode::fnegx,
|
||||
0b0001000000 => return PpcOpcode::mcrfs,
|
||||
0b0001000110 => return PpcOpcode::mtfsb0x,
|
||||
0b0001001000 => return PpcOpcode::fmrx,
|
||||
0b0010000110 => return PpcOpcode::mtfsfix,
|
||||
0b0010001000 => return PpcOpcode::fnabsx,
|
||||
0b0100001000 => return PpcOpcode::fabsx,
|
||||
0b1001000111 => return PpcOpcode::mffsx,
|
||||
0b1011000111 => return PpcOpcode::mtfsfx,
|
||||
0b1100101110 => return PpcOpcode::fctidx,
|
||||
0b1100101111 => return PpcOpcode::fctidzx,
|
||||
0b1101001110 => return PpcOpcode::fcfidx,
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// FPU arithmetic (bits 26-30)
|
||||
match extract_bits(code, 26, 30) {
|
||||
0b10010 => PpcOpcode::fdivx,
|
||||
0b10100 => PpcOpcode::fsubx,
|
||||
0b10101 => PpcOpcode::faddx,
|
||||
0b10110 => PpcOpcode::fsqrtx,
|
||||
0b10111 => PpcOpcode::fselx,
|
||||
0b11001 => PpcOpcode::fmulx,
|
||||
0b11010 => PpcOpcode::frsqrtex,
|
||||
0b11100 => PpcOpcode::fmsubx,
|
||||
0b11101 => PpcOpcode::fmaddx,
|
||||
0b11110 => PpcOpcode::fnmsubx,
|
||||
0b11111 => PpcOpcode::fnmaddx,
|
||||
_ => PpcOpcode::Invalid,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_decode_addi() {
|
||||
// addi r3, r1, 0x10 => opcode 14, rD=3, rA=1, SIMM=0x10
|
||||
let raw: u32 = (14 << 26) | (3 << 21) | (1 << 16) | 0x10;
|
||||
let instr = decode(raw, 0);
|
||||
assert_eq!(instr.opcode, PpcOpcode::addi);
|
||||
assert_eq!(instr.rd(), 3);
|
||||
assert_eq!(instr.ra(), 1);
|
||||
assert_eq!(instr.simm16(), 0x10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decode_lwz() {
|
||||
// lwz r5, 0x20(r1) => opcode 32
|
||||
let raw: u32 = (32 << 26) | (5 << 21) | (1 << 16) | 0x20;
|
||||
let instr = decode(raw, 0);
|
||||
assert_eq!(instr.opcode, PpcOpcode::lwz);
|
||||
assert_eq!(instr.rd(), 5);
|
||||
assert_eq!(instr.ra(), 1);
|
||||
assert_eq!(instr.d(), 0x20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decode_branch() {
|
||||
// b +0x100 => opcode 18, LI=0x40 (shifted left 2 = 0x100), AA=0, LK=0
|
||||
let raw: u32 = (18 << 26) | (0x40 << 2);
|
||||
let instr = decode(raw, 0);
|
||||
assert_eq!(instr.opcode, PpcOpcode::bx);
|
||||
assert_eq!(instr.li(), 0x100);
|
||||
assert!(!instr.aa());
|
||||
assert!(!instr.lk());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decode_stw() {
|
||||
// stw r7, 0x8(r2)
|
||||
let raw: u32 = (36 << 26) | (7 << 21) | (2 << 16) | 0x8;
|
||||
let instr = decode(raw, 0);
|
||||
assert_eq!(instr.opcode, PpcOpcode::stw);
|
||||
assert_eq!(instr.rs(), 7);
|
||||
assert_eq!(instr.ra(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decode_ori_nop() {
|
||||
// ori r0, r0, 0 = NOP
|
||||
let raw: u32 = 24 << 26;
|
||||
let instr = decode(raw, 0);
|
||||
assert_eq!(instr.opcode, PpcOpcode::ori);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_bits() {
|
||||
assert_eq!(extract_bits(0xFFFF_FFFF, 0, 5), 0x3F);
|
||||
assert_eq!(extract_bits(0x8000_0000, 0, 0), 1);
|
||||
assert_eq!(extract_bits(0x0000_0001, 31, 31), 1);
|
||||
}
|
||||
}
|
||||
276
crates/xenia-cpu/src/disasm.rs
Normal file
276
crates/xenia-cpu/src/disasm.rs
Normal file
@@ -0,0 +1,276 @@
|
||||
use crate::decoder::DecodedInstr;
|
||||
use crate::opcode::PpcOpcode;
|
||||
use std::fmt::Write;
|
||||
|
||||
/// Disassemble a decoded instruction into PPC assembly text.
|
||||
pub fn disassemble(instr: &DecodedInstr) -> String {
|
||||
let mut out = String::new();
|
||||
match instr.opcode {
|
||||
// Branch instructions
|
||||
PpcOpcode::bx => {
|
||||
let target = if instr.aa() {
|
||||
instr.li() as u32
|
||||
} else {
|
||||
instr.addr.wrapping_add(instr.li() as u32)
|
||||
};
|
||||
let mnemonic = if instr.lk() { "bl" } else { "b" };
|
||||
write!(out, "{} 0x{:08X}", mnemonic, target).unwrap();
|
||||
}
|
||||
PpcOpcode::bcx => {
|
||||
let bo = instr.bo();
|
||||
let bi = instr.bi();
|
||||
let target = if instr.aa() {
|
||||
instr.bd() as u32
|
||||
} else {
|
||||
instr.addr.wrapping_add(instr.bd() as u32)
|
||||
};
|
||||
let mnemonic = if instr.lk() { "bcl" } else { "bc" };
|
||||
write!(out, "{} {},{},0x{:08X}", mnemonic, bo, bi, target).unwrap();
|
||||
}
|
||||
PpcOpcode::bclrx => {
|
||||
let mnemonic = if instr.lk() { "bclrl" } else { "bclr" };
|
||||
write!(out, "{} {},{}", mnemonic, instr.bo(), instr.bi()).unwrap();
|
||||
}
|
||||
PpcOpcode::bcctrx => {
|
||||
let mnemonic = if instr.lk() { "bcctrl" } else { "bcctr" };
|
||||
write!(out, "{} {},{}", mnemonic, instr.bo(), instr.bi()).unwrap();
|
||||
}
|
||||
|
||||
// System call
|
||||
PpcOpcode::sc => {
|
||||
write!(out, "sc").unwrap();
|
||||
}
|
||||
|
||||
// D-form load/store
|
||||
PpcOpcode::lwz | PpcOpcode::lwzu | PpcOpcode::lbz | PpcOpcode::lbzu |
|
||||
PpcOpcode::lhz | PpcOpcode::lhzu | PpcOpcode::lha | PpcOpcode::lhau |
|
||||
PpcOpcode::lfs | PpcOpcode::lfsu | PpcOpcode::lfd | PpcOpcode::lfdu => {
|
||||
write!(out, "{:?} r{},{}(r{})", instr.opcode, instr.rd(), instr.d(), instr.ra()).unwrap();
|
||||
}
|
||||
PpcOpcode::stw | PpcOpcode::stwu | PpcOpcode::stb | PpcOpcode::stbu |
|
||||
PpcOpcode::sth | PpcOpcode::sthu |
|
||||
PpcOpcode::stfs | PpcOpcode::stfsu | PpcOpcode::stfd | PpcOpcode::stfdu => {
|
||||
write!(out, "{:?} r{},{}(r{})", instr.opcode, instr.rs(), instr.d(), instr.ra()).unwrap();
|
||||
}
|
||||
|
||||
// D-form immediate ALU
|
||||
PpcOpcode::addi | PpcOpcode::addis | PpcOpcode::addic | PpcOpcode::addicx |
|
||||
PpcOpcode::subficx | PpcOpcode::mulli => {
|
||||
write!(out, "{:?} r{},r{},{}", instr.opcode, instr.rd(), instr.ra(), instr.simm16()).unwrap();
|
||||
}
|
||||
|
||||
// D-form immediate logical
|
||||
PpcOpcode::ori | PpcOpcode::oris | PpcOpcode::xori | PpcOpcode::xoris |
|
||||
PpcOpcode::andix | PpcOpcode::andisx => {
|
||||
write!(out, "{:?} r{},r{},0x{:04X}", instr.opcode, instr.ra(), instr.rs(), instr.uimm16()).unwrap();
|
||||
}
|
||||
|
||||
// Compare
|
||||
PpcOpcode::cmpi => {
|
||||
write!(out, "cmp{}i cr{},r{},{}", if instr.l() { "d" } else { "w" },
|
||||
instr.crfd(), instr.ra(), instr.simm16()).unwrap();
|
||||
}
|
||||
PpcOpcode::cmpli => {
|
||||
write!(out, "cmpl{}i cr{},r{},0x{:04X}", if instr.l() { "d" } else { "w" },
|
||||
instr.crfd(), instr.ra(), instr.uimm16()).unwrap();
|
||||
}
|
||||
PpcOpcode::cmp => {
|
||||
write!(out, "cmp{} cr{},r{},r{}", if instr.l() { "d" } else { "w" },
|
||||
instr.crfd(), instr.ra(), instr.rb()).unwrap();
|
||||
}
|
||||
PpcOpcode::cmpl => {
|
||||
write!(out, "cmpl{} cr{},r{},r{}", if instr.l() { "d" } else { "w" },
|
||||
instr.crfd(), instr.ra(), instr.rb()).unwrap();
|
||||
}
|
||||
|
||||
// X-form ALU (3-register)
|
||||
PpcOpcode::addx | PpcOpcode::addcx | PpcOpcode::addex | PpcOpcode::addzex |
|
||||
PpcOpcode::addmex | PpcOpcode::subfx | PpcOpcode::subfcx | PpcOpcode::subfex |
|
||||
PpcOpcode::subfzex | PpcOpcode::subfmex | PpcOpcode::negx |
|
||||
PpcOpcode::mullwx | PpcOpcode::mulhwx | PpcOpcode::mulhwux |
|
||||
PpcOpcode::divwx | PpcOpcode::divwux |
|
||||
PpcOpcode::mulldx | PpcOpcode::mulhdx | PpcOpcode::mulhdux |
|
||||
PpcOpcode::divdx | PpcOpcode::divdux => {
|
||||
write!(out, "{:?} r{},r{},r{}", instr.opcode, instr.rd(), instr.ra(), instr.rb()).unwrap();
|
||||
}
|
||||
|
||||
// X-form logical
|
||||
PpcOpcode::andx | PpcOpcode::andcx | PpcOpcode::orx | PpcOpcode::orcx |
|
||||
PpcOpcode::xorx | PpcOpcode::norx | PpcOpcode::nandx | PpcOpcode::eqvx => {
|
||||
write!(out, "{:?} r{},r{},r{}", instr.opcode, instr.ra(), instr.rs(), instr.rb()).unwrap();
|
||||
}
|
||||
|
||||
// Shift/rotate
|
||||
PpcOpcode::slwx | PpcOpcode::srwx | PpcOpcode::srawx | PpcOpcode::sldx |
|
||||
PpcOpcode::srdx | PpcOpcode::sradx => {
|
||||
write!(out, "{:?} r{},r{},r{}", instr.opcode, instr.ra(), instr.rs(), instr.rb()).unwrap();
|
||||
}
|
||||
PpcOpcode::srawix => {
|
||||
write!(out, "srawi r{},r{},{}", instr.ra(), instr.rs(), instr.sh()).unwrap();
|
||||
}
|
||||
PpcOpcode::sradix => {
|
||||
write!(out, "sradi r{},r{},{}", instr.ra(), instr.rs(), instr.sh64()).unwrap();
|
||||
}
|
||||
|
||||
// Rotate
|
||||
PpcOpcode::rlwinmx => {
|
||||
write!(out, "rlwinm r{},r{},{},{},{}", instr.ra(), instr.rs(), instr.sh(), instr.mb(), instr.me()).unwrap();
|
||||
}
|
||||
PpcOpcode::rlwimix => {
|
||||
write!(out, "rlwimi r{},r{},{},{},{}", instr.ra(), instr.rs(), instr.sh(), instr.mb(), instr.me()).unwrap();
|
||||
}
|
||||
PpcOpcode::rlwnmx => {
|
||||
write!(out, "rlwnm r{},r{},r{},{},{}", instr.ra(), instr.rs(), instr.rb(), instr.mb(), instr.me()).unwrap();
|
||||
}
|
||||
|
||||
// Special register moves
|
||||
PpcOpcode::mfspr => {
|
||||
let spr_name = match instr.spr() {
|
||||
1 => "xer",
|
||||
8 => "lr",
|
||||
9 => "ctr",
|
||||
268 => "tbl",
|
||||
269 => "tbu",
|
||||
_ => "",
|
||||
};
|
||||
if spr_name.is_empty() {
|
||||
write!(out, "mfspr r{},{}", instr.rd(), instr.spr()).unwrap();
|
||||
} else {
|
||||
write!(out, "mf{} r{}", spr_name, instr.rd()).unwrap();
|
||||
}
|
||||
}
|
||||
PpcOpcode::mtspr => {
|
||||
let spr_name = match instr.spr() {
|
||||
1 => "xer",
|
||||
8 => "lr",
|
||||
9 => "ctr",
|
||||
_ => "",
|
||||
};
|
||||
if spr_name.is_empty() {
|
||||
write!(out, "mtspr {},r{}", instr.spr(), instr.rs()).unwrap();
|
||||
} else {
|
||||
write!(out, "mt{} r{}", spr_name, instr.rs()).unwrap();
|
||||
}
|
||||
}
|
||||
PpcOpcode::mfcr => {
|
||||
write!(out, "mfcr r{}", instr.rd()).unwrap();
|
||||
}
|
||||
PpcOpcode::mtcrf => {
|
||||
write!(out, "mtcrf 0x{:02X},r{}", instr.crm(), instr.rs()).unwrap();
|
||||
}
|
||||
|
||||
// Extend
|
||||
PpcOpcode::extsbx => write!(out, "extsb r{},r{}", instr.ra(), instr.rs()).unwrap(),
|
||||
PpcOpcode::extshx => write!(out, "extsh r{},r{}", instr.ra(), instr.rs()).unwrap(),
|
||||
PpcOpcode::extswx => write!(out, "extsw r{},r{}", instr.ra(), instr.rs()).unwrap(),
|
||||
PpcOpcode::cntlzwx => write!(out, "cntlzw r{},r{}", instr.ra(), instr.rs()).unwrap(),
|
||||
PpcOpcode::cntlzdx => write!(out, "cntlzd r{},r{}", instr.ra(), instr.rs()).unwrap(),
|
||||
|
||||
// X-form load/store
|
||||
PpcOpcode::lwzx | PpcOpcode::lwzux | PpcOpcode::lbzx | PpcOpcode::lbzux |
|
||||
PpcOpcode::lhzx | PpcOpcode::lhzux | PpcOpcode::lhax | PpcOpcode::lhaux |
|
||||
PpcOpcode::lwax | PpcOpcode::lwaux | PpcOpcode::ldx | PpcOpcode::ldux |
|
||||
PpcOpcode::lfsx | PpcOpcode::lfsux | PpcOpcode::lfdx | PpcOpcode::lfdux |
|
||||
PpcOpcode::lwbrx | PpcOpcode::lhbrx | PpcOpcode::ldbrx |
|
||||
PpcOpcode::lwarx | PpcOpcode::ldarx => {
|
||||
write!(out, "{:?} r{},r{},r{}", instr.opcode, instr.rd(), instr.ra(), instr.rb()).unwrap();
|
||||
}
|
||||
PpcOpcode::stwx | PpcOpcode::stwux | PpcOpcode::stbx | PpcOpcode::stbux |
|
||||
PpcOpcode::sthx | PpcOpcode::sthux | PpcOpcode::stdx | PpcOpcode::stdux |
|
||||
PpcOpcode::stfsx | PpcOpcode::stfsux | PpcOpcode::stfdx | PpcOpcode::stfdux |
|
||||
PpcOpcode::stwbrx | PpcOpcode::sthbrx | PpcOpcode::stdbrx |
|
||||
PpcOpcode::stwcx | PpcOpcode::stdcx | PpcOpcode::stfiwx => {
|
||||
write!(out, "{:?} r{},r{},r{}", instr.opcode, instr.rs(), instr.ra(), instr.rb()).unwrap();
|
||||
}
|
||||
|
||||
// Cache/sync ops (no-ops for interpreter)
|
||||
PpcOpcode::dcbf | PpcOpcode::dcbi | PpcOpcode::dcbst |
|
||||
PpcOpcode::dcbt | PpcOpcode::dcbtst | PpcOpcode::icbi => {
|
||||
write!(out, "{:?} r{},r{}", instr.opcode, instr.ra(), instr.rb()).unwrap();
|
||||
}
|
||||
PpcOpcode::dcbz | PpcOpcode::dcbz128 => {
|
||||
write!(out, "{:?} r{},r{}", instr.opcode, instr.ra(), instr.rb()).unwrap();
|
||||
}
|
||||
PpcOpcode::sync | PpcOpcode::eieio | PpcOpcode::isync => {
|
||||
write!(out, "{:?}", instr.opcode).unwrap();
|
||||
}
|
||||
|
||||
// Load/store multiple
|
||||
PpcOpcode::lmw => write!(out, "lmw r{},{}(r{})", instr.rd(), instr.d(), instr.ra()).unwrap(),
|
||||
PpcOpcode::stmw => write!(out, "stmw r{},{}(r{})", instr.rs(), instr.d(), instr.ra()).unwrap(),
|
||||
|
||||
// DS-form loads/stores
|
||||
PpcOpcode::ld | PpcOpcode::ldu | PpcOpcode::lwa => {
|
||||
write!(out, "{:?} r{},{}(r{})", instr.opcode, instr.rd(), instr.ds(), instr.ra()).unwrap();
|
||||
}
|
||||
PpcOpcode::std | PpcOpcode::stdu => {
|
||||
write!(out, "{:?} r{},{}(r{})", instr.opcode, instr.rs(), instr.ds(), instr.ra()).unwrap();
|
||||
}
|
||||
|
||||
// CR logical ops
|
||||
PpcOpcode::crand | PpcOpcode::crandc | PpcOpcode::creqv | PpcOpcode::crnand |
|
||||
PpcOpcode::crnor | PpcOpcode::cror | PpcOpcode::crorc | PpcOpcode::crxor => {
|
||||
write!(out, "{:?} {},{},{}", instr.opcode, instr.crbd(), instr.crba(), instr.crbb()).unwrap();
|
||||
}
|
||||
PpcOpcode::mcrf => {
|
||||
write!(out, "mcrf cr{},cr{}", instr.crfd(), instr.crfs()).unwrap();
|
||||
}
|
||||
|
||||
// Trap
|
||||
PpcOpcode::tdi => write!(out, "tdi {},r{},{}", instr.rd(), instr.ra(), instr.simm16()).unwrap(),
|
||||
PpcOpcode::twi => write!(out, "twi {},r{},{}", instr.rd(), instr.ra(), instr.simm16()).unwrap(),
|
||||
PpcOpcode::td => write!(out, "td {},r{},r{}", instr.rd(), instr.ra(), instr.rb()).unwrap(),
|
||||
PpcOpcode::tw => write!(out, "tw {},r{},r{}", instr.rd(), instr.ra(), instr.rb()).unwrap(),
|
||||
|
||||
// Default: just print opcode and raw hex
|
||||
_ => {
|
||||
write!(out, "{:?} [{:08X}]", instr.opcode, instr.raw).unwrap();
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Disassemble a range of instructions from a byte slice.
|
||||
pub fn disassemble_block(data: &[u8], base_addr: u32, count: usize) -> Vec<(u32, String)> {
|
||||
let mut result = Vec::new();
|
||||
for i in 0..count {
|
||||
let offset = i * 4;
|
||||
if offset + 4 > data.len() {
|
||||
break;
|
||||
}
|
||||
let raw = u32::from_be_bytes([
|
||||
data[offset],
|
||||
data[offset + 1],
|
||||
data[offset + 2],
|
||||
data[offset + 3],
|
||||
]);
|
||||
let addr = base_addr + offset as u32;
|
||||
let instr = crate::decode(raw, addr);
|
||||
let text = disassemble(&instr);
|
||||
result.push((addr, text));
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::decoder::decode;
|
||||
|
||||
#[test]
|
||||
fn test_disasm_nop() {
|
||||
// ori r0, r0, 0 = NOP
|
||||
let instr = decode(0x60000000, 0);
|
||||
let text = disassemble(&instr);
|
||||
assert!(text.contains("ori"), "Expected 'ori', got: {}", text);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_disasm_addi() {
|
||||
let raw = (14u32 << 26) | (3 << 21) | (1 << 16) | 16;
|
||||
let instr = decode(raw, 0);
|
||||
let text = disassemble(&instr);
|
||||
assert!(text.contains("addi"), "Got: {}", text);
|
||||
assert!(text.contains("r3"), "Got: {}", text);
|
||||
}
|
||||
}
|
||||
2529
crates/xenia-cpu/src/interpreter.rs
Normal file
2529
crates/xenia-cpu/src/interpreter.rs
Normal file
File diff suppressed because it is too large
Load Diff
9
crates/xenia-cpu/src/lib.rs
Normal file
9
crates/xenia-cpu/src/lib.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
pub mod context;
|
||||
pub mod decoder;
|
||||
pub mod disasm;
|
||||
pub mod interpreter;
|
||||
pub mod opcode;
|
||||
|
||||
pub use context::PpcContext;
|
||||
pub use decoder::decode;
|
||||
pub use opcode::PpcOpcode;
|
||||
196
crates/xenia-cpu/src/opcode.rs
Normal file
196
crates/xenia-cpu/src/opcode.rs
Normal file
@@ -0,0 +1,196 @@
|
||||
/// All PPC opcodes supported by the Xbox 360, including VMX128 extensions.
|
||||
/// Directly mirrors the C++ PPCOpcode enum from ppc_opcode.h.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
#[repr(u32)]
|
||||
#[allow(non_camel_case_types)]
|
||||
pub enum PpcOpcode {
|
||||
// ALU
|
||||
addcx, addex, addi, addic, addicx, addis, addmex, addx, addzex,
|
||||
andcx, andisx, andix, andx,
|
||||
// Branch
|
||||
bcctrx, bclrx, bcx, bx,
|
||||
// Compare
|
||||
cmp, cmpi, cmpl, cmpli,
|
||||
// Count leading zeros
|
||||
cntlzdx, cntlzwx,
|
||||
// Condition register
|
||||
crand, crandc, creqv, crnand, crnor, cror, crorc, crxor,
|
||||
// Data cache
|
||||
dcbf, dcbi, dcbst, dcbt, dcbtst, dcbz, dcbz128,
|
||||
// Division
|
||||
divdux, divdx, divwux, divwx,
|
||||
// Sync/barrier
|
||||
eieio,
|
||||
// Logical
|
||||
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,
|
||||
// Instruction cache
|
||||
icbi, isync,
|
||||
// Load byte
|
||||
lbz, lbzu, lbzux, lbzx,
|
||||
// Load doubleword
|
||||
ld, ldarx, ldbrx, ldu, ldux, ldx,
|
||||
// Load float
|
||||
lfd, lfdu, lfdux, lfdx, lfs, lfsu, lfsux, lfsx,
|
||||
// Load halfword
|
||||
lha, lhau, lhaux, lhax, lhbrx, lhz, lhzu, lhzux, lhzx,
|
||||
// Load multiple/string
|
||||
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,
|
||||
// Load word
|
||||
lwa, lwarx, lwaux, lwax, lwbrx, lwz, lwzu, lwzux, lwzx,
|
||||
// Move CR
|
||||
mcrf, mcrfs, mcrxr,
|
||||
// Move from special
|
||||
mfcr, mffsx, mfmsr, mfspr, mftb, mfvscr,
|
||||
// Move to special
|
||||
mtcrf, mtfsb0x, mtfsb1x, mtfsfix, mtfsfx, mtmsr, mtmsrd, mtspr, mtvscr,
|
||||
// Multiply
|
||||
mulhdux, mulhdx, mulhwux, mulhwx, mulldx, mulli, mullwx,
|
||||
// Logical
|
||||
nandx, negx, norx, orcx, ori, oris, orx,
|
||||
// Rotate
|
||||
rldclx, rldcrx, rldiclx, rldicrx, rldicx, rldimix, rlwimix, rlwinmx, rlwnmx,
|
||||
// System call
|
||||
sc,
|
||||
// Shift
|
||||
sldx, slwx, sradix, sradx, srawix, srawx, srdx, srwx,
|
||||
// Store byte
|
||||
stb, stbu, stbux, stbx,
|
||||
// Store doubleword
|
||||
std, stdbrx, stdcx, stdu, stdux, stdx,
|
||||
// Store float
|
||||
stfd, stfdu, stfdux, stfdx, stfiwx, stfs, stfsu, stfsux, stfsx,
|
||||
// Store halfword
|
||||
sth, sthbrx, sthu, sthux, sthx,
|
||||
// Store multiple/string
|
||||
stmw, stswi, stswx,
|
||||
// Store vector
|
||||
stvebx, stvehx, stvewx, stvewx128, stvlx, stvlx128, stvlxl, stvlxl128,
|
||||
stvrx, stvrx128, stvrxl, stvrxl128,
|
||||
stvx, stvx128, stvxl, stvxl128,
|
||||
// Store word
|
||||
stw, stwbrx, stwcx, stwu, stwux, stwx,
|
||||
// Subtract
|
||||
subfcx, subfex, subficx, subfmex, subfx, subfzex,
|
||||
// Sync
|
||||
sync,
|
||||
// Trap
|
||||
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,
|
||||
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,
|
||||
// XOR immediate
|
||||
xori, xoris, xorx,
|
||||
// Invalid
|
||||
Invalid,
|
||||
}
|
||||
|
||||
impl PpcOpcode {
|
||||
/// Returns true if this opcode is a branch instruction.
|
||||
pub fn is_branch(&self) -> bool {
|
||||
matches!(self, Self::bx | Self::bcx | Self::bclrx | Self::bcctrx)
|
||||
}
|
||||
|
||||
/// Returns true if this opcode is a system call.
|
||||
pub fn is_syscall(&self) -> bool {
|
||||
matches!(self, Self::sc)
|
||||
}
|
||||
|
||||
/// 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
|
||||
)
|
||||
}
|
||||
|
||||
/// 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
|
||||
)
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Invalid => "invalid",
|
||||
_ => {
|
||||
// Use debug formatting to get the variant name
|
||||
// This is a placeholder - in practice we'd have a lookup table
|
||||
"?"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for PpcOpcode {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
std::fmt::Debug::fmt(self, f)
|
||||
}
|
||||
}
|
||||
13
crates/xenia-debugger/Cargo.toml
Normal file
13
crates/xenia-debugger/Cargo.toml
Normal file
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "xenia-debugger"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
xenia-types = { workspace = true }
|
||||
xenia-memory = { workspace = true }
|
||||
xenia-cpu = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
7
crates/xenia-debugger/src/breakpoint.rs
Normal file
7
crates/xenia-debugger/src/breakpoint.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
/// A code breakpoint at a specific guest address.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Breakpoint {
|
||||
pub addr: u32,
|
||||
pub enabled: bool,
|
||||
pub condition: Option<String>,
|
||||
}
|
||||
125
crates/xenia-debugger/src/lib.rs
Normal file
125
crates/xenia-debugger/src/lib.rs
Normal file
@@ -0,0 +1,125 @@
|
||||
pub mod breakpoint;
|
||||
pub mod trace;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use xenia_cpu::context::PpcContext;
|
||||
use xenia_memory::MemoryAccess;
|
||||
|
||||
pub use breakpoint::Breakpoint;
|
||||
pub use trace::TraceEntry;
|
||||
|
||||
/// The debugger. Hooks into every instruction step for observation.
|
||||
pub struct Debugger {
|
||||
pub breakpoints: HashMap<u32, Breakpoint>,
|
||||
pub trace_log: Vec<TraceEntry>,
|
||||
pub trace_enabled: bool,
|
||||
pub max_trace_entries: usize,
|
||||
pub paused: bool,
|
||||
pub step_mode: StepMode,
|
||||
break_pending: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum StepMode {
|
||||
/// Run freely until breakpoint or pause
|
||||
Run,
|
||||
/// Execute one instruction then pause
|
||||
StepInto,
|
||||
/// Run but break after current function returns (when LR changes)
|
||||
StepOver { return_addr: u32 },
|
||||
}
|
||||
|
||||
impl Debugger {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
breakpoints: HashMap::new(),
|
||||
trace_log: Vec::new(),
|
||||
trace_enabled: true,
|
||||
max_trace_entries: 100_000,
|
||||
paused: true, // Start paused for debugging
|
||||
step_mode: StepMode::StepInto,
|
||||
break_pending: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Called before each instruction executes.
|
||||
pub fn pre_step(&mut self, ctx: &PpcContext, _mem: &dyn MemoryAccess) {
|
||||
// Check breakpoints
|
||||
if let Some(bp) = self.breakpoints.get(&ctx.pc) {
|
||||
if bp.enabled {
|
||||
self.break_pending = true;
|
||||
tracing::info!("Breakpoint hit at {:#010x}", ctx.pc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Called after each instruction executes.
|
||||
pub fn post_step(&mut self, ctx: &PpcContext, _mem: &dyn MemoryAccess) {
|
||||
// Log to trace
|
||||
if self.trace_enabled {
|
||||
if self.trace_log.len() >= self.max_trace_entries {
|
||||
self.trace_log.remove(0);
|
||||
}
|
||||
self.trace_log.push(TraceEntry {
|
||||
pc: ctx.pc,
|
||||
cycle: ctx.cycle_count,
|
||||
gpr_snapshot: [ctx.gpr[0], ctx.gpr[1], ctx.gpr[3], ctx.gpr[4]],
|
||||
lr: ctx.lr,
|
||||
});
|
||||
}
|
||||
|
||||
// Handle step mode
|
||||
match self.step_mode {
|
||||
StepMode::StepInto => {
|
||||
self.break_pending = true;
|
||||
}
|
||||
StepMode::StepOver { return_addr } => {
|
||||
if ctx.pc == return_addr {
|
||||
self.break_pending = true;
|
||||
}
|
||||
}
|
||||
StepMode::Run => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Should we break execution?
|
||||
pub fn should_break(&self) -> bool {
|
||||
self.break_pending || self.paused
|
||||
}
|
||||
|
||||
/// Add a breakpoint at the given address.
|
||||
pub fn add_breakpoint(&mut self, addr: u32) {
|
||||
self.breakpoints.insert(addr, Breakpoint { addr, enabled: true, condition: None });
|
||||
}
|
||||
|
||||
/// Remove a breakpoint.
|
||||
pub fn remove_breakpoint(&mut self, addr: u32) {
|
||||
self.breakpoints.remove(&addr);
|
||||
}
|
||||
|
||||
/// Continue execution.
|
||||
pub fn continue_execution(&mut self) {
|
||||
self.paused = false;
|
||||
self.break_pending = false;
|
||||
self.step_mode = StepMode::Run;
|
||||
}
|
||||
|
||||
/// Step one instruction.
|
||||
pub fn step_into(&mut self) {
|
||||
self.paused = false;
|
||||
self.break_pending = false;
|
||||
self.step_mode = StepMode::StepInto;
|
||||
}
|
||||
|
||||
/// Clear break state after handling.
|
||||
pub fn acknowledge_break(&mut self) {
|
||||
self.break_pending = false;
|
||||
self.paused = true;
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Debugger {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
9
crates/xenia-debugger/src/trace.rs
Normal file
9
crates/xenia-debugger/src/trace.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
/// A single entry in the instruction trace log.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TraceEntry {
|
||||
pub pc: u32,
|
||||
pub cycle: u64,
|
||||
/// Snapshot of key GPRs: [r0, r1(sp), r3(arg0/retval), r4(arg1)]
|
||||
pub gpr_snapshot: [u64; 4],
|
||||
pub lr: u64,
|
||||
}
|
||||
13
crates/xenia-gpu/Cargo.toml
Normal file
13
crates/xenia-gpu/Cargo.toml
Normal file
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "xenia-gpu"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
xenia-types = { workspace = true }
|
||||
xenia-memory = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
byteorder = { workspace = true }
|
||||
17
crates/xenia-gpu/src/command_processor.rs
Normal file
17
crates/xenia-gpu/src/command_processor.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
/// PM4 command processor stub.
|
||||
/// Will parse the GPU command ring buffer and dispatch to render operations.
|
||||
pub struct CommandProcessor {
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
impl CommandProcessor {
|
||||
pub fn new() -> Self {
|
||||
Self { enabled: false }
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for CommandProcessor {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
21
crates/xenia-gpu/src/lib.rs
Normal file
21
crates/xenia-gpu/src/lib.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
pub mod command_processor;
|
||||
pub mod register_file;
|
||||
|
||||
/// Stub GPU system for initial implementation.
|
||||
pub struct GpuSystem {
|
||||
pub register_file: register_file::RegisterFile,
|
||||
}
|
||||
|
||||
impl GpuSystem {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
register_file: register_file::RegisterFile::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for GpuSystem {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
28
crates/xenia-gpu/src/register_file.rs
Normal file
28
crates/xenia-gpu/src/register_file.rs
Normal file
@@ -0,0 +1,28 @@
|
||||
/// Xenos GPU register file. 0x6000 32-bit registers.
|
||||
pub struct RegisterFile {
|
||||
pub regs: Vec<u32>,
|
||||
}
|
||||
|
||||
impl RegisterFile {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
regs: vec![0u32; 0x6000],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read(&self, index: u32) -> u32 {
|
||||
self.regs.get(index as usize).copied().unwrap_or(0)
|
||||
}
|
||||
|
||||
pub fn write(&mut self, index: u32, value: u32) {
|
||||
if let Some(r) = self.regs.get_mut(index as usize) {
|
||||
*r = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for RegisterFile {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
10
crates/xenia-hid/Cargo.toml
Normal file
10
crates/xenia-hid/Cargo.toml
Normal file
@@ -0,0 +1,10 @@
|
||||
[package]
|
||||
name = "xenia-hid"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
xenia-types = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
47
crates/xenia-hid/src/lib.rs
Normal file
47
crates/xenia-hid/src/lib.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
/// Human input device system stub.
|
||||
pub struct InputSystem {
|
||||
pub gamepad: GamepadState,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Copy)]
|
||||
pub struct GamepadState {
|
||||
pub buttons: u16,
|
||||
pub left_trigger: u8,
|
||||
pub right_trigger: u8,
|
||||
pub left_stick_x: i16,
|
||||
pub left_stick_y: i16,
|
||||
pub right_stick_x: i16,
|
||||
pub right_stick_y: i16,
|
||||
}
|
||||
|
||||
/// Xbox 360 button flags
|
||||
pub mod buttons {
|
||||
pub const DPAD_UP: u16 = 0x0001;
|
||||
pub const DPAD_DOWN: u16 = 0x0002;
|
||||
pub const DPAD_LEFT: u16 = 0x0004;
|
||||
pub const DPAD_RIGHT: u16 = 0x0008;
|
||||
pub const START: u16 = 0x0010;
|
||||
pub const BACK: u16 = 0x0020;
|
||||
pub const LEFT_THUMB: u16 = 0x0040;
|
||||
pub const RIGHT_THUMB: u16 = 0x0080;
|
||||
pub const LEFT_SHOULDER: u16 = 0x0100;
|
||||
pub const RIGHT_SHOULDER: u16 = 0x0200;
|
||||
pub const A: u16 = 0x1000;
|
||||
pub const B: u16 = 0x2000;
|
||||
pub const X: u16 = 0x4000;
|
||||
pub const Y: u16 = 0x8000;
|
||||
}
|
||||
|
||||
impl InputSystem {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
gamepad: GamepadState::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for InputSystem {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
13
crates/xenia-kernel/Cargo.toml
Normal file
13
crates/xenia-kernel/Cargo.toml
Normal file
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "xenia-kernel"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
xenia-types = { workspace = true }
|
||||
xenia-memory = { workspace = true }
|
||||
xenia-cpu = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
763
crates/xenia-kernel/src/exports.rs
Normal file
763
crates/xenia-kernel/src/exports.rs
Normal file
@@ -0,0 +1,763 @@
|
||||
//! HLE kernel export implementations (xboxkrnl.exe).
|
||||
//! Each export mirrors a function from xboxkrnl_table.inc.
|
||||
|
||||
use crate::objects::KernelObject;
|
||||
use crate::state::{KernelState, ModuleId};
|
||||
use xenia_cpu::PpcContext;
|
||||
use xenia_memory::{GuestMemory, MemoryAccess};
|
||||
|
||||
pub fn register_exports(state: &mut KernelState) {
|
||||
use ModuleId::Xboxkrnl;
|
||||
|
||||
// Debug
|
||||
state.register_export(Xboxkrnl, 0x01, "DbgBreakPoint", dbg_break_point);
|
||||
state.register_export(Xboxkrnl, 0x03, "DbgPrint", dbg_print);
|
||||
|
||||
// ExCreateThread and friends
|
||||
state.register_export(Xboxkrnl, 0x0D, "ExCreateThread", ex_create_thread);
|
||||
state.register_export(Xboxkrnl, 0x10, "ExGetXConfigSetting", ex_get_xconfig_setting);
|
||||
state.register_export(Xboxkrnl, 0x15, "ExRegisterTitleTerminateNotification", stub_success);
|
||||
state.register_export(Xboxkrnl, 0x19, "ExTerminateThread", ex_terminate_thread);
|
||||
|
||||
// Hal
|
||||
state.register_export(Xboxkrnl, 0x28, "HalReturnToFirmware", hal_return_to_firmware);
|
||||
|
||||
// I/O
|
||||
state.register_export(Xboxkrnl, 0x3C, "IoDismountVolumeByFileHandle", stub_success);
|
||||
|
||||
// Ke* Threading/Sync
|
||||
state.register_export(Xboxkrnl, 0x4D, "KeAcquireSpinLockAtRaisedIrql", stub_return_zero);
|
||||
state.register_export(Xboxkrnl, 0x52, "KeBugCheck", ke_bug_check);
|
||||
state.register_export(Xboxkrnl, 0x53, "KeBugCheckEx", ke_bug_check_ex);
|
||||
state.register_export(Xboxkrnl, 0x5A, "KeDelayExecutionThread", stub_success);
|
||||
state.register_export(Xboxkrnl, 0x5D, "KeEnableFpuExceptions", stub_success);
|
||||
state.register_export(Xboxkrnl, 0x5F, "KeEnterCriticalRegion", stub_success);
|
||||
state.register_export(Xboxkrnl, 0x66, "KeGetCurrentProcessType", ke_get_current_process_type);
|
||||
state.register_export(Xboxkrnl, 0x6B, "KeLockL2", stub_success);
|
||||
state.register_export(Xboxkrnl, 0x6C, "KeUnlockL2", stub_success);
|
||||
state.register_export(Xboxkrnl, 0x74, "KeInitializeSemaphore", ke_initialize_semaphore);
|
||||
state.register_export(Xboxkrnl, 0x7D, "KeLeaveCriticalRegion", stub_success);
|
||||
state.register_export(Xboxkrnl, 0x81, "KeQueryBasePriorityThread", stub_return_zero);
|
||||
state.register_export(Xboxkrnl, 0x83, "KeQueryPerformanceFrequency", ke_query_performance_frequency);
|
||||
state.register_export(Xboxkrnl, 0x84, "KeQuerySystemTime", ke_query_system_time);
|
||||
state.register_export(Xboxkrnl, 0x85, "KeRaiseIrqlToDpcLevel", stub_return_zero);
|
||||
state.register_export(Xboxkrnl, 0x88, "KeReleaseSemaphore", stub_return_zero);
|
||||
state.register_export(Xboxkrnl, 0x89, "KeReleaseSpinLockFromRaisedIrql", stub_success);
|
||||
state.register_export(Xboxkrnl, 0x8F, "KeResetEvent", stub_return_zero);
|
||||
state.register_export(Xboxkrnl, 0x92, "KeResumeThread", stub_return_zero);
|
||||
state.register_export(Xboxkrnl, 0x97, "KeSetAffinityThread", stub_return_zero);
|
||||
state.register_export(Xboxkrnl, 0x99, "KeSetBasePriorityThread", stub_return_zero);
|
||||
state.register_export(Xboxkrnl, 0x9B, "KeSetCurrentStackPointers", stub_success);
|
||||
state.register_export(Xboxkrnl, 0x9D, "KeSetEvent", stub_return_zero);
|
||||
state.register_export(Xboxkrnl, 0xAE, "KeTryToAcquireSpinLockAtRaisedIrql", ke_try_acquire_spinlock);
|
||||
state.register_export(Xboxkrnl, 0xAF, "KeWaitForMultipleObjects", stub_success);
|
||||
state.register_export(Xboxkrnl, 0xB0, "KeWaitForSingleObject", stub_success);
|
||||
state.register_export(Xboxkrnl, 0xB1, "KfAcquireSpinLock", stub_return_zero);
|
||||
state.register_export(Xboxkrnl, 0xB3, "KfLowerIrql", stub_success);
|
||||
state.register_export(Xboxkrnl, 0xB4, "KfReleaseSpinLock", stub_success);
|
||||
state.register_export(Xboxkrnl, 0x0152, "KeTlsAlloc", ke_tls_alloc);
|
||||
state.register_export(Xboxkrnl, 0x0153, "KeTlsFree", stub_success);
|
||||
state.register_export(Xboxkrnl, 0x0154, "KeTlsGetValue", ke_tls_get_value);
|
||||
state.register_export(Xboxkrnl, 0x0155, "KeTlsSetValue", ke_tls_set_value);
|
||||
state.register_export(Xboxkrnl, 0x01DF, "KiApcNormalRoutineNop", stub_success);
|
||||
|
||||
// Memory
|
||||
state.register_export(Xboxkrnl, 0xBA, "MmAllocatePhysicalMemoryEx", mm_allocate_physical_memory_ex);
|
||||
state.register_export(Xboxkrnl, 0xBB, "MmCreateKernelStack", mm_create_kernel_stack);
|
||||
state.register_export(Xboxkrnl, 0xBC, "MmDeleteKernelStack", stub_success);
|
||||
state.register_export(Xboxkrnl, 0xBD, "MmFreePhysicalMemory", stub_success);
|
||||
state.register_export(Xboxkrnl, 0xBE, "MmGetPhysicalAddress", mm_get_physical_address);
|
||||
state.register_export(Xboxkrnl, 0xC4, "MmQueryAddressProtect", mm_query_address_protect);
|
||||
state.register_export(Xboxkrnl, 0xC6, "MmQueryStatistics", mm_query_statistics);
|
||||
|
||||
// Nt*
|
||||
state.register_export(Xboxkrnl, 0xCC, "NtAllocateVirtualMemory", nt_allocate_virtual_memory);
|
||||
state.register_export(Xboxkrnl, 0xCD, "NtCancelTimer", stub_success);
|
||||
state.register_export(Xboxkrnl, 0xCE, "NtClearEvent", stub_success);
|
||||
state.register_export(Xboxkrnl, 0xCF, "NtClose", nt_close);
|
||||
state.register_export(Xboxkrnl, 0xD1, "NtCreateEvent", nt_create_event);
|
||||
state.register_export(Xboxkrnl, 0xD2, "NtCreateFile", nt_create_file);
|
||||
state.register_export(Xboxkrnl, 0xD5, "NtCreateSemaphore", nt_create_semaphore);
|
||||
state.register_export(Xboxkrnl, 0xD7, "NtCreateTimer", nt_create_timer);
|
||||
state.register_export(Xboxkrnl, 0xD9, "NtDeviceIoControlFile", stub_success);
|
||||
state.register_export(Xboxkrnl, 0xDA, "NtDuplicateObject", stub_success);
|
||||
state.register_export(Xboxkrnl, 0xDB, "NtFlushBuffersFile", stub_success);
|
||||
state.register_export(Xboxkrnl, 0xDC, "NtFreeVirtualMemory", stub_success);
|
||||
state.register_export(Xboxkrnl, 0xDF, "NtOpenFile", nt_open_file);
|
||||
state.register_export(Xboxkrnl, 0xE4, "NtQueryDirectoryFile", nt_query_directory_file);
|
||||
state.register_export(Xboxkrnl, 0xE7, "NtQueryFullAttributesFile", nt_query_full_attributes_file);
|
||||
state.register_export(Xboxkrnl, 0xE8, "NtQueryInformationFile", stub_success);
|
||||
state.register_export(Xboxkrnl, 0xEE, "NtQueryVirtualMemory", stub_success);
|
||||
state.register_export(Xboxkrnl, 0xEF, "NtQueryVolumeInformationFile", stub_success);
|
||||
state.register_export(Xboxkrnl, 0xF0, "NtReadFile", nt_read_file);
|
||||
state.register_export(Xboxkrnl, 0xF3, "NtReleaseSemaphore", stub_return_zero);
|
||||
state.register_export(Xboxkrnl, 0xF5, "NtResumeThread", stub_return_zero);
|
||||
state.register_export(Xboxkrnl, 0xF6, "NtSetEvent", stub_success);
|
||||
state.register_export(Xboxkrnl, 0xF7, "NtSetInformationFile", stub_success);
|
||||
state.register_export(Xboxkrnl, 0xFA, "NtSetTimerEx", stub_success);
|
||||
state.register_export(Xboxkrnl, 0xFC, "NtSuspendThread", stub_return_zero);
|
||||
state.register_export(Xboxkrnl, 0xFD, "NtWaitForSingleObjectEx", stub_success);
|
||||
state.register_export(Xboxkrnl, 0xFE, "NtWaitForMultipleObjectsEx", stub_success);
|
||||
state.register_export(Xboxkrnl, 0xFF, "NtWriteFile", nt_write_file);
|
||||
state.register_export(Xboxkrnl, 0x0101, "NtYieldExecution", stub_success);
|
||||
|
||||
// Object
|
||||
state.register_export(Xboxkrnl, 0x0103, "ObCreateSymbolicLink", stub_success);
|
||||
state.register_export(Xboxkrnl, 0x0104, "ObDeleteSymbolicLink", stub_success);
|
||||
state.register_export(Xboxkrnl, 0x0105, "ObDereferenceObject", stub_success);
|
||||
state.register_export(Xboxkrnl, 0x010B, "ObLookupThreadByThreadId", stub_success);
|
||||
state.register_export(Xboxkrnl, 0x010E, "ObOpenObjectByPointer", stub_success);
|
||||
state.register_export(Xboxkrnl, 0x0110, "ObReferenceObjectByHandle", stub_success);
|
||||
|
||||
// RTL
|
||||
state.register_export(Xboxkrnl, 0x0119, "RtlCaptureContext", rtl_capture_context);
|
||||
state.register_export(Xboxkrnl, 0x011B, "RtlCompareMemoryUlong", rtl_compare_memory_ulong);
|
||||
state.register_export(Xboxkrnl, 0x0125, "RtlEnterCriticalSection", rtl_enter_critical_section);
|
||||
state.register_export(Xboxkrnl, 0x0126, "RtlFillMemoryUlong", rtl_fill_memory_ulong);
|
||||
state.register_export(Xboxkrnl, 0x0127, "RtlFreeAnsiString", stub_success);
|
||||
state.register_export(Xboxkrnl, 0x012B, "RtlImageXexHeaderField", rtl_image_xex_header_field);
|
||||
state.register_export(Xboxkrnl, 0x012C, "RtlInitAnsiString", rtl_init_ansi_string);
|
||||
state.register_export(Xboxkrnl, 0x012D, "RtlInitUnicodeString", rtl_init_unicode_string);
|
||||
state.register_export(Xboxkrnl, 0x012E, "RtlInitializeCriticalSection", rtl_initialize_critical_section);
|
||||
state.register_export(Xboxkrnl, 0x012F, "RtlInitializeCriticalSectionAndSpinCount", rtl_initialize_critical_section);
|
||||
state.register_export(Xboxkrnl, 0x0130, "RtlLeaveCriticalSection", rtl_leave_critical_section);
|
||||
state.register_export(Xboxkrnl, 0x0133, "RtlMultiByteToUnicodeN", rtl_multi_byte_to_unicode_n);
|
||||
state.register_export(Xboxkrnl, 0x0135, "RtlNtStatusToDosError", rtl_nt_status_to_dos_error);
|
||||
state.register_export(Xboxkrnl, 0x0136, "RtlRaiseException", rtl_raise_exception);
|
||||
state.register_export(Xboxkrnl, 0x013B, "sprintf", stub_sprintf);
|
||||
state.register_export(Xboxkrnl, 0x013F, "RtlTimeFieldsToTime", stub_success);
|
||||
state.register_export(Xboxkrnl, 0x0140, "RtlTimeToTimeFields", stub_success);
|
||||
state.register_export(Xboxkrnl, 0x0141, "RtlTryEnterCriticalSection", rtl_try_enter_critical_section);
|
||||
state.register_export(Xboxkrnl, 0x0142, "RtlUnicodeStringToAnsiString", stub_success);
|
||||
state.register_export(Xboxkrnl, 0x0143, "RtlUnicodeToMultiByteN", stub_success);
|
||||
state.register_export(Xboxkrnl, 0x0147, "RtlUnwind", rtl_unwind);
|
||||
state.register_export(Xboxkrnl, 0x014D, "_vsnprintf", stub_vsnprintf);
|
||||
|
||||
// Stfs
|
||||
state.register_export(Xboxkrnl, 0x0259, "StfsCreateDevice", stub_success);
|
||||
state.register_export(Xboxkrnl, 0x025A, "StfsControlDevice", stub_success);
|
||||
|
||||
// Video
|
||||
state.register_export(Xboxkrnl, 0x01B1, "VdCallGraphicsNotificationRoutines", stub_success);
|
||||
state.register_export(Xboxkrnl, 0x01B4, "VdEnableDisableClockGating", stub_success);
|
||||
state.register_export(Xboxkrnl, 0x01B6, "VdEnableRingBufferRPtrWriteBack", stub_success);
|
||||
state.register_export(Xboxkrnl, 0x01B9, "VdGetCurrentDisplayGamma", stub_return_zero);
|
||||
state.register_export(Xboxkrnl, 0x01BA, "VdGetCurrentDisplayInformation", stub_success);
|
||||
state.register_export(Xboxkrnl, 0x01BD, "VdGetSystemCommandBuffer", vd_get_system_command_buffer);
|
||||
state.register_export(Xboxkrnl, 0x01C2, "VdInitializeEngines", stub_success);
|
||||
state.register_export(Xboxkrnl, 0x01C3, "VdInitializeRingBuffer", stub_success);
|
||||
state.register_export(Xboxkrnl, 0x01C5, "VdInitializeScalerCommandBuffer", stub_success);
|
||||
state.register_export(Xboxkrnl, 0x01C6, "VdIsHSIOTrainingSucceeded", vd_is_hsio_training_succeeded);
|
||||
state.register_export(Xboxkrnl, 0x01C7, "VdPersistDisplay", stub_success);
|
||||
state.register_export(Xboxkrnl, 0x01C9, "VdQueryVideoFlags", stub_return_zero);
|
||||
state.register_export(Xboxkrnl, 0x01CA, "VdQueryVideoMode", vd_query_video_mode);
|
||||
state.register_export(Xboxkrnl, 0x0269, "VdRetrainEDRAM", stub_success);
|
||||
state.register_export(Xboxkrnl, 0x026A, "VdRetrainEDRAMWorker", stub_success);
|
||||
state.register_export(Xboxkrnl, 0x01D3, "VdSetDisplayMode", stub_success);
|
||||
state.register_export(Xboxkrnl, 0x01D5, "VdSetGraphicsInterruptCallback", stub_success);
|
||||
state.register_export(Xboxkrnl, 0x01D9, "VdSetSystemCommandBufferGpuIdentifierAddress", stub_success);
|
||||
state.register_export(Xboxkrnl, 0x01DC, "VdShutdownEngines", stub_success);
|
||||
state.register_export(Xboxkrnl, 0x025B, "VdSwap", vd_swap);
|
||||
|
||||
// Audio
|
||||
state.register_export(Xboxkrnl, 0x01F3, "XAudioRegisterRenderDriverClient", xaudio_register_render_driver);
|
||||
state.register_export(Xboxkrnl, 0x01F4, "XAudioUnregisterRenderDriverClient", stub_success);
|
||||
state.register_export(Xboxkrnl, 0x01F5, "XAudioSubmitRenderDriverFrame", stub_success);
|
||||
state.register_export(Xboxkrnl, 0x01F7, "XAudioGetVoiceCategoryVolumeChangeMask", stub_return_zero);
|
||||
state.register_export(Xboxkrnl, 0x01F8, "XAudioGetVoiceCategoryVolume", stub_success);
|
||||
state.register_export(Xboxkrnl, 0x0224, "XMACreateContext", xma_create_context);
|
||||
state.register_export(Xboxkrnl, 0x0226, "XMAReleaseContext", stub_success);
|
||||
|
||||
// Crypto
|
||||
state.register_export(Xboxkrnl, 0x0192, "XeCryptSha", stub_success);
|
||||
state.register_export(Xboxkrnl, 0x0256, "XeKeysConsolePrivateKeySign", stub_success);
|
||||
state.register_export(Xboxkrnl, 0x0257, "XeKeysConsoleSignatureVerification", stub_success);
|
||||
|
||||
// Xex module
|
||||
state.register_export(Xboxkrnl, 0x0194, "XexCheckExecutablePrivilege", stub_return_zero);
|
||||
state.register_export(Xboxkrnl, 0x0195, "XexGetModuleHandle", stub_return_zero);
|
||||
state.register_export(Xboxkrnl, 0x0197, "XexGetProcedureAddress", xex_get_procedure_address);
|
||||
|
||||
// Exception handling
|
||||
state.register_export(Xboxkrnl, 0x01A5, "__C_specific_handler", c_specific_handler);
|
||||
}
|
||||
|
||||
// ===== Generic stubs =====
|
||||
|
||||
fn stub_success(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
ctx.gpr[3] = 0; // STATUS_SUCCESS
|
||||
}
|
||||
|
||||
fn stub_return_zero(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
ctx.gpr[3] = 0;
|
||||
}
|
||||
|
||||
// ===== Debug =====
|
||||
|
||||
fn dbg_break_point(_ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
tracing::warn!("DbgBreakPoint hit");
|
||||
}
|
||||
|
||||
fn dbg_print(ctx: &mut PpcContext, mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
let str_ptr = ctx.gpr[3] as u32;
|
||||
if str_ptr != 0 {
|
||||
let s = read_cstring(mem, str_ptr);
|
||||
tracing::info!("DbgPrint: {}", s);
|
||||
}
|
||||
ctx.gpr[3] = 0;
|
||||
}
|
||||
|
||||
// ===== Threading =====
|
||||
|
||||
fn ex_create_thread(ctx: &mut PpcContext, mem: &mut GuestMemory, state: &mut KernelState) {
|
||||
// r3 = handle_ptr, r4 = stack_size, r5 = thread_id_ptr, r6 = xapi_startup
|
||||
// r7 = start_address, r8 = start_context, r9 = creation_flags
|
||||
let handle_ptr = ctx.gpr[3] as u32;
|
||||
let thread_id_ptr = ctx.gpr[5] as u32;
|
||||
|
||||
let tid = state.next_thread_id;
|
||||
state.next_thread_id += 1;
|
||||
let handle = state.alloc_handle_for(KernelObject::Thread { id: tid });
|
||||
|
||||
if handle_ptr != 0 {
|
||||
mem.write_u32(handle_ptr, handle);
|
||||
}
|
||||
if thread_id_ptr != 0 {
|
||||
mem.write_u32(thread_id_ptr, tid);
|
||||
}
|
||||
tracing::info!("ExCreateThread: handle={:#x} tid={}", handle, tid);
|
||||
ctx.gpr[3] = 0; // STATUS_SUCCESS
|
||||
}
|
||||
|
||||
fn ex_terminate_thread(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
tracing::info!("ExTerminateThread: exit_status={:#x}", ctx.gpr[3]);
|
||||
ctx.gpr[3] = 0;
|
||||
}
|
||||
|
||||
fn hal_return_to_firmware(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
tracing::warn!("HalReturnToFirmware: reason={:#x}", ctx.gpr[3]);
|
||||
ctx.gpr[3] = 0;
|
||||
}
|
||||
|
||||
// ===== Ke* =====
|
||||
|
||||
fn ke_bug_check(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
tracing::error!("KeBugCheck: code={:#x}", ctx.gpr[3]);
|
||||
ctx.gpr[3] = 0;
|
||||
}
|
||||
|
||||
fn ke_bug_check_ex(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
tracing::error!("KeBugCheckEx: code={:#x} p1={:#x} p2={:#x} p3={:#x}",
|
||||
ctx.gpr[3], ctx.gpr[4], ctx.gpr[5], ctx.gpr[6]);
|
||||
ctx.gpr[3] = 0;
|
||||
}
|
||||
|
||||
fn ke_get_current_process_type(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
ctx.gpr[3] = 1; // PROC_USER
|
||||
}
|
||||
|
||||
fn ke_query_performance_frequency(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
ctx.gpr[3] = 50_000_000; // 50 MHz
|
||||
}
|
||||
|
||||
fn ke_query_system_time(ctx: &mut PpcContext, mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
let time_ptr = ctx.gpr[3] as u32;
|
||||
if time_ptr != 0 {
|
||||
let fake_time: u64 = 132_500_000_000_000_000; // ~2021 FILETIME
|
||||
mem.write_u32(time_ptr, (fake_time >> 32) as u32);
|
||||
mem.write_u32(time_ptr + 4, fake_time as u32);
|
||||
}
|
||||
}
|
||||
|
||||
fn ke_initialize_semaphore(ctx: &mut PpcContext, mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
// r3 = semaphore_ptr, r4 = count, r5 = limit
|
||||
let sem_ptr = ctx.gpr[3] as u32;
|
||||
if sem_ptr != 0 {
|
||||
// Zero-init the KSEMAPHORE structure (0x14 bytes)
|
||||
for i in (0..0x14).step_by(4) {
|
||||
mem.write_u32(sem_ptr + i, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ke_try_acquire_spinlock(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
ctx.gpr[3] = 1; // TRUE (acquired successfully in single-threaded mode)
|
||||
}
|
||||
|
||||
fn ke_tls_alloc(ctx: &mut PpcContext, _mem: &mut GuestMemory, state: &mut KernelState) {
|
||||
ctx.gpr[3] = state.tls_alloc() as u64;
|
||||
}
|
||||
|
||||
fn ke_tls_get_value(ctx: &mut PpcContext, _mem: &mut GuestMemory, state: &mut KernelState) {
|
||||
let index = ctx.gpr[3] as u32;
|
||||
ctx.gpr[3] = state.tls_get(index);
|
||||
}
|
||||
|
||||
fn ke_tls_set_value(ctx: &mut PpcContext, _mem: &mut GuestMemory, state: &mut KernelState) {
|
||||
let index = ctx.gpr[3] as u32;
|
||||
let value = ctx.gpr[4];
|
||||
state.tls_set(index, value);
|
||||
ctx.gpr[3] = 1; // TRUE
|
||||
}
|
||||
|
||||
fn ex_get_xconfig_setting(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
ctx.gpr[3] = 0; // STATUS_SUCCESS (writes nothing)
|
||||
}
|
||||
|
||||
// ===== Memory =====
|
||||
|
||||
fn nt_allocate_virtual_memory(ctx: &mut PpcContext, mem: &mut GuestMemory, state: &mut KernelState) {
|
||||
// r3 = base_addr_ptr (in/out), r4 = region_size_ptr (in/out)
|
||||
// r5 = alloc_type, r6 = protect
|
||||
let base_ptr = ctx.gpr[3] as u32;
|
||||
let size_ptr = ctx.gpr[4] as u32;
|
||||
|
||||
let requested_base = mem.read_u32(base_ptr);
|
||||
let requested_size = mem.read_u32(size_ptr);
|
||||
|
||||
let aligned_size = (requested_size + 0xFFF) & !0xFFF;
|
||||
if aligned_size == 0 {
|
||||
ctx.gpr[3] = 0xC000_0010; // STATUS_INVALID_PARAMETER
|
||||
return;
|
||||
}
|
||||
|
||||
let base = if requested_base != 0 {
|
||||
// Try to allocate at the requested address
|
||||
let protect = xenia_memory::page_table::MemoryProtect::READ
|
||||
| xenia_memory::page_table::MemoryProtect::WRITE;
|
||||
if mem.alloc(requested_base, aligned_size, protect).is_ok() {
|
||||
requested_base
|
||||
} else {
|
||||
// Already allocated? Treat as success (common for re-commit)
|
||||
requested_base
|
||||
}
|
||||
} else {
|
||||
// Allocate from heap
|
||||
match state.heap_alloc(aligned_size, mem) {
|
||||
Some(addr) => addr,
|
||||
None => {
|
||||
tracing::warn!("NtAllocateVirtualMemory: heap exhausted (size={:#x})", aligned_size);
|
||||
ctx.gpr[3] = 0xC000_0017; // STATUS_NO_MEMORY
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
mem.write_u32(base_ptr, base);
|
||||
mem.write_u32(size_ptr, aligned_size);
|
||||
tracing::info!("NtAllocateVirtualMemory: base={:#010x} size={:#x}", base, aligned_size);
|
||||
ctx.gpr[3] = 0; // STATUS_SUCCESS
|
||||
}
|
||||
|
||||
fn mm_allocate_physical_memory_ex(ctx: &mut PpcContext, mem: &mut GuestMemory, state: &mut KernelState) {
|
||||
// r3 = size, r4 = protect, r5 = min_addr, r6 = max_addr, r7 = alignment
|
||||
let size = ctx.gpr[3] as u32;
|
||||
match state.heap_alloc(size, mem) {
|
||||
Some(addr) => ctx.gpr[3] = addr as u64,
|
||||
None => ctx.gpr[3] = 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn mm_create_kernel_stack(ctx: &mut PpcContext, mem: &mut GuestMemory, state: &mut KernelState) {
|
||||
// r3 = stack_size, r4 = reserved
|
||||
let size = std::cmp::max(ctx.gpr[3] as u32, 0x4000); // Min 16KB
|
||||
match state.stack_alloc(size, mem) {
|
||||
Some(top) => {
|
||||
tracing::info!("MmCreateKernelStack: top={:#010x} size={:#x}", top, size);
|
||||
ctx.gpr[3] = top as u64;
|
||||
}
|
||||
None => ctx.gpr[3] = 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn mm_get_physical_address(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
// r3 = virtual address -> return physical address
|
||||
ctx.gpr[3] = ctx.gpr[3] & 0x1FFF_FFFF; // Mask to 512MB physical
|
||||
}
|
||||
|
||||
fn mm_query_address_protect(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
// Return PAGE_READWRITE (0x04)
|
||||
ctx.gpr[3] = 0x04;
|
||||
}
|
||||
|
||||
fn mm_query_statistics(ctx: &mut PpcContext, mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
// r3 = stats_ptr — write fake memory statistics
|
||||
let ptr = ctx.gpr[3] as u32;
|
||||
if ptr != 0 {
|
||||
// Total physical = 512MB
|
||||
mem.write_u32(ptr + 0x04, 512 * 1024 * 1024); // TotalPhysicalPages (in bytes)
|
||||
mem.write_u32(ptr + 0x10, 256 * 1024 * 1024); // AvailablePages
|
||||
}
|
||||
ctx.gpr[3] = 0;
|
||||
}
|
||||
|
||||
// ===== File I/O =====
|
||||
|
||||
fn nt_create_file(ctx: &mut PpcContext, _mem: &mut GuestMemory, state: &mut KernelState) {
|
||||
let handle = state.alloc_handle_for(KernelObject::File { path: String::new() });
|
||||
tracing::info!("NtCreateFile: handle={:#x}", handle);
|
||||
ctx.gpr[3] = 0;
|
||||
}
|
||||
|
||||
fn nt_open_file(ctx: &mut PpcContext, _mem: &mut GuestMemory, state: &mut KernelState) {
|
||||
let handle = state.alloc_handle_for(KernelObject::File { path: String::new() });
|
||||
tracing::info!("NtOpenFile: handle={:#x}", handle);
|
||||
ctx.gpr[3] = 0;
|
||||
}
|
||||
|
||||
fn nt_read_file(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
ctx.gpr[3] = 0xC000_0011; // STATUS_END_OF_FILE
|
||||
}
|
||||
|
||||
fn nt_write_file(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
ctx.gpr[3] = 0; // STATUS_SUCCESS (discard data)
|
||||
}
|
||||
|
||||
fn nt_query_full_attributes_file(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
ctx.gpr[3] = 0xC000_0034; // STATUS_OBJECT_NAME_NOT_FOUND
|
||||
}
|
||||
|
||||
fn nt_query_directory_file(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
ctx.gpr[3] = 0xC000_0034; // STATUS_OBJECT_NAME_NOT_FOUND
|
||||
}
|
||||
|
||||
fn nt_close(ctx: &mut PpcContext, _mem: &mut GuestMemory, state: &mut KernelState) {
|
||||
let handle = ctx.gpr[3] as u32;
|
||||
state.objects.remove(&handle);
|
||||
ctx.gpr[3] = 0;
|
||||
}
|
||||
|
||||
fn nt_create_event(ctx: &mut PpcContext, mem: &mut GuestMemory, state: &mut KernelState) {
|
||||
// r3 = handle_ptr, r4 = obj_attrs, r5 = event_type, r6 = initial_state
|
||||
let handle_ptr = ctx.gpr[3] as u32;
|
||||
let manual_reset = ctx.gpr[5] != 0;
|
||||
let signaled = ctx.gpr[6] != 0;
|
||||
let handle = state.alloc_handle_for(KernelObject::Event { manual_reset, signaled });
|
||||
if handle_ptr != 0 {
|
||||
mem.write_u32(handle_ptr, handle);
|
||||
}
|
||||
ctx.gpr[3] = 0;
|
||||
}
|
||||
|
||||
fn nt_create_semaphore(ctx: &mut PpcContext, mem: &mut GuestMemory, state: &mut KernelState) {
|
||||
// r3 = handle_ptr, r4 = obj_attrs, r5 = initial_count, r6 = max_count
|
||||
let handle_ptr = ctx.gpr[3] as u32;
|
||||
let count = ctx.gpr[5] as i32;
|
||||
let max = ctx.gpr[6] as i32;
|
||||
let handle = state.alloc_handle_for(KernelObject::Semaphore { count, max });
|
||||
if handle_ptr != 0 {
|
||||
mem.write_u32(handle_ptr, handle);
|
||||
}
|
||||
ctx.gpr[3] = 0;
|
||||
}
|
||||
|
||||
fn nt_create_timer(ctx: &mut PpcContext, mem: &mut GuestMemory, state: &mut KernelState) {
|
||||
let handle_ptr = ctx.gpr[3] as u32;
|
||||
let handle = state.alloc_handle_for(KernelObject::Timer);
|
||||
if handle_ptr != 0 {
|
||||
mem.write_u32(handle_ptr, handle);
|
||||
}
|
||||
ctx.gpr[3] = 0;
|
||||
}
|
||||
|
||||
// ===== RTL =====
|
||||
|
||||
fn rtl_initialize_critical_section(ctx: &mut PpcContext, mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
// r3 = critical_section_ptr (28 bytes on Xbox 360)
|
||||
let cs_ptr = ctx.gpr[3] as u32;
|
||||
if cs_ptr != 0 {
|
||||
for i in (0..28).step_by(4) {
|
||||
mem.write_u32(cs_ptr + i, 0);
|
||||
}
|
||||
// Set recursion count to -1 (unlocked)
|
||||
mem.write_u32(cs_ptr + 8, 0xFFFF_FFFF_u32);
|
||||
}
|
||||
ctx.gpr[3] = 0;
|
||||
}
|
||||
|
||||
fn rtl_enter_critical_section(ctx: &mut PpcContext, mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
// r3 = critical_section_ptr
|
||||
// For single-threaded: increment lock count, always succeed
|
||||
let cs_ptr = ctx.gpr[3] as u32;
|
||||
if cs_ptr != 0 {
|
||||
let lock_count = mem.read_u32(cs_ptr + 4) as i32;
|
||||
mem.write_u32(cs_ptr + 4, (lock_count + 1) as u32);
|
||||
let recursion = mem.read_u32(cs_ptr + 8) as i32;
|
||||
mem.write_u32(cs_ptr + 8, (recursion + 1) as u32);
|
||||
}
|
||||
ctx.gpr[3] = 0;
|
||||
}
|
||||
|
||||
fn rtl_leave_critical_section(ctx: &mut PpcContext, mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
let cs_ptr = ctx.gpr[3] as u32;
|
||||
if cs_ptr != 0 {
|
||||
let lock_count = mem.read_u32(cs_ptr + 4) as i32;
|
||||
mem.write_u32(cs_ptr + 4, (lock_count - 1) as u32);
|
||||
let recursion = mem.read_u32(cs_ptr + 8) as i32;
|
||||
mem.write_u32(cs_ptr + 8, (recursion - 1) as u32);
|
||||
}
|
||||
ctx.gpr[3] = 0;
|
||||
}
|
||||
|
||||
fn rtl_try_enter_critical_section(ctx: &mut PpcContext, mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
// Always succeed in single-threaded mode
|
||||
let cs_ptr = ctx.gpr[3] as u32;
|
||||
if cs_ptr != 0 {
|
||||
let lock_count = mem.read_u32(cs_ptr + 4) as i32;
|
||||
mem.write_u32(cs_ptr + 4, (lock_count + 1) as u32);
|
||||
let recursion = mem.read_u32(cs_ptr + 8) as i32;
|
||||
mem.write_u32(cs_ptr + 8, (recursion + 1) as u32);
|
||||
}
|
||||
ctx.gpr[3] = 1; // TRUE
|
||||
}
|
||||
|
||||
fn rtl_init_ansi_string(ctx: &mut PpcContext, mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
let dest_ptr = ctx.gpr[3] as u32;
|
||||
let src_ptr = ctx.gpr[4] as u32;
|
||||
if src_ptr != 0 {
|
||||
let mut len: u16 = 0;
|
||||
let mut addr = src_ptr;
|
||||
while mem.read_u8(addr) != 0 {
|
||||
len += 1;
|
||||
addr += 1;
|
||||
}
|
||||
mem.write_u16(dest_ptr, len);
|
||||
mem.write_u16(dest_ptr + 2, len + 1);
|
||||
mem.write_u32(dest_ptr + 4, src_ptr);
|
||||
} else {
|
||||
mem.write_u16(dest_ptr, 0);
|
||||
mem.write_u16(dest_ptr + 2, 0);
|
||||
mem.write_u32(dest_ptr + 4, 0);
|
||||
}
|
||||
}
|
||||
|
||||
fn rtl_init_unicode_string(ctx: &mut PpcContext, mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
let dest_ptr = ctx.gpr[3] as u32;
|
||||
let src_ptr = ctx.gpr[4] as u32;
|
||||
if src_ptr != 0 {
|
||||
let mut len: u16 = 0;
|
||||
let mut addr = src_ptr;
|
||||
while mem.read_u16(addr) != 0 {
|
||||
len += 2;
|
||||
addr += 2;
|
||||
}
|
||||
mem.write_u16(dest_ptr, len);
|
||||
mem.write_u16(dest_ptr + 2, len + 2);
|
||||
mem.write_u32(dest_ptr + 4, src_ptr);
|
||||
} else {
|
||||
mem.write_u16(dest_ptr, 0);
|
||||
mem.write_u16(dest_ptr + 2, 0);
|
||||
mem.write_u32(dest_ptr + 4, 0);
|
||||
}
|
||||
}
|
||||
|
||||
fn rtl_capture_context(ctx: &mut PpcContext, mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
// r3 = context_ptr — write CPU registers to CONTEXT structure
|
||||
let ptr = ctx.gpr[3] as u32;
|
||||
if ptr != 0 {
|
||||
// Write GPRs at offset 0 (simplified)
|
||||
for i in 0..32 {
|
||||
mem.write_u64(ptr + (i * 8) as u32, ctx.gpr[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn rtl_compare_memory_ulong(ctx: &mut PpcContext, mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
// r3 = source, r4 = length, r5 = pattern
|
||||
let source = ctx.gpr[3] as u32;
|
||||
let length = ctx.gpr[4] as u32;
|
||||
let pattern = ctx.gpr[5] as u32;
|
||||
let mut matched: u32 = 0;
|
||||
let count = length / 4;
|
||||
for i in 0..count {
|
||||
let val = mem.read_u32(source + i * 4);
|
||||
if val != pattern {
|
||||
break;
|
||||
}
|
||||
matched += 4;
|
||||
}
|
||||
ctx.gpr[3] = matched as u64;
|
||||
}
|
||||
|
||||
fn rtl_fill_memory_ulong(ctx: &mut PpcContext, mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
// r3 = destination, r4 = length, r5 = pattern
|
||||
let dest = ctx.gpr[3] as u32;
|
||||
let length = ctx.gpr[4] as u32;
|
||||
let pattern = ctx.gpr[5] as u32;
|
||||
let count = length / 4;
|
||||
for i in 0..count {
|
||||
mem.write_u32(dest + i * 4, pattern);
|
||||
}
|
||||
}
|
||||
|
||||
fn rtl_image_xex_header_field(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
// r3 = xex_header_ptr, r4 = field_id
|
||||
// Return 0 for all fields
|
||||
ctx.gpr[3] = 0;
|
||||
}
|
||||
|
||||
fn rtl_multi_byte_to_unicode_n(ctx: &mut PpcContext, mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
// r3 = unicode_str, r4 = max_bytes_out, r5 = bytes_written_ptr
|
||||
// r6 = multi_byte_str, r7 = multi_byte_len
|
||||
let uni_ptr = ctx.gpr[3] as u32;
|
||||
let max_bytes = ctx.gpr[4] as u32;
|
||||
let written_ptr = ctx.gpr[5] as u32;
|
||||
let mb_ptr = ctx.gpr[6] as u32;
|
||||
let mb_len = ctx.gpr[7] as u32;
|
||||
|
||||
let max_chars = max_bytes / 2;
|
||||
let count = std::cmp::min(mb_len, max_chars);
|
||||
for i in 0..count {
|
||||
let byte = mem.read_u8(mb_ptr + i);
|
||||
mem.write_u16(uni_ptr + i * 2, byte as u16);
|
||||
}
|
||||
if written_ptr != 0 {
|
||||
mem.write_u32(written_ptr, count * 2);
|
||||
}
|
||||
ctx.gpr[3] = 0;
|
||||
}
|
||||
|
||||
fn rtl_nt_status_to_dos_error(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
// Simple mapping for common cases
|
||||
let status = ctx.gpr[3] as u32;
|
||||
ctx.gpr[3] = match status {
|
||||
0 => 0, // ERROR_SUCCESS
|
||||
0xC000_0034 => 2, // ERROR_FILE_NOT_FOUND
|
||||
0xC000_0011 => 38, // ERROR_HANDLE_EOF
|
||||
_ => status as u64, // Pass through
|
||||
};
|
||||
}
|
||||
|
||||
fn rtl_raise_exception(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
tracing::warn!("RtlRaiseException: record_ptr={:#010x}", ctx.gpr[3]);
|
||||
// Don't halt — just log and return
|
||||
}
|
||||
|
||||
fn rtl_unwind(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
tracing::warn!("RtlUnwind: target_frame={:#010x}", ctx.gpr[3]);
|
||||
// Stub — in a real implementation this would walk the stack
|
||||
}
|
||||
|
||||
fn stub_sprintf(ctx: &mut PpcContext, mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
let dest = ctx.gpr[3] as u32;
|
||||
let fmt = ctx.gpr[4] as u32;
|
||||
if fmt != 0 && dest != 0 {
|
||||
let mut addr = fmt;
|
||||
let mut daddr = dest;
|
||||
loop {
|
||||
let c = mem.read_u8(addr);
|
||||
mem.write_u8(daddr, c);
|
||||
if c == 0 { break; }
|
||||
addr += 1;
|
||||
daddr += 1;
|
||||
}
|
||||
}
|
||||
ctx.gpr[3] = 0;
|
||||
}
|
||||
|
||||
fn stub_vsnprintf(ctx: &mut PpcContext, mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
// r3 = buffer, r4 = count, r5 = format, r6 = va_list
|
||||
let dest = ctx.gpr[3] as u32;
|
||||
let fmt = ctx.gpr[5] as u32;
|
||||
if fmt != 0 && dest != 0 {
|
||||
let mut addr = fmt;
|
||||
let mut daddr = dest;
|
||||
loop {
|
||||
let c = mem.read_u8(addr);
|
||||
mem.write_u8(daddr, c);
|
||||
if c == 0 { break; }
|
||||
addr += 1;
|
||||
daddr += 1;
|
||||
}
|
||||
}
|
||||
ctx.gpr[3] = 0;
|
||||
}
|
||||
|
||||
// ===== Video =====
|
||||
|
||||
fn vd_query_video_mode(ctx: &mut PpcContext, mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
let mode_ptr = ctx.gpr[3] as u32;
|
||||
if mode_ptr != 0 {
|
||||
mem.write_u32(mode_ptr, 1280);
|
||||
mem.write_u32(mode_ptr + 4, 720);
|
||||
mem.write_u32(mode_ptr + 8, 0); // is_interlaced
|
||||
mem.write_u32(mode_ptr + 12, 1); // is_widescreen
|
||||
mem.write_u32(mode_ptr + 16, 60); // refresh_rate
|
||||
}
|
||||
ctx.gpr[3] = 0;
|
||||
}
|
||||
|
||||
fn vd_get_system_command_buffer(ctx: &mut PpcContext, mem: &mut GuestMemory, state: &mut KernelState) {
|
||||
// r3 = cmd_buffer_ptr_ptr, r4 = cmd_buffer_size_ptr
|
||||
let buf_ptr_ptr = ctx.gpr[3] as u32;
|
||||
let buf_size_ptr = ctx.gpr[4] as u32;
|
||||
|
||||
if state.gpu_command_buffer == 0 {
|
||||
// Allocate a 64KB command buffer
|
||||
if let Some(addr) = state.heap_alloc(0x10000, mem) {
|
||||
state.gpu_command_buffer = addr;
|
||||
}
|
||||
}
|
||||
|
||||
if buf_ptr_ptr != 0 {
|
||||
mem.write_u32(buf_ptr_ptr, state.gpu_command_buffer);
|
||||
}
|
||||
if buf_size_ptr != 0 {
|
||||
mem.write_u32(buf_size_ptr, 0x10000);
|
||||
}
|
||||
ctx.gpr[3] = 0;
|
||||
}
|
||||
|
||||
fn vd_is_hsio_training_succeeded(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
ctx.gpr[3] = 1; // TRUE
|
||||
}
|
||||
|
||||
fn vd_swap(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
tracing::info!("VdSwap (frame boundary)");
|
||||
ctx.gpr[3] = 0;
|
||||
}
|
||||
|
||||
// ===== Audio =====
|
||||
|
||||
fn xaudio_register_render_driver(ctx: &mut PpcContext, _mem: &mut GuestMemory, state: &mut KernelState) {
|
||||
let handle = state.alloc_handle();
|
||||
tracing::info!("XAudioRegisterRenderDriverClient: handle={:#x}", handle);
|
||||
// r3 = callback_ptr, r4 = driver_ptr -> write handle
|
||||
ctx.gpr[3] = 0;
|
||||
}
|
||||
|
||||
fn xma_create_context(ctx: &mut PpcContext, _mem: &mut GuestMemory, state: &mut KernelState) {
|
||||
let handle = state.alloc_handle();
|
||||
tracing::info!("XMACreateContext: handle={:#x}", handle);
|
||||
ctx.gpr[3] = handle as u64;
|
||||
}
|
||||
|
||||
// ===== Xex =====
|
||||
|
||||
fn xex_get_procedure_address(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
let ordinal = ctx.gpr[4] as u32;
|
||||
tracing::warn!("XexGetProcedureAddress: ordinal={:#x} not found", ordinal);
|
||||
ctx.gpr[3] = 0xC000_0034; // STATUS_OBJECT_NAME_NOT_FOUND
|
||||
}
|
||||
|
||||
// ===== Exception handling =====
|
||||
|
||||
fn c_specific_handler(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
tracing::warn!("__C_specific_handler called (exception handling stub)");
|
||||
ctx.gpr[3] = 1; // ExceptionContinueSearch
|
||||
}
|
||||
|
||||
// ===== Helpers =====
|
||||
|
||||
fn read_cstring(mem: &GuestMemory, addr: u32) -> String {
|
||||
let mut s = String::new();
|
||||
let mut a = addr;
|
||||
loop {
|
||||
let c = mem.read_u8(a);
|
||||
if c == 0 { break; }
|
||||
s.push(c as char);
|
||||
a += 1;
|
||||
if s.len() > 512 { break; } // Safety limit
|
||||
}
|
||||
s
|
||||
}
|
||||
6
crates/xenia-kernel/src/lib.rs
Normal file
6
crates/xenia-kernel/src/lib.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
pub mod exports;
|
||||
pub mod objects;
|
||||
pub mod state;
|
||||
pub mod xam;
|
||||
|
||||
pub use state::{KernelState, ModuleId};
|
||||
12
crates/xenia-kernel/src/objects.rs
Normal file
12
crates/xenia-kernel/src/objects.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
//! Kernel object tracking for HLE.
|
||||
|
||||
/// Kernel object types tracked by handle.
|
||||
#[derive(Debug)]
|
||||
pub enum KernelObject {
|
||||
Event { manual_reset: bool, signaled: bool },
|
||||
Semaphore { count: i32, max: i32 },
|
||||
File { path: String },
|
||||
Thread { id: u32 },
|
||||
Timer,
|
||||
Mutex,
|
||||
}
|
||||
159
crates/xenia-kernel/src/state.rs
Normal file
159
crates/xenia-kernel/src/state.rs
Normal file
@@ -0,0 +1,159 @@
|
||||
use std::collections::HashMap;
|
||||
use xenia_cpu::PpcContext;
|
||||
use xenia_memory::GuestMemory;
|
||||
|
||||
use crate::objects::KernelObject;
|
||||
|
||||
/// Function signature for HLE kernel exports.
|
||||
pub type KernelExportFn = fn(&mut PpcContext, &mut GuestMemory, &mut KernelState);
|
||||
|
||||
/// Module identifier for kernel exports.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum ModuleId {
|
||||
Xboxkrnl,
|
||||
Xam,
|
||||
Xbdm,
|
||||
}
|
||||
|
||||
/// Central kernel state tracking all guest OS state.
|
||||
pub struct KernelState {
|
||||
exports: HashMap<(ModuleId, u32), (&'static str, KernelExportFn)>,
|
||||
next_handle: u32,
|
||||
pub tls_slots: HashMap<u32, u64>,
|
||||
next_tls_index: u32,
|
||||
/// Kernel object table: handle → object
|
||||
pub objects: HashMap<u32, KernelObject>,
|
||||
/// Bump allocator for guest heap (NtAllocateVirtualMemory etc.)
|
||||
pub heap_cursor: u32,
|
||||
/// Stack allocator cursor for MmCreateKernelStack
|
||||
pub stack_cursor: u32,
|
||||
/// GPU command buffer address (set by VdGetSystemCommandBuffer)
|
||||
pub gpu_command_buffer: u32,
|
||||
/// Image base of the loaded XEX (for XexExecutableModuleHandle etc.)
|
||||
pub image_base: u32,
|
||||
/// Next thread ID
|
||||
pub next_thread_id: u32,
|
||||
}
|
||||
|
||||
impl KernelState {
|
||||
pub fn new() -> Self {
|
||||
let mut state = Self {
|
||||
exports: HashMap::new(),
|
||||
next_handle: 0x1000,
|
||||
tls_slots: HashMap::new(),
|
||||
next_tls_index: 0,
|
||||
objects: HashMap::new(),
|
||||
heap_cursor: 0x4000_0000, // Start of user heap region
|
||||
stack_cursor: 0x7100_0000, // Above main stack
|
||||
gpu_command_buffer: 0,
|
||||
image_base: 0,
|
||||
next_thread_id: 1,
|
||||
};
|
||||
crate::exports::register_exports(&mut state);
|
||||
crate::xam::register_exports(&mut state);
|
||||
state
|
||||
}
|
||||
|
||||
pub fn register_export(
|
||||
&mut self,
|
||||
module: ModuleId,
|
||||
ordinal: u32,
|
||||
name: &'static str,
|
||||
func: KernelExportFn,
|
||||
) {
|
||||
self.exports.insert((module, ordinal), (name, func));
|
||||
}
|
||||
|
||||
pub fn call_export(
|
||||
&mut self,
|
||||
module: ModuleId,
|
||||
ordinal: u32,
|
||||
ctx: &mut PpcContext,
|
||||
mem: &mut GuestMemory,
|
||||
) -> bool {
|
||||
if let Some(&(name, func)) = self.exports.get(&(module, ordinal)) {
|
||||
tracing::info!(
|
||||
"Kernel call: {:?}:{:#x} ({}) args=[{:#x}, {:#x}, {:#x}, {:#x}]",
|
||||
module, ordinal, name,
|
||||
ctx.gpr[3], ctx.gpr[4], ctx.gpr[5], ctx.gpr[6]
|
||||
);
|
||||
func(ctx, mem, self);
|
||||
tracing::info!(" -> returned {:#x}", ctx.gpr[3]);
|
||||
true
|
||||
} else {
|
||||
tracing::warn!(
|
||||
"Unimplemented kernel export: {:?}:{:#x}",
|
||||
module, ordinal
|
||||
);
|
||||
// Return 0 (STATUS_SUCCESS) by default for unimplemented calls
|
||||
ctx.gpr[3] = 0;
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn export_name(&self, module: ModuleId, ordinal: u32) -> Option<&'static str> {
|
||||
self.exports.get(&(module, ordinal)).map(|&(name, _)| name)
|
||||
}
|
||||
|
||||
pub fn alloc_handle(&mut self) -> u32 {
|
||||
let h = self.next_handle;
|
||||
self.next_handle += 4;
|
||||
h
|
||||
}
|
||||
|
||||
pub fn alloc_handle_for(&mut self, obj: KernelObject) -> u32 {
|
||||
let h = self.alloc_handle();
|
||||
self.objects.insert(h, obj);
|
||||
h
|
||||
}
|
||||
|
||||
pub fn tls_get(&self, index: u32) -> u64 {
|
||||
self.tls_slots.get(&index).copied().unwrap_or(0)
|
||||
}
|
||||
|
||||
pub fn tls_set(&mut self, index: u32, value: u64) {
|
||||
self.tls_slots.insert(index, value);
|
||||
}
|
||||
|
||||
pub fn tls_alloc(&mut self) -> u32 {
|
||||
let idx = self.next_tls_index;
|
||||
self.next_tls_index += 1;
|
||||
idx
|
||||
}
|
||||
|
||||
/// Allocate guest memory from the heap bump allocator.
|
||||
/// Returns the base address of the allocated region.
|
||||
pub fn heap_alloc(&mut self, size: u32, mem: &mut GuestMemory) -> Option<u32> {
|
||||
let aligned_size = (size + 0xFFF) & !0xFFF; // Page-align
|
||||
let base = self.heap_cursor;
|
||||
if base.checked_add(aligned_size).is_none() || base + aligned_size > 0x6FFF_FFFF {
|
||||
return None;
|
||||
}
|
||||
let protect = xenia_memory::page_table::MemoryProtect::READ
|
||||
| xenia_memory::page_table::MemoryProtect::WRITE;
|
||||
if mem.alloc(base, aligned_size, protect).is_err() {
|
||||
return None;
|
||||
}
|
||||
self.heap_cursor += aligned_size;
|
||||
Some(base)
|
||||
}
|
||||
|
||||
/// Allocate a kernel stack.
|
||||
pub fn stack_alloc(&mut self, size: u32, mem: &mut GuestMemory) -> Option<u32> {
|
||||
let aligned_size = (size + 0xFFF) & !0xFFF;
|
||||
let base = self.stack_cursor;
|
||||
let protect = xenia_memory::page_table::MemoryProtect::READ
|
||||
| xenia_memory::page_table::MemoryProtect::WRITE;
|
||||
if mem.alloc(base, aligned_size, protect).is_err() {
|
||||
return None;
|
||||
}
|
||||
self.stack_cursor += aligned_size;
|
||||
Some(base + aligned_size) // Return top of stack
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for KernelState {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
253
crates/xenia-kernel/src/xam.rs
Normal file
253
crates/xenia-kernel/src/xam.rs
Normal file
@@ -0,0 +1,253 @@
|
||||
//! HLE kernel export implementations (xam.xex).
|
||||
|
||||
use crate::state::{KernelState, ModuleId};
|
||||
use xenia_cpu::PpcContext;
|
||||
use xenia_memory::{GuestMemory, MemoryAccess};
|
||||
|
||||
pub fn register_exports(state: &mut KernelState) {
|
||||
use ModuleId::Xam;
|
||||
|
||||
// Net
|
||||
state.register_export(Xam, 0x01, "NetDll_WSAStartup", stub_success);
|
||||
state.register_export(Xam, 0x02, "NetDll_WSACleanup", stub_success);
|
||||
|
||||
// Input
|
||||
state.register_export(Xam, 0x0190, "XamInputGetCapabilities", xam_input_not_connected);
|
||||
state.register_export(Xam, 0x0191, "XamInputGetState", xam_input_not_connected);
|
||||
state.register_export(Xam, 0x0192, "XamInputSetState", xam_input_not_connected);
|
||||
state.register_export(Xam, 0x0198, "XamInputGetKeystrokeEx", xam_input_not_connected);
|
||||
|
||||
// Inactivity
|
||||
state.register_export(Xam, 0x01A0, "XamEnableInactivityProcessing", stub_success);
|
||||
state.register_export(Xam, 0x01A1, "XamResetInactivity", stub_success);
|
||||
|
||||
// Loader
|
||||
state.register_export(Xam, 0x01A4, "XamLoaderLaunchTitle", xam_loader_launch_title);
|
||||
state.register_export(Xam, 0x01A9, "XamLoaderTerminateTitle", xam_loader_terminate_title);
|
||||
|
||||
// Task
|
||||
state.register_export(Xam, 0x01AF, "XamTaskSchedule", xam_task_schedule);
|
||||
state.register_export(Xam, 0x01B1, "XamTaskCloseHandle", stub_success);
|
||||
state.register_export(Xam, 0x01B3, "XamTaskShouldExit", stub_return_zero);
|
||||
|
||||
// Alloc
|
||||
state.register_export(Xam, 0x01EA, "XamAlloc", xam_alloc);
|
||||
state.register_export(Xam, 0x01EC, "XamFree", stub_success);
|
||||
|
||||
// Msg
|
||||
state.register_export(Xam, 0x01F4, "XMsgInProcessCall", stub_success);
|
||||
state.register_export(Xam, 0x01F7, "XMsgStartIORequest", stub_success);
|
||||
state.register_export(Xam, 0x01FC, "XMsgStartIORequestEx", stub_success);
|
||||
|
||||
// User
|
||||
state.register_export(Xam, 0x020A, "XamUserGetXUID", xam_user_get_xuid);
|
||||
state.register_export(Xam, 0x020E, "XamUserGetName", xam_user_get_name);
|
||||
state.register_export(Xam, 0x0210, "XamUserGetSigninState", stub_return_zero);
|
||||
state.register_export(Xam, 0x0219, "XamUserReadProfileSettings", xam_user_read_profile_settings);
|
||||
state.register_export(Xam, 0x021A, "XamUserWriteProfileSettings", stub_success);
|
||||
|
||||
// Enum
|
||||
state.register_export(Xam, 0x0250, "XamEnumerate", stub_error_no_more_files);
|
||||
|
||||
// Content
|
||||
state.register_export(Xam, 0x0258, "XamContentCreate", stub_success);
|
||||
state.register_export(Xam, 0x025A, "XamContentClose", stub_success);
|
||||
state.register_export(Xam, 0x025B, "XamContentDelete", stub_success);
|
||||
state.register_export(Xam, 0x025C, "XamContentCreateEnumerator", stub_success);
|
||||
state.register_export(Xam, 0x025E, "XamContentGetDeviceData", stub_success);
|
||||
state.register_export(Xam, 0x025F, "XamContentGetDeviceName", stub_success);
|
||||
state.register_export(Xam, 0x0260, "XamContentSetThumbnail", stub_success);
|
||||
state.register_export(Xam, 0x0262, "XamContentGetCreator", stub_success);
|
||||
state.register_export(Xam, 0x0265, "XamContentGetDeviceState", stub_success);
|
||||
|
||||
// System
|
||||
state.register_export(Xam, 0x0280, "XamGetExecutionId", xam_get_execution_id);
|
||||
state.register_export(Xam, 0x0282, "XamGetSystemVersion", xam_get_system_version);
|
||||
|
||||
// Notify
|
||||
state.register_export(Xam, 0x028A, "XamNotifyCreateListener", xam_notify_create_listener);
|
||||
state.register_export(Xam, 0x028B, "XNotifyGetNext", xnotify_get_next);
|
||||
state.register_export(Xam, 0x028C, "XNotifyPositionUI", stub_success);
|
||||
|
||||
// Achievements/Stats
|
||||
state.register_export(Xam, 0x02EE, "XamUserCreateAchievementEnumerator", stub_success);
|
||||
state.register_export(Xam, 0x02F7, "XamUserCreateStatsEnumerator", stub_success);
|
||||
|
||||
// UI
|
||||
state.register_export(Xam, 0x02BC, "XamShowSigninUI", stub_success);
|
||||
state.register_export(Xam, 0x02C1, "XamShowKeyboardUI", stub_success);
|
||||
state.register_export(Xam, 0x02CB, "XamShowDeviceSelectorUI", stub_success);
|
||||
state.register_export(Xam, 0x02D5, "XamShowGamerCardUIForXUID", stub_success);
|
||||
state.register_export(Xam, 0x02D9, "XamShowDirtyDiscErrorUI", stub_success);
|
||||
state.register_export(Xam, 0x02DC, "XamShowMessageBoxUIEx", stub_success);
|
||||
|
||||
// Session
|
||||
state.register_export(Xam, 0x0316, "XamSessionCreateHandle", xam_session_create_handle);
|
||||
state.register_export(Xam, 0x0317, "XamSessionRefObjByHandle", stub_success);
|
||||
|
||||
// Locale
|
||||
state.register_export(Xam, 0x03CB, "XGetAVPack", xget_avpack);
|
||||
state.register_export(Xam, 0x03CC, "XGetGameRegion", xget_game_region);
|
||||
state.register_export(Xam, 0x03CD, "XGetLanguage", xget_language);
|
||||
state.register_export(Xam, 0x03D1, "XGetVideoMode", xget_video_mode);
|
||||
}
|
||||
|
||||
// ===== Generic stubs =====
|
||||
|
||||
fn stub_success(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
ctx.gpr[3] = 0;
|
||||
}
|
||||
|
||||
fn stub_return_zero(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
ctx.gpr[3] = 0;
|
||||
}
|
||||
|
||||
fn stub_error_no_more_files(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
ctx.gpr[3] = 0x12; // ERROR_NO_MORE_FILES
|
||||
}
|
||||
|
||||
// ===== Input =====
|
||||
|
||||
fn xam_input_not_connected(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
ctx.gpr[3] = 0x48F; // ERROR_DEVICE_NOT_CONNECTED
|
||||
}
|
||||
|
||||
// ===== Loader =====
|
||||
|
||||
fn xam_loader_launch_title(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
tracing::warn!("XamLoaderLaunchTitle called");
|
||||
ctx.gpr[3] = 0;
|
||||
}
|
||||
|
||||
fn xam_loader_terminate_title(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
tracing::warn!("XamLoaderTerminateTitle called");
|
||||
ctx.gpr[3] = 0;
|
||||
}
|
||||
|
||||
// ===== Task =====
|
||||
|
||||
fn xam_task_schedule(ctx: &mut PpcContext, _mem: &mut GuestMemory, state: &mut KernelState) {
|
||||
let handle = state.alloc_handle();
|
||||
tracing::info!("XamTaskSchedule: handle={:#x}", handle);
|
||||
ctx.gpr[3] = 0;
|
||||
}
|
||||
|
||||
// ===== Alloc =====
|
||||
|
||||
fn xam_alloc(ctx: &mut PpcContext, mem: &mut GuestMemory, state: &mut KernelState) {
|
||||
// r3 = flags, r4 = size, r5 = out_ptr_ptr
|
||||
let size = ctx.gpr[4] as u32;
|
||||
let out_ptr = ctx.gpr[5] as u32;
|
||||
|
||||
match state.heap_alloc(size, mem) {
|
||||
Some(addr) => {
|
||||
if out_ptr != 0 {
|
||||
mem.write_u32(out_ptr, addr);
|
||||
}
|
||||
ctx.gpr[3] = 0; // SUCCESS
|
||||
}
|
||||
None => {
|
||||
ctx.gpr[3] = 0x8007_000E; // E_OUTOFMEMORY
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===== User =====
|
||||
|
||||
fn xam_user_get_xuid(ctx: &mut PpcContext, mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
// r3 = user_index, r4 = xuid_ptr
|
||||
let xuid_ptr = ctx.gpr[4] as u32;
|
||||
if xuid_ptr != 0 {
|
||||
mem.write_u64(xuid_ptr, 0); // No XUID
|
||||
}
|
||||
ctx.gpr[3] = 0;
|
||||
}
|
||||
|
||||
fn xam_user_get_name(ctx: &mut PpcContext, mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
// r3 = user_index, r4 = buffer, r5 = buffer_size
|
||||
let buffer = ctx.gpr[4] as u32;
|
||||
if buffer != 0 {
|
||||
mem.write_u8(buffer, 0); // Empty string
|
||||
}
|
||||
ctx.gpr[3] = 0;
|
||||
}
|
||||
|
||||
fn xam_user_read_profile_settings(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
// Return error — no profile
|
||||
ctx.gpr[3] = 0x0000_048B; // ERROR_NOT_FOUND
|
||||
}
|
||||
|
||||
// ===== System =====
|
||||
|
||||
fn xam_get_execution_id(ctx: &mut PpcContext, mem: &mut GuestMemory, state: &mut KernelState) {
|
||||
// r3 = execution_id_ptr_ptr — write pointer to execution info
|
||||
let ptr_ptr = ctx.gpr[3] as u32;
|
||||
if ptr_ptr != 0 {
|
||||
// Allocate and fill a fake XEX_EXECUTION_ID structure
|
||||
if let Some(exec_id_addr) = state.heap_alloc(0x18, mem) {
|
||||
mem.write_u32(exec_id_addr, 0x535107D4); // title_id (Project Sylpheed)
|
||||
mem.write_u32(exec_id_addr + 4, 0x2D2E2EEB); // media_id
|
||||
mem.write_u16(exec_id_addr + 8, 0); // version
|
||||
mem.write_u16(exec_id_addr + 10, 0); // base_version
|
||||
mem.write_u16(exec_id_addr + 12, 1); // disc_number
|
||||
mem.write_u16(exec_id_addr + 14, 1); // disc_count
|
||||
mem.write_u32(ptr_ptr, exec_id_addr);
|
||||
}
|
||||
}
|
||||
ctx.gpr[3] = 0;
|
||||
}
|
||||
|
||||
fn xam_get_system_version(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
ctx.gpr[3] = 0x2000_0000; // System version
|
||||
}
|
||||
|
||||
// ===== Notify =====
|
||||
|
||||
fn xam_notify_create_listener(ctx: &mut PpcContext, _mem: &mut GuestMemory, state: &mut KernelState) {
|
||||
let handle = state.alloc_handle();
|
||||
ctx.gpr[3] = handle as u64;
|
||||
}
|
||||
|
||||
fn xnotify_get_next(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
// r3 = handle, r4 = id_ptr, r5 = param_ptr
|
||||
ctx.gpr[3] = 0; // FALSE (no notifications)
|
||||
}
|
||||
|
||||
// ===== Session =====
|
||||
|
||||
fn xam_session_create_handle(ctx: &mut PpcContext, mem: &mut GuestMemory, state: &mut KernelState) {
|
||||
// r3 = handle_ptr
|
||||
let handle_ptr = ctx.gpr[3] as u32;
|
||||
let handle = state.alloc_handle();
|
||||
if handle_ptr != 0 {
|
||||
mem.write_u32(handle_ptr, handle);
|
||||
}
|
||||
ctx.gpr[3] = 0;
|
||||
}
|
||||
|
||||
// ===== Locale =====
|
||||
|
||||
fn xget_avpack(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
ctx.gpr[3] = 0x16; // HDMI
|
||||
}
|
||||
|
||||
fn xget_game_region(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
ctx.gpr[3] = 0xFF; // All regions
|
||||
}
|
||||
|
||||
fn xget_language(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
ctx.gpr[3] = 1; // English
|
||||
}
|
||||
|
||||
fn xget_video_mode(ctx: &mut PpcContext, mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
// r3 = video_mode_ptr
|
||||
let ptr = ctx.gpr[3] as u32;
|
||||
if ptr != 0 {
|
||||
mem.write_u32(ptr, 1280); // width
|
||||
mem.write_u32(ptr + 4, 720); // height
|
||||
mem.write_u32(ptr + 8, 0); // is_interlaced
|
||||
mem.write_u32(ptr + 12, 1); // is_widescreen
|
||||
mem.write_u32(ptr + 16, 60); // refresh_rate
|
||||
}
|
||||
ctx.gpr[3] = 0;
|
||||
}
|
||||
17
crates/xenia-memory/Cargo.toml
Normal file
17
crates/xenia-memory/Cargo.toml
Normal file
@@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "xenia-memory"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
xenia-types = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
bitflags = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
libc = "0.2"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows-sys = { version = "0.59", features = ["Win32_System_Memory", "Win32_Foundation"] }
|
||||
47
crates/xenia-memory/src/access.rs
Normal file
47
crates/xenia-memory/src/access.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
/// Trait for all guest memory access. Every load/store goes through this,
|
||||
/// enabling MMIO checking and debugger observation on every access.
|
||||
/// This is the key abstraction that eliminates the need for MMIO exception handlers.
|
||||
pub trait MemoryAccess {
|
||||
fn read_u8(&self, addr: u32) -> u8;
|
||||
fn read_u16(&self, addr: u32) -> u16;
|
||||
fn read_u32(&self, addr: u32) -> u32;
|
||||
fn read_u64(&self, addr: u32) -> u64;
|
||||
fn read_f32(&self, addr: u32) -> f32 {
|
||||
f32::from_bits(self.read_u32(addr))
|
||||
}
|
||||
fn read_f64(&self, addr: u32) -> f64 {
|
||||
f64::from_bits(self.read_u64(addr))
|
||||
}
|
||||
|
||||
fn write_u8(&mut self, addr: u32, val: u8);
|
||||
fn write_u16(&mut self, addr: u32, val: u16);
|
||||
fn write_u32(&mut self, addr: u32, val: u32);
|
||||
fn write_u64(&mut self, addr: u32, val: u64);
|
||||
fn write_f32(&mut self, addr: u32, val: f32) {
|
||||
self.write_u32(addr, val.to_bits());
|
||||
}
|
||||
fn write_f64(&mut self, addr: u32, val: f64) {
|
||||
self.write_u64(addr, val.to_bits());
|
||||
}
|
||||
|
||||
/// Read a block of bytes from guest memory.
|
||||
fn read_bytes(&self, addr: u32, buf: &mut [u8]) {
|
||||
for (i, byte) in buf.iter_mut().enumerate() {
|
||||
*byte = self.read_u8(addr.wrapping_add(i as u32));
|
||||
}
|
||||
}
|
||||
|
||||
/// Write a block of bytes to guest memory.
|
||||
fn write_bytes(&mut self, addr: u32, buf: &[u8]) {
|
||||
for (i, &byte) in buf.iter().enumerate() {
|
||||
self.write_u8(addr.wrapping_add(i as u32), byte);
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a direct host pointer for the given guest address.
|
||||
/// Returns None if the address is invalid or in an MMIO region.
|
||||
fn translate(&self, addr: u32) -> Option<*const u8>;
|
||||
|
||||
/// Get a mutable direct host pointer for the given guest address.
|
||||
fn translate_mut(&mut self, addr: u32) -> Option<*mut u8>;
|
||||
}
|
||||
265
crates/xenia-memory/src/heap.rs
Normal file
265
crates/xenia-memory/src/heap.rs
Normal file
@@ -0,0 +1,265 @@
|
||||
use crate::access::MemoryAccess;
|
||||
use crate::mmio::MmioRegion;
|
||||
use crate::page_table::{AllocationState, MemoryProtect, PageEntry};
|
||||
use crate::MemoryError;
|
||||
|
||||
const PAGE_SIZE: u32 = 4096;
|
||||
/// Total guest address space: 4GB.
|
||||
const GUEST_ADDRESS_SPACE: usize = 0x1_0000_0000;
|
||||
/// Number of 4K pages in the 4GB address space.
|
||||
const PAGE_COUNT: usize = GUEST_ADDRESS_SPACE / PAGE_SIZE as usize;
|
||||
/// Physical memory mask (512MB physical address space).
|
||||
const PHYSICAL_ADDR_MASK: u32 = 0x1FFF_FFFF;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum HeapType {
|
||||
GuestVirtual,
|
||||
GuestXex,
|
||||
GuestPhysical,
|
||||
}
|
||||
|
||||
/// The core guest memory system. Manages a 4GB virtual address space
|
||||
/// via mmap/VirtualAlloc, with page-level tracking and MMIO dispatch.
|
||||
pub struct GuestMemory {
|
||||
/// Host pointer to the base of the 4GB guest address space.
|
||||
membase: *mut u8,
|
||||
/// Page table tracking allocation state for each 4K page.
|
||||
page_table: Vec<PageEntry>,
|
||||
/// Registered MMIO regions (sorted by base address for binary search).
|
||||
mmio_regions: Vec<MmioRegion>,
|
||||
/// Whether the memory mapping is owned (should be unmapped on drop).
|
||||
owned: bool,
|
||||
}
|
||||
|
||||
unsafe impl Send for GuestMemory {}
|
||||
unsafe impl Sync for GuestMemory {}
|
||||
|
||||
impl GuestMemory {
|
||||
/// Create a new guest memory space by reserving a 4GB virtual address region.
|
||||
pub fn new() -> Result<Self, MemoryError> {
|
||||
let membase = crate::platform::reserve_address_space(GUEST_ADDRESS_SPACE)?;
|
||||
Ok(Self {
|
||||
membase,
|
||||
page_table: vec![PageEntry::default(); PAGE_COUNT],
|
||||
mmio_regions: Vec::new(),
|
||||
owned: true,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the host base pointer for the guest address space.
|
||||
pub fn membase(&self) -> *const u8 {
|
||||
self.membase
|
||||
}
|
||||
|
||||
/// Get a mutable host base pointer.
|
||||
pub fn membase_mut(&mut self) -> *mut u8 {
|
||||
self.membase
|
||||
}
|
||||
|
||||
/// Translate a guest virtual address to a host pointer.
|
||||
pub fn translate_virtual(&self, guest_addr: u32) -> *const u8 {
|
||||
unsafe { self.membase.add(guest_addr as usize) }
|
||||
}
|
||||
|
||||
/// Translate a guest virtual address to a mutable host pointer.
|
||||
pub fn translate_virtual_mut(&mut self, guest_addr: u32) -> *mut u8 {
|
||||
unsafe { self.membase.add(guest_addr as usize) }
|
||||
}
|
||||
|
||||
/// Translate a guest physical address to a host pointer.
|
||||
pub fn translate_physical(&self, guest_addr: u32) -> *const u8 {
|
||||
let phys = guest_addr & PHYSICAL_ADDR_MASK;
|
||||
unsafe { self.membase.add(phys as usize) }
|
||||
}
|
||||
|
||||
/// Register an MMIO region.
|
||||
pub fn add_mmio_region(&mut self, region: MmioRegion) {
|
||||
let base = region.base_address;
|
||||
let idx = self
|
||||
.mmio_regions
|
||||
.binary_search_by_key(&base, |r| r.base_address)
|
||||
.unwrap_or_else(|i| i);
|
||||
self.mmio_regions.insert(idx, region);
|
||||
}
|
||||
|
||||
/// Check if an address is in a registered MMIO region.
|
||||
fn find_mmio(&self, addr: u32) -> Option<&MmioRegion> {
|
||||
self.mmio_regions.iter().find(|r| r.contains(addr))
|
||||
}
|
||||
|
||||
/// Allocate a region in the guest address space.
|
||||
pub fn alloc(
|
||||
&mut self,
|
||||
base: u32,
|
||||
size: u32,
|
||||
protect: MemoryProtect,
|
||||
) -> Result<u32, MemoryError> {
|
||||
let page_start = (base / PAGE_SIZE) as usize;
|
||||
let page_count = ((size + PAGE_SIZE - 1) / PAGE_SIZE) as usize;
|
||||
|
||||
// Commit pages via platform
|
||||
let host_ptr = unsafe { self.membase.add(base as usize) };
|
||||
crate::platform::commit_memory(host_ptr, (page_count * PAGE_SIZE as usize) as usize)?;
|
||||
|
||||
// Update page table
|
||||
for i in 0..page_count {
|
||||
let idx = page_start + i;
|
||||
if idx < self.page_table.len() {
|
||||
let entry = &mut self.page_table[idx];
|
||||
entry.set_base_address(page_start as u32);
|
||||
entry.set_region_page_count(page_count as u32);
|
||||
entry.set_allocation_protect(protect);
|
||||
entry.set_current_protect(protect);
|
||||
entry.set_state(AllocationState::RESERVE | AllocationState::COMMIT);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(base)
|
||||
}
|
||||
|
||||
/// Read a slice of bytes from guest memory (bypassing MMIO for bulk reads).
|
||||
pub fn read_bulk(&self, addr: u32, buf: &mut [u8]) {
|
||||
let ptr = self.translate_virtual(addr);
|
||||
unsafe {
|
||||
std::ptr::copy_nonoverlapping(ptr, buf.as_mut_ptr(), buf.len());
|
||||
}
|
||||
}
|
||||
|
||||
/// Write a slice of bytes to guest memory (bypassing MMIO for bulk writes).
|
||||
pub fn write_bulk(&mut self, addr: u32, buf: &[u8]) {
|
||||
let ptr = self.translate_virtual_mut(addr);
|
||||
unsafe {
|
||||
std::ptr::copy_nonoverlapping(buf.as_ptr(), ptr, buf.len());
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a guest address has been allocated/committed.
|
||||
pub fn is_mapped(&self, addr: u32) -> bool {
|
||||
let page = (addr / PAGE_SIZE) as usize;
|
||||
if page >= self.page_table.len() {
|
||||
return false;
|
||||
}
|
||||
self.page_table[page].state().contains(AllocationState::COMMIT)
|
||||
}
|
||||
|
||||
/// Get a page table entry for a given address.
|
||||
pub fn page_entry(&self, addr: u32) -> &PageEntry {
|
||||
let page = (addr / PAGE_SIZE) as usize;
|
||||
&self.page_table[page]
|
||||
}
|
||||
}
|
||||
|
||||
impl MemoryAccess for GuestMemory {
|
||||
fn read_u8(&self, addr: u32) -> u8 {
|
||||
if !self.is_mapped(addr) { return 0; }
|
||||
let ptr = self.translate_virtual(addr);
|
||||
unsafe { *ptr }
|
||||
}
|
||||
|
||||
fn read_u16(&self, addr: u32) -> u16 {
|
||||
if let Some(mmio) = self.find_mmio(addr) {
|
||||
(mmio.read_callback)(addr) as u16
|
||||
} else if !self.is_mapped(addr) {
|
||||
0
|
||||
} else {
|
||||
let ptr = self.translate_virtual(addr) as *const [u8; 2];
|
||||
u16::from_be_bytes(unsafe { *ptr })
|
||||
}
|
||||
}
|
||||
|
||||
fn read_u32(&self, addr: u32) -> u32 {
|
||||
if let Some(mmio) = self.find_mmio(addr) {
|
||||
(mmio.read_callback)(addr)
|
||||
} else if !self.is_mapped(addr) {
|
||||
0
|
||||
} else {
|
||||
let ptr = self.translate_virtual(addr) as *const [u8; 4];
|
||||
u32::from_be_bytes(unsafe { *ptr })
|
||||
}
|
||||
}
|
||||
|
||||
fn read_u64(&self, addr: u32) -> u64 {
|
||||
if let Some(mmio) = self.find_mmio(addr) {
|
||||
let hi = (mmio.read_callback)(addr) as u64;
|
||||
let lo = (mmio.read_callback)(addr.wrapping_add(4)) as u64;
|
||||
(hi << 32) | lo
|
||||
} else if !self.is_mapped(addr) {
|
||||
0
|
||||
} else {
|
||||
let ptr = self.translate_virtual(addr) as *const [u8; 8];
|
||||
u64::from_be_bytes(unsafe { *ptr })
|
||||
}
|
||||
}
|
||||
|
||||
fn write_u8(&mut self, addr: u32, val: u8) {
|
||||
if !self.is_mapped(addr) { return; }
|
||||
let ptr = self.translate_virtual_mut(addr);
|
||||
unsafe { *ptr = val };
|
||||
}
|
||||
|
||||
fn write_u16(&mut self, addr: u32, val: u16) {
|
||||
if let Some(mmio) = self.find_mmio(addr) {
|
||||
(mmio.write_callback)(addr, val as u32);
|
||||
} else if !self.is_mapped(addr) {
|
||||
return;
|
||||
} else {
|
||||
let ptr = self.translate_virtual_mut(addr);
|
||||
unsafe {
|
||||
std::ptr::copy_nonoverlapping(val.to_be_bytes().as_ptr(), ptr, 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn write_u32(&mut self, addr: u32, val: u32) {
|
||||
if let Some(mmio) = self.find_mmio(addr) {
|
||||
(mmio.write_callback)(addr, val);
|
||||
} else if !self.is_mapped(addr) {
|
||||
return;
|
||||
} else {
|
||||
let ptr = self.translate_virtual_mut(addr);
|
||||
unsafe {
|
||||
std::ptr::copy_nonoverlapping(val.to_be_bytes().as_ptr(), ptr, 4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn write_u64(&mut self, addr: u32, val: u64) {
|
||||
if let Some(mmio) = self.find_mmio(addr) {
|
||||
(mmio.write_callback)(addr, (val >> 32) as u32);
|
||||
(mmio.write_callback)(addr.wrapping_add(4), val as u32);
|
||||
} else if !self.is_mapped(addr) {
|
||||
return;
|
||||
} else {
|
||||
let ptr = self.translate_virtual_mut(addr);
|
||||
unsafe {
|
||||
std::ptr::copy_nonoverlapping(val.to_be_bytes().as_ptr(), ptr, 8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn translate(&self, addr: u32) -> Option<*const u8> {
|
||||
if self.find_mmio(addr).is_some() || !self.is_mapped(addr) {
|
||||
None
|
||||
} else {
|
||||
Some(self.translate_virtual(addr))
|
||||
}
|
||||
}
|
||||
|
||||
fn translate_mut(&mut self, addr: u32) -> Option<*mut u8> {
|
||||
if self.find_mmio(addr).is_some() {
|
||||
None
|
||||
} else {
|
||||
Some(self.translate_virtual_mut(addr))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for GuestMemory {
|
||||
fn drop(&mut self) {
|
||||
if self.owned && !self.membase.is_null() {
|
||||
unsafe {
|
||||
crate::platform::release_address_space(self.membase, GUEST_ADDRESS_SPACE);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
31
crates/xenia-memory/src/lib.rs
Normal file
31
crates/xenia-memory/src/lib.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
pub mod access;
|
||||
pub mod heap;
|
||||
pub mod mmio;
|
||||
pub mod page_table;
|
||||
|
||||
mod platform;
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
pub use access::MemoryAccess;
|
||||
pub use heap::{GuestMemory, HeapType};
|
||||
pub use mmio::MmioRegion;
|
||||
pub use page_table::PageEntry;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum MemoryError {
|
||||
#[error("Failed to allocate guest address space: {0}")]
|
||||
AllocationFailed(String),
|
||||
|
||||
#[error("Invalid guest address: {0:#010x}")]
|
||||
InvalidAddress(u32),
|
||||
|
||||
#[error("MMIO access at {0:#010x}")]
|
||||
MmioAccess(u32),
|
||||
|
||||
#[error("Protection violation at {0:#010x}")]
|
||||
ProtectionViolation(u32),
|
||||
|
||||
#[error("Out of memory in heap {0:?}")]
|
||||
OutOfMemory(HeapType),
|
||||
}
|
||||
27
crates/xenia-memory/src/mmio.rs
Normal file
27
crates/xenia-memory/src/mmio.rs
Normal file
@@ -0,0 +1,27 @@
|
||||
/// Represents a mapped MMIO region with read/write callbacks.
|
||||
/// Instead of trapping access violations (as the C++ JIT does), the interpreter
|
||||
/// explicitly checks each memory access against registered MMIO regions.
|
||||
pub struct MmioRegion {
|
||||
pub base_address: u32,
|
||||
pub mask: u32,
|
||||
pub size: u32,
|
||||
pub read_callback: Box<dyn Fn(u32) -> u32 + Send + Sync>,
|
||||
pub write_callback: Box<dyn Fn(u32, u32) + Send + Sync>,
|
||||
}
|
||||
|
||||
impl MmioRegion {
|
||||
pub fn contains(&self, addr: u32) -> bool {
|
||||
let masked = addr & self.mask;
|
||||
masked >= self.base_address && masked < self.base_address + self.size
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for MmioRegion {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("MmioRegion")
|
||||
.field("base_address", &format_args!("{:#010x}", self.base_address))
|
||||
.field("mask", &format_args!("{:#010x}", self.mask))
|
||||
.field("size", &format_args!("{:#x}", self.size))
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
122
crates/xenia-memory/src/page_table.rs
Normal file
122
crates/xenia-memory/src/page_table.rs
Normal file
@@ -0,0 +1,122 @@
|
||||
use bitflags::bitflags;
|
||||
|
||||
/// Describes a single page in the page table.
|
||||
/// Mirrors the C++ `PageEntry` union from memory.h:82-99.
|
||||
#[derive(Clone, Copy, Default)]
|
||||
pub struct PageEntry(u64);
|
||||
|
||||
impl PageEntry {
|
||||
/// Base address of the allocated region in 4K pages (20 bits).
|
||||
pub fn base_address(&self) -> u32 {
|
||||
(self.0 & 0xFFFFF) as u32
|
||||
}
|
||||
|
||||
pub fn set_base_address(&mut self, val: u32) {
|
||||
self.0 = (self.0 & !0xFFFFF) | (val as u64 & 0xFFFFF);
|
||||
}
|
||||
|
||||
/// Total number of pages in the allocated region (20 bits).
|
||||
pub fn region_page_count(&self) -> u32 {
|
||||
((self.0 >> 20) & 0xFFFFF) as u32
|
||||
}
|
||||
|
||||
pub fn set_region_page_count(&mut self, val: u32) {
|
||||
self.0 = (self.0 & !(0xFFFFF << 20)) | ((val as u64 & 0xFFFFF) << 20);
|
||||
}
|
||||
|
||||
/// Protection bits specified during region allocation (4 bits).
|
||||
pub fn allocation_protect(&self) -> MemoryProtect {
|
||||
MemoryProtect::from_bits_truncate(((self.0 >> 40) & 0xF) as u32)
|
||||
}
|
||||
|
||||
pub fn set_allocation_protect(&mut self, val: MemoryProtect) {
|
||||
self.0 = (self.0 & !(0xF << 40)) | ((val.bits() as u64 & 0xF) << 40);
|
||||
}
|
||||
|
||||
/// Current protection bits (4 bits).
|
||||
pub fn current_protect(&self) -> MemoryProtect {
|
||||
MemoryProtect::from_bits_truncate(((self.0 >> 44) & 0xF) as u32)
|
||||
}
|
||||
|
||||
pub fn set_current_protect(&mut self, val: MemoryProtect) {
|
||||
self.0 = (self.0 & !(0xF << 44)) | ((val.bits() as u64 & 0xF) << 44);
|
||||
}
|
||||
|
||||
/// Allocation state (2 bits).
|
||||
pub fn state(&self) -> AllocationState {
|
||||
AllocationState::from_bits_truncate(((self.0 >> 48) & 0x3) as u32)
|
||||
}
|
||||
|
||||
pub fn set_state(&mut self, val: AllocationState) {
|
||||
self.0 = (self.0 & !(0x3 << 48)) | ((val.bits() as u64 & 0x3) << 48);
|
||||
}
|
||||
|
||||
pub fn is_committed(&self) -> bool {
|
||||
self.state().contains(AllocationState::COMMIT)
|
||||
}
|
||||
|
||||
pub fn is_reserved(&self) -> bool {
|
||||
self.state().contains(AllocationState::RESERVE)
|
||||
}
|
||||
|
||||
pub fn is_free(&self) -> bool {
|
||||
self.state().is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct MemoryProtect: u32 {
|
||||
const READ = 1 << 0;
|
||||
const WRITE = 1 << 1;
|
||||
const NO_CACHE = 1 << 2;
|
||||
const WRITE_COMBINE = 1 << 3;
|
||||
}
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct AllocationState: u32 {
|
||||
const RESERVE = 1 << 0;
|
||||
const COMMIT = 1 << 1;
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for PageEntry {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("PageEntry")
|
||||
.field("base_address", &format_args!("{:#x}", self.base_address()))
|
||||
.field("region_page_count", &self.region_page_count())
|
||||
.field("allocation_protect", &self.allocation_protect())
|
||||
.field("current_protect", &self.current_protect())
|
||||
.field("state", &self.state())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_page_entry_bitfields() {
|
||||
let mut entry = PageEntry::default();
|
||||
assert!(entry.is_free());
|
||||
|
||||
entry.set_base_address(0x100);
|
||||
entry.set_region_page_count(0x10);
|
||||
entry.set_allocation_protect(MemoryProtect::READ | MemoryProtect::WRITE);
|
||||
entry.set_current_protect(MemoryProtect::READ);
|
||||
entry.set_state(AllocationState::RESERVE | AllocationState::COMMIT);
|
||||
|
||||
assert_eq!(entry.base_address(), 0x100);
|
||||
assert_eq!(entry.region_page_count(), 0x10);
|
||||
assert_eq!(
|
||||
entry.allocation_protect(),
|
||||
MemoryProtect::READ | MemoryProtect::WRITE
|
||||
);
|
||||
assert_eq!(entry.current_protect(), MemoryProtect::READ);
|
||||
assert!(entry.is_committed());
|
||||
assert!(entry.is_reserved());
|
||||
}
|
||||
}
|
||||
98
crates/xenia-memory/src/platform.rs
Normal file
98
crates/xenia-memory/src/platform.rs
Normal file
@@ -0,0 +1,98 @@
|
||||
use crate::MemoryError;
|
||||
|
||||
/// Reserve a contiguous virtual address region without committing physical pages.
|
||||
#[cfg(unix)]
|
||||
pub fn reserve_address_space(size: usize) -> Result<*mut u8, MemoryError> {
|
||||
unsafe {
|
||||
let ptr = libc::mmap(
|
||||
std::ptr::null_mut(),
|
||||
size,
|
||||
libc::PROT_NONE,
|
||||
libc::MAP_PRIVATE | libc::MAP_ANONYMOUS | libc::MAP_NORESERVE,
|
||||
-1,
|
||||
0,
|
||||
);
|
||||
if ptr == libc::MAP_FAILED {
|
||||
Err(MemoryError::AllocationFailed(format!(
|
||||
"mmap failed for {} bytes: {}",
|
||||
size,
|
||||
std::io::Error::last_os_error()
|
||||
)))
|
||||
} else {
|
||||
Ok(ptr as *mut u8)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Commit (make accessible) a region within a previously reserved address space.
|
||||
#[cfg(unix)]
|
||||
pub fn commit_memory(ptr: *mut u8, size: usize) -> Result<(), MemoryError> {
|
||||
unsafe {
|
||||
let result = libc::mprotect(ptr as *mut libc::c_void, size, libc::PROT_READ | libc::PROT_WRITE);
|
||||
if result != 0 {
|
||||
Err(MemoryError::AllocationFailed(format!(
|
||||
"mprotect failed for {} bytes: {}",
|
||||
size,
|
||||
std::io::Error::last_os_error()
|
||||
)))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Release a previously reserved address space.
|
||||
#[cfg(unix)]
|
||||
pub unsafe fn release_address_space(ptr: *mut u8, size: usize) {
|
||||
unsafe { libc::munmap(ptr as *mut libc::c_void, size); }
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub fn reserve_address_space(size: usize) -> Result<*mut u8, MemoryError> {
|
||||
unsafe {
|
||||
let ptr = windows_sys::Win32::System::Memory::VirtualAlloc(
|
||||
std::ptr::null_mut(),
|
||||
size,
|
||||
windows_sys::Win32::System::Memory::MEM_RESERVE,
|
||||
windows_sys::Win32::System::Memory::PAGE_NOACCESS,
|
||||
);
|
||||
if ptr.is_null() {
|
||||
Err(MemoryError::AllocationFailed(format!(
|
||||
"VirtualAlloc reserve failed for {} bytes",
|
||||
size,
|
||||
)))
|
||||
} else {
|
||||
Ok(ptr as *mut u8)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub fn commit_memory(ptr: *mut u8, size: usize) -> Result<(), MemoryError> {
|
||||
unsafe {
|
||||
let result = windows_sys::Win32::System::Memory::VirtualAlloc(
|
||||
ptr as *mut _,
|
||||
size,
|
||||
windows_sys::Win32::System::Memory::MEM_COMMIT,
|
||||
windows_sys::Win32::System::Memory::PAGE_READWRITE,
|
||||
);
|
||||
if result.is_null() {
|
||||
Err(MemoryError::AllocationFailed(format!(
|
||||
"VirtualAlloc commit failed for {} bytes",
|
||||
size,
|
||||
)))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub unsafe fn release_address_space(ptr: *mut u8, size: usize) {
|
||||
let _ = size;
|
||||
windows_sys::Win32::System::Memory::VirtualFree(
|
||||
ptr as *mut _,
|
||||
0,
|
||||
windows_sys::Win32::System::Memory::MEM_RELEASE,
|
||||
);
|
||||
}
|
||||
11
crates/xenia-types/Cargo.toml
Normal file
11
crates/xenia-types/Cargo.toml
Normal file
@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "xenia-types"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
bitflags = { workspace = true }
|
||||
byteorder = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
122
crates/xenia-types/src/endian.rs
Normal file
122
crates/xenia-types/src/endian.rs
Normal file
@@ -0,0 +1,122 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt;
|
||||
use std::marker::PhantomData;
|
||||
|
||||
/// Big-endian value wrapper matching `xe::be<T>` from the C++ codebase.
|
||||
/// Stores the value in big-endian byte order and transparently converts
|
||||
/// on access. Used for guest memory structures that are natively big-endian.
|
||||
#[derive(Clone, Copy, Serialize, Deserialize)]
|
||||
#[repr(transparent)]
|
||||
pub struct Be<T: BeSwap>(T::Bytes, PhantomData<T>);
|
||||
|
||||
impl<T: BeSwap> Be<T> {
|
||||
pub fn new(val: T) -> Self {
|
||||
Self(val.to_be_bytes(), PhantomData)
|
||||
}
|
||||
|
||||
pub fn get(self) -> T {
|
||||
T::from_be_bytes(self.0)
|
||||
}
|
||||
|
||||
pub fn set(&mut self, val: T) {
|
||||
self.0 = val.to_be_bytes();
|
||||
}
|
||||
|
||||
pub fn raw_bytes(&self) -> &T::Bytes {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: BeSwap + Default> Default for Be<T> {
|
||||
fn default() -> Self {
|
||||
Self::new(T::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: BeSwap + fmt::Debug> fmt::Debug for Be<T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
self.get().fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: BeSwap + fmt::Display> fmt::Display for Be<T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
self.get().fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: BeSwap + PartialEq> PartialEq for Be<T> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
// Compare raw bytes for efficiency (same byte order)
|
||||
self.0.as_ref() == other.0.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: BeSwap + Eq> Eq for Be<T> {}
|
||||
|
||||
/// Trait for types that can be converted to/from big-endian byte representations.
|
||||
pub trait BeSwap: Copy {
|
||||
type Bytes: Copy + AsRef<[u8]> + serde::Serialize + for<'de> serde::Deserialize<'de>;
|
||||
fn to_be_bytes(self) -> Self::Bytes;
|
||||
fn from_be_bytes(bytes: Self::Bytes) -> Self;
|
||||
}
|
||||
|
||||
macro_rules! impl_be_swap {
|
||||
($t:ty) => {
|
||||
impl BeSwap for $t {
|
||||
type Bytes = [u8; std::mem::size_of::<$t>()];
|
||||
fn to_be_bytes(self) -> Self::Bytes {
|
||||
<$t>::to_be_bytes(self)
|
||||
}
|
||||
fn from_be_bytes(bytes: Self::Bytes) -> Self {
|
||||
<$t>::from_be_bytes(bytes)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl_be_swap!(u8);
|
||||
impl_be_swap!(u16);
|
||||
impl_be_swap!(u32);
|
||||
impl_be_swap!(u64);
|
||||
impl_be_swap!(i8);
|
||||
impl_be_swap!(i16);
|
||||
impl_be_swap!(i32);
|
||||
impl_be_swap!(i64);
|
||||
impl_be_swap!(f32);
|
||||
impl_be_swap!(f64);
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_be_u32() {
|
||||
let v = Be::<u32>::new(0x12345678);
|
||||
assert_eq!(v.get(), 0x12345678);
|
||||
assert_eq!(v.raw_bytes(), &[0x12, 0x34, 0x56, 0x78]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_be_u16() {
|
||||
let v = Be::<u16>::new(0xABCD);
|
||||
assert_eq!(v.get(), 0xABCD);
|
||||
assert_eq!(v.raw_bytes(), &[0xAB, 0xCD]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_be_mutate() {
|
||||
let mut v = Be::<u32>::new(1);
|
||||
assert_eq!(v.get(), 1);
|
||||
v.set(42);
|
||||
assert_eq!(v.get(), 42);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_be_f32() {
|
||||
let v = Be::<f32>::new(1.0);
|
||||
assert_eq!(v.get(), 1.0);
|
||||
// IEEE 754: 1.0f = 0x3F800000 => bytes [0x3F, 0x80, 0x00, 0x00]
|
||||
assert_eq!(v.raw_bytes(), &[0x3F, 0x80, 0x00, 0x00]);
|
||||
}
|
||||
}
|
||||
27
crates/xenia-types/src/error.rs
Normal file
27
crates/xenia-types/src/error.rs
Normal file
@@ -0,0 +1,27 @@
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum XeniaError {
|
||||
#[error("Invalid XEX2 file: {0}")]
|
||||
InvalidXex(String),
|
||||
|
||||
#[error("Invalid XISO file: {0}")]
|
||||
InvalidXiso(String),
|
||||
|
||||
#[error("Memory error: {0}")]
|
||||
Memory(String),
|
||||
|
||||
#[error("Unimplemented opcode: {0}")]
|
||||
UnimplementedOpcode(String),
|
||||
|
||||
#[error("Unimplemented kernel export: module={module} ordinal={ordinal:#x}")]
|
||||
UnimplementedExport { module: String, ordinal: u32 },
|
||||
|
||||
#[error("Invalid guest address: {0:#010x}")]
|
||||
InvalidAddress(u32),
|
||||
|
||||
#[error("I/O error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
}
|
||||
|
||||
pub type XeniaResult<T> = Result<T, XeniaError>;
|
||||
6
crates/xenia-types/src/lib.rs
Normal file
6
crates/xenia-types/src/lib.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
pub mod endian;
|
||||
pub mod error;
|
||||
pub mod vec128;
|
||||
|
||||
pub use endian::Be;
|
||||
pub use vec128::Vec128;
|
||||
206
crates/xenia-types/src/vec128.rs
Normal file
206
crates/xenia-types/src/vec128.rs
Normal file
@@ -0,0 +1,206 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt;
|
||||
|
||||
/// 128-bit vector register type matching the Xbox 360's VMX128 registers.
|
||||
/// Stored in big-endian byte order (matching guest memory layout).
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[repr(C, align(16))]
|
||||
pub struct Vec128 {
|
||||
pub bytes: [u8; 16],
|
||||
}
|
||||
|
||||
impl Vec128 {
|
||||
pub const ZERO: Self = Self { bytes: [0; 16] };
|
||||
|
||||
pub fn from_u32x4(a: u32, b: u32, c: u32, d: u32) -> Self {
|
||||
let mut bytes = [0u8; 16];
|
||||
bytes[0..4].copy_from_slice(&a.to_be_bytes());
|
||||
bytes[4..8].copy_from_slice(&b.to_be_bytes());
|
||||
bytes[8..12].copy_from_slice(&c.to_be_bytes());
|
||||
bytes[12..16].copy_from_slice(&d.to_be_bytes());
|
||||
Self { bytes }
|
||||
}
|
||||
|
||||
pub fn from_f32x4(a: f32, b: f32, c: f32, d: f32) -> Self {
|
||||
Self::from_u32x4(a.to_bits(), b.to_bits(), c.to_bits(), d.to_bits())
|
||||
}
|
||||
|
||||
/// Read the i-th u32 element (big-endian, 0-indexed).
|
||||
pub fn u32x4(&self, i: usize) -> u32 {
|
||||
let off = i * 4;
|
||||
u32::from_be_bytes([
|
||||
self.bytes[off],
|
||||
self.bytes[off + 1],
|
||||
self.bytes[off + 2],
|
||||
self.bytes[off + 3],
|
||||
])
|
||||
}
|
||||
|
||||
/// Write the i-th u32 element (big-endian, 0-indexed).
|
||||
pub fn set_u32x4(&mut self, i: usize, val: u32) {
|
||||
let off = i * 4;
|
||||
self.bytes[off..off + 4].copy_from_slice(&val.to_be_bytes());
|
||||
}
|
||||
|
||||
/// Read the i-th f32 element (big-endian, 0-indexed).
|
||||
pub fn f32x4(&self, i: usize) -> f32 {
|
||||
f32::from_bits(self.u32x4(i))
|
||||
}
|
||||
|
||||
/// Write the i-th f32 element (big-endian, 0-indexed).
|
||||
pub fn set_f32x4(&mut self, i: usize, val: f32) {
|
||||
self.set_u32x4(i, val.to_bits());
|
||||
}
|
||||
|
||||
/// Read the i-th u16 element (big-endian, 0-indexed).
|
||||
pub fn u16x8(&self, i: usize) -> u16 {
|
||||
let off = i * 2;
|
||||
u16::from_be_bytes([self.bytes[off], self.bytes[off + 1]])
|
||||
}
|
||||
|
||||
/// Write the i-th u16 element (big-endian, 0-indexed).
|
||||
pub fn set_u16x8(&mut self, i: usize, val: u16) {
|
||||
let off = i * 2;
|
||||
self.bytes[off..off + 2].copy_from_slice(&val.to_be_bytes());
|
||||
}
|
||||
|
||||
/// Read the i-th u8 element (0-indexed).
|
||||
pub fn u8x16(&self, i: usize) -> u8 {
|
||||
self.bytes[i]
|
||||
}
|
||||
|
||||
/// Write the i-th u8 element (0-indexed).
|
||||
pub fn set_u8x16(&mut self, i: usize, val: u8) {
|
||||
self.bytes[i] = val;
|
||||
}
|
||||
|
||||
/// Read as two u64 values (big-endian).
|
||||
pub fn u64x2(&self, i: usize) -> u64 {
|
||||
let off = i * 8;
|
||||
u64::from_be_bytes([
|
||||
self.bytes[off],
|
||||
self.bytes[off + 1],
|
||||
self.bytes[off + 2],
|
||||
self.bytes[off + 3],
|
||||
self.bytes[off + 4],
|
||||
self.bytes[off + 5],
|
||||
self.bytes[off + 6],
|
||||
self.bytes[off + 7],
|
||||
])
|
||||
}
|
||||
|
||||
pub fn set_u64x2(&mut self, i: usize, val: u64) {
|
||||
let off = i * 8;
|
||||
self.bytes[off..off + 8].copy_from_slice(&val.to_be_bytes());
|
||||
}
|
||||
|
||||
/// Get all 4 u32 elements as an array.
|
||||
pub fn as_u32x4(&self) -> [u32; 4] {
|
||||
[self.u32x4(0), self.u32x4(1), self.u32x4(2), self.u32x4(3)]
|
||||
}
|
||||
|
||||
/// Get all 4 f32 elements as an array.
|
||||
pub fn as_f32x4(&self) -> [f32; 4] {
|
||||
[self.f32x4(0), self.f32x4(1), self.f32x4(2), self.f32x4(3)]
|
||||
}
|
||||
|
||||
/// Get all 8 u16 elements as an array.
|
||||
pub fn as_u16x8(&self) -> [u16; 8] {
|
||||
[
|
||||
self.u16x8(0), self.u16x8(1), self.u16x8(2), self.u16x8(3),
|
||||
self.u16x8(4), self.u16x8(5), self.u16x8(6), self.u16x8(7),
|
||||
]
|
||||
}
|
||||
|
||||
/// Get all 16 bytes as an array.
|
||||
pub fn as_bytes(&self) -> [u8; 16] {
|
||||
self.bytes
|
||||
}
|
||||
|
||||
/// Create from a byte array.
|
||||
pub fn from_bytes(bytes: [u8; 16]) -> Self {
|
||||
Self { bytes }
|
||||
}
|
||||
|
||||
/// Create from a u32 array (big-endian elements).
|
||||
pub fn from_u32x4_array(arr: [u32; 4]) -> Self {
|
||||
Self::from_u32x4(arr[0], arr[1], arr[2], arr[3])
|
||||
}
|
||||
|
||||
/// Create from an f32 array (big-endian elements).
|
||||
pub fn from_f32x4_array(arr: [f32; 4]) -> Self {
|
||||
Self::from_f32x4(arr[0], arr[1], arr[2], arr[3])
|
||||
}
|
||||
|
||||
/// Create from a u16 array (big-endian elements).
|
||||
pub fn from_u16x8_array(arr: [u16; 8]) -> Self {
|
||||
let mut v = Self::ZERO;
|
||||
for i in 0..8 { v.set_u16x8(i, arr[i]); }
|
||||
v
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Vec128 {
|
||||
fn default() -> Self {
|
||||
Self::ZERO
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for Vec128 {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"Vec128({:08X}_{:08X}_{:08X}_{:08X})",
|
||||
self.u32x4(0),
|
||||
self.u32x4(1),
|
||||
self.u32x4(2),
|
||||
self.u32x4(3),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Vec128 {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
fmt::Debug::fmt(self, f)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_u32x4_roundtrip() {
|
||||
let v = Vec128::from_u32x4(0xDEADBEEF, 0xCAFEBABE, 0x12345678, 0x9ABCDEF0);
|
||||
assert_eq!(v.u32x4(0), 0xDEADBEEF);
|
||||
assert_eq!(v.u32x4(1), 0xCAFEBABE);
|
||||
assert_eq!(v.u32x4(2), 0x12345678);
|
||||
assert_eq!(v.u32x4(3), 0x9ABCDEF0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_f32x4_roundtrip() {
|
||||
let v = Vec128::from_f32x4(1.0, -2.5, 3.14, 0.0);
|
||||
assert_eq!(v.f32x4(0), 1.0);
|
||||
assert_eq!(v.f32x4(1), -2.5);
|
||||
assert!((v.f32x4(2) - 3.14).abs() < f32::EPSILON);
|
||||
assert_eq!(v.f32x4(3), 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_u16x8() {
|
||||
let v = Vec128::from_u32x4(0x00010002, 0x00030004, 0x00050006, 0x00070008);
|
||||
assert_eq!(v.u16x8(0), 0x0001);
|
||||
assert_eq!(v.u16x8(1), 0x0002);
|
||||
assert_eq!(v.u16x8(6), 0x0007);
|
||||
assert_eq!(v.u16x8(7), 0x0008);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_zero() {
|
||||
let v = Vec128::ZERO;
|
||||
for i in 0..4 {
|
||||
assert_eq!(v.u32x4(i), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
12
crates/xenia-vfs/Cargo.toml
Normal file
12
crates/xenia-vfs/Cargo.toml
Normal file
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "xenia-vfs"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
xenia-types = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
byteorder = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
54
crates/xenia-vfs/src/device.rs
Normal file
54
crates/xenia-vfs/src/device.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
use crate::{VfsDevice, VfsEntry, VfsError};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// Host filesystem pass-through device.
|
||||
pub struct HostPathDevice {
|
||||
name: String,
|
||||
root: PathBuf,
|
||||
}
|
||||
|
||||
impl HostPathDevice {
|
||||
pub fn new(name: impl Into<String>, root: impl AsRef<Path>) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
root: root.as_ref().to_path_buf(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl VfsDevice for HostPathDevice {
|
||||
fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
fn list_root(&self) -> Result<Vec<VfsEntry>, VfsError> {
|
||||
let mut entries = Vec::new();
|
||||
for entry in std::fs::read_dir(&self.root)? {
|
||||
let entry = entry?;
|
||||
let metadata = entry.metadata()?;
|
||||
entries.push(VfsEntry {
|
||||
name: entry.file_name().to_string_lossy().into_owned(),
|
||||
is_directory: metadata.is_dir(),
|
||||
size: metadata.len(),
|
||||
offset: 0,
|
||||
});
|
||||
}
|
||||
Ok(entries)
|
||||
}
|
||||
|
||||
fn read_file(&self, path: &str) -> Result<Vec<u8>, VfsError> {
|
||||
let full_path = self.root.join(path);
|
||||
std::fs::read(&full_path).map_err(VfsError::from)
|
||||
}
|
||||
|
||||
fn stat(&self, path: &str) -> Result<VfsEntry, VfsError> {
|
||||
let full_path = self.root.join(path);
|
||||
let metadata = std::fs::metadata(&full_path)?;
|
||||
Ok(VfsEntry {
|
||||
name: path.to_string(),
|
||||
is_directory: metadata.is_dir(),
|
||||
size: metadata.len(),
|
||||
offset: 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
185
crates/xenia-vfs/src/disc_image.rs
Normal file
185
crates/xenia-vfs/src/disc_image.rs
Normal file
@@ -0,0 +1,185 @@
|
||||
use crate::{VfsDevice, VfsEntry, VfsError};
|
||||
use std::io::{Read, Seek, SeekFrom};
|
||||
|
||||
/// XISO disc image device. Parses Xbox 360 disc images (GDFX/XISO format).
|
||||
pub struct DiscImageDevice {
|
||||
name: String,
|
||||
path: std::path::PathBuf,
|
||||
game_offset: u64,
|
||||
/// Cached root directory buffer (typically small, a few KB).
|
||||
root_buffer: Vec<u8>,
|
||||
}
|
||||
|
||||
/// XISO sector size
|
||||
pub const SECTOR_SIZE: u64 = 0x800;
|
||||
|
||||
/// GDFX magic string
|
||||
const GDFX_MAGIC: &[u8; 20] = b"MICROSOFT*XBOX*MEDIA";
|
||||
|
||||
/// File attribute: directory
|
||||
const FILE_ATTRIBUTE_DIRECTORY: u8 = 0x10;
|
||||
|
||||
/// Known game partition offsets to try
|
||||
const LIKELY_OFFSETS: &[u64] = &[
|
||||
0x0000_0000,
|
||||
0x0000_FB20,
|
||||
0x0002_0600,
|
||||
0x0208_0000,
|
||||
0x0FD9_0000,
|
||||
];
|
||||
|
||||
impl DiscImageDevice {
|
||||
pub fn open(name: impl Into<String>, path: &std::path::Path) -> Result<Self, VfsError> {
|
||||
let mut file = std::fs::File::open(path)?;
|
||||
|
||||
// Find the game partition by locating the GDFX magic at sector 32
|
||||
let mut game_offset = 0u64;
|
||||
let mut magic_found = false;
|
||||
let mut magic_buf = [0u8; 20];
|
||||
|
||||
for &offset in LIKELY_OFFSETS {
|
||||
let magic_pos = offset + 32 * SECTOR_SIZE;
|
||||
if file.seek(SeekFrom::Start(magic_pos)).is_ok()
|
||||
&& file.read_exact(&mut magic_buf).is_ok()
|
||||
&& magic_buf == *GDFX_MAGIC
|
||||
{
|
||||
game_offset = offset;
|
||||
magic_found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if !magic_found {
|
||||
return Err(VfsError::InvalidFormat(
|
||||
"GDFX magic not found - not a valid XISO disc image".into(),
|
||||
));
|
||||
}
|
||||
|
||||
// Read root directory info from sector 32 header
|
||||
let fs_ptr = game_offset + 32 * SECTOR_SIZE;
|
||||
file.seek(SeekFrom::Start(fs_ptr + 20))?;
|
||||
let mut buf4 = [0u8; 4];
|
||||
file.read_exact(&mut buf4)?;
|
||||
let root_sector = u32::from_le_bytes(buf4) as u64;
|
||||
file.read_exact(&mut buf4)?;
|
||||
let root_size = u32::from_le_bytes(buf4) as u64;
|
||||
|
||||
let root_byte_offset = game_offset + root_sector * SECTOR_SIZE;
|
||||
|
||||
// Read the root directory buffer into memory (typically small)
|
||||
file.seek(SeekFrom::Start(root_byte_offset))?;
|
||||
let mut root_buffer = vec![0u8; root_size as usize];
|
||||
file.read_exact(&mut root_buffer)?;
|
||||
|
||||
Ok(Self {
|
||||
name: name.into(),
|
||||
path: path.to_path_buf(),
|
||||
game_offset,
|
||||
root_buffer,
|
||||
})
|
||||
}
|
||||
|
||||
/// Read all directory entries from the root directory tree.
|
||||
fn read_entries(&self) -> Vec<VfsEntry> {
|
||||
let mut entries = Vec::new();
|
||||
self.read_entry(&self.root_buffer, 0, &mut entries);
|
||||
entries
|
||||
}
|
||||
|
||||
/// Recursively read a directory entry from the binary tree structure.
|
||||
fn read_entry(&self, buffer: &[u8], ordinal: u16, entries: &mut Vec<VfsEntry>) {
|
||||
let p = ordinal as usize * 4;
|
||||
if p + 14 > buffer.len() {
|
||||
return;
|
||||
}
|
||||
|
||||
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 attributes = buffer[p + 12];
|
||||
let name_length = buffer[p + 13] as usize;
|
||||
|
||||
if p + 14 + name_length > buffer.len() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Traverse left subtree first (smaller names)
|
||||
if node_l != 0 && node_l != 0xFFFF {
|
||||
self.read_entry(buffer, node_l, entries);
|
||||
}
|
||||
|
||||
// Read this entry's name
|
||||
let name = String::from_utf8_lossy(&buffer[p + 14..p + 14 + name_length]).to_string();
|
||||
let is_directory = (attributes & FILE_ATTRIBUTE_DIRECTORY) != 0;
|
||||
|
||||
let file_offset = self.game_offset + sector * SECTOR_SIZE;
|
||||
|
||||
entries.push(VfsEntry {
|
||||
name,
|
||||
is_directory,
|
||||
size: length,
|
||||
offset: file_offset,
|
||||
});
|
||||
|
||||
// Traverse right subtree (larger names)
|
||||
if node_r != 0 && node_r != 0xFFFF {
|
||||
self.read_entry(buffer, node_r, entries);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl VfsDevice for DiscImageDevice {
|
||||
fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
fn list_root(&self) -> Result<Vec<VfsEntry>, VfsError> {
|
||||
Ok(self.read_entries())
|
||||
}
|
||||
|
||||
fn read_file(&self, path: &str) -> Result<Vec<u8>, VfsError> {
|
||||
let entries = self.read_entries();
|
||||
let entry = entries.iter()
|
||||
.find(|e| e.name.eq_ignore_ascii_case(path) && !e.is_directory)
|
||||
.ok_or_else(|| VfsError::NotFound(path.to_string()))?;
|
||||
|
||||
let offset = entry.offset;
|
||||
let size = entry.size as usize;
|
||||
|
||||
// Read from file using seek
|
||||
let mut file = std::fs::File::open(&self.path)?;
|
||||
let file_len = file.seek(SeekFrom::End(0))?;
|
||||
if offset + size as u64 > file_len {
|
||||
return Err(VfsError::NotFound(format!(
|
||||
"File data extends past end of image: {} (offset={:#x}, size={:#x}, image_len={:#x})",
|
||||
path, offset, size, file_len
|
||||
)));
|
||||
}
|
||||
file.seek(SeekFrom::Start(offset))?;
|
||||
let mut buf = vec![0u8; size];
|
||||
let bytes_read = file.read(&mut buf)?;
|
||||
if bytes_read < size {
|
||||
// Try reading the rest
|
||||
let mut total = bytes_read;
|
||||
while total < size {
|
||||
let n = file.read(&mut buf[total..])?;
|
||||
if n == 0 {
|
||||
return Err(VfsError::NotFound(format!(
|
||||
"Short read: got {} of {} bytes for {}",
|
||||
total, size, path
|
||||
)));
|
||||
}
|
||||
total += n;
|
||||
}
|
||||
}
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
fn stat(&self, path: &str) -> Result<VfsEntry, VfsError> {
|
||||
let entries = self.read_entries();
|
||||
entries.into_iter()
|
||||
.find(|e| e.name.eq_ignore_ascii_case(path))
|
||||
.ok_or_else(|| VfsError::NotFound(path.to_string()))
|
||||
}
|
||||
}
|
||||
33
crates/xenia-vfs/src/lib.rs
Normal file
33
crates/xenia-vfs/src/lib.rs
Normal file
@@ -0,0 +1,33 @@
|
||||
pub mod device;
|
||||
pub mod disc_image;
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum VfsError {
|
||||
#[error("I/O error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
#[error("Invalid format: {0}")]
|
||||
InvalidFormat(String),
|
||||
|
||||
#[error("File not found: {0}")]
|
||||
NotFound(String),
|
||||
}
|
||||
|
||||
/// A virtual filesystem entry (file or directory).
|
||||
#[derive(Debug)]
|
||||
pub struct VfsEntry {
|
||||
pub name: String,
|
||||
pub is_directory: bool,
|
||||
pub size: u64,
|
||||
pub offset: u64,
|
||||
}
|
||||
|
||||
/// Trait for VFS device implementations (XISO, STFS, host path, etc.)
|
||||
pub trait VfsDevice: Send + Sync {
|
||||
fn name(&self) -> &str;
|
||||
fn list_root(&self) -> Result<Vec<VfsEntry>, VfsError>;
|
||||
fn read_file(&self, path: &str) -> Result<Vec<u8>, VfsError>;
|
||||
fn stat(&self, path: &str) -> Result<VfsEntry, VfsError>;
|
||||
}
|
||||
17
crates/xenia-xex/Cargo.toml
Normal file
17
crates/xenia-xex/Cargo.toml
Normal file
@@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "xenia-xex"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
xenia-types = { workspace = true }
|
||||
xenia-memory = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
byteorder = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
aes = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
1
crates/xenia-xex/build.rs
Normal file
1
crates/xenia-xex/build.rs
Normal file
@@ -0,0 +1 @@
|
||||
fn main() {}
|
||||
128
crates/xenia-xex/src/header.rs
Normal file
128
crates/xenia-xex/src/header.rs
Normal file
@@ -0,0 +1,128 @@
|
||||
use serde::Serialize;
|
||||
|
||||
/// XEX2 file header. Parsed from the beginning of an Xbox 360 executable.
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct Xex2Header {
|
||||
pub magic: u32,
|
||||
pub module_flags: u32,
|
||||
pub header_size: u32,
|
||||
pub security_offset: u32,
|
||||
pub header_count: u32,
|
||||
pub optional_headers: Vec<Xex2OptionalHeader>,
|
||||
pub security_info: Option<Xex2SecurityInfo>,
|
||||
/// Parsed file format info (if present).
|
||||
pub file_format_info: Option<FileFormatInfo>,
|
||||
/// Parsed import libraries (addresses only until resolve_imports is called).
|
||||
pub import_libraries: Vec<ImportLibrary>,
|
||||
/// Execution info (title ID, media ID, etc.).
|
||||
pub execution_info: Option<ExecutionInfo>,
|
||||
/// Original PE name from the XEX header.
|
||||
pub original_pe_name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct Xex2OptionalHeader {
|
||||
pub key: u32,
|
||||
pub value: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct Xex2SecurityInfo {
|
||||
pub image_size: u32,
|
||||
pub load_address: u32,
|
||||
pub export_table_address: u32,
|
||||
pub image_flags: u32,
|
||||
/// Encrypted session key (decrypted with retail/devkit key to get actual session key).
|
||||
pub aes_key: [u8; 16],
|
||||
pub page_descriptors: Vec<Xex2PageDescriptor>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize)]
|
||||
pub struct Xex2PageDescriptor {
|
||||
pub size_and_info: u32,
|
||||
}
|
||||
|
||||
impl Xex2PageDescriptor {
|
||||
pub fn page_count(&self) -> u32 {
|
||||
self.size_and_info >> 4
|
||||
}
|
||||
|
||||
pub fn info(&self) -> u32 {
|
||||
self.size_and_info & 0xF
|
||||
}
|
||||
}
|
||||
|
||||
/// File format info (compression and encryption types).
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct FileFormatInfo {
|
||||
pub info_size: u32,
|
||||
pub encryption_type: u16,
|
||||
pub compression_type: u16,
|
||||
/// For basic compression: list of (data_size, zero_size) block pairs.
|
||||
pub basic_blocks: Vec<BasicCompressionBlock>,
|
||||
/// For normal (LZX) compression: window size.
|
||||
pub normal_window_size: u32,
|
||||
/// For normal (LZX) compression: first block size (from header).
|
||||
pub normal_first_block_size: u32,
|
||||
/// For normal (LZX) compression: first block hash (from header).
|
||||
pub normal_first_block_hash: [u8; 20],
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize)]
|
||||
pub struct BasicCompressionBlock {
|
||||
pub data_size: u32,
|
||||
pub zero_size: u32,
|
||||
}
|
||||
|
||||
/// An imported library with its resolved imports.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct ImportLibrary {
|
||||
pub name: String,
|
||||
pub id: u32,
|
||||
pub version_min: u32,
|
||||
pub version_cur: u32,
|
||||
/// Import entries. Before `resolve_imports`, these contain addresses but no ordinals.
|
||||
/// After `resolve_imports`, ordinals and record types are filled in from the PE image.
|
||||
pub imports: Vec<ImportEntry>,
|
||||
}
|
||||
|
||||
/// A single import entry within an import library.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct ImportEntry {
|
||||
pub ordinal: u16,
|
||||
pub record_type: u8, // 0 = variable, 1 = thunk
|
||||
pub address: u32,
|
||||
}
|
||||
|
||||
/// Execution info parsed from the XEX header.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct ExecutionInfo {
|
||||
pub media_id: u32,
|
||||
pub title_id: u32,
|
||||
pub disc_number: u8,
|
||||
pub disc_count: u8,
|
||||
}
|
||||
|
||||
/// XEX2 magic: "XEX2"
|
||||
pub const XEX2_MAGIC: u32 = 0x58455832;
|
||||
|
||||
/// Compression types
|
||||
pub const COMPRESSION_NONE: u16 = 0;
|
||||
pub const COMPRESSION_BASIC: u16 = 1;
|
||||
pub const COMPRESSION_NORMAL: u16 = 2;
|
||||
|
||||
/// Encryption types
|
||||
pub const ENCRYPTION_NONE: u16 = 0;
|
||||
pub const ENCRYPTION_NORMAL: u16 = 1;
|
||||
|
||||
/// Optional header keys
|
||||
pub mod header_keys {
|
||||
pub const ENTRY_POINT: u32 = 0x00010100;
|
||||
pub const IMAGE_BASE_ADDRESS: u32 = 0x00010201;
|
||||
pub const IMPORT_LIBRARIES: u32 = 0x000103FF;
|
||||
pub const TLS_INFO: u32 = 0x00020200;
|
||||
pub const EXECUTION_INFO: u32 = 0x00040006;
|
||||
pub const DEFAULT_STACK_SIZE: u32 = 0x00020104;
|
||||
pub const ORIGINAL_PE_NAME: u32 = 0x000183FF;
|
||||
pub const FILE_FORMAT_INFO: u32 = 0x000003FF;
|
||||
}
|
||||
6
crates/xenia-xex/src/lib.rs
Normal file
6
crates/xenia-xex/src/lib.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
pub mod header;
|
||||
pub mod loader;
|
||||
pub mod lzx;
|
||||
pub mod pe;
|
||||
|
||||
pub use header::Xex2Header;
|
||||
571
crates/xenia-xex/src/loader.rs
Normal file
571
crates/xenia-xex/src/loader.rs
Normal file
@@ -0,0 +1,571 @@
|
||||
use crate::header::*;
|
||||
use aes::cipher::{BlockDecrypt, KeyInit};
|
||||
use aes::Aes128;
|
||||
use byteorder::{BigEndian, ReadBytesExt};
|
||||
use std::io::{self, Cursor, Read, Seek, SeekFrom};
|
||||
|
||||
/// Parse a XEX2 header from raw file data.
|
||||
pub fn parse_xex2_header(data: &[u8]) -> io::Result<Xex2Header> {
|
||||
let mut cursor = Cursor::new(data);
|
||||
|
||||
let magic = cursor.read_u32::<BigEndian>()?;
|
||||
if magic != XEX2_MAGIC {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("Invalid XEX2 magic: {:#010x} (expected {:#010x})", magic, XEX2_MAGIC),
|
||||
));
|
||||
}
|
||||
|
||||
let module_flags = cursor.read_u32::<BigEndian>()?;
|
||||
let header_size = cursor.read_u32::<BigEndian>()?;
|
||||
let _reserved = cursor.read_u32::<BigEndian>()?;
|
||||
let security_offset = cursor.read_u32::<BigEndian>()?;
|
||||
let header_count = cursor.read_u32::<BigEndian>()?;
|
||||
|
||||
let mut optional_headers = Vec::new();
|
||||
for _ in 0..header_count {
|
||||
let key = cursor.read_u32::<BigEndian>()?;
|
||||
let value = cursor.read_u32::<BigEndian>()?;
|
||||
optional_headers.push(Xex2OptionalHeader { key, value });
|
||||
}
|
||||
|
||||
// Parse security info
|
||||
let security_info = if (security_offset as usize) < data.len() {
|
||||
cursor.seek(SeekFrom::Start(security_offset as u64))?;
|
||||
Some(parse_security_info(&mut cursor)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Parse file format info
|
||||
let file_format_info = parse_file_format_info(data, &optional_headers);
|
||||
|
||||
// Parse import libraries (addresses only; call resolve_imports after decompression)
|
||||
let import_libraries = parse_import_libraries(data, &optional_headers);
|
||||
|
||||
// Parse execution info
|
||||
let execution_info = parse_execution_info(data, &optional_headers);
|
||||
|
||||
// Parse original PE name
|
||||
let original_pe_name = parse_original_pe_name(data, &optional_headers);
|
||||
|
||||
Ok(Xex2Header {
|
||||
magic,
|
||||
module_flags,
|
||||
header_size,
|
||||
security_offset,
|
||||
header_count,
|
||||
optional_headers,
|
||||
security_info,
|
||||
file_format_info,
|
||||
import_libraries,
|
||||
execution_info,
|
||||
original_pe_name,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_security_info(cursor: &mut Cursor<&[u8]>) -> io::Result<Xex2SecurityInfo> {
|
||||
// xex2_security_info layout (from xex2_info.h):
|
||||
// 0x000: header_size (u32)
|
||||
// 0x004: image_size (u32)
|
||||
// 0x008: rsa_signature (0x100 bytes)
|
||||
// 0x108: unk_108 (u32)
|
||||
// 0x10C: image_flags (u32)
|
||||
// 0x110: load_address (u32)
|
||||
// 0x114: section_digest (0x14 bytes)
|
||||
// 0x128: import_table_count (u32)
|
||||
// 0x12C: import_table_digest (0x14 bytes)
|
||||
// 0x140: xgd2_media_id (0x10 bytes)
|
||||
// 0x150: aes_key (0x10 bytes)
|
||||
// 0x160: export_table (u32)
|
||||
// 0x164: header_digest (0x14 bytes)
|
||||
// 0x178: region (u32)
|
||||
// 0x17C: allowed_media_types (u32)
|
||||
// 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
|
||||
|
||||
// Skip RSA signature (0x100 bytes)
|
||||
let mut rsa_sig = [0u8; 0x100];
|
||||
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
|
||||
|
||||
// Skip section_digest (0x14 bytes)
|
||||
let mut digest = [0u8; 0x14];
|
||||
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
|
||||
|
||||
// Skip xgd2_media_id (0x10 bytes)
|
||||
let mut media_id = [0u8; 0x10];
|
||||
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
|
||||
|
||||
let export_table_address = cursor.read_u32::<BigEndian>()?; // 0x160
|
||||
|
||||
// Skip header_digest (0x14 bytes)
|
||||
cursor.read_exact(&mut digest)?; // 0x164
|
||||
|
||||
let _region = cursor.read_u32::<BigEndian>()?; // 0x178
|
||||
let _allowed_media = cursor.read_u32::<BigEndian>()?; // 0x17C
|
||||
|
||||
let page_descriptor_count = cursor.read_u32::<BigEndian>()?; // 0x180
|
||||
|
||||
let mut page_descriptors = Vec::new();
|
||||
for _ in 0..page_descriptor_count {
|
||||
let size_and_info = cursor.read_u32::<BigEndian>()?;
|
||||
// Skip data_digest (0x14 bytes per descriptor)
|
||||
cursor.read_exact(&mut digest)?;
|
||||
page_descriptors.push(Xex2PageDescriptor { size_and_info });
|
||||
}
|
||||
|
||||
Ok(Xex2SecurityInfo {
|
||||
image_size,
|
||||
load_address,
|
||||
export_table_address,
|
||||
image_flags,
|
||||
aes_key,
|
||||
page_descriptors,
|
||||
})
|
||||
}
|
||||
|
||||
/// Parse file format info from the optional header data.
|
||||
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 offset = header.value as usize;
|
||||
if offset + 8 > data.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut cursor = Cursor::new(data);
|
||||
cursor.seek(SeekFrom::Start(offset as u64)).ok()?;
|
||||
|
||||
let info_size = cursor.read_u32::<BigEndian>().ok()?;
|
||||
let encryption_type = cursor.read_u16::<BigEndian>().ok()?;
|
||||
let compression_type = cursor.read_u16::<BigEndian>().ok()?;
|
||||
|
||||
let mut basic_blocks = Vec::new();
|
||||
let mut normal_window_size = 0u32;
|
||||
let mut normal_first_block_size = 0u32;
|
||||
let mut normal_first_block_hash = [0u8; 20];
|
||||
|
||||
match compression_type {
|
||||
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 };
|
||||
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 });
|
||||
}
|
||||
}
|
||||
COMPRESSION_NORMAL => {
|
||||
normal_window_size = cursor.read_u32::<BigEndian>().ok()?;
|
||||
// Read first_block: block_size (4) + block_hash (20)
|
||||
normal_first_block_size = cursor.read_u32::<BigEndian>().ok()?;
|
||||
cursor.read_exact(&mut normal_first_block_hash).ok()?;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Some(FileFormatInfo {
|
||||
info_size,
|
||||
encryption_type,
|
||||
compression_type,
|
||||
basic_blocks,
|
||||
normal_window_size,
|
||||
normal_first_block_size,
|
||||
normal_first_block_hash,
|
||||
})
|
||||
}
|
||||
|
||||
/// Parse import libraries from the optional header data.
|
||||
/// 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) {
|
||||
Some(h) => h,
|
||||
None => return Vec::new(),
|
||||
};
|
||||
|
||||
let offset = header.value as usize;
|
||||
if offset + 12 > data.len() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
fn be_u32(data: &[u8], off: usize) -> u32 {
|
||||
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]])
|
||||
}
|
||||
|
||||
let total_size = be_u32(data, offset) as usize;
|
||||
let string_table_size = be_u32(data, offset + 4) as usize;
|
||||
let string_count = be_u32(data, offset + 8) as usize;
|
||||
|
||||
// Parse string table (null-terminated, 4-byte aligned)
|
||||
let string_data_start = offset + 12;
|
||||
let mut strings = Vec::new();
|
||||
let mut spos = 0usize;
|
||||
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();
|
||||
spos += name.len() + 1;
|
||||
// 4-byte alignment
|
||||
if spos % 4 != 0 { spos += 4 - (spos % 4); }
|
||||
strings.push(name);
|
||||
}
|
||||
|
||||
// Parse libraries
|
||||
let mut libs = Vec::new();
|
||||
let mut lib_off = offset + 12 + string_table_size;
|
||||
|
||||
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; }
|
||||
|
||||
let id = be_u32(data, lib_off + 0x18);
|
||||
let version_cur = be_u32(data, lib_off + 0x1C);
|
||||
let version_min = be_u32(data, lib_off + 0x20);
|
||||
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 mut imports = Vec::new();
|
||||
for i in 0..count {
|
||||
let record_addr = be_u32(data, lib_off + 0x28 + i * 4);
|
||||
imports.push(ImportEntry {
|
||||
ordinal: 0,
|
||||
record_type: 0xFF,
|
||||
address: record_addr,
|
||||
});
|
||||
}
|
||||
|
||||
libs.push(ImportLibrary {
|
||||
name: lib_name,
|
||||
id,
|
||||
version_min,
|
||||
version_cur,
|
||||
imports,
|
||||
});
|
||||
lib_off += lib_size;
|
||||
}
|
||||
|
||||
libs
|
||||
}
|
||||
|
||||
/// Resolve import ordinals and record types from the decompressed PE image.
|
||||
/// Must be called after `load_image` provides the PE data.
|
||||
pub fn resolve_imports(header: &mut Xex2Header, pe_image: &[u8]) {
|
||||
let image_base = get_image_base(header).unwrap_or(0);
|
||||
|
||||
for lib in &mut header.import_libraries {
|
||||
for imp in &mut lib.imports {
|
||||
let pe_off = imp.address.wrapping_sub(image_base) as usize;
|
||||
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],
|
||||
]);
|
||||
imp.record_type = ((val >> 24) & 0xFF) as u8;
|
||||
imp.ordinal = (val & 0xFFFF) as u16;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse execution info from optional header data.
|
||||
fn parse_execution_info(data: &[u8], headers: &[Xex2OptionalHeader]) -> Option<ExecutionInfo> {
|
||||
// EXECUTION_INFO key is 0x00040006 — the low byte 0x06 means the value
|
||||
// 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 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 disc_number = data[off + 18];
|
||||
let disc_count = data[off + 19];
|
||||
|
||||
Some(ExecutionInfo {
|
||||
media_id,
|
||||
title_id,
|
||||
disc_number,
|
||||
disc_count,
|
||||
})
|
||||
}
|
||||
|
||||
/// 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 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;
|
||||
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())
|
||||
}
|
||||
|
||||
/// Get an optional header value by key.
|
||||
pub fn get_opt_header(header: &Xex2Header, key: u32) -> Option<u32> {
|
||||
header.optional_headers.iter()
|
||||
.find(|h| h.key == key)
|
||||
.map(|h| h.value)
|
||||
}
|
||||
|
||||
/// Get the entry point address from the XEX2 header.
|
||||
pub fn get_entry_point(header: &Xex2Header) -> Option<u32> {
|
||||
get_opt_header(header, header_keys::ENTRY_POINT)
|
||||
}
|
||||
|
||||
/// Get the image base address.
|
||||
pub fn get_image_base(header: &Xex2Header) -> Option<u32> {
|
||||
get_opt_header(header, header_keys::IMAGE_BASE_ADDRESS)
|
||||
}
|
||||
|
||||
/// Get the default stack size.
|
||||
pub fn get_stack_size(header: &Xex2Header) -> u32 {
|
||||
get_opt_header(header, header_keys::DEFAULT_STACK_SIZE).unwrap_or(0x10_0000) // Default 1MB
|
||||
}
|
||||
|
||||
/// Load the XEX image data into a flat buffer (decompressing if needed).
|
||||
/// Returns the decompressed image bytes ready to map into guest memory.
|
||||
pub fn load_image(data: &[u8], header: &Xex2Header) -> io::Result<Vec<u8>> {
|
||||
let source = &data[header.header_size as usize..];
|
||||
|
||||
match &header.file_format_info {
|
||||
Some(info) if info.compression_type == COMPRESSION_BASIC => {
|
||||
load_basic_compressed(source, info)
|
||||
}
|
||||
Some(info) if info.compression_type == COMPRESSION_NORMAL => {
|
||||
load_normal_compressed(source, info, header)
|
||||
}
|
||||
_ => {
|
||||
// Uncompressed (or no format info = treat as uncompressed)
|
||||
Ok(source.to_vec())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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()
|
||||
.map(|b| b.data_size as u64 + b.zero_size as u64)
|
||||
.sum();
|
||||
|
||||
let mut output = vec![0u8; total_size as usize];
|
||||
let mut src_offset = 0usize;
|
||||
let mut dst_offset = 0usize;
|
||||
|
||||
for block in &info.basic_blocks {
|
||||
let data_size = block.data_size as usize;
|
||||
let zero_size = block.zero_size as usize;
|
||||
|
||||
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()),
|
||||
));
|
||||
}
|
||||
|
||||
// Copy data block
|
||||
if dst_offset + data_size <= output.len() {
|
||||
output[dst_offset..dst_offset + data_size]
|
||||
.copy_from_slice(&source[src_offset..src_offset + data_size]);
|
||||
}
|
||||
src_offset += data_size;
|
||||
dst_offset += data_size;
|
||||
|
||||
// Zero-filled gap (already zeroed from vec initialization)
|
||||
dst_offset += zero_size;
|
||||
}
|
||||
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
/// 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,
|
||||
];
|
||||
|
||||
/// Xbox 360 devkit AES key (all zeros).
|
||||
#[allow(dead_code)]
|
||||
const XEX2_DEVKIT_KEY: [u8; 16] = [0u8; 16];
|
||||
|
||||
/// AES-128-CBC decryption with zero IV (matching Xbox 360 XEX decryption).
|
||||
fn aes_decrypt_cbc(key: &[u8; 16], input: &[u8]) -> Vec<u8> {
|
||||
let cipher = Aes128::new(key.into());
|
||||
let mut output = vec![0u8; input.len()];
|
||||
let mut iv = [0u8; 16];
|
||||
|
||||
for (i, chunk) in input.chunks(16).enumerate() {
|
||||
if chunk.len() < 16 {
|
||||
// Partial block at end - copy as-is
|
||||
output[i * 16..i * 16 + chunk.len()].copy_from_slice(chunk);
|
||||
break;
|
||||
}
|
||||
let mut block = aes::Block::clone_from_slice(chunk);
|
||||
cipher.decrypt_block(&mut block);
|
||||
// XOR with IV (previous ciphertext block)
|
||||
for j in 0..16 {
|
||||
block[j] ^= iv[j];
|
||||
}
|
||||
iv.copy_from_slice(chunk);
|
||||
output[i * 16..(i + 1) * 16].copy_from_slice(&block);
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
/// Derive the session key by decrypting the XEX's aes_key field with the retail key.
|
||||
/// Falls back to devkit key if retail produces invalid results.
|
||||
fn derive_session_key(header: &Xex2Header) -> [u8; 16] {
|
||||
let sec = match &header.security_info {
|
||||
Some(s) => s,
|
||||
None => return [0u8; 16],
|
||||
};
|
||||
|
||||
let decrypted = aes_decrypt_cbc(&XEX2_RETAIL_KEY, &sec.aes_key);
|
||||
let mut session_key = [0u8; 16];
|
||||
session_key.copy_from_slice(&decrypted[..16]);
|
||||
session_key
|
||||
}
|
||||
|
||||
/// De-block compressed data: strip block headers and extract chunk payloads.
|
||||
///
|
||||
/// 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)
|
||||
/// 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();
|
||||
let mut pos = 0usize;
|
||||
let mut cur_block_size = first_block_size as usize;
|
||||
|
||||
while cur_block_size > 0 && pos < input.len() {
|
||||
let next_block_pos = pos + cur_block_size;
|
||||
|
||||
// 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
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
// Skip block_info header (4 bytes size + 20 bytes hash)
|
||||
let mut p = pos + 4 + 20;
|
||||
|
||||
// Read chunks within this block
|
||||
loop {
|
||||
if p + 2 > input.len() {
|
||||
break;
|
||||
}
|
||||
let chunk_size = ((input[p] as usize) << 8) | (input[p + 1] as usize);
|
||||
p += 2;
|
||||
if chunk_size == 0 {
|
||||
break;
|
||||
}
|
||||
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()),
|
||||
));
|
||||
}
|
||||
output.extend_from_slice(&input[p..p + chunk_size]);
|
||||
p += chunk_size;
|
||||
}
|
||||
|
||||
if next_block_pos <= pos {
|
||||
break; // Prevent infinite loop
|
||||
}
|
||||
pos = next_block_pos;
|
||||
cur_block_size = next_block_size;
|
||||
}
|
||||
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
/// Load normal (LZX) compressed image data.
|
||||
/// Pipeline: decrypt → de-block → LZX decompress (pure Rust)
|
||||
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);
|
||||
|
||||
if uncompressed_size == 0 {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"Cannot decompress: image_size is 0",
|
||||
));
|
||||
}
|
||||
|
||||
// Step 1: Decrypt if needed
|
||||
let decrypted;
|
||||
let input = if info.encryption_type == ENCRYPTION_NORMAL {
|
||||
let session_key = derive_session_key(header);
|
||||
decrypted = aes_decrypt_cbc(&session_key, source);
|
||||
&decrypted
|
||||
} else {
|
||||
source
|
||||
};
|
||||
|
||||
// Step 2: De-block (strip block headers, extract chunk payloads)
|
||||
let deblocked = deblock(input, info.normal_first_block_size)?;
|
||||
|
||||
if deblocked.is_empty() {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"De-blocking produced no data",
|
||||
));
|
||||
}
|
||||
|
||||
// Step 3: LZX decompress using pure Rust decoder
|
||||
let window_bits = match info.normal_window_size {
|
||||
s if s == 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}")))?;
|
||||
|
||||
tracing::info!("LZX decompressed: {} -> {} bytes", deblocked.len(), uncompressed_size);
|
||||
|
||||
Ok(output)
|
||||
}
|
||||
692
crates/xenia-xex/src/lzx.rs
Normal file
692
crates/xenia-xex/src/lzx.rs
Normal file
@@ -0,0 +1,692 @@
|
||||
//! LZX decompressor for Xbox 360 XEX2 "normal compression".
|
||||
//! Ported from libmspack lzxd.c (C) 2003-2013 Stuart Caie, LGPL 2.1.
|
||||
|
||||
use std::fmt;
|
||||
|
||||
// ── LZX constants ───────────────────────────────────────────────────────────
|
||||
|
||||
const LZX_MIN_MATCH: usize = 2;
|
||||
const LZX_NUM_CHARS: usize = 256;
|
||||
const LZX_BLOCKTYPE_VERBATIM: u8 = 1;
|
||||
const LZX_BLOCKTYPE_ALIGNED: u8 = 2;
|
||||
const LZX_BLOCKTYPE_UNCOMPRESSED: u8 = 3;
|
||||
const LZX_NUM_PRIMARY_LENGTHS: usize = 7;
|
||||
const LZX_NUM_SECONDARY_LENGTHS: usize = 249;
|
||||
const LZX_FRAME_SIZE: usize = 32768;
|
||||
const HUFF_MAXBITS: usize = 16;
|
||||
|
||||
const PRETREE_MAXSYMS: usize = 20;
|
||||
const PRETREE_TABLEBITS: usize = 6;
|
||||
const MAINTREE_MAXSYMS: usize = LZX_NUM_CHARS + 290 * 8; // 2576
|
||||
const MAINTREE_TABLEBITS: usize = 12;
|
||||
const LENGTH_MAXSYMS: usize = LZX_NUM_SECONDARY_LENGTHS + 1; // 250
|
||||
const LENGTH_TABLEBITS: usize = 12;
|
||||
const ALIGNED_MAXSYMS: usize = 8;
|
||||
const ALIGNED_TABLEBITS: usize = 7;
|
||||
const LENTABLE_SAFETY: usize = 64;
|
||||
|
||||
const BITBUF_WIDTH: u32 = 32;
|
||||
|
||||
// ── Static tables ───────────────────────────────────────────────────────────
|
||||
|
||||
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,
|
||||
];
|
||||
|
||||
#[rustfmt::skip]
|
||||
static POSITION_BASE: [u32; 290] = [
|
||||
0, 1, 2, 3, 4, 6, 8, 12, 16, 24, 32, 48, 64, 96, 128, 192, 256, 384, 512,
|
||||
768, 1024, 1536, 2048, 3072, 4096, 6144, 8192, 12288, 16384, 24576, 32768,
|
||||
49152, 65536, 98304, 131072, 196608, 262144, 393216, 524288, 655360,
|
||||
786432, 917504, 1048576, 1179648, 1310720, 1441792, 1572864, 1703936,
|
||||
1835008, 1966080, 2097152, 2228224, 2359296, 2490368, 2621440, 2752512,
|
||||
2883584, 3014656, 3145728, 3276800, 3407872, 3538944, 3670016, 3801088,
|
||||
3932160, 4063232, 4194304, 4325376, 4456448, 4587520, 4718592, 4849664,
|
||||
4980736, 5111808, 5242880, 5373952, 5505024, 5636096, 5767168, 5898240,
|
||||
6029312, 6160384, 6291456, 6422528, 6553600, 6684672, 6815744, 6946816,
|
||||
7077888, 7208960, 7340032, 7471104, 7602176, 7733248, 7864320, 7995392,
|
||||
8126464, 8257536, 8388608, 8519680, 8650752, 8781824, 8912896, 9043968,
|
||||
9175040, 9306112, 9437184, 9568256, 9699328, 9830400, 9961472, 10092544,
|
||||
10223616, 10354688, 10485760, 10616832, 10747904, 10878976, 11010048,
|
||||
11141120, 11272192, 11403264, 11534336, 11665408, 11796480, 11927552,
|
||||
12058624, 12189696, 12320768, 12451840, 12582912, 12713984, 12845056,
|
||||
12976128, 13107200, 13238272, 13369344, 13500416, 13631488, 13762560,
|
||||
13893632, 14024704, 14155776, 14286848, 14417920, 14548992, 14680064,
|
||||
14811136, 14942208, 15073280, 15204352, 15335424, 15466496, 15597568,
|
||||
15728640, 15859712, 15990784, 16121856, 16252928, 16384000, 16515072,
|
||||
16646144, 16777216, 16908288, 17039360, 17170432, 17301504, 17432576,
|
||||
17563648, 17694720, 17825792, 17956864, 18087936, 18219008, 18350080,
|
||||
18481152, 18612224, 18743296, 18874368, 19005440, 19136512, 19267584,
|
||||
19398656, 19529728, 19660800, 19791872, 19922944, 20054016, 20185088,
|
||||
20316160, 20447232, 20578304, 20709376, 20840448, 20971520, 21102592,
|
||||
21233664, 21364736, 21495808, 21626880, 21757952, 21889024, 22020096,
|
||||
22151168, 22282240, 22413312, 22544384, 22675456, 22806528, 22937600,
|
||||
23068672, 23199744, 23330816, 23461888, 23592960, 23724032, 23855104,
|
||||
23986176, 24117248, 24248320, 24379392, 24510464, 24641536, 24772608,
|
||||
24903680, 25034752, 25165824, 25296896, 25427968, 25559040, 25690112,
|
||||
25821184, 25952256, 26083328, 26214400, 26345472, 26476544, 26607616,
|
||||
26738688, 26869760, 27000832, 27131904, 27262976, 27394048, 27525120,
|
||||
27656192, 27787264, 27918336, 28049408, 28180480, 28311552, 28442624,
|
||||
28573696, 28704768, 28835840, 28966912, 29097984, 29229056, 29360128,
|
||||
29491200, 29622272, 29753344, 29884416, 30015488, 30146560, 30277632,
|
||||
30408704, 30539776, 30670848, 30801920, 30932992, 31064064, 31195136,
|
||||
31326208, 31457280, 31588352, 31719424, 31850496, 31981568, 32112640,
|
||||
32243712, 32374784, 32505856, 32636928, 32768000, 32899072, 33030144,
|
||||
33161216, 33292288, 33423360,
|
||||
];
|
||||
|
||||
// ── Error type ──────────────────────────────────────────────────────────────
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum LzxError {
|
||||
BadHuffmanTable,
|
||||
Decrunch(String),
|
||||
}
|
||||
|
||||
impl fmt::Display for LzxError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::BadHuffmanTable => write!(f, "failed to build Huffman table"),
|
||||
Self::Decrunch(msg) => write!(f, "LZX decrunch error: {msg}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for LzxError {}
|
||||
|
||||
// ── Bit reader (MSB order, 16-bit LE pairs) ────────────────────────────────
|
||||
|
||||
struct BitReader<'a> {
|
||||
data: &'a [u8],
|
||||
pos: usize,
|
||||
buf: u32,
|
||||
left: i32,
|
||||
}
|
||||
|
||||
impl<'a> BitReader<'a> {
|
||||
fn new(data: &'a [u8]) -> Self {
|
||||
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 b1 = if self.pos < self.data.len() {
|
||||
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;
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn ensure(&mut self, n: i32) {
|
||||
while self.left < n { self.fill(); }
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn peek(&self, n: u32) -> u32 {
|
||||
self.buf >> (BITBUF_WIDTH - n)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn remove(&mut self, n: u32) {
|
||||
self.buf <<= n;
|
||||
self.left -= n as i32;
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn read(&mut self, n: u32) -> u32 {
|
||||
self.ensure(n as i32);
|
||||
let v = self.peek(n);
|
||||
self.remove(n);
|
||||
v
|
||||
}
|
||||
|
||||
/// 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 }
|
||||
}
|
||||
|
||||
/// Re-align the bitstream at a frame boundary.
|
||||
fn align_frame(&mut self) {
|
||||
if self.left > 0 { self.ensure(16); }
|
||||
let r = self.left & 15;
|
||||
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 {
|
||||
let mut pos: usize = 0;
|
||||
let table_mask = 1usize << nbits;
|
||||
let mut bit_mask = table_mask >> 1;
|
||||
|
||||
// Short codes: direct mapping
|
||||
for bit_num in 1..=nbits {
|
||||
for sym in 0..nsyms {
|
||||
if length[sym] as usize != bit_num { continue; }
|
||||
let leaf = pos;
|
||||
pos += bit_mask;
|
||||
if pos > table_mask { return true; }
|
||||
for i in leaf..leaf + bit_mask {
|
||||
table[i] = sym as u16;
|
||||
}
|
||||
}
|
||||
bit_mask >>= 1;
|
||||
}
|
||||
|
||||
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 pos32 = (pos as u32) << 16;
|
||||
let table_mask32 = (table_mask as u32) << 16;
|
||||
let mut bit_mask32: u32 = 1 << 15;
|
||||
|
||||
// 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; }
|
||||
|
||||
let mut leaf = (pos32 >> 16) as usize;
|
||||
|
||||
for fill in 0..(bit_num - nbits) {
|
||||
if table[leaf] == 0xFFFF {
|
||||
table[next_symbol << 1] = 0xFFFF;
|
||||
table[(next_symbol << 1) + 1] = 0xFFFF;
|
||||
table[leaf] = next_symbol as u16;
|
||||
next_symbol += 1;
|
||||
}
|
||||
leaf = (table[leaf] as usize) << 1;
|
||||
if (pos32 >> (15 - fill as u32)) & 1 != 0 {
|
||||
leaf += 1;
|
||||
}
|
||||
}
|
||||
table[leaf] = sym as u16;
|
||||
pos32 += bit_mask32;
|
||||
}
|
||||
bit_mask32 >>= 1;
|
||||
}
|
||||
|
||||
pos32 != table_mask32
|
||||
}
|
||||
|
||||
// ── Huffman symbol decoder ──────────────────────────────────────────────────
|
||||
|
||||
fn read_huffsym(
|
||||
br: &mut BitReader,
|
||||
table: &[u16],
|
||||
lens: &[u8],
|
||||
tablebits: usize,
|
||||
maxsyms: usize,
|
||||
) -> Result<usize, LzxError> {
|
||||
br.ensure(HUFF_MAXBITS as i32);
|
||||
let mut sym = table[br.peek(tablebits as u32) as usize] as usize;
|
||||
if sym >= maxsyms {
|
||||
let mut i: u32 = 1 << (BITBUF_WIDTH - tablebits as u32);
|
||||
loop {
|
||||
i >>= 1;
|
||||
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; }
|
||||
}
|
||||
}
|
||||
br.remove(lens[sym] as u32);
|
||||
Ok(sym)
|
||||
}
|
||||
|
||||
// ── LZX decoder state ───────────────────────────────────────────────────────
|
||||
|
||||
pub struct LzxDecoder {
|
||||
window: Vec<u8>,
|
||||
window_size: usize,
|
||||
window_posn: usize,
|
||||
frame_posn: usize,
|
||||
frame: usize,
|
||||
num_offsets: usize,
|
||||
|
||||
r0: u32,
|
||||
r1: u32,
|
||||
r2: u32,
|
||||
|
||||
block_type: u8,
|
||||
block_length: usize,
|
||||
block_remaining: usize,
|
||||
|
||||
header_read: bool,
|
||||
intel_filesize: i32,
|
||||
intel_curpos: i32,
|
||||
intel_started: bool,
|
||||
|
||||
// Huffman code lengths
|
||||
pretree_len: Vec<u8>,
|
||||
maintree_len: Vec<u8>,
|
||||
length_len: Vec<u8>,
|
||||
aligned_len: Vec<u8>,
|
||||
|
||||
// Huffman decode tables
|
||||
pretree_table: Vec<u16>,
|
||||
maintree_table: Vec<u16>,
|
||||
length_table: Vec<u16>,
|
||||
aligned_table: Vec<u16>,
|
||||
|
||||
length_empty: bool,
|
||||
}
|
||||
|
||||
impl LzxDecoder {
|
||||
pub fn new(window_bits: u32) -> Self {
|
||||
assert!((15..=21).contains(&window_bits));
|
||||
let window_size = 1usize << window_bits;
|
||||
let num_offsets = (POSITION_SLOTS[(window_bits - 15) as usize] as usize) << 3;
|
||||
|
||||
Self {
|
||||
window: vec![0u8; window_size],
|
||||
window_size,
|
||||
window_posn: 0,
|
||||
frame_posn: 0,
|
||||
frame: 0,
|
||||
num_offsets,
|
||||
r0: 1, r1: 1, r2: 1,
|
||||
block_type: 0,
|
||||
block_length: 0,
|
||||
block_remaining: 0,
|
||||
header_read: false,
|
||||
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],
|
||||
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_empty: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn build_table(
|
||||
lens: &[u8], table: &mut [u16], maxsyms: usize, tablebits: usize,
|
||||
) -> Result<(), LzxError> {
|
||||
if make_decode_table(maxsyms, tablebits, lens, table) {
|
||||
Err(LzxError::BadHuffmanTable)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn build_table_maybe_empty(
|
||||
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)
|
||||
for i in 0..maxsyms {
|
||||
if lens[i] > 0 {
|
||||
return Err(LzxError::BadHuffmanTable);
|
||||
}
|
||||
}
|
||||
Ok(true) // empty
|
||||
} else {
|
||||
Ok(false) // not empty
|
||||
}
|
||||
}
|
||||
|
||||
/// Read Huffman code lengths using the pretree (lzxd_read_lens).
|
||||
fn read_lens(
|
||||
br: &mut BitReader,
|
||||
lens: &mut [u8],
|
||||
pretree_len: &mut [u8],
|
||||
pretree_table: &mut [u16],
|
||||
first: usize,
|
||||
last: usize,
|
||||
) -> Result<(), LzxError> {
|
||||
// Build pretree: 20 symbols, 4 bits each
|
||||
for i in 0..20 {
|
||||
pretree_len[i] = br.read(4) as u8;
|
||||
}
|
||||
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)?;
|
||||
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; }
|
||||
} 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; }
|
||||
} 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 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; }
|
||||
} else {
|
||||
// Delta: code 0..16
|
||||
let mut val = lens[x] as i32 - z as i32;
|
||||
if val < 0 { val += 17; }
|
||||
lens[x] = val as u8;
|
||||
x += 1;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Decompress the full LZX stream into the output buffer.
|
||||
pub fn decompress(&mut self, input: &[u8], output_len: usize) -> Result<Vec<u8>, LzxError> {
|
||||
let mut br = BitReader::new(input);
|
||||
let mut output = Vec::with_capacity(output_len);
|
||||
let mut offset: usize = 0;
|
||||
|
||||
let end_frame = (output_len / LZX_FRAME_SIZE) + 1;
|
||||
|
||||
while self.frame < end_frame {
|
||||
// Read header once
|
||||
if !self.header_read {
|
||||
let i_bit = br.read(1);
|
||||
let (hi, lo) = if i_bit != 0 {
|
||||
(br.read(16), br.read(16))
|
||||
} else {
|
||||
(0, 0)
|
||||
};
|
||||
self.intel_filesize = ((hi << 16) | lo) as i32;
|
||||
self.header_read = true;
|
||||
}
|
||||
|
||||
// Frame size
|
||||
let frame_size = if output_len > 0 && (output_len - offset) < LZX_FRAME_SIZE {
|
||||
output_len - offset
|
||||
} else {
|
||||
LZX_FRAME_SIZE
|
||||
};
|
||||
|
||||
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 {
|
||||
br.raw_byte();
|
||||
}
|
||||
// Read block type (3 bits) and length (24 bits)
|
||||
self.block_type = br.read(3) as u8;
|
||||
let hi = br.read(16) as usize;
|
||||
let lo = br.read(8) as usize;
|
||||
self.block_length = (hi << 8) | lo;
|
||||
self.block_remaining = self.block_length;
|
||||
|
||||
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)?;
|
||||
// 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)?;
|
||||
}
|
||||
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)?;
|
||||
}
|
||||
LZX_BLOCKTYPE_UNCOMPRESSED => {
|
||||
self.intel_started = true;
|
||||
// Align to byte boundary
|
||||
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(); }
|
||||
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]]);
|
||||
}
|
||||
_ => return Err(LzxError::Decrunch("bad block type".into())),
|
||||
}
|
||||
}
|
||||
|
||||
let mut this_run = self.block_remaining as i32;
|
||||
if this_run > bytes_todo { this_run = bytes_todo; }
|
||||
bytes_todo -= this_run;
|
||||
self.block_remaining -= this_run as usize;
|
||||
|
||||
let window_size = self.window_size;
|
||||
|
||||
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)?;
|
||||
if main_element < LZX_NUM_CHARS {
|
||||
self.window[self.window_posn] = main_element as u8;
|
||||
self.window_posn += 1;
|
||||
this_run -= 1;
|
||||
} else {
|
||||
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)?;
|
||||
match_length += footer;
|
||||
}
|
||||
match_length += LZX_MIN_MATCH;
|
||||
|
||||
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; }
|
||||
_ => {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
if self.window_posn + match_length > window_size {
|
||||
return Err(LzxError::Decrunch("match overrun".into()));
|
||||
}
|
||||
self.copy_match(match_offset as usize, match_length);
|
||||
this_run -= match_length as i32;
|
||||
}
|
||||
}
|
||||
}
|
||||
LZX_BLOCKTYPE_ALIGNED => {
|
||||
while this_run > 0 {
|
||||
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;
|
||||
this_run -= 1;
|
||||
} else {
|
||||
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)?;
|
||||
match_length += footer;
|
||||
}
|
||||
match_length += LZX_MIN_MATCH;
|
||||
|
||||
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; }
|
||||
_ => {
|
||||
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)?;
|
||||
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)?;
|
||||
match_offset += aligned as u32;
|
||||
} else if extra > 0 {
|
||||
let verbatim_bits = br.read(extra);
|
||||
match_offset += verbatim_bits;
|
||||
} else {
|
||||
match_offset = 1;
|
||||
}
|
||||
self.r2 = self.r1; self.r1 = self.r0; self.r0 = match_offset;
|
||||
}
|
||||
}
|
||||
|
||||
if self.window_posn + match_length > window_size {
|
||||
return Err(LzxError::Decrunch("match overrun".into()));
|
||||
}
|
||||
self.copy_match(match_offset as usize, match_length);
|
||||
this_run -= match_length as i32;
|
||||
}
|
||||
}
|
||||
}
|
||||
LZX_BLOCKTYPE_UNCOMPRESSED => {
|
||||
let run = this_run as usize;
|
||||
for _ in 0..run {
|
||||
self.window[self.window_posn] = br.raw_byte();
|
||||
self.window_posn += 1;
|
||||
}
|
||||
}
|
||||
_ => return Err(LzxError::Decrunch("bad block type in decode".into())),
|
||||
}
|
||||
|
||||
// Overrun accounting
|
||||
if this_run < 0 {
|
||||
let overrun = (-this_run) as usize;
|
||||
if overrun > self.block_remaining {
|
||||
return Err(LzxError::Decrunch("overrun past block end".into()));
|
||||
}
|
||||
self.block_remaining -= overrun;
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
)));
|
||||
}
|
||||
|
||||
// Re-align bitstream
|
||||
br.align_frame();
|
||||
|
||||
// Intel E8 postprocessing
|
||||
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]);
|
||||
|
||||
let mut i = 0usize;
|
||||
let limit = frame_size - 10;
|
||||
let mut curpos = self.intel_curpos;
|
||||
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 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;
|
||||
}
|
||||
i += 5;
|
||||
curpos += 5;
|
||||
}
|
||||
self.intel_curpos += frame_size as i32;
|
||||
|
||||
let to_write = frame_size.min(output_len - offset);
|
||||
output.extend_from_slice(&e8_buf[..to_write]);
|
||||
offset += to_write;
|
||||
} else {
|
||||
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;
|
||||
}
|
||||
|
||||
// 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; }
|
||||
}
|
||||
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
/// Copy a match from the window (handles wrap-around).
|
||||
fn copy_match(&mut self, match_offset: usize, match_length: usize) {
|
||||
let window_size = self.window_size;
|
||||
let mut remaining = match_length;
|
||||
|
||||
if match_offset > self.window_posn {
|
||||
// Source wraps around window end
|
||||
let j = match_offset - self.window_posn;
|
||||
let mut src = window_size - j;
|
||||
if j < remaining {
|
||||
remaining -= j;
|
||||
for _ in 0..j {
|
||||
self.window[self.window_posn] = self.window[src];
|
||||
self.window_posn += 1;
|
||||
src += 1;
|
||||
}
|
||||
src = 0; // wrap to start
|
||||
}
|
||||
for _ in 0..remaining {
|
||||
self.window[self.window_posn] = self.window[src];
|
||||
self.window_posn += 1;
|
||||
src += 1;
|
||||
}
|
||||
} else {
|
||||
let mut src = self.window_posn - match_offset;
|
||||
for _ in 0..remaining {
|
||||
self.window[self.window_posn] = self.window[src];
|
||||
self.window_posn += 1;
|
||||
src += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
68
crates/xenia-xex/src/pe.rs
Normal file
68
crates/xenia-xex/src/pe.rs
Normal file
@@ -0,0 +1,68 @@
|
||||
//! Minimal PE parser for Xbox 360 executables.
|
||||
//! PE headers are little-endian even on the big-endian Xbox 360.
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
pub struct PeSection {
|
||||
pub name: String,
|
||||
pub virtual_address: u32,
|
||||
pub virtual_size: u32,
|
||||
pub raw_offset: u32,
|
||||
pub raw_size: u32,
|
||||
pub flags: u32,
|
||||
}
|
||||
|
||||
impl PeSection {
|
||||
pub fn is_code(&self) -> bool {
|
||||
self.flags & 0x20000000 != 0 // IMAGE_SCN_MEM_EXECUTE
|
||||
}
|
||||
}
|
||||
|
||||
fn le_u16(data: &[u8], off: usize) -> u16 {
|
||||
u16::from_le_bytes([data[off], data[off + 1]])
|
||||
}
|
||||
|
||||
fn le_u32(data: &[u8], off: usize) -> u32 {
|
||||
u32::from_le_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]])
|
||||
}
|
||||
|
||||
pub fn parse_sections(pe: &[u8]) -> anyhow::Result<Vec<PeSection>> {
|
||||
anyhow::ensure!(pe.len() >= 64, "PE too small");
|
||||
anyhow::ensure!(pe[0] == b'M' && pe[1] == b'Z', "not a PE (bad MZ)");
|
||||
|
||||
let e_lfanew = le_u32(pe, 0x3C) as usize;
|
||||
anyhow::ensure!(e_lfanew + 4 <= pe.len(), "e_lfanew out of bounds");
|
||||
|
||||
let nt_sig = le_u32(pe, e_lfanew);
|
||||
anyhow::ensure!(nt_sig == 0x00004550, "bad PE signature: 0x{nt_sig:08X}");
|
||||
|
||||
let file_header_off = e_lfanew + 4;
|
||||
let num_sections = le_u16(pe, file_header_off + 2) as usize;
|
||||
let opt_header_size = le_u16(pe, file_header_off + 16) as usize;
|
||||
|
||||
let section_table_off = file_header_off + 20 + opt_header_size;
|
||||
|
||||
let mut sections = Vec::new();
|
||||
for i in 0..num_sections {
|
||||
let s = section_table_off + i * 40;
|
||||
if s + 40 > pe.len() { break; }
|
||||
|
||||
let name_bytes = &pe[s..s + 8];
|
||||
let name = std::str::from_utf8(name_bytes)
|
||||
.unwrap_or("???")
|
||||
.trim_end_matches('\0')
|
||||
.to_string();
|
||||
|
||||
sections.push(PeSection {
|
||||
name,
|
||||
virtual_size: le_u32(pe, s + 8),
|
||||
virtual_address: le_u32(pe, s + 12),
|
||||
raw_size: le_u32(pe, s + 16),
|
||||
raw_offset: le_u32(pe, s + 20),
|
||||
flags: le_u32(pe, s + 36),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(sections)
|
||||
}
|
||||
Reference in New Issue
Block a user