[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [Top] [Contents] [Index] [ ? ]

2.2 GNU lightning's instruction set

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:

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.

Binary ALU operations
These accept three operands; the last one can be an immediate value for integer operands, or a register for all operand types. 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 >> O3(1)
rshi     i  ui  l  ul           O1 = O2 >> O3(2)

Unary ALU operations
These accept two operands, both of which must be registers.
 
negr     i     l         f  d  O1 = -O2
notr     i  ui l  ul           O1 = ~O2

Compare instructions
These accept three operands; again, the last can be an immediate value for integer data types. The last two operands are compared, and the first operand is set to either 0 or 1, according to whether the given condition was met or not.

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)

Transfer operations
These accept two operands; for 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.

Network extensions
These accept two operands, both of which must be registers; these two instructions actually perform the same task, yet they are assigned to two mnemonics for the sake of convenience and completeness. As usual, the first operand is the destination and the second is the source.
 
hton       us ui          Host-to-network (big endian) order
ntoh       us ui          Network-to-host order 

Load operations
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)

Store operations
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

Stack management
These accept a single register parameter. These operations are not guaranteed to be efficient on all architectures.

 
pushr                     i  ui  l  ul  p   push O1 on the stack
popr                      i  ui  l  ul  p   pop O1 off the stack

Argument management
These are:
 
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.

Branch instructions
Like 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 O1(4)
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

Jump and return operations
These accept one argument except 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 section 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 section 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


[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [Top] [Contents] [Index] [ ? ]

This document was generated by Alistair Turnbull on April, 12 2005 using texi2html