Bootstrap

二、如何从实模式(16位)进入保护模式(32位)

32 位保护模式是现代 CPU 的一种工作模式,它主要解决了 16 位实模式下的一些限制和问题,提供了更强大的内存管理和保护机制,以支持多任务操作系统等更复杂的应用场景。

在 16 位实模式下,处理器的地址总线只有 20 位,可寻址的内存空间最大为 1MB,且所有段寄存器是 16 位,采用分段技术后物理地址最大也只能达到 20 位。而在 32 位保护模式中,地址线扩展到 32 位,可访问的内存空间达到 4GB。

然而,为了兼容 8086 等早期架构,32 位机器中的操作地址的段寄存器(如 CS、DS、ES、SS、FS、GS)仍然是 16 位的,只有通用寄存器(如 AX、BX、CX、DX、SP、BP、SI、DI)扩展成了 32 位。

为了使用 16 位的段寄存器来访问 32 位的内存地址,引入了段描述符和全局描述符表(GDT)的概念。段描述符用来记录段的地址、大小、访问权限等信息,每个段描述符为 64 位。由于无法直接将 64 位的段描述符存放在 16 位的段寄存器中,于是将所有段描述符存放在内存中的全局描述符表中,而段寄存器中存放的则是段选择子,它是段描述符在 GDT 中的索引。通过段选择子可以在 GDT 中找到对应的段描述符,从而获取 32 位的段地址。

全局描述符表(Global Descriptor Table,GDT):进入保护模式以后,数据段、代码段等内存段不再是通过段寄存器获得段基址就可以使用,我们需要把段定义好,并且登记好,全局描述符表便是用来记录这些段信息的数据结构。

GDT表可以类比为一个数组,里面都是结构体,结构体中有相应的字段进行配置,由GDTR寄存器控制

GDT表中的段描述符及其释义

这里截取一些英特尔开发手册原文释义帮助更好理解以下是个人通俗易懂解释

Segment limit和G:决定段的大小,段的大小决定所能访问的内存的大小,intel引入了一个级别的概念,由G(粒度)决定,G的数字越高,一个段单位的字节和增量成比例翻倍

Base address:定义段的字节0在4-GB字节线性地址空间中的位置

Type和S:决定在GDT表中具体是哪个段,数据段还是代码段还是某某段

DPL:权限

P:指示段是否存在于内存中

                                                      也可参考这篇博客,比较详细

                                             全局描述符表-腾讯云开发者社区-腾讯云

下面谈谈如何用代码实现

typedef unsigned char uint8_t;
typedef unsigned short uint16_t;
typedef unsigned int uint32_t;

struct {uint16_t limit_l, base_l, basehl_attr, base_limit;}gdt_table[256] __attribute__((aligned(8))) = {
    [KERNEL_CODE_SEG / 8] = {0xffff, 0x0000, 0x9a00, 0x00cf}, 
    [KERNEL_DATA_SEG / 8] = {0xFFFF, 0x0000, 0x9200, 0x00cf},
};    

定义了一个结构体数组 gdt_table ,并使用指定初始化器进行初始化。
[KERNEL_CODE_SEG / 8] 和 [KERNEL_DATA_SEG / 8] 是指定元素的初始化。其中,每个元素包含四个字段:limit_l(低 16 位的段界限)、base_l(低 16 位的段基址)、basehl_attr(段基址的高 8 位与属性)、base_limit(高 4 位的段界限和其他属性)。

其中__attribute__((aligned(8)))是一个 GCC 编译器的属性声明,用于指定结构体数组gdt_table的对齐方式为 8 字节。这意味着编译器在为gdt_table数组分配内存时,会确保数组元素的起始地址是 8 的倍数。

    cli     //关中断
	lgdt gdt_desc   //让GDTR寄存器识别到GDT表
	mov $1, %eax     //CR0寄存器,最低位PE位置1就会进入保护模式
	lmsw %ax        //加载及机器状态字 
	jmp $KERNEL_CODE_SEG, $_start_32    //将cs段寄存器设置为8

在前一个代码段中已经将cs寄存器对应的gdt表中的内核代码段中的各值都初始化为0 ,所以偏移值决定了在内存中相对与0地址的某一个位置运行

进入保护模式的操作比较固定,依照例程来即可

_start_32:
	//.fill 64*1024, 1, 0x35

	mov $KERNEL_DATA_SEG, %ax  //由上图可知ds寄存器要访问内核数据段,所以将值设置为16
	mov %ax, %ds     //其余寄存器按照默认置0
	mov %ax, %es
	mov %ax, %ss
	mov %ax, %gs
	mov %ax, %fs
	mov $_start, %esp

	jmp .
	
gdt_desc:
	.word (256*8) - 1    //16位设置gdt界限 
	.long gdt_table      //32位伪指令代表起始地址

运行可以看到CR0寄存器的最后一位地址是1,GDT,LDT表都已经加载,说明已经进入保护模式

 

;