Next: GNU lightning macros, Previous: Installation, Up: Using GNU lightning
gnu lightning's instruction set was designed by deriving instructions that closely match those of most existing RISC architectures, or that can be easily syntesized if absent. Each instruction is composed of:
sub or mul
r or i)
The second and third field are separated by an underscore; thus,
examples of legal mnemonics are addr_i (integer add, with three
register operands) and muli_l (long integer multiply, with two
register operands and an immediate operand). Each instruction takes
two or three operands; in most cases, one of them can be an immediate
value instead of a register.
gnu lightning supports a full range of integer types: operands can be 1, 2 or 4 bytes long (64-bit architectures might support 8 bytes long operands), either signed or unsigned. The types are listed in the following table together with the C types they represent:
c signed char uc unsigned char s short us unsigned short i int ui unsigned int l long ul unsigned long f float d double p void *
Some of these types may not be distinct: for example, (e.g., l
is equivalent to i on 32-bit machines, and p is
substantially equivalent to ul).
There are at least seven integer registers, of which six are
general-purpose, while the last is used to contain the frame pointer
(FP). The frame pointer can be used to allocate and access local
variables on the stack, using the allocai instruction.
Of the general-purpose registers, at least three are guaranteed to be
preserved across function calls (V0, V1 and
V2) and at least three are not (R0, R1 and
R2). Six registers are not very much, but this
restriction was forced by the need to target CISC architectures
which, like the x86, are poor of registers; anyway, backends can
specify the actual number of available registers with the macros
JIT_R_NUM (for caller-save registers) and JIT_V_NUM
(for callee-save registers).
In addition, there is a special RET register which contains
the return value of the current function (not the return value
of callees—use the retval instruction for this). You should
always remember, however, that writing this register could overwrite
either a general-purpose register or an incoming parameter, depending
on the architecture.
There are at least six floating-point registers, named FPR0 to
FPR5. These are caller-save and are separate from the integer
registers on all the supported architectures; on Intel architectures,
the register stack is mapped to a flat register file. As for the
integer registers, the macro JIT_FPR_NUM yields the number of
floating-point registers, and the special FPRET register contains
the return value of the current function.
The complete instruction set follows; as you can see, most non-memory operations only take integers, long integers (either signed or unsigned) and pointers as operands; this was done in order to reduce the instruction set, and because most architectures only provide word and long word operations on registers. There are instructions that allow operands to be extended to fit a larger data type, both in a signed and in an unsigned way.
addx operations must directly follow addc, and
subx must follow subc; otherwise, results are undefined.
addr i ui l ul p f d O1 = O2 + O3
addi i ui l ul p O1 = O2 + O3
addxr i ui l ul O1 = O2 + (O3 + carry)
addxi i ui l ul O1 = O2 + (O3 + carry)
addcr i ui l ul O1 = O2 + O3, set carry
addci i ui l ul O1 = O2 + O3, set carry
subr i ui l ul p f d O1 = O2 - O3
subi i ui l ul p O1 = O2 - O3
subxr i ui l ul O1 = O2 - (O3 + carry)
subxi i ui l ul O1 = O2 - (O3 + carry)
subcr i ui l ul O1 = O2 - O3, set carry
subci i ui l ul O1 = O2 - O3, set carry
rsbr i ui l ul p f d O1 = O3 - O2
rsbi i ui l ul p O1 = O3 - O2
mulr i ui l ul f d O1 = O2 * O3
muli i ui l ul O1 = O2 * O3
hmulr i ui l ul O1 = high bits of O2 * O3
hmuli i ui l ul O1 = high bits of O2 * O3
divr i ui l ul f d O1 = O2 / O3
divi i ui l ul O1 = O2 / O3
modr i ui l ul O1 = O2 % O3
modi i ui l ul O1 = O2 % O3
andr i ui l ul O1 = O2 & O3
andi i ui l ul O1 = O2 & O3
orr i ui l ul O1 = O2 | O3
ori i ui l ul O1 = O2 | O3
xorr i ui l ul O1 = O2 ^ O3
xori i ui l ul O1 = O2 ^ O3
lshr i ui l ul O1 = O2 << O3
lshi i ui l ul O1 = O2 << O3
rshr i ui l ul O1 = O2 >> O31
rshi i ui l ul O1 = O2 >> O32
negr i l f d O1 = -O2
notr i ui l ul O1 = ~O2
The conditions given below are for the standard behavior of C, where the “unordered” comparison result is mapped to false.
ltr i ui l ul p f d O1 = (O2 < O3)
lti i ui l ul p O1 = (O2 < O3)
ler i ui l ul p f d O1 = (O2 <= O3)
lei i ui l ul p O1 = (O2 <= O3)
gtr i ui l ul p f d O1 = (O2 > O3)
gti i ui l ul p O1 = (O2 > O3)
ger i ui l ul p f d O1 = (O2 >= O3)
gei i ui l ul p O1 = (O2 >= O3)
eqr i ui l ul p f d O1 = (O2 == O3)
eqi i ui l ul p O1 = (O2 == O3)
ner i ui l ul p f d O1 = (O2 != O3)
nei i ui l ul p O1 = (O2 != O3)
unltr f d O1 = !(O2 >= O3)
unler f d O1 = !(O2 > O3)
ungtr f d O1 = !(O2 <= O3)
unger f d O1 = !(O2 < O3)
uneqr f d O1 = !(O2 < O3) && !(O2 > O3)
ltgtr f d O1 = !(O2 >= O3) || !(O2 <= O3)
ordr f d O1 = (O2 == O2) && (O3 == O3)
unordr f d O1 = (O2 != O2) || (O3 != O3)
ext both of them must be
registers, while mov accepts an immediate value as the second
operand.
Unlike movr and movi, the other instructions are applied
between operands of different data types, and they need two
data type specifications. You can use extr to convert between
integer data types, in which case the first must be smaller in size
than the second; for example extr_c_ui is correct while
extr_ul_us is not. You can also use extr to convert
an integer to a floating point value: the only available possibilities
are extr_i_f and extr_i_d. The other instructions
convert a floating point value to an integer, so the possible
suffixes are _f_i and _d_i.
movr i ui l ul p f d O1 = O2
movi i ui l ul p f d O1 = O2
extr c uc s us i ui l ul f d O1 = O2
roundr i f d O1 = round(O2)
truncr i f d O1 = trunc(O2)
floorr i f d O1 = floor(O2)
ceilr i f d O1 = ceil(O2)
Note that the order of the arguments is destination first,
source second as for all other gnu lightning instructions, but
the order of the types is always reversed with respect to that
of the arguments: shorter—source—first,
longer—destination—second. This happens for historical
reasons.
hton us ui Host-to-network (big endian) order ntoh us ui Network-to-host order
ld accepts two operands while ldx accepts three;
in both cases, the last can be either a register or an immediate
value. Values are extended (with or without sign, according to
the data type specification) to fit a whole register.
ldr c uc s us i ui l ul p f d O1 = *O2
ldi c uc s us i ui l ul p f d O1 = *O2
ldxr c uc s us i ui l ul p f d O1 = *(O2+O3)
ldxi c uc s us i ui l ul p f d O1 = *(O2+O3)
st accepts two operands while stx accepts three; in
both cases, the first can be either a register or an immediate
value. Values are sign-extended to fit a whole register.
str c uc s us i ui l ul p f d *O1 = O2
sti c uc s us i ui l ul p f d *O1 = O2
stxr c uc s us i ui l ul p f d *(O1+O2) = O3
stxi c uc s us i ui l ul p f d *(O1+O2) = O3
prepare i f d
pusharg c uc s us i ui l ul p f d
getarg c uc s us i ui l ul p f d
arg c uc s us i ui l ul p f d
retval c uc s us i ui l ul p
Of these, the first two are used by the caller, while the last two
are used by the callee. A code snippet that wants to call another
procedure and has to pass registers must, in order: use the
prepare instruction, giving the number of arguments to
be passed to the procedure (once for each data type); use
pusharg to push the arguments in reverse order;
and use calli or finish (explained below) to
perform the actual call.
arg and getarg are used by the callee.
arg is different from other instruction in that it does not
actually generate any code: instead, it is a function which returns
a value to be passed to getarg.3 You should call
arg as soon as possible, before any function call or, more
easily, right after the prolog or leaf instructions
(which are treated later).
getarg accepts a register argument and a value returned by
arg, and will move that argument to the register, extending
it (with or without sign, according to the data type specification)
to fit a whole register. These instructions are more intimately
related to the usage of the gnu lightning instruction set in code
that generates other code, so they will be treated more
specifically in Generating code at run-time.
Finally, the retval instruction fetches the return value of a
called function in a register. The retval instruction takes a
register argument and copies the return value of the previously called
function in that register. A function should put its own return value
in the RET register before returning. See the Fibonacci numbers, for an example.
You should observe a few rules when using these macros. First of all, it is not allowed to call functions with more than six arguments; this was done to simplify and speed up the implementation on architectures that use registers for parameter passing.
You should not nest calls to prepare, nor call zero-argument
functions (which do not need a call to prepare) inside a
prepare/calli or prepare/finish block. Doing this
might corrupt already pushed arguments.
You cannot pass parameters between subroutines using the six general-purpose registers. This might work only when targeting particular architectures.
On the other hand, it is possible to assume that callee-saved registers
(R0 through R2) are not clobbered by another dynamically
generated function which does not use them as operands in its code and
which does not return a value.
arg, these also return a value which, in this case,
is to be used to compile forward branches as explained in
Fibonacci numbers. They accept a pointer to the
destination of the branch and two operands to be compared; of these,
the last can be either a register or an immediate. They are:
bltr i ui l ul p f d if (O2 < O3) goto O1 blti i ui l ul p if (O2 < O3) goto O1 bler i ui l ul p f d if (O2 <= O3) goto O1 blei i ui l ul p if (O2 <= O3) goto O1 bgtr i ui l ul p f d if (O2 > O3) goto O1 bgti i ui l ul p if (O2 > O3) goto O1 bger i ui l ul p f d if (O2 >= O3) goto O1 bgei i ui l ul p if (O2 >= O3) goto O1 beqr i ui l ul p f d if (O2 == O3) goto O1 beqi i ui l ul p if (O2 == O3) goto O1 bner i ui l ul p f d if (O2 != O3) goto O1 bnei i ui l ul p if (O2 != O3) goto O1 bunltr f d if !(O2 >= O3) goto O1 bunler f d if !(O2 > O3) goto O1 bungtr f d if !(O2 <= O3) goto O1 bunger f d if !(O2 < O3) goto O1 buneqr f d if !(O2 < O3) && !(O2 > O3) goto O1 bltgtr f d if !(O2 >= O3) || !(O2 <= O3) goto O1 bordr f d if (O2 == O2) && (O3 == O3) goto O1 bunordr f d if !(O2 != O2) || (O3 != O3) goto O1 bmsr i ui l ul if O2 & O3 goto O1 bmsi i ui l ul if O2 & O3 goto O1 bmcr i ui l ul if !(O2 & O3) goto O1 bmci i ui l ul if !(O2 & O3) goto O14 boaddr i ui l ul O2 += O3, goto O1 on overflow boaddi i ui l ul O2 += O3, goto O1 on overflow bosubr i ui l ul O2 -= O3, goto O1 on overflow bosubi i ui l ul O2 -= O3, goto O1 on overflow
ret which has none; the
difference between finish and calli is that the
latter does not clean the stack from pushed parameters (if any)
and the former must always follow a prepare
instruction.
calli (not specified) function call to O1 callr (not specified) function call to a register finish (not specified) function call to O1 finishr (not specified) function call to a register jmpi/jmpr (not specified) unconditional jump to O1 ret (not specified) return from subroutine retval c uc s us i ui l ul p f d move return value to register
Like branch instruction, jmpi also returns a value which is to
be used to compile forward branches. See Fibonacci numbers.
prolog (not specified) function prolog for O1 args leaf (not specified) the same for leaf functions allocai (not specified) reserve space on the stack
Results are undefined when using function calls in a leaf function.
allocai receives the number of bytes to allocate and returns
the offset from the frame pointer register FP to the base of
the area. The area is aligned to an int; future versions of
gnu lightning may provide more fine-grained control on the alignment of
stack-allocated variables.
As a small appetizer, here is a small function that adds 1 to the input
parameter (an int). I'm using an assembly-like syntax here which
is a bit different from the one used when writing real subroutines with
gnu lightning; the real syntax will be introduced in See Generating code at run-time.
incr:
leaf 1
in = arg_i ! We have an integer argument
getarg_i R0, in ! Move it to R0
addi_i RET, R0, 1 ! Add 1, put result in return value
ret ! And return the result
And here is another function which uses the printf function from
the standard C library to write a number in hexadecimal notation:
printhex:
prolog 1
in = arg_i ! Same as above
getarg_i R0, in
prepare 2 ! Begin call sequence for printf
pusharg_i R0 ! Push second argument
pusharg_p "%x" ! Push format string
finish printf ! Call printf
ret ! Return to caller
[1] The sign bit is propagated for signed types.
[2] The sign bit is propagated for signed types.
[3] “Return a value” means that gnu lightning macros that compile these instructions return a value when expanded.
[4] These mnemonics mean, respectively, branch if mask set and branch if mask cleared.