ITEEDU

有时候程序中可能有逻辑错误,这需要我们用DEBUG将其排除掉,将一个可执行文件调入DEBUG中的方法在前面已经应用过:

C:\ASM\>DEBUG PROG7.COM[Enter] 

或者这样操作:

L(LOAD)命令的作用是装入一 个文件,文件名由N命令给出。

C:\ASM\>DEBUG[Enter]
-NPROG7.COM[Enter]
-L[Enter]

文件装入后即可用T、P命令跟踪执行或用U命令反汇编,下面是PROG7的反汇编形式:

-U[Enter]
0A3E:0100		B0B6		MOV		AL,B6
0A3E:0102		E643		OUT		43,AL
0A3E:0104		B8A904		MOV		AX,04A9
0A3E:0107		E642		OUT		42,AL
0A3E:0109		8AC4		MOV		AL,AH
0A3E:010B		E642		OUT		42,AL
0A3E:010D 		E461 		IN 		AL,61
0A3E:010F		50			PUSH	AX
0A3E:0110		0C03		OR		AL,03
0A3E:0112		E661		OUT		61,AL
0A3E:0114		B401		MOV		AH,01
0A3E:0116		CD21		INT		21
0A3E:0118		58			POP		AX
0A3E:0119		E661		OUT		61,AL
0A3E:011B 		C3 			RET 		

PROG7的结构过于简单了,还没有涉及有关分段的更多概念,下面给出的这个例子更复杂一些,这个例程中具有"数据"和"代码"两个段,而且有两个"过程"。程序中新出现了一个陌生的指令:──SUB

助记符:SUB(subtract)
用 途:将两个数据作减法
格 式:SUB 寄存器,立即数
SUB 寄存器,寄存器
SUB 寄存器,存储单元
SUB 存储单元,寄存器
SUB 存储单元,立即数
执 行:两个数据相减,结果保存在左边的寄存器或存储器中,标志寄存器中相关位被设置

PROG7-A
PORT_B	equ	61H								;#1--常量定义
data	segment								;#2--数据段定义
	assume	ds:data							;通知编译程序默认DS寄存器指向数据段
			
			
mess	db 'Press any key to stop!!!',24h 	;定义一个字符串 	
			
data	ends								;数据段结束
			
code	segment								;#3--代码段开始
	assume	cs:code							;通知编译程序默认CS寄存器指向代码段
sound	proc	near						;#4--SOUND子过程开始
	mov	al,10110110b						;初始化定时器
	out	43h,al	
			
	mov	ax,4a9h								;设定频率
	out	42h,al	
	mov	al,ah	
	out	42h,al	
			
	in	al,PORT_B 							;打开定时器和与门
			
		al,3	
	or	PORT_B,al	
	out		
			
	ret										;子过程返回
sound	endp		
			
main	proc	far							;#5--主过程开始
	push	ds								;#6--初始化堆栈,建立返回地址
	sub	ax,ax	
	push	ax	
			
	mov	ax,data								;#7--初始化DS寄存器
	mov	ds,ax	
	mov	dx,offset mess						;#8--显示MESS
	mov	ah,09h	
	int	21h	
	 call	sound							;调用SOUND发声
	mov	ah,1	
	int	21h									;等待按键
	in	al,PORT_B	
	and	al,0fch								;停止发声
	out	PORT_B,al	
	ret										;主过程返回
main	endp								;主过程结束
code	ends								;代码段结束
	end 	main 							;进程结束 

使用SUB指令的一些注意事项将在后面介绍。请注意这个程序中用许多前面加了"#"的注释文字将源程序分成了小块,这些说明性的文字可以不必录入,但如果大家在自己编制程序时能够加入必要的注释将会培养出一种很受欢迎的编程风格。

程序的"#1"部分作用是定义常量,所谓常量其实就是用一些具有特殊含义的字串来代替数字用在程序中。常量定义有两种方法:

常量名 EQU 数字

常量名 = 数字

“=”和“EQU”伪指令的作用是一样的。注意汇编语言并不要求用户在编制每一个程序时都定义常量,但是把一些程序中经常用到的数据定义成常量还是很有好处的,因为一旦需要修改数据,则只需在常量定义部分修改即可,而不必在源程序中苦苦寻找。

“#2”部分定义了一个数据段,其定义方法同4.1.2节介绍的代码段一样,也使用“SEGMENT”伪操作。需要说明的是数据段中多了一个伪指令--ASSUME,这个伪指令说明了段 与段寄存器的联系,有了这个伪指令,编译程序就知道了在程序中引用某段中的数据时应该用哪个段寄存器给出段地址。

在数据段中我们使用了"DB"伪指令定义的一个字符串,字符串前面的"MESS"是一个"标号",它表示这个字符串的地址。标号可以是除汇编语言保留字外的任何字串,但不能以数字打头。标号有时也用于代码中,表示其后指令的地址,这样一来当我们用JMP、JZ等转移指令时就可以不必关心具体的目的地址了。

数据段结束也使用ENDS伪指令,具体形式同代码段一样。

"#3"部分定义了一个代码段,并用"ASSUME"伪指令说明了与其关联的段寄存器。

这里有个很重要的问题要提醒大家,虽然在源程序中可以把数据与代码分别安排在两个段内,但这并不意味着在编译成的可执行文件里数据与代码仍然分成两个段。实际上源程序中分段的概念与内存的分段是有些区别的,因为源程序中的段定义还可以加入"定位类型"、"组合类型"之类信息,这些内容在刚刚给出的这个程序中没有体现。有关这一部分内容将在本书最后一章进行说明。

"#4"部分是一个具有"NEAR"属性的过程,它是这个程序的核心,作用是使定时器发声。在这个过程中我们使用了前面定义的常量PORT_B来代替以前用的数字61H。SOUND是这个过程的名字,它实际上也表示了这个过程的起始地址。这样在主过程中用CALL指令调用这个子过程时就不必给出具体的地址了,只需引用这个过程名就行了。这同DEBUG相比是个不小的优点。

从"#5"部分开始就是这个程序的主过程,"#6"、"#7"部分看上去好象多余,其实不然。由于主过程具有"FAR"属性,因此主过程结束时的RET指令将被编译程序译成"RETF"远程返回指令。我们之所以事先在堆栈中设定返回地址为DS:0是为了能正确结束程序并返回DOS。那么DS:0处究竟有什么奥秘呢?用DEBUG分析一下这个程序就清楚了。

C:\ASM\>DEBUG PROG7-A.EXE[Enter]
-r
AX=0000 BX=0000 CX=0052 DX=0000 SP=0000 BP=0000 SI=0000 DI=0000
DS=0A3F ES=0A3F SS=0A4F CS=0A51 IP=0102 NV UP EI PL NZ NA PO NC
0A51:0014 1E PUSH DS    注意IP寄存器的指向

用R命令列出寄存器后就会发现DS寄存器和CS的值不一样。

-uds:0 6[Enter]
0A3F:0000		CD20		INT		20
0A3F:0002		FF9F009A	CALL FAR		[BX+9A00]
0A3F:0006		F0			LOCK		

又是"INT 20"。大家是不是又看到一些似曾相识的数据呢?

事实上,无论是"COM"还是"EXE"文件,DOS在调入它们时都要保留256字节来预置一些数据,我们把这256字节称为"程序段前缀"(PSP--Program Segment Prefix),对于一个"COM"文件,由于只有一个段,所以PSP、代码、数据和堆栈都在这个段中,PSP在头部,堆栈在尾部,中间是代码和数据,一个"COM"程序在调入内存执行时DOS会自动在堆栈中存入一个0,所以"COM"程序只需使用近程的RET指令就能返回DOS,并且无需自己初始化堆栈。

"EXE"文件的PSP、数据和代码分在不同的段内,当一个"EXE"程序调入内存执行时,DOS会使DS、ES寄存器指向PSP所在段,使SS寄存器指向堆栈段而使CS寄存器指向代码段。所以"EXE"程序若要用"RET"指令结束返回系统,就必须自己在堆栈中填入返回地址,这个返回地址就是"DS:0"。由于这个地址是远程的,所以"EXE"程序的主过程要定义成"FAR"属性,这使得过程最后的"RET"指令实际被编译为"RETF"。

有关PSP的内容还有很多,在处理文件时我们还要使用其中很多的数据,这些知识将在第七章详细介绍。

"#9"部分是用于显示"MESS"字符串的,由于09H功能要求字符串的段地址必须在DS寄存器而偏移地址要在DX寄存器中,因此程序需要取到MESS的偏移地址,前面我们谈到标号MESS表示了字符串的地址,但并未说明它究竟表示段地址还是偏移地址。实际上标号既表示段地址也表示偏移地址。我们现在想取到偏移地址,所以用了一个"分析运算符"--OFFSET,它表示我们要取的是偏移地址。如果我们要取段地址,我们就要使用"SEG"运算符,相应的程序可以写成"MOV DX,SEG MESS"。不过我们现在无需再取MESS的段地址,"#7"部分已经做了这件事。

接下来的"CALL"指令调用了SOUND子过程发声。代码段结束后我们用"END"伪指令结束进程并指出了主过程。注意由于这个程序中的主过程并不在代码段的起始处,所以"END"后面必须指明主过程,否则编译程序将认为位于代码段起始处是这个程序的入口,这将导致错误。

源程序录入后我们就可以将其编译、连接成可执行程序。执行TLINK时不能再加"/T"参数,因为这个程序不能转成"COM"文件。

通过这两个例子,我想大家对源程序的编制、编译和连接已有了大体的印象。而且也能发现COM和EXE两种可执行程序的差异:

① COM文件很短,不能超过64K,它只有一个段,代码、数据和堆栈都在一个段内;EXE文件可以很大,它可以有多个段,代码、数据和堆栈分散在不同的段内。
② COM文件的入口必须在代码段偏移100H处,而EXE程序的入口可以在任何位置,但如果不在代码段起始处,要在源程序中指明。