0 Day:操作系统的一些问题

发布于:2022-11-16 ⋅ 阅读:(1383) ⋅ 点赞:(1)

前言:从今天开始就是进入我们操作系统开发的第0天,我将会介绍一下什么是操作系统,也会记录一部分操作系统中常见的问题(实际上是我从书上偷来的),如果这些问题你都了解可以直接跳过这一章。为什么是第0天呢?因为这样就可以多模一天🐟了。


本日开发参考以下资料:
《操作系统真象还原》

  操作系统WIKI
 各种博客,之后会有说明

① 什么是操作系统?

操作系统(英语:Operating jies System,缩写:OS)是一组主管并控制计算机操作、运用和运行硬件、软件资源和提供公共服务来组织用户交互的相互关联的系统软件程序。根据运行的环境,操作系统可以分为桌面操作系统,手机操作系统,服务器操作系统,嵌入式操作系统等。 --来自百度百科

操作系统实际上就是一款软件一段程序,很多人说那我肯定知道啊,书上都讲了。那讲的形象一点呢,计算机中处处是分层,人类社会也是如此。操作系统在我眼里就像是一个人类社会,社会中有各种组织,不同的组织执行不同的功能,负责不同事务。比如你寄一封信,作为用户的你只需将信放到信箱(接口),然后有专门的邮递员和邮局帮你送信寄信,一个工作有专人负责无需重复劳动,你已经有邮局了何必自己再跑一趟呢,这就是操作系统提供的资源。同时你作为普通人,再没有邮局赋予权力和资源的情况下,不能自己跑到邮局去转送别人的邮件,这也就是操作系统和用户进程的关系,普通的用户进程无法肆无忌惮的访问操作系统内核资源,否则会出大问题!

② 软件是如何访问硬件的呢?

IO接口

硬件各种各样,且更新换代迅速。不同的硬件有很大的区别,硬件运行的驱动方法也很关键。但是不可能说你每更新一次硬件,作为OS开发者的我就给你去写一个驱动,那肯定不行。此时我们就应该想到 设计模式的一个模式:适配器模式(设计模式🐂)没错,这种适配设备就是IO接口

  •  输入输出分类:串行,并行
  •  接口分类:串行接口,并行接口

从外部访问硬件方式

(1)外设内存映射

将外设内存映射到某个地址空间范围,当CPU访问到该地址范围时,就落到外设的内存中。这样就访问CPU外设的内存就如同访问主板内存一样。

打个比方:显卡

显卡是显示器的适配器,显卡有显存,而CPU不直接与显示器交互,而是与显卡通信。它将显存映射到主机物理内存的0xB8000~0xBFFFF,此时CPU访问这片内存时就相当于是访问显存,并且在该内存地址写入字节也就是往屏幕上打印内容。

(2)IO接口与CPU

CPU访问外设,就是访问IO接口。CPU是不知道外设是否存在的(因为分层),他只负责与IO接口通信,IO接口将信息传递给另一端的外设。

那IO接口又是如何被访问到的呢,IO接口上面有许多寄存器(寄存器的功能是存储二进制代码,它是由具有存储功能的触发器组合起来构成的),寄存器就相对于是IO接口的接口,访问IO接口实际上就是访问寄存器,IO接口根据寄存器的写入进行反应。

③ 操作系统和应用程序是怎么融合在一起的

操作系统也是软件,应用程序也是软件。而CPU可不管这些东西,只负责读取cs:ip寄存器(之后会说)然后执行指令,那CPU如何知道正在执行的是操作系统还是应用程序呢?如何保证操作系统和应用程序的权限呢?

操作系统是管理计算机的一套管理方法(管理项目)

应用程序则是用语言编写出来,由编译器转成机器语言。

而光应用程序还不够,还是个半成品,只有 应用程序+操作系统才是完成品,而在这中间需要有编译器提供的库函数,库函数种封装了系统调用,就像print需要调用系统函数将字符打印到屏幕上一样。

用户态和内核态

而此时CPU的状态就分为用户态和内核态(注:CPU的状态)

用户进程陷入内核态:由于外部或内部中断发生,导致当前进程暂时停止,进程的上下文被内核程序保存起来,然后执行内核代码。此时的应用程序已经不再CPU上面运行,运行的已经是内核程序,至于发生了什么应用程序是完全不知道的,但并不是说应用程序变成了内核程序,两个东西是完全分开的。

相信很多人都看过名侦探柯南,大家可以把毛利小五郎理解为应用程序,而柯南是内核程序,当毛利小五郎的破案遇到困难时,柯南发射催眠针(中断信号),此时毛利小五郎睡着了,由内核柯南来破案,整个过程毛利小五郎是不知道的。大家可以带入用户进程陷入内核态思考一下。

④ 内存访问为什么要分段

我们先讨论在实模式下的内存,内存是随机读写设备,即你给出地址,便可以访问该地址的内存。

那为什么要分段呢?

  •  进行程序重定位

在远古计算机时代,CPU和寄存器都是16位的,也就是说最多访问2^16= 65536 = 64KB,那个时候还没有虚拟地址,全是物理地址。所以编译器编译出来的程序要变为机器指令,指令的地址必须是绝对物理地址。当运行程序时,需要存入物理内存中,地址也是绝对物理地址,如果此时有两个程序物理地址相同,就会导致只有其中一个程序能运行

于是便有了分段,CPU采用“段基址+段内偏移地址”来访问内存,这样就可以对重新进行重定位。

我们先来说说 段基址+段内偏移地址 这一访问方式

在CPU中使用专门的段基址 寄存器 cs,ds,es等等,程序需要使用那块内存,只需将段基址和偏移地址的和相加放入地址总线即可

比如:

段基址为 0xC00 访问物理内存 0xC01 用 0xC00:0x01来表示即可

段基址为 0xC00 访问物理内存 0xC04 用 0xC00:0x04,0xC02:0x02,0xC01,0x03 都可以

你可以理解为坐高铁到北京

上海到北京还差100km 这上海就是基址,100km就是偏移地址,北京就是访问地址

理解 段基址+段内偏移地址这个概念后,理解重定位就会方便很多,只要程序分了段,无论将段平移到任何位置,段内的地址相对于基址是不变的,无论段基址是多少,只需给出相应的段基址即可

  • 将大内存分成可以访问的小内存

在远古时期,最小的内存都有1MB但是CPU和寄存器都是16位,我们来算个数,1MB是 2^20而寄存器是 2^16,这就很难受了,这样子最多只能访问64KB捏,但是有了段的这个概念,我们就可以让这个段在这个内存中移动飘逸,之前我们有说,段基址和段偏移地址都是可以变化的,

打比方:你有一根长1米的杆子(寄存器),20米宽的河流(内存),原本你的最远距离是1米,但是你先在可以坐船了,船在河流中位置随意变化,你可以理解为段基址,即使你的在某个河流位置能碰到的最远距离还是1米,但是你能访问到的地方更多了。

但是,我们仔细想想,就算有段基址,他的最大范围也只是 2^16  0~0xFFFF啊,我们知道 两个相同的个数字相加 不过是 n<<1罢了,所以即使是段基址和偏移地址都是最大值,也只能访问到2^17 128KB呀。

其实CPU设计者对地址处理单元做了手脚,地址部件接收到 段基址+段偏移地址 后,会将段基址左移4位,再加上段偏移地址,这下子就是20位了。太baby了

随着分页的发展,分段机制也在渐渐弱化,但是分段仍然是x86固有机制。

⑤ 代码中为什么分为代码段,数据段

首先第一点:程序不一定要分成段才能运行,不管是饭菜分装还是做成煲仔饭,饭都是可以吃的。

分段只是为了程序变得更优美,也可以在某种程度上提高运行效率。

在x86架构下必须用分段机制访问内存,分段是必然的,平坦模式下内存分段最大为4GB。多段模式下,内存段大小不一。(之后会细说)所以说在平坦模式下无需多次切换段基址寄存器,因为整个4GB内存里面都在同一个段里面,但是多段由于位于不同的段,所以需要多次切换段基址寄存器。

对于代码中是否要分段, 这取决于操作系统是否在平坦模型

一般的 级语言不允许程序员自己将代码分成各种各样的段,这是因为其所用的编译器是针对某个操 作系统编写的,该操作系统采用的是平坦模型,所以该编译器要编译出适合此操作系统加载运行的程序。 由于处理器支持了具有分页机制的虚拟内存,操作系统也采用了分页模型,因此编译器会将程序按内 分成代码段和数据段,如编译器 gee 会把 言写 出的程序划分成代码段、数据段、战段、 .bss 段、堆 部分 这会由操作系统将编译器编译出来的用户程序中的各个段分配到不同的物理内存上 对于目前咱们 用高级语 编码来说,我们之所以不用关心如何将程序分段,正是由于编译器按平坦模型编译,而程 依赖的操作系统又采用了虚拟内存 理,即处理器的分页机制。像汇编这种低级语 允许程序员为 己的 程序分段,能够灵活地编排布局,这就属于人为将程序分成段了,也就是采用多段模型编程。(总结不下来,直接CV了)

CPU是如何获取下一条指令的

按我的理解我总结为三点:

  • 程序计数器cs:eip寄存器负责自动获取下一条指令
  • 指令之间是紧挨着的彼此之间无间隙,下一条指令的地址按照前面指令的尺寸拍下来的(有点ziplist的思想)这也是程序计数器的原理
  • 数据和非指令部分可以用 jmp指令 (理解为c的goto)跳过,形成逻辑上的连续。

实际上逻辑上的连续和物理上的连续都可以

物理上连续 

 看不懂汇编的同学可以这么理解:
 mov ds,ax 就相当于是 ds = ax

这里我们不关注代码含义只关注指令连续这一概念

由图可知:

下一条指令地址0x0002是因为上一条指令字节为2,所以是0x0000+0x02,依次类推

可以得出该公式:

当前指令地址+当前指令长度=下一条指令地址

逻辑上连续

1 jmp start    跳转到第二行的 start ,这是 CPU 直接执行的指令
2 var dd 1     定义变量 var 并赋值为 。分配变量不是 CPU 的工作
               ;汇编器负责分配空间并为变量编址
3 start:       标号名为 tart ,会被汇编器翻译为某个地址
4 mov ax , 0   ax 赋值为0

用JMP跳过数据段,形成逻辑上的连续,如果不用jmp跳过CPU可能会出现异常。

你可以在程序中加一堆jmp,来达到逻辑上连续,只是不是很美观

平常开发中,我们要尽量把数据和方法分开,有点MVC设计模式的思想(设计模式🐂),其实这不仅仅为了美观,也可以提高程序运行效率。

  • 提高CPU内部缓存的命中率:缓存起作用是因为他的局部性原理,CPU内部也有缓存,将其分开来写,可以增强程序局部性,同时CPU也有针对代码和数据的两种不同缓存机制
  • 节省内存:内存中无需存在多个相同的代码段,只需写一个共享的即可,也就是函数

谁赋予的数据段和代码段属性

众所周之,数据段是可以更改的,但是代码段是万万不可的,那如何分开两个段呢?

首先肯定不是我,我写代码就已经够辛苦了,怎么还有时间给你分几个段嘞。

其次也不是编译器,编译器只负责编译出来代码,把代码段放在一个连续的区域,数据段放在一个连续的区域。

既然不是程序员也不是编译器那我们只能看看CPU了,在CPU保护模式下有一个数据结构叫做GDT 全局描述符表(好熟悉啊,上一代操作系统被他折磨了很久),在表中有一个段描述符,保存了每一个段的元信息,之后会详细说,你可以理解为是人的身份证。🆗,既然表有了,谁去填表呢。

我们来看看整个的流程

1,编译器负责给程序片段分类,分出可写数据段,只读代码段。

2,操作系统通过设置GDT全局描述符表来构建段描述符,指明段的位置,大小,属性等

3,CPU的段寄存器被操作系统赋予了相应的选择子,从而确定指向段,执行时根据段属性来确定是否违规

所以说整个保护模式 是由 编译器,CPU,操作系统一起构成的,负责检测指令中的违规行为。

内存的段和编译出来的段是一个东西吗 

是也不是

毕竟编译出来的段是人为划分,为了“美观”而进行的规划,方便管理的,好像和内存段也没什么太大关系。

但是能如果进行分段了,他又是实实在在以内存段的形式划分,然后根据段基址+段内偏移地址去访问的(具体的就不细说了)他好像又是一个东西。

⑥ 物理地址,逻辑地址,有效地址,线性地址,虚拟地址

好多地址啊,看的就头疼,我还是说说我的邮箱地址吧,v50

  • 物理地址

物理地址具有唯一性,是物理内存的真正地址,不管你是什么模式,什么虚拟地址,线性地址,什么牛马,最终都要通过物理地址访问内存。

实模式下,段基址+段内偏移地址,输出的就是物理地址

  • 虚拟地址和线性地址

保护模式下,CPU不能让你肆无忌惮的访问内存(因为很有可能是操作系统的内存段),

所以段地址+段偏移地址就成为了线性地址,它相当于一个索引,根据这个索引再GDT中找到相应的段描述符(之前说过GDT是一个表,可以理解为数组),而未开启地址分页功能,线性地址可以直接作为物理地址使用,直接访问内存。开启分页功能此时线性地址又叫虚拟地址,虚拟地址需要根据CPU页部件转换成具体的物理地址,才能送到地址总线访问内存。

 

  • 有效地址,逻辑地址

有效地址是段内偏移地址,也叫做逻辑地址,即程序员可见的地址,之后你们就会看到了

⑦ 平坦模型

在实模式下,访问超过64KB的内存,需要指定不同的段基址。但是在保护模式下呢,他是32位的,所以段内偏移地址也是32位的,一个段就可以访问所有内存,也就是说段的大小可以是地址总线能达到的范围。整点复杂的,经过对比就会发现平坦模式要比多段模式不断切换段地址舒服多啦,而且如果想进行地址回绕还需打开A20总线

A20总线,地址回绕

地址回绕:我们知道在实模式下,16位寄存器最大可以访问 1MB(段基址*4 + 段偏移地址别忘了噢),但是如果内存超过1MB是完完全全有可能的吧,比如你 0xFFFFF此时要多+1,就要回绕到0,而打开第A20总线便可以实现地址回绕(小trick,进位消1补0)

 ⑧ 段寄存器有哪些,位宽又是多少

 cs -代码段寄存器( Code Segment Register ),其值为代码段的段基值
 DS一-数据段寄存器( Data Segment Register ),其值为数据段的段基值。
 ES -附加段寄存器( Extra Segment Register ),其值为附加数据段的段基值,称为“附加”是
因为此段寄存器用途不像其他 sreg 那样固定,可以额外做他用。
 FS一一附加段寄存器( Extra Segment Register ),其值为附加数据段的段基值,同上,用途不固定,
使用上灵活机动。
 GS一一附加段寄存器( Ex Segment Register ),其值为附加数据段的段基值。
 SS 一一堆枝段寄存器( Stack Segment Register ),其值为堆枝段的段值。

32位CPU有实模式和保护模式,不管是什么模式,段寄存器位宽都是16位

⑨ 为什么Linux的应用不能再Windows系统下运行

看到书里面有这个我就拿出来说一说,Windows NO.1

  • 系统API不同:Linux是通过中断调用,Windows是通过DLL
  • 格式不同:linux是elf,windows是PE

⑩ 局部变量和函数参数为什么要放到栈中

这个问题其实很好,最重要的原因就是  局部性,不管是局部遍历还是函数参数都是局部的,你永远不知道要调用多少次,放到内存中纯粹浪费空间,放到栈中就十分方便清理,用了就推出。

⑩① 大端字节序,小端字节序

好熟悉啊,之前就在Redis ZipList里面见过这个概念(Redis🐂),今天我也来梳理梳理。

首先内存是以一个字节为最小单位

首先问个问题,0x1234这个数,数值的高位应该放在前还是后捏

答案是 : 都可以于是便有了大端与小端字节序

小端字节序是数值的低宇节放在内存的低地址处,数值的高宇节放在内存的高地址。

0x1234就是  34 12

大端字节序是数值的低宇节放在内存的高地址处,数值的高宇节放在内存的低地址。

0x1234就是 12 34

两者也有不同的优势

  • 小端字节序就是方便类型转换,比如short转int,就可以直接在地址前面加00000

0x1234 变成 0x00001234

  • 大端字节序就是方便硬件读取,可以直接读取到符号位,减少读取时钟

从左到右嘛,从低到高,所以先读0x12就可以判断是否为正负了

架构的字节序选择

小端:x86,ARM

大端:IBM,ARM 还有一堆不太认识

但是网络的字节序是大端字节序,所以x86在发送时要转

⑩② BIOS中断,DOS中断,Linux中断

中断这一事件讲起来有点复杂,可能今天这篇文章讲不完,所以这里草草说一下,我会放在之后做笔记介绍。

BIOS中断会使用INT中断指令,去中断向量表中调用中断

DOS中断会使用INT中断指令,但是他只有0x21一个中断号,不过他会根据ah寄存器的值实现多功能中断调用

Linux中断会使用INT 0x80中断指令,只不过进入中断程序后会根据eax去寻找相应的中断调用

⑩③ section和segment

section 称为节,是指在汇编源码中经由关键字 section segment 修饰、逻辑划分的指令或数据区域, 汇编器会将这两个关键字修饰的区域在目标文件中编译成节,也就是说“节”最初诞生于目标文件中

segment 称为段,是链接器根据目标文件中属性相同的多个 section 合并后的 section 集合,这个集合 称为 segment ,也就是段,链接器把目标文件链接成可执行文件,因此段最终诞生于可执行文件中 我们 平时所说的可执行程序内存空间中的代码段和数据段就是指的 segment

⑩④ CPU是如何执行下一条指令的

老实说这个问题,有够底层的,其实按道理我们直接答程序计数器即可。那我们能不能用mov去修改CS:IP的值呢,这个还是要深扒的话可以从CPU体系架构开聊。

x86架构,其程序计数器并不是一种寄存器,而是CS:IP,mov指令只能单改其中一种,如果只修改一个就有可能导致错误,如果修改两次又不是原子性的,所以便提供了jmp,ret这些来进行原子性操作

arm架构:他拥有专门的寄存器,所以可以直接用mov

先讲到这里吧,其实还有很多问题,但是我决定还是在后面的章节在一一讲,敲了快一万字了,之后有些问题我们再一一解决,下一章就要开始操作系统之旅了!

本文含有隐藏内容,请 开通VIP 后查看