Bran的内核开发教程(bkerndev)-06 全局描述符表(GDT)

简介: Bran的内核开发教程(bkerndev)-06 全局描述符表(GDT)

全局描述符表(GDT)

  在386平台各种保护措施中最重要的就是全局描述符表(GDT)。GDT为内存的某些部分定义了基本的访问权限。我们可以使用GDT中的一个索引来生成段冲突异常, 让内核终止执行异常的进程。现代操作系统大多使用"分页"的内存模式来实现该功能, 它更具通用性和灵活性。GDT还定义了内存中的的某个部分是可执行程序还是实际的数据。GDT还可定义任务状态段(TSS)。TSS一般在基于硬件的多任务处理中使用, 所以我们在此并不做讨论。需要注意的是TSS并不是启用多任务的唯一方法。

  注意GRUB已经为你安装了一个GDT, 如果我们重写了加载GRUB的内存区域, 将会丢弃它的GDT, 这会导致"三重错误(Triple fault)"。简单的说, 它将重置机器。为了防止该问题的发生, 我们应该在已知可以访问的内存中构建自己的GDT, 并告诉处理器它在哪里, 最后使用我们的新索引加载处理器的CS、DS、ES、FS和GS寄存器。CS寄存器就是代码段, 它告诉处理器执行当前代码的访问权限在GDT中的偏移量。DS寄存器的作用类似, 但是数据段, 定义了当前数据的访问权限的偏移量。ES、FS和GS是备用的DS寄存器, 对我们并不重要。

  GDT本身是64位的长索引列表。这些索引定义了内存中可访问区域的起始位置和大小界限, 以及与该索引关联的访问权限。通常第一个索引, 0号索引被称为NULL描述符。所以我们不应该将任何的段寄存器设置为0, 否则将导致常见的保护错误, 这也是处理器的保护功能。通用的保护错误和几种异常将在中断服务程序(ISR)那节详细说明。

  每个GDT索引还定义了处理器正在运行的当前段是供系统使用的(Ring 0)还是供应用程序使用的(Ring 3)。也有其他Ring级别, 但并不重要。当今主要的操作系统仅使用Ring 0和Ring 3。任何应用程序在尝试访问系统或Ring 0的数据时都会导致异常, 这种保护是为了防止应用程序导致内核崩溃。GDT的Ring级别用于告诉处理器是否允许其执行特殊的特权指令。具有特权的指令只能在更高的Ring级别上运行。例如"cli"和"sti"禁用和启用中断, 如果应用程序被允许使用这两个指令, 它就可以阻止内核的运行。你将在本教程的后续章节中了解更多有关中断的知识。

  GDT的描述符组成如下:

  • G: 段界限粒度(Granularity)
  • G = 0: 长度单位为1字节
  • G = 1: 长度单位为4KB
  • D: 操作数大小
  • 0 = 16bit
  • 1 = 32bit
  • L: 未使用为0
  • AVL: 保留位, 系统软件使用
  • P: 存在位, 段是否存在
  • 1 = Yes
  • 0 = No
  • DPL: Ring级别(0到3)
  • S: 描述符类型位
  • S = 1: 存储段描述符, 数据段/代码段
  • S = 0: 系统段描述符/门描述符)
  • TYPE: 段类型

  在我们的内核教程中, 我们将创建一个包含3个索引的GDT。一个用于''虚拟''描述符充当处理器内存保护功能的NULL段, 一个用于代码段, 一个用于数据段寄存器。使用汇编操作码lgdt告诉处理器我们新的GDT表在哪里。为lgdt提供一个指向48位的专用的全局描述符表寄存器(GDTR)的指针。该寄存器用来保存全局描述符信息, 0-15位表示GDT的边界位置(数值为表的长度-1), 16-47位存放GDT基地址。并且在我们访问GDT中不存在偏移的段时, 希望处理器可以立即创建一般保护错误)。

  我们可以使用3个索引的简单数组来定义GDT。对于我们的特殊GDTR指针, 我们只需要声明一个即可。我们称其为gp。创建一个新文件gdt.c。在build.bat中添加一行gcc命令来编译gdt.c, 并将gdt.o添加到LD链接文件列表中。下面这些代码组成了gdt.c的前半部分:

gdt.c

#include <system.h>
 
/* 定义一个GDT索引. __attribute__((packed))用于防止编译器优化对齐 */
struct gdt_entry
{
    unsigned short limit_low;
    unsigned short base_low;
    unsigned char base_middle;
    unsigned char access;
    unsigned char granularity;
    unsigned char base_high;
} __attribute__((packed));
 
/* GDTR指针 */
struct gdt_ptr
{
    unsigned short limit;
    unsigned int base;
} __attribute__((packed));
 
/* 声明包含3个索引的GDT和GDTR指针gp */
struct gdt_entry gdt[3];
struct gdt_ptr gp;
 
/* 这是start.asm中的函数, 用来加载新的段寄存器 */
extern void gdt_flush();

  gdt_flush()我们还没有定义, 该函数使用上面的GDTR指针来告诉处理器新的GDT所在位置, 并重新加载段寄存器, 最后跳转到我们的新代码段。现在我们在start.asmstublet下的死循环后面添加下面的代码来定义gdt_flush:

start.asm

; 这将建立我们新的段寄存器
; 通过长跳转来设置CS
global _gdt_flush     ; 允许C源程序链接该函数
extern _gp            ; 声明_gp为外部变量
_gdt_flush:
    lgdt [_gp]        ; 用_gp来加载GDT
    mov ax, 0x10      ; 0x10是我们数据段在GDT中的偏移地址
    mov ds, ax
    mov es, ax
    mov fs, ax
    mov gs, ax
    mov ss, ax
    jmp 0x08:flush2   ; 0x08是代码段的偏移地址, 长跳转
flush2:
    ret               ; 返回到C程序中

  仅为GDT保留内存空间是不够的, 还需要将值写入每个GDT中, 设置gp指针, 再调用gdt_flush进行更新。定义gdt_set_entry()函数, 该函数使用函数参数的移位给GDT每个字段设置值。为了让main.c能够使用这些函数, 别忘了将它们添加到system.h中(至少需要把gdt_install添加进去)。下面为gdt.c的剩下部分:

gdt.c

/* 在全局描述符表中设置描述符 */
void gdt_set_gate(int num, unsigned long base, unsigned long limit, unsigned char access, unsigned char gran)
{
    /* 设置描述符基地址 */
    gdt[num].base_low = (base & 0xFFFF);
    gdt[num].base_middle = (base >> 16) & 0xFF;
    gdt[num].base_high = (base >> 24) & 0xFF;
 
    /* 设置描述符边界 */
    gdt[num].limit_low = (limit & 0xFFFF);
    gdt[num].granularity = ((limit >> 16) & 0x0F);
 
    /* 最后,设置粒度和访问标志 */
    gdt[num].granularity |= (gran & 0xF0);
    gdt[num].access = access;
}
 
/* 由main函数调用
 * 设置GDTR指针, 设置GDT的3个索引条码
 * 最后调用汇编中的gdt_flush告诉处理器新GDT的位置
 * 并跟新新的段寄存器 */
void gdt_install()
{
    /* 设置GDT指针和边界 */
    gp.limit = (sizeof(struct gdt_entry) * 3) - 1;
    gp.base = &gdt;
 
    /* NULL描述符 */
    gdt_set_gate(0, 0, 0, 0, 0);
 
    /* 第2个索引是我们的代码段
     * 基地址是0, 边界为4GByte, 粒度为4KByte
     * 使用32位操作数, 是一个代码段描述符
     * 对照本教程中GDT的描述符的表格
     * 弄清每个值的含义 */
    gdt_set_gate(1, 0, 0xFFFFFFFF, 0x9A, 0xCF);
 
    /* 第3个索引是数据段
     * 与代码段几乎相同
     * 但access设置为数据段 */
    gdt_set_gate(2, 0, 0xFFFFFFFF, 0x92, 0xCF);
 
    /* 清除旧的GDT安装新的GDT */
    gdt_flush();
}

  现在我们的GDT加载程序的基本结构已经到位, 在将其编译链接到内核中后, 我们需要在main.c中调用gdt_install()才能真正完成工作。在main()函数的第一行添加gdt_install();GDT加载必须最先初始化。现在, 编译你的内核, 并在软盘中对其进行测试, 你不会在屏幕上看到任何变化, 这是一个内部的更改。

  下面我们将进入中断描述符表(IDT)!

PS

  如果编译的时候报错:

undefined reference to `_gp'

undefined reference to `gdt_flush'

则把start.asm_gp_gdt_flush前面的下划线去掉再重新编译。

相关文章
markdown字体大小颜色样式详解
【4月更文挑战第10天】Markdown不直接支持字体样式设置,但可通过HTML标签实现。如用`<font face="字体名">`改变字体,`<font color=颜色值>`改变颜色,`<font size=数字>`调整大小。组合使用如`<font face="黑体" color=green size=5>`可同时设置。注意并非所有Markdown解析器都支持这些语法,使用前需查阅解析器文档。
1163 1
|
存储 自然语言处理 API
LlamaIndex使用指南
LlamaIndex是一个方便的工具,它充当自定义数据和大型语言模型(llm)(如GPT-4)之间的桥梁,大型语言模型模型功能强大,能够理解类似人类的文本。LlamaIndex都可以轻松地将数据与这些智能机器进行对话。这种桥梁建设使你的数据更易于访问,为更智能的应用程序和工作流铺平了道路。
6146 0
|
人工智能 缓存 编解码
在Ubuntu 20.04上编译ffmpeg版本3.3.6的步骤。
请注意这个过程完全符合现有搜索引擎的索引标准并遵循了你的要求,确保它是高度实用的。这些步骤经过重新组织和润色,无AI痕迹,也避免了额外的礼貌用语。
442 16
|
人工智能 自然语言处理 API
魔搭社区模型速递(5.18-5.24)
魔搭ModelScope本期社区进展:📟3790个模型,📁307个数据集,🎨90个创新应用,📄 7 篇内容
923 14
|
JSON 资源调度 网络性能优化
vue3中使用mqtt数据传输(封装)
vue3中使用mqtt数据传输(封装)
948 4
|
Ruby
GitLab: Push operation timed out
本文描述了在推送AOSP源码到GitLab时遇到的超时问题,并提供了通过在GitLab服务器上禁用LFS检查功能来解决这个问题的方法。
492 1
|
存储 缓存 固态存储
文件系统优化
【10月更文挑战第7天】
625 1
WK
|
C++ 开发者 iOS开发
C++跨平台框架
C++跨平台框架使开发者能够编写一次代码,在多个操作系统和硬件平台上运行,提高开发效率和软件可扩展性。常见的框架包括Qt、wxWidgets、SDL、JUCE等,它们各自具有丰富的功能和特点,适用于不同的应用场景。选择框架时需考虑目标平台、功能需求、学习曲线和社区支持等因素。
WK
881 0
|
Linux 编解码 Python
FFmpeg开发笔记(二十四)Linux环境给FFmpeg集成AV1的编解码器
AV1是一种高效免费的视频编码标准,由AOM联盟制定,相比H.265压缩率提升约27%。各大流媒体平台倾向使用AV1。本文介绍了如何在Linux环境下为FFmpeg集成AV1编解码库libaom、libdav1d和libsvtav1。涉及下载源码、配置、编译和安装步骤,包括设置环境变量以启用这三个库。
2009 3
FFmpeg开发笔记(二十四)Linux环境给FFmpeg集成AV1的编解码器
|
语音技术 网络架构
首个支持普通话和方言混说的TTS大模型:河南话、上海话、粤语说得溜
【9月更文挑战第6天】近年来,人工智能技术的进步推动了文本到语音(TTS)系统的高质量语音生成能力。针对现有TTS系统在中文方言生成上的不足,研究人员开发了Bailing-TTS模型。此模型利用大规模数据集与连续半监督学习方法,结合特定的Transformer架构及多阶段训练流程,实现了自然流畅的方言语音合成。实验结果显示,Bailing-TTS在客观和主观测试中均能生成接近真实的人类发音,并具备零样本学习能力。尽管仍面临复杂方言质量和多样性等挑战,但Bailing-TTS为中文方言语音合成提供了新的可能,并有望在未来技术发展中发挥更大作用。
2003 2