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

news2024/11/15 10:44:00

写在前面

本系列文章将会尝试以学习笔记的形式展开,即每篇文章都没有一个明确的主线,主要是堆砌实验过程中遇到的知识点和解决的问题
本系列涉及的学习对象是MIT面向研究生开设的操作系统工程课程的Lab部分,该课程的编号为MIT6.828
(区别一下MIT6.S081,这个课程是面向本科生开设的,也叫Operating System Engineering,Lab的难度相对较低,本系列不讨论这门课程)
本系列将直接就这门课程的Lab部分进行学习,忽略这门课程的授课部分

课程网站

https://pdos.csail.mit.edu/6.828/2018/
点击上方导航栏的Labs菜单栏可以进入每个Lab的页面

Lab1学习笔记(第一部分)

Lab1涉及到了大量的涉及到x86硬件的知识,以及x86指令集的知识,所以该Lab的笔记分为多个部分记录。
Lab1主要向我们展示了一台计算机是怎样启动的,涉及到了如何编写Bootloader,如何加载内核,物理内存的分布和用途,实模式与保护模式等。
这个lab的链接如下:
https://pdos.csail.mit.edu/6.828/2018/labs/lab1/
下面正式开始对于Lab1的笔记。

计算机是如何启动的?

对于不同体系结构的计算机,其启动方式会略有差异,这里只讨论x86架构的计算机的启动流程(因为这个课程也是针对x86设计的,其他平台可以很轻易地推广)。

1.加载BIOS

主板上有一块只读的存储区域(ROM),这里面保存了BIOS代码,BIOS会负责执行基础的系统初始化,如启动显示卡,检查内存容量等。
计算机启动的第一个步骤是把BIOS加载到内存中,对于不同的CPU,BIOS的加载位置可能略有差异。对于实验中使用的QEMU模拟器,BIOS大小为64KB,在物理内存中从0x000F0000到0x000FFFFF
(注:本实验中有很多参数都是和具体的CPU型号有关,本文后面就不着重强调这一点了,后面涉及到的参数都默认是QEMU模拟器的参数)

2.执行BIOS的第一个指令

在加载完BIOS之后,CPU会把CS寄存器设置为0xf000,把IP寄存器设置为0xfff0,把控制权交给BIOS。
这里刚启动的时候CPU还处于实模式,CPU对于指令的寻址方式是使用段基址+段内偏移地址的方法来计算物理地址的,其中段基址保存在CS寄存器中,IP寄存器就是计算机组成原理里面的提到的PC(Program Counter)寄存器,指向了下一条将要执行的指令的地址。
所以BIOS第一条指令在物理内存中的地址是0xFFFF0

3.加载并运行Bootloader

BIOS程序会读取磁盘的第一个扇区(大小512KB),如果第一个扇区的最后两个字节是0x55AA,那么BIOS就会认为这个磁盘是个启动介质,并把第一个扇区加载到内存中的0x7C00的位置,然后把CS设置为0x0000,IP设置为0x7C00,把控制权交给Bootloader

4.Bootloader加载并运行内核

Bootloader的指令将会从磁盘中把完整的内核加载到内存中,然后把IP寄存器设置为kernel的起始位置的地址,把控制权交给内核。
实际操作系统中还会在Bootloader里面把CPU转换到保护模式,并配置好段表GDT。

JOS项目是如何启动的?

Lab官方网站上给出的启动方法是使用cmake,只需要make && make qemu即可启动整个项目,但是这其中具体的流程是怎样的呢?这是很值得探索的,这将有助于我们更深入地理解操作系统的启动流程。
首先介绍一下项目的重要组成部分:

  • boot目录:这里面有boot.S和main.c,这是Bootloader的代码
  • kern目录:这里面保存的是kernel的代码
  • obj目录:最终输出文件的目录

1.编译Bootloader

这个步骤是为了得到只包含指令的Bootloader,然后把它填充到512KB,并把最后2个字节设置为0x55AA。这个步骤得到的二进制文件是需要直接写到磁盘的第一个扇区里面的。
具体的步骤可以见boot目录下的Makefrag文件,如下图所示
在这里插入图片描述
首先把boot.S和main.c编译为elf格式的可执行文件;然后使用链接器把两个可执行文件链接起来;然后提取链接后的elf文件的text段,也就是指令部分;最后使用一个perl脚本填充得到的指令文件,并把最后2个字节设置为0x55AA

为什么要这么麻烦,先编译,再链接,再提取text段呢?
因为代码中涉及到大量的符号,例如下图所示的跳转到main.c里面写的bootmain函数
在这里插入图片描述
在bootloader里面,是只能用实际地址的,所以必须要使用链接器来把bootmain这一类的符号替换成具体的地址数值。

2.编译Kernel

把kern目录下的源码编译成elf格式可执行文件,然后链接起来,最终得到一个名为kernel的elf文件。
这其中的链接部分稍微有点复杂,目前暂时还没有去深入研究这部分。

3.打包启动镜像kernel.img

这一步就很简单粗暴了,就是纯纯的文件复制。
首先新建一个空白的kernel.img,然后把之前的那个512KB的boot文件复制进去,然后再把kernel这个elf文件复制进去,就完成了镜像的制作。
这个步骤具体涉及到的指令如下图(kern/Makefrag)所示
在这里插入图片描述
需要注意的是,这个步骤产出的kernel.img是可以在真正的计算机上运行的启动镜像。也就是说,如果把这个kernel.img写入到一个U盘里面,然后再把U盘插到一台电脑上启动,那么这个电脑屏幕上就会显示出和下图QEMU窗口所示一模一样的画面
在这里插入图片描述
(其实这里的QEMU窗口可以理解为一个显示器了,这里面的文字都是kernel使用底层代码展示出来的,至于是如何展示的,后文会揭晓答案)

4.启动QEMU模拟器

查看根目录下的GNUmakefile可知,该项目使用了如下图所示的options来启动QEMU
在这里插入图片描述
也就是说,在启动时候把kernel.img作为了磁盘里的内容。

分析entry.S

首先分析整个bootloader的入口,也就是entry.S
(关于为什么入口是它?是因为:观察boot/Makefrag,在link的时候,指定了程序入口是start这个符号,而start符号是在entry.S的最开头就定义了的)

汇编语言

关于汇编语法,整个项目的语法风格使用的都是AT&T风格,具体区别可以访问http://www.delorie.com/djgpp/doc/brennan/brennan_att_inline_djgpp.html
简单了解一下。

伪指令

汇编语言代码中会涉及到一些伪指令,这些指令是用来指导编译器的行为的,这里参考了博客文章https://blog.csdn.net/Roland_Sun/article/details/107705952
对Bootloader中会用到的伪指令做简单介绍

.set

相当于是定义了一个常量,如下图
在这里插入图片描述
相当于是定义了一个名叫PROT_MODE_CSEG的常量,以后可以直接使用$PROT_MODE_CSEG 来表示这个常量对应的具体值

.global

这个伪指令告诉编译器,某个符号后面还会用到,所以把这个符号标记为全局符号。
如下图这段代码
在这里插入图片描述
这里的.global start告诉编译器在编译entry.S的时候,要保留start这个符号,因为后面在链接的时候还会用上这个符号的

.code16 .code32

这个伪指令是告诉编译器接下来的代码是以16位(32位)的格式编译,16位与32位的寻址方式有区别,如果CPU已经进入保护模式了,但是代码却还是用16位寻址方式编译的,那么就会出问题。
(关于这一点,后面会有具体的例子来说明)
这里只需要知道,在编译器遇到.code16之后,会把后面的代码都以16位寻址方式编译;当遇到.code32之后,就会把后面的代码都以32位寻址方式编译

.word .long

这是填充数据的指令,编译器在遇到这一系列指令后,会向目标文件中填充指定的数据

代码解析:entry.S

下面这段代码首先关中断,避免系统启动时被中断打扰,然后初始化了数据段寄存器DS,栈段寄存器SS和另一个段寄存器ES

start:
  .code16                     # Assemble for 16-bit mode
  cli                         # Disable interrupts
  cld                         # String operations increment

  # Set up the important data segment registers (DS, ES, SS).
  xorw    %ax,%ax             # Segment number zero
  movw    %ax,%ds             # -> Data Segment
  movw    %ax,%es             # -> Extra Segment
  movw    %ax,%ss             # -> Stack Segment

接下来,按照注释的说法,是开启了A20,这里不再深究其内部机制了

  # Enable A20:
  #   For backwards compatibility with the earliest PCs, physical
  #   address line 20 is tied low, so that addresses higher than
  #   1MB wrap around to zero by default.  This code undoes this.
seta20.1:
  inb     $0x64,%al               # Wait for not busy
  testb   $0x2,%al
  jnz     seta20.1

  movb    $0xd1,%al               # 0xd1 -> port 0x64
  outb    %al,$0x64

seta20.2:
  inb     $0x64,%al               # Wait for not busy
  testb   $0x2,%al
  jnz     seta20.2

  movb    $0xdf,%al               # 0xdf -> port 0x60
  outb    %al,$0x60

接下来这段代码是让CPU进入保护模式,然后通过一个jump来清空指令流水线里面的16位指令(https://www.kancloud.cn/digest/protectedmode/121470)因为接下来CPU将会进入32位模式
(关于实模式与保护模式,后面会重点阐述一下的,这里暂时认为保护模式下寻址方式使用32位寻址方式即可)

  # 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    gdtdesc
  movl    %cr0, %eax
  orl     $CR0_PE_ON, %eax
  movl    %eax, %cr0

  # Jump to next instruction, but in 32-bit code segment.
  # Switches processor into 32-bit mode.
  ljmp    $PROT_MODE_CSEG, $protcseg

接下来就到了protcseg部分,这部分做的事情就是首先把数据段,代码段的段描述符设置了(这个段描述符和保护模式有关,后面会提到),然后设置栈的起始位置为0x7C00,最后使用一个call指令来把控制权交给写在main.c里面的bootmain函数

  .code32                     # Assemble for 32-bit mode
protcseg:
  # Set up the protected-mode data segment registers
  movw    $PROT_MODE_DSEG, %ax    # Our data segment selector
  movw    %ax, %ds                # -> DS: Data Segment
  movw    %ax, %es                # -> ES: Extra Segment
  movw    %ax, %fs                # -> FS
  movw    %ax, %gs                # -> GS
  movw    %ax, %ss                # -> SS: Stack Segment

  # Set up the stack pointer and call into C.
  movl    $start, %esp
  call bootmain

按照道理,bootmain函数是不会使用ret指令返回的,为了防止意外情况,这里写了一个死循环

  # If bootmain returns (it shouldn't), loop.
spin:
  jmp spin

死循环下面就是对于全局段表GDT的配置,这个会放在后面来详细解释。这里相当于是通知编译器向输出的二进制文件中直接写入GDT,由于这段二进制文件中的内容在启动的时候会被加载到内存中,所以就相当于在内存中开辟了一块区域用来存放GDT表项。

# Bootstrap GDT
.p2align 2                                # force 4 byte alignment
gdt:
  SEG_NULL                              # null seg
  SEG(STA_X|STA_R, 0x0, 0xffffffff)     # code seg
  SEG(STA_W, 0x0, 0xffffffff)           # data seg

gdtdesc:
  .word   0x17                            # sizeof(gdt) - 1
  .long   gdt                             # address gdt

实模式与保护模式

这部分内容主要参考了博客文章https://www.kancloud.cn/digest/protectedmode/121468和《深入理解Linux内核》的内容

两者的区别

实模式和保护模式主要有以下几个差别:

  1. 寻址方式不同
  2. 保护模式包含有硬件级别的安全包含,而实模式没有

下面来具体看看这些区别。

寻址方式

首先是寻址方式的区别,如果CPU处于实模式下,那么CPU的寻址方式就是直接使用段寄存器+偏移量的方式去寻找。
例如,对于指令MOV 0x7C04 %ax,CPU做的事情是:
首先把数据段寄存器DS的值与0x7C04相加,得到要访问的物理地址,然后访问这个物理地址,读取数据。

如果CPU处于保护模式,那么CPU就是使用硬件进行段页式寻址。这过程中涉及到3类地址:逻辑地址,线性地址和物理地址,其中逻辑地址就是指令里面写的地址,例如MOV 0x7C04 %ax中的0x7C04;物理地址是RAM上面的地址,是数据在物理介质上存储的地址。
保护模式下的CPU的寻址方式分为2个步骤:
首先是逻辑地址会通过分段单元(硬件)转换为线性地址,然后线性地址再由分页单元(同样也是硬件)转换为物理地址,如下图所示。
在这里插入图片描述
具体而言,段寄存器(例如DS)存储的内容不再是段基址,而是段选择符(Segment Selector),段选择符会指向全局描述符表GDT中的某一个段描述符(Segment Descriptor),段描述符记录了段基址,段类型,权限等管理信息。使用段描述符中的段基址+逻辑地址就得到了线性地址。
接下来分页单元会查页表,把线性地址映射到物理地址。现代CPU通常都是使用的多级页表,例如下图所示的80x86处理器的分页方式
在这里插入图片描述
同时还会使用TLB来缓存页表项,来减少访问主存的频率,提高效率。

安全保护

在段选择符中有一个字段叫RPL,这个字段表示了当前CPU的特权等级
在这里插入图片描述
特权等级从0到3,依次递减。CPU在执行指令时会判断当前的特权等级,如果等级不足,会拒绝执行。例如,像LGDT这类的操作GDT的指令,就需要特权等级为0才能执行
在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

浅谈 Linux 孤儿进程和僵尸进程

文章目录 前言孤儿进程僵尸进程 前言 本文介绍 Linux 中的 孤儿进程 和 僵尸进程。 孤儿进程 在 Linux 中,就是父进程已经结束了,但是子进程还在运行,这个子进程就被称作 孤儿进程。 需要注意两点: 孤儿进程最终会进入孤儿院…

frp 内网穿透 linux部署版

frp 内网穿透 linux部署版 前提安装 frp阿里云服务器配置测试服务器配置访问公网 前提 使用 frp,您可以安全、便捷地将内网服务暴露到公网,通过访问公网 IP 直接可以访问到内网的测试环境。准备如下: 公网 IP已部署好的测试服务 IP:端口号阿…

Linux系统部署前后端分离项目

一、Nginx简介 1.1 什么是nginx? Nginx(发音同"engine x")是一个高性能的反向代理和 Web 服务器软件,最初是由俄罗斯人 Igor Sysoev 开发的。Nginx 的第一个版本发布于 2004 年,其源代码基于双条款 BSD 许可证发布&am…

人脸高清算法GFPGAN之TensorRT推理

1. 综述 最近由于做数字人项目,采用的是wav2lip GFPGAN进行人脸面部高清,但GFPGAN模型本身比较大,所以想着使用TensorRT来代替原始的pth推理看看能否提升运行速度,于是便开始了这趟windows1之下进行GFPGAN的trt推理的折腾之旅。…

Python Web开发记录 Day5:jQuery(JavaScript库)

名人说:莫道桑榆晚,为霞尚满天。——刘禹锡(刘梦得,诗豪) 创作者:Code_流苏(CSDN)(一个喜欢古诗词和编程的Coder😊) 目录 五、jQuery1、jQuery-选择器和菜单案例①快速上…

看视频,学习使用MindOpt APL 建模语言编码数学规划问题,练习语法,实战拿奖品

活动介绍 活动名称:看视频,补充代码,拿精美礼品 活动规则: 浏览视频学习MAPL,完善“例题”。需要完善的内容:补充约束条件、读取csv表格数据,将决策变量的取值输出为csv表格,验证一…

leetcode 热题 100_最长连续序列

题解一: 哈希表:找连续最长的数字序列,很容易联想到排序,但排序的时间复杂度O(nlogN)过大,判题容易超时。因此我们需要使用哈希表来快速查找,序列中是否存在与某个数相邻的数。用HashSet建立哈希表并去重&a…

【.NET Core】深入理解IO - FileSteam流

【.NET Core】深入理解IO - FileSteam流 文章目录 【.NET Core】深入理解IO - FileSteam流一、IO流概述二、文件流FileStream2.1 FileStream概述2.2 FileStream检测流位置更改2.3 FileStream构造函数2.4 FileStream常用属性2.5 FileStream.Read方法2.6 FileStream.Write方法2.7…

逻辑漏洞(pikachu)

#水平,垂直越权,未授权访问 通过个更换某个id之类的身份标识,从而使A账号获取(修改、删除)B账号数据 使用低权限身份的账号,发送高权限账号才能有的请求,获得其高权限操作 通过删除请求中的认…

Python学习DAY09_文件和异常

文件和异常 实际开发中常常会遇到对数据进行持久化操作的场景,而实现数据持久化最直接简单的方式就是将数据保存到文件中。 在 Python 中实现文件的读写操作其实非常简单,通过 Python 内置的 open 函数,我们可以指定文件名、操作模式、编码信…

物联网主机E6000引领工业自动化的新篇章

E6000——多协议、多接口的全能战士 在工业4.0的大潮中,物联网的应用正在逐步深入到各个领域。而E6000物联网主机就是其中的佼佼者,它以其卓越的性能和强大的功能,成为推动工业自动化发展的一股重要力量。 E6000是一款多协议、多接口的物联…

Tomcat安装,配置文件、组件

一、Tomcat的基本功能 1.1.Tomcat是什么? Tomcat服务器是一个免费的开放源代码的Web应用服务器,属于轻量级应用服务器,在中小型系统和并发访问用户不是很多的场合下被普遍使用,是开发和调试JSP程序的首选。一般来说,T…

【Django】执行查询—跨关系查询中的跨多值关联问题

跨多值查询 跨越 ManyToManyField 或反查 ForeignKey (例如从 Blog 到 Entry )时,对多个属性进行过滤会产生这样的问题:是否要求每个属性都在同一个相关对象中重合。 filter() 先看filter(),通过一个例子看&#xf…

mysql,for循环执行sql

遇到一个问题,我需要模拟上百万数据来优化sql,线上数据down不下来,测试库又没有,写代码执行要么慢要么就是sql语句太长。 于是,直接用mysql自带的功能去实现! 简单而简单 mysql可以for循环?没…

vue中将某个不太规则的json转成对象,或者将对象转成json字符串

vue中将某个不太规则的json转成对象,或者将对象转成json字符串 以我自己做的项目某个不规则的json为例 将json对象转成json字符串: JSON.stringify(jsonData); 将不规则json字符串转成对象并获取对应的属性的值: JSON.parse(jsonData).Name…

机器学习项目外包注意事项

将机器学习项目外包给外部团队或合作伙伴是一种常见的做法,特别是当您的团队缺乏特定领域的专业知识或资源时。以下是一些关于机器学习项目外包的要点和注意事项,希望对大家有所帮助。北京木奇移动技术有限公司,专业的软件外包开发公司&#…

JavaScript new、apply call 方法

new、apply、call、bind JavaScript 中的 apply、call和 bind 方法是前端代码开发中相当重要的概念,并且与 this 的指向密切相关 new new 关键词的主要作用 就是执行一个构造函数、返回一个实例对象 根据构造函数的情况,来确定是否可以接受参数的传递…

电车降价,不如腾讯云服务器价格降得多,61元一年服不服?

腾讯云优惠活动2024新春采购节活动上线,云服务器价格已经出来了,云服务器61元一年起,配置和价格基本上和上个月没什么变化,但是新增了8888元代金券和会员续费优惠,腾讯云百科txybk.com整理腾讯云最新优惠活动云服务器配…

寒假作业Day 02

这是第二天的作业,fighting! Day 02 一、选择题 首先char* s[6]是指针数组,也就是其存储的都是这些字符串的地址,其实际上的类型为char**,而fun函数传入了s数组的首地址。而后续fun函数中打印字符,p[i]即…

ShardingSphere 5.x 系列【18】自定义类分片算法

有道无术,术尚可求,有术无道,止于术。 本系列Spring Boot 版本 3.1.0 本系列ShardingSphere 版本 5.4.0 源码地址:https://gitee.com/pearl-organization/study-sharding-sphere-demo 文章目录 1. 概述2. ClassBasedShardingAlgorithm3. 案例演示3.1 STANDARD3.2 COMPLEX…