Skip to main content

nub_linker/
link.rs

1//! ELF → PVM2 (raw RV+C+custom-0 bytes) linker.
2//!
3//! Pipeline:
4//! 1. **Concatenate code sections** at their ELF vaddr offsets (typical
5//!    LLD PIE output places each function in its own `.text.<sym>`).
6//! 2. **Resolve AUIPC pairs.** *Data* references (an `auipc` paired with
7//!    a load/store/addi of a low-memory address) fold to absolute
8//!    `lui`+lo12 — data is relocated to its runtime address in
9//!    `[DATA_BASE, …)` (the ELF's `[0, extent)` data layout shifted up
10//!    by `DATA_BASE`), unrelated to where code maps. *Code* references
11//!    (`R_RISCV_CALL_PLT` and
12//!    code-targeting `PCREL_HI20`) stay native `auipc`+`jalr`/`addi`:
13//!    code is mapped at [`CODE_BASE`], so the
14//!    PC-relative computation lands on the right code VA. Kept pairs are
15//!    re-encoded after step 4 if fallthrough injection shifts the layout
16//!    (LUI is absolute, so injection-stable, and needs no fixup).
17//! 3. **Replace standard ECALL markers**. The guest convention is
18//!    `csrrw x0, 0x800/0x801, x0` followed by `ecall`; the marker slot
19//!    becomes a NOP and the `ecall` a custom-0 `ecall.jar` / `ecalli`.
20//! 4. **Inject fallthrough markers** before branch/jal/endpoint targets
21//!    that aren't already post-terminator, so the predecoder's strict
22//!    basic-block-start set — derived purely from the instruction stream
23//!    — covers every reachable jump target. `jalr` targets are validated
24//!    against that set at *runtime*; the linker never emits a trusted
25//!    target table (the recompiler runs untrusted code).
26//! 5. **Validate producer output**: no x3/x4 use, no remaining standard
27//!    `ecall` / `ebreak`, no CSR / atomic / FP / custom-1 / privileged
28//!    encodings (see `~/docs/pvm-isa/05-pvm2-rv-diff.md`). `auipc`/`jalr`
29//!    are standard PVM2 instructions and are accepted; x3/x4 are valid
30//!    runtime GPRs but outside the hot-register ABI this linker emits.
31//! 6. **Emit [`ProgramBlob`]** with the raw code bytes in
32//!    [`ProgramBlob::code`], mapped read-only at the fixed `CODE_BASE`
33//!    by the runtime. The recompiler consumes the raw bytes directly.
34
35use crate::LinkError;
36use crate::elf::parse_linked_elf;
37use nub_program::abi::{CODE_BASE, DATA_BASE, PAGE_SIZE, SP_REG};
38use nub_program::{Endpoint, ProgramBlob, Regions};
39use std::collections::BTreeMap;
40
41/// RV opcode major (low 7 bits) for AUIPC.
42const OP_AUIPC: u32 = 0b001_0111;
43/// RV opcode major for LUI.
44const OP_LUI: u32 = 0b011_0111;
45/// RV opcode major for SYSTEM (CSR ops, ECALL, EBREAK).
46const OP_SYSTEM: u32 = 0b111_0011;
47/// RV opcode major for OP-IMM (addi etc.). Used by the test module.
48#[cfg(test)]
49const OP_OP_IMM: u32 = 0b001_0011;
50/// RV opcode major for custom-0 (PVM2 host ops).
51const OP_CUSTOM_0: u32 = 0b000_1011;
52/// RV opcode major for custom-1 (PVM2 `callf`).
53const OP_CUSTOM_1: u32 = 0b010_1011;
54/// RV opcode major for JAL.
55const OP_JAL: u32 = 0b110_1111;
56/// RV opcode major for JALR.
57const OP_JALR: u32 = 0b110_0111;
58
59/// 32-bit canonical NOP: `addi x0, x0, 0`.
60const NOP_BYTES: [u8; 4] = [0x13, 0x00, 0x00, 0x00];
61
62/// PVM ecall-marker CSR numbers (custom range).
63const CSR_ECALL_JAR: u32 = 0x800;
64const CSR_ECALLI: u32 = 0x801;
65
66/// Link an RV ELF into a PVM2 [`ProgramBlob`]. [`ProgramBlob::code`]
67/// holds the raw RV+C+custom-0 bytes, mapped read-only at [`CODE_BASE`]
68/// by the runtime.
69pub fn link_elf(elf_data: &[u8]) -> Result<ProgramBlob, LinkError> {
70    let elf = parse_linked_elf(elf_data)?;
71
72    // ---- 1. Concatenate code sections ------------------------------
73    //
74    // Multi-section ELFs from lld place each function in its own
75    // `.text.<symbol>` section so dead-code elimination can drop
76    // unused ones. To keep all reloc data working unchanged, we
77    // preserve the original RV vaddr layout in the output: take the
78    // minimum vaddr as the base, allocate a buffer spanning to
79    // `max_vaddr + max_section_size`, and copy each section in at
80    // its vaddr offset. Gaps stay zero (RVC `c.illegal`); the
81    // predecoder records them as Reserved and codegen emits a panic
82    // — fine because gaps shouldn't be reached during execution.
83    if elf.code_sections.is_empty() {
84        return Err(LinkError::InvalidSection(
85            "link_elf: ELF has no code sections".into(),
86        ));
87    }
88    let mut sections_by_vaddr: Vec<&(u64, u64, Vec<u8>)> = elf.code_sections.iter().collect();
89    sections_by_vaddr.sort_by_key(|(_, v, _)| *v);
90    let base_vaddr = sections_by_vaddr[0].1;
91    let mut code_end_vaddr = base_vaddr;
92    for (_, v, d) in &sections_by_vaddr {
93        let end = v.saturating_add(d.len() as u64);
94        if end > code_end_vaddr {
95            code_end_vaddr = end;
96        }
97    }
98    let span = (code_end_vaddr - base_vaddr) as usize;
99    let mut code: Vec<u8> = vec![0u8; span];
100    for (_, v, d) in &sections_by_vaddr {
101        let off = (v - base_vaddr) as usize;
102        code[off..off + d.len()].copy_from_slice(d);
103    }
104    let code_len = code.len();
105
106    let vaddr_to_offset = |v: u64| -> Option<usize> {
107        if v < base_vaddr {
108            return None;
109        }
110        let o = (v - base_vaddr) as usize;
111        if o >= code_len { None } else { Some(o) }
112    };
113
114    let is_code_addr = |addr: u64| -> bool {
115        elf.code_ranges
116            .iter()
117            .any(|(start, end)| addr >= *start && addr < *end)
118    };
119
120    // ---- 2. Resolve AUIPC pairs ------------------------------------
121    //
122    // lld emits each `auipc rd, hi20` with `hi20` chosen so that
123    //   anchor := auipc_pc + sext(hi20 << 12)
124    // sits within ±2 KiB of the symbol; the paired LO12 instruction
125    // (load/store/addi/jalr) carries `lo12 = target - anchor`.
126    //
127    // *Data* references address the runtime `[DATA_BASE, …)` mapping
128    // absolutely, so we fold them to `lui rd, hi; <op> rd, lo12`
129    // (loading `target + DATA_BASE`; the +0x800 carry compensates
130    // lo12's sign extension). LUI is absolute — unaffected by later
131    // fallthrough injection.
132    //
133    // *Code* references stay native `auipc`/`jalr`/`addi`: code maps at
134    // `CODE_BASE`, so `auipc`'s PC-relative result is already the right
135    // code VA. We only record them here; their displacement is
136    // re-encoded in step 4b after injection settles the final offsets.
137    //
138    // `code_auipc`: auipc byte-offset → target byte-offset.
139    // `code_lo12`:  (lo12 offset, anchor-auipc offset, target offset).
140    let mut code_auipc: BTreeMap<usize, usize> = BTreeMap::new();
141    let mut code_lo12: Vec<(usize, usize, usize)> = Vec::new();
142
143    // CALL_PLT — always a code target: `auipc` + `jalr` at +4.
144    for (&call_v, &target) in &elf.call_targets {
145        let auipc_off = vaddr_to_offset(call_v).ok_or_else(|| {
146            LinkError::InvalidSection(format!(
147                "link_elf: CALL_PLT AUIPC at vaddr {call_v:#x} outside code section"
148            ))
149        })?;
150        let target_off = vaddr_to_offset(target).ok_or_else(|| {
151            LinkError::InvalidSection(format!(
152                "link_elf: CALL_PLT target {target:#x} (from {call_v:#x}) outside code section"
153            ))
154        })?;
155        expect_auipc(&code, auipc_off, call_v)?;
156        code_auipc.insert(auipc_off, target_off);
157        if let Some(jalr_off) = vaddr_to_offset(call_v + 4) {
158            code_lo12.push((jalr_off, auipc_off, target_off));
159        }
160    }
161
162    // PCREL_HI20 — code target stays native `auipc`; data folds to LUI.
163    for (&hi20_v, &target) in &elf.hi20_targets {
164        let auipc_off = vaddr_to_offset(hi20_v).ok_or_else(|| {
165            LinkError::InvalidSection(format!(
166                "link_elf: PCREL_HI20 AUIPC at vaddr {hi20_v:#x} outside code section"
167            ))
168        })?;
169        if is_code_addr(target) {
170            let target_off = vaddr_to_offset(target).ok_or_else(|| {
171                LinkError::InvalidSection(format!(
172                    "link_elf: PCREL_HI20 code target {target:#x} (from {hi20_v:#x}) out of range"
173                ))
174            })?;
175            expect_auipc(&code, auipc_off, hi20_v)?;
176            code_auipc.insert(auipc_off, target_off);
177        } else {
178            // Data target: relocate from the ELF's `[0, extent)` data
179            // layout to the runtime `[DATA_BASE, …)` mapping.
180            let data_target = target.wrapping_add(u64::from(DATA_BASE));
181            fold_auipc_to_lui(
182                &mut code,
183                auipc_off,
184                hi20_v,
185                (data_target & 0xFFFF_FFFF) as u32,
186            )?;
187        }
188    }
189
190    // PCREL_LO12 — code lo12 re-encoded in step 4b; data lo12 patched
191    // to the absolute target's low 12 bits now.
192    for (&lo_v, &target) in &elf.lo12_targets {
193        let Some(lo_off) = vaddr_to_offset(lo_v) else {
194            continue;
195        };
196        if lo_off + 4 > code.len() {
197            continue;
198        }
199        if is_code_addr(target) {
200            if let Some(&hi20_v) = elf.lo12_to_hi20.get(&lo_v)
201                && let (Some(auipc_off), Some(target_off)) =
202                    (vaddr_to_offset(hi20_v), vaddr_to_offset(target))
203            {
204                code_lo12.push((lo_off, auipc_off, target_off));
205            }
206        } else {
207            // Data target: relocate to the runtime `[DATA_BASE, …)`
208            // mapping (DATA_BASE is page-aligned, so the low 12 bits the
209            // LO12 carries are unchanged — kept explicit for clarity).
210            let data_target = target.wrapping_add(u64::from(DATA_BASE));
211            patch_lo12_abs(&mut code, lo_off, (data_target & 0xFFFF_FFFF) as u32);
212        }
213    }
214
215    // ---- 3. ECALL marker replacement -------------------------------
216    //
217    // The guest emits the same CSRRW(0x800/0x801) + ECALL sequence as
218    // the PVM path. We scan the code for those exact two-instruction
219    // sequences and rewrite them in-place:
220    //
221    //  - CSRRW x0, 0x800, x0 → NOP, then ECALL → custom-0 ecall.jar.
222    //  - CSRRW x0, 0x801, x0 → NOP, then ECALL → custom-0 ecalli imm.
223    //
224    // For ecalli, the host-call selector is in x5 (t0); we leave that
225    // intact and use ecalli with imm=0 (the actual selector flows
226    // through x5 at runtime — matching today's PVM ecalli behaviour).
227    rewrite_ecall_markers(&mut code)?;
228
229    // ---- 4. Fallthrough injection ----------------------------------
230    //
231    // Branch / jal / endpoint / rodata-code-pointer targets aren't
232    // necessarily post-terminator. Inject `fallthrough` (4 bytes,
233    // custom-0, terminator no-op) before each such target so the
234    // predecode's strict basic-block-start set — derived purely from
235    // the instruction stream, never trusted from linker metadata —
236    // covers everything reachable. `jalr` targets are validated against
237    // that set at runtime.
238    //
239    // A statically-known jalr target must be a block start too. Resume
240    // PCs (the instruction after a call's jalr) already are — jalr is a
241    // terminator, so its successor is post-terminator. But the *call
242    // targets* (CALL_PLT and code-`hi20` function entries reached via
243    // `auipc`+`jalr`) are not static jal edges, so they're fed as
244    // `extra_targets` alongside endpoint entries and `.rodata`
245    // code-pointer targets. `align_branch_targets` only injects where a
246    // target isn't already post-terminator, so entries that already
247    // follow a `ret`/`j` cost nothing.
248    let endpoint_entries_pre: Vec<usize> = {
249        match crate::elf::find_all_section_bytes(elf_data, ".nub.endpoints") {
250            Ok(sections) => sections
251                .iter()
252                .flat_map(|s| s.chunks(16))
253                .filter_map(|chunk| {
254                    if chunk.len() < 8 {
255                        return None;
256                    }
257                    let fn_ptr = u64::from_le_bytes(chunk[0..8].try_into().unwrap());
258                    if fn_ptr < base_vaddr {
259                        return None;
260                    }
261                    Some((fn_ptr - base_vaddr) as usize)
262                })
263                .collect(),
264            Err(_) => Vec::new(),
265        }
266    };
267    let rodata_targets_pre: Vec<usize> = elf
268        .abs_code_ptrs
269        .iter()
270        .filter_map(|&(_, rv_target, _)| {
271            if is_code_addr(rv_target) {
272                Some(rv_target.wrapping_sub(base_vaddr) as usize)
273            } else {
274                None
275            }
276        })
277        .collect();
278    let mut extra_targets: Vec<usize> = endpoint_entries_pre;
279    extra_targets.extend_from_slice(&rodata_targets_pre);
280    extra_targets.extend(code_auipc.values().copied());
281    let offset_map = align_branch_targets(&mut code, &extra_targets)?;
282
283    // ---- 4b. Re-encode kept code-relative pairs --------------------
284    //
285    // Injection may have shifted offsets between an `auipc` and its
286    // target, invalidating the original displacement. Recompute each
287    // kept pair's PC-relative split against the post-injection layout.
288    fixup_code_pcrel(&mut code, &offset_map, &code_auipc, &code_lo12)?;
289
290    // ---- 5. Validation pass ----------------------------------------
291    //
292    // Walk every 2- or 4-byte instruction boundary (RV+C self-describes
293    // length via op[1:0]) and reject anything that PVM2 forbids.
294    validate_pvm2(&code)?;
295
296    // ---- 5b. Rewrite code pointers in .rodata -----------------------
297    //
298    // Function pointer tables (e.g. LLVM jump tables, vtables) store
299    // code addresses as raw u32/u64 values in .rodata. The original
300    // values are ELF vaddrs; at runtime a `jalr` through such a pointer
301    // validates the target VA against the basic-block-start set, so each
302    // pointer must become `CODE_BASE + post-injection byte offset`.
303    //
304    // SUB32-based relative jump tables (entries `target - base`) are
305    // left as-is: their base register is loaded from a *data* address
306    // (the table lives in `.rodata`), so `base + delta` reconstructs the
307    // ELF vaddr, not `CODE_BASE + offset`. Such a `jalr` target fails the
308    // runtime block-start check and faults loudly rather than corrupting
309    // state — relocating relative tables into the CODE_BASE model is a
310    // follow-up (TODO). The absolute-pointer path below is correct.
311    let mut ro_data_rewritten = elf.ro_data.clone();
312    let ro_base = elf.stack_size as u64;
313    {
314        // Build a set of vaddrs handled via sub32 (so we skip them in
315        // the absolute-rewrite pass).
316        let sub32_data_vaddrs: std::collections::HashSet<u64> =
317            elf.sub32_relocs.iter().map(|(v, _)| *v).collect();
318
319        // Translate a code address (RV vaddr) to its guest VA:
320        // `CODE_BASE + post-injection byte offset within the region`.
321        let translate_code_addr = |rv_target: u64| -> u32 {
322            let pre = rv_target.wrapping_sub(base_vaddr) as usize;
323            let off = offset_map.get(&pre).copied().unwrap_or(pre);
324            CODE_BASE.wrapping_add(off as u32)
325        };
326
327        for &(data_vaddr, rv_target, size) in &elf.abs_code_ptrs {
328            if sub32_data_vaddrs.contains(&data_vaddr) {
329                // Relative entry — uniform shift preserves the diff.
330                continue;
331            }
332            if !is_code_addr(rv_target) {
333                continue;
334            }
335            if data_vaddr < ro_base {
336                continue;
337            }
338            let off = (data_vaddr - ro_base) as usize;
339            let new_val = translate_code_addr(rv_target);
340            match size {
341                4 if off + 4 <= ro_data_rewritten.len() => {
342                    ro_data_rewritten[off..off + 4].copy_from_slice(&new_val.to_le_bytes());
343                }
344                8 if off + 8 <= ro_data_rewritten.len() => {
345                    ro_data_rewritten[off..off + 8]
346                        .copy_from_slice(&(new_val as u64).to_le_bytes());
347                }
348                _ => {}
349            }
350        }
351
352        // Heuristic: 8-byte values in .rodata that look like code
353        // pointers but aren't covered by an explicit reloc.
354        let mut off = 0;
355        let already_covered: std::collections::HashSet<u64> =
356            elf.abs_code_ptrs.iter().map(|&(v, _, _)| v).collect();
357        while off + 8 <= ro_data_rewritten.len() {
358            let val = u64::from_le_bytes(ro_data_rewritten[off..off + 8].try_into().unwrap());
359            if is_code_addr(val) {
360                let vaddr = ro_base + off as u64;
361                if !already_covered.contains(&vaddr) {
362                    let new_val = translate_code_addr(val);
363                    ro_data_rewritten[off..off + 8]
364                        .copy_from_slice(&(new_val as u64).to_le_bytes());
365                }
366            }
367            off += 8;
368        }
369    }
370
371    // ---- 5c. Relocate absolute data pointers ------------------------
372    //
373    // Pointers stored in data that point *into data* (e.g. `&'static`
374    // constants in `.data.rel.ro`) hold ELF data vaddrs (the `[0,
375    // extent)` layout). The runtime maps data at `[DATA_BASE, …)`, so
376    // shift each by `+DATA_BASE`. Data-targeting abs relocs the parser
377    // captured but the code-pointer pass above ignored. A pointer that
378    // lands in neither the RO nor RW blob is unrelocatable — error
379    // loudly rather than emit a silently-wrong pointer.
380    let mut rw_data_rewritten = elf.rw_data.clone();
381    {
382        let ro_base = elf.stack_size as u64;
383        let rw_base = elf.rw_base;
384        for &(data_vaddr, target, size) in &elf.abs_data_ptrs {
385            let new_val = target.wrapping_add(u64::from(DATA_BASE));
386            let n = size as usize;
387            let bytes = new_val.to_le_bytes();
388            if data_vaddr >= ro_base
389                && (data_vaddr - ro_base) as usize + n <= ro_data_rewritten.len()
390            {
391                let off = (data_vaddr - ro_base) as usize;
392                ro_data_rewritten[off..off + n].copy_from_slice(&bytes[..n]);
393            } else if data_vaddr >= rw_base
394                && (data_vaddr - rw_base) as usize + n <= rw_data_rewritten.len()
395            {
396                let off = (data_vaddr - rw_base) as usize;
397                rw_data_rewritten[off..off + n].copy_from_slice(&bytes[..n]);
398            } else {
399                return Err(LinkError::InvalidSection(format!(
400                    "link_elf: absolute data pointer at vaddr {data_vaddr:#x} (→ {target:#x}) \
401                     falls outside the RO/RW data blobs; cannot relocate to DATA_BASE"
402                )));
403            }
404        }
405    }
406
407    // ---- 6. Endpoints -----------------------------------------------
408    //
409    // `entry_pc` stays a code-region byte offset; the runtime adds
410    // `CODE_BASE` when it seeds the PC. Remap through `offset_map` to
411    // account for any fallthrough injection.
412    let mut endpoints = read_endpoint_descriptors(elf_data, base_vaddr, code.len())?;
413    for def in endpoints.values_mut() {
414        let pre = def.entry_pc as usize;
415        if let Some(&new) = offset_map.get(&pre) {
416            def.entry_pc = new as u64;
417        }
418    }
419
420    // ---- 7. Region geometry + blob emission -------------------------
421    //
422    // Regions are page counts only; `Regions::iter()` derives the
423    // placement, stacking stack/ro/rw/heap linearly from DATA_BASE.
424    // `ProgramBlob::new` zero-extends the ro/rw buffers to whole pages
425    // and enforces the geometry bounds (code below DATA_BASE, data
426    // within the 4 GiB guest range).
427    //
428    // Code is mapped RO at the fixed `CODE_BASE` by the runtime — not
429    // via a declarative mapping, so an untrusted program cannot
430    // relocate it. Only data regions appear in the geometry.
431    let regions = Regions {
432        stack_pages: elf.stack_size / PAGE_SIZE,
433        ro_pages: (ro_data_rewritten.len() as u32).div_ceil(PAGE_SIZE),
434        rw_pages: (rw_data_rewritten.len() as u32).div_ceil(PAGE_SIZE),
435        heap_pages: elf.heap_pages,
436    };
437
438    let stack_top = regions.stack_top();
439    for def in endpoints.values_mut() {
440        def.initial_regs.insert(SP_REG, stack_top);
441    }
442
443    Ok(ProgramBlob::new(
444        code,
445        regions,
446        ro_data_rewritten,
447        rw_data_rewritten,
448        endpoints,
449    )?)
450}
451
452/// Verify the 4 bytes at `off` decode to an `auipc`; error otherwise.
453/// Used before recording a code reference whose AUIPC we keep native.
454fn expect_auipc(code: &[u8], off: usize, v: u64) -> Result<(), LinkError> {
455    if off + 4 > code.len() {
456        return Err(LinkError::InvalidSection(format!(
457            "link_elf: AUIPC reloc at vaddr {v:#x} truncated by section end"
458        )));
459    }
460    let word = u32::from_le_bytes([code[off], code[off + 1], code[off + 2], code[off + 3]]);
461    if word & 0x7F != OP_AUIPC {
462        return Err(LinkError::InvalidSection(format!(
463            "link_elf: reloc at vaddr {v:#x} not an AUIPC (opcode {:#x})",
464            word & 0x7F
465        )));
466    }
467    Ok(())
468}
469
470/// Fold a *data* `auipc rd, hi20` at `off` into `lui rd, hi` loading the
471/// absolute 4 KiB-aligned base of `eff` (the paired lo12 supplies the
472/// rest). The +0x800 carry compensates the lo12's sign extension.
473fn fold_auipc_to_lui(code: &mut [u8], off: usize, v: u64, eff: u32) -> Result<(), LinkError> {
474    if off + 4 > code.len() {
475        return Err(LinkError::InvalidSection(format!(
476            "link_elf: AUIPC reloc at vaddr {v:#x} truncated by section end"
477        )));
478    }
479    let word = u32::from_le_bytes([code[off], code[off + 1], code[off + 2], code[off + 3]]);
480    if word & 0x7F != OP_AUIPC {
481        return Err(LinkError::InvalidSection(format!(
482            "link_elf: reloc at vaddr {v:#x} not an AUIPC (opcode {:#x})",
483            word & 0x7F
484        )));
485    }
486    let rd = (word >> 7) & 0x1F;
487    let new_word = (eff.wrapping_add(0x800) & 0xFFFF_F000) | (rd << 7) | OP_LUI;
488    code[off..off + 4].copy_from_slice(&new_word.to_le_bytes());
489    Ok(())
490}
491
492/// Patch a *data* LO12 instruction (I- or S-type) with the absolute
493/// low 12 bits of `eff` (sign-extended).
494fn patch_lo12_abs(code: &mut [u8], off: usize, eff: u32) {
495    let new_lo12 = ((eff as i32) << 20) >> 20;
496    match code[off] & 0x7F {
497        // I-type (load, addi, jalr) — imm in [31:20].
498        0b0000011 | 0b0010011 | 0b1100111 => patch_imm_i(&mut code[off..off + 4], new_lo12),
499        // S-type (store) — imm[11:5] in [31:25], imm[4:0] in [11:7].
500        0b0100011 => patch_imm_s(&mut code[off..off + 4], new_lo12),
501        _ => {}
502    }
503}
504
505/// Re-encode the displacement of every kept code-relative `auipc` pair
506/// against the post-injection layout. The `auipc` carries the high 20
507/// bits (with the +0x800 carry) and the paired `jalr`/`addi`/load/store
508/// the low 12 (sign-extended), both relative to the *AUIPC's* PC.
509fn fixup_code_pcrel(
510    code: &mut [u8],
511    offset_map: &BTreeMap<usize, usize>,
512    code_auipc: &BTreeMap<usize, usize>,
513    code_lo12: &[(usize, usize, usize)],
514) -> Result<(), LinkError> {
515    let remap = |o: usize| -> Result<usize, LinkError> {
516        offset_map.get(&o).copied().ok_or_else(|| {
517            LinkError::InvalidSection(format!("fixup_code_pcrel: offset {o:#x} not in offset_map"))
518        })
519    };
520    for (&auipc_off, &target_off) in code_auipc {
521        let na = remap(auipc_off)?;
522        let nt = remap(target_off)?;
523        if na + 4 > code.len() {
524            continue;
525        }
526        let word = u32::from_le_bytes([code[na], code[na + 1], code[na + 2], code[na + 3]]);
527        if word & 0x7F != OP_AUIPC {
528            return Err(LinkError::InvalidSection(format!(
529                "fixup_code_pcrel: expected AUIPC at offset {na:#x} (opcode {:#x})",
530                word & 0x7F
531            )));
532        }
533        let disp = nt as i64 - na as i64;
534        let rd = (word >> 7) & 0x1F;
535        let new_word = ((disp as u32).wrapping_add(0x800) & 0xFFFF_F000) | (rd << 7) | OP_AUIPC;
536        code[na..na + 4].copy_from_slice(&new_word.to_le_bytes());
537    }
538    for &(lo12_off, auipc_off, target_off) in code_lo12 {
539        let nl = remap(lo12_off)?;
540        let na = remap(auipc_off)?;
541        let nt = remap(target_off)?;
542        if nl + 4 > code.len() {
543            continue;
544        }
545        let disp = nt as i64 - na as i64;
546        let new_lo12 = ((disp as i32) << 20) >> 20;
547        match code[nl] & 0x7F {
548            0b0000011 | 0b0010011 | 0b1100111 => patch_imm_i(&mut code[nl..nl + 4], new_lo12),
549            0b0100011 => patch_imm_s(&mut code[nl..nl + 4], new_lo12),
550            _ => {}
551        }
552    }
553    Ok(())
554}
555
556/// Walk `code` and rewrite ECALL-related sequences:
557///
558/// - `CSRRW(0x800) + ECALL` → `NOP + custom-0 ecall.jar`.
559/// - `CSRRW(0x801) + ECALL` → `NOP + custom-0 ecalli imm=0`.
560/// - Bare standard `ECALL` (not preceded by a marker) → custom-0
561///   `ecalli imm=0`. This mirrors the legacy fallback in the PVM
562///   transpiler (`riscv.rs`: "No marker (legacy) — treat as ecalli for
563///   backward compat").
564fn rewrite_ecall_markers(code: &mut [u8]) -> Result<(), LinkError> {
565    let n = code.len();
566    let mut i = 0;
567    while i + 2 <= n {
568        // RVC slots have op[1:0] != 11; skip them.
569        let lo = u16::from_le_bytes([code[i], code[i + 1]]);
570        if lo & 0b11 != 0b11 {
571            i += 2;
572            continue;
573        }
574        if i + 4 > n {
575            break;
576        }
577        let word = u32::from_le_bytes([code[i], code[i + 1], code[i + 2], code[i + 3]]);
578        let opcode = word & 0x7F;
579        let funct3 = (word >> 12) & 0x7;
580        if opcode == OP_SYSTEM && funct3 == 0b001 {
581            // CSRRW. Check csr field.
582            let csr = (word >> 20) & 0xFFF;
583            if csr == CSR_ECALL_JAR || csr == CSR_ECALLI {
584                code[i..i + 4].copy_from_slice(&NOP_BYTES);
585                let j = i + 4;
586                if j + 4 <= n {
587                    let nxt = u32::from_le_bytes([code[j], code[j + 1], code[j + 2], code[j + 3]]);
588                    if is_full_length(nxt) && is_standard_ecall(nxt) {
589                        let new_word = if csr == CSR_ECALL_JAR {
590                            encode_custom0_ecall_jar()
591                        } else {
592                            encode_custom0_ecalli(0)
593                        };
594                        code[j..j + 4].copy_from_slice(&new_word.to_le_bytes());
595                        i = j + 4;
596                        continue;
597                    }
598                }
599                // Marker without follow-up ECALL — pass through as NOP,
600                // keep scanning.
601                i += 4;
602                continue;
603            }
604        }
605        if opcode == OP_SYSTEM && funct3 == 0 && is_standard_ecall(word) {
606            // Bare ECALL with no preceding marker → custom-0 ecalli imm=0.
607            let new_word = encode_custom0_ecalli(0);
608            code[i..i + 4].copy_from_slice(&new_word.to_le_bytes());
609        }
610        i += 4;
611    }
612    Ok(())
613}
614
615/// Which 5-bit fields of a 4-byte RV instruction encode registers
616/// (vs. parts of an immediate). Used by [`validate_pvm2`] so we don't
617/// flag S/B-type immediates that happen to match x3/x4 as "register
618/// use".
619#[derive(Clone, Copy)]
620struct RegFields {
621    rd: bool,
622    rs1: bool,
623    rs2: bool,
624}
625const REG_NONE: RegFields = RegFields {
626    rd: false,
627    rs1: false,
628    rs2: false,
629};
630
631/// Return which fields of `w` carry register numbers, given the
632/// 7-bit major opcode.
633fn reg_fields_for(opcode: u32) -> RegFields {
634    match opcode {
635        // R-type: rd, rs1, rs2 (OP, OP-32).
636        0b011_0011 | 0b011_1011 => RegFields {
637            rd: true,
638            rs1: true,
639            rs2: true,
640        },
641        // I-type loads (LOAD).
642        0b000_0011 => RegFields {
643            rd: true,
644            rs1: true,
645            rs2: false,
646        },
647        // I-type ALU (OP-IMM, OP-IMM-32) and JALR — rd + rs1 are
648        // registers; the I-type slot holds the immediate.
649        0b001_0011 | 0b001_1011 | 0b110_0111 => RegFields {
650            rd: true,
651            rs1: true,
652            rs2: false,
653        },
654        // S-type stores: rs1, rs2 are regs; rd slot is imm[4:0].
655        0b010_0011 => RegFields {
656            rd: false,
657            rs1: true,
658            rs2: true,
659        },
660        // B-type branches: rs1, rs2 are regs; rd slot is imm.
661        0b110_0011 => RegFields {
662            rd: false,
663            rs1: true,
664            rs2: true,
665        },
666        // U-type (LUI, AUIPC): rd is reg; rs1/rs2 slots are imm.
667        0b011_0111 | 0b001_0111 => RegFields {
668            rd: true,
669            rs1: false,
670            rs2: false,
671        },
672        // J-type (JAL): rd is reg; rs1/rs2 slots are imm.
673        0b110_1111 => RegFields {
674            rd: true,
675            rs1: false,
676            rs2: false,
677        },
678        // MISC-MEM (FENCE): no registers in scope.
679        0b000_1111 => REG_NONE,
680        // custom-0 (PVM2 host ops): trap/ecall.jar/ecalli — all reg
681        // fields are zero. ecalli's imm lives in the I-type slot,
682        // so we treat it as I-type for safety (rd = x0 always).
683        0b000_1011 => RegFields {
684            rd: true,
685            rs1: true,
686            rs2: false,
687        },
688        _ => REG_NONE,
689    }
690}
691
692/// Validate that `code` contains only the producer-emitted PVM2 subset.
693///
694/// Reject: standard ECALL (not preceded by a marker — so any remaining
695/// ECALL after the rewrite pass is unaccounted for), EBREAK, CSR ops,
696/// atomics, FP/V, privileged, x16..x31, and producer-forbidden references
697/// to x3/x4. The runtime still treats x3/x4 as valid spilled RV64E GPRs.
698fn validate_pvm2(code: &[u8]) -> Result<(), LinkError> {
699    let n = code.len();
700    let mut i = 0;
701    while i < n {
702        if i + 2 > n {
703            break;
704        }
705        let lo16 = u16::from_le_bytes([code[i], code[i + 1]]);
706        if lo16 & 0b11 != 0b11 {
707            // RVC. RVC reg fields use x8..x15 (3-bit encoding), which
708            // can't reference x3/x4. RVC `c.ebreak` is allowed by RV
709            // but PVM2 wants standard ebreak rejected; c.ebreak is
710            // encoding 0x9002 — reject it explicitly.
711            if lo16 == 0x9002 {
712                return Err(LinkError::InvalidSection(format!(
713                    "link_elf: c.ebreak at offset {:#x} (forbidden)",
714                    i
715                )));
716            }
717            i += 2;
718            continue;
719        }
720        if i + 4 > n {
721            break;
722        }
723        let w = u32::from_le_bytes([code[i], code[i + 1], code[i + 2], code[i + 3]]);
724        let opcode = w & 0x7F;
725        match opcode {
726            OP_CUSTOM_1 => {
727                return Err(LinkError::InvalidSection(format!(
728                    "link_elf: custom-1 opcode at offset {:#x} is reserved in PVM2",
729                    i
730                )));
731            }
732            OP_SYSTEM => {
733                let funct3 = (w >> 12) & 0x7;
734                let csr_or_imm = (w >> 20) & 0xFFF;
735                if funct3 == 0 {
736                    return Err(LinkError::InvalidSection(format!(
737                        "link_elf: standard ECALL/EBREAK at offset {:#x} (imm={:#x})",
738                        i, csr_or_imm
739                    )));
740                }
741                return Err(LinkError::InvalidSection(format!(
742                    "link_elf: CSR op at offset {:#x} (funct3={})",
743                    i, funct3
744                )));
745            }
746            0b010_1111 => {
747                return Err(LinkError::InvalidSection(format!(
748                    "link_elf: atomic op at offset {:#x}",
749                    i
750                )));
751            }
752            0b000_0111 | 0b010_0111 => {
753                return Err(LinkError::InvalidSection(format!(
754                    "link_elf: FP load/store at offset {:#x}",
755                    i
756                )));
757            }
758            0b101_0011 => {
759                return Err(LinkError::InvalidSection(format!(
760                    "link_elf: FP arithmetic at offset {:#x}",
761                    i
762                )));
763            }
764            _ => {}
765        }
766        // Check register fields based on the instruction encoding type.
767        let rf = reg_fields_for(opcode);
768        let rd = (w >> 7) & 0x1F;
769        let rs1 = (w >> 15) & 0x1F;
770        let rs2 = (w >> 20) & 0x1F;
771        // Producer-forbidden registers: x3/x4 are valid runtime GPRs but
772        // outside the 13-hot-register ABI this linker emits. x16..x31 do not
773        // exist in RV64E and are reserved/illegal at runtime too. Kept local
774        // so the transpiler need not depend on the executor crate.
775        let check = |name: &str, r: u32| -> Result<(), LinkError> {
776            if r == 3 || r == 4 || r >= 16 {
777                return Err(LinkError::InvalidSection(format!(
778                    "link_elf: forbidden register x{} ({}) at offset {:#x}",
779                    r, name, i
780                )));
781            }
782            Ok(())
783        };
784        if rf.rd {
785            check("rd", rd)?;
786        }
787        if rf.rs1 {
788            check("rs1", rs1)?;
789        }
790        if rf.rs2 {
791            check("rs2", rs2)?;
792        }
793        i += 4;
794    }
795    Ok(())
796}
797
798/// Identify the `.nub.endpoints` section, parse its 16-byte
799/// descriptors, and resolve `fn_ptr` (RV vaddr) into an RV-byte-offset
800/// PC. The identity map `(rv_vaddr - base_vaddr) -> pc` works because
801/// the rewritten code keeps each instruction at its original offset.
802fn read_endpoint_descriptors(
803    elf_data: &[u8],
804    base_vaddr: u64,
805    code_len: usize,
806) -> Result<BTreeMap<u8, Endpoint>, LinkError> {
807    let sections = crate::elf::find_all_section_bytes(elf_data, ".nub.endpoints")?;
808    const DESCRIPTOR_SIZE: usize = 16;
809    let mut endpoints: BTreeMap<u8, Endpoint> = BTreeMap::new();
810    for section_bytes in &sections {
811        if section_bytes.len() % DESCRIPTOR_SIZE != 0 {
812            return Err(LinkError::InvalidSection(format!(
813                ".nub.endpoints size {} is not a multiple of {}",
814                section_bytes.len(),
815                DESCRIPTOR_SIZE
816            )));
817        }
818        for chunk in section_bytes.chunks(DESCRIPTOR_SIZE) {
819            let fn_ptr = u64::from_le_bytes(chunk[0..8].try_into().unwrap());
820            let index = chunk[8];
821            let arg_registers = chunk[9];
822            // Third metadata byte is opaque to the linker; a personality
823            // may interpret it (JAVM reads it as the arg-cnode size).
824            let arg_meta = chunk[10];
825            if fn_ptr < base_vaddr || fn_ptr >= base_vaddr + code_len as u64 {
826                return Err(LinkError::InvalidSection(format!(
827                    "nub_rt endpoint {} fn_ptr {:#x} outside code section",
828                    index, fn_ptr
829                )));
830            }
831            let rv_pc = fn_ptr - base_vaddr;
832            if endpoints
833                .insert(
834                    index,
835                    Endpoint {
836                        entry_pc: rv_pc,
837                        arg_registers,
838                        arg_meta,
839                        initial_regs: BTreeMap::new(),
840                    },
841                )
842                .is_some()
843            {
844                return Err(LinkError::InvalidSection(format!(
845                    "duplicate #[nub_rt::endpoint({})] declaration",
846                    index
847                )));
848            }
849        }
850    }
851    if endpoints.is_empty() {
852        return Err(LinkError::InvalidSection(
853            ".nub.endpoints section is absent or empty: \
854             the guest must declare at least one #[nub_rt::endpoint(N)]"
855                .into(),
856        ));
857    }
858    Ok(endpoints)
859}
860
861/// True iff the 32-bit RV word is a "full-length" (4-byte) instruction
862/// (bits[1:0] == 11). For 16-bit RVC instructions the same byte
863/// position has bits[1:0] != 11 in the low 16 bits.
864#[inline]
865fn is_full_length(word: u32) -> bool {
866    word & 0b11 == 0b11
867}
868
869/// Patch an I-type instruction's 12-bit imm (bits [31:20]) in place.
870/// `imm` is the signed 12-bit value; only the low 12 bits are used.
871fn patch_imm_i(slot: &mut [u8], imm: i32) {
872    let w = u32::from_le_bytes([slot[0], slot[1], slot[2], slot[3]]);
873    let cleared = w & 0x000F_FFFF;
874    let imm12 = (imm as u32) & 0xFFF;
875    let patched = cleared | (imm12 << 20);
876    slot[0..4].copy_from_slice(&patched.to_le_bytes());
877}
878
879/// Patch an S-type instruction's 12-bit imm (bits [31:25] | [11:7]).
880fn patch_imm_s(slot: &mut [u8], imm: i32) {
881    let w = u32::from_le_bytes([slot[0], slot[1], slot[2], slot[3]]);
882    let cleared = w & 0x01FF_F07F;
883    let imm12 = (imm as u32) & 0xFFF;
884    let hi7 = (imm12 >> 5) & 0x7F;
885    let lo5 = imm12 & 0x1F;
886    let patched = cleared | (hi7 << 25) | (lo5 << 7);
887    slot[0..4].copy_from_slice(&patched.to_le_bytes());
888}
889
890/// True for the standard RV `ECALL` encoding `0x00000073`.
891#[inline]
892fn is_standard_ecall(word: u32) -> bool {
893    word == 0x0000_0073
894}
895
896/// Encode custom-0 `ecall.jar`: `(funct3=001)(rest=0)`.
897#[inline]
898fn encode_custom0_ecall_jar() -> u32 {
899    // funct3 = 001 in bits [14:12]; opcode in [6:0].
900    (0b001 << 12) | OP_CUSTOM_0
901}
902
903/// Encode custom-0 `ecalli imm`: `(funct3=010)(imm[19:0])`.
904/// imm placed in bits [31:20] (12-bit signed I-type slot).
905#[inline]
906fn encode_custom0_ecalli(imm: i32) -> u32 {
907    let imm12 = (imm as u32) & 0xFFF;
908    (imm12 << 20) | (0b010 << 12) | OP_CUSTOM_0
909}
910
911/// Encode custom-0 `fallthrough` (funct3 = 100; all other fields zero).
912/// A 4-byte terminator no-op that creates a bb_start at the next byte.
913#[inline]
914fn encode_custom0_fallthrough() -> u32 {
915    (0b100 << 12) | OP_CUSTOM_0
916}
917
918/// Decode J-type immediate (sign-extended 21-bit).
919fn imm_j(w: u32) -> i32 {
920    let b20 = (w >> 31) & 1;
921    let b10_1 = (w >> 21) & 0x3FF;
922    let b11 = (w >> 20) & 1;
923    let b19_12 = (w >> 12) & 0xFF;
924    let raw = (b20 << 20) | (b19_12 << 12) | (b11 << 11) | (b10_1 << 1);
925    ((raw as i32) << 11) >> 11
926}
927
928/// Decode B-type immediate (sign-extended 13-bit).
929fn imm_b(w: u32) -> i32 {
930    let b12 = (w >> 31) & 1;
931    let b11 = (w >> 7) & 1;
932    let b10_5 = (w >> 25) & 0x3F;
933    let b4_1 = (w >> 8) & 0xF;
934    let raw = (b12 << 12) | (b11 << 11) | (b10_5 << 5) | (b4_1 << 1);
935    ((raw as i32) << 19) >> 19
936}
937
938/// Encode B-type immediate into an existing branch instruction word.
939fn encode_b_imm(opcode_and_regs: u32, imm: i32) -> u32 {
940    let v = imm as u32;
941    let b12 = (v >> 12) & 0x1;
942    let b11 = (v >> 11) & 0x1;
943    let b10_5 = (v >> 5) & 0x3F;
944    let b4_1 = (v >> 1) & 0xF;
945    // Clear the imm-bearing bits, then OR in the new ones.
946    let cleared = opcode_and_regs & 0x01FF_F07F;
947    cleared | (b12 << 31) | (b10_5 << 25) | (b4_1 << 8) | (b11 << 7)
948}
949
950/// Walk the code and inject a `fallthrough` (4 bytes) before every
951/// JAL / branch target that isn't already preceded by a terminator
952/// instruction. After injection, all reachable static targets are
953/// guaranteed to be in the strict bb_starts set the predecode computes.
954///
955/// Mutates `code` in place. Returns `old_pc → new_pc` map so the
956/// caller can remap PC values stored elsewhere (endpoint entries,
957/// `.rodata` code-pointers) consistently.
958///
959/// `extra_targets` lets the caller mark additional PCs (e.g. endpoint
960/// entries, `.rodata` code-pointer targets) as required bb_starts so
961/// they get fallthrough injection too.
962fn align_branch_targets(
963    code: &mut Vec<u8>,
964    extra_targets: &[usize],
965) -> Result<BTreeMap<usize, usize>, LinkError> {
966    // ---- Pass 1: scan instructions, identify terminators by PC ----
967    // We need to know which PCs follow a terminator (= legitimate
968    // bb_starts) so we can skip injection where it isn't needed.
969
970    // Decode each instruction at its byte offset; record:
971    //  - The set of all instruction-start byte offsets (`inst_starts`).
972    //  - The set of terminator instruction END offsets (their next_pc).
973    //  - The list of (branch_or_jal_pc, target_pc) static edges.
974    let n = code.len();
975    let mut inst_starts: Vec<usize> = Vec::with_capacity(n / 4);
976    let mut post_terminator: std::collections::HashSet<usize> = std::collections::HashSet::new();
977    post_terminator.insert(0); // PC=0 is always a bb_start.
978    let mut static_edges: Vec<(usize, usize)> = Vec::new(); // (instruction_pc, target_pc)
979
980    let mut pc: usize = 0;
981    while pc < n {
982        inst_starts.push(pc);
983        let lo = u16::from_le_bytes([code[pc], code[pc + 1]]);
984        let inst_len: usize;
985        let is_terminator: bool;
986        let target: Option<i64>;
987        if lo & 0b11 != 0b11 {
988            // Compressed (2 bytes).
989            inst_len = 2;
990            // RVC encodings that are terminators in PVM2:
991            //   c.j imm    (op=01, f3=101)        — static jump
992            //   c.beqz / c.bnez (op=01, f3=110/111) — conditional branches
993            //   c.jr (op=10, f3=100) — `jalr x0, rs1, 0` (return /
994            //     indirect jump): a terminator. c.jalr is a call (also a
995            //     terminator); c.ebreak is Reserved (panics, terminator).
996            //   c.illegal  (= 0x0000)             — reserved (terminator)
997            // Other RVC ops are non-terminators.
998            let op = lo & 0b11;
999            let f3 = (lo >> 13) & 0b111;
1000            if lo == 0 {
1001                // c.illegal: terminator.
1002                is_terminator = true;
1003                target = None;
1004            } else if op == 0b01 && f3 == 0b101 {
1005                // c.j imm — terminator, has a static target.
1006                let imm = decompress_cj_imm(lo);
1007                is_terminator = true;
1008                target = Some(pc as i64 + imm as i64);
1009            } else if op == 0b01 && (f3 == 0b110 || f3 == 0b111) {
1010                // c.beqz / c.bnez — terminators with static targets.
1011                let imm = decompress_cb_imm(lo);
1012                is_terminator = true;
1013                target = Some(pc as i64 + imm as i64);
1014            } else if op == 0b10 && f3 == 0b100 {
1015                // (op=10, f3=100) family. Discriminate by bit12 / rdrs1 / rs2:
1016                //   (0, r, 0) r!=0  → c.jr     (= retf, terminator)
1017                //   (0, r, s) both!=0 → c.mv  (NOT a terminator)
1018                //   (1, 0, 0)        → c.ebreak (Reserved, terminator)
1019                //   (1, r, 0) r!=0   → c.jalr (= callf, terminator)
1020                //   (1, r, s) both!=0 → c.add (NOT a terminator)
1021                let bit12 = (lo >> 12) & 1;
1022                let rdrs1 = (lo >> 7) & 0x1F;
1023                let rs2 = (lo >> 2) & 0x1F;
1024                // c.jr (bit12=0, rdrs1!=0, rs2=0)
1025                // c.ebreak (bit12=1, rdrs1=0, rs2=0)
1026                // c.jalr (bit12=1, rdrs1!=0, rs2=0)
1027                let is_jr_like = rs2 == 0 && (bit12 == 1 || rdrs1 != 0);
1028                is_terminator = is_jr_like;
1029                target = None;
1030            } else {
1031                is_terminator = false;
1032                target = None;
1033            }
1034        } else {
1035            // 4-byte instruction.
1036            if pc + 4 > n {
1037                break;
1038            }
1039            inst_len = 4;
1040            let w = u32::from_le_bytes([code[pc], code[pc + 1], code[pc + 2], code[pc + 3]]);
1041            let opcode = w & 0x7F;
1042            let funct3 = (w >> 12) & 0x7;
1043            match opcode {
1044                OP_JAL => {
1045                    let imm = imm_j(w);
1046                    is_terminator = true;
1047                    target = Some(pc as i64 + imm as i64);
1048                }
1049                OP_JALR => {
1050                    // jalr — return / indirect call. A terminator; its
1051                    // successor comes via the runtime dispatch table, not
1052                    // a static immediate.
1053                    is_terminator = true;
1054                    target = None;
1055                }
1056                0b110_0011 => {
1057                    // B-type branch (BEQ/BNE/etc.).
1058                    let imm = imm_b(w);
1059                    is_terminator = true;
1060                    target = Some(pc as i64 + imm as i64);
1061                }
1062                OP_CUSTOM_0 => {
1063                    // trap / ecalli / ecall.jar / fallthrough — all
1064                    // terminators with no statically-embedded successor.
1065                    is_terminator = true;
1066                    target = None;
1067                    let _ = funct3;
1068                }
1069                _ => {
1070                    is_terminator = false;
1071                    target = None;
1072                }
1073            }
1074        }
1075        let next_pc = pc + inst_len;
1076        if is_terminator && next_pc < n {
1077            post_terminator.insert(next_pc);
1078        }
1079        if let Some(t) = target
1080            && t >= 0
1081            && (t as usize) < n
1082        {
1083            static_edges.push((pc, t as usize));
1084        }
1085        pc = next_pc;
1086    }
1087
1088    // ---- Pass 2: identify targets needing fallthrough injection ----
1089    let inst_starts_set: std::collections::HashSet<usize> = inst_starts.iter().copied().collect();
1090    let mut needs_inject: std::collections::BTreeSet<usize> = std::collections::BTreeSet::new();
1091    for &(_, target) in &static_edges {
1092        if !post_terminator.contains(&target) && inst_starts_set.contains(&target) {
1093            needs_inject.insert(target);
1094        }
1095    }
1096    // Also: endpoint entries / .rodata code-pointer targets — any PC
1097    // the host (or future indirect-dispatch lowering) might enter at.
1098    // The caller passes these via `extra_targets`.
1099    for &t in extra_targets {
1100        if t < n && !post_terminator.contains(&t) && inst_starts_set.contains(&t) {
1101            needs_inject.insert(t);
1102        }
1103    }
1104
1105    if needs_inject.is_empty() {
1106        // No injections needed — old offsets are identity.
1107        let identity: BTreeMap<usize, usize> = inst_starts.into_iter().map(|p| (p, p)).collect();
1108        return Ok(identity);
1109    }
1110
1111    // ---- Pass 3: build new code with fallthrough injected ----
1112    let new_len = n + needs_inject.len() * 4;
1113    let mut new_code: Vec<u8> = Vec::with_capacity(new_len);
1114    // old_pc → new_pc (only for old instruction-start positions; mid-
1115    // instruction bytes don't get mapped).
1116    let mut offset_map: BTreeMap<usize, usize> = BTreeMap::new();
1117    let fallthrough_word = encode_custom0_fallthrough();
1118    let fallthrough_bytes = fallthrough_word.to_le_bytes();
1119
1120    let mut next_inject_iter = needs_inject.iter().peekable();
1121    let mut old_idx = 0;
1122    while old_idx < inst_starts.len() {
1123        let old_pc = inst_starts[old_idx];
1124        // If any injection is scheduled at this old_pc, emit fallthrough first.
1125        while let Some(&&inject_pc) = next_inject_iter.peek() {
1126            if inject_pc == old_pc {
1127                new_code.extend_from_slice(&fallthrough_bytes);
1128                next_inject_iter.next();
1129            } else {
1130                break;
1131            }
1132        }
1133        offset_map.insert(old_pc, new_code.len());
1134        let next_pc = inst_starts.get(old_idx + 1).copied().unwrap_or(n);
1135        let inst_len = next_pc - old_pc;
1136        new_code.extend_from_slice(&code[old_pc..old_pc + inst_len]);
1137        old_idx += 1;
1138    }
1139
1140    // ---- Pass 4: re-encode branch / jal / callf offsets in new_code ----
1141    // Iterate over OLD instruction starts (not new_code) so we never
1142    // encounter the injected fallthrough instructions during this pass.
1143    for &old_pc in &inst_starts {
1144        let new_pc = offset_map[&old_pc];
1145        let lo = u16::from_le_bytes([new_code[new_pc], new_code[new_pc + 1]]);
1146        if lo & 0b11 != 0b11 {
1147            // RVC. c.j and c.beqz/c.bnez have static targets.
1148            let op = lo & 0b11;
1149            let f3 = (lo >> 13) & 0b111;
1150            if op == 0b01 && f3 == 0b101 {
1151                let old_imm = decompress_cj_imm(lo);
1152                let old_target = (old_pc as i64 + old_imm as i64) as usize;
1153                let new_target = *offset_map.get(&old_target).ok_or_else(|| {
1154                    LinkError::InvalidSection(format!(
1155                        "align_branch_targets: c.j old target {:#x} not in offset_map",
1156                        old_target
1157                    ))
1158                })?;
1159                let new_imm = new_target as i64 - new_pc as i64;
1160                if new_imm != old_imm as i64 {
1161                    let new_h = encode_cj_imm(lo, new_imm as i32).ok_or_else(|| {
1162                        LinkError::InvalidSection(format!(
1163                            "align_branch_targets: c.j at new_pc {:#x} new_imm {} \
1164                             out of ±2 KiB range",
1165                            new_pc, new_imm
1166                        ))
1167                    })?;
1168                    new_code[new_pc..new_pc + 2].copy_from_slice(&new_h.to_le_bytes());
1169                }
1170            } else if op == 0b01 && (f3 == 0b110 || f3 == 0b111) {
1171                let old_imm = decompress_cb_imm(lo);
1172                let old_target = (old_pc as i64 + old_imm as i64) as usize;
1173                let new_target = *offset_map.get(&old_target).ok_or_else(|| {
1174                    LinkError::InvalidSection(format!(
1175                        "align_branch_targets: c.beqz/c.bnez old target {:#x} not in offset_map",
1176                        old_target
1177                    ))
1178                })?;
1179                let new_imm = new_target as i64 - new_pc as i64;
1180                if new_imm != old_imm as i64 {
1181                    let new_h = encode_cb_imm(lo, new_imm as i32).ok_or_else(|| {
1182                        LinkError::InvalidSection(format!(
1183                            "align_branch_targets: c.beqz/c.bnez at new_pc {:#x} new_imm {} \
1184                             out of ±256 byte range",
1185                            new_pc, new_imm
1186                        ))
1187                    })?;
1188                    new_code[new_pc..new_pc + 2].copy_from_slice(&new_h.to_le_bytes());
1189                }
1190            }
1191        } else {
1192            let w = u32::from_le_bytes([
1193                new_code[new_pc],
1194                new_code[new_pc + 1],
1195                new_code[new_pc + 2],
1196                new_code[new_pc + 3],
1197            ]);
1198            let opcode = w & 0x7F;
1199            match opcode {
1200                OP_JAL => {
1201                    let old_imm = imm_j(w);
1202                    let old_target = (old_pc as i64 + old_imm as i64) as usize;
1203                    if let Some(&new_target) = offset_map.get(&old_target) {
1204                        let new_imm = new_target as i64 - new_pc as i64;
1205                        if !(-(1 << 20)..(1 << 20)).contains(&new_imm) {
1206                            return Err(LinkError::InvalidSection(format!(
1207                                "align_branch_targets: JAL at new_pc {:#x} out of ±1 MiB \
1208                                 range after injection (new_imm = {})",
1209                                new_pc, new_imm
1210                            )));
1211                        }
1212                        let rd = (w >> 7) & 0x1F;
1213                        let v = new_imm as u32;
1214                        let b20 = (v >> 20) & 0x1;
1215                        let b10_1 = (v >> 1) & 0x3FF;
1216                        let b11 = (v >> 11) & 0x1;
1217                        let b19_12 = (v >> 12) & 0xFF;
1218                        let imm_field = (b20 << 31) | (b10_1 << 21) | (b11 << 20) | (b19_12 << 12);
1219                        let new_w = imm_field | (rd << 7) | OP_JAL;
1220                        new_code[new_pc..new_pc + 4].copy_from_slice(&new_w.to_le_bytes());
1221                    }
1222                }
1223                0b110_0011 => {
1224                    let old_imm = imm_b(w);
1225                    let old_target = (old_pc as i64 + old_imm as i64) as usize;
1226                    if let Some(&new_target) = offset_map.get(&old_target) {
1227                        let new_imm = new_target as i64 - new_pc as i64;
1228                        if !(-(1 << 12)..(1 << 12)).contains(&new_imm) {
1229                            return Err(LinkError::InvalidSection(format!(
1230                                "align_branch_targets: B-type branch at new_pc {:#x} out of ±4 KiB \
1231                                 range after injection (new_imm = {})",
1232                                new_pc, new_imm
1233                            )));
1234                        }
1235                        let new_w = encode_b_imm(w, new_imm as i32);
1236                        new_code[new_pc..new_pc + 4].copy_from_slice(&new_w.to_le_bytes());
1237                    }
1238                }
1239                _ => {}
1240            }
1241        }
1242    }
1243
1244    *code = new_code;
1245    Ok(offset_map)
1246}
1247
1248/// Decompress a c.j (compressed jump) into a signed byte offset.
1249/// CJ-type immediate encoding (RV unprivileged spec).
1250fn decompress_cj_imm(h: u16) -> i32 {
1251    let h = h as u32;
1252    let b11 = (h >> 12) & 0x1;
1253    let b4 = (h >> 11) & 0x1;
1254    let b9_8 = (h >> 9) & 0x3;
1255    let b10 = (h >> 8) & 0x1;
1256    let b6 = (h >> 7) & 0x1;
1257    let b7 = (h >> 6) & 0x1;
1258    let b3_1 = (h >> 3) & 0x7;
1259    let b5 = (h >> 2) & 0x1;
1260    let raw = (b11 << 11)
1261        | (b10 << 10)
1262        | (b9_8 << 8)
1263        | (b7 << 7)
1264        | (b6 << 6)
1265        | (b5 << 5)
1266        | (b4 << 4)
1267        | (b3_1 << 1);
1268    ((raw as i32) << 20) >> 20
1269}
1270
1271/// Decompress a c.beqz / c.bnez (compressed branch) into a signed byte offset.
1272fn decompress_cb_imm(h: u16) -> i32 {
1273    let h = h as u32;
1274    let b8 = (h >> 12) & 0x1;
1275    let b4_3 = (h >> 10) & 0x3;
1276    let b7_6 = (h >> 5) & 0x3;
1277    let b2_1 = (h >> 3) & 0x3;
1278    let b5 = (h >> 2) & 0x1;
1279    let raw = (b8 << 8) | (b7_6 << 6) | (b5 << 5) | (b4_3 << 3) | (b2_1 << 1);
1280    ((raw as i32) << 23) >> 23
1281}
1282
1283/// Encode a new imm into a c.beqz / c.bnez instruction, preserving
1284/// funct3 / rs1' / opcode fields. `imm` must fit in 9 bits signed
1285/// (range ±256 bytes); returns None on overflow.
1286fn encode_cb_imm(h: u16, imm: i32) -> Option<u16> {
1287    if !(-(1 << 8)..(1 << 8)).contains(&imm) {
1288        return None;
1289    }
1290    if imm & 1 != 0 {
1291        return None;
1292    }
1293    let v = imm as u32;
1294    let b8 = (v >> 8) & 0x1;
1295    let b7_6 = (v >> 6) & 0x3;
1296    let b5 = (v >> 5) & 0x1;
1297    let b4_3 = (v >> 3) & 0x3;
1298    let b2_1 = (v >> 1) & 0x3;
1299    // Preserve: bits 15:13 (funct3), bits 9:7 (rs1'), bits 1:0 (opcode).
1300    let preserved = (h as u32) & 0b1110_0011_1000_0011;
1301    let new_imm = (b8 << 12) | (b4_3 << 10) | (b7_6 << 5) | (b2_1 << 3) | (b5 << 2);
1302    Some((preserved | new_imm) as u16)
1303}
1304
1305/// Encode a new imm into a c.j instruction, preserving funct3 / opcode.
1306/// `imm` must fit in 12 bits signed (range ±2 KiB); returns None on overflow.
1307fn encode_cj_imm(h: u16, imm: i32) -> Option<u16> {
1308    if !(-(1 << 11)..(1 << 11)).contains(&imm) {
1309        return None;
1310    }
1311    if imm & 1 != 0 {
1312        return None;
1313    }
1314    let v = imm as u32;
1315    let b11 = (v >> 11) & 0x1;
1316    let b10 = (v >> 10) & 0x1;
1317    let b9_8 = (v >> 8) & 0x3;
1318    let b7 = (v >> 7) & 0x1;
1319    let b6 = (v >> 6) & 0x1;
1320    let b5 = (v >> 5) & 0x1;
1321    let b4 = (v >> 4) & 0x1;
1322    let b3_1 = (v >> 1) & 0x7;
1323    // Preserve: bits 15:13 (funct3), bits 1:0 (opcode).
1324    let preserved = (h as u32) & 0b1110_0000_0000_0011;
1325    let new_imm = (b11 << 12)
1326        | (b4 << 11)
1327        | (b9_8 << 9)
1328        | (b10 << 8)
1329        | (b6 << 7)
1330        | (b7 << 6)
1331        | (b3_1 << 3)
1332        | (b5 << 2);
1333    Some((preserved | new_imm) as u16)
1334}
1335
1336#[cfg(test)]
1337mod tests {
1338    use super::*;
1339
1340    #[test]
1341    fn nop_encoding_matches_addi_x0_x0_0() {
1342        let w = u32::from_le_bytes(NOP_BYTES);
1343        assert_eq!(w & 0x7F, OP_OP_IMM, "opcode must be OP-IMM");
1344        assert_eq!((w >> 7) & 0x1F, 0, "rd must be x0");
1345        assert_eq!((w >> 15) & 0x1F, 0, "rs1 must be x0");
1346        assert_eq!((w >> 20) & 0xFFF, 0, "imm must be 0");
1347        assert_eq!((w >> 12) & 0x7, 0, "funct3 must be 0 (ADDI)");
1348    }
1349
1350    #[test]
1351    fn custom0_ecall_jar_decodes() {
1352        let w = encode_custom0_ecall_jar();
1353        assert_eq!(w & 0x7F, OP_CUSTOM_0);
1354        assert_eq!((w >> 12) & 0x7, 0b001);
1355        // Other fields zero.
1356        assert_eq!((w >> 7) & 0x1F, 0);
1357        assert_eq!((w >> 15) & 0x1F, 0);
1358    }
1359
1360    #[test]
1361    fn custom0_ecalli_decodes() {
1362        let w = encode_custom0_ecalli(42);
1363        assert_eq!(w & 0x7F, OP_CUSTOM_0);
1364        assert_eq!((w >> 12) & 0x7, 0b010);
1365        assert_eq!((w >> 20) & 0xFFF, 42);
1366    }
1367
1368    #[test]
1369    fn custom0_fallthrough_decodes() {
1370        let w = encode_custom0_fallthrough();
1371        assert_eq!(w & 0x7F, OP_CUSTOM_0);
1372        assert_eq!((w >> 12) & 0x7, 0b100);
1373    }
1374
1375    #[test]
1376    fn cb_imm_round_trips() {
1377        // (op=01, f3=110 = beqz, rs1'=8, imm=0 placeholder) — start with a real beqz.
1378        // c.beqz x8 (rs1'=0), imm=0: f3=110, op=01, rs1'=0, all imm=0.
1379        let base = (0b110u16 << 13) | (0b01u16);
1380        for &imm in &[0, 2, -2, 4, -4, 128, -128, 254, -256] {
1381            let h = encode_cb_imm(base, imm).expect("in range");
1382            assert_eq!(
1383                decompress_cb_imm(h),
1384                imm,
1385                "round-trip failed for imm={}",
1386                imm
1387            );
1388        }
1389        assert!(encode_cb_imm(base, 256).is_none());
1390        assert!(encode_cb_imm(base, -258).is_none());
1391    }
1392
1393    #[test]
1394    fn cj_imm_round_trips() {
1395        // c.j with f3=101, op=01.
1396        let base = (0b101u16 << 13) | (0b01u16);
1397        for &imm in &[0, 2, -2, 4, -4, 512, -512, 2046, -2048] {
1398            let h = encode_cj_imm(base, imm).expect("in range");
1399            assert_eq!(
1400                decompress_cj_imm(h),
1401                imm,
1402                "round-trip failed for imm={}",
1403                imm
1404            );
1405        }
1406        assert!(encode_cj_imm(base, 2048).is_none());
1407    }
1408
1409    #[test]
1410    fn rewrite_ecall_marker_jar() {
1411        // CSRRW x0, 0x800, x0 = csr=0x800, rs1=0, funct3=1, rd=0, op=SYSTEM
1412        let csrrw = (0x800u32 << 20) | (0b001 << 12) | OP_SYSTEM;
1413        let ecall: u32 = 0x0000_0073;
1414        let mut code = Vec::new();
1415        code.extend_from_slice(&csrrw.to_le_bytes());
1416        code.extend_from_slice(&ecall.to_le_bytes());
1417        rewrite_ecall_markers(&mut code).unwrap();
1418        let w0 = u32::from_le_bytes(code[0..4].try_into().unwrap());
1419        assert_eq!(w0, u32::from_le_bytes(NOP_BYTES));
1420        let w1 = u32::from_le_bytes(code[4..8].try_into().unwrap());
1421        assert_eq!(w1, encode_custom0_ecall_jar());
1422    }
1423
1424    #[test]
1425    fn rewrite_ecall_marker_ecalli() {
1426        let csrrw = (0x801u32 << 20) | (0b001 << 12) | OP_SYSTEM;
1427        let ecall: u32 = 0x0000_0073;
1428        let mut code = Vec::new();
1429        code.extend_from_slice(&csrrw.to_le_bytes());
1430        code.extend_from_slice(&ecall.to_le_bytes());
1431        rewrite_ecall_markers(&mut code).unwrap();
1432        let w1 = u32::from_le_bytes(code[4..8].try_into().unwrap());
1433        assert_eq!(w1, encode_custom0_ecalli(0));
1434    }
1435
1436    #[test]
1437    fn validate_accepts_auipc_and_jalr() {
1438        // PVM2 now uses native RISC-V control flow: AUIPC computes a
1439        // code VA, JALR jumps to it (validated against bb_starts at
1440        // runtime). Both are accepted by the linker.
1441        let auipc = (0x1000u32 << 12) | (1 << 7) | OP_AUIPC; // auipc x1, 0x1000
1442        validate_pvm2(&auipc.to_le_bytes()).unwrap();
1443        let jalr = (1u32 << 15) | (1 << 7) | OP_JALR; // jalr x1, x1, 0
1444        validate_pvm2(&jalr.to_le_bytes()).unwrap();
1445        // The producer ABI still forbids x3/x4 even though the runtime
1446        // executes them through the spilled-register path.
1447        let jalr_x3 = (3u32 << 15) | (1 << 7) | OP_JALR; // jalr x1, x3, 0
1448        assert!(validate_pvm2(&jalr_x3.to_le_bytes()).is_err());
1449    }
1450
1451    #[test]
1452    fn validate_rejects_standard_ecall() {
1453        let code = 0x0000_0073u32.to_le_bytes().to_vec();
1454        let err = validate_pvm2(&code).unwrap_err();
1455        assert!(matches!(err, LinkError::InvalidSection(_)));
1456    }
1457
1458    #[test]
1459    fn validate_rejects_producer_x3_use() {
1460        // addi x3, x0, 0  (rd=3, rs1=0, imm=0, funct3=0, op=OP-IMM)
1461        let w = (3u32 << 7) | OP_OP_IMM;
1462        let code = w.to_le_bytes().to_vec();
1463        let err = validate_pvm2(&code).unwrap_err();
1464        let LinkError::InvalidSection(msg) = err else {
1465            panic!();
1466        };
1467        assert!(msg.contains("x3"));
1468    }
1469
1470    #[test]
1471    fn validate_accepts_clean_addi() {
1472        // addi x1, x0, 5  (rd=1, rs1=0, imm=5, funct3=0, op=OP-IMM)
1473        let w = (5u32 << 20) | (1 << 7) | OP_OP_IMM;
1474        let code = w.to_le_bytes().to_vec();
1475        validate_pvm2(&code).unwrap();
1476    }
1477
1478    #[test]
1479    fn validate_accepts_rvc() {
1480        // c.li x10, 5 = 0x4515 (h = 0x4515, low 2 bits = 01, RVC)
1481        let cli = 0x4515u16;
1482        let code = cli.to_le_bytes().to_vec();
1483        validate_pvm2(&code).unwrap();
1484    }
1485}