当前位置:鱼C工作室 >Win32汇编 > 查看文章

使用MASM12(使用子程序和堆栈平衡原理)- Win32汇编语言020

使用MASM12(使用子程序和堆栈平衡原理)

 

让编程改变世界

Change the world by program


 

使用子程序

 

当程序中相同功能的一段代码用得比较频繁时,可以将它分离出来写成一个子程序,在主程序中用 call 指令来调用它。

这样可以不用重复写相同的代码,而用 call 指令就可以完成多次同样的工作了。

Win32 汇编中的子程序采用堆栈来传递参数,这样就可以用 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 指令一次保存和恢复所有寄存器来得方便。

 

【参数和类型】参数指参数的名称,在定义参数名的时候不能跟全局变量和子程序中的局部变量重名。

 

对于类型,由于 Win32 中的参数类型只有 32 位(dword)一种类型,所以可以省略。

在参数定义的最后还可以跟 VARARG,表示在已确定的参数后还可以跟多个数量不确定的参数,在Win32 汇编中唯一使用 VARARG 的 API 就是wsprintf,类似于 C 语言中的 printf,其参数的个数取决于要显示的字符串中指定的变量个数。

 

完成了定义之后,可以用 invoke 伪指令来调用子程序,当 invoke 伪指令位于子程序代码之前的时候,处理到 invoke 语句的时候编译器还没有扫描到子程序定义信息的记录,所以会有以下错误的信息:

error A2006: undefined symbol: _ProcWinMain

 

学过类似C语言的朋友就会举手说话啦,这个问题其实很简单:不就是没有定义的错误表现嘛~

invoke 伪指令无法得知子程序的定义情况,所以无法进行参数的检测。相当于 没有声明。

 

在这种情况下,为了让 invoke 指令能正常使用,必须在程序的头部用 proto 伪操作定义子程序的信息。

功能是提前告诉 invoke 语句关于子程序的信息,当然,如果子程序定义在前的话,用 proto 的定义就可以省略了。

 

由于程序的调试过程中可能常常对一些子程序的参数个数进行调整,为了使它们保持一致,就需要同时修改 proc 语句和 proto 语句。

在写源程序的时候有意识地把子程序的位置提到invoke 语句的前面,省略掉 proto 语句,可以简化程序和避免出错。

因此,我们看到的Win32 汇编代码基本是先写子程序,然后最后才是主程序的。

 

参数传递和堆栈平衡

 

我们在了解子程序的定义方法后,接着让我们继续深入了解了程序的使用细节。

原来在调用子程序时,参数的传递是通过堆栈进行的,也就是说,调用者把要传递给子程序的参数压入堆栈,子程序在堆栈中取出相应的值再使用。

 

比如要调用:SubRouting(Var1, Var2, Var3)

经过编译后的最终代码可能是(注意这里只是可能,具体情况根据不同的环境、约定而定)=>>

push Var3

push Var2

push Var1

call SubRouting

add esp, 12

 

也就是说,调用者首先把参数压入堆栈,然后调用子程序,在完成后,由于堆栈中先前压入的数不再有用,调用者或者被调用者必须有一方把堆栈指针修正到调用前的状态,即堆栈的平衡。

 

我们看到参数是最右边的先入堆栈还是最左边的先入堆栈、还有由调用者还是被调用者来修正堆栈都必须有个约定,不然就会产生错误的结果。

因为有几种不同的约定,所以就是在上述文字中使用“可能”这两个字的原因。

 

各种语言中调用子程序的约定是不同的,所以在proc以及proto语句的语言属性中确定语言类型后,编译器才可能将invoke伪指令翻译成正确的样子。

注:VARARG表示参数的个数可以是不确定的,如wsprintf函数,本表中特殊的地方是StdCall的堆栈清除平时是由子程序完成的,但使用VARARG时是由调用者清除的。

 

我们来反汇编一个程序看下我们都学习了些什么:Example & Disassembly

我们发现,虽然是完成一样的功能,但是实现的方式各不相同,所谓殊途同归。

Win32约定的类型是StdCall,所以在程序中调用子程序或系统API后,不必自己来平衡堆栈,免去了很多麻烦。


为您推荐

报歉!评论已关闭.