Repository navigation
Matching MSC 5.1 output instruction-for-instruction on a 16-bit DOS reconstruction #9498
Replies: 1 comment
|
Following up on the toolkit question — the tools are public:
How it's usedTypical iteration: The non-obvious part documented in the post above: at this point the remaining diffs are almost never "wrong decompilation" — they're compiler-state differences (symbol interning order, declaration placement, register-residency) that change instruction selection on otherwise-correct C. That's the layer we haven't seen written up anywhere else, and it's what makes byte-exact MSC reconstruction slow. |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Sharing some hard-won findings from reconstructing a ~900KB overlaid 16-bit DOS game executable built with Microsoft C 5.1/6.0-era tools — back into readable C that recompiles to instruction-equivalent code. Currently ~600 of 753 routines match gold instruction-for-instruction, whole modules byte-identical modulo relocations.
The loop
Ghidra headless decompile → clean up into idiomatic C → compile with the period MSC (CL.EXE under a DOS VM) → instruction-level diff against retail function extents → iterate until
match. Binary equivalence is the proof the C is right — a routine isn't done until readable source produces it.The hard part: MSC 5.1 codegen is compiler-state-driven, not source-local
The single biggest surprise: whether
cmp A, Borx + ymaterializes operand A or B intoaxis decided by module-global symbol-table interning state, not just the source text. The same source line emits both forms in the same module at different parse states. Levers that flip it, in rough order of usefulness:externdecls.extern int foo;inside a function's declaration block perturbs the interner without emitting any data — and can flip a compare's operand pick, change a register-resident loop variable, or shorten a block enough to cross a short-branch boundary. We used them to turn apush [bp-2]into gold'spush ax(saving 2 bytes and pulling ajzback inside ±127 range).int a,b,c;decl list drives its stack slot and whether MSC's allocator promotes it to a register. Movingint cntfrom first-decl to mid-decl flipped a loop test frommov ax,[cnt]to gold'smov ax,[NPLANES]. Moving aregister intlater in the decl block killed itsdipromotion entirely (gold kept it in memory — decl order beats theregisterkeyword).==/!=and relational compares pick differently; unsigned-vs-signed operands flip which lands inax.sum(uppercased identifier bytes) % 16; decompiler-guessed names (cnt,endval,di2) intern differently than the originals did, so the same source compiles differently until decl context is restored.x + ya param operand beats a global intoaxalmost unconditionally — but gold sometimes shows the global first, meaning the original expression shape differed.registerkeyword. MSC 5.1 honors it as a hint and auto-promotes; declaration order beats the keyword.Tooling traps worth knowing
/Osvs/Otis per-module and visible: shared vs inlined epilogues, and/Otemits nop pads before odd branch targets.add al,[0x619a]) that don't exist anywhere in the file. Always spot-check against the raw image before trusting a mismatch.function too large for global optimizationspast a symbol threshold — your padding decls can push a routine over it and change codegen everywhere.Happy to answer questions or compare notes with anyone else doing byte-exact MSC/16-bit reconstruction — the "compiler-state archaeology" layer turned out to be most of the work.
All reactions