⭐ 欢迎来到虫虫下载站! | 📦 资源下载 📁 资源专辑 ℹ️ 关于我们
⭐ 虫虫下载站

📄 003.txt

📁 会变语言实现的一些程序
💻 TXT
📖 第 1 页 / 共 5 页
字号:

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

MASM中可以定义的变量类型相当多,具体如表3.2所示。

表3.2  变量的类型

名   称   表示方式   缩   写 长度(字节)
 
字节
 byte
 db
 1
 
 word
 dw
 2
 
双字(doubleword)
 dword
 dd
 4
 
三字(farword)
 fword
 df
 6
 
四字(quadword)
 qword
 dq
 8
 
十字节BCD码(tenbyte)
 tbyte
 dt
 10
 
有符号字节(signbyte)
 sbyte
  
 1
 
有符号字(signword)
 sword
  
 2
 
有符号双字(signdword)
 sdword
  
 4
 
单精度浮点数
 real4
  
 4
 
双精度浮点数
 real8
  
 8
 
10字节浮点数
 real10
  
 10
 

所有使用到变量类型的情况中,只有定义全局变量的时候类型才可以用缩写,现在先来看全局变量定义的几个例子:

.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

● 例1定义了一个未初始化的word类型变量,名称为wHour。

● 例2定义了一个名为wMinute的word类型变量。

● 例3定义了一个双字类型的变量_hWnd。

● 例4定义了一组字,以0001,0002,0001,0002,…的顺序在内存中重复100遍,一共是200个字。

● 例5定义了一个1 024字节的缓冲区。

● 例6定义了一个字符串,总共占用了12字节。两头的单引号是定界的符号,并不属于字符串中真正的内容。

在byte类型变量的定义中,可以用引号定义字符串和数值定义的方法混用,假设要定义两个字符串“Hello,World! ”和“Hello again”,每个字符串后面跟回车和换行符,最后以一个0字符结尾,可以定义如下:

szText   db   ~Hello,world!~,0dh,0ah,~Hello again~,0dh,0ah,0

2. 全局变量的初始化值

全局变量在定义中既可以指定初值,也可以只用问号预留空间,在 .data?段中,只能用问号预留空间,因为 .data?不能指定初始值,这里就有一个问题:既然可以用问号预留空间,那么在实际运行的时候,这个未初始化的值是随机的还是确定的呢?在全局变量中,这个值就是0,所以用问号指定的全局变量如果要以0为初始值的话,在程序中可以不必为它赋值。




3.3.3  局部变量

局部变量这个名称最早源于高级语言,主要是为了定义一些仅在单个函数里面有用的变量而提出的,使用局部变量能带来一些额外的好处,它使程序的模块化封装变得可能,试想一下,如果要用到的变量必须定义在程序的数据段里面,假设在一个子程序中要用到一些变量,当把这个子程序移植到别的程序时,除了把代码移过去以外,还必须把变量定义移过去。而即使把变量定义移过去了,由于这些变量定义在大家都可以用的数据段中,就无法对别的代码保持透明,别的代码有可能有意无意地修改它们。还有,在一个大的工程项目中,存在很多的子程序,所有的子程序要用到的变量全部定义到数据段中,会使数据段变得很大,混在一起的变量也使维护变得非常不方便。

局部变量这个概念出现以后,两个以上子程序都要用到的数据才被定义为全局变量统一放在数据段中,仅在子程序内部使用的变量则放在堆栈中,这样子程序可以编成“黑匣子”的模样,使程序的模块结构更加分明。

局部变量的作用域是单个子程序,在进入子程序的时候,通过修改堆栈指针esp来预留出需要的空间,在用ret指令返回主程序之前,同样通过恢复esp丢弃这些空间,这些变量就随之无效了。它的缺点就是因为空间是临时分配的,所以无法定义含有初始化值的变量,对局部变量的初始化一般在子程序中由指令完成。

在DOS时代,低版本的宏汇编本来无所谓全局变量和局部变量,所有的变量都是定义在数据段里面的,能让被所有的子程序或主程序存取,就相当于现在所说的全局变量,用汇编语言在堆栈中定义局部变量是很麻烦的一件事情。要和高级语言做混合编程的时候,程序员往往很痛苦地在边上准备一张表,表上的内容是局部变量名和ebp指针的位置关系。

1. 局部变量的定义

MASM用local伪指令提供了对局部变量的支持。定义的格式是:

local 变量名1[[重复数量]][:类型],变量名2[[重复数量]][:类型]……

local伪指令必须紧接在子程序定义的伪指令proc后、其他指令开始前,这是因为局部变量的数目必须在子程序开始的时候就确定下来,在一个local语句定义不下的时候,可以有多个local语句,语法中的数据类型不能用表3.2中的缩写,如果要定义数据结构,可以用数据结构的名称当做类型。Win32汇编默认的类型是dword,如果定义dword类型的局部变量,则类型可以省略。当定义数组的时候,可以 [] 括号括起来。不能使用定义全局变量的dup伪指令。局部变量不能和已定义的全局变量同名。局部变量的作用域是当前的子程序,所以在不同的子程序中可以有同名的局部变量。

这里有几个定义局部变量的例子:

local   loc1[1024]:byte   ;例1

local   loc2 ;例2

local   loc3:WNDCLASS   ;例3

● 例1定义了一个1 024字节长的局部变量loc1。

● 例2定义了一个名为loc2的局部变量,类型是默认值dword。

● 例3定义了一个WNDCLASS数据结构,名为loc3。

下面是局部变量使用的一个典型的例子:

TestProc proc

local  @loc1:dword,@loc2:word

local  @loc3:byte

 

mov   eax,@loc1

mov   ax,@loc2

mov   al,@loc3

ret

 

TestProc endp

这是一个名为TestProc的子程序,用local语句定义了3个变量,@loc1是dword类型,@loc2是word类型,@loc3是byte类型,在程序中分别有3句存取3个局部变量的指令,然后就返回了,编译成可执行文件后,再把它反汇编就得到了以下指令:

: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指令后,CPU把返回的地址压入堆栈,再转移到子程序执行,esp在程序的执行过程中可能随时用到,不可能用esp来随时存取局部变量,大家一定有印象,在介绍寄存器的时候提到过ebp寄存器也是以堆栈段为默认数据段的,所以,可以用ebp做指针,于是,在初始化前,先用一句push ebp指令把原来的ebp保存起来,然后把esp的值放到ebp中,供存取局部变量做指针用,再后面就是在堆栈中预留空间了,由于堆栈是向下增长的,所以要在esp中加一个负值,FFFFFFF8就是–8。慢着!一个dword加一个word加一个字节不是7吗,为什么是8呢?这是因为在80386处理器中,以dword为界对齐时存取内存速度最快,所以MASM宁可浪费一个字节,执行了这3句指令后,初始化完成,就可以进行正常的操作了,从指令中可以看出局部变量在堆栈中的位置排列,如表3.3所示。

表3.3  上例中局部变量排列的顺序

ebp偏移 内   容
 
ebp+4
 由call推入的返回地址
 
Ebp
 push ebp指令推入的原ebp值,然后新的ebp=现在的esp
 
ebp-4
 第一个局部变量
 
ebp-6
 第二个局部变量
 
ebp-7
 第三个局部变量
 

在程序退出的时候,必须把正确的esp设置回去,否则,ret指令会从堆栈中取出错误的地址返回,看程序可以发现,ebp就是正确的esp值,因为子程序开始的时候已经有一句mov ebp,esp,所以要返回的时候只要先mov esp,ebp,然后再pop ebp,堆栈就是正确的了。

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

明白了局部变量使用的原理,就很容易理解使用时的注意点:ebp寄存器是关键,它起到保存原始esp的作用,并随时用做存取局部变量的指针基址,所以在任何时刻,不要尝试把ebp用于别的用途,否则会带来意想不到的后果。

Win32汇编中局部变量的使用方法可以解释一个很有趣的现象:在DOS汇编的时候,如果在子程序中的push指令和pop指令不配对,那么返回的时候ret指令从堆栈里得到的肯定是错误的返回地址,程序也就死掉了。但在Win32汇编中,push指令和pop指令不配对可能在逻辑上产生错误,却不会影响子程序正常返回,原因就是在返回的时候esp不是靠相同数量的push和pop指令来保持一致的,而是靠leave指令从保存在ebp中的原始值中取回来的,也就是说,即使把esp改得一塌糊涂也不会影响到子程序的返回,当然,“窍门”就在ebp,把ebp改掉,程序就玩完了!

2. 局部变量的初始化值

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

 在API函数使用的大量数据结构中,往往用0做默认值,如果用局部变量定义数据结构,初始化时只定义了其中的一些字段,那么其余字段的当前值可以是编程者预想不到的数值,传给API函数后,执行的结果可能是意想不到的,这是初学者很容易忽略的一个问题。所以最好的办法是:在赋值前首先将整个数据结构填0,然后再初始化要用的字段,这样其余的字段就不必一个个地去填0了,RtlZeroMemory这个API函数就是实现填0的功能的。

 




3.3.4  数据结构

数据结构实际上是由多个字段组成的数据“样板”,相当于一种自定义的数据类型,数据结构中间的每一个字段可以是字节、字、双字、字符串或所有可能的数据类型。比如在API函数RegisterClass中要使用到一个叫做WNDCLASS的数据结构,Microsoft的手册上是如下定义的:

typedef struct _WNDCLASS {

  UINT style;

  WNDPROC   lpfnWndProc;

  Int   cbClsExtra;

  int cbWndExtra;

  HINSTANCE   hInstance;

  HICON   hIcon;

  HCURSOR   hCursor;

  HBRUSH hbrBackground;

  LPCTSTR   lpszMenuName;

  LPCTSTR   lpszClassName;

} WNDCLASS, *PWNDCLASS;

注意,这是C语言格式的,这个数据结构包含了10个字段,字段的名称是style,lpfnWndProc和cbClsExtra等,前面的UINT和WNDPROC等是这些字段的类型,在汇编中,数据结构的写法如下:

结构名   struct

 

字段1   类型   ?

字段2   类型   ?

⌨️ 快捷键说明

复制代码 Ctrl + C
搜索代码 Ctrl + F
全屏模式 F11
切换主题 Ctrl + Shift + D
显示快捷键 ?
增大字号 Ctrl + =
减小字号 Ctrl + -