29.12.11-5Zcmp: PUSH/POP — Semantics, Traps & Idempotency

Part I Linux boot: optional Vol. I (Unprivileged) pp. 183–190 · ~3 min read

  • push/pop idempotency

Zcmp’s PUSH/POP group — cm.push, cm.pop, cm.popret, cm.popretz — are the first RVC instructions that are not single-instruction expansions: each executes as a sequence of stores/loads plus a stack adjustment (plus, for POPRET, a return). That makes their trap story the interesting part, and a preview of how Vol II attributes faults.

What they do

  • cm.push {reg_list}, -stack_adj — store ra and s0–sN at the top of the new frame, then move sp down.
  • cm.pop {reg_list}, stack_adj — reload the registers, then move sp up (tail-call epilogues, no return).
  • cm.popret[z] — pop, adjust, then ret; popretz also writes a0 = 0 first (the ubiquitous return 0).

The payoff is dramatic: picojpeg’s processMarkers prologue+epilogue shrinks from 60 bytes (plain RVC) to 14 bytes (-msave-restore millicode) to 4 bytes — one cm.push + one cm.popretz — while also removing the millicode call/return branches.

The software view: tearable body, atomic tail

What may tear vs what must be atomic
Sequence body (may tear)Commit group (all-or-nothing)
cm.pushStores of the register list, below the OLD spThe sp adjustment
cm.pop / cm.popret[z]Loads of the register listli a0,0 (popretz) + sp adjustment + ret
Dotted-underlined cells have explanations — click one.

Fault handling: restart, don’t resume

A trap anywhere in the sequence sets xEPC to the PUSH/POP instruction’s own PC (xTVAL gets the faulting address, xCAUSE the type), and after the handler returns the entire sequence re-executes from scratch — multiple traps across one instruction are possible. Two consequences:

  1. Idempotent memory required. The stack must tolerate repeated stores/loads. For non-idempotent regions, an implementation may use the idempotency PMA (Vol II) to detect and raise a load/store access-fault instead of misbehaving.
  2. Interrupt admission is implementation-defined — a core may make the sequence uninterruptible (simple, higher latency) or interruptible (the software view above makes it safe).

Bus-side imprecise store faults are platform territory: the core may wait for responses before the tail commits, or complete the PUSH and handle errors later.

Hardware Designer Notes

If you build an embedded Zcmp core: this is a microsequencer with one golden rule — no architectural tail-commit until the body is fault-free. The clean design points a store/load address-check pass (or simply issuing all accesses before the tail) and a restart flag instead of resumption state. Note also c.not/c.mul from Zcb complete this page range’s instruction pages — both were covered in the Zcb catalog.

Minimal Linux-boot hart MUST

  • Report xEPC = the cm.* PC for any mid-sequence trap, and restart the whole sequence on return
  • Commit the tail group (sp adjust [, li a0,0][, ret]) atomically, only when the body cannot fault
  • Guarantee ret executes once the POPRET sp adjustment commits

MAY simplify / trap-and-emulate

  • Skip Zcmp entirely on application-class cores (encoding conflict with Zcd — ch. 29.1 page)
  • Make the sequence uninterruptible, or admit interrupts (the tear rules make both legal)
  • PMA-detect non-idempotent targets and access-fault them
  • Implement as microcode/state-machine issuing ordinary load/store µops — the software view is designed for exactly that

Check yourself — PUSH/POP semantics

1.A page fault hits the 7th store of a cm.push sequence. What does the trap frame show, and what happens on return?

2.Why must the sp adjustment of cm.push commit only when the whole instruction is certain to commit?

3.Your core doesn't want to support cm.push to device memory. What does the spec sanction?

4.cm.popretz {ra, s0-s3}, 32 partially executes: s3 and s2 already loaded when an interrupt arrives. Is that architecturally visible?

4 questions