The shifting range in RISC-V
There are several commonly used RISC-V
instruction pairs
with 32-bit immediates.
Below is an example of loading a 32-bit immediate
into a register using lui
/addi
:
lui rd,imm[31:12]
addi rd,rd,imm[11:0]
Here lui
places
a (sign-extended) 20-bit immediate into register rd
and fills the lowest 12 bits with zeros,
and addi
adds a sign-extended 12-bit immediate
to register rd
.
Question: does this work for any 32-bit immediate? It may be trickier than you think.
Clearly, on 32-bit systems,
this instruction pair can be used to load any 32-bit immediate
in the range [-231, 231-1].
For example,
to load 0x7fffff00
,
one may use lui
with a 20-bit 0x80000
and
addi
with a 12-bit 0xf00
,
as adding 0x8000000
and 0xffffff00
(sign-extended from 0xf00
) produces 0x7fffff00
.
How about 64-bit systems?
Specifically,
let’s look at loading the 32-bit immediate 0x7fffff00
again.
Note that the same 20-bit and 12-bit values used on 32-bit systems won’t work,
as adding 0xffffffff_8000000
(sign-extended from 0x80000000
) and
0xffffffff_ffffff00
(sign-extended from 0xf00
) produces 0x0fffffff_f7ffff00
.
Does there exist any 20-bit and 12-bit values
that make lui
/addi
work on 64-bit systems?
Similar questions can be asked about other instruction pairs,
such as lui
/ld
for loading a value at a 32-bit address,
or auipc
/jalr
for jumping to a 32-bit pc-relative offset.
The short answer is no. You may be interested in the discussion in the RISC-V ISA Dev group started by Luke Nelson, which prompted the RISC-V ISA specification to clarify the range in the “RV64I Base Integer Instruction Set” chapter:
Note that the set of address offsets that can be formed by pairing LUI with LD, AUIPC with JALR, etc. in RV64I is [−231−211, 231−211−1].
In other words,
the 32-bit range reachable by such instruction pairs
in 64-bit RISC-V is shifted by -211
from [-231, 231-1].
Therefore, 0x7fffff00
doesn’t fall in the range.
Intuitively, the shifting is caused by the choice of the sign extension of immediates in the RISC-V ISA. See examples of issues reported in coreboot and the BPF JIT for RV64 in the Linux kernel, as well as our upcoming Jitterbug paper.
To check the correctness of the range, I wrote a simple Rosette program, as follows:
#lang rosette
; integer register width in bits
(define XLEN 64)
; symbolic 20-bit and 12-bit values
(define-symbolic imm20 (bitvector 20))
(define-symbolic imm12 (bitvector 12))
; mimic the result of an instruction pair
(define v (bvadd (sign-extend (concat imm20 (bv 0 12)) (bitvector XLEN))
(sign-extend imm12 (bitvector XLEN))))
; lower and upper bounds
(define-symbolic lower upper (bitvector XLEN))
; find the lower and upper bounds via optimization
(optimize #:maximize (list lower)
#:minimize (list upper)
#:guarantee (assert (forall (list imm12 imm20)
(&& (bvsge v lower)
(bvsle v upper)))))
Rosette invokes the Z3 SMT solver to find the lower and upper bounds of the reachable range (you may also use SMT or the Z3 API directly). The output of the above program is:
(model
[lower (bv #xffffffff7ffff800 64)]
[upper (bv #x000000007ffff7ff 64)])
This is consistent with the clarification in the RISC-V ISA specification.
Exercise:
if you were asked to make one change in the 64-bit RISC-V ISA
(e.g., the semantics of an instruction)
to make the range remain
[-231, 231-1],
what would you do?
The above Rosette program may be helpful.
You may also want to check the addiw
instruction.
Acknowledgments: James Bornholt, Luke Nelson, and Emina Torlak provided feedback on a draft of this post.