win32 010 使用masm32

使用MASM07

 

让编程改变世界

Change the world by program


 

标号、变量和数据结构

 

当程序中要跳转到另一位置时,需要有一个标识来指示位置,这就是标号。

通过在目的地址的前面放上一个标号,可以在指令中使用标号来代替直接使用地址。

 

关于变量的使用是任何编程语言都要遇到的工作,Win32汇编也不例外,在MASM中使用变量也有需要注意的几个问题,错误地使用变量定义或用错误的方法初始化变量会带来难以定位的错误。

变量是计算机内存中已命名的存储位置,在C语言中有很多种类的变量,如整数型、浮点型和字符型等,不同的变量有不同的用途和尺寸。

 

比如说虽然长整数和单精度浮点数都是32位长,但它们的用途不同。

变量的值在程序运行中是经常改变的,所以它必须定义在可写的段内,如.data和.data,或者在堆栈内。

按照定义的位置不同,MASM中的变量也分为全局变量和局部变量两种。

 

在MASM中标号和变量的命名规范是相同的,它们是:

– 1)可以用字母、数字、下划级及符号@、$和。

– 2)第一个符号不能是数字。

– 3)长度不能超过240个字符。

– 4)不能使用指令名等关键字。

– 5)在作用域内必须是唯一的。

 

这些规则是大部分编程语言约定俗成的!

 

标号的定义

 

当在程序中使用一条跳转指令的时候,可以用标号来表示跳转的目的地,编译器在编译的时候会把它替换成地址。

标号既可以定义在目的指令同一行的头部,也可以在目的指令前一行单独用一行定义,标号定义的格式是:

– 格式一 标号名: 目的指令

– 格式二 标号名:: 目的指令

 

我们比较常用的方法是使用格式一(一个冒号那个),注意这时候标号的作用域是当前的子程序,在不同子程序中可以存在同样名字的标号。

这也就意味着这种格式不能从一个子程序通过标号跳转到另一个子程序中。

 

那如果实在痒,想跳,怎么办?

格式二就应运而生了!没错,当我们需要从一个子程序中用指令跳到另一个子程序中的标号位置时候,我们用格式二,但代码并不和谐!

 

@@

 

OK,大家一定想到了很多类似的肥猪流火星文***符号,很高兴我们学Win32汇编居然赶上了时代的主流阿,尼玛啊,有木有啊!

在DOS时代,为标号起名是个麻烦的事情,因为汇编指令用到跳转指令特别多,任何比较和测试等都要涉及跳转,所以在程序中会有很多标号,

在整个程序范围内起个不重名的标号要费一番功夫,结果常常用addr1和addr2之类的标号一直延续下去……

 

事实上很多标号会使用一到两次,而且不一定非要起个有意义的名称,如汇编程序中下列代码结构很多:

mov cx,1234h
    cmp flag,1
    je loc1
    mov cx,1000h
loc1:
    loop loc1

 

在别的地方事实上就不会用到loc1了,尼玛啊,编程要讲究低碳,低碳啊,有木有?!

对于这种不环保的做法,我们用@@来取而代之!程序改下如下:

mov cx,1234h
    cmp flag,1
    je @F
    mov cx,1000h
@@:
    loop @B  // B 这里是Before的意思

 

是不是既方便,又美观?!

当用@@做标号时,@F表示本条指令后的第一个@@标号,@B表示本条指令前的第一个@@标号。

注意,当程序中可以有多个@@标号,@B和@F只寻找匹配最近的一个。

 

不要在间隔太远的代码中使用@@标号,因为在以后的修改中@@和@B,@F中间可能会被无意中插入一个新的@@。

这样一来,@B或@F就会引用到错误的地方去,距离最好限制在编辑器能够显示的同一屏幕的范围内。

使用MASM08

 

让编程改变世界

Change the world by program


 

全局变量的定义

 

全局变量的作用域是整个程序,Win32汇编的全局变量定义在.data 或.data 段内,可以同时定义变量的类型和长度,格式如:

变量名 类型 初始值1, 初始值2,…

变量名 类型 重复数量 dup (初始值1,初始值2,…)

 

MASM中可以定义的变量类型相当多,也很实在,都是表达占地多少?!

注意:所有使用到变量类型的情况中,只有定义全局变量的时候类型才可以用缩写!

 

【举例】

.data
	wHour          dw                           ;例1
	wMinute        dw          10               ;例2
	_hWnd          dd                           ;例3
	word_Buffer    dw          100 dup (1,2)    ;例4
	szBuffer       byte        1024 dup ( )     ;例5
	szText         db          ‘Hello,world!’   ;例6

 

在byte类型变量的定义中,可以用引号定义字符串和数值定义的方法混用。

假设要定义两个字符串Hello,World! 和 Hello again,每个字符串后面中回车和换行符,最后以一个0字符结尾,可以定义如下:

szText db ‘Hello,World!’, 0dh, 0ah, ’Hello again’, 0dh, 0ah, 0

使用MASM09

 

让编程改变世界

Change the world by program


 

局部变量的使用

 

TestProc    proc   ;名为TestProc的子程序
	local        @loc1:dword, @loc2:word
	local        @loc3:byte    ;用local语句定义了3个变量
 
	mov eax, @loc1    ;对应类型进行存储,然后返回
	mov ax, @loc2
	mov al, @loc3
	ret
TestProc    endp

 

我们来看看它反汇编之后是什么样子的:

:00401000   55        push ebp
:00401001   8BEC      mov ebp, esp
:00401003   83C4F8    add esp, FFFFFFF8
:00401006   8B45FC    mov eax, dword ptr [ebp-04]
:00401009   668B45FA  mov ax, word ptr [ebp-06]
:0040100D   8A45F9    mov al, byte ptr [ebp-07]
:00401010   C9        leave
:00401011   C3        ret

 

可以看到,反汇编后的指令比源程序多了前后两段指令,它们是:

:00401000 55          push ebp
:00401001 8BEC        mov ebp, esp
:00401003 83C4F8      add esp, FFFFFFF8
 
:00401010 C9          leave

 

这些就是使用局部变量所必需的指令,分别用于局部变量的准备工作和扫尾工作。

 

【分析过程】

当调用者执行了call TestProc指令后,CPU把返回的地址(当前地址)压入堆栈,再转移(jmp)到子程序执行。

esp在程序的执行过程中可能随时用到,不可能用esp来随时存取局部变量,ebp寄存器(可以理解为小三)是以堆栈段为默认数据段的,所以,可以用ebp做指针指向堆栈替代esp。

于是,在初始化前,先用一句push ebp指令把原来的dbp保存起来,然后把esp的值放到ebp中。

(介绍局部变量怎么腾出空间的)再后面就是堆栈中预留空间了,由于堆栈是向下增长的。所以要在esp中加一个负值,FFFFFFF8就是-8。

 

我们来考虑另一个问题:一个dword加一个word加一个byte不是7吗,为什么刚刚我们在堆栈为局部变量让出了8个字节的空间呢?

这是因为在80386处理器中,以dword (32位)为界对齐时存取内存速度最快,所以MASM宁可浪费一个字节,执行了这3句指令后,初始化完成,就可以进行正常的操作了,从指令中可以看出局部变量在堆栈中的位置排列。

 

在程序退出的时候,必须把正确的esp设置回去,否则,ret指令会从堆栈中取出错误的地址返回,看程序可以发现,ebp就是正确的esp值。

因为子程序开始的时候已经有一句mov ebp,esp,所以要返回的时候只要先mov esp,ebp,然后再pop ebp,堆栈就是正确的了。

 

在80386指令集中有一条指令可以在一句中实现这些功能,就是leave指令,所以,编译器在ret指令之前只使用了一句leave指令。

明白了局部变量使用的原理,就很容易理解使用时的注意点:ebp寄存器是关键(第一次听说小三是关键)

 

因为它起到保存原始esp的作用,并随时用做存取局部变量的指针基址,所以在任何时刻,不要尝试把ebp用于别的用途,否则会带来意想不到的后果。(在任何时候不要做对不起小三的事情,不然后果很严重 T_T)

 

闲言碎语

 

Win32汇编中局部变量的使用方法可以解释一个很有趣的现象:在DOS汇编的时候,如果在子程序中的push指令和pop指令不配对,那么返回的时候ret指令从堆栈里得到的肯定是错误的返回地址,程序也就死掉了。

但在Win32汇编中,push指令和pop指令不配对可能在逻辑上产生错误,却不会影响子程序正常返回,原因就是在返回的时候esp不是靠相同数量的push和pop指令来保持一致的,而是靠leave指令从保存在ebp中的原始值中取回来的。

也就是说,即使把esp改得一塌糊涂也不会影响到子程序的返回,当然,窍门就在ebp,把ebp改掉,程序就玩完了!

 

局部变量的初始化值

 

显然,局部变量是无法在定义的时候指定初始化值的,因为local伪指令只是简单地把空间给留出来,那么开始使用时它里面是什么值呢?

和全局变量不一样,局部变量的初始值是随机的,是其他子程序执行后在堆栈里留下的垃圾(因为我们知道,腾出空间只是改变栈指针esp)。

所以,对局部变量的值一定要初始化,特别是定义为结构后当参数传递给API函数的时候。

 

在API函数使用的大量数据结构中,往往用0做默认值,如果用局部变量定义数据结构,初始化时只定义了其中的一些字段,那么其余字段的当前值可以是编程者预想不到的数值,传给API函数后,执行的结果可能是意想不到的,这是初学者很容易忽略的一个问题。

所以最好的办法是:在赋值前首先将整个数据结构填0,然后再初始化要用的字段,这样其余的字段就不必一个个地去填0了,RtlZeroMemory这个API函数就是实现填0的功能的。

masm教学, MASM教程 Win32汇编

使用MASM10(变量的使用)

 

让编程改变世界

Change the world by program


 

变量的使用

 

接着上一节的话题,我们继续谈变量,书本原本的数据结构我们后边介绍。

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

 

在MASM中以不同的类型访问不会对变量造成影响。

例如,以db方式定义一个缓冲区:

szBuffer db 1024 dup ( )

然后

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,为什么呢?(DOS汇编基础不错的同学,应该能理解)

现在我们先来看反汇编的内容:详见视频……

 

所以说呢,刚才这个例子说明了汇编中用ptr强制覆盖变量长度的时候,实质上是只用了变量的地址而禁止编译器进行检验。

编译器并不会考虑定界的问题,程序员在使用的时候必须对内存中的数据排列有个全局概念,以免越界存取到意料之外的数据。

如果程序员的本意是类似于C语言的强制类型转换,想把bTest1的一个字节扩展到一个字或一个双字再放到ax 或 eax中,高位保持0而不是越界存取到其他的变量,要肿么办呢?

 

80386处理器提供的 movzx 指令可以实现这个功能,例如:

movzx ax,bTest1 ; ax == 0012h

movzx eax,bTest1 ; eax == 00000012h

movzx eax,cl ; eax == 000000(cl)

movzx eax,ax ; eax == 0000(ax)

 

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

使用MASM11(变量的使用2)

 

让编程改变世界

Change the world by program


 

变量的尺寸和数量

 

在源程序中用到变量的尺寸和数量的时候,可以用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的值是Hello,world! 字符串的长度加上一个字节的0结束符:13

edx的值是一个双字的长度:4

esi等于4个双字的长度:16

 

如果把所有的 sizeof 换成 lengthof,那么eax会等于1,因为只定义了1项WNDCLASS

而ecx同样等于13

esi则等于4

lenghof WNDCLASST 和 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’, odh, oah

db ‘World’, 0

 

那么 sizeof szHello 是多少呢?

注意!是7,而不是13。MASM 中的变量定义只认一行,后一行db ‘World’, 0 实际上是另一个没有名称的数据定义,编译器认为sizeof szHello是第一行字符的数量。(尝试一下)

 

虽然把 szHello 的地址当参数传给 MessageBox 等函数显示时会把两行都显示出来,但严格地说这是越界使用变量。

虽然在实际的应用中这样定义长字符串的用法很普遍,因为如果要显示一屏幕帮助,一行是不够的。

但要注意的是:要用到这种字符串的长度时,千万不要用 sizeof 去表示,最好是在程序中用lstrlen 函数去计

用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后,不必自己来平衡堆栈,免去了很多麻

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值