mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-10-01 14:58:14 +00:00
Boot the VSH on every firmware, 1.50 through 6.61
Backwards from 5.01, one release at a time, against all 39 versions that ship on a disc plus the download-only 6.61. Nothing here is an offset - it's almost entirely Sony renumbering the kernel *_driver NIDs, which sends an import we mean to HLE into the real firmware module instead. Four more NIDs each for sceRtc_driver/sceRtcSetAlarmTick and sceHprm_driver/sceHprmReadLatch, covering 1.50 up. The rtc one is what parked every thread on a SceSysconSync semaphore; the hprm one runs once a frame, so unresolved it was most of the boot log. Also sceImposeGetParam/sceImposeChanges (1.50 - 2.xx) and sceKernelLoadModuleVSH (1.x, which is how the shell loads its own plugins - unresolved it got module id 0 and StartModule failed). sceRtcIsAlarmed had to be implemented too; it returns 0, as in JPCSP. As a null entry it returned LIBRARY_NOT_YET_LINKED, and the 3.0x-3.5x VSH read that as "ask the hardware instead" and went back to blocking on syscon. Two structural findings: - Up to 4.05, scePaf's heap allocator is a separate heaparea1.prx that paf imports as scePafHeaparea. Load it when it's there. Its pool pointer needs the same pre-fill paf's does, at gp - 0x7FCC rather than gp - 0x7E88. - 1.50's vshmain.prx declares no module attributes at all - PSP_MODULE_VSH_MODE only appears from 1.52 - so the whole VSH bootstrap was being skipped. Accept the module name too. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
1 parent
480c5e44e2
commit
63eb3d5b21
6 files changed
+178
-80
No files matched your search
@@ -100,6 +100,11 @@ const HLEFunction sceHprm_driver[] =
|
||||
{0X5FC5E53B, &WrapU_U<sceHprmReadLatch>, "sceHprmReadLatch", 'x', "x"}, // 6.00 - 6.20
|
||||
{0X748FC3C8, &WrapU_V<sceHprm_driver_DC895B2B>, "sceHprm_driver_DC895B2B", 'x', ""}, // 6.31 - 6.39
|
||||
{0X605DEA7A, &WrapU_U<sceHprmReadLatch>, "sceHprmReadLatch", 'x', "x"}, // 5.03 - 5.55
|
||||
{0XA6E8D4F0, &WrapU_U<sceHprmReadLatch>, "sceHprmReadLatch", 'x', "x"}, // 3.95 - 4.05
|
||||
{0X8C728076, &WrapU_U<sceHprmReadLatch>, "sceHprmReadLatch", 'x', "x"}, // 3.72 - 3.90
|
||||
{0XF0AA1FB9, &WrapU_U<sceHprmReadLatch>, "sceHprmReadLatch", 'x', "x"}, // 3.71
|
||||
// Same story as sceRtc_driver: 3.51 and older export it to kernel mode under the user-mode NID.
|
||||
{0X40D2F9F0, &WrapU_U<sceHprmReadLatch>, "sceHprmReadLatch", 'x', "x"}, // 1.50 - 3.51
|
||||
};
|
||||
|
||||
void Register_sceHprm_driver()
|
||||
|
||||
@@ -262,6 +262,15 @@ const HLEFunction sceImpose_driver[] = {
|
||||
{0XBB12F974, &WrapI_I<sceImposeSetStatus>, "sceImposeSetStatus", 'i', "i" },
|
||||
// The 6.60 name for the same call the user-mode module exports as 0x8C943191.
|
||||
{0X5557F4E2, &WrapU_UU<sceImposeGetBatteryIconStatus>, "sceImposeGetBatteryIconStatus", 'x', "xx"},
|
||||
// The 1.50 - 2.xx NIDs for the same two calls - impose.prx of that era exports them to kernel
|
||||
// mode only, with no user-mode alias to match them against, so these were identified from the
|
||||
// function bodies: GetParam is the same dispatch on a0 returning 0x8000xxxx for a bad index,
|
||||
// and Changes is the same read-and-clear of one word in the impose context (at +0x84 there,
|
||||
// +0xBC by 6.60). The VSH calls Changes once a frame, so unresolved they were most of the
|
||||
// boot log on those versions.
|
||||
// NOTE: new entries go at the end - the syscall opcode in a savestate is an index into this array.
|
||||
{0X531C9778, &WrapI_I<sceImposeGetParam>, "sceImposeGetParam", 'i', "i" },
|
||||
{0XB415FC59, &WrapI_V<sceImposeChanges>, "sceImposeChanges", 'i', "" },
|
||||
};
|
||||
|
||||
void Register_sceImpose_driver() {
|
||||
|
||||
@@ -1228,6 +1228,15 @@ static void LoadAndStartVshKernelModules() {
|
||||
LoadAndStartVshKernelModule(ResolveVshModelModule(path).c_str(), nullptr);
|
||||
}
|
||||
|
||||
// Firmwares up to about 4.05 keep scePaf's heap allocator in a module of its own, which paf
|
||||
// imports as scePafHeaparea and can't allocate a single byte without. 5.01 and later compiled
|
||||
// it into paf.prx and dropped the module, so this is absent (and unwanted) on those - hence
|
||||
// the existence check rather than a warning from the loader. heaparea1 and heaparea2 are the
|
||||
// same code with different compiled-in pool sizes; the first is the one the shell asks for.
|
||||
if (pspFileSystem.GetFileInfo("flash0:/vsh/module/heaparea1.prx").exists) {
|
||||
LoadAndStartVshKernelModule("flash0:/vsh/module/heaparea1.prx", nullptr);
|
||||
}
|
||||
|
||||
static const char *const vshUiKernelModulePaths[] = {
|
||||
"flash0:/kd/vshbridge.prx",
|
||||
"flash0:/vsh/module/paf.prx",
|
||||
@@ -1536,8 +1545,11 @@ static PSPModule *__KernelLoadELFFromPtr(const u8 *ptr, size_t elfSize, u32 load
|
||||
|
||||
module->nm.nsegment = reader.GetNumSegments();
|
||||
module->nm.attribute = modinfo->moduleAttrs;
|
||||
if ((module->nm.attribute & PSP_MODULE_VSH_MODE) != 0) {
|
||||
// Used by the PSP's Visual Shell (VSH/XMB) and modules it loads, such as vshmain.prx.
|
||||
// Used by the PSP's Visual Shell (VSH/XMB) and modules it loads, such as vshmain.prx. The
|
||||
// name check is for firmware 1.50, whose vshmain.prx declares no attributes at all - Sony
|
||||
// only started setting PSP_MODULE_VSH_MODE in 1.52. Without it the whole VSH bootstrap
|
||||
// below was skipped and the shell ran with none of its support modules loaded.
|
||||
if ((module->nm.attribute & PSP_MODULE_VSH_MODE) != 0 || equals(modinfo->name, "vsh_module")) {
|
||||
g_runningVSH = true;
|
||||
INFO_LOG(Log::sceModule, "VSH mode module detected: %s", modinfo->name);
|
||||
}
|
||||
@@ -1567,30 +1579,43 @@ static PSPModule *__KernelLoadELFFromPtr(const u8 *ptr, size_t elfSize, u32 load
|
||||
module->nm.gp_value = modinfo->gp;
|
||||
strncpy(module->nm.name, modinfo->name, ARRAY_SIZE(module->nm.name));
|
||||
|
||||
if (equals(module->nm.name, "scePaf_Module")) {
|
||||
// scePaf's own heap allocator expects a real memory-pool base address to already be
|
||||
// patched into this BSS slot before any of its code runs. Real hardware's loader (or
|
||||
// an early kernel init step) apparently does this - checked all 27 of scePaf's
|
||||
// exported data vars, this address isn't one of them, so it's not the normal NID
|
||||
// var-import linking path. Without this, offsets from scePaf's internal
|
||||
// bump-allocator get used directly as absolute pointers, crashing almost immediately
|
||||
// when a client module (e.g. vsh_module) makes its first heap allocation.
|
||||
// See docs/VSHBootInvestigation.md for the full investigation.
|
||||
//
|
||||
// The slot is the second of the two pool pointers scePaf's own init fills in with
|
||||
// sceKernelTryAllocateFpl. Its offset from the module base moves with every build, but
|
||||
// it sits at a fixed distance below gp in all of them - checked against the paf.prx of
|
||||
// 6.00, 6.20, 6.31, 6.37, 6.39, 6.60 and 6.61, where the base-relative offset ranges
|
||||
// over 0x18CCD8..0x18D728 and gp - slot is 0x7E88 every time.
|
||||
const u32 scePafHeapArenaGpOffset = 0x7E88;
|
||||
u32 scePafHeapArenaSize = 0x00850000; // Matches scePaf's own compiled-in default heap size.
|
||||
// scePaf's heap allocator expects a real memory-pool base address to already be in one of its
|
||||
// BSS slots before any of its code runs. The module that owns the allocator fills that slot in
|
||||
// itself, from its own module_start - but that start thread hasn't been scheduled yet when
|
||||
// vshmain makes its first allocation, so the pointer is still null and the shell writes
|
||||
// through it. Real hardware's kernel bootstrap starts these modules one at a time and waits;
|
||||
// we can't, so pre-fill the slot with a real block instead. Without this the boot dies almost
|
||||
// immediately, either on a null write inside scePaf (5.01+) or on vshmain storing the null the
|
||||
// allocator handed back (up to 4.05). See docs/VSHBootInvestigation.md for the investigation.
|
||||
//
|
||||
// Which module owns it moved: up to about 4.05 the allocator is a separate heaparea1.prx, and
|
||||
// from 5.01 it's compiled into paf.prx. Either way the slot is the second of the two pool
|
||||
// pointers that module's init fills in with sceKernelTryAllocateFpl, and either way its offset
|
||||
// from the module base moves with every build while its offset from gp does not - checked
|
||||
// against paf.prx on 6.00, 6.20, 6.31, 6.37, 6.39, 6.60 and 6.61 (base-relative 0x18CCD8 to
|
||||
// 0x18D728, gp - slot 0x7E88 every time) and heaparea1.prx on 3.95 and 4.05.
|
||||
struct PafHeapOwner {
|
||||
const char *moduleName;
|
||||
u32 poolPointerGpOffset;
|
||||
};
|
||||
static const PafHeapOwner pafHeapOwners[] = {
|
||||
{ "scePaf_Module", 0x7E88 },
|
||||
{ "scePafHeaparea_Module", 0x7FCC },
|
||||
};
|
||||
for (const PafHeapOwner &owner : pafHeapOwners) {
|
||||
if (!equals(module->nm.name, owner.moduleName)) {
|
||||
continue;
|
||||
}
|
||||
// Matches the compiled-in default pool size in both modules.
|
||||
u32 scePafHeapArenaSize = 0x00850000;
|
||||
u32 arenaAddr = userMemory.Alloc(scePafHeapArenaSize, false, "scePafHeapArena");
|
||||
u32 patchAddr = module->nm.gp_value - scePafHeapArenaGpOffset;
|
||||
u32 patchAddr = module->nm.gp_value - owner.poolPointerGpOffset;
|
||||
if (arenaAddr != (u32)-1 && Memory::IsValid4AlignedAddress(patchAddr)) {
|
||||
Memory::WriteUnchecked_U32(arenaAddr, patchAddr);
|
||||
} else {
|
||||
WARN_LOG(Log::sceModule, "Failed to patch scePaf heap arena pointer");
|
||||
WARN_LOG(Log::sceModule, "Failed to patch %s heap arena pointer", owner.moduleName);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (equals(module->nm.name, "vsh_module")) {
|
||||
@@ -3163,6 +3188,12 @@ const HLEFunction ModuleMgrForKernel[] = {
|
||||
{0xD675EBB8, &WrapU_UUU<sceKernelSelfStopUnloadModule>, "sceKernelSelfStopUnloadModule", 'x', "xxx", HLE_KERNEL_SYSCALL },
|
||||
{0xD5DDAB1F, &WrapU_CUU<sceKernelLoadModuleVSH>, "sceKernelLoadModuleVSH", 'x', "sxx", HLE_KERNEL_SYSCALL },
|
||||
{0xD86DD11B, &WrapU_C<sceKernelSearchModuleByName>, "sceKernelSearchModuleByName", 'x', "s", HLE_KERNEL_SYSCALL },
|
||||
// The 1.x NID for sceKernelLoadModuleVSH - same function, matched by its callee set in
|
||||
// modulemgr.prx (sceKernelIsIntrContext, sceIoOpen/Ioctl/Close, sceKernelGetUserLevel).
|
||||
// This is how the VSH loads its own plugins, so leaving it unresolved meant vshmain got
|
||||
// module id 0 back and the sceKernelStartModule after it failed with UNKNOWN_MODULE.
|
||||
// NOTE: new entries go at the end - the syscall opcode in a savestate is an index into this array.
|
||||
{0xA4370E7C, &WrapU_CUU<sceKernelLoadModuleVSH>, "sceKernelLoadModuleVSH", 'x', "sxx", HLE_KERNEL_SYSCALL },
|
||||
};
|
||||
|
||||
void Register_ModuleMgrForUser() {
|
||||
|
||||
+15
-1
@@ -946,6 +946,14 @@ static int sceRtcSetAlarmTick(u32 unknown1, u32 unknown2) {
|
||||
return hleLogError(Log::sceRtc, 0, "UNIMPL");
|
||||
}
|
||||
|
||||
// "Has the RTC alarm fired?" - we don't model one and sceRtcSetAlarmTick above is a no-op, so the
|
||||
// answer is always no. Same as JPCSP, which returns a bare 0. Left unimplemented this returned
|
||||
// SCE_KERNEL_ERROR_LIBRARY_NOT_YET_LINKED, and the 3.0x-3.5x VSH took that as "ask the hardware
|
||||
// instead" and blocked forever on a syscon reply.
|
||||
static int sceRtcIsAlarmed() {
|
||||
return hleLogDebug(Log::sceRtc, 0);
|
||||
}
|
||||
|
||||
// Real signature per uofw (sceRtc_C2DDBEB5, src/kd/rtc/rtc.c): s32 sceRtcGetAlarmTick(u64 *tick).
|
||||
// PPSSPP doesn't track a real hardware RTC alarm, so there's nothing meaningful to report -
|
||||
// but leaving this fully unimplemented (nullptr in the function table) meant callers got back
|
||||
@@ -1143,7 +1151,7 @@ const HLEFunction sceRtc[] =
|
||||
{0X203CEB0D, &WrapI_U<sceRtcGetLastReincarnatedTime>, "sceRtcGetLastReincarnatedTime", 'i', "x" },
|
||||
{0X7D1FBED3, &WrapI_UU<sceRtcSetAlarmTick>, "sceRtcSetAlarmTick", 'i', "xx" },
|
||||
{0XF5FCC995, nullptr, "sceRtcGetCurrentNetworkTick", '?', "" },
|
||||
{0X81FCDA34, nullptr, "sceRtcIsAlarmed", '?', "" },
|
||||
{0X81FCDA34, &WrapI_V<sceRtcIsAlarmed>, "sceRtcIsAlarmed", 'i', "" },
|
||||
{0XFB3B18CD, nullptr, "sceRtcRegisterCallback", '?', "" },
|
||||
{0X6A676D2D, nullptr, "sceRtcUnregisterCallback", '?', "" },
|
||||
{0XC2DDBEB5, &WrapI_U<sceRtcGetAlarmTick>, "sceRtcGetAlarmTick", 'i', "x" },
|
||||
@@ -1173,6 +1181,12 @@ const HLEFunction sceRtc_driver[] = {
|
||||
{0X54B9C589, &WrapI_UU<sceRtcSetAlarmTick>, "sceRtcSetAlarmTick", 'i', "xx" }, // 6.31 - 6.39
|
||||
{0X68AED59A, &WrapI_UU<sceRtcSetAlarmTick>, "sceRtcSetAlarmTick", 'i', "xx" }, // 6.00 - 6.20
|
||||
{0XADAF231F, &WrapI_UU<sceRtcSetAlarmTick>, "sceRtcSetAlarmTick", 'i', "xx" }, // 5.03 - 5.55
|
||||
{0X55AC1C23, &WrapI_UU<sceRtcSetAlarmTick>, "sceRtcSetAlarmTick", 'i', "xx" }, // 3.95 - 4.05
|
||||
{0X827BCB3F, &WrapI_UU<sceRtcSetAlarmTick>, "sceRtcSetAlarmTick", 'i', "xx" }, // 3.72 - 3.90
|
||||
{0X329E8E3A, &WrapI_UU<sceRtcSetAlarmTick>, "sceRtcSetAlarmTick", 'i', "xx" }, // 3.71
|
||||
// 3.51 and older didn't renumber it for kernel mode at all - the driver library exports it
|
||||
// under the same NID as the user-mode one.
|
||||
{0X7D1FBED3, &WrapI_UU<sceRtcSetAlarmTick>, "sceRtcSetAlarmTick", 'i', "xx" }, // 2.71 - 3.51
|
||||
};
|
||||
|
||||
void Register_sceRtc_driver()
|
||||
|
||||
@@ -1160,10 +1160,13 @@ bool EraseInstalledFirmware(const Path &nandRoot, std::string *error) {
|
||||
}
|
||||
|
||||
bool FirmwareVersionSupportsVSH(std::string_view version) {
|
||||
// Every firmware from 5.01 up boots to an interactive XMB, checked one release at a time
|
||||
// against every version that ships on a disc (5.01, 5.02, 5.03, 5.50, 5.55, 6.00, 6.10,
|
||||
// 6.20, 6.30, 6.31, 6.35, 6.37, 6.39, 6.60) plus the download-only 6.61. 4.05 and below
|
||||
// still die on a null write inside vsh_module - a separate problem, not an offset to move.
|
||||
// Every firmware boots to an interactive XMB, checked one release at a time against all 39
|
||||
// versions that ship on a disc - 1.50 through 6.60 - plus the download-only 6.61. So the
|
||||
// only question left is whether this is a firmware at all: a fonts-only NAND has no version
|
||||
// and nothing to boot.
|
||||
//
|
||||
// The lower bound is a sanity check rather than a real limit. 1.50 is the oldest firmware
|
||||
// there is, so anything below it isn't a version string we wrote.
|
||||
//
|
||||
// "6.61" -> 661. Sony always writes the minor part with two digits, but don't rely on it:
|
||||
// a single-digit one is a tens value ("5.5" is 5.50, not 5.05).
|
||||
@@ -1186,7 +1189,7 @@ bool FirmwareVersionSupportsVSH(std::string_view version) {
|
||||
} else if (version.size() - dot != 3) {
|
||||
return false;
|
||||
}
|
||||
return numeric >= 501;
|
||||
return numeric >= 150;
|
||||
}
|
||||
|
||||
std::string BundledUpdateInfo::Describe() const {
|
||||
|
||||
@@ -68,71 +68,100 @@ changing repeats byte-identically, and a live menu does not.
|
||||
|
||||
## Which firmware versions work
|
||||
|
||||
**5.01 through 6.61**, all of them. `FirmwareVersionSupportsVSH()`
|
||||
(`Core/Util/PSARUnpack.cpp`) is the gate the UI uses.
|
||||
**All of them - 1.50 through 6.61.** `FirmwareVersionSupportsVSH()` (`Core/Util/PSARUnpack.cpp`)
|
||||
is the gate the UI uses, and it now only asks whether a firmware is installed at all.
|
||||
|
||||
Checked one release at a time against every version that ships on a UMD - 5.01, 5.02, 5.03, 5.50,
|
||||
5.55, 6.00, 6.10, 6.20, 6.30, 6.31, 6.35, 6.37, 6.39, 6.60 - plus the download-only 6.61. Each
|
||||
reaches an interactive XMB; 6.00 was confirmed by rendering a GE dump off the running shell, which
|
||||
is the same picture 6.60 gives.
|
||||
Checked one release at a time against every one of the 39 versions that ships on a UMD - 1.50,
|
||||
1.52, 2.00, 2.50, 2.60, 2.71, 2.80, 2.81, 2.82, 3.03, 3.11, 3.30, 3.40, 3.50, 3.51, 3.52, 3.71,
|
||||
3.72, 3.73, 3.80, 3.90, 3.95, 3.96, 4.01, 4.05, 5.01, 5.02, 5.03, 5.50, 5.55, 6.00, 6.10, 6.20,
|
||||
6.30, 6.31, 6.35, 6.37, 6.39, 6.60 - plus the download-only 6.61. 1.50 and 6.00 were also
|
||||
confirmed by rendering a GE dump off the running shell; both give the same interactive XMB 6.60
|
||||
does.
|
||||
|
||||
**4.05 and below** still die on a `SIGSEGV at 00000000` inside `vsh_module` itself. Different
|
||||
problem, not an offset to move.
|
||||
### Sony renumbered the kernel NIDs, and that is what blocked everything below 6.60
|
||||
|
||||
5.55 was briefly the odd one out: none of its `flash0:/kd` decrypted, because its modules are
|
||||
tagged `0x4C941AF0`/`0x4C941BF0` and those two were the only entries of JPCSP's PRX tag table we
|
||||
were missing. The shell came up anyway - vshmain and paf are user modules - but with not a single
|
||||
driver behind it. Both keys are in `PrxDecrypter.cpp` now.
|
||||
Not offsets - NIDs. A function PPSSPP HLEs under its 6.6x `*_driver` NID is unrecognized on an
|
||||
older build, so the import resolves to the **real firmware module** instead, and the real module
|
||||
goes places the emulator can't follow.
|
||||
|
||||
### What it took to get below 6.60
|
||||
| Function | 6.60/6.61 | 6.31-6.39 | 6.00-6.20 | 5.03-5.55 | 3.95-4.05 | 3.72-3.90 | 3.71 | 1.50-3.51 |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| `sceRtc_driver` `sceRtcSetAlarmTick` | `E09880CF` | `54B9C589` | `68AED59A` | `ADAF231F` | `55AC1C23` | `827BCB3F` | `329E8E3A` | `7D1FBED3` |
|
||||
| `sceHprm_driver` `sceHprmReadLatch` | `E9B776BE` | `A3A87975` | `5FC5E53B` | `605DEA7A` | `A6E8D4F0` | `8C728076` | `F0AA1FB9` | `40D2F9F0` |
|
||||
|
||||
Two things, both the same shape: **Sony renumbered the kernel `*_driver` NIDs between firmware
|
||||
versions**, so a function PPSSPP HLEs under the 6.6x NID is a stranger on an older build - and
|
||||
the import then lands in the *real* firmware module instead, which is where the trouble starts.
|
||||
The rtc one is the interesting failure. Without the HLE, the VSH's alarm call ran the real
|
||||
`rtc.prx`, which called on into `syscon.prx` and blocked forever on a `SceSysconSync` semaphore.
|
||||
The symptom was a boot where every thread was parked and `idle0` was running, and the tell was a
|
||||
**fourth** `SceSysconSync` waiter that a healthy boot doesn't have (there are three, one each for
|
||||
sceSYSCON_Driver, sceRTC_Service and SceWlanMac - that's their normal idle state).
|
||||
`hle.backtrace thread=<SCE_VSH_GRAPHICS>` named the whole chain: vsh_module -> vshbridge -> rtc ->
|
||||
syscon -> wait. The hprm one is only a performance bug, but a loud one: the VSH reads the latch
|
||||
once a frame, so an older firmware's 12-second boot logged ~20000 lines of the same import.
|
||||
|
||||
- **`sceRtc_driver` `sceRtcSetAlarmTick`** is `0xE09880CF` on 6.60/6.61, `0x54B9C589` on
|
||||
6.31-6.39, `0x68AED59A` on 6.00-6.20 and `0xADAF231F` on 5.03-5.55. Without the HLE the VSH's
|
||||
alarm call ran the real `rtc.prx`, which called on into `syscon.prx` and blocked forever on a
|
||||
`SceSysconSync` semaphore. That was the whole "stalls in sceVshBridge_Driver" symptom: every
|
||||
thread parked, `idle0` running, and a fourth `SceSysconSync` waiter that a healthy boot doesn't
|
||||
have.
|
||||
- **`sceHprm_driver` `sceHprmReadLatch`** is `0xE9B776BE` on 6.60/6.61, `0xA3A87975` on 6.31-6.39,
|
||||
`0x5FC5E53B` on 6.00-6.20 and `0x605DEA7A` on 5.03-5.55. The VSH calls it once a frame, so
|
||||
before this an older firmware's boot log was ~20000 lines of one unresolved import.
|
||||
Two more of the same shape, for 1.50-2.xx: `sceImpose_driver` exports `sceImposeGetParam` as
|
||||
`0x531C9778` and `sceImposeChanges` as `0xB415FC59` there, with no user-mode alias. Changes runs
|
||||
once a frame too. And `ModuleMgrForKernel` numbers `sceKernelLoadModuleVSH` `0xA4370E7C` on 1.x -
|
||||
that's how the VSH loads its own plugins, so unresolved it got module id 0 back and the
|
||||
`sceKernelStartModule` after it failed, exactly the way `0xD5DDAB1F` did on 6.61 before it was
|
||||
implemented.
|
||||
|
||||
**How to map a NID between versions.** Disassemble the same module from both firmwares with
|
||||
`--re-module` and match by address: the builds move code around a little, but each of these
|
||||
functions is also exported from the plain user-mode library under a NID that never changed
|
||||
(`sceRtc/0x7D1FBED3`, `sceHprm/0x40D2F9F0`), so anchor on that export's address in each build and
|
||||
read off the `*_driver` NID sitting at the same place. That is mechanical, and it is how all eight
|
||||
NIDs above were found.
|
||||
`--re-module` and match by address. Don't compare raw addresses - the builds move code - anchor on
|
||||
the plain user-mode export whose NID never changed (`sceRtc/0x7D1FBED3` for SetAlarmTick,
|
||||
`sceHprm/0x40D2F9F0` for ReadLatch) and read off the `*_driver` NID at the same address. Below
|
||||
3.95 `sceRtcSetAlarmTick` isn't in the user-mode library at all, so anchor on a neighbour instead:
|
||||
it is the driver export immediately below `sceRtcIsAlarmed`. Where even that fails, match the
|
||||
function body - that is how the two impose calls and the 1.x LoadModuleVSH were identified.
|
||||
|
||||
Expect more of these as other paths get exercised - `sceVshBridge`, `sceDisplay_driver`,
|
||||
`sceImpose_driver` and `sceCtrl_driver` all have version-specific NIDs that are currently
|
||||
unresolved on every version, 6.61 included, and the boot survives them.
|
||||
`sceRtcIsAlarmed` also had to be implemented (it returns 0, as in JPCSP). Left as a null entry it
|
||||
returned `SCE_KERNEL_ERROR_LIBRARY_NOT_YET_LINKED`, and the 3.0x-3.5x VSH read that as "ask the
|
||||
hardware instead" and went back to blocking on syscon.
|
||||
|
||||
### Making the two module patches version-safe
|
||||
### The scePaf heap pool, and where it lives
|
||||
|
||||
Both patches used to be hardcoded offsets from the module base, applied to any module of the right
|
||||
name. That is fine for the version they were derived from and quietly wrong for every other one.
|
||||
scePaf's allocator wants a pool base already written into one of its BSS slots. The module that
|
||||
owns the allocator does fill that slot in itself, from its own module_start - but that start
|
||||
thread hasn't been scheduled yet when vshmain makes its first allocation, so the pointer is still
|
||||
null and the shell writes through it. Real hardware's kernel bootstrap starts these modules one at
|
||||
a time and waits; `LoadAndStartVshKernelModules()` can't, so it pre-fills the slot instead.
|
||||
|
||||
- **The scePaf heap arena slot is now found via gp**, not the module base. Its base-relative offset
|
||||
moves with every build (0x18CCD8 on 6.00 through 0x18D728 on 6.60/6.61), but it sits at
|
||||
`gp - 0x7E88` in all of them - checked against 6.00, 6.20, 6.31, 6.37, 6.39, 6.60 and 6.61. The
|
||||
slot is the second of the two pool pointers scePaf's own init fills in with
|
||||
`sceKernelTryAllocateFpl`, which is how to re-find it in a build not listed here: disassemble
|
||||
`paf.prx`, find the one function that calls `sceKernelTotalMemSize`, and read the address handed
|
||||
to the second `sceKernelTryAllocateFpl` as `a1`.
|
||||
- **The vsh_module alarm-category patch now checks what it is overwriting.** That offset is in
|
||||
rodata, so there is no gp anchor for it; instead the patch only fires when the word at
|
||||
`+0x455C4` is the `0x3F666666` it was derived from. On 6.39 that word is a different float, and
|
||||
on 6.20/6.00 it is ASCII string data (`5f746c75`, `776f6461`) - the old unconditional write was
|
||||
corrupting a string table on those. Only 6.60/6.61 need the patch at all; every older version
|
||||
reaches the XMB without it.
|
||||
Which module owns it moved. **Up to 4.05 the allocator is a separate `flash0:/vsh/module/heaparea1.prx`**,
|
||||
which paf imports as `scePafHeaparea` and cannot allocate a byte without; from 5.01 it is compiled
|
||||
into paf.prx and heaparea1 is gone. Loading heaparea1 when it's present was the missing piece for
|
||||
every 3.x and 4.x version - without it `scePafHeaparea_ACCE25B2` was an unresolved import, paf
|
||||
built a heap out of an uninitialized stack pair, and vshmain died storing the null the allocator
|
||||
handed back.
|
||||
|
||||
Getting the paf patch right on its own turned an immediate `SIGSEGV at 0000000c` inside
|
||||
`scePaf_Module` (a null pool base plus a field offset) into a clean stall for every 6.0x-6.3x
|
||||
version, which is what moved the blocker on to the rtc NID above.
|
||||
Either way the slot is the second of the two pool pointers that module's init fills in with
|
||||
`sceKernelTryAllocateFpl`, and either way its offset from the module base moves with every build
|
||||
while its offset from gp does not:
|
||||
|
||||
| Module | gp - slot | Checked against |
|
||||
|---|---|---|
|
||||
| `paf.prx` | `0x7E88` | 6.00, 6.20, 6.31, 6.37, 6.39, 6.60, 6.61 (base-relative 0x18CCD8..0x18D728) |
|
||||
| `heaparea1.prx` | `0x7FCC` | 3.95, 4.05 |
|
||||
|
||||
To re-find it in a build not listed: disassemble the module, find the one function that calls
|
||||
`sceKernelTotalMemSize`, and read the address handed to the **second** of its two
|
||||
`sceKernelTryAllocateFpl` calls as `a1`. The shape is identical in both modules - TotalMemSize, a
|
||||
`> 0x2400000` test picking 0xA00000/0xC50000 pool sizes over the compiled-in defaults, then two
|
||||
`sceKernelCreateFpl` + `sceKernelTryAllocateFpl` pairs writing to adjacent slots.
|
||||
|
||||
### 1.50 declares no module attributes
|
||||
|
||||
`g_runningVSH` was set from `PSP_MODULE_VSH_MODE` in the module's attribute word. 1.50's
|
||||
vshmain.prx has attribute `0000` - Sony only started setting the flag in 1.52 - so the entire VSH
|
||||
bootstrap was skipped and the shell ran with none of its support modules loaded. The check also
|
||||
accepts the module *name* `vsh_module` now.
|
||||
|
||||
### The vsh_module alarm-category patch only 6.6x needs
|
||||
|
||||
That offset is in rodata, so there is no gp anchor for it; instead the patch only fires when the
|
||||
word at `+0x455C4` is the `0x3F666666` it was derived from. On 6.39 that word is a different
|
||||
float, and on 6.20/6.00 it is ASCII string data (`5f746c75`, `776f6461`) - the old unconditional
|
||||
write was corrupting a string table on those. Every version below 6.60 reaches the XMB without the
|
||||
patch, which is itself a hint that whatever precondition makes that scan safe on real hardware was
|
||||
lost somewhere between 6.39 and 6.60.
|
||||
|
||||
### Kernel modules with per-model builds
|
||||
|
||||
@@ -140,7 +169,14 @@ version, which is what moved the blocker on to the rtc NID above.
|
||||
`wlanfirm_01g.prx` by name. A firmware unpacked for one model ships only that model's build, and
|
||||
PPSSPP's own updater unpack defaults to 02g, so all three failed to load. `ResolveVshModelModule()`
|
||||
now substitutes the emulated model's suffix when that file exists, falling back to `_01g` for a
|
||||
dump unpacked with model `any` (which has every model's).
|
||||
dump unpacked with model `any` (which has every model's). Firmwares older than about 3.60 predate
|
||||
the PSP-2000 and have no per-model split at all, so they are unaffected.
|
||||
|
||||
### 5.55 needed two PRX keys
|
||||
|
||||
Its `flash0:/kd` modules are tagged `0x4C941AF0`/`0x4C941BF0`, and those two were the only entries
|
||||
of JPCSP's PRX tag table PPSSPP was missing. The shell came up anyway - vshmain and paf are user
|
||||
modules - but with not a single driver behind it.
|
||||
|
||||
## The red error screen
|
||||
|
||||
|
||||
Reference in new issue
Block a user