win32汇编·指令
[名字] 助记符 <操作数> [;注释]
常用伪指令
类型 | 助记符 | 简写 | 字节数 | 数字范围 |
---|---|---|---|---|
字节 | BYTE | DB | 1 | 0~255 |
字 | WORD | DW | 2 | 0~65535 |
双节 | DWORD | DD | 4 | |
远节 | FWORD | DF | 6 | |
四节 | QWORD | DQ | 8 | |
十字节 | TBYTE | DT | 10 | |
带符号字节 | SBYTE | 1 | -128~127 | |
带符号字 | SWORD | 2 | -32768~32767 | |
带符号双字 | SDWORD | 4 |
数字常量
一个字节 8位 两位十六进制
十进制数:以D结尾,汇编默认十进制数,D可省略
二进制数:B
十六进制:H
八进制:Q或O
字符串常量
单引号,其值为ASCII
'A':41H
'ab':6162H
预留空间
?,不赋初值
复制重复
<n> DUP(操作数,...)
M1 DB 15,67H,11110000B,?
M2 DB '15','AB$'
M3 DW 4*5
M4 DD 1234H
M5 DB 2 DUP(5,'A')
M6 DW M2 ;M2的偏移量
M7 DD M2 ;M2偏移量,段基址
0F 67 F0 00
31 35 41 42 24
14 00
34 12 00 00
05 41 05 41
04 00
04 00 xx xx
符号定义伪指令
符号名 EDQ 表达式
CR EQU 0DH ;CR表示回车符的ASCII
LF EQU 0AH ;LF表示换行符ASCII
PORT_B EQU 61H ;用PORT_B表示B端口61H
B EQU [BP+6] ;用B表示操作数[BP+6]
MOV AL,CR ;(AL) = 0DH
ADD BL,B ;(BL) = (BL) + (SS:[BP+6])
IN AL,PORT_B ;从61H端口输入一个字节的数据
OUT PORT_B,AL ;再输出到61H端口
等号伪指令
符号名 = 数值表达式
只能是常数或数值表达式
DPL = 20H
K = 1
操作符伪指令
$
表示当前地址计数器的值
ORG 数值表达式
设置地址计数器内容为数值表达式的值
OFFSET [变量 | 标号]
取出变量或标号的地址
算术运算符
+ - * / MOD
/取商的整数部分
MOD取余数
逻辑操作符
AND OR XO NOT
关系操作符
EQ 等于
NE 不等于
LT 小于
LE 小于等于
GT 大于
GE 大于等于
框架定义
伪指令格式 | 功能 |
---|---|
.DATA | 定义数据段 |
.DATA? | 定义存放为初始化变量的数据段 |
.CONST | 定义存放常量的数据段 |
.CODE | 定义代码段 |
.STARTUP | 指定加载后的程序入口点 |
.EXIT | 返回DOS或父进程 |
.STACK size | 建立一个堆栈段并定义其大小(size以字节为单位。若未指定则默认为1KB) |
.MODEL 内存模式[,调用规则][,其他模式] | 定义程序工作的模式 |
Windows中,DATA,DATA?,CONST,STACK都视为数据区,堆栈空间一般是系统自动分配的,用户程序不必考虑 |
样例
.386:定义了程序使用30386指令集
.model flat:每一个程序都拥有其相对独立的4GB地址空间。因此Windows可执行程序只有一种内存模式,即flat模式,从00000000H到0FFFFFFFFH
srdcall:使用此规则调用子程序,堆栈平衡将由被调用者(子程序)用RET n指令实现。故在程序中调用Windows API函数或子程序后,不必调用者考虑堆栈平衡的问题
option:在Win32中需要定义option casemap:none
用来说明程序中的变量和子程序名是否对大小写敏感。由于Windows API中的函数名称是区分大小写的,所以需要指定
includelib:汇编程序中调用一些外部模块来完成部分功能,例如printf放在msvcrt.dll动态链接库中
函数声明语句
函数名称 PROTO [调用规则]:[第一个参数类型][,:后续参数类型]
printf函数声明:
_CRTIMP int _cdecl printf(const char *, ...);
例如上例子中:
printf PROTO C:ptr sbyte,: VARARG
printf函数的调用规则为C调用规则(_cdecl,即c_declare),第一个参数是字符串指针,后面的参数数量及类型不定。如果函数使用C调用规则,则PROTO后跟一个C。接下来是参数的说明。如果参数个数、类型不定,则用VARARG说明。
汇编中,用ptrs byte 代表const char*
include 语句
include user32.inc
采用C语言办法,把所有函数声明及常量定义等公用部分预先放在一个头文件中
程序结束
END [过程名]
程序在遇到end语句时结束,end语句后指出程序执行的入口点,即装入执行的第一条指令的位置。
跨行语句
某语句过长,加\
做换行符,将这句分成几行来写
数据存放
(1)可读可写初始变量:定义在data区
(2)可读可写未初始变量:可以在data,也可以在data?中
(3)常量数据:不需要修改已具有初值,放在const中,也可以在data中
invoke伪指令
invoke 函数名[,参数1][,参数2]
MessageBox
MessageBox属于usr32.dll,是Windows的一个API函数。
第一个参数是一个窗口句柄,即消息框父窗口,这里使用NULL表示它没有父窗口。
第二个参数是一个字符串指针,指向消息框中显示的正文。
第三个参数也是一个字符串指针,指向在消息框的窗口标题。
第四个参数是一个整数,指定消息框类型,这里使用MB_OK,消息框中显示一个OK(确定)按钮。
输入输出有关的API函数
所有用到的库函数,在程序开始部分必须预先声明,包括函数名称、参数类型。
函数名称 PROTO [调用规则]:[第一个参数类型][,:后续参数类型]
调用规则是可选项, 可以是stdcall,也可以是C等。缺省时使用model语句中指定的调用规则。
如果函数使用C调用规则,则PROTO后跟一个C。
参数的说明中如果参数个数、类型不定,则用VARARG说明
printf
在C语言头文件stdio.h中printf函数声明为:
_CRTIMP int _cdecl printf(const char *, ...);
调用规则为C调用规则(_cdecl,即c_declare),第一个参数是字符串指针,后面的参数数量及类型不定。
printf PROTO C:ptrs byte,:varargs
实际上调用时只注重它的类型,并不关心其名称,因此在程序中参数类型经常用DWORD来表示,他可以代表字符串指针、结构指针、整数等,例如printf也可以声明为:
printf PROTO C:dword,:varargs
printf及其他msvcrt.dll 输出的函数的连接信息豆子这个库文件中。因此在程序开头应有以下语句:
include lib msvcrt.lib
invoke printf, offset szOut,x,n,p
其中,szOut要在数据区中定义,如:
szOut byte 'x=%d n=%d x(n)=%d',0ah,0
效果等价于:
printf("x=%d n=%d x(n)=%d\n",x,n,p);
scanf
scanf链接信息也在msvcrt.lib库中。调用规则和参数类型说明为:
scan PROTO C:dword,:vararg
第一个参数是格式化字符串地址,后面参数个数可变,可以一个没有
szlnFmtStr byte '%d %c %d',0
invoke scant, offset szlnFmtStr,offset a,offset b,offset d
第一个参数是格式化字符串szlnFmtStr地址
第2,3,4个参数分别是a,b,d地址,效果等价于:
scanf("%d %c %d",&a,&b,&d);
分支与循环
单分支结构
求带符号数A和B的MAX_AB = MAX(A,B)
MOV EAX,A
CMP EAX,B
JGE AlsLarger ;如果A≥B,跳转到AlsLarger标号处
MOV EAX,B
AlsLarger:
MOV MAXAB,EAX
无符号类比
求无符号数A和B的MAX_AB = MAX(A,B)
MOV EAX,A
CMP EAX,B
JAE AlsAbove ;如果A≥B,跳转到AlsAbove标号处
MOV EAX,B
AlsAbove:
MOV MAXAB,EAX
IF_THEN_ELSE结构
求带符号数X符号,如果X≥0,把SIGNX置为1;如果X<0,SIGNX置为-1
X SDWORD 45
SIGNX SDWORD ?
MOV SIGNX,0
CMP X,0
JGE XisPostive ;X≥0跳转
MOV SIGNX,-1
JMP HERE ;这样可以跳过MOV SIGNX,1
XisPostive:
MOV SIGNX,1
HERE:
升序数组查找一个数(折半查找)
数组名dArray,数组字节型,下标为EBX,在程序中用dArray[EBX]来表示下标为EBX的元素
.386
.model flat,stdcall
option casemap:none
includelib msvcrt.lib
printf PROTO C:dword,:VARARG
.DATA
dArray byte 15,27,39,40,68,71,82,100,200,230
Items equ ($-dArray) ;定义数组中元素个数
Element byte 83 ;假设查找的数字为82
Index dword ? ;在数组中的序号
Count dword ? ;查找的次数
szFmt byte 'Index=%d Count=%d Element=%d',0ah,0 ;输出格式字符串
szErrMsg byte 'Not Found, Count=%d Element=%d',0ah,0
.CODE
start:
xor eax,eax ;xor相同则置为0,相当于清空了
mov Index,-1 ;赋初值,假设找不到
mov Count,0 ;赋初值,查找次数为0
mov esi,0 ;ESI表示查找范围的下届
mov edi,Items-1 ;EDI表示查找范围的上届
mov al,Element ;EAX是要在数组中查找的数字
Compare:
cmp esi,edi ;下界是否超过上届
jg NotFound ;如果下界超过上届,未找到
mov ebx,esi ;取下界和上届的中点
add ebx,edi ;
shr ebx,1 ;EBX右移一位,相当于EBX=(ESI+EDI)/2
inc Count ;查找次数+1
cmp al,dArray[ebx] ;与中点上的元素比较
jz Found ;相等则查找结束
ja MoveLow ;较大则移动下界,取上半段,故将下界设为中点
mov edi,ebx ;较小,移动上界,取下半段,故将上界设为中点
dec edi ;ebx中点位置已经比较过,不再比较,自减1
jmp Compare ;范围缩小后,继续查找
MoveLow:
mov esi,ebx ;较大,移动下界
inc esi ;ebx中点位置已经比较过,不再比较,自增1
jmp Compare ;范围缩小后,继续查找
Found:
mov Index,ebx ;找到,ebx是下标
xor eax,eax
mov al,dArray[ebx]
invoke printf,offset szFmt,Index,Count,eax
ret
NotFound:
invoke printf,offset szErrMsg,Count,eax
ret
end start
SWITCH_CASE结构分支程序
编制一个管理文件的菜单程序,要求能够实现建立文件、修改文件、删除文件、显示文件和退出应用程序5个主控功能。首先在屏幕上显示5种功能,然后从键盘上输入数字1~5即可转入相应的功能,而输入其他字符则提示输入非法。若选择退出功能,则能正确返回;若选择其他功能,应能返回到主菜单。
对于SWITCH_CASE结构,由于分支众多,可以把各分支入口地址集中在一起构成一个地址表,把这个地址表称为跳转表。设建立文件分支入口标号为CR,修改文件分支入口标号为UP,删除文件分支入口标号为DE,显示文件分支入口标号为PR,退出分支入口标号为QU。
JMPTAB DD OFFSET CR ;跳转表
DD OFFSET UP
DD OFFSET DE
DD OFFSET PR
DD OFFSET QU
跳转表
索引号=K-起始功能号(例如功能号为1,2,3,…,N,则索引号=K-1,相当于一个从0开始,一个从1开始)。
位移量=索引号×每项入口地址占用的字节数。对于用DD定义的则为4字节。
表项地址=表基址+位移量。
.386
.model flat,stdcall
option casemap:none
includelib msvcrt.lib
printf PROTO C:ptr sbyte,:VARARG
scanf PROTO C:ptr sbyte,:VARARG
.DATA
Msg1 db '1--create',0ah ;菜单字符串
db '2--update',0ah
db '3--delete',0ah
db '4--print',0ah
db '5--quit',0ah,0
Msg2 db 'input select:',0ah,0 ;输入提示字符串
Fmt2 db '%d',0 ;scanf格式字符串
op dd ? ;scanf结果(用户输入的整数)
Msg3 db 'Error!',0ah,0 ;输入错误后显示的字符串
MsgC db 'Create a File',0ah,0 ;选择菜单一后显示的字符串
MsgU db 'Update a File',0ah,0 ;选择菜单二后显示的字符串
MsgD db 'Delete a File',0ah,0 ;选择菜单三后显示的字符串
MsgP db 'Print a File',0ah,0 ;选择菜单四后显示的字符串
MsgQ db 'Quit',0ah,0 ;选择菜单五后显示的字符串
JmpTab dd offset CR ;跳转表,保存5个符号
dd offset UP
dd offset DE
dd offset PR
dd offset QU
.CODE
start:
invoke printf,offset Msg1
Rdkb:
invoke printf,offset Msg2 ;显示提示
invoke scanf,offset Fmt2,offset op
cmp op,1 ;与1比较
jb Beep ;输入的数字比1小,不合法
cmp op,5
ja Beep ; 输入的数字比5大,不合法
mov ebx,op
dec ebx ;减1得到索引值
jmp JmpTab[ebx*4] ;得到表项对应的标号,并跳转过去
Beep:
invoke printf,offset Msg3 ;提示输入错误
jmp Rdkb
CR:
invoke printf,offset MsgC
jmp start ;回到主菜单,继续运行
UP:
invoke printf,offset MsgU
jmp start
DE:
invoke printf,offset MsgD
jmp start
PR:
invoke printf,offset MsgP
jmp start
QU:
invoke printf,offset MsgQ
ret ;返回系统
end start
循环程序设计
(1)循环初始化。它包括设置循环次数的初始值、地址指针的初始设置等。
(2)循环体。这是循环工作的主体,包括要重复执行的操作,以及循环的修改部分。修改部分包括地址指针的修改、循环控制条件的修改等。
(3)循环控制部分。它是控制循环的关键,判断循环条件满足与否。例如判断循环次数是否为0等。
计算1+2+…+100用循环实现
.386
.model flat,stdcall
option casemap:none
includelib msvcrt.lib
printf PROTO C:ptr sbyte,:VARARG
.DATA
sum dword 0
szMsg db 'ecx=%d eax=%d sum=%d',0ah,0
.CODE
start:
mov ecx,100
mov eax,1
d10:
add sum,eax
inc eax
loop d10
invoke printf,offset szMsg,ecx,eax,sum
ret
end start
计算n的阶乘
.386
.model flat,stdcall
option casemap:none
includelib msvcrt.lib
printf PROTO C:ptr sbyte,:VARARG
.DATA
Fact dword ?
N equ 6
szFmt db 'factorial(%d)=%d',0ah,0
.CODE
start:
mov ecx,N ;循环初值
mov eax,1 ;Fact初值
e10:
imul eax,ecx ;Fact=Fact*ecx
loop e10 ;循环N次
mov Fact,eax
invoke printf,offset szFmt,N,Fact
ret
end start
逻辑尺
设数组X、Y中分别存有10个双字型数据。试实现以下计算并把结果存入Z单元。
Z1=X1+Y1 Z2=X2+Y2 Z3=X3-Y3 Z4=X4-Y4 Z5=X5-Y5 Z6=X6+Y6 Z7=X7-Y7 Z8=X8-Y8 Z9=X9+Y9 Z10=X10+Y10
10组数进行运算,运算操作符不同,且无规律可循。若直接用前边介绍的循环程序难以实现。这里设想把加用某个值表示(设用 0),减用另一个值表示(设用1),10个式子的操作用10位二进制数表示。对于本例,若按Z10、Z9、…、Z1的计算顺序把它们的操作符自右至左排列起来,则操作符数值化后得到一串二进制位0011011100,把它放入一个32位的内存变量中,高22位无意义 (此处用0填充),这种存储单元一般被叫做逻辑尺。计算时按照Z1…Z10顺序,先求Z1的值。每次把逻辑尺右移一位,对移出位进行判断,若该位为0则加,为1则减。
.386
.model flat,stdcall
option casemap:none
includelib msvcrt.lib
printf PROTO C:ptr sbyte,:VARARG
.DATA
x dd 1,2,3,4,5,6,7,8,9,10
y dd 5,4,3,2,1,10,9,8,7,6
Rule dd 0000000011011100B
z dd 10 dup(?)
szFmt db 'Z[%d] = %d',0ah,0
.CODE
start:
mov ecx,10 ;循环初值,loop对应此初值
mov edx,Rule ;逻辑尺
mov ebx,0
next:
mov eax,x[ebx] ;取x中的一个数
shr edx,1 ;逻辑尺右移一位
jc subs ;分支判断并实现转移
add eax,y[ebx] ;两数加
jmp short result
subs:
sub eax,y[ebx] ;两数减
result:
mov z[ebx],eax ;存结果
add ebx,4 ;修改地址指针
loop next
xor ebx,ebx ;清空ebx,地址偏移
PrintNext:
invoke printf,offset szFmt,ebx,z[ebx*4] ;打印结果
inc ebx ;ebx下标+1
cmp ebx,10 ;是否已经全部打印
jb PrintNext ;继续打印
ret
end start
将一个字符串大写字符转换为小写字符
要求:字符串以0结尾
分析:大写字符的ASCII码值为41H~5AH
,小写字符的ASCII码值为61H~7AH
。对大写字符,将它加上20H,即可以转换为小写字符。当遇到字符0时,循环结束。
.386
.model flat,stdcall
option casemap:none
includelib msvcrt.lib
printf PROTO C:ptr sbyte,:VARARG
.DATA
szStr db 'Hello World!',0
.CODE
start:
mov esi,offset szStr
g10:
mov al,[esi]
cmp al,0
jz g30 ;判断为0说明走到尾了
cmp al,'A' ;小于A说明是普通字符
jb g20
cmp al,'Z' ;大于Z说明是小写字符和普通字符
ja g20
add al,'a'-'A'
mov [esi],al
g20:
inc esi
jmp g10
g30:
invoke printf,offset szStr
ret
end start
多重循环设计-冒泡排序
把数组中的7个元素用冒泡法按从小到大的顺序排列
在设计冒泡排序的程序时,需要两层循环。外层循环的循环次数是n-1,以第0次、第1次、…、第n-2次循环表示。第i次外循环中,内层循环对数组下标为0至n-i-1的元素依次“比较、交换”。内层循环的循环次数是n-i-1。
//C形式
void bubbleSort(int arr[], int len)
{
int temp;
for (int i = len -1; i > 0; i--)
{//外循环为排序趟数,len个数进行len-1趟
for (int j = 0; j < i; j++)
{ //内循环为每趟比较的次数,第i趟比较len-i次
if (arr[j] > arr[j + 1])
{ //相邻元素比较,若逆序则交换(升序为左大于右,降序反之)
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
.386
.model flat,stdcall
option casemap:none
includelib msvcrt.lib
printf PROTO C:ptr sbyte,:VARARG
scanf PROTO C:ptr sbyte,:VARARG
.DATA
dArray dd 20,15,70,30,32,89,12
items equ ($-dArray)/4 ;数组中元素个数
szFmt db 'dArray[%d]=%d',0ah,0
.CODE
start:
mov ecx,items-1
i10:
xor esi,esi
i20:
mov eax,dArray[esi*4]
mov ebx,dArray[esi*4+4]
cmp eax,ebx
jl i30 ;排序满足需求,不用交换
mov dArray[esi*4],ebx
mov dArray[esi*4+4],eax
i30:
inc esi ;递增一个
cmp esi,ecx ;比总数小则再来一个内循环
jb i20
loop i10 ;(ecx-1),同时进行外循环
xor edi,edi
i40:
invoke printf,offset szFmt,edi,dArray[edi*4]
inc edi
cmp edi,items
jb i40
ret
end start
浮点运算
专用于数值计算的浮点运算指令,包括浮点数的传送、浮点算术运算、浮点比较与控制等。
浮点处理单元x87 FPU
IEEE浮点数格式
浮点数规格化
规格化浮点数的尾数域最左位(最高有效位)总是1,故这一位经常不予存储,而认为隐藏在小数点的左边。否则以修改阶码同时左右移小数点位置的办法,使其变为规格化数的形式。
在浮点数格式中,扩展双精度类型没有隐含位,因此它的有效位数与尾数位数一致,而单精度类型和双精度类型均有一个隐含整数位,因此它的有效位数比位数多一个。
浮点数存储
float Var1 = 119.054f; //定义float型变量Var1,f强制为单精度浮点型
double Var2 = 119.054; //定义double型变量Var2
int main()
{
Var1 = Var1;
Var2 = Var2;
return 0;
}
对应的汇编码
6: Var1 = Var1;
0040E6B8 mov eax,[Var1(00426608)]
0040E6BD mov [Var1 (00426608)],eax
7: Var2 = Var2;
0040E6C2 mov ecx,dword ptr [Var2 (00426610)]
0040E6C8 mov dword ptr [Var2 (00426610)],ecx
0040E6CE mov edx,dword ptr [Var2+4 (00426614)]
0040E6D4 mov dword ptr [_Var2+4 (00426614)],edx
8: return 0;
0040E6DA xor eax,eax
对单精度数Var1
从内存00426608处取出变量Var1保存的值为:A6 1B EE 42,转化 为二进制(逆序存放):
01000010 11101110 00011011 10100110
根据单精度的划分方式把32位划分成三部分:
1.符号位为0,为正数;
2.指数为 10000101(133),减去127得6(移码);
3.尾数加上1后为1.11011100001101110100110,十进制表示为: 1.86021876
尾数乘以2的6次方后可得结果为:119.05400(单精度7~8位有效数字)
对双精度数Var2
从内存00426610和00426614处取出变量Var1保存的值为:FA 7E 6A BC 74 C3 5D 40,转化为二进制(逆序存放):
01000000 01011101 11000011 01110100 10111100 01101010 01111110 11111010
1.符号位为0,为正数;
2.指数为10000000101(1029),减去1023得6;
3.尾数加上1后为 1.1101110000110111010010111100011010100111111011111010
转化为10进制后乘以2的6次方后可得结果为119.054000000000 (双精度15~16位有效数字)