§ 第4章 第一个程序 - 图2

为了能够彻底理解一个完整的程序,我们将经历一个漫长的过程。

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

§ 第4章 第一个程序 - 图3

1.1 编写汇编源程序

使用文本编辑器用汇编语言编写汇编源程序,产生了一个存储源程序的文本文件

1.2 对源程序进行编译连接

  1. 是用汇编语言编译程序对源程序文件中的源程序进行编译,产生目标文件;
  2. 再用连接程序对目标文件进行连接,生成可在操作系统中直接运行的可执行文件

可执行程序包含两部分内容

§ 第4章 第一个程序 - 图4

这一步工作的结果:产生了一个可在操作系统中运行的可执行文件。

1.3 执行可执行文件中的程序

早操作系统中,执行可执行文件中的程序

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

二、源程序

下面通过一段源程序来学习汇编语言

  1. assume cs:codesg
  2. codesg segment
  3. mov ax,0123H
  4. mov bx,0456H
  5. add ax,bx
  6. add ax,ax
  7. mov ax,4c00H
  8. int 21H
  9. codesg ends
  10. end

2.1 伪指令

在汇编语言源程序中,包含两种指令

  • 汇编指令
  • 伪指令

汇编指令有对应的机器码的指令(之前看到的mov对应B8),可以被编译为机器指令,最终为CPU所执行。而伪指令没有对应的机器指令,最终不被CPU所执行。那么谁来执行伪指令呢?伪指令是由编译器来执行的指令,编译器根据伪指令来进行相关的编译工作

在上面的程序中,出现了3种伪指令

§ 第4章 第一个程序 - 图5

2.1.1 伪指令segment .. end

  1. ; 下面定义了一个段,XXX是段名
  2. XXX segment
  3. ; 段中的内容
  4. XXX ends

segmentends是一对成对使用的伪指令,这是在写可被编译器编译的汇编程序时,必须要用到的一对伪指令。segmentends的功能是定义一个段,segment说明一个段开始,ends表示段的结束,其中XXX就是这段的名字,向上面的例子中短命就是codesegment

一个汇编程序是由多个段组成的,这些段被用来存放代码、数据或当作栈空间来使用。我们之前课程中学的段在汇编源程序中充分体现出来,一个程序中将所有被计算机所处理的信息:指令、数据和栈被划分到了不同的段中,一个汇编程序中必须有至少一个段

2.1.2 伪指令end

end是一个汇编程序的结束标记,编译器在编译汇编程序的过程中,如果碰到了伪指令end就结束对源程序的编译。所以,在写完程序的时候,需要在结尾处加上伪指令end

2.1.3 伪指令assume

这条伪指令的含义是:假设某一段寄存器和程序中的某一个segment ... ends定义的段相关联。

上面的源程序中,就是将代码段codesg和CS寄存器关联起来

  1. assume cs:codesg

2.2 将源程序转成程序

  • 源程序是指下图左边的的源程序文件
  • 程序是指下图右边的可执行文件

§ 第4章 第一个程序 - 图6

2.3 例子

任务:编程运算§ 第4章 第一个程序 - 图7,源程序中应该怎么写呢?

  1. assume cs:codesg
  2. codesg segment
  3. mov ax,2
  4. add ax,ax
  5. add ax,ax
  6. codesg ends
  7. end

2.4 程序结束

我们的程序最先以汇编指令的形式存在源程序中,经编译、连接后转变为机器码,存储在可执行文件中,那么,他怎么运行起来呢?

一个程序P2在可执行文件中,必须有一个正在运行的程序P1,将P2从可执行文件中加载入内存中,将CPU的控制权交给P2,P2才能得以运行。P2开始运行后,P1暂停运行。

而当P2运行完毕后,应该将CPU的控制权交还给使它得以运行的程序P1,此后,P1继续运行。

现在,知道了一个程序结束后,将CPU的控制权交还给使它得以运行的程序,这个过程称为程序返回。那么,如何返回呢?应该在程序的末尾添加返回的程序段。

上面的源程序例子中,下面两条指令实现的功能就是程序返回

  1. mov ax,4c00H
  2. int 21H

现在知道的关于结束的几个概念

§ 第4章 第一个程序 - 图8

三、程序的编译、连接、运行

我们在学习汇编语言的过程中,需要用到以下的工具来运行一个完整的汇编程序,而这些工具都是在操作系统之上运行的程序,所以我们的学习过程必须在有操作系统的环境中进行。

§ 第4章 第一个程序 - 图9

关于这些工具的安装,前往另一篇教程中查看,那里有完整的示例程序,所以这里只给出一些知识点,但是这里仍然推荐按照书上的步骤重新运行一遍,书上有好多细节(比如,link连接目标文件时出现的Library为什么忽略之类的),而教程中的例子太粗糙了。

3.1 编译

在编译的过程中,我们提供了一个输入,即源程序文件。最多能够得到3个输出:

  • 目标文件(.obj)
  • 列表文件(.lst)
  • 交叉引用文件(.crf)

这3个输出文件中,目标文件是我们最终要得到的结果,而另外两个是中间结果。

3.2 连接

连接的作用都有什么?

  1. 当源程序很大时,可以将它分为多个源程序文件来编译,每个源程序编译成目标文件后,再用连接程序将它们连接到一起,生成一个可执行文件
  2. 程序中调用了某个库文件中的子程序,需要将这个库文件和该程序生成的目标文件连接到一起,生成一个可执行文件
  3. 一个源程序编译后,得到了存有机器码的目标文件,目标文件中的有些内容还不能直接用来生成可执行文件,连接程序将这些内容处理为最终的可执行信息。所以,在只有一个源程序文件,而又不需要调用某个库中的子程序的情况下,也必须使用连接程序对目标文件进行处理,生成可执行文件。

四、思考§ 第4章 第一个程序 - 图10

之前学习过,在DOS中,可执行文件中的程序P1若要运行,必须有一个正在运行的程序P2,将P1从可执行文件中加载入内存,将控制权交给CPU,P1才能得以运行;当P1运行完毕以后,将CPU的控制权交还给P2.

问题1

在之前的例子中,P2是谁?

问题2

P1程序运行结束后,返回到了那里?

提示、答案

要回答上述两个问题,就需要对DOS有比较深入的了解,阅读以下内容

§ 第4章 第一个程序 - 图11

问题1的答案:是正在运行的command将P1的程序加载入内存,command设置CPU的CS : IP指向程序的第一条指令(即程序的入口),从而使程序得以运行

问题2的答案:程序返回结束后,返回到command中,CPU继续执行command

汇编程序从写出到执行的过程:

§ 第4章 第一个程序 - 图12

五、程序执行过程的跟踪

我们写的程序在逻辑上不一定总是正确的,对于简单的错误,仔细检查源程序就可以发现;对于隐藏较深的错误,就必须对程序的执行过程进行跟踪分析才能发现。而Debug是用来跟踪一个程序的运行过程的

我们知道,在DOS运行一个程序的时候,是由command将程序可执行文件中加载入内存,并使其得以执行。但是,这样我们就不能逐条指令看到程序的执行过程,因为command的程序加载,设置CS:IP指向程序入口的操作是连续完成的,而当CS:IP一指向程序的入口,command就放弃了CPU的控制权,CPU立即开始运行程序,直至程序结束。

输入,debug testass.exe后,按下Enter键,Debug将程序从testass.exe中加载入内存,进行相关初始化后设置CS:IP指向程序的入口。

5.1 r命令

通过r命令可以查看各个寄存器的设置情况。

§ 第4章 第一个程序 - 图13

Debug程序将可执行文件加载入内存后,cx中存放的是程序的长度为30个字节(程序机器码一共有30个,对应1E)

上述文件的载入过程如下

§ 第4章 第一个程序 - 图14

那么,我们的程序被装入内存的什么地方?如何得知?

  1. 程序加载后,ds中存放着所在内存区的段地址,这个内存区的偏移地址为0,则程序所在的内存区的地址为:ds:0
  2. 这个内存区的前256个字节中存放的是PSP,DOS用来和程序进行通信。从256字节处往后的空间存放的是程序。

所以,从ds中可以得到PSP的段地址SA,PSP的偏移地址为0,则物理地址为§ 第4章 第一个程序 - 图15

因为PSP占256(100H)字节,所以程序的物理地址为:

§ 第4章 第一个程序 - 图16%5Ctimes%2016%2B0%20%3D%20(%5Ctext%7BSA%7D%2B10%5Ctext%7BH%7D)%5Ctimes%2016%2B0%0A#card=math&code=%5Ctext%7B%E7%A8%8B%E5%BA%8F%E7%9A%84%E7%89%A9%E7%90%86%E5%9C%B0%E5%9D%80%3A%20SA%7D%5Ctimes%2016%2B0%2B256%3D%5Ctext%7BSA%7D%5Ctimes16%2B0%2B16%5Ctimes%2016%3D%28%5Ctext%7BSA%7D%2B16%29%5Ctimes%2016%2B0%20%3D%20%28%5Ctext%7BSA%7D%2B10%5Ctext%7BH%7D%29%5Ctimes%2016%2B0%0A&id=SB8kP)

可用段地址和偏移地址表示为:SA+10H : 0

上图中的DS=075A,所以数据段的地址为075A : 0程序的地址为**(075A+10):0**即为**076A:0**

在上述程序中,CS=076A, IP=0000CS:IP指向程序的第一条指令,正好和上面我们算出来的结果是一样的

§ 第4章 第一个程序 - 图17

5.2 u命令

使用u命令看一下命令生成的机器码

§ 第4章 第一个程序 - 图18

5.3 t命令

使用t命令单步执行程序中的每一条指令,并根据每条指令的执行结果,到了INT 21

§ 第4章 第一个程序 - 图19

5.4 p命令

到达int 21后,使用p命令运行(为什么使用p命令,不需要知道,记住就行),之后程序正常结束。

§ 第4章 第一个程序 - 图20

这里是Debug将程序加载入内存,所以程序运行结束回到Debug中

5.5 q命令

使用1命令退出Debug,回到command中

§ 第4章 第一个程序 - 图21

六、实验三

6.1 实验1

将下面的程序保存为t1.asm,将其生成可执行文件t1.exe

  1. assume cs:codesg
  2. codesg segment
  3. mov ax,2000H
  4. mov ss,ax
  5. mov sp,0
  6. add sp,10
  7. pop ax
  8. pop bx
  9. push ax
  10. push bx
  11. pop ax
  12. pop bx
  13. mov ax,4c00H
  14. int 21H
  15. codesg ends
  16. end

6.2 实验2

用Debug跟踪t1.exe的执行过程,写出每一步执行后,相关寄存器中的内容和栈顶的内容。

6.3 实验3

PSP的头两个字节是CD 20,用Debug加载t1.exe,查看PSP的内容