一、显卡和显存

为了显示文字,通常需要两种硬件,

  • 显示器
  • 显卡

显 卡:为显示器提供内容,并控制显示器的显示模式和状态。
显示器:将那些内容以视觉可见的方式呈现在屏幕上。

集成显卡:主板集成显卡是指集成在主板北桥中的显卡
核心显卡:处理器集成显卡就是指集成在cpu内部的显卡
独立显卡:有独立的显示芯片,自己本身是一张独立的卡的显卡,一般均有独立显卡,采用PCI接口插槽

显卡控制显示器的最小单位是像素,一个像素对应着屏幕上的一个点。
屏幕上通常有数十万乃至更多的像素,通过控制每个像素的明暗和颜色,我们就能让这大量的像素形成文字和美丽的图像。


显卡都有自己的存储器,因为它位于显卡上,故称显示存储器(Video RAM: VRAM),简称显存
要显示的内容都预先写入显存。

和其他半导体存储器一样,显存并没有什么特殊的地方,也是一个按字节访问的存储器件。


二、黑白和真彩色

对显示器来说,显示黑白图像是最简单的,因为只需要控制每个像素是亮,还是不亮。
如果把不亮当成比特“0”,亮看成比特“1”,那就好办了。
因为,只要将显存里的每个比特和显示器上的每个像素对应起来,就能实现这个目标。

如上图所示:
显存的第 1 个字节对应着屏幕左上角连续的 8 个像素;
第 2 个字节对应着屏幕上后续的 8 个像素,后面的依次类推。

显卡的工作是周期性地从显存中提取这些比特,并把它们按顺序显示在屏幕上。



黑色和白色只需要 1 个比特就能表示,但要显示更多的颜色,1 个比特就不够了。
现在最流行的,是用 24 个比特,即 3 个字节,来对应一个像素。
因为 2^24=16777216,所以在这种模式下,同屏可以显示 16777216 种颜色,这称为真彩色


三、文本模式

不管是显示图片,还是文字,对显示器来说没有什么不同,因为所有的内容都是由像素组成的,区别仅仅在于这些像素组成的是什么。

问题是,操作显存里的比特,使得屏幕上能显示出字符的形状,是非常麻烦、非常繁重的工作,因为你必须计算该字符所对应的比特位于显存里的什么位置。

为了方便,工程师们想出了一个办法。
就像一个二进制数既可以是一个普通的数,也可以代表一条处理器指令一样,他们认为每个字符也可以表示成一个数
比如,数字 0x4C 就代表字符“L”,这个数被称为是字符“L”的 ASCII 代码,后面会讲到。

如上图所示,可以将字符的代码存放到显存里,第 1 个代码对应着屏幕左上角第 1 个字符,第 2 个代码对应着屏幕左上角第 2 个字符,后面的依次类推。

剩下的工作是如何用代码来控制屏幕上的像素,使它们或明或暗以构成字符的轮廓,这是 “字符发生器”“控制电路” 的事情。

传统上,这种专门用于显示字符的工作方式称为 文本模式

文本模式图形模式是显卡的两种基本工作模式,可以用指令访问显卡,设置它的显示模式。
在不同的工作模式下,显卡对显存内容的解释是不同的。


四、显存映射

为了给出要显示的字符,处理器需要访问显存,把字符的 ASCII 码写进去。
但是,显存是位于显卡上的,访问显存需要和显卡这个外围设备打交道。

同时,多一道手续自然是不好的,这当中最重要的考量是速度和效率。
为了实现一些快速的游戏动画效果,或者播放高码率的电影,不直接访问显存是办不到的。

为此,计算机系统的设计者们,这些敢想敢干的人,决定把显存映射到处理器可以直接访问的地址空间里,也就是内存空间里。

8086 可以访问 1MB 内存。
其中,0x00000~9FFFF 属于常规内存,由内存条提供;
0xF0000~0xFFFFF 由主板上的一个芯片提供,即 ROM-BIOS。

这样一来,中间还有一个 320KB 的空洞,即 0xA0000~0xEFFFF。传统上,这段地址空间由特定的外围设备来提供,其中就包括显卡。
因为显示功能对于现代计算机来说实在是太重要了。

由于历史的原因,所有在个人计算机上使用的显卡,在加电自检之后都会把自己初始化到 80×25 的文本模式。
在这种模式下,屏幕上可以显示 25 行,每行 80 个字符,每屏总共 2000 个字符。

一直以来,0xB8000~0xBFFFF 这段物理地址空间,是留给显卡的,由显卡来提供,用来显示文本


五、初始化段寄存器

和访问主内存一样,为了访问显存,也需要使用逻辑地址,也就是采用“段地址:偏移地址” 的形式,
这是处理器的要求

考虑到文本模式下显存的起始物理地址是 0xB8000,这块内存可以看成是段地址为 0xB800,偏移地址从 0x0000 延伸到 0xFFFF 的区域,因此我们可以把段地址定为 0xB800

访问内存可以使用段寄存器 DS,但这不是强制性的,也可以使用 ES
因为 DS 还有别的用处,所以在这里我们使用 ES 来指向显存所在的段。

首先把立即数 0xB800 传送到 AX,然后再把 AX 的值传送到 ES
这样,附加段寄存器 ES 就指向 0xb800 段(段基地址为 0xB800)。

mov ax,0xB800
mov es,ax

为什么不这样写?

mov es,0xb800

原因是不存在这样的指令,Intel 的处理器不允许将一个立即数传送到段寄存器,它只允许这样的指令:

mov 段寄存器,通用寄存器 
mov 段寄存器,内存单元

一旦将显存映射到处理器的地址空间,并且将基地址传送到段寄存器,我们就可以使用普通的传送指令(mov)来读写它。

我们把 0xB800 作为段地址传送到附加段寄存器 ES,以后就用ES 来读写显存。
这样,段内偏移为 0 的位置就对应着屏幕左上角的字符。


六、ASCII代码

在计算机中,每个用来显示在屏幕上的字符,都有一个二进制代码。

这些代码和普通的二进制数字没有什么不同,唯一的区别在于,
发送这些数字的硬件和接收这些数字的硬件把它们解释为“字符”,而不是“指令”或者用于计算的“数字”。

这就是说,在计算机中,所有的东西都是无差别的数字,它们的意义,只取决于生成者和使用者之间的约定。

为了在终端和大型主机,以及主机和打印机、显示器之间交换信息,
1967 年,美国国家标准学会制定了美国信息交换标准代码(American Standard Code for Information Interchange,ASCII)。

ASCII 是 7 位代码,只用了一个字节中的低 7 比特,最高位通常置 0

这意味着,ASCII 只包含 128 个字符的编码。

所以,在表中,水平方向给出了代码的高 3 比特,而垂直方向给出了代码的低 4 比特。
比如字符“*”,它的代码是二进制数的 010 1010,即 0x2A

ASCII 表中有相当一部分代码是不可打印和显示的,它们用于控制通信过程。
比如,LF 是换行;CR 是回车;DELBS 分别是删除和退格,在我们平时用的键盘上也是有的;

BEL 是振铃(使远方的终端响铃,以引起注意);SOH 是文头;EOT 是文尾;ACK 是确认,等等。

屏幕上的每个字符对应着显存中的“两个连续字节”,
前一个是字符的 ASCII 代码,后面是字符的显示属性,包括字符颜色(前景色)和底色(背景色)。

如上图所示,
字符“H”的 ASCII 代码是 0x48,其显示属性是 0x07
字符“e”的 ASCII 代码是 0x65,其显示属性是 0x07

字符的显示属性(1 字节)分为两部分,低 4 位定义的是前景色,高 4 位定义的是背景色。
色彩主要由 R、G、B 这 3 位决定,毕竟我们知道,可以由红(R)、绿(G)、蓝(B)三原色来配出其他所有颜色。

K 是闪烁位,为 0 时不闪烁,为 1 时闪烁;
I 是亮度位,为 0 时正常亮度,为 1 时呈高亮。

上图中的字符属性 0x07 可以解释为黑底白字,无闪烁,无加亮。

你可能觉得奇怪,当屏幕上一片漆黑,什么内容都没有的时候,显存里会是什么内容呢?

实际上,这个时候,屏幕上显示的全是黑底白字的空白字符,也叫空格字符(Space),ASCII 代码是 0x20
当你用大拇指按动键盘上最长的那个键时,就产生这个字符。因为它是空白,自然就无法在黑底上看到任何痕迹了。


七、显示字符

为了方便,多数汇编语言编译器允许在指令中直接使用字符的字面值来代替数值形式的 ASCII 码:

mov byte [es:0x00],'L'

这等效于

mov byte [es:0x00],0x4c

尽管通过查表可以知道字符“L”的 ASCII 代码是 0x4C,但毕竟费事。

不过,要在指令中使用字符的字面值,这个字符必须用“引号”围起来。

在源程序的编译阶段,汇编语言编译器会将它转换成 ASCII 码的形式。

当前的 mov 指令是将立即数传送到内存单元,目的操作数是内存单元,源操作数是立即数(ASCII 代码)。

为了访问内存单元,只需要在指令中给出偏移地址,在这里,偏移地址是 0x00

一般情况下,如果没有附加任何指示,段地址默认在段寄存器 DS。比如:

mov byte [0x00],’L’

当执行这条指令后,处理器把段寄存器 DS 的内容左移 4 位(相当于乘以十进制数 16 或者十六进制数 0x10),加上这里的偏移地址 0x00,就得到了物理地址。

但是实际上,显存的段地址位于段寄存器 ES 中,我们希望使用 ES 来访问内存。
因此,这里使用了段超越前缀 “es:”。这就是说,我们明确要求处理器在生成物理地址时,使用段寄存器 ES,而不是默认情况下的 DS

因为指令中给出的偏移地址是 0x00,且 ES 的值已经在前面被设为 0xB800,故它指向 ES 段中,偏移地址为 0 的内存单元,
0xB800:0x0000,也就是物理地址 0xB8000,这个内存单元对应着屏幕左上角第一个字符的位置。

因为目的操作数给出的是一个内存地址,我们要用源操作数来修改这个地址里的内容,
所以,目的操作数必须用方括号围起来,以表明它是一个地址,
处理器应该用这个地址再次访问内存,将源操作数写进这个单元。实际上,这类似于高级语言里的指针


八、数据宽度判断

mov byte [es:0x00],'L'

关键字“byte”用来修饰目的操作数,指出本次传送是以字节的方式进行的。

在 16 位的处理器上,单次操作的数据宽度可以是 8 位,也可以是 16 位。

到底是 8 位,还是 16 位,可以根据目的操作数或者源操作数来判断。

遗憾的是,在这里,目的操作数是偏移地址 0x00,它可以是字节单元,也可以是字单元,到底是哪一种,无法判断;
而源操作数呢,是立即数 0x4C,它既可以解释为 8 位的 0x4C,也可以解释为 16 位的 0x004C

在这种情况下,编译器将无法搞懂你的真实意图,只能报告错误,所以必须用“byte”或者“word”进行修饰(明确指示)。
于是,一旦目的操作数被指明是“byte”的,那么,源操作数的宽度也就明确了。

相反地,下面的指令就不需要任何修饰:

mov [0x00],AL    ;按字节操作 
mov AX,[0x02]    ;按字操作