参考文章《操作系统真象还原》第四章 ---- 剑指Loader 刃刺GDT 开启新纪元保护模式 解放32位一、为什么需要保护模式?前3章一直在实模式下开发 MBR/Loader而CPU的16位实模式具有以下缺陷寻址空间极小只有 20 位地址线最大 1MB 内存0 ~ 0xFFFFF无任何保护操作系统与用户程序同特权级Ring0程序可随意修改任何内存、段寄存器逻辑地址 物理地址直接访问硬件极易崩溃、互相破坏分段弱每个段最大 64KB跨段麻烦无多任务、无虚拟内存基础相比之下CPU的32位保护模式则具有以下优势32 位最大4GB 寻址硬件级内存保护、特权级Ring0~3、权限检查段机制重构 支持分页这是后面第5章将会涉及到的概念为多任务、虚拟内存、用户 / 内核隔离打下硬件基础二、保护模式下的核心概念GDT、段描述符、选择子1、GDT(Global Descriptor Table) ----全局描述符表保护模式下所有段的 “注册表”放在内存中是一张数组表每个表项叫段描述符8 字节描述一个段的基址、界限、属性、权限CPU 用GDTR寄存器48 位16 位表界限 32 位基址指向 GDT第 0 项NULL 描述符必须保留、不可用防止选择子未初始化时乱访问2. 段描述符8 字节64位关键字段段描述符的格式具体如下所示每位含义段基址32 位段在物理内存的起始地址拆分成3处高32位中的31~24位、7~0位低32位中得31~16位段界限20 位段的大小单位字节 / 4KB 页由 G 位决定P 位段是否在物理内存中PresentDPL2 位段特权级0 内核3 用户S 位系统段 / 数据 代码段TYPE4 位段类型代码 / 数据 / 栈、可读 / 可写 / 可执行、向上 / 向下扩展G 位粒度0 字节14KBD/B 位默认操作数宽度016 位132 位3. 选择子Selector16 位在保护模式下段寄存器CS/DS/ES/SS等寄存器不再存段基址而是存 “选择子”选择子的格式具体如下所示索引13 位在 GDT/LDT 中的下标找哪个段描述符TI 位0 查 GDT1 查 LDT本章只用 GDTRPL2 位请求特权级请求访问的权限作用用选择子 → 对GDT进行查找 → 得到段描述符进而得到段的基址 界限 权限→ 再做地址转换与检查4、段描述符缓冲寄存器隐藏每个段寄存器对应一个不可见的 64 位隐藏寄存器加载选择子时硬件自动查 GDT、做权限检查把段描述符完整装入缓冲寄存器后续访存直接用缓冲寄存器里的基址、界限、属性不用再查内存 GDT三、代码实现---修改mbr.s因为我们要正式编写loader.S了而书上写着的loader.S的实际大小是要大于512字节的仅仅1个扇区无法完全装下loader程序所以我们需要修改一下mbr.S读取扇区的个数修改后重新写入mbr.binnasm -I include/ -o mbr.bin mbr.S 在include/这个路径里面去寻找头文件然后进行编译 dd if“你的mbr.bin文件的位置” of“你所创建的虚拟硬盘的位置” bs512 count1 convnotrunc四、代码实现---更新配置文件boot.inc更新后代码如下所示LOADER_START_SECTOR equ 2 ;把 LOADER_START_SECTOR 这个符号直接替换成数值 2 ;表明Loader 程序在磁盘上的起始扇区号是2扇区 ;扇区从0开始计数第0扇区是MBR第1扇区是1所以2就是第3个扇区 LOADER_BASE_ADDR equ 0x600 ;Loader 程序被加载到内存的起始地址是 0x600 ;MBR 引导程序在调用 BIOS 中断读取磁盘时会把 0x600 作为目标内存地址把 Loader 读到这里然后跳过去执行 ;对gdt描述符属性进行定义高32位 ;下划线没有语法作用只是为了让人看清哪一位是哪一位 DESC_G_4K equ 1_00000000000000000000000b ;第23位G位表示4K或者1字节位,将此位设置为1则表明此时粒度的大小则为4kB DESC_D_32 equ 1_0000000000000000000000b ;第22位D/B位将此位设置为1表明此时使用32位保护模式 DESC_L equ 0_000000000000000000000b ;第21位L位将此位设置为0表明此时所写的代码都是32位的代码 DESC_AVL equ 0_00000000000000000000b ;第20位软件可用位暂时用不到它设置为0 DESC_LIMIT_CODE2 equ 1111_0000000000000000b ;第16-19位段界限的高四位 全部初始化为1 因为最大段界限*粒度必须等于 ;0xffffffff即4GB的大小 段界限的后16位是在低32位中的到时由我们自行写入 DESC_LIMIT_DATA2 equ DESC_LIMIT_CODE2 ;相同的值 数据段与代码段段界限相同 DESC_LIMIT_VIDEO2 equ 0000_0000000000000000b ;这里将显存段的段界限高4位填0000显存不需要4GB那么大只需一小块 DESC_P equ 1_000000000000000b ;第15位P位设置为1用于判断段是否存在于内存 ;1 段存在,必须是 1否则 CPU 抛异常 DESC_DPL_0 equ 00_0000000000000b ;第13-14位,权限位 DESC_DPL_1 equ 01_0000000000000b ;0为操作系统内核权力最高 3为用户程序权力最低 DESC_DPL_2 equ 10_0000000000000b DESC_DPL_3 equ 11_0000000000000b DESC_S_sys equ 0_000000000000b ;第12位为0 则表示系统段,为1则表示数据段或代码段 DESC_S_CODE equ 1_000000000000b ;判断是否为系统段还是数据段 DESC_S_DATA equ DESC_S_CODE ;代码段、数据段都填 1 ;第9-11位表示该段类型 1000 可执行 不允许可读 已访问位0 ;当第12位为1时若11位为1则为代码段若11位为0则为数据段 DESC_TYPE_CODE equ 1000_00000000b ;1000表示代码段、可执行、不可读、未访问 DESC_TYPE_DATA equ 0010_00000000b ;0010表示数据段、可读写 ;拼出 GDT 代码段描述符的 “高32位”将其特征属性放在一起高32位全是属性位用宏定义拼写较省力 ;低32位根据需要要写什么就写什么格式为段基址段界限 ;DESC_G_4K--4KB粒度 ;DESC_D_32--32位保护模式 ;DESC_L--32位代码段 ;DESC_LIMIT_CODE2--段界限的最高4位1111段最大能访问 4GB ;DESC_P--段在内存中 ;DESC_DPL_0--最高权限内核 ;DESC_S_CODE--代码/数据段 ;DESC_TYPE_CODE--若是代码段则是1000即可执行 ;0x00--段基址是0x00段界限 DESC_CODE_HIGH4 equ (0x0024) DESC_G_4K DESC_D_32 \ DESC_L DESC_AVL DESC_LIMIT_CODE2 \ DESC_P DESC_DPL_0 DESC_S_CODE DESC_TYPE_CODE 0X00 ;拼出 GDT 数据段描述符的 “高32位”将其特征属性放在一起 DESC_DATA_HIGH4 equ (0x0024) DESC_G_4K DESC_D_32 \ DESC_L DESC_AVL DESC_LIMIT_DATA2 \ DESC_P DESC_DPL_0 DESC_S_DATA DESC_TYPE_DATA 0X00 ;拼出 GDT 显存段描述符的 “高32位”将其特征属性放在一起 ;显存基址是 0xB8000高字节是 0x0B DESC_VIDEO_HIGH4 equ (0x0024) DESC_G_4K DESC_D_32 \ DESC_L DESC_AVL DESC_LIMIT_VIDEO2 \ DESC_P DESC_DPL_0 DESC_S_DATA DESC_TYPE_DATA 0X0B ; ;-------------------- 选择子属性 RPL0 equ 00b ;4个访问段时的特权级与前面段本身的特权级是两个概念注意不要混淆 RPL1 equ 01b RPL2 equ 10b RPL3 equ 11b TI_GDT equ 000b ;TI 1 表示用 LDT局部表,0 表示用 GDT全局表 TI_LDT equ 100b四、代码实现---更新loader.s代码如下%include boot.inc SECTION loader vstartLOADER_BASE_ADDR ;告诉汇编器这段代码的运行地址是 0x600 LOADER_STACK_TOP equ LOADER_BASE_ADDR ;栈顶指针 0x600,实模式和保护模式都需要栈这里同样设置为0x600 jmp loader_start ;下面存放数据段 构建gdt 跳跃到下面的代码区 ;--------------------------------------构建全局描述附表 ;db define byte,dw define word,dd define dword ;一个段描述符是8个字节 GDT_BASE : dd 0x00000000 ;Intel 规定GDT 第 0 个段描述符不可用必须是空描述符必须全 0 dd 0x00000000 CODE_DESC : dd 0x0000FFFF ;代码段描述符权限内核级、可执行、可读 dd DESC_CODE_HIGH4 ;FFFF(4GB)是与其他的几部分相连接 形成0XFFFFF段界限0x00000000是基地址 ;下面这个dd就是我们在boot.inc中组合出的段描述符的高32位 DATA_STACK_DESC : dd 0x0000FFFF ;数据段描述符权限内核级、可读写 dd DESC_DATA_HIGH4 ;下面这个dd就是我们在boot.inc中组合出的段描述符的高32位 ;显卡段描述符作用保护模式下往屏幕写字 VIDEO_DESC : dd 0x80000007 ;0xB8000 到0xBFFFF为文字模式显示内存 B只能在boot.inc中出现定义了 此处不够空间了 8000刚好够 dd DESC_VIDEO_HIGH4 ;0x0007 (bFFFF-b8000)/4k 0x7 GDT_SIZE equ $ - GDT_BASE ;GDT 总大小当前位置减去GDT_BASE的地址 GDT_LIMIT equ GDT_SIZE - 1 ;GDT 界限必须是大小 - 1 times 60 dq 0 ;预留60个 8字节型 描述符以后可以继续添加段 ;选择子描述符索引高13位 TI中1位 RPL低2位 ;索引按照我们前面的定义代码段 索引 1; 数据段 索引 2; 显卡段 索引 3 ;TI去 GDT 找 还是 LDT 找我们只用 GDT → 0 ;RPL权限内核 0用户 3 ;拼出一个 16 位的段选择子让保护模式下的段寄存器CS/DS/ES使用 SELECTOR_CODE equ (0X00013) TI_GDT RPL0 ;左移3是因为要把低3位空出来给TIRPL SELECTOR_DATA equ (0X00023) TI_GDT RPL0 SELECTOR_VIDEO equ (0X00033) TI_GDT RPL0 ;gdt指针48位格式 低位16位界限 高位32位起始地址 gdt_ptr dw GDT_LIMIT dd GDT_BASE loadermsg db welcome to loader zone! ;loadermsg 加载区显示字符 ;------------------------------------------------------------- ;-------------执行loader函数的具体代码 loader_start: mov sp,LOADER_BASE_ADDR ;将loader程序的地址加载进栈ss:sp 0x0000:0x600 mov bp,loadermsg ;把字符串的偏移地址放到 bp 寄存器BIOS 中断 0x10 的规定字符串地址 ES:BP mov cx,22 ;设置字符串长度22 mov ax,cs ;字符串在代码段里 mov es,ax ;通过ax 赋值给es mov ax,0x1301 ;ah 13 表示调用 13 号功能打印字符串 al 0x1表示打印后光标移动到字符串末尾 mov bx,0x001f ;页码属性 可以不管 mov dx,0x1800 ;dh 0x18 24 意思是最后一行 0列开始 int 0x10 ; --------------------------------- 设置进入保护模式 ----------------------------- ; 1 打开A20 gate ; 2 加载gdt ; 3 将cr0 的 pe位置1 in al,0x92 ;端口号0x92是A20控制端口 or al,0000_0010b ;第1位变成1,表示打开A20控制端口 out 0x92,al lgdt [gdt_ptr] ;加载 GDT 全局描述符表,lgdtload gdt ,把gdt_ptr里的GDT基地址 GDT界限加载到 CPU 的 GDTR 寄存器 mov eax,cr0 ;cr0寄存器第0位设置位1代表开启保护模式 or eax,0x00000001 mov cr0,eax ;-------------------------------- 已经打开保护模式 --------------------------------------- jmp dword SELECTOR_CODE:p_mode_start ;刷新流水线开始执行 32 位指令p_mode_start32 位代码入口的偏移 [bits 32] ;告诉汇编器下面的代码按照 32 位模式编译 p_mode_start: ;初始化数据段、附加段、栈段 mov ax,SELECTOR_DATA ;保护模式下段寄存器不能直接赋值必须装选择子,这里装的选择子是数据段 mov ds,ax ;把 DS、ES、SS 都指向数据段 mov es,ax mov ss,ax mov esp,LOADER_STACK_TOP ;设置32 位栈指针栈顶 0x600 mov ax,SELECTOR_VIDEO ;保护模式下段寄存器不能直接赋值必须装选择子,这里装的选择子是显存段 mov gs,ax ;把 GS 指向显存段 mov byte [gs:160],P ;通过 GS 寄存器直接操作屏幕即在保护模式下进行在屏幕上写字符 jmp $ ;在当前位置进行死循环进行编译把库目录路径链接给MBR.S生成二进制文件并将二进制文件写入虚拟硬盘仍然是在bochs下打开终端 在include/这个路径里面去寻找头文件然后进行编译 nasm -I include/ -o mbr.bin mbr.s 因为现在编写的loader.s的大小已超过1个扇区512字节所以在写入loader.s时要留出超过1个扇区的空间这里选择count2 dd if“你的mbr.bin文件的位置” of“你所创建的虚拟硬盘的位置” bs512 count2 seek2 convnotrunc五、验证进入bochs--选择6进行仿真--输入c继续运行输入 ctrlc中断运行然后输入命令info gdt查看全局描述符表的信息发现全局描述符表中各类段的信息都被写入了验证成功