这里写个往期推荐,这样可以来回跳跃(狗头
0x00-环境准备
0x01-BIOS以及MBR
0x02-MBR支持显卡
0x03-MBR操作硬盘以及Loader
0x04-进入保护模式
0x00 开机
大家都知道,开机时咱们得首先得将操作系统载入内存RAM区才可以继续我们的工作,但是咱们操作系统都没运行,那我们是如何实现加载这一过程呢,难道说是妥妥的灵异事件?
其实并非如此,咱们是不是忘了还有个磁盘呢,而且不要认为没操作系统就无法运行程序,实际上操作系统只是给咱们提供了一个方便运行程序的环境,如果没有操作系统程序也是可以执行的,因为程序执行只需要简单的两个元素,指令序列以及CPU按序列执行即可,只不过没操作系统你可能实现函数十分繁琐,也就比如说虚拟地址与物理地址的映射,但是这种情况在早期没有保护模式这一概念时,程序员们也确实是直接运用物理地址进行编程。何况还有更早的程序员打纸孔带呢(
现在来回答刚刚的问题,那就是开机如何加载呢,首先给出一个答案,开机后咱们地一个需要运行的程序是位于磁盘的BIOS程序:
0x01 软件接力第一棒,BIOS
BIOS全称为 Base Input & Output System,也即基本输入输出系统,他的主要工作那就是基本输入输出了(手动狗头
1. 实模式
实模式是什么呢,简单粗暴来讲那就是哥们只用物理地址的模式,因为最开始开机时还不存在页表,更不存在页映射一说,所以咱们最开始的地址只能通过物理地址来进行编程,并且此时编程也只能用汇编。
而在Intel 8086时期只有20条地址线,也就是说若按字节寻址的话咱们的发挥空间就只有1MB,用16进制表示就是从0x00000到0xFFFFF,以下我先给出实模式下的地址分布:
开始地址 |
结束地址 |
大小 |
用途 |
FFFF0 |
FFFFF |
16B |
BIOS入口,这么小的一个位置实际上仅仅是一个跳转指令 |
F0000 |
FFFEF |
64KB-16B |
系统BIOS |
C8000 |
EFFFF |
160KB |
映射硬件适配器的ROM或内存映射式I/O |
C0000 |
C7FFF |
32KB |
显示适配器BIOS |
B8000 |
BFFFF |
32KB |
文本显示适配器 |
B0000 |
B7FFF |
32KB |
黑白显示适配器 |
A0000 |
AFFFF |
64KB |
彩色显示适配器 |
9FC00 |
9FFFF |
1KB |
EBDA |
7E00 |
9FBFF |
约608KB |
可用 |
7C00 |
7DFF |
512B |
MBR加载地址 |
500 |
7BFF |
约30KB |
可用 |
400 |
4ff |
256B |
BIOS数据区域 |
000 |
3FF |
1KB |
中断向量表 |
2. BIOS
从上表也可以直观的看出来,在0xF0000~0xFFFFF这儿的64KB就保存的BIOS代码,而BIOS的功能就是检测初始化硬件。但是具体是如何初始化呢,硬件自身会实现一些初始化的功能调用,这里BIOS直接调用即可,就跟咱们高级程序调库类似,但是BIOS面向的是硬件,而咱们面向的是操作系统或者说是程序员自己实现的库。
除了上述功能BIOS还做了一个伟大的事,那就是建立中断向量表,这样咱们就可以通过"int 中断号"来进行硬件调用(每次看到这个int我都想到int 0x80哈哈哈,来自pwner奇奇怪怪的直觉)
但是我们这里还得清楚一件事情,那就是BIOS是放哪儿的,这里直接说答案,他是存放在内存ROM区中,学过机组的伙伴可能知道主存一般分为RAM和ROM,其中RAM大多由DRAM这种存储器构成,但是他是断电就消失,并且他要保持数据必须一定时间内还要不断刷新行,所以咱们的BIOS不能放到rAM中,但是ROM断电是不会消失的,就像刻光盘一样刻上面了,所以咱们的BIOS是放在ROM区中的。
而BIOS其实也是指令流,也是个程序,所以肯定也得有入口地址,这个入口地址便是0xFFFF0,这时候就得考虑如何找到这个地址了,这里有个既定规则,那就是实模式下寄存器宽度为16位,而程序一般都是通过分段机制来进行寻址,分段机制需要用到两个寄存器,那就是cs,ip,相信会汇编的同学知道,所以咱们寻址都是通过cs:ip来进行寻址,但是如何通过两个16位宽度的寄存器来表示20位的地址呢,这里已经有前人作答,咱们站在巨人肩膀上了已经,那就是通过将cs的值左移四位,然后再加上ip地址值,这样就刚好是20位了,我话个图:
这里图中所表示的也就是真实情况下cs:ip的值,但是你可能会疑惑明明有其他的方案为什么只有这个是真实的呢,你说的确实对,但是这是人家规定了的,没有理由可言,就相当与为什么负数小于正书,这是人为规定的。还有个至于为什么cs会设定为0xf000,这也是加点他自动变的,没有为什么。
在这里也给大家说清楚,这里BIOS的入口地址为啥是0xFFFF0,此时留给BIOS的大小只有16字节了,16字节能干啥呢,没错啥也干不了,这里的16字节就只是一个jmp指令,也就是说你先执行BIOS时,你得先到0xFFF0,然后通过这里的跳转指令再跳到别处(过于滑稽,而这里的指令具体就是jmp far f000:e05b
,也就是远眺指令到0xfe05b的物理地址,而这里才是真正BIOS代码开始的地方
而接下来BIOS所做的事情就是各种检测内存显卡等外设信息,然后在0x000到0x3ff处建立中断向量表。
在完成上述工作之后,BIOS的工作告一段落,之后就是大名鼎鼎的MBR了。
0x02 主引导记录MBR
BIOS最后的工作就是检验0盘0道1扇区的内容,这里历史遗留问题所以说这里扇区是从1开始,大家不用刻意管这个,只需要记住BIOS最后检验的是地一个磁盘扇区即可,在检验过程中,若BIOS检验出该磁盘末尾两个字节是0x55和0xaa则认定其为活动区,便加载到物理地址0x7c00,然后跳转过去,即可开始执行MBR。
这里的0x55,0xaa是一对魔数,就跟java字节码文件开头的0xcafebaby一样,没啥实际意义,而0x7c00,跟上面一样,规定为MBR起始地址。(这里我看了大象书有详细的解释,但是我觉得意义不大,就没写出来
MBR为咱们地一个在编写操作系统中自行构造的程序,理论上现在咱们无所不能(中二起来惹 , 这里还有几个规定,我在这里一一说出:
- MBR大小为512字节
- 地511以及512字节必须是0xaa,0x55,这是由于咱们模拟的是x86平台,所以采用小端序
- 凑行(笑死
1. 汇编编程基础
咱们本次的汇编采用NASM编译器,所以采用他的编译规则,其中比较常用的符号这里得讲讲:
- $:表示当前汇编代码行地址
- $$:表示本section地址
- section: 汇编代码中的节,这是程序员自行设定的,声明自己这个是个干什么的节
2. NASM简单用法
咱们不用掌握全,知道目前需要用到的即可
nasm -f <format><filename> [-o <output>]
其中-f就是指定输出文件格式。若要知道有多少格式,咱们可用nasm -hf来查看
,具体我借个图如下:
由于代码较繁琐,就不贴这儿了,不过我还是会讲解。
3.编写思路
虽然这里不会贴代码,但是我会给出思路,大家可以自己尝试,然后去github上clone我的源码查看即可,里面注释也比较详细
-
首先清屏,这里利用了BIOS所建立的中断向量表,用0x06号功能,即int 0x10,而我们实现系统调用的操作只需我们将功能号送入ah(注意是ah,也就是说向ax中传入0x600)寄存器,然后执行int 0x10即可。
-
而在我们编写的section后面加上vstart=0x7c00表示告诉编译器把我这个起始地址编译为0x7c00
-
然后我们再通过中断3号功能来获取光标位置
-
然后我们来实现打印字符串,此刻由于我们未使用IO知识,所以我们还是用中断向量来实现(现在知道BIOS的伟大之处了),运用13号子功能
-
打印结束记得填充,nasm中有自带的填充语句,即为
times 指令
-
最后两字节填充0xaa55
4.代码实现
这里由于代码不多所以直接贴这儿,具体代码可以去我的github上面拷下来看,由于之后操作系统的编写代码就不止这么少,所以我之后很少会贴,毕竟十分看起来水贴
;主引导程序
;------------------------------------------
SECTION MBR vstart=0x7c00 ; 向编译器表示咱们这儿起始地址应为0x7c00
mov ax,cs
mov dx,ax ; 由于BIOS通过 0:0x7c00跳转MBR,所以此时cs为0,因此借他来初始化寄存器
mov es,ax
mov ss,ax
mov fs,ax
mov sp,0x7c00
; 清屏利用0x06号子功能,上卷全部行即可清屏
;-------------------------------------------
;INT 0x10 功能号:0x06 功能:上卷窗口
;-------------------------------------------
;输入:
;AH 功能号:0x06
;AL = 上卷行数(若为0则表示全部行,太适合我们辣)
;BH = 上卷行属性
;(CL,CH) = 窗口左上角的(X,Y)位置
;(DL,DH) = 窗口右下角的(X,Y)位置
;无返回值
mov ax,0x600
mov bx,0x700
mov cx,0 ; 左上角(0,0)
mov dx,0x184f ; 右下角(0x4f,0x18),在VGA文本模式中一行只能容纳80字符
int 0x10
;;;;;;;; 下面三行获取光标位置 ;;;;;;;;;;;;;;;
; ,get_cursor获取光标位置,并在光标位置打印字符
mov ah,3 ;输入: 3号子功能即为获取当前光标位置
mov bh,0 ;bh 寄存器存储的是待获取光标的页号
int 0x10 ;输出 ch=光标开始行,cl =光标结束行
;dh=光标所在行号,dl=光标所在页号
;;;;;;;; 获取光标结束 ;;;;;;;;;;;;;;
;;;;;;;;;;; 打印字符串 ;;;;;;;;;;;;;;;;
;依旧用中断的0x13号子功能
mov ax, message ;文件末会声明此字符串
mov bp, ax ;es:bp为串首地址,es此时由于最开始的初始化,同cs一致
;光标位置需要用到dx寄存器内容,cx中的光标位置可忽略
mov cx,0xa ;cx 为串长度,不包括结束副\x00
mov ax,0x1301 ;ah=13为显示字符功能号
;al = 01 表示该功能的模式,具体模式有以下几种
;(1)0,显示字符串,光标返回起始位置
;(2)1,显示字符串,光标跟随到新位置
;(3)2,显示字符串以及属性,光标返回起始位置
;(4)3,显示字符串以及属性,光标跟随到新位置
mov bx,0x2 ;bh存储要显示的页号,此时为0页,bl中是字符属性,bl=02h表示黑底绿字
int 0x10 ; 执行BIOS 0x10中断
;;;;;;;;;; 打印字符串结束 ;;;;;;;;;;;;;;;
jmp $ ;悬停指针
message db "peiwithhao"
times 510-($-$$) db 0 ;$-$$表示该指令行距离section起始地址的偏移,这里也可表示为目前指令大小
db 0x55,0xaa
5.汇编
运用我们之前的知识
nasm -o mbr.bin mbr.S
之后咱们用ls查看下是否512字节,利用ls即可验证
-rw-rw-r-- 1 dawn dawn 512 Dec 26 07:41 mbr.bin
咱们会发现确实无误,这样咱们就可以做接下里的步骤,那就是把咱们的MBR程序给拷到磁盘上,而Linux本身提供了一个dd命令,他被成为穿甲弹,他可以深入磁盘任何一个扇区,这里给出几个选项的示意:
- of=FILE 指定要读的文件
- bs=BYTES 指定块大小
- count=BLOCKS 指定块数
- seek=BLOCKS 制定当我们把块输出到文件是要跳过多少块
- conv=CONVS 指定如何转换文件
介绍结束,之后我们使用这个命令将mbr.bin打入相应磁盘扇区,也就是第一快512扇区,还记得之前那个0,0,1吗,就是BIOS结束的工作所找寻的快
dd if=/你的路径/mbr.bin of=/你的路径/bochs/hd60M.img bs=512 count=1 conv=notrunc
执行完出现以下提示即表示成功打入
然后咱们就可以开始测试了
0x03 测试代码
激动的心,颤抖的手,这是咱们地一个自己实现的代码,还是跟之前一样
bin/bochs -f bochsrc.disk
我们在最开始执行的指令也可以发现这个就是jmp,跟我们最开始说BIOS执行的第一条指令的论述是完全一致的!
一切如咱所愿,大成功!!
0x04 总结
本结不难,大伙可能会遇到些什么功能号,上卷窗口类似的感觉很杂,但是不需担心,这并不是我们写OS的重点,所以稍微了解即可
这次得源码依旧在github上保持更新,分支名为BIOS,欢迎大家指正
传送门