I try to implement functional ISA simulator: targets are RISC-V and MIPS. It is step by step instruction interpreter.
abstract step:
while(num_steps)
{
try
{
take_interrupt();// take pending interrupts
fetch(); // fetch instruction from memory
decode(); // find handler to instruction
execute(); // perform instruction
}
catch (Trap& e)
{
take_trap(e); //configure appropriate system registers and jump to trap vector.
}
}
As you can see C++ exceptions are used to transfer the control flow. Maybe there can be more handsome design?
Question: What is best way/practise to implement traps at functional ISA simulators. Also i interested in exceptions/trap implementation at translation simulators, like QEMU.
Note: by the word trap i mean ISA defined traps, not application error: misaligned memory access, illegal instruction, system register access fault, privilege level change, etc.
QEMU uses the C setjmp()/longjmp() mechanism for dealing with most exceptions: when we detect something like a page fault we set some flags to indicate the type of exception, and then longjmp() out to the top-level "execute code" loop. That loop then looks at the flags and sets the CPU state up for "enter exception handler" before continuing to execute guest code.
So we use the C equivalent of throwing an exception; as NonNumeric says there is no requirement to implement guest exceptions like this (the coincidence of names is just coincidence). But since a memory access triggering a page fault is the non-common case, it's more efficient to longjmp or throw a C++ exception rather than include "handle failure return" in all the memory access codepaths. Guest memory access is a particular hotspot and QEMU implements its memory access fastpath with a bit of custom inline assembly, so we care about the extra handful of instructions that would be required to exit to the top level loop on a page fault without doing the longjmp. A simulator which uses a simple "fetch/decode/execute" loop without doing JIT of guest code doesn't care so much about performance, so your choice may come down to preferences for code style and maintainability.
QEMU is written in C so it doesn't use C++ exceptions. You don't have to handle ISA traps via C++ exceptions either. Exceptions should be used when useful to you as implementer, nothing more.
Also note that the traps are not something way too special, they are still part of the emulated system's workflow. It is perfectly legal to encode division like:
if (reg[divisor] != 0)
reg[target] = reg[divident] / reg[divisor];
else
trap(TRAP_DIV0)
Where the trap()
function directly updates the archutecture state so that the next instruction to emulate would be from the exception handler.
void trap(int trap_id)
{
// save relevant registers according to platform spec
...
// set instruction pointer to trap handler start
reg[IP_INDEX] = trap_table[trap_id].ip;
// update other registers according to spec
...
}
C++ exceptions can make your life easier. For example memory accesses on many platforms need to convert virtual to physical addresses. This conversion may result in a trap (due to insufficient access or wrong configuration). It may be easier to write:
void some_isa_instruction_handler()
{
int value1 = read_memory(address1);
int value2 = read_memory(address2);
int res = perform_something(value1, value2);
write_memory(address3, res);
}
where read_memory()
and write_memory()
would simply throw C++ exception when ISA trap is needed than manually checking if each operation has generated a trap. Then the take_trap()
function would rollback whatever changes were performed by the interrupted instruction handler (if needed) and set up execution to go emulate the trap handler as trap()
above did.
Emulating a CISC system may benefit more from such style.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With