Descriptors are one of the key structures of Protected Mode. They are used to hold information about various system objects and are used as entries in the three system tables - GDT, LDT and IDT.

Common properties

All descriptors are 8 bytes in size. Different types may have different format.

One thing they all have in common is the "type_attr" field (byte at offset 5) that contains descriptor type and some common attributes. Thus, if you have a descriptor of yet unknown type, you should look at the "type_attr" byte to find out the type. (Maybe you want to analyze and display the contents of some table)

// a "general" structure of a descriptor whose type we don't know,
// only for the sake of discussion - you will want to use a more specific structure
// according to the type you're dealing with, or define a union for all types
struct Descr{
    uint8_t bytes[8];

    // --- accessor methods
    uint8_t type_attr(){ return bytes[5]; };
};

"type_attr" has the following bit fields:

  • bit 7: "P" (Present) flag
Indicates whether the descriptor is being used. Accessing an object through a descriptor which has P=0 will cause a General Protection Fault Segment Not Present Fault (Int 0Bh) - or, if it's trying to be used as a Stack Segment, a Stack Fault (Int 0Ch).
  • bits 5..6: DPL - Descriptor Privilege Level (2 bits, 0..3)
Used to control access to the object described by the descriptor
  • bit 4: "S" - whether this is a data/code segment (S=1) or a system segment (some other object) (S=0).
One can think of it as "(data/code) Segment" flag. This, combined with the "type" code, can be used to determine the descriptor type and structure.
  • bits 0..3: type (4 bits, 0..0xF)

Here are the formulas:

type (type_attr & 0xF)
S ((type_attr>>4) & 1)
DPL ((type_attr>>5) & 3)
P ((type_attr>>7) & 1)

Code/Data Segment Descriptors

These are descriptors in the GDT that have S=1. Bit 3 of "type" indicates whether it's (0) Data or (1) Code. The interpretation of the other bits are given further down this page.

The structure for a code/data descriptor is as follows:

struct SegDescr{
    uint16_t limit_1;   // limit, bits 0..15
    uint16_t base_1;    // base, bits 0..15
    uint8_t base_2;     // base, bits 16..23
    uint8_t type_attr;  // type_attr
    uint8_t lim_attr;
      //^ bits 0..3: limit, bits 16..19
      //^ bits 4..7: additional data/code attributes
    uint8_t base_3;     // base, bits 24..31
};

When concatenating all chunks, base has 32 bits and limit has 20 bits. To allow for a wide range of sizes, the G bit influences how the 20 bits of the limit field are converted to a 32-bit limit

  • when using G=0 (granularity: bytes), the amount of accessible bytes can be from 1b to 1Mb (bytes 0x0 to limit. byte 0 will always be writable)
  • when using G=1 (granularity: pages), the amount of accessible bytes can be from 4Kb to 4Gb (= 0x1000 * limit + 0xfff)

Additional attributes from lim_attr (bits 4..7):

bit 7 G Granularity 0 (limit is in bytes)
1 (limit is in pages of 4096 bytes)
bit 6 D/B Default operand size/Big 0 for 16-bit segments
1 for 32-bit segments
bit 5 L 64-bit code segment 0 normally
1 if this is a 64-bit code segment in IA-32e mode
bit 4 AVL Available For use by system software
(your OS can use this as you choose)

The following snippet calculates the base and 20-bit limit from a descriptor:

// computing base and limit values - putting the bits together

uint32_t SegDescr::base(){
 return
   base_1 |
   (base_2<<16) |
   (base_3<<24);
};

uint32_t SegDescr::limit(){
 return
  limit_1 |
  ((lim_attr&0xf)<<16);
};

Type bits for Data segments

bit 3 Data/Code 0 (data)
bit 2 Expand-down 0 (normal)
1 (expand-down)
bit 1 Writable 0 (read-only)
1 (read-write)
bit 0 Accessed 0 (hasn't been accessed)
1 (has been accessed)

Type bits for Code segments

bit 3 Data/Code 1 (code)
bit 2 Conforming 0 (non-conforming)
1 (conforming)
bit 1 Readable 0 (execute-only)
1 (executable and readable)
bit 0 Accessed 0 (hasn't been accessed)
1 (has been accessed)

See Also