Optimized kprobes and the Breakpoint to Jump Path

A plain kprobe is implemented as a software breakpoint: the kernel overwrites the first byte of the probed instruction with int3 (0xCC on x86), and every time a CPU reaches it, the hardware raises a breakpoint exception, the kernel takes the slow exception-entry path, runs your handler, single-steps an out-of-line copy of the displaced instruction, and resumes. That trap is expensive — it serializes the pipeline and unwinds the full exception machinery on every single hit. An optimized kprobe (CONFIG_OPTPROBES, also called an optprobe or jump-optimized kprobe) eliminates the trap: when conditions allow, the kernel replaces the int3 with a 5-byte relative jump (JMP.d32) to a per-probe detour buffer (trampoline) that saves registers, calls your handler, executes the displaced instruction(s) out-of-line, and jumps back to the original code path — turning a synchronous exception into a couple of jumps (kprobes.rst, v6.12). The original patch series measured an optimized kprobe hit at ~0.06µs versus ~0.68µs/0.91µs for a plain int3 kprobe on the same hardware — roughly an order of magnitude faster (LWN/Hiramatsu 2009). Promotion is automatic and best-effort: every kprobe starts as an int3 probe and is upgraded to a jump only if a battery of safety checks pass; if not, it stays a perfectly correct (just slower) breakpoint probe.

This note assumes familiarity with the plain kprobe int3 mechanism. It explains only the optimization: why the trap is slow, what the jump replaces it with, the safety conditions, and the live-patching synchronization that makes overwriting running kernel text safe.

Why the int3 Trap Is Slow

When a CPU executes the planted 0xCC, it raises vector 3, the breakpoint exception. On x86 this is a full trap: the CPU pushes an exception frame, switches to the exception entry path, and the kernel runs kprobe_int3_handler() (core.c, v6.12). The handler then arranges to single-step an out-of-line copy of the displaced original instruction, which on x86 means a second trap (either a hardware single-step debug trap, or a second int3 placed after the copy). So an unoptimized kprobe costs, per hit, two exception round-trips plus the exception-entry/exit bookkeeping. Each exception serializes the instruction pipeline (discarding speculatively-fetched work), saves and restores a large register frame, and runs through the generic IDT dispatch. On a function called millions of times a second, that per-hit tax is the difference between “negligible” and “this probe is now my bottleneck.”

The optimization’s entire purpose is to replace the trap with a jump. A relative jump is just control flow — no exception, no pipeline serialization beyond a normal taken branch, no IDT dispatch. The handler still runs; it is reached by a call from the detour buffer instead of from the exception path.

Mental Model

Picture the probed instruction sitting at address P. A plain kprobe overwrites the first byte at P with int3 and stashes the original byte and a relocatable copy of the whole instruction. An optimized kprobe instead overwrites a 5-byte region starting at P with JMP rel32 (one opcode byte 0xE9 + a 4-byte signed displacement) that targets a freshly-allocated detour buffer. The detour buffer is a small executable trampoline, built per-probe, containing, in order: code to push the CPU registers (so it looks to your handler exactly like a trap did), a call to a small thunk that invokes your pre_handler, code to restore registers, the displaced original instruction(s) executed out-of-line, and finally a jmp back to the instruction after the optimized region in the original function. So a hit flows P → detour buffer → (handler) → run displaced insns → jmp back, never trapping.

The defining difficulty is that a 5-byte jump overwrites more than one byte — it can clobber several instructions following P. That is only safe if the kernel can prove that nothing ever jumps into the middle of those clobbered bytes (otherwise control could land on a fragment of the jump and execute garbage), and that no CPU is currently executing inside that region while you rewrite it. Those proofs are the can_optimize() safety analysis and the live-patching synchronization, and they are what most of this note is about.

flowchart TB
  subgraph BEFORE["Plain int3 kprobe (slow path)"]
    A1["CPU hits 0xCC at P"]
    A2["breakpoint EXCEPTION (trap)"]
    A3["kprobe_int3_handler:<br/>run pre_handler"]
    A4["single-step out-of-line copy<br/>(SECOND trap)"]
    A5["resume after P"]
    A1 --> A2 --> A3 --> A4 --> A5
  end
  subgraph AFTER["Optimized kprobe (jmp path)"]
    B1["CPU hits JMP.d32 at P<br/>(no trap)"]
    B2["detour buffer:<br/>push regs (emulate trap frame)"]
    B3["call optimized_callback<br/>-> opt_pre_handler (your handler)"]
    B4["restore regs"]
    B5["execute displaced original insn(s)<br/>out-of-line"]
    B6["jmp back to P + optimized_region_size"]
    B1 --> B2 --> B3 --> B4 --> B5 --> B6
  end

The two execution paths for the same probe. What it shows: the plain path takes two breakpoint exceptions (hit + single-step) through the IDT; the optimized path is pure control flow — a jmp into a detour buffer that fakes the trap frame, calls your handler, runs the displaced instructions out-of-line, and jmps back. The insight to take: the handler is identical in both cases; optimization only changes how the handler is reached, trading a synchronous exception (≈0.68µs) for a couple of jumps (≈0.06µs). The cost is that a 5-byte jump overwrites multiple instructions, which is only legal where the safety analysis proves nothing jumps into that region.

The Detour Buffer

The detour buffer is assembled from an architecture template, optprobe_template_entry…optprobe_template_end (opt.c, v6.12). On x86-64 the template, in order, pushes the segment/stack-pointer/flags and all general-purpose registers (SAVE_REGS_STRING) so the handler sees a struct pt_regs exactly as a breakpoint trap would have produced — the documentation calls this “code to push the CPU’s registers (emulating a breakpoint trap)” (kprobes.rst, v6.12). It then has placeholder slots (filled in at build time) for mov $op, %rsi (passing the struct optimized_kprobe * as the first argument) and a call optimized_callback, then restores registers and adjusts the stack. arch_prepare_optimized_kprobe() copies this template into the allocated slot and patches the placeholders, then copies the displaced original instructions after the template and appends a jmp back (opt.c, v6.12):

/* Copy arch-dep-instance from template */
memcpy(buf, optprobe_template_entry, TMPL_END_IDX);
 
/* Copy instructions into the out-of-line buffer */
ret = copy_optimized_instructions(buf + TMPL_END_IDX, op->kp.addr,
				  slot + TMPL_END_IDX);
...
op->optinsn.size = ret;
len = TMPL_END_IDX + op->optinsn.size;
 
synthesize_clac(buf + TMPL_CLAC_IDX);                 /* SMAP: clear AC if needed */
synthesize_set_arg1(buf + TMPL_MOVE_IDX, (unsigned long)op);   /* arg1 = op */
synthesize_relcall(buf + TMPL_CALL_IDX,
		   slot + TMPL_CALL_IDX, optimized_callback);  /* call handler thunk */
 
/* Set returning jmp instruction at the tail of out-of-line buffer */
synthesize_reljump(buf + len, slot + len,
		   (u8 *)op->kp.addr + op->optinsn.size);   /* jmp back to original code */
len += JMP32_INSN_SIZE;
 
/* The slot is RO executable text; install via text_poke */
text_poke(slot, buf, len);

The handler thunk it calls, optimized_callback(), is the optimized analogue of kprobe_int3_handler (opt.c, v6.12):

static void optimized_callback(struct optimized_kprobe *op, struct pt_regs *regs)
{
	if (kprobe_disabled(&op->kp))
		return;
 
	preempt_disable();
	if (kprobe_running()) {
		kprobes_inc_nmissed_count(&op->kp);   /* re-entrant hit: count, skip */
	} else {
		struct kprobe_ctlblk *kcb = get_kprobe_ctlblk();
		regs->sp += sizeof(long);             /* fix up the faked stack */
		regs->cs = __KERNEL_CS;
		regs->ip = (unsigned long)op->kp.addr + INT3_INSN_SIZE;
		regs->orig_ax = ~0UL;
 
		__this_cpu_write(current_kprobe, &op->kp);
		kcb->kprobe_status = KPROBE_HIT_ACTIVE;
		opt_pre_handler(&op->kp, regs);       /* <-- runs the user's pre_handler */
		__this_cpu_write(current_kprobe, NULL);
	}
	preempt_enable();
}

The important behavioral consequence is encoded in the line regs->ip = op->kp.addr + INT3_INSN_SIZE: the optimized path fakes what the trap would have left and runs your pre_handler, but it does not offer the single-step / post_handler semantics, and a pre_handler that rewrites regs->ip to redirect execution is ignored under optimization. The documentation is explicit: “when the probe is optimized, that modification is ignored.” If you need to change the execution path from pre_handler, you must suppress optimization (specify a non-NULL post_handler, or sysctl -w debug.kprobes_optimization=0) (kprobes.rst, v6.12).

The Safety Analysis: can_optimize()

Before a probe may be optimized, the kernel must prove the 5-byte overwrite is safe. The work is done by can_optimize() in x86’s opt.c (opt.c, v6.12). It decodes the entire enclosing function and applies these checks:

  • The whole function must be locatable. kallsyms_lookup_size_offset() must succeed; if the kernel can’t determine the function’s start and size, it bails. The 5-byte region must fit: if (size - offset < JMP32_INSN_SIZE) return 0; — there must be at least 5 bytes from the probe to the function end.
  • Not entry/exit text. Addresses in [__entry_text_start, __entry_text_end) are rejected: “Do not optimize in the entry code due to the unstable stack handling and registers setup.” The syscall/interrupt entry trampolines run with a half-set-up stack where the detour buffer’s register save would be wrong.
  • No exception-table fixup targets in the function. search_exception_tables(addr) is checked for every instruction; if any fixup code can jump into this function, it cannot be optimized — “Since some fixup code will jumps into this function, we can’t optimize kprobe in this function.” A fault fixup could land mid-jump.
  • No instruction jumps into the optimized region. This is the core proof. The kernel decodes each instruction and uses insn_is_indirect_jump() and insn_jump_into_range() to verify that no direct or indirect branch targets any byte of the region that the 5-byte jump will overwrite (other than the very first byte P). If some jcc or jmp elsewhere in the function targets, say, the 3rd byte of the optimized region, then after optimization that branch would land in the middle of the relative-jump displacement and execute nonsense.
  • Each instruction must be executable out-of-line. copy_optimized_instructions() requires every displaced instruction to be relocatable and “boostable” (can_boost), and rejects the region if it overlaps ftrace, alternatives, static-keys/jump-label, or static-call patched text (ftrace_text_reserved, alternatives_text_reserved, jump_label_text_reserved, static_call_text_reserved) — those mechanisms patch the same bytes and must not collide (opt.c, v6.12).

The documentation summarizes the same conditions in prose: the optimized region must “lie entirely within one function,” nothing may “jump into the optimized region” (no indirect jump, no fixup re-entry, no near jump except to the first byte), and “For each instruction in the optimized region, Kprobes verifies that the instruction can be executed out of line.” (kprobes.rst, v6.12). On top of can_optimize, there is a pre-optimization gate: the probe must have no post_handler, no other probe inside the optimized region, and must not be disabled — otherwise optimization is deferred (these are temporary conditions the optimizer retries).

Live-Patching Safely: the int3 Bridge and text_poke_bp

Even after proving the region is logically safe to overwrite, the kernel cannot just blast 5 bytes over running kernel text — another CPU might be executing exactly there. The solution is the same int3-bridge live-patching technique used by [[Static Keys and Code Patching|static keys and text_poke]]: patch the bytes in a sequence that is correct at every intermediate step, using the breakpoint as a temporary forwarding stub.

arch_optimize_kprobes() installs the jump with text_poke_bp() (opt.c, v6.12):

void arch_optimize_kprobes(struct list_head *oplist)
{
	struct optimized_kprobe *op, *tmp;
	u8 insn_buff[JMP32_INSN_SIZE];
 
	list_for_each_entry_safe(op, tmp, oplist, list) {
		s32 rel = (s32)((long)op->optinsn.insn -
			((long)op->kp.addr + JMP32_INSN_SIZE));
 
		/* Backup the bytes the jump's 4-byte displacement will overwrite */
		memcpy(op->optinsn.copied_insn, op->kp.addr + INT3_INSN_SIZE, DISP32_SIZE);
 
		insn_buff[0] = JMP32_INSN_OPCODE;          /* 0xE9 */
		*(s32 *)(&insn_buff[1]) = rel;             /* relative target = detour buffer */
 
		text_poke_bp(op->kp.addr, insn_buff, JMP32_INSN_SIZE, NULL);
 
		list_del_init(&op->list);
	}
}

text_poke_bp() is the int3-bridge primitive. Conceptually it patches the 5 bytes in three phases, each safe at every CPU: (1) write int3 over the first byte and issue a serializing IPI (text_poke_sync) so every CPU sees it — any CPU that hits P now traps and is forwarded to the new code via the breakpoint exception handler, which knows the in-progress emulation target; (2) write the remaining 4 bytes (the jump displacement) while the int3 still guards the first byte, so no CPU can execute the partial jump; (3) overwrite the first byte’s int3 with the real jump opcode 0xE9 and sync again. At no intermediate state can a CPU execute a malformed instruction: before step 3 the first byte is int3 (handled), and the trailing bytes are only ever written while the int3 is in place. This is exactly the mechanism described for static-key patching in Static Keys and Code Patching; optprobes reuse it.

There is one more synchronization, and it is subtle. Because the optimized region can be multiple instructions, a task could have been preempted with its instruction pointer inside the region (say, at the 2nd instruction) before optimization, then resume after the bytes were rewritten — landing in the middle of the new jump. To prevent this, the optimizer waits for a quiescence period before installing the jump. In kernel/kprobes.c, the optimizer workqueue runs (kprobes.c, v6.12):

static void kprobe_optimizer(struct work_struct *work)
{
	mutex_lock(&kprobe_mutex);
	cpus_read_lock();
	mutex_lock(&text_mutex);
 
	do_unoptimize_kprobes();        /* Step 1 */
	synchronize_rcu_tasks();        /* Step 2: wait for preempted tasks to make progress */
	do_optimize_kprobes();          /* Step 3: NOW it is safe to install the jumps */
	do_free_cleaned_kprobes();      /* Step 4 */
 
	mutex_unlock(&text_mutex);
	cpus_read_unlock();
	if (!list_empty(&optimizing_list) || !list_empty(&unoptimizing_list))
		kick_kprobe_optimizer();    /* Step 5 */
	mutex_unlock(&kprobe_mutex);
}

synchronize_rcu_tasks() is the key: an RCU-tasks grace period only completes once every task has voluntarily scheduled (or is idle/in userspace) at least once, which guarantees no task is still sitting preempted in the middle of the region being optimized. Only after that does do_optimize_kprobes() call arch_optimize_kprobes() to install the jumps. (The comment source-of-truth here is the live code; the documentation’s “Optimization” section still describes the older synchronize_sched() + stop_machine()/text_poke_smp() approach and a CONFIG_PREEMPT=n-only restriction — see the Uncertain flag below.)

Uncertain

Verify: the documentation (kprobes.rst, “Optimization”/“Unoptimization” subsections) states optprobe installation uses synchronize_sched() + stop_machine() via text_poke_smp() and “supports only kernels with CONFIG_PREEMPT=n.” The v6.12 code instead uses synchronize_rcu_tasks() + text_poke_bp() (int3-bridge), with no apparent stop_machine/CONFIG_PREEMPT=n gate in the x86 install path. Reason: the prose docs were written for the 2.6.x-era implementation and were not fully updated; the RCU-tasks + text_poke_bp path is what the source shows. To resolve: this note follows the code as authoritative (RCU-tasks + int3-bridge), and treats the doc’s stop_machine/CONFIG_PREEMPT=n claim as historical. Confirm there is no remaining CONFIG_PREEMPT=n requirement on 6.12 by enabling optprobes on a PREEMPT kernel and checking debug/kprobes-optimization. uncertain

Promotion and Demotion: the Optimizer Workqueue

Optimization is asynchronous and batched, not done inline at registration. When register_kprobe() finishes planting the int3, it calls try_to_optimize_kprobe(), which (if the pre-optimization gates pass) prepares the detour buffer and enqueues the probe on optimizing_list, then kick_kprobe_optimizer() schedules the optimizing_work delayed workqueue with a small delay (OPTIMIZE_DELAY = 5 jiffies) (kprobes.c, v6.12). Batching lets the single expensive synchronize_rcu_tasks() amortize across many probes registered close together. Until the work runs, the probe is a normal int3 kprobe; if it is hit in that window, setup_detour_execution() redirects regs->ip into the already-prepared detour buffer’s copied-instructions section, “thus at least avoiding the single-step” (kprobes.rst, v6.12).

Demotion (unoptimization) is the mirror. When an optimized probe is unregistered, disabled, or shadowed by another probe inside its region, arch_unoptimize_kprobe() restores the original bytes, but not all at once (opt.c, v6.12):

void arch_unoptimize_kprobe(struct optimized_kprobe *op)
{
	u8 new[JMP32_INSN_SIZE] = { INT3_INSN_OPCODE, };   /* int3 + saved trailing bytes */
	u8 old[JMP32_INSN_SIZE];
	u8 *addr = op->kp.addr;
 
	memcpy(old, op->kp.addr, JMP32_INSN_SIZE);
	memcpy(new + INT3_INSN_SIZE, op->optinsn.copied_insn,
	       JMP32_INSN_SIZE - INT3_INSN_SIZE);
 
	text_poke(addr, new, INT3_INSN_SIZE);                /* 1: first byte -> int3 */
	text_poke_sync();
	text_poke(addr + INT3_INSN_SIZE, new + INT3_INSN_SIZE,
		  JMP32_INSN_SIZE - INT3_INSN_SIZE);         /* 2: restore the trailing 4 bytes */
	text_poke_sync();
	...
}

The two-step is the same int3-bridge logic in reverse: write int3 over the first byte (forwarding any concurrent hit through the breakpoint handler), sync, then restore the 4 trailing bytes (the ones the jump displacement had overwritten, recovered from op->optinsn.copied_insn), sync again. The probe is left as a plain int3 kprobe — “the jump is replaced with the original code (except for an int3 breakpoint in the first byte)” (kprobes.rst, v6.12). The slow unoptimize+free path also runs through the same workqueue with its own synchronize_rcu_tasks() quiescence.

Configuration and the kprobes-optimization Toggle

CONFIG_OPTPROBES is the build-time switch; x86 selects HAVE_OPTPROBES and OPTPROBES is def_bool y when the arch supports it (x86/Kconfig, v6.12). At runtime there is a global sysctl that turns all optimization on or off:

/proc/sys/debug/kprobes-optimization

Writing 1 optimizes all eligible kprobes; writing 0 unoptimizes them all (reverting to int3). The handler is registered in kernel/kprobes.c (kprobes.c, v6.12):

static struct ctl_table kprobe_sysctls[] = {
	{
		.procname	= "kprobes-optimization",
		.data		= &sysctl_kprobes_optimization,
		.maxlen		= sizeof(int),
		.mode		= 0644,
		.proc_handler	= proc_kprobes_optimization_handler,
		.extra1		= SYSCTL_ZERO,
		.extra2		= SYSCTL_ONE,
	},
};
static void __init kprobe_sysctls_init(void)
{
	register_sysctl_init("debug", kprobe_sysctls);    /* => /proc/sys/debug/... */
}

register_sysctl_init("debug", ...) is why the path is under /proc/sys/debug/. The handler proc_kprobes_optimization_handler calls optimize_all_kprobes() or unoptimize_all_kprobes() accordingly. This is the knob you flip to force int3 semantics: set it to 0 when you need a pre_handler that rewrites regs->ip (which optimization ignores), or when debugging whether optimization itself is the cause of a problem.

Uncertain

Verify: the exact filename spelling exposed to userspace — the sysctl procname in the v6.12 source is "kprobes-optimization" (hyphen), giving /proc/sys/debug/kprobes-optimization, while older docs and the in-text debug.kprobes_optimization sysctl-dotted form use an underscore. Reason: the file path uses the procname literally (hyphen); the sysctl -w debug.kprobes_optimization=n form in kprobes.rst uses a dot+underscore. Both refer to the same knob but the literal file path is the hyphenated one per the v6.12 ctl_table. To resolve: cat /proc/sys/debug/kprobes-optimization on a 6.12 box. uncertain

The Speedup, Quantified

The original jump-optimization patch series (Masami Hiramatsu, merged for 2.6.34) published benchmarks on an Intel Xeon E5410 @ 2.33GHz (LWN/Hiramatsu 2009):

  • Plain kprobe: 0.68µs (x86-32) / 0.91µs (x86-64) per hit.
  • “Boosted” kprobe (single-step elided via out-of-line execution, an intermediate optimization): 0.27µs / 0.40µs.
  • Jump-optimized kprobe: 0.06µs / 0.06µs per hit.

That is roughly an 11× speedup on x86-32 and ~15× on x86-64 versus the plain int3 probe, and the patch summary’s own headline figure was conservatively “5 times faster than a kprobe” (Hiramatsu, systemtap list 2010). The “~10×” rule of thumb commonly quoted sits comfortably between these; the exact factor depends on the CPU’s exception-entry cost and the probed function. Kretprobes benefit too: their entry kprobe is optimizable, dropping return-probe overhead from 0.95/1.21µs to 0.30/0.35µs in the same benchmark.

Uncertain

Verify: these are 2009-era figures on a Xeon E5410. The relative speedup (jump vs trap) is mechanism-driven and still holds qualitatively on 6.12 hardware, but the absolute microsecond numbers and the exact multiplier will differ on modern CPUs (deeper pipelines raise exception cost; mitigations like retpolines/IBT and KPTI change both paths). Reason: no 6.12-era re-benchmark was located during research. To resolve: run the kernel’s samples/kprobes or a perf bench-style microbenchmark on a 6.12 box with kprobes-optimization toggled. uncertain

Failure Modes and Common Misunderstandings

  • “My pre_handler changes regs->ip but it has no effect.” The probe was optimized; the optimized path ignores regs->ip rewrites. Force int3 mode by adding a (possibly empty) post_handler or echo 0 > /proc/sys/debug/kprobes-optimization (kprobes.rst, v6.12).
  • “My kprobe didn’t get optimized.” Entirely normal and not an error — can_optimize() failed one of its checks (function too small for a 5-byte jump, a branch jumps into the region, it overlaps ftrace/alternatives/jump-label text, it’s entry code, or the probe has a post_handler). The probe still works as an int3 kprobe. You can tell which kprobes are optimized via /sys/kernel/debug/kprobes/list (the [OPTIMIZED] marker).
  • Optimization is delayed. Because it runs on a workqueue after an RCU-tasks grace period, a freshly-registered probe is not optimized for at least OPTIMIZE_DELAY jiffies plus a grace period. A microbenchmark that registers a probe and immediately measures will see int3 cost. wait_for_kprobe_optimizer() exists for tests that need to block until the queue drains.
  • Collision with other text patchers. Because optprobes share the same kernel text as ftrace, static keys, static calls, and alternatives, the safety check refuses to optimize a region those mechanisms reserve. On a function that ftrace is already instrumenting (e.g. via a kprobe_multi/fprobe attach), the plain-int3 kprobe on the same spot may be refused optimization.

Alternatives and When to Choose Them

Optimization is automatic, so “choosing” it mostly means choosing whether to allow it. The genuine alternatives are at the level of which instrumentation mechanism to use:

  • For function entry/exit on a modern BTF kernel, an fentry/fexit BPF trampoline (fentry fexit and BPF Trampolines) or kprobe_multi/ftrace-based attach is cheaper still than an optimized kprobe and avoids the per-function detour-buffer allocation — it rides the compiler’s __fentry__ nop that the dynamic ftrace mechanism already maintains. Prefer these for mass attach.
  • An optimized kprobe wins precisely where you need a probe at an arbitrary instruction offset inside a function (not just entry/exit), with low overhead — the niche only a kprobe occupies, now made fast.
  • A plain int3 kprobe is the right (and only) choice when you must rewrite the execution path from pre_handler, or when the region simply cannot be optimized.

Production Notes

In production the optimization is mostly invisible: tools like bpftrace and BCC register kprobes and let the kernel auto-optimize them, and the headline benefit is that tracing a hot kernel function with a kprobe costs tens of nanoseconds rather than hundreds, which is the difference between a tolerable and an intolerable measurement perturbation. The two things worth knowing operationally are: (1) /proc/sys/debug/kprobes-optimization is a global emergency brake — if a code-patching interaction is suspected, setting it to 0 reverts every probe to int3 without unregistering anything; and (2) the optimization shares the kernel’s single text-patching path with static keys, static calls, ftrace, and alternatives, all serialized under text_mutex and using the same int3-bridge text_poke primitive (Static Keys and Code Patching) — so a bug in any one (historically, optprobe interactions with CONFIG_RETHUNK/retpoline thunks required fixes) tends to surface as a text-patching consistency issue rather than a kprobe-specific one. The mechanism has been stable and on-by-default on x86 for over a decade; the practical caveat for kernel developers writing pre_handlers that redirect control flow remains the “optimization ignores regs->ip” gotcha.

See Also