The privilege boundary on x86-64 is not a bit. It is a comparison against a number stored in a table the CPU reads on every mode change, and the number is a field inside an eight-byte descriptor that also says whether the segment is code or data, readable or writable, present or not, and 32-bit or 64-bit. Five decisions in eight bytes, and the first of them is the one people call “privilege”.
What makes this concept worth a full page rather than a paragraph is a measurement that contradicts the sentence almost everybody uses to describe it. The sentence is the descriptor tables are privileged. The measurement is that SIDT and SGDTboth return from ring 3, and that reading one byte of either table does not, and that the failure is si_code 1 with an si_addr equal to the base the instruction just printed.
A segment selector is not an index. It is three fields with different jobs, and the two-bit field at the bottom is the one that is checked:
$ ./sysdump | sed -n '/1B5\./,/CALL-GATE rule/p'
SEL | 15:3 | index | which descriptor in the GDT or LDT
SEL | 2 | TI | 0 = GDT, 1 = the LDT
SEL | 1:0 | RPL | requested privilege level, 0 to 3
SEL | 0xffff | a NULL selector: index 8191, TI 1, RPL 3
The four privilege numbers and the two comparisons:
CPL the current privilege level, from CS.RPL in long mode
RPL requested, from the low two bits of the selector loaded
DPL descriptor privilege level, bits 14:13 of the descriptor
for a DATA or CODE segment: max(CPL, RPL) <= DPL or #GP
for a CALL GATE: max(CPL, RPL) <= DPL or #GP(tss)
for an INTERRUPT gate: CPL <= DPL or #GP(vector)
Three rules, not one, and the difference between the second and the third is the whole of a common mistake. The first two compare the larger of the current and requested levels against the descriptor's. The third does not, and cannot: an interrupt gate is indexed by a vector number, and a vector number has no RPL field. Every textbook that writes MAX(CPL,RPL) ≤ DPL as “the rule for gates” is quoting the call-gate rule for something that is not a call gate, and a kernel that implements the third check with the first will refuse its own timer.
And the descriptor itself. A 64-bit system segment descriptor is eight bytes, and the fields that matter for this page are two: the type byte, whose low three bits are the access type and whose high bits include P (present) and DPL, and the flags byte, whose L bit is what makes a code segment 64-bit. Long mode adds one rule that has no analogue in 32-bit protected mode: a data selector's base and limit are ignored entirely. That is why %ds being zero is harmless in a 64-bit program, and it is the fact that surprises people who arrive from 32-bit code.
Two instructions, two ten-byte structures, and a field order that is the opposite of what the prose implies:
$ ./sysdump | sed -n '/the ten bytes SIDT/,/UNFALSIFIABLE/p'
the ten bytes SIDT actually wrote, in memory order:
00 00 00 00 ff ff ff ff ff ff
SIDT | base 0xffffffff00000000 | limit 65535 | entries 4096 of 16 bytes
SGDT | base 0xfffffffe00000000 | limit 65535 | entries 8192 of 8 bytes
THE FIELD ORDER, and it is the whole of this sub-section:
offset 0..7 BASE first
offset 8..9 LIMIT second
Read the documented-in-prose 'limit first, then base' way and:
base 0xffffffffffff0000 limit 0
NO FAULT, and no obviously impossible value either. A limit of
0 is INDISTINGUISHABLE from a kernel that installed an empty
IDT, so the wrong reading is not merely wrong, it is
UNFALSIFIABLE. A tool written that way reports a plausible
base in the middle of nowhere and never tells you.
The ten bytes are printed before they are interpreted, and reading them is how you check the interpretation: 00 00 00 00 ff ff ff ff is a base of zero and ff ff is a limit of 65535, and the base-first order is the only reading under which the two halves are individually plausible. A limit of 65535 is the largest a 16-bit field can hold, which is the architectural statement; where the base points is a kernel choice, and the two numbers above differ by exactly 232 bytes, which is a decision this kernel made and not a rule.
$ ./sysdump | sed -n '/6A\./,/PAGE-TABLE fact/p'
ABSENT | what | si_code | si_addr | si_addr equals
ABSENT | read one byte of the IDT | 1 | 0xffffffff00000000 | the IDT base SIDT printed
ABSENT | read one byte of the GDT | 1 | 0xfffffffe00000000 | the GDT base SGDT printed
ABSENT | both are si_code 1, SEGV_MAPERR, which is the shape of
ABSENT | a MAPPING failure and not the shape of a #GP. So the
ABSENT | register is readable, the address is readable, and the
ABSENT | table is not, and the reason is that no user mapping
ABSENT | covers it. That is a PAGE-TABLE fact, not a privilege
ABSENT | fact, and it is a better story than 'the IDT is
ABSENT | privileged' because it predicts the si_addr.
This is the sharpest thing in the concept, and it is a correction rather than a finding. The obvious sentence is “a ring-3 process cannot read the IDT because the IDT is privileged.” The measurement says something more specific and more useful: the register is readable, the address is readable, and the table is not — and the failure carries an si_addr that is exactly the base SIDT printed. That is a mapping failure, not a general-protection one, and it distinguishes three stories that all sound alike:
And note the two different si_addr values: 0xffffffff00000000 for the IDT and 0xfffffffe00000000 for the GDT. The harness checks each against the base the corresponding instruction printed, so the claim is a comparison and not a coincidence.
All five of the “store the descriptor table registers” instructions are in one bucket, and the bucket is not the one most answers give:
$ ./sysdump | grep -E '^ EDGE \| (lidt|lgdt|ltr|lldt|sidt|sgdt|sldt|str|smsw) '
EDGE | lidt | descriptor tables | FAULTED | sig 11 | si_code 128 | si_addr 0x000000000000
EDGE | lgdt | descriptor tables | FAULTED | sig 11 | si_code 128 | si_addr 0x000000000000
EDGE | ltr | descriptor tables | FAULTED | sig 11 | si_code 128 | si_addr 0x000000000000
EDGE | lldt | descriptor tables | FAULTED | sig 11 | si_code 128 | si_addr 0x000000000000
EDGE | sidt | descriptor tables | RETURNED | sig 0 | si_code 0 | si_addr 0x000000000000
EDGE | sgdt | descriptor tables | RETURNED | sig 0 | si_code 0 | si_addr 0x000000000000
EDGE | sldt | descriptor tables | RETURNED | sig 0 | si_code 0 | si_addr 0x000000000000
EDGE | str | descriptor tables | RETURNED | sig 0 | si_code 0 | si_addr 0x000000000000
EDGE | smsw | descriptor tables | RETURNED | sig 0 | si_code 0 | si_addr 0x000000000000
Four writes fault and five reads return, and the split is not arbitrary. The writes replace a table the CPU will consult on the next mode change, so they are gated. The reads report where a table is, and that is a diagnostic the architecture deliberately provides — except that CR4.UMIP can close the door. And whatever this kernel has UMIP set to, it is not blocking them, which is a measurement of the effect and not a reading of the bit.STR is the one everybody gets wrong: the check for writing the task register is a CPL-versus-IOPL comparison, and the check for reading it is nothing at all.
And the privilege check itself, made to fail on purpose by loading a selector whose descriptor is not there:
$ ./sysdump | grep -E '^ EDGE \| mov 0x1234'
EDGE | mov 0x1234,%ax; mov %ax,%ds | segments | FAULTED | sig 11 | si_code 128 | si_addr 0x000000000000
EDGE | mov 0x1234,%ax; mov %ax,%ss | segments | FAULTED | sig 11 | si_code 128 | si_addr 0x000000000000
0x1234 is index 0x246 of the GDT, which is past the end of an 8192-entry table, and the CPU raises a #GP with a zero error code — hence si_code 128 and no address. The same instruction with %ds instead of %ss also works, which is the answer to “do data selectors get checked differently from stack selectors”: in long mode, no, and the reason is that both selectors' privilege checks are the same two-bit comparison against a field that was never consulted for the base.
Read the six selectors this process is actually running with, decoded by the three fields:
$ ./sysdump | grep -E '^ SEG \|' | head -3
SEG | cs 0x0033 ss 0x002b ds 0x0000 es 0x0000 fs 0x0000 gs 0x0000
SEG | decoded as index<<3 | TI<<2 | RPL:
SEG | cs | selector 0x0033 | index 6 TI 0 RPL 3 | CPL IS THE RPL OF CS IN LONG MODE
SEG | ss | selector 0x002b | index 5 TI 0 RPL 3 |
SEG | ds | selector 0x0000 | index 0 TI 0 RPL 0 |
CS = 0x0033: index 6, GDT, RPL 3. So this process is at CPL 3, and in long mode that is the whole of what CS does — the index is not checked against anything, because there is no code descriptor behind it. The privilege level is the two low bits.SS = 0x002b: index 5, RPL 3, matching. Linux keeps the kernel's code at index 6 and the user stack at index 5 with a DPL of 3, and the fact that the indices are 6 and 5 rather than 3 and 4 is a consequence of reserving the low ones — a layout decision, and one you can read out of a running process.DS, ES, FS and GS are all 0x0000. A null selector, loaded deliberately and legally. FS and GS are how thread-local storage is addressed, and their bases come from the FS-base and GS-base MSRs rather than from the descriptor — which is why a 64-bit program can have a thread pointer at 0x7ffff7000000 and a null selector at the same time. The assembly course's flags concept is where the FS-base mechanism itself is a register story; here the point is only that the selector and the base are now unrelated.So in long mode a segment register has exactly two jobs left, and both of them are on this page: the two low bits of CS are the current privilege level, and the two low bits of a data selector are checked against a DPL that nothing else consults. Everything else about segments — bases, limits, expand-down, the 32/64-bit flag for data — is either ignored or moved into a model-specific register. Segmentation survives on x86-64 as a privilege mechanism and nothing else, which is a strange sentence to write about a feature that occupied a chapter in every architecture text for fifteen years.
cd courses/x86sys/assets/samples && ./build_samples.sh, then read section 2B. (Expect the ten raw bytes, the correct base-first reading, and the wrong one with a limit of 0 and no fault. If your kernel's IDT base is not 0xffffffff00000000 that is fine and it is the point: the placement is a kernel choice, and the artifact prints what it read rather than asserting a number.)base + limit + 1 and read the answer off a hex string. (Expect it to be wrong: each table is exactly 216 bytes and the two bases are 232 bytes apart, so the gap is 232 minus 216 and not zero. The retraction is printed in the artifact with both numbers beside it, because a retraction without the numbers is a rumour.)max(CPL, RPL) ≤ DPL and see what happens when a ring-3 process takes a timer interrupt. (Expect it to work, because RPL is 0 in a vector, and then expect the same code to be wrong the moment your gate has a nonzero RPL field set for some unrelated reason. The bug is real and it is silent, which is the worst combination.)CS and SS and decode the indices as this page does, then read the GDT base with SGDT and try to read descriptor 5 and 6 yourself. (Expect the read to die with si_code 1 and an address equal to the GDT base — exactly the shape of the IDT result, and for the same reason. If it does not, your kernel maps the GDT into user space on purpose, which some hardened kernels do, and the asymmetry this course measured is not universal.)%fs bases are almost the only places segmentation appears. (Expect the answer to be two or three instructions in the entire program, and expect the reason to be that the ABI you have to implement is a register convention. That is the same shape as the argument for the fourth argument being R10, and it is why the machine's segment machinery is now a compatibility feature rather than a mechanism.)Backwards, the privilege course's gate-table concept is where this page's central measurement was first taken — SIDT returns and the table does not — and the difference is that this course also reads the GDT, decodes the selector, and shows that the reason for the asymmetry is a mapping rather than a privilege. Its ring-diagram concept is the why behind the max(CPL,RPL) ≤ DPL comparison; this page is the reference for which comparison applies to which gate.
Forwards. The fourth concept is the five control registers, and the connection is the row that failed: CR4.UMIP is the bit that decides whether the five reads in this concept's table are legal at all, and reading it is a fault. This concept measured the consequence; the next one names the bit and says it cannot be read. That pairing is the shape of the whole course and it recurs in every remaining concept.
Outward. The IA32_STAR MSR holds the two selectors a syscall loads on the way in and the two it restores on the way out, which is the one place where this page's material is still load-bearing in long mode: syscall is defined in terms of the very segment registers whose descriptors stopped existing. The ISA course covers the mode-switch machinery; the eighth concept here is where the MSRs themselves are shown to be unreadable.