.386 语句是汇编语句的伪指令,类似指令有:.8086 、 .186 、.286 、.386/.386p 、 .486/.486p 和 .586/.586p 用于告诉编译器在本程序中使用的指令集。
在DOS的汇编中默认使用的是8086指令集,那时候如果在源程序中写入 80386所特有的指令 或使用 32位的寄存器 就会报错。Win32环境工作在80386及以上的处理器中,386是必不可少的。
另外,后面带p的伪指令则表示程序中可以使用特权指令,如:mov cr0,eax 这一类指令必须在特权极0上运行。
如果我们要写的程序是VxD等驱动程序,中间要用到特权指令,那么必须定义.386p ,在应用程序级别的Win32编程中,程序都是运行在优先级3上,不会用到特权指令,只需定义 .386就够了。
如果程序要用到80486处理器 或 Pentium处理器的指令,则必须定义 .486 或 .586。
如果使用了MMX指令,除了定义 .586之外,还要 .mmx伪指令。
其它一些简单程序,.386就可以了
.model 内存模式 [,语言模式] [,其它模式] (用中括号括起来的是可要可不要)
内存模式的定义影响最后生成的可执行文件,执行文件的规模从小到大,可以很多类型。
语言模式即子程序调用方式,它指出了调用子程序或Win32 API时参数传递的次序和堆栈平衡的方法。
_stdcall调用——是Pascal程序的缺省调用方式,参数采用从右到左的压栈方式(倒序压入),被调用函数 自身在返回前清空堆栈。Win32 API 都采用这种调用方式。
_cdecl 调用 ——是C/C++的缺省调用方式,参数从右到左的压栈方式,调用者 清空堆栈。 所以产生的可执行文件大小会比调用_stdcall函数大。
option casemap:none 的意义是告诉编译器程序中变量名和子程序名是否对大小写敏感。 none 对大小写敏感。
Win32 API的函数名称本质是区分大小写的,所以必须指定这个选项。
.386
.model flat,stdcall
option casemap:none
<一些include语句>
.stack [堆栈段的大小]
.data
<一些初始化过的变量定义>
.data?
<一些没有初始化过的变量定义>
.const
<一些常量定义>
.code
<代码>
<开始标号>
<其它语句>
end 开始标号
1).data 、 .data? 、 .const定义的是数据段,分别对应不同方式的数据定义,在最后生成的可执行文件中也分别放在不同的节区(Section)中
2)程序中的数据定义可以归纳为3类:
①可读可写的已定义变量。必须定义在.data段中
②可读可写的未定义变量。汇编语言的特色,一般是当做缓冲区或者程序执行后才开始使用的,可以定义在.data段中,也可以定义在.data?段中,但一般把它放到 .data?段中。 虽然定义在这两个段中都可以正常使用,但定义在 .data?段中不会增大 .exe文件的大小。 .data?段在可执行文件中存放在_BSS节区中。
③数据是一些常量,如果进行了对.const段中的数据做写操作的指令,会引起保护错误,Windows会显示一个提示框并结束程序
④在.data?段中,只能用问号预留空间,不能指定初始值
所有指令都必须写在代码段中,在可执行文件中,代码段是放在_TEXT节区(区块)中的。
end [开始地址]表示源程序结束,所有的代码必须在end语句之前。
一个源程序不必非要指定入口标号,这时可以把开始地址忽略不屑,这发生在编写多模块程序的单个模块的时候。只有一个主模块指定入口地址。
一行最后用反斜杠(\)做换行符。
API函数参数:参数类型只是声明了参数的大小 在DOS下是放在寄存器(ah)中。
API函数参数中的等值定义(宏):
API函数返回值:返回值类型对汇编程序来说也只有dword一种类型,永远放在eax中。返回的是字符串的话,eax存的是字符串存放地址的指针。
函数声明:在调用API函数的时候,函数原型也必须预先声明。否则,编译器会不认这个函数。invoke伪指令也无法检查参数个数。
声明函数的格式:
函数名 proto [距离][语言][参数1]:数据类型,[参数2]:数据类型
在Win32中 在定义时距离是忽略的,语言类型是.model那些类型,如果忽略,则使用.model定义的默认值。
DLL事实上只是一个大大的集装箱,装着各种系统的API函数,应用程序在使用的时候有Windows自动载入DLL程序并调用相应的函数。
Win32的基础就是有DLL组成。Win32 API的核心由3个DLL提供
①KERNEL32.DLL —— 系统服务功能。包括内存管理、任务管理和动态链接等。
②GDI32.DLL —— 图形设备接口,处理图形绘制。
③USER32.DLL —— 用户接口服务。建立窗口和传送消息
不同的DLL提供了不同的系统功能,如 使用TCP/IP协议进行网络通信的DLL是Wsock32.dll;专用于电话服务方面的Tapi32.dll
它不是80386处理器的指令,而是一个MASM编译器的伪指令,编译时,他把下面的指令展开成我们需要的4个push指令和一个call指令,同时,进行参数数量的检查工作,如果带的参数数量和声明时的数量不符,编译器报错。
格式:
invoke 函数名 [,参数1][,参数2]...[,参数n]
invoke MessageBox,NULL,offset szText,offset szCaption,MB_OK
格式语法:
include 文件名
或
include <文件名>
includelib语句
格式语法:
includelib 库文件名
或
includelib <库文件名>
标号:当程序中要跳转到另一位置时,需要有一个标识来指示位置。通过在目的地址的前面放上一个标号,可以在指令中使用标号来代替直接使用地址。编译器翻译时,也将其翻译成地址。
格式:
① 标号名:目的指令
② 标号名::目的指令
比较常用的是格式一,标号的作用域是当前的子程序,在不同子程序中可以存在同样名字的标号,这也就意味着这种格式不能从一个子程序通过标号跳转到另一个子程序中。
如果我们需要从一个子程序中用指令跳到另一个子程序中的标号位置的时候,用格式二。
@@标记符—— 一个程序中可以有多个
mov cx,1234h
cmp flag,1
je @F ;跳到后面的@@处,@F表示本条指令 后 第一个@@标号
mov cx,1000h
@@:
loop @B ;跳到之前的@@处,@B表示本条指令 前 第一个@@标号
这两个只寻找匹配最近一个。
变量:计算机内存中 已命名的存储位置。
①全局变量的定义:
Win32汇编的全局变量定义在 .data 或 .data? 段内,可以同时定义变量的类型和长度,格式如:
变量名 类型 初始值1,初始值2,...
变量名 类型 重复数量 dup (初始值1,初始值2,...)
初始值默认为0;
注意:所有使用到变量类型的情况中,只有定义全局变量的时候类型才可以用缩写
举例:
.data
wHour dw ? ;未初始化
word_Buffer dw 100 dup(1,2) ;
关于CR和LF
Dos 和 windows采用回车+换行(CR/LF)表示下一行
CR用符号'\r'表示,十进制ASCII代码是13,十六进制为0xd
LF用符号'\n'表示,十进制ASCII代码是10,十六进制为0xa
②局部变量:
在进入子程序的时候,通过修改堆栈指针esp来预留出需要的空间,在用ret指令返回主程序之前,同样通过恢复esp丢弃这些空间,这些变量就随之无效了。
缺点:因为空间是临时分配的,所以无法定义含有初始值的变量,对局部变量的初始化一般在子程序中有指令完成的。
格式:
local 变量名 [[重复数量]] [:类型], 变量名2 [[重复数量]][:类型]....
local伪指令必须紧接在子程序定义的伪指令proc后、其它指令开始前,这是因为局部变量的数目必须在子程序开始的时候就确定下来,在一个local语句定义不下的时候,可以有多个local语句。
注意:
语句中的数据类型不能缩写。
当定义数组的时候,可以[]括号起来。不能使用定义全局变量的dup伪指令。
变量不能和已定义的全局变量同名。
③类型转换
在C语言中将浮点数转换成整数后,小数点后的内容就丢失了。但在MSAM中以不同的类型访问不会对变量造成影响,但必须显式地指出要访问的长度,这样编译器忽略语法上的长度检验,仅使用变量的地址。
格式: 类型 ptr 变量名
例 : mov ax,word ptr szBuffer
mov eax,dword ptr szBuffer
汇编用ptr强制覆盖变量长度的时候,实质上只用了变量的地址而禁止编译器进行检验。编译器并不会考虑定界的问题,程序员在使用的时候必须到内存中的数据排列有个全局概念,以免越界存取到。
④变量的尺寸和数量
用sizeof 和 lengthof 伪指令来实现,格式是:
1.sizeof 变量名、数据类型或数据结构名
2.lengthof 变量名
sizeof 伪指令可以取得变量、数据类型或数据结构以字节为单位的长度,然而 lengthof则可以取得变量中的数据的项数(有几个变量)。
要用到字符串的长度时,不要用sizeof去表示,最好是在程序中用lstrlen函数去计算。
⑤获取变量地址
获取变量地址的操作对于全局变量和局部变量是不同的。
对于全局变量,它的地址在编译的时候已经由编译器确定了,mov 寄存器,offset 变量名 ,其中offset是取变量地址的伪操作符,和sizeof一样,它仅把变量的地址带到指令中去。
对于局部变量,可以用 addr 局部变量名 或 全局变量名
当addr后跟全局变量名的时候,用法和offset是相同的。
当addr后跟局部变量名的时候,编译器自动用lea指令先把地址取到eax中,然后用eax来代替变量地址使用。
但addr伪操作符只能在invoke的参数中使用,不能用在move ax,addr 局部变量名。可以是invoke Test,eax,addr,szHello,不过addr会把之前的eax值覆盖掉,所以在要使用addr时,不能使用eax寄存器
标号和变量的命名规范是相同的:
1.可以用字母、数字、下划线及符号@、$和?
2.第一个符号不能是数字。
3.长度不能超过240个字符
4.不能使用指令名等关键字
5.在作用域内必须是唯一的
定义:
子程序名 proc [距离][语言类型][可视区域][USES寄存器列表][,参数:类型]...[VARARG]
local 局部变量列表
指令
子程序名 endp
proc和endp伪指令定义了子程序开始和结束的位置,proc后面跟的参数是子程序的属性和输入参数。
1.子程序属性
[距离]可以是NEAR、FAR、NEAR16、NEAR32、FAR16或FAR32,Win32中只有一个平坦的段,无所谓距离,所以对距离的定义往往忽略。
[语言类型]表示参数的使用方法和堆栈平衡的方式。可以实StdCall、C、SysCall、BASIC、FORTRAN和PASCAL,如果忽略,则使用程序头部.model定义的值。
[可视区域]可以是PRIVATE(表示子程序只对本模块可见)、PUBLIC(对所有的模块可见)和EXPORT(导出函数,当编写DLL的时候要将某个函数导出的时候可以使用)。默认PUBLIC。
[USES寄存器列表]表示由编译器在子程序指令开始前自动安排push寄存器的指令,并在ret前自动安排pop指令,用于保存执行环境,但是这种方法不如在开头和结尾用pushad和popad指令一次保存和恢复所有寄存器。
[参数和类型]在定义参数名的时候,不能重名
1.条件语句的语法:
在MASM条件测试的基本表达式:
寄存器或变量 操作符 操作数
两个以上的表达式可以用逻辑运算符连接:
表达式1 逻辑运算符 表达式2
例:
x == 3 ;x等于3
eax != 3 ;eax不等于3
(y >=3 ) && ebx ;y大于等于3且ebx为非零值。
(z&1) || !eax ; z为非零值 或者 eax 为非零值
限制:
①表达式的左边只能是变量或寄存器,不能为常数。
②表达式两边不能同时为变量,但可以实寄存器。
程序中常常要根据系统标志寄存器中的各种标志位来做条件跳转。而高级汇编有增加了一下标志位的状态指示,如图
2.分支语句的语法
.if 条件表达式1
指令
[.elseif 条件表达式2]
指令
[.else]
指令
.endif
3.循环语句
①.while 条件测试表达式
指令
[.break[.if ]]
[.continue]
.endw
②类似于C语言的do while
.repeat
指令
[.break[.if ]]
[.continue]
.until 条件测试表达式 (.untilcxz [条件测试表达式])
全局变量的定义使用标准的匈牙利表示法,在参数的前面加上下划线。
局部变量的前面加@符号
在内部子程序的名称前面加下划线,以便和系统的API区别
①大小写问题
汇编程序中对于指令和寄存器的书写是不分大小写的,但小写代码比大写代码便于阅读,所以程序中的指令和寄存器等采用小写字母,而用equ伪操作符定义的常量则使用大写,变量和标号使用匈牙利表示法,大小写混合.
②代码组织
对于主程序使用比较频繁的部分,以及便于封装。但一个子程序的规模不应该太大,行数尽量限制在几百行之内,功能则限于完成单个功能。对于子程序,定义参数的时候要尽可能精简,对可能引起程序崩溃的参数,如指针等,要进行合法性检测。子程序中在使用完申请的资源的时候,注意在退出前要释放所用资源,包括申请的内存和其他句柄等,对于打开的文件则要关闭。