Skip to content

ARM64/ELF: Inline single-precision literal pool reads - #614

Closed
nichinichisou0609 wants to merge 1 commit into
SamboyCoding:developmentfrom
nichinichisou0609:feat/arm64-literal-pool-upstream
Closed

nichinichisou0609 wants to merge 1 commit into
SamboyCoding:developmentfrom
nichinichisou0609:feat/arm64-literal-pool-upstream

Conversation

@nichinichisou0609

Copy link
Copy Markdown

Problem

On arm64 a float constant that does not fit an immediate is emitted into a literal pool and loaded via adrp/ldr:

adrp x8, <page>
ldr  s0, [x8, #<off>]

NewArmV8InstructionSet already folds that pair into an absolute address (the adrpOffsets path), so what reaches IlGenerator is a MemoryOperand with IsConstant == true. But the MemoryOperand case there only handles a byref dereference, so every literal pool read degrades to a diagnostic plus a zero:

Cpp2ILHelpers.NoteDecompilerIssue("Unmanaged memory load: [1ED0730]");
... = 0;

The value is lost, and since the pushed zero often has the wrong stack type, ILSpy then reports type mismatches around it.

Change

Three files, +59/-0.

  • Il2CppBinary.IsVirtualAddressReadOnly(ulong) — new virtual predicate defaulting to false, the same conservative shape as the neighbouring IsExportedFunction. A format only answers true once it actually reports segment permissions.
  • ElfFile overrides it: the address must lie in a PT_LOAD segment with PF_W clear, and below RawSize — bytes past that are zero-filled at load time rather than read from the file, so they are not constants.
  • NewArmV8InstructionSet, in the LDR case: when the destination register is S0–S31 and the source is a constant address in a read-only segment, the operand becomes a FloatLiteral instead of a MemoryOperand.

Producing the literal in the ISIL layer rather than at IL emission means IlGenerator's existing case FloatLiteral handles it with no change, and the analysis passes get to see a constant as well.

Why only Sn

An LDR Dn may be supplying two .2S lanes rather than one double, so reading 8 bytes there can fabricate a value out of two adjacent floats. Sn has an unambiguous width.

This is not hypothetical: an earlier revision of this patch took the width from the expected type instead of the destination register, and produced doubles such as 7.032822090631087E+19 — whose bit pattern is exactly (57.2958f, 570f), i.e. 180/π next to its neighbour in the pool.

Effect

Two different Unity 2022.3 arm64 ELF binaries, --output-as dll_il_recovery:

binary literal pool reads inlined methods recovered [Fail]
A 3,219 100% (135773 / 135776) 0
B 3,206 100% (135096 / 135099) 0

Recovery rate and failure count are unchanged, so the change is additive. Within one assembly of binary A, the decompiled output loses 463 Unmanaged memory load notes and 223 Expected X, but got Y remarks, and Invalid comparison between … drops from 150 to 64.

Spot checks on recovered values: a UI fade path that decompiled as DOFade(0f, 0f) now reads DOFade(0f, 0.2f); recovered constants include 3.1415927410125732, which is (double)(float)π and so confirms a 4-byte read rather than an 8-byte one, and 57.29578f.

Cpp2IL.Core.Tests (76) and LibCpp2ILTests (5) pass unchanged.

Not covered

  • Only ELF implements the predicate. PE / Mach-O / NSO / WASM return false and are unaffected. The same ISIL shape exists on x86 — X86InstructionSet emits MemoryOperand(addend: IPRelativeMemoryAddress) for RIP-relative operands — so the predicate is reusable there, but I have no x86 sample to verify against and would rather not add an unexercised path.
  • LDR Dn is deliberately left alone, for the width reason above.
  • The repository's test samples are all Windows PE (GameAssembly.dll), so there is no arm64 ELF fixture to write a unit test against. Glad to add one if you would want such a sample included.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant