1

in RISCV assembly, "li" is an pseudoinstruction. I have this instruction:

li      t2, 0x1800
csrc    mstatus, t2

The "li" is assembled into following 2 instruction.

lui x7 2
addi x7 x7 -2048

My question , why 2 and -2048? and why "li" assembled into lui and addi? are there a document for this kind of behavior?

I have used "riscv64-unknown-elf-as" as assembler.

anru
  • 1,335
  • 3
  • 16
  • 31
  • RISC-V immediates are 12 bits wide, and `0x1800` has non-zero bits in the low 12 and outside that. `0x2000 - 0x800` is `0x1800`, and `-0x800` is encodeable as a 12-bit 2's complement signed integer. See [RISC-V build 32-bit constants with LUI and ADDI](https://stackoverflow.com/q/50742420) for more detail about how a 32-bit constant breaks down into lo and hi parts in general. – Peter Cordes May 25 '23 at 11:12

2 Answers2

4

are there a document for this kind of behavior?

This is not really considered behavior, but rather a clever yet well known code sequence to shorten composition of immediates, used by assemblers and compilers.

The only behavior of the processor is sign extension of the 12-bit immediate in all I-Type instructions.

The reason the designers do this is a combination of two things:

  • That they want to allow for negative immediates for instructions like addi, as well as for lw and sw deeming that negative offsets are sufficiently useful, as they can be used for frame pointer relative arithmetic to access local variables, or reaching the header of a block that immediately precedes the block, among other things.

  • And further, they want the hardware to have only one kind of 12-bit extension, namely signed extension.

These two points, taken together, mean that lui and one of: addi, lw, sw, can accomplish full 32-bit addresses / values, all working the same: sign extension of the second instruction may require incrementing the constant used for the lui.

They didn't have to architect it this way; for example, they could have provided an another instruction addui that clears the upper 20 bits before adding; or, they could have provided versions of lw and sw that do the same, or defined lw and sw to support only 12-bit unsigned immediates.

But what they chose was a compromise to both allow negative immediates in general, and otherwise the simpler of the hardware alternatives.

The designers have gone to some lengths to simplify the hardware with consideration for embedded and otherwise power/size limited processors


why 2 and -2048?

To avoid the sign extension feature of addi with negative 12-bit numbers, you would have to limit immediates to 11 bits unsigned, which would leave the 12th bit, sign bit, as zero, and thus would not be negative in 12 bits, so would never extend a negative sign.  For example, 0x400 fits in 11 bits, so with that we can do:

lui x7, 1
addi x7, x7, 0x400
addi x7, x7, 0x400

achieving 0x1000 + 0x400 + 0x400 = 0x1800.

However, as you can see that involves three instructions!

To shorten the code sequence, we must take advantage of the extra 12th (sign) bit, even though it is going to be set/on/true/1/negative, and will cause -1 value for the upper 20 bits of the immediate before use by the addi.

That -1 (of the upper 20 bits caused by sign extension of the 12 bit immediate) needs to be offset by +1 (of the upper 20 bits) to obtain the desired number, and that +1 offset is done in the lui instruction, hence lui x7, 2 instead of 1, and addi x7, x7, 0x800 to accomplish the 2 instruction sequence.  0x800 taken as a signed 12-bit number is -2048, so: 2 and -2048: 0x2000=8192; 8192 + -2048 = 6144; 6144=0x1800.

Erik Eidt
  • 23,049
  • 2
  • 29
  • 53
0

you may check RISCV_CONST_HIGH_PART in binutils source code for more details.

sunway
  • 237
  • 1
  • 2
  • 7