win32汇编-标号、变量和数据结构

 

当程序中要跳转到另一位置时,需要有一个标识来指示新的位置,这就是标号,通过在目标地址的前面放上一个标号,可以在指令中使用标号来代替直接使用地址。

使用变量是任何编程语言都要遇到的工作, Win 32汇编也不例外, 在MASM中使用变量也有需要注意的几个问题,错误地使用变量定义或用错误的方法初始化变量会带来难以定位的错误。变量是计算机内存中已命名的存储位置,在大部分的语言中都有很多种类的变量,如整数型、浮点型和字符串等,不同的变量有不同的用途和尺寸,比如说虽然长整数和单精度浮点数都是32位长,但它们的用途不同。

顾名思义, 变量的值在程序运行中是需要改变的, 所以它必须定义在可写的段内, 如.data 和.data?, 或者在堆栈内。按照定义的位置不同, MASM中的变量也分为全局变量和局部变量两种。
在MASM中标号和变量的命名规范是相同的, 它们是:

(1)可以用字母、数字、下划线及符号@、$和?。

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

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

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

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

标号

1.标号的定义

 

当在程序中使用一条跳转指令的时候,可以用标号来表示跳转的目的地,编译器在编译的时候会把它替换成地址,标号既可以定义在目的指令同一行的头部,也可以在目的指令前行单独用一行定义,标号定义的格式是:
标号名:目的指令   ;方法1

标号名::目的指令     ; 方法2

 

常用的方法是使用方法1(标号后跟一个冒号),这时标号的作用域是当前的子程序,在单个

子程序中的标号不能同名,否则编译器不知该用哪个地址,但在不同的子程序中可以有相同名称的标号,这意味着不能从一个子程序中用跳转指令跳到另一个子程序中

需要从一个子程序中用跳转指令跳到另一个子程序中的标号时,可以用方法2(标号后跟两个冒号)来定义,这时标号的作用域是整个程序,对任何其他子程序都是可见的

 

2.MASM中的@@

在DOS时代, 为标号起名是个麻烦的事情, 因为汇编指令用到跳转指令特别多, 任何比较和测试等都要涉及跳转,所以在程序中会有很多标号,在整个程序范围内起个不重名的标号要费一番工夫, 结果常常用addr1和addr2之类的标号一直延续下去, 如果后来要在中间插一个标号, 那么就常常出现addr1_1和loop10_5之类奇怪的标号。实际上,很多标号只会使用一到两次,而且不一定非要起个有意义的名称,如汇编程序中下列代码结构很多:

 

loc1在别的地方就再也用不到了, 对于这种情况, 高版本的MASM用@@标号代替它:

 

当用@@做标号时,可以用@F和@B来引用它,@F表示本条指令后的第一个@@标号@B表示本条指令前的第一个@@标号,程序中可以有多个@@标号,但@B和@F只寻找匹配最近的一个。

 

全局变量

1.全局变量的定义
全局变量的作用域是整个程序, Win 32汇编的全局变量定义在.data或.data?段内, 可以同时定义变量的类型和长度,

格式是:

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

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

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

 

 

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

 

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

 

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

 

局部变量

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

1.局部变量的定义MASM用local伪指令提供了对局部变量的支持。

定义的格式是:

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

2.局部变量的初始化值
显然, 局部变量是无法在定义的时候指定初始化值的, 因为local伪指令只是简单地把空

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

 

数据结构

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

 

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

在汇编中,数据结构的写法如下:

上面的WNDCLASS结构定义用汇编的格式来表示就是:

 

和大部分的常量一样, 几乎所有API所涉及的数据结构在Windows.inc文件中都已经有定义了。要注意的是,定义了数据结构实际上只是定义了一个“样板”,上面的定义语句并不会在哪个段中产生数据, 与Word中使用各种“信纸”与“文书”等模板类似, 定义了数据结构以后就可以多次在源程序中用这个“样板”当做数据类型来定义数据,

使用数据结构在数据段中定义数据的方法如下:

 

这个例子定义了一个以WNDCLASS为结构的变量stWndClass, 第一段的定义方法是未初始化的定义方法,第二段是在定义的同时指定结构中各字段的初始值,各字段的初始值用逗号隔开,在这个例子中10个字段的初始值都指定为1。

在汇编中, 数据结构的引用方法有好几种, 以上面的定义为例, 如果要使用st Wnd Class 中的1pfnWndProc字段, 最直接的办法是:

 

它表示把lpfnWndProc字段的值放入eax中去, 假设stWndClass在内存中的地址是403000h, 这句指令会被编译成mov eax, [403004h] , 因为lpfnWndProc是stWndClass中的第二个字段, 第一个字段是dword,已经占用了4字节的空间。

在实际使用中, 常常有使用指针存取数据结构的情况, 如果使用esi寄存器做指针寻址,可以使用下列语句完成同样的功能:

 

注意:第二句是[esi+WND CLASS.IpfnWndProc] 而不是[esi+stWndClass.IpfnWndProc] ,因为前者会被编译成mov eax, [esi+4] , 而后者会被编译成mov eax, [esi+403004h] , 后者的结果显然是错误的!如果要对一个数据结构中的大量字段进行操作,这种写法显然比较烦琐,

MASM还有一个用法, 可以用assume伪指令把寄存器预先定义为结构指针, 再进行操作:

 

这样,使用寄存器也可以用逗点引用字段名,程序的可读性比较好。这样的写法在最后编译成可执行程序的时候产生同样的代码。注意:在不再使用esi寄存器做指针的时候要用assumeesi:nothing取消定义。

结构的定义也可以嵌套, 如果要定义一个新的NEW_WND CLASS结构, 里面包含一个老的WND CLASS结构和一个新的dw Option字段, 那么可以如下定义:

 

假设现在esi是指向一个NEW_WNDCLASS的指针, 那么引用里面嵌套的oldWndClass 中的IpfnWndProc字段时, 就可以用下面的语句:

 

 

变量的使用

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

 

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

举一个简单的例子,先以db方式定义一个缓冲区:szBuffer db 1024 dup  (?)

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

mov ax, szBuffer

 

意思是无效的指令操作, 为什么呢?因为szBuffer是用db定义的, 而ax的尺寸是一个word, 等于两个字节, 尺寸不符合。

MASM中, 如果要用指定类型之外的长度访问变量, 必须显式地指出要访问的长度,这样,编译器忽略语法上的长度检验,仅使用变量的地址。

使用的方法是:

  类型 ptr 变量名

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

mov ax, word ptr szBuffer
mov eax, dword ptr szBuffer

上述语句能通过编译,当然,类型必须和操作的寄存器长度匹配。

在这里要注意的是,指定类型的参数访问并不会去检测长度是否溢出,看下面一段代码:

 

 

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

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

 

 

2.变量的尺寸和数量

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

 

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

如定义了以下数据:

复制代码
        .data
        stWndClass   WNDCLASS   <>
        szHello         db    'Hello,world!',0
        dwTest         dd      1,2,3,4
        .code
start:
        mov eax,sizeof stWndClass   ;执行后eax的值是stWndClass结构的长度40
        mov ebx,sizeof WNDCLASS     ;ebx同样是40
        mov ecx,sizeof szHello      ;ecx的值是13,就是“Hello,world!”字符串的长度加上一个字节的0结束符
        mov edx,sizeof dword      ;edx的值是一个双字的长度:4
        mov esi,sizeof dwTest    ;esi则等于4个双字的长度16.

        
        
        
        end    start
复制代码

 

复制代码
        .data
        stWndClass   WNDCLASS   <>
        szHello         db    'Hello,world!',0
        dwTest         dd      1,2,3,4
        .code
start:
        mov eax,lengthof  stWndClass   ;执行后eax的值=1,因为只定义了1项WNDCLASS
        ;mov ebx,lengthof WNDCLASS     ;非法的用法

        mov ecx,lengthof szHello      ;ecx的值是13
        ;mov edx,lengthof dword      ;非法的用法

        mov esi,lengthof dwTest    ;esi等于4

        
        
        
        end    start
复制代码

 

 

 

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指令中, 这种限制很好理解, 因为这种情况下, lea指令如何能被代到语句里面呢:

 

addr 和 offset 的区别

一、相同点

1、addr 和 offset 操作符都是获得操作数的偏移地址;
2、addr 和 offset 的处理都是先检查处理的是全局还是局部变量,若是全局变量则把其地址放到目标文件中

二、不同点

1、addr    伪操作符,只能用在 invoke 伪指令语句中;

2、offset 伪操作符可以用在任何可能涉及偏移地址的指令(当然包括 invoke 伪指令)并想获取操作数偏移地址的场合中;
3、addr 不能处理向前引用(即 addr 引用的操作数必须在使用 addr 前就得定义或声明),而offset 则能(不管引用的操作数是其前或其后定义或声明); 

4、addr 是运行阶段在堆栈中分配内存空间,offset 是编译阶段由编译器解释。因此,addr 可以处理局部变量而 offset 则不能

5、addr 如果检查到待处理的变量是局部变量,就在执行 invoke 语句前产生如下指令序列:   

lea    eax,operand
push    eax 

 

总结:为了避免出现错误,建议除在局部变量中引用 addr 操作符外,其它场合使用 offset

 

 

 

 

 

posted @   天子骄龙  阅读(235)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
历史上的今天:
2021-10-13 opencv-SparseMat稀疏矩阵
点击右上角即可分享
微信分享提示

目录导航