本章节所有代码托管在miniOS_32
章节任务
操作显存显示内容
上一节,我们介绍了计算机在开机后BIOS是如何从磁盘中加载mbr程序并运行的,为了测试mbr程序,我们使用BIOS中断写了一段在屏幕上显示内容的代码,以此来测试BIOS加载mbr程序的过程,如果BIOS能够成功加载mbr程序并运行,则必然会在屏幕上显示我们在mbr程序中写的内容
本节我们将介绍显卡与显存,而我们的第一个任务,就是将通过显存来控制屏幕输出内容,替换掉BIOS中断的方法
前置知识
显卡与显存
为了显示文字,通常需要两种硬件,一是显示器,二是显卡。
- 显卡的职责是为显示器提供内容,并控制显示器的显示模式和状态。
- 显卡未必一定是独立的插卡。为了节省使用者的成本,有的显卡会直接做在主板上,这样的显卡也有个名字,叫集成显卡。
- 显卡控制显示器的最小单位是像素。
- 显示器的职责是将显卡的内容以视觉可见的方式呈现在屏幕上。
显存就是存放要在显示器上显示的内容的器件,因为它位于显卡上,故称显示存储器,简称显存,要显示的内容都预先写入显存。
所以,显存可以认为就是一块特殊的内存,用于存储显示在屏幕上的内容,**为了给出要显示的字符,处理器需要访问显存,把字符的ASCII码写进去。**但是,显存是位于显卡上的,访问显存需要和显卡这个外围设备打交道。同时,多一道手续自然是不好的,这当中最重要的考量是速度和效率。
为此,计算机系统的设计者们,决定把显存映射到处理器可以直接访问的地址空间里,也就是内存空间里。
如图所示,8086可以访问1MB内存。其中,0x00000~9FFFF属于常规内存,由内存条提供;0xF0000~0xFFFFF由主板上的一个芯片提供,即ROM-BIOS。而0xB8000~0xBFFFF这段物理地址空间,是留给显卡的,由显卡来提供,用来显示文本。
屏幕是如何显示内容的
**显卡的工作是周期性地从显存中提取这些比特,并把它们按顺序显示在屏幕上。如果是比特“0”,则像素保持原来的状态不变,因为屏幕本来就是黑的;如果是比特“1”,则点亮对应的像素。**其工作原理如下所示
屏幕上的每个字符对应着显存中连续2字节,前一个是字符的ASCII代码,后面是字符的显示属性,包括字符颜色(前景色)和底色(背景色)。
如图所示,字符“H”的ASCII代码是0x48,其显示属性是0x07;字符“e”的ASCII代码是0x65,其显示属性是0x07。
字符的显示属性(1字节)分为两部分,
- 低4位定义的是前景色
- 高4位定义的是背景色
- 色彩主要由R、G、B这3位决定
- K是闪烁位,为0时不闪烁,为1时闪烁
- I是亮度位,为0时正常亮度,为1时呈高亮
如下,给出了背景色和前景色的所有可能值
mbr功能实现
编写mbr.S代码
|
|
编译代码
|
|
将程序写入磁盘,注意一定要用绝对路径
|
|
运行,运行时也一定要在bochs安装目录下
|
|
从硬盘中加载程序到内存
目前为止,我们已经实现的事情是
完成了一个简单的mbr程序,它存放在磁盘的0盘0道1扇区
BIOS启动后会自动从磁盘的0盘0道1扇区的位置寻找代码文件,如果该代码文件的最后两个字节的内容为0x55和0xaa,就说明该文件是mbr程序,于是BIOS就将该文件加载到内存的0x7c00位置开始运行
接下来我们要做的事情是
- 由于mbr程序只有512字节大小,不能完成内核的所有初始化任务,因此需要有另外一个程序,专门负责加载内核程序
- 因此mbr真正的任务其实是将将来的loader程序从磁盘加载到内存的某一位置
是的,mbr的任务和BIOS的任务是一样的:
BIOS将mbr加载到内存的0x7c00之后运行mbr,mbr运行后将loader程序从磁盘再加载到内存的某一位置,唯一的区别在于,mbr从磁盘加载文件到内存的任务由我们自定义实现
因此,接下来我们就要对磁盘有一个简单的认识
磁盘的基本介绍
磁盘的读写操作
让硬盘工作,需要通过读写硬盘控制器的端口,端口就是位于 I0 控制器上的寄存器,此处的端口是指硬盘控制器上的寄存器,这里需要解释一些概念:
- 硬盘控制器属于IO接口,硬盘控制器同硬盘的关系,如同显卡和显示器一样,它们都是专门驱动外部设备的模块电路,CPU 只同它们说话,由它们将 CPU的话转译给外部设备。这是它们的共同点,但不同的是显卡和显示器是分开的,硬盘控制器和硬盘是连接在一起的。
- IO接口通过寄存器的方式同CPU通信,其内部有专用于数据交互的寄存器,只不过这里所说的这些寄存器位于IO接口中,为了区别于CPU内部的寄存器,I0 接口中的寄存器就称为端口(这可不是网络应用程序所开的那种端口,如网络服务器会启动80端口,这是两码事)
IO端口的访问
Intel 汇编语言的形式是:操作码目的操作数,源操作数。Intel 采用这种格式的原因可能是觉得这样表达“目的操作数”=“源操作数”更形象,如同a-6这种形式。
in 指令用于从端口中读取数据,其一般形式是:
- in al,dx;
- in ax,dx;
其中al和ax用来存储从端口获取的数据,dx是指端口号。
这是固定用法,只要用in指令,源操作数(端口号)必须是dx,而目的操作数是用 al,还是 ax,取决于dx端口指代的寄存器是8位宽度,还
是16位宽度。
out指令用于往端口中写数据,其一般形式是:
- out dx,al;
- out dx,ax;
- out立即数,al;
- out立即数,ax
另外
- in指令中,端口号只能用dx寄存器
- out指令中,可以选用dx寄存器或立即数充当端口号
硬盘控制器的主要端口介绍
这里也需要解释一些概念
- 端口是按照通道给出的,并非是针对某块硬盘,通道其实就是主板跟硬盘通信的IDE接口插槽,一个主板一般对应有两个这样的插槽,也就是说,一个主板对应两个通道(主通道与副通道),一个通道对应两块硬盘(主盘和副盘)
- 每个通道都分别有主盘和从盘
- 要想操作某个通道上的某块硬盘,需要单独指定,这一点可通过指定device寄存器的第4位实现,0表示使用主盘,1表示使用从盘
另外,端口用途在读硬盘和写硬盘时还是有点区别的
比如拿 Primary通道上的 0x1F1端口来说,读操作时若读取失败,里面存储的是失败状态信息,所以称为error 寄存器,并且 0x1F2端口中存储未读的扇区数写操作时就变成了feauture 寄存器,此寄存器用于写命令的参数。
常用的硬盘操作方法
让MBR使用硬盘
编写mbr程序
上述我们对硬盘做了几个简单的介绍,接下来我们就要改造目前的mbr程序,让其能够读取硬盘,并把硬盘上的loader程序加载到内存
关于loader程序也有以下几点需要说明:
- loader中需要定义一些内核需要的数据结构,因此加载到内存就不能被覆盖
- 随着内核的功能越来越多,其占用的内存也就越来越多,故其内存地址也会想越来越高的位置发展,所以loader程序尽量放在低处
所以,我们对loader程序的规划如下:
- loader程序的磁盘位置在第0块磁盘的第2扇区
- loader程序在内存中的加载地址为0x900
对此,我们可以将上述配置写入一个单独的文件中,并使用类似宏命令定义上述配置
boot.inc
|
|
定义好loader的基本配置后,我们就可以着手改造mbr程序了
mbr.S
|
|
编译mbr程序
|
|
将MBR程序写入磁盘的MBR扇区内
|
|
此时如果我们运行mbr程序,会发现程序依旧显示的是原来mbr的内容,这是因为我们并未实现loader程序,也就是说此时磁盘的0块2号扇区的位置是空的
实现内核加载器
上个部分,我们完成了mbr程序的书写,并使用汇编实现了一个从磁盘加载文件到内存的函数
本部分内容我们将使用这个函数将loader加载到内存
为了验证是否成功的将loader程序加载到指定内存并运行,我们也简单将loader程序实现了一个屏幕打印功能来做测试,具体与内核相关的功能我们在后续章节详述
编写loader程序
loader.S
|
|
编译loader程序
|
|
将loader程序写入磁盘内
按照之前的约定,我们对loader程序在磁盘中的位置安排在2号扇区内
|
|
bs=512
:bs
代表块大小(Block Size)。指定数据块的大小为 512 字节。即,每次读取和写入操作将处理 512 字节的数据。通常,磁盘的扇区大小为 512 字节。count=1
:count
指定了要复制的块数。在这里值为 1,表示只复制 1 个块(512 字节)。因此,loader.bin
文件的前 512 字节将被写入到目标磁盘映像的特定位置。seek=2
:seek
指定了目标文件中的偏移量(以块为单位)。此处的seek=2
表示将目标文件的写入位置移动到距离文件开头 2 个块的位置。由于每个块是 512 字节,所以这意味着文件将被写入到磁盘映像的 1024 字节处(2 * 512 = 1024 字节)。该选项常用于在磁盘映像文件中跳过某些区域(例如引导扇区),并将文件内容写入指定的偏移位置。conv=notrunc
:conv
是转换选项,notrunc
表示在写入时不截断输出文件。默认情况下,如果目标文件已经存在,dd
会将其截断为 0 长度文件,然后开始写入。如果使用notrunc
,则文件将继续写入而不截断它。这样可以确保在目标磁盘映像中不会丢失原有数据,且loader.bin
会追加写入到指定位置。
在boch安装位置处运行bochs模拟器
|
|
如上,屏幕输出了loader程序的内容,因此mbr成功将loader程序从我们规定的磁盘位置加载到了我们规定的内存位置