cm.popretz {reg_list}, stack_adj is the complete return 0; epilogue
in one 16-bit instruction: reload the register list, write a0 ← 0,
release the frame, ret.
Software view of cm.popretz {ra, s0-s3}, 32:
# body — may reorder, repeat, partially update before a trap
lw s3, 28(sp)
lw s2, 24(sp)
lw s1, 20(sp)
lw s0, 16(sp)
lw ra, 12(sp)
# atomic tail — all-or-nothing, only after every load is fault-free
li a0, 0
addi sp, sp, 32
ret
The three tail operations are indivisible: a0’s zero is never observable without the frame release and the return, and once the sp adjustment commits, the ret must execute.
Hardware Designer Notes
One microsequencer nuance: the reloaded ra is both a body result (a
load destination) and the tail’s jump target — the sequencer must source
the ret target from the committed reload, including on re-execution
after a mid-body trap.
Minimal Linux-boot hart MUST
- Keep li a0,0 inside the atomic tail — zeroing a0 early corrupts the return value on trap re-execution paths where a0 held a live argument
- Predict the ret through the RAS like any jalr x0, x1 (the reloaded ra is the target)
MAY simplify / trap-and-emulate
- Fuse the tail into one commit slot — nothing between the three ops is observable
Check yourself — cm.popretz
1.Why does cm.popretz exist as a separate instruction from cm.popret?
2.In cm.popretz, when may software observe a0=0 without the return having happened?