前言:这是王爽版汇编语言第四版第187页的一道题目,因为我觉得有点难,非常有借鉴意义,所以我打算写一篇博客,顺便把题目再写一遍,这样也许会看的更仔细。我会在题目的中间加上自己的思考和分析,因为对于初学者,里面的内容确实有点多,我不加分析的话,一时半会儿自己也看不明白,具体写代码的时候,我也得看着自己的分析才能写(带颜色的部分就是我的思考)。这篇文章更适合刚入门8086汇编的小白,就是个人对复杂问题的思考。
测试环境:visual studio code的masm编译器插件和dosbox环境插件。
以下是原题:
这个编程任务必须在进行下面的课程之前独立完成,因为后面的课程中,需要通过这个实验而获得的编程经验。
编程:在屏幕中间分别显示绿色、绿底红色、白底红色的字符串‘welcome to masm!’。
我们先来分析以下题目的要求:
1、要在dos环境下在终端上显示一个字符串‘welcome to masm!’;
2、必须要在屏幕中间显示,而不是随便找一个地方(一开始测试的时候可以随便一点);
3、显示一句字符串还不够,题目的意思是要显示三句字符串,颜色有区别的。
编程所需的只是通过阅读、分析下面的材料获得。
80x25彩色字符模式显示缓冲区(以下简称为显示缓冲区)的结构
内存地址空间中,B8000H~BFFFFH共32KB的空间,为80x25彩色字符模式的显示缓冲区。向这个地址空间写入数据,写入的内容将立即出现在显示器上。
含义:B8000H~BFFFFH这一段地址与显示有关,往里面写入内容就会相应地显示在屏幕上(具体用法还得参考后面地介绍)
在80x25彩色字符模式下,显示器可以显示25行,每行80个字符,每个字符可以有256种属性(背景色、前景色、闪烁、高亮等组合信息)
这样,一个字符在显示缓冲区中就要占两个字节,分别存放字符的ASCII码和属性。80x25模式下,一屏的内容在显示缓冲区中共占4000个字节。
含义:简单介绍了’80x25‘这个名字的含义,并且粗略指明了这个缓冲区的用法:
一个字符信息要用两个字节才能表示----字符的ASCII和字符的属性。
显示缓冲区分为8页,每页4KB(≈4000B),显示器可以显示任意一页的内容。一般清空下,显示第0页的内容。也就是通常情况下,B8000H~B8F9FH中的4000个字节的内容将出现在显示器上。
含义:80x25彩色字符模式的显示缓冲区有32KB,但是我们这道题目并不需要用到这么大的空间,这32KB被分成了8份,每份是4KB,我们只要把握这4KB即可,也就是说B8000H~B8F9FH,这一段空间我们要进行编程的安排。这一段的偏移地址咱们可以先确定下来:B800H,但是我们在写的时候要写0B800H,前面有说过我们不能直接把字母放在第一个,如果第一个是字母的话,前面再放一个0.
在一页的显示缓冲区中:
偏移000~09F 对应显示器上的第1行(80个字符占160字节);
偏移0A0~13F对应显示器的第2行;
偏移140~1DF对应显示器上的第3行。
以此类推,可知,偏移F00~F9F对应显示器上的第25行。
咳咳,前面的都是开胃小菜,从这里开始我们就要进行推理和计算了。作者在这里告诉我们,前面的4000B的显示缓冲区的内容是可以被拆分的,也就是说B800:0000H代表了dos环境的左上角区域,而B800:0F0FH则代表了dos环境的右下角的区域,然后这中间的地方分别就是dos环境的中间的区域。并且一行有160个字节,一共有25行。此时我们回忆一下:我们的第二个任务就是在屏幕的中间部分显示字符串,我们需要一定的推理,把dos环境的中间部分给推理出来。
因为我们要在屏幕上写入四句”welcome to masm!“,而一页dos共有25行,25/2就是12.5,所以我们可以把这四句分别写在11,12,13行这样的地方,每一行间隔A0H,根据推算,这四段内存地址的偏移地址的行分别为((25-3)/2)*160=06E0H,0780H,0820H。
分析完行,我们再去分析具体位置,‘welcome to masm!’一共有16个字符,占16个字节,然后字符+字符属性凑成一组,所以每一句都要占16+16=32个字节--32B,而每一行都有160字节,所以我们的每一行的起始地址都是(160-32)/2=64字节。
在具体写入内存的时候,我们需要把以上两段绿色字体的内容给结合起来,作为偏移地址写进去。
在一行中,一个字符占两个字节的存储空间(一个字),低位字节存储字符的ASCII吗,高位字节存储字符的属性。一行共有80个字符,占160字节。
即在一行中:
00~01 单元对应显示器上的第1列,;
02~03 单元对应显示器上的第2列;
04~05 单元对应显示器上的第3列;
以此类推,可知,9E~9F 单元对应显示器上的第80列。
含义:作者在这里又给我们出了一个”难题“,我们需要在B800H这一段的空间中写入咱们的字符串和字符串属性,而一组这样的字符串和字符串属性占了两个字节,我们并不需要一个一个来给它移动,因为我们有占用两个字节并且可以拆分的通用寄存器AX,我们可以利用它可拆分的特点,用AH存储字符属性,用AL存储字符本身,然后把这个AX寄存器整体赋值给那一段内存空间(咱们之前已经分析好的那一段)
例:在显示器的0行0列显示黑底绿色的字符串‘ABCDEF’
(‘A’的ASCII码值为41H,02H表示黑底绿色)
显示缓冲区里的内容为:
00 01 02 03 04 05 06 07 08 09 0A 0B 0C ... 0E 0F
B800:0000 41 02 42 02 43 02 44 02 45 02 46 02 ...
...
...
B800:00A0 .. .. .. .. .. .. .. .. .. ..
可以看出,在显示缓冲区中,偶地址存放字符,奇地址存放字符的颜色属性。
说明:以上内容是原文中的一个例子,我认为这个例子很重要,看懂了这个例子,那么程序也就八九不离十地能写出来了,我们一会儿再尝试写一个样板程序(样板程序:根据提示我们可以写出一个差不多的程序但是只是完成了任务的某几个不是完成全部任务,因为学习也是需要循序渐进的,一般人不太能一来就写出一个完美的程序,需要后期修修改改)
含义:我们要输出一个字符串,但是目前我们的能力还是只能一个字符一个字符这样子去操作,而不是直接就像C++、Python那样定义一个字符串类型(对象)。我们需要把这个字符串‘welcome to masm!’拆成16个字符利用循环一个一个来操作。然后在显示缓冲区中每一个字符的显示需要有两个部分:1、字符对应的ASCII码(1B);2、字符的属性(1B)。那么也就是两个刚好挨在一起字节就能够显示一个字符了!并且还有规律:字符在前属性在后,我们可以看那个例子,也可以看作者给我们总结出来的经验。
那么我就按照我的思路先写出来一个样板程序了。
assume cs:codesg,ds:datasg
datasg segment
db 'welcome to masm!';定义了这个字符串放在ds数据段
db 16 dup (01000010b);定义了每个字符的属性
datasg ends
codesg segment
start: mov ax,datasg
mov ds,ax
mov ax,0b800h;简单起见,我先把文字放在左上角
mov es,ax
mov ax,0
mov si,0
mov bx,0
mov cx,16
s:mov al,[bx];把字符存放在低位
mov ah,[bx+16];把属性存放在高位
inc bx
mov es:[si+00a0h],ax
add si,2
loop s
mov ax,4c00h
int 21h
codesg ends
end start
这个样板程序里,我先把文字放在左上角,因为我们现在研究的是王爽老师给我们的那个例子,检验一下我们的分析是否是对的,在程序里我们可以看到,我们把字符和属性分别放在了低位和高位,结果成功了。
其实按照目前的情况,我们离完成王爽老师给我们布置的任务已经非常接近了,但是还是缺点什么,我在这里罗列一下,以方便后续的改进:
1、字符串只有一个,还不是三个;
2、字符串没有显示在dos框的正中间,而是左上角;
3、字符串的颜色并没有符合我们任务的要求。
一个在屏幕上显示的字符,具有前景(字符色)和背景(底色)两种颜色,字符还可以以高亮度和闪烁的方式显示。前景色、背景色、闪烁、高亮等信息被记录在属性字节中。
属性字节的格式:
7 6 5 4 3 2 1 0
含义: BL R G B I R G B
闪烁 背景 高亮 前景
R:红色
G:绿色
B:蓝色
可以按位设置属性字节,从而配出各种不同的前景色和背景色。
说明:这一段其实非常好理解,我们从前面的知识可以知道,我们字符属性是用一个字节(8位)来确定的,为了能够很好地把握字体和背景的颜色,我们必须拆分了这仅有的一个字节,把分出来的8个位分别拿来用,以达到比较好的效果。
我们再对这八个位分别来分析:第一位是闪烁位,可以把它当作是Boolean类型的值,如果是1就闪烁,如果是0就不闪烁;同理,高亮也是一样的道理。当然了,作者并没有要求我们使用这二者,所以默认是0即可,如果想再尝试高难度的显示,可以使用这二者。
比如:
红底绿色:属性字节为:01000010B;
红底闪烁绿色,属性字节为:11000010B;
红底高亮绿色,属性字节为:01001010B;
黑底白色,属性字节为:00000111B;
白底蓝色,属性字节为:01110001B.
说明:这里作者给我们举了一些例子,来说明这个属性的8个位应该如何使用,属性字节末尾的那个B不是字节的意思,而是二进制的意思。二进制b,十进制d,十六进制h。
例:在显示器的0行0列显示红底高亮闪烁绿色的字符串‘ABCDEF’
(红底高亮闪烁绿色,属性字节为:11001010B-->CAH)
显示缓冲区里的内容为:
00 01 02 03 04 05 06 07 08 09 0A 0B ... 9E 9F
B800:0000 41 CA 42 CA 43 CA 44 CA 45 CA 46 CA ....
...
...
B800:00A0
注意:闪烁的效果必须在全屏DOS的方式下才能看到。
说明:作者的提示到此就结束了,我们此时也明白了作者的意图,按照作者的提示我们可以写出程序。我们还需要根据提示把任务要求的颜色的8位二进制给写出来。
绿色:00000010B -> 02H
绿底红色:01000010B -> 42H
白底蓝色:01110001 -> 71H
说明:因为作者在前面给了我们一些颜色的例子,所以我们可以根据例子里的颜色提取出对应的RGB颜色,然后再拼凑出来题目中要求的颜色,过程我就不写了。(如果对颜色还有疑问请再多看看作者的例子(doge))
至此,题目的信息我们已经全部掌握了,现在我们可以开始写我们的程序了。
以下是我写的程序:
assume cs:codesg,ds:datasg
datasg segment
db 'welcome to masm!';这是我们的目标字符串
db 02h,42h,71h;这是我们的目标颜色(十六进制)
datasg ends
codesg segment
start:mov ax,datasg
mov ds,ax
mov ax,0b800h
mov es,ax
mov bx,0
mov si,16
mov di,06e0h
mov bp,64
mov ax,0
mov cx,3;因为我们要输出三句字符串,需要创造两层循环
s1:mov ax,0
mov dx,cx
mov cx,16
mov ah,[si]
inc si
s:mov al,[bx]
inc bx
mov es:[bp+di],ax
add bp,2
loop s
mov bx,0
add di,00a0h
mov bp,64
mov cx,dx
loop s1
mov ax,4c00h
int 21h
codesg ends
end start
我写的并不好,但是为了多用到前面学到的东西,所以我写了两层循环,并且可寻址的寄存器都用掉了。
终于用汇编能在屏幕上显示一点东西了,有点感动。
不得不说王爽老师写的书确实不错,既通俗易懂,又循循善诱。