Lab1:练习3——分析bootloader进入保护模式的过程
练习三:分析bootloader进入保护模式的过程。
1.题目要求
BIOS将通过读取硬盘主引导扇区到内存,并转跳到对应内存中的位置执行bootloader。请分析bootloader是如何完成从实模式进入保护模式的。
提示:需要阅读小节“保护模式和分段机制”和lab1/boot/bootasm.S源码,了解如何从实模式切换到保护模式,需要了解:
- 为何开启A20,以及如何开启A20
- 如何初始化GDT表
- 如何使能和进入保护模式
2.预备知识
bootloader实际上就是完成了一些最基本的功能:
把80386的保护模式打开,使得现在软件进入了32位的寻址空间,就是寻址方式发生了改变。为了能够做好这件事情,它还需要开启A20、初始化GDT表(全局描述符表)、使能和进入保护模式
执行完ljmp之后,计算机会进入32位保护模式
(1)汇编
Ucore中用到的是AT&T格式的汇编,在 AT&T 汇编格式中
(2)bootloader的作用
1、关闭中断
2、A20 使能
3、全局描述符表初始化
4、保护模式启动
5、设置段寄存器(长跳转更新CS,根据设置好的段选择子更新其他段寄存器)
6、设置堆栈,esp 0x700 ebp 0
7、进入bootmain后读取内核映像到内存,检查是否合法,并启动操作系统,控制权交给它
(3) 实模式
CPU复位(reset)或加电(power on)的时候以实模式启动,处理器以实模式工作。在实模式下,内存寻址方式和8086相同,由16位段寄存器的内容乘以16(10H)当做段基地址,加上16位偏移地址形成20位的物理地址,最大寻址空间1MB,最大分段64KB。可以使用32位指令。32位的x86 CPU用做高速的8086。在实模式下,所有的段都是可以读、写和可执行的。
实模式将整个物理内存看成分段的区域,程序代码和数据位于不同区域,操作系统和用户程序并没有区别对待,而且每一个指针都是指向实际的物理地址。这样,用户程序的一个指针如果指向了操作系统区域或其他用户程序区域,并修改了内容,那么其后果就很可能是灾难性的。通过修改A20地址线可以完成从实模式到保护模式的转换。
(4)保护模式
实模式下,程序地址为真实的物理地址,可以访问任意地址空间,这样不同进程可能访问到其它进程程序,造成严重错误。而保护模式下,程序地址为虚拟地址,然后由OS系统管理内存访问权限,这样每个进程只能访问分配给自己的物理内存空间,保证了程序的安全性。例如Linux系统地址访问采用分页机制,在加载程序时,由OS分配的进程可以访问的物理页空间,并设置了页目录项和页表项,才能保证程序正常运行。这样程序运行时地址间接地由OS进行管理,防止进程之间互相影响,全部由OS稳定性保证。
(5)CR0
CR0是控制寄存器,其中包含了6个预定义标志,0位是保护允许位PE(Protedted Enable),用于启动保护模式。如果PE位置1,则保护模式启动,如果PE=0,则在实模式下运行。
关于CR0及其他控制寄存器的详细内容可以参考以下链接:https://blog.csdn.net/wyt4455/article/details/8691500
3.实验步骤
(一)代码分析
1、bootasm.S的代码
2、<asm.h>的内容
(二)问题解答
1.1为何开启A20,以及如何开启A20
首先说明一点,这是一个历史遗留问题。
1981年8月,IBM公司最初推出的个人计算机IBM PC使用的CPU是Inter 8088.在该微机中地址线只有20根。在当时内存RAM只有几百KB或不到1MB时,20根地址线已经足够用来寻址这些 内存。其所能寻址的最高地址是0xffff,
也就是0x10ffef。对于超出0x100000(1MB)的寻址地址将默认地环绕到0xffef。当IBM公司与1985年引入AT机时,使用的是Inter 80286 CPU,具有24根地址线,最高可寻址16MB,并且有一个与8088那样实现地址寻址的环绕。
但是当时已经有一些程序是利用这种环绕机制进行工作的。为了实现完全的兼容性,IBM公司发明了使用一个开关来开启或禁止0x100000地址比特位。由于当时的8042键盘控制器上恰好有空闲的端口引脚(输出端口P2,引脚P21),
于是便使用了该引脚来作为与门控制这个地址比特位。该信号即被称为A20。如果它为零,则比特20及以上地址都被清除。从而实现了兼容性。
当A20地址线控制禁止时,程序就像运行在8086上,1MB以上的地址是不可访问的,只能访问奇数MB的不连续的地址。为了使能所有地址位的寻址能力,必须向键盘控制器8082发送一个命令,键盘控制器8042会将A20线置于高电位,使全部32条地址线可用,实现访问4GB内存。
1.2打开A20 Gate的具体步骤(参考bootasm.S)
控制 A20 gate 的方法有 3 种:
1.804x 键盘控制器法
2.Fast A20 法
3.BIOS 中断法
ucore实验中用了第一种 804x 键盘控制器法,这也是最古老且效率最慢的一种。
由于在机器启动时,默认条件下,A20地址线是禁止的,所以操作系统必须使用适当的方法来开启它。
- 等待8042 Input buffer为空;
- 发送Write 8042 Output Port (P2)命令到8042 Input buffer;
- 等待8042 Input buffer为空;
- 将8042 Output Port(P2)得到字节的第2位置1,然后写入8042 Input buffer
打开A20 Gate的代码为:
第一步是向 804x 键盘控制器的 0x64 端口发送命令。这里传送的命令是 0xd1,这个命令的意思是要向键盘控制器的 P2 写入数据。这就是 seta20.1 代码段所做的工作。
第二步就是向键盘控制器的 P2 端口写数据了。写数据的方法是把数据通过键盘控制器的 0x60 端口写进去。写入的数据是 0xdf,因为 A20 gate 就包含在键盘控制器的 P2 端口中,随着 0xdf 的写入,A20 gate 就被打开了。
接下来要做的就是进入“保护模式”了。
2.1什么是GDT表
GDT全称是Global Descriptor Table,中文名称叫“全局描述符表”,想要在“保护模式”下对内存进行寻址就先要有 GDT。GDT 表里的每一项叫做“段描述符”,用来记录每个内存分段的一些属性信息,每个“段描述符”占 8 字节。
在保护模式下,我们通过设置GDT将内存空间被分割为了一个又一个的段(这些段是可以重叠的),这样我们就能实现不同的程序访问不同的内存空间。这和实模式下的寻址方式是不同的, 在实模式下我们只能使用address = segment << 4 | offset的方式进行寻址(虽然也是segment + offset的,但在实模式下我们并不会真正的进行分段)。在这种情况下,任何程序都能访问整个1MB的空间。而在保护模式下,通过分段的方式,程序并不能访问整个内存空间
2.2初始化GDT表
为了使分段存储管理机制正常运行,需要建立好段描述符和段描述符表,全局描述符表是一个保存多个段描述符的“数组”,其起始地址保存在全局描述符表寄存器GDTR中。GDTR长48位,其中高32位为基地址,低16位为段界限。这里只需要载入已经静态存储在引导区的GDT表和其描述符到GDTR寄存器:
gdtdesc 和 gdt 一起放在了 bootasm.S 文件的最底部
48 位传给了 GDTR 寄存器,到此 GDT 就准备好了
3.1如何使能和进入保护模式
3.1.1修改CR0寄存器的PE值
如同 A20 gate 这个开关负责打开 1MB 以上内存寻址一样,想要进入“保护模式”我们也需要打开一个开关,这个开关叫“控制寄存器”,x86 的控制寄存器一共有 4 个分别是 CR0、CR1、CR2、CR3(这四个寄存器都是 32 位的),而控制进入“保护模式”的开关在 CR0 上。
CR0中包含了6个预定义标志,0位是保护允许位PE(Protedted Enable),用于启动保护模式,如果PE位置1,则保护模式启动,如果PE=0,则在实模式下运行。
CR0 上和保护模式有关的位,如图所示:
打开保护模式的代码为:
因为我们无法直接操作 CR0,所以我们首先要用一个通用寄存器来保存当前 CR0 寄存器的值,这里第一行就是用通用寄存器 eax 来保存 cr0 寄存器的值;
然后 CR0_PE 这个宏的定义在 mmu.h 文件中,是个数值 0x00000001,将这个数值与 eax 中的 cr0 寄存器的值做“或”运算后,就保证将 cr0 的第 0 位设置成了 1 即 PE = 1 保证打开了保护模式的开关。
而 cr0 的第 31 位 PG = 0 表示我们只使用分段式,不使用分页,这时再将新的计算后的 eax 寄存器中的值写回到 cr0 寄存器中就完成了到保护模式的切换。
3.1.2通过长跳转,更新CS寄存器的基地址
其中protcseg是一个标号(标号的用途在本文中的实验相关部分已说明)
由于已经使能了保护模式,所以这里要使用逻辑地址,而不是之前实模式的地址了
这里还要注意PROT_MODE_CSEG和PROT_MODE_DSEG,这两者分别定义为0x8和0x10,表示代码段和数据段的选择子。
根据段选择子的格式定义,0x8就翻译成:
INDEX TI CPL
0000 0000 1000
INDEX代表GDT中的索引,TI代表使用GDTR中的GDT, CPL代表处于特权级。
3.1.3设置段寄存器,并建立堆栈
注意这里建立堆栈,ebp寄存器按理来说是栈帧的,但是这里并不需要把它设置为0x7c00,因为这里0x7c00是栈的最高地址,它上面没有有效内容,而之后因为调用,ebp会被设置为被调用的那个函数的栈的起始地址,这里就不用管它了。
3.1.4转到保护模式完成,进入boot主方法
4 总结
Bootload的启动过程可以概括如下:
首先,BIOS将第一块扇区(存着bootloader)读到内存中物理地址为0x7c00的位置,同时段寄存器CS值为0x0000,IP值为0x7c00,之后开始执行bootloader程序。CLI屏蔽中断(屏蔽所有的中断:为中断提供服务通常是操作系统设备驱动程序的责任,因此在bootloader的执行全过程中可以不必响应任何中断,中断屏蔽是通过写CPU提供的中断屏蔽寄存器来完成的);CLD使DF复位,即DF=0,通过执行cld指令可以控制方向标志DF,决定内存地址是增大(DF=0,向高地址增加)还是减小(DF=1,向地地址减小)。设置寄存器 ax,ds,es,ss寄存器值为0;A20门被关闭,高于1MB的地址都默认回卷到0,所以要激活A20,给8042发命令激活A20,8042有两个IO端口:0x60和0x64, 激活流程: 发送0xd1命令到0x64端口 --> 发送0xdf到0x60,打开A20门。从实模式转换到保护模式(实模式将整个物理内存看成一块区域,程序代码和数据位于不同区域,操作系统和用户程序并没有区别对待,而且每一个指针都是指向实际的物理地址,地址就是IP值。这样,用户程序的一个指针如果指向了操作系统区域或其他用户程序区域,并修改了内容,那么其后果就很可能是灾难性的),所以就初始化全局描述符表使得虚拟地址和物理地址匹配可以相互转换;lgdt汇编指令把通过gdt处理后的(asm.h头文件中处理函数)描述符表的起始位置和大小存入gdtr寄存器中;将CR0的第0号位设置为1,进入保护模式;指令跳转由代码段跳到protcseg的起始位置。设置保护模式下数据段寄存器;设置堆栈寄存器并调用bootmain函数;
__EOF__

本文链接:https://www.cnblogs.com/huilinmumu/p/16217843.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角【推荐】一下。您的鼓励是博主的最大动力!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
· AI与.NET技术实操系列(六):基于图像分类模型对图像进行分类