Changelog
Every build, and what was wrong with it.
One entry per published build, newest first, each identified by the checksum of the file itself rather than by a version number two builds can share. What was known to be broken at the time is listed beside what was fixed.
Five bugs that could make a whole function come out wrong, all of them the analysis knowing something the decompiler never heard.
Fixed
- **A stripped ARM binary was decoded as ARM even when it is Thumb.** Every piece of evidence the loader weighed lives in the symbol table, so a stripped image had none and the ARM default stood. That is wrong from the first byte:
70 47, which isbx lrand the commonest instruction in Thumb code, disassembles assvclt #0x4770, and pseudocode built on that listing is confident and fictional. Bit 0 of the ELF entry point survives stripping and is the ARM ABI's own statement of which state execution begins in, so it is consulted now — but only when nothing better is available, so it cannot outvote real evidence on an image that still has some. Across 58 stripped Cortex-M firmware images, files containing bad instruction data fell from 51 to 5. - **Calls into a shared library showed no name on a recent Linux build.** Those binaries carry two procedure linkage tables:
.pltholds the lazy-binding stubs, and anendbr64-guarded stub in.plt.secis what every call is actually compiled against. Only.pltwas named, so the addresses a program really calls were anonymous —sub_3a50()where the name isexit(). Both are named now. - **Functions ran past their end into the next one.** Three separate causes, each enough on its own.
exitandabortreached through a PLT stub were not recognised as never returning, so the call kept a path after it that does not exist. A local wrapper around one of them was recognised and then lost the mark a moment later, because the rebuild that recognising it triggered took the function's flags fresh and dropped it. And which block ended a function was read from a graph built before any of this was known. A one-block routine could come out carrying thirty-seven blocks of its neighbours. - **A tail call was pasted in whole instead of shown as a call.** A
jmpto another function is a call that reuses the current frame, and compilers emit it constantly for a wrapper whose last act is one call. The decompiler was following it and inlining the callee, so a two-line function came back as a hundred lines of borrowed registers it never set. - **A segment's zero-fill tail was decoded as code.** It is mapped but backed by no bytes in the file and reads as zeroes, and a run of zeroes is a valid instruction on most architectures — so sweeping one manufactured code out of nothing. Instruction fetches there are refused now, while data reads still read zero, which is what a
.bssactually holds. - Analysis could give different answers at different worker counts in one narrow case, and
--archwas accepted and then ignored on ELF.
Known limitations
- Eight of the 58 ARM images in that corpus carry no entry-point Thumb bit and are still decoded as ARM. Reading
.ARM.attributescovers them and is not done yet. - The exactness above is measured for control-flow structure only. Type recovery and byte-exact recompilation are not measured by it.
- The installer is still not code-signed, so SmartScreen warns on download. The SHA-256 is on the download page; check it before running.
- **A stripped ARM binary was decoded as ARM even when it is Thumb.** Every piece of evidence the loader weighed lives in the symbol table, so a stripped image had none and the ARM default stood. That is wrong from the first byte:
Running an older build?
The eight characters beside each version are the start of that installer’s SHA-256, which the download page publishes in full — so the way to tell which build you have is to check the file you installed from. Inside Klarion, Help › About and klarion-cli --version report the version and the commit it was built from, which is a different identifier and does not appear here.