1use crate::TranspileError;
36use crate::elf::parse_linked_elf;
37use crate::layout::{
38 CODE_BASE, DATA_BASE, HEAP_CAP_INDEX, MAX_CODE_SIZE, PVM_PAGE_SIZE, ProgramLayout,
39 RO_CAP_INDEX, RW_CAP_INDEX, STACK_CAP_INDEX,
40};
41use javm_cap::Key;
42use javm_cap::abi::BARE_YIELD_RECEIVER_SLOT;
43use javm_cap::image::{EndpointDef, Image, ImageBuilder, MemoryMapping};
44use javm_cap::slot::SlotPath;
45use std::collections::BTreeMap;
46
47const SP_REG: u8 = 1;
49
50const OP_AUIPC: u32 = 0b001_0111;
52const OP_LUI: u32 = 0b011_0111;
54const OP_SYSTEM: u32 = 0b111_0011;
56#[cfg(test)]
58const OP_OP_IMM: u32 = 0b001_0011;
59const OP_CUSTOM_0: u32 = 0b000_1011;
61const OP_CUSTOM_1: u32 = 0b010_1011;
63const OP_JAL: u32 = 0b110_1111;
65const OP_JALR: u32 = 0b110_0111;
67
68const NOP_BYTES: [u8; 4] = [0x13, 0x00, 0x00, 0x00];
70
71const CSR_ECALL_JAR: u32 = 0x800;
73const CSR_ECALLI: u32 = 0x801;
74
75pub fn link_elf(elf_data: &[u8]) -> Result<Image, TranspileError> {
78 let elf = parse_linked_elf(elf_data)?;
79
80 if elf.code_sections.is_empty() {
92 return Err(TranspileError::InvalidSection(
93 "link_elf: ELF has no code sections".into(),
94 ));
95 }
96 let mut sections_by_vaddr: Vec<&(u64, u64, Vec<u8>)> = elf.code_sections.iter().collect();
97 sections_by_vaddr.sort_by_key(|(_, v, _)| *v);
98 let base_vaddr = sections_by_vaddr[0].1;
99 let mut code_end_vaddr = base_vaddr;
100 for (_, v, d) in §ions_by_vaddr {
101 let end = v.saturating_add(d.len() as u64);
102 if end > code_end_vaddr {
103 code_end_vaddr = end;
104 }
105 }
106 let span = (code_end_vaddr - base_vaddr) as usize;
107 let mut code: Vec<u8> = vec![0u8; span];
108 for (_, v, d) in §ions_by_vaddr {
109 let off = (v - base_vaddr) as usize;
110 code[off..off + d.len()].copy_from_slice(d);
111 }
112 let code_len = code.len();
113
114 let vaddr_to_offset = |v: u64| -> Option<usize> {
115 if v < base_vaddr {
116 return None;
117 }
118 let o = (v - base_vaddr) as usize;
119 if o >= code_len { None } else { Some(o) }
120 };
121
122 let is_code_addr = |addr: u64| -> bool {
123 elf.code_ranges
124 .iter()
125 .any(|(start, end)| addr >= *start && addr < *end)
126 };
127
128 let mut code_auipc: BTreeMap<usize, usize> = BTreeMap::new();
149 let mut code_lo12: Vec<(usize, usize, usize)> = Vec::new();
150
151 for (&call_v, &target) in &elf.call_targets {
153 let auipc_off = vaddr_to_offset(call_v).ok_or_else(|| {
154 TranspileError::InvalidSection(format!(
155 "link_elf: CALL_PLT AUIPC at vaddr {call_v:#x} outside code section"
156 ))
157 })?;
158 let target_off = vaddr_to_offset(target).ok_or_else(|| {
159 TranspileError::InvalidSection(format!(
160 "link_elf: CALL_PLT target {target:#x} (from {call_v:#x}) outside code section"
161 ))
162 })?;
163 expect_auipc(&code, auipc_off, call_v)?;
164 code_auipc.insert(auipc_off, target_off);
165 if let Some(jalr_off) = vaddr_to_offset(call_v + 4) {
166 code_lo12.push((jalr_off, auipc_off, target_off));
167 }
168 }
169
170 for (&hi20_v, &target) in &elf.hi20_targets {
172 let auipc_off = vaddr_to_offset(hi20_v).ok_or_else(|| {
173 TranspileError::InvalidSection(format!(
174 "link_elf: PCREL_HI20 AUIPC at vaddr {hi20_v:#x} outside code section"
175 ))
176 })?;
177 if is_code_addr(target) {
178 let target_off = vaddr_to_offset(target).ok_or_else(|| {
179 TranspileError::InvalidSection(format!(
180 "link_elf: PCREL_HI20 code target {target:#x} (from {hi20_v:#x}) out of range"
181 ))
182 })?;
183 expect_auipc(&code, auipc_off, hi20_v)?;
184 code_auipc.insert(auipc_off, target_off);
185 } else {
186 let data_target = target.wrapping_add(u64::from(DATA_BASE));
189 fold_auipc_to_lui(
190 &mut code,
191 auipc_off,
192 hi20_v,
193 (data_target & 0xFFFF_FFFF) as u32,
194 )?;
195 }
196 }
197
198 for (&lo_v, &target) in &elf.lo12_targets {
201 let Some(lo_off) = vaddr_to_offset(lo_v) else {
202 continue;
203 };
204 if lo_off + 4 > code.len() {
205 continue;
206 }
207 if is_code_addr(target) {
208 if let Some(&hi20_v) = elf.lo12_to_hi20.get(&lo_v)
209 && let (Some(auipc_off), Some(target_off)) =
210 (vaddr_to_offset(hi20_v), vaddr_to_offset(target))
211 {
212 code_lo12.push((lo_off, auipc_off, target_off));
213 }
214 } else {
215 let data_target = target.wrapping_add(u64::from(DATA_BASE));
219 patch_lo12_abs(&mut code, lo_off, (data_target & 0xFFFF_FFFF) as u32);
220 }
221 }
222
223 rewrite_ecall_markers(&mut code)?;
236
237 let endpoint_entries_pre: Vec<usize> = {
257 match crate::elf::find_all_section_bytes(elf_data, ".subsoil.endpoints") {
258 Ok(sections) => sections
259 .iter()
260 .flat_map(|s| s.chunks(16))
261 .filter_map(|chunk| {
262 if chunk.len() < 8 {
263 return None;
264 }
265 let fn_ptr = u64::from_le_bytes(chunk[0..8].try_into().unwrap());
266 if fn_ptr < base_vaddr {
267 return None;
268 }
269 Some((fn_ptr - base_vaddr) as usize)
270 })
271 .collect(),
272 Err(_) => Vec::new(),
273 }
274 };
275 let rodata_targets_pre: Vec<usize> = elf
276 .abs_code_ptrs
277 .iter()
278 .filter_map(|&(_, rv_target, _)| {
279 if is_code_addr(rv_target) {
280 Some(rv_target.wrapping_sub(base_vaddr) as usize)
281 } else {
282 None
283 }
284 })
285 .collect();
286 let mut extra_targets: Vec<usize> = endpoint_entries_pre;
287 extra_targets.extend_from_slice(&rodata_targets_pre);
288 extra_targets.extend(code_auipc.values().copied());
289 let offset_map = align_branch_targets(&mut code, &extra_targets)?;
290
291 fixup_code_pcrel(&mut code, &offset_map, &code_auipc, &code_lo12)?;
297
298 validate_pvm2(&code)?;
303
304 let mut ro_data_rewritten = elf.ro_data.clone();
320 let ro_base = elf.stack_size as u64;
321 {
322 let sub32_data_vaddrs: std::collections::HashSet<u64> =
325 elf.sub32_relocs.iter().map(|(v, _)| *v).collect();
326
327 let translate_code_addr = |rv_target: u64| -> u32 {
330 let pre = rv_target.wrapping_sub(base_vaddr) as usize;
331 let off = offset_map.get(&pre).copied().unwrap_or(pre);
332 CODE_BASE.wrapping_add(off as u32)
333 };
334
335 for &(data_vaddr, rv_target, size) in &elf.abs_code_ptrs {
336 if sub32_data_vaddrs.contains(&data_vaddr) {
337 continue;
339 }
340 if !is_code_addr(rv_target) {
341 continue;
342 }
343 if data_vaddr < ro_base {
344 continue;
345 }
346 let off = (data_vaddr - ro_base) as usize;
347 let new_val = translate_code_addr(rv_target);
348 match size {
349 4 if off + 4 <= ro_data_rewritten.len() => {
350 ro_data_rewritten[off..off + 4].copy_from_slice(&new_val.to_le_bytes());
351 }
352 8 if off + 8 <= ro_data_rewritten.len() => {
353 ro_data_rewritten[off..off + 8]
354 .copy_from_slice(&(new_val as u64).to_le_bytes());
355 }
356 _ => {}
357 }
358 }
359
360 let mut off = 0;
363 let already_covered: std::collections::HashSet<u64> =
364 elf.abs_code_ptrs.iter().map(|&(v, _, _)| v).collect();
365 while off + 8 <= ro_data_rewritten.len() {
366 let val = u64::from_le_bytes(ro_data_rewritten[off..off + 8].try_into().unwrap());
367 if is_code_addr(val) {
368 let vaddr = ro_base + off as u64;
369 if !already_covered.contains(&vaddr) {
370 let new_val = translate_code_addr(val);
371 ro_data_rewritten[off..off + 8]
372 .copy_from_slice(&(new_val as u64).to_le_bytes());
373 }
374 }
375 off += 8;
376 }
377 }
378
379 let mut rw_data_rewritten = elf.rw_data.clone();
389 {
390 let ro_base = elf.stack_size as u64;
391 let rw_base = elf.rw_base;
392 for &(data_vaddr, target, size) in &elf.abs_data_ptrs {
393 let new_val = target.wrapping_add(u64::from(DATA_BASE));
394 let n = size as usize;
395 let bytes = new_val.to_le_bytes();
396 if data_vaddr >= ro_base
397 && (data_vaddr - ro_base) as usize + n <= ro_data_rewritten.len()
398 {
399 let off = (data_vaddr - ro_base) as usize;
400 ro_data_rewritten[off..off + n].copy_from_slice(&bytes[..n]);
401 } else if data_vaddr >= rw_base
402 && (data_vaddr - rw_base) as usize + n <= rw_data_rewritten.len()
403 {
404 let off = (data_vaddr - rw_base) as usize;
405 rw_data_rewritten[off..off + n].copy_from_slice(&bytes[..n]);
406 } else {
407 return Err(TranspileError::InvalidSection(format!(
408 "link_elf: absolute data pointer at vaddr {data_vaddr:#x} (→ {target:#x}) \
409 falls outside the RO/RW data blobs; cannot relocate to DATA_BASE"
410 )));
411 }
412 }
413 }
414
415 let mut endpoints = read_subsoil_endpoints_rv(elf_data, base_vaddr, code.len())?;
421 for def in endpoints.values_mut() {
422 let pre = def.entry_pc as usize;
423 if let Some(&new) = offset_map.get(&pre) {
424 def.entry_pc = new as u64;
425 }
426 }
427
428 let ro_data = ro_data_rewritten;
430 let rw_data = rw_data_rewritten;
431
432 let stack_pages = elf.stack_size / PVM_PAGE_SIZE;
433 let ro_pages = (ro_data.len() as u32).div_ceil(PVM_PAGE_SIZE);
434 let rw_pages = (rw_data.len() as u32).div_ceil(PVM_PAGE_SIZE);
435 let layout = ProgramLayout::compute(stack_pages, ro_pages, rw_pages, elf.heap_pages);
436 let stack_top = layout.stack_top();
437
438 for def in endpoints.values_mut() {
439 def.initial_regs.insert(SP_REG, stack_top);
440 }
441
442 let mut memory_mappings: Vec<MemoryMapping> = Vec::new();
451 let page_bytes = u64::from(PVM_PAGE_SIZE);
452 let mut builder = ImageBuilder::new();
453
454 let stack_slot = Key::from(STACK_CAP_INDEX);
455 let stack_size = u64::from(layout.stack.page_count) * page_bytes;
456 memory_mappings.push(MemoryMapping {
457 start: u64::from(layout.stack.base_page) * page_bytes,
458 size: stack_size,
459 source: SlotPath::root(stack_slot.clone()),
460 });
461 builder = builder.initial_data(stack_slot, Vec::new(), stack_size);
462
463 if let Some(ro) = &layout.ro {
464 let ro_slot = Key::from(RO_CAP_INDEX);
465 let size = u64::from(ro.page_count) * page_bytes;
466 memory_mappings.push(MemoryMapping {
467 start: u64::from(ro.base_page) * page_bytes,
468 size,
469 source: SlotPath::root(ro_slot.clone()),
470 });
471 builder = builder.pinned_data(ro_slot, ro_data, size);
472 }
473
474 if let Some(rw) = &layout.rw {
475 let rw_slot = Key::from(RW_CAP_INDEX);
476 let size = u64::from(rw.page_count) * page_bytes;
477 memory_mappings.push(MemoryMapping {
478 start: u64::from(rw.base_page) * page_bytes,
479 size,
480 source: SlotPath::root(rw_slot.clone()),
481 });
482 builder = builder.initial_data(rw_slot, rw_data, size);
483 }
484
485 if let Some(heap) = &layout.heap {
486 let heap_slot = Key::from(HEAP_CAP_INDEX);
487 let size = u64::from(heap.page_count) * page_bytes;
488 memory_mappings.push(MemoryMapping {
489 start: u64::from(heap.base_page) * page_bytes,
490 size,
491 source: SlotPath::root(heap_slot.clone()),
492 });
493 builder = builder.initial_data(heap_slot, Vec::new(), size);
494 }
495
496 let code_base = u64::from(CODE_BASE);
501 let code_size = (code.len() as u64).div_ceil(page_bytes) * page_bytes;
502 if code_base + code_size > u64::from(DATA_BASE) {
503 return Err(TranspileError::InvalidSection(format!(
504 "link_elf: code size {code_size:#x} exceeds MAX_CODE_SIZE {:#x} (would overlap DATA_BASE {:#x})",
505 MAX_CODE_SIZE, DATA_BASE,
506 )));
507 }
508 let data_end = u64::from(DATA_BASE) + u64::from(layout.total_data_pages()) * page_bytes;
509 if data_end > (1u64 << 32) {
510 return Err(TranspileError::InvalidSection(format!(
511 "link_elf: data end {data_end:#x} exceeds the 4 GiB guest range"
512 )));
513 }
514 builder = builder.code(code);
519 for (key, ep) in endpoints {
520 builder = builder.endpoint(key, ep);
521 }
522 for mapping in memory_mappings {
523 builder = builder.mapping(mapping);
524 }
525 Ok(builder
526 .yield_receiver_slot(Some(Key::from(BARE_YIELD_RECEIVER_SLOT)))
527 .build())
528}
529
530fn expect_auipc(code: &[u8], off: usize, v: u64) -> Result<(), TranspileError> {
533 if off + 4 > code.len() {
534 return Err(TranspileError::InvalidSection(format!(
535 "link_elf: AUIPC reloc at vaddr {v:#x} truncated by section end"
536 )));
537 }
538 let word = u32::from_le_bytes([code[off], code[off + 1], code[off + 2], code[off + 3]]);
539 if word & 0x7F != OP_AUIPC {
540 return Err(TranspileError::InvalidSection(format!(
541 "link_elf: reloc at vaddr {v:#x} not an AUIPC (opcode {:#x})",
542 word & 0x7F
543 )));
544 }
545 Ok(())
546}
547
548fn fold_auipc_to_lui(code: &mut [u8], off: usize, v: u64, eff: u32) -> Result<(), TranspileError> {
552 if off + 4 > code.len() {
553 return Err(TranspileError::InvalidSection(format!(
554 "link_elf: AUIPC reloc at vaddr {v:#x} truncated by section end"
555 )));
556 }
557 let word = u32::from_le_bytes([code[off], code[off + 1], code[off + 2], code[off + 3]]);
558 if word & 0x7F != OP_AUIPC {
559 return Err(TranspileError::InvalidSection(format!(
560 "link_elf: reloc at vaddr {v:#x} not an AUIPC (opcode {:#x})",
561 word & 0x7F
562 )));
563 }
564 let rd = (word >> 7) & 0x1F;
565 let new_word = (eff.wrapping_add(0x800) & 0xFFFF_F000) | (rd << 7) | OP_LUI;
566 code[off..off + 4].copy_from_slice(&new_word.to_le_bytes());
567 Ok(())
568}
569
570fn patch_lo12_abs(code: &mut [u8], off: usize, eff: u32) {
573 let new_lo12 = ((eff as i32) << 20) >> 20;
574 match code[off] & 0x7F {
575 0b0000011 | 0b0010011 | 0b1100111 => patch_imm_i(&mut code[off..off + 4], new_lo12),
577 0b0100011 => patch_imm_s(&mut code[off..off + 4], new_lo12),
579 _ => {}
580 }
581}
582
583fn fixup_code_pcrel(
588 code: &mut [u8],
589 offset_map: &BTreeMap<usize, usize>,
590 code_auipc: &BTreeMap<usize, usize>,
591 code_lo12: &[(usize, usize, usize)],
592) -> Result<(), TranspileError> {
593 let remap = |o: usize| -> Result<usize, TranspileError> {
594 offset_map.get(&o).copied().ok_or_else(|| {
595 TranspileError::InvalidSection(format!(
596 "fixup_code_pcrel: offset {o:#x} not in offset_map"
597 ))
598 })
599 };
600 for (&auipc_off, &target_off) in code_auipc {
601 let na = remap(auipc_off)?;
602 let nt = remap(target_off)?;
603 if na + 4 > code.len() {
604 continue;
605 }
606 let word = u32::from_le_bytes([code[na], code[na + 1], code[na + 2], code[na + 3]]);
607 if word & 0x7F != OP_AUIPC {
608 return Err(TranspileError::InvalidSection(format!(
609 "fixup_code_pcrel: expected AUIPC at offset {na:#x} (opcode {:#x})",
610 word & 0x7F
611 )));
612 }
613 let disp = nt as i64 - na as i64;
614 let rd = (word >> 7) & 0x1F;
615 let new_word = ((disp as u32).wrapping_add(0x800) & 0xFFFF_F000) | (rd << 7) | OP_AUIPC;
616 code[na..na + 4].copy_from_slice(&new_word.to_le_bytes());
617 }
618 for &(lo12_off, auipc_off, target_off) in code_lo12 {
619 let nl = remap(lo12_off)?;
620 let na = remap(auipc_off)?;
621 let nt = remap(target_off)?;
622 if nl + 4 > code.len() {
623 continue;
624 }
625 let disp = nt as i64 - na as i64;
626 let new_lo12 = ((disp as i32) << 20) >> 20;
627 match code[nl] & 0x7F {
628 0b0000011 | 0b0010011 | 0b1100111 => patch_imm_i(&mut code[nl..nl + 4], new_lo12),
629 0b0100011 => patch_imm_s(&mut code[nl..nl + 4], new_lo12),
630 _ => {}
631 }
632 }
633 Ok(())
634}
635
636fn rewrite_ecall_markers(code: &mut [u8]) -> Result<(), TranspileError> {
645 let n = code.len();
646 let mut i = 0;
647 while i + 2 <= n {
648 let lo = u16::from_le_bytes([code[i], code[i + 1]]);
650 if lo & 0b11 != 0b11 {
651 i += 2;
652 continue;
653 }
654 if i + 4 > n {
655 break;
656 }
657 let word = u32::from_le_bytes([code[i], code[i + 1], code[i + 2], code[i + 3]]);
658 let opcode = word & 0x7F;
659 let funct3 = (word >> 12) & 0x7;
660 if opcode == OP_SYSTEM && funct3 == 0b001 {
661 let csr = (word >> 20) & 0xFFF;
663 if csr == CSR_ECALL_JAR || csr == CSR_ECALLI {
664 code[i..i + 4].copy_from_slice(&NOP_BYTES);
665 let j = i + 4;
666 if j + 4 <= n {
667 let nxt = u32::from_le_bytes([code[j], code[j + 1], code[j + 2], code[j + 3]]);
668 if is_full_length(nxt) && is_standard_ecall(nxt) {
669 let new_word = if csr == CSR_ECALL_JAR {
670 encode_custom0_ecall_jar()
671 } else {
672 encode_custom0_ecalli(0)
673 };
674 code[j..j + 4].copy_from_slice(&new_word.to_le_bytes());
675 i = j + 4;
676 continue;
677 }
678 }
679 i += 4;
682 continue;
683 }
684 }
685 if opcode == OP_SYSTEM && funct3 == 0 && is_standard_ecall(word) {
686 let new_word = encode_custom0_ecalli(0);
688 code[i..i + 4].copy_from_slice(&new_word.to_le_bytes());
689 }
690 i += 4;
691 }
692 Ok(())
693}
694
695#[derive(Clone, Copy)]
700struct RegFields {
701 rd: bool,
702 rs1: bool,
703 rs2: bool,
704}
705const REG_NONE: RegFields = RegFields {
706 rd: false,
707 rs1: false,
708 rs2: false,
709};
710
711fn reg_fields_for(opcode: u32) -> RegFields {
714 match opcode {
715 0b011_0011 | 0b011_1011 => RegFields {
717 rd: true,
718 rs1: true,
719 rs2: true,
720 },
721 0b000_0011 => RegFields {
723 rd: true,
724 rs1: true,
725 rs2: false,
726 },
727 0b001_0011 | 0b001_1011 | 0b110_0111 => RegFields {
730 rd: true,
731 rs1: true,
732 rs2: false,
733 },
734 0b010_0011 => RegFields {
736 rd: false,
737 rs1: true,
738 rs2: true,
739 },
740 0b110_0011 => RegFields {
742 rd: false,
743 rs1: true,
744 rs2: true,
745 },
746 0b011_0111 | 0b001_0111 => RegFields {
748 rd: true,
749 rs1: false,
750 rs2: false,
751 },
752 0b110_1111 => RegFields {
754 rd: true,
755 rs1: false,
756 rs2: false,
757 },
758 0b000_1111 => REG_NONE,
760 0b000_1011 => RegFields {
764 rd: true,
765 rs1: true,
766 rs2: false,
767 },
768 _ => REG_NONE,
769 }
770}
771
772fn validate_pvm2(code: &[u8]) -> Result<(), TranspileError> {
779 let n = code.len();
780 let mut i = 0;
781 while i < n {
782 if i + 2 > n {
783 break;
784 }
785 let lo16 = u16::from_le_bytes([code[i], code[i + 1]]);
786 if lo16 & 0b11 != 0b11 {
787 if lo16 == 0x9002 {
792 return Err(TranspileError::InvalidSection(format!(
793 "link_elf: c.ebreak at offset {:#x} (forbidden)",
794 i
795 )));
796 }
797 i += 2;
798 continue;
799 }
800 if i + 4 > n {
801 break;
802 }
803 let w = u32::from_le_bytes([code[i], code[i + 1], code[i + 2], code[i + 3]]);
804 let opcode = w & 0x7F;
805 match opcode {
806 OP_CUSTOM_1 => {
807 return Err(TranspileError::InvalidSection(format!(
808 "link_elf: custom-1 opcode at offset {:#x} is reserved in PVM2",
809 i
810 )));
811 }
812 OP_SYSTEM => {
813 let funct3 = (w >> 12) & 0x7;
814 let csr_or_imm = (w >> 20) & 0xFFF;
815 if funct3 == 0 {
816 return Err(TranspileError::InvalidSection(format!(
817 "link_elf: standard ECALL/EBREAK at offset {:#x} (imm={:#x})",
818 i, csr_or_imm
819 )));
820 }
821 return Err(TranspileError::InvalidSection(format!(
822 "link_elf: CSR op at offset {:#x} (funct3={})",
823 i, funct3
824 )));
825 }
826 0b010_1111 => {
827 return Err(TranspileError::InvalidSection(format!(
828 "link_elf: atomic op at offset {:#x}",
829 i
830 )));
831 }
832 0b000_0111 | 0b010_0111 => {
833 return Err(TranspileError::InvalidSection(format!(
834 "link_elf: FP load/store at offset {:#x}",
835 i
836 )));
837 }
838 0b101_0011 => {
839 return Err(TranspileError::InvalidSection(format!(
840 "link_elf: FP arithmetic at offset {:#x}",
841 i
842 )));
843 }
844 _ => {}
845 }
846 let rf = reg_fields_for(opcode);
848 let rd = (w >> 7) & 0x1F;
849 let rs1 = (w >> 15) & 0x1F;
850 let rs2 = (w >> 20) & 0x1F;
851 let check = |name: &str, r: u32| -> Result<(), TranspileError> {
856 if r == 3 || r == 4 || r >= 16 {
857 return Err(TranspileError::InvalidSection(format!(
858 "link_elf: forbidden register x{} ({}) at offset {:#x}",
859 r, name, i
860 )));
861 }
862 Ok(())
863 };
864 if rf.rd {
865 check("rd", rd)?;
866 }
867 if rf.rs1 {
868 check("rs1", rs1)?;
869 }
870 if rf.rs2 {
871 check("rs2", rs2)?;
872 }
873 i += 4;
874 }
875 Ok(())
876}
877
878fn read_subsoil_endpoints_rv(
883 elf_data: &[u8],
884 base_vaddr: u64,
885 code_len: usize,
886) -> Result<BTreeMap<Key, EndpointDef>, TranspileError> {
887 let sections = crate::elf::find_all_section_bytes(elf_data, ".subsoil.endpoints")?;
888 const DESCRIPTOR_SIZE: usize = 16;
889 let mut endpoints: BTreeMap<Key, EndpointDef> = BTreeMap::new();
892 for section_bytes in §ions {
893 if section_bytes.len() % DESCRIPTOR_SIZE != 0 {
894 return Err(TranspileError::InvalidSection(format!(
895 ".subsoil.endpoints size {} is not a multiple of {}",
896 section_bytes.len(),
897 DESCRIPTOR_SIZE
898 )));
899 }
900 for chunk in section_bytes.chunks(DESCRIPTOR_SIZE) {
901 let fn_ptr = u64::from_le_bytes(chunk[0..8].try_into().unwrap());
902 let index = chunk[8];
903 let arg_registers = chunk[9];
904 let arg_cnode_size = chunk[10];
905 if fn_ptr < base_vaddr || fn_ptr >= base_vaddr + code_len as u64 {
906 return Err(TranspileError::InvalidSection(format!(
907 "subsoil endpoint {} fn_ptr {:#x} outside code section",
908 index, fn_ptr
909 )));
910 }
911 let rv_pc = fn_ptr - base_vaddr;
912 if endpoints
913 .insert(
914 Key::from(index),
915 EndpointDef {
916 entry_pc: rv_pc,
917 arg_registers,
918 arg_cnode_size,
919 initial_regs: BTreeMap::new(),
920 },
921 )
922 .is_some()
923 {
924 return Err(TranspileError::InvalidSection(format!(
925 "duplicate #[subsoil::endpoint({})] declaration",
926 index
927 )));
928 }
929 }
930 }
931 if endpoints.is_empty() {
932 return Err(TranspileError::InvalidSection(
933 ".subsoil.endpoints section is absent or empty: \
934 the guest must declare at least one #[subsoil::endpoint(N)]"
935 .into(),
936 ));
937 }
938 Ok(endpoints)
939}
940
941#[inline]
945fn is_full_length(word: u32) -> bool {
946 word & 0b11 == 0b11
947}
948
949fn patch_imm_i(slot: &mut [u8], imm: i32) {
952 let w = u32::from_le_bytes([slot[0], slot[1], slot[2], slot[3]]);
953 let cleared = w & 0x000F_FFFF;
954 let imm12 = (imm as u32) & 0xFFF;
955 let patched = cleared | (imm12 << 20);
956 slot[0..4].copy_from_slice(&patched.to_le_bytes());
957}
958
959fn patch_imm_s(slot: &mut [u8], imm: i32) {
961 let w = u32::from_le_bytes([slot[0], slot[1], slot[2], slot[3]]);
962 let cleared = w & 0x01FF_F07F;
963 let imm12 = (imm as u32) & 0xFFF;
964 let hi7 = (imm12 >> 5) & 0x7F;
965 let lo5 = imm12 & 0x1F;
966 let patched = cleared | (hi7 << 25) | (lo5 << 7);
967 slot[0..4].copy_from_slice(&patched.to_le_bytes());
968}
969
970#[inline]
972fn is_standard_ecall(word: u32) -> bool {
973 word == 0x0000_0073
974}
975
976#[inline]
978fn encode_custom0_ecall_jar() -> u32 {
979 (0b001 << 12) | OP_CUSTOM_0
981}
982
983#[inline]
986fn encode_custom0_ecalli(imm: i32) -> u32 {
987 let imm12 = (imm as u32) & 0xFFF;
988 (imm12 << 20) | (0b010 << 12) | OP_CUSTOM_0
989}
990
991#[inline]
994fn encode_custom0_fallthrough() -> u32 {
995 (0b100 << 12) | OP_CUSTOM_0
996}
997
998fn imm_j(w: u32) -> i32 {
1000 let b20 = (w >> 31) & 1;
1001 let b10_1 = (w >> 21) & 0x3FF;
1002 let b11 = (w >> 20) & 1;
1003 let b19_12 = (w >> 12) & 0xFF;
1004 let raw = (b20 << 20) | (b19_12 << 12) | (b11 << 11) | (b10_1 << 1);
1005 ((raw as i32) << 11) >> 11
1006}
1007
1008fn imm_b(w: u32) -> i32 {
1010 let b12 = (w >> 31) & 1;
1011 let b11 = (w >> 7) & 1;
1012 let b10_5 = (w >> 25) & 0x3F;
1013 let b4_1 = (w >> 8) & 0xF;
1014 let raw = (b12 << 12) | (b11 << 11) | (b10_5 << 5) | (b4_1 << 1);
1015 ((raw as i32) << 19) >> 19
1016}
1017
1018fn encode_b_imm(opcode_and_regs: u32, imm: i32) -> u32 {
1020 let v = imm as u32;
1021 let b12 = (v >> 12) & 0x1;
1022 let b11 = (v >> 11) & 0x1;
1023 let b10_5 = (v >> 5) & 0x3F;
1024 let b4_1 = (v >> 1) & 0xF;
1025 let cleared = opcode_and_regs & 0x01FF_F07F;
1027 cleared | (b12 << 31) | (b10_5 << 25) | (b4_1 << 8) | (b11 << 7)
1028}
1029
1030fn align_branch_targets(
1043 code: &mut Vec<u8>,
1044 extra_targets: &[usize],
1045) -> Result<BTreeMap<usize, usize>, TranspileError> {
1046 let n = code.len();
1055 let mut inst_starts: Vec<usize> = Vec::with_capacity(n / 4);
1056 let mut post_terminator: std::collections::HashSet<usize> = std::collections::HashSet::new();
1057 post_terminator.insert(0); let mut static_edges: Vec<(usize, usize)> = Vec::new(); let mut pc: usize = 0;
1061 while pc < n {
1062 inst_starts.push(pc);
1063 let lo = u16::from_le_bytes([code[pc], code[pc + 1]]);
1064 let inst_len: usize;
1065 let is_terminator: bool;
1066 let target: Option<i64>;
1067 if lo & 0b11 != 0b11 {
1068 inst_len = 2;
1070 let op = lo & 0b11;
1079 let f3 = (lo >> 13) & 0b111;
1080 if lo == 0 {
1081 is_terminator = true;
1083 target = None;
1084 } else if op == 0b01 && f3 == 0b101 {
1085 let imm = decompress_cj_imm(lo);
1087 is_terminator = true;
1088 target = Some(pc as i64 + imm as i64);
1089 } else if op == 0b01 && (f3 == 0b110 || f3 == 0b111) {
1090 let imm = decompress_cb_imm(lo);
1092 is_terminator = true;
1093 target = Some(pc as i64 + imm as i64);
1094 } else if op == 0b10 && f3 == 0b100 {
1095 let bit12 = (lo >> 12) & 1;
1102 let rdrs1 = (lo >> 7) & 0x1F;
1103 let rs2 = (lo >> 2) & 0x1F;
1104 let is_jr_like = rs2 == 0 && (bit12 == 1 || rdrs1 != 0);
1108 is_terminator = is_jr_like;
1109 target = None;
1110 } else {
1111 is_terminator = false;
1112 target = None;
1113 }
1114 } else {
1115 if pc + 4 > n {
1117 break;
1118 }
1119 inst_len = 4;
1120 let w = u32::from_le_bytes([code[pc], code[pc + 1], code[pc + 2], code[pc + 3]]);
1121 let opcode = w & 0x7F;
1122 let funct3 = (w >> 12) & 0x7;
1123 match opcode {
1124 OP_JAL => {
1125 let imm = imm_j(w);
1126 is_terminator = true;
1127 target = Some(pc as i64 + imm as i64);
1128 }
1129 OP_JALR => {
1130 is_terminator = true;
1134 target = None;
1135 }
1136 0b110_0011 => {
1137 let imm = imm_b(w);
1139 is_terminator = true;
1140 target = Some(pc as i64 + imm as i64);
1141 }
1142 OP_CUSTOM_0 => {
1143 is_terminator = true;
1146 target = None;
1147 let _ = funct3;
1148 }
1149 _ => {
1150 is_terminator = false;
1151 target = None;
1152 }
1153 }
1154 }
1155 let next_pc = pc + inst_len;
1156 if is_terminator && next_pc < n {
1157 post_terminator.insert(next_pc);
1158 }
1159 if let Some(t) = target
1160 && t >= 0
1161 && (t as usize) < n
1162 {
1163 static_edges.push((pc, t as usize));
1164 }
1165 pc = next_pc;
1166 }
1167
1168 let inst_starts_set: std::collections::HashSet<usize> = inst_starts.iter().copied().collect();
1170 let mut needs_inject: std::collections::BTreeSet<usize> = std::collections::BTreeSet::new();
1171 for &(_, target) in &static_edges {
1172 if !post_terminator.contains(&target) && inst_starts_set.contains(&target) {
1173 needs_inject.insert(target);
1174 }
1175 }
1176 for &t in extra_targets {
1180 if t < n && !post_terminator.contains(&t) && inst_starts_set.contains(&t) {
1181 needs_inject.insert(t);
1182 }
1183 }
1184
1185 if needs_inject.is_empty() {
1186 let identity: BTreeMap<usize, usize> = inst_starts.into_iter().map(|p| (p, p)).collect();
1188 return Ok(identity);
1189 }
1190
1191 let new_len = n + needs_inject.len() * 4;
1193 let mut new_code: Vec<u8> = Vec::with_capacity(new_len);
1194 let mut offset_map: BTreeMap<usize, usize> = BTreeMap::new();
1197 let fallthrough_word = encode_custom0_fallthrough();
1198 let fallthrough_bytes = fallthrough_word.to_le_bytes();
1199
1200 let mut next_inject_iter = needs_inject.iter().peekable();
1201 let mut old_idx = 0;
1202 while old_idx < inst_starts.len() {
1203 let old_pc = inst_starts[old_idx];
1204 while let Some(&&inject_pc) = next_inject_iter.peek() {
1206 if inject_pc == old_pc {
1207 new_code.extend_from_slice(&fallthrough_bytes);
1208 next_inject_iter.next();
1209 } else {
1210 break;
1211 }
1212 }
1213 offset_map.insert(old_pc, new_code.len());
1214 let next_pc = inst_starts.get(old_idx + 1).copied().unwrap_or(n);
1215 let inst_len = next_pc - old_pc;
1216 new_code.extend_from_slice(&code[old_pc..old_pc + inst_len]);
1217 old_idx += 1;
1218 }
1219
1220 for &old_pc in &inst_starts {
1224 let new_pc = offset_map[&old_pc];
1225 let lo = u16::from_le_bytes([new_code[new_pc], new_code[new_pc + 1]]);
1226 if lo & 0b11 != 0b11 {
1227 let op = lo & 0b11;
1229 let f3 = (lo >> 13) & 0b111;
1230 if op == 0b01 && f3 == 0b101 {
1231 let old_imm = decompress_cj_imm(lo);
1232 let old_target = (old_pc as i64 + old_imm as i64) as usize;
1233 let new_target = *offset_map.get(&old_target).ok_or_else(|| {
1234 TranspileError::InvalidSection(format!(
1235 "align_branch_targets: c.j old target {:#x} not in offset_map",
1236 old_target
1237 ))
1238 })?;
1239 let new_imm = new_target as i64 - new_pc as i64;
1240 if new_imm != old_imm as i64 {
1241 let new_h = encode_cj_imm(lo, new_imm as i32).ok_or_else(|| {
1242 TranspileError::InvalidSection(format!(
1243 "align_branch_targets: c.j at new_pc {:#x} new_imm {} \
1244 out of ±2 KiB range",
1245 new_pc, new_imm
1246 ))
1247 })?;
1248 new_code[new_pc..new_pc + 2].copy_from_slice(&new_h.to_le_bytes());
1249 }
1250 } else if op == 0b01 && (f3 == 0b110 || f3 == 0b111) {
1251 let old_imm = decompress_cb_imm(lo);
1252 let old_target = (old_pc as i64 + old_imm as i64) as usize;
1253 let new_target = *offset_map.get(&old_target).ok_or_else(|| {
1254 TranspileError::InvalidSection(format!(
1255 "align_branch_targets: c.beqz/c.bnez old target {:#x} not in offset_map",
1256 old_target
1257 ))
1258 })?;
1259 let new_imm = new_target as i64 - new_pc as i64;
1260 if new_imm != old_imm as i64 {
1261 let new_h = encode_cb_imm(lo, new_imm as i32).ok_or_else(|| {
1262 TranspileError::InvalidSection(format!(
1263 "align_branch_targets: c.beqz/c.bnez at new_pc {:#x} new_imm {} \
1264 out of ±256 byte range",
1265 new_pc, new_imm
1266 ))
1267 })?;
1268 new_code[new_pc..new_pc + 2].copy_from_slice(&new_h.to_le_bytes());
1269 }
1270 }
1271 } else {
1272 let w = u32::from_le_bytes([
1273 new_code[new_pc],
1274 new_code[new_pc + 1],
1275 new_code[new_pc + 2],
1276 new_code[new_pc + 3],
1277 ]);
1278 let opcode = w & 0x7F;
1279 match opcode {
1280 OP_JAL => {
1281 let old_imm = imm_j(w);
1282 let old_target = (old_pc as i64 + old_imm as i64) as usize;
1283 if let Some(&new_target) = offset_map.get(&old_target) {
1284 let new_imm = new_target as i64 - new_pc as i64;
1285 if !(-(1 << 20)..(1 << 20)).contains(&new_imm) {
1286 return Err(TranspileError::InvalidSection(format!(
1287 "align_branch_targets: JAL at new_pc {:#x} out of ±1 MiB \
1288 range after injection (new_imm = {})",
1289 new_pc, new_imm
1290 )));
1291 }
1292 let rd = (w >> 7) & 0x1F;
1293 let v = new_imm as u32;
1294 let b20 = (v >> 20) & 0x1;
1295 let b10_1 = (v >> 1) & 0x3FF;
1296 let b11 = (v >> 11) & 0x1;
1297 let b19_12 = (v >> 12) & 0xFF;
1298 let imm_field = (b20 << 31) | (b10_1 << 21) | (b11 << 20) | (b19_12 << 12);
1299 let new_w = imm_field | (rd << 7) | OP_JAL;
1300 new_code[new_pc..new_pc + 4].copy_from_slice(&new_w.to_le_bytes());
1301 }
1302 }
1303 0b110_0011 => {
1304 let old_imm = imm_b(w);
1305 let old_target = (old_pc as i64 + old_imm as i64) as usize;
1306 if let Some(&new_target) = offset_map.get(&old_target) {
1307 let new_imm = new_target as i64 - new_pc as i64;
1308 if !(-(1 << 12)..(1 << 12)).contains(&new_imm) {
1309 return Err(TranspileError::InvalidSection(format!(
1310 "align_branch_targets: B-type branch at new_pc {:#x} out of ±4 KiB \
1311 range after injection (new_imm = {})",
1312 new_pc, new_imm
1313 )));
1314 }
1315 let new_w = encode_b_imm(w, new_imm as i32);
1316 new_code[new_pc..new_pc + 4].copy_from_slice(&new_w.to_le_bytes());
1317 }
1318 }
1319 _ => {}
1320 }
1321 }
1322 }
1323
1324 *code = new_code;
1325 Ok(offset_map)
1326}
1327
1328fn decompress_cj_imm(h: u16) -> i32 {
1331 let h = h as u32;
1332 let b11 = (h >> 12) & 0x1;
1333 let b4 = (h >> 11) & 0x1;
1334 let b9_8 = (h >> 9) & 0x3;
1335 let b10 = (h >> 8) & 0x1;
1336 let b6 = (h >> 7) & 0x1;
1337 let b7 = (h >> 6) & 0x1;
1338 let b3_1 = (h >> 3) & 0x7;
1339 let b5 = (h >> 2) & 0x1;
1340 let raw = (b11 << 11)
1341 | (b10 << 10)
1342 | (b9_8 << 8)
1343 | (b7 << 7)
1344 | (b6 << 6)
1345 | (b5 << 5)
1346 | (b4 << 4)
1347 | (b3_1 << 1);
1348 ((raw as i32) << 20) >> 20
1349}
1350
1351fn decompress_cb_imm(h: u16) -> i32 {
1353 let h = h as u32;
1354 let b8 = (h >> 12) & 0x1;
1355 let b4_3 = (h >> 10) & 0x3;
1356 let b7_6 = (h >> 5) & 0x3;
1357 let b2_1 = (h >> 3) & 0x3;
1358 let b5 = (h >> 2) & 0x1;
1359 let raw = (b8 << 8) | (b7_6 << 6) | (b5 << 5) | (b4_3 << 3) | (b2_1 << 1);
1360 ((raw as i32) << 23) >> 23
1361}
1362
1363fn encode_cb_imm(h: u16, imm: i32) -> Option<u16> {
1367 if !(-(1 << 8)..(1 << 8)).contains(&imm) {
1368 return None;
1369 }
1370 if imm & 1 != 0 {
1371 return None;
1372 }
1373 let v = imm as u32;
1374 let b8 = (v >> 8) & 0x1;
1375 let b7_6 = (v >> 6) & 0x3;
1376 let b5 = (v >> 5) & 0x1;
1377 let b4_3 = (v >> 3) & 0x3;
1378 let b2_1 = (v >> 1) & 0x3;
1379 let preserved = (h as u32) & 0b1110_0011_1000_0011;
1381 let new_imm = (b8 << 12) | (b4_3 << 10) | (b7_6 << 5) | (b2_1 << 3) | (b5 << 2);
1382 Some((preserved | new_imm) as u16)
1383}
1384
1385fn encode_cj_imm(h: u16, imm: i32) -> Option<u16> {
1388 if !(-(1 << 11)..(1 << 11)).contains(&imm) {
1389 return None;
1390 }
1391 if imm & 1 != 0 {
1392 return None;
1393 }
1394 let v = imm as u32;
1395 let b11 = (v >> 11) & 0x1;
1396 let b10 = (v >> 10) & 0x1;
1397 let b9_8 = (v >> 8) & 0x3;
1398 let b7 = (v >> 7) & 0x1;
1399 let b6 = (v >> 6) & 0x1;
1400 let b5 = (v >> 5) & 0x1;
1401 let b4 = (v >> 4) & 0x1;
1402 let b3_1 = (v >> 1) & 0x7;
1403 let preserved = (h as u32) & 0b1110_0000_0000_0011;
1405 let new_imm = (b11 << 12)
1406 | (b4 << 11)
1407 | (b9_8 << 9)
1408 | (b10 << 8)
1409 | (b6 << 7)
1410 | (b7 << 6)
1411 | (b3_1 << 3)
1412 | (b5 << 2);
1413 Some((preserved | new_imm) as u16)
1414}
1415
1416#[cfg(test)]
1417mod tests {
1418 use super::*;
1419
1420 #[test]
1421 fn nop_encoding_matches_addi_x0_x0_0() {
1422 let w = u32::from_le_bytes(NOP_BYTES);
1423 assert_eq!(w & 0x7F, OP_OP_IMM, "opcode must be OP-IMM");
1424 assert_eq!((w >> 7) & 0x1F, 0, "rd must be x0");
1425 assert_eq!((w >> 15) & 0x1F, 0, "rs1 must be x0");
1426 assert_eq!((w >> 20) & 0xFFF, 0, "imm must be 0");
1427 assert_eq!((w >> 12) & 0x7, 0, "funct3 must be 0 (ADDI)");
1428 }
1429
1430 #[test]
1431 fn custom0_ecall_jar_decodes() {
1432 let w = encode_custom0_ecall_jar();
1433 assert_eq!(w & 0x7F, OP_CUSTOM_0);
1434 assert_eq!((w >> 12) & 0x7, 0b001);
1435 assert_eq!((w >> 7) & 0x1F, 0);
1437 assert_eq!((w >> 15) & 0x1F, 0);
1438 }
1439
1440 #[test]
1441 fn custom0_ecalli_decodes() {
1442 let w = encode_custom0_ecalli(42);
1443 assert_eq!(w & 0x7F, OP_CUSTOM_0);
1444 assert_eq!((w >> 12) & 0x7, 0b010);
1445 assert_eq!((w >> 20) & 0xFFF, 42);
1446 }
1447
1448 #[test]
1449 fn custom0_fallthrough_decodes() {
1450 let w = encode_custom0_fallthrough();
1451 assert_eq!(w & 0x7F, OP_CUSTOM_0);
1452 assert_eq!((w >> 12) & 0x7, 0b100);
1453 }
1454
1455 #[test]
1456 fn cb_imm_round_trips() {
1457 let base = (0b110u16 << 13) | (0b01u16);
1460 for &imm in &[0, 2, -2, 4, -4, 128, -128, 254, -256] {
1461 let h = encode_cb_imm(base, imm).expect("in range");
1462 assert_eq!(
1463 decompress_cb_imm(h),
1464 imm,
1465 "round-trip failed for imm={}",
1466 imm
1467 );
1468 }
1469 assert!(encode_cb_imm(base, 256).is_none());
1470 assert!(encode_cb_imm(base, -258).is_none());
1471 }
1472
1473 #[test]
1474 fn cj_imm_round_trips() {
1475 let base = (0b101u16 << 13) | (0b01u16);
1477 for &imm in &[0, 2, -2, 4, -4, 512, -512, 2046, -2048] {
1478 let h = encode_cj_imm(base, imm).expect("in range");
1479 assert_eq!(
1480 decompress_cj_imm(h),
1481 imm,
1482 "round-trip failed for imm={}",
1483 imm
1484 );
1485 }
1486 assert!(encode_cj_imm(base, 2048).is_none());
1487 }
1488
1489 #[test]
1490 fn rewrite_ecall_marker_jar() {
1491 let csrrw = (0x800u32 << 20) | (0b001 << 12) | OP_SYSTEM;
1493 let ecall: u32 = 0x0000_0073;
1494 let mut code = Vec::new();
1495 code.extend_from_slice(&csrrw.to_le_bytes());
1496 code.extend_from_slice(&ecall.to_le_bytes());
1497 rewrite_ecall_markers(&mut code).unwrap();
1498 let w0 = u32::from_le_bytes(code[0..4].try_into().unwrap());
1499 assert_eq!(w0, u32::from_le_bytes(NOP_BYTES));
1500 let w1 = u32::from_le_bytes(code[4..8].try_into().unwrap());
1501 assert_eq!(w1, encode_custom0_ecall_jar());
1502 }
1503
1504 #[test]
1505 fn rewrite_ecall_marker_ecalli() {
1506 let csrrw = (0x801u32 << 20) | (0b001 << 12) | OP_SYSTEM;
1507 let ecall: u32 = 0x0000_0073;
1508 let mut code = Vec::new();
1509 code.extend_from_slice(&csrrw.to_le_bytes());
1510 code.extend_from_slice(&ecall.to_le_bytes());
1511 rewrite_ecall_markers(&mut code).unwrap();
1512 let w1 = u32::from_le_bytes(code[4..8].try_into().unwrap());
1513 assert_eq!(w1, encode_custom0_ecalli(0));
1514 }
1515
1516 #[test]
1517 fn validate_accepts_auipc_and_jalr() {
1518 let auipc = (0x1000u32 << 12) | (1 << 7) | OP_AUIPC; validate_pvm2(&auipc.to_le_bytes()).unwrap();
1523 let jalr = (1u32 << 15) | (1 << 7) | OP_JALR; validate_pvm2(&jalr.to_le_bytes()).unwrap();
1525 let jalr_x3 = (3u32 << 15) | (1 << 7) | OP_JALR; assert!(validate_pvm2(&jalr_x3.to_le_bytes()).is_err());
1529 }
1530
1531 #[test]
1532 fn validate_rejects_standard_ecall() {
1533 let code = 0x0000_0073u32.to_le_bytes().to_vec();
1534 let err = validate_pvm2(&code).unwrap_err();
1535 assert!(matches!(err, TranspileError::InvalidSection(_)));
1536 }
1537
1538 #[test]
1539 fn validate_rejects_producer_x3_use() {
1540 let w = (3u32 << 7) | OP_OP_IMM;
1542 let code = w.to_le_bytes().to_vec();
1543 let err = validate_pvm2(&code).unwrap_err();
1544 let TranspileError::InvalidSection(msg) = err else {
1545 panic!();
1546 };
1547 assert!(msg.contains("x3"));
1548 }
1549
1550 #[test]
1551 fn validate_accepts_clean_addi() {
1552 let w = (5u32 << 20) | (1 << 7) | OP_OP_IMM;
1554 let code = w.to_le_bytes().to_vec();
1555 validate_pvm2(&code).unwrap();
1556 }
1557
1558 #[test]
1559 fn validate_accepts_rvc() {
1560 let cli = 0x4515u16;
1562 let code = cli.to_le_bytes().to_vec();
1563 validate_pvm2(&code).unwrap();
1564 }
1565}