Concept 1 established that the ISA is a set of documents. This concept takes the document that matters most for a decoder — the base integer set — apart at the bit level, and it opens with a retraction, because the plan this course was written from got the central prediction backwards.
The plan said: “the same register number sits in different bit positions in different formats, and that is why an assembler is not a lookup table.” Measured, the first half is false and the second half is true for a different reason. Here is the measurement, and then what it costs.
$ python3 rvdec.py --fields | sed -n '/^ field /,/rs2, in the R/p'
field xor mask vars the bit positions it moved what
rd, in the R format 0x00000f80 20 7,8,9,10,11 rd
rs1, in the R format 0x000f8000 20 15,16,17,18,19 rs1
rs2, in the R format 0x01f00000 20 20,21,22,23,24 rs2
And those same three positions, on the I format, the S format, the B format and the U format: bits[11:7], bits[19:15], bits[24:20], every time. The manual says so and says why, and it is QUOTED, section 2.2:
“The RISC-V ISA keeps the source (rs1 and rs2) and destination (rd)
registers at the same position in all formats to simplify decoding.”
and in the note: “Decoding register specifiers is usually on the critical
paths in implementations, and so the instruction format was chosen to keep all
register specifiers at the same position in all formats at the expense of
having to move immediate bits across formats.”
The registers stay still and the immediates pay for it. That is the whole trade, in the specification’s own words, and the rest of this concept is the bill.
The count is six and the manual’s own count is four, and both are right about different things. QUOTED, section 2.2: “In the base RV32I ISA, there are four core instruction formats (R/I/S/U)”; and then section 2.3: “There are a further two variants of the instruction formats (B/J) based on the handling of immediates.”
So: four core formats plus two immediate variants, and the six names are the union. A course that said “RISC-V has six formats” and left it there would be implying the manual counts six, and it does not. The distinction is not pedantic: the four core formats are four field layouts, and the two variants are rearrangements of layouts that already exist. B is S with the immediate moved. J is U with the immediate moved and the two register slots spent on the displacement instead.
format inst[31:0] name what it is for
R funct7|rs2|rs1|funct3|rd|opcode reg-reg no immediate at all
I imm[11:0]|rs1|funct3|rd|opcode reg-imm CONTIGUOUS at inst[31:20]
S imm[11:5]|rs2|rs1|funct3|imm[4:0]|opcode store no rd, so the imm is split
B imm[12|10:5]|rs2|rs1|funct3|imm[4:1|11]|op branch target must be EVEN
U imm[31:12]|rd|opcode upper imm 20 bits already in place
J imm[20|10:1|11|19:12]|rd|opcode jump no rs1, no rs2, so 20 bits
become 21
Read the R row and the S row together, because that pair is the whole of the register-position decision. R has no immediate, so its five register-ish fields use all 32 bits: seven of opcode, three of funct3, seven of funct7, five each of rd, rs1 and rs2. S has no destination — a store writes to memory, not to a register — and the five bits the destination would have occupied are the low five bits of the immediate instead. That is the whole of the S format and it is why the S immediate is in two places.
And read the B row against the S row again, because the relationship is stated in the manual and it is the sharpest single fact in the base encoding. QUOTED, section 2.3:
“The only difference between the S and B formats is that the 12-bit
immediate field is used to encode branch offsets in multiples of 2 in the B
format. Instead of shifting all bits in the instruction-encoded immediate
left by one in hardware as is conventionally done, the middle bits (imm[10:1])
and sign bit stay in fixed positions, while the lowest bit in S format
(inst[7]) encodes a high-order bit in B format.”
Read it twice. In S, inst[7] is the immediate’s bit 0. In B, inst[7] is the immediate’s bit 11. The same bit position, the same instruction width, the same seven-bit funct7 slot above it — and it is the top of the displacement instead of the bottom. The masks see it exactly:
$ python3 rvdec.py --fields | sed -n '/the S immediate/,/the B funct3/p'
the S immediate, via sw 0xfe000e00 11 9,10,11,25,26,27,28,29,30,31 imm
the B immediate, 17 offsets 0xfe000f80 17 7,8,9,10,11,25,26,27,28,29,30,31 imm
Two masks, ten bits each, and they differ by exactly bit 7. One bit, one position, the two ends of a displacement.
The method: assemble an instruction, change one operand, assemble it again, and OR the XOR over a sweep of variants. Every bit that moves belongs to that operand. Three rules make it work, and all three are the results of getting it wrong first.
addi a1, a1, 0 against addi a1, a1, 0x800 differs in ONE bit of a twelve-bit field, and the first version of this artifact reported a one-bit imm — a plausible-looking wrong answer rather than an obvious failure.-march that rewrites the format. The first version ran at -march=rv64gcv, and then lw a0, 8(a1) assembled to a two-bytec.lw. The XOR mixed a 2-byte word with 4-byte words and produced a mask with nine bits scattered across both halves of the instruction. A field map measured through an extension that rewrites the format is a field map of the wrong architecture. The base sweeps now run at -march=rv64g.Here is the base table, and the immediate rows are the interesting half because the registers are the same everywhere:
$ python3 rvdec.py --fields | sed -n '/^ field /,/the opcode/p'
field xor mask vars the bit positions it moved
rd, in the R format 0x00000f80 20 7,8,9,10,11
rs1, in the R format 0x000f8000 20 15,16,17,18,19
rs2, in the R format 0x01f00000 20 20,21,22,23,24
funct7+funct3, 9 base R ops 0x42007000 9 12,13,14,25,30
funct3, the 6 other load sizes 0x00007000 6 12,13,14
imm[11:0], the I format 0xfff00000 15 20,21,22,23,24,25,26,27,28,29,30,31
the S immediate, via sw 0xfe000e00 11 9,10,11,25,26,27,28,29,30,31
the B immediate, 17 offsets 0xfe000f80 17 7,8,9,10,11,25,26,27,28,29,30,31
the B funct3, 6 branches 0x00007000 5 12,13,14
the U immediate, 20 bits 0xfffff000 10 12,13,...,31
the J immediate, 20 offsets 0xfffff000 18 12,13,...,31
A WINDOW AND A FIELD ARE DIFFERENT OBJECTS. That is the sentence to carry to the next architecture. The U and J masks are identical — both 0xfffff000, twenty contiguous bits at inst[31:12] — and the two immediates are nothing alike. U is 20 bits at the top of a 32-bit value with twelve zeros below. J is 21 bits scaled by two with a zero at the bottom, spread across inst[19:12] and inst[30:20] in a permuted order. A mask says two fields occupy the same WINDOW. It does not say they are the same FIELD, and a decoder that reads the J immediate as a contiguous 20-bit field gets every displacement in [-2048, 2046] right — which is the small ones — and every larger one wrong.
The funct7+funct3 row moved bits 25 and 30 out of a seven-bit field, and a reader is entitled to ask whether the measurement failed. It did not, and the reason is the field is not seven bits of one thing:
$ python3 rvdec.py --section 4 | sed -n '/^ source /,/fclass/p'
source word inst[31:25] inst[26:25] funct3
add a0, a1, a2 0x00c58533 0000000 00 0
sub a0, a1, a2 0x40c58533 0100000 00 0
sll a0, a1, a2 0x00c59533 0000000 00 1
mul a0, a1, a2 0x02c58533 0000001 01 0
fadd.s fa0, fa1, fa2 0x00c5f553 0000000 00 7
fsub.s fa0, fa1, fa2 0x08c5f553 0000100 00 7
fmin.s fa0, fa1, fa2 0x28c58553 0010100 00 0
fsqrt.s fa0, fa1 0x5805f553 0101100 00 7
fcvt.w.s a0, fa0 0xc0057553 1100000 00 7
add has funct7 = 0b0000000 and sub has 0b0100000. One bit apart. Those are the only two values the base R format can reach from an add/sub pair, so a sweep over nine base operations moves bit 30 and not the other six — except that mul has funct7 = 0b0000001, which moves bit 25. To move the REST you have to leave the base ISA, because in the base ISA there is nothing else in funct7. The F and D extensions fill it, and the table above reaches all seven positions.
Both numbers are true, they measure different things, and a course that printed only the first would have claimed a two-bit funct7:
The base sweep moved bits 25,30 of funct7's seven positions.
The table above reaches all seven. Both numbers are true,
they measure different things, and a course that printed
only the first would have claimed a two-bit funct7.
And the reason the manual’s funct7 column heading is a simplification: under opcode 0x53 those seven bits are a five-bit operation selector at inst[31:27] and a two-bit width at inst[26:25], and there is no way to read them as one seven-bit funct7 at all. fadd.s and fadd.d differ in inst[25] — one bit. The first version of this artifact’s r_fp model read a seven-bit funct7 and therefore could not tell them apart, and separately could not tell fsqrt.s from fadd.s, because for the square root the same five bits are the high half of the selector.
One more thing about funct7 that is a dispatch bug rather than a width bug, and it is the defect that shaped the whole of concept 5: a sub-opcode selector is not self-contained.fmadd.s is inst[31:27] = 0b01101 with opcode 0x43. fmin.s is inst[31:27] = 0b00101 with opcode 0x53. Two different five-bit values, so that is not the sharpest case — but fadd.s is inst[31:27] = 0b00001 under opcode 0x53 and add is inst[31:25] = 0b0000000 under opcode 0x33, and the floating-point one has funct3 = 7 where the integer one has funct3 = 0. A dispatch that asks models in order without first reading the opcode resolves these by list position, which is not a decoding strategy, it is a coin flip with a plausible-looking output.
Everything above is the base ISA, and everything above is the same in every format. Here is the one place where RISC-V has a second way to name a register, and it is the trap this course calls the sharpest:
$ python3 rvdec.py --fields | sed -n '/C CR: rd/,/C CB: rd/p'
C CR: rd, five bits 0x00000f80 20 7,8,9,10,11 rd (5 bits)
C CR: rs2, five bits 0x0000007c 20 2,3,4,5,6 rs2 (5 bits)
C CI: rd, five bits 0x00000f80 20 7,8,9,10,11 rd (5 bits)
C CA: rd-prime 0x00000380 8 7,8,9 rd' (3 bits)
C CA: rs2-prime 0x0000001c 8 2,3,4 rs2' (3 bits)
C CL: rd-prime 0x0000001c 8 2,3,4 rd' (3 bits)
C CL: rs1-prime 0x00000380 8 7,8,9 rs1' (3 bits)
Four fields, two positions, two widths. c.mv keeps rd at bits[11:7] and c.mv’s rs2 at bits[6:2] — the base positions, unchanged, which is what the manual means by “when the full 5-bit destination register specifier is present, it is in the same place as in the 32-bit RISC-V encoding”. And c.add rd', rs2' puts rd' at bits[9:7] and rs2' at bits[4:2]: three bits each, two positions lower.
And here is the part the bit positions do not tell you: the value 0 in rd' does not mean x0. It means x8. The three-bit field indexes the eight registers x8 to x15. The manual prints the table:
000 x8 001 x9 010 x10 011 x11 100 x12 101 x13 110 x14 111 x15
s0 s1 a0 a1 a2 a3 a4 a5
A decoder that reads a compressed register with a five-bit lookup reports x0 to x7 where the encoding means x8 to x15. Every register name in the disassembly is seven too small, every operand is a real register, and nothing about the output looks wrong. That is a decoder bug with no symptom, which is why concept 5 is worth its twenty minutes.
And the QUOTED reason those eight are those eight — because the coupling is the point, and it is between three things no single specification reveals:
“The RISC-V ABI was changed to make the frequently used registers map to
registers x8-x15. This simplifies the decompression decoder by having a
contiguous naturally aligned set of register numbers.”
So the compression is not only an encoding decision. It is a decision that REQUIRES an ABI that puts the eight most-used registers in a contiguous run, and the ABI was changed to make it possible. A coupling between an instruction encoding, a register file and a calling convention. Concept 3 measures what that coupling costs.
sw a0, N(a1) over N = 0, 4, 8, …, 2040 and OR the XOR. Expect mask 0xfe000e00, bits[31:25] and bits[11:7], ten positions. Then sweep beq a0, a1, N over even N. Expect 0xfe000f80 and expect the difference to be bit 7 and nothing else. Then ask the assembler for sw a0, 2048(a1) and read why.)rvdec.py, change creg3 from return xreg(8 + (n & 7)) to return xreg(n & 7) and re-run. Expect the section 10 cross-check to report a large disagreement count that is entirely c.* words, and expect every one of them to be a register name that is seven too small. Nothing crashes. Nothing looks wrong. This is the exercise the whole cross-check concept is built for, and doing it by hand is worth more than reading about it.)c.jr from c.jalr and then find the model that gets it wrong.(It is inst[12], and the artifact’s first version dispatched on rs1 == 0 instead — which is not a bit of that cell at all. Expect c.mv to have been unreachable and 33 corpus words to have reported as c.add, and expect c.jalr to have reported as c.jr, which is a decoder that turns every indirect call into an indirect jump. The cross-check found it only because the canonical form for c.jr and c.jalr was written so that conflating them produces a disagreement rather than a silent error.)Backwards, concept 1 is where the funct7 value that the M extension fills came from, and this page is where the cost of it becomes visible: a64-encoding measured 58 field positions for AArch64 and found the class field four bits wide, because AArch64 has no prefixes and no length arithmetic and so can spend bits on a class. RISC-V has the same goal — keep the register specifiers still — and a completely different mechanism, because the C extension’s second register encoding means the register position is not uniform after all, only uniform within the base.
Forwards, concept 3 finishes the compressed half: the nine formats, the reserved code points, and the eleven families of displacement that share five bit positions and are assigned seven different orders. Concept 4 takes the two masks that differ by one bit and finds out what a 13-bit permuted displacement actually reaches.