Instruction Set
The instruction set of the MyJIT intermediate language is inspired by GNU lightning and in some aspects resembles architecture of RISC processors. Each operation has up to four operands which can be immediate values (numbers, constants) or registers. The number of available registers is virtually unlimited.
All general purpose registers and integer values are treated as signed integers and have the same size which corresponds to the register size of the CPU. Note that i386 and SPARC processors have 32 bit wide registers and AMD64 has 64 bit wide registers. In order to overcome this inconsistency, MyJIT provides the jit_value type which has the same size as the CPU's general purpose register. In specific cases, e.g., if smaller or unsigned value is needed and it is appropriate, you may specify the size or type of the value. This topic is discussed later.
All floating-point numbers and registers are internally treated as double precision values. However, if necessary, the value can be converted into a single precision value.
Typically, name of each instruction consists of three parts:
- name of the operation (e.g., add for addition, mul for multiplication, etc.)
- the name of the operation is often accompanied with the suffix r or i indicating whether the operation is taking merely registers or if it also takes an immediate value as its argument
- name of the operation can be equipped with additional flag delimited by the underscore (typically, this is used to identify operations handling unsigned numbers)
Registers
MyJIT supports arbitrary number of register. If the number of used registers is higher than the number of available hardware registers, MyJIT emulates them. Nevertheless, to achieve the best performance, it is a good practice not to use too many registers. All registers are denoted by positive integers including zero. To refer to these registers you should use macros R(x) and FR(x) identifying general purpose and floating point registers, respectively. Note that registers R(x) and FR(x) are completely different register and do not occupy the same space.
Besides, MyJIT has two special purpose registers---R_FP and R_OUT. R_FP serves as the frame pointer and is used to access dynamically allocated memory on the stack. The R_OUT can be used to handle the return values more efficiently. It can be used to read the return value right after the return from the function. Otherwise, the value of the register is undefined. Furthermore, it can be used right before the return from the function to set the return value more efficiently. If the value is set earlier, it can lead to undefined behavior. However, in most cases register allocator can optimize this which makes this register almost obsolete.
Notation
In order to describe instruction set, we are using symbols reg and freg to denote general purpose and floating-point registers, respectively. Analogously, imm and fimm denote immediate integer values and floating-point values. Particular instructions (e.g., load and store operations) have an extra operand which specifies the size (number of bytes) of data they work with. This operand shall be denoted by size and fsize. The value passed by the operand size can be 1, 2, 4, or 8. However, only the AMD64 port supports operation processing 8 bytes long values. The value passed by the operand fsize can be either 4 or 8. In other words, fsize denotes precision of the value.
Instructions
Transfer Operations
These operations allow to assign a value into a register. The first operand is always a register and the second one can be either an immediate value or register.
movi reg, imm O1 := O2 movr reg, reg O1 := O2 fmovr freg, freg O1 := O2 fmov freg, fimm O1 := O2
Binary Arithmetic Operations
Each binary arithmetic operation has exactly three operands. First two operands are always registers and the last one can be an immediate value or register. These operations are fully compatible with those in the GNU lightning instruction set.
addr reg, reg, reg O1 := O2 + O3 addi reg, reg, imm O1 := O2 + O3 addxr reg, reg, reg O1 := O2 + (O3 + carry) addxi reg, reg, imm O1 := O2 + (O3 + carry) addcr reg, reg, reg O1 := O2 + O3, set carry addci reg, reg, imm O1 := O2 + O3, set carry subr reg, reg, reg O1 := O2 - O3 subi reg, reg, imm O1 := O2 - O3 subxr reg, reg, reg O1 := O2 - (O3 + carry) subxi reg, reg, imm O1 := O2 - (O3 + carry) subcr reg, reg, reg O1 := O2 - O3, set carry subci reg, reg, imm O1 := O2 - O3, set carry rsbr reg, reg, reg O1 := O3 - O2 rsbi reg, reg, imm O1 := O3 - O2 mulr reg, reg, reg O1 := O2 * O3 muli reg, reg, imm O1 := O2 * O3 hmulr reg, reg, reg O1 := high bits of O2 * O3 hmuli reg, reg, imm O1 := high bits of O2 * O3 divr reg, reg, reg O1 := O2 / O3 divi reg, reg, imm O1 := O2 / O3 modr reg, reg, reg O1 := O2 % O3 modi reg, reg, imm O1 := O2 % O3 andr reg, reg, reg O1 := O2 & O3 andi reg, reg, imm O1 := O2 & O3 orr reg, reg, reg O1 := O2 | O3 ori reg, reg, imm O1 := O2 | O3 xorr reg, reg, reg O1 := O2 ^ O3 xori reg, reg, imm O1 := O2 ^ O3 lshr reg, reg, reg O1 := O2 << O3 lshi reg, reg, imm O1 := O2 << O3 rshr reg, reg, reg O1 := O2 >> O3 rshi reg, reg, imm O1 := O2 >> O3 rshr_u reg, reg, reg O1 := O2 >> O3 (unsigned variant) rshi_u reg, reg, imm O1 := O2 >> O3 (unsigned variant)
Operations subx and addx have to directly follow subc and addc otherwise the result is undefined. Note that you can use the unsigned flag with the rshr operation to propagate the first bit accordingly.
There are also equivalent operations for floating-point values.
faddr freg, freg, freg O1 := O2 + O3 faddi freg, freg, fimm O1 := O2 + O3 fsubr freg, freg, freg O1 := O2 - O3 fsubi freg, freg, fimm O1 := O2 - O3 frsbr freg, freg, freg O1 := O3 - O2 frsbi freg, freg, fimm O1 := O3 - O2 fmulr freg, freg, freg O1 := O2 * O3 fmuli freg, freg, fimm O1 := O2 * O3 fdivr freg, freg, freg O1 := O2 / O3 fdivi freg, freg, fimm O1 := O2 / O3
Unary Arithmetic Operations
These operations have two operands, both of which have to be registers.
negr reg O1 := -O2 notr reg O1 := ~O2 fnegr freg O1 := -O2
Load Operations
These operations transfer data from the memory into a register. Each operation has 3 or 4 operands. The last operand is an immediate value indicating the "size" of the data processed by this operation, i.e., a number of bytes copied from the memory to the register. It can be one of the following values: 1, 2, 4, or 8. Furthermore, the size cannot be larger than the size of the register. If the size of the data copied from the memory is smaller than the size of the register, the value is expanded to fit the entire register. Therefore, it may be necessary to specify additional sign flag.
ldr reg, reg, size O1 := *O2 ldi reg, imm, size O1 := *O2 ldr_u reg, reg, size O1 := *O2 (unsigned variant) ldi_u reg, imm, size O1 := *O2 (unsigned variant) ldxr reg, reg, reg, size O1 := *(O2 + O3) ldxi reg, reg, imm, size O1 := *(O2 + O3) ldxr_u reg, reg, reg, size O1 := *(O2 + O3) (unsigned variant) ldxi_u reg, reg, imm, size O1 := *(O2 + O3) (unsigned variant) fldr freg, reg, fsize O1 := *O2 fldi freg, imm, fsize O1 := *O2 fldxr freg, reg, reg, fsize O1 := *(O2 + O3) fldxi freg, reg, imm, fsize O1 := *(O2 + O3)
Store Operations
These operations transfer data from the register into the memory. Each operation has 3 or 4 operands. The last operand is an immediate value and indicates the "size" of the data, see "Load Operations" for more details. The first operand can be either an immediate or register. Other operands must be registers.
str reg, reg, size *O1 := O2 sti imm, reg, size *O1 := O2 stxr reg, reg, reg, size *(O1 + O2) := O3 stxi imm, reg, reg, size *(O1 + O2) := O3 fstr reg, freg, fsize *O1 := O2 fsti imm, freg, fsize *O1 := O2 fstxr reg, reg, freg, fsize *(O1 + O2) := O3 fstxi imm, reg, freg, fsize *(O1 + O2) := O3
Compare Instructions
These operations compare last two operands and store one or zero (if the condition was met or not, respectively) into the first operand. All these operations have three operands. The first two operands have to be registers and the last one can be either a register or an immediate value.
ltr reg, reg, reg O1 := (O2 < O3) lti reg, reg, imm O1 := (O2 < O3) ltr_u reg, reg, reg O1 := (O2 < O3) (unsigned variant) lti_u reg, reg, imm O1 := (O2 < O3) (unsigned variant) ler reg, reg, reg O1 := (O2 <= O3) lei reg, reg, imm O1 := (O2 <= O3) ler_u reg, reg, reg O1 := (O2 <= O3) (unsigned variant) lei_u reg, reg, imm O1 := (O2 <= O3) (unsigned variant) gtr reg, reg, reg O1 := (O2 > O3) gti reg, reg, imm O1 := (O2 > O3) gtr_u reg, reg, reg O1 := (O2 > O3) (unsigned variant) gti_u reg, reg, imm O1 := (O2 > O3) (unsigned variant) ger reg, reg, reg O1 := (O2 >= O3) gei reg, reg, imm O1 := (O2 >= O3) ger_u reg, reg, reg O1 := (O2 >= O3) (unsigned variant) gei_u reg, reg, imm O1 := (O2 >= O3) (unsigned variant) eqr reg, reg, reg O1 := (O2 == O3) eqi reg, reg, imm O1 := (O2 == O3) ner reg, reg, reg O1 := (O2 != O3) nei reg, reg, imm O1 := (O2 != O3)
Conversions
Register for integer and floating-pint values are independent and in order to convert value from one type to another you have to use one of the following operations.
extr freg, reg O1 := O2 truncr reg, freg O1 := trunc(O2) ceilr reg, freg O1 := ceil(O2) floorr reg, freg O1 := floor(O2) roundr reg, freg O1 := round(O2)
The operation truncr rounds the value towards zero and is the fastest one. Operations floorr and ceilr rounds the value towards negative or positive infinity, respectively. roundr rounds the given value to the nearest integer.
Function declaration
The following operations and auxiliary macros help to create a function, read its arguments, and return value.
Operation prolog imm has one operand which is an immediate value, which is a reference to a pointer of the function defined by the intermediate code. In other words, MyJIT generates machine code for a function which resides somewhere in the memory. The address of the functions is handed by this reference. See the "Getting started" section, for more details and for an illustrative example.
Operations retr reg, reti imm, fretr freg, fsize, and freti freg, fsize set the return value and return control to the calling procedure (or function).
Operation declare_arg imm, imm is not an actual operation but rather an auxiliary function which declares the type of the argument and its size (in this order); declare_arg can take the following types of arguments
- JIT_SIGNED_NUM -- signed integer number
- JIT_UNSIGNED_NUM -- unsigned integer number
- JIT_FLOAT -- floating-point number
- JIT_PTR -- pointer
To read an argument there are getarg reg, imm and getarg freg, imm operations having two arguments. The destination register where the input argument will be stored and the immediate value which identifies position of the argument.
Operation allocai imm reserves space on the stack which has at least the size specified by its operand. Note that the stack space may be aligned to some higher value. The macro returns an integer number which is an offset from the frame pointer R_FP!
Function calls
Each function call is done in three steps. The call is initiated by the operation prepare having no argument. In the second step, arguments are passed to a function using putarg or fputarg. (The arguments are passed in the normal order not in reverse, cf. GNU Lightning.) Afterwards, the function is called with the call operation. To retrieve the returned value you can use operations retval or fretval.
Let us make few notes on function calls:
- If calling a function defined in the same instance of the compiler (e.g., recursive function), you cannot pass values through registers. Each function has its own set of registers.
- Only putargr, putargi, fputargr, and fputargi operations are allowed inside the prepare-call block, otherwise, the behavior of the library is unspecified.
List of operations related to function calls:
- prepare -- prepares function call (generic)
- putargr reg -- passes an argument to a function
- putargi imm -- passes an argument to a function
- fputargr freg, fsize -- passes the argument to a function
- fputargi fimm, fsize -- passes the argument to a function
- call imm -- calls a function
- callr reg
- retval reg -- reads return value
- fretval freg, fsize -- reads return value
Jumps
Operations jmpi and jmpr can be used to implement unconditional jumps. Both operations have one operand, an address to jump to. To obtain this address you can use the get_label operation or use the forward declaration along with the patch operation.
- get_label is not an actual operation; it is a function that returns a jit_label value---value which corresponds to the current position in the code. This value can be passed to jmpi/call or to a branch operation.
- It may happen that one need to jump into a code which will be defined later. Therefore, one can use the forward declaration and set the address later. This means, one can declare that the operation jmpi or a branch operations jumps to the place defined by the JIT_FORWARD macro and store the pointer to the operation into some jit_op * value. To set the address later, there is the patch imm operation with an argument which is the patched operation. The following code illustrates the situation.
op = jmpi JIT_FORWARD ; ; some code ; patch op
Branch Operations
Branch operations represent conditional jumps and all have three operands. The first operand is an immediate value and represents the address to jump to. The latter two are values to be compared. The last operand can be either an immediate value or register.
bltr imm, reg, reg if (O2 < O3) goto O1 blti imm, reg, imm if (O2 < O3) goto O1 bltr_u imm, reg, reg if (O2 < O3) goto O1 blti_u imm, reg, imm if (O2 < O3) goto O1 bler imm, reg, reg if (O2 <= O3) goto O1 blei imm, reg, imm if (O2 <= O3) goto O1 bler_u imm, reg, reg if (O2 <= O3) goto O1 blei_u imm, reg, imm if (O2 <= O3) goto O1 bgtr imm, reg, reg if (O2 > O3) goto O1 bgti imm, reg, imm if (O2 > O3) goto O1 bgtr_u imm, reg, reg if (O2 > O3) goto O1 bgti_u imm, reg, imm if (O2 > O3) goto O1 bger imm, reg, reg if (O2 >= O3) goto O1 bgei imm, reg, imm if (O2 >= O3) goto O1 bger_u imm, reg, reg if (O2 >= O3) goto O1 bgei_u imm, reg, imm if (O2 >= O3) goto O1 beqr imm, reg, reg if (O2 == O3) goto O1 beqi imm, reg, imm if (O2 == O3) goto O1 bner imm, reg, reg if (O2 != O3) goto O1 bnei imm, reg, imm if (O2 != O3) goto O1 bmsr imm, reg, reg if (O2 & O3) goto O1 bmsi imm, reg ,imm if (O2 & O3) goto O1 bmcr imm, reg ,reg if !(O2 & O3) goto O1 bmci imm, reg ,imm if !(O2 & O3) goto O1 boaddr imm, reg, reg O2 += O3, goto O1 on overflow boaddi imm, reg, imm O2 += O3, goto O1 on overflow bnoaddr imm, reg, reg O2 += O3, goto O1 on not overflow bnoaddi imm, reg, imm O2 += O3, goto O1 on not overflow bosubr imm, reg, reg O2 -= O3, goto O1 on overflow bosubi imm, reg, imm O2 -= O3, goto O1 on overflow bnosubr imm, reg, reg O2 -= O3, goto O1 on not overflow bnosubi imm, reg, imm O2 -= O3, goto O1 on not overflow fbltr imm, freg, freg if (O2 < O3) goto O1 fblti imm, freg, fimm if (O2 < O3) goto O1 fbler imm, freg, freg if (O2 <= O3) goto O1 fblei imm, freg, fimm if (O2 <= O3) goto O1 fbgtr imm, freg, freg if (O2 > O3) goto O1 fbgti imm, freg, fimm if (O2 > O3) goto O1 fbger imm, freg, freg if (O2 >= O3) goto O1 fbgei imm, freg, fimm if (O2 >= O3) goto O1 fbeqr imm, freg, freg if (O2 == O3) goto O1 fbeqi imm, freg, fimm if (O2 == O3) goto O1 fbner imm, freg, freg if (O2 != O3) goto O1 fbnei imm, freg, fimm if (O2 != O3) goto O1
Misc
There is an operation that allows to emit raw bytes of data into a generated code:
data_byte imm
This operation emits only one byte to a generated code. For convenience there are auxiliary macros emitting a sequence of bytes, string of chars (including the trailing 0), empty area, and values of common sizes, respectively.
jit_data_bytes(struct jit *jit, int count, unsigned char *data) jit_data_str(jit, str) jit_data_emptyarea(jit, size) jit_data_word(jit, a) jit_data_dword(jit, a) jit_data_qword(jit, a)
If you are emitting raw data into a code, it is your responsibility to properly align code. For this purpose there is an operation:
jit_align imm
This operation takes care of proper code alignment. Note that particular platforms have their specific requirements. On SPARC all instructions have to be aligned to 4 bytes, AMD64 favors alignment to 16 bytes, but it is not mandatory, etc. Safe bet is to use 16 as an operand of this operation.
To obtain reference to a data or code you can use two operations:
ref_data reg, imm ref_code reg, imm
That loads address of the label (second operand) into a register. The ref_data operation is intended for addresses of data (emitted with data_* operations) and ref_code is for address within an ordinary code. Note that address obtained with ref_code can be used only for local jumps inside a function. If necessary, for instance, if a some sort of branch table is needed, it is possible to emit address as a data with two operations.
data_code imm data_data imm
Note that mixing code and data may not be a generally good idea and may lead to various issues, e.g. poor performance, weird behavior, etc. Albeit this feature is part of the library, users are encouraged to place data to some specific part of code (for instance, to the end of code) or use data that are not part of the code and are allocated elsewhere, for instance, with ordinary malloc.