BIOS
实模式
实模式是什么呢,简单来讲就是只用物理地址的模式,因为最开始开机时还不存在页表,更不存在页映射一说,所以咱们最开始的地址只能通过物理地址来进行编程,并且此时编程也只能用汇编。
而在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 | 中断向量表 |
BIOS
在0xF0000~0xFFFFF的64KB保存的是BIOS代码,而BIOS的功能就是检测初始化硬件。但是具体是如何初始化呢,硬件自身会实现一些初始化的功能调用,这里BIOS直接调用即可。除了上述功能BIOS还做了一件事,那就是建立中断向量表,这样咱们就可以通过"int 中断号"来进行硬件调用。
BIOS其实也是指令流,也是个程序,所以肯定也得有入口地址,这个入口地址便是0xFFFF0,这时候就得考虑如何找到这个地址了,这里有个既定规则,那就是实模式下寄存器宽度为16位,而程序一般都是通过分段机制来进行寻址,分段机制需要用到两个寄存器,那就是cs,ip,但是如何通过两个16位宽度的寄存器来表示20位的地址呢,那就是通过将cs的值左移四位,然后再加上ip地址值,这样就刚好是20位了。
主引导记录MBR
BIOS最后的工作就是检验0盘0道1扇区的内容,这里历史遗留问题所以说这里扇区是从1开始,大家不用刻意管这个,只需要记住BIOS最后检验的是地一个磁盘扇区即可,在检验过程中,若BIOS检验出该磁盘末尾两个字节是0x55和0xaa则认定其为活动区,便加载到物理地址0x7c00,然后跳转过去,即可开始执行MBR。
这里的0x55,0xaa是一对魔数,表明这段程序的结尾。为什么是0x55和0xaa呢,我们转成二进制可以发现,这两个数是01010101和10101010,是连续的高低电平交替,这样不容易和正常代码混在一起。
- MBR大小为512字节
- 地511以及512字节必须是0xaa,0x55,这是由于咱们模拟的是x86平台,所以采用小端序
汇编编程基础
咱们本次的汇编采用NASM编译器,所以采用他的编译规则,其中比较常用的符号这里得讲讲:
- $:表示当前汇编代码行地址
- $$:表示本section地址
- section: 汇编代码中的节,这是程序员自行设定的,声明自己这个是个干什么的节
NASM简单用法
nasm -f <format><filename> [-o <output>]
其中-f就是指定输出文件格式。
编写思路
-
首先清屏,这里利用了BIOS所建立的中断向量表,用0x06号功能,即int 0x10,而我们实现系统调用的操作只需我们将功能号送入ah(注意是ah,也就是说向ax中传入0x600)寄存器,然后执行int 0x10即可。
-
而在我们编写的section后面加上vstart=0x7c00表示告诉编译器把我这个起始地址编译为0x7c00
-
然后我们再通过中断3号功能来获取光标位置
-
然后我们来实现打印字符串,此刻由于我们未使用IO知识,所以我们还是用中断向量来实现(现在知道BIOS的伟大之处了),运用13号子功能
-
最后两字节填充0xaa55
代码实现
;主引导程序
;------------------------------------------
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 "Hello lfh"
times 510-($-$$) db 0 ;$-$$表示该指令行距离section起始地址的偏移,这里也可表示为目前指令大小
db 0x55,0xaa
汇编
运用我们之前的知识
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
执行完出现以下提示即表示成功打入
测试代码
bin/bochs -f bochsrc.disk
我们在最开始执行的指令也可以发现这个就是jmp,跟我们最开始说BIOS执行的第一条指令的论述是完全一致的!