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 stack pointer
(SP
). The stack pointer can be used to allocate and access local
variables on the stack (which is supposed to grow downwards in memory
on all architectures).
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 caller- and callee-save
registers.
In addition, there is a special RET
register which contains
the return value. 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 separate from the integer registers on
all the supported architectures; on Intel architectures, the
register stack is mapped to a flat register file.
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
pushr i ui l ul p push O1 on the stack popr i ui l ul p pop O1 off the stack
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
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.
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. Results are undefined when using function calls
in a leaf function.
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 prolog (not specified) function prolog for O1 args leaf (not specified) the same for leaf functions 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.
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.