有时候程序中可能有逻辑错误,这需要我们用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 存储单元,立即数
执 行:两个数据相减,结果保存在左边的寄存器或存储器中,标志寄存器中相关位被设置
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程序的入口可以在任何位置,但如果不在代码段起始处,要在源程序中指明。