☰
王爽汇编实验九:显存地址、属性字节与彩色字符显示实战解析
2026/10/14 12:44:46 网站建设 项目流程

王爽《汇编语言》第九章实验九,我当年做到这里的时候,第一次有了一种"我不是在做题,而是在写程序"的感觉。实验要求不算复杂——在屏幕中间分别用绿色、绿底红色、白底蓝色显示字符串"welcome to masm!",但如果你没搞懂显存地址、属性字节和偏移计算这三件事,代码半天憋不出来。这篇文章我把一份能直接编译运行的源码完整放出来,再把每一条指令为什么这么写、踩坑点在哪、怎么用调试器验证都讲透,适合正在学第九章、或者卡在实验九想对照参考的同学。代码可以直接抄,但更建议你先看完原理再动手,这样后面第十章、第十一章才不会继续犯迷糊。

1. 实验九的题面与它真正想考的东西

1.1 题面拆解:三个颜色条件和一个位置条件

先把这个实验的要求拆开看。屏幕上要显示三行内容,内容都是同一个字符串"welcome to masm!",区别在于颜色:

  • 第一行:绿色,也就是黑底绿字。
  • 第二行:绿底红色。
  • 第三行:白底蓝色。

同时还有位置要求:三行都要出现在屏幕中间,而不是左上角、右下角或者随便什么地方。屏幕在文本模式下是80列×25行,所以这里的"中间"指的是水平方向字符串居中、垂直方向三行整体居中。算下来,三行放在第12、13、14行(行号从0开始数)最合适,每行的起始列是第32列,因为字符串一共16个字符,在80列里居中就是 (80-16)/2 = 32。

这三个颜色条件,最终会翻译成三个十六进制属性字节:02H、24H、71H。这三个数字是实验九的核心,代码里所有的颜色控制都靠它们。

1.2 为什么这个实验叫"分水岭"

第九章的章节主题叫"转移指令的原理",但实验九做的却是一件看起来和转移指令关系不大的事:直接往内存地址写数据,让屏幕显示彩色字符。很多初学者做到这里会蒙——刚刚还在学jmp、loop、offset,突然就要操作显存了。

其实这正是这本教材的套路。前面几章在教你"怎么访问内存里的数据",实验九则是把"访问内存"这件事延伸到一个非常具体的场景:内存中的一个特定区域,你往里面写什么,屏幕就显示什么。它考的不是某个新指令,而是你对段地址、偏移地址、循环嵌套和数据布局的综合掌握。

我见过不少同学在这里抄代码抄得飞快,第二天再问他"为什么屏幕中间是第12行不是第13行"就答不上来了。这个实验真正值得学的不是最后显示出来的三行彩色文字,而是"从需求到内存操作"的整个思考过程。这部分想明白了,后面的实验十、实验十一会顺畅很多。

2. 写代码前必须搞清楚的三件事

2.1 显示缓冲区:B8000H 是什么

文本模式下,CPU 会把屏幕上的每一个字符映射到内存里,这块内存叫显示缓冲区(显存)。对于 80×25 彩色文本模式,显存从物理地址 B8000H 开始,到 BFFFFH 结束,一共 32KB。

这里特别容易混的一个点是段地址和物理地址的写法。物理地址 B8000H 由"段地址 ×16 + 偏移地址"得到,所以你在代码里给段寄存器赋的值应该是 0B800H,不是 0B8000H。写成mov ax, 0b8000h汇编阶段就会直接报错,因为 0B8000H 超过了一个 16 位寄存器能装下的范围。这个坑几乎每届都有人踩,我当年也在这卡了十分钟。

用 debug 看显存的时候,输入d b800:0000,这里的 b800:0000 代表的物理地址就是 B8000H。段地址是 b800,偏移是 0000。

2.2 每个字符占用两个字节:ASCII码 + 属性

80×25 彩色文本模式下的显存布局是:每行 80 个字符,但每个字符不是只占一个字节,而是占两个字节。第一个字节是字符的 ASCII 码,第二个字节是属性字节,控制这个字符的颜色、背景色以及是否闪烁。

也就是说,屏幕上显示一个"w",需要往显存里写两个字节:第一个写 57H(w 的 ASCII 码),第二个写颜色属性。整个屏幕一行 80 个字符,实际上要占 80×2=160 字节。这也是后面计算偏移的基础:从一行的某个位置到下一行的同一列,偏移要加 160,也就是十六进制的 0A0H。

这一步我多说一句:很多人一开始容易把显存想象成"像二维数组一样,一个格子一个字符",然后直接往格子里写字符。但机器并不认识"格子",它只认连续的字节。字符和属性交替排列,所以写入的时候一定是"写一个字符字节 + 写一个属性字节"这样成对地写。

2.3 属性字节:高四位背景、低四位前景

属性字节的 8 个 bit 分成两半:低 4 位控制前景色(字符自身的颜色),高 4 位控制背景色。每种颜色用 4 位二进制表示,其中位0是蓝色、位1是绿色、位2是红色。

所以各颜色的编码是:

颜色红色(位2)绿色(位1)蓝色(位0)对应数值
黑0000H
蓝0011H
绿0102H
红1004H
白1117H

实验九要求的三种效果,分别这样算:

  • 黑底绿字:背景色为 0,前景色为 2,属性 = 00H + 02H = 02H。
  • 绿底红字:背景色为绿,要放进高4位,所以背景部分是 2<<4 = 20H;前景色为红,是 4。属性 = 20H + 04H = 24H。
  • 白底蓝字:背景色为白,高4位是 70H;前景色为蓝,是 01H。属性 = 70H + 01H = 71H。

所以数据段里定义的三个属性字节就是02h, 24h, 71h。这三个数字不是凭空来的,把属性字节的位结构搞清楚,你不但能看懂这个实验,以后想换成红底黄字之类也随时会算。

2.4 行列偏移计算:160字节/行与居中位置

现在算偏移地址。显示缓冲区每一行占 160 字节,第 n 行开头的偏移就是 n×160。如果行号从 0 开始数,第 12 行的行偏移是 12×160=1920,十六进制是 0780H;第 13 行是 13×160=2080,也就是 0820H;第 14 行是 2240,即 08C0H。

这只是行偏移。字符串要水平居中,字符串 16 个字符占 32 字节,所以起始列是第 32 列,列偏移是 32×2=64,十六进制是 40H。最终每行的起始偏移要加上这个 40H:

行号行偏移加列偏移后十六进制
第12行0780H07C0H07C0H
第13行0820H0860H0860H
第14行08C0H0900H0900H

代码里把起始偏移定为 07C0H,每次处理完一行就加 0A0H,就能依次指向第 13 行、第 14 行的同一列。这样写的好处是,以后你想把三行显示到第 5、6、7 行,只需要改一个数。

3. 完整源码与运行环境

3.1 可直接编译运行的完整源码

下面这份源码用 MASM 语法编写,在 DOSBox 里用 masm 汇编、link 链接后可以直接运行:

assume cs:code, ds:data data segment str db 'welcome to masm!' att db 02h, 24h, 71h data ends code segment start: mov ax, data mov ds, ax mov ax, 0b800h mov es, ax mov di, 07c0h ; 起始显存偏移:第12行居中 mov si, 0 ; si指向att数组的当前属性 mov cx, 3 ; 外层循环:共3行 rows: push cx push di ; 保存本行起始偏移 mov cx, 16 mov bx, 0 ; bx指向str的当前字符 cols: mov al, str[bx] ; 取字符 mov es:[di], al ; 写入字符字节 mov al, att[si] ; 取属性 mov es:[di+1], al ; 写入属性字节 inc bx add di, 2 loop cols pop di add di, 0a0h ; 下一行相同列 inc si ; 切换属性 pop cx loop rows mov ax, 4c00h int 21h code ends end start

代码里我用str和att两个标签把字符串、属性分开,这样逻辑非常清晰。字符串占 16 字节,属性数组占 3 字节,数据段总共 19 字节。

3.2 运行前的准备:编辑器、MASM、DOSBox

这个实验是 16 位实模式程序,现代的 64 位 Windows 通常没法直接运行,最省事的办法是用 DOSBox 搭一个 DOS 环境。你需要准备三样东西:

  • 一个纯文本编辑器,写汇编源码,保存成.asm文件。
  • MASM 汇编器,一般包括masm.exe和link.exe。
  • DOSBox,用来模拟 DOS 环境运行编译后的 exe。

DOSBox 的配置不复杂:把包含masm.exe、link.exe和你写的.asm文件的目录挂载为一个盘符,比如mount c: d:\asm,然后进入 C 盘操作就行。这个环节本身不难,但经常有人卡在"文件路径找不到"或者"编译报错找不到 masm.exe"上,所以我把完整流程放在后面第 6 章专门讲。

4. 逐段拆解:每一行代码为什么这么写

4.1 段定义与assume:让CPU知道段寄存器怎么用

源码开头:

assume cs:code, ds:data data segment ... data ends code segment start: ... code ends end start

assume不是一条真正的机器指令,它只是告诉汇编器:在后面的代码里,cs寄存器对应code段,ds寄存器对应data段。汇编器在编译mov al, str[bx]这种指令时,知道str在data段,会自动生成基于ds的寻址。

end start表示程序入口在start标号处,链接器和调试器都靠它找到程序开始执行的位置。

4.2 ds指向数据段,es指向显存段

程序一开始的两条赋值是这个实验的精髓:

mov ax, data mov ds, ax mov ax, 0b800h mov es, ax

第一条把数据段地址放进ds,这样后面mov al, str[bx]才能取到字符串。第二条把显存段地址放进es,这样mov es:[di], al才能写屏幕。

为什么不用ds直接指显存段?因为ds一次只能指向一个段。如果让ds=0B800H,写显存倒是方便了,可字符串数据在 data 段里,再想取字符就得先把ds切回去,来回切非常麻烦。用es专门负责显存,ds继续负责数据,两个段各司其职,是实模式汇编里非常典型的做法。

4.3 内外层循环与寄存器分工

这段代码用了两层循环:

  • 外层rows负责行数,循环 3 次,每次处理一行。
  • 内层cols负责这一行的 16 个字符,循环 16 次,每次处理一个字符。

寄存器分工是这样的:

寄存器作用
bx当前字符串中字符的下标,每处理一个字符加1
si当前属性数组的下标,每换一行加1
di当前显存偏移,每处理一个字符加2,每换一行加0A0H
cx循环计数器,内层用完后外层需要恢复

这里有个关键细节:内层loop cols会把cx减到 0,外层loop rows还需要用cx计数。所以在进入内层循环之前,外层先把当前的cx压栈保存,内层循环结束后再从栈里恢复。

rows: push cx push di ... pop di ... pop cx loop rows

di也要压栈保存。因为内层循环每写一个字符,di就加 2,循环结束后di已经指向这行末尾右侧的位置了。不恢复的话,下一行直接从错误位置开始写,屏幕输出就会乱七八糟。压栈恢复是一个非常通用的技巧,后面很多实验都会用到。

4.4 写显存的三条核心指令

整个程序的核心代码是这三条:

mov al, str[bx] ; 取字符 mov es:[di], al ; 写入字符字节 mov al, att[si] ; 取属性 mov es:[di+1], al ; 写入属性字节

第一行执行完,al里放的是当前要显示的字符 ASCII 码;第二行把它写到显存偏移di处;第三行取当前颜色属性;第四行写到di+1处。注意di+1用的是寄存器相对寻址,它不影响di本身的值,所以不需要额外做减法。

写完一个字符后,inc bx让字符指针往后走一位,add di, 2让显存指针跳到下一个字符的位置(因为每字符占 2 字节)。这个"成对写入"的节奏一定要养成肌肉记忆,漏写属性字节或者写错位置,屏幕显示就会完全不对。

4.5 用 4C00H 中断正常退出

程序最后:

mov ax, 4c00h int 21h

这是 DOS 环境下结束程序的固定写法。ah=4C表示"程序终止",al=00表示返回码 0,int 21h触发 DOS 中断执行退出。如果不写这两行,程序执行完loop rows后会继续往下走,落到 data 段数据上当成指令执行,结果就是屏幕乱码甚至死机。

这个细节我在很多初学者代码里看到过:循环写对了,屏幕也能显示,但程序一结束就卡死,最后发现就是没写中断退出。代码里这两行不能省。

4.6 另一种常见写法对照

网上还有一种很常见的参考写法,不在数据段里用str、att标签,而是把数据和属性都放在段开头,然后用数字偏移访问:

data segment db 'welcome to masm!' db 02h, 24h, 71h data ends

这样字符串从偏移 0 开始,属性从偏移 16 开始,代码里用一个寄存器指向 16,另一个寄存器指向 0。两种写法原理完全一样。我推荐你理解有标签的写法,因为str[bx]和att[si]一眼就能看出是在访问哪个数据,调试的时候不容易晕。等你有把握了,再看无标签的写法就会觉得很简单。

5. 最容易翻车的五个错误与排查思路

5.1 屏幕没反应:段地址和偏移地址是重灾区

现象:程序运行完,屏幕干干净净,什么也没出现。

最常见的原因是把段地址写错,比如mov ax, 0b8000h。这个数超过 16 位寄存器范围,编译直接报错,属于"想起来就疼"的错误。另一个隐蔽的原因是偏移算错——比如把起始偏移写成了07c0h之外的数字,数据被写到了屏幕当前不可见的区域。文本模式 25 行的显示缓冲区一共需要 80×25×2 = 4000 字节,也就是 0FA0H 字节。如果你的偏移在 0FA0H 之外,写入的内容不在屏幕范围内,自然看不见。

这种问题我建议直接查es和di的值。用 debug 单步执行,执行到写显存那条指令前,看一眼es=0B800H、di=07C0H是否符合预期,基本能定位。

5.2 颜色对不上:属性字节的位运算反了

现象:内容显示出来了,但颜色不是题目要求。比如第一行该绿的不绿,第二行该红的不红。

这是属性字节高低四位搞反了。24H 是绿底红字,42H 是红底绿字,两者看起来差不多,在屏幕上效果完全不同。还有的人把 71H 写成 17H,白底蓝字就变成了蓝底白字。

排查思路:回到属性字节的二进制,一位一位对。02H = 0000 0010,背景 0(黑)、前景 2(绿);24H = 0010 0100,背景 2(绿)、前景 4(红);71H = 0111 0001,背景 7(白)、前景 1(蓝)。别靠背,靠算。

5.3 只显示一行或显示到屏幕外:外层循环忘记恢复 di

现象:屏幕上只有第一行有字,或者第二行出现在莫名其妙的位置。

如果内层循环结束之后没有pop di,外层第二次循环用的是已经走到行尾的di,再继续写就是下一行行尾后面的位置,效果五花八门。正确做法是进入外层循环体时push di保存本行起始偏移,内层消耗完di之后pop di恢复,再add di, 0a0h往下走一行。

这里我建议你在纸上模拟一遍三行循环的di变化:第一次是 07C0H → … → 内层结束 → 恢复 07C0H → 加0A0H → 0860H;第二次同上变到 0900H;第三次结束后 cx=0 退出。把寄存器值变化画出来,比盯着屏幕瞎猜有效率得多。

5.4 第二行开始乱码:字符串指针没有重置

现象:第一行正常,第二行开始出现莫名其妙的字符。

原因基本只有一个:内层循环每行开始时,bx没有重置为 0。字符串只有 16 个字符,第一行写完bx已经等于 16,第二行继续用就取到 data 段里 16 号偏移之后的数据——那里是属性字节,把它们当字符写进显存,自然乱码。

我写的版本里,内层cols开头固定写mov bx, 0,就是为了保证每行都从字符串头部重新取。这个 bug 特别隐蔽,因为它不是一眼能看出的语法错误,而是逻辑错误。

5.5 程序卡死:loop 的 cx 被冲掉

现象:程序运行后屏幕有输出,但程序不退出,DOSBox 里像死循环一样。

原因通常是内层循环和外层循环共用了cx,但没有压栈保护。进入外层时cx=3,进入内层后cx=16,内层循环结束cx=0,回到外层loop rows时判断cx=0直接退出——看起来只是少循环两次,但如果代码里哪里多写了一个loop,就会导致计数器减到负数或者跳到错误位置。

解决方法和 5.3 一样:外层循环体开头push cx,结尾pop cx。这是处理嵌套循环的标准姿势,不是可选项。

5.6 常见错误速查表

现象可能原因排查重点
屏幕无任何变化段地址/偏移地址错误es、di 初值
颜色不对属性字节高位低位写反02H、24H、71H 二进制值
只显示一行di 没有恢复或没加0A0Hpush/pop di,add di,0a0h
第二行乱码字符串指针未重置内层循环开头 bx 置0
程序卡死/不退出cx 嵌套没有保护push/pop cx,退出中断

这五类问题几乎覆盖了实验九九成以上的报错。你在实际操作中遇到问题时,逐个对照这张表,比自己瞎试快得多。

6. 从编译到运行:DOSBox + MASM 完整流程

6.1 DOSBox + MASM 环境配置

先说工具链。MASM 是老汇编器,典型文件是masm.exe和link.exe。DOSBox 模拟出来的环境可以直接运行这两个程序。

我的建议是建一个干净的目录,比如D:\asm,把这三个东西放进去:

  • masm.exe
  • link.exe
  • test.asm(你写的源码)

启动 DOSBox 后执行:

mount c d:\asm c:

这样 DOSBox 里的 C 盘就对应你电脑上的D:\asm目录,所有文件都在里面,路径清晰,不会出现找不到文件的问题。

6.2 编译、链接、运行的三个命令

编译源码生成目标文件:

masm test.asm;

回车后它默认生成test.obj。分号表示后面所有提示都用默认值,不用一个一个回车确认。

链接生成可执行文件:

link test.obj;

这一步生成test.exe。如果 link 提示找不到模块或者入口,检查一下源码里有没有end start。

运行:

test

正常情况下,屏幕上会出现三行不同颜色的 "welcome to masm!"。第一行绿色、第二行绿底红字、第三行白底蓝字,位置在屏幕中间偏下一点。这个效果一出来,实验就算通了。

6.3 用 debug 验证显存内容

如果你不满足于"看到了"三个字,想确认程序是不是真的按预期写入了显存,用 debug 看一眼最直观。

debug test.exe

在 debug 提示符下输入:

g

程序会运行到int 21h退出,debug 会提示Program terminated normally。这时输入:

d b800:07c0

你会看到显存第一行右侧的数据:57 02 65 02 6C 02 ...。其中 57H 是字符 'w' 的 ASCII 码,紧跟着的 02H 是绿色属性。再查第二行:

d b800:0860

属性字节变成 24H,第三行d b800:0900属性字节变成 71H。这就从内存层面验证了程序的正确性,比单纯用眼睛看屏幕更可靠,也是以后调试更复杂程序时非常好用的方法。

6.4 从一行到三行的分步推进策略

最后分享一个调试思路:不要一上来就写三层逻辑。

第一步,先写死只显示一行,cx直接设成 1,di固定 07C0H。确认屏幕中间能出现16个绿色字符。

第二步,把属性改成第二行的 24H 或者第三行的 71H,确认颜色切换逻辑正确。

第三步,再加外层循环,让三行都显示出来。如果这时候出问题,你就可以确定问题集中在外层循环和di的行偏移上,排查范围小了很多。

这个"先把简单版本跑通,再逐步增加复杂度"的习惯,在做实验九的时候特别受用。不要小看这三步,它省下来的调试时间绝对比多写几行代码的时间多。

最后再讲一个我自己的体会:实验九的代码量虽然不大,但它是整本教材里第一个让我认真坐下来画寄存器变化图的实验。画完之后我才真正理解"程序就是在操作寄存器"这句话什么意思。你如果现在卡在循环嵌套这里,也建议拿张纸,把 bx、si、di、cx 在每一轮循环里的变化一步步写出来,比盯着屏幕看一个小时后放弃要有效得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询