[AUTOMATED] feat(analysis): relocrebase - rebase load-time analysis facts for relocatable objects (DIV-79) - #309
Merged
Conversation
…ocatable objects (DIV-79) [AUTOMATED] Fixes GH-289. `relocobjects` (DIV-8/DIV-70) lays an ELF ET_REL `.o` / COFF `.obj` out synthetically above RELOC_BASE, so every address the engine holds is post-layout. The load-time analysis passes never learned that: each re-parses the same file through its own `object::File` and computes pre-link, section-relative addresses. Both spaces then landed in one inventory -- `kuna functions ptx.o` reported 95 entries (26 real plus 27 phantoms at 0x0/0x20/0x34/...) while `decompile-all` reported the correct 26, breaking the DIV-68 agreement invariant. Silently, string literals and DWARF-named globals were keyed to pre-link addresses and never attached to the loaded image. The phantoms are not a missing base address: 26 are `.eh_frame` FDE `initial_location` fields whose PC-relative relocation the linker has not applied (an unrelocated PC-relative field reads back as its own section offset), and 1 is DWARF, where `DW_AT_low_pc` reads 0 for every subprogram -- as does every `DW_FORM_strp`, so the whole object's DWARF collapsed onto one function named after `.debug_str`+0. #286 fixed the Listing/xref half by declining. Declining here would throw real information away (a -g `.o` carries full DWARF), so this rebases instead -- and rebases the analyzer tier's INPUT rather than each output fact, because a fact is a bare u64 by the time it reaches AnalysisOutput and every section of a relocatable object sits at address 0, which makes `.text`+0x20 and `.rodata`+0x20 the same number. loader/kuna_relocrebase.rs re-presents the object: each laid-out section carries the loader's own relocated bytes and its load VMA (ELF sh_addr, COFF VirtualAddress); each unlaid `.debug_*` section has its relocations applied here (a target in a laid-out section resolves to its load VMA, a debug-to-debug target to its own section-relative offset); and each ELF symbol defined in a laid-out section has st_value shifted by ITS OWN section's delta -- the layout is non-contiguous, so there is no single global offset. A COFF symbol needs no shift (`object` reports it as VirtualAddress + value). A fact that still lands in no laid-out section is dropped, not passed through unrebased; a NoReturnFact is the one exception, kept with its address zeroed so the commit's name fallback still fires. Measured on the in-repo fixtures: ptx.o `functions` 95 -> 68 with zero entries below RELOC_BASE and `decompile-all` still 26 (the two now agree), recognized string literals 2 -> 41, DWARF parameter names and types applied (`char to_uchar(char ch)`, `ignore_case`/`folded_chars` instead of `dat_403520`/`0x403160`); arm_thumb_le32.o goes from decompiling nothing to both functions (the ARM TMode paints now land); msvc_mangled.obj recovers its `Bar *this`. Every executable in tests/bug-repro/ is byte-identical, and `--option relocrebase off` is byte-identical to origin/main on all 14 audited targets. Loader-tier gate, so it uses the i386_pie_plt/relocobjects env bridge (KUNA_RELOCREBASE): the analyzer tier runs inside `load file`, upstream of every per-function option, and it cannot be deferred to the commit because it changes what every pass reads. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VBn8vgwoHqPFnnV4ZMApfq
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #289
Root cause
relocobjects(DIV-8/DIV-70) lays a relocatable object — an ELFET_REL.o, a COFF.obj— out synthetically aboveRELOC_BASE, so every address the engine holds is a post-layout one. The load-time analysis passes never learned that: each re-parses the same file through its ownobject::Fileand therefore computes pre-link, section-relative addresses. Both spaces then landed in one inventory.The visible half, on the in-repo
decompiler/crates/kuna-analysis/tests/fixtures/ptx.o:— the two surfaces disagreed on a
.o, which is exactly the invariant DIV-68 established. The phantoms decompose into two distinct mechanisms, and neither is a plain "off by a base address":.eh_frameFDEinitial_locationfields whose PC-relative relocation the linker has not applied. An unrelocated PC-relative field reads back as 0, so the FDE oracle computessection_addr(0) + field_offset + 0— i.e. the FDE's own offset in.eh_frame— which happens to fall inside.text's[0, 0x2d9a)pre-link extent and survives every plausibility filter.0x20/0x34/0x5care the FDE bodies at.eh_frame+0x18/0x2c/0x54.DW_AT_low_pcis likewise relocated, so every subprogram reads 0 — and so does everyDW_FORM_strp, so the whole object's DWARF collapsed onto one function at address 0 named after whatever string sits at.debug_str+0 (output_format, which is not even a function).The silent half: string literals and DWARF-named globals were keyed to pre-link addresses and never attached to the loaded image at all (
ptx.orecognized 2 string literals out of 41).#286 fixed the Listing/xref half by declining for such an object. Declining here would throw real information away — a
-g.ocarries full DWARF — so this rebases instead.The fix: rebase the INPUT, not each output fact
A fact is a bare
u64by the time it reachesAnalysisOutput, and in a relocatable object every section sits at address 0 —.text+0x20 and.rodata+0x20 are the same number — so a post-hoc, per-fact rebase cannot tell which section's delta to apply. And the fields that matter are not offsets at all until their relocation is applied (see above).So
decompiler/crates/kuna-analysis/src/loader/kuna_relocrebase.rsre-presents the object to the analyzer tier before any pass reads it:.text/.eh_frameread exactly as the engine decodes them),.debug_*tables — has them applied here: a target in a laid-out section resolves to that section's load VMA, a debug-to-debug target to its own section-relative offset (S = 0, which is what a single-object link leaves in place),sh_addr, COFFVirtualAddress) is set to its load VMA, andst_valueshifted by its own section's delta — sections are laid out non-contiguously (alignment padding, the empty-section skip), so there is no single global offset. A COFF symbol needs no separate shift:objectreports it asVirtualAddress + value, so step 3 already moved it.Every pass then produces an already-rebased fact with no source change of its own. The section→VMA and extern-slot maps come from the layout the loader already builds (
RelocLayout::section_vma/extern_addr, newly exposed).Safety net. A field with no relocation still yields an address in no laid-out section (a hand-written
.eh_frame, anSHN_COMMONsymbol whosest_valueis an alignment, a symbol in a discarded section).retain_in_imagedrops exactly those — the phantom class — rather than passing a pre-link address through. One documented exception: aNoReturnFactis kept with its address zeroed, because the commit resolves it by NAME when the address does not resolve (an undefinedexitin a.ohas always had address 0, and dropping the fact would lose a real no-return marking).Per-pass coverage
noreturn_knownnoreturnst_value; an UND libc symbol keeps address 0 and still resolves by namestringsstringssh_addr/VirtualAddressentry_discentries,entry_names,context_paints.eh_frameFDE fields relocated by the byte splice; prologue scan +in_executable_sectionread patched section addresses; funcsym filter reads shiftedst_valueeh_frame_full,fdeinteriorentries,fde_bodies.eh_framesplicefuncstart_patterns,cortexmvectorsentries,context_paintsarm_markerscontext_paintsst_value($t/ STT_FUNC LSB) — this is what makesarm_thumb_le32.odecompile at allmips_gp,mips_isatracked_regs,context_paintsst_valuecppsigcpp_sig.proven/.inferred(address-keyed)st_valuedwarf(+cppproto,typedepth)symbols,data_objects,locals,cpp_dwarf.*.debug_info/.debug_str/.debug_rnglists/… relocations applieddwarf_linescomments.debug_linerelocations appliedoperand_refs(deferred, default-off)strings,readonlyrun_operand_refslibproto,libcsigs,callfixupitaniumrttiET_RELpeimportcallexternref.objdatasymsnoreturn_disc,noreturn_propagate,fid,aif,ptrentry,poolentry,tailcallentry,fast_funcdisc)Deliberately excluded
.obj(.debug$S/.debug$T) — kuna'spdbpass is PE-only and reads an external, fingerprint-matched.pdb; it never parses a.obj's embedded CodeView, so there is no address-keyed fact to rebase. A.objCodeView reader is a separate feature.ObjectFormat::relocatable_layoutis false for Mach-O (DIV-70), so a Mach-O.oloads through the ordinary mapped-segment path and its analysis addresses already are the loaded ones.macho_dwarf.o/macho_min.oare byte-identical before and after (their sub-RELOC_BASEaddresses are correct).ET_REL— the header patcher declines rather than emit a half-patched image (the sibling relocation engine inreloc_objectis little-endian-only too), so such an object keeps today's behavior.Before / after
kuna functions(entries belowRELOC_BASEare the phantom class):ptx.oRELOC_BASEfid_lib_x86_64.oarm_thumb_le32.ocoff_obj.objcoff_comdat_i386.objmsvc_mangled.obj/pe_min.obj/ftol_i386.objmacho_dwarf.o/macho_min.oThe two surfaces now agree on
ptx.o:functionsreports 26 real functions + 42 extern stubs and nothing belowRELOC_BASE;decompile-allstill reports 26. The 26 real entries are byte-identical to before — the option only removes the phantom half.DWARF now lands on the right addresses (
readelf --debug-dump=info ptx.oshowsmain'sDW_AT_low_pcrelocated against.text.startup+0, which the layout places at0x404280— wheremainis):Strings / named globals now attach to the loaded image —
ptx.orecognized string literals 2 -> 41, and the DWARF/.symtabglobals resolve:Two more object fixtures moved, both strictly better:
arm_thumb_le32.o:decompile-allwent from[](nothing) to both functions — the ARMTModecontext paints fromarm_markersnow land at the rebased addresses, so the Thumb code decodes as Thumb.msvc_mangled.obj:int Bar::foo(unsigned long long a0, int a1)->int Bar::foo(Bar *this, int a1)—cppsig's address-keyed prototypes now match.Gating
Default-ON (DIV-79): the addresses it replaces are provably wrong — they name a different address space than the one the engine decodes in — so this is a correctness fix, not a judgement call.
The whole analyzer tier runs inside
load file, upstream of every per-functionoption, and unlike the commit-gated passes (datasyms,itaniumrtti, …) this gate cannot be deferred to the commit: it changes the inputs every pass reads, so honouring it at commit would mean running the whole pass list twice. It therefore uses thei386_pie_plt/relocobjectsenv-var bridge (KUNA_RELOCREBASE), exported bykuna decompileonto thedecomp_dbgsubprocess and bydecompile_all::apply_loadtime_envonto its own process beforebootstrap_from_object. TheArchitecture::analysis_relocrebasebool exists for catalog visibility and thephase cataloglivecurrentfield.Proof both CLI paths honour it, both directions (
ptx.o):Off is byte-identical to
origin/main(a fresh build of 40c829e):decompile-all --jsoncompared overtests/bug-repro/{grep,sort,faillog,libselinux.so.1}**and all ten.o/.objfixtures` — 14/14 identical.Collateral
Linked images are structurally untouched (
rebased_viewreturnsNoneunlessreloc_object::is_synthetically_laid_out), and it is measured, not just argued:decompile-all --json, on vs off, overtests/bug-repro/{grep,sort,faillog,libselinux.so.1}— identical, all four.functions --json, origin/main vs this branch, over the same four plusbetaflight_STM32F405.elf— identical, all five.betaflightdecompile-all— identical too (background run,BF_IDENTICAL).Speed
Interleaved min-of-11,
origin/mainbuild vs this branch, alternating run by run (the box was shared with three other agents' decompile sweeps, which is exactly why min-of-N interleaved):decompile-all tests/bug-repro/grep --json(the no-op path)decompile-all ptx.o --json(the.opath)The
grepdelta is contention noise in kuna's favour — that path is structurally unchanged (rebased_viewreturnsNonebefore doing any work on a linked image). The.ocost is real and is the whole budget of the feature: one extralayout_relocatable(the loader's own layout is not plumbed through to the analyzer tier), onebytes.to_vec(), and the relocation pass over the non-alloc.debug_*sections — +20 ms on a 120 KB, 1067-.debug_info-relocation object.Tests
No stages XML is possible — that path never constructs an
ObjectLoadImage(it usesLoadImageXmlover<bytechunk>), sotests/stages/,decompiler/crates/kuna-base/src/xml.rs's corpus count anddocs/baseline-stages.jsonare all untouched. The gate is the cargo suite instead:kuna-analysisunit tests (loader/kuna_relocrebase.rs) — ELFET_RELsections/symbols/FDE starts all land in the loaded image,.debug_inforeally is relocated, both COFF.objfixtures rebase, the gate declines when off, and a linked ELF is never touched.kuna-console/tests/verify_relocrebase.rs— the DIV-68 agreement invariant overptx.o+ both.objs +arm_thumb_le32.o(no inventory entry is a pre-link address), and the DWARF parameter name/type reaching the rebased function end-to-end.kuna-console/tests/verify_relocrebase_gate.rs— the on/off arms, in their own test binary because the gate is a process-global env var; the off arm asserts the phantoms return, so the on arm's clean inventory is a property of this option and not of the fixture.Counters bumped
phases.tomlsettable row (all fields incl.tier/symptoms),p0_knowledge/options.rsKUNA_OPTION_NAMES,kuna_console.rskuna_live_value,Architecture::analysis_relocrebase(+ default-on),set_kuna_optionarm.kuna_phases/tests.rs: settables 108 -> 109, tiers(24, 47, 37)->(24, 47, 38),},\ncount 107 -> 108,relocrebaseadded to the no-live_fieldallowlist.catalog_bytecompat.rs: 108 -> 109 (x3) +tests/fixtures/phase_catalog.jsonregenerated via thedecomp_dbgopenfile writecapture.docs/options.mdregenerated;kuna catalog --checkgreen.docs/history.mdDIV-79 row;docs/spec/01-program-prep.md(the rebasing contract,Anchors:chapter forkuna-analysis/src);docs/missing-ghidra-analyses.md(the pass contract's address-space clause).make check-specgreen in strict mode too.Every count was read off a green build, never derived by arithmetic.
Gates
All four run in the worktree at
9dc29292, on top oforigin/main40c829e9. Verbatim tails:(
python3 tools/check_spec.py --strict→check-spec OK (strict mode).)313
test result: oklines, zeroFAILED;options_md_matches_the_generator_byte_for_byte ... ok;kuna catalog --check→catalog OK: documents exactly the registered kuna options.Follow-up (not bundled)
Now that a relocatable object's analysis facts are rebased, #286's Listing/xref decline is worth revisiting:
listing_seedswould be correct against the rebased view, and the Listing already decodes through the (rebased)ObjectLoadImage, so the recursive-descent walk,aifandnoreturn_propagatecould run on a.oinstead of being skipped. That is a behavior change of its own with its own measurement, so it is deliberately left out of this PR.🤖 Generated with Claude Code
https://claude.ai/code/session_01VBn8vgwoHqPFnnV4ZMApfq