📄 汇007.txt
字号:
LEA DX, MSG ;DS:DX指向待统计的字符串
CALL COUNT ;调用子程序统计出各类字符的个数
CALL DISPBX ;调用子程序显示数字字符的个数
MOV BX, CX
CALL DISPBX ;调用子程序显示字母的个数
MOV BX, DI
CALL DISPBX ;调用子程序显示其它字符的个数
.EXIT 0
END
7.3.3 堆栈传递参数
堆栈是一个特殊的数据结构,它通常是用来保存程序的返回地址。当用它来传递参数时,势必会造成数据和返回地址混合在一起的局面,用起来要特别仔细。
具体做法如下:
(1)、当用堆栈传递入口参数时,要在调用子程序前把有关参数依次压栈,子程序从堆栈中取到入口参数;
(2)、当用堆栈传递出口参数时,要在子程序返回前,把有关参数依次压栈(这里还需要做点额外操作,要保证返回地址一定在栈顶),调用程序就可以从堆栈中取到出口参数。
在通常情况下,我们用堆栈传入口参数,用寄存器传出口参数。
1、用堆栈传递入口参数的调用方法:
…
PUSH Para1
…
PUSH Paran ;把n个字的参数压栈
CALL SUBPRO ;调用子程序SUBPRO
…
2、在子程序中取入口参数的方法:
、段内调用子程序
由于是段内调用,所以,CALL指令只把返回地址的偏移量(即IP的内容)压栈,如图7.6(a)所示。在进入子程序后,为了能读取传递过来的参数,需要用BP来访问堆栈,所以要先保护BP原来的值,再把当前SP的值传送给BP。
于是,当前BP所指向的堆栈单元与最后一个参数Paran之间隔着BP的原值和返回地址的偏移量,也就是说:二者之间相差4个字节。具体情况如图7.6(b)所示。
(a)、进入子程序时堆栈情况 (b)、子程序寄存器保护后的堆栈情况
图7.6 在段内调用情况下子程序所能访问的堆栈情况
在子程序中读取用堆栈传递参数的一般方法如下程序片段所示。
SUBPRO PROC NEAR
PUSH BP ;保护寄存器BP
MOV BP, SP ;用寄存器BP来访问堆栈,读取参数
… ;保护其它寄存器的指令
MOV Paran, [BP+4] ;保护其它寄存器的指令
…
MOV Para1, [BP+4+2*(n-1)]
…
SUBPRO ENDP
、段间调用子程序
在段间调用子程序时,CALL指令会把返回地址的偏移量和段寄存器CS的内容都压栈,如图7.7(a)所示。在进入子程序后,与前面“段内调用子程序”一样,也需要用BP来读取传递过来的参数,所以,也要先保护BP原来的值,再把当前SP的值传送给BP。
这时,当前BP所指向的堆栈单元与最后一个参数Paran之间隔着BP的原值、返回地址的偏移量和段地址,所以,二者之间相差6个字节。具体情况如图7.7(b)所示。
(a)、进入子程序时堆栈情况 (b)、子程序寄存器保护后的堆栈情况
图7.7 在段间调用情况下子程序所能访问的堆栈情况
在段间调用时,除了多一个返回段地址外,其它的内容与“段内调用”的情况完全一致,所以,在读取第i个参数时,只要用[BP+6+4*(n-i)]代替[BP+4+2(n-i)]即可(假设每个参数都是字类型)。
7.4 寄存器的保护与恢复
由于计算机的硬件资源只有一套,当子程序修改了寄存器的内容后,返回到调用它的程序时,这些寄存器的内容也就不会是调用子程序前的内容。这样,子程序修改寄存器内容就可能变成了调用它的副作用,这种副作用常常会导致调用程序的出错。为此,在编写子程序时,除了能对作为入口和出口参数的寄存器进行修改外,对其它寄存器的修改对调用程序来说都要是透明的,也就是说,在调用子程序指令的前后,除了作为入口和出口参数的寄存器内容可以不同外,其它寄存器的内容要保持不变。有时,也要求作为入口参数的寄存器内容保持不变。
在子程序中,保存和恢复寄存器内容的主要方法是:在子程序的开始把它所用到的寄存器压进栈,在返回前,再把它们弹出栈。这样编写的好处是该子程序可以被任何其它程序来调用。在调用指令前,不需要保存寄存器,在调用指令后,也无需恢复寄存器。
利用堆栈来保存和恢复寄存器内容方法的一般形式如下:
XXXXX PROC
PUSH REG1
…
PUSH REGn ;把子程序要使用的寄存器压栈,REGi代表某个寄存器
…
…
… ;子程序的处理功能语句
POP REGn ;把前面压栈的寄存器弹出,注意它们的次序
…
POP REG1
RET
XXXXX ENDP
例7.2就是一个在子程序中利用堆栈来保存和恢复寄存器内容的例子。利用堆栈来实现此项功能时,应注意以下几点:
、用堆栈保存和恢复寄存器的内容,要注意堆栈“先进后出”的操作特点;
、通常情况下不保护入口参数寄存器的内容,当然,也可以根据事先的约定而对它们加以保护;
、如果用寄存器带回子程序的处理结果,那么,这些寄存器就一定不能加以保护;
、整个子程序的执行几乎肯定要改变标志位,可用PUSHF和POPF来保护和恢复标志位,但一般在子程序中不保护标志位,除非有此特殊需要;
7.5 子程序的完全定义
在7.1节所给出的子程序定义格式是一个最基本的、最简单的定义格式,它不能为子程序提供更简洁的调用方式。在宏汇编MASM 6.11系统中,为微机汇编语言的子程序提供了更加丰富的定义方式。
虽然子程序的这种定义方式显得稍微有点复杂,但它不仅为子程序的调用带来了极大的方便,而且其调用方式与高级语言中子程序的调用方式相一致,这就大大地降低了程序员熟练掌握它的难度。
7.5.1 子程序完全定义格式
子程序名 PROC [distance] [langtype] [visibility] [<prologuearg>]
[USES 寄存器列表] [,参数[:数据类型]]...
[LOCAL varlist]
子程序的程序体
子程序名 ENDP
定义子程序时,可使用参数表来直接指明其所要的参数,但程序员必须先用.MODEL伪指令,或使用<langtype>参数来说明本子程序所使用的程序设计语言类型。
程序员在定义子程序时,最好能象在高级语言(如:C/C++)定义过程那样,先说明该子程序的原型(用伪指令PROTO),这样,在调用时,系统可以自动进行类型检查,也可以使用更方便的调用伪指令INVOKE来调用该子程序。有关子程序的原型说明伪指令和调用伪指令在随后第7.5.8和7.5.9小节中加以介绍。
子程序通常用RET指令来结束其执行,也可用指令“RET n”来指明在结束子程序执行后从堆栈弹出n个字节。有关返回指令请参阅7.2.2节中的叙述。
汇编程序在处理子程序时能自动产生“起始”代码(PROLOGUE Code)和“结束”代码(EPILOGUE code)。这两段特殊的代码分别完成:在调用子程序时,能把传递给子程序的参数压栈,在子程序结束时能把先前压栈的参数弹出。有了这两段代码,程序员在调用子程序时就不用自行考虑子程序的参数传递问题。
若子程序用指令RETN、RETF或IRETF作为子程序的结束指令,那么,汇编程序将不生成“结束”代码。
程序员可以用自己定义宏来替代缺省的“起始”和“结束”的代码段。这种替代方法是使用伪指令:OPTION PROLOGUE和OPTION EPILOGUE。
若子程序没有参数、局部变量,没使用USES子句,也不会产生新的段或段组,那么,子程序是可以嵌套定义的。程序员也可以使用返回指令RETN和RETF来避免子程序的嵌套。
在子程序内部,可以在指令之前使用伪指令LOCAL来说明其局部变量,有关规定在随后的第7.5.10节中有详细的说明。
下面就来介绍该定义格式中各个说明项的作用。
7.5.2 子程序的位距
子程序的位距(Distance)有:Near、Far、Near16、Far16、Near32和Far32。
子程序位距描述符告诉汇编程序该子程序是在本段之内(Near),还是在本段之外(Far)。Near和Far描述符表示使用当前的段规模(Segment Size),Near16、Far16、Near32和Far32描述符是告诉汇编程序忽略当前的段规模,而使用指定16位或32位的段规模。
若选用类型Near或Far,那么,汇编程序将根据当前段的规模来决定选用16位,还是32位的Near或Far。
若程序员不指定该选项,那么汇编程序将根据当前的存储模式(由.MODEL来决定)和处理机类型来决定子程序类型。若不使用伪指令.MODEL,那么,Near是缺省的类型。
7.5.3 子程序的语言类型
子程序语言类型(Language Type)可以是任何一种有效的程序设计语句类型,由它来告诉汇编程序将使用什么样的标识符的命名风格、子程序的调用和返回约定。该语言类型说明可使汇编语言程序与其它语言程序达到共享的目的。所有有效的语言类型及其书写规定如表7.1所列。
表7.1 语言类型及其书写规定
C SYSCALL STDCALL Basic Fortran Pascal
前导下划线 X X
字母大写化 X X X
参数从左到右 X X X
参数从右到左 X X X
调用程序清空堆栈 X *
保存指针寄存器BP X X X
使用VARARG参数 X X X
*若使用:VARARG参数,则调用程序清空堆栈,否则,被调用的子程序清空堆栈。
程序员可用另外三种方法来设置程序的语言类型:.MODEL、OPTION LANGTYPE:和命令行选项/Gx。若在程序和命令行中都说明了语言类型,那么,前者的说明优先。
另外,程序员也可用命令行选项/H来限定标识符的最大长度。
例如:
Pascal语言风格:OPTION LANGUAGE:PASCAL、/Gc
C语言风格:OPTION LANGUAGE:C、/Gd
7.5.4 子程序的可见性
子程序的可见性(Visibility)决定该子程序对其它模块是否可用。它共有三个属性值:PRIVATE、PUBLIC和EXPORT。
PUBLIC属性是子程序标准的缺省属性,但该缺省属性可以用伪指令OPTION PROC来修改。EXPORT属性意味着该子程序是一个“远”的、具有PUBLIC属性的子程序,并要求连接程序在生成可执行文件时把其入口地址放入导出入口地址表中。
例如:
OPTION PROC : PRIVATE ;说明子程序的可见性为:PRIVATE
OPTION PROC : EXPORT ;说明子程序的可见性为:EXPORT
7.5.5 子程序的起始和结束操作
当程序员想用自己定义的宏来替代缺省的“起始”和“结束”的代码段时,可用下列说明语句来实现:
OPTION PROLOGUE : MacroName1
OPTION EPILOGUE : MacroName2
PROLOGUE和EPILOGUE分别指定MacroName1和MacroName2为“起始”和“结束”代码段的宏名。
汇编程序对用户定义的宏MacroName1和MacroName2的形式有较严格的规定,要求宏的定义形式如下:
MacroName MACRO ProcName, flags, argbytes, localbytes, <reglist>, userparms:VARARG
该宏定义的每个参数都有详细的说明,感兴趣的读者可看有关技术资料或MASM 6.11中的帮助,详细的说明在此从略,但建议使用缺省的宏。
如果想取消当前指定的宏名,而恢复使用缺省的“起始”和“结束”代码段的宏名,那么,可用下列说明语句,即指定二个缺省的宏名PrologueDef和EpilogueDef。
OPTION PROLOGUE : PrologueDef
OPTION EPILOGUE : EpilogueDef
若程序员不要汇编程序自动产生“起始”和“结束”代码,则可用NONE来代替说明语句中的宏名,即:
OPTION PROLOGUE : NONE
OPTION EPILOGUE : NONE
7.5.6 寄存器的保护和恢复
保护寄存器说明子句的说明格式:
USES 寄存器列表
该说明子句要求汇编程序为其生成保护和恢复寄存器的指令序列,即:在进入子程序执行指令之前,把寄存器列表中的寄存器压进堆栈,在结束子程序执行时,把先前压进堆栈的寄存器弹出,以达到保护寄存器的目的。
寄存器列表:列举出在子程序中需要保护的寄存器名,即:在子程序开始时需要把内容进栈的寄存器名。若有多个寄存器名,则在寄存器名之间要用“空格”来分开。
例如:
Dsip PROC USES AX DX, FUNC:WORD, MSG:PTR BYTE
MOV DX, MSG
MOV AX, FUNC
INT 21H
RET
Disp ENDP
汇编程序在处理该子程序时,会根据子句USES的作用,在第一条指令“MOV DX, MSG”之前,插入把寄存器AX和DX进栈的指令序列,即:
PUSH AX
PUSH DX
而在返回指令RET之前插入把寄存器DX和AX的值弹出的指令序列,即:
POP DX
POP AX
注意:若子程序含有多个RET或IRET指令,那么,汇编程序在每个RET或IRET指令前都将增加相应的弹出堆栈指令序列。
从子句USES的功能来看,它与前面7.4节“寄存器的包含与恢复”中所用的方法完全一致,所不同的是:用USE子句进行寄存器保护和恢复的代码是由汇编程序自动产生的,程序员不用关心如何去做,有点象高级语言的编程风格,而7.4节中的代码则是由程序员自己来安排的。
7.5.7 子程序的参数传递
子程序参数是用来向子程序传递信息的数据。若有多个参数,则参数之间要用逗号分割。为了能说明子程序的参数,程序员必须事先指定参数所遵循的语言类型或使用“语言类型”参数。
参数的数据类型可以是任何一个有效的数据类型说明符或VARARG。VARARG数据类型允许向子程序传递“个数”不定的参数,其参数之间要用逗号“,”来分开。
若参数表中含有VARARG说明的参数,那么,该参数一定是该子程序的最后一个参数。其规定隐含地说明了在参数表中只能有一个用VARARG说明的参数。
当子程序的语言类型是C、SYSCALL和STDCALL时,在其参数表中才能使用VARARG数据类型的参数。见前面的表7.1中所列。
如果没有显式地指定某个参数的数据类型,那么,在16位段规模的情况下,其缺省的数据类型是WORD;在32位段规模的情况下,其缺省的数据类型是DWORD。
7.5.8 子程序的原型说明
子程序原型的说明格式如下:
子程序名 PROTO [distance] [langtype] [,[parameter]:tag]...
该说明语句告诉汇编程序该子程序的若干属性,如:位距、语语言类型、参数个数及其类型等。这样,汇编程序就可以对其定义进行适当的检查。
如果对所有基于堆栈的过程都定义一个原型,那么,就可把这些原型存放在一个独立的包含文件(用伪指令INCLDUE来装入)中。使用这种方法对将来把所有子程序放入自定义的库文件中是非常方便的。
该原型说明语句中参数distance、langtype、parameter和tag等的含义与前面的叙述相一致,在此不再重复。
7.5.9 子程序的调用伪指令
子程序调用伪指令INVOKE与子程序的调用指令CALL在功能上是一致的,但它使汇编语言的子程序调用方法高级语言化,程序员可不用理会一些调用细节问题。
调用伪指令INVOKE的使用格式如下:
INVOKE expression [, arguments]
其中:expression—地址表达式,通常为子程序名;
arguments—传递的各参数之间用逗号','分开,参数可以是寄存器、表达式或ADDR 标识符等。
该伪指令是调用基于堆栈的子程序的方法,它把所有参数压栈,子程序结束时,又把参数自动弹出堆栈。
在参数传递时,汇编程序将根据子程序的原型进行数据类型检查。若需要进行参数类型转换的话,汇编程序则会自动生成一段代码来满足其数据类型转换的要求。
例如:
INVOKE TEST, AX, 12+34, ADDR MSG
其中:TEST是子程序名,寄存器AX和表达式“12+34”是参数,“ADDR MSG”是传递变量MSG的地址。
例7.6 编写一个累加参数数值的子程序。其中参数的个数不定,参数的个数由第一个参数来确定。
解:
.MODEL SMALL
.STACK 256
.CODE
;第一个参数parmcount确定其后面参数parmvalues中所含参数的个数
ADDUP PROC NEAR C, parmcount:WORD, parmvalues:VARARG
XOR AX, AX
XOR SI, SI
MOV CX, parmcount
.REPEAT
ADD AX, parmvalues[SI]
ADD SI, 2
.UNTILCXZ
RET
ADDUP ENDP
.STARTUP
INVOKE ADDUP, 3, 5, 2, 4 ;调用子程序ADDUP,计算5+2+4
INVOKE ADDUP, 4, 1, 2, 3, 4 ;调用子程序ADDUP,计算1+2+3+4
.EXIT 0
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -