Join the discussion

Write your take first — we'll ask for email only when you're ready to publish.

  • Hacker News
  • On par with QEMU, but still far behind Rosetta...
  • blows my mind apple plans to sunset rosetta, it's like a core part of computing for me now that frees me from needing a second device
  • And Box64, but I think the point is that this is closer to being guaranteed to work.
  • Isn't part of that due to Rosetta relying on Apple extensions to ARM to mimic x86-64 memory semantics?
  • Where is the source code?
  • Cute, but Rice's theorem remains, and while they translated every byte as code, still no handling is possible for

       char buf[] = {0xB8, 0x2A, 0x00, 0x00, 0x00, 0xC3};
       return ((int (*)(void))buf)();
    
    static translation is only possible when you assume no adversarial code AND mostly assume compiler-produced binaries. hand-rolled asm gets hard, and adversarial code is provably unsolvable in all cases.

    still, pretty cool for cooperative binaries

  • No based on the abstract it can handle that code. What it can't handle is runtime code generation.
  • I think this is handled by Rosetta.
  • It looks like their system would just generate return 42;
  • I read those bytes and immediately thought "mov eax, 42; ret".
  • But in fact no modern processor/OS executes this either. Pages are marked as executable or not, and static data is loaded as non-executable pages.
    by tlb
  • I only read the abstract but I got the impression that their solution to this is they have both. They translate all the data as if it was code and if it gets called into they use the translation where if it gets read as memory they use the original.

    Edit I found this in the paper

    > Elevator sidesteps the code-versus-data determination altogether through an application of superset disassembly [6]: we simultaneously interpret every executable byte offset in the original binary as (i) data and (ii) the start of a potential instruction sequence beginning at that offset, and we build the superset control flow graph from every one of the resulting candidate decodes. Every potential target of indirect jumps, callbacks, or other runtime dispatch mechanisms that cannot be statically analyzed therefore has a corresponding landing point in the rewritten binary. These targets are resolved at runtime through a lookup table from original instruction addresses to translated code addresses that we embed in the final binary.

    by fsmv
  • Can it handle self-modifying code?

    Why only x86_64? It has more sense to convert 32-bit programs, like many old games.

  • > Can it handle self-modifying code

    If it did, it wouldn't be "fully static" anymore. It's fundamentally contradictory.

  • Why doesn't it clean my garage also? I've got some leaves to rake as well.
  • On the greenfield x86 development side: Self-modifying code, while possible, is generally terrible because it obliterates cache lines and pipeline branch prediction performance too. And it also violates W^X so it generally has to be used in JIT-compatible memory pages. So avoid it almost always. It was kind of a thing in 486 and P5 days like using code immediates as inner loop variables, but not so much now.

    There's a lot of x86 crufty edge-cases to handle to achieve perfect(ish) emulation or translation.

  • I think self-modifying outside of JIT runtimes is a pretty rare thing these days compared to the 80s or 90s, .text sections are mostly RO these days and security requirements aren't going to decrease that.
  • Consider reading the linked article, where this is explicitly addressed:

    > Self Modifying and JIT-Compiled Code. Elevator, like all fully static binary rewriters, does not support self modifying or just-in-time-compiled code.

  • 50x isn't reasonable, it's a cache disaster. Any perf win from avoiding JIT gets eaten alive.
  • I wouldn't jump to conclusions. Instructions aren't so big anyways and they are optimized JIT by CPU.
  • This is a great case for link-time code reordering. You can put all the hot code together so the unused code will never be loaded.
    by wmf
  • Only if it is all actually used at runtime; and presumably the vast majority of possible decoding starting points won't be.
  • The certification angle is the most interesting part to me. Regulated industries (aviation, medical devices) often can't use JIT for exactly this reason, the code that runs has to be the code that was certified. Static translation that produces a signable binary is a real unlock there, code bloat notwithstanding.
  • I wonder: how relevant is this portion of the software industry? Because I’m guessing there is also no way they can apply LLms at scale, which is never discussed in the larger AI at work narrative
  • > Elevator considers all possible interpretations of every byte and produces a separate translation for each feasible one ahead of time [...] pruning only those leading to abnormal termination.

    So any real program with the possibility to crash is pruned?

  • Presumably just set to a canonical crash in the lookup table of address-to-code; which'd still get you a crash, just not that of the directly-run invalid code.
  • i really like the superset CFG idea, but the following are noteworthy for anybody planning to read the article:

    - ~4.75x runtime speed increase (significantly slower than box64, faster than QEMU), 7x executed instruction count increase, 50x binary size increase

    - emulates x86 abi until it calls out to external

    - has to emulate a large part of x86 cpu state like EFLAGS, compute complex movs individually, etc

    - only supports single-thread binaries

    - no exception handling/unwinding

    - doesnt support the full ISA

  • This is neat. I haven’t looked into it, but I would think relative offsets could still be an issue, but it seems there must be some translation layer/mmu since the codegen will be different sizes anyway. This would impact jump tables and internal branches, primarily.

    I mostly work on stuff from the 90s, but disassemblers make a lot of assumptions about where code starts and ends, but occasionally a binary blob is not discoverable unless you have some prior knowledge (pointer at a fixed location to an entry point).

    I would think after a few passes you could refine the binary into areas that are definitely code.

  • I was always curious, how do translators handle indirect jumps? When analyzing a binary we can discover only segments of code connected with direct jumps, where the destination address is known. So it means that whenever indirect jump happens, we need to find the target function, optionally translate it and jump back to translated code. Isn't it slow? Are there faster methods? Can we make translated function addresses match original functions? Or do we place jumps to translated code at original addresses?
  • The translator I made is only hobbyist quality, but I just have a big table that says “if you indirect jmp to address X then the associated block is at location Y”.

    This is slower than a direct jmp (which doesn’t use the table) but also indirect jumps were slower in the original program to begin with and typically don’t occur in performance-critical loops.