Search code examples
x86pagingcpu-registerssegment

Details about segment selectors in x86 system


I'm studying about protection ring of x86 system.

Examples of accessing data segments

In this picture, there are segment selectors. My questions are...

  1. segment selectors are in the RAM?
  2. who create segment selectors? automatically?
  3. one segment can have multiple segment selectors?
  4. relationship btw segment selectors and segment registers?
  5. this kind of structure(i.e, paged segmentation, segment selectors, ...) are used on modern system?

Thanks.


Solution

  • I will attempt to explain this in terms as simple as possible.

    What is segmentation?

    Segmentation is the idea of dividing memory into segments. These segments have a base address and a limit (or size).

    Traditionally, in Real Mode (DOS era), segments were defined as follows:

    base_address = segment_register * 16
    limit = 64 KiB
    

    In Protected Mode, however, segments are no longer defined like this. Instead, there's a table which contains details about every segment. As a bonus, it not only describes the base address and the limit, but also the privilege required to use it (that's why it's called Protected Mode) and if it's a 16-bit or 32-bit segment.


    The Global Descriptor Table (GDT)

    The GDT is a table (i.e. array) set up by an operating system in memory which contains descriptors. These come in 2 types: segment descriptors (which define segments, along with all their attributes) and system descriptors (which define various OS structures in memory, some are important, some not so much).

    The operating system tells the CPU where this table is located using the privileged lgdt instruction. However, if you attempt it in user-mode code, it will fault.

    Segment descriptors

    Segment descriptors define segments. See below to see how to actually load and use one. A segment descriptor has this format.

    Segment selectors

    Segment selectors are simply indexes into the GDT. They have a specific format in which the last 3 bits are used for other purposes (bit 2 is the Table Indicator, while bits 1-0 are the Requested Privilege Level). Only bits 15-3 describe the actual index. For example, selector 0x20 indicates index 4 (starting from 0) in the GDT.

    Segment registers

    The segment registers (CS, DS, ES, FS, GS and SS) are designed to store these selectors while they are in use. However, when they are not in use, they can be stored in memory or in any other place.

    The NULL selector acts as a sort of NULL pointer from C. You can freely load it into segment registers, but any attempt to use it will fault.

    Example: a memory access such as [ebx] will be relative to the segment base described by DS (or you can explicitly specify a segment register, such as [es:ebx]).

    In Real Mode, such an instruction would have accessed the memory address ds * 16 + ebx. However, in Protected Mode, the base address is taken from the GDT (at the index indicated by DS), and the offset ebx is added to it. The good part is that if the descriptor indicated by DS is privileged, but the code making the memory access is not, it will fault with a General Protection Fault.


    How are segment descriptors created?

    The segment descriptors, as mentioned before, are part of the GDT and are created by the operating system. The segment selectors are (usually) given by the OS to the application. The application is free to set its own selectors if it wants, provided that the corresponding descriptors exist and are accessible (which in modern OSes is almost never the case).

    Can you have multiple segment selectors?

    One can and always has multiple selectors. This is because CS must reference a code segment descriptor, while everything else references a data segment descriptor.

    Isn't it slow to access the GDT for every memory access?

    It is. If that was the case, the CPU would have to access the correct GDT entry every time someone accessed memory, in order to validate privilege levels and to get the segment base and limit for calculations.

    Fortunately, the CPU caches these entries (into the so-called descriptor caches) whenever you load a segment register. This is why loading a segment register is a rather expensive operation. These caches are then guaranteed to remain unchanged until you reload the corresponding segment register again. This includes switching out of Protected Mode, without restoring the descriptor caches (and this is how you enter the Unreal Mode).


    Segmentation on modern 32-bit systems

    Segmentation cannot be disabled on x86, but it can be...well...bypassed. By setting all descriptors as having base 0 and a 4GiB limit, you essentially bypass all advantages and disadvantages of segmentation.

    All modern systems use paging to achieve memory protection. However, segmentation is still required because it enforces the privilege level. Each descriptor in the GDT has a DPL (Descriptor Privilege Level) field. This is what enforces the so-called privilege Rings. If a user-mode application had access to Ring 0 selectors, it could execute privileged instructions and bypass every protection mechanism, including paging.

    Apart from that, there are a couple of system structures (defined in the GDT using system descriptors) such as the Task State Segment (TSS) which are critical for the OS. The TSS, for example, ensures that when any transition from Ring 3 to Ring 0 happens, the stack (SS:ESP) is switched to a well-known kernel stack, since the user-mode stack can't be trusted.

    Segmentation is still used for the FS and GS registers. In Windows, FS references a descriptor which points to the Thread Information Block, while GS is NULL.

    Segmentation on 64-bit systems

    In 64-bit mode, segmentation is almost disabled. The only selector that truly matters is CS, which sets the privilege level according to the DPL field. The DS, ES and SS registers are unused. DS and ES are set to 0 (in 64-bit mode the NULL selector does nothing, it doesn't fault when used), while SS still points to a descriptor (loading NULL faults), though I'm not really sure why it can't be 0.

    However, the descriptors can't set a base or a limit*. Essentially, the CPU creates the same environment that has to be created manually in 32-bit mode. Memory protection is achieved by paging, which is mandatory in 64-bit mode.

    There is a way to set the base for the FS and GS selectors (see the WRFSBASE and WRGSBASE instructions). This doesn't affect the GDT entries at all (they still only have space for a 32-bit base), but instead directly modifies the descriptor caches (the registers themselves are typically set to 0). Windows 64-bit uses GS instead of FS to point to the TIB.


    * Some CPUs support setting a segment limit in the last 4GiB of the 256TiB address space. From what I understand, this was done to facilitate software virtualization before hardware virtualization (VT-x and AMD-V) was a thing.