第三章: 堆栈平衡

 

 

摘自:win32汇编---罗云彬

 

 


第3章 使用MASM


3.3 标号、变量和数据结构(2)
2. 局部变量的初始化值

显然,局部变量是无法在定义的时候指定初始化值的,因为local伪指令只是简单地把空间给留出来,那么开始使用时它里面是什么值呢?和全局变量不一样,局部变量的起始值是随机的,是其他子程序执行后在堆栈里留下的垃圾,所以,对局部变量的值一定要初始化,特别是定义为结构后当参数传递给API函数的时候。


 

3.3 标号、变量和数据结构(4)

   
3.3.5  变量的使用

1. 以不同的类型访问变量

这个话题有点像C语言中的数据类型强制转换,C语言中的类型转换指的是把一个变量的内容转换成另外一种类型,转换过程中,数据的内容已经发生了变化,如把浮点数转换成整数后,小数点后的内容就丢失了。在MASM中以不同的类型访问不会对变量造成影响。

举一个简单的例子,先以db方式定义一个缓冲区:

szBuffer    db      1024 dup (?)

然后从其他地方取得了数据,但数据的格式是以字方式组织的,要处理数据,最有效的方法是两个字节两个字节地处理,但如果在程序中把szBuffer的值放入ax:

mov     ax,szBuffer

编译器会报一个错:

error A2070: invalid instruction operands

意思是无效的指令操作,为什么呢?因为szBuffer是用db定义的,而ax的尺寸是一个word,等于两个字节,尺寸不符合。MASM中,如果要用指定类型之外的长度访问变量,必须显式地指出要访问的长度,这样,编译器忽略语法上的长度检验,仅使用变量的地址。使用的方法是:

类型 ptr 变量名

类型可以是byte,word,dword,fword,qword,real8和real10。如:

mov     ax,word ptr szBuffer

mov     eax,dword ptr szBuffer

上述语句能通过编译,当然,类型必须和操作的寄存器长度匹配。在这里要注意的是,指定类型的参数访问并不会去检测长度是否溢出,看下面一段代码:

                .data

bTest1          db      12h

wTest2          dw      1234h

dwTest3         dd      12345678h

                …

 

                .code

                …

                mov     al,bTest1

                mov     ax,word ptr bTest1

                mov     eax,dword ptr bTest1

                …

上面的程序片断,每一句执行后寄存器中的值是什么呢,mov al,bTest1这一句很显然使al等于12h,下面的两句呢,ax和eax难道等于0012h和00000012h吗?实际运行结果很“奇怪”,竟然是3412h和78123412h,为什么呢?先来看反汇编的内容:

;.data段中的变量

:00403000 12 34 12 78 56 34 12 ...

            │   │     │

            │   │     └─→ dwTest3

            │   └──────→ wTest2

            └─────────→    bTest1

 

;.code段中的代码

:00401000 A000304000                mov al, byte ptr [00403000]

:00401005 66A100304000          mov ax, word ptr [00403000]

:0040100B A100304000            mov eax, dword ptr [00403000]

.data段中的变量是按顺序从低地址往高地址排列的,对于超过一个字节的数据,80386处理器的数据排列方式是低位数据在低地址,所以wTest2的1234h在内存中的排列是34h 12h,因为34h是低位。同样,dwTest3在内存中以78h 56h 34h 12h从低地址往高地址存放,在执行指令mov ax,word ptr bTest1的时候,是从bTest1的地址403000h处取一个字,其长度已经超过了bTest1的范围并落到了wTest2中,从内存中看,是取了bTest1的数据12h和wTest2的低位34h,在这两个字节中,12h位于低地址,所以ax中的数值是3412h。同样道理,看另一条指令:

mov     eax,dword ptr bTest1

这条指令取了bTest1,wTest2的全部和dwTest3的最低位78h,在内存中的排列是12h  34h 12h 78h,所以eax等于78123412h。

这个例子说明了汇编中用ptr强制覆盖变量长度的时候,实质上是只用了变量的地址而禁止编译器进行检验,编译器并不会考虑定界的问题,程序员在使用的时候必须对内存中的数据排列有个全局概念,以免越界存取到意料之外的数据。

如果程序员的本意是类似于C语言的强制类型转换,想把bTest1的一个字节扩展到一个字或一个双字再放到ax或eax中,高位保持0而不是越界存取到其他的变量,可以用80386的扩展指令来实现。80386处理器提供的movzx指令可以实现这个功能,例如:

movzx       ax,bTest1          ;例1

movzx       eax,bTest1         ;例2

movzx       eax,cl             ;例3

movzx       eax,ax             ;例4

●   例1把单字节变量bTest1的值扩展到16位放入ax中。

●   例2把单字节变量bTest1的值扩展到32位放入eax中。

●   例3把cl中的8位值扩展到32位放入eax中。

●   例4把ax中的16位值扩展到32位放入eax中。

用movzx指令进行数据长度扩展是Win32汇编中经常用到的技巧。

2. 变量的尺寸和数量

在源程序中用到变量的尺寸和数量的时候,可以用sizeof和lengthof伪指令来实现,格式是:

sizeof            变量名、数据类型或数据结构名

lengthof          变量名、数据类型或数据结构名

sizeof伪指令可以取得变量、数据类型或数据结构以字节为单位的长度,lengthof可以取得变量中数据的项数。假如定义了以下数据:

stWndClass     WNDCLASS     <>

szHello        db           'Hello,world!',0

dwTest         dd           1,2,3,4

               …

               .code

               …

               mov          eax,sizeof stWndClass

               mov          ebx,sizeof WNDCLASS

               mov          ecx,sizeof szHello

               mov          edx,sizeof dword

               mov          esi,sizeof dwTest

执行后eax的值是stWndClass结构的长度40,ebx同样是40,ecx的值是13,就是“Hello,world!”字符串的长度加上一个字节的0结束符,edx的值是一个双字的长度:4,而esi则等于4个双字的长度16。

如果把所有的sizeof换成lengthof,那么eax会等于1,因为只定义了1项WNDCLASS,而ecx同样等于13,esi则等于4,而lengthof WNDCLASS和lengthof dword是非法的用法,编译程序会报错。

要注意的是,sizeof和lengthof的数值是编译时候产生的,由编译器传递到指令中去,上边的指令最后产生的代码就是:

mov        eax,40

mov        ebx,40

mov        ecx,13

mov        edx,4

mov        esi,16

 

 

 


第3章 使用MASM


3.4 使用子程序

   
当程序中相同功能的一段代码用得比较频繁时,可以将它分离出来写成一个子程序,在主程序中用call指令来调用它。这样可以不用重复写相同的代码,而用call指令就可以完成多次同样的工作了。Win32汇编中的子程序也采用堆栈来传递参数,这样就可以用invoke伪指令来进行调用和语法检查工作。

3.4.1  子程序的定义

子程序的定义方式如下所示。

子程序名  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

这并不是说子程序的编写有错误,而是invoke伪指令无法得知子程序的定义情况,所以无法进行参数的检测。在这种情况下,为了让invoke指令能正常使用,必须在程序的头部用proto伪操作定义子程序的信息,“提前”告诉invoke语句关于子程序的信息,proto的用法见3.2.2节。当然,如果子程序定义在前的话,用proto的定义就可以省略了。

由于程序的调试过程中可能常常对一些子程序的参数个数进行调整,为了使它们保持一致,就需要同时修改proc语句和proto语句。在写源程序的时候有意识地把子程序的位置提到invoke语句的前面,省略掉proto语句,可以简化程序和避免出错。

3.4.2  参数传递和堆栈平衡

了解了子程序的定义方法后,让我们继续深入了解子程序的使用细节。在调用子程序时,参数的传递是通过堆栈进行的,也就是说,调用者把要传递给子程序的参数压入堆栈,子程序在堆栈中取出相应的值再使用,比如,如果要调用:

SubRouting(Var1,Var2,Var3)

经过编译后的最终代码可能是(注意只是“可能”):

push  Var3

push  Var2

push  Var1

call  SubRouting

add   esp,12

也就是说,调用者首先把参数压入堆栈,然后调用子程序,在完成后,由于堆栈中先前压入的数不再有用,调用者或者被调用者必须有一方把堆栈指针修正到调用前的状态,即堆栈的平衡。参数是最右边的先入堆栈还是最左边的先入堆栈、还有由调用者还是被调用者来修正堆栈都必须有个约定,不然就会产生错误的结果,这就是在上述文字中使用“可能”这两个字的原因。各种语言中调用子程序的约定是不同的,所以在proc以及proto语句的语言属性中确定语言类型后,编译器才可能将invoke伪指令翻译成正确的样子,不同语言的不同点如表3.4所示。

表3.4  不同语言调用方式的差别

C             SysCall        StdCall         BASIC         FORTRAN      PASCAL
 
最先入栈参数
 右
 右
 右
 左
 左
 左
 
清除堆栈者
 调用者
 子程序
 子程序
 子程序
 子程序
 子程序
 
允许使用VARARG
 是
 是
 是注
 否
 否
 否
 

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

为了了解编译器对不同类型子程序的处理方式,先来看一段源程序:

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

Sub1    proc         C _Var1,_Var2

        mov          eax,_Var1

        mov          ebx,_Var2

        ret

Sub1    endp

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

Sub2    proc         PASCAL _Var1,_Var2

        mov          eax,_Var1

        mov          ebx,_Var2

        ret

Sub2    endp

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

Sub3    proc         _Var1,_Var2

        mov          eax,_Var1

        mov          ebx,_Var2

        ret

b3                   endp

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

        …

        invoke       Sub1,1,2

        invoke       Sub2,1,2

        invoke       Sub3,1,2

编译后再进行反汇编,看编译器是如何转换处理不同类型的子程序的:

; 这里是Sub1 - C类型

:00401000 55                    push ebp

:00401001 8BEC                  mov ebp, esp

:00401003 8B4508                    mov eax, dword ptr [ebp+08]

:00401006 8B5D0C                mov ebx, dword ptr [ebp+0C]

:00401009 C9                    leave

:0040100A C3                    ret

; 这里是Sub2 - PASCAL类型

:0040100B 55                    push ebp

:0040100C 8BEC                  mov ebp, esp

:0040100E 8B450C                mov eax, dword ptr [ebp+0C]

:00401011 8B5D08                mov ebx, dword ptr [ebp+08]

:00401014 C9                    leave

:00401015 C20800                ret 0008

; 这里是Sub3 — StdCall类型

:00401018 55                    push ebp

:00401019 8BEC                  mov ebp, esp

:0040101B 8B4508                mov eax, dword ptr [ebp+08]

:0040101E 8B5D0C                mov ebx, dword ptr [ebp+0C]

:00401021 C9                    leave

:00401022 C20800                ret 0008

        …

; 这里是invoke Sub1,1,2 — C类型

:00401025 6A02                  push 00000002

:00401027 6A01                  push 00000001

:00401029 E8D2FFFFFF            call 00401000

:0040102E 83C408                add esp, 00000008

; 这里是invoke Sub2,1,2 — PASCAL类型

:00401031 6A01                  push 00000001

:00401033 6A02                  push 00000002

:00401035 E8D1FFFFFF            call 0040100B

; 这里是invoke Sub3,1,2 — StdCall类型

:0040103A 6A02                  push 00000002

:0040103C 6A01                  push 00000001

:0040103E E8D5FFFFFF            call 00401018

可以清楚地看到,在参数入栈顺序上,C类型和StdCall类型是先把右边的参数先压入堆栈,而PASCAL类型是先把左边的参数压入堆栈。在堆栈平衡上,C类型是在调用者在使用call指令完成后,自行用add esp,8指令把8个字节的参数空间清除,而PASCAL和StdCall的调用者则不管这个事情,堆栈平衡的事情是由子程序用ret 8来实现的,ret指令后面加一个操作数表示在ret后把堆栈指针esp加上操作数,完成的是同样的功能。

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

存取参数和局部变量都是通过堆栈来定义的,所以参数的存取也是通过ebp做指针来完成的。在探讨局部变量的时候,已经就没有参数的情况下ebp指针和局部变量的对应关系做了分析,现在来分析一下ebp指针和参数之间的对应关系,注意,这里是以Win32中的StdCall为例,不同的语言类型,指针的顺序可能是不同的。

假定在一个子程序中有两个参数,主程序调用时在 push 第一个参数前的堆栈指针esp为X,那么压入两个参数后的esp为X-8,程序开始执行call指令,call指令把返回地址压入堆栈,这时候eps为X-C,接下去是子程序中用push ebp来保存ebp的值,esp变为X-10,再执行一句mov ebp,esp,就可以开始用ebp存取参数和局部变量了,图3.4说明了这个过程。


图3.4  ebp指针、参数和局部变量的关系

在源程序中,由于参数、局部变量和ebp的关系是由编译器自动维护的,所以读者不必关心它们的具体关系,但到了用Soft-ICE等工具来分析其他软件的时候,遇到调用子程序的时候一定要先看清楚它们之间的类型差别。

在子程序中使用参数,可以使用与存取局部变量同样的方法,因为这两者的构造原理几乎一模一样,所以,在子程序中有invoke语句时,如果要用到输入参数的地址当做invoke的参数,同样要遵循局部变量的使用方式,不能用offset伪操作符,只能用addr来完成。同样,所有对局部变量使用的限制几乎都可以适用于参数。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值