MASM中标号和变量的命名规范
MASM中标号和变量的命名规范
MASM中标号和变量的命名规范是相同的,如下:
1) 可以用字母、数字、下划线及符号@、$和?。
2) 第一个符号不能是数字。
3) 长度不能超过240个字符。
4) 不能使用指令名等关键字。
5) 在作用域内必须是唯一的。
全局变量
全局变量的作用域是整个程序
Win32汇编的全部变量定义在.data或.data?段内,这两个段都是可写的。可以同时定义变量的类型和长度。
全局变量的定义格式如下:
变量名 类型 初始值1,初始值2,……
变量名 类型 重复数量 dup (初始值1,初始值2,……)
MASM支持的变量类型如下表:
| 名称 | 表示方式 | 缩写 | 长度(字节) |
| 字节 | Byte | db | 1 |
| 字 | word | dw | 2 |
| 双字(double word) | dword | dd | 4 |
| 三字(far word) | fword | df | 6 |
| 四字(quad word) | qword | dq | 8 |
| 10字节BCD码(ten byte) | tbyte | dt | 10 |
| 有符号字节(sign byte) | sbyte | 1 | |
| 有符号字(sign word) | sword | 2 | |
| 有符号双字(sign dword) | sdword | 4 | |
| 单精度浮点数 | Real4 | 4 | |
| 双精度浮点数 | Real8 | 8 | |
| 10字节浮点数 | Real10 | 10 |
注意:只有定义全局变量的时候,类型才可以用缩写。
在byte类型变量的定义中,可以用引号定义字符串和数值定义的方法混用。
例如:szText db ‘Hello,world!’,0dh,0ah,’Hello again’,0dh,0ah,0
全局变量的初始化:
全局变量在定义中既可以指定初值,也可以只用问号预留空间。
全局变量定义在.data?段中时,只能用问号预留空间,因为.data?段不能指定初始值。
定义时用问号指定的全局变量的初始值是0。
局部变量
局部变量的好处是使程序的模块结构更加分明。
局部变量的缺点是因为空间是临时分配的,所以无法定义含有初始化值的变量,对局部变量的初始化一般在子程序中由指令完成。
局部变量的作用域是单个子程序。
局部变量定义在堆栈中。
局部变量的定义格式如下:
local 变量名1[[重复数量]][:类型],变量名2[[重复数量]][:类型] ……
local是MASM提供的伪指令,用于支持局部变量的定义。有了local伪指令降低不少难度。
定义局部变量需注意以下几点:
a) local伪指令必须紧接在子程序定义的伪指令proc后、其它指令开始之前,因为局部变量的数目必须在子程序开始的时候就确定下来;
b) 定义局部变量时数据类型不能用缩写。如果要定义数据结构,可以用数据结构的名称当作类型;
c) Win32汇编中,参数的默认类型是dword,如果定义dword类型的局部变量,类型可以省略;
d) 当定义数组类型的局部变量时,重复数量可以用“[]”括起来,不能使用定义全局变量的dup伪指令。
e) 局部变量不能和已定义的全局变量同名。
f) 局部变量的作用域是当前的子程序,所以在不同的子程序中可以有同名的局部变量。
局部变量的初始化:
局部变量无法在定义的时候指定初始化值,因为local伪指令只是为局部变量留出空间。
局部变量的初始值是随机的,所以,对局部变量的值一定要初始化。
一般在子程序中使用指令来初始化局部变量。
使用局部变量时的注意点:
a) ebp寄存器是关键,它起到保存原始esp寄存器值的作用;
b) 另外,ebp寄存器随时用做存取局部变量的指针基址,所以绝不能把ebp寄存器用于别的用途;
c) ebp寄存器的值绝对不能被改变,把ebp寄存器的值改掉,程序就玩完;
数据结构
数据结构相当于一种自定义的数据类型,类似C语言中的struct定义。
汇编中,数据结构的定义方法如下:
结构名 struct
字段1 类型 ?
字段2 类型 ?
……
结构名 ends
定义数据结构并不会在某个段中产生数据,只有使用数据结构在数据段中定义数据后,才会产生数据。
使用数据结构在数据段中定义数据的两种方法如下:
第一种定义方法是未初始化的定义方法:
.data?
stWndClass WNDCLASS <>
……
第二种定义方法是定义的同时指定结构中个字段的初始值:
.data
stWndClass WNDCLASS <1,1,1,1,1,1,1,1,1,1>
……
汇编中,对数据结构变量的几种引用方法如下:
a) 最直接的方法:
mov eax,stWndClass.lpfnWndProc
如果stWndClass结构变量在内存中的起始地址是403000h,那么这句指令会被编译成mov eax,[403004h]
b) 在实际使用中,常有使用指针存取数据结构变量的情况:
如果使用esi寄存器做指针寻址
mov esi,offset stWndClass
mov eax,[esi + WNDCLASS.lpfnWndProc]
第二句指令将被编译成mov eax,[esi+4]
c) 使用assume伪指令把寄存器预先定义为结构指针,在进行操作:
mov esi,offset stWndClass
assume esi:ptr WNDCLASS
mov eax,[esi].lpfnWndClass
……
assume esi:nothing
编译后产生同样的代码,不过程序的可读性比较好。
注意:在不使用esi寄存器做指针的时候要用assume esi:nothing取消定义。
结构的嵌套定义如下:
NEW_WNDCLASS struct
dwOption dword ?
oldWndClass WNDCLASS <>
NEW_WNDCLASS ends
引用嵌套的oldWndClass结构变量的lpfnWndProc字段的方法:
assume esi:ptr NEW_WNDCLASS
mov eax,[esi].oldWndClass.lpfnWndProc
……
assume esi:nothing
windows.inc文件定义了大部分Win32 API所涉及的常量和数据结构。
以不同的类型访问变量
MASM中以不同的类型访问不会对变量造成影响。而C语言中的数据类型强制转换过程中,数据的内容已经发生变化。
MASM中,如果要用指定类型之外的长度访问变量,必须显式地指出要访问的长度,这样,编译器忽略语法上的长度校验,仅使用变量的地址。
访问变量是显式地指出要访问长度的方法是:
类型 ptr 变量名
例如:
mov ax,word ptr szBuffer
mov eax,dword ptr szBuffer
类型可以设置为byte、word、dword、fword、qword、real8和real10。
类型必须和操作的寄存器长度匹配,否则无法通过编译。
需要注意的是:
指定类型的访问变量并不会去检测长度是否溢出。
变量的尺寸和数量
sizeof伪操作符可以取得变量、数据类型或数据结构以字节为单位的长度(尺寸)。
格式:
sizeof 变量、数据类型或数据结构名
lengthof伪操作符可以取得变量、数据类型或数据结构中数据的项数(数量)
格式:
length 变量、数据类型或数据结构名
对字符串使用sizeof伪操作符,取得的长度包括结束符0。
需要注意的是:
sizeof伪操作符和length伪操作符取得的数值是编译期产生的,由编译器直接替换到指令中去。所以,在反汇编得到的代码中没有sizeof或lengthof,而只有它们取得的数值。
取得字符串长度的一种特殊情况:
如果szHello的定义分成两行:
szHello db ‘Hello’,0dh,0ah
db ‘World’,0
sizeof szHello得到的数值是7而不是13。
这种定义方式实质为越界使用字符串变量。
MASM中的变量定义只认一行,后一行db ‘World’,0实际上是另一个没有名称的数据定义。
要取得这种字符串的长度时,千万不能用sizeof伪指令,最好是在程序中用lstrlen函数去计算。
获取变量地址
获取全局变量地址和获取局部变量地址的操作是不同的。
因为全局变量定义在数据段中,而局部变量在堆栈中。全局变量的地址可以在编译期确定,而局部变量的地址只能在运行期确定。
全局变量的地址在编译期已经由编译器确定了。
获取全局变量的地址使用offset伪操作符,这个操作在编译期而不是运行期完成。
mov 寄存器,offset 变量名
不可能用offset伪操作符来获取局部变量地址的原因是:
局部变量是用ebp来做指针访问的,由于ebp的值随着程序的执行环境不同可能是不同的,所以局部变量的地址值在编译期也是不确定的。
获取局部变量的地址使用lea指令
lea指令是80386处理器指令集中的一条指令。
lea eax,[ebp-4]
在invoke伪指令的参数中用到某个局部变量的地址,使用MASM提供的伪操作符addr。
格式为:
addr 局部变量名和全局变量名
addr伪操作符即可用于局部变量,也可用于全局变量
使用addr伪操作符需要注意以下几点:
a) 对局部变量取地址的时候,addr伪操作符只能用在invoke的参数中,不能用在如下的mov指令中。
mov eax,addr 局部变量名 ;这是错误的用法
因为在这句mov指令中,编译器无法把addr伪操作符替换成lea指令。
b) 当在invoke中使用addr伪操作符时,在addr伪操作符的左边不能使用eax寄存器,否则eax寄存器的值会被覆盖掉,当然eax寄存器用在addr伪操作符的右边的参数中是可以的。
MASM对于这种情况会报编译期错误。
条件测试语句
MASM的条件测试的语法和C语言相同。
同样,对于不含比较符的单个变量或寄存器,MASM也是将所有非零值认为是“真”,零值认为是“假”。
与C语言的条件测试相同,MASM的条件测试伪操作符并不会改变被测试的变量或寄存器的值。
MASM的条件测试伪操作符经过编译器编译会翻译成类似cmp或test之类的比较或位测试的指令。
MASM条件测试的基本表达式如下:
寄存器或变量 操作符 操作数
两个以上的表达式可以用逻辑运算符连接:
(表达式1)逻辑运算符(表达式2)逻辑运算符(表达式3)…
条件测试中的操作符和逻辑运算符如下表
| 操作符和逻辑运算符 | 操作 | 用途 |
| == | 等于 | 变量和操作数之间的比较 |
| != | 不等于 | 变量和操作数之间的比较 |
| > | 大于 | 变量和操作数之间的比较 |
| >= | 大于等于 | 变量和操作数之间的比较 |
| < | 小于 | 变量和操作数之间的比较 |
| <= | 小于等于 | 变量和操作数之间的比较 |
| & | 位测试 | 将变量和操作数做“与”操作 |
| ! | 逻辑取反 | 对变量取反或对表达式的结果取反 |
| && | 逻辑与 | 对两个表达式的结果进行逻辑“与”操作 |
| || | 逻辑或 | 对两个表达式的结果进行逻辑“或”操作 |
MASM的条件测试语句有如下几点限制:
a) 表达式的左边只能是变量或寄存器,不能为常数;
b) 表达式的两边不能同时为变量,但可以同时为寄存器;
这些限制来自于80×86的指令。
分支语句
MASM中的分支伪指令的语法如下:
.if 条件表达式1
表达式1为“真”时执行的指令
[.elseif 条件表达式2]
表达式2为“真”时执行的指令
[.elseif 条件表达式3]
表达式3为“真”时执行的指令
…
[.else]
所有表达式为“否”时执行的指令
.endif
注意:
关键字if/elseif/else/endif的前面有个小数点,如果不加小数点,就变成宏汇编中的条件汇编伪操作。功能完全不一样。
if/else/endif是宏汇编中条件汇编宏操作的伪操作指令,作用是根据条件决定在最后的可执行文件中包不包括某一段代码。
由.if/.elseif/.else/.endif条件分支伪指令构成的分支结构只能有一个条件被满足。
如果需要构成的分支结构对于所有的表达式为“真”都要执行相应的代码,可以利用多个.if/endif来完成,如下:
.if 表达式1
表达式1为“真”要执行的指令
.endif
.if 表达式2
表达式2为“真”要执行的指令
.endif
循环语句
循环语句的语法如下:
.while 条件测试表达式
指令
[.break[.if 退出条件]] ;如果.break伪指令后面跟一个.if测试伪指令的话,那么当退出条件为“真”时才执行.break伪指令。
[.continue]
.endw
或
.repeat
指令
[.break[.if 退出条件]] ;如果.break伪指令后面跟一个.if测试伪指令的话,那么当退出条件为“真”时才执行.break伪指令。
[.continue]
.until 条件测试表达式(或.untilcxz [条件测试表达式])
其中,.while/.break/.continue/.endw/.repeat/.until/.untilcxz都是伪指令。
循环体中可以使用.break伪指令强制退出循环。
循环体中可以使用.continue伪指令忽略以后的指令。
.while/.endw和.repeat/.until的区别如下:
a) 前者可能一次也不会执行循环体内的指令,而后者至少会执行一次循环体内的指令。
b) 前者当判断条件为FALSE时退出循环,而后者当判断条件为TRUE时退出循环。
MASM的条件测试总是把操作数当作无符号数看待。
这就是说,在分支和循环的伪指令反汇编后可以发现,在使用>,>=,<和<=比较符时,MASM的伪指令总是将比较以后的跳转指令使用为jb和jnb等无符号数比较跳转的指令。
所以,如果程序中需要构造有符号数的比较分支或循环结构,那么必须另外用jl和jg等有符号数比较跳转的指令来完成,使用条件测试配合分支或循环伪指令可能会得到错误的结果