汇编网首页登录博客注册
unixandlinux的学习博客
博客首页博客互动【做检测题】论坛求助

我的博客

个人首页 |  我的文章 |  我的相册 |  我的好友 |  最新访客 |  文章收藏 |  论坛提问 |  友情链接 |  给我留言  
图片载入中
学习动态
最新评论
最新留言
好友圈
友情链接

[2023-04-21 18:24] 《汇编语言》(王爽 著) —— 学习笔记(第4章)

第 4 章 第一个程序

前述章节都是在 Debug 中编写和运行写指令,本章开始学习编写完整的汇编语言程序,用编译和连接程序将汇编源程序编译连接成可执行文件(如 *.exe 文件),在操作系统中运行。

4.1 一个源程序从写出到执行的过程

第一步:编写汇编源程序
通过文本编辑器(Edit、记事本等),用汇编语言编写汇编源程序,形成一个存储源程序的文本文件。

第二步:对源程序进行编译连接
用汇编语言编译程序编译源程序,生成目标文件;再用连接程序对目标文件进行连接,生成可在操作系统中直接运行的可执行文件。
可执行文件包含两部分内容:
1. 程序(从源程序中的汇编指令翻译过来的机器码)和数据(源程序中定义的数据);
2. 相关的描述信息(比如,程序有多大、要占用多少内存空间等)。

第三步:执行可执行文件
在操作系统中,执行可执行文件。
操作系统依照可执行文件中的描述信息,将可执行文件中的机器码和数据加载入内存,并进行相关的初始化(比如设置 CS:IP 指向第一条要执行的指令),然后由 CPU 执行。

4.2 源程序

以下面一段汇编语言源程序为例:
assume cs:codesg
codesg segment
        mov ax, 0123H
        mov bx, 0456H
        add ax, bx
        add ax, ax

        mov ax, 4c00H
        int 21H

codesg ends
end

1. 伪指令
汇编语言源程序包含两种指令:汇编指令和伪指令。汇编指令有对应的机器码,可被编译为机器指令,最终为 CPU 所执行。伪指令没有对应的机器码,在最终生成的可执行文件中不会存在由伪指令产生而可被 CPU 执行的成分;伪指令由编译器执行 —— 编译器根据伪指令来进行相关的编译操作。
(1) segment 和 ends
这是成对使用的伪指令,在写可被编译器编译的汇编程序时必须用到它们。其功能是定义一个段,segment 说明一个段开始,ends 说明一个段结束。
段必须有一个名称作为标识,格式如下:
段名 segment
...
段名 ends
一个汇编程序由多个段组成。段被用来存放代码、数据或当作栈空间来使用。前面章节所学的段概念,在汇编源程序中得到应用与体现。程序中所有将被计算机处理的信息(指令、数据、栈),被划分到不同的段中。
一个有意义的汇编程序至少包含一个段,这个段用来存放代码。
(2) end
end 是一个汇编程序的结束标记。编译器在编译时,如果碰到伪指令 end,就结束编译。因此,程序写完就要在结束处添加伪指令 end,否则,编译时编译器无法知道程序在何处结束。
注意不要混淆 end 和前面的 ends;ends 与 segment 是成对出现的,其含义可理解为“end segment” —— ends 末尾的“s”可看作是 segment 的首字符。
(3) assume
伪指令 assume 的含义是“假设”。它假设某一段寄存器和程序中的某一个用 segment ... ends 定义的段相关联 —— 在有需要的情况下,编译程序可以将段寄存器和某一个具体的段相联系。也就是说,assume 将有特定用途的段和相关的段寄存器关联起来。其格式如下:
assume 寄存器名:段名

2. 源程序中的“程序”
源程序文件中的所有内容称为源程序。源程序中最终由计算机执行、处理的指令或数据,称为程序。程序先以汇编指令的形式存在于源程序中,后经编译、连接后转变为机器码,存储在可执行文件中。
学到下面第 5 点“程序返回”时,对“程序”概念可有更深入的理解。

3. 标号
除汇编指令和伪指令外,汇编源程序中还有标号,例如示例中的“codesg”。一个标号指代一个地址。作为段名称,最终将被编译、连接程序处理为一个段的段地址,于是段名称就成为一个标号。

4. 程序的结构
以下通过编写一个实现 2³ 运算的汇编源程序来了解汇编程序的结构。
(1) 定义一个名为 abc 的段
abc segment
...
abc ends
(2) 在 abc 段中写入实现 2³ 运算的汇编指令
abc segment
        mov ax, 2
        add ax, ax
        add ax, ax
abc ends
此处的汇编指令可参考第 2 章的检测点 2.1 第 2 题。
(3) 标记程序结束位置
abc segment
        mov ax, 2
        add ax, ax
        add ax, ax
abc ends
end
(4) 如需将段名 abc 当作代码段使用,则应该将 abc 和 CS 寄存器相联系
assume cs:abc
abc segment
        mov ax, 2
        add ax, ax
        add ax, ax
abc ends
end

5. 程序返回
在上面第 2 点“源程序中的‘程序’”中讲到,“程序先以汇编指令的形式存在于源程序中,后经编译、连接后转变为机器码,存储在可执行文件中”,那么如何运行一个存储在可执行文件中的程序呢?
一个存储在可执行文件中的程序,其代码必须先被加载到内存才能被 CPU 所执行,而执行加载操作的,只是一个正在运行中的程序。也就是说,正在被 CPU 运行的程序 P1 将程序 P2 的代码加载到内存,再由 CPU 来执行 P2 程序的代码。
一个程序正在被 CPU 运行,就说该程序拥有 CPU 的控制权 —— 它可以通过自身的代码来控制 CPU 执行怎样的操作。因此当 CPU 的控制权由 P1 程序交到 P2 程序手中时,P2 就开始运行,而 P1 则暂停运行。当 P2 运行完毕,CPU 的控制权应该交还给使程序 P2 得以运行的程序 P1,而此后 P1 继续运行。
一个程序结束后,将 CPU 的控制权交还给使得它得以运行的程序,这个过程称为程序返回。程序返回也是一种需要 CPU 执行的操作,因此必须通过拥有 CPU 控制权的程序代码来实现。因此需要在拥有 CPU 控制权的程序的末尾添加程序返回的程序段。
在本节开头的示例代码末尾的两条指令:
mov ax, 4c00H
int 21H
这两条指令所实现的功能就是程序返回。在目前阶段的学习中,只需要知道这两条指令可以实现程序返回,无需对其含义作更深入理解。
有几个和结束相关概念,如“段结束”、“程序结束”、“程序返回”,它们的区别如下表所示:

目的                 相关指令  指令性质  指令执行者
通知编译器一个段结束 段名 ends  伪指令 编译时,由编译器执行
通知编译器程序结束    end       伪指令 编译时,由编译器执行
程序返回"mov ax, 4c00H”、“int 21H”汇编指令 运行程序时,由 CPU 执行

6. 语法错误和逻辑错误
程序如果没有编写程序返回功能的代码,在编译阶段是不会显现出来的,因为程序返回是在运行时由 CPU 执行的操作,而不是编译器编译时执行的操作。
一般地,程序在编译时被编译器发现的错误是语法错误。运行程序时发生的错误是逻辑错误。语法错误易发现也易纠正,而逻辑错误通常不容易发现和改正。

4.3 编辑源程序

可以用任意的文本编辑器来编辑源程序,只要最终将其存储为纯文本文件即可。

4.4 编译

本课程采用微软 masm5.0 汇编编译器,文件名为 masm.exe。假设汇编编译器在 c:\masm 目录下。
编译操作如下:
1. 进入 DOS 方式,进入 c:\masm 目录,运行 masm.exe。
提示输入源程序文件名,假设文件名为 1.asm。
Source filename [.ASM]:1.asm
默认的文件扩展名是 asm,比如,要编译的源程序文件名是 pl.asm,只要输入 pl 即可。如果源程序文件不是以 asm 为扩展名,就要输入它的全名。比如源程序文件名为 pl.txt,就要输入全名 pl.txt。
在输入源程序文件名时一定要指明它所在的路径。如果文件在当前路径下,只输入文件名即可,如果文件在其他目录,则要输入路径。
2. 输入要编译的源程序文件名后,按 Enter 键。
继续提示输入编译生成目标文件的名称:
Object filename [1.0BJ]:
因为前面输入了文件名 1.asm,所以此处显示默认生成的目标文件名为 1.OBJ。若需使用其他名称,则需另行指定文件名。如果直接按 Enter 键,将在当前目录下生成 1.obj 目标文件。
可以指定生成的目标文件所在目录,比如想让编译程序在“c:\windows\desktop” 下生成目标文件 1.obj,
则可输入“c:\windows\desktop\1”。
3. 选择是否生成中间文件 —— 列表文件。
确定目标文件名称后,提示输入列表文件的名称:
Source listing [NUL.LST]:
这是编译器编译过程中产生的中间结果。若不想生成这个文件,直接按 Enter 键即可。
4. 选择是否生成中间文件 —— 交叉引用文件。
提示输入交叉引用文件的名称:
Cross-reference [NUL.CRF]:
与列表文件一样,这是编译器编译过程中产生的中间结果。若不想生成这个文件,直接按 Enter 键即可。
5. 源程序的编译结束,屏幕显示编译结果。

按上述过程编译后,在编译器 masm.exe 运行的目录将出现一个新的文件 1.obj,这就是编译所得的结果。
如果编译过程中出现错误,就不会生成该目标文件。一般地,有两类错误都将无法生成目标文件:
(1) 程序中有“Severe Errors”。
(2) 找不到源程序文件。
注意,编译过程中需提供源程序文件,可以得到 3 个输出,即目标文件(.obj)、列表文件(.lst)、交叉引用文件(.crf),这 3 个输出文件中,目标文件是最终要得到的结果,另外两个只是中间结果,可以让编译器忽略对它们的生成。

4.5 连接

在对源程序进行编译得到目标文件后,就需要对目标文件进行连接,从而得到可执行文件。上一节中已经对 c:\1.asm 进行编译得到 c:\masm\1.obj,现在再将 c:\masm\1.obj 连接为 c:\masm\1.exe。
采用微软的 Overlay Linker3.60 连接器,文件名为 link.exe。假设连接器在 c:\masm 目录下。
连接操作如下:
1. 进入 DOS 方式,进入 c:\masm 目录,运行 link.exe。
提示输入需要被连接的目标文件名称:
Object Modules [.OBJ]:
默认文件扩展名为 obj,比如要连接的目标文件名是“p1.obj”,只要输入“p1” 即可。如果文件扩展名不是 obj,就要输入它的全名。比如目标文件名为“p1.bin”,就要输入全名。
在输入目标文件名时,要注意指明它所在的路径。如果文件在当前目录下,则可省略路径名。
2. 输入要连接的目标文件名后,按 Enter 键。
继续提示输入要生成的可执行文件名称:
Run File [1.EXE]:
因为前面输入目标文件名为 1.obj,则默认要输出的可执行文件名为 1.EXE,可以不必再另行指定文件名。直接按 Enter 键,连接程序将在当前目录下生成 1.EXE 文件。
也可以指定生成的可执行文件所在目录。若想在“c:\windows\desktop”下生成可执行文件 1.EXE,则可输入
“c:\windows\desktop\1”。
3. 选择是否生成中间文件 —— 映像文件。
提示输入映像文件的名称:
List File [NUL.MAP]:
这是连接过程中产生的中间结果,若不想生成该文件,直接按 Enter 键即可。
4. 要求输入程序中用到的库文件。
提示输入库文件的名称:
Libraries [.LIB]:
如果程序中调用了某个库文件中的子程序,就需要将库文件和目标文件连接,以生成可执行文件。如果没有调用任何子程序,此处则忽略库文件名的输入,直接按 Enter 键即可。
5. 源程序的连接结束,屏幕显示连接的结果。

按上述过程进行连接后,在连接器 link.exe 运行的目录(即当前路径)生成一个新文件 1.exe,这就是对目标文件 1.obj 进行连接所得的结果。连接过程中如果出现错误,将得不到可执行文件。
连接的作用有以下几个:
(1) 当源程序很大时,可将它分为多个源程序文件来编译,每个源程序编译成为目标文件后,再用连接程序将它们连接到一起,生成一个可执行文件。
(2) 程序调用了库文件中的子程序,就要将此库文件和该程序生成的目标文件相连接,以生成一个可执行文件。
(3) 源程序编译生成存储着机器码的目标文件,目标文件中有些内容还不能直接用来生成可执行文件,连接程序将这些内容处理为最终的可执行信息。所以在只有一个源程序文件,而又不需要调用某个库中的子程序情况下,也仍然必须用连接程序对目标文件进行处理,以生成可执行文件。
再次强调,学习汇编的主要目的,就是通过用汇编语言编程,从而深入地理解计算机底层的基本工作机理,达到可以随心所欲地控制计算机的目的。基于此,本书的编程大都直接对硬件进行。直接对硬件编程,又要避开机器码,所以就要使用汇编语言。这就要借助编程工具:编辑器(Edit)、编译器(masm)、连接器(link)以及调试工具(Debug)等。这些都是运行在操作系统之上的程序,所以学习过程必须在操作系统的环境中进行。

4.6 以简化的方式进行编译和连接

通过前两节学习使用 masm 和 link 进行编译和连接,可以知道,编译、连接的最终目的是经由源程序文件以生成可执行文件,在这个过程中所产生的中间文件都可以忽略。可以用一种较为简捷的方式进行编译、连接,即在命令行中输入 masm(link),其后是路径名、文件名,末尾再加上一个分号,按 Enter 键执行。例如:
masm c:\1;
按 Enter 键后,编译器 masm 就对 c:\1.asm 进行编译,在当前路径下生成目标文件 1.obj,并在编译过程中自动忽略中间文件的生成。
link 1;
按 Enter 键后,link 就对当前路径下的 1.obj 进行处理,在当前路径下生成可执行文件 1.exe,并自动忽略中间文件的生成。

4.7 1.exe 的执行

经过编译、连接后,由源程序文件 1.asm 生成了一个可在操作系统下执行的程序文件 1.exe。执行该文件的方法是在文件的所在目录内输入文件名,按 Enter 键即可。
Windows(DOS)操作系统对于命令的搜索路径,已经设置在环境变量 PATH 中。对于输入的命令名,首先是从输入该命令的当前路径下搜索,如果找到就执行之,如果没找到,就到环境变量 PATH 所指定的路径内搜索。

4.8 谁将可执行文件中的程序装载进入内存并使它运行?

在 DOS 中,要运行程序 P1,必须有一个正在运行的程序 P2 将 P1 从其可执行文件中加载入内存,把 CPU 的控制权交给 P1,P1 才得以运行。当 P1 运行完毕后,应该将 CPU 的控制权交还给使它得以运行的程序 P2。整个过程如下:
(1) 在 DOS 中直接执行 1.exe 时,是正在 DOS 下运行的 command,将 1.exe 中的程序加载入内存。
(2) command 设置 CPU 的 CS:IP 指向程序的第一条指令(即程序的入口),从而使程序得以运行。
(3) 程序运行结束后,返回到 command 中,CPU 继续运行 command。

                                                        操作系统的外壳

操作系统是由多个功能模块组成的庞大、复杂的软件系统。任何通用的操作系统,都要提供一个称为 shell(外壳)的程序,用户(操作人员)使用这个程序来操作计算机系统进行工作。
DOS 中有一个程序 command.com,又称命令解释器,也就是 DOS 系统的 shell。
DOS 启动时,先完成其他重要的初始化工作,然后运行 command.com,command.com 运行后,执行完其他相关任务后,就在屏幕上显示由当前盘符和当前路径组成的提示符,比如“c:\”或“c:\windows”等,称为命令提示符,在命令提示符下等待用户的输入。
用户可以输入所要执行的命令,比如,cd、dir、type 等,这些命令由 command 执行,command 执行完这些命令后,再次显示命令提示符,等待用户的输入。
如果用户要执行一个程序,则输入该程序的可执行文件名称,command 首先根据文件名找到可执行文件,然后将这个可执行文件中的程序加载入内存,设置 CS:IP 指向程序入口。此后,command 暂停运行,CPU 运行程序。程序运行结束后返回到 command 中,command 再次显示由命令提示符,等待用户的输入。
在 DOS 中,command 处理各种输入:命令或要执行的程序的文件名。
命令行方式下,用户(操作人员)就是通过 command 来进行工作的。

汇编程序从使用编辑器编写到由 CPU 执行的全过程:
 编程 -> 1.asm -> 编译 -> 1.obj -> 连接 -> 1.exe -> 加载 -> 内存中的程序 -> 运行
(Edit)                         (masm)                          (link)                 (command)                                   (CPU)

4.9 程序执行过程的跟踪

在 DOS 中运行一个程序,是由 command 将程序从可执行文件中加载入内存,并使其得以执行,但这样就无法逐条指令地看到程序的执行过程;因为 command 执行加载程序、设置 CS:IP 指向程序入口等操作是连续完成的,CS:IP 一旦指向程序入口,command 就放弃了对 CPU 的控制权,CPU 立即开始运行程序,直至程序结束。
为观察程序的运行过程,可以使用 Debug。Debug 将程序加载入内存,设置 CS:IP 指向程序的入口,但并不放弃对 CPU 的控制,这样即可使用 Debug 的命令来单步执行程序,以查看每一条指令的执行结果。
在命令提示符下输入“debug 1.exe”(假设需要执行的程序在可执行文件 1.exe 中),按 Enter 键,Debug 将程序从 1.exe 加载入内存,经相关初始化后设置 CS:IP 指向程序的入口,之后便进入 Debug 运行界面了。
执行 R 命令可以看到,CX 寄存器存放着程序的长度值。1.exe 程序的机器码共有 15 个字节,因此 CX 中的内容为 000FH。
在 DOS 系统中,.EXE 文件程序的加载过程如下:
1. 找到一段起始地址为 SA:0000(即起始地址的偏移地址为 0)的容量足够的空闲内存区。
2. 在这段内存区的前 256 个字节中,创建一个称为程序段前缀(PSP)的数据区,DOS 要利用 PSP 来和被加载程序进行通信。(PSP 的作用涉及 DOS 的原理,无需深入了解,只需知道存在这样的数据区即可)
3. 从这段内存区的 256 字节处开始(在 PSP 的后面)将程序装入,程序的地址被设为 SA+10H:0。
空闲内存区从 SA:0 开始,0~255 字节为 PSP,从 256 字节处开始存放程序,为更好地区分 PSP 和程序,DOS 一般将它们划分到不同的段中,所以,有了如下所示的地址安排:
(1) 空闲内存区:SA:0
(2) PSP 区:SA:0
(3) 程序区:SA+10H:0
注意:PSP 区和程序区虽然物理地址连续,却有不同的段地址。举例如下:
假设 SA 为 1000H,则 PSP 区起始物理地址是 10000H,PSP 区大小是 256 个字节,即 256 个内存单元,其偏移地址范围是 0000H~00FFH,也就是说,PSP 区的地址范围是 1000:0000~1000:00FF,因此其终止物理地址是 100FFH。可见从 10100H 地址开始即属于程序区,所以程序区的起始地址为 10100H,其段地址即为 1010H,也就是 1000H+10H=SA+10H。
4. 将该内存区的段地址(SA)存入 DS 寄存器中,初始化其它相关寄存器后,设置 CS:IP 指向程序的入口。
由于 CS 寄存器存储着程序区的入口段地址,因此 CS = SA + 10H = DS + 10H。
在上述 .EXE 程序加载过程中,关于重定位的操作未予讲述,因为它与操作系统关系较大,不作讨论。

注意,源程序中的指令“mov ax,0123H”,在 Debug 中记为“mov ax,0123”,这是因为 Debug 默认所有数值都用十六进制表示 —— 将所有数值都读作一个十六进制数。因此如果在 Debug 中书写指令为“mov ax,0123H”,则 Debug 会将数值 0123H 中的字符 H 认作一个十六进制数,但十六进制数字中不存在 H,该指令就成为非法指令而报错。
通过 Debug 的 U 命令可以查看所有汇编指令,用 T 命令单步执行程序中的每一条指令,即可观察程序的运行过程。但注意,执行到最后一条指令“int 21”时,要使用 P 命令来执行这条指令 —— 只需记住此操作,无需究查其原因。
由于是 Debug 加载程序入内存的,所以程序运行结束后要返回到 Debug 中;而 Debug 是由 command 加载运行的,所以用 Q 命令退出 Debug 后将返回 command。

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

(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
        编译命令:
masm t1;
        连接命令:
link t1;

(2) 用 Debug 跟踪 t1.exe 的执行过程,写出每一步执行后,相关寄存器中的内容和栈顶的内容。
在 command 中用 Debug 加载 t1.exe:
debug t1.exe
进入 Debug 界面,执行 U 命令查看程序的汇编指令代码:
-u
076E:0000 B80020        MOV AX,2000H
076E:0003 8ED0                MOV SS,AX
076E:0005 BC0000        MOV SP,0
076E:0008 83C40A        ADD SP,+0A
076E:000B 58                POP AX
076E:000C 5B                POP BX
076E:000D 50                PUSH AX
076E:000E 53                PUSH BX
076E:000F 58                POP AX
076E:0010 5B                POP BX
076E:0011 B8004C        MOV AX,4C00H
0763:0014 CD21                INT 21H
用 R 命令查看各个 CPU 寄存器的情况:
-r
AX=FFFF BX=0000 CX=0016 DX=0000 SP=0000 BP=0000 SI=0000 DI=0000
DS=075E ES=075E SS=076D CS=076E IP=0000 NV UP EI PL NZ NA PO NC
076E:0000 B80020        MOV AX,2000
用 T 命令执行指令,查看各个寄存器的内容和栈顶内容:
-t
AX=2000 ...                        SP=0000
DS=075E ..                        IP=0003
076E:0003 8ED0                MOV SS,AX
-d 2000:0 10
2000:0000 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00
2000:0010 00
执行 mov ax,2000H 指令后,将 2000H 传送到 AX 寄存器,再用 D 命令查看将要成为栈空间的内存区域。继续用 T 命令执行指令并用 D 命令查看:
-t
AX=2000 ...                                SP=0000
DS=075E ...        SS=2000 ... IP=0008
076E:0008 83C40A        ADD SP,+0A
-d 2000:0 10
2000:0000 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00
2000:0010 00
执行 mov ss,ax 指令后,将 AX 寄存器所存储的数据 2000H 传送到 SS 寄存器中,这时自动继续执行下一条指令 mov sp,0,将数据 0H 传送到 SP 寄存器。再用 D 命令查看将要成为栈空间的内存区域,发现栈空间存储的数据并没有变化。继续用 T 命令执行指令并用 D 命令查看:
-t
AX=2000 ...        SP=000A
DS=075E ...        SS=2000 ... IP=000B
076E:000B 58        POP AX
-d 2000:0 10
2000:0000 00 20 00 00 0B 00 6E 07-A5 01 00 00 00 00 00 00
2000:0010 00
执行 mov sp,10 指令后,将十进制数据 10(AH)传送到 SP 寄存器中,再用 D 命令查看将要成为栈空间的内存区域,发现栈空间存储的数据已经发生变化 —— 栈顶标识值数据产生了,并存储在 SP-1 地址空间。继续用 T 命令执行指令并用 D 命令查看:
-t
AX=0000 ...        SP=000C
DS=075E ...        SS=2000 ...        IP=000C
076E:000C 5B        POP BX
-d 2000:0 10
2000:0000 00 20 00 00 00 00 0C 00-6E 07 A5 01 00 00 00 00
2000:0010 00
执行 pop ax 指令后,将栈顶元素数据 0 传送到 AX 寄存器中,再用 D 命令查看将要成为栈空间的内存区域,发现栈顶元素越界 2 个字节。继续用 T 命令执行指令并用 D 命令查看:
-t
AX=0000 BX=0000 ...                SP=000E
DS=075E ...        SS=2000 ...        IP=000D
076E:000D 50        PUSH AX
-d 2000:0 10
2000:0000 00 20 00 00 00 00 00 00-0D 00 6E 07 A5 01 00 00
2000:0010 00
执行 pop bx 指令后,将栈顶元素数据 0 传送到 BX 寄存器中,再用 D 命令查看将要成为栈空间的内存区域,发现栈顶元素越界 4 个字节。继续用 T 命令执行指令并用 D 命令查看:
-t
AX=0000 BX=0000 ...                SP=000C
DS=075E ...        SS=2000 ...        IP=000E
076E:000E 53        PUSH BX
-d 2000:0 10
2000:0000 00 20 00 00 00 00 0E 00-6E 07 A5 01 00 00 00 00
2000:0010 00
执行 push ax 指令后,将寄存器 AX 中存储的数据 0 传送到栈顶元素字存储单元中,再用 D 命令查看将要成为栈空间的内存区,栈顶元素仍然越界 2 个字节。继续用 T 命令执行指令并用 D 命令查看:
-t
AX=0000 BX=0000 ...                SP=000A
DS=075E ...        SS=2000 ...        IP=000F
076E:000F 58        POP AX
-d 2000:0 10
2000:0000 00 00 00 00 0F 00 6E 07-A5 01 00 00 00 00 00 00
2000:0010 00
执行 push bx 指令后,将寄存器 BX 中存储的数据 0 传送到栈顶元素字存储单元中,再用 D 命令查看将要成为栈空间的内存区,栈顶元素恢复到设计位置。继续用 T 命令执行指令并用 D 命令查看:
-t
AX=0000 ...                                SP=000C
DS=075E ...        SS=2000 ...        IP=0010
076E:0010 5B        POP BX
-d 2000:0 10
2000:0000 00 00 00 00 00 00 10 00-6E 07 A5 01 00 00 00 00
2000:0010 00
执行 pop ax 指令后,将栈顶元素数据 0 传送到 AX 寄存器中,再用 D 命令查看将要成为栈空间的内存区域,发现栈顶元素越界 2 个字节。继续用 T 命令执行指令并用 D 命令查看:
-t
AX=0000 BX=0000 ...                SP=000E
DS=075E ...        SS=2000 ...        IP=0011
076E:0011 B8004C        MOV AX,4c00
-d 2000:0 10
2000:0000 00 00 00 00 00 00 00 00-11 00 6E 07 A5 01 00 00
2000:0010 00
执行 pop bx 指令后,将栈顶元素数据 0 传送到 BX 寄存器中,再用 D 命令查看将要成为栈空间的内存区域,发现栈顶元素越界 4 个字节。继续用 T 命令执行指令并用 D 命令查看:
-t
AX=4C00 BX=0000 ...                SP=000E
DS=075E ...        SS=2000 ...        IP=0014
076E:0014 CD21                INT 21
-d 2000:0 10
2000:0000 00 00 00 00 00 00 00 00-11 00 6E 07 A5 01 00 00
2000:0010 00
执行 mov ax,4c00H,将数据 4c00H 传送到 AX 寄存器。栈顶空间数据没有变化。
下一条指令是 int 21H,是本程序结束返回主调程序的命令,所以用 P 命令执行该指令:
-p

Program terminated normally
-d 2000:0 10
2000:0000 00 00 00 00 00 4C 00 00-16 00 6E 07 06 72 00 00
2000:0010 00
-r
AX=4C00 BX=00 CX=0016 DX=0000 SP=000E BP=0000 SI=0000 DI=0000
DS=075E ES=075E SS=2000 CS=076E IP=0014 NV UP EI PL NZ NA PE NC
076E:0014 CD21                INT 21
执行 int 21H 指令,显示“Program terminated normally”信息,返回 Debug,程序正常结束 —— 参见 4.9 节 p93 页所述。

(3) PSP 的头两个字节是 CD20,用 Debug 加载 t1.exe,查看 PSP 的内容。
PSP 区的段地址存储在 DS 寄存器中,参照 (2) 的运行数据可知,DS=075EH。PSP 区占用了 256 个字节,所以 PSP 区的地址范围是 075E:0000~075E:00FF,因此查看 PSP 区的内容可以使用 Debug 的 D 命令:
-d 075e:0 ff
075E:0000 CD 20 FF ... 8A 03
075E:0010 A5 01 17 ...
075E:0020 ...
....
075E:00E0 00 00 ...
075E:00F0 00 00 ...
再用 U 命令查看:
-u 075e:0 f
075E:0000 CD20                INT 20
075E:0002 FF9F00EA        CALL FAR [BX+EA00]
075E:0006 FFFF                ???        DI
...
可以看到,PSP 区起始处两个字节存储的数据是 CD20,对应的汇编指令是“int 20H”,可见程序返回指令“int 21H”(4.2 节第 5 点“程序返回”)正是它的回应。
评论次数(0)  |  浏览次数(142)  |  类型(学习笔记) |  收藏此文  | 
 
 请输入验证码  (提示:点击验证码输入框,以获取验证码