Global Descriptor Table (GDT) in Linux x86-64

Even though x86 memory segmentation is largely unused in 64 bits mode, the Linux kernel still initializes the CPU’s Global Descriptor Table (GDT) and points the gdt register to it. I was curious about its content and how segment selectors can be used today. This article is a brief summary of my experiments and findings.

To begin with, we can read the gdt register calling native_store_gdt in kernel space (arch/x86/include/asm/desc.h):

We see there that the register contains a virtual address, so segmentation is previous to pagination in protected mode.

Now that we know the GDT location and size, we can dump it from memory. Each entry is described by struct desc_struct (arch/x86/include/asm/desc_defs.h).

The table describes a few plain memory segments in its first entries, with base address values of 0x0 and sizes of 2^32 –size being determined by the limit value and ‘page’ granularity-. I’ve not showed beyond the 15th entry for space reasons but can tell you that they are all empty.

It’s worth noticing that every entry is 8 bytes long (base address of 4 bytes) except for GDT_ENTRY_TSS and GDT_ENTRY_LDT, which are 16 bytes long (base address of 8 bytes). GDT_ENTRY_TSS appears to be the only one with some information. There is a selector register named tr pointing to this entry, which can be read calling native_store_tr (arch/x86/include/asm/desc.h).

The tr register value is an index to the GDT -as it is any selector register-. We can read the entry in the GDT, obtain the base address and finally read the TSS structure:

The structure that describes the TSS entry in the GDT is struct ldttss_desc64 (arch/x86/include/asm/desc.h). The structure that describes the TSS is struct tss_struct (arch/x86/include/asm/processor.h).

TSS stands for Task State Segment (see more here). Just for curiosity I printed some values:

SP0 seems to contain the base of the stack.

Going back to the GDT, I wondered how Thread-Local Storage (TLS) works if its entries (GDT_ENTRY_TLS_MIN to GDT_ENTRY_TLS_MAX) are empty. The fact that these entries have a base address of 4 bytes is an indication that they are not used in 64 bits; otherwise, possible TLS locations would be limited in the virtual address range and definitely not suitable for kernel use.

In glibc’s source code, we see that the FS segment selector is not directly set with a mov instruction -to indicate the TLS entry index in the GDT-. However, there is a system call named arch_prctl, executed with a ARCH_SET_FS flag, which looks related.

Long story short, arch_prctl + ARCH_SET_FS does not set neither the FS segment selector -which continues to be 0- nor the GDT entry -which continues to be empty-, but a special MSR_FS_BASE register. The address value passed through the system call is also kept in the task_struct.

We read the MSR_FS_BASE register in kernel space with:

rdmsrl is located in arch/x86/include/asm/msr.h.

We read the FS segment selector in kernel space with:

savesegment is located in arch/x86/include/asm/segment.h.

Every use of the FS selector for a memory-access operation makes the MSR_FS_BASE value to be added to the index: the FS selector is not used as an index to the GDT and the base address is not retrieved from there.

The GS selector works quite the same than FS: user-space code can set it with arch_prctl + ARCH_SET_GS, the segment selector register remains 0, the address value is set in MSR_GS_BASE and stored in task_struct. But there is a difference: the MSR_GS_BASE register holds the value only while user-space code is executed. As soon as execution moves to the kernel, the x86 swapgs instruction swaps its value with the MSR_KERNEL_GS_BASE register. The reason is that the kernel uses the GS base to point to its own per-CPU local storage area.

Finally, I wanted to know what happens if we write a GDT entry and set an index in the FS segment selector pointing to it, like in the old x86-32 times.

Here we have the experiment code for kernel:

set_desc_base, set_desc_limit and native_write_gdt_entry are located in arch/x86/include/asm/desc.h, while loadsegment is in arch/x86/include/asm/segment.h.

This is the result:

Immediately after setting the FS segment selector, its descriptor entry in the GDT is read and the MSR_FS_BASE register set with the base address. There is probably no reason to set MSR_FS_BASE in this way and be -unnecessarily- limited to 32 bits addresses.

Leave a Reply

Your email address will not be published.