OS专题-开始编写GDT

OS专题-开始编写GDT

还记得我们之前在了解16位实模式下的内存编制说到的段寻址嘛?在32位模式下,分段方式不同了,因为位数变大了,容量也就变大了,我们就要重新规划寻址方式了。现在偏移量对应的是GDT中的段描述符(SD)的索引,段描述符定义了基地址(32位)、大小(20位)以及一些标志,如只读、权限等。

切换到32位保护模式,我们就不能再使用BIOS。这意味着32位操作系统必须为机器的所有硬件(例如键盘、屏幕、磁盘驱动器、鼠标等)提供自己的驱动程序。实际上,32位保护模式操作系统可以临时切换回16位模式,从而使用BIOS,但这种技术可能会带来更多麻烦,特别是在性能方面。因为要来回的切换,损耗一定是很高的。

在切换到保护模式时,我们将遇到的第一个问题是知道如何在屏幕上打印消息,以便我们可以看到正在发生的事情。以前我们曾要求BIOS在屏幕上打印一个ASCII字符,但这如何导致在计算机屏幕的适当位置高亮显示适当的像素?现在,只要知道显示设备可以以文本模式和图形模式中的一种模式被配置成几种分辨率中的一种,并且在屏幕上显示的是特定范围的存储器的可视表示就足够了。因此,为了操作屏幕,我们必须操作它在当前模式下使用的特定内存范围。显示设备的显示是根据内存映射来的,因为它以这种方式工作。当然这个我们已经在上一章解决了,但我们仍然需要了解一下它的原理。

当大多数计算机启动时,尽管它们实际上可能具有更先进的图形硬件,但它们在具有80x25个字符的简单视频图形阵列(VGA)彩色文本模式下开始。在文本模式下,程序员不需要渲染单独的像素来描述特定字符,因为简单的字体已经不需要了,它们已经存在于VGA显示设备的内部存储器中。屏幕的每个字符单元由内存中的两个字节表示:

  • 第一个字节是要显示的字符的ASCII码。
  • 第二个字节编码字符的属性,例如前景和背景颜色以及字符是否应该闪烁。

所以,如果我们想要在屏幕上显示一个字符,则需要将其ASCII代码和属性设置在当前VGA模式的正确内存地址,通常是地址0xb8000。

我们之前实现的打印函数的缺点是它总是将字符串打印到屏幕的左上角,所以每次打印将覆盖之前的消息而不是滚动。我们可以花时间增加这个汇编例程的复杂性,但是我们不要把事情弄得太困难,因为在掌握切换到保护模式之后,我们将很快启动用更高级别的语言编写的代码,在那里我们可以轻松地完成这些工作。

理解GDT

在深入讨论细节之前我们必须理解这个GDT的要点,它是开启保护模式的基础。我们已经知道物理地址的转换是根据GDT中特定段描述符(SD)的索引计算的。段描述符是一个8字节结构,它定义了以下保护模式管理的属性:

  • 基址(32位),表示段在物理内存中的开始位置
  • 数据段限制(20位),表示数据段的大小
  • 各种标志,它们影响CPU解释段的方式,例如在段中运行的代码的特权级别,或者它是只读的还是只写的。

上图显示了段描述符的实际结构。Intel将最简单可行的段寄存器配置描述为基本AT模型,因此定义了两个重叠的段,覆盖了全部4GB的可寻址内存,一个用于代码,另一个用于数据。在此模型中,这两个段重叠的事实意味着不会尝试保护一个段不受另一个段的影响,也不会尝试使用虚拟内存的分页功能。早期保持简单是有好处的,特别是因为稍后一旦我们引导到更高级别的语言,我们可能会更容易地改变段描述符。除了代码和数据段,CPU还要求GDT中的第一个条目故意是无效的空描述符(即8个零字节的结构)。空描述符是一种简单的机制,用于捕获在访问地址之前忘记设置特定段寄存器的错误,如果我们将一些段寄存器设置为0x0,但在切换到保护模式后忘记将它们更新为适当的段描述符,则很容易做到这一点。如果尝试使用空描述符进行寻址,则CPU将引发异常,这本质上是一个中断,尽管与概念并不太相似,但不要将其与Java等高级语言中的异常混淆。

我们要实现的GDT有如下的配置:

  • Base: 0x0
  • Limit: 0xfffff
  • Present: 1 用于确定段存在于内存还是虚拟内存中
  • Privilige: 0 0代表最高权限
  • Descriptor type: 1代表代码或数据, 0代表陷入指令
  • Type:
    • Code: 1 代表这是一个代码段
    • Conforming: 0 意味着具有较低权限的段中的代码不能调用该段中的代码——这是内存保护的关键
    • Readable: 1 1表示可读,0表示只执行。可读允许我们读取代码中定义的常量。
    • Accessed: 0 这通常用于调试和虚拟内存技术,因为CPU在访问段时会设置该位
  • Other flags
    • Granularity: 1, 如果设置了该值,这将我们的Limit乘以4K(即161616),因此我们的0xfffff将变成0xfffff000(即向左移动3个十六进制数字),允许我们的段跨越4Gb内存
    • 32-bit default: 1, 值为1时我们的段将保存32位代码,值为0时我们段将保存16位代码。这实际上为操作设置了默认的数据单元大小(例如,push 0x4将扩展到32位数等)
    • 64-bit code segment: 0, 在32位处理器上未使用
    • AVL: 0, 用户保留字段,我们可以将其设置为我们自己使用(例如调试),但我们一般不会使用它

之前使用的是一个简单的AT模型,但当具有两个重叠的代码段和数据段时,此时数据段是相同的,但类型标志有所不同:

  • Code: 0 代表数据
  • Expand down: 0 . 这允许段向下扩展
  • Writable: 1. 这允许数据段被写入,否则它将被只读
  • Accessed: 0 这通常用于调试和虚拟内存技术,因为CPU在访问段时会设置该位

陷入指令(Traps),又叫做自陷指令或访管指令,出现在计算机操作系统中,用于实现在用户态下运行的进程调用操作系统内核程序,即当运行的用户进程或系统实用进程欲请求操作系统内核为其服务时,可以安排执行一条陷入指令引起一次特殊异常。

我们根据这些信息编写我们自己的GDT,在lib文件夹中新建32_gdt.asm文件,代码如下:

;lib/32_gdt.asm
gdt_start: ; 不要删除这些标签,他们用来流程控制和计算大小
    ; GDT的开头时8字节的空白数据
    dd 0x0 ; 4 byte
    dd 0x0 ; 4 byte

; GDT代码段部分 基数= 0x00000000,长度= 0xfffff
gdt_code:
    dw 0xffff    ; 段长, bits 0-15
    dw 0x0       ; 段基址, bits 0-15
    db 0x0       ; 段基址, bits 16-23
    db 10011010b ; 标志 (8 bits)
    db 11001111b ; 标志 (4 bits) + 段长, bits 16-19
    db 0x0       ; 段基址, bits 24-31

; GDT的数据段部分。与代码段相同
gdt_data:
    dw 0xffff
    dw 0x0
    db 0x0
    db 10010010b
    db 11001111b
    db 0x0

gdt_end:

; GDT描述符
gdt_descriptor:
    dw gdt_end - gdt_start - 1 ; 大小 (16 bit), 总是比它的真实尺寸小1
    dd gdt_start ; 地址 (32 bit)

; 常量定义
CODE_SEG equ gdt_code - gdt_start
DATA_SEG equ gdt_data - gdt_start

OS专题-开始编写GDT
https://blog.cikaros.top/doc/cb82816a.html
作者
Cikaros
发布于
2022年9月23日
许可协议