2015.12.16 - 2016.01.07
读《汇编语言》I
读《汇编语言》II
12.16
通过学习关键指令来深入理解机器工作的基本原理,培养底层编程意识和思想 - 如何利用硬件系统的编程结构和指令集有效灵活地控制系统进行工作。
显示器中的显示与计算机内存中高低电平的对应关系 - 计算机内的高低电平高显示器中的显示。
汇编指令时机器指令[Page.1]便于记忆的书写格式 - 汇编指令与机器指令一一对应。
汇编编译器将显示在显示器上的汇编指令在内存中的高低电平序列转换为汇编指令所对应机器指令在内存中的高低电平序列。
在汇编程序(本书基于的汇编指令)中,由end伪指令指定的段为代码(指令)段(即将此地址赋值给CS:IP),其余当数据处理。
在C语言中,代码段(指令)从main处开始……
——————————————
所有可寻到的内存单元(最小内存单位)就构成内存地址空间 - 随机存储器、(装BIOS的)ROM、接口卡上的RAM这些存储器在物理上是独立的器件,但CPU在操控它们时,把它们都当作内存(主板上的随机存储器 - 主随机存储器)来对待,把它们总的看作一个由若干存储单元组成的逻辑存储器,每个物理存储器在逻辑存储器中都占有一个地址段(物理上独立的存储器件都和CPU总线相连,CPU读写它们的操作也相同)- 这个逻辑存储器即为内存地址空间。CPU在往逻辑存储器的地址中读/写内容时,实际上就是在读/写相应的物理存储器中的内存单元(P.12)。
这段内存地址空间跟输入译码器的地址数据相对应。译码器根据输入的地址数据输出/连接到对应的物理内存单元之上。
在(避开有保护内存能力的操作系统的情况下),编写程序时要清楚内存地址空间的分配情况,不能直接写存有重要数据或代码的内存(空间)。从而需要寻找一段安全的内存空间供咱使用(如在DOS下,DOS和其它合法程序不会使用0:200 ~ 0:2ff这256字节空间)。
在基于一个计算机硬件编程时,须知道内存地址空间的分配情况 - 内存地址空间与实际物理存储器的对应关系。
所有内存单元构成的存储空间是一个一维的线性空间 - 内存地址空间,每一个内存单元在这个空间中都有唯一的地址,将这个唯一的地址称为物理地址(物理地址构成内存逻辑存储地址)。
(1) 从CS:IP指向的内存单元读取指令,读取的指令进入指令缓冲器;
(2) IP = IP + 所读取指令的长度,从而指向下一条指令;
(3) 执行指令。转到步骤(1),重复这个过程。
12.17
在设计CPU(或内存)时,有没有在寄存器、内存单元间设计相连的通道(导线)。如8086PC“不支持立即数到段寄存器”、“内存单元与内存单元”之间的传输。
最后进入栈中的数据,最先出去。它有两个最基本的操作:入栈(将新的元素放到栈顶)和出栈(从栈顶取出元素)。8086CPU提供相关的指令(push/pop)来以栈的方式访问内存空间。这意味着,在基于8086CPU编程时,可以将一段内存当作栈来使用(“指令 + 寄存器”实现将内存当作栈来使用)。 - 通过所提供的指令将栈机制用在内存的使用上。
12.18
DOS加载程序过程见P.92。程序的加载到运行的过程简示 - 加载程序到内存,备份硬件资源的值,修改硬件资源的值(如将CS:IP指向程序的入口),程序运行完毕返回(恢复加载程序的运行状态)到加载程序中。
12.19
可执行文件由描述信息和程序组成:
2016.01.03
CPU通过接口芯片的端口和外部设备进行联系。
不可屏蔽中断类型码固定为2,所以不可屏蔽中断过程没有以上中断的第一步。
2016.01.04
可以通过依据数据,直接计算出所要找的元素的位置的表,我们称其为直接定址表。
2016.01.05
由一个硬件操作触发一个(串)高/低电平序列并输入给另一个硬件部件,该硬件部件根据输入的高/低电平序列得到相应的输出并将输出输入给下一个硬件部件……
寄存器 | 功能 |
CS = M, IP = N | 8086CPU将从内存M x 16 + N单元开始,读取一条指令并执行 - 任意时刻,CPU将CS:IP指向的内容当作指令并会执行。 |
DS和[地址] | [1]CS:IP指向的内存单元中的内容为指令,其余寄存器或表达式指向的内存单元中的内容都为数据 [2]在访问内存单元时(执行访问内存的指令如mov ax, [0001] - 在debug.exe中,masm.exe将mov ax, [0001]翻译为mov ax, 0001),默认取ds的值作为偏移地址([地址])的段地址 |
[bx || si || di],[bx || si || di + itada], [bx + si || di], [bx + si || di + idata] | 用这些偏移地址取内存中的数据时,默认的段地址为ds |
SS和SP | 执行栈指令(PUSH/POP)时,访问SS:SP指向的内存单元[任意时刻,SS:SP指向栈顶元素] |
[bp],[bp + si || di],[bp + si || di + idata] | 用这种格式作为偏移地址取内存中的数据时,默认的段地址为ss |
AX || AX + DX | div指令:如果除数为8位,则被除数为16位且放在AX中,AL存储除法操作的商,AH存储除法操作的余数;如果除数为16位,则被除数为32位且低16位放在AX中高16位放AX中,AX存储除法操作的商,DX存储存储除法操作的余数 |
AX || AX + DX | mul指令:两个8位数相乘,一个默认放在AL中,另一个放在8位reg或内存字节单元中,乘法结果放在AX中;如果是16位,一个默认在AX中,另一个放在16为reg或内存字单元中,乘法结果高位放在DX中,低位放在AX中 |
标志寄存器中的CF和OF标志 | 这两个标志位是按照二进制数进/借位原理设计的。当相关指令执行后,若将用无符号数方式操作内存中的数时(相应地,先关指令涉及的操作数也成了无符号数),可查看CF标志位,判断是否有借/进位;当用有符号数方式操作内存中的数时(相应地,先关指令涉及的操作数也成了有符号数),可查看OF标志位,判断是否有溢出。CPU对运算结果的一种记录,CPU包含了有符号和无符号数的两种含义。 |
0000:0000 ~ 0000:03FF内存单元 | 8086CPU在这段内存中存放中断向量表(中断类型码N x 4存储中断处理程序偏移地址,N x 4 + 2存储中断处理程序段地址) |
…… | …… |
在编写汇编程序时,利用这些寄存器的机制[硬件体系结构机制]就可以编写出具逻辑层次的代码段、数据段、栈段的程序。注:8086CPU不知道/不关心代码、数据和栈所占内存有多大,它只将CS:IP指向的内存单元的内容当作指令,将SS:SP指向的内存当作栈顶。所以编写代码时要用指令约束CS:IP的值,不让CS:IP的值进入(IP自动增值或调整)非指令的内存中。同时也要防止SS:SP超过所分配栈内存的总大小(超越栈顶/栈底)。
8086CPU以段的方式实用内存:CS:IP指向代码段;DS用于保存数据段的段地址;SS:SP指向栈顶。编写汇编程序时,要根据8086CPU以段机制使用内存的方式用所对应的汇编语句编写程序(每个段的大小有编译器-连接器整理)。
12.16
(1) 一个CPU的寻址能力为8KB,那么它的地址总线宽度为13。
(2) 1KB的存储器有1024个存储单元。存储单元的编号从0到1023。
(3) 1KB的存储器可以存储1024 x 8个bit,1024个Byte。
(4) 1GB、1MB、1KB分别是2的30、20、10次方Bytes。- P.6
(5) 8080、8088、8086、80286、80386的地址总线宽度分别为16根、20根、24根、32根,则他们的寻址能力分别为64KB、1MB、16MB、4GB。
(6) 8080、8088、8086、80286、80386的数据总线宽度分别为8根、8根、16根、16根、32根。则它们一次可以传送的数据为:1B、1B、2B、2B、4B。
(7) 从内存中读取1024字节的数据,8086至少要读512次,80386至少要读256次。
(8) 在存储器中,数据和程序以二进制(高低电平)形式存储。
(1) 写出每条汇编指令执行后相关寄存器中的值。
指令 | 寄存器的值 |
mov ax, 62627 | AX = 31A3H |
mov ah, 31H | AX = 31A3H |
mov al, 23H | AX = 3123H |
add ax, ax | AX = 6246H |
mov bx, 826CH | BX = 826CH |
mov cx, ax | CX = 6246H |
mov ax, bx | AX = 826CH |
add ax, bx | AX = 04D8H |
mov al, bh | AX = 0482H |
add ah, ah | AX = 0882H |
add al, 6 | AX = 0888H |
add al, al | AX = 0910H |
mov ax, cx | AX = 6246H |
(2) 只能使用目前学过的汇编指令,最多使用4条指令,编程计算2的4次方。
目前学过的指令:mov和add。
编程
mov ax, 2 ;ax = 2的1次方
add ax, ax ;ax = 2的2次方
add ax, ax ;ax = 2的3次方
add ax, ax ;ax = 2的4次方
(1) 给定段地址0001H,仅通过变化偏移地址寻址,CPU的寻址范围为00010H到1000FH。
(2) 有一数据存放在内存20000H单元中,现给定段地址为SA,若想用偏移地址寻到此单元。则SA应满足的条件是:最小为20000H - FFFF 右移4位将移丢的1补在最低位得1001H,最大为2000H。
12.17
下面的3条指令执行后,CPU几次修改IP?都是在什么时候?最后IP中的值是多少?
mov ax, bx
sub ax, ax
jmp ax
CPU 4次修改IP寄存器。
前3次:读取三条指令后,都会发生“IP += 所读指令长度”操作。第4次:执行jmp ax指令将ax的值赋值给IP。
最后IP中的值为0。
注:IP指向下一条指令发生在执行指令之前。
(1) 在Debug中,用“d 0:0 1f”查看内存,结果如下。
0000:0000 70 80 F0 30 EF 60 30 E2-00 80 80 12 66 20 22 60
0000:0010 62 26 E6 D6 CC 2E 3C 3B-AB BA 00 00 26 06 66 88
下面的程序执行前,AX=0, BX=0,写出每条汇编指令执行完后相关寄存器中的值。
mov ax, 1
mov ds, ax
mov ax, [0000] ;ax=2662H - 小端方式存储
mov bx, [0001] ;bx=E626H
mov ax, bx ;ax=E626H
mov ax, [0000] ;ax=2662H
mov bx, [0002] ;bx=D6E6H
add ax, bx ;ax=FD48H
add ax, [0004] ;ax=2C14H
mov ax, 0 ;ax=0
mov al, [0002] ;ax=E6h
mov bx, 0 ;bx=0
mov bl, [000C] ;bx=26h
add al, bl ;ax=10CH
注:在debug中写指令时,mov ax, [0000]表示mov ax, ds:0,在编辑器中写代码时,masm.exe汇编编译器将mov ax, [0000]翻译成mov ax, 0;将mov ax, ds:[0]和mov ax, [bx]分别翻译为mov ax, ds:0和mov ax, ds:bx。
(2) 内存中的情况下图所示。
内存中的内容示图
各寄存器的初始值:CS=2000H,IP=0,DS=1000H,AX=0,BX=0;
[1] 写出CPU执行的指令序列(用汇编指令写出)。
mov ax, 6622H
jmp 0ff0:0100
mov ax, 2000H
mov ds, ax
mov ax, [0008]
mov ax, [0002]
[2] 写出CPU执行每条指令后,CS、IP和相关寄存器中的数值。
mov ax, 6622H ;CS=2000H, IP=3, AX=6622H
jmp 0ff0:0100 ;CS=0FF0H, IP=0100H(IP被修改两次)
mov ax, 2000H ;CS=0FF0H, IP=0103H, AX=2000H
mov ds, ax ;CS=0FF0H, IP=0105H, DS=2000H
mov ax, [0008] ;CS=0FF0H, IP=0108H, AX=02A1H
mov ax, [0002] ;CS=0FF0H, IP=010BH, AX=8E20H
[3] 再次体会,数据和程序有区别吗?如何确定内存中的信息哪些是数据,哪些是程序?
对于内存来说,数据和程序无区别(都是高低电平序列)。8086CPU将CS:IP指向内存中的内容当成程序(指令),将DS:[地址](其余寄存器)指向内存中的内容当作数据。
(1) 补全下面的程序,使其可以将10000H ~ 1000FH中的8个字,逆序复制到20000H ~ 2000FH中。逆序复制的含义如图下图所示(图中内存里的数据均为假设)。
逆序复制示意图
mov ax, 1000H
mov ds, ax
mov ax, 2000H
mov ss, ax
mov sp, 11H
push [0]
push [2]
push [4]
push [6]
push [8]
push [A]
push [C]
push [E]
push [偏移地址]的含义是将“ds:偏移地址”内存单元中的字存入SS:SP指向的内存中(字)。且push先将SP的值减去2后再将值存入SS:SP中。
(2) 补全下面的程序,使其可以将10000H ~ 1000FH中的8个字,逆序复制到20000H ~ 2000FH中。
mov ax, 2000H
mov ds, ax
mov ax, 1000H
mov ss, ax
mov sp, 0000H
pop [E]
pop [C]
pop [A]
pop [8]
pop [6]
pop [4]
pop [2]
pop [0]
pop [地址]的含义是将SS:SP指向的内存单元中各内容取出来保存到ds:[地址]中去。且pop 指令执行后再对SP-2。
(1) 下面的程序实现依次用内存0:0 ~ 0:15单元中的内容改写程序中的数据,完成程序:
assume cs:codesg
codesg segment
dw 0123h, 0456h, 0789h, 0abch, 0defh, 0fedh, 0cbah, 0987h
start: mov ax, 0
mov ds, ax
mov bx, 0
mov cx, 8 ;循环次数
s: mov ax, [bx] ;mov ax, ds:[bx]
mov cs:[bx], ax ;填空内容
add bx, 2 ;下一字偏移地址
loop s
mov ax, 4c00h ;程序返回
int 21h
codesg ends
end start ;告知编译器程序入口为start处
(2) 下面的程序实现依次用内存0:0 ~ 0:15单元中的内容改写程序中的数据,数据的传送用栈来进行。栈空间设置在程序内。完成程序:
assume cs:codesg
codesg segment
dw 0123h, 0456h, 0789h, 0abch, 0defh, 0fedh, 0cbah, 0987h
dw 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ;10个字节
start: mov ax, cs ;填空内容cs
mov ss, ax
mov sp, 1ah ;填空内容22h(忽略栈从程序功能上来讲12h ~ 1ah之间的值皆可)
mov ax, 0
mov ds, ax
mov bx, 0
mov cx, 8
s: push [bx] ;push ds:[bx]
pop cs:[bx] ;填空内容
add bx, 2 ;下一字偏移地址
loop s
mov ax, 4c00h ;程序返回
int 21h
codesg ends
end start ;告知编译器程序入口为start处
(1) 程序如下。
assume cs:code
data segment
?
data ends
code segment
start:
mov ax, data
mov ds, ax
mov bx, 0
jmp word ptr [bx + 1]
code ends
end start
若要使程序中的jmp指令执行后,CS:IP指向程序的第一条指令,在data段中应该定义哪些数据?
分析:”jmp word ptr 内存单元地址”为段内转移指令,从内存单元地址处开始存放着一个字,是转移的目的偏移地址。那么,”jmp word ptr [bx + 1]”功能为IP = ds:[bx + 1]。start在代码段中的偏移地址为0,故而data段的内容应该如下:
data segment
dw 0 ;开始一个字的内容为0即可
data ends
(2) 程序如下。
assume cs:code
data segment
dd 12345678H
data ends
code segment
start:
mov ax, data
mov ds, ax
mov bx, 0
mov [bx], ____
mov [bx + 2], ____
jmp dword ptr ds:[0]
code ends
end start
补全程序,使jmp指令执行后,CS:IP指向程序的第一条指令。
分析:”jmp dword ptr 内存单元”为段间转移指令,功能为从内存单元地址处开始存放着两个字,高地址处的字是转移的目的段地址,低地址处是转移的目的偏移地址。”jmp dword ptr ds:[0]”执行后,CS=ds:[0]高字内容,IP=ds:[0]低字内容。那么ds:[0]高字内容应该为代码段的段地址cs或code;ds:[0]应该为代码段第一条指令的偏移地址0。补全两条指令如下:
mov [bx], bx
mov [bx + 2], code
(3) 用Debug查看内存,结果如下:
2000:1000 BE 00 06 00 00 00 ……
则此时,CPU执行指令:
mov ax, 2000H
mov es, ax
jmp dword ptr es:[1000H]
后,(CS)=?,(IP)=?
分析:jmp dword ptr es:[1000H]将实现段间转移,见(2)。那么,CS=0006H,IP=00BEH。
补全编程,利用jcxz指令,实现在内存2000H段中查找第一个值为0的字符,找到后,将它的偏移地址存储在dx中。
assume cs:code
code segment
start:
mov ax, 2000H
mov ds, ax
mov bx, 0
s:
________
________
________
________
jmp short s
ok:
mov dx, bx
mov ax, 4c00h
int 21h
code ends
end start
打算:将内存中的值读到cx中,然后用jcxz指令跳转到ok处。补全编程如下:
mov cx, 1 ;其它使ch=0且cl!=0的值
mov cl, [bx] ;以字节为单位将内存中的内容读到cl中
jcxz ok ;如果cl为0则跳转到ok处
inc bx ;读下一字节
补全程序,利用loop指令,实现在内存2000H段中查找第一个值为0的字节,找到后,将它的偏移地址存储在dx中。
assume cs:code
code segment
start:
mov ax, 2000H
mov ds, ax
mov bx, 0
s:
mov cl, [bx]
mov ch, 0
________
inc bx
loop s
ok:
dec bx ;dec的功能和inc相反,dec bx进行的操作为:(bx) = (bx) - 1
mov dx, bx
mov ax, 4c00h
int 21h
code ends
end start
分析:横线处的语句是使得当cl=0时,退出循环执行ok处的指令。所以横线处的指令应为:
inc cx ;或inc cl
12.22
补全程序,实现从内存1000:0000处开始执行指令。
assume cs:code
stack segment
db 16 dup (0)
stack ends
code segment
start:
mov ax, stack
mov ss, ax
mov sp, 16
mov ax, ____
push ax
mov ax, ____
push ax
retf
code ends
end start
分析:retf指令的功能为POP IP, POP CS。那么程序中两次入栈的顺序应该为0和1000:
mov ax, 0
push ax
mov ax, 1000
push ax
下面的程序执行后,ax中的数值为多少?
;内存地址 机器码 汇编指令
1000:0 b8 00 00 mov ax, 0
1000:3 e8 01 00 call s
1000:6 40 inc ax
1000:7 58 s:pop ax
分析:读取“call s”指令后,IP指向下一条指令即IP = 6;然后执行“call s”指令:PUSH IP,IP += 01 = 7,下一次执行的指令为pop ax即ax = 6。
下面的程序执行后,ax中的数值为多少?
;内存地址 机器码 汇编指令
1000:0 b8 00 00 mov ax, 0
1000:3 9A 09 00 00 10 call far ptr s
1000:8 40 inc ax
1000:9 58 s: pop ax
add ax, ax
pop bx
add ax, bx
分析:读取”call far ptr s”指令后,IP=8;执行”call far ptr s”指令 - PUSH CS;PUSH IP, IP = 9, CS=1000(标号所在段中的偏移地址和标号所在段的段地址)。执行s标号处的程序,pop ax(ax=9),add ax, ax(ax=13H), pop bx(bx=1000), add ax, bx(ax=1013H)。
下面的程序执行后,ax中的数值为多少?
;内存地址 机器码 汇编指令
1000:0 b8 06 00 mov ax, 6
1000:3 ff d0 call ax
1000:5 40 inc ax
1000:6 mov bp, sp
add ax, [bp]
分析:读取”call ax”后IP = 5;执行”call ax”指令 - PUSH IP(5);IP = ax(6);执行mov bp, sp;add ax, [bp](ax += 5 = 11)。
(1) 下面的程序执行后,ax中的数值为多少?(注意:用call指令的原理分析,不要在Debug中单步跟踪来验证你的结论。对于此程序,在Debug中单步跟踪的结果,不能代表CPU的实际执行结果。)
assume cs:code
stack segment
dw 8 dup (0)
stack ends
code segment
start:
mov ax, stack
mov ss, ax
mov sp, 16
mov ds, ax
mov ax, 0
call word ptr ds:[0EH]
inc ax
inc ax
inc ax
mov ax, 4c00h
int 21h
code ends
end start
分析:读取”call word ptr ds:[0EH]”指令后,IP指向下一条指令;执行”call word ptr ds:[0EH]” - IP被压入栈中即IP值在stack段中的最后两个字节中,IP=ds:[0EH]指向的内存单元中的内容即原本IP的值即IP指向第一条inc ax指令,那么程序执行完后ax的值为3(若执行了mov ax, 4c00h,则ax=4c00h)。
(2) 下面的程序执行后,ax和bx中的数值为多少?
assume cs:code
data segment
dw 8 dup(0)
data ends
code segment
start:
mov ax, data
mov ss, ax
mov sp, 16
mov word ptr ss:[0], offset s
mov ss:[2], cs
call dword ptr ss:[0]
nop
s:
mov ax, offset s
sub ax, ss:[0cH]
mov bx, cs
sub bx, ss:[0eH]
mov ax, 4c00h
int 21h
code ends
end start
分析:读取”call dword ptr ss:[0]”指令,IP指向nop指令;执行”call dword ptr ss:[0]”指令 - IP被压入data段的0C 0D两个字节中,CS被压入data段的0E 0F两个字节中,IP=标号s的偏移地址,CS=CS。执行s标号开始的指令 - ax=nop指令的长度,bx=0(在未执行mov ax, 4c00h指令之前)。
12.28
写出下面每条指令执行后,ZF、PF、SF标志位的值。
sub al, al ;ZF=1, PF=1, SF=0
mov al, 1 ;ZF=0, PF=0, SF=0
push ax ;ZF=0, PF=0, SF=0,不响应标志位
pop bx ;ZF=0, PF=0, SF=0, 不影响标志位
add al, bl ;ZF=0, PF=0, SF=0
add al, 10 ;ZF=0, PF=0, SF=1
mul al ;ZF=0, PF=0, SF=0
写出下面每条指令执行后,ZF、PF、SF、CF、OF标志位的值。
;CF OF SF ZF PF
sub al, al ;0 0 0 1 1
mov al, 10H ;0 0 0 1 1,mov指令不影响标志位
add al, 90H ;0 0 1 0 1
mov al, 80H ;0 0 1 0 1
add al, 80H ;1 1 0 0 1
mov al, 0FCH ;1 1 0 0 1
add al, 05H ;1 1 0 0 0
mov al, 7DH ;1 1 0 0 0
add al, 0bH ;0 1 1 0 1
(1) 补全下面的程序,统计F000:0处32个字节中,大小在[32, 128]的数据的个数。
mov ax, 0f000h
mov ds, ax
mov bx, 0
mov dx, 0
mov cx, 32
s: mov al, [bx]
cmp al, 32
—————————— ;补充指令:jb s0
cmp al, 128
_________ ;补充指令:ja s0
inc dx
s0: inc bx
loop s
(2) 补全下面的程序,统计F000:0处的32个字节中,大小在(32, 128)的数据的个数。
mov ax, 0f000h
mov ds, ax
mov bx, 0
mov dx, 0
mov cx, 32
s: mov al, [bx]
cmp al, 32
__________ ;补充指令:jna s0
cmp al, 128 ;补充指令:jnb s0
__________
inc dx
s0: inc bx
loop s
下面的程序执行后:(ax)=?
mov ax, 0
push ax
popf
mov ax, 0fff0h
add ax, 0010h
pushf
pop ax
and al, 11000101B
and ah, 00001000B
(ax)=0843H
12.29
(1)用Debug查看内存,情况如下:
0000:0000 68 10 A7 00 8B 01 70 00-16 00 9D 03 8B 01 70 00
则3号中断源对应的中断处理程序的入口地址为:0070:018B.
(2) 存储N号中断源对应的中断处理程序入口的偏移地址的内存单元的地址为:N x 4。存储N号中断源对应的中断处理程序入口的段地址的内存单元的地址为:N x 4 + 2。
12.30
**(1) 在上面的内容(Page.257)中,我们用7ch中断历程实现loop的功能,则上面的7ch中断例程所能进行的最大转移位移是多少?**2^16 - 1。
(2) 用7ch中断例程完成jmp near ptr指令的功能,用bx向中断例程传送转移位移。
应用举例:在屏幕的第12行,显示data段中以0结尾的字符串。
assume cs:code
data segment
db 'conversation', 0
data ends
code segment
start:
mov ax, data
mov ds, ax
mov si, 0
mov ax, 0b800h
mov es, ax
mov di, 12 * 160
s:
cmp byte ptr [si], 0
je ok
mov al, [si]
mov es:[di], al
inc si
add di, 2
mov bx, offset s - offset ok
int 7ch
ok:
mov ax, 4c00h
int 21h
code ends
end start
7ch中断例程完成jmp near ptr指令功能的代码如下(未验证):
add ss:[sp], bx
iret
12.31
因为无ss:[sp]的访问方式,所以正确的代码如下:
push bp
mov bp, sp
add ss:[bp], bx
pop bp
iret
判断下面说法的正误:
(1) 我们可以变成改变FFFF:0处的指令,使得CPU不去执行BIOS中的硬件系统检测和初始化程序。误。
(2) int 19h中断例程,可以由DOS提供。误。
(1) 编程,读取CMOS RAM的2号单元的内容。
assume cs:codesg
codesg segment
start:
mov al, 2
out 70h, al
in al, 71h
mov ax, 4c00h
int 21h
codesg ends
end start
在DOSBOX中用masm.exe和link.exe编译、连接以上程序得1411.exe。DOSBOX中用debug.exe运行1411.exe:
CMOSRS RAM2号单元的值在al中,它与当前“时间的分”一致:
(2) 编程,向CMORS RAM的2号单元写入0。
assume cs:codesg
codesg segment
start:
mov al, 2
out 70h, al
mov al, 0
out 71h, al
in al, 71h
mov ax, 4c00h
int 21h
codesg ends
end start
在DOSBOX中用masm.exe和link.exe编译、连接以上程序得1411.exe。用debug.exe加载并运行1411.exe:
根据程序运行结果说明,向CMORS RAM的2号单元中写入0然后立即将其读出(验证是否写入成功),发现并没有将0写入2号单元中,读出的依旧是当前的时间分值。
2016.01.01
编程,用加法和移位指令计算(ax)=(ax)*10。
提示,(ax)*10=(ax)*2 + (ax)*8。
assume cs:codesg
codesg segment
start:
mov ax, 1
call multen
mov ax, 4c00h
int 21h
multen:
push bx
mov bx, ax
shl bx, 1
shl ax, 3
add ax, bx
pop bx
ret
codesg ends
end start
对于ax的值小于2^13的数来说,multen都正确。对于编写完全正确的用加法和移位来代替乘法运算应该比较复杂(这应该是编译器的某小块的内容),此处不深纠。
2016.01.03
(1) 仔细分析一下上面(P.280 ~ P.281)的int 9中断例程,看看是否可以精简一下?
**其实在我们的int 9中断例程中,模拟int指令调用原int 9中断例程的程序段是可以精简的,因为在进入中断例程后,IF和TF都已经置0,没有必要再进行设置了。对于程序段:
pushf
pushf
pop ax
and ah, 11111100b
push ax
popf
call dword ptr ds:[0]
可以精简为:
pushf
call dword ptr ds:[0]
两条指令。
(2) 仔细分析上面程序中的主程序,看看有什么潜在的问题?
在主程序中,如果在执行设置int 9中断例程的段地址和偏移地址的指令之间发生了键盘中断,则CPU将转去一个错误的地址执行,将发生错误。
找出这样的程序段,改写它们,排除潜在的问题。
提示,注意sti和cli指令的用法。
在P.280中,将以下程序段:
mov word ptr es:[9*4], offset int9
mov es:[9*4+2], cs
改写为:
cli
mov word ptr es:[9*4], offset int9
mov es:[9*4+2], cs
sti
2016.01.04
下面的程序将code段中的a处的8个数据累加,结果存储到b处的双字中,补全程序。
assume cs:code
code segment
a dw 1, 2, 3, 4, 5, 6, 7, 8
b dd 0
start:
mov si, 0
mov cx, 8
s:
mov ax, ____
add ____, ax
adc ____, 0
add si, 2
loop s
mov ax, 4c00h
int 21h
code ends
end start
补全后的程序如下:
assume cs:code
code segment
a dw 1, 2, 3, 4, 5, 6, 7, 8
b dd 0
start:
mov si, 0
mov cx, 8
s:
mov ax, a[si]
add word ptr b[0], ax
adc word ptr b[2], 0
add si, 2
loop s
mov ax, 4c00h
int 21h
code ends
end start
这段程序被masm.exe编译后的汇编代码(以下是经link.exe连接后、经debug.exe加载后用u命令查看的汇编代码):
此检测点主要是检测编译器对不带冒号的标号的处理(不带冒号的标号虽然既能表示地址也能表示地址单元的长度,但也能够用ptr指定始于该标号的内存单元的大小)。
下面的程序将data段中的a处的8个数据累加,结果存储到b处的字中,补全程序。
assume cs:code, es:data
data segment
a db 1, 2, 3, 4, 5, 6, 7, 8
b dw 0
data ends
code segment
start:
________
________
mov si, 0
mov cx, 8
s:
mov al, a[si]
mov ah, 0
add b, bx
inc si
loop s
mov ax, 4c00h
int 21h
code ends
end start
补全程序如下:
assume cs:code, es:data
data segment
a db 1, 2, 3, 4, 5, 6, 7, 8
b dw 0
data ends
code segment
start:
mov ax, data
mov es, ax
mov si, 0
mov cx, 8
s:
mov al, a[si]
mov ah, 0
add b, bx
inc si
loop s
mov ax, 4c00h
int 21h
code ends
end start
在DOSBOX中用masm.exe和link.exe编译、连接以上程序得162.exe,用debug.exe加载162.exe并用u命令查看其源码:
这里主要考察编译器对assume伪指令的处理。当在某段中声明不带冒号的标号时,必须要用某个段寄存器与它关联。这样编译器处理这样的标号时会以这个段寄存器的值为其段地址(不是为该段寄存器赋值 ,赋值语句应该在代码段中实现。如add b, ax会被编译器翻译为add es:[b], ax)。
2016.01.06
“在int 16h中断例程中,一定有设置IF=1的指令。”这种说法对吗?
对。(可用debug.exe单步调试验证 - 使用t命令单步进入int 16h中断例程中)
12.17
平台:windows 10 x64
下载DOSBOX和debug.exe。
解压下载包。将debug.exe拷贝到某盘根目录,如E盘。
安装DOSBOX(DOSBox0.74-win32-installer.exe)。打开DOSBOX,在DOSBOX中输入“mount c e:\”命令(此命令输入后有“Drive C is mounted as local directory e:\”,再输入“c:”回车。验证准备的环境:在DOSBOX中输入“debug”后回车,输入“r”后回车,若此时能够在DOSBOX中看见各寄存器的值则表明调试器环境可用。
汇编调试器环境准备完毕。
[1] 使用Debug,将下面的程序段(见P.45)写入内存,逐条执行,观察每条指令执行后CPU中相关寄存器中内容的变化。
打算:选择debug的E命令(以机器码的形式)从内存1000:0处开始写入程序段;并分别使用D和U命令查看写入内存中的内容;用r命令修改CS:IP指向1000:0;再用T命令执行指令并观看相关寄存器中内容的变化。
在debug中,用E命令输入机器码,用U命令查看输入的机器码
让CS:IP指向输入的指令后,就可以用T命令开始执行输入的指令了。
用T命令执行指令后,CS:IP和指令操作的相关寄存器的值都发生了变化。
[2] 将下面3条指令写入从2000:0开始的内存单元中,利用这3条指令计算2的8次方。
mov ax, 1
add ax, ax
jmp 2000:0003
打算:运行debug后,用A命令往内存2000:0处写入以上3条指令。然后通过r命令修改CS:IP的值指向2000:0,再用T命令执行指令,当ax中的值为256时停止执行。
输入指令并修改CS:IP指向首条指令
然后就可以用T命令执行指令了,当AX=0100时即可停止T命令。
[3] 查看内存中的内容
PC机主板中写有一个生产日期,在内存FFF00H ~ FFFFFH的某几个单元中,请找到这个生产日期并试图改变它。
打算:进入debug后,用d命令查看FFF00H ~ FFFFFH中的内容,再用E命令试图修改保存日期的几个内存单元中的值,再用d命令查看是否修改成功。
用d命令查看FFF00H ~ FFFFFH内存中的内容,生产日期保存在FFF0:00F0 - FFF0:FF这几个单元中。
再用e命令修改保存生产日期这几个内存单元
可见,并没有将这几个内存单元的值修改掉 - 因为在8086PC中的内存地址空间分配中,C0000H ~ FFFFFH属于各类ROM地址空间(只读)。
[4] 向内存从B8100H开始的单元中填写数据,如:-e B810:0000 01 01 02 02 03 03 04 04。
先填写不同的数据,观察产生的现象;再改变填写地址,观察产生的现象。
打算:在DOSBOX中运行debug,用e命令往从B8100H开始处的单元中写入61H, 62H, 63H(’a’, ‘b’, ‘c’的ASCII值)数据;观察现象,再将这几个数据写入开始于2000:0的内存单元,观察现象。
当往B8100H写入61H时,屏幕上靠右位置出现白色的’a’,继续写入62H时,a被蒙上了颜色,写入63H时,紧跟a之后出现白色的’c’。将这几个数据写入2000:0开始的内存单元,除这几个内存单元的值被改变外无其它现象。 - 因为在8086PC机中的内存地址空间分配中,A0000H ~ BFFFFH属于显存地址空间(屏幕将会显示显存中数据所对应的符号)。
12.18
(1) 使用Debug,将下面的程序段写入内存,逐条执行,根据指令执行后的实际运行情况天空。
mov ax, ffff
mov ds, ax
mov ax, 2200
mov ss, ax
mov sp, 0100
mov ax, [0] ;ax=C0EAH
add ax, [2] ;ax=C0FCH
mov bx, [4] ;bx=30F0H
add bx, [6] ;bx=6021H
push ax ;sp=FE;修改的内存单元的地址是2200:FE,内容为C0FCH
push bx ;sp=FC;修改的内存单元地址是2200:FC,内容为6021H
pop ax ;sp=FE;ax=6021H
pop bx ;sp=100;bx=C0FCH
push [4] ;sp=FE;修改的内存单元地址是2200:FE,内容为30F0H
push [6] ;sp=FC;修改的内存单元地址是2200:FC,内容为2F31H
打算:直接用debug的d命令查看一下以ffff:0地址开始的内存单元的内容为多少,然后填空[注释部分]。
根据此图返回到以上代码部分,直接填空。
(2) 仔细观察下图的实验过程,然后分析:为什么2000:0 ~ 2000:f中的内容会发生变化。
用debug进行的实验
观察上图中的实验,在执行073F:0100 ~ 073F:0105这段指令之前,先用e命令将内存2000:0 ~ 2000:F这段内存的内容全都设置成了0。当执行073F:0100 ~ 073F:0105这段指令后,2000:7 ~ 2000:F这段内存中的内容发生了改变。- 这是因为在使用D命令时往栈(即SS:SP指向的内存单元)中压入的备份[如在使用d 2000:0命令时,d命令的代码会将2000的值赋给ds,在给ds赋值前需要对ds原先的值备份 - 将ds原先的值压入栈中,在退出d命令后好恢复(出栈)ds的值]。
环境准备
平台:windows 10 x64
[1] 下载/安装DOSBOX和debug.exe。
[2] 下载masm5。将masm5解压,masm5下包含汇编编译器masm.exe和汇编连接器link.exe。
将masm5、debug放置于e:\Learn\Hb目录下。
(1) 将下面的程序保存为t1.asm文件,将其生成可执行文件t1.exe。
assume cs:codesg
codesg segment
mov ax, 2000H
mov ss, ax
mov sp, 0
add sp, 10
pop ax
pop bx
push ax
push bx
pop ax
pop bx
mov ax, 4c00H
int 21H
codesg ends
end
用记事本/notepad++编辑器编写以上汇编代码,保存为t1.asm文件到E:\Learn\Hb\src\p3目录下。打开DOSBOX,输入以下命令编译、连接程序得到t1.exe:
t1.exe被输出到src\p3目录下。
(2) 用Debug跟踪t1.exe的执行过程,写出每一步执行后,相关寄存器中的内容和栈顶的内容。
用“debug src\p3\t1.exe”将t1.exe加载至内存中,用T、P命令执行执行,将相关寄存器和栈顶的内容注释如下。
assume cs:codesg
codesg segment
mov ax, 2000H ;ax=2000H
mov ss, ax ;ss=2000H
mov sp, 0 ;sp=0,栈顶内容为0
add sp, 10 ;sp=10,栈顶内容为0
pop ax ;ax=0,栈顶内容为0
pop bx ;bx=0,栈顶内容为0
push ax ;栈顶内容为0
push bx ;栈顶内容为0
pop ax ;0-0
pop bx ;0-0
mov ax, 4c00H
int 21H
codesg ends
end
(3) PSP的头两个字节是CD 20,用Debug加载t1.exe,查看PSP的内容。
在DOS中,.exe文件加载后,ds:0保存PSP的首地址,PSP共占用256字节。那么debug加载.exe程序也遵循同样原则吗?看看(显示前7行):
根据PSP开头两个字节为CS 20的提示可推测这段内容就是PSP的内容 - debug加载程序遵循了DOS加载程序的方式(起码这一点)。
(1) 编程,向内存0:200 ~ 0:23F依次传送数据0 ~ 63(3FH)。
汇编程序如下(注意masm用常数的后缀区别常数的进制,不带后缀默认为10进制数):
assume cs:codesg
codesg segment
mov ax, 0020H
mov ds, ax
mov bx, 0
mov cx, 40H
lable: mov [bx], bl
inc bx
loop lable
mov ax, 4C00H
int 21H
codesg ends
end
打开DOSBOX,分别用masm.exe和link.exe编译连接以上汇编程序(p41.asm)得到可执行文件p41.exe。再用debug.exe将p41.exe载入,用“g mov ax, 4C00H的偏移地址”命令执行mov ax, 4C00H之前的程序。然后在debug.exe中用”d ds:0”命令查看0:200 ~ 0:23F中的内容,这64个单元的内容如下(表明程序编写正确):
(2) 编程,向内存0:200 ~ 0:23FH依次传送数据0 ~ 63(3FH),程序中只能使用9条指令,9条指令包括“mov ax, 4c00h”和“int 21h”。
见(1)。
(3) 下面的程序的功能是将”mov ax, 4c00h”之前的指令复制到内存0:200处,补全程序,上机调试,跟踪运行结果。
assume cs:code
code segment
mov ax, ____
mov ds, ax
mov ax, 0020H
mov es, ax
mov bx, 0
mov cx, ____
s: mov al, [bx]
mov es:[bx], al
inc bx
loop s
mov ax, 4c00H
int 21h
code ends
end
分析:在s标号代表的循环中,从ds:[bx]拷贝数据到es:[bx]中,那么ds代表当前代码的首地址(cs或code都能够代表此段代码的首地址);显然cx代表循环迭代次数,迭代次数决定了从当前代码首地址开始能够复制多少字节的数据到es:[bx]中。根据P.91 - Debug将程序加载到内存中后,cx中存放的是整个程序的长度。那么只要知道”mov ax, 4c00H”和”int 21H”指令的长度就可以得到从代码开始到mov ax, 4c00h之前的指令的长度。用debug测试二指令的长度:
从中可以看出”mov ax, 4c00H”和”int 21H”指令所占内存为5字节。所以,第二个空中cx的值可为cx - 5。
将以上所分析的答案填入到程序中。打开DOSBOX,用masm.exe编译以上程序(p43.asm) – 编译器不支持cx - 5(“寄存器 运算符 常数” - 除了将这种语句转变为等效的汇编语句似乎无其它更好的办法,然而”- 5”属于编译器处理掉的东西 )的语法。故而采取网上的办法 - 在“mov cx, __”一空中填写一个常数,将其编译、连接后用debug加载,加载后用r命令看cx的值,然后将此值(经测试cx=1DH)减去5(得18H)再填回程序中。得程序如下:
assume cs:code
code segment
mov ax,code ;or cs(若有assume cs:code伪指令)
mov ds, ax
mov ax, 0020H
mov es, ax
mov bx, 0
mov cx, 18H ;cx后的常数占用1个字节
s: mov al, [bx]
mov es:[bx], al
inc bx
loop s
mov ax, 4c00H
int 21h
code ends
end
用18H补全mov cx, 18H语句。重新在DOSBOX中编译、连接并用g命令执行此程序。用”u 0:200”验证程序的正确性:
此图表明填空内容正确。
12.19
(1) 将下面的程序编译、连接,用Debug加载、跟踪,然后回答问题。
assume cs:code, ds:data, ss:stack
data segment
dw 0123h, 0456h, 0789h, 0abch, 0defh, 0fedh, 0cbah, 0987h
data ends
stack segment
dw 0, 0, 0, 0, 0, 0, 0, 0
stack ends
code segment
start: mov ax, stack
mov ss, ax
mov sp, 16
mov ax, data
mov ds, ax
push ds:[0]
push ds:[2]
pop ds:[2]
pop ds:[0]
mov ax, 4c00h
int 21h
code ends
end start
(未在DOSBOX中用debug加载、跟踪该程序。)
[1] CPU执行程序,程序返回前,data段中的数据为多少?
data段中的数据为:0123h, 0456h, 0789h, 0abch, 0defh, 0fedh, 0cbah, 0987h
[2] CPU执行程序,程序返回前,cs=code(填空内容)、ss=stack(填空内容)、ds=data(填空内容)。- 具体数值得用debug跟踪
[3] 设程序加载后,code段的段地址为X,则data段的段地址为(X-2)H(填空内容),stack段的段地址为(X-1)H(填空内容)。
说明:debug以16字节对齐(16的倍数)连续加载程序中的段。因为该程序中的栈段、数据段的内容都未超过64KB,故而各段的段地址是连续的。
(2) 将下面的程序编译、连接,用Debug加载、跟踪,然后回答问题。
assume cs:code, ds:data, ss:stack
data segment
dw 0123h, 0456h
data ends
stack segment
dw 0, 0
stack ends
code segment
start: mov ax, stack
mov ss, ax
mov sp, 16
mov ax, data
mov ds, ax
push ds:[0]
push ds:[2]
pop ds:[2]
pop ds:[0]
mov ax, 4c00h
int 21h
code ends
end start
[1] CPU执行程序,程序返回前,data段的数据为多少?
data段的数据为0123h, 0456h。
[2] CPU执行程序,程序返回前,cs=code(填空内容)、ss=stack(填空内容)、ds=data(填空内容)。- 具体数值得用debug跟踪
[3] 设程序加载后,code段的段地址为X,则data段的段地址为(X-2)H(填空内容),stack段的段地址为(X-1)H(填空内容)。
说明:debug以16字节对齐(16的倍数)连续加载程序中的段。因为该程序中的栈段、数据段的内容都未超过64KB,故而各段的段地址是连续的。
[4] 对于如下定义的段(…):
name segment
...
name ends
如果段中的数据占N个字节,则程序加载后,该段实际占有的空间为N(N%16=0)或[(N/16 + 1) x 16]KB且小于64KB(填空内容)。
(3) 将下面的程序编译、连接,用Debug加载、跟踪,然后回答问题。
assume cs:code, ds:data, ss:stack
code segment
start: mov ax, stack
mov ss, ax
mov sp, 16
mov ax, data
mov ds, ax
push ds:[0]
push ds:[2]
pop ds:[2]
pop ds:[0]
mov ax, 4c00h
int 21h
code ends
data segment
dw 0123H, 0456H
data ends
stack segment
dw 0, 0
stack ends
end start
[1] CPU执行程序,程序返回前,data段中的数据为多少?
data段中的数据为0123H, 0456H。
[2] CPU执行程序,程序返回前,cs=code(填空内容)、ss=stack(填空内容)、ds=data(填空内容)。- 具体数值得用debug跟踪
[3] 设程序加载后,code段的段地址为X,则data段的段地址为(X+1)H(填空内容),stack段的段地址为(X+2)H(填空内容)。
说明:debug以16字节对齐(16的倍数)连续加载程序中的段。因为该程序中的代码段、数据段的内容都未超过64KB,故而各段的段地址是连续的。
(4) 如果将(1)、(2)、(3)题中的最后一条伪指令“end start”改为“end”(即不指明程序入口),则哪个程序仍然可以正确执行?请说明原因。
(3)题中的程序仍然可以正确执行。
因为DOS(或debug)加载程序到内存中的方式和默认机制(P.92):整个程序的物理地址为ds x 16 + 256(这也是CS:IP所指向的地址) ,再加上它们加载程序的方式是按照段在源程序的先后顺序并以段为单位进行连续加载。
(5) 程序如下,编写code段中的代码,将a段和b段中的数据依次相加,将结果存到c段中。
assume cs:code
a segment
db 1, 2, 3, 4, 5, 6, 7, 8
a ends
b segment
db 1, 2, 3, 4, 5, 6, 7, 8
b ends
c segment
db 0, 0, 0, 0, 0, 0, 0, 0
c ends
code segment
start:
?
code ends
end start
代码段中缺失的代码如下:
mov ax, a
mov ds, ax ;获取a段段地址
mov ax, b
mov es, ax ;获取b段段地址
;将a和b段中的数据相加存到a段中
mov bx, 0
mov cx, 8
s1: mov al, es:[bx]
add ds:[bx], al
inc bx
loop s1
;将a段中的数据拷贝到c段中
mov ax, c
mov es, ax ;获取c段段地址
mov bx, 0
mov cx, 8
s2: mov ax, ds:[bx]
mov es:[bx], ax
add bx, 2
loop s2
mov ax, 4c00h
int 21h
将任务分两步来做:加法和拷贝。也可以用第三个段寄存器(如ss)来一条龙完成加法然后将值拷贝到c段中[不过不能用cs段寄存器,在使用ss段时应该考虑ss段的备份与恢复 ]。
在DOSBOX中用masm.exe和link.exe编译连接以上程序得到p55.exe,再用debug.exe加载p55.exe,用g命令运行程序至mov ax, 4c00h处,用”d es:0 f”命令(此时es还代表c段的段地址)查看结果是否正确:
程序执行后,c段中的内容
(6) 程序如下,编写code段中的代码,用push指令将a段中的前8个字型数据,逆序存储到b段中。
assume cs:code
a segment
dw 1, 2, 3, 4, 5, 6, 7, 8, 9, 0ah, 0bh, 0ch, 0dh, 0eh, 0fh, 0ffh
a ends
b segment
dw 0, 0, 0, 0, 0, 0, 0, 0
b ends
code segment
start:
?
code ends
end start
代码段中缺失的代码如下:
start:
mov ax, a
mov ds, ax ;获取a段的段地址
mov bx, 0 ;a段的偏移地址
mov ax, b
mov ss, ax ;获取b段的段地址
mov sp, 10H ;栈底下一个字
mov cx, 8 ;循环8次 - 拷贝前8个字
s: push ds:[bx]
add bx, 2
loop s
mov ax, 4c00h
int 21h
在DOSBOX中用masm.exe和link.exe编译、连接以上程序得p56.exe,用debug加载p56.exe并用g命令运行到该程序的mov ax, 4c00h处,用”d ss:0”查看b段中的内容[执行此命名前sp=0,所以不会往ss:0后的内存中压入数据]:
运行程序,查看b段中的内容,根据上图,逆序push似已成功。
12.20
编程,将datasg段中的每个单词的前4个字母改为大写字母。
assume cs:codesg, ss:stacksg, ds:datasg
stacksg segment
dw 0, 0, 0, 0, 0, 0, 0, 0
stacksg ends
datasg segment
db '1. display ' ;.后1个空格,y后6个空格
db '2. brows ' ;.后1个空格,s后8个空格
db '3. replace ' ;.后1个空格,e后6个空格
db '4. modify ' ;.后1个空格,y后7个空格
datasg ends
codesg segment
start:
codesg ends
end start
分析:
补充代码段的地址如下:
codesg segment
start:
mov ax, datasg
mov ds, ax ;获取datasg段地址
mov cx, 4 ;循环次数
mov si, 0 ;相对于首字母的偏移值
mov al, 11011111b ;将字母的第5位置0即能保证字母为大写
;将每个单词的前4个字母转换为大写字母
lable:
mov bx, 0 ;第1个单词
and [bx + si + 3], al
add bx, 16 ;第2个单词
and [bx + si + 3], al
add bx, 16 ;第3个单词
and [bx + si + 3], al
add bx, 16 ;第4个单词
and [bx + si + 3], al
inc si ;下一个字母
loop lable
mov ax, 4c00h
int 21h
codesg ends
如果单词过多,可用循环来实现n次”add bx, 16; and [bx + si + 3], al”(注意对cx的备份和恢复)。
在DOSBOX中用masm.exe和link.exe编译、连接以上程序得p62.exe,再用debug.exe加载p62.exe,用g命令执行到mov ax, 4c00h处,再用“d ds:0 3f”命令查看datasg段中的内容:
每个单词的前4个字母被转换成了大写字母。
12.21
Power idea公司从1975年成立一直到1995年的基本情况如下。
年份 | 收入(千美元) | 雇员(人) | 人均收入 |
1975 | 16 | 3 | ? |
1976 | 22 | 7 | ? |
1977 | 382 | 9 | ? |
1978 | 1356 | 13 | ? |
1979 | 2390 | 28 | ? |
1980 | 8000 | 38 | ? |
. . . |
|||
1995 | 5937000 | 17800 | ? |
下面的程序中,已经定义好了这些数据:
assume cs:codesg
data segment
db '1975', '1976', '1977', '1978', '1979', '1980', '1981', '1982', '1983'
db '1984', '1985', '1986', '1987', '1988', '1989', '1990', '1991', '1992'
db '1993', '1994', '1995'
;以上是表示21年的21个字符串
dd 16, 22, 382, 1356, 2390, 8000, 16000, 24486, 50065, 97479, 140417, 197514
dd 345980, 590827, 803530, 1183000, 1843000, 2759000, 3753000, 4649000, 5937000
;以上是表示21年公司总收入的21个dword型数据
dw 3, 7, 9, 13, 28, 38, 130, 220, 476, 778, 1001, 1442, 2258, 2793, 4037, 5635, 8226
dw 11542, 14430, 15257, 17800
;以上是表示21年公司雇员人数的21个word型数据
data ends
table segment
db 21 dup ('year summ ne ?? ') ;16字节
table ends
编程,将data段中的数据按如下格式写入到table段中,并计算21年中的人均收入(取整),结果也按照下面的格式保存在table段中。
分析。
可将data段用C语言的结构体来描述如下(以行):
struct _data {
int y[21]; //以1个字节为单位的存储/读取
int m[21]; //以4个字节为单位的存储/读取
short int pn[21; //以2个字节微单的存储/读取
}data;
题目的要求时将data中的数据拷贝到table中(除额外的空格和人均收入)。可对应到具体的寻址方式来访问这些元素:
以列为单位读取数据写入table相应的行中。
struct _table {
int y; //相对于table[i]的偏移地址为0
char s1; //相对于table[i]的偏移地址为4
int m; //相对于table[i]的偏移地址为5
char s2; //相对于table[i]的偏移地址为9
short int pn; //相对于table[i]的偏移地址为A
char s3; //相对于table[i]的偏移地址为C
short int avr; //相对于table[i]的偏移地址为D
char s4; //相对于table[i]的偏移地址为F
}table[21];
table段的段地址相当于table,table[i]可用table + i * 16得到。所以,table中的元素地址可用(bp += 16).offset得到。
本程序代码段的代码如下:
codesg segment
start:
mov ax, data
mov ds, ax ;data段的段地址
mov si, 0 ;通过"add si, 4"来访问下一个元素,初始值为0
mov di, 0 ;通过"add di, 2"来访问下一个元素,初始值为0
mov cx, 21 ;拷贝数据的循环次数
mov ax, table
mov es, ax ;table段的段地址
mov bp, 0 ;作为table的行索引
mecp:
;----年份----
mov bx, 0
mov ax, [bx][si] ;4字节中的低两字节
mov es:[bp], ax
add si, 2 ;4字节中的高两字节
mov ax, [bx][si]
mov es:[bp].2, ax
sub si, 2 ;恢复以4字节为单位操作
;----空格----
mov byte ptr es:[bp].4, ' '
;----收入----
add bx, 84
mov ax, [bx][si] ;4字节中的低两字节
mov es:[bp].5, ax
add si, 2 ;4字节中的高两字节
mov dx, [bx][si]
mov es:[bp].7, dx
sub si, 2 ;恢复以4字节为单位操作
;----空格----
mov byte ptr es:[bp].9, ' '
;----雇员数----
add bx, 84
push ax ;备份收入4字节的低两字节
mov ax, [bx][di]
mov es:[bp].0AH, ax
pop ax ;恢复收入4字节的低两字节
;----空格----
mov byte ptr es:[bp].0CH, ' '
;----人均收入----
div word ptr [bx][di] ;收入DX-AX / 雇员数[bx][di],除法操作的商保存在AX中
mov es:[bp].0DH, ax
;----空格----
mov byte ptr es:[bp].0FH, ' '
add si, 4 ;读下一个年份和钱
add di, 2 ;读下一个年份的雇员数
add bp, 16 ;table的下一行
loop mecp
;----程序返回----
mov ax, 4c00h
int 21h
codesg ends
end start
也可以根据下标关系用更少的寄存器来表示每一种数据类型的下标索引。
在DOSBOX中用masm.exe和link.exe编译、连接以上程序得p7.exe,在用debug.exe加载p7.exe,用g命令运行程序到mov ax, 4c00h处,用”d es:0 14f”查看所拷贝和计算的内容:
通过抽取某一年的数据验证,可表明以上程序正确。
12.22
分析下面的程序,在运行前思考:这个程序可以正确返回吗?
运行后再思考:为什么是这种结果?
通过这个程序加深对相关内容的理解。
assume cs:codesg
codesg segment
mov ax, 4c00h
int 21h
start:
mov ax, 0
s:
nop
nop
mov di, offset s
mov si, offset s2
mov ax, cs:[si]
mov cs:[di], ax
s0:
jmp short s
s1:
mov ax, 0
int 21h
mov ax, 0
s2:
jmp short s1
nop
codesg ends
end start
根据指令含义,在脑子里面执行这段程序。在执行s0标号处语句jmp short s之前,程序将s2标号处的2字节内容拷贝到了s处 - 即s处的语句为jmp short (s1 - s2)。由于s1 - s2 = mov ax, 4c00h语句处 - s,所以这个程序能够被正确返回。
编程:在屏幕中间分别显示绿色、绿底红色、白底蓝色的字符串’welcome to masm!’。
编程所需的知识通过阅读、分析下面的材料获得。
80x25彩色字符模式显示缓冲区(以下简称为显示缓冲区)的结构:
内存地址空间中,B8000H ~BFFFFH共32KB的空间,为80x25彩色字符模式的显示缓冲区。向这个地址空间写入数据,写入的内容将理解出现在显示器上。
在80x25彩色字符模式下,显示器可以显示25行,每行80个字符,每个字符可以有256种属性(背景色、前景色、闪烁、高亮等组合信息)。
这样,一个字符在显示缓冲区中就要占两个字节,分别存放字符的ASCII码和属性。80x25模式下,显示第0页的内容。也就是说通常情况下,B8000H ~ B8F9FH中的4000个字节的内容将出现在显示器上。
在第一页显示缓冲区中:
偏移000 ~ 09F对应显示器上的第1行(80个字符占160个字节);
偏移0A0 ~ 13F对应显示器上的第2行;
偏移140 ~ 1DF对应显示器的第3行;
依此类推,可知,偏移F00 ~ F9F对应显示器上的第25行。
在一行中,一个字符占两个字节的存储空间(一个字),低位字节存储字符的ASCII码,高位字节存储字符的属性。一行共有80个字符,占160个字节。
即在一行中:
00 ~ 01单元对应显示器上的第1列;
02 ~ 03单元对应显示器上的第2列;
04 ~ 05单元对应显示器上的第3列;
依此类推,可知,9E ~ 9F单元对应显示器上的第80列。
例:在显示器的0行0列显示黑低绿色的字符串’ABCDEF’(‘A’的ASCII码值为41H,02H表示黑底绿色)
显示缓冲区里的内容为:
B800:0000 41 02 42 02 43 02 44 02 45 02 46 03 ……
…
B800:0000 … … … … … … … … … … … … … … … … … … …
可以看出,在显示缓冲区中,欧迪芝存放字符,奇地址存放字符的颜色属性。
一个在屏幕上显示的字符,具有前景(字符色)和背景(底色)两种颜色,字符还可以以高亮度和闪烁的方式显示。前景色、背景色、闪烁、高亮等信息被记录在属性字节中。
属性字节的格式:
7( BL 闪烁)— 6 5 4(R G B 背景)— 3(I 高亮) — 2 1 0(R G B 前景)
R:红色
G:绿色
B:蓝色
可以按位设置属性字节,从而配出各种不同的前景色和背景色。
比如:
红底绿字,属性字节为:01000010B;
红底闪烁绿字,属性字节为:11000010B;
红底高亮绿字,属性字节为:01001010B;
黑底白字,属性字节为:00000111B;
白底篮字,属性字节为:01110001B。
例:在显示器的0行0列显示红底高亮闪烁绿色的字符串’ABCDEF’(红底高亮闪烁绿色,属性字节为:11001010B,CAH)
显示缓冲区的内容为:
B800:0000 41 CA 42 CA 43 CA 44 CA 45 CA 46 CA…
…
B800:00A0 …….
注意,闪烁的效果必须在全屏DOS方式下才能看到。
根据题目要求和所提供的材料编程如下。
assume cs:codesg
data segment
db 'welcome to masm!'
data ends
codesg segment
start:
mov ax, data
mov ds, ax ;数据段 - 字符串的段地址
mov bx, 0 ;数据段 - 字符串的偏移地址
mov ax, 0B800H
mov es, ax ;显存的段地址
mov si, 720H ;第12行(12 - 1) x 160 + 第33列(33 - 1) x 2
mov cx, 10H ;字符串长度 - 循环次数
sl:
;----第12行----
mov al, [bx]
mov es:[si], al ;字符
mov byte ptr es:[si].1, 02H ;绿色
;----第13行----
add si, 160
mov es:[si], al
mov byte ptr es:[si].1, 24H ;绿底红色
;----第14行----
add si, 160
mov es:[si], al
mov byte ptr es:[si].1, 71H ;白底蓝色
inc bx ;下一个字符
sub si, 320 ;恢复到第12行
add si, 2 ;显示下一个字符的缓存
loop sl
mov ax, 4c00h
int 21h
codesg ends
end start
在DOSBOX中用masm.exe和link.exe编译、连接以上程序得p9.exe,再在DOSBOX中运行p9.exe,显示在DOSBOX中的字符串如下:
12.23
问题
显示字符串是现实工作中经常要用到的功能,应该编写一个通用的子程序来实现这个功能。我们应该提供灵活的调用接口,使调用者可以决定显示的位置(行、列)、内容和颜色。
子程序描述
名称:show_str
功能:在指定的位置,用指定的颜色,显示一个用0结束的字符串。
参数:(dh)=行号(取值范围0 ~ 24),(dl)=列号(取值范围0 ~ 79),(cl)=颜色,ds:si指向字符串的首地址
返回:无
应用举例:在屏幕的8行3列,用绿色显示data段中的字符串。
assume cs:code
data segment
db 'welcome to masm!', 0
data ends
code segment
start:
mov dh, 8
mov dl, 3
mov cl, 2
mov ax, data
mov ds, ax
mov si, 0
call show_str
mov ax, 4c00h
int 21h
show_str:
...
code ends
end start
编写显示字符串的子函数show_str
;子函数show_str
;功能,在指定的位置,用指定的颜色,显示一个用0结束的字符串
;参数:(dh)=行号(0 - 24),(dl)=列号(0 - 79),(cl)=颜色,ds:si指向字符串的首地址
show_str:
;备份在子程序中使用的寄存器
push es
push ax
push bx
push cx
push si
mov ax, 0B800H
mov es, ax ;显存的段地址
mov bx, 0 ;表示线程的偏移地址
mov al, cl ;备份颜色值
mov cx, 0 ;条件转移判断
;将行列转换为显存中的偏移地址 - (dh - 1) * 160 + (dl - 1) * 2
dec dh
mov cl, dh
lp1:
add bx, 160
loop lp1
dec dl
mov cl, dl
lp2:
add bx, 2
loop lp2
s: ;将字符串读到显存中
mov cl, [si]
jcxz r ;字符串结束则程序返回
mov es:[bx], cl ;字符
mov es:[bx].1, al ;颜色
inc si ;下一个字符
add bx, 2 ;显示下一个字符的显存偏移地址
jmp short s
r: ;恢复进入子程序前寄存器的值
pop si
pop cx
pop bx
pop ax
pop es
ret ;程序返回
将show_str的代码放在应用举例中,在DOSBOX中用masm.exe和link.exe编译和连接应用举例程序得pa1.exe,再在DOSBOX中运行pa1.exe得结果如下:
show_str被应用举例调用,在屏幕的8行3列显示绿色的字符串
问题
div指令可以做除法。当进行8位除法的时候,用al存储结果的商,ah存储结果的余数;进行16位除法的时候,用ax存储结果的商,dx存储结果的余数。可是,现在有一个问题,如果结果的商大于al或ax所能存储的最大值,那么将如何?
比如,下面的程序段:
mov bh, 1
mov ax, 1000
div bh
进行的是8位除法,结果的商为1000,而1000在al中放不下。
又比如,下面的程序段:
mov ax, 1000H
mov dx, 1
mov bx, 1
div bx
进行的是16位除法,结果的商为11000H,而11000H在ax中存放不下。
我们在用div指令做除法的时候,很可能发生上面的情况:结果的商过大,超出了寄存器所能存储的范围。当CPU执行div等除法指令的是偶,如果发生这样的情况,将引发CPU一个内部错误,这个错误被称为:除法溢出。我们可以通过特殊的程序来处理这个错误,但在这里我们不讨论这个错误的处理,这是后面课程中要涉及的内容。
我们已经清楚了问题的所在:用div指令做除法的时候可能产生除法溢出。由于这样的问题,在进行除法运算的时候要注意除数和被除数的值,比如1000000/10就不能用div指令来计算。那么怎么办呢?用下面的子程序divdw解决。
子程序描述
名称:divdw
功能:进行不会产生溢出的触发运算,被除数为dword型,除数为word型,结果为dword型。
参数:(ax)=dword型数据的低16位,(dx)=dword型数据的高16位,(cx)=除数
返回:(dx)=结果的高16位,(ax)=结果的低16位,(cx)=余数
应用举例:计算1000000/10(F4240H/0AH)
mov ax, 4240H
mov dx, 000FH
mov cx, 0AH
call divdw
结果:(dx)=0001H,(ax)=86A0H,(cx)=0。
编写子程序divdw如下
;名称:divdw
;功能:进行不会产生溢出的触发运算,被除数为dword型,除数为word型,结果为dword型。
;参数:(ax)=dword型数据的低16位,(dx)=dword型数据的高16位,(cx)=除数
;返回:(dx)=结果的高16位,(ax)=结果的低16位,(cx)=余数
divdw:
;备份在子函数中要用到的寄存器的值
push bx
push di
;----X/N=int(H/N)*65536 + [REM(H/N)*65536 + L]/N----
mov bx, ax ;备份被除数低16位
;X/N=int(H/N)*65536,ax=商,dx=余数
mov ax, dx
mov dx, 0
div cx
;[REM(H/N)*65536 + L]/N
mov di, ax ;备份商的高16位
mov ax, bx
div cx ;dx=余数,ax=商
;程序返回
mov cx, dx ;余数
mov dx, di ;商高16位
;子函数返回前,恢复备份的寄存器的值
pop di
pop bx
ret
用应用举例调用divdw子程序,在DOSBOX中用masm.exe和link.exe中编译、连接以上程序得pa2.exe,用debug.exe加载运行pa2.exe,用g命令执行程序到mov ax, 4c00h处,查看返回值:
问题
编程,将data段中的数据以十进制的形式显示出来。
data segment
dw 123, 12666, 1, 8, 3, 38
data ends
这些数据在内存中都是二进制信息,标记了数据的大小。要把它们显示到屏幕上,成为我们能够读懂的信息,需要进行信息的转化。比如,数值12666,在机器中存储为二进制信息:0011000101111010B(317AH),计算机可以理解它。而要在显示器上读到可以理解的数值12666,我们看到的应该是一字符串”12666”。由于显卡尊徐的是ASCII编码,为了让我们能在显示器上看到这串字符串,它在机器中应该以ASCII的形式存储为:31H、32H、36H、36H(字符“0”~“9”对应的ASCII码为30H ~ 39H)。
通过上面的分析可以看到,在概念世界中,有一个抽象的数据12666,它表示了一个数值的大小。在现实世界中它可以有多种表示形式,可以在电子机器中以高低电平(二进制)的形式存储,也可以在纸上、黑板上、屏幕上以人类的语言“12666”来书写。现在,我们面临的问题是,要将同一抽象的数据,从一种表示形式转化为另一种表示形式。
可见,要将数据用十进制形式显示到屏幕上,要进行两步工作:
第二步我们在本次试验的第一个子程序中已经实现,在这里只要调用以下show_str即可。我们来讨论第一步,因为将二进制信息转变为十进制形式的字符串也是经常要用到的功能,我们应该为它编写一个通用的子程序。
子程序描述
名称:dtoc
功能:将word型数据转变为表示十进制数的字符串,字符串以0为结尾符。
参数:(ax)=word型数据,ds:si指向字符串的首地址
返回:无
应用举例:编程,将数据12666以十进制的形式在屏幕的8行3列,用绿色显示出来。在显示时我们调用本次试验中的第一个子程序show_str。
assume cs:code
data segment
db 10 dup (0)
data ends
code segment
start:
mov ax, 12666
mov bx, data
mov ds, bx
mov si, 0
call dtoc
mov dh, 8
mov dl, 3
mov cl, 2
call show_str
mov ax, 4c00h
int 21h
;子函数show_str
;功能,在指定的位置,用指定的颜色,显示一个用0结束的字符串
;参数:(dh)=行号(0 - 24),(dl)=列号(0 - 79),(cl)=颜色,ds:si指向字符串的首地址
show_str:
;备份在子程序中使用的寄存器
push es
push ax
push bx
push cx
push si
mov ax, 0B800H
mov es, ax ;显存的段地址
mov bx, 0 ;表示线程的偏移地址
mov al, cl ;备份颜色值
mov cx, 0 ;条件转移判断
;将行列转换为显存中的偏移地址 - (dh - 1) * 160 + (dl - 1) * 2
dec dh
mov cl, dh
lp1:
add bx, 160
loop lp1
dec dl
mov cl, dl
lp2:
add bx, 2
loop lp2
s: ;将字符串读到显存中
mov cl, [si]
jcxz r ;字符串结束则程序返回
mov es:[bx], cl ;字符
mov es:[bx].1, al ;颜色
inc si ;下一个字符
add bx, 2 ;显示下一个字符的显存偏移地址
jmp short s
r: ;恢复进入子程序前寄存器的值
pop si
pop cx
pop bx
pop ax
pop es
ret ;程序返回
...
code ends
end start
编写dtoc子程序
;名称:dtoc
;功能:将word型数据转变为表示十进制数的字符串,字符串以0为结尾符。
;参数:(ax)=word型数据,ds:si指向字符串的首地址
dtoc:
;备份在子程序中使用的寄存器
push ax
push bx
push cx
push dx
push si
push di
mov cx, ax
jcxz mer ;ax=0则直接返回
mov dx, 0 ;除法高16位为0
mov bx, 10 ;做取个位数的除数
mov di, 0 ;记录被压入栈中余数的个数
mejp:
div bx ;ax=商,dx=余数
add dx, 30h ;将数字转为字符
push dx ;将余数压入栈中
inc di
mov cx, ax
jcxz mentry ;将入栈的字符存入ds:si中
mov dx, 0
jmp short mejp
mentry:
mov cx, di
melp:
pop dx
mov [si], dl
inc si
loop melp
mov byte ptr [si], 0 ;字符串以0结尾
mer:;恢复进入子程序前寄存器的值
pop di
pop si
pop dx
pop cx
pop bx
pop ax
ret
将以上编写的dtoc子函数加入应用举例程序中,在DOSBOX中用masm.exe和link.exe编译、连接得pa3.exe,在DOXBOX中运行pa3.exe输出以下结果:
数字12666在屏幕上显示。
12.28
编写一个子程序,将包含任意字符,以0结尾的字符串中的小写字母转变成大写的字母。描述如下。
名称:letterc
功能:将以0结尾的字符串中的小写字母转变成大写字母
参数:ds:si指向字符串的首地址
应用举例:
assume cs:codesg
datasg segment
db "Beginner's All-purpose Symbolic Instruction Code.", 0
datasg ends
codesg segment
begin:
mov ax, datasg
mov ds, ax
mov si, 0
call letterc
mov ax, 4c00h
int 21h
letterc:
...
codesg ends
end begin
注意需要进行转化的是字符串中的小写字母a~z,而不是其它字符。
letterc子程序的代码如下:
letterc:
push cx
push si
lc_l2s:
mov cl, [si]
jcxz lc_ret
cmp cl, 'a'
jb gn
cmp cl, 'z'
ja gn
and byte ptr [si], 11011111B
gn: inc si
jmp lc_l2s
lc_ret:
pop si
pop cx
ret
在DOSBOX中用masm.exe和link.exe编译、连接以上程序得pb.exe,用debug.exe加载pb.exe,用g命令运行程序到mov ax, 4c00h处,然后用d命令查看datasg中的内容:
12.29
编写0号中断的处理程序,使得在除法溢出发生时,在屏幕中间显示字符串”divide error!”,然后返回到DOS。
分析:
(1) 0号中断处理程序的存储 - 怎么将0号中断处理程序存储到一段操作系统不会操作的内存空间中 - 只要将0号中断处理程序存储在这段内存空间中,在不重启或改写这段内存空间的情况下,0号中断处理程序都会在这段内存空间中。
(2) CPU进入中断处理程序后,如何从中断处理程序中返回到DOS中?[书中用了mov ax, 4c00h, int 21h - 看来,加载可执行程序运行和进入中断处理程序的机制(过程一致)]。
编写供除法溢出时调用的0号中断处理程序
程序内容框架
assume cs:codesg
codesg segment
start:
;----拷贝0号中断处理程序到开始于0000:0200内存处(显示较短的字符串的程序不会超过256字节)----
;----设置0号中断处理程序的中断向量----
;----测试0号中断代码----
mov ax, 4c00h
int 21h
;----0号中断处理程序内容----
mov ax, 4c00h
int 21h
codesg ends
end start
打算:以上程序后,0号中断处理程序被拷贝至开始于0000:0200的内存空间中。
0号中断处理程序内容
;----0号中断处理程序内容----
ip0: jmp short ip0start
db "divide error!"
ip0start:
push ds
push es
push ax
push cx
push si
push di
mov ax, cs
mov ds, ax
mov si, 202h ;ds:si指向字符串 - jmp指令占2字节
mov ax, 0b800h
mov es, ax
mov di, 12 * 160 + 36 * 2 ;es:di指向屏幕中间位置
mov cx, 13 ;字符串长度
it_lp:
mov al, [si]
mov es:[di], al
inc si
add di, 2
loop it_lp
pop di
pop si
pop cx
pop ax
pop es
pop ds
mov ax, 4c00h
int 21h
ip0end:nop ;ip0 - ip0end之间的内容为0号中断处理程序内容
将字符串数据也写在中断处理程序中,这样保证该字符串与中断处理程序的生命周期一致。(若字符串的内存由操作系统以栈的形式分配,那么当父程序返回后,该字符串的内存很可能会被操作系统作它用)
拷贝(0号)中断处理程序
;----拷贝0号中断处理程序到开始于0000:0200内存处(显示较短的字符串的程序不会超过256字节)----
mov ax, cs
mov ds, ax
mov si, offset ip0 ;ds:si指向0号中断处理程序
mov ax, 0
mov es, ax
mov di, 200h
mov cx, offset ip0end - offset ip0 ;用编译器的offset伪指令计算两个标号之间内容占用的字节
cld
rep movsb
程序利用编译器的伪指令offset和movsb指令来拷贝0号中断处理程序。
设置(0号)中断处理程序的中断向量
;----设置0号中断处理程序的中断向量----
mov ax, 0
mov es, ax
mov word ptr es:[0 * 4], 0200h
mov word ptr es:[0 * 4 + 2], 0 ;0号中断处理程序的中断向量
N号中断程序的中断向量(中断程序的地址)在(N x 4 + 2):(N x 4)中。
测试0号中断处理程序的代码
;----测试0号中断的程序----
mov ax, 1000h
mov bh, 1
div bh
将以上各部分程序写在pc.asm文件中,在DOSBOX中用masm.exe和link.exe编译、连接pc.asm得pc.exe,在DOSBOX中执行pc.exe:
发现读《汇编语言》I中“编写0号中断程序”中的乌龙程序,程序因为出栈顺序没有保持让中断程序中的iret指令正确返回。那,在0号中断处理程序中,使用iret指令为何不能正确返回到发生中断语句处(后,即程序不能正常返回)?网上有人这样解释这个现象:div 引发的除零或除法溢出中断为故障。当控制转移到故障处理程序时,所保存的断点CS及IP的值指向引起故障的指令。这样,在故障处理程序把故障排除后,执行IRET返回到引起故障的程序继续执行时,刚才引起故障的指令可重新得到执行。由于栈中的ip指向引起故障的指令本身, 所以如果在故障处理程序中不处理的话,iret返回后系统仍然会执行那条引起故障的指令 并再次引发除法中断,从而无休无止。
(1) 编写并安装int 7ch中断例程,功能为显示一个用0结束的字符串,中断例程安装在0:200处。
参数:(dh)=行号,(dl)=列号,(cl)=颜色,ds:si指向字符串首地址。
编写并安装题设要求的int 7ch中断例程(pd1.asm)。
assume cs:codesg
codesg segment
start:
;----安装7ch中断例程----
mov ax, cs
mov ds, ax
mov si, offset ip7c_show_str_start ;所拷贝程序开始的偏移地址
mov ax, 0
mov es, ax
mov di, 0200h ;程序被拷贝的目的地址
mov cx, offset ip7c_show_str_end - offset ip7c_show_str_start
cld
rep movsb ;--拷贝7ch中断例程到内存中--
mov ax, 0
mov es, ax
mov word ptr es:[7ch * 4], 0200h
mov word ptr es:[7ch * 4 + 2], 0 ;--设置7ch中断例程的中断向量--
mov ax, 4c00h
int 21h
;----编写7ch中断例程----
;中断类型码:7ch
;功能:显示一个用0结束的字符串
;参数:(dh)=行号,(dl)=列号,(cl)=颜色,ds:si指向字符串首地址
;返回:无
ip7c_show_str_start:
push es
push ax
push bx
push cx
push si
mov ax, 0B800H
mov es, ax ;显存的段地址
mov bx, 0 ;表示线程的偏移地址
mov al, cl ;备份颜色值
mov cx, 0 ;条件转移判断
;----将行列转换为显存中的偏移地址 - dh * 160 + dl * 2----
mov cl, dh
sr_lp_radd:
jcxz sr_pre_cadd
add bx, 160
loop sr_lp_radd
sr_pre_cadd:
mov cl, dl
sr_lp_cadd:
jcxz sr_jp_ch2show
add bx, 2
loop sr_lp_cadd
;----将字符串读到显存中----
sr_jp_ch2show:
mov cl, [si]
jcxz sr_ret
mov es:[bx], cl
mov es:[bx].1, al
inc si
add bx, 2
jmp short sr_jp_ch2show
sr_ret:
pop si
pop cx
pop bx
pop ax
pop es
iret
ip7c_show_str_end:nop
codesg ends
end start
以上中断例程安装成功后,对下面的程序进行单步跟踪,尤其观察int、iret指令执行前后CS、IP和栈中的状态。
assume cs:code
data segment
db "welcome to masm!", 0
data ends
code segment
start:
mov dh, 10
mov dl, 10
mov cl, 2
mov ax, data
mov ds, ax
mov si, 0
int 7ch
mov ax, 4c00h
int 21h
code ends
end start
可将此段代码作为验证“编写并安装int 7ch中断例程”的程序。在DOSBOX中用masm.exe和link.exe编译、连接pd1.asm得pd1.exe,再在DOSBOX中运行pd1.exe。在DOSBOX中用masm.exe和link.exe编译、连接以上程序得test_pd1.exe,再运行test_pd1.exe。执行该段程序的结果如下:
运行结果
用debug.exe在DOSBOX中跟踪运行test_pd1.exe:
int指令执行前
iret指令执行前
iret指令执行后
12.31
(2) 编写并安装int 7ch中断例程,功能为完成loop指令的功能。
参数:(cx)=循环次数,(bx)=位移。
代码(pd2.asm):
assume cs:codesg
codesg segment
start:
;----安装7ch中断例程到0000:0200h处----
mov ax, cs
mov ds, ax
mov si, offset ip_7c_start
mov ax, 0
mov es, ax
mov di, 0200h
mov cx, offset ip_7c_end - offset ip_7c_start
cld
rep movsb ;--拷贝7ch中断处理程序到0:0200h处----
mov ax, 0
mov es, ax
mov word ptr es:[7ch * 4], 0200h
mov word ptr es:[7ch * 4 + 2], 0 ;--设置7ch中断处理程序的中断向量表--
mov ax, 4c00h
int 21h
;----实现loop指令的7ch中断例程----
;功能:完成loop指令的功能
;参数:(cx)=循环次数,(bx)=位移
;返回:无
ip_7c_start:
push bp
dec cx
jcxz ip_7c_ret
mov bp, sp
add ss:[bp + 2], bx ;无ss:[sp]格式
pop bp
iret
ip_7c_ret:
pop bp
iret
ip_7c_end:nop
codesg ends
end start
因为必须要用基础寄存器作为偏移地址值,所以用bp来代替sp的值。当bp入栈后,存储IP的栈偏移地址为sp + 2。
以上中断例程安装成功后,对下面的程序进行单步跟踪,尤其注意观察int、iret指令执行前后CS、IP和栈中的状态。
在屏幕中间显示80个“!”(test_pd2.asm)。
assume cs:code
code segment
start:
mov ax, 0b800h
mov es, ax
mov di, 160 * 12
mov bx, offset s - offset se
mov cx, 80
s:
mov byte ptr es:[di], '!'
add di, 2
int 7ch
se:
nop
mov ax, 4c00h
int 21h
code ends
end start
在DOSBOX中用masm.exe和link.exe分别编译、连接pd2.asm和test_pd2.asm得pd2.exe和test_pd2.exe,先运行pd2.exe安装7ch中断处理程序,然后运行test_pd2.exe测试7ch中断例程是否编写、安装正确。
在屏幕中间显示80个’!’(单步跟踪过程略,见13.1中的单步跟踪)
(3) 下面的程序,分别在屏幕的第2、4、6、8、行显示4句英文诗,补全程序。
assume cs:code
code segment
s1: db 'Good,better,best,', '$'
s2: db 'Never let it rest,', '$'
s3: db 'Till good is better,', '$'
s4: db 'And better,best.', '$'
s: dw offset s1, offset s2, offset s3, offset s4
row: db 2, 4, 6, 8
start:
mov ax, cs
mov ds, ax
mov bx, offset s
mov si, offset row
mov cx, 4
ok:
mov bh, 0
mov dh, ____ ;mov dh, [si]
mov dl, 0
mov ah, 2
int 10h
mov dx, ____ ;mov dx, [bx]
mov ah, 9
int 21h
________ ;inc si
________ ;add bx, 2
loop ok
mov ax, 4c00h
int 21h
code ends
end start
完成编译运行,体会其中的编程思想。
根据提供的中断例程9h和21h的功能反推每个填空中的内容(作为注释内容)。因为s的偏移值较小,所以ok后的mov bh, 0语句对bx + 2无影响。以上程序将数据存在内存中,然后以首地址为基础再以偏移的形式访问每个元素 – 即数组。在DOSBOX中用masm.exe和link.exe编译、连接以上程序得pd3.exe,运行pd3.exe:
编程,以“年/月/日 时:分:秒”的格式,显示当前的日期、时间。
注意:CMOS RAM中存储着系统的配置信息,除了保存时间信息的单元外,不要向其他的单元中写入内容,否则将引起一些系统错误。
P.266-P.267介绍了读CMORS RAM的方法;P.269页介绍了CMOS RAM中存储的时间信息。
assume cs:codesg
codesg segment
date_show_symbol: db "// :: "
cmos_ram_unit: db 9, 8, 7, 4, 2, 0
start:
mov ax, cs
mov ds, ax
mov si, offset cmos_ram_unit
mov di, offset date_show_symbol
mov ax, 0b800h
mov es, ax
mov bx, 160 * 12 + 7 * 2
mov cx, 6
show_time:
push cx
mov al, [si]
out 70h, al
in al, 71h
mov ah, al
mov cl, 4
shr ah, cl
and al, 00001111b
add ah, 30h
add al, 30h
mov es:[bx], ah
mov byte ptr es:[bx].1, 2
mov es:[bx].2, al
mov byte ptr es:[bx].3, 2
mov al, ds:[di]
mov es:[bx].4, al
mov byte ptr es:[bx].5, 2
inc si
inc di
add bx, 6
pop cx
loop show_time
mov ax, 4c00h
int 21h
codesg ends
end start
在DOSBOX中用masm.exe和link.exe编译、连接以上程序得pe.exe,在DOSBOX中运行pe.exe得以下结果:
2016.01.04
安装一个新的int 9中断例程,功能:在DOS下,按下“A”键后,除非不再松开,如果松开,就显示满屏幕的“A”,其它的键照常处理。
提示,按下一个键时产生的扫描码称为通码,松开一个键产生的扫描码称为断码。断码=通码+80h。
分析
键盘中有一个芯片对键盘上的每一个键的开关状态进行扫描。按下/松开一个键时,将产生一个扫描码(通码/断码)且被发送到主板相关芯片中的寄存器中(60h端口)。该芯片将产生编号为9的外部中断,这会让CPU执行完当前指令后转去执行类型码(编号)为9的中断处理程序[ CS = 0:(9 * 4 + 2), IP = 0:(9 * 4)]。
那么需要将编号为9的外中断处理程序的段地址和偏移地址分别存在0:(9 * 4 + 2)和0:(9 * 4)中。
当按下按钮时,新的int 9中断处理程序将会被执行,按照实验15的要求,在新的int 9中断处理程序中应该完成以下工作:
汇编程序模块大概如下:
assume cs:codesg
codesg segment
start:
;----安装新的int 9中断处理程序----
;--拷贝新的int 9中断处理程序到操作系统不使用的内存空间中(如0:200h开始的内存空间)--
;--备份原int 9中断处理程序的段地址和偏移地址--
;--将新的int 9中断处理程序的地址写入0:9*4中
mov ax, 4c00h
int 21h
;----新的int 9中断处理程序----
codesg ends
end start
按照以往安装中断程序和新int 9中断程序的内容完成整个程序。
实现
assume cs:codesg
codesg segment
start:
;----安装新的int 9中断处理程序----
;--拷贝新的int 9中断处理程序到操作系统不使用的内存空间中(如0:200h开始的内存空间)--
mov ax, cs
mov ds, ax
mov si, offset nip9_start
mov ax, 0
mov es, ax
mov di, 204h
mov cx, offset nip9_end - offset nip9_start
cld
rep movsb
;--备份原int 9中断处理程序的段地址和偏移地址--
push es:[9 * 4]
pop es:[200h]
push es:[9 * 4 + 2]
pop es:[202h]
;--将新的int 9中断处理程序的地址写入0:9*4中
cli
mov word ptr es:[9 * 4], 204h
mov word ptr es:[9 * 4 + 2], 0
sti
mov ax, 4c00h
int 21h
;----新的int 9中断处理程序----
nip9_start:
push ax
push bx
push cx
push es
in al, 60h ;读按键扫描码
pushf
call dword ptr cs:[200h] ;模拟调用原int 9中断处理程序
cmp al, 1EH + 80H
jne nip9_ret ;判断是否为A键松开
mov ax, 0b800h
mov es, ax
mov bx, 0
mov cx, 25
row_lp:
push cx
mov cx, 80
col_lp:
mov byte ptr es:[bx], 'A'
add bx, 2
loop col_lp
pop cx
loop row_lp ;显示满屏'A'
nip9_ret:
pop es
pop cx
pop bx
pop ax
iret
nip9_end:nop
codesg ends
end start
在DOSBOX中用masm.exe和link.exe编译、连接以上程序得pf.exe,在DOSBOX中运行pf.exe,再按’A’键,松开’A’键时DOSBOX中的现象如下所示:
新安装int 9中断处理程序后,按下’A’键送开后DOSBOX全屏’A’。
2016.01.05
安装一个新的int 7ch中断例程,为显示输出提供如下功能子程序。
(1) 清屏;
(2) 设置前景色;
(3) 设置背景色;
(4) 向上滚动一行。
入口参数说明如下。
(1) 用ah寄存器传递功能号:0表示清屏,1表示设置前景色,2表示设置背景色,3表示向上滚动一行;
(2) 对于1、2号功能,用al传送颜色值,(al)属于{0, 1, 2, 3, 4, 5, 6, 7}。
整个程序的模块
assume cs:codesg
codesg segment
start:
;----安装int 7ch中断例程----
mov ax, 4c00h
int 21h
;----int 7ch中断例程内容----
;功能/参数:
; 用ah寄存器传递功能号 - 0表示清屏,1表示设置前景色,2表示设置背景色,3表示向上滚动一行;
; 对于1、2号功能,用al传送颜色值,(al)属于{0, 1, 2, 3, 4, 5, 6, 7}。
ip7c_start:
clr_screen:
set_fg_screen:
set_bg_screen:
set_upl_screen:
iret
ip7c_end:nop
codesg ends
end start
分析
将int 7ch中断例程安装到操作系统不使用的内存空间中:0000:0200 ~ 0000:02FF(已写过几次安装中断程序的代码)。用直接定址表的方式实现包含多个功能子程序的中断例程int 7ch。
安装包含题目所要求的多个功能子程序的中断例程模块的实现
安装int 7ch的代码
;----安装int 7ch中断例程----
mov ax, cs
mov ds, ax
mov si, offset ip7c_start
mov ax, 0
mov es, ax
mov di, 0200h
mov cx, offset ip7c_end - offset ip7c_start
cld
rep movsb ;拷贝int 7ch中断例程到开始于0:0200h的内存空间段
mov word ptr es:[7ch * 4], 0200h
mov word ptr es:[7ch * 4 + 2], 0 ;设置int 7ch的中断向量
mov ax, 4c00h
int 21h
int 7ch中断例程各模块结构
org 200h ;告知编译器计算后续标号的偏移地址以200h为基址
;----int 7ch中断例程内容----
;功能/参数:
; 用ah寄存器传递功能号 - 0表示清屏,1表示设置前景色,2表示设置背景色,3表示向上滚动一行;
; 对于1、2号功能,用al传送颜色值,(al)属于{0, 1, 2, 3, 4, 5, 6, 7}。
ip7c_start:
jmp short ip7c_set
dat dw clr_screen, set_fg_screen, set_bg_screen, set_upl_screen
ip7c_set:
push bx
cmp ah, 0
jna ip7c_ret
cmp ah, 3
ja ip7c_ret
mov bl, ah
mov bh, 0
add bx, bx ;各子程序标号占用内存dw - 2字节
call word ptr dat[bx]
ip7c_ret:
pop bx
iret
;功能:清屏
;参数:无
;返回值:无
clr_screen:
ret
;功能:设置前景色
;参数:(al)=前景色
;返回值:无
set_fg_screen:
ret
;功能:设置背景色
;参数:(al)=背景色
;返回值:无
set_bg_screen:
ret
;屏幕往上滚动一行
;参数:无
;返回值:无
set_upl_screen:
ret
ip7c_end:nop
org 200h的作用是告知编译器后续标号的偏移地址的计算以200h为基址 - 保证”call word ptr dat[bx]”的正确性:原整个程序计算各标号的偏移地址以start为基址且CS:IP为start开始的段和偏移地址指向指令;而将int 7ch中断例程拷贝到0:200h后,CS:IP将以0段地址和以200h为开始偏移地址指向各指令。所以使用org 200h让后续标号的偏移地址都以200h为基址才能够保证能够用call指令调用到中断例程中的子函数。
定义子函数的地方
要在中断例程返回后(即iret指令后定义各子函数),不然这些子函数会被执行。这样,子函数中的ret指令会扰乱栈中的内容而让程序不能够正确返回。
完整的程序
ph.asm - 安装含有子功能的int 7ch中断例程
assume cs:codesg
codesg segment
start:
;----安装int 7ch中断例程----
mov ax, cs
mov ds, ax
mov si, offset ip7c_start
mov ax, 0
mov es, ax
mov di, 0200h
mov cx, offset ip7c_end - offset ip7c_start
cld
rep movsb ;拷贝int 7ch中断例程到开始于0:0200h的内存空间段
mov ax, 0
mov es, ax
cli
mov word ptr es:[7ch * 4], 0200h
mov word ptr es:[7ch * 4 + 2], 0 ;设置int 7ch的中断向量
std
mov ax, 4c00h
int 21h
org 200h ;告知编译器计算后续标号的偏移地址以200h为基址
;----int 7ch中断例程内容----
;功能/参数:
; 用ah寄存器传递功能号 - 0表示清屏,1表示设置前景色,2表示设置背景色,3表示向上滚动一行;
; 对于1、2号功能,用al传送颜色值,(al)属于{0, 1, 2, 3, 4, 5, 6, 7}。
ip7c_start:
setscreen: jmp short ip7c_set
dat dw clr_screen, set_fg_screen, set_bg_screen, set_upl_screen
ip7c_set:
push bx
cmp ah, 0
jb ip7c_ret
cmp ah, 3
ja ip7c_ret
mov bl, ah
mov bh, 0
add bx, bx ;各子程序标号占用内存dw - 2字节
call word ptr dat[bx]
ip7c_ret:
pop bx
iret
;功能:清屏
;参数:无
;返回值:无
clr_screen:
push bx
push cx
push es
mov bx, 0b800h
mov es, bx
mov bx, 0
mov cx, 25 * 80
cn:
mov byte ptr es:[bx], ' '
add bx, 2
loop cn
pop es
pop cx
pop bx
ret
;功能:设置前景色
;参数:(al)=前景色
;返回值:无
set_fg_screen:
push bx
push cx
push es
cmp al, 0
jb sfn_ret
cmp al, 7
ja sfn_ret
mov bx, 0b800h
mov es, bx
mov bx, 1
mov cx, 25 * 80
sfn:
and byte ptr es:[bx], 11111000b
or es:[bx], al
add bx, 2
loop sfn
sfn_ret:
pop es
pop cx
pop bx
ret
;功能:设置背景色
;参数:(al)=背景色
;返回值:无
set_bg_screen:
push bx
push cx
push es
cmp al, 0
jb sbn_ret
cmp al, 7
ja sbn_ret
mov cx, 4
shl al, cl
mov bx, 0b800h
mov es, bx
mov bx, 1
mov cx, 25 * 80
sbn:
and byte ptr es:[bx], 10001111b
or es:[bx], al
add bx, 2
loop sbn
sbn_ret:
pop es
pop cx
pop bx
ret
;屏幕往上滚动一行
;参数:无
;返回值:无
set_upl_screen:
push cx
push ds
push es
push si
push di
mov cx, 0b800h
mov es, cx
mov ds, cx
mov si, 160
mov di, 0
mov cx, 24
cld
supln_cover:
push cx
mov cx, 160
rep movsb
pop cx
loop supln_cover
mov cx, 80
mov si, 0
supln_space:
mov byte ptr [160 * 24 + si], ' '
add si, 2
loop supln_space
pop di
pop si
pop es
pop ds
pop cx
ret
ip7c_end:nop
codesg ends
end start
验证程序
test_ph0.asm - 验证0号功能
assume cs:codesg
codesg segment
start:
mov ax, 0
int 7ch
mov ax, 4c00h
int 21h
codesg ends
end start
test_ph1.asm - 验证1号功能
assume cs:codesg
codesg segment
start:
mov ax, 0102h
int 7ch
mov ax, 4c00h
int 21h
codesg ends
end start
test_ph2.asm - 验证2号功能
assume cs:codesg
codesg segment
start:
mov ax, 0204h
int 7ch
mov ax, 4c00h
int 21h
codesg ends
end start
test_ph3.asm - 验证3号功能
assume cs:codesg
codesg segment
start:
mov ax, 0300h
int 7ch
mov ax, 4c00h
int 21h
codesg ends
end start
验证
在DOSBOX中,用masm.exe和link.exe先后编译、连接ph.asm、test_ph1.asm、test_ph2.asm、test_ph3.asm以及test_ph0.asm得ph.exe、test_ph1.exe、test_ph2.asm和test_ph3.exe。再在DSOBOX中先后运行ph.exe、test_ph1.exe、test_ph2.exe、test_ph3.exe和test_ph0.exe,分别得以下运行结果(ph.exe运行后无现象):
运行test_ph1.exe(设置前景色)
运行test_ph2.exe(设置背景色)
运行test_ph0.exe(清屏)
注:最后一行为运行完相应的可执行程序后DOS向上滚动一行后给出的提示符
12.24
任务:将实验7中的Power idea公司的数据按照下图所示的格式在屏幕上显示出来。
在这个程序中,要用到我们前面学到的几乎所有的知识,注意选择适当的寻址方式和相关子程序设计和应用。另外,要注意,因为程序要显示的数据有些已经大于65535,应该编写一个新的数据到字符串转换的子程序,完成dword型数据到字符串的转换,说明如下。
名称:dtoc
功能:将dword型数据转变为表示十进制数的字符串,字符串以0为结尾符。
参数:(ax)=dword型数据的低16位,(dx)dword型数据的高16位,ds:si指向字符串的首地址
返回:无
在这个子程序中要注意除法的移除的问题,可以用在实验10中涉及的子程序divdw来解决。
[1] 重编dtoc
通过用“辗转相除法依次获取被除数个位、十位、…上的数字”的方式将数字转换为字符串时需要考虑“除法溢出”情况。可用之前的divdw子程序来代替div指令。其余地方跟之前的dtoc编写思路差不多。
;名称:dtoc
;功能:将dword型数据转变为表示十进制数的字符串,字符串以0为结尾符。
;参数:(ax)=dword型数据的低16位,(dx)=dword型数据的高16位,ds:si指向字符串的首地址
dtoc:
;备份在子程序中使用的寄存器
push ax
push bx
push cx
push dx
push si
push di
mov di, 0 ;记录数字的位数
;用辗转相除法获取数字的每位上的数据并压入栈中
mejp:
mov cx, ax
add cx, dx
jcxz mentry ;如果被除数已为0则结束辗转相除法
mov cx, 10
call divdw ;(cx)=余数
add cx, 30H ;将一位数字转换为对应的字符,如'3'=3 + 30H
push cx
inc di
jmp short mejp
;从栈中取出数字字符存入ds:si中
mentry:
mov cx, di
jcxz mer
melp:
pop bx
mov [si], bl
inc si
loop melp
mov byte ptr [si], 0 ;字符串以0为结束标志
mer:;恢复进入子程序前寄存器的值
pop di
pop si
pop dx
pop cx
pop bx
pop ax
ret
将重新编写的dtoc子程序放入上下文中测试:
assume cs:codesg
data segment
db 10 dup (0)
data ends
codesg segment
start:
mov dx, 000FH
mov ax, 4240H
mov bx, data
mov ds, bx
mov si, 0
call dtoc
mov dh, 8
mov dl, 3
mov cl, 2
call show_str
mov ax, 4c00h
int 21h
;名称:dtoc
;功能:将dword型数据转变为表示十进制数的字符串,字符串以0为结尾符。
;参数:(ax)=dword型数据的低16位,(dx)=dword型数据的高16位,ds:si指向字符串的首地址
dtoc:
;备份在子程序中使用的寄存器
push ax
push bx
push cx
push dx
push si
push di
mov di, 0 ;记录数字的位数
;用辗转相除法获取数字的每位上的数据并压入栈中
mejp:
mov cx, ax
add cx, dx
jcxz mentry ;如果被除数已为0则结束辗转相除法
mov cx, 10
call divdw ;(cx)=余数
add cx, 30H ;将一位数字转换为对应的字符,如'3'=3 + 30H
push cx
inc di
jmp short mejp
;从栈中取出数字字符存入ds:si中
mentry:
mov cx, di
jcxz mer
melp:
pop bx
mov [si], bl
inc si
loop melp
mov byte ptr [si], 0 ;字符串以0为结束标志
mer:;恢复进入子程序前寄存器的值
pop di
pop si
pop dx
pop cx
pop bx
pop ax
ret
;子函数show_str
;功能,在指定的位置,用指定的颜色,显示一个用0结束的字符串
;参数:(dh)=行号(0 - 24),(dl)=列号(0 - 79),(cl)=颜色,ds:si指向字符串的首地址
show_str:
...
ret ;程序返回
;名称:divdw
;功能:进行不会产生溢出的触发运算,被除数为dword型,除数为word型,结果为dword型。
;参数:(ax)=dword型数据的低16位,(dx)=dword型数据的高16位,(cx)=除数
;返回:(dx)=结果的高16位,(ax)=结果的低16位,(cx)=余数
divdw:
...
ret
codesg ends
end start
以上程序是将数字1000000(F4240H)显示在第8行、3列以绿色显示data段字符串的程序。在DOSBOX中用masm.exe和link.exe编译、连接以上程序得cd1.exe,再在DOSBOX中运行cd1.exe得以下结果:
12.25
[2] show_str子程序中的BUG
在循环中调用show_str子程序显示字符串时,发现其中包含错误。
在show_str中修改如下:
;子函数show_str
;功能,在指定的位置,用指定的颜色,显示一个用0结束的字符串
;参数:(dh)=行号(0 - 24),(dl)=列号(0 - 79),(cl)=颜色,ds:si指向字符串的首地址
show_str:
;备份在子程序中使用的寄存器
push es
push ax
push bx
push cx
push si
...
;将行列转换为显存中的偏移地址 - (dh) * 160 + (dl) * 2
mov cl, dh
lp1:
jcxz enlp2 ;若dh=0则进入计算列对应显存的偏移地址
add bx, 160
loop lp1
enlp2:
mov cl, dl
lp2:
jcxz s ;若dl=0则进入s开始处的程序
add bx, 2
loop lp2
...
ret ;程序返回
[3] 在“实验7”的程序中调用show_str、dtoc和divdw
打算
因为show_str显示字符串的方便性以及divdw能解决除法溢出的特性,所以将二者用于课程设计中:
显示公司年份:
...
;----显示年代----
mov ax, data
mov ds, ax
mov si, 0 ;ds:[si]用于访问年代数据
mov ax, 0B800H
mov es, ax
mov bp, 160 ;es:[bp]用于表示显存地址
mov cx, 15H ;读取年代的次数 - 21
mey:
mov al, [si]
mov es:[bp], al
mov al, [si].1
mov es:[bp].2, al
mov al, [si].2
mov es:[bp].4, al
mov al, [si].3
mov es:[bp].6, al
add si, 4
add bp, 160
loop mey
显示公司历年收入:
mov ax, data
mov es, ax
mov bp, 84 ;es:[bp]用于指向公司数据段中的收入数据
mov ax, table
mov ds, ax
mov si, 0 ;ds:[si]指向保存字符串的内存
mov dh, 0 ;显示收入行数的初始值
mov dl, 20 ;显示收入的列
mov cx, 15H
mem:
push dx
mov ax, es:[bp]
mov dx, es:[bp].2
call dtoc
pop dx
push cx
inc dh
mov cl, 07H
call show_str
add bp, 4 ;下一个收入
pop cx
loop mem
显示公司员工数:
mov ax, data
mov es, ax
mov bp, 84 * 2 ;es:[bp]用于指向公司员工数
mov ax, table
mov ds, ax
mov si, 0 ;ds:[si]指向保存字符串的内存
mov dh, 0 ;显示收入行数的初始值
mov dl, 35 ;显示收入的列
mov cx, 15H
mepn:
push dx
mov ax, es:[bp]
mov dx, 0
call dtoc
pop dx
push cx
inc dh
mov cl, 07H
call show_str
add bp, 2 ;下一个员工数
pop cx
loop mepn
显示公司历年员工均收入:
mov ax, data
mov es, ax
mov bp, 84 ;es:[bp]指向公司历年收入
mov bx, 84 * 2 ;es:[bx]指向公司历年员工数
mov ax, table
mov ds, ax
mov si, 0 ;ds:[si]指向保存字符串的内存
mov dh, 0 ;显示均收入的行
mov dl, 50 ;显示均收入的列
mov cx, 15H
meavr:
push dx
mov ax, es:[bp]
mov dx, es:[bp].2 ;(dx)=收入高16位,(ax)=收入低16位
push cx
mov cx, es:[bx] ;(cx)=公司员工数
call divdw ;此函数执行div cx,(dx)=均收入高16位,(ax)=均收入低16位,(cx)=余数
pop cx
call dtoc ;此函数将dx-ax数字转换为为字符串,存入ds:si指向的内存
pop dx
push cx
inc dh
mov cl, 07H
call show_str
pop cx
add bp, 4 ;公司下一年的收入
add bx, 2 ;公司下一年的员工数
loop meavr
将以上代码替换原实验7中的代码,将show_str、divdw和dtoc子程序的源码放入代码段中。在DOSBOX中用masm.exe和link.exe编译、连接该程序得cd1.exe,在DOSBOX中运行cd1.exe得以下结果:
结果显示出来了,但因为原背景内容的存在,使得界面看起来不清晰。在开始显示结果之前做清屏操作即可解决该问题。
清屏
;名称:screen
;功能:用空格清屏
;参数:(dl)=起始行,(dh)=结束行;(bl)=起始列,(bh)=结束列
screen:
push es
push bx
push cx
push dx
push bp
mov cx, 0B800H
mov es, cx
mov bp, 0 ;es:[bp]指向显存地址
mov cx, 0
;第dl行
mov cl, dl
melpr:
jcxz melpc
add bp, 160
loop melp
;第bl列
mov cl, bl
melpc:
jcxz menlpo
add bp, 2
loop melpc
;清(dl, bl) - (dh, bh)屏幕部分
menlpo:
sub dh, dl
sub bh, bl
mov cl, dh ;外循环 - 清行
jcxz oneline
melpo:
push bp
push cx
mov cl, bh
jcxz onecoli
melpi: ;内循环 - 清列
mov byte ptr es:[bp], ' '
add bp, 2
loop melpi
onecoli: ;bh!=bl时每次内循环结束后这句依旧会执行
mov byte ptr es:[bp], ' '
pop cx
pop bp
add bp, 160
loop melpo
oneline:
mov cl, bh
jcxz onecolo
melpii: ;内循环 - 清列
mov byte ptr es:[bp], ' '
add bp, 2
loop melpii
onecolo:
mov byte ptr es:[bp], ' '
pop bp
pop dx
pop cx
pop bx
pop es
ret
将screen子程序加入程序中(给以合适的参数),在DOSBOX中重新用masm.exe和link.exe编译、连接程序得cd1.exe,在DOSBOX中重新运行cd1.exe得如下结果:
上图中运行结果对应的所有汇编代码如下:
assume cs:codesg
data segment
db '1975', '1976', '1977', '1978', '1979', '1980', '1981', '1982', '1983'
db '1984', '1985', '1986', '1987', '1988', '1989', '1990', '1991', '1992'
db '1993', '1994', '1995'
;以上是表示21年的21个字符串
dd 16, 22, 382, 1356, 2390, 8000, 16000, 24486, 50065, 97479, 140417, 197514
dd 345980, 590827, 803530, 1183000, 1843000, 2759000, 3753000, 4649000, 5937000
;以上是表示21年公司总收入的21个dword型数据
dw 3, 7, 9, 13, 28, 38, 130, 220, 476, 778, 1001, 1442, 2258, 2793, 4037, 5635, 8226
dw 11542, 14430, 15257, 17800
;以上是表示21年公司雇员人数的21个word型数据
data ends
table segment
db 48 dup(0) ;48字节
table ends
stack segment
db 48 dup(0) ;48字节
stack ends
codesg segment
start:
mov ax, stack
mov ss, ax
mov sp, 49
;清屏
mov dl, 1
mov dh, 21
mov bl, 1
mov bh, 80
call screen
;----显示年代----
mov ax, data
mov ds, ax
mov si, 0 ;ds:[si]用于访问年代数据
mov ax, 0B800H
mov es, ax
mov bp, 160 ;es:[bp]用于表示显存地址
mov cx, 15H ;读取年代的次数 - 21
mey:
mov al, [si]
mov es:[bp], al
mov al, [si].1
mov es:[bp].2, al
mov al, [si].2
mov es:[bp].4, al
mov al, [si].3
mov es:[bp].6, al
add si, 4
add bp, 160
loop mey
;----显示公司历年收入----
mov ax, data
mov es, ax
mov bp, 84 ;es:[bp]用于指向公司数据段中的收入数据
mov ax, table
mov ds, ax
mov si, 0 ;ds:[si]指向保存字符串的内存
mov dh, 0 ;显示收入行数的初始值
mov dl, 20 ;显示收入的列
mov cx, 15H
mem:
push dx
mov ax, es:[bp]
mov dx, es:[bp].2
call dtoc
pop dx
push cx
inc dh
mov cl, 07H
call show_str
add bp, 4 ;下一个收入
pop cx
loop mem
;----显示公司员工数----
mov ax, data
mov es, ax
mov bp, 84 * 2 ;es:[bp]用于指向公司员工数
mov ax, table
mov ds, ax
mov si, 0 ;ds:[si]指向保存字符串的内存
mov dh, 0 ;显示收入行数的初始值
mov dl, 35 ;显示收入的列
mov cx, 15H
mepn:
push dx
mov ax, es:[bp]
mov dx, 0
call dtoc
pop dx
push cx
inc dh
mov cl, 07H
call show_str
add bp, 2 ;下一个员工数
pop cx
loop mepn
;----显示公司历年员工均收入----
mov ax, data
mov es, ax
mov bp, 84 ;es:[bp]指向公司历年收入
mov bx, 84 * 2 ;es:[bx]指向公司历年员工数
mov ax, table
mov ds, ax
mov si, 0 ;ds:[si]指向保存字符串的内存
mov dh, 0 ;显示均收入的行
mov dl, 50 ;显示均收入的列
mov cx, 15H
meavr:
push dx
mov ax, es:[bp]
mov dx, es:[bp].2 ;(dx)=收入高16位,(ax)=收入低16位
push cx
mov cx, es:[bx] ;(cx)=公司员工数
call divdw ;此函数执行div cx,(dx)=均收入高16位,(ax)=均收入低16位,(cx)=余数
pop cx
call dtoc ;此函数将dx-ax数字转换为为字符串,存入ds:si指向的内存
pop dx
push cx
inc dh
mov cl, 07H
call show_str
pop cx
add bp, 4 ;公司下一年的收入
add bx, 2 ;公司下一年的员工数
loop meavr
;----程序返回----
mov ax, 4c00h
int 21h
;名称:screen
;功能:用空格清屏
;参数:(dl)=起始行,(dh)=结束行;(bl)=起始列,(bh)=结束列
screen:
push es
push bx
push cx
push dx
push bp
mov cx, 0B800H
mov es, cx
mov bp, 0 ;es:[bp]指向显存地址
mov cx, 0
;第dl行
mov cl, dl
melpr:
jcxz melpc
add bp, 160
loop melp
;第bl列
mov cl, bl
melpc:
jcxz menlpo
add bp, 2
loop melpc
;清(dl, bl) - (dh, bh)屏幕部分
menlpo:
sub dh, dl sub bh, bl mov cl, dh ;外循环 - 清行
jcxz oneline
melpo:
push bp
push cx
mov cl, bh
jcxz onecoli
melpi: ;内循环 - 清列
mov byte ptr es:[bp], ' '
add bp, 2
loop melpi
onecoli: ;bh!=bl时每次内循环结束后这句依旧会执行
mov byte ptr es:[bp], ' '
pop cx
pop bp
add bp, 160
loop melpo
oneline:
mov cl, bh
jcxz onecolo
melpii: ;内循环 - 清列
mov byte ptr es:[bp], ' '
add bp, 2
loop melpii
onecolo:
mov byte ptr es:[bp], ' '
pop bp
pop dx
pop cx
pop bx
pop es
ret
;名称:dtoc
;功能:将dword型数据转变为表示十进制数的字符串,字符串以0为结尾符。
;参数:(ax)=dword型数据的低16位,(dx)=dword型数据的高16位,ds:si指向字符串的首地址
dtoc:
;备份在子程序中使用的寄存器
push ax
push bx
push cx
push dx
push si
push di
mov di, 0 ;记录数字的位数
;用辗转相除法获取数字的每位上的数据并压入栈中
mejp:
mov cx, ax
add cx, dx
jcxz mentry ;如果被除数已为0则结束辗转相除法
mov cx, 10
call divdw ;(cx)=余数
add cx, 30H ;将一位数字转换为对应的字符,如'3'=3 + 30H
push cx
inc di
jmp short mejp
;从栈中取出数字字符存入ds:si中
mentry:
mov cx, di
jcxz mer
melp:
pop bx
mov [si], bl
inc si
loop melp
mov byte ptr [si], 0 ;字符串以0为结束标志
mer:;恢复进入子程序前寄存器的值
pop di
pop si
pop dx
pop cx
pop bx
pop ax
ret
;名称:divdw
;功能:进行不会产生溢出的触发运算,被除数为dword型,除数为word型,结果为dword型。
;参数:(ax)=dword型数据的低16位,(dx)=dword型数据的高16位,(cx)=除数
;返回:(dx)=结果的高16位,(ax)=结果的低16位,(cx)=余数
divdw:
;备份在子函数中要用到的寄存器的值
push bx
push di
;----X/N=int(H/N)*65536 + [REM(H/N)*65536 + L]/N----
mov bx, ax ;备份被除数低16位
;X/N=int(H/N)*65536,ax=商,dx=余数
mov ax, dx
mov dx, 0
div cx
;[REM(H/N)*65536 + L]/N
mov di, ax ;备份商的高16位
mov ax, bx
div cx ;dx=余数,ax=商
;程序返回
mov cx, dx ;余数
mov dx, di ;商高16位
;子函数返回前,恢复备份的寄存器的值
pop di
pop bx
ret
;子函数show_str
;功能,在指定的位置,用指定的颜色,显示一个用0结束的字符串
;参数:(dh)=行号(0 - 24),(dl)=列号(0 - 79),(cl)=颜色,ds:si指向字符串的首地址
show_str:
;备份在子程序中使用的寄存器
push es
push ax
push bx
push cx
push si
mov ax, 0B800H
mov es, ax ;显存的段地址
mov bx, 0 ;表示线程的偏移地址
mov al, cl ;备份颜色值
mov cx, 0 ;条件转移判断
;将行列转换为显存中的偏移地址 - (dh - 1) * 160 + (dl - 1) * 2
mov cl, dh
lp1:
jcxz enlp2 ;若dh=0则进入计算列对应显存的偏移地址
add bx, 160
loop lp1
enlp2:
mov cl, dl
lp2:
jcxz s ;若dl=0则进入s开始处的程序
add bx, 2
loop lp2
s: ;将字符串读到显存中
mov cl, [si]
jcxz r ;字符串结束则程序返回
mov es:[bx], cl ;字符
mov es:[bx].1, al ;颜色
inc si ;下一个字符
add bx, 2 ;显示下一个字符的显存偏移地址
jmp short s
r: ;恢复进入子程序前寄存器的值
pop si
pop cx
pop bx
pop ax
pop es
ret ;程序返回
codesg ends
end start
加上空行和注释,一共369行代码。过几天再看代码或者再添加代码(后期代码编写已有后续感觉)有些凌乱感觉。对以上程序有必要进行“整理”。
[0] 尽量纠正掉程序中的错误。
[1] 尽量去掉程序中没必要的代码。
[2] 尽量将程序模块化。
功能:根据指定背景色(ah)和前景色(al)清屏[dl, bl] ~ [dh, bh]区域
参数:(dl) = 清屏起始行,(dh) = 清屏结束行,(bl) = 清屏起始列,(bh) = 清屏结束列;(al) = 清屏格式(前、背景色)
返回:无
函数体内容:
经整理后的screen子程序内容如下:
;名称:screen
;功能:根据指定背景色和前景色(al)清屏[dl, bl] ~ [dh, bh]区域
;参数:(dl) = 清屏起始行,(dh) = 清屏结束行,(bl) = 清屏起始列,(bh) = 清屏结束列;
; (al) = 清屏格式(前、背景色)
;返回:无
screen:
push es
push bx
push cx
push dx
push bp
mov cx, 0B800H
mov es, cx
mov bp, 0 ;es:[bp]指向欲清屏部分对应的显存地址
mov cx, 0
;---计算屏幕第dl行第bl列对应的显存地址----
mov cl, dl
sn_lp_radd:
jcxz sn_pre_cadd
add bp, 160
loop sn_lp_radd
sn_pre_cadd:
mov cl, bl
sn_lp_cadd:
jcxz sn_pre_clr
add bp, 2
loop sn_lp_cadd
;----清[dl, bl] ~ [dh, bh]屏幕部分----
sn_pre_clr:
sub dh, dl
inc dh
sub bh, bl
inc bh
mov cl, dh
sn_lp_clr_r:
push cx
push bp
mov cl, bh
sn_lp_clr_c:
mov byte ptr es:[bp], ' '
mov es:[bp].1, al
add bp, 2 ;下一列
loop sn_lp_clr_c
pop bp
add bp, 160 ;下一行
pop cx
loop sn_lp_clr_r
pop bp
pop dx
pop cx
pop bx
pop es
ret
整理子程序screen还包括对其“标号命名”、“代码风格”(二者的可读性待提升)以及“注释”的修改 - 尽可能让源程序可读性变高。注意al的值 - 前景色和背景色的搭配要能够显示前景色(如黑白配)。
主要修改了show_str、divdw以及dtoc子程序的“标号命名”、“代码风格”以及“注释”。代码几乎没变(有机会发现错误再修改)。接下来继续整理子程序的调用。
12.28
所有的子程序以及调用子程序的代码都被写在了codesg段中。编译器和连接器提供了在编辑器中编写汇编指令的模式(格式) - 根据伪指令给操作系统(载入程序)提供信息。打算:根据masm.exe和link.exe提供的伪指令模块化以上程序。
subfun_name proc far
…
retf
subfun_name endp
proc/endp是定义程序段的伪指令。far属性用来说明调用该函数属于段间调用,调用者和该函数可以在同一个段中也可不在同一个段中。将dtoc、divdw和show_str用该伪指令定义可被在段间调用的子程序。
assume cs:codesg
data segment
...
data ends
table segment
db 48 dup(0) ;48字节
table ends
stack segment
db 48 dup(0) ;48字节
stack ends
codesg segment
start:
...
;清屏
...
;----显示年代----
...
;----显示公司历年收入----
...
mem:
...
call far ptr dtoc
...
call far ptr show_str
...
loop mem
;----显示公司员工数----
...
mepn:
...
call far ptr dtoc
...
call far ptr show_str
...
loop mepn
;----显示公司历年员工均收入----
...
meavr:
...
call far ptr divdw ;此函数执行div cx,(dx)=均收入高16位,(ax)=均收入低16位,(cx)=余数
pop cx
call far ptr dtoc ;此函数将dx-ax数字转换为为字符串,存入ds:si指向的内存
...
call far ptr show_str
...
loop meavr
;----程序返回----
mov ax, 4c00h
int 21h
;名称:screen
;功能:根据指定背景色和前景色(al)清屏[dl, bl] ~ [dh, bh]区域
;参数:(dl) = 清屏起始行,(dh) = 清屏结束行,(bl) = 清屏起始列,(bh) = 清屏结束列;
; (al) = 清屏格式(前、背景色)
;返回:无
screen:
...
ret
;名称:dtoc
;功能:将dword型数据转变为表示十进制数的字符串,字符串以0为结尾符。
;参数:(ax)=dword型数据的低16位,(dx)=dword型数据的高16位,ds:si指向字符串的首地址
dtoc proc far
...
retf
dtoc endp
;名称:divdw
;功能:进行不会产生溢出的触发运算,被除数为dword型,除数为word型,结果为dword型。
;参数:(ax)=dword型数据的低16位,(dx)=dword型数据的高16位,(cx)=除数
;返回:(dx)=结果的高16位,(ax)=结果的低16位,(cx)=余数
divdw proc far
...
retf
divdw endp
;子函数show_str
;功能,在指定的位置,用指定的颜色,显示一个用0结束的字符串
;参数:(dh)=行号[1, 24],(dl)=列号[1, 80],(cl)=颜色,ds:si指向字符串的首地址
show_str proc far
...
retf
show_str endp
codesg ends
end start
当codesg代码段从某处开始超过64KB时,后续内容就会被操作系统(载入程序)载入下一个段中。在codesg代码段中用proc far/enp伪指令定义子程序,调用段间远调用的call指令的形式也要用call (far ptr)subfun_name/retf。这样,即使代码段的内容超过64KB时,调用子程序的过程也不会出错。
在以上对代码整理过程中,用proc far/endp伪指令在代码段中定义了三个子程序(过程,子函数)。因为screen子程序为课程设计1中隐含的一个要求,打算将screen定义到另外一个名为toolfuns.asm的文件中。
proc far/endp + public/extrn
将清屏子程序screen定义在名为toolfuns.asm的文件的代码段中:
;toolfuns.asm
;定义常用的过程(子程序)供主代码段调用
assume cs:toolfuns
;名称:screen
;功能:根据指定背景色和前景色(al)清屏[dl, bl] ~ [dh, bh]区域
;参数:(dl) = 清屏起始行,(dh) = 清屏结束行,(bl) = 清屏起始列,(bh) = 清屏结束列;
; (al) = 清屏格式(前、背景色)
;返回:无
public screen
toolfuns segment
screen proc far
push es
push bx
push cx
push dx
push bp
mov cx, 0B800H
mov es, cx
mov bp, 0 ;es:[bp]指向欲清屏部分对应的显存地址
mov cx, 0
;---计算屏幕第dl行第bl列对应的显存地址----
mov cl, dl
sn_lp_radd:
jcxz sn_pre_cadd
add bp, 160
loop sn_lp_radd
sn_pre_cadd:
mov cl, bl
sn_lp_cadd:
jcxz sn_pre_clr
add bp, 2
loop sn_lp_cadd
;----清[dl, bl] ~ [dh, bh]屏幕部分----
sn_pre_clr:
sub dh, dl
inc dh
sub bh, bl
inc bh
mov cl, dh
sn_lp_clr_r:
push cx
push bp
mov cl, bh
sn_lp_clr_c:
mov byte ptr es:[bp], ' '
mov es:[bp].1, al
add bp, 2 ;下一列
loop sn_lp_clr_c
pop bp
add bp, 160 ;下一行
pop cx
loop sn_lp_clr_r
pop bp
pop dx
pop cx
pop bx
pop es
retf
screen endp
toolfuns ends
end
在名为cd1.asm的文件中编写主程序(延用之前的“标号命名风格”和“代码风格”):
assume cs:codesg
data segment
db '1975', '1976', '1977', '1978', '1979', '1980', '1981', '1982', '1983'
db '1984', '1985', '1986', '1987', '1988', '1989', '1990', '1991', '1992'
db '1993', '1994', '1995'
;以上是表示21年的21个字符串
dd 16, 22, 382, 1356, 2390, 8000, 16000, 24486, 50065, 97479, 140417, 197514
dd 345980, 590827, 803530, 1183000, 1843000, 2759000, 3753000, 4649000, 5937000
;以上是表示21年公司总收入的21个dword型数据
dw 3, 7, 9, 13, 28, 38, 130, 220, 476, 778, 1001, 1442, 2258, 2793, 4037, 5635, 8226
dw 11542, 14430, 15257, 17800
;以上是表示21年公司雇员人数的21个word型数据
data ends
table segment
db 48 dup(0) ;48字节
table ends
stack segment
db 48 dup(0) ;48字节
stack ends
extrn screen:far
codesg_main segment
start:
mov ax, stack
mov ss, ax
mov sp, 49
;----清屏----
mov dx, 1501H
mov bx, 5001H
mov al, 07H
call screen
;----显示年代----
mov ax, data
mov ds, ax
mov si, 0 ;ds:[si]用于访问年代数据
mov ax, 0B800H
mov es, ax
mov bp, 160 ;es:[bp]用于表示显存地址
mov cx, 15H ;读取年代的次数 - 21
cn_year:
mov al, [si]
mov es:[bp], al
mov al, [si].1
mov es:[bp].2, al
mov al, [si].2
mov es:[bp].4, al
mov al, [si].3
mov es:[bp].6, al
add si, 4
add bp, 160
loop cn_year
;----显示公司历年收入----
mov ax, data
mov es, ax
mov bp, 84 ;es:[bp]用于指向公司数据段中的收入数据
mov ax, table
mov ds, ax
mov si, 0 ;ds:[si]指向保存字符串的内存
mov dh, 0 ;显示收入行数的初始值
mov dl, 20 ;显示收入的列
mov cx, 15H
cn_money:
push dx
mov ax, es:[bp]
mov dx, es:[bp].2
call far ptr dtoc
pop dx
push cx
inc dh
mov cl, 07H
call far ptr show_str
add bp, 4 ;下一个收入
pop cx
loop cn_money
;----显示公司员工数----
mov ax, data
mov es, ax
mov bp, 84 * 2 ;es:[bp]用于指向公司员工数
mov ax, table
mov ds, ax
mov si, 0 ;ds:[si]指向保存字符串的内存
mov dh, 0 ;显示收入行数的初始值
mov dl, 35 ;显示收入的列
mov cx, 15H
cn_pn:
push dx
mov ax, es:[bp]
mov dx, 0
call far ptr dtoc
pop dx
push cx
inc dh
mov cl, 07H
call far ptr show_str
add bp, 2 ;下一个员工数
pop cx
loop cn_pn
;----显示公司历年员工均收入----
mov ax, data
mov es, ax
mov bp, 84 ;es:[bp]指向公司历年收入
mov bx, 84 * 2 ;es:[bx]指向公司历年员工数
mov ax, table
mov ds, ax
mov si, 0 ;ds:[si]指向保存字符串的内存
mov dh, 0 ;显示均收入的行
mov dl, 50 ;显示均收入的列
mov cx, 15H
cn_avr:
push dx
mov ax, es:[bp]
mov dx, es:[bp].2 ;(dx)=收入高16位,(ax)=收入低16位
push cx
mov cx, es:[bx] ;(cx)=公司员工数
call far ptr divdw ;此函数执行div cx,(dx)=均收入高16位,(ax)=均收入低16位,(cx)=余数
pop cx
call far ptr dtoc ;此函数将dx-ax数字转换为为字符串,存入ds:si指向的内存
pop dx
push cx
inc dh
mov cl, 07H
call far ptr show_str
pop cx
add bp, 4 ;公司下一年的收入
add bx, 2 ;公司下一年的员工数
loop cn_avr
;----程序返回----
mov ax, 4c00h
int 21h
;名称:dtoc
;功能:将dword型数据转变为表示十进制数的字符串,字符串以0为结尾符。
;参数:(ax)=dword型数据的低16位,(dx)=dword型数据的高16位,ds:si指向字符串的首地址
dtoc proc far
push ax
push bx
push cx
push dx
push si
push di
mov di, 0 ;记录数字的位数
;----用辗转相除法获取数值每位上的数字并压入栈中----
dc_jp_num2char:
mov cx, ax
add cx, dx
jcxz dc_pre_get_nc
mov cx, 10
call far ptr divdw ;divaw执行后,dx:ax为被除数,(cx)=余数
add cx, 30H ;将一位数字转换为对应的字符,如'3'=3 + 30H
push cx
inc di
jmp short dc_jp_num2char
;----从栈中取出数字字符存入ds:si中----
dc_pre_get_nc:
mov cx, di
jcxz dc_ret
dc_lp_get_nc:
pop bx
mov [si], bl
inc si
loop dc_lp_get_nc
mov byte ptr [si], 0 ;ds:si的字符串以0结束
dc_ret:
pop di
pop si
pop dx
pop cx
pop bx
pop ax
retf
dtoc endp
;名称:divdw
;功能:进行不会产生溢出的触发运算,被除数为dword型,除数为word型,结果为dword型。
;参数:(ax)=dword型数据的低16位,(dx)=dword型数据的高16位,(cx)=除数
;返回:(dx)=结果的高16位,(ax)=结果的低16位,(cx)=余数
divdw proc far
push bx
push di
;----X/N=int(H/N)*65536 + [REM(H/N)*65536 + L]/N----
mov bx, ax ;备份被除数低16位
;X/N=int(H/N)*65536,ax=商,dx=余数
mov ax, dx
mov dx, 0
div cx
;[REM(H/N)*65536 + L]/N
mov di, ax ;备份商的高16位
mov ax, bx
div cx ;dx=余数,ax=商
;将结果返回
mov cx, dx ;余数
mov dx, di ;商高16位
pop di
pop bx
retf
divdw endp
;子函数show_str
;功能,在指定的位置,用指定的颜色,显示一个用0结束的字符串
;参数:(dh)=行号[1, 24],(dl)=列号[1, 80],(cl)=颜色,ds:si指向字符串的首地址
show_str proc far
push es
push ax
push bx
push cx
push si
mov ax, 0B800H
mov es, ax ;显存的段地址
mov bx, 0 ;表示线程的偏移地址
mov al, cl ;备份颜色值
mov cx, 0 ;条件转移判断
;----将行列转换为显存中的偏移地址 - dh * 160 + dl * 2----
mov cl, dh
sr_lp_radd:
jcxz sr_pre_cadd
add bx, 160
loop sr_lp_radd
sr_pre_cadd:
mov cl, dl
sr_lp_cadd:
jcxz sr_jp_ch2show
add bx, 2
loop sr_lp_cadd
;----将字符串读到显存中----
sr_jp_ch2show:
mov cl, [si]
jcxz sr_ret
mov es:[bx], cl
mov es:[bx].1, al
inc si
add bx, 2
jmp short sr_jp_ch2show
sr_ret:
pop si
pop cx
pop bx
pop ax
pop es
retf
show_str endp
codesg_main ends
end start
在主代码段codesg_main前用伪指令”extrn screen:far”声明了screen这个子程序在其它文件定义(所以,用masm.exe汇编编译器编译cd1.asm时不会有screen未定义的错误)。同时在toolfuns.asm中用伪指令”public screen”声明了toolfuns段中的screen子过程[连接器link.exe将根据”extrn screen:far”和”public screen”两条伪指令中的相同的“screen”字符串将cd1.asm中的”call screen”指令更改为”call screen的地址(或能够找到screen程序在内存中地址的其它值)”]。
在DOSBOX中,先用masm.exe分别编译cd1.asm和toolfuns.asm,再用link.exe连接cd1.asm和toolfuns.asm的目标文件(link cd1.obj toolfuns.asm)得最终的可执行文件cd1.exe。运行cd1.exe,运行结果跟之前一样。
去年寒假时按照书中的步骤用的tc平台进行的实验(见“[Hb-XVII] 计算机的抽象层次-简 使用寄存器 使用内存空间 程序执行过程 使用main函数规定 不定参数函数机制 C”)。不知今年7月有无机会另用(Linux平台下的)gcc编译器来完成综合研究实验。