MIT6.828操作系统工程实验学习笔记(二)

前言

这篇文章是接上文的内容,依然是对Lab1的记录

如何启动保护模式

要启动保护模式,需要完成以下三个步骤:

  1. 在内存中加载GDT,设置GDTR
  2. 设置CR0寄存器的PE(Protected Enable)位,启用保护模式
  3. 通过一个far jump来重置段寄存器

内存中加载GDT

注意看boot.S的末尾部分

# Bootstrap GDT
.p2align 2                                # force 4 byte alignment
gdt:SEG_NULL                              # null segSEG(STA_X|STA_R, 0x0, 0xffffffff)     # code segSEG(STA_W, 0x0, 0xffffffff)           # data seggdtdesc:.word   0x17                            # sizeof(gdt) - 1.long   gdt                             # address gdt

gdt标签后的内容就是GDT的内容,写在汇编语言里的GDT会被编译到二进制可执行文件中,而这个二进制可执行文件会被原封不动地加载到内存中,从而实现了把GDT加载到内存中这个目的。
gdtdesc标签后的数据是用于设置GDTR寄存器,这里的word是2个字节,long是4个字节,具体可见下图的Intel白皮书对于LGDT指令的描述
在这里插入图片描述
举例来说,如果gdt标签开始的物理地址是0x7D00,由于每个段描述符占8字节,所以gdtdesc的地址是0x7D18
那么0x7D18处往后6字节的数据就是
0x17 0x00 0x00 0x7D 0x00 0x00
(考虑到小端存放)
而从0x7D000x7D18这块内存区域里面存放的就是GDT的内容。
所以只需要把0x7D18,即gdtdesc这个标签的地址传递给LGDT指令,就可以实现GDT的设定,即指定GDT的基地址为0x7D00,并且大小为0x18
所以在进入保护模式的汇编代码里面,有这么一句指令:

  lgdt    gdtdesc

设置CR0寄存器

CR系列寄存器是CPU专用的控制寄存器(Control Register),其中CR0的第0位是PE位,当这一位是1的时候表示CPU处于保护模式。
所以才有下面这段汇编代码:

  movl    %cr0, %eaxorl     $CR0_PE_ON, %eaxmovl    %eax, %cr0

这里面常量CR0_PE_ON=0x1

far jump重置段寄存器

寻址问题

在设置完CR0寄存器之后,CPU就已经在保护模式运行了,但是这个时候CS段寄存器仍然是之前的值,这个时候就有个问题了:观察下面这段代码,为什么CPU依然能够寻址到并能执行最下面那条ljmp指令呢?

  # Switch from real to protected mode, using a bootstrap GDT# and segment translation that makes virtual addresses # identical to their physical addresses, so that the # effective memory map does not change during the switch.lgdt    gdtdescmovl    %cr0, %eaxorl     $CR0_PE_ON, %eaxmovl    %eax, %cr0# Jump to next instruction, but in 32-bit code segment.# Switches processor into 32-bit mode.ljmp    $PROT_MODE_CSEG, $protcseg

这里我给出一个我个人的猜想,如果有错误还恳请批评指正:
CPU很懒,对代码进行访存的时候,使用的都是CS寄存器用户不可见的部分,这部分在启动的时候默认是0,所以段基址部分也就是0,所以CPU就以0位代码段基址来对代码进行寻址(即0+IP寄存器的值);而恰好,运行这段汇编代码的时候,就是以0位段基址来运行的,所以不会产生冲突。
(后来我去stackoverflow上提出了这个问题,热心网友的解答也和这个猜想大致重合,意思是设置CR0寄存器不会更新CS寄存器里面的不可见部分,所以CPU仍然按照之前的段基址来寻址的,问题链接:https://stackoverflow.com/questions/78105088/)

指令长度问题

此外,还有一个问题:这里的ljmp指令仍然是在.code16的管辖范围的,既然CR0寄存器已经设置了,那么为什么这里还能正常执行这条ljmp指令呢?

首先查看反汇编后的boot.asm,发现这条指令的完整十六进制是EA 32 7C 08 00
通过查阅Intel白皮书,发现以EA开头的JMP指令有2种形式,如下图:
在这里插入图片描述
那么CPU是怎么确定该如何解读EA开头的JMP指令呢?这个答案还得去白皮书中找。
在Volume 2的Chapter 3.1.1.3记录了怎么解读这两者的区别,如下图:
在这里插入图片描述
这其中提到了一个关键词:operand-size attribute。这部分的内容在Volume 1 Chapter 3.6中有提到,如下图所示:
在这里插入图片描述
这段话的意思就是:CPU选择哪种解读方式取决于段描述符里面的D字段(当处于保护模式下时,如果处于实模式,那么始终都是取operand-size为16),如果D字段被设置了,那么operand-size就取32,否则就取16。

我们可以看一下代码段的段描述符:FFFF 0000 009A CF00,其中D位是1。但是按照上文的说法,在设置了CR0寄存器以后,CS寄存器的隐藏部分并未被改动,所以这个时候用的还是实模式下的“段描述符”,所以这个时候是按照operand-size为16来解读指令的。

重置CS寄存器

回归正题,我们最好还是使用段选择符重新设置一下CS和DS,SS寄存器,所以设置CR0之后就紧跟了一个far jump,这条指令中,段选择符PROT_MODE_CSEG是个提前定义好的常量,其值为0x8,根据下图的段选择符结构进行解读
在这里插入图片描述
可以得知,这个段是GDT的下标为1的段,即第二个段,回顾一下之前gdt的结构,可以发现这个段只有执行和读的权限,是很适合做代码段的。
在执行这个ljmp过程中,CPU会根据这个段选择符去加载对应的段描述符,从而达到设置CS寄存器的目的。

关于GDT的设计

如果仔细观察GDT的内容,就会发现,代码段和数据段的基址都是0,大小都是0xffffffff。这样设计其实是为了方便,因为在这样的设定下,访问的逻辑地址就是线性地址,就不必要考虑分段了。例如,MOV 0X7C04 %eax,这条指令操作的内存空间的物理地址就直接是0x7C04(在不考虑分页的情况下)

一些反汇编错误

由于boot.S同时有16位和32位的指令,而反汇编时都是按照32位来解读的,所以反汇编出来的文件obj/boot/boot.asm里面是存在一些错误反汇编的。
例如,下图所示的反汇编结果:
在这里插入图片描述
根据Intel白皮书,LGDT的指令如下图
在这里插入图片描述
0x0F 0X01和上图里的对应上了,这里如果按照16bit的解读方式的话,如下表
在这里插入图片描述
后面应该是紧跟一个16位的立即数,对应二进制指令里面的0x64 0x7C,而后面的那个0x0F是下一条指令的Opcode了。
关于白皮书的解读方法可以参考这篇文章:https://www.cnblogs.com/scu-cjx/p/6879041.html,主要需要注意Volume 2中的2.1.5和3.1.1里面的内容,这些内容对于解读指令会起到重要作用。

关于指令的解读,再补充一个例子,如下图
在这里插入图片描述
这里的反汇编是出错了的。看jmp的指令说明
在这里插入图片描述
这里很明显应该对应EA cd的情况,看3.1.1.3这一章,如下图
在这里插入图片描述
ptr16:16应该是视为一个oprand,所以0x32 0x7C 0x08 0x00是以小端存放的操作数,高2字节0x0008是段选择符,0x7C32是段内的偏移量。

为什么要关中断?

因为原有的中断向量表是实模式下使用的,其寻址方式也是实模式的方式,在进入保护模式以后,就不能使用实模式下的地址进行寻址了,所以必须关中断。

main.c的分析

在boot.S中,使用call bootmain指令跳转到main.c里面的bootmain函数中(通过链接器找到bootmain这个符号的地址)。所以我们从bootmain入手分析。
代码段如下:

void
bootmain(void)
{struct Proghdr *ph, *eph;// read 1st page off diskreadseg((uint32_t) ELFHDR, SECTSIZE*8, 0);// is this a valid ELF?if (ELFHDR->e_magic != ELF_MAGIC)goto bad;// load each program segment (ignores ph flags)ph = (struct Proghdr *) ((uint8_t *) ELFHDR + ELFHDR->e_phoff);eph = ph + ELFHDR->e_phnum;for (; ph < eph; ph++)// p_pa is the load address of this segment (as well// as the physical address)readseg(ph->p_pa, ph->p_memsz, ph->p_offset);// call the entry point from the ELF header// note: does not return!((void (*)(void)) (ELFHDR->e_entry))();bad:outw(0x8A00, 0x8A00);outw(0x8A00, 0x8E00);while (1)/* do nothing */;
}

可以大致分析出bootmain是尝试借助elf文件格式,从磁盘中读取一个elf可执行程序并加载到内存中,最后跳转到这个程序的入口地址。
下面来具体分析一下程序的实现:

首先看看引用头文件部分:

#include <inc/x86.h>
#include <inc/elf.h>

elf.h定义了elf文件的结构,这个不再赘述。x86.h定义和实现了一些x86硬件相关的函数,这些函数通常用汇编语言实现,例如:向一个IO端口输出数据的函数实现如下:

static inline void
outb(int port, uint8_t data)
{asm volatile("outb %0,%w1" : : "a" (data), "d" (port));
}

接下来是两个常量的定义:

#define SECTSIZE	512
#define ELFHDR		((struct Elf *) 0x10000) // scratch space

其中SECTSIZE是一个扇区(Sector)的大小,ELFHDR是这个elf头在内存中的物理地址。

接下来是2个用于读取磁盘的辅助函数:

void readsect(void*, uint32_t);
void readseg(uint32_t, uint32_t, uint32_t);

其中readsect是读取磁盘上的一个扇区,readseg是读取一定大小的数据(借助readsect来实现)
具体而言,readsect的实现如下所示:

void
waitdisk(void)
{// wait for disk reaadywhile ((inb(0x1F7) & 0xC0) != 0x40)/* do nothing */;
}void
readsect(void *dst, uint32_t offset)
{// wait for disk to be readywaitdisk();outb(0x1F2, 1);		// count = 1outb(0x1F3, offset);outb(0x1F4, offset >> 8);outb(0x1F5, offset >> 16);outb(0x1F6, (offset >> 24) | 0xE0);outb(0x1F7, 0x20);	// cmd 0x20 - read sectors// wait for disk to be readywaitdisk();// read a sectorinsl(0x1F0, dst, SECTSIZE/4);
}

(关于x86磁盘访问部分可以参考博客文章https://blog.csdn.net/fjlq1994/article/details/49472827)
这里的waitdisk做的事就是进行循环等待,直到0x1F7端口的第7位不再是1,这时候表示磁盘读取任务结束了。
readsect函数则是写IO端口,把第offset个扇区的数据读入到内存区域dst中
readseg的实现如下:

// Read 'count' bytes at 'offset' from kernel into physical address 'pa'.
// Might copy more than asked
void
readseg(uint32_t pa, uint32_t count, uint32_t offset)
{uint32_t end_pa;end_pa = pa + count;// round down to sector boundarypa &= ~(SECTSIZE - 1);// translate from bytes to sectors, and kernel starts at sector 1offset = (offset / SECTSIZE) + 1;// If this is too slow, we could read lots of sectors at a time.// We'd write more to memory than asked, but it doesn't matter --// we load in increasing order.while (pa < end_pa) {// Since we haven't enabled paging yet and we're using// an identity segment mapping (see boot.S), we can// use physical addresses directly.  This won't be the// case once JOS enables the MMU.readsect((uint8_t*) pa, offset);pa += SECTSIZE;offset++;}
}

按照注释,这个函数是从磁盘的第1个扇区往后数offset个字节处,读取count字节的数据,存储到pa这个内存区域中(从第一个扇区开始是因为,第0个扇区的512字节存储的是bootloader)。

最后再回顾一下bootmain函数,这个函数首先加载elf头,然后根据elf头加载各个程序段,需要注意的是,每个程序段被加载的内存地址是它的LMA,这也就使得我们可以通过链接器来很方便地控制kernel在内存中的分布情况(例如,知道text段的具体物理地址等)。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/272508.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【深度学习笔记】6_9 深度循环神经网络deep-rnn

注&#xff1a;本文为《动手学深度学习》开源内容&#xff0c;部分标注了个人理解&#xff0c;仅为个人学习记录&#xff0c;无抄袭搬运意图 6.9 深度循环神经网络 本章到目前为止介绍的循环神经网络只有一个单向的隐藏层&#xff0c;在深度学习应用里&#xff0c;我们通常会用…

使用API有效率地管理Dynadot域名,进行DNS域名解析

关于Dynadot Dynadot是通过ICANN认证的域名注册商&#xff0c;自2002年成立以来&#xff0c;服务于全球108个国家和地区的客户&#xff0c;为数以万计的客户提供简洁&#xff0c;优惠&#xff0c;安全的域名注册以及管理服务。 Dynadot平台操作教程索引&#xff08;包括域名邮…

算法设计与分析(超详解!) 第一节 算法概述

1.算法的定义 算法的非形式化定义&#xff1a;算法是规则的有限集合&#xff0c;是为解决特定问题而规定的一系列操作。 可以理解为&#xff1a;算法&#xff08;algorithm&#xff09;是指在解决问题时&#xff0c;按照某种机械的步骤一定可以得到问题的结果&#xff08;有的…

图形库实战丨C语言扫雷小游戏(超2w字,附图片素材)

目录 效果展示 游玩链接&#xff08;无需安装图形库及VS&#xff09; 开发环境及准备 1.VS2022版本 2.图形库 游戏初始化 1.头文件 2.创建窗口 3.主函数框架 开始界面函数 1.初始化 1-1.设置背景颜色及字体 1-2.处理背景音乐及图片素材 1-3.处理背景图位置 2.选…

【软件安装教程】Anaconda

【软件安装教程】Anaconda 系统: Windows11 64位版本: Anaconda3-2024.02-1官方地址: Anaconda网盘地址: 百度网盘 下载 点击此连接 Anaconda 进入官网下载最新版 点击此连接 百度网盘 进入网盘下载 Anaconda3-2024.02-1 安装 双击下载好的文件 点击 Next 点击 I Agree …

求根节点到叶节点数字之和

题目 题目链接 . - 力扣&#xff08;LeetCode&#xff09; 题目描述 代码实现 class Solution { public:int sumNumbers(TreeNode* root) {return _sumNumbers(root, 0);}int _sumNumbers(TreeNode* root, int preSum){preSum preSum * 10 root->val;if(root->left…

cocos creator 3.7.2使用shader实现图片扫光特效

简介 功能&#xff1a;图片实现扫光效果 引擎&#xff1a;cocos Creator 3.7.2 开发语言&#xff1a;ts 完整版链接 链接https://lengmo714.top/284d90f4.html 效果图 shader代码 // Copyright (c) 2017-2020 Xiamen Yaji Software Co., Ltd. CCEffect %{techniques:- pas…

Kafka数据推送配置 | 如何设置账号密码验证?

背景&#xff1a;之前资产信息用网络接口进行数据推送&#xff0c;但是接口推送需要验证而且反应较慢。Kafak中间件提供了另一种可行的数据推送方式&#xff0c;它可以进行消息队列推送&#xff0c;且反应速度快。但是Kafka需部署在公网环境&#xff0c;并进行登录验证&#xf…

Microsoft office Word和有道云写的笔记复制粘贴到csdn,图片加载失败的具体解决方法

由于CSDN的博客接口关闭&#xff08;可能是这个原因&#xff09; 此方法失效&#xff0c;之后找了一个新的方法如下&#xff1a; 1.有道云笔记&#xff1a;转为word格式 2.打开火狐浏览器&#xff0c;即可从Microsoft office Word粘贴文章到CSDN。

软考70-上午题-【面向对象技术2-UML】-UML中的图1

一、图的定义 图是一组元素的图形表示&#xff0c;大多数情况下把图画成顶点、弧的联通图。 顶点&#xff1a;代表事物&#xff1b; 弧&#xff1a;代表关系。 可以从不同的角度画图&#xff0c;UML提供了13种图&#xff1a;&#xff08;只看9种&#xff09; 类图&#xff…

大华IPC网络摄像机如何保存视频

一、背景 通常网络相机&#xff08;IPC&#xff09;不会自带存储功能&#xff0c;需要接入录像机&#xff08;NVR&#xff09;进行保存。 其中NVR也分软件存储及硬件存储&#xff0c;这里不提&#xff0c;这边单独说FTP存储 二、配置前提 要配置FTP存储需要&#xff1a;①网络…

Linux——文件重定向

目录 前言 一、重定向 二、重定向的运用 三、dup2 四、命令行中的重定向 五、为什么要有标准错误 前言 在之前我们学习了文件标识符&#xff0c;直到close可以使用文件标识符进行关闭&#xff0c;但是当我们关闭1号&#xff08;stdout&#xff09;时&#xff0c;无法往显…

【Python刷题】合并两个有序列表

问题描述 将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。 示例 1&#xff1a; 输入&#xff1a;l1 [1,2,4], l2 [1,3,4] 输出&#xff1a;[1,1,2,3,4,4]示例 2&#xff1a; 输入&#xff1a;l1 [], l2 [] 输出&…

docker 使用官方镜像搭建 PHP 环境

一、所需环境&#xff1a; 1、PHP&#xff1a;7.4.33-fpm 的版本 2、Nginx&#xff1a;1.25.1 的版本 3、MySQL&#xff1a; 5.7 的版本 4、Redis&#xff1a;7.0 的版本 1.1、拉取官方的镜像 docker pull php:7.4.33-fpm docker pull nginx:1.25.1 docker pull mysql:5.7 do…

HarmonyOS 数据持久化 关系型数据库之 初始化操作

上文 HarmonyOS 数据持久化之首选项 preferences 我们有说用户首选项 但它只能处理一些比较简单的数据类型结构 的持久化处理 如果是一些批量较大 结构较为复杂的数据结构 那么 首选项就无法满足了 我们就要选择 关系型数据库 通过 SQLite 组件实现的一种本地数据库&#xff0…

Learn OpenGL 04 纹理

纹理环绕方式 纹理坐标的范围通常是从(0, 0)到(1, 1)&#xff0c;那如果我们把纹理坐标设置在范围之外会发生什么&#xff1f;OpenGL默认的行为是重复这个纹理图像&#xff08;我们基本上忽略浮点纹理坐标的整数部分&#xff09;&#xff0c;但OpenGL提供了更多的选择&#xf…

YOLOv8.1.0安装

【YOLO】YOLOv8训练环境配置 python 3.8.18 cuda 11.3.1 cudnn 8.2.1 pytorch 1.12.1-gpu版 - 知乎 (zhihu.com) 一、Anaconda 默认装好了可用的Anaconda&#xff0c;安装教程见Win10系统anaconda安装 - 知乎 (zhihu.com) 二、在虚拟环境下用conda安装 1.创建虚拟环境 …

【决策树】预测用户用电量

决策树预测用户用电量 文章目录 决策树预测用户用电量  &#x1f449;引言&#x1f48e;一、 数据预处理数据预处理初步数据分析 二、 机器学习算法决策树回归预测用电量决策树模型介绍&#xff1a;回归预测 三、 可视化结果四、 数据分析与结论代码如下 &#x1f449;引言&a…

UE5.1_使用技巧(常更)

UE5.1_使用技巧&#xff08;常更&#xff09; 1. 清除所有断点 运行时忘记蓝图中的断点可能会出现运行错误的可能&#xff0c;务必运行是排除一切断点&#xff0c;逐个排查也是办法&#xff0c;但是在事件函数多的情况下会很复杂且慢节奏&#xff0c;学会一次性清除所有很有必…

如何使用LEAKEY轻松检测和验证目标服务泄露的敏感凭证

关于LEAKEY LEAKEY是一款功能强大的Bash脚本&#xff0c;该脚本能够检测和验证目标服务中意外泄露的敏感凭证&#xff0c;以帮助广大研究人员检测目标服务的数据安全状况。值得一提的是&#xff0c;LEAKEY支持高度自定义开发&#xff0c;能够轻松添加要检测的新服务。 LEAKEY主…