4.8 谁将可执行文件中的程序装载进入内存并使它运行?
我们前面讲过,在DOS中,可执行文件中的程序P1若要运行,必须有一个正在运行的程序P2,将P1从可执行文件中载入内存,将CPU的控制权交给它,P1才能得以运行;当P1运行完毕后,应该将CPU的控制权交还给使它得以运行的程序P2。
按照上面的原理,再来看一下4.7节中1.exe的执行过程(思考相关的问题)。
- 在提示符“c:\masm”后面输入可执行文件的名字“1”,按Enter键。这时,请思考问题4.1。
- 1.exe中的程序运行,
- 运行结束,返回,再次显示提示符“c:\masm”。请思考问题4.2。
问题4.1
此时,有一个正在运行的程序将1.exe中的程序加载入内存,这个正在运行的程序是什么?它将程序加载入内存后,如何使程序得以运行?
问题4.2
程序运行结束后,返回到哪里?
如果你对DOS有比较深入的了解,那么,很容易回答问题4.1、问题4.2中所提出的问题。如果没有这种了解,可以先阅读下面的内容。
操作系统的外壳
操作系统是由多个功能模块组成的庞大、复杂的软件系统。任何通用的操作系统,都要提供一个称为shell(外壳)的程序,用户(操作人员)使用这个程序来操作计算机系统进行工作。
DOS中有一个程序command.com,这个程序在DOS中称为命令解释器,也就是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来进行工作的。
现在回答问题4.1和4.2中所提出的问题。
- 在DOS中直接指向1.exe时,是正在运行的command,将1.exe中的程序加载入内存;
- command设置CPU的CS:IP指向程序的第一条指令(即程序的入口),从而使程序得以运行;
- 程序运行结束后,返回到command中,CPU继续运行command。
汇编程序从写出到执行的过程
到此,完成了一个汇编程序从写出到执行的全部过程。我们经历了这样一个历程:
编程→1.asm→编译→1.obj→连接→1.exe→加载→内存中的程序→运行
(Edit) (masm) (link) (command) (CPU)
4.9 程序执行过程的跟踪
可以使用Debug来跟踪一个程序的运行过程,这通常是必须要做的工作。我们写的程序在逻辑上不一定总是正确的,对于简单的错误,仔细检查一下源程序就可以发现;而对于隐藏较深的错误,就必须对程序的执行过程进行跟踪分析才容易发现。
下面以在前面的内容中生成的可执行文件1.exe为例,讲解如何同Debug对程序的执行过程进行跟踪。
现在我们知道,在DOS中运行一个程序的时候,是由command将程序从可执行文件中加载入内存,并使其得以运行。但是,这样我们不能逐条指令地看到程序的执行过程,因为command的程序加载,设置CS:IP指向程序的入口的操作是连续完成的,而当CS:IP一指向程序入口,command就放弃了CPU的控制权,CPU立即开始运行程序,直至程序结束。
为了观察程序的处理过程,可以使用Debug。Debug可以将程序加载入内存,设置CS:IP指向程序的入口,但Debug并不放弃对CPU的控制,这样,我们就可以使用Debug的相关命令来单步执行程序,查看每一条指令的执行结果。
具体方法如图4.18所示。
在提示符后输入“debug 1.exe”,按Enter键,Debug将程序从1.exe中加载入内存,进行相关的初始化后设置CS:IP指向程序的入口。
接下来可以用R命令看一下各个寄存器的设置情况,如图4.19所示。
可以看到,Debug将程序中可执行文件加载入内存后,cx中存放的是程序的长度。1.exe中程序的机器码共有15个字节。则1.exe加载后,cx中的内容为000FH。
现在程序已从1.exe中装入内存,接下来查看一下它的内容,可是我们查看哪里的内存呢?程序被装入了内存的什么地方?我们如何得知?
这里,需要讲解一下在DOS系统中.EXE文件中的程序的加载过程。图4.20针对我们的问题,简要地展示了这个过程。
注意,有一步称为重定位的工作在图4.20中没有讲解,因为这个问题和操作系统的关系较大,我们不作讨论。
那么,我们的程序被装入内存的什么地方?我们如何得知?从图4.20中,我们知道以下的信息。
- 程序加载后,ds中存放着程序所在内存区的段地址,这个内存区的偏移地址为0,则程序所在的内存区的地址为ds:0。
- 这个内存区的前256个字节中存放的是PSP,DOS用来和程序进行通信。从256字节处向后的空间存放的是程序。
所以,从ds中可以得到PSP的段地址SA,PSP的偏移地址为0,物理地址为SAX16+0。
因为PSP占256(100H)字节,所以程序的物理地址是:
SA*16+0+256=SA*16+16*16+0=(SA+16)*16+0
可用段地址和便宜地址表示为:SA+10H:0。
现在,我们看一下图4.19中DS的值,DS=129E,则PSP的地址为129E:0,程序的地址为12AE:0(即129E+10H:0)
图4.19中,CS=12AE,IP=0000,CS:IP指向程序的第一条指令。注意,源程序中的指令是mov ax,0123H,在Debug中标记位mov ax,0123,这是因为Debug默认所有数据都用十六进制表示。
可以用U命令看一下其他指令,如图4.21所示。
可以看到,从12AE:0000~12AE:000E都是程序的机器码。
现在,我们可以开始跟踪了,用T命令单步执行程序中的每一条指令,并观察每条指令的执行结果,到了int21,我们要用P命令执行,如图4.22所示。
图4.22中,int 21执行后,显示出“Program terminated normally”,返回Debug中。表示程序正常结束。注意,要使用P命令执行int 21。这里不必考虑是为什么,只要记住这一点就可以了。
需要注意的是,在DOS中运行程序时,是command将程序加载入内存,所以程序运行结束后返回到command中,而在这里是Debug将程序加载入内存,所以程序运行结束后要返回到Debug中。
使用Q命令退出Debug,将返回到command中,因为Debug是由command加载运行的。在DOS中用“debug 1.exe”运行Debug对1.exe进行跟踪时,程序加载的顺序是:command加载Debug,Debug加载1.exe。返回的顺序是:从1.exe中的程序返回到Debug,从Debug返回到command。