读《汇编语言》3E-III[摘 检测点 实验 课程设计]

2015.12.16 - 2016.01.07
读《汇编语言》I
读《汇编语言》II

1. 描述

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字节空间)。

在基于一个计算机硬件编程时,须知道内存地址空间的分配情况 - 内存地址空间与实际物理存储器的对应关系。

物理地址

所有内存单元构成的存储空间是一个一维的线性空间 - 内存地址空间,每一个内存单元在这个空间中都有唯一的地址,将这个唯一的地址称为物理地址(物理地址构成内存逻辑存储地址)。

8086CPU工作简要过程

(1) 从CS:IP指向的内存单元读取指令,读取的指令进入指令缓冲器;
(2) IP = IP + 所读取指令的长度,从而指向下一条指令;
(3) 执行指令。转到步骤(1),重复这个过程。

12.17

寄存器、内存之间的数据传输

在设计CPU(或内存)时,有没有在寄存器、内存单元间设计相连的通道(导线)。如8086PC“不支持立即数到段寄存器”、“内存单元与内存单元”之间的传输。

栈 - 是一种特殊方式(机制、方法)的角度

最后进入栈中的数据,最先出去。它有两个最基本的操作:入栈(将新的元素放到栈顶)和出栈(从栈顶取出元素)。8086CPU提供相关的指令(push/pop)来以栈的方式访问内存空间。这意味着,在基于8086CPU编程时,可以将一段内存当作栈来使用(“指令 + 寄存器”实现将内存当作栈来使用)。 - 通过所提供的指令将栈机制用在内存的使用上

12.18

(DOS下)debug加载运行程序

  • 外壳程序备份当前的运行状态(寄存器值等),运行debug程序。
  • debug获得对资源的控制权(寄存器等),debug根据编译器和连接器在可执行文件中加入的信息将程序载入内存中[根据编译器-连接器所给信息(来自源程序-伪指令等)为代码、数据分配内存空间等],响应用户输入并操作相关的硬件资源。
  • 运行程序时,debug备份当前的运行状态,debug将资源控制权交给用户程序,用户程序运行完毕后返回,debug恢复运行状态,继续运行。
  • debug程序返回,外壳程序恢复所备份的运行状态,继续运行。

DOS加载程序过程见P.92。程序的加载到运行的过程简示 - 加载程序到内存,备份硬件资源的值,修改硬件资源的值(如将CS:IP指向程序的入口),程序运行完毕返回(恢复加载程序的运行状态)到加载程序中。

12.19

可执行文件的组成

可执行文件由描述信息和程序组成:

  • 描述信息:主要是编译、连接器对源程序中相关伪指令进行处理得到的信息(如汇编程序入口)。
  • 程序:汇编指令和数据。

2016.01.03

CPU 外设

CPU通过接口芯片的端口和外部设备进行联系。

可屏蔽中断过程·简

  1. 取得中断类型码N(内中断的中断类型码来自CPU内部;外中断的中断类型码是()通过数据总线传递给CPU的);
  2. pushf
  3. TF=0, IF=0
  4. push CS
  5. push IP
  6. (IP)=(N*4), (CS)=(N*4+2)[该步完成后,CPU开始执行由程序员编写的中断处理程序]

不可屏蔽中断类型码固定为2,所以不可屏蔽中断过程没有以上中断的第一步。

2016.01.04

直接定址表

可以通过依据数据,直接计算出所要找的元素的位置的表,我们称其为直接定址表。

2016.01.05

硬件完成

由一个硬件操作触发一个(串)高/低电平序列并输入给另一个硬件部件,该硬件部件根据输入的高/低电平序列得到相应的输出并将输出输入给下一个硬件部件……

8086寄存器(内存)功能

寄存器 功能
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以段机制使用内存的方式用所对应的汇编语句编写程序(每个段的大小有编译器-连接器整理)。

2. 检测点

12.16

检测点1.1

(1) 一个CPU的寻址能力为8KB,那么它的地址总线宽度为13
(2) 1KB的存储器有1024个存储单元。存储单元的编号从01023
(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根,则他们的寻址能力分别为64KB1MB16MB4GB
(6) 8080、8088、8086、80286、80386的数据总线宽度分别为8根、8根、16根、16根、32根。则它们一次可以传送的数据为:1B1B2B2B4B
(7) 从内存中读取1024字节的数据,8086至少要读512次,80386至少要读256次。
(8) 在存储器中,数据和程序以二进制(高低电平)形式存储。

检测点2.1

(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次方

检测点2.2

(1) 给定段地址0001H,仅通过变化偏移地址寻址,CPU的寻址范围为00010H1000FH
(2) 有一数据存放在内存20000H单元中,现给定段地址为SA,若想用偏移地址寻到此单元。则SA应满足的条件是:最小为20000H - FFFF 右移4位将移丢的1补在最低位得1001H,最大为2000H

12.17

检测点2.3

下面的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指向下一条指令发生在执行指令之前。

检测点3.1

(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:[地址](其余寄存器)指向内存中的内容当作数据。

检测点3.2

(1) 补全下面的程序,使其可以将10000H ~ 1000FH中的8个字,逆序复制到20000H ~ 2000FH中。逆序复制的含义如图下图所示(图中内存里的数据均为假设)。
读《汇编语言》3E-III[摘 检测点 实验 课程设计]_第1张图片
逆序复制示意图

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。

检测点 6.1

(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处

检测点 9.1

(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。

检测点 9.2

补全编程,利用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          ;读下一字节

检测点 9.3

补全程序,利用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

检测点 10.1

补全程序,实现从内存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

检测点 10.2

下面的程序执行后,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。

检测点 10.3

下面的程序执行后,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)。

检测点 10.4

下面的程序执行后,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)。

检测点 10.5

(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

检测点 11.1

写出下面每条指令执行后,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

检测点 11.2

写出下面每条指令执行后,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 

检测点 11.3

(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

检测点 11.4

下面的程序执行后:(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

检测点 12.1

(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

检测点 13.1

**(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

检测点 13.2

判断下面说法的正误:
(1) 我们可以变成改变FFFF:0处的指令,使得CPU不去执行BIOS中的硬件系统检测和初始化程序。误。
(2) int 19h中断例程,可以由DOS提供。误。

检测点 14.1

(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

检测点 14.2

编程,用加法和移位指令计算(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

检测点 15.1

(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

检测点 16.1

下面的程序将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指定始于该标号的内存单元的大小)。

检测点 16.2

下面的程序将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

检测点 17.1

“在int 16h中断例程中,一定有设置IF=1的指令。”这种说法对吗?
对。(可用debug.exe单步调试验证 - 使用t命令单步进入int 16h中断例程中)

3. 实验

12.17

实验1 查看CPU和内存,用机器指令和汇编指令编程

(1) (调试器)环境准备

平台: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中看见各寄存器的值则表明调试器环境可用。
读《汇编语言》3E-III[摘 检测点 实验 课程设计]_第2张图片

汇编调试器环境准备完毕。

(2) 实验任务

[1] 使用Debug,将下面的程序段(见P.45)写入内存,逐条执行,观察每条指令执行后CPU中相关寄存器中内容的变化。
打算:选择debug的E命令(以机器码的形式)从内存1000:0处开始写入程序段;并分别使用D和U命令查看写入内存中的内容;用r命令修改CS:IP指向1000:0;再用T命令执行指令并观看相关寄存器中内容的变化。
读《汇编语言》3E-III[摘 检测点 实验 课程设计]_第3张图片
在debug中,用E命令输入机器码,用U命令查看输入的机器码

用d命令查看内存中的值
用d命令查看内存中的值(指令) - 皆为高低电平序列

读《汇编语言》3E-III[摘 检测点 实验 课程设计]_第4张图片
在debug中用r命令修改CS:IP的值

让CS:IP指向输入的指令后,就可以用T命令开始执行输入的指令了。
读《汇编语言》3E-III[摘 检测点 实验 课程设计]_第5张图片
用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命令查看是否修改成功。
读《汇编语言》3E-III[摘 检测点 实验 课程设计]_第6张图片
用d命令查看FFF00H ~ FFFFFH内存中的内容,生产日期保存在FFF0:00F0 - FFF0:FF这几个单元中。

再用e命令修改保存生产日期这几个内存单元
读《汇编语言》3E-III[摘 检测点 实验 课程设计]_第7张图片
可见,并没有将这几个内存单元的值修改掉 - 因为在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的内存单元,观察现象。
读《汇编语言》3E-III[摘 检测点 实验 课程设计]_第8张图片
当往B8100H写入61H时,屏幕上靠右位置出现白色的’a’,继续写入62H时,a被蒙上了颜色,写入63H时,紧跟a之后出现白色的’c’。将这几个数据写入2000:0开始的内存单元,除这几个内存单元的值被改变外无其它现象。 - 因为在8086PC机中的内存地址空间分配中,A0000H ~ BFFFFH属于显存地址空间(屏幕将会显示显存中数据所对应的符号)。

12.18

实验2 用机器指令和汇编指令编程

(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中的内容会发生变化。
读《汇编语言》3E-III[摘 检测点 实验 课程设计]_第9张图片
用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的值]

实验3 编程、编译、连接、跟踪

环境准备
平台: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:
读《汇编语言》3E-III[摘 检测点 实验 课程设计]_第10张图片
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行):
读《汇编语言》3E-III[摘 检测点 实验 课程设计]_第11张图片
根据PSP开头两个字节为CS 20的提示可推测这段内容就是PSP的内容 - debug加载程序遵循了DOS加载程序的方式(起码这一点)。

实验 4 [bx]和loop的使用

(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”验证程序的正确性:
读《汇编语言》3E-III[摘 检测点 实验 课程设计]_第12张图片
此图表明填空内容正确。

12.19

实验5 编写、调试具有多个段的程序

(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

实验6 实践课程中的程序

编程,将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

分析:

  • 每个单词占用16个字节。
  • 首个字母在每个单词中的偏移量为4。
  • 在一次循环中将4个单词中具相同偏移量的字母转换为大写字母,用si表每行中字母的偏移(相对于首字母),用bx指向每一行(bx = bx + 16)。
  • 循环四次(cx = 4)。

补充代码段的地址如下:

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年公司雇员人数的21word型数据
data    ends

table   segment
    db 21 dup ('year summ ne ?? ')    ;16字节
table   ends

编程,将data段中的数据按如下格式写入到table段中,并计算21年中的人均收入(取整),结果也按照下面的格式保存在table段中。
读《汇编语言》3E-III[摘 检测点 实验 课程设计]_第13张图片

分析。
可将data段用C语言的结构体来描述如下(以行):

struct  _data {
    int        y[21];         //以1个字节为单位的存储/读取
    int        m[21];        //以4个字节为单位的存储/读取
    short int  pn[21;        //以2个字节微单的存储/读取
}data;

题目的要求时将data中的数据拷贝到table中(除额外的空格和人均收入)。可对应到具体的寻址方式来访问这些元素:

  • data段的段地址分别相当于data;
  • data.y的地址相当于table + 0,以4字节为单位访问到下一个元素 - (data ).[bx + si];si += 4;
  • data.m相当于data + 0 + 84(itada),以4字节为单位访问下一个元素 - (data).[bx+ 84 + si];si += 4;
  • data.pn相当于data + 0 + 84 x 2(itada),以2字节为单位访问下一个元素 - (data).[bx + 84 x 2 + di];di += 2。

以列为单位读取数据写入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”查看所拷贝和计算的内容:
读《汇编语言》3E-III[摘 检测点 实验 课程设计]_第14张图片
通过抽取某一年的数据验证,可表明以上程序正确。

12.22

实验8 分析一个奇怪的程序

分析下面的程序,在运行前思考:这个程序可以正确返回吗?
运行后再思考:为什么是这种结果?
通过这个程序加深对相关内容的理解。

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,所以这个程序能够被正确返回。

实验9 根据材料编程

编程:在屏幕中间分别显示绿色、绿底红色、白底蓝色的字符串’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 ;恢复到第12add si, 2   ;显示下一个字符的缓存

    loop sl

    mov ax, 4c00h
    int 21h
codesg  ends
end start

在DOSBOX中用masm.exe和link.exe编译、连接以上程序得p9.exe,再在DOSBOX中运行p9.exe,显示在DOSBOX中的字符串如下:
读《汇编语言》3E-III[摘 检测点 实验 课程设计]_第15张图片

12.23

实验 10 编写子程序

(1) 显示字符串

问题
显示字符串是现实工作中经常要用到的功能,应该编写一个通用的子程序来实现这个功能。我们应该提供灵活的调用接口,使调用者可以决定显示的位置(行、列)、内容和颜色。

子程序描述
名称: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列显示绿色的字符串

(2) 解决除法溢出的问题

问题
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处,查看返回值:
这里写图片描述

(3) 数值显示

问题
编程,将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

实验 11 编写子程序

编写一个子程序,将包含任意字符,以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

实验 12 编写0号中断的处理程序

编写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返回后系统仍然会执行那条引起故障的指令 并再次引发除法中断,从而无休无止。

实验 13 编写、应用中断例程

(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。执行该段程序的结果如下:
读《汇编语言》3E-III[摘 检测点 实验 课程设计]_第16张图片
运行结果

用debug.exe在DOSBOX中跟踪运行test_pd1.exe:

int指令执行前

读《汇编语言》3E-III[摘 检测点 实验 课程设计]_第17张图片
int指令执行后


iret指令执行前


iret指令执行后

12.31
(2) 编写并安装int 7ch中断例程,功能为完成loop指令的功能。
参数:(cx)=循环次数,(bx)=位移。

编写并安装int 7ch中断例程。
分析:
读《汇编语言》3E-III[摘 检测点 实验 课程设计]_第18张图片

代码(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中断例程是否编写、安装正确。
读《汇编语言》3E-III[摘 检测点 实验 课程设计]_第19张图片
在屏幕中间显示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:

实验 14 访问CMORS RAM

编程,以“年/月/日 时:分:秒”的格式,显示当前的日期、时间。
注意: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得以下结果:
读《汇编语言》3E-III[摘 检测点 实验 课程设计]_第20张图片

2016.01.04

实验 15 安装新的int 9 中断例程

安装一个新的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中断处理程序中应该完成以下工作:

  • 读键的扫描码。
  • 调用原来的int 9中断处理程序处理键盘输入[在安装int 9中断处理程序时需要备份原int 9中断处理程序的地址]。
  • 判断键盘的扫描码是否为A的断码。若为A的断码,则执行满屏A的代码,否则返回。

汇编程序模块大概如下:

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中的现象如下所示:
读《汇编语言》3E-III[摘 检测点 实验 课程设计]_第21张图片
新安装int 9中断处理程序后,按下’A’键送开后DOSBOX全屏’A’。

2016.01.05

实验 16 编写包含多个功能子程序的中断例程

安装一个新的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表示向上滚动一行;
    ;   对于12号功能,用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运行后无现象):
读《汇编语言》3E-III[摘 检测点 实验 课程设计]_第22张图片
运行test_ph1.exe(设置前景色)


运行test_ph2.exe(设置背景色)

读《汇编语言》3E-III[摘 检测点 实验 课程设计]_第23张图片
运行test_ph3.exe(向上滚动一行)


运行test_ph0.exe(清屏)

注:最后一行为运行完相应的可执行程序后DOS向上滚动一行后给出的提示符

4 课程设计

12.24

课程设计 1

任务:将实验7中的Power idea公司的数据按照下图所示的格式在屏幕上显示出来。
读《汇编语言》3E-III[摘 检测点 实验 课程设计]_第24张图片

在这个程序中,要用到我们前面学到的几乎所有的知识,注意选择适当的寻址方式和相关子程序设计和应用。另外,要注意,因为程序要显示的数据有些已经大于65535,应该编写一个新的数据到字符串转换的子程序,完成dword型数据到字符串的转换,说明如下。
名称:dtoc
功能:将dword型数据转变为表示十进制数的字符串,字符串以0为结尾符。
参数:(ax)=dword型数据的低16位,(dx)dword型数据的高16位,ds:si指向字符串的首地址
返回:无

在这个子程序中要注意除法的移除的问题,可以用在实验10中涉及的子程序divdw来解决。

(1) 实现功能

[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得以下结果:
读《汇编语言》3E-III[摘 检测点 实验 课程设计]_第25张图片

12.25
[2] show_str子程序中的BUG
在循环中调用show_str子程序显示字符串时,发现其中包含错误。

  • DOSBOX中第一行开始处对应的显存地址为B800:A0。
  • 在根据参数dh, dl计算行、列对应的显存地址时未处理dh=dl=0的情况。

在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能解决除法溢出的特性,所以将二者用于课程设计中:

  • 将4字节的年份(字符串)读入ds:si指向的数据段中,在末尾添0,然后调用show_str子程序显示年份;
  • 对于4字节的收入,将高2字节读入dx,低2字节读入ax,再调用dtoc将读到dx,ax中的收入转换为ds:si指向的字符串,再调用show_str子程序显示;对于2字节的员工数,将dx=0,将员工数读入ax中,再调用dtoc将读到dx,ax中的收入转换为ds:si指向的字符串,再调用show_str子程序显示。
  • 调用divdw来计算员工的平均收入,然后调用dtoc将平均收入转换为ds:si指向的字符串,再用show_str显示。

显示公司年份:

    ...
;----显示年代----
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得以下结果:
读《汇编语言》3E-III[摘 检测点 实验 课程设计]_第26张图片
结果显示出来了,但因为原背景内容的存在,使得界面看起来不清晰。在开始显示结果之前做清屏操作即可解决该问题。

清屏

  ;名称: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)=收入低16push 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行代码。过几天再看代码或者再添加代码(后期代码编写已有后续感觉)有些凌乱感觉。对以上程序有必要进行“整理”。

(2) 整理程序

[0] 尽量纠正掉程序中的错误。
[1] 尽量去掉程序中没必要的代码。
[2] 尽量将程序模块化。

[1] 子(清屏)程序整理

功能:根据指定背景色(ah)和前景色(al)清屏[dl, bl] ~ [dh, bh]区域
参数:(dl) = 清屏起始行,(dh) = 清屏结束行,(bl) = 清屏起始列,(bh) = 清屏结束列;(al) = 清屏格式(前、背景色)
返回:无
函数体内容:

  • 对在子程序中使用的寄存器进行入栈备份;
  • 由[dl, bl]计算对应的显存(起始)地址;由(dh - dl + 1)和(bh - bl + 1)得清屏行、列数(纠正了原清屏子程序screen中的错误
  • 在循环中用指定的前、背景色清屏;
  • 出栈经备份的寄存器。

经整理后的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

[2] 程序模块化

所有的子程序以及调用子程序的代码都被写在了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)=收入低16push 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  ;商高16pop 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,运行结果跟之前一样。

5 综合研究

去年寒假时按照书中的步骤用的tc平台进行的实验(见“[Hb-XVII] 计算机的抽象层次-简 使用寄存器 使用内存空间 程序执行过程 使用main函数规定 不定参数函数机制 C”)。不知今年7月有无机会另用(Linux平台下的)gcc编译器来完成综合研究实验。

  • 读《汇编语言》第三版3的所有汇编练习笔记代码保存地址:r_hb_src。
  • 计划:“实验 17”、“课程设计 2”和“综合研究(用gcc)”可能会留到今年7月份完成[前两部分内容会结合《30天自制操作系统》一书完成(若到时候软盘还没坏)]。
  • 练习中的“代码”、“答案”以及“操作”,难免有错误或不完美性,有待进一步提升。
    [2015.12.16]

你可能感兴趣的:(汇编语言)