栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > C/C++/C#

详解操作系统之启动

C/C++/C# 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

详解操作系统之启动

视频链接

文章目录
  • 前言
    • 本篇主要讲解关于操作系统启动方面的知识。
  • 一. 一些汇编基础,必看
    • 1.寄存器
    • 2.寻址方式
    • 3.段寄存器移位操作
    • 4.汇编指令
    • 6.零碎的知识点
  • 二、从打开电源开始
  • 三、0磁道0扇区(boot引导扇区)的代码
    • bootsect模块总结
  • 四、现在是setup来干活了
    • setup模块总结:
  • 五、到了操作系统核心部分了
    • head.s(汇编文件)
    • main函数,一堆init(),初始化一堆数据结构
    • 初始化完毕
    • system模块总结
  • 总结


前言 本篇主要讲解关于操作系统启动方面的知识。 一. 一些汇编基础,必看 1.寄存器

寄存器就是用来暂时存放数据的。

(1)cs(Code Segment):代码段地址寄存器 ip(Instruction Pointer):指令指针寄存器
①CPU将cs、ip中的内容当做指令的段地址和偏移地址,用它们合成指令的物理地址。
②CPU将cs、ip指向的内容当做指令执行(即PC)

(2) si(Source Index):16位寄存器,源变址(偏移地址)寄存器,存放相对于ds段之源变址指针
ds(Data Segment register) :16位段寄存器,作用是存放当前数据段的段地址
ds通常与si连用,ds:si

(3)di(Destination Index):16位寄存器,目的变址(偏移地址)寄存器,存放相对于 es 段之目的变址指针
es(Extra Segment):附加数据段的段基址寄存器
di一般与es连用,es:di

(4) ss(Stack Segment):堆栈的栈顶段寄存器
sp(Stack Pointer):栈顶的偏移地址
ss与sp连用,ss:sp

(5)ax(accumulator):累加器寄存器,常用于存放算术、逻辑运算中的操作数或结果
(ah:ax的高位 ,al:ax的低位)
bx(base):基址寄存器,常用来存放访问内存时的地址。
cx(count): 计数寄存器,在循环、串操作指令中用作计数器
dx(data):数据寄存器,在寄存器间接寻址中的I/O指令中存放I/O端口的地址
参考自此篇文章







2.寻址方式

参考的第一篇文章
参考的第二篇文章

本课只要知道实模式下,对于段寻址方式,物理地址=段地址左移4位+偏移地址
(cs<<4+ip)





3.段寄存器移位操作

①:首先通常我们所说的地址是指内存中的地址
②:地址表示方法通常为16进制,一个16进制=4位2进制,0x16进制,表示以0x开始的数据表示16进制,0x只是一个标志,表示0x后面的数据为16进制(0x只是一个前缀而已)0x17=17(H)。
③将段地址左移4位,就是针对地址的二进制形式左移4位。
左移4位相当于16进制乘10,10进制乘16
右移4位相当于16进制除10,10进制除16
2^4=16(o)=10(H)

1234H=0x1234=1001000110100B,左移4位
↓左移4位
12340H=10010001101000000B

0x7c00=111110000000000B
↓右移4位
0x07c0=000011111000000B





4.汇编指令

①:mov指令(传送)
格式:mov 目的数,源操作数
作用: 将源操作数的值复制到目的数当中去

汇编语言中的#号代表的寻址方式是立即寻址,#号去掉,寻址方式变为直接寻址
1.mov ax,#BOOTSEG (BOOTSEG=0x07C0)
这是将BOOTSEG的值(07C0)赋给ax,ax最后也=0x07C0

2.mov ax,BOOTSEG
此时,BOOTSEG就不是值了,变为地址,这条指令将BOOTSEG(0x07c0)的地址赋给ax,ax=07c0的地址,07c0本来就是个地址,ax这里相当于地址的地址

mox指令注意事项:(1)目的数不能是CS、IP
(2)目的数和源操作数类型要一致
参考的文章1
参考的文章2

②:sub指令,减法指令
1.sub ax,bx
就是ax中的值减bx中的值,然后把这个结果放入ax中
2.sub r0,#1
直接把1放入到r0寄存器中
3.sub r0,r0,#1
相当于r0=r0-1

③:xor指令,异或操作
作用:对两个操作数进行逻辑异或操作
异或:两个数相同为0,不同为1


④:cmp指令
格式:cmp 操作对象1,操作对象2
作用:计算操作对象1-操作对象2,但不保存结果,仅仅根据计算结果对标志寄存器进行设置。比如cmp ax,ax。他是做ax-ax的运算,结果为0,但并不在ax中保存,仅影响flag的相关各位


⑤:int 指令
格式:int n,n为中断类型码,它的功能是引发中断过程,我们通常叫几号几号中断
int 0x13:读软盘(读数据)
int 0x007:显示属性
int 0x10:读光标
int 0x15:获取物理内存大小


⑥:cli/sti
cli:禁止中断发生(关中断)
sti:允许中断发生(开中断)
1.这两个指令只能在内核模式下执行,不可以在用户模式下执行
2.在cli指令后需要有与其配对的sti指令


⑦:rep指令,rep→repeat,重复执行的意思,直到CX=0

MOVSB:传送一个字节,之后SI和DI(或者ESI和EDI)加/减1
MOVSW:传送一个字,之后SI和DI(或者ESI和EDI)加/减2
MOVSD:传送一个双字,之后SI和DI(或者ESI和EDI)加/减4

单纯的movsb/ movsw/ movsd只能执行一次,如果希望处理器自动地反复执行,可以加上指令前缀rep;在寄存器CX(16位模式)或者ECX(32位模式)中设置传送的次数。当CX/ECX不等于0时,则执行movsb/ movsw/ movsd,执行后,CX/ECX的值减一,直到减为0为止。
参考自此文章


⑧:跳转指令
一、无条件跳转: JMP;
二、根据 CX、ECX 寄存器的值跳转: JCXZ(CX 为 0 则跳转)、JECXZ(ECX 为 0 则跳转);
三、根据 EFLAGS 寄存器的标志位跳转, 这个太多了.
请参考此文章

6.零碎的知识点

①:高位,低位:一般一个16位(双字节)的数据,比如FF1A(16进制)的高位字节为:FF,低位字节为1A
如果是32(4字节)的数据,比如3F68415B,它高位字为:3F68,低位字为:415B
右边是低位,左边是高位


②.s代表汇编的意思,表示这一段代码代表的是汇编语言,汇编可以直接控制机器







二、从打开电源开始


我们知道,计算机工作的方式是取指执行、取指执行,那我们开机后,只要通了电,计算机就会工作,那计算机的第一条指令是什么?

实际上,第一条指令是已经设置好的

1.所以上电后, 80x86结构的CPU将自动进入实模式,而且cs ip已经被置好了初值(这是由计算机体系结构设计者决定的),然后cpu寻址,cs<<4+ip,cpu寻址到0xFFFF0,所以打开电源的首地址就是0xFFFF0(这里是ROM BIOS映射区)。

2.找到了地址后,自动执行代码,这里的代码会检查RAM,键盘,显示器,软硬磁盘,然后将磁盘0磁盘0扇区读入0x7c00处,然后设置cs=0x07c0,ip0x0000,cpu会执行跳转指令跳转到该处执行

3.其中0磁道0扇区,就是操作系统的引导扇区,也就是操作系统的第一段代码。
4.OK,然后我们跳转到0x7c00处,启动机器运行,现在真正来到OS的部分了




注:
①:其中BIOS=Basic I/O system,基本输入输出系统
ROM:只读存储器
②:BIOS并不是放在内存中的,而是放在ROM(只读存储器).而BIOS由硬件来加载,被加载到 0xF0000~0xFFFFF处.
③:BIOS是存储在ROM里面的,不用调入内存就可直接被CPU运行.
④:BIOS 运行初期,CPU 其实是不能访问内存的。BIOS 所在的 FLASH 是那种可以被 CPU 直接寻址的 FLASH 芯片。
⑤:BIOS会读取硬盘的第一个扇区(0扇区),内含512个字节。这些数据叫做主引导记录(Master Boot Record简称MBR)。
一般说来,它包含两个极其重要的部分:一个是位于MBR开头的操作系统相关的引导程序,另一个是紧跟其后的磁盘分区表。
BIOS 丝毫不关心这些事情:它只是简单的加载MBR的内容到内存地址0x7C00处,并跳转到此处开始执行,不管MBR里的代码是什么。

参考自此文章





三、0磁道0扇区(boot引导扇区)的代码

BIOS映射区的代码将磁盘0磁道0扇区的代码读入0x7c00处后,我们来看一下这里面的代码

BOOTSEG=0x07C0
INITSEG=0x9000
SETUPSEG=0x9020,来看从start:开始的代码

mov ax,#BOOTSEG     mov ds,ax           #07c0先赋给ax,再赋给ds,ds=0x07c0
mov ax,#INITSEG        mov es,ax        #9000先赋给ax,再赋给es,es=0x9000
#所以现在当前数据段基址:0x07c0          目标段基址:0x9000
mov cx,#256                             #cx=256
sub si,si                sub  di,di
 #自减,si=0;ds:si(当前数据段地址)=0x07c00              
 #di=0;es:di(目标段地址)=0x90000
rep movw                #移动字,移动cx=256个字(512个字节),从7c00移动到90000处
                        #  也就是将0x7c0:0x0000处的256个字移动到0x9000:0x0000处
jmpi go,INITSEG      # 把go赋给ip,INITSEG赋给cs,cs=0x9000,jmpi是段间跳转

所以干了我们一件事:把7c00处的代码移动的90000处,为什么要移动呢?实际上是为了腾出空间

0磁道0扇区放的就是操作系统的引导扇区(也就是boot扇区),引导界面我们都见过吧,(至少见过windows的logo),我们的操作系统是放在磁盘中的,我们只把它的引导扇区载入进来了,他还有setup模块和system模块,所以现在我们要将他剩下的部分全部载入到内存当中来了,这样才能取指执行。


↑
各个模块功能介绍:
①:boot扇区模块:操作系统引导模块
②setup模块:操作系统加载模块
③:system模块:操作系统的内核模块

go:
.........
// 都是一些简单的赋值指令
// 注意一下mov es,ax(es=ax=0x9000)


load_setup:            //好,从这里开始,要载入setup模块了
.......
mov bx,#0x0200           //bx=0x0200
mov ax,#0x0200+SETUPSELEN          int 0x13 
 //看上面的图,setup模块️四个扇区,
 前面已经载入一个引导扇区了,
 所以我们从第二个扇区开始(ah=02),
 一共载入四个扇区。
 加载到哪呢?加载到0x90200,为什么是90200呢
 因为es:bx合成了这个内存地址
 如何载入呢?13号中断
 ↓


所以我们完成了另一件事:载入setup模块到0x90200处




jnc ok_load_setup(载入setup扇区后干嘛呢,操作系统不止这么一点吧,还有一些部分吧,所以我还要继续往下读)


①:mov bp,#msg1
msg1:就是你要显示在屏幕上的字符串,比如windows的logo,不过在我们这里做的比较简单而已.
那么如何让他显示呢:调用10号中断
而bp就是你要显示的字符串的地址

②:mov cx,#24呢就是你要显示的字符串的长度

③:mov ax,#SYSSEG
mov es,ax (es寄存器存放目的段基址)
此时es=ax=SYSSEG=0x1000,也就是说我要把system载入到0x10000处

④:call read_it,载入setup模块后我们要载入system模块了

所以我们在最后一句话jmpi 0,SETUPSEG前完成了两件事:
1.加载完setup后打印一段信息
2.载入system模块






具体是如何载入system模块的呢
函数read_it读入system模块到0x10000处
mov ax,es
// ax=es=0x1000(在载入system前es已经被置为0x1000)

载入system模块后,read_it调用iret返回,返回后是jmpi 0,SETUPSEG。
SETUPSEG=0x9020,ip=0x0000,转入到setup执行





bootsect模块总结

bootsect.s(引导扇区代码)一共干了这几件事
1.将自己从0x7c00移动到0x90000处,腾出空间
2.载入setup模块到0x90200处
3.载入setuo模块后打印出logo
4.载入system模块到0x10000
5.转入到setup模块执行,bootsect使命结束







四、现在是setup来干活了

首先,setup要干什么????
根据名字就能猜到,他应该要完成操作系统启动前的设置
↓
从现在开始,OS开始接管硬件,获取计算机各种信息(内存大小等),为操作系统初始化做好准备,当获取这些信息后,OS会形成很多数据结构来管理这些设备

mov ax,#INITSEG       mov ds,ax 
// ax=ds=INITSEG=0x9000

mov [0],dx     // [0]→0x90000,取出光标位置(包括其他硬件参数)到0x90000处,
也就是说此时内存0x90000处存放的是光标位置

int 0x15   //获取物理内存的大小
mov [2],ax     //[2]→0x90002,此时0x90002处存放扩展内存数

然后关中断,mov ax,#0x0000  //ax=0x0000
再开中断
              

所以我们这里干了一件事:获取各种硬件参数,让ax=0x0000



do_move:mov es,ax   //es=ax=0x0000
       add ax,#0x1000 //ax=0x1000
       cmp ax,#0x9000    jz end_move
       mov ds,ax       // ds=ax=0x1000
         sub di,di
       sub si,si
       mov cx,#0x8000
       rep 
       movsw
    //其实就是把 0x10000-0x8ffff 处的代码(system模块)整块移动到0地址处

所以这里又完成了一件事:把system模快 从0x10000-0x8ffff 整块移动到0地址处,然后OS一直停留在0地址处,为操作系统的初始化做好准备。
↓这也回答了为什么bootsect要把自己从0x7c00处移动到0x90000处

要把0x90000以下的地方空出来留给OS的和代码



把system模块移动到0地址处后,setup模块差不多就要结束使命了,但操作系统不能断啊,所以setup还做了最后一件事:进入保护模式

jz end_move             //jz:如果标志位为0则跳转,
      所以当移动完system模块后,我们要进入到end_move函数中 

(图片上面是将system模块移动到0地址处,不是将setup模块移动到0地址处)

所以我们完成了一件事:进入保护模式,并初始化idt,gdt表







来看看这个进入保护模式的命令到底是什么

jmpi 0,8

分析下这条指令
①:如果按原来的寻址方式,cs<<4+ip=80H,跳到80H处,但我们应该要跳到0地址处,跳到0x80处只有一种后果,那就是死机,80H是非法指令
②:所以此时寻址方式发生了改变
③:为什么要改变,cs是16位段寄存器,ip也是16位。cs左移4位最多达到20位,2^20=1M(计算机的寻址空间,能访问到的内存只有1M,计算机能力太弱,而我们现在内存有4G,要访问4G的内存,传统的cs<<4+ip肯定不行)
④:1M=16位机
4G=32位机=2^32→处理32位2进制数,寻址范围4G,所以我们要从传统的16位机切换到32位机
⑤:16位机和32位机的本质区别:CPU解释方式不一样(CPU处理的数据宽度,参与运算的寄存器的数据长度,寻址范围)
⑥:切换成新的寻址模式(32位模式)后,这时能访问到的内存就变得很大了




那么靠什么切换呢?cr0,一个很酷很猛的寄存器
↓

mov ax,#0x0001
mov cr0,ax

如果cr0的末位为0,那么就是实模式;如果被置成1,那么就启动保护模式

执行完上两条指令后,此时cr0的末位为1,CPU开始走另一条解释执行的电路








那CPU到底要怎样解释执行呢?

1.保护模式下的int n和cs:ip解释不再和实模式一样

2.int n解释通过查idt表,cs:ip解释通过查gdt表

3.查表其实是通过硬件来实现的,其实原来实模式cs:ip的解释执行也是通过硬件实现的,主要目的就是为了速度快

4.保护模式下,cs叫选择子【cs中存的为查表的索引(也就是下标)】,实模式下存的是地址,那么现在真正的地址放在gdt表中

①:cs=8,选择表中对应的表项,从项中取出基址,加上ip产生32位地址
②:所以一定要有这个表,他也叫全局描述符表(gdt),且表中一定要有内容(setup已经帮我们临时做好了这个表)
↓

1.一个word16位
2.每个表项64位(也就是一行),寻址以字节为单位




表初始化后,现在来查那个表

这个地址的构成确实很难理解
参考的第一篇文章
参考的第二篇文章
①:GDT就是一个数组,每一个元素就是一个描述符,多个组合一起就构成了全局描述符表。而每一个描述符共64位,包含了以下的这些信息:段基址、段长度、属性。原来的段寄存器,比如CS,DS等存的值则不是段偏移了,而是GDT的索引,通过该索引就可以找到对应的描述符。

②:如何根据选择子找到表中第几项呢?
先看看选择子的组成

所以我是这么理解的,欢迎指正


③那么找到了第一个表项后,就可以根据表项里的内容得到段基址了

如何得到段基址呢?
先来看看GDT表项的组成,和表项中给出的地址
图片1↓
图片2↓


①:我们来详细剖析一下这两张图
一个表项64位(64个二进制数,16个16进制数),一个word16位(4个16进制数),四个word。
注意:一行中,4个word从左至右,是从最低的4位到最高的4位。也就是0x07FF是最低位,0x00C0是最高位,所以总共的16位16进制数是这么表示的:
0x00C0 9A00 0000 07FF(高位←低位,63←0,一个16进制数=4位二进制数)

②根据gdt表组成格式,基地址=第15位→第31位 + 第32位→39位 + 第55位→64位,因此得到段基址:0x0000 00 00(为了让你看清,我特意加了空格)

③:所以最后得到段基址:0x00000000
也就是说 jmpi 0,8,查完表后得到的基址为0x0000,因为ip也等于0,最后最后的到的物理地址也是0,跳到0地址处,此时0地址处是system模块,所以我们跳到system模块去执行,setup使命结束




setup模块总结:

1.获取各种硬件参数,为初始化做准备
2.将system模块移动到0地址处,操作系统核心代码一直在0地址处
3.进入保护模式,跳到system模块,setup使命结束







五、到了操作系统核心部分了

现在进入到system模块了。
bootsect由bootsect.s编译而来,setup由setup.s编译而来,而system由一堆源码编译而来 所以我怎么保证我要执行的就是system模块的第一段代码呢?
↓
严格控制OS启动顺序是由bootsect→setup→到system,否则死机。
也就是说我最后要形成操作系统的这个样子,必须是由BIOS读bootsect,bootsect把setup和system读进来,再由setup跳到system的第一个部分,这样操作系统才能顺利有序的执行,否则死机
↓
所以我们还要考虑一件非常重要的事,编写操作系统的控制代码
↓
Linu中非常重要的makefile文件,在编写大型软件时(OS也是个大型软件)用来控制软件合成结构(为什么我们平常在写代码时,不需要考虑软件合成结构?因为集成开发环境已经帮你做好了。)
↓makefile简介
①一堆源码通过makefile产生OS镜像文件image,镜像样子会被 (也必须被) 固定成一个模样
②:image可以通过工具写到磁盘任意扇区(所以我们现在把他写到0磁道0扇区)
↓image就变成这样


所以此时,boot模块就有了,setup模块就有了,system模块也有了。
那么我再用image进行开机引导,OS就被建立了起来(待会我们再把它初始化,就产生了桌面,OS启动完成)





①:阅读Image:后面的代码我们可以知道,Makefile是一种树状结构。
②:先是boot依赖于→bootsect
后是setuo依赖于→setup tools等
tool又依赖于一堆模块等…
③:所以很明显,模块依赖于模块(父依赖于子,每个子完成了,树才能建立起来),模块都有了,就执行链接指令,把他们链接在一起,形成新的模块

(举个例子,对于system模块的形成:首先要有.c模块(源码你总得有吧),.c有了就产生.o文件,然后bootsect和setup模块也有了(当然还有一些其他的东西也要有),就链接在一起形成一个模块,这样system模块就有了,system模块中的第一个是head.s,(system:boot/head.o init/main.o,head在main的前面)),.s代表汇编文件
↓
image就产生了



head.s(汇编文件)

所以我们jmpi 0,8其实是跳到head.s去执行,head.s呢就是

我们这里调用了 call setup_idt,call setup_gdt,相当于又建立了gdt表和idt表。
因为:①:之前那个gdt表是临时在setup模块建立的,目的是为了能jmpi 0,8
②:现在到了system模块,OS真正要工作了,而且我现在一直在保护模式下,所以我需要再建立表 gdt,idt
1:开启20号地址线是为了能访问4G内存
2:head.s就是用来设置一些数据结构(gdt,idt,页表等)


注:在保护模式下是32位汇编,和之前的不一样了
下面这张图介绍了三种汇编
↓

参考此篇文章 碰到了再简单查阅一下就行
对于内嵌汇编我们暂时不要管(在系统调用时会讲到),就是在一些.c文件中,有些指令必须要严格按照我们的方式执行时采用内嵌汇编



jmp after_page_tables  //跳到另外一个函数去执行了


ret要弹出参数,我们之前push进去了几个参数(0,0,0,L6,main)

P3=0
P2=0
P1=0
返回地址=L6
执行ret后,执行main函数,如果main要返回了,返回到L6,又jmp L6,死循环,所以当main返回的时候,进入死循环,系统死机 ,main永远不应该返回(main开机后弹出的是个桌面,你什么时候见过(正常情况下)桌面消失过吗,OS是一个永不停止的程序)



main函数,一堆init(),初始化一堆数据结构

用一堆函数来初始化各个设备(形成各个设备对应的数据结构),操作系统是利用这些数据结构来管理这些设备的,所以必须得初始化

①:如何保证main函数永远也不会返回
↓

if(!fork()){init();}
........ 下面还有几句代码,省略

这样就能保证它不会退出


②:来看一下内存的初始化函数,men_init()
↓

1:其实就是初始化mem_map这个数组(表格),当数组(表格)为0时,没有内容,内存没有使用

2:>>=12,也就是右移12位,除以2^12=4K,4K作为一片(一页)内存,为什么是4K,这在之后的内存管理中会讲

while(end_men -- >0)   //从存放完操作系统的那个地方开始,每次往下减4K,也就是往下移动位置
men_map[i++]=0;       //减完后,这一片置为0

3.所以所谓内存初始化就是形成这个表格,用这个表格来表示内存中哪些是使用的哪些是没有使用的(0在最上面)
↓(前面的几个100放的是操作系统)
这个men_map有多长,都是由传递进来的参数(end_mem)决定的,这些参数来自0x90002,由setup读入内存大小(别忘了setup要干什么事,它要读硬件参数)





初始化完毕

其他设备同理初始化,初始化好后,操作系统就可以工作了,你就可以见到桌面了





system模块总结

1.head.s会设置一些数据结构,如gdt、idt表、页表等
2.然后从head.s跳到main.c函数,main.c函数会初始化各个设备对应的数据结构,因为OS要管理各个设备是通过他们对应的数据结构来管理的。
3.main.c是一个永远也不会停止的函数





总结

全部来自此文章

1.加电稳定后,CPU进入实模式并从 BIOS引导程序 入口地址 0xFFFF0 开始自动执行代码。BIOS引导程序 初始化硬件并在物理地址0处放置大小为1KB的中断向量表,它将 磁盘引导块程序 bootsect.s 读入内存地址 0x7C00,CPU执行跳转指令跳转到该处执行。

2.磁盘引导块程序 bootsect.s 执行时把自己移动到0x90000 处,同时将 操作系统加载程序 setup.s 加载到内存 0x90200 处,操作系统内核 system 模块加载到内存 0x10000 开始的地方。

3.操作系统加载程序 setup.s 利用BIOS中断读取机器系统参数,并将参数覆盖保存 bootsect.s 的位置,同时将 system 模块整体向下移动到 0x0000 处覆盖BISO中断。最后,CPU进入保护模式,跳转到 system 初始化一些重要的数据结构,为将来操作系统管理这些设备做准备,从而成功启动操作系统。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/913205.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号