I find out that it's possibly to read GDTR by SGDT assembly command. Inserting this piece of assembly in my C code I get Error: operand type mismatch for 'sgdt'
unsigned long j;
asm("sgdt %0" : "=r"(j));
sgdt
can only take a memory operand, not a register, so it has to be "=m"
. The operand-size is 2+8 bytes (for x86-64; limit then address in that order) so you need a struct; using a long
will result in storing outside the object.
Read the manual! https://www.felixcloutier.com/x86/sgdt
Other caveats:
UMIP (User Mode Instruction Prevention) lets a kernel stop user-space (privilege level 3) from running this instruction, because it only helps user-space defeat kernel ASLR or with other exploits; user-space has no legitimate use for this address. Under a normal kernel like Linux, user-space can't dereference the virtual address it gets from this. So Linux does enable UMIP if supported by hardware (Zen 2, Cannon Lake, Goldmont Plus).
The Linux kernel has a macro for this: store_gdt(dtr)
, which uses
asm volatile("sgdt %0":"=m" (*dtr));
with struct desc_ptr *dtr
.
On my Linux 5.18 system with a Skylake CPU (no UMIP support), I put sgdt [rsp]
(NASM syntax) into a static executable so I could single-step it with GDB (starti
/ stepi
). After that instruction:
x /1hx $rsp
shows the limit was 0x007f
(stored in 2 bytes, what GDB calls a half-word, what Intel calls a word).x /1gx $rsp+2
shows the qword address happened to be 0xfffffe00000ed000
, which is a valid kernel address (48-bit sign-extended, but fairly far from the very top of the upper half of the canonical range of address space.) According to docs/x86/x86-64/mm.txt
, the 0.5TB starting at fffffe0000000000
holds the cpu_entry_area
mapping, so that's an unsurprising place to find the GDT, along with other kernel stuff whose address is exposed to user-space (on CPUs without UMIP), and has to be mapped all the time, even on CPUs with Meltdown.