学习Win32汇编(Windows下的32位汇编)
第一个程序Hello world
控制台输出(新建Console App工程)
Debug输出(新建Win32 App工程)
后面代码的头文件在这里
伪指令DUP与数组
运算符
子过程(函数)的传参与调用
获取数组长度和字节数等
数据对齐
获取变量地址以及伪指令this的使用
loop的使用
堆栈以及相关指令
二进制相关函数
标志寄存器
数据传送指令
逻辑运算指令
位测试与位扫描指令
移位指令
符号扩展指令
加减指令
乘除指令
跳转指令
串指令
条件及循环伪指令
结构体
书籍推荐
- Windows环境下32位汇编语言程序设计(典藏版)
- 环境配置
- 方案一
- 方案二(推荐)
- 参考博客
基础篇
第一个程序Hello world
;模式定义
.386 ;汇编伪指令,指明了程序使用80386指令集
.model flat, stdcall ;汇编伪指令,指明了程序工作模式,Win32程序只有一种内存模式,即flat。stdcall指明了编译器参数传递的约定,即函数调用时,实参入栈从右往左。
option casemap:none ;指明标识符区分大小写
;引入头文件和库文件,inc文件主要包含函数或常量的声明,lib文件包含了动态库函数的地址信息和静态库的函数代码
include windows.inc ;windows.inc包含着Win32程序用到的常量、结构的声明,下面用到的MB_OK常量就是在其中声明
include user32.inc
include kernel32.inc
includelib user32.lib ;user32.dll是用户服务接口, 负责消息管理等,MessageBox函数来自user32.dll
includelib kernel32.lib ;kernel32.dll是系统服务接口, 负责内存管理等,ExitProcess函数来自kernel32.dll
;数据段(.data表示已初始化的数据)
.data
szCaption db 'Hi', 0
szMsg db 'Hello World!', 0 ;定义字符串变量,0表示字符串结尾
;代码段
.code
start:
invoke MessageBox, NULL, addr szMsg, addr szCaption, MB_OK
invoke ExitProcess, NULL ;invoke是调用函数的伪指令,MessageBox(显示消息框)和ExitProcess(退出程序)为API函数
end start
控制台输出(新建Console App工程)
.386
.model flat, stdcall
option casemap:none
include msvcrt.inc
includelib msvcrt.lib
.data
szFmt db 'EAX=%d; ECX=%d; EDX=%d', 0
.code
start:
mov eax, 11
mov ecx, 22
mov edx, 33
invoke crt_printf, addr szFmt, eax, ecx, edx ;crt_printf就是C语言里面的printf函数
ret ;ret是用于子程序返回的指令。在没有生成Win32窗口时可以使用ret代替ExitProcess
end start
Debug输出(新建Win32 App工程)
.386
.model flat, stdcall
option casemap:none
include windows.inc
include kernel32.inc
include masm32.inc
include debug.inc
includelib kernel32.lib
includelib masm32.lib
includelib debug.lib
.data
szText db 'Hello World!', 0
.code
start:
PrintLine ;----------------------------------------
PrintString szText ;Hello World!
PrintLine ;----------------------------------------
ret
end start
后面代码的头文件在这里
.386
.model flat, stdcall
option casemap :none
include windows.inc
include kernel32.inc
include user32.inc
include masm32.inc
include debug.inc
includelib kernel32.lib
includelib user32.lib
includelib masm32.lib
includelib debug.lib
伪指令DUP与数组
;数组的实现
.data
;初始化数组,每个数组元素大小为2个字节
val dw 11,22,33
.code
start:
lea ebx, val ;等价于mov ebx,offset val
mov esi, type val ;每个元素大小为2个字节
xor ax, ax
mov ax, word ptr [ebx+esi*0]
PrintDec ax ;11
;也可以使用movzx eax,word ptr [ebx+esi*1]
;也可以使用mov ax, word ptr [ebx][esi*1]或者mov ax, word ptr val[esi*1]
mov ax, word ptr [ebx+esi*1]
PrintDec ax ;22
mov ax, word ptr [ebx+esi*2]
PrintDec ax ;33
ret
end start
;伪指令DUP的使用
.data?
v1 dd 4096 dup(?) ;未初始化变量应该放在.data?段,如果放在.data段,在生成exe文件时会多占4096*4字节的内存
.data
v2 dd 2 dup(1,2,3)
.code
start:
DumpMem offset v2, 24 ;01000000-02000000-03000000-01000000-02000000-03000000共24字节
ret
end start
运算符
.code
start:
;注意:下面这些运算符都是伪指令,在80386中由编译器执行。而在Dos中它们有对应字节码,由CPU执行。
;算数运算符
PrintDec 7 / 3 ;2
;关系操作符:eq(=),ne(!=),lt(<),le(<=),gt(>),ge(>=),满足条件输出-1,否则输出0
PrintDec 2 eq 1 ;0
PrintDec 2 eq 2 ;-1
;逻辑操作符
PrintHex 0FFFFFFFFh and 0FFFF0000h ;FFFF0000
;高低分离符
PrintHex high 11223344h ;00000033
PrintHex highword 11223344h ;00001122
;移位运算符
PrintHex 12345678h shl 4 ;23456780
ret
end start
子过程(函数)的传参与调用
;局部变量定义与过程函数调用
.code
proc1 proc
PrintDec 1
ret
proc1 endp
proc2 proc
PrintDec 2
ret ;过程返回,如果不写这个的话,会继续执行下面语句
proc2 endp
main proc
;局部变量中的类型不能使用缩写
LOCAL v1: dword,v2: dword
;数组
LOCAL v3[3]: dword
PrintDec v1
mov eax, v3[0]
PrintDec eax
call proc2 ;调用过程proc2
call proc1
ret
main endp
end main
;输出结果 0 0 2 1
;求和函数传参与调用
;sum proto :dword, :dword, :dword ;函数声明的主要是参数类型, 一般省略参数名
.code
sum proc v1:dword, v2:dword, v3:dword
mov eax, v1
add eax, v2
add eax, v3
ret
sum endp
main proc
invoke sum, 11, 22, 33 ;invoke是调用函数的伪指令。调用函数sum并传参。
PrintDec eax ;66
ret
main endp
end main
获取数组长度和字节数等
.data?
v1 dw 10 dup(0)
v1size=$-v1 ;获取变量v1占的字节数
v1len=($-v1)/2 ;获取变量v1长度
vaddr=$ ;$用于获取当前语句的地址。在.data中,只有v1,v2这种变量会产生字节开销
v2 dw 11,22,33,44
v2Size = $ - v2
.code
main proc
;获取类型占几个字节,对于数组是获取每个元素占几个字节
PrintDec (type v1) ;2
;测试$的作用
PrintDec v1size ;20
PrintDec v1len ;10
;可以得出v1size到v2之间的语句的$都等于v2地址
PrintDec vaddr ;4206796
mov eax,offset v2
PrintDec eax ;4206796
;获取数组元素个数以及总字节数
PrintDec (lengthof v1) ;10
PrintDec (sizeof v1) ;20
main endp
end main
数据对齐
.data
v1 db 0
align 4;让下一个变量的起始地址保证是4的倍数
;even表示偶对齐,等价于align 2
;org 100表示跨越100个字节存储下一个变量
v2 db 0
v3 db 0
.code
main proc
PrintDec offset v1 ;4206592
PrintDec offset v2 ;4206596
PrintDec offset v3 ;4206597
ret
main endp
end main
获取变量地址以及伪指令this的使用
OFFSET和ADDR的异同:
1、offset不能获取局部变量的地址;
2、addr只能用于调用函数(invoke)中, 不能用于赋值操作;
3、addr面对局部变量时会转换为lea等指令, addr面对全局变量时则直接调用offset;
例如:lea ebx,dwVal等价于mov ebx, offset dwVal。Lea是专门获取地址的指令
4、在invoke中应尽量使用addr, 其他只用offset.
;this伪指令的使用
.data
TextAddr equ this byte ;伪指令this可让当前变量和下一个变量同址
szText db 'Asm', 0
.code
main proc
PrintHex offset szText ;00403000
PrintHex offset TextAddr ;00403000
PrintString szText ;Asm
mov [TextAddr], 'a' ;给TextAddr所在地址单元赋值
PrintString szText ;asm
ret
main endp
end main
loop的使用
;数组求和
.data
dwArr dd 1,2,3,4,5
.code
main proc
lea edi, dwArr
mov ecx, lengthof dwArr
xor eax, eax
L1:
add eax, [edi]
add edi, type dwArr ;获取下一个元素的地址
loop L1
PrintDec eax ;15
ret
main endp
end main
;复制字符串
.data
szSource db 'Hello World!', 0
szDest db sizeof szSource dup(0)
.code
main proc
mov esi, 0
mov ecx, sizeof szSource
L1:
mov al, szSource[esi]
mov szDest[esi], al
add esi, type szSource ;调整索引
loop L1
;也可以使用API完成字符串复制:invoke szCopy, addr szSource, addr szDest
PrintString szDest
ret
main endp
end main
堆栈以及相关指令
- 程序把内存划分区域
- 全局数据区
- 全局变量在堆里
- 堆中数据由上向下排列,内存增大方向
- 局部数据区
- 局部变量,局部常量,子程序参数在栈里
- 栈中数据由下向上排列,内存减小方向
- 其它
- 全局数据区
- 栈顶指针ESP
- Win32的PUSH只可以压入32位(默认)或16位的数据,因此ESP只能±2或者±4
- push的应用
-
函数调用(invoke)的本质
- 本质是从右往左依次push参数,最后call函数,然后pop出栈(因为push和pop必须成对出现)
-
保护数据
- 调用函数前最需要保护的是EIP,因为它保存着函数调用结束后下一条指令的地址
1 执行call指令,CPU把返回地址(EIP)压入堆栈(4个字节) 2 esp在程序执行中随时可能用到,不可能使用它存取局部变量。ebp也是以堆栈段作为默认数据段的。所以先push ebp,再mov ebp, esp 3 正常操作 4 先mov esp,bsp,再pop ebp(leave指令可以起到这两个作用) 5 执行ret指令,CPU将返回地址(EIP)出栈
-
;交换变量值
.data
val1 dd 111
val2 dd 999
;方案一:使用堆栈
.code
main proc
push val1
push val2
pop val1
pop val2
PrintDec val1 ;999
PrintDec val2 ;111
ret
main endp
end main
;方案二:使用XCHG指令
.code
main proc
mov eax, val1
xchg eax, val2 ;eax存储val2的值,val2存储val1的值
mov val1, eax
PrintDec val1 ;999
PrintDec val2 ;111
ret
main endp
end main
;翻转字符串
.data
szText db 'Hello World!', 0
.code
main proc
;压栈
mov ecx, sizeof szText - 1
xor esi, esi
@@: movzx eax, szText[esi] ;懒得给标号取名,可以使用@@,@B表示前面最近的一个标号、@F表示后面最近的一个标号
push eax
inc esi
loop @B
;出栈
mov ecx, sizeof szText - 1
xor esi, esi
@@: pop eax
mov szText[esi], al
inc esi
loop @B
PrintString szText ;!dlroW olleH
ret
main endp
end main
二进制相关函数
;学习查看二进制是为了看到EFLAGS寄存器中的二进制位的变化
.data
szBin db 8 dup(?), 0
.code
main proc
lahf ;lahf指令是把EFLAGS寄存器的低8位字节读入ah
invoke byt2bin_ex, ah, addr szBin ;从byte数字转为二进制字符串
PrintString szBin ;01000110
ret
main endp
end main
标志寄存器
;置位 stc
stc ;CF=1
;复位 clc
clc ;CF=0
;取反 cmc
cmc ;CF=not CF
;如果要观察整个EFLAGS的32位, 可用PUSHFD和POPFD指令让EFLAGS进栈、出栈
数据传送指令
//以下指令均不影响标志寄存器EFlags
;mov 数值传送
;lea 地址传送
;xchg 交换指令
;xlat 换码指令
.data
szText db 'ABCDEFG', 0
.code
main proc
lea ebx, szText ;先将源地址放入ebx
mov al, 1 ;将要访问的字节序号放入al
xlat ;xlat无参数,操作和ebx、al相关
PrintHex al ;42,可以看出指定字节被读入到al
ret
main endp
end main
;movzx 零扩展传送
;movsx 符号扩展传送
MOVZX 和 MOVSX 的区别是:
1、MOVZX会将目标寄存器中高出的位补0
2、如果源操作数的最高位是1, MOVSX会将目标寄存器中高出的位补1; 反之补0
.data
bVal db 90h
dwVal1 dw 7FFFh
dwVal2 dw 8000h
.code
main proc
movzx eax, dwVal1
movsx edx, dwVal1
PrintHex eax ;00007FFF
PrintHex edx ;00007FFF
movzx eax, dwVal2
movsx edx, dwVal2
PrintHex eax ;00008000
PrintHex edx ;FFFF8000
mov cl, bVal
movzx ax, cl
movsx dx, cl
PrintHex ax ;0090
PrintHex dx ;FF90
ret
main endp
end main
逻辑运算指令
;and 逻辑与,or 逻辑或,xor 逻辑异或
指令会影响EFlags,置CF=OF=0,结果影响SF、ZF、PF
可以使用xor或者not用于加密与解密字符串
;not 逻辑取反
指令不影响EFlags
;test 测试逻辑与
指令会影响EFlags,置CF=OF=0,结果影响SF、ZF、PF
test同and,但它不修改运算数,只改变标志寄存器。即它只尝试and的结果
常用于影响ZF(当test结果为0时,ZF=1),test其后往往跟着条件转移指令
【举个栗子春暖花开】
;判断字符串中每个字符的二进制位的最后一位是1还是0,统计为0为1个数
.data
szText db 'Delphi', 0
.code
main proc
;清空两个寄存器用于计数
xor eax, eax ;存储末位为1字符个数
xor edx, edx ;存储末位为0字符个数
lea esi, szText
mov ecx, lengthof szText - 1
L1: test byte ptr [esi], 00000001b ;循环测试每个字符的最后一位是1还是0
jz L2 ;如果是0则跳转到L2,edx+1
inc eax ;反之给eax+1
jmp L3
L2: inc edx
L3: inc esi
loop L1
PrintDec eax ;2
PrintDec edx ;4
ret
main endp
end main
位测试与位扫描指令
;位测试指令 结果影响CF
BT:位测试 Bit Test
BTS:位测试并置位 Bit Test and Set
BTR:位测试并复位 Bit Test and Reset
BTC:位测试并取反 Bit Test and Complement
【举个栗子春暖花开】
;BT把10000001的第7位复制到CF,可以看出是1
mov dx,10000001b
bt dx,7
lahf
PrintHex ah ;47,即01000111b(CF=1)
;BTS:BT+置1,BTR:BT+置0,BTC:BT+取反
;位扫描指令 结果影响ZF
BSF:位扫描,由低->高
BSR:位扫描,由高->低
1 扫描的是参数二,找到是1的位后,把位置数给参数一,并置ZF=0
2 找不到是1的位,参数一的值不变,置ZF=1
【举个栗子春暖花开】
mov dx, 0000111100001100b
bsf cx, dx
PrintDec cx ;2,即从右往左数第2位
移位指令
;移位指令 结果影响OF、SF、ZF、PF、CF
SHL、SHR:逻辑左移、逻辑右移
SAL、SAR:算数左移、算数右移
SHL和SAL:左移,低位补0,高位进CF
SHR:右移,低位进CF,高位补0
SAR:右移,低位进CF,高位不变
【举个栗子春暖花开】
mov al, 11100111b
sar al, 2 ;3表示右移两位
PrintHex al ;F9,即11111001b
;循环移位指令 结果影响OF、CF
ROL:循环左移,高位到低位并送CF
ROR:循环右移, 低位到高位并送CF
RCL:循环左移, 进位值(原CF)到低位, 高位进位到CF
RCR:循环右移, 进位值(原CF)到高位, 低位进位到CF
【举个栗子春暖花开】
clc ;CF=0
mov al, 11101011b
rcr al, 2 ;循环右移一位,al=01110101,CF=1;再右移一位,al=10111010,CF=1
PrintHex al ;BA - 10111010b
;双精度移位 结果影响OF、SF、ZF、PF、CF
三个操作数:操作数一是目的操作数,操作数二一直不变且须是寄存器,操作数三是移位数目
SHLD:双精度左移,左边被移出的位由操作数二相同数目的高位填充
SHRD:双精度右移,右边被移出的位由操作数二相同数目的低位填充
【举个栗子春暖花开】
.code
main proc
;SHLD
mov ax, 1100110011110000b
mov dx, 1111111100000000b
shld ax, dx, 2
PrintHex ax ;33C3 - 0011001111000011b
;SHRD
mov ax, 0000111100110011b
mov dx, 0000000011111111b
shrd ax, dx, 2
PrintHex ax ;C3CC - 1100001111001100b
ret
main endp
end main
符号扩展指令
CBW:将AL扩展为AX,等价于movsx ax, al
CWDE:将AX扩展为EAX,等价于movsx eax, ax
CBW和CWDE对EFLAGS无影响
movsx的特征:如果源操作数的最高位是1, movsx会将目标寄存器中高出的位补1; 反之补0
符号扩展指令的本质:一个正数(无符号)或负数(有符号)在扩展储存空间时, 使用这些指令可保证原值不变
【举个栗子春暖花开】
.code
main proc
mov al, 68
cbw
PrintHex ax ;0044
PrintDec ax ;68
mov al, -68
cbw
PrintHex ax ;FFBC,BC为-68的补码
PrintDec ax ;-68
ret
main endp
end main
加减指令
inc,dec,neg(求补或者求反),add,adc,sub,sbb,cmp 结果影响OF、SF、ZF、AF、PF、CF
【辨析NEG与NOT】
;neg就相当于取反(not)+1
mov val,44
not val
inc val
等价于
mov val,44
neg val
【举个栗子春暖花开】
cmp隐含执行sub,但并不改写操作数,只是影响标志位ZF和SF
.code
main proc
mov eax, 3
cmp eax, 3
lahf
PrintHex ah ;46,即01000110b(ZF=1 说明两个数相等)
mov eax, 3
cmp eax, 2
lahf
PrintHex ah ;02,即00000010b(SF=0、ZF=0 说明前者>后者)
mov eax, 3
cmp eax, 4
lahf
PrintHex ah ;76,即10010111b(SF=1、ZF=0 说明前者<后者)
ret
main endp
end main
乘除指令
【只有一个参数】
格式:[mul 参数] 无符号乘 影响OF、CF
如果参数是8位,则将al做乘数,结果放在ax
如果参数是16位,则将ax做乘数,结果放在dx:ax中
如果参数是32位,则将eax做乘数,结果放在edx:eax中
【只有一个参数】
格式:[imul 参数] 有符号乘 影响OF、CF
如果参数是8位,则将al做乘数,结果放在ax
如果参数是16位,则将ax做乘数,结果放在dx:ax中
如果参数是32位,则将eax做乘数,结果放在edx:eax中
【有符号乘和无符号乘结果的一致性】
如果操作数(比如7Fh和7Fh)都没有符号位, 结果一致
如果操作数的其中之一(比如7Fh和80h)有符号位, 结果不一致
如果操作数(比如80h和80h)都有符号位, 结果也一致
【有多个参数】
imul r16/r32, r16/r32/m16/m32/i ;双操作数, (1)*(2) -> (1)
imul r16/r32, r16/r32/m16/m32, i ;三操作数, (2)*(3) -> (1)
其中常数 i 的位数可以 <= 但不能 > 其他操作数
【举个栗子春暖花开】
.data
val dd 8
.code
main proc
;IMUL 两个操作数
mov eax, 7
imul eax, val
PrintDec eax ;56
;IMUL 三个操作数
imul eax, val, 9
PrintDec eax ;72
ret
main endp
end main
【只有一个参数】
格式:[div/idiv 参数] 无符号除、有符号除 对EFLAGS无影响
如果参数是8位, 将把 AX做被除数;商->AL,余数->AH
如果参数是16位, 将把DX:AX做被除数;商->AX,余数->DX
如果参数是32位, 将把EDX:EAX做被除数;商->EAX,余数->EDX
【有符号除和无符号除结果的一致性】
与乘法相同
跳转指令
- 无条件跳转:jmp
- 根据cx,ecx寄存器的值跳转:jcxz(cx=0则跳转),jecxz(ecx为0则跳转)
- 根据EFLAGS标志位跳转:je,jne,jz等等
请参考汇编语言 王爽
串指令
- 什么是"串"?
- 不单指字符串,包括所有连续的数据(比如,数组)。
- 串指令只用于内存操作
;移动字符串:从esi到edi,执行后根据df的值,esi和edi同方向变化
.data
szSource db 'Delphi 2010', 0
len equ $ - szSource - 1 ;计算szSource字符串长度
szDest db len dup(?), 0
.code
main proc
lea esi, szSource
lea edi, szDest
mov ecx, len
cld ;设置df=0,以让串地址由低到高
rep movsb
PrintString szDest ;Delphi 2010
ret
main endp
end main
;比较数组相不相等:比较esi和edi,执行后根据df的值,esi和edi同方向变化
.data
dwArr1 dw 1,2,3,4,5
dwArr2 dw 1,2,3,4,5,6
.code
main proc
lea esi, dwArr1
lea edi, dwArr2
mov ecx, lengthof dwArr1
cmp ecx, lengthof dwArr2
jne L1
cld
repe cmpsw
jne L1
PrintText '两数组相等'
jmp L2
L1: PrintText '两数组不等'
L2: ret
main endp
end main
;扫描某数据是否存在:依据al/ax/eax中的数据扫描edi指向的数据,执行后根据df的值,edi变化
.data
szText db 'ABCDEFGH', 0
.code
main proc
lea edi, szText
mov al, 'F'
mov ecx, lengthof szText - 1
cld
repne scasb
je L1
PrintText '没找到'
jmp L2
L1: sub ecx, lengthof szText - 1
neg ecx
PrintDec ecx ;如果找得到, 这里显示是第几个字符; 本例结果是 6
L2: ret
main endp
end main
;储存数据:将al/ax/eax中的数据储存到edi指向的数据,执行后根据df的值,edi变化
.data
len = 31
szText db len dup(0), 0
.code
main proc
lea edi, szText
mov al, 'x'
mov ecx, len
cld
rep stosb
PrintString szText ;xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
ret
main endp
end main
;载入数据之数组求和:将esi指向的数据载入到al/ax/eax中,执行后根据df的值,edi变化
.data
dwArr dw 1,2,3,4,5,6,7,8,9,10
.code
main proc
lea esi, dwArr
mov ecx, lengthof dwArr
xor edx, edx
xor eax, eax
cld
@@: lodsw
add edx, eax
loop @B
PrintDec edx ;55
ret
main endp
end main
条件及循环伪指令
;和C语言类似
--------------
.if 条件
//语句
.elseif
//语句
.else
//语句
.endif
--------------
.while 条件
//语句
.endw
--------------
.repeat
//语句
.until 条件
--------------
【举个栗子春暖花开】
.code
main proc
mov eax, 9
.while TRUE
PrintDec eax
dec eax
.break .if eax == 5
.endw
ret
main endp
end main
【举个栗子春暖花开】
.code
main proc
mov eax, 0
.repeat
inc eax
.continue .if eax == 2
PrintDec eax
.until eax > 3
ret
main endp
end main
结构体
;结构体的使用
MyPoint struct
X dd ?
Y dd ?
MyPoint ends
.data
pt1 MyPoint <11,22>
.code
main proc
lea ebx, pt1
PrintDec (MyPoint ptr [ebx]).X ;11
PrintDec (MyPoint ptr [ebx]).Y ;22
ret
main endp
end main
;使用SYSTEMTIME结构获取系统时间
.data
sysTime SYSTEMTIME <>
.code
main proc
;SYSTEMTIME结构定义在windows.inc,GetLocalTime函数声明在kernel32.inc
invoke GetLocalTime, addr sysTime
PrintDec sysTime.wYear ;2021
PrintDec sysTime.wMonth ;2
PrintDec sysTime.wDay ;8
ret
main endp
end main