Global Descriptor Table

La table de descripteur globale à été introduite avec le processeur 16bit d'intel (le 80286) pour gérer la mémoire sous forme de segments.

La segmentation ne devrais plus être utilisé, elle a été remplacé par le paging. Le paging est toujours obligatoire pour passer du 32 au 64 bit avec l'architecture x86.

Cependant la GDT est aussi utilisée pour contenir la tss. La structure est différente entre le 32 et 64 bit.

La table globale de descripteur est principalement formée de 2 structures:

  • la gdtr (le registre de segments)
  • le segment

Le registre de segments

le registre de segments en mode long (x86_64) doit être construit comme ceci:

nomtaille
taille16 bit
adresse de la table64 bit

taille: Le registre taille doit contenir la taille de la table de segment, soit le nombre de segment multiplié par la taille du segment, cependant en 64bit la taille du segment de la TSS est doublé, il faut alors compter le double.

adresse de la table: L'adresse de la table doit pointer directement vers la table de segments.

Les segments

Un segment en x86_64 est formé comme ceci:

nomtaille
limite basse (0-15)16 bit
base basse (0-15)16 bit
base milieu (16-23)8 bit
flag8 bit
limite haute (16-19)4 bit
granularité4 bit
base haute (24-31)8 bit

Les registres base

Le registre base est le début du segment, en mode long il faut le mettre à 0.

Les registres limite

Le registre limite est une adresse 20bit, il représente la fin du segment. Il est multiplié par 4096 si le bit granularité est à 1. En mode long (64 bit) il faut le mettre à 0xfffff pour demander à ce que le segment prenne toute la mémoire.

Le registre flag

Les flags d'un segment est formé comme ceci:

nomtaille
accédé1 bit
écriture/lisible1 bit
direction/conformité1 bit
executable1 bit
type de descripteur1 bit
niveau de privilège2 bit
segment présent1 bit

accédé : Doit être à 0, il est mit à 1 quand le processeur l'utilise.

écriture/lisible:

  • Si c'est un segment de donnée: si le bit est à 1 alors l'écriture est autorisé avec le segment, si le bit est à 0 alors le segment est seulement lisible.
  • Si c'est un segment de code: si le bit est à 1 alors on peut lire le segment sinon le segment ne peut pas être lu.

direction/conformité:

  • Pour les descripteurs de données:

    • Le bit défini le sens du segment, si il est mit alors le sens du segment est vers le bas, il doit être à 0 pour le 64 bit.
  • Pour les descripteurs de code:

    • Si le bit est à 1 alors le code peut être éxécuté par un niveau de privilège plus bas ou égal au registre niveau de privilège.
    • Si le bit est à 0 alors le code peut seulement être éxecuté par le registre niveau de privilège.

executable: Définis si le segment est éxécutable ou non, si il est à 0 alors le segment ne peut pas être exécuté (c'est un segment de donné data) mais s'il est à 1 alors c'est un segment qui peut être exécuté (c'est un segment de code code).

type de descripteur: Doit être mit à 1 pour les segment de code/data et il doit être à 0 pour la tss.

niveau de privilège: Représente le niveau de privilège du descripteur (de 0 à 3).

segment présent: Doit être mit à 1 pour tout descripteur (sauf pour le descripteur null).

Le registre granularité

Le registre granularité d'un segment est formé comme ceci:

nomtaille
granularité1 bit
taille1 bit
mode long1 bit
zéro1 bit

granularité: Le bit granularité doit être mit quand la limite est fixe, cependant si le bit est à 1 alors la limite est multipliée par 4096.

taille: Le bit taille doit être mit à 0 pour le 16bit/64bit, 1 pour le 32bit.

mode long: Le bit doit être à 1 pour les descripteur de code en 64bit sinon il reste à 0.

Types de segment

Il y a différents type de segments:

Le segment null

L'entrée 0 d'une gdt est une entrée nulle, tout le segment est à 0.

Le segment code du kernel

La première entrée doit être un segment pour le kernel éxecutable soit un segment de code:

  • Dans le type il faut que le bit 'type de descripteur' soit à 1.
  • Il faut que le segment ait l'accès en écriture.
  • Il faut que le bit executable soit mit.
  • Le niveau de privilège doit être à 0.

Cela produit un type pour le mode x86_64: 0b10011010

La granularité doit être à 0b10

Le segment data du kernel

La seconde entrée doit être un segment de donnée pour le kernel.

  • Il faut utiliser la même démarche que le segment de code sauf qu'il faut mettre le bit executable à 0.

Cela produit un type pour le mode x86_64: 0b10010010

La granularité doit être à 0

Le segment code des utilisateurs

La troisième entrée doit être un segment pour les applications éxecutable depuis l'anneau (niveau de privilège) 3.

  • Il faut reproduire la même démarche que pour le segment code du kernel sauf que le niveau de privilège doit être à 3 pour le segment.

Cela produit un type pour le mode x86_64: 0b11111010

La granularité doit être à 0b10.

Le segment données des utilisateurs

La quatrième entrée doit être un segment pour les données d'applications depuis l'anneau (niveau de privilège) 3. Il faut reproduire la même démarche que pour le segment data du kernel sauf que le niveau de privilège doit être à 3.

Cela produit un type pour le mode x86_64: 0b11110010.

La granularité doit être à 0.

Le chargement d'une gdt

Pour charger un registre d'une gdt il faut utiliser l'instruction:

lgdt [registre]

Avec le registre contenant l'adresse du registre de la gdt. Cependant en 64bit il faut charger les registre du segment de code et de donnée. Ici nous allons utiliser l'instruction retf qui permet de charger un segment de code:

gdtr_install:
    lgdt [rdi]
    ; met tout les segments avec leurs valeurs ciblants le segment de données
    mov ax, 0x10

    mov ds, ax
    mov es, ax
    mov ss, ax

    mov rax, qword .trampoline ; addresse de retour
    push qword 0x8 ; segment de code
    push rax 

    o64 retf  ; fait un far return

.trampoline:
    pop rbp
    ret

Références