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.
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.
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.
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 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.
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:
lui is ABSOLUTE; AArch64’s adrp is PC-RELATIVE. It is visible in the names: RISC-V’s are R_RISCV_HI20 and R_RISCV_LO12_I with no PC in either, and AArch64’s carry _PREL_ in both. For position-independent code — the default on both targets — the RISC-V compiler has to switch to auipc, which is why the corpus objects contain auipc and not lui.lui and AArch64’s adrp are not the same reach. A U-type carries 20 bits at the top of a 32-bit value; adrp carries 21 bits of a page-aligned address. That is a different pair of halves, and the two architectures solved the same problem with the same shape and different widths — which is exactly the sort of difference a course that teaches only the shape would miss.addi that says “this addi is the second half of an address”. The pairing is a property of the RELOCATION TABLE, which means a decoder reading bytes in isolation — which is what this file’s part one does — cannot recover the address a program intended. AArch64 has the same property and solves it the same way.riscv64-linux-gnu-ld is absent. What is measured is the two instructions and their two relocations, which is the half of the story that is in the bytes. The other half — that the pair lands on the right address — is not measured here, is not measured anywhere in this course, and would need the link course and a machine.clang at this version on this source, and the auipc-versus-lui choice in particular is a position-independent-code decision that a different link model would change.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.)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.)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.)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.