配合视频学习体验更佳!https://www.bilibili.com/video/BV1Rk4y1L7p2/?vd_source=701807c4f8684b13e922d0a8b116af31
代码仓库地址:https://github.com/xukanshan/the_truth_of_operationg_system
在上一章中,我们是通过bios中断来实现打印字符串。现在,我们尝试绕过bios来直接来显示字符。这是为了应对进入保护模式后不能使用bios中断做准备。
能这样的原理是因为显卡的内存已经编排到了cpu能够寻址的范围之内,当cpu操作这部分“内存”时,实际上是直接在和显卡打交道。显卡拿到了数据处理之后,显示器最终会按照要求显示这些数据。内存中的显存映射的地址范围如下:
显示器上每个字符占两字节,低字节是字符ASCII码,高字节是用来控制颜色的。高字节低4位是字符前景色,也就是字符的颜色(RGB是红蓝绿三种颜色的调和,I位表示是否高亮),高字节高4位是字符的背景色(RGB是红蓝绿三种颜色的调和,K位控制是否闪烁)。所以我们向显卡的对应内存操作时,也应按照如下格式:
RGB配色的效果如下表:
接下来我们编写代码,来直接感受与显卡打交道
p110代码mbr.S剖析:
1、代码功能
不使用bios中断的显示字符串功能,直接使用显卡在内存中的映射来显示字符
2、实现原理
显卡将自己的内存编排在了CPU可以寻址的范围之内,CPU可寻址范围中有一块区域是显卡的显存。通过对这块内存区域进行特定的操作,可以与显卡打交道,进而与显示器打交道来显示内容
3、代码逻辑
A、清屏
B、显示字符
4、怎么写代码?
A、指定vstart=0x7c00,这是告诉编译器把本程序的起始地址编译为0x7c00;
B、查询并调用bios中断来进行清屏(在之后甚至直接与显卡打交道来实现此功能);
C、指定段基址,用gs=0xb800,b800是由于显卡内存在内存中的位置决定。gs是随意选择的,也可以选择es,因为我们是用[段基址:偏移]的形式来访问显卡内存,并用规定的格式向0xb8000([段基址:偏移])开始的位置移入字符与颜色设定;
D、死循环;填入MBR规定510字节大小剩下的0;固定结尾两字节0x55,0xaa
5、代码实现如下 (myos/boot/mbr.S)
;主引导程序
;
;LOADER_BASE_ADDR equ 0xA000
;LOADER_START_SECTOR equ 0x2
;------------------------------------------------------------
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
; 输出背景色绿色,前景色红色,并且跳动的字符串"1 MBR"
mov byte [gs:0x00],'1'
mov byte [gs:0x01],0xA4 ; A表示绿色背景闪烁,4表示前景色为红色
mov byte [gs:0x02],' '
mov byte [gs:0x03],0xA4
mov byte [gs:0x04],'M'
mov byte [gs:0x05],0xA4
mov byte [gs:0x06],'B'
mov byte [gs:0x07],0xA4
mov byte [gs:0x08],'R'
mov byte [gs:0x09],0xA4
jmp $ ; 通过死循环使程序悬停在此
times 510-($-$$) db 0
db 0x55,0xaa
6、其他代码详解查看书p111
接下来我们要用MBR做点实事了,MBR只有510B,能做的事情非常少,所以不能指望它做完所有事情。所以,我们用它把操作系统的loader加载到指定位置,然后跳转到loader执行,loader由于大小可以比MBR大得多,所以能做的就很多了。所以,MBR要加载loader,就必须要和磁盘打交道。打交道的方式很简单,就是通过in 与out指令与磁盘暴露在外的寄存器交互。下图是in与out指令的用法:
磁盘端口寄存器对应的用途(详细请见书p127)
其中Status与Device寄存器比较复杂,它们的结构的含义如下:
虽然操作磁盘很复杂,但都是有章可循的,按照如下步骤操作磁盘即可:
我们现在来写一个mbr程序,来实现从磁盘中加载我们的loader
p131剖析mbr.S代码:
1、代码功能
从磁盘中加载操作系统的loader,该loader由我们自己写入磁盘
2、实现原理
计算机发展到现在,已经将对磁盘的种种操作,简化成了对磁盘暴露在外的寄存器的操作,对这些寄存器的操作需要通过in与out指令
3、代码逻辑
A、清屏
B、通过对内存特定区域的操作显示字符
C、从磁盘特定区域读取特定大小的数据到特定内存位置中
4、怎么写代码?
A、include boot.inc,这里面定义了loader在磁盘中的位置(我们会将其写入磁盘2号扇区),与loader加载进入内存后将要存放的位置(在第二章的内存布局图中,找一个靠前的可用位置就行了,本代码用的是0x900)
B、定义vstart=0x7c00,调用bios中断清屏,对特定内存区域放入数据来显示字符
C、按照与磁盘打交道的7个步骤来完成从磁盘取出数据存放到内存指定区域(这7个步骤就是用in与out操作特定通道的寄存器)
E、跳转到内存中的loader位置执行
F、填充MBR要求的510字节剩下的0,定义MBR要求的标准结尾0x55,0xaa
5、代码实现如下 (myos/boot/include/boot.inc)
;------------- loader和kernel ----------
LOADER_BASE_ADDR equ 0x900
LOADER_START_SECTOR equ 0x2
myos/boot/mbr.S
;主引导程序
;------------------------------------------------------------
%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~27位
or al,0xe0 ; 设置7~4位为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 ;di当中存储的是要读取的扇区数
mov dx, 256 ;每个扇区512字节,一次读取两个字节,所以一个扇区就要读取256次,与扇区数相乘,就等得到总读取次数
mul dx ;8位乘法与16位乘法知识查看书p133,注意:16位乘法会改变dx的值!!!!
mov cx, ax ; 得到了要读取的总次数,然后将这个数字放入cx中
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
6、其他代码详解查看书p132
然后我们写个简单的显示字符的程序loader.S(512字节),用dd命令放入磁盘的2号分区中,来检验我们的MBR能够正常加载并跳转到这个程序中执行,以下是代码,代码剖析略。 (myos/boot/loader.S)
%include "boot.inc"
section loader vstart=LOADER_BASE_ADDR
; 输出背景色绿色,前景色红色,并且跳动的字符串"1 MBR"
mov byte [gs:0x00],'2'
mov byte [gs:0x01],0xA4 ; A表示绿色背景闪烁,4表示前景色为红色
mov byte [gs:0x02],' '
mov byte [gs:0x03],0xA4
mov byte [gs:0x04],'L'
mov byte [gs:0x05],0xA4
mov byte [gs:0x06],'O'
mov byte [gs:0x07],0xA4
mov byte [gs:0x08],'A'
mov byte [gs:0x09],0xA4
mov byte [gs:0x0a],'D'
mov byte [gs:0x0b],0xA4
mov byte [gs:0x0c],'E'
mov byte [gs:0x0d],0xA4
mov byte [gs:0x0e],'R'
mov byte [gs:0x0f],0xA4
jmp $ ; 通过死循环使程序悬停在此
用nasm编译我们的mbr.s与loader.s,命令:nasm -o xxx xxx.s -I include/。-I意为包含指定的库文件。在/include目录下我们放入boot.inc
用dd命令将mbr写入磁盘0号分区,将loader写入磁盘的2号分区(dd if=loader of=/bochs/hd60M.img seek=2 bs=512 count=1 conv=notrunc,seek意为跳过多少分区的意思)。