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