Why does this x86_64 self-modifying loop work on bare metal / QEMU, but Segfaults only on native Linux under high CPU load? #203834
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
🏷️ Discussion TypeQuestion 💬 Feature/Topic AreaAssembly / x86_64 Architecture BodyHey everyone, I'm writing a lightweight x86_64 JIT engine, and I ran into a bizarre hardware/kernel-level issue that I can't wrap my head around. I have a small routine that dynamically patches its own immediate values inside a loop: section .textglobal_start_start:movrbx,10 ; Loop counter.loop: ; Self-modifying instruction targetmovrax,0x1122334455667788patch_target equ $ -8 ; Modify the immediate value dynamically for next iterationadd qword [rel patch_target],1decrbxjnz .loop ; Exit syscallmovrax,60xorrdi,rdisyscallThe Problem:
Is this an L1 Instruction Cache (i-cache) pipeline coherency issue where the CPU prefetches the unpatched instruction before Do I need an explicit Guidelines
|
Replies: 3 comments 1 reply
Hi @studioframes! This issue comes down to two hardware-level phenomena on modern x86_64 CPUs: Store Buffer vs. IFU race conditions and Cache-Line Tearing. 1. Store Buffer vs. IFU (Stale Data)The Instruction Fetch Unit (IFU) fetches from L1i and does not snoop the CPU's local Store Buffer. Under heavy CPU load, the updated immediate value written by 2. Cache-Line Tearing (Segfaults)Your 8-byte immediate ( Why QEMU works: QEMU (TCG) translates blocks sequentially and invalidates Translation Blocks synchronously, bypassing store buffer mechanics entirely. How to Fix ItOption A: Avoid SMC in Tight Loops (Recommended) movrax,0x1122334455667788.loop: ; ... do work ...addrax,1decrbxjnz .loopOption B: Alignment + Serialization (If SMC is required)
.loop:movrax,0x1122334455667788patch_target equ $ -8add qword [rel patch_target],1mfenceserialize ; or cpuid on older CPUsdecrbxjnz .loop |
Hi! Your self-modifying code crashes because it crosses cache boundaries. Non-atomic cross-line writes split your instruction in half. Fix 1: Modify RegistersAvoid memory writes completely by updating a register. movrax,0x1122334455667788.loop:addrax,1 ; Modify register directlydecrbxjnz .loopFix 2: Align and SerializeForce cache alignment and flush the CPU pipeline. align64 ; Keep instruction inside one cache line.loop:movrax,0x1122334455667788 patch_target equ \$ -8add qword [rel patch_target],1mfence ; Flush store bufferserialize ; Clear pipeline desyncdecrbxjnz .loop |
Hi @studioframes! Both @AntonAzer and @willsun0 nailed the root cause, but there is an additional subtle layer regarding how the instruction pointer (RIP) behaves during cross-cache-line fetches that explains why you get an actual The "Mystery" Segfault ExplainedLooking closely at your assembly snippet: .loop:movrax,0x1122334455667788patch_target equ \$ -8add qword [rel patch_target],1If the 8-byte immediate value ( However, notice that your The Why Linux vs. QEMU?
The Correct FixesOption A: Avoid SMC entirely (Best Performance)Modifying instructions in a tight loop forces an SMC Machine Clear, which destroys pipeline performance on modern x86_64 chips. Mutate the register instead: movrax,0x1122334455667788.loop: ; ... do your JIT work here ...addrax,1decrbxjnz .loopOption B: Alignment + Pipeline Serialization (If SMC is strictly required)If your JIT compiler must test self-modifying code architectures, you have to strictly enforce that the instruction stays within a single cache line, and explicitly serialize execution. On Intel/AMD, a simple section .textglobal_start_start:movrbx,10 ; Align loop entry point to clear cache boundaries .balign 64.loop:movrax,0x1122334455667788patch_target equ \$ -8add qword [rel patch_target],1 ; Serialize instruction stream & clear prefetch queuejmp .serialize_target.serialize_target: ; serialize ; Use if compiling for newer targets supporting Intel 'SERIALIZE'decrbxjnz .loop |
Hi @studioframes!
Both @AntonAzer and @willsun0 nailed the root cause, but there is an additional subtle layer regarding how the instruction pointer (RIP) behaves during cross-cache-line fetches that explains why you get an actual
SIGSEGVinstead of just a bad value insideRAX.The "Mystery" Segfault Explained
Looking closely at your assembly snippet:
If the 8-byte immediate value (
0x1122334455667788) crosses a 64-byte cache line boundary, the write is non-atomic. Under high CPU load and multi-core context switching, the Instruction Fetch Unit (IFU) can read a partially written, corrupted…