GDT Tutorial
难度等级 |
---|
初学者 |
在 IA-32 和 x86-64 架构上,更准确地说,在 保护模式-Protected Mode 或 长模式-Long Mode 中,控制中断服务例程 ISR-Interrupt Service Routines 和做好内存管理都需要通过描述符表(descriptors table)。 每个描述符存储CPU在某个时间点可能需要的单个目标(例如服务例程、任务、代码或数据块)信息。 例如,如果你尝试将新值加载到段寄存器中时, CPU需要执行安全和访问控制检查,以查看你是否实际上有权访问该特定内存区域。 一旦执行了检查,有用的值(如最低和最高地址)就会根据描述符,被读取并缓存到也许不可见的CPU寄存器中。
在这些体系结构上,有三种此类型的表:全局描述符表-Global Descripptor Table、本地描述符表-Local DescrippTable和中断描述符表(它取代了中断向量表-Interrupt Vector Table)。 上面每个表都用它们的大小和 线性地址进行了定义, 并分别通过 LGDT、LLDT 和LIDT 指令发送到CPU。 在几乎所有的用例中,这些表都只在启动时放入内存一次,然后在需要时进行编辑。
必知词汇表
- 分段-Segment
- 逻辑上连续的内存块,从CPU的角度具有一致的属性。
- 段寄存器(Segment Register)
- 一类CPU的寄存器,它指向的是用于特定目的的段 (CS,DS,SS,ES) 或用于一般用途的 (FS,GS)
- 段选择器-Segment Selector
- 对描述符(descriptor)的引用,你可以将其加载到段寄存器中; 选择器(selector)是指向描述符表(descriptor table)中条目(Entry)之一的偏移量。 这些条目通常为8字节长,因此仅第3位及以上用以声明描述符表条目偏移,而第2位指定该选择器是GDT还是LDT选择器(LDT位设置1,GDT位清除0), 同时第0-1位声明需要对应描述符表项的DPL字段的Ring级。 如果级别不对,则发生一般保护故障(General Protection Fault);如果它确实对应,则所用选择器的CPL安全级别会相应地改变。
- 段描述符-Segment Descriptor
- 描述符表中的条目。 这是一个二进制数据结构,告诉CPU给定段的属性。
在GDT中放入什么
基础内容
为了合理使用CPU,你应该始终将以下项目存储在GDT中:
- 描述符表中的条目0,空描述符从不被处理器引用,并且应该始终不包含任何数据。 如果你没有Null Descriptor,某些模拟器 (例如Bochs) 会报限制异常(limit exceptions)。 有些人使用这个描述符来存储指向GDT本身的指针(与LGDT指令一起使用)。 空描述符为8字节宽,其中指针为6字节宽,因此它可能正是进行此操作的最佳位置。
- 一个DPL 0 代码段 描述符 (用于你的内核)
- 一个数据段描述符(不允许写入代码段)
- 一个Task State Segment段描述符(至少有一个是非常有用的)
- 如果需要,可以容纳更多细分段 (例如用户级别,LDT,更多TSS,等等)
Flat / Long Mode 设置
如果你不希望使用分段将内存分隔为多个受保护区域,则只需使用几个段描述符即可。 一个原因可能是你希望仅使用分页来保护内存。 此外,此设置方式在Long Mode中必须严格执行,因为base和limit值已经被忽略了。
在此方案中,唯一需要的段描述符-Segment Descriptor是空描述符, 以及一个描述符(由期望的特权级别、段类型和执行模式的组合而成),以及系统描述符。 通常,这将包括内核和用户模式的一个代码段和一个数据段,以及一个Task State Segment。
选择器 | 用途 | 内容 |
---|---|---|
0x0000 | Null Descriptor | Base = 0 Limit = 0x00000000 Access Byte = 0x00 Flags = 0x0 |
0x0008 | 内核模式代码段 | Base = 0 Limit = 0xFFFFF Access Byte = 0x9A Flags = 0xC |
0x0010 | 内核模式数据段 | Base = 0 Limit = 0xFFFFF Access Byte = 0x92 Flags = 0xC |
0x0018 | 用户模式代码段 | Base = 0 Limit = 0xFFFFF Access Byte = 0xFA Flags = 0xC |
0x0020 | 用户模式数据段 | Base = 0 Limit = 0xFFFFF Access Byte = 0xF2 Flags = 0xC |
0x0028 | Task State Segment | Base = &TSS Limit = sizeof(TSS) Access Byte = 0x89 Flags = 0x0 |
选择器 | 用途 | 内容 |
---|---|---|
0x0000 | Null Descriptor | Base = 0 Limit = 0x00000000 Access Byte = 0x00 Flags = 0x0 |
0x0008 | 内核模式代码段 | Base = 0 Limit = 0xFFFFF Access Byte = 0x9A Flags = 0xA |
0x0010 | 内核模式数据段 | Base = 0 Limit = 0xFFFFF Access Byte = 0x92 Flags = 0xC |
0x0018 | 用户模式代码段 | Base = 0 Limit = 0xFFFFF Access Byte = 0xFA Flags = 0xA |
0x0020 | 用户模式数据段 | Base = 0 Limit = 0xFFFFF Access Byte = 0xF2 Flags = 0xC |
0x0028 | Task State Segment (64-bit System Segment) |
Base = &TSS Limit = sizeof(TSS) Access Byte = 0x89 Flags = 0x0 |
细分内核设置
如果你希望将内存分离到代码和数据的受保护区域中,则必须将表中每个条目的 Base 和 Limit 值设置为所需的格式。
例如,你可能希望有两个段,一个4MiB代码段从4MiB开始,另一个4MiB数据段从8MiB开始,这两个段都只能由Ring 0访问。 在这种情况下,你的GDT可能如下所示:
选择器 | 用途 | 内容 |
---|---|---|
0x0000 | Null Descriptor | Base = 0 Limit = 0x00000000 Access Byte = 0x00 Flags = 0x0 |
0x0008 | 内核模式代码段 | Base = 0x00400000 Limit = 0x003FFFFF Access Byte = 0x9A Flags = 0xC |
0x0010 | 内核模式数据段 | Base = 0x00800000 Limit = 0x003FFFFF Access Byte = 0x92 Flags = 0xC |
0x0018 | Task State Segment | Base = &TSS Limit = sizeof(TSS) Access Byte = 0x89 Flags = 0x0 |
这意味着在物理地址4 MiB加载的内容将在CS:0处显示为代码,在物理地址8 MiB加载的内容将在DS:0处显示为数据。
以上设置并不是推荐的设计,但是展示了如何考虑使用GDT来定义独立的段。
SYSENTER / SYSEXIT
如果你使用的是英特尔 SYSENTER / SYSEXIT 例程(routines),GDT 必须包含四个特殊条目,第一个条目由 IA32_SYSENTER_CS 中的值指向 模型特定寄存器 (MSR 0x0174)。
有关详细信息,请参阅《英特尔软件开发人员手册》 第2-B卷 第4.3章:中的Chapter 4.3: Instructions (M-U)部分。
选择器 | 用途 |
---|---|
前置的一些条目(Entry) | Null描述符 内核段 等. |
IA32_SYSENTER_CS + 0x0000 | DPL 0 Code Segment SYSENTER Code |
IA32_SYSENTER_CS + 0x0008 | DPL 0 Data Segment SYSENTER Stack |
IA32_SYSENTER_CS + 0x0010 | DPL 3 Code Segment SYSEXIT Code |
IA32_SYSENTER_CS + 0x0018 | DPL 3 Data Segment SYSEXIT Stack |
随后的一些条目 | 其他任何描述符 |
存储在这些段中的实际值将取决于你的系统设计。
如何设置GDT
禁用中断
如果启用了中断,需 “绝对确定” 将其关闭,否则你可能会遇到不希望的行为和异常。 这可以通过CLI汇编指令实现。
填写表
上面的 GDT 结构说明中还没有向你展示如何以正确的格式编写条目。 由于与286的GDT向后兼容,描述符的实际结构有点混乱。 Base地址分为三个不同的字段,并且你不能对随意选择的limit进行编码。
void encodeGdtEntry(uint8_t *target, struct GDT source)
{
// 检查limit以确保可以对其进行编码
if (source.limit > 0xFFFFF) {kerror("GDT cannot encode limits larger than 0xFFFFF");}
// 对limit进行编码
target[0] = source.limit & 0xFF;
target[1] = (source.limit >> 8) & 0xFF;
target[6] = (source.limit >> 16) & 0x0F;
//对base进行编码
target[2] = source.base & 0xFF;
target[3] = (source.base >> 8) & 0xFF;
target[4] = (source.base >> 16) & 0xFF;
target[7] = (source.base >> 24) & 0xFF;
// 编码access byte
target[5] = source.access_byte;
// 对各标志位进行编码
target[6] |= (source.flags << 4)
}
为了填写GDT表,你需要为每个条目使用一次此函数,这里*target指向Segment Descriptor的逻辑地址,source是你设计的包含必要信息的结构体。
当然,你也可以在 GDTsource结构体 中对值实现进行硬编码,而不是在运行时转换它们。
告诉CPU表在哪里
设置CPU GDT表位置需要一些汇编。 虽然你可以使用 内联汇编,但 LGDT 和 LIDT 指令所期望的是内存包,这使编写小型汇编例程来实现更加容易。 如上所述,你将使用LGDT指令加载Base和GDT的Limit。 由于基地址应该是线性地址,根据当前的MMU设置,你需要进行一些调整。
实模式
这里的线性地址应计算为 段*16+偏移量。 以下代码中 GDT 和 GDT_end 被假定为当前数据段中的符号。
gdtr DW 0 ; For limit storage
DD 0 ; For base storage
setGdt:
XOR EAX, EAX
MOV AX, DS
SHL EAX, 4
ADD EAX, ''GDT''
MOV [gdtr + 2], eax
MOV EAX, ''GDT_end''
SUB EAX, ''GDT''
MOV [gdtr], AX
LGDT [gdtr]
RET
Protected Mode, Flat Model
“Flat”表示数据段的基地址为0(无论是否启用了分页)。 例如,如果你的代码刚刚被 GRUB 引导,就是这种情况。 在System V ABI中,参数在堆栈中按相反顺序传递,因此可以setGdt(limit, base)的函数调用可能类似于以下示例代码。
gdtr DW 0 ; For limit storage
DD 0 ; For base storage
setGdt:
MOV AX, [esp + 4]
MOV [gdtr], AX
MOV EAX, [ESP + 8]
MOV [gdtr + 2], EAX
LGDT [gdtr]
RET
Protected Mode, Non-Flat Model
如果你的数据段具有非零的base,则必须调整上述序列的指令,以包括添加数据段的base offset的功能,offset应该是你的已知值。 你可以将其作为参数传入,并将此函数调用为setGdt(limit, base, offset)。
gdtr DW 0 ; For limit storage
DD 0 ; For base storage
setGdt:
MOV AX, [esp + 4]
MOV [gdtr], AX
MOV EAX, [ESP + 8]
ADD EAX, [ESP + 12]
MOV [gdtr + 2], EAX
LGDT [gdtr]
RET
Long Mode
在Long Mode中,Base字段的长度是8个字节,而不是4个字节。 同样,System V ABI 通过 RDI 和 RSI 寄存器传递前两个参数。 因此,这个示例代码可以这样setGdt(limit, base)调用。 此外,在长模式下,只有flat model是可能的,因此不必考虑其他情况。
gdtr DW 0 ; For limit storage
DQ 0 ; For base storage
setGdt:
MOV [gdtr], DI
MOV [gdtr+2], RSI
LGDT [gdtr]
RET
重新加载段寄存器
在将新的段选择器(Segment Selectors)加载到段寄存器(Segment Registers)中之前,对GDT所做的任何操作都不会对CPU产生影响。 对于这些寄存器中的大多数,过程与使用MOV指令一样简单,但是更改CS寄存器需要类似于jump或call别处的代码,因为这是更改其值的唯一方式。
保护模式
在这种情况下,直接在jump指令之后,重新加载CS与执行到所需段的far jump一样简单:
reloadSegments:
; 重新加载包含代码选择器的CS寄存器:
JMP 0x08:.reload_CS ; 0x08代表是你的代码段
.reload_CS:
; 重新加载数据段寄存器:
MOV AX, 0x10 ; 0x10代码是你的数据段
MOV DS, AX
MOV ES, AX
MOV FS, AX
MOV GS, AX
MOV SS, AX
RET
在这里可以找到上述代码的详细解释。
Long Mode
在Long Mode中,更改CS的过程并不简单,因为不能使用far jump。 建议改用far return来代替:
reloadSegments:
; Reload CS register:
PUSH 0x08 ; 将代码段推送到堆栈,0x08代表是你的代码段
LEA RAX, [rel .reload_CS] ; 将.reload_CS的地址加载到RAX中
PUSH RAX ; 将此值推入栈
RETFQ ; 根据语法执行far return、RETFQ或LRETQ
.reload_CS:
; 重新加载数据段寄存器
MOV AX, 0x10 ; 0x10代表是你的数据段
MOV DS, AX
MOV ES, AX
MOV FS, AX
MOV GS, AX
MOV SS, AX
RET
LDT(本地描述符表)
与GDT (全局描述符表) 非常相似,LDT (“本地” 描述符表) 包含了用于内存段描述,调用门(call gates)等的描述符。 LDT的好处是每个任务都可以有自己的LDT,并且当你使用硬件任务切换时,处理器会自动切换到正确的LDT。
由于其内容在每个任务中可能不同,因此LDT不是放置系统内容 (例如TSS或其他LDT描述符) 的合适位置: 这些只能放在是GDT中。 因为它需要经常更改,所以用于加载LDT的命令与GDT和IDT加载略有不同。 这些参数不是直接给出LDT的基地址和大小,而是存储在GDT的描述符中 (具有适当的 “LDT” 类型),并给出该条目的选择器。
GDTR (base + limit) +-- GDT ------------+ | | SELECTOR ---> [LDT descriptor ]----> LDTR (base + limit) | | +-- LDT ------------+ | | | | ... ... ... ... +-------------------+ +-------------------+
请注意,对于386+处理器,分页已经使LDT几乎过时,并且不再需要多个LDT描述符,因此在操作系统开发中几乎可以安全地忽略LDT,除非需要通过设计许多不同的段来进行存储。
IDT介绍以及为什么需要它
如上所述,IDT (中断描述符表) 的加载方式与GDT大致相同,其结构大致相同,只是它只包含调用门(gate)而不包含段。 每个门都给出一段代码的完整引用(代码段、特权级别和该段代码的偏移量),每段代码绑定到0到255之间的数字(IDT中的插槽-slot)。
IDT将是内核序列中最先启用的内容之一,这样你就可以进行硬件异常捕获、监听外部事件等。 有关X86系列中断的更多信息,请参见 中断。
一些让你的生活更轻松的东西
用于轻松创建GDT条目的工具。
// 用于创建64位整数形式的GDT段描述符。
#include <stdio.h>
#include <stdint.h>
// 这里的每个定义都针对描述符中的特定标志。
// 请参阅英特尔文档,了解每项功能的说明。
#define SEG_DESCTYPE(x) ((x) << 0x04) // 描述符类型 (系统为0,代码/数据为1)
#define SEG_PRES(x) ((x) << 0x07) // Present
#define SEG_SAVL(x) ((x) << 0x0C) // 可供系统使用
#define SEG_LONG(x) ((x) << 0x0D) // Long mode
#define SEG_SIZE(x) ((x) << 0x0E) // Size(16位为0,32位为1)
#define SEG_GRAN(x) ((x) << 0x0F) // Granularity-粒度 (10b-1 mb为0,4KB-4gb为1)
#define SEG_PRIV(x) (((x) & 0x03) << 0x05) // 设置权限级别(0-3)
#define SEG_DATA_RD 0x00 // Read-Only
#define SEG_DATA_RDA 0x01 // Read-Only, accessed
#define SEG_DATA_RDWR 0x02 // Read/Write
#define SEG_DATA_RDWRA 0x03 // Read/Write, accessed
#define SEG_DATA_RDEXPD 0x04 // Read-Only, expand-down
#define SEG_DATA_RDEXPDA 0x05 // Read-Only, expand-down, accessed
#define SEG_DATA_RDWREXPD 0x06 // Read/Write, expand-down
#define SEG_DATA_RDWREXPDA 0x07 // Read/Write, expand-down, accessed
#define SEG_CODE_EX 0x08 // Execute-Only
#define SEG_CODE_EXA 0x09 // Execute-Only, accessed
#define SEG_CODE_EXRD 0x0A // Execute/Read
#define SEG_CODE_EXRDA 0x0B // Execute/Read, accessed
#define SEG_CODE_EXC 0x0C // Execute-Only, conforming
#define SEG_CODE_EXCA 0x0D // Execute-Only, conforming, accessed
#define SEG_CODE_EXRDC 0x0E // Execute/Read, conforming
#define SEG_CODE_EXRDCA 0x0F // Execute/Read, conforming, accessed
#define GDT_CODE_PL0 SEG_DESCTYPE(1) | SEG_PRES(1) | SEG_SAVL(0) | \
SEG_LONG(0) | SEG_SIZE(1) | SEG_GRAN(1) | \
SEG_PRIV(0) | SEG_CODE_EXRD
#define GDT_DATA_PL0 SEG_DESCTYPE(1) | SEG_PRES(1) | SEG_SAVL(0) | \
SEG_LONG(0) | SEG_SIZE(1) | SEG_GRAN(1) | \
SEG_PRIV(0) | SEG_DATA_RDWR
#define GDT_CODE_PL3 SEG_DESCTYPE(1) | SEG_PRES(1) | SEG_SAVL(0) | \
SEG_LONG(0) | SEG_SIZE(1) | SEG_GRAN(1) | \
SEG_PRIV(3) | SEG_CODE_EXRD
#define GDT_DATA_PL3 SEG_DESCTYPE(1) | SEG_PRES(1) | SEG_SAVL(0) | \
SEG_LONG(0) | SEG_SIZE(1) | SEG_GRAN(1) | \
SEG_PRIV(3) | SEG_DATA_RDWR
void
create_descriptor(uint32_t base, uint32_t limit, uint16_t flag)
{
uint64_t descriptor;
// Create the high 32 bit segment
descriptor = limit & 0x000F0000; // 设置limit位19:16
descriptor |= (flag << 8) & 0x00F0FF00; // 设置类型,p,dpl,s,g,d/b,l和avl字段
descriptor |= (base >> 16) & 0x000000FF; // 设置base位23:16
descriptor |= base & 0xFF000000; //设置base位31:24
// 移位32以启用段的低位部分
descriptor <<= 32;
// 创建低32位段
descriptor |= base << 16; // 设置base位15:0
descriptor |= limit & 0x0000FFFF; // 设置limit位15:0
printf("0x%.16llX\n", descriptor);
}
int
main(void)
{
create_descriptor(0, 0, 0);
create_descriptor(0, 0x000FFFFF, (GDT_CODE_PL0));
create_descriptor(0, 0x000FFFFF, (GDT_DATA_PL0));
create_descriptor(0, 0x000FFFFF, (GDT_CODE_PL3));
create_descriptor(0, 0x000FFFFF, (GDT_DATA_PL3));
return 0;
}
另见
文章
- Global Descriptor Table
- http://web.archive.org/web/20190424213806/http://www.osdever.net/tutorials/view/the-world-of-protected-mode - 如何在汇编程序中设置GDT