4. 用户程序的工作流程

1. 初始化段寄存器和堆栈切换

伪指令 resb(REServe Byte)的意思是从当前位置开始,保留指定数量的字节,但不初始化它们的值。

2. 调用字符串显示例程

在屏幕上显示字符,所做的仅仅是填充显存,只要所填充的内容不超过一屏所能显示的字符数,其他的事不需要你操心。当字符在一行上显示不下时,显示系统会自动移到下一行接着显示,这也和你无关。

有时候我们希望有自行换行的能力,而不管那一行是否已经到头(屏幕最右边)。

在 128 个 ASCII 代码中,大部分是可显示和打印的字符,还有一部分用于控制显示和打印那些字符的设备。比如 0x0d 是回车,0x0a 是换行。

回车和换行的概念最早起源于老式打字机。那种打字机上有滚筒,用于使纸张上下卷动,每敲击一个按键,字车往右移动一格,位于下一个可打印的位置。在这种古老而不失先进性的设备上,将字车推到最左边,也就是一行的开始,叫做回车(Carriage Return);而拧一下滚筒,将纸上卷一行,叫做换行(Line Feed)。如果既回车,又换行,那么,字车将位于下一行的行首。这个过程通常叫做回车换行(CRLF)。

回车分配的 ASCII 码是 0x0d,换行分配的则是 0x0a。

第 175~191 行,凡是需要回车换行的地方,都使用了 0x0d 和 0x0a。而且,在第 191 行,也就是所有要显示内容最后,是数值 0,用来标志字符串的结束,这样的字符串称为是 0 终止的字符串,在高级语言里经常使用(ASCII中0是NUL,也就是空字符的意思)。

4. 屏幕光标控制

过程 put_char 用于显示一个字符。但它与常规方法的不同之处在于,它能判断回车和换行,还能在超过屏幕上最后一行的时候上滚内容,就是我们经常说的卷屏或者滚屏。除此之外,它还使用了光标跟随技术。

光标(Cursor)是在屏幕上有规律地闪动的一条小橫线,通常用于指示下一个要显示的字符位置。早期所有的软件都在文本模式下工作,而基于硬件的光标只在文本模式下才会出现。

多年前形成的 VGA 显示标准在每块显卡中都完好地保留下来了,包括对光标的支持。原因很简单,在显卡中集成一块支持128个ASCII代码的字符发生器非常方便,在程序中显示一个字符也只要给出它的ASCII码。显示图形的代价太大,在计算机加电启动的时候,以及其他一些根本没必要、也没条件使用图形模式的场合,这是最好的选择。

光标在屏幕上的位置保存在显卡内部的两个光标寄存器中,每个寄存器是8位的,合起来形成一个16位的数值。比如,0表示光标在屏幕上第0行第0列,80表示它在第1行第0列,因为标准VGA文本模式是25行,每行80个字符。这样算来,当光标在屏幕右下角时,该值为25×80-1=1999

光标寄存器是可读可写的。你可以从中读出光标的位置,也可以通过它设置光标的位置。能够通过写入一个数值来设定光标的位置,因为显卡从来不自动移动光标位置(!!!!!),必须你自己写入。

5. 取当前光标位置

显卡的操作非常复杂,内部的寄存器也不是一般地多。为了不过多占用主机的I/O空间,很多寄存器只能通过索引寄存器间接访问

索引寄存器的端口号是0x3d4,可以向它写入一个值,用来指定内部的某个寄存器。比如,两个8位的光标寄存器,其索引值分别是14(0x0e)和15(0x0f),分别用于提供光标位置的高 8位和低8位。

指定了寄存器之后,要对它进行读写,这可以通过数据端口 0x3d5 来进行。

图8-19给出了过程put_char的工作流程图。

4. 用户程序的工作流程

第 43~48 行,在过程put_char的开始部分先将用到的部分寄存器压栈保存,其中包括两个段寄存器 DS 和 ES。

第 51~53 行,通过索引端口告诉显卡,现在要操作 0x0e 号寄存器。

第 54~56 行,通过数据端口从 0x0e 号端口读出 1 字节的数据,并传送到寄存器 AH 中, 这是屏幕光标位置的高 8 位。

同样地,第58~62行,从0x0f号寄存器读出光标位置的低8位。现在,寄存器AX中是完整的光标位置数据。第63行,将这个数值传送到寄存器BX中保存,因为马上就要用到寄存器 AX。

6. 处理回车和换行字符

过程 put_char 仅接受一个寄存器参数 CL,用于提供要显示的 ASCII 码。常规字符和回车、换行符将不同对待,为此,需要首先别出它们。

第 65、66 行,先判断是不是回车符0x0d。如果是的话,继续往下执行,如果不是,则转移到标号.put_0a 处执行。

先来看看如果是 0x0d 的情况。

如果是回车符0x0d,那么,应将光标移动到当前行的行首。每行有 80 个字符,那么,用当前光标位置除以80,余数不要,就可以得到当前行的行号。接着,再乘以80,就是当前行行首的光标数值。

第 67~69 行,用寄存器 AX 中的光标位置除以寄存器 BL 中的 80,在 AL中得到的是当前行的行号。

7. 显示可打印字符

下面开始正常显示可打印字符。

第81、82行,将附加段寄存器ES设置为指向显存。注意,在过程开始处,已经将ES的内容压栈保存了,这里可以随意使用该寄存器。

标准模式下,屏幕上可以同时显示2000个字符。光标占用一个字符的位置,但整个屏幕只有一个,只能出现在2000个字符位置中的一个上。典型地,程序员要用光标位置来记载和跟踪下一个字符应当显示在什么位置。光标用来指示字符位置,而一个字符在显存中对应两个字节。如此一来,可以将光标位置乘以2,来得到该位置(字符)在显存中的偏移地址。

第 83 行,将寄存器BX的内容逻辑左移1次,这相当于将其乘以 2。毕竟只是乘以2,而且BX中的数值不大,这样做,比使用乘法指令 mul 来得方便。

第 84 行,用 BX 的内容做为偏移地址,来访问段寄存器 ES 所指向的显存,来写入要显示的字符。你可能觉得奇怪,为什么后面没有写显示属性字节。原因很简单,在写入其他内容之前,显存里全是黑底白字的空白字符,所以不需要重写黑底白字的属性。过程 put_char 是以黑底白字来显示字符的。

第 87、88 行,将寄存器BX的内容除以2,恢复它的光标位置身份。接着,将其增加1(在数值上,将光标推进到下一个位置,毕竟还没开始设置光标呢)。指令shr是已经讲过的逻辑右移指令,相当于除以 2。

不管是换行,还是正常显示字符后推进光标,都会使寄存器 BX 的内容超过 1999。下面,就来判断这个情况,并决定是否滚动屏幕内容。

8. 滚动屏幕内容

第 91、92 行,比较寄存器BX中的内容是否小于2000。如果是的话,很好,很正常,直接转移到标号.set_cursor处设置光标;否则继续往下执行以滚动屏幕内容。

滚动屏幕内容,实质上就是将屏幕上第2~25行的内容整体往上提一行,最后用黑底白字的空白字符填充第25行,使这一行什么也不显示。

程序里采用的是将数据从一个内存区域(块)搬运到另一个内存区域(块)的做法,核心指令是 movsw。

第 94~101 行,设定源区域从显存内偏移地址为 0xa0(屏幕第 2 行第 1 列的位置)的地方开始,该区域的段地址在段寄存器 DS 中,偏移地址在变址寄存器 SI 中;目标区域从显存内偏 移地址为 0x00(屏幕第1行第1列的位置)的地方开始,该区域的段地址在段寄存器ES中,偏移地址在变址寄存器DI中。同时,设置方向标志,并在寄存器 CX 中设置要传送的字数 1920(24 行乘以 80 个字符/行,再乘以每个字符占用的字节数2,再除以 2 字节/字)。最后,执行rep movsw 以完成传送工作。

屏幕最下面一行(第25行)还有原来的内容,必须予以清除。第 25 行第 1 列在显存中的偏移地址是 3840。为此,第 102~107 行,使用黑底白字的空白字符循环写入这一行。

最后,第 109 行,滚屏之后,光标应当位于最后一行的第 1 列,其数值为 1920,这一行的指令将这个新的数值传送到寄存器 BX 中。

9. 重置光标

不管是回车、换行,还是显示可打印的字符,上面的各处都给出了光标位置的新数值。下面的工作就是按给出的数值在屏幕上设置光标。

第 112~123 行,还是依照老规矩,通过索引端口指定光标寄存器 0x0e 和 0x0f,并分别将寄存器 BX 中的高 8 位和低 8 位通过数据段口 0x3d5 写入它们。

最后,第125~130行,从堆栈中依次弹出并恢复各个寄存器的原始内容。

第 132 行,指令 ret 从堆栈中恢复指令指针寄存器 IP 的内容,返回到调用者 put_string 过程。当字符串 msg0 中所有的字符都显示完毕后,过程put_string返回到用户主程序,从第 147行接着往下执行。

10. 切换到另一个代码段中执行

11. 访问另一个数据段