Something happens every time your program asks for a thing it cannot do for itself — read a file, change a page's permissions, ask the clock. Every one of those events passes through a hardware table, and on x86-64 that table has a name, an address, and a size. You can read two of those three from user mode.
That sounds like a curiosity. It is not. It is the entire privilege story in one asymmetry, and the rest of this course is a consequence of it. A process that knows where the gates are, and cannot read one, can still name a vector and get a fault — which is exactly what lets a program ask a question and get an answer, and exactly what stops it choosing the answer. Understanding that difference, precisely, is what separates “the operating system trusts this process” from “the hardware refuses to let this process do that”, and only the second kind survives a compromised kernel.
There is also a trap here that has caught the author of this course, and it is worth stating up front because it is the sort of error that does not announce itself.
SIDT stores the IDT register into a ten-byte region of memory. That region has a fixed layout, and the order is the base first and the limit second:
The limit is a byte count and every entry is 16 bytes, so the number of entries the hardware will look up is limit/16 + 1. On the machine this course was written for:
$ ./privbench | sed -n '/the ten bytes SIDT/,/entries of 16 bytes/p' the ten bytes SIDT actually wrote: 00 00 00 00 ff ff ff ff ff ff SIDT base = 0xffffffff00000000 SIDT limit = 65535 -> 4096 entries of 16 bytes
Three things to take from those ten bytes.
First, the base is not an allocation.0xffffffff00000000 is exactly 232 below the top of a 48-bit address space, and it is the address the x86-64 architecture itself uses. A kernel may choose another; this one did not. Notice that the artifact does not assume this — it prints the number it read and then checks whether the number has the expected shape, because a course that hard-codes the answer teaches the answer.
Second, 4096 entries is not a claim that 4096 interrupts exist. It is a bound, and it is a deliberately conservative one: 65535 is the largest limit a 16-bit field can express, so setting it to the maximum means that int 0xNN with any possible byte is a bounds check against the table and not a wild read. A kernel that set the limit to 64 would be faster to reason about and would turn every stray int into memory corruption. 4096 entries is a security decision wearing a number's clothes.
Third, and this is the concept: the register is readable and the table is not.
$ ./privbench | sed -n '/Reading the first 8 bytes/,/cost a fork/p'
Reading the first 8 bytes of the IDT from ring 3: FAULTED
the child died of signal 11 (SIGSEGV)
The register is readable; the TABLE is not. SIDT tells a process where
the gates are, and gives it no way to read one. That asymmetry is the
whole privilege story in one line, and it cost a fork to establish.
Note the method. The read runs in a forked child, not in the benchmark itself, and the child is reported by the signal it died from. This is not caution for its own sake: an earlier version of this course's artifact read an MSR in the parent process and killed every measurement in the run with it. Any experiment whose expected outcome is a fault belongs in a child process, and the harness should be built that way from the start rather than after it has cost you a run.
Almost every prose description of the pseudo-descriptor says “the 16-bit limit and the 64-bit base”, in that order. It is wrong. And — here is what makes it a genuinely dangerous error rather than a merely embarrassing one — reading it the documented way does not fault. It returns numbers.
$ ./privbench | sed -n '/read the other way round/,/not loud/p'
read the other way round -- limit first, then base:
base = 0xffffffffffff0000 limit = 0
No fault. No obviously impossible value. A tool written that way
reports a limit of 0 and a base in the middle of nowhere, and a
limit of 0 is INDISTINGUISHABLE from a kernel that installed an
empty IDT. This is the worst kind of wrong: it is not loud.
This is worth pausing on, because it generalises past descriptors. A struct declared { uint16_t limit; uint64_t base; } and marked packed is 10 bytes and looks correct, and the compiler will not warn you, and the program will run, and the output will be a plausible base in the middle of nowhere with a limit of zero. The failure mode is silent, it is stable, and it is indistinguishable from a legal state.
The general rule: prefer a reading that fails loudly to one that returns a plausible wrong answer. A parser that validates its input and rejects it can be debugged. A parser that confidently reports a limit of zero will be believed, and the belief will propagate.
Each of the 4096 slots is a 16-byte descriptor. In long mode it is:
And here is a second thing that the tables in textbooks get wrong, in a way that is easy to miss because the error is a missing term rather than a wrong one. The rule most courses state for “gates” is
That is the call-gate rule, and it is correct — for call gates. It is not the interrupt-gate rule, because an IDT entry is reached by a vector number and a vector number has no RPL field to carry. Intel's own pseudocode is one-sided:
and AMD states the reason explicitly in its privilege-checks section: “unlike call gates, no RPL comparison takes place. This is because the gate descriptor is referenced in the IDT using the interrupt vector number rather than a selector, and no RPL field exists in the interrupt vector number.”
Where does the folklore come from, then? From the manuals themselves, being internally inconsistent: Intel's task-gate pseudocode includes the RPL comparison, and the task gate is also reached by a vector number. So the manuals disagree with each other in the same volume, and the widely-repeated two-sided rule is a synthesis that fits one gate type and not the other. If you are writing a kernel and you use the two-sided rule for IDT gates, you will reject transitions you were supposed to allow, and the symptom will be a system that will not boot for reasons nobody can find.
Build it and ask it things. The three below are the ones worth asking first, and the third is the one that teaches the most.
Q3 is the one to do. Exercise 3 exists because a check that has never been seen to fail is a check nobody has reason to believe — and the memory course made the same argument about its own verification. Break it, watch the specific checks fail, then put it back.
00 00 00 00 ff ff ff ff ff ff, write the base and the limit, then explain why the base is 00 00 00 00 ff ff ff ff and not ff ff ff ff 00 00 00 00. (Little-endian, offset 0 first — and the "offset 0 first" is the whole lesson.)int $0x80 is a memory-safety question and not a style question. Hint: what does the hardware do with a vector for which (vector << 4) + 15 exceeds the limit?SGDT is the same shape and has the same field order; use it on a 32-bit bootloader or read it under a hypervisor. Then explain why the GDT's limit is a different kind of number from the IDT's.Forward, the next concept asks what is in the 4096 slots — and finds that eight of the thirty-two defined numbers mean different things to Intel and AMD, which is a stronger statement about the boundary than anything in this one. The doors concept then shows that two of the three ways into the kernel never consult this table at all, which is why they are faster and why they are also unchecked by hardware. The three-architectures concept is where the DPL-vs-RPL subtlety reappears as a genuinely different design: RISC-V makes the delegation a writable field the firmware owns.
Backwards, the security course's posture concept enumerated the control-register bits as flags a hardening policy can set, and never asked who is allowed to read them. Answer: nobody in ring 3, which is why this course reports SMEP and SMAP as a report rather than a measurement. And the memory course's translation concept measured what happens when an address reaches the page tables with nothing there — this concept is the layer above that, where the address is rejected before it ever gets the chance.
Outward: if you are writing a compiler backend, this is the point where “the program runs” stops being a metaphor. Your code will eventually execute syscall; someone has to have put a gate in that table, and the DPL on that gate is what makes your program's request reach a handler instead of a wall. You do not need to know the answer to build the next course in this chain. You do need to know the question exists.