操作系统——显存(三)
操作系统——MBR与显存
2020-09-11 14:50:02 hawk
概述
因为上一节我们已经简单学习了汇编语言中访问内存以及一些跳转的基础指令,因此这节中我们学习通过CPU直接与外设进行通信,从而避免通过使用BIOS的终端功能来进行交互。
IO接口
随着计算机的不断发展,当前的外部设备种类繁多、原理各异、特性不同、数据格式有差异,因此让CPU直接与这些外部设备进行通信是十分不现实的。因此解决这个问题同样依靠计算机方面的优秀传统之一——添加抽象层来解决问题。即通过在外部设备和CPU之间添加一个抽象层,即IO接口,通过这个IO接口来在CPU和外设中完成数据的协调转换工作,从而确保CPU可以高效、方便的和外部设备进行通信。而为了简化CPU访问外设的工作,使其能够轻松的同其他任何外设进行通信,大家约定好了IO接口的功能,基本如下所示
1. 设置数据缓冲,解决CPU与外设之间的速度不匹配问题;
2. 设置信号电平转换电路,解决CPU使用TTL电平无法驱动大部分机电设备类型的外设;
3. 设置数据格式转换,解决CPU和外设数据格式、字长和串并型等匹配问题;
4. 设置时许控制电路来同步CPU和外设;
5. 提供地址译码,使CPU可以选择IO接口上的具体某一个端口,即IO接口上的某一个寄存器,使其可以访问数据总线。
这里再介绍一下CPU如何与IO接口进行通信。CPU和IO接口的通信,实际上是CPU和IO接口的端口,即寄存器,通过总线相连,修改IO接口的端口的过程。因此往往CPU访问IO接口就相当于访问IO接口上的端口。这个访问大体可以分为两种情况——内存映射和独立变址。对于内存映射来说,也就是将端口内容直接映射到内存中,因此CPU想要读写IO接口的端口,只需要读写对应的内存即可,通过mov命令接口实现;而对于独立编址来说,CPU需要通过in和out指令来单独处理,从而访问IO接口的端口。对于in指令,其一般形式如下所示
in al, dx in ax, dx
其中dx中的值是独立编址的端口号,也就是寄存器被独立编译址的地址,而al/ax寄存器用来存储指定IO接口的端口的数据。对于out指令是类似的,其一般形式如下所示
out dx, al out dx, ax out 立即数, al out 立即数, ax
其中立即数/dx的值为独立编址的端口号,也就是寄存器被独立编址的地址 ,而al/ax寄存器用来存储待写入IO接口的端口的数据。
显存、显卡和显示器
由于前面实现的MBR程序的功能就是打印一个字符串,但是是通过调用BIOS的终端实现的。这里通过直接读写显存,不借助任何的软件依赖,同样实现字符串的显示功能。我们将分别介绍一下有关显存、显卡和显示器的相关信息,从而实现这个MBR程序。
CPU、显存、显卡和显示器之间的关系
实际上,为了能从计算机上显示图像,我们需要显示器这个外部设备来帮忙。而无论是那种显示器,其都需要由显卡来进行控制。而显卡实际上就是我们前面讲到的外设和CPU之间的抽象层,其上包含IO接口和显存。因此,CPU只要通过与IO接口和显存进行通信,即可实现与显示器的外部设备的通信。
显存与输出
实际上,只要我们向对应的显存中输出数据,则显卡会处理这些输入的输出,然后显示在显示器上。而结合前面分析的实模式下内存的布局,与显卡的通信我认为是属于内存映射型的,因此我们直接通过访问内存即可完成与显卡的通信,从而完成显示器的显示。下面的问题在于显卡是如何处理这些数据的,从而我们通过输入设置好的内容,经显卡处理后,在显示器上显示处我们想要的结果。
实际上根据实模式下内存布局我们可以看出来,显卡支持三种模式,如下表所示
起始地址 | 结束地址 | 大小 | 用途 |
0xC0000 |
0xC7FFF |
32KB | 显示适配器的BIOS |
0xB8000 | 0xBFFFF | 32KB | 用于文本模式显示适配器 |
0xB0000 | 0xB7FFF | 32KB | 用于黑白显示适配器 |
0xA0000 | 0xAFFFF | 64KB | 用于彩色显示适配器 |
可以看到,其支持文本模式、黑白模式以及彩色模式。由于之后我们实现的操作系统是文本模式的终端界面,因此我们主要关注文本模式即可。
因此可以看到,根据内存映射,只要我们想0xB8000到0xBFFFF这片32KB大小的内存中输出数据,则相当于向显存输出数据,则显卡会将这些数据处理后按规则显示在显示适配器上。这里我们简单介绍一下文本模式下显卡处理数据的规则,首先每一个字符通过2字节的数据进行表示,其每一位的内容如下图所示
这里我们列表说明一下不同的R、G、B和I所组合出来的最终颜色,如下所示
R | G | B | 颜色 |
|
I=0/背景色 | I=1 | |||
0 | 0 | 0 | 黑 | 灰 |
0 | 0 | 1 | 蓝 | 浅蓝 |
0 | 1 | 0 | 绿 | 浅绿 |
0 | 1 | 1 | 青 | 浅青 |
1 | 0 | 0 | 红 | 浅红 |
1 | 0 | 1 | 品红 | 浅品红 |
1 | 1 | 0 | 棕 | 黄 |
1 | 1 | 1 | 白 | 亮白 |
这里我们基本就完成了显卡处理文本模式的规则。下面还要说明一下,显卡的文本模式也是分为多种模式的,用“列数 * 行数”来进行表明,如80 * 25等。一般情况下,在显卡加电后,默认的模式就为80 * 25,即2000个字符,也就是需要4000B的内存。这样基本介绍完了显存与输出的关系。
实验
本次实验的全部代码已经放置在我的仓库中,点此进行跳转
这次我们要实现的功能,是直接通过与显存进行交互,使其在蓝色的背景色下,闪烁浅品红色的“Hawk’s MBR”字符串即可。下面同样给出对应的源代码,如下所示
; 简单的主引导程序,但并没有实现引导的功能,仅仅实现输出字符串 ; 但是其并不需要借助任何库等,这次通过直接访问显存内存,从而输出字符串 ;------------------------------------------------------------------------ SECTION MBR vstart=0x7c00 ;这个地址表示将起始地址设置为0x7c00——因为BIOS会将MBR程序加载到0x7c00处 mov ax, cs mov ds, ax ;由于BIOS跳转到MBR时,使用指令jmp 0:0x7c00,因此cs段寄存器为0,这里将ds段寄存器也设置为了0 mov sp, 0x7c00 ;根据已知,至少0x500-0x7DFF为可用区域,则将其当用作栈即可 mov ax, 0xB800 mov es, ax ;根据已知,实模式1MB内存中0xB8000-0xBFFFF为文本模式的显示适配器,方便之后通过直接寻址读写内存 ; 下面首先清空屏幕,这里使用BIOS提供的中断即可, ;------------------------------------------------------------------------ ; INT 0x10; 功能号:0x06 功能描述:上卷窗口 ;------------------------------------------------------------------------ ; 输入: ; AH--功能号: 0x06 ; AL--上卷的行数(如果为0,表示全部) ; BH--上卷行属性 ; (CL, CH)--窗口左上角(X, Y)位置 ; (DL, DH)--窗口右下角(X, Y)位置 ; 输出: ; 空 mov ax, 0x600 ;AH = 0x06; AL = 0x0; mov bx, 0x700 ;BH = 0x07; BL = 0x0; mov cx, 0x0 ;CH = 0x0; CL = 0x0; mov dx, 0x184f ;DH = 0x18; DL = 0x4f; int 0x10 ;------------------------------------------------------------------------ ; 我们将上面的系统调用分析一下 ; 输入: AH--0x06; AL--0x0; BH--0x7; CL--0x0;CH--0x0; DL--0x4f;DH--0x18; ; 也就是我们调用了功能号为0x6的BIOS中断,窗口左上角为(0x0/0, 0x0/0),窗口右上角坐标为(0x4f/80, 0x18/25),上卷所有的窗口 ; 在VGA文本模式中,一般一行容纳80个字符,共25行,也就相当于清空了整个屏幕 ; 向1MB内存中的文本模式的显示适配器区域写入数据 ;------------------------------------------------------------------------ ; 每个字符2字节,其低字节为字符对应的ASCII码,高字节为字符的属性; 由于其为背景蓝色,前景色浅品红色,不闪烁,其高字节值为 00011101b ;------------------------------------------------------------------------ mov cx, 0x0 mov byte al, [format]; 初始化计数器cx,。由于前面已经设置了ds段寄存器为0,该指令相当于将字符属性字节读入ax寄存器中 LOOP: mov di, cx mov byte dl, [di + string]; 这里通过变址寻址访问内存,由于前面设置了ds段寄存器为0,这里直接获取字符串中的对应字符 sub dl, 0 jz LOOPEND; 判断字符串是否结束。有条件跳转,因此仅仅修改段偏移地址,由于cs始终为0,自然跳转到LOOPEND对应的位置 add di, di mov byte [es:di], dl; 这里通过变址寻址访问内存 add di, 1 mov byte [es:di], al; 这里通过变址寻址访问内存 add cx, 1 jmp LOOP; 计数器+1后,无条件相对近跳转,会重新跳转到LOOP处执行循环 LOOPEND: ;------------------------------------------------------------------------ ; 我们将上面的指令分析一下 ; 可以看到,对于内存寻址来说,这里通过直接寻址进行寻址 ; 我们每一次输入两个字节信息,其中低字节是上面分析的字符的属性 ; 高字节是字符对应的ascii码,从而完成了内存的写入。 ; 下面进行循环,确保程序悬停在该处,从而观察输出 ;------------------------------------------------------------------------ jmp $ ;------------------------------------------------------------------------ ; 我们将上面的指令分析一下 ; $表示当前行的地址,这样子相当于始终执行这一行指令,从而使程序悬停 ; 下面进行常量设置 ;------------------------------------------------------------------------ string db "This is Hawk's MBR", 0; 即伪操作指令,表示每一个元素大小为1字节, 并且在结尾为\x00表明字符串结束 format db 10011101b; 这里是显存中的字符属性,表明其为背景蓝色,前景色浅品红色,并且闪烁 ; 下面进行空白填充,确保最后程序为512字节 ;------------------------------------------------------------------------ times 510 - ($ - $$) db 0 ;------------------------------------------------------------------------ ; 我们将上面的汇编语句分析一下 ; $表示当前行的地址,$$表示当前SECTION的起始地址,times也是伪操作指令,相当于将后面的数据重复指定次数 ; 这个指令确保了将程序填充至512字节,中间部分以0填充 ; 下面我们最后填充该512字节删除的最后两个字节,为0x55,0xaa,从而使BIOS成功识别MBR ;------------------------------------------------------------------------ db 0x55, 0xaa
实际上上面的注释已经很详细了,这里在具体说明一下
1. 前面仍然通过BIOS中断调用清空屏幕,然后通过jmp实现循环,向指定的内存中写入对应的值。
2. 这次代码涉及实模式下的内存访问,这里可以参考一下上一篇博客,其中简单介绍了一下实模式下的汇编相关的知识。这里重要使用了变址寻址和直接寻址。
这里还需要说明一下,不知道什么原因,通过qemu运行无法显示颜色的闪烁现象,这里使用bochs运行该机器,其中bochs的配置可点击链接查看(可能的错误解决方案),也可以查看《操作系统,真象还原》书籍中的介绍,这里我们正常编译,如图所示
下面我们使用配置好的bochs软件进行启动,查看最后的结果(如果使用qemu的话,无法观察到字符的闪烁),结果如下所示