The RISC-V Privileged Architecture

The CSR Address and the Twelve Instructions That Touch It

26 minadvancedModule 1: The Address, the Bits, and the Trap
26 min · Concept 1 of 4 · module: the address, the bits, and the trap · The RISC-V Privileged Architecture

The whole privilege convention is four bits of an address

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.

Twelve instructions, and one bit between the two families

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.

How wide is the field? Ask the assembler to refuse

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.

Zicsr is not in the base ISA, and the ISA string does not notice

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.

What was measured here, and what was not

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.

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.

Try it

  1. Find the one bit yourself.(Take the twelve words in the table above and XOR each register form against its immediate form by hand. Three XORs, three identical values, and one bit. Then ask the design question the answer implies: if 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.)
  2. Build a corpus with a hole in it and watch the sweep report a smaller field.(Take 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.)
  3. Write a decoder for the CSR address and then write its test.(Take ten words from 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.)

Connect

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.