Start with the number, because the number is where the privilege lives. A CSR address is twelve bits, and the specification splits the top four of them into two pairs with two different jobs: the upper pair says whether the register can be written, and the pair below it says which privilege level is allowed to touch it at all.
$ python3 rvpriv.py --run | grep -A 2 'The top two bits'
The top two bits (csr[11:10]) indicate whether the register is read/write
(00, 01, or 10) or read-only (11). The next two bits (csr[9:8]) encode the
lowest privilege level that can access the CSR.
Now put two registers next to each other, which is what makes the convention worth reading twice:
$ python3 rvpriv.py --run | grep -A 3 'So `stvec` at 0x105'
So `stvec` at 0x105 is 00 / 01 / 0x05: read/write, lowest privilege S,
number 5. `mtvec` at 0x305 is 00 / 11 / 0x05: read/write, lowest privilege
M, number 5. THE SAME NUMBER, FIVE, IN BOTH, AND THE ONLY DIFFERENCE IS TWO
BITS OF PRIVILEGE.
Same register number, same access class, two bits apart. A supervisor kernel and a machine kernel both want a trap vector, both call it number 5, and the privilege bits are the entire reason they are different registers. That is what a convention is: not a name, but a number whose parts are assigned.
And the table in the artifact prints the row you will not find in most summaries. The privilege field has four encodings and only three meanings — U, S, M — so one of them is a hole:
$ python3 rvpriv.py --run | grep -A 2 'THE RESERVED ROW IS ALSO'
THE RESERVED ROW IS ALSO IN THE TABLE, which is worth having: the
user-level trap registers at 0x041 to 0x044 have `csr[9:8] = 0`, and 0x105's
encoding value 2 in the privilege field is a hole that no standard register
occupies.
And the specification does that by leaving it out of its own table, which is the quietest way a standard can define a hole: there are four encodings of a two-bit field, three privilege levels, and the fourth value simply has no row. A reader who assumed all four values of a two-bit field were meaningful would expect four privilege levels. There are three.
Printing the hole is not pedantry. A table with a gap and a table with a hole are different tables, and a reader who has only ever seen the filled-in version cannot tell which one they are looking at — so when they extend the table themselves, they will fill the hole without noticing.
The accessibility half is measured too, which is the check on it. cycle at 0xc00 and mvendorid at 0xf11 both have csr[11:10] = 11 and both are read-only; stvec at 0x105 and satp at 0x180 both have 00 and both are read/write. The artifact’s sentence about that is the one worth keeping:
$ python3 rvpriv.py --run | grep -A 2 'AND THE READ-ONLY CLASS'
So the bit that says "you cannot write this" is in the address, and the
SAME instruction with the same five bits in its operand slot is a legal read
and an illegal write depending on two bits of a constant three instructions
earlier.
Three instructions earlier. That is the whole difficulty of a privileged architecture in one clause: the permission is not in the instruction, it is in a number the instruction carries, and the number was put there by whatever code computed it.
The CSR address tells you which register. Six instructions tell you what you may do with it, in two source forms, and every one of the twelve words is read out of the object file rather than out of the manual.
$ python3 rvpriv.py --run | sed -n '/THE TWELVE INSTRUCTIONS/,/^$/p' | tail -5
op register form word funct3 immediate form word funct3 csr
CSRRW csrrw 0xc00312f3 1 csrrwi 0xc002d2f3 5 0xc00
CSRRS csrrs 0xc00322f3 2 csrrsi 0xc002e2f3 6 0xc00
CSRRC csrrc 0xc00332f3 3 csrrci 0xc002f2f3 7 0xc00
Three operations — write-and-set, set, write-and-clear — and two families. Look at the last two columns: the register forms take funct3 1, 2, 3 and the immediate forms take 5, 6, 7. Now do the XOR, which is the entire finding of this section:
$ python3 rvpriv.py --run | sed -n '/THE ONE BIT/,/^$/p' | tail -5
register form funct3 immediate form funct3 xor where
csrrw 1 csrrwi 5 0x04 bit 2 of funct3
csrrs 2 csrrsi 6 0x04 bit 2 of funct3
csrrc 3 csrrci 7 0x04 bit 2 of funct3
1 ^ 5 = 2 ^ 6 = 3 ^ 7 = 4, and 4 is bit 2 of a three-bit field. Three pairs, three identical deltas, one bit. A register operand and an immediate operand are the same instruction with one bit flipped, and the bit is inside the selector rather than anywhere in the operand fields.
That is a design decision rather than an accident, and the reason is worth a sentence. A register field and an immediate field are the same width — five bits. A designer who had put them in different places would have had to spend a bit somewhere. RISC-V spends it in the selector instead, where it costs nothing at decode time because the selector is read first, and the reader gets one selector with six values instead of two selectors with three values each.
The alias table is where the mechanism becomes concrete. A pseudo-instruction is not a separate encoding; it is a three-operand form with x0 in one of the two register slots, so csrw cycle, t0 and csrrw zero, cycle, t0 are the same four bytes. All eight comparisons in the artifact’s table are byte-identical.
One of those eight is a mistake worth pre-empting, because it is the mistake this course’s own first draft made. csrw is csrrw, not csrs — you might assume the w stands for “write the value in” rather than for the instruction’s own mnemonic, and if you do, the comparison prints a difference of 0x00000000, which is a real difference and not a typo: the two words differ in funct3 alone, 1 against 2, a write against a set. That is retraction R3, and the way to tell which kind of difference you are looking at is to look at the XOR itself.
Every manual draws the CSR field as twelve bits. The interesting question is not what the diagram says but whether the tool agrees, and the cheapest way to find out is to walk the tool up to its own boundary and read what it says.
$ python3 rvpriv.py --run | sed -n '/^ source /,/^ csrw/p'
source verdict size word the assembler's own words
csrr t0, 0x7ff accepted 4 bytes 0x7ff022f3 csrr
csrr t0, 0x800 accepted 4 bytes 0x800022f3 csrr
csrr t0, 0xfff accepted 4 bytes 0xfff022f3 csrr
csrr t0, 0x1000 REFUSED - - immediate must be an integer in the range [0, 4095]
csrwi 0xfff, 0x1f accepted 4 bytes 0xffffd073 csrwi
csrwi 0xfff, 0x20 REFUSED - - immediate must be an integer in the range [0, 31]
Two boundaries, two fields, one message shape. 212−1 = 4095 and 25−1 = 31, and the field widths are the only difference between them — which is a check on the quoted “twelve bits” and the immediate form’s five at the same time. Every refusal is quoted in the assembler’s own words rather than summarised, because a summarised refusal is a refusal a reader has to trust.
And there is a second measurement of the same width, which is the one that actually taught this section something. The artifact also sweeps the field: XOR every CSR word against a base and count which bits move.
$ python3 rvpriv.py --run | grep -B 1 -A 2 'A MASK IS A LOWER BOUND'
register forms differ in only TWO funct3 values out of the three the field
can hold, because the base is funct3 = 2 and the sweep moved to 1 and 3. A
MASK IS A LOWER BOUND AND A SWEEP OF THREE INSTRUCTIONS IN A FOUR-VALUE
FIELD CANNOT FIND THE FOURTH.
The raw sweep moved 20 bits. The isolated sweep — over the two-operand csrr words only, so that funct3 and rs1 are held constant rather than masked away afterwards — moved the CSR number and nothing else: bits 20 to 31, contiguous, twelve of them.
And then the isolated sweep got it wrong too. The first version moved eleven bits, because every CSR number in the corpus happened to have bit 5 clear — so it reported a field of eleven bits for a field the specification says is twelve, and the missing bit was bit 25. Nothing in the output looked like a gap. A corpus in which every address has the same bit clear will report a field one bit narrower than it is.
That is why the width here rests on two measurements and not one: the sweep says twelve bits moved, and the assembler’s refusal at 0x1000 says twelve bits is all there is. A sweep alone would have said eleven and would have been right about its corpus — the least comforting kind of right.
One fact here is quoted, because it is a fact about a document: the base integer set no longer contains the six CSR instructions, and a hardware implementation that includes the privileged architecture is expected to require them anyway. The base is rv64i; Zicsr is a separate standard extension that used to be folded into it.
What is measurable is what the toolchain does about that, and the answer is the finding of this section. The corpus contains a file that is csrr t0, cycle and ret and nothing else, so the object cannot contain anything but the extension in question. Same source, three -march settings:
$ python3 rvpriv.py --run | sed -n '/THE SAME TWO INSTRUCTIONS, THREE/,/^$/p' | tail -5
-march Tag_RISCV_arch in the object word 0 claims zicsr?
rv64i rv64i2p1 0xc00022f3 NO
rv64i_zicsr rv64i2p1_zicsr2p0 0xc00022f3 YES
rv64i_zicsr_zifencei rv64i2p1_zicsr2p0_zifencei2p0 0xc00022f3 YES
Read the first row again. -march=rv64i — the base, no Zicsr — and the object contains 0xc00022f3, which is a Zicsr instruction, and the ISA string says rv64i2p1 with no zicsr in it.
What this does and does not show. It shows that the assembler emitted an instruction outside the ISA the object claims to implement, and gave no diagnostic. It does not show a broken toolchain: the privileged architecture is expected to require these instructions anyway, so a linker or loader that sees rv64i2p1 and rejects the file would be wrong in the direction that matters. The honest statement is that the ISA string is a record of the request, not a description of the contents — which is a different claim from the one rv-isa makes about the same string, and the reason both pages exist.
Not one instruction on this page has run. That does not weaken the encodings — a word in an object file is either the encoding or it is not — but it does bound the privilege claim completely.
Tag_RISCV_arch strings.csr[11:10] is accessibility and csr[9:8] is privilege, with the document and section; that Zicsr is outside the base integer set; that encoding 2 in the privilege field is reserved.mtvec from S-mode raises an illegal-instruction exception. The privilege bits are measured as numbers; the rule that turns the numbers into an exception is quoted, and there is no S-mode on this host to be refused by. Every enforcement claim on this page is a quotation with a section number attached, and concept 4 is where all of them are listed in one table with their labels.Two retractions came out of this page, and both are printed in full in the artifact. R1 is that the borrowed decoder covers what this course needs of the CSR address space — it prints the number and stops, and printing the number is not decoding the privilege. The correction is a model prepended in this file, with the sibling untouched. R2 is the mask sweep above, and its lesson generalises past this architecture: a mask with a hole in it is a number about the set you swept, not about the field.
rd and zimm are both five bits wide, why does the architecture need a selector bit at all? Write your answer before reading the paragraph above that gave it.)csr.s, which deliberately includes two addresses with bit 5 set so that the isolated sweep can find all twelve bits. Delete those two lines, rebuild, and re-run. The sweep will report eleven bits moved — for a field the specification says is twelve — and there will be nothing in the output that looks like a gap. The assembler’s refusal at 0x1000 will be unchanged, because the assembler knows the real width. That disagreement is the whole lesson of this page.)csr.o, decode the CSR number, the accessibility pair and the privilege pair, and check your output against the artifact’s table. Then extend it with one address whose privilege encoding is 2. Your decoder will happily print “RESERVED”, which is right — and you should notice that you got that right by having read the specification rather than by having measured anything.)Back to rv-encoding, which is where the base ISA fields this page opens were laid out; the twelve bits here are inst[31:20] of an instruction that reads and writes them, which is the shape of difference that rv-encoding deliberately did not cover. And forward to concept 3, where the same twelve bits decide which of the five trap registers a program may touch and where the same funct3 bit decides whether the write is discardable.
Across the collection, a64-syscall and x86-syscall ask the same question on machines this collection can run. The difference worth carrying between them is not the answer but the method: those courses could watch a trap happen, and this one cannot, so it had to find something else that a build machine can check. Concept 2 is the other half of that answer.