Keyboard Shortcuts

Ctrl+K
Open search
Esc
Close search / dialog
↑
Back to top

The Immediates That Do Not Fit

26 minadvancedModule 2: The Encoding: What the Bytes AreContinues from rv-compressed
26 min · RISC-V: The Encoding Spectrum

Why this matters

Concept 2 measured that RISC-V keeps all three register fields at the same bit position in every format, and quoted the price: “at the expense of having to move immediate bits across formats.” Moving immediate bits across formats is what a permutation is. So RISC-V’s immediate story is not five independent encodings to memorise. It is one decision — keep the registers still — paid for five different ways, and the masks in concept 2 are the receipts.

There is also a decision hiding inside one of them that nobody will tell you if you only read the field table, and it is the reason two instructions you would expect to be interchangeable are not. The shift amount in slli is six bits wide in a twelve-bit field.inst[25] selects SRAI from SRLI and inst[24] is required to be zero, so the amount is not imm[11:0] at all. The only place the number 63 appears is in the assembler’s refusal message.

The five, and what each one costs

Every figure below is MEASURED two ways that agree: the field transcribed from the manual’s figures, and the reach computed from the encoding and then confirmed by asking a real assembler for the value just inside and just outside it. The agreement is the check on the transcription.

$ python3 rvdec.py --run | sed -n '/format   field     the reach/,/^$/p'
  format   field     the reach        MEASURED by asking    what it says one step past
  I        4096 bits  [-2048, 2047]    [-2048, 2047]        operand must be a symbol with %lo
  S        4096 bits  [-2048, 2047]    [-2048, 2047]        operand must be a symbol with %lo
  B        8191 bits  [-4096, 4094]    [-1048572, 1048578]  fixup value out of range
  U        33554417 bits             a 20-bit field, always shifted left 12  no signed range
  J        2097151 bits  [-1048576, 1048574]  [-1048576, 1048574]  fixup value out of range
                

Read the first two rows together, because the answer is counter-intuitive and it is the first relief a decoder author gets on this architecture. The I-type and the S-type immediates are in DIFFERENT bit positions — inst[31:20] for one, inst[31:25] with inst[11:7] for the other — and the assembler refuses both with the SAME two numbers. Two permutations, one range, and the range is a property of the field’s WIDTH rather than of its position. That is a direct consequence of the decision to keep the register fields still, and it is why the S format can split its immediate across the instruction without costing the programmer anything.

The third refusal is a different kind of measurement and it is more interesting than the first two. slli with an amount of 64 is refused with “[0, 63]”, and the reason is that the amount is a six-bit field:

  slli a0, a0, 64 -> immediate must be an integer in the range [0, 63]
                

So the shift amount is also the thing that collides with the register positions — an immediate that has to dodge a register field is an immediate whose width is not obvious from its position, and the assembler’s diagnostic is the only place the number 63 appears. This is a real decoder bug the artifact had: the first version printed the whole inst[31:20] window as the amount, so every srai in the corpus reported a shift amount 1,024 too large. Eight words were wrong and every one of them was a real instruction.

The B type, whose bits are not contiguous

This is the case the plan called out and it is correct. The encoding:

  imm[12]    = inst[31]     the sign, alone in bit 31 of the instruction
  imm[11]    = inst[7]      the HIGHEST bit, in the S format's LOWEST slot
  imm[10:5]  = inst[30:25]
  imm[4:1]   = inst[11:8]
  imm[0]     = 0            a CONSTANT -- not a field
                

Three things there are worth reading twice. imm[0] is not a field, it is a hard zero, and it is there because a branch target must be even: IALIGN is 16 after the compressed extension is added and 32 without it, so bit 0 of a branch displacement is always zero and spending a bit on it would waste a bit. The manual’s reason is QUOTED and it is a hardware reason, not a tidiness one: “By rotating bits in the instruction encoding of B and J immediates instead of using dynamic hardware multiplexers to multiply the immediate by 2, we reduce instruction signal fanout and immediate multiplexer costs by around a factor of 2.”

Second: the field is NOT contiguous and NOT in one place. It lives in inst[31], inst[30:25] and inst[11:7], and nowhere else. Thirteen of the thirty-two bits, inst[24:12], are not part of the displacement at all. And third, the fact this whole course has been building toward: the field’s top bit is in the position the S format uses for the field’s bottom bit — the two masks differ by exactly bit 7, and concept 2 measured it.

Reconstructed from the bits, not quoted

The transcription is checked against known immediates rather than trusted. Twenty-six offsets assembled and decoded back, and the ends are the rows worth reading:

$ python3 rvdec.py --run | sed -n '/asked for   the word/,/^$/p' | tail -8
  2048        0x00b500e3    beq	a0, a1, 0x800 <T+0x800>                     2048
  4094        0x7eb50fe3    beq	a0, a1, 0xffe <T+0xffe>                     4094
  -2048       0x80b500e3    beq	a0, a1, 0xfffffffffffff800 <...>       -2048
  -4096       0x80b50063    beq	a0, a1, 0xfffffffffffff000 <...>       -4096
                

Now the row that matters, and the arithmetic behind it:

$ python3 rvdec.py --run | sed -n '/format    near end/,/^$/p'
  format    near end    far end     symmetry                    scaled
  B         4094        4096        +4094 / 4096  NOT symmetric  x2
  I         2047        2048        +2047 / 2048  NOT symmetric
  J         1048574     1048576     +1048574 / 1048576  NOT symmetric  x2
  S         2047        2048        +2047 / 2048  NOT symmetric
  U         16777200    16777216    +16777200 / 16777216  NOT symmetric
                

The B-type reach is +4,094 and −4,096. A 13-bit two’s-complement field scaled by two, and the ends are NOT the same number. The asymmetry is arithmetic: the field holds [−4,096, +4,095], the low bit is forced to zero, so the positive end is +4,094 and the negative end is −4,096. A sign extension is not a magnitude, and a field with a forced zero at bit 0 is not symmetric about zero. The first draft of this artifact computed the range arithmetically and got [−4,096, +4,096], and the AArch64 course retracted the same class of mistake twice in its own section. B and J are not symmetric for this reason; I and S are, because their bit 0 is not forced.

The best measurement in the course: what the assembler does instead of refusing

The plan said the assembler’s diagnostic at the edge of the B-type range would be the best measurement in the concept. It is — but not for the reason the plan expected, and getting this wrong is the single most useful thing on this page.

Ask for a branch past the end of the B type’s reach. The first version of this artifact was built on the expectation that a diagnostic appears, and a section written around “ask the assembler and read the error” would have found no error to read:

$ python3 rvdec.py --run | sed -n '/beq \.    /,/^$/p'
    beq .    +4096   4 bytes, bne	a0, a1, 0x8 <T+0x8>
    beq .    +8192   4 bytes, bne	a0, a1, 0x8 <T+0x8>
    beq .  +65536   4 bytes, bne	a0, a1, 0x8 <T+0x8>
    beq . +1048576   4 bytes, bne	a0, a1, 0x8 <T+0x8>
    beq . -1048576   REFUSED: fixup value out of range
                

IT DOES NOT REFUSE. It RELAXES.beq a0, a1, .+4096 is accepted and assembled into TWO instructions: an inverted branch over an unconditional jump, because a B-type cannot reach that far and the assembler has a longer sequence that means the same thing. A reader who writes a branch past the B-type’s reach and then decodes the result expecting a branch finds a conditional jump and an unconditional jump, and the branch they wrote is gone.

That is a refusal that is not a refusal, and it is dangerous in the specific way this course keeps looking for:

And the diagnostic does appear eventually, at 1 MiB, where the relaxation itself runs out of room. Note the word: “fixup”, not “displacement”. It is the assembler telling you the RELAXATION could not be applied, which is a different failure from the encoding being out of range, and a tool that reports only the second would be misleading you about the first. The reloc course is about what a fixup is; this is the first place a reader meets one, and it is in an error message.

And the pair the short reach forces into existence

The ±2 KiB B-type range is why auipc+addi pairs exist. A branch that can only reach 2 KiB cannot reach across a function, let alone across a translation unit, so every far call in a real program is a PAIR of instructions with the address split across them. Measured on the same C, from the same compiler, and the relocations are the point — they are what makes the pair a pair rather than two instructions that happen to be adjacent:

$ python3 rvdec.py --run | sed -n '/THE RELOCATIONS/,/^$/p'
  offset    relocation                  symbol
  0x0002    R_RISCV_HI20                table
  0x0002    R_RISCV_RELAX               *ABS*
  0x0006    R_RISCV_LO12_I              table
  0x0006    R_RISCV_RELAX               *ABS*
  0x0012    R_RISCV_HI20                label
  0x0012    R_RISCV_RELAX               *ABS*
  0x0016    R_RISCV_LO12_I              label
  0x0016    R_RISCV_RELAX               *ABS*
                

Note that the second half is an ADD and not a generic immediate add: the RISC-V encoding has one 12-bit immediate field and the linker has to know which instruction owns it. Compare the AArch64 pair on the same C, for the same two halves:

  offset    relocation                        symbol
  0x0000    R_AARCH64_ADR_PREL_PG_HI21        table
  0x0004    R_AARCH64_ADD_ABS_LO12_NC         table
  0x0014    R_AARCH64_ADR_PREL_PG_HI21        label
  0x0018    R_AARCH64_LDST8_ABS_LO12_NC       label
                

Three differences, all measured, and two of them are design differences rather than notational ones:

What this section cannot show

Try it

  1. Ask the assembler for the boundary you predicted and read what it prints.(beq a0, a1, .+4094 assembles to one instruction. beq a0, a1, .+4096 assembles to two. Disassemble both. Then write the one-instruction version by hand as .insn 4, 0x… and confirm the assembler will not take it. That is the boundary, and it is discoverable three ways: arithmetic, a refusal, and the shape of the output.)
  2. Reconstruct the B immediate by hand for three displacements and check your permutation.(+2,048 is 0x00b500e3 and −4,096 is 0x80b50063. Get +2,048 wrong in a way that keeps the parity — for example by putting inst[8] in imm[0], which the artifact’s first version did — and the target comes out odd, which is a displacement no RISC-V instruction can encode. That bug cost 26 words of the corpus and every one of them produced a plausible-looking address.)
  3. Find the six-bit field nobody documented in the field table.(slli a0, a0, 64 is refused with “[0, 63]”. Ask for slli a0, a0, 32 and get the four bytes, then read inst[25:20] and notice that two of the six bits are not part of the amount. A decoder that prints inst[31:20] gets slli right by luck and srai wrong by 1,024, which is what this artifact did for eight words before the cross-check found it.)

Connect

Backwards, concept 2 measured the two masks that differ by one bit, and this page is what that one bit costs in reach. Across, a64-immediate is the same problem with a different answer: AArch64’s logical immediate is a rotate and a run length and reaches 1,302 of 4,294,967,296 constants, where RISC-V reaches every 12-bit value and reaches nothing beyond it without a pair. exe-verify is the course that would check that the pair lands on the right address, and this page is the last place before it where the question is open.

Forwards, concept 5 is where the fixup value out of range message in this page gets its explanation — because that word is the first appearance of a relocation in the course, and the verification concept is where relocations stop being an error message and become a thing you can read.