一、关于寄存器
寄存器有EAX,EBX,ECX,EDX,EDI,ESI,ESP,EBP等,似乎IP也是寄存器,但只有在CALL/RET在中会默认使用它,其它情况很少使用到,暂时可以不用理会。
EAX是WIN32 API 默认的返回值存放处。
ECX是LOOP指令自动减一的寄存器。
ESP是堆栈指针。
EBP经常用来在堆栈中寻址。
ESI好像常常用在指针寻址中,EDI不大清楚。
二、关于内存寻址
WIN32中内存是平坦的,对于每个程序来说都可以使用2G范围的地址,但各个程序之间并不会干扰,这是因为各个程序所使用到的物理内存被Windows自行安排,不会互相覆盖,而且一个程序不会随意地访问到另一个程序的地址空间。
三、关于堆栈
Windows为每个程序安排了堆栈段,它是从高地址向低地址延伸的,之所以采用这种方式,是因为这样可以使堆栈指针始终指向最近入栈的元素的起始地址,这样的话,为访问这个元素提供了非常便利的方式。
ESP作为堆栈指针始终指向栈顶,如果看一下PUSH和POP的操作就可以明白这句话:
PUSH: ESP <-- ESP-4 (ESP+3,ESP) <-- 入栈元素
POP: 出栈元素 <-- (ESP+3,ESP) ESP <-- ESP+4
因为PUSH和POP自动修改了ESP的值,使它始终指向栈顶了。当然也可以自己来修改ESP的值,例如我们可以:
sub esp,4 ;这样就把栈顶指针向下移动了。
这种操作常常用在局部变量的分配中,在子程序中使用到局部变量时,就在堆栈中为它们提供空间,这样可以使子程序退出时收回局部变量占用的空间,有利于子程序的模块化。
我们可以用ESP来寻址堆栈中的元素,比如ESP指向当前栈顶元素的起始地址,ESP-4指向前一个元素的起始地址,不过因为ESP常常在变化,这样用ESP在堆栈中寻址的话不方便,所以我们就用EBP来代替ESP寻址,首先把EBP入栈保存,然后把ESP赋值给EBP,这样就可以用EBP来寻址堆栈中的数据了。我用一个例子来说明堆栈的变化。
push 0x00000001 ;1
push ebp ;2
mov ebp,esp ;3
push 0x12345678 ;4
mov eax,dword ptr[ebp+4] ;5
mov ebx,dword ptr[ebp-4] ;6
mov ax,word ptr[ebp-2] ;7
mov al,byte ptr[ebp-1] ;8
mov al,byte ptr[ebp-3] ;9
mov ax,word ptr[ebp-3] ;10
5 eax=0x00000001
6 ebx=0x12345678
7 ax=0x1234
8 al=0x12
9 al=0x56
10 ax=0x3456
堆栈使用在子程序的实现中,当调用子程序时,首先把参数入栈,然后把返回IP入栈,然后转移到子程序处,如果有局部变量,则下移ESP,然后初始化该局部变量,这样用到EBP来寻址局部变量,参数的寻址同样要用到EBP。
四、简单的几个关键字
ptr 显式指定后面的数据的类型
offset 全局变量的地址
addr 局部变量的地址,也可以用在全局变量上
local 定义局部变量
proc 定义子程序
proto 声明子程序
五、例子
Hello.asm文件的内容如下:
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 第一部分:模式和源程序格式的定义语句
.386 ; 指令集
.model flat,stdcall ; 工作模式
option casemap:none ; 格式
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; Include 文件定义
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
include windows.inc
include user32.inc
includelib user32.lib
include kernel32.inc
includelib kernel32.lib
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 数据段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
.data
szCaption db 'A MessageBox !',0
szText db 'Hello, World !',0
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 代码段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
.code
start:
invoke MessageBox,NULL,offset szText,offset szCaption,MB_OK
invoke ExitProcess,NULL
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
end start ; 指定程序的入口
1. 第一部分模式和源程序格式的定义语句
第一行 指定使用的指令集(编译器使用)
Win32环境工作在80386及以上的处理器中,所以必须定义.386。如果程序(VxD等驱动程序)中要用到特权指令,那么必须定义.386p。
第二行 定义程序工作的模式(包括内存模式、语言模式、其它模式)
对Win32程序来说,只有一种内存模式,即flat(平坦)模式。
Win32 API调用使用的是stdcall格式,所以Win32汇编中必须在.model中加上stdcall参数。
第三行 option语句
由于Win32 API中的API名称区分大小写,所以必须定义option casemap:none,来表明程序中的变量和子程序名对大小写敏感。
2. 包含全部段的源程序结构:
.386
.model flat,stdcall
option casemap:none
<一些include语句>
.stack [堆栈段的大小]
.data
<一些初始化过的变量定义>
.data?
<一些没有初始化过的变量定义>
.const
<一些常量定义>
.code
<代码>
<开始标记>
<其他语句>
end 开始标记
3. 段的定义
数据段
.data
已初始化数据段,可读可写的已定义变量;
当程序装入完成时,这些值就已经在内存中;
数据定义在.data段中会增加可执行文件的大小;
.data段一般存放在可执行文件的_DATA节区(Section)内;
.data?
未初始化数据段,可读可写的未定义变量,在可执行文件中不占空间;
这些变量一般作为缓冲区或者在程序执行后才开始使用。
数据定义在.data?数据段中不会增加可执行文件的大小;
.data?段一般存放在可执行文件的_BSS节区内;
.const
常量,可读不可写的变量;
代码段
.code
所有的指令都必须写在代码段中;
Win32中,数据段是不可执行的,只有代码段有可执行的属性;
对于运行在特权级3的应用程序,.code段不可写。除非把可执行文件PE头部中的属性位改成可写;
对于运行在特权级0的程序,所有的段都有读写权限,包括代码段;
.code代码段一般存放在可执行文件的_TEXT节区内;
堆栈段
.stack
与DOS汇编不同,Win32汇编不必考虑堆栈。系统会自动分配堆栈空间;
堆栈段的内存属性是可读写并且可执行;
靠动态修改代码的反跟踪模块可以拷贝到堆栈中去边修改边执行;
缓冲区溢出技术也会用到这个特性;
4. 调用操作系统功能的方法:
DOS下
操作系统的功能通过各种软中断来实现。
应用程序调用操作系统功能将经历如下三个过程:
把相应的参数放在各个寄存器中再调用相应的中断;
程序控制权转到中断中去执行;
完成以后通过iret中断返回指令回到应用程序中;
DOS下调用系统功能方法的缺点:
所有的功能号定义是难以记忆的数字;
80x86系列处理器能处理的中断最多只能有256个;
通过寄存器来传递参数,对于参数较多的函数很不方便;
Win32下
系统功能模块放在Windows的动态链接库(DLL)中
作为Win32 API核心的3个DLL:
KERNEL32.DLL 系统服务功能。
GDI32.DLL 图形设备接口。
USER32.DLL 用户接口服务。
常用API的参数和函数声明,查看文档《Microsoft Win32 Programmer's Reference》
5. Win32 API的函数原型声明
函数原型声明的汇编格式如下:
函数名 proto [距离] [语言] [参数1]:数据类型, [参数2]:数据类型,......
proto是函数声明的伪指令
距离可以设置为NEAR、FAR、NEAR16、NEAR32、FAR16或FAR32,由于Win32中只有一个平坦的段,无所谓距离,所以在定义时可以忽略距离。
语言类型可是使用.model所定义的默认值。
以消息对话框函数MessageBox为例
C格式如下:
int MessageBox(
HWND hWnd, // Handle to owner window
LPCTSTR lpText, // text in message box
LPCTSTR lpCaption, // message box title
UINT uType // message box style
);
汇编格式如下:
MessageBox Proto hWnd:dword,lpText:dword,lpCaption:dword,uType:dword
或者写为
MessageBox Proto :dword,:dword,:dword,:dword
编译器只对参数的数量和类型感兴趣,参数的名称只是增加可读性,所以可以省略。
对于汇编语言来说,Win32环境中的参数实际上只有一种类型,就是一个32位的整数(dword,double word),双字,四字节。
6. 调用Win32 API
调用API有如下两种方法:
1) invoke
invoke是MASM提供的伪指令;
invoke伪指令的好处就是能够提高代码的可读性,减少错误;
invoke做了下面三件事:
在编译的时候,由编译器把invoke伪指令展开成相应的push指令和call指令;
进行参数数量的检查工作;
如果带的参数数量和声明时的数量不符,编译器会报错;
2) push和call的组合
80386处理器的指令
invoke MessageBox,NULL,offset szText,offset szCaption,MB_OK
也可写为
push NULL
push offset szText
push offset szCaption
push MB_OK
call MessageBox
7. Win32 API函数返回值的处理方法
对于汇编语言来说,Win32 API函数返回值的类型只有dword一种类型,它永远放在eax中。
如果要返回的内容在一个eax中放不下,Win32 API采用如下方法来解决:
a) 一般是eax中返回一个指向返回数据的指针;
b) 在调用参数中提供一个缓冲区地址,数据直接返回到这个缓冲区中去。类似变参的概念;
8. 与字符串相关Win32 API的分类
在Win32环境中,根据两个不同的字符集(ANSI字符集和Unicode字符集),可以把和字符串相关的API分成两类:
a) 处理ANSI字符集的Win32 API函数
函数名称的尾部带一个“A”字符;
ANSI字符串是以NULL结尾的一串字符数组,每一个ANSI字符占一个字节的宽度;
例如:MessageBoxA Proto hWnd:dword,lpText:dword,lpCaption:dword,uType:dword
b) 处理Unicode字符集的Win32 API函数
函数名称的尾部带一个“W”字符;
每一个Unicode字符占两个字节的宽度,所以可以同时定义65536个不同的字符;
例如:MessageBoxW Proto hWnd:dword,lpText:dword,lpCaption:dword,uType:dword
Windows 9x系列不支持Unicode版本的API,绝大多数的API只有ANSI版本。
只有Windows NT系列才完全支持Unicode版本的API。
为了编写在几个平台中都能通用的程序,一般应用程序都使用ANSI版本的API函数集。
提高程序可移植性的一个方法:
一般在源程序中不直接指明使用Unicode还是ANSI版本,而是使用宏汇编中的条件汇编功能来统一替换。
比如,在头文件中做如下定义:
if UNICODE
MessageBox equ <MessageBoxW>
else
MessageBox equ <MessageBoxA>
endif
然后在源程序的头部指定UNICODE=1或UNICODE=0,重新编译后就能产生不同的版本。
9. include语句
include语句的语法是:
include 文件名
或
include <文件名>
用“<>”将文件名括起来,可以避免党文件名和MASM的关键字同名时引起编译器混淆。
include语句的作用:
解决了所用到的Win32 API函数都必须预先声明的麻烦。
把所有用到的Win32 API函数声明预先放在一个头文件中,然后用include语句包含进源程序。
编译器对include语句的处理方法,仅是简单地用指定的文件内容把这行include语句替换掉而已。
和C语言中的#include作用类似。
10. includelib语句
includelib语句的语法是:
includelib 库文件名
或
includelib <库文件名>
用“<>”将文件名括起来,同样可以避免当文件名和MASM的关键字同名时引发编译器混淆。
includelib语句的作用是:
告诉链接器使用哪些导入库。
导入库
WIN32中,API函数的实现代码放在DLL中,导入库中只留有API函数的定位信息和参数数目等简单信息。
DOS下的函数库是静态库
C语言的函数库是典型的静态库
静态库的好处是节省大量的开发时间。
静态库的缺点是每个可执行文件中都包含了要用到的相同函数的代码,即占用了大量的磁盘空间,执行的时候,这些代码也会重复占用内存。
includelib语句和include语句的处理不同,includelib不会把.lib文件的内容插入到源程序中,它只是告诉链接器在链接的时候到指定的库文件中去找Win32 API函数的位置信息而已。
11. MASM中标号和变量的命名规范
MASM中标号和变量的命名规范是相同的,如下:
1) 可以用字母、数字、下划线及符号@、$和?。
2) 第一个符号不能是数字。
3) 长度不能超过240个字符。
4) 不能使用指令名等关键字。
5) 在作用域内必须是唯一的。
12. 标号
标号有如下两种定义方法:
标号名: 目的指令 ;方法1
或
标号名:: 目的指令 ;方法2
方法1和方法2是不同的
方法1
标号名的后面跟一个冒号,表示标号的作用域是当前的子程序。
在单个子程序中的标号不能同名,不能从一个子程序中用跳转指令跳到另一个子程序中。
方法2
标号名的后面跟两个冒号,表示标号的作用域是整个程序。
对任何其它子程序都是可见的。
在低版本MASM中,默认标号的作用域是整个程序。
在高版本MASM中,默认标号的作用域是当前的子程序。
高版本MASM中的@@标号
当用@@做标号时,可以用@F和@B来引用;
@F表示本条指令后的第一个@@标号;
@B表示本条指令前的第一个@@标号;
不要在间隔太远的代码中使用@@标号,源程序中@@标号和跳转指令之间的距离最好限制在编辑器能够显示的同一屏幕的范围内。
13. 全局变量
全局变量的作用域是整个程序
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。
14. 局部变量
局部变量的好处是使程序的模块结构更加分明。
局部变量的缺点是因为空间是临时分配的,所以无法定义含有初始化值的变量,对局部变量的初始化一般在子程序中由指令完成。
局部变量的作用域是单个子程序。
局部变量定义在堆栈中。
局部变量的定义格式如下:
local 变量名1[[重复数量]][:类型],变量名2[[重复数量]][:类型] ......
local是MASM提供的伪指令,用于支持局部变量的定义。有了local伪指令降低不少难度。
定义局部变量需注意以下几点:
a) local伪指令必须紧接在子程序定义的伪指令proc后、其它指令开始之前,因为局部变量的数目必须在子程序开始的时候就确定下来;
b) 定义局部变量时数据类型不能用缩写。如果要定义数据结构,可以用数据结构的名称当作类型;
c) Win32汇编中,参数的默认类型是dword,如果定义dword类型的局部变量,类型可以省略;
d) 当定义数组类型的局部变量时,重复数量可以用“[]”括起来,不能使用定义全局变量的dup伪指令。
e) 局部变量不能和已定义的全局变量同名。
f) 局部变量的作用域是当前的子程序,所以在不同的子程序中可以有同名的局部变量。
局部变量的初始化:
局部变量无法在定义的时候指定初始化值,因为local伪指令只是为局部变量留出空间。
局部变量的初始值是随机的,所以,对局部变量的值一定要初始化。
一般在子程序中使用指令来初始化局部变量。
RtlZeroMemory这个Win32 API函数实现将整个数据结构填0的功能,类似C语言的memset。
在原来的DOS环境下,低版本的MASM中,所有的变量都相当于现在所说的全局变量,都定义在数据段里面。
用汇编语言在堆栈中定义局部变量非常麻烦,需要作一张表,表上的内容是局部变量名和 ebp指针的位置关系。
15. 使用局部变量的一个典型例子与反汇编得到指令的比较:
TestProc proc
local @loc1:dword,@loc2:word
local @loc3:byte
mov eax,@loc1
mov ax,@loc2
mov al,@loc3
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
其中的
push ebp ; 把原来ebp寄存器的值保存起来;
mov ebp,esp ; 把esp寄存器的值复制到ebp寄存器中,供存取局部变量时做指针用;
add esp,FFFFFFF8 ; 在堆栈中预留出空间(即重新设置堆栈指针),由于堆栈是向下增长,所以要把esp加上一个负值。
三条指令用于局部变量的准备工作。
在堆栈中预留出空间时,把esp加上(-8),而不是加上(-7),是因为在80386处理器中,以dword为界对齐时存取内存的速度最快。以空间换时间。
leave是80386指令集中的一条指令,用于局部变量的扫尾工作。
一条leave指令就实现了mov esp,ebp和pop ebp两条指令的功能。
mov esp,ebp ; ebp寄存器中保存了正确的初始esp值,所以把正确的esp设置回去后,ret指令就能从堆栈中取出正确的地址返回。
pop ebp ; 执行这条语句之后,堆栈就是正确的。
由于esp寄存器在程序的执行过程中可能随时会被用到,所以不可能用esp寄存器做指针来存取堆栈中的局部变量。
ebp寄存器也是以堆栈段为默认数据段的,所以可以用ebp做指针来存取堆栈中的局部变量。
局部变量在堆栈中排列的顺序如下表:
ebp偏移
|
内容
|
ebp+4
|
由call指令推入的返回地址。
|
ebp
|
push ebp指令推入的原ebp值,然后新的ebp就等于当前的esp寄存器的值。
|
ebp-4
|
第一个局部变量@loc1:dword (4个字节)
|
ebp-6
|
第二个局部变量@loc2:word (2个字节)
|
ebp-7
|
第三个局部变量@loc3:byte (1个字节)
|
使用局部变量时的注意点:
a) ebp寄存器是关键,它起到保存原始esp寄存器值的作用;
b) 另外,ebp寄存器随时用做存取局部变量的指针基址,所以绝不能把ebp寄存器用于别的用途;
c) ebp寄存器的值绝对不能被改变,把ebp寄存器的值改掉,程序就玩完;
16. 数据结构
数据结构相当于一种自定义的数据类型,类似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所涉及的常量和数据结构。
17. 以不同的类型访问变量
MASM中以不同的类型访问不会对变量造成影响。而C语言中的数据类型强制转换过程中,数据的内容已经发生变化。
MASM中,如果要用指定类型之外的长度访问变量,必须显式地指出要访问的长度,这样,编译器忽略语法上的长度校验,仅使用变量的地址。
访问变量是显式地指出要访问长度的方法是:
类型 ptr 变量名
例如:
mov ax,word ptr szBuffer
mov eax,dword ptr szBuffer
类型可以设置为byte、word、dword、fword、qword、real8和real10。
类型必须和操作的寄存器长度匹配,否则无法通过编译。
需要注意的是:
指定类型的访问变量并不会去检测长度是否溢出。
80386的字节序是:
低位数据在低地址,高位数据在高地址
举例:
下面这段代码存在长度溢出的问题。长度溢出即越界存取到相邻的其它变量。
.data
bTest1 db 12h
wTest2 dw 1234h
dwTest3 dd 12345678h
......
.code
......
mov al,bTest1
mov ax,word ptr bTest1
mov eax,dword ptr bTest1
......
通过反汇编后的内容如下:
; .data段中的变量
:00403000 12 ; 从这里开始的1个字节是变量bTest1
:00403001 34 ; 从这里开始的2个字节是变量wTest2
:00403002 12
:00403003 78 ; 从这里开始的4个字节是变量dwTest3
:00403004 56
:00403005 34
:00403006 12
; .code段中的代码
:00401000 A000304000 mov al,byte ptr [00403000]
:00401005 66A100304000 mov ax,word ptr [00403000]
:0040100B A100304000 mov eax,dword ptr [00403000]
运行结果:
al等于12h
ax等于 3412h
eax 等于 78123412h
从例子可以看出,汇编中用ptr强制覆盖变量长度的时候,实质上只用了变量的地址,编译器并不会考虑定界的问题。
movzx指令用于数据长度的扩展
movzx
指令是80386处理器提供的扩展指令,该指令总是将扩展的数据位用0代替。
movzx指令是安全的强制类型转换方式。
能够像C语言的强制类型转换一样,把一个字节扩展到一个字或一个双字再放到ax或eax中,高位保持0而不是越界存取到其它的变量中。
movsx指令可以完成带符号位的扩展
movsx指令是80386处理器提供的扩展指令;
当被扩展数据的最高位为0时,效果和movzx指令相同;当最高位为1时,则扩展部分的数据位全部用1填充。
18. 变量的尺寸和数量
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函数去计算。
19. 获取变量地址
获取全局变量地址和获取局部变量地址的操作是不同的。
因为全局变量定义在数据段中,而局部变量在堆栈中。全局变量的地址可以在编译期确定,而局部变量的地址只能在运行期确定。
全局变量的地址在编译期已经由编译器确定了。
获取全局变量的地址使用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对于这种情况会报编译期错误。
20. 使用子程序
Win32汇编中的子程序也采用堆栈来传递参数,所以可以用invoke伪指令来调用子程序,并进行语法检查工作。
子程序的定义方式如下:
子程序名 proc [距离] [语言类型] [可视区域] [USES 寄存器列表] [,参数:类型]...[VARARG]
local 局部变量列表
指令
子程序名 endp
proc伪指令和endp伪指令用于定义子程序开始和结束的位置。
子程序有如下属性:
a) 距离 Win32中只有一个平坦的段,无所谓距离,所以对距离的定义往往忽略。
b) 语言类型 表示参数的使用方式和堆栈平衡的方式,如果忽略,则使用程序头部.model定义的值。
c) 可视区域 可以设为PRIVATE、PUBLIC和EXPORT,默认是PUBLIC。
PRIVATE表示子程序只对本模块可见;
PUBLIC表示子程序对所有模块可见(在最后编译链接完成的可执行文件中);
EXPORT表示子程序是DLL的导出函数;
d) USES寄存器列表 表示由编译器在子程序指令开始前自动安排push这些寄存器的指令,并且在ret前自动安排pop指令,用于保护执行环境。
一种更方便的做法是,在子程序的开头和结尾用pushad指令和popad指令一次保存和恢复所有寄存器。
e) 参数和类型 参数指参数的名称,在定义参数名的时候不能跟全局变量和子程序中的局部变量重名。
对于类型,由于Win32中的参数类型只有32位(dword)一种类型,所以可以省略。
在参数定义的最后还可以跟VARARG,表示在已确定的参数后还可以跟多个数量不确定的参数。
在写源程序的时候有意识地把子程序的位置提到invoke语句的前面,省略掉proto语句,可以简化程序和避免出错。
参数传递和堆栈平衡
在调用子程序时,参数的传递是通过堆栈进行的。
调用者把传递给子程序的参数压入堆栈,子程序从堆栈中取出相应的值来使用。
调用约定,约定了参数入栈的顺序和由谁(调用者或子程序)来平衡堆栈。
由于各种语言默认的调用约定是不同的,所以在proc以及proto语句的语言属性中确定语言类型后,编译器才能将invoke为指令翻译成正确的样子。
不同语言调用方式的差别如下表:
语言类型
|
最先入栈参数
|
平衡堆栈者
|
允许使用VARARG
|
C
|
右
|
调用者
|
是
|
SysCall
|
右
|
子程序
|
是
|
StdCall
|
右
|
子程序
|
是
|
BASIC
|
左
|
子程序
|
否
|
FORTRAN
|
左
|
子程序
|
否
|
PASCAL
|
左
|
子程序
|
否
|
注:VARARG表示参数的个数可以是不确定的,如wsprinitf,StdCall的堆栈清除平时是由子程序完成的,但使用VARARG时是由调用者清除的。
从上表可以看出只有C语言是调用者平衡堆栈,其他语言类型都是被调用者来平衡堆栈。
因为Win32约定的类型是StdCall,所以在程序中调用子程序或系统API后,不必自己来平衡堆栈,免去了很多麻烦。
存取参数和局部变量都是通过堆栈来实现的,和存取局部变量类似,参数的存取也是通过ebp做指针来完成的。
所有对局部变量使用的限制几乎都可以适用于参数。
21. 条件测试语句
MASM的条件测试的语法和C语言相同。
同样,对于不含比较符的单个变量或寄存器,MASM也是将所有非零值认为是“真”,零值认为是“假”。
与C语言的条件测试相同,MASM的条件测试伪操作符并不会改变被测试的变量或寄存器的值。
MASM的条件测试伪操作符经过编译器编译会翻译成类似cmp或test之类的比较或位测试的指令。
MASM条件测试的基本表达式如下:
寄存器或变量 操作符 操作数
两个以上的表达式可以用逻辑运算符连接:
(表达式1)逻辑运算符(表达式2)逻辑运算符(表达式3)...
条件测试中的操作符和逻辑运算符如下表
操作符和逻辑运算符
|
操作
|
用途
|
==
|
等于
|
变量和操作数之间的比较
|
!=
|
不等于
|
变量和操作数之间的比较
|
>
|
大于
|
变量和操作数之间的比较
|
>=
|
大于等于
|
变量和操作数之间的比较
|
<
|
小于
|
变量和操作数之间的比较
|
<=
|
小于等于
|
变量和操作数之间的比较
|
&
|
位测试
|
将变量和操作数做“与”操作
|
!
|
逻辑取反
|
对变量取反或对表达式的结果取反
|
&&
|
逻辑与
|
对两个表达式的结果进行逻辑“与”操作
|
||
|
逻辑或
|
对两个表达式的结果进行逻辑“或”操作
|
MASM的条件测试语句有如下几点限制:
a) 表达式的左边只能是变量或寄存器,不能为常数;
b) 表达式的两边不能同时为变量,但可以同时为寄存器;
这些限制来自于80x86的指令。
以下一些系统标志寄存器中的各种标志位的状态指示,本身相当于一个表达式:
CARRY? 表示Carry位是否置位
OVERFLOW? 表示Overflow位是否置位
PARITY? 表示Parity位是否置位
SIGN? 表示Sign位是否置位
ZERO? 表示Zero位是否置位
22. 分支语句
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
23. 循环语句
循环语句的语法如下:
.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等有符号数比较跳转的指令来完成,使用条件测试配合分支或循环伪指令可能会得到错误的结果。