您的位置:首页 > 其它

第三章 使用MASM

2015-08-23 20:06 274 查看
一。Win32汇编源程序的结构
.386 ;告知编译器本程序使用的指令集,使用“.386p”则表示程序可以使用像“mov cr0,eax”这样的特权指令
.model flat,stdcall ;内存模式Win32使用flat,stdcall为子程序调用方式,指出了子程序或Win32API时调用参数传递的次序和堆栈平衡的方式。
option casemap:none ;用来指定是否对大小写敏感,由于Win32API区分大小写,所以必须为none
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; Include 文件定义
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
include windows.inc
include user32.inc
includelib user32.lib
include kernel32.inc
includelib kernel32.lib
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 数据段 .data、,data?、,const都是数据段,win32汇编不必考虑堆栈,系统会为程序分配一个向下扩展的、足够大的段作为堆栈段,所以.stack定义常常被忽略
;1.可读可写的已定义变量:在源程序中已经被定义了初始值,而且在程序执行中有可能被更改,这些数据必须被定义在.data段,.data段一般存放在可执行文件的_DATA节区
;2.可读可写的未定义变量:变量一般当做缓冲区或者程序执行才开始使用的,.data段和.data?段都可以使用。但是定义在.data?段中不会增大.exe文件的大小,放在_BSS节区
;3.常亮:要显示的一些字符串信息,放在.const段,是常亮段,是可读不可写的。
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
.data

szCaption db 'A MessageBox !',0
szText db 'Hello, World !',0
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 代码段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
.code ;代码段,一般放在可执行文件的_TEXT节区。代码段的属性是由可执行文件PE头中的属性位决定的。
start:
invoke MessageBox,NULL,offset szText,offset szCaption,MB_OK
invoke ExitProcess,NULL
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
end start
;当源程序的某一行过长,不利于阅读的时候,可以分行书写,使用“\”做换行符。
二 。调用API
Win32的系统功能模块放在Windows的动态链接库DLL中,DLL是Windows的一种可执行文件,采用的是和.exe文件一样的PE格式,在PE格式文件头的导出表,以字符串形式指出了这个DLL能踢狗的函数列表。
Win32API的3个核心DLL

KERNEL32.DLL----系统服务功能。包括内存管理、任务管理和动态链接等
GDI32.DLL----图形设备接口。利用VGA与DRV之类的显示设备驱动程序完成显示文本和矩形等功能
USER32.DLL----用户接口服务。建立窗口和传送消息。

Win32环境下的参数实际上只有一种类型,那就是32为的证书,都是dword。
Win32API调用中要把参数放入堆栈,顺序是最后一个参数最先近战。
在源程序编译链接成可执行文件之后,call MessageBox语句中的MessageBox会被换成一个地址,指向可执行文件的PE头中导入表,导入表中指向MessageBox函数的实际地址会在程序装入内存的时候,根据User32.dll在内存中的位置由Windows系统动态的填入。
invoke MessageBox,NULL,offset szText,offset szCaption,MB_OK
invoke伪指令,用于调用函数,编译器会自动的把上面的指令展开成我们需要的4个push和一个call指令,同时进行参数数量的检查工作。
返回值:汇编程序的返回值只有dword,永远存放在eax中。如果要返回的内容不是一个eax所能够容纳下的,win32API采用的方法一般是用eax中返回一个指向返回数据的指针,或者在调用参数中提供一个缓冲区地址,干脆把数据直接返回到缓冲区中。
.
函数声明:调用前,必须预先声明。声明格式 函数名 proto 【距离】 【语言】 【参数1】:数据类型,【参数2】:数据类型,……
MessageBox Proto hWnd:dword,lpText:dword,lpCaption:dword,uType:dword
MessageBox Proto :dword,:dword,:dword,:dword
win32只有flat,无所谓距离,忽略,使用.model定义的默认值。win32API只使用dword,而且编译器只关心参数的数量,不关心参数名称,所有可以省略。
win32环境中,和字符串相关的API有两种。Unicode和ANSI,ANSI一个字节,Unicode两字节,一般ANSI版本的函数尾有“A”,Unicode的“W”==WideChar。
include语句:
不但要包含DLL文件本事,还要include DLL文件中的API函数声明列表==》《DLL名.inc》文件; include语句还用来在源程序中包含其他文件。当遇到的文件名可能会和MASM的关键词名称有冲突的时候,使用<>将名字包含起来。
includelib语句:使用API函数,必须知道这个函数在那个DLL文件中,要不然得在DLL中搜索。必须有个文件包括了dll库正确的定位信息。通过导入库来实现。一个dll文件一个导入库。和include语句不通,includelib不会把.lib文件插入到源程序中,它只是告诉连接器在链接的时候到指定的库文件中去找API函数的位置信息而已。

三、标号、变量和数据结构
(1)@@:很多时候跳转需要设置一个跳转标号,loc1什么的如
mov cx,1234h mov cx,1234h
cmp flag,1 cmp flag,1
jz loc1 jz @F
mov cx,100h mov cx,1000h
loc1:………… @@:……
………… ……
loop loc1 loop @B
程序很多的地方得命名只用一两次的标号,所以有了@@
用@@代表标号,@F和@B来引用它,@F表示本条指令后的第一个@@,@B表示本条指令前的第一个@@;程序汇总可以有多个@@标号,但@B和@F只匹配最近的。

(2)全局变量: 变量名 类型 初始值1,初始值2,…………
变量名 类型 重复数量 dup (初始值1,初始值2,…………)
所有使用到变量类型的情况中,只有定义全局变量的时候类型才可以用缩写。
wHour dw ?; wMinute dw 10; word_buffer bw 100 dup (1,2);szText db ‘hello world’ ;换行和回车:0dh,0ah
全局变量初始化:既可以指定初始值,也可以只用问好预留空间。但是在.data?段中,只能用问好来预留空间,不能再里面指定初始值。虽然未初始化,但是默认为0;

(3)局部变量:两个以上子程序都用到的数据才被定义为全局变量统一存放在数据段,仅在子程序内部使用的变量则放在堆栈中。由于局部变量是空间临时分配的,所以无法定义含有初始化的变量,对局部变量的初始化一般在子程序中由指令完成。
局部变量定义与全局不同,除了在要开头加上local外,定义多个重复空间的时候不适用dup而是用【】把数量括起来。
local loc1【1024】:byte; local loc2:WNDCLASS ;local loc3; 默认dword
local伪指令必须紧接在子程序定义的伪指令proc后、其他指令开始之前,因为局部变量的数目必须在子程序开始的时候就确定下来。数据结构可以当做类型名,且数据类型不能缩写。局部变量不能和已有的全局变量同名。默认是dword,不写就是dword
我们一般定义局部变量名的时候在前面加上@,来表明他是个局部变量。
TestProc proc push ebp 将原来的ebp保存用作指针
local @loc1:dword,@loc2:word mov ebp,esp 将原来的栈顶高地址作为新的栈底
local @loc3:byte add esp,ffffff8 fffffff8为负数,esp-8低地址作为新栈顶,虽然函数的变量只占用了7个字节,但是用8,存取内存更快,因

为80386处理器默认dword。

mov eax,@loc1 mov eax,dword ptr [ebp-4]
mov ax,@loc2 mov ax ,word ptr [ebp-06]
mov al,@loc3 mov al ,byte ptr [ebp-07]
ret leave
等于mov esp.ebp + pop ebp
TestProc endp ret 从堆栈中读取返回地址,返回。
在源程序调用TestProc的时候使用call 函数或者invoke的时候,其实已经将要用的自定义变量或者传输的变量推进的栈中。TestProc子程序自我实现的时候,也从新定义的栈顶和栈底,只是为了将通过堆栈传输的变量提取出来罢了,因为子母程序都使用的是同一个堆栈。
局部变量的初始化:局部变量的初始值是随机的,是其他子程序执行后在堆栈里面留下的垃圾。所以在调用API的时候都是先将整个数据结构填0,然后再初始化要用的字段。RtlZeroMemory来时先填0

(4)数据结构:
C语言 汇编
typedef struct _WNDCLASS{ WNDCLASS struct
UNIT style; style DWORD ?
WNDPROC lpfnWndProc; lpfnWndProc DWORD ?
int cbClsExtra; cbClsExtra DWORD

int cbWndExtra; cbWndExtra DWORD

HINSTANCE hInstance; hInstance
DWORD ?
HICON hIcon; hIcon DWORD

……………… ………………
} WNDCLASS,*PWNDCLASS WNDCLASS ends
定义一个以WNDCLASS为结构的变量stWndClass。注意还变是先写 变量名 然后再写变量的格式
(1)未初始化定义: stWndClass WNDCLASS <>
(2)初始化结构的所有字段都为1: stWndClass WNDCLASS <1,1,1,1,1,1,1,1,1,1>
引用stWndClass 结构中的 lpfnWndProc 字段:mov eax,stWndClass. lpfnWndProc

假如stWndClass在内存中的字段是403000h,那么这句话就会被编译成mov
eax,[403004h],因为 lpfnWndProc是结构的第二个字段,且每一个字段都是4字节的dword所以在第4个开始。

若使用指针存取数据结构,使用esi寄存器作为指针寻址的时候,可以如下

mov esi, offset stWndClass

mov eax, [esi + WNDCLASS. lpfnWndProc] 注意此处是 WNDCLASS.
lpfnWndProc,而不是stWndClass. lpfnWndProc

如果要使用这种方法,大批量的对单一数据结构进行操作可以是用assume伪指令把寄存器预定义为结构指针,然后在操作

mov esi, offset stWndClass

assume esi:ptr WNDCLASS

mov eax,[esi]. lpfnWndProc

……

assume esi:nothing
注:结构可以嵌套使用。

(5)变量的使用

x64汇编不会像x86汇编一样自动转型,szBuffer db 1024 dup(?) mov ax,szBuffer 在win32中是不行的,要使用ptr进行匹配。
在进行不同类型数据操作,使用ptr强制覆盖长度,并不会想c语言一样高位强制为0,而只是进行简单的地址扩展,不会考虑定界问题,如果要像C语言一样,使用movzx高位为0,使用movsx可以完成带符号位的扩展,最高位为0的时候和movzx一样,最高位为1的时候扩展数据位直接全部为1
sizeof伪指令可以取得变量、数据结构或者数据类型以字节为单位的长度、lengthof可以取得变量中定义类型数据的项数。
lengthof后用变量名 szhello db 'cqcqcq' lengthof szhllo = 6
若这样定义szHello : szHello db ‘Hello ’,0dh,0ah

db ‘World’,0
这个时候使用sizeof szHello 得到的是7而不是13。MASM中变量的定义只认一行,第二行的world相当于没有名称的数据定义。虽然把szHello的地址当参数传给MessageBox等函数显示时候会吧两行都显示出来,但严格的说这事越界使用变量。求这种字符串的长度的时候,不要使用sizeof
,而是使用lstrlen;

全局变量地址:全局变量的地址,在编译的时候就已经由编译器确定了,直接使用offset:mov 寄存器 ,offset 变量名,offset仅把变量地址带入到指令中,这个操作实在编译时而不是在运行时
局部变量地址:局部变量是由ebp来做指针操作的,假设ebp是40100h,那么局部变量1的地址就是ebp-4=400fch,但是ebp的值随着程序执行的环境不同也可能不同。所以局部变量的地址值在编译的时候也是不确定的,不能使offset来获取。

lea eax,[ebp-4] 使用lea指令,可以取指针的地址,可以在运行的时候按照ebp的值实际计算出地址放入eax中。
如果要在invoke伪指令的参数中用到一个局部变量的地址,不能使用lea,offset也不对,这个时候就要使用addr
addr后跟全局变量名的时候,编译器会自动按照offset的用法来使用。addr后跟局部变量名的时候,编译器会自动用lea指令先把地址取到eax中,然后用eax来代替变量地址使用。
注意1:在对局部变量取地址的时候addr伪操作只能用在invoke中,不能用在想mov这样的指令中。
2. lea是机器指令,offset是伪指令,伪指令在编译的过程中会变成相应的数值或者机器指令。,ebp要在运行的时候才能变成有效的值,故伪指令无效。
3.由与addr指令编译后本质上是eax的一种调用,所以在使用invoke的时候,参数中假如除了使用addr的局部变量名外还有eax也作为参数的话,要注意eax的位置。假如eax在addr左边的话,会出现addr先编译成eax然后作为参数push进入栈中,因为addr在右边,右边的变量会先push入,所以eax应该放在addr的右边才会没事。

四。使用子程序
为了让invoke指令能够正常使用,必须在程序的头部用proto伪操作定义子程序的信息。
堆栈的平衡: 调用者首先把参数压入堆栈,然后调用子程序,在完成后,由于堆栈中先前压入的数不再有用,调用者必须有一方把堆栈指针修正到调用前的状态
ret+n :ret 8 ret后把堆栈指针esp加上操作数,这样来实现堆栈的平衡。c语言清楚堆栈者是调用者,所以子程序用ret,调用程序在调用后使用add esp,xxxxx来清除堆栈。win32使用的STDCall

在写源程序的时候有意识的把子程序的位置提到invoke语句之前,可以省略proto声明语句,可以简化程序和避免出错。
五。高级语法
MASM条件测试语句限制:表达式的左边只能是变量或寄存器,不能是场数;其次表达式的两边不能同时为变量,但是可以同时是寄存器。
标志位 :
(1)进位 CARRY? 是否置位 (2)OVERFLOW? (3)PARITY?奇偶校验 (4)SIGN (5)ZERO
判断:.if .else .elseif .endif
循环:.while .break .continue .endw
.repeat .until (相当于do while)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: