当程序中相同功能的一段代码用得比较频繁时,可以将它分离出来写成一个子程序,在主程序中用call指令来调用它。这样可以不用重复写相同的代码, 仅仅用call指令就可以完成多次同样的工作了。Win 32汇编中的子程序也采用堆栈来传递参数, 这样就可以用invoke 伪指令来进行调用和语法检查工作。
子程序的定义
子程序的定义方式:
子程序名 proc [距离] [语言类型] [可视区域] [USES寄存器列表] [, 参数:类型] ...[VARARG]
local 局部变量列表
指令
子程序名 endp
proc和endp伪指令定义了子程序开始和结束的位置
proc后面跟的参数是子程序的属性和输入参数。子程序的属性有:
●距离——可以是NEAR, FAR, NEAR16, NEAR32, FAR16或FAR32, Win32中只
有一个平坦的段,无所谓距离,所以对距离的定义往往忽略。
●语言类型-一表示参数的使用方式和堆栈平衡的方式,可以是StdCall, C, SysCall,BASIC、FORTRAN和PASCAL, 如果忽略, 则使用程序头部.model定义的值。
●可视区域——可以是PRIVATE, PUBLIC和EXPORT。PRIVATE表示子程序只对本模块可见; PUBLIC表示对所有的模块可见(在最后编译链接完成的.exe文件中) ; EXPORT 表示是导出的函数, 当编写DLL的时候要将某个函数导出的时候可以这样使用。默认的设置是PUBLIC。
●USES寄存器列表——表示由编译器在子程序指令开始前自动安排push这些寄存器的指令, 并且在ret前自动安排pop指令, 用于保存执行环境, 但笔者认为不如自己在开头和结尾用pushad和popad指令一次保存和恢复所有寄存器来得方便。
●参数和类型一参数指参数的名称,在定义参数名的时候不能跟全局变量和子程序中的局部变量重名。对于类型, 由于Win 32中的参数类型只有32位(dword) 一种类型, 所以可以省略。在参数定义的最后还可以跟VAR ARG, 表示在已确定的参数后还可以跟多个数量不确定的参数, 在Win 32汇编中惟一使用VAR ARG的API就是w sprintf,类似于C语言中的printf, 其参数的个数取决于要显示的字符串中指定的变量个数。
完成了定义之后, 可以用invoke伪指令来调用子程序, 当invoke伪指令位于被调用的子程序代码之前的时候, 编译器处理到invoke语句的时候还没有扫描到子程序的定义信息, 所以会有以下错误信息:
error A 2006:undefined symbol 子程序名
这并不是说子程序的编写有错误, 而是invoke伪指令无法得知子程序的定义情况, 所以无法进行参数的检测。在这种情况下, 为了让invoke指令能正常使用, 必须在程序的头部用proto 伪操作定义子程序的信息, “提前”告诉invoke语句关于子程序的信息, proto的用法见https://www.cnblogs.com/liming19680104/p/17756861.html。当然, 如果被调用的子程序定义在invoke语句前面的话, proto语句就可以省略了。
参数传递和堆栈平衡
了解了子程序的定义方法后,让我们继续深入了解子程序的使用细节。在调用子程序时,参数的传递是通过堆栈进行的,也就是说,调用者把要传递给子程序的参数压入堆栈,子程序在堆栈中取出相应的值再使用,比如,如果要调用:SubRouting(Var1, Var2, Var3)经过编译后的最终代码可能是(注意只是“可能”):
push Var3
push Var2
push Var1
call SubRouting
add esp,12
也就是说,调用者首先把参数压入堆栈,然后调用子程序,在完成后,由于堆栈中先前压入的数不再有用,调用者或者被调用者必须有一方把堆栈指针修正到调用前的状态,这就叫堆栈的平衡。参数是最右边的先入堆栈还是最左边的先入堆栈、还有由调用者还是被调用者来修正堆栈都必须有个约定(称为调用约定),不然就会产生错误的结果,这就是在上述文字中使用“可能”这两个字的原因。由于各种语言默认的调用约定是不同的, 所以在proc,以及proto语句的语言属性中确定语言类型后, 编译器才可能将invoke伪指令翻译成正确的样子,不同语言的不同点如表3.4所示。
因为Win 32约定的类型是StdCall, 所以在程序中调用子程序或系统API后, 不必自己来平衡堆栈,免去了很多麻烦。