Writing a new front-end
From Libcpu
Contents |
Templates
In order to understand how frontends work, you should look at other frontends: For CISC CPUs, look at 6502, and for RISC, look at MIPS (for a simple start) and M88K (for a complete frontend).
You might wonder whether your CPU fits into the RISC or the CISC category: The rule is, if it has addressing modes, then it's CISC.
Flags Encoding
CPUs that have condition codes usually encode these bits into a single register. While the libcpu interface code deals with the register, the generated code will work on the individual bits. Therefore on every entry, the flags register is decoded into the single bits, and on every exit, the flags register is encoded again. Look into 6502 or ARM on how to do this.
Disassembler
You can choose to write your own disassembler or import an existing one. The 6502 and M88K frontends have disassemblers that are written from scratch and share data structures with the recompiler. ARM and MIPS have recompilers imported from other projects (under compatible licenses) and have their instruction fetch and string return hooked up to libcpu. This is usually quite straightforward.
Tagging
The frontend consists of three main pieces: Tagging, Conditions and Instruction Recompilation. Tagging decodes the instruction and returns all code flow information in an architecture-independent way, and without generating any code. In its first pass, the libcpu core needs to find all reachable code by getting the code flow information for every instruction.
- Tagging needs to return whether the instruction is a branch (libcpu lingo for a jump, condition or unconditional), a call, a ret or a trap (syscall).
- On some RISCs, the link register can be any register, so it is not clear whether a certain jump to a register is a subroutine return. In this case, just flag it as a return. It is only a hint to the core.
- ORed into the tag are flags whether the instruction is executed conditionally (e.g. a conditional branch) and whether the instruction has a delay slot (legal for branch, call and return only).
- For all branches and calls, the tagger needs to set new_pc to the target address or NEW_PC_NONE if unknown (i.e. jump to register).
- For all calls and conditional branches, it has to set next_pc, which points to the instruction executed after the return of the subroutine or if the conditional branch is not taken. On systems with delay slots, this points to after the delay slot, otherwise it just points to pc + instruction_size.
- In any case, it needs to return the size of the instruction (in RISC, this can be hardcoded).
Conditions
The condition evaluation function is supposed to decode the instruction and generates code that evaluates the condition of a conditional instruction. The function only gets called if tagging declared this instruction as conditional.
- On all architectures, conditional branches and calls need to have their condition decoded here.
- On an architecture like ARM, which has a 4 bit condition in every instruction, this function supposed to deal with this condition.
- On an architecture like i386 that has a conditional move (cmov), the condition of this must be decoded as well.
On MIPS for example, BLTZ branches if a specified register is < 0, so the condition is:
return ICMP_SLT(R(RS),CONST(0));
This actually emits code to do the evaluation of the condition; see below for more information.
Recompilation
Recompilation her to decode the instruction once more and emit code that implements the instructon. libcpu_generic.h defines lots of macros that help describe the function of an instruction in a functional and LLVM-agnostic way, like this:
LET(RD,ADD(R(RS), R(RT)));
The macros RD, RS and RT are defined by your frontend and extract the register index bits out of the opcode. The R() macros fetch the register values, ADD() adds two registers and LET() assigns a register with a value. While all this looks like interpreter code, the macros R(), ADD() and LET() actually emit LLVM IR code that does the respective job.
Note that conditional instructions have their condition already translated, so the condition will not be checked in the recompiler part. A conditional move will only be implemented as a move.
Branches are also special: Since tagging has returned all code flow information, the libcpu core will emit code that deals with the actual branch.
- A (conditional or unconditional) branch for example will emit no code whatsoever in the recompiler. Both conditions as well as branches are done by the core.
- A branch with an unknown target (i.e. a register) will have to emit code to load the guest target address into PC, libcpu will do the rest.
- A subroutine return will have to load the guest target address into PC (i.e. pop it off the stack or copy it form the link register), libcpu will do the rest.
Coding Style
The idea of the front end interface is to be as tabular/descriptive/functional (as opposed to imperative/procedural) as possible, use the macros as much as possible, and be as independent of LLVM as possible. It would be cool if it were possible to one day compile the same frontends with a different set of macros to generate interpreters instead of recompilers.
Therefore, please make your code as simple as possible. Instruction parsing should be done in switch() trees. Do not generate decoder tables at initialization time, and don't use jump tables. A lot is changing in libcpu, and we want the flexibility to batch change all frontends to updated interfaces or completely new paradigms, which is only possible if all frontends work the same, and none tries to be extra smart or optimized.
