ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
emu_seam_mve.c
Go to the documentation of this file.
1
19
20#include <capstone/capstone.h>
21#include <stdio.h>
22#include <string.h>
23
24#include "emu_engine.h"
25#include "emu_exc.h"
26#include "emu_seams.h"
27
28/* ============================================================================
29 * Minimal MVE (Helium) emulation. The RA8D2 is Cortex-M85 (Armv8.1-M, has MVE),
30 * but the closest core Unicorn offers is M33 (Armv8-M, NO MVE), so the Helium
31 * instructions GCC's auto-vectoriser emits trap here as "invalid". GCC vectorises
32 * the memset / struct-zero idiom with two forms, which this handles:
33 * VMOV.I32 Qd, #imm -- set all four 32-bit lanes of Qd to imm
34 * VSTRW.32 Qd, [Rn{, #off}] -- store the 16-byte Qd to memory (no write-back)
35 * The Q registers alias the FPU D registers (Qn == D[2n]:D[2n+1]) -- the M33 core
36 * has those (the firmware uses the FPU) -- so the vector state is read/written
37 * through Unicorn's D registers. Any other MVE form falls through and still
38 * reports as invalid, so nothing is silently mis-executed. capstone (already
39 * linked, and already decoding these for the error path) provides the operands;
40 * the engine handle is opened once and reused. On real silicon all of this just
41 * runs natively on Helium -- this only makes the M33-based emulator faithful to it.
42 * ==========================================================================*/
43enum : uint32_t {
47 k_mve_max_run = 4096U,
48};
50static uint64_t s_mve_emulated = 0U;
67static bool s_mve_nocp_handled = false;
80static uint32_t s_mve_resume_pc = 0U;
93static bool s_mve_resume_armed = false;
94
95/* ---------------------------------------------------------------------------
96 * MVE contiguous load/store family (VLDRB/VLDRH/VLDRW, VSTRB/VSTRH/VSTRW with
97 * an immediate offset or post-index write-back), serviced from the NoCP
98 * UsageFault.
99 *
100 * Armv8.1-M reallocates coprocessor space 0b1110 / 0b1111 to MVE, so this
101 * family reuses the legacy STC/LDC encodings byte for byte. `arm-none-eabi-as
102 * -march=armv8.1-m.main+mve` assembles `stc p15, c7, [r0, #196]` and
103 * `vstrw.32 q3, [r0, #196]` to the SAME word ED80 7F31, and objdump
104 * `-m armv8.1-m.main` renders that word as the MVE form. capstone renders it as
105 * the legacy `stc p15`, so this family must never be decoded through capstone --
106 * the fields below are taken straight from the encoding.
107 *
108 * Unicorn's M33 implements neither MVE nor coprocessor 14/15, so it does NOT
109 * trap these as invalid instructions: it raises a NoCP UsageFault (QEMU
110 * EXCP_NOCP, reported as int_no 17) through UC_HOOK_INTR with PC still at the
111 * faulting instruction. Verified against libunicorn standalone: on a cold
112 * engine the invalid-instruction hook is never called for any form in this
113 * family, so the NoCP fault is the only way in.
114 *
115 * That covers the FIRST instruction of a run. Servicing it means writing PC and
116 * calling `uc_emu_stop` from inside the interrupt hook, and once that has
117 * happened the NEXT instruction of the same run arrives at the
118 * invalid-instruction hook instead -- also verified standalone, with two
119 * consecutive `vstrw.32`. GCC emits exactly such runs (the struct-zero idiom),
120 * so BOTH arrival paths are wired to the same decode via ::internal_mve_mem_try: the
121 * NoCP hook through ::emu_mve_nocp_emulate, and the invalid-instruction
122 * dispatcher through ::emulate_mve. Wiring only one of them leaves every run
123 * of two or more MVE accesses faulting on its second instruction.
124 *
125 * Field layout, confirmed by assembling each form and reading the bytes back:
126 * hw1 = 1110 110P 0 U W L Rn (P=1 offset, P=0 post-index; bit6 is 0)
127 * hw2 = Qd[2:0] 1 1 1 1 size[1:0] imm7
128 * with size 0b00 byte (imm7 scaled by 1), 0b01 halfword (by 2), 0b10 word
129 * (by 4) and 0b11 unallocated. Every form transfers the whole 16-byte vector;
130 * `size` sets the lane width and the immediate scale only, so for a contiguous
131 * little-endian access the data movement is a plain 16-byte copy.
132 *
133 * Aliasing: the single- and double-precision FP stores share hw1 exactly and
134 * differ only in hw2[11:9] -- `vstr s14, [r0, #196]` is ED80 7A31 and
135 * `vstr d7, [r0, #196]` is ED80 7B31, both hw2[12:9] == 0b1101 / 0b1010.
136 * Unicorn executes both natively and correctly, so the hw2[12:9] == 0b1111
137 * guard is load-bearing: widening it would hijack working FP stores and turn
138 * them into silent 16-byte writes.
139 * ===========================================================================
140 */
163
177typedef struct {
178 uint32_t qd;
179 uint32_t rn;
180 uint32_t off;
181 bool load;
182 bool wback;
183 bool add;
184 bool post;
186
209RA8_INTERNAL static bool internal_mve_mem_decode(uint16_t hw1, uint16_t hw2, mve_mem_op_t* op)
210{
211 const uint16_t h1_family = hw1 & (uint16_t)k_mve_mem_h1_mask;
212 if (((h1_family != (uint16_t)k_mve_mem_h1_val) && (h1_family != (uint16_t)k_mve_mem_h1_post)) ||
213 ((hw2 & (uint16_t)k_mve_mem_h2_mask) != (uint16_t)k_mve_mem_h2_val)) {
214 return false;
215 }
216 const uint32_t size =
217 ((uint32_t)hw2 >> (uint32_t)k_mve_mem_size_sh) & (uint32_t)k_mve_mem_size_msk;
218 uint32_t scale;
219 switch (size) {
220 case (uint32_t)k_mve_mem_sz_byte:
221 scale = (uint32_t)k_mve_mem_scl_byte;
222 break;
223 case (uint32_t)k_mve_mem_sz_half:
224 scale = (uint32_t)k_mve_mem_scl_half;
225 break;
226 case (uint32_t)k_mve_mem_sz_word:
227 scale = (uint32_t)k_mve_mem_scl_word;
228 break;
229 default:
230 return false; /* size 0b11 is unallocated -- never emulate it. */
231 }
232 op->qd = ((uint32_t)hw2 >> (uint32_t)k_mve_qd_shift) & (uint32_t)k_mve_qd_mask;
233 op->rn = (uint32_t)hw1 & (uint32_t)k_mve_rn_mask;
234 op->off = ((uint32_t)hw2 & (uint32_t)k_mve_imm7_mask) * scale;
235 op->load = (hw1 & (uint16_t)k_mve_mem_bit_l) != 0U;
236 op->wback = (hw1 & (uint16_t)k_mve_mem_bit_w) != 0U;
237 op->add = (hw1 & (uint16_t)k_mve_mem_bit_u) != 0U;
238 op->post = h1_family == (uint16_t)k_mve_mem_h1_post;
239 return true;
240}
241
265RA8_INTERNAL static void internal_mve_mem_exec(uc_engine* uc, const mve_mem_op_t* op)
266{
267 uint32_t base = 0U;
268 (void)uc_reg_read(uc, k_arm_reg_id[op->rn], &base);
269 const uint32_t adjusted = op->add ? (base + op->off) : (base - op->off);
270 const uint32_t addr = op->post ? base : adjusted;
271 const int d_lo = (int)UC_ARM_REG_D0 + (int)(2U * op->qd);
272 uint8_t buf[k_mve_q_bytes];
273 uint64_t lo = 0U;
274 uint64_t hi = 0U;
275 if (op->load) {
276 (void)emu_mem_read(uc, (uint64_t)addr, buf, (size_t)k_mve_q_bytes);
277 (void)memcpy(&lo, buf, sizeof(lo));
278 (void)memcpy(&hi, buf + sizeof(lo), sizeof(hi));
279 (void)uc_reg_write(uc, d_lo, &lo);
280 (void)uc_reg_write(uc, d_lo + 1, &hi);
281 } else {
282 (void)uc_reg_read(uc, d_lo, &lo);
283 (void)uc_reg_read(uc, d_lo + 1, &hi);
284 (void)memcpy(buf, &lo, sizeof(lo));
285 (void)memcpy(buf + sizeof(lo), &hi, sizeof(hi));
286 (void)emu_mem_write(uc, (uint64_t)addr, buf, (size_t)k_mve_q_bytes);
287 }
288 if (op->wback) {
289 (void)uc_reg_write(uc, k_arm_reg_id[op->rn], &adjusted);
290 }
291}
292
317RA8_INTERNAL static bool internal_mve_mem_try(uc_engine* uc, const uint8_t code[4])
318{
319 const uint16_t hw1 = (uint16_t)(code[0] | ((uint16_t)code[1] << (uint16_t)k_byte_bits));
320 const uint16_t hw2 = (uint16_t)(code[2] | ((uint16_t)code[3] << (uint16_t)k_byte_bits));
321 mve_mem_op_t op = {};
322 if (!internal_mve_mem_decode(hw1, hw2, &op)) {
323 return false;
324 }
325 internal_mve_mem_exec(uc, &op);
326 return true;
327}
328
341RA8_INTERNAL static int internal_mve_q_d(unsigned int qreg, bool high)
342{
343 const unsigned int idx = qreg - (unsigned int)ARM_REG_Q0; /* Q index 0..7. */
344 return (int)UC_ARM_REG_D0 + (int)(2U * idx) + (high ? 1 : 0);
345}
346
359RA8_INTERNAL static bool internal_mve_exec_one(uc_engine* uc, const cs_insn* insn)
360{
361 const cs_arm* d = &insn->detail->arm;
362 const bool op0_q = (d->op_count == 2) && (d->operands[0].type == ARM_OP_REG) &&
363 (d->operands[0].reg >= ARM_REG_Q0) && (d->operands[0].reg <= ARM_REG_Q7);
364 if (!op0_q) {
365 return false;
366 }
367 /* VMOV.I32 Qd, #imm -> replicate the 32-bit immediate into all four lanes. */
368 if ((insn->id == ARM_INS_VMOV) && (d->operands[1].type == ARM_OP_IMM) &&
369 (strstr(insn->mnemonic, ".i32") != nullptr)) {
370 const uint32_t imm = (uint32_t)d->operands[1].imm;
371 const uint64_t pair = ((uint64_t)imm << (uint64_t)k_mve_lane_shift) | (uint64_t)imm;
372 (void)uc_reg_write(uc, internal_mve_q_d(d->operands[0].reg, false), &pair);
373 (void)uc_reg_write(uc, internal_mve_q_d(d->operands[0].reg, true), &pair);
374 return true;
375 }
376 return false;
377}
378
381{
382 static csh s_cs;
383 static bool s_cs_ok = false;
384 if (!s_cs_ok) {
385 if (cs_open(CS_ARCH_ARM, (cs_mode)(CS_MODE_THUMB | CS_MODE_MCLASS), &s_cs) != CS_ERR_OK) {
386 return nullptr;
387 }
388 (void)cs_option(s_cs, CS_OPT_DETAIL, CS_OPT_ON);
389 s_cs_ok = true;
390 }
391 return &s_cs;
392}
393
407bool emulate_mve(uc_engine* uc, uint32_t pc0, const uint8_t code0[4])
408{
409 csh* cs = internal_mve_capstone();
410 if (cs == nullptr) {
411 return false;
412 }
413 uint32_t pc = pc0;
414 uint8_t code[4];
415 (void)memcpy(code, code0, sizeof(code));
416 uint32_t handled = 0U;
417 while (handled < (uint32_t)k_mve_max_run) {
418 /* Contiguous load/store first: capstone renders this family as a legacy
419 * `stc p15`, so it must be decoded from the raw encoding, not through the
420 * disassembler below. */
421 if (internal_mve_mem_try(uc, code)) {
422 handled++;
423 pc += (uint32_t)k_mve_insn_len;
424 if (emu_mem_read(uc, (uint64_t)pc, code, sizeof(code)) != UC_ERR_OK) {
425 break;
426 }
427 continue;
428 }
429 cs_insn* insn = nullptr;
430 const size_t n = cs_disasm(*cs, code, (size_t)k_mve_insn_len, pc, 1, &insn);
431 if (n != 1U) {
432 break;
433 }
434 const bool ok = internal_mve_exec_one(uc, &insn[0]);
435 cs_free(insn, n);
436 if (!ok) {
437 break; /* first non-MVE (valid) instruction -- relaunch resumes here. */
438 }
439 handled++;
440 pc += (uint32_t)k_mve_insn_len;
441 if (emu_mem_read(uc, (uint64_t)pc, code, sizeof(code)) != UC_ERR_OK) {
442 break;
443 }
444 }
445 if (handled > 0U) {
446 (void)uc_reg_write(uc, UC_ARM_REG_PC, &pc);
447 s_mve_emulated += handled;
448 }
449 return handled > 0U;
450}
451
452bool emu_mve_nocp_emulate(uc_engine* uc, uint32_t pc)
453{
454 uint8_t code[k_mve_insn_len] = {};
455 if (emu_mem_read(uc, (uint64_t)pc, code, sizeof(code)) != UC_ERR_OK) {
456 return false;
457 }
458 if (!internal_mve_mem_try(uc, code)) {
459 return false;
460 }
461 const uint32_t next = pc + (uint32_t)k_mve_insn_len;
462 (void)uc_reg_write(uc, UC_ARM_REG_PC, &next);
464 s_mve_nocp_handled = true;
465 s_mve_resume_pc = next;
466 s_mve_resume_armed = true;
467 return true;
468}
469
470bool emu_mve_nocp_spurious(uint32_t pc)
471{
472 const bool armed = s_mve_resume_armed;
473 s_mve_resume_armed = false;
474 if (!armed || (pc != s_mve_resume_pc)) {
475 return false;
476 }
477 s_mve_nocp_handled = true; /* make the run loop relaunch here. */
478 return true;
479}
480
483{
484 const bool hit = s_mve_nocp_handled;
485 s_mve_nocp_handled = false;
486 return hit;
487}
488
491{
492 return s_mve_emulated;
493}
Shared Unicorn engine access utilities for the board emulator.
const int k_arm_reg_id[16]
ARM register index (0..15) -> Unicorn register id.
Definition emu_engine.c:17
Cortex-M exception model constants and interfaces for ra8_emulator.
@ k_byte_bits
Bits per byte (SHPR field width).
Definition emu_exc.h:99
uc_err emu_mem_read(uc_engine *uc, uint64_t address, void *bytes, size_t count)
Read guest memory through the central access seam.
uc_err emu_mem_write(uc_engine *uc, uint64_t address, const void *bytes, size_t count)
Write guest memory through the central access seam.
static uint32_t s_mve_resume_pc
Address the seam advanced PC to after servicing the last NoCP fault.
static RA8_INTERNAL void internal_mve_mem_exec(uc_engine *uc, const mve_mem_op_t *op)
Perform one decoded MVE contiguous load/store against emulated state.
uint64_t emu_mve_emulated_count(void)
Implementation of emu_mve_emulated_count() – plain counter read.
bool emu_mve_nocp_spurious(uint32_t pc)
Report whether an invalid-instruction trap at pc is the bogus one Unicorn raises just after the MVE s...
static bool s_mve_resume_armed
True while one bogus post-NoCP invalid report is still expected.
static uint64_t s_mve_emulated
Count of MVE instructions emulated this run (run-end telemetry).
bool emu_mve_nocp_emulate(uc_engine *uc, uint32_t pc)
Emulate an MVE contiguous load/store from the NoCP UsageFault.
@ k_mve_insn_len
MVE instructions are 32-bit Thumb-2.
@ k_mve_lane_shift
32-bit lane width (two lanes per D register).
@ k_mve_max_run
Loop bound: max consecutive MVE ops per trap.
@ k_mve_q_bytes
Bytes in a Q (128-bit) register.
static RA8_INTERNAL int internal_mve_q_d(unsigned int qreg, bool high)
Map capstone Q-reg qreg to its Unicorn D-register half (low/high).
static RA8_INTERNAL bool internal_mve_mem_decode(uint16_t hw1, uint16_t hw2, mve_mem_op_t *op)
Decode an MVE contiguous load/store from its two halfwords.
mve_mem_field_t
@ k_mve_mem_sz_half
size 0b01: halfword lanes.
@ k_mve_mem_h2_mask
Isolates hw2[12:9], the coprocessor space.
@ k_mve_mem_h1_post
hw1 match for post-index write-back forms.
@ k_mve_mem_scl_half
imm7 scale for halfword lanes.
@ k_mve_mem_scl_word
imm7 scale for word lanes.
@ k_mve_mem_h1_val
hw1 match for immediate-offset forms.
@ k_mve_qd_shift
Qd field position in hw2.
@ k_mve_mem_h1_mask
hw1 fixed bits with P/U/W/L/Rn excluded.
@ k_mve_rn_mask
Rn field (four bits) in hw1[3:0].
@ k_mve_mem_scl_byte
imm7 scale for byte lanes.
@ k_mve_mem_size_msk
Width of the size field (two bits).
@ k_mve_mem_bit_u
hw1[7]: add (1) or subtract (0) the offset.
@ k_mve_mem_h2_val
hw2[12:9] == 0b1111 selects MVE, not FP.
@ k_mve_mem_bit_w
hw1[5]: write the computed address to Rn.
@ k_mve_mem_size_sh
Position of the size field in hw2.
@ k_mve_mem_sz_byte
size 0b00: byte lanes.
@ k_mve_qd_mask
Qd field width (three bits) after shift.
@ k_mve_imm7_mask
imm7 field (unscaled offset) in hw2[6:0].
@ k_mve_mem_bit_l
hw1[4]: load (1) or store (0).
@ k_mve_mem_sz_word
size 0b10: word lanes.
static RA8_INTERNAL csh * internal_mve_capstone(void)
Lazily open the shared Thumb/M-class Capstone handle; nullptr on failure.
static RA8_INTERNAL bool internal_mve_mem_try(uc_engine *uc, const uint8_t code[4])
Perform the MVE contiguous load/store at code, if that is what it is.
static bool s_mve_nocp_handled
Latch: the chunk just ended in a NoCP fault this seam serviced.
static RA8_INTERNAL bool internal_mve_exec_one(uc_engine *uc, const cs_insn *insn)
Execute one decoded MVE instruction (no PC change); true iff handled.
bool emu_mve_nocp_take(void)
Implementation of emu_mve_nocp_take() – test-and-clear the latch.
bool emulate_mve(uc_engine *uc, uint32_t pc0, const uint8_t code0[4])
Emulate a run of consecutive auto-vectoriser MVE instructions.
Armv8.1-M instruction-emulation seams (M85 ops on Unicorn's M33).
#define RA8_INTERNAL
Marker that a function is intended to be static (file-local).
void * memcpy(void *dst, const void *src, size_t n)
Copy memory area between non-overlapping regions.
char * strstr(const char *haystack, const char *needle)
Locate substring in string.
One decoded MVE contiguous load/store.
uint32_t rn
Base core register 0..15, from hw1[3:0].
bool load
True for VLDR*, false for VSTR*.
bool post
True when access precedes write-back adjustment.
uint32_t qd
Vector register Q0..Q7, from hw2[15:13].
bool add
True to add the offset, false to subtract it.
bool wback
True when the computed address is written to Rn.
uint32_t off
Byte offset: imm7 scaled by the element size.