操作系统实现——编写MBR

前置知识

计算机的启动过程

问题1:什么是载入内存

  • 为什么程序要载入内存
    第一,CPU 的硬件电路被设计成只能运行处于内存中的程序,这是硬件基因的问题
    第二,可能为了方便统一,都在内存中运行程序,操作系统和硬件设计都省事
  • 什么是载入内存
    大概分两个部分
    1.程序被加载器(软件或硬件)加载到内存某个区域
    2.CPU CS: ip 寄存器被指向这个程序的起始地址。
    加载器本质就是一堆函数组成的模块

软件接力第一棒BIOS

实模式下的1MB内存布局

在这里插入图片描述BIOS 全称叫 Base Input & Output System ,即基本输入输出系统。
问题2:为什么为基本?

  • 0xF0000-0xFFFFF这块内存是ROM,里面存放的就是BIOS的代码,BIOS的主要工作就是检测、初始化硬件,硬件自己提供了些初始化的功能调用,BIOS直接调用就好了,BIOS还建立了中断向量表,可以通过“int中断号”来实现相关的硬件调用,由于就 64KB大小的空间,不可能把所有硬件的IO操作实现得面面俱到,所以挑些重要的、保证计算机能运行的那些硬件的基本 IO 操作,就行了。这就是 BIOS 称为基本输入输出系统的原因

问题3:为什么我们插在主板上的物理内存并不是CPU眼里"全部的内存"
在这里插入图片描述
问题4:BIOS是如何启动的?

  • BIOS 是计算机上第一个运行的软件,所以它不可能自己加载自己,由此可以知道,它是由硬件加载的,那这个硬件是谁呢?相当于是只读存储器 ROM,只读存储器中的内容是不可擦除的,这种存储介质是用来存储一成不变的数据的,BIOS 代码所做的工作也是一成不变的,于是 BIOS顺理成章地便被写进此 ROM. ROM也是块内存,内存就需要被访问,只要访问此处的地址便是访问了 BIOS ,这个映射是由硬件完成的。
  • BIOS 本身是个程序,程序要执行,就要有个入口地址才行,此入口地址便是0xFFFF0,在开机的,瞬间,也就是接电的一瞬间,CPU CS: IP 寄存器被强制初始化为 0xF00O0: 0xFFF0,在实模式下的段基址要乘以16 ,也就是左移 4位,于是 0xF000:0xFFF0的等效地址将是0xFFFF0

问题5:超过寄存器宽度怎么办

  • 溢出的部分就会回卷0

接力赛的第二棒

BIOS最后一项工作校验启动盘中位于0盘0道1扇区的内容。
而0盘0道1扇区就是磁盘上最开始的那个扇区
如果此扇区末尾的两个字节分别是魔数0x55和0xaa,BIOS便认为此扇区中确实存在可执行的程序(此程序便是久闻大名的主引导记录MBR),便加载到物理地址0x7c00,随后跳转到此地址,继续执行
MBR的大小必须是512字节,这是为了保证0x55和0xaa这两个魔数恰好,出现在该扇区的最后两个字节处,即第 510 字节处和第 511 字节处,这是按起始偏移为算起的
MBR实现如下:

;------------------------------------------------------------
SECTION MBR vstart=0x7c00         
   mov ax,cs      
   mov ds,ax
   mov es,ax
   mov ss,ax
   mov fs,ax
   mov sp,0x7c00
   mov     ax, 0x600
   mov     bx, 0x700
   mov     cx, 0          
   mov     dx, 0x184f	 

   int     0x10            ; int 0x10

   mov ah, 3		 
   mov bh, 0		 

   int 0x10		; 

   mov ax, message 
   mov bp, ax		

   mov cx, 5		
   mov ax, 0x1301	 
   mov bx, 0x2		 
   int 0x10		; 

   jmp $		

   message db "1 MBR"
   times 510-($-$$) db 0
   db 0x55,0xaa

问题6:什么是地址

  • 地址只是数字,描述各种符号在源程序中的位置,它是源代码文件中各符号编译文件开头的距离,由于指令和变量所占内存大小不同,故相对于文件开头的偏移量参差不齐
  • 本质上,程序中各种数据结构的访问,就是通过"该数据结构的起始地址+该数据结构所占内存的大小"来实现的
  • 编译器给程序中各符号(变量名或函数名等)分配的地址,就是各符号相对于文件开头的偏移量
    问题7:vstart关键词含义
  • vstart是虚拟起始地址(注意:这与X86CPU中开启分页后的虚拟地址是两码事),这里面有两个重要的概念,1 虚拟,2 起始
  • vstart的作用是为section内的数据指定一个虚拟的起始地址,也就是根据此地址,在文件中是找不到相关数据的,是虚拟的,假的,文件中的所有符号都不在这个地址上
  • 在section中添加vstart,这个参数是让编译器将section中的数据的地址以vstart的值为起始,不再从整个程序开头算起,只有以程序开头0算起的地址才是真实存在的,在这个地址上能访问到相应的符号,所以不以程序开头算起的地址,必然在程序内部不存在,是虚拟的
  • 地址访问策略是根据程序中给出的地址,到地址处去拿东西,所以这个东西要提前在那个地址处准备好了才行,必须保证自己想要的东西被加载到那个物理内存位置,这就是程序加载器做的事,根据文件头中给出的各段的位置,将它们加载到内存中的相应地址,这样用户程序才能访问到自己所需要的东西
  • 编译器只负责编址,它只会将数据相对于文件开头的偏移量作为该数据的地址,全是以0起始的,所以把整个程序加载到内存地址0处,程序运行肯定是没问题的,但基本上没有哪个程序能有如此待遇在0物理地址上走上一回
  • 编译器以相对于文件开头偏移来编址的好处是利于重定位,整个文件新加载到某个地址后,可以以这个地址为段基址,文件内的数据的地址是以0为开始算的,所以它们直接就可以用作段内偏移地址了
  • vstart只是告诉编译器以新的数字作为后面数据的地址的起始值,它本身没有改变数据本身在文件中的地址(相对于文件开头的偏移量),假如可以更改,那之前的空隙需要使用0来填充,总不该用了vstart后,文件就跟着变大
  • 如果程序员用vstart指定了新的地址,干涉了编译器编址的方式,程序员要清除地知道自己需要的东西是否会出现在物理内存中这个新的地址处

问题8:为什么mbr能够运行正常

  • mbr用vstart=0x7c00来修饰的原因,是因为开发人员知道要被加载器(BIOS)加载到物理地址0x7c00,mbr中后续的物理地址都是0x7c00+
  • 用vstart的时机是:我预先知道我的程序将来被加载到某地址处,程序只有加载到非0地址时vstart才是有用的,程序默认起始地址是0

CPU的实模式

问题9:CPU的控制单元做什么事情

  • 控制单元是CPU的控制中心,CPU需要经过它的帮助才知道自己下一步要做什么,而控制单元由指令寄存器IR,指令译码器ID,操作控制器OC组成,程序被加载到内存后,指令指针寄存器IP指向内存中下一条待执行指令的地址,控制单元根据IP寄存器的指向,将位于内存中的指令逐个装载到指令寄存器中,然后指令译码器将位于指令寄存器中的指令按照指令格式来解码,分析出操作码是什么,操作数在哪里之类的

问题10:CPU的存储单元做什么事情

  • 存储单元是指CPU内部的L1,L2缓存及寄存器,待处理的数据就存放在这些存储单元中,这里的数据是指指令中的操作数

在这里插入图片描述
总结一下CPU的工作原理:控制单元要取下一条待运行的指令,该指令的地址在程序计数器PC中,在X86CPU上,程序计数器就是CS:IP,于是读取IP寄存器后,将此地址送上地址总线,CPU根据此地址便得到了指令,并将其存入到指令寄存器IR中,这时候轮到指令译码器上场了,它根据指令格式检查指令寄存器中的指令,先确定操作码是什么,再检查操作数类型,若是在内存中,就将相应操作数从内存中取回放入自己的存储单元,若操作数是在寄存器中就直接用,免了取操作数这一过程,操作码有了,操作数也齐了,操作控制器给运算单元下令,于是运算单元便真正开始执行指令了,ip寄存器的值被加上当前指令的大小,于是IP又指向了下一条指令的地址,接着控制单元又要取下一条指令了,流程回到了本段开头

实模式下的寄存器

问题11:什么是寄存器

  • 寄存器是一种物理存储元件,只不过它比一般的存储介质要快,能够跟上CPU的步伐,所以在CPU内部有好多这样的寄存器用来给CPU存取数据

各通用寄存器特定的功能:
在这里插入图片描述

实模式下内存分段由来

CPU本来是没有实模式这一称呼的,是因为有了保护模式,为了与老的模式区分开来,所以称老的模式为实模式
实模式的实体现在:程序中用到的地址都是真实的物理地址,“段地址:段内偏移”产生的逻辑地址就是物理地址

实模式下CPU内存寻址方式

寻址方式,从大方向来看可以分为三大类:

  • 寄存器寻址
    最直接的寻址方式就是寄存器寻址,它是指"数"在寄存器中,直接从寄存器中拿数据就行了
mov ax,0x10
mov dx,0x9
mul dx

以上三条指令都是寄存器寻址
只要牵扯到寄存器的操作无论其实源操作数,还是目的操作数,都是寄存器寻址
上面的第一,二条指令,它们的源操作数都是立即数,所以也属于立即数寻址

  • 立即数寻址
    指令由操作码和操作数组成,得到一个数往往不容易,或者说不那么直接,这个数要么在寄存器中,要么在内存中,都是间接给出的,如果操作数"直接"存在指令中,直接拿过来,立即就能用了,为了突显"立即就能用"的高效率,此数便称为立即数,比如
mov ax,0x18
mov ds,ax

第一条指令中的源操作数0x18是立即数,目的操作数ax是寄存器,所以它既是立即数寻址,也是寄存器寻址,第二条指令中,源操作数和目的操作数都是寄存器,所以纯粹是寄存器寻址

  • 内存寻址
    以上两种寻址方式,操作数一个是在寄存器中,一个是在指令中直接给出,它们都不在内存中,操作数在内存中的寻址方式称为内存寻址
    在第三种内存寻址中又分为:
  • 直接寻址
    直接寻址,就是将直接在操作数中给出的数字作为内存地址,通过中括号的形式告诉CPU,取此地址中的值作为操作数
mov ax,[0x1234]
mov ax,[fs:0x5678]

0x1234是段偏移地址,默认的段地址是DS,这条指令是将内存地址DS:0x1234处的值写入ax寄存器
第二条指令,由于使用了段跨越前缀fs,0x5678的段基址则变成了gs寄存器,最终的内存地址是gs寄存器的值*16+0x5678,CPU到此内存地址取值再存入ax寄存器
注意:立即数寻址的数字是直接拿来就用作操作数,直接寻址中的数字是用来进一步寻址的

  • 基址寻址
    基址寻址,就是在操作数中用bx寄存器或bp寄存器作为地址的起始,地址的变化以它为基础,在实模式下必须使用bx或者bp寄存器,到了保护模式下就没这个限制了,基址寄存器可选择的很多,可以是全部的通用寄存器
    bx寄存器的默认段寄存器是DS,而bp寄存器
    的默认段寄存器是SS,即bp和sp都是栈的有效地址
  • 变址寻址
    变址寻址其实和基址寻址类似,只是寄存器由bx,bp换成了si和di,si是指源索引寄存器,di是指目的索引寄存器,两个寄存器的默认段寄存器是ds
mov [di],ax  //将寄存器ax的值存入ds:di指向的内存
mov [si+0x1234],ax //变址中也可以加个偏移量

变址寻址主要是用于字符搬运方面的指令,这两个寄存器在很多指令中都要成对使用,如movsb,movsw,movsd等

  • 基址变址寻址
    从名字上看,这是基址寻址和变址寻址的结合,既基址寄存器bx或者bp加一个变址寄存器si或di
mov [bx+di],ax
add [bx+si],ax

第一条指令是将ax中的值送入以ds为段基址,bx+di为偏移地址的内存,第二条指令是将ax与[ds:bx+si]处的值相加后存入内存[ds:bx+si]
问题12:为什么已经有了sp寄存器来专门访问栈,还要再单独准备个bp

  • sp寄存器作为栈顶指针,相当于栈中数据的游标,这是专门给push指令和pop指令做导航用的寄存器,这么重要的指针,可不能随便乱动,如果动错了,栈中数据错位了,对整个程序的运行将造成不可估量的灾难
  • 处理器为了让开发人员方便控制栈中数据,提供了把栈当成数据段来访问的方式,可以用寄存器bp来给出栈中偏移量,所以bp默认的段寄存器就是SS,这样就可通过SS:bp的方式把栈当成普通的数据段来访问
  • 我们以栈中保存局部变量和函数参数的例子来解释bp寄存器的应用
int a =0;
function(int b,int c)
{
	int d;
}
a++;

假设这是在32位下

  • 调用function(1,2),按照C语言调用规范,参数入栈的顺序从右到左,会先压入2,再压入1,每个参数在栈中各占4字节
  • 栈中再压入function的返回地址,此时栈顶的值是执行"a++"相关指令的地址
    下面是堆栈框架的指令:
  • push ebp;将ebp压入栈,栈中备份ebp的值,占用4字节
  • move ebp,esp;将esp的值复制到ebp,ebp作为堆栈框架的基址,可用于对栈中的局部变量和其他参数寻址
  • 此时的ebp便是栈中局部变量的分界线,在这之后,esp将自减一定的值为局部变量在栈中分配空间,该值取决于所有局部变量空间大小的总和
  • sub esp,4,由于函数function中有局部变量d,局部变量是在栈中存放的,故esp要预留出4字节,专门给变量d使用
  • 以ebp为基址对栈中数据寻址,如图所示:
    在这里插入图片描述
  • 函数结束后跳过局部变量的空间:mov esp,ebp
  • 恢复ebp的值:pop ebp
  • 至此函数中堆栈框架的指令结束了,然后是返回指令ret,接着主调函数中执行"add esp,8"来回收参数b和c空间

让MBR使用硬盘

我们的MBR受限于512字节大小,在那么小的空间中,没法为内核准备好环境,更没法将内核成功加载到内存并运行,所以我们要在另一个程序中完成初始化环境及加载内核的任务,这个程序我们称为loader,即加载器,loader在哪里?如何跳过去执行?这就是新款MBR的使命,负责从硬盘上把loader加载到内存,并将接力棒交给它
由于MBR是占据了硬盘的第0扇区(以逻辑LBA方式,扇区从0开始编号,若是以物理CHS方式,扇区则从1开始编号),第一扇区是空闲的,可以用,但离得太近了,所以把loader放到第2扇区,MBR从第2扇区中把它读出来,读出来放到哪?原则上找个空闲地方就行了
代码如下:

;主引导程序 
;------------------------------------------------------------
%include "boot.inc"
SECTION MBR vstart=0x7c00         
   mov ax,cs      
   mov ds,ax
   mov es,ax
   mov ss,ax
   mov fs,ax
   mov sp,0x7c00
   mov ax,0xb800
   mov gs,ax

; 清屏
;利用0x06号功能,上卷全部行,则可清屏。
; -----------------------------------------------------------
;INT 0x10   功能号:0x06	   功能描述:上卷窗口
;------------------------------------------------------
;输入:
;AH 功能号= 0x06
;AL = 上卷的行数(如果为0,表示全部)
;BH = 上卷行属性
;(CL,CH) = 窗口左上角的(X,Y)位置
;(DL,DH) = 窗口右下角的(X,Y)位置
;无返回值:
   mov     ax, 0600h
   mov     bx, 0700h
   mov     cx, 0                   ; 左上角: (0, 0)
   mov     dx, 184fh		   ; 右下角: (80,25),
				   ; 因为VGA文本模式中,一行只能容纳80个字符,25行。
				   ; 下标从0开始,所以0x18=24,0x4f=79
   int     10h                     ; int 10h

   ; 输出字符串:MBR
   mov byte [gs:0x00],'1'
   mov byte [gs:0x01],0xA4

   mov byte [gs:0x02],' '
   mov byte [gs:0x03],0xA4

   mov byte [gs:0x04],'M'
   mov byte [gs:0x05],0xA4	   ;A表示绿色背景闪烁,4表示前景色为红色

   mov byte [gs:0x06],'B'
   mov byte [gs:0x07],0xA4

   mov byte [gs:0x08],'R'
   mov byte [gs:0x09],0xA4
	 
   mov eax,LOADER_START_SECTOR	 ; 起始扇区lba地址
   mov bx,LOADER_BASE_ADDR       ; 写入的地址
   mov cx,1			 ; 待读入的扇区数
   call rd_disk_m_16		 ; 以下读取程序的起始部分(一个扇区)
  
   jmp LOADER_BASE_ADDR
       
;-------------------------------------------------------------------------------
;功能:读取硬盘n个扇区
rd_disk_m_16:	   
;-------------------------------------------------------------------------------
				       ; eax=LBA扇区号
				       ; ebx=将数据写入的内存地址
				       ; ecx=读入的扇区数
      mov esi,eax	  ;备份eax
      mov di,cx		  ;备份cx
;读写硬盘:
;1步:设置要读取的扇区数
      mov dx,0x1f2
      mov al,cl
      out dx,al            ;读取的扇区数

      mov eax,esi	   ;恢复ax

;2步:将LBA地址存入0x1f3 ~ 0x1f6

      ;LBA地址7~0位写入端口0x1f3
      mov dx,0x1f3                       
      out dx,al                          

      ;LBA地址15~8位写入端口0x1f4
      mov cl,8
      shr eax,cl
      mov dx,0x1f4
      out dx,al

      ;LBA地址23~16位写入端口0x1f5
      shr eax,cl
      mov dx,0x1f5
      out dx,al

      shr eax,cl
      and al,0x0f	   ;lba第24~27or al,0xe0	   ; 设置74位为1110,表示lba模式
      mov dx,0x1f6
      out dx,al

;3步:向0x1f7端口写入读命令,0x20 
      mov dx,0x1f7
      mov al,0x20                        
      out dx,al

;4步:检测硬盘状态
  .not_ready:
      ;同一端口,写时表示写入命令字,读时表示读入硬盘状态
      nop
      in al,dx
      and al,0x88	   ;4位为1表示硬盘控制器已准备好数据传输,第7位为1表示硬盘忙
      cmp al,0x08
      jnz .not_ready	   ;若未准备好,继续等。

;5步:从0x1f0端口读数据
      mov ax, di
      mov dx, 256
      mul dx
      mov cx, ax	   ; di为要读取的扇区数,一个扇区有512字节,每次读入一个字,
			   ; 共需di*512/2次,所以di*256
      mov dx, 0x1f0
  .go_on_read:
      in ax,dx
      mov [bx],ax
      add bx,2		  
      loop .go_on_read
      ret

   times 510-($-$$) db 0
   db 0x55,0xaa

```程序最开始的%include "boot.inc",这个%include是nasm编译器中的预处理指令,意思是让编译器在编译之前把boot.inc文件包含起来,boot.inc的内容很简单,目前就两句话,文件内容如下:

```cpp
;-------------	 loader和kernel   ----------
LOADER_BASE_ADDR equ 0x900 
LOADER_START_SECTOR equ 0x2

LOADER_BASE_ADDR定义了loader在内存中的位置,MBR要把loader从硬盘读入后放到此处,它的值是0x900,说明将来loader会在内存地址0x900处
LOADER_START_SECTOR定义了loader在硬盘上的逻辑扇区地址,即LBA地址,它等于0X2,说明loader放在了第2块扇区
MBR就到此为止,因为没还有准备好loader,此时执行此MBR,CPU会直接跳到0x900的地方,程序的运行不可预测

  • 0
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值