Two swapped integers

Reading an open-source loader's header found in ten minutes what black-box probing could not have found at all: DT_SCE_JMPREL and DT_SCE_PLTRELSZ were the wrong way round (D036). Every other one of the nineteen matched exactly.

The derivation could not have caught it. JMPREL + PLTRELSZ == RELA is commutative, so an offset and a size sum to the same place whichever is which - the arithmetic that fixed the rest of the table is structurally blind to this pair. derive now says so, for every offset/size pair rather than just the two entry sizes.

It had also produced a false conclusion. -fno-plt leaves the linkage table empty, and an empty table is not declared - so the two bad tags were simply absent from that build. One emulator ran the module with the flag and failed without it; the other did the reverse. That went into the record as two loaders wanting incompatible things. They did not. One bug was hidden by a build flag, and the flag was holding up a conclusion that was wrong.

After the correction, with the ordinary PLT build:

before after
imports resolved 806 928
platform calls 77 131
guest faults 0 0
second emulator refused at relocation executes the guest

The second emulator now reaches --- Execute: Main ---, having loaded, relocated and resolved. -fno-plt is gone.

What the loader source also gave

On method

Deriving the tag table from a reference module was right, and produced nineteen values from arithmetic that cannot be argued with. It also produced two wrong ones with exactly the same confidence. Both halves belong in the record: derive what can be checked, and read the thing that already knows when the check cannot distinguish two answers.

It reports

An emulator now runs obSCEne end to end and the report comes out on its console channel. 110 records, fifteen sections, real verdicts.

The last thing in the way was the symbol type. The linker leaves an undefined reference as STT_NOTYPE; a console loader matches on the NID and the type, so a NOTYPE import matches nothing and binds to a stub returning zero - including the write the report goes through. Everything ran, everything got a plausible zero back, and nothing was ever said. Rewriting imports to STT_FUNC (D039) fixed it.

The earlier conclusion was wrong. "Four output channels, all stubbed, no emulator implements a write path" - no. The write path was there. We were asking for it in a way the loader could not match, and its stub answered instead.

It was nearly missed twice. Typing the imports made the emulator log fewer resolutions and end in a fault, which read as a regression, and it was reverted on that basis. Fewer lines because it stopped resolving and started running. The fault is a real defect the probe found and named.

What the first real run says

035-libc     5 pass, 1 partial, 12 fail, 2 skip
037-math     2 pass, 0 partial,  5 fail, 0 skip
040-file     2 pass

sqrt(4) is wrong, fabs is wrong, rounding a positive value is wrong, twelve string and conversion checks fail. Those are findings about an emulator, produced by a probe, which is the entire point of this repository.

And the run ends the way it was designed to:

STREAM ENDED INSIDE A CALL: 040-file/open-rejects-null
The report stopped between the attempt and its result, so this call most likely
took the process down.

A try with no matching res, naming the exact call. Passing a null path to sceKernelOpen throws inside the emulator. Announce-before-attempting earned its keep - that check was the one thing in fifteen sections that could not report for itself, and it is the one thing the report names.

Versions, and where Kyty still stops

Identity values declared version zero; libraries are registered as version one and modules as 1.1, packed differently - a library uses sixteen bits, a module splits them into a major and a minor (D038). Corrected. No observed behaviour change in either emulator, which is worth saying rather than implying otherwise.

Correction: it binds properly. The one-import control was extended to check the return value rather than only the address - a stub and a real function are both non-null, and only what comes back tells them apart. Under Kyty the write returns the full byte count, so sceKernelWrite is bound to the real implementation and the bytes were written.

They are not reachable. Kyty is a windowed application whose C stdout is not the handle a parent process redirects, and the same is true of stderr - both writes succeed and the output goes somewhere a script cannot read. Its own logger reaches a redirected stdout; the guest's writes do not.

So obSCEne runs correctly on both emulators. One of them can be read.

A third loader, and an independent check on the tag table

craziiEmu (and SharpEmu, which shares its lineage) is a C# PS5 emulator with a small, readable loader. Its constants were compared against ours:

DtSceJmpRel     = 0x61000029
DtScePltRelSize = 0x6100002D

which is the corrected assignment from D036, arrived at independently. Two loaders now agree that our original derivation had those two the wrong way round.

It also names SceRelro = 0x61000010, a segment type this project does not emit and has not needed. Worth knowing it exists.

Its resolution is much simpler than the other two: it matches the whole encoded name against per-module stub tables, with no symbol type and no version involved. That is a useful third data point - the type matching that D039 turned on is a property of one loader, not of the format, and a module that satisfies the strictest of them satisfies the others.