友情提示:如果本网页打开太慢或显示不完整,请尝试鼠标右键“刷新”本网页!阅读过程发现任何错误请告诉我们,谢谢!! 报告错误
八万小说网 返回本书目录 我的书架 我的书签 TXT全本下载 进入书吧 加入书签

windows环境下32位汇编语言程序设计-第5部分

按键盘上方向键 ← 或 → 可快速上下翻页,按键盘上的 Enter 键可回到本书目录页,按键盘上方向键 ↑ 可回到本页顶部!
————未阅读完?加入书签已便下次继续阅读!



呢?先来看反汇编的内容:

;。data段中的变量

:00403000 12 34 12 78 56 34 12 。。。 

            │   │     │

            │   │     └─→ dwTest3

            │   └──────→ wTest2

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

 

;de段中的代码

: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

               …

               de

               …

               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

 如果为了把Hello和World分两行定义,szHello是这样定义的:

szHello       db      'Hello';0dh;0ah

              db      'World';0

那么sizeof szHello是多少呢?注意!是7而不是13,MASM中的变量定义只认一行,后一行db 'World';0实际上是另一个没有名称的数据定义,编译器认为sizeof szHello是第一行字符的数量。虽然把szHello的地址当参数传给MessageBox等函数显示时会把两行都显示出来,但严格地说这是越界使用变量。虽然在实际的应用中这样定义长字符串的用法很普遍,因为如果要显示一屏幕帮助,一行是不够的,但要注意的是:要用到这种字符串的长度时,千万不要用sizeof去表示,最好是在程序中用lstrlen函数去计算。



 
来源:电子工业出版社 作者:罗云彬 上一页         回书目         下一页          
上一页         回书目         下一页          
  


第3章 使用MASM


3。3 标号、变量和数据结构(5)

    
3。 获取变量地址

获取变量地址的操作对于全局变量和局部变量是不同的。

对于全局变量,它的地址在编译的时候已经由编译器确定了,它的用法大家都不陌生:

mov     寄存器;offset 变量名

其中offset是取变量地址的伪操作符,和sizeof伪操作符一样,它仅把变量的地址代到指令中去,这个操作是在编译时而不是在运行时完成的。

对于局部变量,它是用ebp来做指针操作的,假设ebp的值是40100h,那么局部变量1的地址是ebp…4即400FCh,由于ebp的值随着程序的执行环境不同可能是不同的,所以局部变量的地址值在编译的时候也是不确定的,不可能用offset伪操作符来获取它的地址。

80386处理器中有一条指令用来取指针的地址,就是lea指令,如:

lea     eax;'ebp…4'

该指令可以在运行时按照ebp的值实际计算出地址放到eax中。

如果要在invoke伪指令的参数中用到一个局部变量的地址,该怎么办呢?参数中是不可能写入lea指令的,用offset又是不对的。MASM对此有一个专用的伪操作符addr,其格式为:

addr 局部变量名和全局变量名

当addr后跟全局变量名的时候,用法和offset是相同的;当addr后面跟局部变量名的时候,编译器会自动用lea指令先把地址取到eax中,然后用eax来代替变量地址使用。注意addr伪操作符只能在invoke的参数中使用,不能用在类似于下列的场合:

mov eax;addr 局部变量名   ;注意:错误用法

 假设在一个子程序中有如下invoke指令:

  invoke  Test;eax;addr szHello

其中Test是一个需要两个参数的子程序,szHello是一个局部变量,会发生什么结果呢?编译器会把invoke伪指令和addr翻译成下面这个模样:

lea    eax;'ebp…4'

push   eax    ;参数2:addr szHello

push   eax    ;参数1:eax

call   Test

发现了什么?到push第一个参数eax之前,eax的值已经被lea eax;'ebp…4'指令覆盖了!也就是说,要用到的eax的值不再有效,所以,当在invoke中使用addr伪操作符时,注意在它的前面不能用eax,否则eax的值会被覆盖掉,当然eax在addr的后面的参数中用是可以的。幸亏MASM编译器对这种情况有如下错误提示:

error A2133: register value overwritten by INVOKE

否则,不知道又会引出多少莫名其妙的错误!



 
来源:电子工业出版社 作者:罗云彬 上一页         回书目         下一页          
上一页         回书目         下一页          
  


第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 
返回目录 上一页 下一页 回到顶部 7 5
未阅读完?加入书签已便下次继续阅读!
温馨提示: 温看小说的同时发表评论,说出自己的看法和其它小伙伴们分享也不错哦!发表书评还可以获得积分和经验奖励,认真写原创书评 被采纳为精评可以获得大量金币、积分和经验奖励哦!