Linux does not use x86 segmentation for memory protection, only for thread-local storage. Beyond that, only the ways that x86 requires you to use it: To put the machine into 32 or 64-bit mode, and kernel (privilege level 0) vs. user mode (privilege level aka ring 3), via the segment descriptor selected by CS, and corresponding stuff in other segment regs as required to keep the CPU happy.
Is each region in the diagram a segment?
These are 2 almost-totally-different uses of the word "segment"
- x86 segmentation / segment registers: modern x86 OSes use a flat memory model where all segments have the same base=0 and limit=max in 32-bit mode, the same as hardware enforces that in 64-bit mode, making segmentation kind of vestigial. (Except for FS or GS, used for thread-local storage even in 64-bit mode.)
- Linker / program-loader sections / segments. (What's the difference of section and segment in ELF file format)
The usages have a common origin: if you were using a segmented memory model (especially without paged virtual memory), you might have data and BSS addresses be relative to the DS segment base, stack relative to the SS base, and code relative to the CS base address.
So multiple different programs could be loaded to different linear addresses, or even moved after starting, without changing the 16 or 32-bit offsets relative to the segment bases.
But then you have to know which segment a pointer is relative to, so you have "far pointers" and so on. (Actual 16-bit x86 programs often didn't need to access their code as data, so could use a 64k code segment somewhere, and maybe another 64k block with DS=SS, with the stack growing down from high offsets, and data at the bottom. Or a tiny code model with all segment bases equal).
How x86 segmentation interacts with paging
Address mapping in 32 / 64-bit mode is:
- segment:offset (segment base implied by the register holding the offset, or overridden with an instruction prefix)
- 32 or 64-bit linear virtual address = base+offset. (In a flat memory model like Linux uses, pointers / offsets = linear addresses too. Except when accessing TLS relative to FS or GS.)
- page tables (cached by TLB) map linear to 32 (legacy mode), 36 (legacy PAE), or 52-bit (x86-64) physical address. (https://stackoverflow.com/questions/46509152/why-in-64bit-the-virtual-address-are-4-bits-short-48bit-long-compared-with-the).
This step is optional: paging has to be enabled during bootup by setting a bit in a control register. Without paging, linear addresses are physical addresses.
Notice that segmentation does not let you use more than 32 or 64 bits of virtual address space in a single process (or thread), because the flat (linear) address space everything is mapped into only has the same number of bits as offsets themselves. (This wasn't the case for 16-bit x86, where segmentation was actually useful for using more than 64k of memory with mostly 16-bit registers and offsets.)
The CPU caches segment descriptors loaded from the GDT (or LDT), including the segment base. When you dereference a pointer, depending on what register it's in, it defaults to either DS or SS as the segment. The register value (pointer) is treated as an offset from the segment base.
Since the segment base is normally zero, CPUs do special-case this. Or from another perspective, if you do have a non-zero segment base, loads have extra latency because the "special" (normal) case of bypassing adding the base address doesn't apply.
How Linux sets up x86 segment registers:
The base and limit of CS/DS/ES/SS are all 0 / -1 in 32 and 64-bit mode. This is called a flat memory model because all pointers point into the same address space.
(AMD CPU architects neutered segmentation by enforcing a flat memory model for 64-bit mode because the mainstream OSes weren't using it anyway, except for no-exec protection which was provided in a much better way by paging with the PAE or x86-64 page-table format.)
- TLS (Thread Local Storage): FS and GS are not fixed at base=0 in long mode.
(They were new with 386, and not used implicitly by any instructions, not even the
rep
-string instructions which use ES). x86-64 Linux sets the FS base address for each thread to the address of the TLS block.
e.g. mov eax, [fs: 16]
loads a 32-bit value from 16 bytes into the TLS block for this thread.
the CS segment descriptor chooses what mode the CPU is in (16/32/64-bit protected mode / long mode). Linux uses a single GDT entry for all 64-bit user-space processes, and another GDT entry for all 32-bit user-space processes. (For the CPU to work right, DS/ES also have to be set to valid entries, and so does SS). It also chooses the privilege level (kernel (ring 0) vs. user (ring 3)), so even when returning to 64-bit user-space, the kernel still has to arrange for CS to change, using iret
or sysret
instead of a normal jump or ret instruction.
In x86-64, the syscall
entry point uses swapgs
to flip GS from user-space's GS to the kernel's, which it uses to find the kernel stack for this thread. (A specialized case of thread-local storage). The syscall
instruction doesn't change the stack pointer to point at the kernel stack; it's still pointing to the user stack when the kernel reaches the entry point1.
DS/ES/SS also have to be set to valid segment descriptors for the CPU to work in protected mode / long mode, even though the base / limit from those descriptors are ignored in long mode.
So basically x86 segmentation is used for TLS, and for the mandatory x86 osdev stuff that the hardware requires you to do.
Footnote 1: Fun history: there are mailing list archives of messages between kernel devs and AMD architects from a couple years before AMD64 silicon was released, resulting in tweaks to the design of syscall
so it was usable. See links in this answer for details.