查看“GDT Tutorial”的源代码
←
GDT Tutorial
跳到导航
跳到搜索
因为以下原因,您没有权限编辑本页:
您请求的操作仅限属于该用户组的用户执行:
用户
您可以查看和复制此页面的源代码。
{{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 table)。 每个描述符存储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。 在几乎所有的用例中,这些表都只在启动时放入内存一次,然后在需要时进行编辑。 == 必知词汇表 == ; '''[[Segmentation|分段-Segment]]''' : 逻辑上连续的内存块,从CPU的角度具有一致的属性。 ; '''段寄存器(Segment Register)''' : 一类CPU的寄存器,它指向的是用于特定目的的段 ('''CS''','''DS''','''SS''','''ES''') 或用于一般用途的 ('''FS''','''GS''') ; '''[[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安全级别会相应地改变。 ; '''[[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]],更多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 !选择器 !! 用途 |- |前置的一些条目(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"> 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) } </source> 为了填写GDT表,你需要为每个条目使用一次此函数,这里<tt>*target</tt>指向'''Segment Descriptor'''的逻辑地址,<tt>source</tt>是你设计的包含必要信息的结构体。 当然,你也可以在 '''GDT'''source结构体 中对值实现进行硬编码,而不是在运行时转换它们。 === 告诉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"> 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 </source> ==== Protected Mode, Flat Model ==== “Flat”表示数据段的基地址为0(无论是否启用了'''[[Paging|分页]]''')。 例如,如果你的代码刚刚被 [[GRUB]] 引导,就是这种情况。 在'''[[System V ABI]]'''中,参数在堆栈中按相反顺序传递,因此可以<tt>setGdt(limit, base)</tt>的函数调用可能类似于以下示例代码。 <source lang="asm"> 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 </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 EAX, [ESP + 8] ADD EAX, [ESP + 12] MOV [gdtr + 2], EAX LGDT [gdtr] RET </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"> 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 </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 (“本地” 描述符表) 包含了用于内存段描述,调用门(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系列中断的更多信息,请参见 [[Interrupts|中断]]。 ==一些让你的生活更轻松的东西== 用于轻松创建GDT条目的工具。 <source lang="c"> // 用于创建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; } </source> ==另见== ===文章=== * [[Global Descriptor Table]] * http://web.archive.org/web/20190424213806/http://www.osdever.net/tutorials/view/the-world-of-protected-mode - 如何在汇编程序中设置GDT ===论坛主题=== ===外部链接=== [[Category:Tutorials]] [[Category:X86 CPU]]
本页使用的模板:
模板:Eq
(
查看源代码
)
模板:Eq1
(
查看源代码
)
模板:If
(
查看源代码
)
模板:Rating
(
查看源代码
)
模板:Show1
(
查看源代码
)
返回至“
GDT Tutorial
”。
导航菜单
个人工具
登录
命名空间
页面
讨论
变体
已展开
已折叠
查看
阅读
查看源代码
查看历史
更多
已展开
已折叠
搜索
导航
首页
最近更改
随机页面
MediaWiki帮助
工具
链入页面
相关更改
特殊页面
页面信息