一、关于寄存器
寄存器有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 |
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个字节) |
语言类型 | 最先入栈参数 | 平衡堆栈者 | 允许使用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)... 条件测试中的操作符和逻辑运算符如下表
操作符和逻辑运算符 | 操作 | 用途 |
== | 等于 | 变量和操作数之间的比较 |
!= | 不等于 | 变量和操作数之间的比较 |
> | 大于 | 变量和操作数之间的比较 |
>= | 大于等于 | 变量和操作数之间的比较 |
< | 小于 | 变量和操作数之间的比较 |
<= | 小于等于 | 变量和操作数之间的比较 |
& | 位测试 | 将变量和操作数做“与”操作 |
! | 逻辑取反 | 对变量取反或对表达式的结果取反 |
&& | 逻辑与 | 对两个表达式的结果进行逻辑“与”操作 |
|| | 逻辑或 | 对两个表达式的结果进行逻辑“或”操作 |
更多推荐
WIN32汇编基础
发布评论