From f4b43bc2d895172b3c156a5fcd0fa19ecd5cbb61 Mon Sep 17 00:00:00 2001 From: SaveEditors Date: Thu, 23 Jul 2026 10:04:22 -0400 Subject: [PATCH 1/3] Update loader for IDA Pro 9.4 --- CMakeLists.txt | 17 ++- README.md | 78 ++++++++++- formats/xbe.cpp | 3 +- formats/xex.cpp | 30 +++-- formats/xex.hpp | 5 +- idaloader.cpp | 4 +- idaloader_xbe.cpp | 29 ++-- scripts/Check-IdaEnv.ps1 | 264 +++++++++++++++++++++++++++++++++++++ scripts/Test-IdaCorpus.ps1 | 190 ++++++++++++++++++++++++++ scripts/Test-IdaLoader.ps1 | 90 +++++++++++++ scripts/Test-IdaLoader.py | 113 ++++++++++++++++ xex1tool.cpp | 2 +- 12 files changed, 798 insertions(+), 27 deletions(-) create mode 100644 scripts/Check-IdaEnv.ps1 create mode 100644 scripts/Test-IdaCorpus.ps1 create mode 100644 scripts/Test-IdaLoader.ps1 create mode 100644 scripts/Test-IdaLoader.py diff --git a/CMakeLists.txt b/CMakeLists.txt index 8f8e036..a288759 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -27,6 +27,8 @@ project(idaxex CXX C) set(CMAKE_CXX_STANDARD 17) set(CMAKE_C_STANDARD 11) +option(IDAXEX_STRICT_WARNINGS + "Enable strict compiler warnings for compatibility validation" OFF) # `cmake --build` writes the loader into the build tree (build/bin/loaders/). # `cmake --install` (below) is what copies it into IDA's directory. This must @@ -78,7 +80,20 @@ endif() target_compile_options(idaxex PRIVATE $<$:-Wno-non-pod-varargs>) -ida_disable_warnings(idaxex) +if(IDAXEX_STRICT_WARNINGS) + target_compile_options(idaxex PRIVATE + $<$:/W4> + $<$:/wd4201> + $<$:/W4> + $<$:/wd4201> + $<$:-Wall> + $<$:-Wextra> + $<$:-Wall> + $<$:-Wextra> + ) +else() + ida_disable_warnings(idaxex) +endif() # `cmake --install build` installs the loader so IDA picks it up automatically. # Default the install prefix to IDA's per-user directory (loaders/ subdir): diff --git a/README.md b/README.md index 3b2d255..3fcede0 100644 --- a/README.md +++ b/README.md @@ -33,12 +33,73 @@ Includes support for the following Xbox executables: - XBE: tries naming SDK library functions using [XbSymbolDatabase](https://github.com/Cxbx-Reloaded/XbSymbolDatabase) & data from XTLID section ## Install + Prebuilt releases are available for supported IDA Pro 9.x versions. -Copy the loader files into the matching IDA installation's loader/plugin directory, or follow the build steps below and install the resulting binary into your IDA SDK output folder. +IDA Professional is required for Xbox 360 XEX analysis because IDA Free does +not include the PowerPC processor module. + +The recommended Windows installation uses an IDA user directory so IDA's +installation files remain unchanged: + +1. Create a directory for idaxex, for example + `%APPDATA%\Hex-Rays\IDA Pro\idaxex`. +2. Copy `loaders\idaxex.dll`, `til\x360.til`, and `til\xkelib.til` from the + release package into the same relative directories there. +3. Add that directory to the `IDAUSR` environment variable. If `IDAUSR` + already contains other directories, append the new directory using a + semicolon on Windows. +4. Restart IDA. + +Installing the package's `loaders` and `til` directories directly into the +matching IDA installation is also supported, but normally requires +administrator access. For PPC Altivec analysis, the PPCAltivec plugin remains a useful companion: https://github.com/hayleyxyz/PPC-Altivec-IDA +## Loading an executable + +1. Start IDA Professional 9.4 and choose **New** or **File > Open**. +2. Select the XEX or XBE executable. Change the file filter to **All files** + if the executable is not displayed. +3. In the load dialog, verify the detected file type: + - Xbox 360 files should show an `Xbox360 XEX...` format provided by + `idaxex.dll` and a PowerPC processor. + - Original Xbox files should show `Xbox XBE file` and the `metapc` + processor. +4. Accept the load settings. The memory-mapping information dialog shown for + PowerPC files is expected. +5. If IDA offers to locate a PDB, select a matching PDB when one is available; + otherwise decline the prompt. A missing PDB does not prevent the executable + from loading. +6. Confirm successful operation in IDA's Output window. It should report that + the file was successfully loaded and should identify `idaxex.dll` as the + selected loader. + +For automated compatibility testing, `scripts\Test-IdaLoader.ps1` runs IDA +non-interactively, verifies the detected file type and processor, checks the +created segments, entry points, functions, imports, and names, and creates a +test database: + +```powershell +scripts\Test-IdaLoader.ps1 ` + -IdaExe "C:\Program Files\IDA Professional 9.4\ida.exe" ` + -InputFile "C:\samples\default.xex" +``` + +`scripts\Test-IdaCorpus.ps1` applies the same structural checks to a +SHA-256-deduplicated corpus and writes its inventory, exclusions, per-file +results, and IDA logs below the specified output directory: + +```powershell +scripts\Test-IdaCorpus.ps1 ` + -IdaExe "C:\Program Files\IDA Professional 9.4\idat.exe" ` + -InputRoot "C:\samples\xex;D:\additional-samples" ` + -OutputRoot "C:\idaxex-validation" ` + -Magic "XEX0;XEX1;XEX2;XEX-;XEX?;XEX%;XBEH" ` + -MaximumCases 30 +``` + ## Building Dependencies are pulled in as submodules, so clone recursively: @@ -56,6 +117,21 @@ cmake -S . -B build -G Ninja cmake --build build ``` +The repository vendors the official IDA SDK through `3rdparty/ida-sdk`. For a +warning-focused compatibility build, configure with +`-DIDAXEX_STRICT_WARNINGS=ON`. On Windows, the environment and vendored SDK can +be checked before building: + +```powershell +scripts\Check-IdaEnv.ps1 ` + -IdaExe "C:\Program Files\IDA Professional 9.4\ida.exe" ` + -ExpectedIdaVersion 9.4 ` + -ExpectedSdkVersion 940 ` + -ExpectedSdkTag v9.4.0-release ` + -RequireOfficialSdk ` + -RequireBuildReady +``` + This builds the loader at `build/bin/loaders/` (`idaxex.dll` on Windows, `idaxex.so` on Linux, `idaxex.dylib` on macOS). To install it into IDA's per-user directory so it's picked up automatically, run: diff --git a/formats/xbe.cpp b/formats/xbe.cpp index 1cb9df0..5b4ea55 100644 --- a/formats/xbe.cpp +++ b/formats/xbe.cpp @@ -137,7 +137,8 @@ bool XBEFile::load(void* file) if (tls_directory_.AddressOfCallBacks) { auto callback_offset = xbe_va_to_offset(tls_directory_.AddressOfCallBacks); - dbgmsg("[+] Reading TLS callbacks from 0x%X (directory: 0x%X)\n", tls_directory_.AddressOfCallBacks, tls_directory_va_); + dbgmsg("[+] Reading TLS callbacks from 0x%X (directory: 0x%X)\n", + uint32_t(tls_directory_.AddressOfCallBacks), tls_directory_va_); if (image_length_ >= (callback_offset + sizeof(uint32_t))) { diff --git a/formats/xex.cpp b/formats/xex.cpp index b507942..c983c68 100644 --- a/formats/xex.cpp +++ b/formats/xex.cpp @@ -123,7 +123,7 @@ bool XEXFile::load(void* file) // Read security info has_secinfo_ = read_secinfo(file); if (has_secinfo_) { - uint32_t state = verify_secinfo(file); + verify_secinfo(file); // Kinda hacky way to get the security info size... // Maybe should save this somewhere when reading instead? @@ -324,7 +324,6 @@ bool XEXFile::read_imports(void* file) // Get import table hashes ready for verifying... // (Hash is of +4 into the table, ie skipping the TableSize field) uint8_t hash_expected[20]; - uint8_t hash[20]; std::copy_n(security_info_.ImageInfo.ImportDigest, 20, hash_expected); valid_imports_hash_ = true; @@ -339,6 +338,7 @@ bool XEXFile::read_imports(void* file) // TODO: this only seems to work for XEX2 atm, need to find method for XEX1... if (valid_imports_hash_) // Only check import hashes while they're valid { + uint8_t hash[20]; xe::be table_size; read(&table_size, 4, 1, file); @@ -423,7 +423,8 @@ bool XEXFile::read_imports(void* file) *(uint32_t*)(pe_data() + record_offset + 4) = xe::byte_swap(0x38800000 | ordinal); } else // todo: does this ever appear? - dbgmsg("[+] %s import %d (@ 0x%X) unknown type %d!\n", libname.c_str(), ordinal, record_addr, record_type); + dbgmsg("[+] %s import %d (@ 0x%X) unknown type %d!\n", + libname.c_str(), ordinal, uint32_t(record_addr), uint32_t(record_type)); imports_[libname][ordinal] = imp; } @@ -466,7 +467,9 @@ bool XEXFile::read_imports(void* file) // Sanity check the callcap info, values from first dword should match values in second if (ordinal_1 != ordinal_2 || moduleidx_1 != moduleidx_2) { - dbgmsg("[!] Invalid callcap at 0x%X? (%X %X %X %X)\n", addr, ordinal_1, ordinal_2, moduleidx_1, moduleidx_2); + dbgmsg("[!] Invalid callcap at 0x%X? (%X %X %X %X)\n", + uint32_t(addr), uint32_t(ordinal_1), uint32_t(ordinal_2), + uint32_t(moduleidx_1), uint32_t(moduleidx_2)); continue; } @@ -498,6 +501,9 @@ bool XEXFile::read_imports(void* file) // Reads function info defined inside XEX export table bool XEXFile::read_exports(void* file) { +#ifdef IDALDR + (void)file; +#endif uint32_t exports_va = security_info_.ImageInfo.ExportTableAddress; if (xex_header_.Magic == MAGIC_XEX1 && directory_entries_.count(XEX_HEADER_EXPORTS_XEX1)) exports_va = directory_entries_[XEX_HEADER_EXPORTS_XEX1]; @@ -516,7 +522,9 @@ bool XEXFile::read_exports(void* file) export_table.Magic[1] != XEX_HV_MAGIC_HVE || export_table.Magic[2] != XEX_HV_MAGIC_2) { - dbgmsg("[+] Export table magic is invalid! (0x%X 0x%X 0x%X)\n", export_table.Magic[0], export_table.Magic[1], export_table.Magic[2]); + dbgmsg("[+] Export table magic is invalid! (0x%X 0x%X 0x%X)\n", + uint32_t(export_table.Magic[0]), uint32_t(export_table.Magic[1]), + uint32_t(export_table.Magic[2])); return false; } @@ -552,6 +560,9 @@ bool XEXFile::read_exports(void* file) uint32_t XEXFile::verify_secinfo(void* file) { +#ifdef IDALDR + (void)file; +#endif valid_signature_ = false; valid_header_hash_ = false; @@ -1155,7 +1166,6 @@ bool XEXFile::pe_load(const uint8_t* data) auto* callbacks = reinterpret_cast*>(data + callback_offset); while (*callbacks) { - uint32_t callback = *callbacks; tls_callbacks_.push_back(*callbacks); callbacks++; } @@ -1203,11 +1213,11 @@ bool XEXFile::pe_load(const uint8_t* data) if (!cv_ptr) continue; - std::vector data; - data.resize(dir.SizeOfData); - std::copy_n((uint8_t*)cv_ptr, dir.SizeOfData, data.data()); + std::vector cv_data; + cv_data.resize(dir.SizeOfData); + std::copy_n((uint8_t*)cv_ptr, dir.SizeOfData, cv_data.data()); - codeview_data_.push_back(data); + codeview_data_.push_back(cv_data); } } } diff --git a/formats/xex.hpp b/formats/xex.hpp index 915e59b..ebd7cdd 100644 --- a/formats/xex.hpp +++ b/formats/xex.hpp @@ -207,9 +207,10 @@ class XEXFile uint32_t encryption_key_index() { return key_index_; } uint8_t* session_key() { return session_key_; } - bool is_encrypted() { return data_descriptor_->Flags != 0; } + bool is_encrypted() { return data_descriptor_ != nullptr && data_descriptor_->Flags != 0; } bool is_compressed() { - return (data_descriptor_->DataFormat() == xex_opt::XexDataFormat::Compressed || + return data_descriptor_ != nullptr && + (data_descriptor_->DataFormat() == xex_opt::XexDataFormat::Compressed || data_descriptor_->DataFormat() == xex_opt::XexDataFormat::DeltaCompressed); } diff --git a/idaloader.cpp b/idaloader.cpp index 7f81bb7..c26cd89 100644 --- a/idaloader.cpp +++ b/idaloader.cpp @@ -185,7 +185,6 @@ void pe_add_sections(linput_t* li, XEXFile& file) seg_perms |= SEGPERM_WRITE; bool has_code = (section.Characteristics & IMAGE_SCN_CNT_CODE); - bool has_data = (section.Characteristics & IMAGE_SCN_CNT_INITIALIZED_DATA) || (section.Characteristics & IMAGE_SCN_CNT_UNINITIALIZED_DATA); const char* seg_class = has_code ? "CODE" : "DATA"; @@ -304,7 +303,6 @@ void pe_parse_pdata(XEXFile& file) sec_addr = section.PointerToRawData; sec_size = section.SizeOfRawData; // TODO: verify this? } - ea_t seg_addr = (ea_t)file.base_address() + (ea_t)section.VirtualAddress; // Size could be beyond file bounds, if so fix the size to what we can fit if (sec_addr + sec_size > file.image_size()) sec_size = file.image_size() - sec_addr; @@ -741,7 +739,7 @@ static int idaapi accept_file( else if (magic == MAGIC_XEX3F) { valid = 1; - *fileformatname = "Xbox360 XEX?/XEX3F File (>=1434)"; + *fileformatname = "Xbox360 XEX?/XEX3F File (>=1529)"; } else if (magic == MAGIC_XEX0) { diff --git a/idaloader_xbe.cpp b/idaloader_xbe.cpp index 4068a08..39741ce 100644 --- a/idaloader_xbe.cpp +++ b/idaloader_xbe.cpp @@ -299,6 +299,18 @@ void mark_lib_func(ea_t func_ea) del_items(func_ea); auto_make_proc(func_ea); +#if IDA_SDK_VERSION >= 940 + if (get_func_start(func_ea) != BADADDR) + { + set_func_flag(func_ea, FUNC_LIB); + } + else + { + func_entry_info_t func(func_ea, BADADDR); + func.set_flag(FUNC_LIB); + add_function_ex(&func); + } +#else func_t* existing = get_func(func_ea); if (existing) { @@ -310,20 +322,21 @@ void mark_lib_func(ea_t func_ea) func_t func(func_ea, BADADDR, FUNC_LIB); add_func_ex(&func); } +#endif } static int num_dbsymbols = 0; -static void reg_cb(const char* library_str, - uint32_t library_flag, +static void reg_cb(const char*, + uint32_t, uint32_t xref_index, - const char* symbol_str, + const char*, xbaddr address, - uint32_t build_version, + uint32_t, uint32_t symbol_type, - uint32_t call_type, - uint32_t param_count, - const XbSDBSymbolParam* param_list) + uint32_t, + uint32_t, + const XbSDBSymbolParam*) { num_dbsymbols++; const char* symbol_name = XbSDB_SymbolReferenceToString(xref_index); @@ -649,7 +662,7 @@ bool load_application_xbe(linput_t* li) } //------------------------------------------------------------------------------ -void idaapi load_file_xbe(linput_t* li, ushort _neflags, const char* fileformatname) +void idaapi load_file_xbe(linput_t* li, ushort _neflags, const char*) { bool reloading = (_neflags & NEF_RELOAD) == NEF_RELOAD; diff --git a/scripts/Check-IdaEnv.ps1 b/scripts/Check-IdaEnv.ps1 new file mode 100644 index 0000000..3dc41a8 --- /dev/null +++ b/scripts/Check-IdaEnv.ps1 @@ -0,0 +1,264 @@ +param( + [string]$IdaExe = $env:IDAEXE, + [string]$IdaSdk = $env:IDASDK, + [string]$ExpectedIdaVersion = "", + [int]$ExpectedSdkVersion = 0, + [string]$ExpectedSdkBranch = "", + [string]$ExpectedSdkTag = "", + [string]$ExpectedSdkCommit = "", + [switch]$RequireOfficialSdk, + [switch]$RequireBuildReady, + [switch]$Json +) + +if (-not $IdaSdk) { + $repoRoot = Split-Path -Parent $PSScriptRoot + $IdaSdk = Join-Path $repoRoot '3rdparty\ida-sdk' +} + +function Resolve-IdaSdkDir { + param([string]$Path) + + if (-not $Path -or -not (Test-Path $Path)) { + return $null + } + + $resolved = (Resolve-Path $Path).Path + if (Test-Path (Join-Path $resolved 'include\pro.h')) { + return $resolved + } + + $srcPath = Join-Path $resolved 'src' + if (Test-Path (Join-Path $srcPath 'include\pro.h')) { + return $srcPath + } + + return $resolved +} + +function Get-IdaSdkVersion { + param([string]$SdkDir) + + $proPath = Join-Path $SdkDir 'include\pro.h' + if (-not (Test-Path $proPath)) { + return $null + } + + $match = Select-String -LiteralPath $proPath -Pattern '^\s*#define\s+IDA_SDK_VERSION\s+(\d+)' | Select-Object -First 1 + if ($match -and $match.Matches.Count -gt 0) { + return [int]$match.Matches[0].Groups[1].Value + } + + return $null +} + +function Get-IdaInstallVersion { + param([string]$InstallDir) + + if (-not $InstallDir) { + return $null + } + + $releaseNotes = Join-Path $InstallDir 'release-notes.md' + if (Test-Path $releaseNotes) { + $heading = Select-String -LiteralPath $releaseNotes -Pattern '^#\s+IDA\s+([0-9]+(?:\.[0-9]+)+)' | Select-Object -First 1 + if ($heading -and $heading.Matches.Count -gt 0) { + return $heading.Matches[0].Groups[1].Value + } + } + + $folderMatch = [regex]::Match($InstallDir, 'IDA(?:\s+Professional)?\s+([0-9]+(?:\.[0-9]+)+)', 'IgnoreCase') + if ($folderMatch.Success) { + return $folderMatch.Groups[1].Value + } + + return $null +} + +function Invoke-Git { + param( + [string]$WorkingDir, + [string[]]$Arguments + ) + + if (-not $WorkingDir -or -not (Test-Path $WorkingDir)) { + return $null + } + + $output = & git -C $WorkingDir @Arguments 2>$null + if ($LASTEXITCODE -ne 0) { + return $null + } + + return ($output -join "`n").Trim() +} + +function Get-GitInfo { + param([string]$Path) + + $root = Invoke-Git $Path @('rev-parse', '--show-toplevel') + if (-not $root) { + return $null + } + + $branch = Invoke-Git $root @('rev-parse', '--abbrev-ref', 'HEAD') + $head = Invoke-Git $root @('rev-parse', 'HEAD') + $upstream = Invoke-Git $root @('rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}') + $tags = Invoke-Git $root @('tag', '--points-at', 'HEAD') + $remoteName = $null + $remoteUrl = $null + if ($upstream -and $upstream -match '^([^/]+)/') { + $remoteName = $Matches[1] + $remoteUrl = Invoke-Git $root @('remote', 'get-url', $remoteName) + } + if (-not $remoteUrl) { + $remoteName = 'origin' + $remoteUrl = Invoke-Git $root @('remote', 'get-url', $remoteName) + } + + return [ordered]@{ + Root = $root + Branch = $branch + Head = $head + Upstream = $upstream + Tags = @($tags -split "`n" | Where-Object { $_ }) + RemoteName = $remoteName + RemoteUrl = $remoteUrl + } +} + +$idaInstallDir = $null +if ($IdaExe -and (Test-Path $IdaExe)) { + $idaInstallDir = Split-Path -Parent $IdaExe +} +$idaVersion = Get-IdaInstallVersion $idaInstallDir + +$resolvedSdk = Resolve-IdaSdkDir $IdaSdk +$sdkVersion = $null +if ($resolvedSdk) { + $sdkVersion = Get-IdaSdkVersion $resolvedSdk +} +$sdkGitInfo = $null +if ($resolvedSdk) { + $sdkGitInfo = Get-GitInfo $resolvedSdk +} + +$idaLibCandidates = @() +if ($resolvedSdk) { + $idaLibCandidates = @( + (Join-Path $resolvedSdk 'lib\x64_win_vc_64\ida.lib'), + (Join-Path $resolvedSdk 'lib\x64_win_64\ida.lib') + ) +} +$idaLibPath = $idaLibCandidates | Where-Object { Test-Path $_ } | Select-Object -First 1 +$versionMatches = $true +if ($ExpectedSdkVersion -gt 0) { + $versionMatches = ($sdkVersion -eq $ExpectedSdkVersion) +} +$idaVersionMatches = $true +if ($ExpectedIdaVersion) { + $idaVersionMatches = ($idaVersion -like "$ExpectedIdaVersion*") +} +$sdkBranchMatches = $true +if ($ExpectedSdkBranch) { + $sdkBranchMatches = $sdkGitInfo -and ( + $sdkGitInfo.Branch -eq $ExpectedSdkBranch -or + $sdkGitInfo.Upstream -eq "origin/$ExpectedSdkBranch" -or + $sdkGitInfo.Upstream -like "*/$ExpectedSdkBranch" + ) +} +$sdkCommitMatches = $true +if ($ExpectedSdkCommit) { + $sdkCommitMatches = $sdkGitInfo -and $sdkGitInfo.Head -and $sdkGitInfo.Head.StartsWith($ExpectedSdkCommit, [StringComparison]::OrdinalIgnoreCase) +} +$sdkTagMatches = $true +if ($ExpectedSdkTag) { + $sdkTagMatches = $sdkGitInfo -and $sdkGitInfo.Tags -contains $ExpectedSdkTag +} +$officialSdk = $false +if ($sdkGitInfo -and $sdkGitInfo.RemoteUrl) { + $officialSdk = $sdkGitInfo.RemoteUrl -match 'github\.com[:/]+HexRaysSA/ida-sdk(\.git)?$' +} +$officialSdkMatches = (-not $RequireOfficialSdk) -or $officialSdk +$sdkVersionGatePasses = $versionMatches +$sdkGateClassification = 'no-sdk-version-required' +if ($ExpectedSdkVersion -gt 0) { + if ($versionMatches) { + $sdkGateClassification = 'exact-sdk-version-match' + } else { + $sdkGateClassification = 'sdk-version-mismatch' + } +} + +$status = [ordered]@{ + IdaExe = $IdaExe + IdaInstallDir = $idaInstallDir + IdaVersion = $idaVersion + ExpectedIdaVersion = $(if ($ExpectedIdaVersion) { $ExpectedIdaVersion } else { $null }) + IdaVersionMatchesExpected = $idaVersionMatches + IdaSdk = $IdaSdk + ResolvedIdaSdk = $resolvedSdk + SdkVersion = $sdkVersion + ExpectedSdkVersion = $(if ($ExpectedSdkVersion -gt 0) { $ExpectedSdkVersion } else { $null }) + SdkVersionMatchesExpected = $versionMatches + SdkVersionGatePasses = $sdkVersionGatePasses + SdkGateClassification = $sdkGateClassification + SdkGitRoot = $(if ($sdkGitInfo) { $sdkGitInfo.Root } else { $null }) + SdkGitBranch = $(if ($sdkGitInfo) { $sdkGitInfo.Branch } else { $null }) + ExpectedSdkBranch = $(if ($ExpectedSdkBranch) { $ExpectedSdkBranch } else { $null }) + SdkBranchMatchesExpected = $sdkBranchMatches + SdkGitHead = $(if ($sdkGitInfo) { $sdkGitInfo.Head } else { $null }) + ExpectedSdkCommit = $(if ($ExpectedSdkCommit) { $ExpectedSdkCommit } else { $null }) + SdkCommitMatchesExpected = $sdkCommitMatches + SdkGitUpstream = $(if ($sdkGitInfo) { $sdkGitInfo.Upstream } else { $null }) + SdkGitTags = $(if ($sdkGitInfo) { $sdkGitInfo.Tags } else { @() }) + ExpectedSdkTag = $(if ($ExpectedSdkTag) { $ExpectedSdkTag } else { $null }) + SdkTagMatchesExpected = $sdkTagMatches + SdkGitRemoteUrl = $(if ($sdkGitInfo) { $sdkGitInfo.RemoteUrl } else { $null }) + RequireOfficialSdk = [bool]$RequireOfficialSdk + IsOfficialHexRaysSdk = $officialSdk + OfficialSdkMatchesExpected = $officialSdkMatches + HasIdaExe = [bool]($IdaExe -and (Test-Path $IdaExe)) + HasInstalledLoader = [bool]($idaInstallDir -and (Test-Path (Join-Path $idaInstallDir 'loaders\idaxex.dll'))) + HasSdkRoot = [bool]($IdaSdk -and (Test-Path $IdaSdk)) + HasSdkHeaders = $false + HasPeHeader = $false + HasIdaLib = $false + IdaLibPath = $idaLibPath + HasCMakeBootstrap = $false + HasCMakePackage = $false + HasIdaCMake = $false + CanBuildIdaxex = $false + CanConfigureCMake = $false +} + +if ($resolvedSdk) { + $status.HasSdkHeaders = Test-Path (Join-Path $resolvedSdk 'include\pro.h') + $status.HasPeHeader = Test-Path (Join-Path $resolvedSdk 'ldr\pe\pe.h') + $status.HasIdaLib = [bool]$idaLibPath + $status.HasCMakeBootstrap = (Test-Path (Join-Path $resolvedSdk 'cmake\bootstrap.cmake')) -or (Test-Path (Join-Path $resolvedSdk 'ida-cmake\bootstrap.cmake')) + $status.HasCMakePackage = Test-Path (Join-Path $resolvedSdk 'cmake\idasdkConfig.cmake') + $status.HasIdaCMake = $status.HasCMakeBootstrap -or $status.HasCMakePackage -or (Test-Path (Join-Path $resolvedSdk 'ida-cmake\common.cmake')) + $status.CanBuildIdaxex = $status.HasSdkHeaders -and $status.HasPeHeader -and $status.HasIdaLib -and $sdkVersionGatePasses -and $idaVersionMatches -and $sdkBranchMatches -and $sdkTagMatches -and $sdkCommitMatches -and $officialSdkMatches + $status.CanConfigureCMake = $status.CanBuildIdaxex -and $status.HasIdaCMake +} + +if ($Json) { + $status | ConvertTo-Json -Depth 3 +} else { + $status.GetEnumerator() | ForEach-Object { + "{0}: {1}" -f $_.Key, $_.Value + } + + if (-not $status.HasSdkRoot) { + Write-Host "" + Write-Host "Set IDASDK to the IDA SDK checkout root or src directory before building." -ForegroundColor Yellow + } elseif (-not $status.CanBuildIdaxex) { + Write-Host "" + Write-Host "IDASDK is set, but the SDK tree is incomplete or not the expected version for loader builds." -ForegroundColor Yellow + } +} + +if ($RequireBuildReady -and -not $status.CanBuildIdaxex) { + exit 1 +} diff --git a/scripts/Test-IdaCorpus.ps1 b/scripts/Test-IdaCorpus.ps1 new file mode 100644 index 0000000..21048d0 --- /dev/null +++ b/scripts/Test-IdaCorpus.ps1 @@ -0,0 +1,190 @@ +param( + [Parameter(Mandatory = $true)] + [string]$IdaExe, + [Parameter(Mandatory = $true)] + [string]$InputRoot, + [Parameter(Mandatory = $true)] + [string]$OutputRoot, + [int]$TimeoutSeconds = 300, + [string]$Magic = 'XEX0;XEX1;XEX2;XEX-;XEX?;XEX%;XBEH', + [string]$ExcludeSHA256 = '', + [int]$MaximumCases = 0, + [switch]$KeepDatabases +) + +$ErrorActionPreference = 'Stop' +$loaderRunner = Join-Path $PSScriptRoot 'Test-IdaLoader.ps1' +$recognizedMagic = @('XEX0', 'XEX1', 'XEX2', 'XEX-', 'XEX?', 'XEX%', 'XBEH') +$inputRoots = @($InputRoot -split ';' | Where-Object { $_ }) +$selectedMagic = @($Magic -split ';' | Where-Object { $_ }) +$excludedHashes = @($ExcludeSHA256 -split ';' | Where-Object { $_ }) + +foreach ($root in $inputRoots) { + if (-not (Test-Path -LiteralPath $root)) { + throw "Input root not found: $root" + } +} + +New-Item -ItemType Directory -Force -Path $OutputRoot | Out-Null +$logRoot = Join-Path $OutputRoot 'logs' +$databaseRoot = Join-Path $OutputRoot 'databases' +New-Item -ItemType Directory -Force -Path $logRoot, $databaseRoot | Out-Null + +$candidates = foreach ($root in $inputRoots) { + Get-ChildItem -LiteralPath $root -Recurse -File | + Where-Object { $_.Extension.ToLowerInvariant() -in @('.xex', '.exe', '.xbe') } | + ForEach-Object { + $stream = [IO.File]::OpenRead($_.FullName) + try { + $header = New-Object byte[] 4 + $bytesRead = $stream.Read($header, 0, 4) + $magic = if ($bytesRead -eq 4) { + [Text.Encoding]::ASCII.GetString($header) + } else { + '' + } + } finally { + $stream.Dispose() + } + + [pscustomobject]@{ + Root = $root + Path = $_.FullName + Bytes = $_.Length + Magic = $magic + SHA256 = (Get-FileHash -LiteralPath $_.FullName -Algorithm SHA256).Hash + } + } +} + +$recognized = @($candidates | Where-Object { + $_.Magic -in $recognizedMagic -and $_.Magic -in $selectedMagic +}) +$uniqueAll = @( + $recognized | + Sort-Object Magic, SHA256, Path | + Group-Object SHA256 | + ForEach-Object { + $canonical = $_.Group | Sort-Object Path | Select-Object -First 1 + [pscustomobject]@{ + Root = $canonical.Root + Path = $canonical.Path + Bytes = $canonical.Bytes + Magic = $canonical.Magic + SHA256 = $canonical.SHA256 + CopyCount = $_.Count + AllPaths = ($_.Group.Path -join ' | ') + } + } | + Sort-Object Magic, SHA256 +) +$excluded = @($uniqueAll | Where-Object { $_.SHA256 -in $excludedHashes }) +$unique = @($uniqueAll | Where-Object { $_.SHA256 -notin $excludedHashes }) +if ($MaximumCases -gt 0) { + $unique = @($unique | Select-Object -First $MaximumCases) +} + +$inventoryPath = Join-Path $OutputRoot 'inventory.csv' +$excludedPath = Join-Path $OutputRoot 'excluded.csv' +$resultsPath = Join-Path $OutputRoot 'results.csv' +$recognized | Sort-Object SHA256, Path | Export-Csv -LiteralPath $inventoryPath -NoTypeInformation +$excluded | Export-Csv -LiteralPath $excludedPath -NoTypeInformation + +$results = [Collections.Generic.List[object]]::new() +for ($index = 0; $index -lt $unique.Count; $index++) { + $item = $unique[$index] + $caseId = '{0:D3}-{1}-{2}' -f ($index + 1), ($item.Magic -replace '[^A-Za-z0-9]', '_'), $item.SHA256.Substring(0, 12) + $logPath = Join-Path $logRoot "$caseId.log" + $runnerPath = Join-Path $logRoot "$caseId.runner.log" + $databasePath = Join-Path $databaseRoot "$caseId.i64" + $started = Get-Date + $exitCode = 0 + $errorMessage = '' + + Write-Host ('[{0}/{1}] {2} {3}' -f ($index + 1), $unique.Count, $item.Magic, $item.Path) + try { + & $loaderRunner ` + -IdaExe $IdaExe ` + -InputFile $item.Path ` + -LogPath $logPath ` + -OutputDatabase $databasePath ` + -TimeoutSeconds $TimeoutSeconds *> $runnerPath + } catch { + $exitCode = 1 + $errorMessage = $_.Exception.Message + } + + $verification = $null + if (Test-Path -LiteralPath $logPath) { + $marker = Get-Content -LiteralPath $logPath | + Where-Object { $_ -like '[[]idaxex-verify[]]*' } | + Select-Object -Last 1 + if ($marker) { + try { + $verification = ($marker -replace '^\[idaxex-verify\]\s*', '') | ConvertFrom-Json + } catch { + $errorMessage = "Unable to parse verification record: $($_.Exception.Message)" + $exitCode = 1 + } + } + } + + if (-not $verification) { + $exitCode = 1 + if (-not $errorMessage) { + $errorMessage = 'IDA did not emit a verification record.' + } + } elseif (-not $verification.passed) { + $exitCode = 1 + $errorMessage = ($verification.errors -join '; ') + } + + $results.Add([pscustomobject]@{ + Case = $caseId + Passed = ($exitCode -eq 0) + Magic = $item.Magic + SHA256 = $item.SHA256 + Bytes = $item.Bytes + CopyCount = $item.CopyCount + Path = $item.Path + AllPaths = $item.AllPaths + FileType = if ($verification) { $verification.file_type } else { '' } + Processor = if ($verification) { $verification.processor } else { '' } + Segments = if ($verification) { $verification.segment_count } else { '' } + LoadedSegments = if ($verification) { $verification.loaded_segment_count } else { '' } + Functions = if ($verification) { $verification.function_count } else { '' } + Entries = if ($verification) { $verification.entry_count } else { '' } + Names = if ($verification) { $verification.name_count } else { '' } + ImportModules = if ($verification) { $verification.import_module_count } else { '' } + Warnings = if ($verification) { $verification.warnings -join '; ' } else { '' } + Error = $errorMessage + Seconds = [Math]::Round(((Get-Date) - $started).TotalSeconds, 1) + LogPath = $logPath + DatabasePath = $databasePath + }) + $results | Export-Csv -LiteralPath $resultsPath -NoTypeInformation + + if (-not $KeepDatabases -and $exitCode -eq 0 -and (Test-Path -LiteralPath $databasePath)) { + Remove-Item -LiteralPath $databasePath -Force + } +} + +$failed = @($results | Where-Object { -not $_.Passed }) +$summary = [pscustomobject]@{ + CandidatePaths = $candidates.Count + RecognizedPaths = $recognized.Count + UniqueRecognizedFiles = $uniqueAll.Count + DuplicatePathsExcluded = $recognized.Count - $uniqueAll.Count + ExplicitlyExcludedFiles = $excluded.Count + SelectedCases = $unique.Count + Passed = $results.Count - $failed.Count + Failed = $failed.Count + Inventory = $inventoryPath + Exclusions = $excludedPath + Results = $resultsPath +} + +$summary | Format-List +if ($failed) { + exit 1 +} diff --git a/scripts/Test-IdaLoader.ps1 b/scripts/Test-IdaLoader.ps1 new file mode 100644 index 0000000..dac22b5 --- /dev/null +++ b/scripts/Test-IdaLoader.ps1 @@ -0,0 +1,90 @@ +param( + [Parameter(Mandatory = $true)] + [string]$IdaExe, + [Parameter(Mandatory = $true)] + [string]$InputFile, + [string]$LogPath, + [string]$OutputDatabase, + [string]$FileType, + [string]$Processor, + [int]$TimeoutSeconds = 180 +) + +if (-not (Test-Path $IdaExe)) { + throw "IDA executable not found: $IdaExe" +} + +if (-not (Test-Path $InputFile)) { + throw "Input file not found: $InputFile" +} + +$repoRoot = Split-Path -Parent $PSScriptRoot +$scriptPath = Join-Path $PSScriptRoot 'Test-IdaLoader.py' +$smokeDir = Join-Path $repoRoot 'smoke' +New-Item -ItemType Directory -Force -Path $smokeDir | Out-Null + +function Quote-IdaArgument { + param([Parameter(Mandatory = $true)][string]$Argument) + + if ($Argument -notmatch '[\s"]') { + return $Argument + } + + '"' + ($Argument -replace '"', '\"') + '"' +} + +if (-not $LogPath) { + $baseName = [IO.Path]::GetFileNameWithoutExtension($InputFile) + $LogPath = Join-Path $smokeDir "$baseName-idat.log" +} + +$baseName = [IO.Path]::GetFileNameWithoutExtension($InputFile) +if (-not $OutputDatabase) { + $OutputDatabase = Join-Path $smokeDir "$baseName.i64" +} + +foreach ($outputPath in @($LogPath, $OutputDatabase)) { + $outputDir = Split-Path -Parent $outputPath + if ($outputDir) { + New-Item -ItemType Directory -Force -Path $outputDir | Out-Null + } + if (Test-Path -LiteralPath $outputPath) { + Remove-Item -LiteralPath $outputPath -Force + } +} + +$arguments = @( + '-A' + '-c' + "-L$LogPath" + "-o$OutputDatabase" + "-S$scriptPath" +) + +if ($FileType) { + $arguments += "-T$FileType" +} + +if ($Processor) { + $arguments += "-p$Processor" +} + +$arguments += $InputFile + +$argumentLine = ($arguments | ForEach-Object { Quote-IdaArgument $_ }) -join ' ' +$process = Start-Process -FilePath $IdaExe -ArgumentList $argumentLine -WindowStyle Hidden -PassThru +if (-not $process.WaitForExit($TimeoutSeconds * 1000)) { + Stop-Process -Id $process.Id -Force + throw "IDA batch smoke test timed out after $TimeoutSeconds seconds." +} + +if ($process.ExitCode -ne 0) { + throw "IDA batch smoke test exited with code $($process.ExitCode). See $LogPath" +} + +Write-Host "OutputDatabase: $OutputDatabase" +if (Test-Path $LogPath) { + Get-Content $LogPath -Tail 200 +} else { + Write-Host "Smoke test exited cleanly, but IDA did not emit a log file." +} diff --git a/scripts/Test-IdaLoader.py b/scripts/Test-IdaLoader.py new file mode 100644 index 0000000..eec8e9d --- /dev/null +++ b/scripts/Test-IdaLoader.py @@ -0,0 +1,113 @@ +import json + +import ida_bytes +import ida_entry +import ida_funcs +import ida_ida +import ida_idaapi +import ida_loader +import ida_name +import ida_nalt +import ida_pro +import ida_segment + + +def segment_for_ea(ea): + return ida_segment.get_segment_ea(ea) != ida_idaapi.BADADDR + + +input_path = ida_nalt.get_input_file_path() +file_type = ida_loader.get_file_type_name() +processor = ida_ida.inf_get_procname() +start_ea = ida_ida.inf_get_start_ea() +errors = [] +warnings = [] +segments = [] +loaded_segments = 0 + +for index in range(ida_segment.get_segm_qty()): + segment = ida_segment.segment_info_t() + if not ida_segment.get_segment_info_by_num( + segment, index, ida_segment.GSI_NAME + ): + errors.append("segment %d is unavailable" % index) + continue + + start = int(segment.start_ea) + end = int(segment.end_ea) + if start >= end: + errors.append("segment %d has an invalid range" % index) + + if segments and start < segments[-1]["end_ea"]: + errors.append("segment %d overlaps the preceding segment" % index) + + loaded = ida_bytes.is_loaded(start) + if loaded: + loaded_segments += 1 + + segments.append( + { + "index": index, + "name": segment.get_name(), + "start_ea": start, + "end_ea": end, + "size": end - start, + "permissions": int(segment.get_perm()), + "start_loaded": bool(loaded), + } + ) + +function_count = ida_funcs.get_func_qty() +orphan_functions = 0 +for index in range(function_count): + function_ea = ida_funcs.get_func_ea_by_num(index) + if function_ea == ida_idaapi.BADADDR or not segment_for_ea(function_ea): + orphan_functions += 1 + +entry_count = ida_entry.get_entry_qty() +orphan_entries = 0 +for index in range(entry_count): + ordinal = ida_entry.get_entry_ordinal(index) + ea = ida_entry.get_entry(ordinal) + if ea != ida_idaapi.BADADDR and not segment_for_ea(ea): + orphan_entries += 1 + +if not file_type.startswith("Xbox"): + errors.append("unexpected file type: %s" % file_type) +if processor.upper() != "PPC": + errors.append("unexpected processor: %s" % processor) +if not segments: + errors.append("loader created no segments") +if segments and not loaded_segments: + errors.append("no segment begins with loaded data") +if start_ea != ida_idaapi.BADADDR and not segment_for_ea(start_ea): + errors.append("start address is outside all segments") +if orphan_functions: + errors.append("%d functions begin outside all segments" % orphan_functions) +if orphan_entries: + errors.append("%d entry points are outside all segments" % orphan_entries) +if function_count == 0: + warnings.append("database contains no functions") +if entry_count == 0: + warnings.append("database contains no entry points") + +result = { + "schema": 1, + "input": input_path, + "file_type": file_type, + "processor": processor, + "start_ea": int(start_ea), + "segment_count": len(segments), + "loaded_segment_count": loaded_segments, + "function_count": function_count, + "entry_count": entry_count, + "name_count": ida_name.get_nlist_size(), + "import_module_count": int(ida_nalt.get_import_module_qty()), + "segments": segments, + "warnings": warnings, + "errors": errors, + "passed": not errors, +} + +print("[idaxex-verify] %s" % json.dumps(result, sort_keys=True)) +ida_pro.qexit(0 if result["passed"] else 1) diff --git a/xex1tool.cpp b/xex1tool.cpp index e332c52..f08266b 100644 --- a/xex1tool.cpp +++ b/xex1tool.cpp @@ -143,7 +143,7 @@ void PrintInfo(XEXFile& xex, bool print_mem_pages) break; case MAGIC_XEX3F: exe_type = "XEX3F ('XEX?')"; - exe_versions = ">=1434"; + exe_versions = ">=1529"; break; case MAGIC_XEX0: exe_type = "XEX0"; From f37c6831946e3d1d45e6d0ca0a3027c3c57b6b2f Mon Sep 17 00:00:00 2001 From: SaveEditors Date: Thu, 23 Jul 2026 18:28:21 -0400 Subject: [PATCH 2/3] xex3f: restore >=1434 min build in format label --- idaloader.cpp | 2 +- xex1tool.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/idaloader.cpp b/idaloader.cpp index c26cd89..c35d34e 100644 --- a/idaloader.cpp +++ b/idaloader.cpp @@ -739,7 +739,7 @@ static int idaapi accept_file( else if (magic == MAGIC_XEX3F) { valid = 1; - *fileformatname = "Xbox360 XEX?/XEX3F File (>=1529)"; + *fileformatname = "Xbox360 XEX?/XEX3F File (>=1434)"; } else if (magic == MAGIC_XEX0) { diff --git a/xex1tool.cpp b/xex1tool.cpp index f08266b..e332c52 100644 --- a/xex1tool.cpp +++ b/xex1tool.cpp @@ -143,7 +143,7 @@ void PrintInfo(XEXFile& xex, bool print_mem_pages) break; case MAGIC_XEX3F: exe_type = "XEX3F ('XEX?')"; - exe_versions = ">=1529"; + exe_versions = ">=1434"; break; case MAGIC_XEX0: exe_type = "XEX0"; From c86402d53885fd796110a0c1e9a6a14672d2b742 Mon Sep 17 00:00:00 2001 From: SaveEditors Date: Thu, 13 Aug 2026 14:31:07 -0400 Subject: [PATCH 3/3] fix: complete post-rebase integration --- CMakeLists.txt | 84 ++++++++++++++++++++++++--------------- formats/xex.cpp | 2 - scripts/Test-IdaLoader.py | 62 +++++++++++++++++++++++------ xex1tool.cpp | 59 +++++++++++++++------------ xex1tool/CMakeLists.txt | 29 +++++++++++++- 5 files changed, 165 insertions(+), 71 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index a288759..91fb0a1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -40,30 +40,38 @@ endif() find_package(idasdk REQUIRED) +set(IDAXEX_FIRST_PARTY_SOURCES + idaloader.cpp + idaloader_xbe.cpp + namegen.cpp + namegen_xtlid.cpp + formats/xbe.cpp + formats/xex.cpp +) + +set(IDAXEX_THIRD_PARTY_SOURCES + 3rdparty/excrypt/src/excrypt_aes.c + 3rdparty/excrypt/src/rijndael.c + 3rdparty/excrypt/src/excrypt_sha.c + 3rdparty/lzx.cpp + 3rdparty/mspack/lzxd.c + 3rdparty/mspack/system.c + 3rdparty/XbSymbolDatabase/src/lib/libXbSymbolDatabase.c + 3rdparty/XbSymbolDatabase/src/OOVPADatabase/D3D8_OOVPA.c + 3rdparty/XbSymbolDatabase/src/OOVPADatabase/D3D8LTCG_OOVPA.c + 3rdparty/XbSymbolDatabase/src/OOVPADatabase/DSound_OOVPA.c + 3rdparty/XbSymbolDatabase/src/OOVPADatabase/JVS_OOVPA.c + 3rdparty/XbSymbolDatabase/src/OOVPADatabase/XActEng_OOVPA.c + 3rdparty/XbSymbolDatabase/src/OOVPADatabase/Xapi_OOVPA.c + 3rdparty/XbSymbolDatabase/src/OOVPADatabase/XGraphic_OOVPA.c + 3rdparty/XbSymbolDatabase/src/OOVPADatabase/XNet_OOVPA.c + 3rdparty/XbSymbolDatabase/src/OOVPADatabase/XOnline_OOVPA.c +) + ida_add_loader(idaxex SOURCES - idaloader.cpp - idaloader_xbe.cpp - namegen.cpp - namegen_xtlid.cpp - formats/xbe.cpp - formats/xex.cpp - 3rdparty/excrypt/src/excrypt_aes.c - 3rdparty/excrypt/src/rijndael.c - 3rdparty/excrypt/src/excrypt_sha.c - 3rdparty/lzx.cpp - 3rdparty/mspack/lzxd.c - 3rdparty/mspack/system.c - 3rdparty/XbSymbolDatabase/src/lib/libXbSymbolDatabase.c - 3rdparty/XbSymbolDatabase/src/OOVPADatabase/D3D8_OOVPA.c - 3rdparty/XbSymbolDatabase/src/OOVPADatabase/D3D8LTCG_OOVPA.c - 3rdparty/XbSymbolDatabase/src/OOVPADatabase/DSound_OOVPA.c - 3rdparty/XbSymbolDatabase/src/OOVPADatabase/JVS_OOVPA.c - 3rdparty/XbSymbolDatabase/src/OOVPADatabase/XActEng_OOVPA.c - 3rdparty/XbSymbolDatabase/src/OOVPADatabase/Xapi_OOVPA.c - 3rdparty/XbSymbolDatabase/src/OOVPADatabase/XGraphic_OOVPA.c - 3rdparty/XbSymbolDatabase/src/OOVPADatabase/XNet_OOVPA.c - 3rdparty/XbSymbolDatabase/src/OOVPADatabase/XOnline_OOVPA.c + ${IDAXEX_FIRST_PARTY_SOURCES} + ${IDAXEX_THIRD_PARTY_SOURCES} INCLUDES 3rdparty/excrypt/src 3rdparty/XbSymbolDatabase/include @@ -81,16 +89,30 @@ target_compile_options(idaxex PRIVATE $<$:-Wno-non-pod-varargs>) if(IDAXEX_STRICT_WARNINGS) - target_compile_options(idaxex PRIVATE - $<$:/W4> - $<$:/wd4201> - $<$:/W4> - $<$:/wd4201> - $<$:-Wall> - $<$:-Wextra> - $<$:-Wall> - $<$:-Wextra> + # ida_add_loader() suppresses warnings for the complete mixed-source target. + # Remove that target-wide switch, then enable strict checks only for code + # maintained in this repository. Vendored sources keep their own warning + # policy and cannot obscure new first-party warnings. + get_target_property(_idaxex_compile_options idaxex COMPILE_OPTIONS) + if(_idaxex_compile_options) + list(FILTER _idaxex_compile_options EXCLUDE REGEX "^(/w|/W0|-w)$") + set_property(TARGET idaxex PROPERTY COMPILE_OPTIONS + "${_idaxex_compile_options}") + endif() + + set_property(SOURCE ${IDAXEX_FIRST_PARTY_SOURCES} APPEND PROPERTY + COMPILE_OPTIONS + $<$:/W4> + $<$:/wd4201> + $<$:-Wall> + $<$:-Wextra> + ) + set_property(SOURCE ${IDAXEX_THIRD_PARTY_SOURCES} APPEND PROPERTY + COMPILE_OPTIONS + $<$:/W0> + $<$>:-w> ) + unset(_idaxex_compile_options) else() ida_disable_warnings(idaxex) endif() diff --git a/formats/xex.cpp b/formats/xex.cpp index c983c68..98b3d2c 100644 --- a/formats/xex.cpp +++ b/formats/xex.cpp @@ -501,9 +501,7 @@ bool XEXFile::read_imports(void* file) // Reads function info defined inside XEX export table bool XEXFile::read_exports(void* file) { -#ifdef IDALDR (void)file; -#endif uint32_t exports_va = security_info_.ImageInfo.ExportTableAddress; if (xex_header_.Magic == MAGIC_XEX1 && directory_entries_.count(XEX_HEADER_EXPORTS_XEX1)) exports_va = directory_entries_[XEX_HEADER_EXPORTS_XEX1]; diff --git a/scripts/Test-IdaLoader.py b/scripts/Test-IdaLoader.py index eec8e9d..b0c73c6 100644 --- a/scripts/Test-IdaLoader.py +++ b/scripts/Test-IdaLoader.py @@ -13,7 +13,44 @@ def segment_for_ea(ea): - return ida_segment.get_segment_ea(ea) != ida_idaapi.BADADDR + if hasattr(ida_segment, "get_segment_ea"): + return ida_segment.get_segment_ea(ea) != ida_idaapi.BADADDR + return ida_segment.getseg(ea) is not None + + +def segment_details(index): + if hasattr(ida_segment, "segment_info_t"): + segment = ida_segment.segment_info_t() + if not ida_segment.get_segment_info_by_num( + segment, index, ida_segment.GSI_NAME + ): + return None + + return { + "name": segment.get_name(), + "start_ea": int(segment.start_ea), + "end_ea": int(segment.end_ea), + "permissions": int(segment.get_perm()), + } + + segment = ida_segment.getnseg(index) + if segment is None: + return None + + return { + "name": ida_segment.get_segm_name(segment), + "start_ea": int(segment.start_ea), + "end_ea": int(segment.end_ea), + "permissions": int(segment.perm), + } + + +def function_ea_by_num(index): + if hasattr(ida_funcs, "get_func_ea_by_num"): + return ida_funcs.get_func_ea_by_num(index) + + function = ida_funcs.getn_func(index) + return ida_idaapi.BADADDR if function is None else function.start_ea input_path = ida_nalt.get_input_file_path() @@ -26,15 +63,13 @@ def segment_for_ea(ea): loaded_segments = 0 for index in range(ida_segment.get_segm_qty()): - segment = ida_segment.segment_info_t() - if not ida_segment.get_segment_info_by_num( - segment, index, ida_segment.GSI_NAME - ): + segment = segment_details(index) + if segment is None: errors.append("segment %d is unavailable" % index) continue - start = int(segment.start_ea) - end = int(segment.end_ea) + start = segment["start_ea"] + end = segment["end_ea"] if start >= end: errors.append("segment %d has an invalid range" % index) @@ -48,11 +83,11 @@ def segment_for_ea(ea): segments.append( { "index": index, - "name": segment.get_name(), + "name": segment["name"], "start_ea": start, "end_ea": end, "size": end - start, - "permissions": int(segment.get_perm()), + "permissions": segment["permissions"], "start_loaded": bool(loaded), } ) @@ -60,7 +95,7 @@ def segment_for_ea(ea): function_count = ida_funcs.get_func_qty() orphan_functions = 0 for index in range(function_count): - function_ea = ida_funcs.get_func_ea_by_num(index) + function_ea = function_ea_by_num(index) if function_ea == ida_idaapi.BADADDR or not segment_for_ea(function_ea): orphan_functions += 1 @@ -74,8 +109,11 @@ def segment_for_ea(ea): if not file_type.startswith("Xbox"): errors.append("unexpected file type: %s" % file_type) -if processor.upper() != "PPC": - errors.append("unexpected processor: %s" % processor) +expected_processor = "metapc" if file_type.startswith("Xbox XBE") else "PPC" +if processor.upper() != expected_processor.upper(): + errors.append( + "unexpected processor: %s (expected %s)" % (processor, expected_processor) + ) if not segments: errors.append("loader created no segments") if segments and not loaded_segments: diff --git a/xex1tool.cpp b/xex1tool.cpp index e332c52..0a5cb6d 100644 --- a/xex1tool.cpp +++ b/xex1tool.cpp @@ -109,7 +109,6 @@ void PrintImports(XEXFile& xex) { for (auto& imp : lib.second) { auto imp_name = DoNameGen(libname, imp.first, version); - auto imp_addr = imp.second.ThunkAddr; printf(" %3d) %s\n", imp.first, imp_name.c_str()); } @@ -729,8 +728,8 @@ void PrintInfo(XEXFile& xex, bool print_mem_pages) auto* title_ids = xex.opt_header_ptr(XEX_HEADER_ALTERNATE_TITLE_IDS); if (title_ids) { - uint32_t size = xe::byte_swap(*title_ids); - uint32_t count = (size - 4) / sizeof(uint32_t); + uint32_t title_ids_size = xe::byte_swap(*title_ids); + uint32_t count = (title_ids_size - 4) / sizeof(uint32_t); if (count > 0) { printf("\nAlternate Title Ids\n"); @@ -857,7 +856,7 @@ void PrintInfo(XEXFile& xex, bool print_mem_pages) uint32_t address = xex.base_address(); for (auto page : page_descriptors) { - auto size = page.Size * page_size; + auto page_byte_size = page.Size * page_size; auto details = "Data"; if (page.Info & xex::PageInfoFlag_NoWrite) { @@ -867,8 +866,9 @@ void PrintInfo(XEXFile& xex, bool print_mem_pages) details = "Code"; } - printf(" %3d) %08X - %08X : %s\n", i, address, address + size, details); - address += size; + printf(" %3d) %08X - %08X : %s\n", i, address, + address + page_byte_size, details); + address += page_byte_size; i++; } } @@ -915,13 +915,13 @@ int main(int argc, char* argv[]) printf("Reading and parsing input XEX file...\n"); - FILE* file; - auto res = fopen_s(&file, filepath.c_str(), "rb"); + FILE* file = nullptr; + int input_open_result = fopen_s(&file, filepath.c_str(), "rb"); - if (!file) + if (input_open_result != 0 || !file) { printf("Error opening XEX file %s\n", filepath.c_str()); - return 0; + return input_open_result != 0 ? input_open_result : 1; } XEXFile xex; @@ -931,6 +931,7 @@ int main(int argc, char* argv[]) if (!loadresult) { printf("Error %d while loading XEX file %s\n", xex.load_error(), filepath.c_str()); + fclose(file); return xex.load_error(); } @@ -957,23 +958,23 @@ int main(int argc, char* argv[]) if (xex.header().Magic != MAGIC_XEX2) printf("XEX isn't XEX2, addresses might not be correct!\n"); - uint32_t result = 0; + uint32_t converted_address = 0; if (rva >= xex.base_address()) { - result = xex.xex_va_to_offset(rva); + converted_address = xex.xex_va_to_offset(rva); printf("Virtual Address -> File Offset\n"); printf("Virtual Addr: 0x%X\n", rva); - printf("File Offset: 0x%X\n", result); + printf("File Offset: 0x%X\n", converted_address); } else { - result = xex.xex_offset_to_va(rva); + converted_address = xex.xex_offset_to_va(rva); printf("File Offset -> Virtual Address\n"); printf("File Offset: 0x%X\n", rva); - printf("Virtual Addr: 0x%X\n", result); + printf("Virtual Addr: 0x%X\n", converted_address); } - if (!result) + if (!converted_address) { printf("\nThe given address was unable to be converted, either:\n"); printf("- The given number is invalid\n"); @@ -989,10 +990,11 @@ int main(int argc, char* argv[]) if (result.count("b")) { auto& basefile = result["b"].as(); - FILE* output; - auto res = fopen_s(&output, basefile.c_str(), "wb"); - if (res != 0 || !output) { - printf("Error %d opening basefile %s for write\n", res, basefile.c_str()); + FILE* output = nullptr; + int basefile_open_result = fopen_s(&output, basefile.c_str(), "wb"); + if (basefile_open_result != 0 || !output) { + printf("Error %d opening basefile %s for write\n", + basefile_open_result, basefile.c_str()); } else { fwrite(xex.pe_data(), 1, xex.pe_data_length(), output); @@ -1037,9 +1039,12 @@ int main(int argc, char* argv[]) dumped_names.push_back(sectname); std::filesystem::path res_path = dump_path / sectname; - FILE* file; - if (auto res = fopen_s(&file, res_path.string().c_str(), "wb") != 0 || !file) { - printf("Error %d opening file %s for writing\n", res, res_path.string().c_str()); + FILE* resource_file = nullptr; + int resource_open_result = fopen_s( + &resource_file, res_path.string().c_str(), "wb"); + if (resource_open_result != 0 || !resource_file) { + printf("Error %d opening file %s for writing\n", + resource_open_result, res_path.string().c_str()); } else { @@ -1048,8 +1053,9 @@ int main(int argc, char* argv[]) addr = section.PointerToRawData; auto* data = xex.pe_data() + addr; - fwrite(data, 1, std::min(section.SizeOfRawData, section.VirtualSize), file); - fclose(file); + fwrite(data, 1, std::min(section.SizeOfRawData, section.VirtualSize), + resource_file); + fclose(resource_file); printf("Extracted resource %.8s to %s\n", section.Name, res_path.string().c_str()); } } @@ -1061,4 +1067,7 @@ int main(int argc, char* argv[]) if (result["l"].as() || result["m"].as()) PrintInfo(xex, result["m"].as()); + + fclose(file); + return 0; } diff --git a/xex1tool/CMakeLists.txt b/xex1tool/CMakeLists.txt index 8e53c18..5b3c85d 100644 --- a/xex1tool/CMakeLists.txt +++ b/xex1tool/CMakeLists.txt @@ -9,12 +9,17 @@ endif() set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) +option(IDAXEX_STRICT_WARNINGS + "Enable strict compiler warnings for compatibility validation" OFF) -add_executable(xex1tool +set(XEX1TOOL_FIRST_PARTY_SOURCES ../xex1tool.cpp ../namegen.cpp ../formats/xdbf.cpp ../formats/xex.cpp +) + +set(XEX1TOOL_THIRD_PARTY_SOURCES ../3rdparty/excrypt/src/excrypt_aes.c ../3rdparty/excrypt/src/rijndael.c ../3rdparty/excrypt/src/excrypt_bn.c @@ -29,6 +34,11 @@ add_executable(xex1tool ../3rdparty/mspack/system.c ) +add_executable(xex1tool + ${XEX1TOOL_FIRST_PARTY_SOURCES} + ${XEX1TOOL_THIRD_PARTY_SOURCES} +) + target_include_directories(xex1tool PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/../3rdparty/excrypt/src") @@ -51,3 +61,20 @@ endif() # target suppresses the same warning. target_compile_options(xex1tool PRIVATE $<$:-Wno-non-pod-varargs>) + +if(IDAXEX_STRICT_WARNINGS) + set_property(SOURCE ${XEX1TOOL_FIRST_PARTY_SOURCES} APPEND PROPERTY + COMPILE_OPTIONS + $<$:/W4> + $<$:/wd4201> + $<$:/wd4127> + $<$:/wd4702> + $<$:-Wall> + $<$:-Wextra> + ) + set_property(SOURCE ${XEX1TOOL_THIRD_PARTY_SOURCES} APPEND PROPERTY + COMPILE_OPTIONS + $<$:/W0> + $<$>:-w> + ) +endif()