“GDT Tutorial”的版本间差异

来自osdev
跳到导航 跳到搜索
(创建页面,内容为“{{Rating|1}} 在 Intel Architecture 中,更确切地说,在 protected mode 中,大多数 memory managementInterrupt Service Routines 都是通过描述符表来控制的。 每个描述符存储关于CPU在某个时间可能需要的单个对象 (例如,服务例程、任务、代码或数据块,无论什么) 的信息。 例如,如果您尝试将新值加载到 段寄存器 中,则CPU需要执行…”)
 
第1行: 第1行:
{{Rating|1}}
{{Rating|1}}
在 [[:Category:x86 | Intel Architecture]] 中,更确切地说,在 [[protected mode]] 中,大多数 [[memory management]] [[Interrupt Service Routines]] 都是通过描述符表来控制的。 每个描述符存储关于CPU在某个时间可能需要的单个对象 (例如,服务例程、任务、代码或数据块,无论什么) 的信息。 例如,如果您尝试将新值加载到 [[Segment | 段寄存器]] 中,则CPU需要执行安全和访问控制检查,以查看您是否实际上有权访问该特定内存区域。 一旦执行了检查,有用的值 (例如最低和最高地址) 就会缓存在CPU的不可见寄存器中。
在 [[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寄存器中。


英特尔定义了3种类型的表: [[Interrupt Descriptor Table]] (取代 [[IVT]]) 、全局描述符表 ([[GDT]]) 和本地描述符表。 每个表分别通过 <tt>LIDT</tt>,<tt>LGDT</tt>,<tt>LLDT</tt> 指令定义为 (大小,[[线性地址]]) 到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的说法)
: 逻辑上连续的内存块,从CPU的角度具有一致的属性。
; Segment Register
; '''段寄存器(Segment Register)'''
: 您的CPU的寄存器,它引用了用于特殊用途的段 (例如 <tt>SS</tt><tt>CS</tt><tt>DS</tt> ...)
: 一类CPU的寄存器,它指向的是用于特定目的的段 ('''CS''','''DS''','''SS''''''ES''') 或用于一般用途的 ('''FS''''''GS''')
; Selector
; '''[[Segment Selector|段选择器-Segment Selector]]'''
: 对可以加载到段寄存器中的描述符的引用; 选择器是描述符表项的偏移量。 这些条目有8个字节长。 因此,位3及以上仅声明描述符表条目偏移,而位2指定此选择器是GDT还是LDT选择器 (LDT位设置,GDT位清除),和位0-1声明需要对应描述符表项的DPL字段的Ring级。 如果没有,则发生一般保护故障; 如果确实对应,则相应地更改所用选择器的CPL级别。
: 对描述符(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给定段的属性的内存结构 (表的一部分)
: 描述符表中的条目。 这是一个二进制数据结构,告诉CPU给定段的属性。


= = 在GDT里放什么 = =
==在GDT中放入什么==


=== 基础 ===
=== 基础内容 ===


出于理智的目的,您应该始终将这些项目存储在GDT中:
为了合理使用CPU,你应该始终将以下项目存储在GDT中:


* 处理器从未引用的空描述符。 某些仿真器 (例如Bochs) 会抱怨如果您没有出现限制异常。 有些使用此描述符来存储指向GDT本身的指针 (与LGDT指令一起使用)。 空描述符是8字节宽,指针是6字节宽,所以它可能只是这个完美的地方。
* 描述符表中的条目0,'''空描述符'''从不被处理器引用,并且应该始终不包含任何数据。 如果你没有Null Descriptor,某些模拟器 (例如Bochs) 会报限制异常(limit exceptions)。 有些人使用这个描述符来存储指向GDT本身的指针(与LGDT指令一起使用)。 空描述符为8字节宽,其中指针为6字节宽,因此它可能正是进行此操作的最佳位置。
* 一个代码段描述符 (对于您的内核,它应该具有type = 0x9A)
* 一个DPL 0 '''代码段''' 描述符 (用于你的内核)
* 数据段描述符 (您不能写入代码段,因此请使用type = 0x92添加它)
* 一个'''数据段'''描述符(不允许写入代码段)
* [[TSS]] 段描述符 (相信我,至少保留一个位置)
* 一个'''[[Task State Segment]]'''段描述符(至少有一个是非常有用的)
* 如果需要,可以容纳更多细分段 (例如用户级别,[[LDT]],更多ts,等等)
* 如果需要,可以容纳更多细分段 (例如用户级别,[[LDT]],更多TSS,等等)


=== Sysenter/Sysexit ===
=== Flat / Long Mode 设置===


如果您使用的是Intel <tt>SYSENTER</tt>/<tt>SYSEXIT</tt> 例程,则GDT的结构必须如下:
如果你不希望使用'''[[Segmentation|分段]]'''将内存分隔为多个受保护区域,则只需使用几个段描述符即可。 一个原因可能是你希望仅使用分页来保护内存。 此外,此设置方式在'''[[Long Mode]]]'''中必须'''严格执行''',因为base和limit值已经被忽略了。


* 前面的任何描述符 (空描述符、特殊内核的东西等)
在此方案中,唯一需要的'''[[Global_Descriptor_Table#段描述符(Segment_Descriptor)|段描述符-Segment Descriptor]]'''是'''空描述符''', 以及一个描述符(由期望的特权级别、段类型和执行模式的组合而成),以及系统描述符。 通常,这将包括内核和用户模式的一个代码段和一个数据段,以及一个'''[[Task State Segment]]'''。
* 一个 [[DPL]] 0代码段描述符 (<tt>SYSENTER</tt> 将使用的那个)
* 一个DPL 0数据段描述符 (用于 <tt>SYSENTER</tt> 堆栈)
* DPL 3代码段 (用于 <tt>SYSEXIT</tt> 之后的代码)
* DPL 3数据段 (用于 <tt>SYSEXIT</tt> 之后的用户模式堆栈)
* 任何其他描述符


DPL 0代码段的段被加载到 [[MSR]] 中。 其他的是根据该值计算的。 有关更多信息,请参阅 <tt>SYSENTER</tt> <tt>SYSEXIT</tt> 的英特尔说明参考。
{|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>
|}


=== Flat设置 ===
===细分内核设置===


您想要完整的4个GiB地址未翻译: 只需使用
如果你希望将内存分离到代码和数据的受保护区域中,则必须将表中每个条目的 '''Base''' 和 '''Limit''' 值设置为所需的格式。


<source lang="c">
例如,你可能希望有两个段,一个4MiB代码段从4MiB开始,另一个4MiB数据段从8MiB开始,这两个段都只能由Ring 0访问。 在这种情况下,你的GDT可能如下所示:
GDT[0] = {.base=0, .limit=0, .type=0};                    // Selector 0x00 cannot be used
 
GDT[1] = {.base=0, .limit=0xffffffff, .type=0x9A};        // Selector 0x08 will be our code
{|class="wikitable" style="display: inline-table;"
GDT[2] = {.base=0, .limit=0xffffffff, .type=0x92};        // Selector 0x10 will be our data
|+ Small Kernel
GDT[3] = {.base=&myTss, .limit=sizeof(myTss), .type=0x89}; // You can use LTR(0x18)
!选择器 !! 用途 !! 内容
</source>
|-
| 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)。


如果您希望 (出于特定原因) 将代码和数据清楚地分开 (假设两者都有4 MiB,也从4 MiB开始),只需使用:
有关详细信息,请参阅《英特尔软件开发人员手册》 第2-B卷 第4.3章:中的'''Chapter 4.3: Instructions (M-U)'''部分。


<source lang="c">
{|class="wikitable"
GDT[0] = {.base=0, .limit=0, .type=0};                      // Selector 0x00 cannot be used
|+ GDT
GDT[1] = {.base=0x400000, .limit=0x3fffff, .type=0x9A}; // Selector 0x08 will be our code
!选择器 !! 用途
GDT[2] = {.base=0x800000, .limit=0x3fffff, .type=0x92}; // Selector 0x10 will be our data
|-
GDT[3] = {.base=&myTss, .limit=sizeof(myTss), .type=0x89};  // You can use LTR(0x18)
|前置的一些条目(Entry) || Null描述符 <br> 内核段 <br> 等.
</source>
|-
| 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
|-
| 随后的一些条目 || 其他任何描述符
|}


这意味着您在物理地址4 MiB加载的任何内容都将在 <tt>CS:0</tt> 处显示为代码,而您在物理地址8 MiB加载的内容将在 <tt>DS:0</tt> 处显示为数据。 然而,这可能不是最好的设计。
存储在这些段中的实际值将取决于你的系统设计。


= = 怎么做 = =
==如何设置GDT==


=== 禁用中断 ===
===禁用中断===


如果启用了它们,请将其关闭,否则您将遇到麻烦。
如果启用了中断,则 “绝对确定” 将其关闭,否则你可能会遇到不希望的行为和异常。 这可以通过'''CLI'''汇编指令实现。


=== 填表 ===
=== 填写表 ===


<tt>GDT[]</tt> 的上述结构并不完整。 对于向后兼容286的GDT,描述符的实际结构有点混乱。 基址在3个不同的字段上分割,您不能编码任何您想要的限制。 另外,在这里和那里,如果你想让事情正常工作,你需要正确设置这些标志。
上面的 '''GDT''' 结构说明中还没有向你展示如何以正确的格式编写条目。 由于与286的'''GDT'''向后兼容,描述符的实际结构有点混乱。 Base地址分为三个不同的字段,并且你不能对随意选择limit进行编码。


<source lang="c">
<source lang="c">
/**
* \param target A pointer to the 8-byte GDT entry
* \param source An arbitrary structure describing the GDT entry
*/
void encodeGdtEntry(uint8_t *target, struct GDT source)
void encodeGdtEntry(uint8_t *target, struct GDT source)
{
{
     // Check the limit to make sure that it can be encoded
     // 检查limit以确保可以对其进行编码
     if ((source.limit > 65536) && ((source.limit & 0xFFF) != 0xFFF)) {
     if (source.limit > 0xFFFFF) {kerror("GDT cannot encode limits larger than 0xFFFFF");}
        kerror("You can't do that!");
 
    }
     // 对limit进行编码
    if (source.limit > 65536) {
        // Adjust granularity if required
        source.limit = source.limit >> 12;
        target[6] = 0xC0;
    } else {
        target[6] = 0x40;
    }
   
     // Encode the 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] |= (source.limit >> 16) & 0xF;
     target[6] = (source.limit >> 16) & 0x0F;
   
 
     // Encode the base
     //对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;
   
 
     // And... Type
     // 编码access byte
     target[5] = source.type;
     target[5] = source.access_byte;
 
    // 对各标志位进行编码
    target[6] |= (source.flags << 4)
}
}
</source>
</source>


当然,您可以对其进行硬编码,而不是在运行时将其转换。 此代码假定您只需要32位段。
为了填写GDT表,你需要为每个条目使用一次此函数,这里<tt>*target</tt>指向'''Segment Descriptor'''的逻辑地址,<tt>source</tt>是你设计的包含必要信息的结构体。
 
当然,你也可以在 '''GDT'''源结构体 中对值实现进行硬编码,而不是在运行时转换它们。


=== 告诉CPU表的位置 ===
=== 告诉CPU表在哪里===


这里需要一些汇编示例。 虽然您可以使用 [[内联程序集]],但 <tt>LGDT</tt> <tt>LIDT</tt> 所期望的内存打包使编写小型程序集例程变得更加容易。 如上所述,您将使用 <tt>LGDT</tt> 指令加载基地址和GDT的限制。 由于基址应该是一个线性地址,因此您需要根据当前的 [[MMU]] 设置进行一些调整。
设置CPU GDT表位置需要一些汇编。 虽然你可以使用 [[inline assembly|内联汇编]],但 '''LGDT''' '''LIDT''' 指令所期望的是内存包,这使编写小型汇编例程来实现更加容易。 如上所述,你将使用'''LGDT'''指令加载Base和GDT的Limit。 由于基地址应该是线性地址,根据当前的[[MMU]]设置,你需要进行一些调整。


==== 从实模 ====
==== 实模式 ====


此处的线性地址应计算为 <tt> 段 * 16偏移 </tt>。 <tt>GDT</tt> 和 <tt>GDT_end</tt> 被假定为当前数据段中的符号。
这里的线性地址应计算为 <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 (无论分页是打开还是关闭)。 例如,如果您只是被 [[GRUB]] 引导,情况就是这样。 您应该将其称为 <tt>setGdt(GDT,sizeof(GDT))</tt>
“Flat”表示数据段的基地址为0(无论是否启用了'''[[Paging]]''')。 例如,如果你的代码刚刚被 [[GRUB]] 引导,就是这种情况。 在'''[[System V ABI]]'''中,参数在堆栈中按相反顺序传递,因此可以<tt>setGdt(limit, base)</tt>的函数调用可能类似于以下示例代码。


<source lang="asm">
<source lang="asm">
第149行: 第188行:


setGdt:
setGdt:
   MOV  EAX, [esp + 4]
   MOV  AX, [esp + 4]
  MOV  [gdtr], AX
  MOV  EAX, [ESP + 8]
   MOV  [gdtr + 2], EAX
   MOV  [gdtr + 2], EAX
   MOV  AX, [ESP + 8]
  LGDT  [gdtr]
  DEC  AX
  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>


==== 来自非flat保护模式 ====
==== 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


如果您的数据段具有非零基数 (例如,您在分段技巧中使用的是 [[较高的半内核]]), 您必须在上面序列的 “<tt>MOV EAX,...</tt>” 和 “<tt>MOV...,EAX</tt>” 指令之间 “<tt> 添加EAX,base_of_your_data_segment_which_you_should_know</tt>”。
setGdt:
  MOV  [gdtr], DI
  MOV  [gdtr+2], RSI
  LGDT  [gdtr]
  RET
</source>


=== 重新加载段寄存器 ===
=== 重新加载段寄存器 ===


无论您对GDT做什么,都不会对CPU产生影响,直到您将选择器加载到段寄存器中。 您可以使用以下方法执行此操作:
在将新的'''段选择器(Segment Selectors)'''加载到'''段寄存器(Segment Registers)'''中之前,对'''GDT'''所做的任何操作都不会对CPU产生影响。 对于这些寄存器中的大多数,过程与使用'''MOV'''指令一样简单,但是更改'''CS'''寄存器需要类似于jump或call别处的代码,因为这是更改其值的唯一方式。
 
==== 保护模式 ====
 
在这种情况下,直接在jump指令之后,重新加载'''CS'''与执行到所需段的far jump一样简单:


<source lang="asm">
<source lang="asm">
reloadSegments:
reloadSegments:
   ; Reload CS register containing code selector:
   ; 重新加载包含代码选择器的CS寄存器:
   JMP  0x08:.reload_CS ; 0x08 points at the new code selector
   JMP  0x08:.reload_CS ; 0x08代表是你的代码段
.reload_CS:
.reload_CS:
   ; Reload data segment registers:
   ; 重新加载数据段寄存器:
   MOV  AX, 0x10 ; 0x10 points at the new data selector
   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 here]
[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 ==
==LDT(本地描述符表)==


与GDT (全局描述符表) 非常相似,LDT (“本地” 描述符表) 包含用于内存段描述,调用门等的描述符。 LDT的好处是,每个任务都可以有自己的LDT,并且当您使用硬件任务切换时,处理器会自动切换到正确的LDT。
与GDT (全局描述符表) 非常相似,LDT (“本地” 描述符表) 包含了用于内存段描述,调用门(call gates)等的描述符。 LDT的好处是每个任务都可以有自己的LDT,并且当你使用硬件任务切换时,处理器会自动切换到正确的LDT。


由于其内容在每个任务中可能不同,因此LDT不是放置系统内容 (例如TSS或其他LDT描述符) 的合适位置: 这些是GDT的唯一特别之处。 由于要经常更改,因此用于加载LDT的命令与GDT和IDT加载有点不同。 这些参数不是直接给出LDT的基地址和大小,而是存储在GDT的描述符中 (具有适当的 “LDT” 类型),并给出该条目的选择器。
由于其内容在每个任务中可能不同,因此LDT不是放置系统内容 (例如TSS或其他LDT描述符) 的合适位置: 这些只能放在是GDT中。 因为它需要经常更改,所以用于加载LDT的命令与GDT和IDT加载略有不同。 这些参数不是直接给出LDT的基地址和大小,而是存储在GDT的描述符中 (具有适当的 “LDT” 类型),并给出该条目的选择器。


                 GDTR (base + limit)
                 GDTR (base + limit)
第198行: 第291行:
               +-------------------+    +-------------------+
               +-------------------+    +-------------------+


请注意,使用386处理器,分页使LDT几乎过时,并且不再需要多个LDT描述符,因此您几乎可以安全地忽略LDT进行操作系统开发,除非您设计了许多不同的段来存储。
请注意,对于386+处理器,分页已经使LDT几乎过时,并且不再需要多个LDT描述符,因此在操作系统开发中几乎可以安全地忽略LDT,除非需要通过设计许多不同的段来进行存储。


== The IDT and why it's needed ==
== IDT介绍以及为什么需要它==


如上所述,IDT (中断描述符表) 的加载方式与GDT大致相同,其结构大致相同,只是它只包含门而不包含段。 每个门给出了对一段代码 (代码段、权限级别和该段中的代码的偏移量) 的完整引用,该代码现在绑定到0和255之间的数字 (IDT中的时隙)。
如上所述,IDT (中断描述符表) 的加载方式与GDT大致相同,其结构大致相同,只是它只包含调用门(gate)而不包含段。 每个门都给出一段代码的完整引用(代码段、特权级别和该段代码的偏移量),每段代码绑定到0到255之间的数字(IDT中的插槽-slot)。


IDT将是在内核序列中启用的第一件事,以便您可以捕获硬件异常,侦听外部事件等。 有关X86系列中断的更多信息,请参见 [[中断]]。
IDT将是内核序列中最先启用的内容之一,这样你就可以进行硬件异常捕获、监听外部事件等。 有关X86系列中断的更多信息,请参见 [[Interrupts|中断]]。


== 一些让你的生活变得轻松的东西 ==
==一些让你的生活更轻松的东西==


轻松创建GDT条目的工具。
用于轻松创建GDT条目的工具。
<source lang="c">
<source lang="c">
// Used for creating GDT segment descriptors in 64-bit integer form.
// 用于创建64位整数形式的GDT段描述符。
   
   
#include <stdio.h>
#include <stdio.h>
#include <stdint.h>
#include <stdint.h>
   
   
// Each define here is for a specific flag in the descriptor.
// 这里的每个定义都针对描述符中的特定标志。
// Refer to the intel documentation for a description of what each one does.
// 请参阅英特尔文档,了解每项功能的说明。
#define SEG_DESCTYPE(x)  ((x) << 0x04) // Descriptor type (0 for system, 1 for code/data)
#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) // Available for system use
#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 (0 for 16-bit, 1 for 32)
#define SEG_SIZE(x)      ((x) << 0x0E) // Size(16位为0,32位为1)
#define SEG_GRAN(x)      ((x) << 0x0F) // Granularity (0 for 1B - 1MB, 1 for 4KB - 4GB)
#define SEG_GRAN(x)      ((x) << 0x0F) // Granularity-粒度 (10b-1 mb为0,4KB-4gb为1)
#define SEG_PRIV(x)    (((x) &  0x03) << 0x05)  // Set privilege level (0 - 3)
#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;        // set limit bits 19:16
     descriptor  =  limit      & 0x000F0000;        // 设置limit位19:16
     descriptor |= (flag <<  8) & 0x00F0FF00;        // set type, p, dpl, s, g, d/b, l and avl fields
     descriptor |= (flag <<  8) & 0x00F0FF00;        // 设置类型,p,dpl,s,g,d/b,l和avl字段
     descriptor |= (base >> 16) & 0x000000FF;        // set base bits 23:16
     descriptor |= (base >> 16) & 0x000000FF;        // 设置base位23:16
     descriptor |=  base        & 0xFF000000;        // set base bits 31:24
     descriptor |=  base        & 0xFF000000;        //设置base位31:24
   
   
     // Shift by 32 to allow for low part of segment
     // 移位32以启用段的低位部分
     descriptor <<= 32;
     descriptor <<= 32;
   
   
     // Create the low 32 bit segment
     // 创建低32位段
     descriptor |= base  << 16;                      // set base bits 15:0
     descriptor |= base  << 16;                      // 设置base位15:0
     descriptor |= limit  & 0x0000FFFF;              // set limit bits 15:0
     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


=== Threads ===
===论坛主题===


=== External Links ===
===外部链接===


[[Category:Tutorials]]
[[Category:Tutorials]]
[[Category:X86 CPU]]
[[Category:X86 CPU]]

2022年3月7日 (一) 06:23的版本

难度等级
Difficulty 1.png
初学者

IA-32x86-64 架构上,更准确地说,在 保护模式-Protected Mode长模式-Long Mode 中,控制中断服务例程 ISR-Interrupt Service Routines 和做好内存管理都需要通过描述符(descriptors)表。 每个描述符存储CPU在某个时间点可能需要的单个目标(例如服务例程、任务、代码或数据块)信息。 例如,如果你尝试将新值加载到段寄存器中时, CPU需要执行安全和访问控制检查,以查看你是否实际上有权访问该特定内存区域。 一旦执行了检查,有用的值(如最低和最高地址)就会根据描述符,被读取并缓存到也许不可见的CPU寄存器中。

在这些体系结构上,有三种此类型的表:全局描述符表-Global Descripptor Table本地描述符表_Local DescrippTable中断描述符表(它取代了[Interrupt Vector Table|中断向量表-Interrupt Vector Table]])。 上面每个表都用它们的大小和 线性地址进行了定义, 并分别通过 LGDTLLDTLIDT 指令发送到CPU。 在几乎所有的用例中,这些表都只在启动时放入内存一次,然后在需要时进行编辑。

必知词汇表

分段-Segment
逻辑上连续的内存块,从CPU的角度具有一致的属性。
段寄存器(Segment Register)
一类CPU的寄存器,它指向的是用于特定目的的段 (CSDSSSES) 或用于一般用途的 (FSGS)
段选择器-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

32-bit
选择器 用途 内容
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
64-bit
选择器 用途 内容
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

细分内核设置

如果你希望将内存分离到代码和数据的受保护区域中,则必须将表中每个条目的 BaseLimit 值设置为所需的格式。

例如,你可能希望有两个段,一个4MiB代码段从4MiB开始,另一个4MiB数据段从8MiB开始,这两个段都只能由Ring 0访问。 在这种情况下,你的GDT可能如下所示:

Small Kernel
选择器 用途 内容
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)部分。

GDT
选择器 用途
前置的一些条目(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表位置需要一些汇编。 虽然你可以使用 内联汇编,但 LGDTLIDT 指令所期望的是内存包,这使编写小型汇编例程来实现更加容易。 如上所述,你将使用LGDT指令加载Base和GDT的Limit。 由于基地址应该是线性地址,根据当前的MMU设置,你需要进行一些调整。

实模式

这里的线性地址应计算为 段*16+偏移量。 以下代码中 GDTGDT_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 通过 RDIRSI 寄存器传递前两个参数。 因此,这个示例代码可以这样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;
}

另见

文章

论坛主题

外部链接