《操作系统真象还原》读书笔记 第2章

0x1 计算机启动过程

为什么程序要载入内存
CPU的硬件电路被设计成只能运行处于内存中的程序,是因为内存内存运速快,且容量大。其次,操作系统可以存储在软盘上,也可以存储在硬盘上,甚至U盘
什么是载入内存
所谓载入内存,大致上分为两部分:
1)程序被加载器(软件或硬件加载到某个区域)。
2)CPU的cs:ip寄存器被指向这个程序的起始地址。
操作系统在加载程序时,是需要某个加载器来将用户程序存储到内存中的。加载器本质上就是一堆函数组成的模块。
从按下主机电源键后,第一个运行的软件是BIOS。

0x2 软件接力第一棒,BIOS

BIOS全称叫Base Input & Output System,即基本输入输出系统。

0x2.1 实模式下的1MB内存分部

Intel8086有20条地址线,故其可以访问1MB的内存空间,范围是0x00000到0xFFFFF。其中0x00000~0x9FFFF的640K是对应到DRAM也就是我们的物理内存,而0xF0000到0xFFFFF这64K内存是ROM。这里面存储的就是我们的BIOS代码。BIOS的工作主要是检测、初始化硬件。
具体初始化的方法是硬件自己提供了一些初始化的功能调用,BIOS直接调用即可。除此之外BIOS还创建了中断向量表,这样就可以通过“int中断号”来实现相关的硬件调用。BIOS建立的这些功能就是对硬件的IO操作,也就是输入输出,但由于就64KB大小的空间,不可能把所有硬件IO操作实现的面面俱到,BIOS只挑一些重要的、保证计算机能运行的那些硬件的基本IO操作,就行了。这就是BIOS被称为基本输入输出系统的原因。

0x2.2 CPU眼中,我们插在主板上的物理内存在它眼里并不是全部的内存

地址总线的宽度决定了可以访问的内存空间大小,如16位的机器地址总线为20位,其地址范围是1MB。32位地址总线宽度是32位,其地址范围是4GB。但是以上范围是地址总线可以触及到的边界,是指计算机在寻址上可以达到的疆域。虽然地址总线有如此大的范围可以访问,但是并不是说计算机寻址范围必须是物理内存(内存条)。
在计算机中,并不是只有插在主板上的内存条需要通过地址总线访问,还有一些外设同样需要地址总线访问。若把全部地址总线都指向物理内存,那其他设备就无法访问了。所以计算机设计者只好在地址总线上提前预留出一些地址空间给外设使用。留够了后,剩余的地址总线再指向DRAM,也就是插在主板上的内存条。
物理内存多大都没有用,主要是看地址总线的宽度。还要看地址总线的设计是不是全部用于访问DRAM。所以说,地址总线是决定我们访问哪里、访问什么,以及访问范围的关键。我们平时用的32位,上面的内存条并不是全部都用到了,按理说内存条大小超过4GB就没有意义了,超过了地址总线的部分就是浪费。这就是为什么安装了4GB内存,电脑却只显示3.8GB左右的原因。
总之,表示地址的那串数字是地址总线的输入,相当于器参数,和内存条没关系。CPU能访问这块地址是地址总线给做的映射,相当于给改地址分配了一个存储单元,而存储单元要么落在某个rom中,要么落在某个外设内存中,要么落在物理内存条上。可以想象成CPU给地址总线提供数字,在地质总线看来,这串数字就是地址。地址分配电路根据此地址的范围,决定在那个存储介质分配一个存储单元,最后将此地址与存储储单元对应起来。

0x2.3 BIOS是如何苏醒的

BIOS是计算机上的第一个软件,他不可能自己加载自己。它是通过硬件ROM(只读存储器)加载的。
ROM只读存储是不可擦除的,他不像动态随机访问存储器DRAM那样,掉电后,里面的数据就会丢失。这种存储介质是用来存储一成不变的数据的,当数据写进去后便无法更改。
BIOS代码所作的工作也一成不变的,在正常情况下,其本身是不需要修改的,平时听说的那些主板坏了要刷新BIOS的情况属于例外。因为BIOS的不变性,所以BIOS被写入ROM硬件中。ROM也是内存,也需要地址总线进行访问。此ROM被映射在地段1MB内存的顶部,即地址0xF0000~0xFFFFF处。只要访问此处的地址便是访问了BIOS,这个映射是硬件完成的。
BIOS本身是个程序,程序需要执行,就需要一个入口地址,此入口地址是0xFFFF0。
在开机的接电的一瞬间,CPU的cs:ip寄存器被强制初始化位0xF000:0xFFF0。由于开机的时候处于实模式,所以组合成的地址为0xFFFF0,也就是BIOS的入口。可以理解为,CPU一加电就指向了BIOS程序的入口点。
但是我们发现0xFFFF0到0xFFFFF之间只有16个字节,根本存储不了多少数据,所以此处代码只能是个跳转指令才能解释通。

0x2.4 为什么是0x7c00

BIOS最后一项工作校验启动盘中位于0盘0道1扇区的内容。而该扇区的内容就是MBR。此扇区的魔数0x55和0xaa,BIOS便认为此扇区中确实存在可执行的程序,便加载到物理地址0x7c00,随后跳转到此地执行。BIOS跳转到0x7c00是用jmp 0:0x7c00实现的,这是jmp指令绝对远跳转移用法,段寄存器cs会被替换,这里的段基址是0,即cs由之前的0xf000变成了0。
如果此扇区最后2个不是0x55和0xaa,即使里面有可执行代码也于事无补。
为什么是0盘0道1扇区的内容
作者推测是为了快速找到MBR内容,方便快速加载程序到指定的0x7c00。
为什么是物理地址0x7c00不是其他地址或好看的地址
0x7c00作用就是魔数,最早出现在IBM PC 5150的ROM BIOS INT 19中断处理程序中。通电开机后,调用BIOS中断0x19,即call int 19h。在此中断处理函数中,BIOS要检测这台计算机有多少硬盘或软盘,如果检测到了任何可用磁盘,BIOS就把它第一扇区加载到0x7c00。
之所以x86手册找不到,是因为它属于BIOS中的规范。所以肯定是由IBM PC 5150 BIOS开发团队规定的数。
MBR不是随便放在哪里都行的,首先不能覆盖已有的数据,其次,不能过早地被其他数据覆盖。通常,MBR的任务是加载某个程序(这个程序一般是内核加载器,很少有直接加载内核的)到指定位置,并将控制权交给他。所谓移交控制权就是jmp过去而已。之后MBR就没有用了,被覆盖也没关系。这里所说的过早覆盖,是指不能让mbr破坏自己,比如被加载的程序,如内核加载器,其放置的位置若是MBR自己所在的范围,这就破坏了自己。
8086CPU要求物理地址0x0~0x3FF存放中断向量表,所以此处不能动了。
按DOS 1.0要求的最小内存32KB 来说,MBR希望有尽可能多的预留空间,这样也是保全自己的做法,免得被过早覆盖。所以MBR只能放在32KB的末尾。
MBR本身也是程序,是程序就要用到栈,栈也是在内存中的,MBR虽然本身只有512字节,但还要为其所用的栈分配点空间,所以其实际作用的内存空间要大于512字节,估计1KB内存就够用了。
结合以上三点,选择32KB最后1KB最为合适。32KB换算为16进制为0x8000,减去1KB(0x400),等于0x7c00。这就是0x7c00的由来。

0x2.5 MBR实现开始

首先我们要明确MBR的大小是512字节,最后两个字节是磁盘魔数0x55和0xaa,即510和511字节处。按照起始偏移为0开始计算。bochs模拟的是x86平台,所以是小端字节序,故最后两个字节内容是0xaa55

0x2.5.1 神奇好用的 $ 和$ $,令人迷惑的section

$和 $ $是编译器NASM预留的关键子,用来表示当前行和本section的地址,起到了标号的作用,它是NASM提供的,并不是CPU原生支持的,相当于伪指令一样,对于CPU来说是假的。
伪指令这是相对于CPU可识别的指令来说的,它(伪指令)知识编译器定义的,CPU中并不存在这个指令,硬让CPU执行伪指令,CPU会抛出“UD(未定义的操作码)”异常。伪指令是编译器为了开发人员写代码方便而提供的一些符号,这些符号在编译时,会由编译器转换成CPU可识别的东西,如指令地址等。
$属于“隐式地”藏在本行代码前的标号,也就是编译器给当前安排的地址,看不到却又无处不在 $在每行都有。或者说只有“显示地” 用了 $的地方,nasm编译器才会将该地址公布出来。

code_start:
	jmp $
//等同于jmp code_start

$ $指代本section的起始地址,此地址同样是编译器安排的地址,默认情况下,它们的值是相对于文本文件开头的偏移量。至于实际安排的是多少,还要看程序员是否在section中添加了vstart。这个关键字可以影响编译器安排地址的行为,如果该section用了vstart=xxxx修饰, $ $的值则是此section的虚拟起始地址xxxx。 $的值是以xxxx为起始地址的顺延。如果用了vstart关键字,想获得本section载文件中的真实偏移量(真实地址)该怎么做?nasm提供了这个方法。

section.节名.start

如果没有定义section,nasm默认全部代码同为一个section,起始地址为0。
接下来复制下作者的代码

;主引导程序
;-----------------------------------------------------
SECTION MBR vstart=0x7c00
	mov ax,cs				;因为BIOS执行完毕后cs:ip为0x0:0x7c00,所以用cs初始化各寄存器
	mov ds,ax				;ds、es、ss、fs不能给立即数初始化,需要用ax寄存器初始化
	mov es,ax
	mov ss,ax
	mov fs,ax
	mov sp,0x7c00			;初始化堆栈指针,因为目前0x7c00以下的内存暂时可用
;清屏利用0x06号功能,上卷全部行,则可清屏
;-----------------------------------------------------
;INT 0x10	功能号:0x06		功能描述:上卷窗口
;-----------------------------------------------------
;输入:
;AH	功能号= 0x06
;AL = 上卷行数(如果为0,表示全部)
;BH = 上卷行属性
;(CL,CH) = 窗口左上角的(X,Y)位置
;(DL,DH) = 窗口右下角的(X,Y)位置
;无返回值:
	mov	ax,0x600			;上卷行数:全部	功能号:06
	mov bx,0x700			;上卷属性
	mov cx,0				;左上角:(00)
	mov dx,0x184f			;右下角:(80,25)
							;VGA文本模式中,一行只能容纳80字节,共25;下标从0开始,所以0x18=240x4f=79
	int 0x10				;int 0x10

;;;;;;;;	下面这三行代码获取光标位置		;;;;;;;;
;.get_cursor获取当前光标位置,在光标位置处打印字符。
	mov ah,3				;输入:3号子功能是获取光标位置,需要存入ah寄存器
	mov bh,0				;bh寄存器存储的是待获取光标的页号
	
	int 0x10				;输出:ch=光标开始行,cl=光标结束行
							;dh=光标所在行号,dl=光标所在列号
;;;;;;;;	获取光标位置结束		;;;;;;;;

;;;;;;;;	打印字符串		;;;;;;;;
	;还是用10h中断,不过这次调用13号子功能打印字符串
	mov	ax,message
	mov bp,ax				;es:bp为串首地址,es此时同cs一致,
							;开头时已经为sreg初始化
	
	;光标位置要用到dx寄存器中内容,cx中光标位置可忽略
	mov cx,5				;cx为串长度,不包括结束符0的字符个数
	mov ax,0x1301			;子功能号13时显示字符及属性,要存入ah寄存器,
							;al设置写字符串方式al=01:显示字符串,光标跟随移动
	mov bx,0x2				;bh存储要显示的页号,此处时第0页,
							;bl中式字符属性,属性黑底绿字(bl = 02h)
	int 0x10				;执行BIOS 0x10 号中断
;;;;;;;;	打字字符串结束		;;;;;;;;

	jmp $					;使用程序悬停在此
	
	message db "1 MBR"
	times 510-($-$$) db 0
	db 0x55,0xaa

将上述代码进行编译nasm -o mbr.bin mbr.S,编译的文件大小是512字节
在这里插入图片描述
接下来根据Linux的dd指令,将我们编译好的二进制文件写入上一章创建的空引导磁盘文件中dd if=/root/mbr.bin of=/root/Downloads/bochs/hd60M.img bs=512 count=1 conv=notrunc
在这里插入图片描述
接下来启动bochs验证程序是否能够输出我们想要的结果
在这里插入图片描述

发布了30 篇原创文章 · 获赞 5 · 访问量 1934

猜你喜欢

转载自blog.csdn.net/AlexSmoker/article/details/104080821