x86 32位汇编知识整理

序言

1.本文中汇编代码是 X86 架构下的32位汇编,在 Windows操作系统中,用masm编译器进行运行和调试工作。而且Visual Studio 2019自带masm编译器,故需进行以下环境配置:① 项目右键 ==> 生成依赖项 ==> 生成自定义 ==> 选择masm并点击确定。 ② 项目右键 ==> 属性 ==> 链接器 ==> 高级 ==> 入口函数 ==> 设置为mian函数

2.作者能力有限,文章难免有不足和勘误。请读者多多包涵。

一.必要知识

1.1 反汇编的使用

可以借助Visual Studio 2019反汇编功能,查看运行时的汇编代码,有以下几个要点:
① 在masm编译器写的汇编,并不一定与真正运行时的汇编完全一样 (编译器实现伪指令或加stdcall的壳子等),可借助反汇编查看有这些变化。
② 当反汇编C/C++代码时,debug模式和release模式下,生成的汇编有非常大的区别。在debug模式中,默认会开辟0C0h的栈空间,且每增加定义一个局部变量,缓冲区增加0xC个字节 。每一个变量都分配0xC个字节,即为每一个变量设置的前后4字节的区域,每个区间都初始化为cc,这样编译器就能检测变量是否越界了,因此第一个局部变量在ebp-8的位置,除此之外还会多一些一些检测语句,和初始化语句等。总之汇编代码的参考价值不大,但有利于详细了解代码的运行和实现过程
③ 在release模式下生成的汇编代码,更接近我们在masm写的汇编。但由于其强大的优化功能,会删去或简化很多代码,故可以用volatile关键字防止编译器的“过度”优化“,尽可能保留原有代码的样子。总之在该模式下的汇编代码更具有参考和学习价值。release模式下不会对内联汇编进行优化

1.2 对齐操作

数据对齐可以提高不同硬件平台的兼容性,和读取数据的熟读,常见的数据对齐有以下几种:
① 数据对齐,以结构体对齐为例:
a.由于X86架构下一次读取4字节数据速度最快,所以要保证结构体的大小是4的整数倍
b.结构体大小是最大原子成员的整数倍,原子成员如int,char,double等。
c.结构体首地址到成员首地址的偏移量,必须是该成员大小的整数倍
d.如果结构体中包含结构体或数组,则将子结构体或数组的拆开来,将所有的成员放在一起,进行对齐
e.综上由经验可得:先进行d步骤,再看如果结构体中有double或longlong类型,则结构体的大小是8的整数倍,如果没有,则结构体的大小是4的整数倍
② 堆栈对齐:
a.32位下任何类型的局部变量都要占4字节
b.64位下任何类型的局部变量都要占8字节,且每次使用的堆栈大小是16的整数倍
③ 代码对齐,在X86架构下指令的获取通常以16字节为单位进行,在循环中为了提高循环的性能,可用 nop 或其它无意义的指令进行代码对齐,把每次循环开始的位置移到0x00XXXXXXX0的位置,或0x00XXXXXXX5的位置,指在提高16字节中所包含的有效代码条数。注意,如果循环次数较少,反而会降低性能,万万不可强行盲目使用
以下为的常用的用于代码对齐的指令。

指令指令长度(字节)
nop1
xchg ax,ax2
nop dword ptr [eax] 或 xor ax,ax3
nop dword ptr [eax+00h]4
nop word ptr [eax+eax]5

注意:*1和+00h 自己在代码时会被优化掉,而在反汇编中也不会显示所以具体指令的实际长度,请看反汇编中代码地址的变化

1.3 其它要点

1.X86架构用的是小端序,即数据的低位放在低地址处,数据的高位放在高地址处。

2.X86架构读取和写入数据,都是从低地址到高地址。通常我们所说的地址,指的也是数据的低地址。

二.32位寄存器介绍

2.1 32位寄存器说明

32位eax寄存器的结构
 

1.虽然寄存器变成了32位, 但是之前的16位和8位寄存器放在了32位寄存器的低位处。如eax,ebx,ecx,edx等。

2.关于寄存器,每个线程都有它自己的一组CPU寄存器,称为线程的上下文,用于保存该线程上次运行时,CPU寄存器的状态,在线程切换时用于还原。 (结构名叫CONTEXT,存在winnt.h文件中)。

2.2 数据寄存器

eax 寄存器又称累加器,主要用于加减乘除,进行数的输入,保存函数返回值等操作。
ebx 寄存器又称基地址寄存器,在内存寻址时存放基地址
ecx 寄存器又称计数寄存器,主要用于控制循环次数
edx 寄存器又称数据寄存器,主要用于加减乘除,保存除法的余数,存放I/O的端口地址。

2.3 变址寄存器

esi(源索引寄存器)和edi(目标索引寄存器)主要用于存放存储单元在段内的偏移量。主要用于对串进行操作,在很多字符串操作指令中, DS:ESI指向源串,而ES:EDI指向目标串。

2.4 指针寄存器

esp 堆栈指针寄存器,主要用于开辟栈的空间,并最后过程平栈来释放栈的空间
ebp 基本指针寄存器,主要用于辅助访问函数传入的实参,和函数内的局部变量

2.5 段寄存器

CS:代码段寄存器 ES:附加段寄存器
DS:数据段寄存器 FS:附加段寄存器
SS:堆栈段寄存器 GS:附件段寄存器

注意在32位操作系统中,段寄存器中存的不再是基址,而是段选择子

2.6 指令指针寄存器

EIP 指令指针寄存器,指向正在执行的指令的地址。通常情况下不能对进行EIP修改或转移操作

2.7 标志寄存器

eflags 是一个存放条件标志控制标志和系统标志的寄存器,是程序现象各种条件判断的基础
在这里插入图片描述

标志位标志全称标志序号标志位说明
CF/CY进位标志位0当执行加减运算时,使最高位产生进位或借位时CF=1,否则为CF=0
PF/PE奇偶标志位2当运算结果(二进制)中1的个数为偶数时PF=1,否则为基数PF=0
AF/AC辅助进位标志4执行加减运算时,结果的低4位向高4位有进位(或借位)时,则AF=1,否则AF=0
ZF/ZR零标志位6运算结果为零,则ZF=1,否则ZF=0
SF/PL符号标志位7若运算结果为负数则SF=1,若为非负数则SF=0
TF单步标志位8为方便程序调试而设计的,TF=1单步执行指令,TF=0则CPU正常执行程序
IF/EI中断使能标志位9当IF=1 CPU可响应可屏蔽中断请求,当设置IF=0则CPU不响应可屏蔽中断请求
DF/UP方向标志位10当DF=0时为正向传送数据(cld),否则为逆向传送数据(std)
OF/OV溢出标志位11记录是否产生了溢出,当补码运算有溢出时OF=1,否则OF=0

三.基础知识

3.1 常用数据类型

名称描述
BYTE占8位,无符号位,可简写为db
WORD占16位,无符号位,可简写为dw
DWORD占32位,无符号位,可简写为dd
QWORD占64位,无符号位,可简写为dq
SBYTE占8位,有符号位
SWORD占16位,有符号位
SDWORD占32位,有符号位

注意:通常数据类型也会反应在指令的后缀上,如movsb,movsw,movsd,movsq

3.2 常用符号

reg代表寄存器, mem代表内存, imm代表立即数

符号语法描述
[][reg/mem]① 相当于C语言中的*解引用操作,取值操作 ② []运算符做了简化操作,完整的写法是:数据类型 ptr [reg/mem] ③ []运算符通常和数据传输指令一起用
?变量名 数据类型 ?在变量初始化阶段使用,表示不对变量进行初始化操作
$Jcc + imm① 与jcc指令配合使用,$表示当前语句的地址,加立即数后用于跳转 ② 在16位的汇编中,声明字符串时$可表示"\0"
offsetoffset [reg/mem]用于取指定对象的地址值,通常和数据传输指令一起使用
<>① <数值1,…> ② <数值计算式>用<>括起来,表示一个整体,通常用于各种变量的初始化
!!字符转义符号相当于c在字符串中的 \,如<2!>3> 的第二个>进行转义

四.32位汇编实例

.586                     ; CUP指令集
.model flat,stdcall     ; 选择内存模式,调用约定 --> 平坦分段模式,基cs段和ds段共一个用4GB的空间。自己写的有参函数的调用约定用stdcall调用约定并自动套上stdcall的壳子 (无参的函数的调用约定自己写)
.stack 2048              ; 设置栈的大小,默认为1024KB
option casemap:none      ; option用于启用和禁用汇编程序的功能,这里是启用名称要区分大小写

includelib ucrt.lib      ; 载入C语言运行时库
includelib legacy_stdio_definitions.lib  ; 载入C语言的stdio和stdilb库
includelib kernel32.lib  ; 载入win32库(注意:即使只使用C语言库,也建议载入,防止出现奇怪的问题)
includelib user32.lib    ; 载入win32库(注意:即使只使用C语言库,也建议载入,防止出现奇怪的问题)

extern printf:proc    ; 声明printf函数,C语言库函数声明方式
MessageBoxA proto hWndx:DWORD,lpText:DWORD,lpCaption:DWORD,uType:DWORD ; win32函数的声明方式,用于弹出窗口,注意要声明函数的参数
ExitProcess proto uCode:DWORD   ; win32函数用于退出程序,注意要声明函数的参数

.data                 ; 定义数据区    
szBuffer db "%d",0
szTitle db "23123",0

.code                 ; 定义代码区 
main proc             ; 起始入口函数
	mov eax,64h
	push eax 
	mov ecx,offset szBuffer   ; 或用 lea ecx,[szBuffer]
	push ecx
	call printf
	add esp,8          ; 需要平栈
	
	push 0 
	push 0 
	push 0 
	push 0 
	call MessageBoxA   ; 调用win32函数,不需要平栈
	invoke MessageBoxA,0,offset szTitle,offset szTitle,0  ; invoke用于简化调用,其会自动push各个参数,并以stdcall调用约定调用函数,不用手动平栈
	push 0
	call ExitProcess   ; 退出程序,win32函数不需要平栈
main endp              ; main函数结束位置
end                    ; 汇编代码结束符符

五.数据传输指令

指令语法描述
movmov reg/mem , , , reg/mem/imm① mov a,b 相当于 a=b ② mov两边的对象的操作数要一样,且至少有一个reg ③ mov不会改变标志位
movzx/movsxmovzx/movsx reg , , , reg/mem① movzx和movsx要求左边对象的操作数,要大于右边对象的操作数 ② 赋值时movzx对左边对象进行低位赋值,高位置0。movsx 对左边对象进行低位赋值,高位置0还是1,由其最高位是0还是1决定 ③ 其它特性同mov
leamov reg/mem , , , reg/memlea a,b 相当于 a=&b,把b的地址赋值给a
xchgxchg reg/mem , , , reg/mem① 用于交换两边的值 ② 两边的对象的操作数要一样
push 和 pop① push reg/mem/imm ② pop reg/mempush 是先 esp-4,再把值压入栈。 ② pop 是先把值弹出栈,再esp+4。 ③ 32位下push 和 pop 操作数为32字节
lahf 和 sahf单独使用占一整行① lahf用于把eflags寄存器前八位的值,放到ah寄存器中 ② sahf用于把ah中的值,放到eflags寄存器前八位中
cmovCCcmovCC reg reg① 该指令只能对寄存器进行操作 ② 当满足某一条件时,该指令会把操作数2的值赋值给操作数1 ③ 使用cmovCC 时 CUP指令集必须高于或等于.686

六.数据计算指令

指令语法描述
addadd reg/mem , , , reg/mem/imm①add a,b 相当于 a = a+b ② 会改变标志位
subsub reg/mem , , , reg/mem/imm①sub a,b 相当于 a = a-b ② 会改变标志位
mul 和 imulmul/imul reg/mem , , , reg/mem/imm① mul用于操作无有符号数,imul用于操作有符号数 ② mul a,b 相当于 a = a*b ③ mul a,b,c 相当于 a = b*c
div 和 idivdiv/idiv reg/mem , , , reg/mem/imm① div用于操作无有符号数,idiv用于操作有符号数 ② 32位中被除数必须是64位,可用 cdq 指令将eax与edx连接,使eax扩展到64位 ③ 余数会存入edx
shl 和 shrshl/shr reg/mem , , , reg/mem/imm对无符号数进行逻辑左移或逻辑右移
sal 和 sarsal/sar reg/mem , , , reg/mem/imm对有符号数进行算数左移或算数右移
inc 和 decinc/dec reg/mem相当于+1和-1操作
cmpcmp reg/mem , , , reg/mem/imm把两边的对象值相减比较,不会有结果,用于改变ZF和CF标志位
and 和 or 和 xorand/or reg/mem , , , reg/mem与,或,异或运算
notnot reg/mem非运算,~x = -(x+1)

七.JCC指令

7.1 根据地址值或标记

1.语法: jmp reg/mem/imm/标号

2.标号的用法类似于goto语句,其本质是一个地址值。标号可以用数据传输指令进行传输,从而弥补无法对eip使用数据传输指令这一问题。(注意可以把标号push到栈中,通过ebp+偏移量来操作)

3.不同位数的汇编中,jmp语句的寻址范围是不同的,但总归是有限的。==可通过设立中间跳转点的方式,间接扩大寻址范围,==即多 jmp 几次。

4…在内联汇编中使用jmp,通常会打乱代码的执行顺序。用户在下调试断点时,只能下在函数的开头,不然会出现程序的崩溃。

#include<stdio.h>

int main()
{
	char* s = "%d\n";
	_asm
	{
		mov eax,tab   ; 把标号放进eax寄存器中
		jmp eax
	tab:
		push 30       
		mov ecx,[s]   ; 注意s是指针不再用取地址了
		push ecx
		call printf
		add esp,8
	}
	 
	_asm
	{
		jmp $+7       ;使用$符号控制跳转的位置,跳转到 push 40
		push 30      
		push 40
		mov ecx, [s]  ; 注意s是指针不再用取地址了
		push ecx
		call printf
		add esp, 8
	}
	return 0;
}

7.2 根据eflags寄存器

1.通过cmp等比较指令,改变eflags寄存器的标志位,根据不同的标志位调用不同的JCC指令。(注意有很多JCC指令名称不同,但功能是相同的)
2.以 cmp ax, bx 语句为例,用于改变ZF零标志位CF进位标志位,可得出以下几种情况。

结果标志位情况指令
ax == bxZF=1je
ax != bxZF=0jne
ax < bxCF=1jb
ax > bxZF=0 && CF=0ja
ax <= bxZF=1 ||CF=1jna
ax >= bxCF=0jnb

八.函数

8.1 函数相关指令

指令语法描述
callcall 函数名call语句的处理过程,先把该语句的下一条语句的地址 push 入栈,用于最后函数返回。再根据函数名,jmp 到函数开头,开始运行函数。
ret① 单独使用占一整行 ② ret N① call语句的处理过程,先把下一条语句的地址 pop 出栈再jmp 回到函数调用位置的下一条,继续执行接下来的语句。② ret N 语句处理过程同上,只是最后多了 add esp,N 语句,实现函数内的平栈。ret N 通常在 stdcall 中使用。 ③ ret语句要放在函数的结尾
leave单独使用占一整行① leave语句的处理过程,先push esp,ebp 释放申请局部变量的空间,再pop edp 还原 edp ② leave 通常在 stdcall 中和 ret N语句一起使用,放在函数的结尾。
proto函数名 proto 参数列表用于声明 win32 函数
invokeinvoke 函数名 参数列表invoke 本质还是push 和call。其为了方便调用win32 函数写有参数列表的函数注意根据实际情况考虑是否平栈
local① local 局部变量名:类型 ② local 标签名① local在普通函数中使用时,用于定义局部变量 ② local在宏函数中使用时,用于定义唯一标签,即每次使用宏函数生成的标签都不一样 (供JCC语句使用) ③ 无论在哪使用,local语句都必须放在开头

8.2 函数调用约定

8.2.1 说明

1.通常在进行参数的传递时,不会使用ebx基地址寄存器,而是eax,ecx,edx三者循环使用。

2.函数内部处理的模板如下,注意对于不同的函数调用约定可能会有些许变化,但主体结构是不变的。

函数的堆栈结构 (不同调用方式会有些许区别)
func proc
	push ebp            ; 保存ebp的值,用于最后还原
	mov esp,ebp         ; 设置ebp的位置,用于通过ebp加减偏移量,来获取局部变量和传入的参数
	; sub esp,XXX       ; 开辟栈空间,用于保存局部变量(有的开辟栈用 add esp,负数)
	; push ebx|esi|edi  ; 如果该函数,和调用该函数的主函数,两者有共用的寄存器(如ebx|esi|edi|ecx等) 则要把对应的寄存器push入栈,进行保存,并在最后还原 (注意寄存器的出栈和入栈的顺序) 如果不满足这部分条件,则该部分不用写。 
	
	; 操作局部变量,传入的参数,或掉用其它函数(注意其它函数的调用约定,确定是否要平栈)
	
	; 如果函数最后要有返回值,则要将返回值mov到eax中
	
	; pop edi|esi|ebx   ;对应上面的 push ebx|esi|edi,pop出相应的寄存器,进行还原。(注意寄存器的出栈顺序)
	; add esp,XXX       ; 平栈,销毁用于保存局部变量而开辟的栈的空间
	pop ebp             ; 还原ebp
	ret   
func end

8.2.2 _cdecl

1._cdecl函数调用约定,是C/C++默认的调用约定,需要在函数调用结束后,手动平栈

2.在用_cdecl函数调用约定的函数中,用ebp+偏移量来获取传入的参数,通常第一个参数在esp+8的位置用ebp-偏移量来获取局部变量,通常第一个局部变量在esp-4的位置。(注意数据类型不同偏移量也不同)

; ============================== 声明 ==============================
; 由于只要显示的声明形参,都会被编译器套上stdcall的壳子。
; 故所有函数都不声明形参,但传参数,有局部变量,自己实现cdecl函数调用约定

.586                     ; CUP指令集
.model flat,stdcall      ; 平坦分段模式,和cdecl函数调用约定
option casemap:none      ; option用于启用和禁用汇编程序的功能,这里是启用名称要区分大小写

includelib ucrt.lib      ; 载入C语言运行时库
includelib legacy_stdio_definitions.lib  ; 载入C语言的stdio和stdilb库
includelib kernel32.lib  ; 载入win32库(注意:即使只使用C语言库,也建议载入,防止出现奇怪的问题)
includelib user32.lib    ; 载入win32库(注意:即使只使用C语言库,也建议载入,防止出现奇怪的问题)

extern printf:proc       ; 声明printf函数,C语言库函数声明方式
ExitProcess proto uCode:DWORD  ; win32函数用于退出程序,注意要声明函数的参数

.data             ; 数据区用于存变量(汇编代码一般分为.const常量区 .data数据区 .code代码区)
szBuffer db "%d %d %d %d",0dh,0ah,0    ; 注意0dh0ah指的是\r\n用于换行,注意直接写字符串中是无效的
     
.code                           ; 代码区
PrintInt proc	                ; 定义无形参函数,自己实现cdecl  
	push ebp                    ; 保存ebp的值,用于最后还原
	mov ebp,esp        ; 设置ebp的位置,用于通过ebp加减偏移量,来获取局部变量和传入的参数
	sub esp,8          ; 开辟栈空间,用于保存局部变量
	
	push esi           ; 假设主函数和子函数共用了esi和edi寄存器
	push edi

	mov eax,555
	mov dword ptr[ebp-4],eax    ; 用ebp-偏移量来获取局部变量,第一个局部变量在esp-4的位置
	mov ecx,666
	mov dword ptr[ebp-8],ecx

	push dword ptr[ebp-8]       ; 注意入栈顺序
	push dword ptr[ebp-4]  
	push dword ptr[ebp+12]      ; 用ebp+偏移量,获取传入的参数
	push dword ptr[ebp+8]       ; 第一个参数必在ebp+8的位置
	push offset szBuffer 	  
	call printf
	add esp,20                  ; 内部调用了cdecl的函数,故要平栈(参数个数*4=20)

	pop edi                     ; 还原esi和edi寄存器(注意寄存器的出栈顺序)
	pop esi

	add esp,8                   ; 也可用mov esp,ebp平栈,销毁用于保存局部变量而开辟的栈的空间
	pop ebp                     ; 还原ebp
	ret
PrintInt endp  			

main proc 

	push 222
	push 111
	call PrintInt               
	add esp,8                   ; cdecl在函数调用结束后,手动平栈
	
	invoke ExitProcess,0        ; 退出程序
main endp				        ; main函数结束位置
end                             ; 汇编代码结束符符

8.2.3 _stdcall

1._stdcall是32位下Win32 API的默认调用约定其在函数内部自动平栈,在函数调用结束后,不用再手动平栈

2.其堆栈的使用过程与_cdecl一样。

; ============================== 声明 ==============================
; 由于只要显示的声明形参,都会被编译器套上stdcall的壳子。
; 故所有函数都不声明形参,但传参数,有局部变量,自己实现stdcall函数调用约定

.586                     ; CUP指令集
.model flat,stdcall      ; 平坦分段模式,和cdecl函数调用约定
option casemap:none      ; option用于启用和禁用汇编程序的功能,这里是启用名称要区分大小写

includelib ucrt.lib      ; 载入C语言运行时库
includelib legacy_stdio_definitions.lib  ; 载入C语言的stdio和stdilb库
includelib kernel32.lib  ; 载入win32库(注意:即使只使用C语言库,也建议载入,防止出现奇怪的问题)
includelib user32.lib    ; 载入win32库(注意:即使只使用C语言库,也建议载入,防止出现奇怪的问题)

extern printf:proc       ; 声明printf函数,C语言库函数声明方式
ExitProcess proto uCode:DWORD  ; win32函数用于退出程序,注意要声明函数的参数

.data             ; 数据区用于存变量(汇编代码一般分为.const常量区 .data数据区 .code代码区)
szBuffer db "%d %d %d %d",0dh,0ah,0    ; 注意0dh0ah指的是\r\n用于换行,注意直接写字符串中是无效的
     
.code                         
PrintInt proc	               
	push ebp                    ; 保存ebp的值,用于最后还原
	mov ebp,esp        ; 设置ebp的位置,用于通过ebp加减偏移量,来获取局部变量和传入的参数
	sub esp,8          ; 开辟栈空间,用于保存局部变量
	
	push esi           ; 假设主函数和子函数共用了esi和edi寄存器
	push edi

	mov eax,555
	mov dword ptr[ebp-4],eax    ; 用ebp-偏移量来获取局部变量,第一个局部变量在esp-4的位置
	mov ecx,666
	mov dword ptr[ebp-8],ecx

	push dword ptr[ebp-8]       ; 注意入栈顺序
	push dword ptr[ebp-4]  
	push dword ptr[ebp+12]      ; 用ebp+偏移量,获取传入的参数
	push dword ptr[ebp+8]       ; 第一个参数必在ebp+8的位置
	push offset szBuffer 	  
	call printf
	add esp,20                  ; 内部调用了cdecl的函数,故要平栈(参数个数*4=20)

	pop edi                     ; 还原esi和edi寄存器(注意寄存器的出栈顺序)
	pop esi

	leave  					    ; leave语句相当于先 mov esp,ebp平栈,销毁用于保存局部变量而开辟的栈的空间,再用 pop ebp,还原ebp 
	ret 8						; ret 8 语句相当于先ret,再 add esp,8 实现函数内平栈
PrintInt endp  			

main proc 

	push 222
	push 111
	call PrintInt               ; stdcall不需要,手动平栈
	                 
	invoke ExitProcess,0        ; 退出程序
main endp				        ; main函数结束位置
end                             ; 汇编代码结束符符

8.2.4 _fastcall 32位

1.32位 _fastcall 的前2个参数用ecx 和 edx传后面的参数通过push堆栈进行传参且函数内部会自动平栈

2.通常ecx和edx传入的值要放到局部变量中,所以用_fastcall的函数默认要开辟8字节的栈空间。在特殊情况下,为提高执行效率,可以不放到局部变量中直接使用,总之具体情况具体分析。

; ============================== 声明 ==============================
; 由于只要显示的声明形参,都会被编译器套上stdcall的壳子。
; 故所有函数都不声明形参,但传参数,有局部变量,自己实现fastcall函数调用约定

.586                     ; CUP指令集
.model flat,stdcall      ; 平坦分段模式,和cdecl函数调用约定
option casemap:none      ; option用于启用和禁用汇编程序的功能,这里是启用名称要区分大小写

includelib ucrt.lib      ; 载入C语言运行时库
includelib legacy_stdio_definitions.lib  ; 载入C语言的stdio和stdilb库
includelib kernel32.lib  ; 载入win32库(注意:即使只使用C语言库,也建议载入,防止出现奇怪的问题)
includelib user32.lib    ; 载入win32库(注意:即使只使用C语言库,也建议载入,防止出现奇怪的问题)

extern printf:proc       ; 声明printf函数,C语言库函数声明方式
ExitProcess proto uCode:DWORD  ; win32函数用于退出程序,注意要声明函数的参数

.data             ; 数据区用于存变量(汇编代码一般分为.const常量区 .data数据区 .code代码区)
szBuffer db "%d %d %d %d %d %d",0dh,0ah,0    ; 注意0dh0ah指的是\r\n用于换行,注意直接写字符串中是无效的
     
.code                          
PrintInt proc	               
	push ebp  
	mov ebp,esp  
	sub esp,8+8    ; 用_fastcall的函数默认要开辟8字节的栈空间,另外8字节是两个局部变量
 
	push esi       ; 假设主函数和子函数共用了esi和edi寄存器
	push edi  

	mov dword ptr [ebp-8],edx   ; 把ecx和edx的中的值放到局部变量中
	mov dword ptr [ebp-4],ecx   

	mov dword ptr [ebp-12],555  ; 给局部变量进行赋值
	mov dword ptr [ebp-16],666  

    push dword ptr [ebp-16]  	; 注意入栈的顺序
	push dword ptr [ebp-12]
	push dword ptr [ebp+12]
	push dword ptr [ebp+8]
	push dword ptr [ebp-8]  
	push dword ptr [ebp-4]
    push offset szBuffer
    call printf
    add esp,28

	pop edi                     ; 还原esi和edi寄存器(注意寄存器的出栈顺序)
	pop esi   
	
	add esp,16                  ; 也可用mov esp,ebp平栈,用于销毁开辟的栈的空间
	pop ebp                     ; 还原ebp
	ret 8						; ret 8 语句相当于先ret,再 add esp,8 实现函数内平栈
PrintInt endp  			

main proc 

	push 444  
	push 333  
	mov edx,222  
	mov ecx,111  
	call PrintInt   			; fastcall不需要,手动平栈
                     
	invoke ExitProcess,0        ; 退出程序
main endp				        ; main函数结束位置
end                             ; 汇编代码结束符符

8.2.5 _fastcall 64位

1.在64位下所有的函数默认都使用 _fastcall 64位函数调用约定。当然也可以用其它函数调用约定。

2.在函数约定下,父函数要为子函数开辟,存参数的空间。开辟空间大小的计算公式为: ( 参数最多的子函数的参数个数 + 局部变量个数 ) ∗ 8 + 8 (参数最多的子函数的参数个数+局部变量个数)*8+8 (参数最多的子函数的参数个数+局部变量个数)8+8,其中 ( 参数最多的子函数的参数个数 ∗ 8 + 8 ) (参数最多的子函数的参数个数*8+8) (参数最多的子函数的参数个数8+8),是给子函数预留的参数空间。注意:上述公式中加8多开辟的8字节,是留给rip的。当call函数时esp会再减去8,此时总的开辟的空间正好被16整除,实现64位下的堆栈平衡。因此如果主函数无参数,且子函数的参数不超过4个时,默认开辟28h个字节。(如果嫌麻烦直接开辟100h即可)。

3.用64位 _fastcall 时前4个参数用rcx,rdx,r8,r9传后面参数直接mov到栈中且函数的参数区由主函数控制,因此不用在平栈

4.通常rcx,rdx,r8,r9传入的值,在函数的开头就要放到父函数预留的栈空间中。在特殊情况下,为提高执行效率,可以不放到局部变量中直接使用,总之具体情况具体分析。64位 _fastcall具有极高的灵活性只要知道核心规则,都可以根据实际情况,进行改变。

64位fastcall的堆栈结构
includelib ucrt.lib   
includelib legacy_stdio_definitions.lib
includelib kernel32.lib  
includelib user32.lib   

extern printf:proc    ; 声明printf函数,C语言库函数声明方式

.data             
szBuffer db "%d %d %d %d %d %d %d %d",0dh,0ah,0  
     
.code                          
PrintInt proc
	; 函数开头先将寄存中的参数,存到父函数预留的栈空间中
	mov dword ptr [rsp+32],r9d  ; 注意 rsp+32 位置没有冲突,因为call的时候rsp-8了
	mov dword ptr [rsp+24],r8d  ; 注意这里进行了堆栈对齐,即使用的是r8d,也要占8用8字节
	mov dword ptr [rsp+16],edx
	mov dword ptr [rsp+8],ecx   ; 注意 rsp+8 位置也没有冲突,因为rsp位置也能存数据
	
	push rbp  ; 保存rbp
	
	push rsi  ; 假设主函数和子函数共用了esi和edi寄存器
	push rdi
	
	sub rsp,96        ; 子函数有9个参数,还有2个局部变量,故要开辟(9+2)*8+8=96(看公式) 
	lea rbp,[rsp+80]  ; 用rsp分割开辟的空间,其中rbp到rsp这80个字节,是为子函数的调用准备的。rbp到rdi的2*8=16个字节,是为局部变量准备的
 
	mov dword ptr[rbp+8],777    ; 给局部变量赋值
	mov dword ptr[rbp+16],888   

	mov eax,dword ptr[rbp+48]   ; 注意计算rbp到参数的偏移量,是64位fastcall的难点,要特别小心
	mov dword ptr [rsp+64],eax
	mov ecx,dword ptr[rbp+56]
	mov dword ptr [rsp+56],ecx
	mov edx,dword ptr[rbp+64]
	mov dword ptr [rsp+48],edx
	mov eax,dword ptr[rbp+72]
	mov dword ptr [rsp+40],eax
	mov ecx,dword ptr[rbp+80]
	mov dword ptr [rsp+32],ecx

	mov r9d,dword ptr [rbp+88]  ; 前4给参数用寄存器传参
	mov r8d,dword ptr [rbp+8]
	mov edx,dword ptr [rbp+16]
	mov rcx,offset szBuffer     
    call printf                 ; 64位默认全是fastcall,故不用平栈
   
	add rsp,96                  ; 也可用lea rsp,[rbp+16]平栈,用于销毁开辟的栈的空间
	
	pop rdi                     ; 还原esi和edi寄存器(注意寄存器的出栈顺序)
	pop rsi
	
	pop rbp         
	ret						
PrintInt endp  			

main proc 
	push rbp
	mov rbp,rsp
	sub rsp,56                    ; 子函数有6个参数,且无局部变量,故要开辟6*8+8=56(看公式) 	       
	
	mov dword ptr [rsp+40],666    ; 超出4个参数的部分,直接mov到栈中
	mov dword ptr [rsp+32],555    ; (注意:第5个参数永远是从[rsp+32]位置开始传)
	mov r9d,444 
	mov r8d,333
	mov edx,222  
	mov ecx,111  
	call PrintInt   			  ; fastcall不需要,手动平栈
    
    add rsp,56
    pop rbp
	ret        					  ; 64位汇编用ret即可退出程序
main endp				      
end     

8.2.6 _thiscall

_thiscall是C++成员函数所的调用约定,主要用ecx或rcx传递类的地址,即this指针,作为成员函数的第一个参数。到函数中时,会把ecx或rcx中的值放到局部变量区中,作为第一个局部变量,故用_thiscall的函数,默认要开辟4字节的空间。_thiscall内部平栈机制实现与_stdcall的内部实现完全一样,都在函数内自动平栈,不用再手动平栈。在_fastcall中ecx或rcx是最后传递的,这与_thiscall相契合,但在64位_fastcall下rcx会被放到变量区

#include<iostream>

class BOOK
{
    int m_a;
public:
    BOOK(int a) :m_a(a){}

    void Look(){
        std::cout << m_a << std::endl;
    }
    
    void Change(int a){
        m_a = a;
        std::cout << m_a << std::endl;
    }
};

int main()
{
    BOOK b{10};

    _asm {
        lea ecx,[b]      
        call BOOK::Look     ; 不用再手动平栈
         
        push 100
        lea ecx,[b]
        call BOOK::Change   ; 不用再手动平栈
    }

    return 0;
}

8.3 不同函数的实现

.586                     ; CUP指令集
.model flat,stdcall      ; 选择内存模式,调用约定 --> 平坦分段模式,基cs段和ds段共一个用4GB的空间。自己写的有参函数的调用约定用stdcall调用约定并自动套上stdcall的壳子 (无参的函数的调用约定自己写)
.stack 2048              ; 设置栈的大小,默认为1024KB
option casemap:none      ; option用于启用和禁用汇编程序的功能,这里是启用名称要区分大小写

includelib ucrt.lib      ; 载入C语言运行时库
includelib legacy_stdio_definitions.lib  ; 载入C语言的stdio和stdilb库
includelib kernel32.lib  ; 载入win32库(注意:即使只使用C语言库,也建议载入,防止出现奇怪的问题)
includelib user32.lib    ; 载入win32库(注意:即使只使用C语言库,也建议载入,防止出现奇怪的问题)

extern printf:proc              ; 声明printf函数,C语言库函数声明方式
ExitProcess proto uCode:DWORD   ; win32函数用于退出程序,注意要声明函数的参数

.data             ; 数据区用于存变量(汇编代码一般分为.const常量区 .data数据区 .code代码区)
szBuffer1 db "%d %d",0dh,0ah,0  ; 注意0dh0ah指的是\r\n用于换行,注意直接写字符串中是无效的
szBuffer2 db "%s",0dh,0ah,0
szBuffer3 db "%c %d %d %c",0dh,0ah,0 
char1 db 't'
num1 dd 123                 

.code

; 1.========================= 定义无形参函数,自己实现cdecl =========================
PrintInt1 proc	                     
	push ebp
	mov ebp,esp
	push dword ptr[ebp+12]          ; 用ebp+偏移量,获取传入的参数
	push dword ptr[ebp+8]           ; 第一个参数必在ebp+8的位置
	push offset szBuffer1 	  
	call printf
	add esp,12    
	pop ebp
	ret
PrintInt1 endp  			

; 2.======================= 定义有形参函数,系统自动实现stdcall ======================
PrintInt2 proc n1:DWORD,n2:DWORD      
	push n2                        ; 直接用形参名获取传入的参数
	push n1						   ; 直接用形参名获取传入的参数
	push offset szBuffer1 	  
	call printf
	add esp,12                 
	ret
PrintInt2 endp   			 

; 3.======================= 定义有形参和局部变量的函数 ===============================
PrintInt3 proc n1:DWORD,n2:DWORD  
	sub esp,8					   ; 开辟8字节空间,存放2个dword变量
	mov eax,n1
	mov dword ptr[ebp-4],eax       ; 用ebp-偏移量,获取局部变量,第一个局部变量在ebp-4的位置
	mov ecx,n2
	mov dword ptr[ebp-8],ecx       

	push dword ptr[ebp-8]          ; 注意入栈的顺序                    
	push dword ptr[ebp-4]						   
	push offset szBuffer1 	  
	call printf
	add esp,12                     ; 平栈,销毁为局部变量而开辟8的字节空间           
	ret
PrintInt3 endp  

; 4.======================= 用local伪指令实现有局部变量的函数 ========================
PrintInt4 proc n1:DWORD,n2:DWORD    
	; 1.用 local 伪指令,用于方便操作局部变量,不用再手动开辟栈空间了
	; 2.局部变量涉及到堆栈开辟,所以必须在函数开头声明
	; 3.local 伪指令所在的函数会自动套上stdcall的壳子,故建议只在stdcall的函数中使用
	
	local @num1:DWORD, @num2:DWORD ; 声明两个局部变量,开头加@用于区分形参
	mov eax,n1
	mov @num1,eax   
	mov ecx,n2
	mov @num2,ecx

	push @num2                      
	push @num1					   
	push offset szBuffer1 	  
	call printf
	add esp,12                 
	ret
PrintInt4 endp  

; 5.======================= 32位下局部变量的堆栈对齐 ==============================
PrintInt5 proc  
	push ebp
	mov ebp,esp
	sub esp,16  ; 32位下的堆栈对齐的约定,任何类型的局部变量都要占4字节,这里使用参数的类型分别为,char,short,int,char

	mov al,102
	mov byte ptr[ebp-1],al        ; 0+1=1
	mov ax,123
	mov word ptr[ebp-6],ax        ; 4+2=6
	mov eax,num1
	mov dword ptr[ebp-12],eax     ; 4+4+4=12
	mov al,char1
	mov byte ptr[ebp-13],al       ; 4+4+4+1=13

	movzx eax,byte ptr[ebp-1]     ; 注意数据类型用movzx语句,因为在32位下push操作数为32字节
	push eax
	movzx ecx,word ptr[ebp-6]     ; 注意数据类型用movzx语句,因为在32位下push操作数为32字节
	push ecx
	mov edx,dword ptr[ebp-12]     ; 注意数据类型用movzx语句,因为在32位下push操作数为32字节
	push edx           
	movzx eax,byte ptr[ebp-13]
	push eax
	push offset szBuffer3 	  
	call printf
	add esp,20

	add esp,16
	pop ebp
	ret
PrintInt5 endp  

main proc  
	push 456
	push num1
	call PrintInt1  ; 调用无参函数  
	add esp,8       ; 因为是cdecl,要手动平栈

	push 456
	push num1
	call PrintInt2  ; 调用有参函数 

	push 456
	push num1
	call PrintInt3  ; 调用有参有局部变量的函数

	push 456
	push num1
	call PrintInt4  ; 调用有local伪指令的函数
	    
	call PrintInt5  ; 32位下局部变量的堆栈对齐
	
	invoke ExitProcess,0       ; 退出程序,win32函数都用stdcall调用约定,故不需要平栈
main endp				       ; main函数结束位置
end                            ; 汇编代码结束符符

九.串的处理

9.1 串处理的相关指令

指令语法描述
cld 或 std单独使用占一整行用于改变eflags寄存器的df位,方向位。在操作串时使esi和edi每次加步长,或每次减步长,即控制顺着遍历,还是倒着遍历
rep后面跟其它指令重复执行该指令后面的汇编代码,执行次数由ecx寄存器控制
repne后面跟其它指令当ZF=0时,即比较的两个值不相等时,重复执行该指令后面的汇编代码,执行次数由ecx寄存器控制
movsb 或 movsw 或 movsd单独使用占一整行用于复制不同大小元素到的串中,每一执行一次就复制一个元素到目标串,并使esi和 edi 加步长。元素放在 al/ax/eax 中
cmpsb 或 cmpsw 或 cmpsd单独使用占一整行用于比较两个串,对两个串中的元素一个个做减法,同时影响eflags寄存器
scasb 或 scasw 或 scasd单独使用占一整行用于搜索串内的元素,返回其所在下标。进行搜索的串放到 edi 中要搜索的元素放在 al/ax/eax 中返回元素的下标放在edi中
stosb 或 stosw 或 stosd单独使用占一整行用于串的填充,把要填充的串放到 edi 中填充用的元素放到 al/ax/eax 中
lodsb 或 lodsw 或 lodsd单独使用占一整行用于取出串中元素的值,把要操作的串放到 edi 中并把取出的元素放到 al/ax/eax 中
dup串名 元素类型 大小 dup(数值)用dup括号里的数值,初始化整个串。

9.2 实现串的处理

主要esi 和 edi 寄存器是对串即数组结构进行操作esi存储原串的地址,edi 存储目标串的地址。如果esi 和 edi指向的长度的串,当有指向较短串的寄存器走到串的最后时,会回到开头继续执行,直到指向较长串的寄存器走到串的最后。

.586                     ; CUP指令集
.model flat,stdcall      ; 选择内存模式,调用约定 --> win32内存模式 
option casemap:none      ; option用于启用和禁用汇编程序的功能,这里是启用名称要区分大小写

includelib ucrt.lib      ; 载入C语言运行时库
includelib legacy_stdio_definitions.lib  ; 载入C语言的stdio和stdilb库
includelib kernel32.lib  ; 载入win32库(注意:即使只使用C语言库,也建议载入,防止出现奇怪的问题)
includelib user32.lib    ; 载入win32库(注意:即使只使用C语言库,也建议载入,防止出现奇怪的问题)

extern printf:proc       ; 声明printf函数,注意C语言库函数用cdecl调用约定
ExitProcess proto uCode:DWORD   ; win32函数用于退出程序

.data                    ; 定义数据区    
szBuffer1 dd 20 dup(7bh) ; 定义一个有20个4字节元素的数组,用dup把每个元素初始化为 0FFFFFFFh
szBuffer2 dd 20 dup(?)   ; dup(?)表示不初始化

szBuffer3 db "abcdefgh",0dh,0ah,0 ; 字符串
szBuffer4 dd 100 dup(?)           ; 数组
szBuffer5 dd 1,2,3,4,5,6,7,8,9,0  ; 数组

szBuffer6 db "%d",0dh,0ah,0       ; 用于输出

.code  
PrintInt proc n1:DWORD            ; 输出指定的值方便观察   
	push eax   ; 保存eax和ecx
	push ecx
	
	push n1
	push offset szBuffer6    
	call printf
	add esp,8  
    
	pop ecx   ; 还原eax和ecx
	pop eax
	ret
PrintInt endp   			   

main proc             
	; 1.============================ 串的复制 ====================================
	mov esi, offset szBuffer1   ; 将原串szBuffer1的的地址放到esi中
	mov edi, offset szBuffer2   ; 将目标串szBuffer2的的地址放到edi中
	cld 						; 用cld改变df位,使其从头开始遍历
	mov ecx,20    ; 用ecx寄存器控制 rep 后面语句的运行次数,20是szBuffer1的元素个数
	rep movsd     ; 开始复制

	mov ecx,0
	lea eax,szBuffer2 
tab1:
	push dword ptr[eax+ecx*4]; 循环输出串中元素的值
	call PrintInt
	add ecx,1
	cmp ecx,20
	jne tab1

	; 2.============================ 串的比较 ====================================
	mov esi, offset szBuffer1	; 将原串szBuffer1的的地址放到esi中
	mov edi, offset szBuffer2	; 将目标串szBuffer2的的地址放到edi中
	cmpsd						; 把两个串中的元素一个个做减法,同时影响标志位

	lahf          ; lahf用于把eflags寄存器前八位的值,放到ah寄存器中
	and ah,64 
	movzx ecx,ah
	push ecx 
	call PrintInt ; 64是相等,不是64是不等

	; 3.======================= 搜索串中的指定元素的下标 ============================
	mov edi,offset szBuffer3  	; 把进行搜索的串放到edi中
	mov al,'f'   				; 把要搜索的元素放在al中,注意数据类型
	cld          
	mov ecx,11    ; 用ecx寄存器控制 rep 后面语句的运行次数,11是szBuffer3的元素个数
	repne scasb   ; repne 当两个字符不等时才继续执行,scasb 影响eflags寄存器的ZF位
	dec edi       ; 同 sub edi,1 因为不等时edi也加了1,所以要减1
	
	push ecx 
	call PrintInt ; 输出该元素的下标

	; 4.============================ 串的填充 ====================================
	mov edi,offset szBuffer4  ; 把要填充的串放到edi中
	mov eax,103			      ; 填充用的字符放到al中,注意数据类型
	cld
	mov ecx,100               ; szBuffer4内元素有100个
	rep stosd
	
	mov ecx,0
	lea eax,szBuffer4 
tab2:
	push dword ptr[eax+ecx*4] ; 循环输出串中元素的值
	call PrintInt
	add ecx,1
	cmp ecx,100
	jne tab2

	; 5.============================ 获取串中元素的值 ==============================
	mov esi, offset szBuffer5 ; 把要操作的串放到edi中
	mov edi,esi
	cld
	mov ecx,10                ; szBuffer5一共与10个元素
	xor edx,edx
tab3:
	lodsd  					  ; 把取出的元素放到eax中,注意数据类型
	add edx, eax              ; 循环累加eax
	loop tab3
	push edx	; 最后的输出值
	call PrintInt

	invoke ExitProcess,0      ; 退出程序,win32函数不需要平栈
main endp                     ; main函数结束位置
end 

十.选择结构

1.用汇编if… else if… else时,通常当满足条件时,继续执行接 下来的语句若不满足条件,则跳转到其它地方

2.用汇编实现switch… case时,通常先把所有的条件判断放一起,再把所有的执行语句放一起,先进行所有条件的判断,一旦满足其中一个,就直接跳转到相应的语句进行执行

3.条件赋值指令 cmovCC 当满足某一条件时,该指令会把操作数2的值赋值给操作数1。注意:条件赋值指令只建议在非常简单的选择结构中使用,而复杂的用请 JCC 指令。

4.以 cmp ax, bx 语句为例,用于改变ZF零标志位CF进位标志位,可得出以下几种情况。

结果标志位情况指令
ax == bxZF=1cmove
ax != bxZF=0cmovne
ax < bxCF=1cmovb
ax > bxZF=0 && CF=0cmova
ax <= bxZF=1 ||CF=1cmovna
ax >= bxCF=0cmovnb
.686                     ; 必须是.686及以上CUP指令集才能支持cmovCC指令
.model flat,stdcall      

includelib ucrt.lib      ; 载入C语言运行时库
includelib legacy_stdio_definitions.lib  ; 载入C语言的stdio库
includelib kernel32.lib  ; 载入win32库
includelib user32.lib    ; 载入win32库

extern printf:proc       ; 声明printf函数,C语言库函数声明方式
ExitProcess proto uCode:DWORD ; win32函数用于退出程序

.data 
szBuffer db "%d",0dh,0ah,0    ; 注意0dh0ah指的是\r\n用于换行,注意直接写字符串中是无效的

.code             
PrintInt proc n1:DWORD        ; 输出指定的值方便观察   
	push n1
	push offset szBuffer   
	call printf
	add esp,8
	ret
PrintInt endp      

; 1.===================== 用条件判断伪指令实现if... else if... else ===================
func1 proc num:DWORD               
	.if num == 10          
		add num,100	
	.elseif num == 20
		add num,200
	.else 
		add num,300
	.endif         
    invoke PrintInt,num
	ret
func1 endp

; 2.===================== 用JCC指令实现if... else if... else  =======================
func2 proc num:DWORD  		     
	cmp num,10
	jne tab1
	add num,100
	jmp last                 ; 每个条件语句的结尾,都要有退出语句
tab1:
	cmp num,20
	jne tab2
	add num,200
	jmp last                 ; 每个条件语句的结尾,都要有退出语句
tab2:
	cmp num,30
	jne last
	add num,300
last:
	invoke PrintInt,num
	ret
func2 endp

; 3.===================== 用cmovCC指令实现简单的if... else if... else ================
func3 proc num:DWORD  		      
	cmp num,10
	mov ecx,100
	cmove eax,ecx            ; cmovCC指令只能对寄存器进行操作
	cmp num,20
	mov ecx,200
	cmove eax,ecx
	cmp num,30
	mov ecx,300
	cmove eax,ecx
	invoke PrintInt,eax
	ret
func3 endp

; 4.===================== 用JCC指令实现switch... case ===============================
func4 proc num:DWORD           		   
	cmp num,10               ; 在汇编中switch... case是先判断完所有条件再跳转
	je tab1
	cmp num,20
	je tab2
	cmp num,30
	je tab3
	jmp dafalut     
tab1:
	add num,100	
	jmp last    			 ; 此时的jmp last语句相当于break语句
tab2:
	add num,200	
	jmp last    			 ; 此时的jmp last语句相当于break语句
tab3:
	add num,300		
	jmp last     			 ; 此时的jmp last语句相当于break语句
dafalut:			         ; dafalut的部分
	add num,400	
last:
	invoke PrintInt,num
	ret
func4 endp
  
main proc   
	invoke func1,11
	invoke func2,20
	invoke func3,30
	invoke func4,40
	invoke ExitProcess,0     ; 退出程序,win32函数不需要平栈
main endp				     ; main函数结束位置
end

十一.循环结构

1.用汇编实现循环结构时,esi,ebi,ebx 通常作为计数器指代i,j,k ,如果计数器不够,就用局部变量。

2.在实际开发中,经过函数开头的异常处理后,通常可以保证计数器初始值和其它变量的安全性,即第一次循环必定会成功,在此前提下,do while循环结构的运行效率是最高的,具体请看下面的实现。

.586                     ; CUP指令集
.model flat,stdcall      ; 选择内存模式,调用约定 --> win32内存模式 

includelib ucrt.lib      ; 载入C语言运行时库
includelib legacy_stdio_definitions.lib  ; 载入C语言的stdio库
includelib kernel32.lib  ; 载入win32库
includelib user32.lib    ; 载入win32库

extern printf:proc       ; 声明printf函数,C语言库函数声明方式
ExitProcess proto uCode:DWORD   ; win32函数用于退出程序

.data 
szBuffer db "%d",0dh,0ah,0    ; 注意0dh0ah指的是\r\n用于换行,注意直接写字符串中是无效的

.code             
PrintInt proc n1:DWORD        ; 输出指定的值方便观察
	push n1
	push offset szBuffer   
	call printf
	add esp,8
	ret
PrintInt endp                  

; 1.===================== 用循环伪指令指令实现while循环 ==============================
func1 proc num:DWORD
	.while num > 0
		.if num == 5
			dec num
			.continue      ; 用法同continue
		.elseif num == 3
			.break         ; 用法同break
		.endif
		invoke PrintInt,num	
		dec num
	.endw                  ; 循环判断结束符
	ret
func1 endp

; 2.===================== 用JCC指令实现for循环 =====================================
func2 proc
	push ebp
	mov ebp,esp
	push esi
	xor esi,esi
	mov esi,0              ; 用当计数器esi
	jmp tab1
tab2:
	inc esi                ; 每次esi加1
tab1:	
	cmp esi,100            ; 注意:for的条件判断放在中间
    jnb last
    invoke PrintInt,esi	
    jmp tab2
last:
	pop esi 
	pop ebp
    ret
func2 endp

; 3.===================== 用JCC指令实现while循环 ====================================
func3 proc
	push ebp
	mov ebp,esp
	push esi
	xor esi,esi
	mov esi,0              ; 用当计数器esi
tab1:
	cmp esi,100			   ; 注意:while的条件判断放在开头
	jnb last
	invoke PrintInt,esi	
	inc esi                ; 每次esi加1
    jmp tab1
last:
	pop esi
	pop ebp
    ret
func3 endp

; 4.===================== 用JCC指令实现do while循环 =================================
func4 proc
	push ebp
	mov ebp,esp
	push esi
	xor esi,esi
	mov esi,0              ; 用当计数器esi
tab1:
	invoke PrintInt,esi	
	inc esi                ; 每次esi加1	
	cmp esi,100            ; 注意:do while的条件判断放在最后
	jb tab1                ; 如果满足条件则跳转,并继续运行
	pop esi 
	pop ebp
    ret
func4 endp

; 4.===================== 用loop指令实现简单的循环 ===================================
func5 proc 
	push ebp
	mov ebp,esp
	xor eax,eax
	xor ecx,ecx
	mov ecx,100   ; loop会根据ecx中的值,跳转相应的次数,当ecx为0时停止跳转 (ecx的值必须大于0)
loop1:
	add eax,ecx
	loop loop1    ; 先ecx减1,再跳转
	invoke PrintInt,eax
	pop ebp
	ret
func5 endp

main proc   
	invoke func1,100
	call func2
	call func3
	call func4
	call func5
	invoke ExitProcess,0   ; 退出程序,win32函数不需要平栈
main endp				   ; main函数结束位置
end

十二.宏定义

1.宏定义通常不直接现在数据区,而是其上面的区域

2.宏定义的数值,不能直接push,可以初始化其它变量。

3.宏函数本质是指令的替换,不是函数,不用遵守任何函数调用约定。注意宏函数的参数本质也是指令的替换

指令语法描述
equ 或 =① 名称 equ 数值 ② 名称 = 数值① equ 和 = 的用法一样,用于把后面的数值,与名称关联起来 。数值可以是小数,整数,字符串 ② 名称的用法,与宏定义的用法一样,本质是值的替换。
macro名称 macro 参数列表① 用于定义宏函数 ② 可嵌套定义,注意子宏定义,要在父宏定义中使用一次才能生效。
endm与 macro 配合使用,放在宏函数的结尾用于框定宏函数代码的区域
&参数1& 参数2 &…① 用法同和特性都同C语言中的##,将参数以字符的形式连起来,如果形成的新字符有特殊含义,则可直接用 ② num& 或 &num 表示num可用参数替换 (一般&放在名称后面)
%%(计算表达式)用于给宏函数传参时,先计算出()里计算表达式的值,再用该值进行替换,而不是直接用计算表达式进行替换。
purgepurge宏函数名取消宏函数的定义
reptrept 次数与endm配合使用,把区域内的代码复制n份,n可以自定义。可以用于定义变量或重复执行某一语句。
irpirp 参数名,<参数1,…>与endm配合使用,把区域内的代码复制n份,每次复制都把对应参数替换成参数列表中的值。(注意:参数名后有逗号
irpcirpc 参数名,字符串与endm配合使用,把区域内的代码复制n份,每次复制都把对应参数替换成字符串中的一个字符(注意:参数名后有逗号
if 或 elseif 或 else 或ifndef 或 ifdef条件汇编语句 条件条件汇编,用法与C语言中条件汇编的用法一样
ifb 或 ifnbifb <变量名>① ifb语句如果没有传指定的变量则为true。 ② ifnb语句如果传了指定的变量名则为true
exitmexitm宏函数名终止当前重复块或宏块的展开,通常与条件伪指令一起使用
includeinclude 文件名与C语言中的用法一样,即在include 的位置直接将include 的文件的复制粘贴进来(文件名可以包含路径
.586                  
.model flat,stdcall    

includelib ucrt.lib     
includelib legacy_stdio_definitions.lib  ;
includelib kernel32.lib  
includelib user32.lib   

extern printf:proc             ; 声明printf函数,C语言库函数声明方式
ExitProcess proto uCode:DWORD  ; win32函数用于退出程序,注意要声明函数的参数

; ================================= 宏定义  ======================================
age = 100                      ; 同 name1 equ 100    
height equ 175.5               ; 定义小数
grade equ <10*10>              ; 用<>括起来表示一个整体   
strwold equ <"hello world",0>  ; 字符串

; ================================= 宏函数  ======================================
MyAdd macro n1,n2    ; 宏函数开始位置,函数名MyAdd,参数n1和n2 
	xor eax,eax
	mov eax,n1
	add eax,n2
endm                 ; 宏函数结束位置 

; ================================= &符号的使用  ==================================
SelectString macro tabID
 	jmp tab&tabID
tab1:   
 	push offset szBuffer3
	jmp tab3
tab2:
	push offset szBuffer4
tab3:
 	push offset szBuffer1
 	call PrintData
endm

; ================================= 嵌套宏定义  ===================================
MyString1 macro
	MyString2 macro
		push offset szBuffer3
 		push offset szBuffer1
 		call PrintData
	endm
	MyString2				   ; 使用子宏定义MyString2使其生效
endm

; ================================= ifb语句  ======================================
CreateString MACRO S1,S2
	ifb <S2>
		S1 DB "only S1&",0
	else
		S1 DB "S1& and S2&",0
	endif
ENDM

; ================================= 可变参宏函数  ===================================
PrintfNumber MACRO Number1,Number2,Number3
	local last                             ; 在开头用local声明唯一标记
	sub esp,4                              ; 在宏函数中申请局部变量
	mov dword ptr[esp],100

	ifnb <Number1>
		add eax,Number1
		add dword ptr[esp],1
	endif
	ifnb <Number2>
		add eax,Number2
		add dword ptr[esp],1
	endif
	ifnb <Number3>
		add eax,Number3
		add dword ptr[esp],1
	endif

	push eax
	push offset szBuffer2
	call PrintData

	cmp dword ptr[esp],103
	jne last
    push esp
	call printf
	add esp,4
last:
	add esp,4
endm


.data  
number1 dd 1234
numberA dd 1234
numberB dd 1234
szBuffer1 db "%s",0dh,0ah,0   
szBuffer2 db "%d",0dh,0ah,0  
szBuffer3 db strwold          ; 用宏定义 strwold 初始化 szBuffer3
szBuffer4 db "I want sleep",0
szBuffer5 db "%c",0dh,0ah,0 
CreateString szBuffer6,%(1+4) ; 先计算出%()里计算表达式的值,再用该值进行替换(用宏函数定义变量)

.code                          
PrintData proc n1:DWORD,n2:DWORD 
	push n2
	push n1 
	call printf
	add esp,8                
	ret
PrintData endp   			   


main proc 
	; 1.======================== 宏定义和宏函数的使用 ===============================
	mov eax,age
	push eax
	push offset szBuffer2
	call PrintData

	push offset szBuffer3
	push offset szBuffer1
	call PrintData

	MyAdd 100,50           ; 调用宏函数,返回值在eax中
	push eax
	push offset szBuffer2
	call PrintData

	SelectString 2         ; 输入1或2选择分支(未用local指令。生成唯一标签,故只能调用一次)

	MyString1              ; 测试嵌套宏定义

	; 2.========================= 重复汇编  =======================================
	rept 5     ; 本质是把以下区域内的代码复制5份 
		MyString1  
	endm

	irp num,<1,2,3,4,5,number1,age>
		push num
		push offset szBuffer2
		call PrintData
	endm 

	xor eax,eax
	irpc ID,1AB
		add eax,number&ID  ; 根据组合成的变量名,去取值
	endm
	push eax
	push offset szBuffer2
	call PrintData
	
	; 3.========================= 测试宏函数  =======================================
	push offset szBuffer6  ; 测试条件汇编语句
	push offset szBuffer1
	call PrintData

	PrintfNumber 123,100,100
	PrintfNumber 213,200,200

	invoke ExitProcess,0  
main endp				       
end        

十三.结构体

1.结构体的声明写在数据区的外面,结构体的定义写在数据区的里面

2.定义的结构体为提高的效率,要进行结构体的对齐。可用align指令辅助该操作,其语法为:align 数据类型数据类型必须是2的幂,即2,4,8… 。它规定下一个变量的地址的偏移量必须被n整除,n取决于的值数据类型。

.586                     ; CUP指令集
.model flat,stdcall      ; 选择内存模式,调用约定 --> win32内存模式 

includelib ucrt.lib      ; 载入C语言运行时库
includelib legacy_stdio_definitions.lib  ; 载入C语言的stdio库
includelib kernel32.lib  ; 载入win32库
includelib user32.lib    ; 载入win32库

extern printf:proc       ; 声明printf函数,C语言库函数声明方式
ExitProcess proto uCode:DWORD   ; win32函数用于退出程序


Point struct         ; 在数据区外,声明结构体
	x dword ?        ; ?表示不初始化值
	y dword ?
Point ends           ; 结构体声明结束位置

Book struct
	bname db "cat and bog",0   ; 也可以用dup定义字符串,大小为12字节(空格和最后的0也要算在内)
	price dd 100               ; 定义整型变量,大小为4字节
Book ends                      ; 总的大小为16+4=20,满足栈平衡

Student struct
	sname db "xiao min",0      ; 定义字符串,大小为9字节
	align dword                ; 用align对齐,下一个变量的地址的偏置必须被4整除,即加了3字节
	age dword ?              
	height dword 180        
	weight dword ?
Student ends                   ; 总的大小为9+3+4+4+4=24,满足栈平衡

.data 
szBuffer1 db "%d %d",0dh,0ah,0
szBuffer2 db "%s %d",0dh,0ah,0
szBuffer3 db "%s %d %d %d",0dh,0ah,0
MyPoint Point<123,456>         ; 定义Point结构体对象,并进行初始化
MyBook Book<>                  ; 直接用Book结构体中的初始值,注意<>中不能写?
MyBookArr Book 20 dup(<>)      ; 定义结构体数组,注意一定要写空的<>
MyStudent Student<,21,,70>     ; 用<>可以选择性的赋值


.code                            
PrintPoint proc poit:DWORD
	mov eax,poit
	push dword ptr[eax+4] 
	push dword ptr[eax]
	push offset szBuffer1   
	call printf
	add esp,12
	ret
PrintPoint endp 

PrintBook proc bok:DWORD
	mov eax,bok	
	push dword ptr[eax+12]
	push eax
	push offset szBuffer2   
	call printf
	add esp,12
	ret
PrintBook endp 

PrintBookArr proc bokArr:DWORD,num:DWORD
	push esi
	push edi
	xor esi,esi                 ; esi用于计数
	xor edi,edi                 ; edi用于存每次Book结构体的地址
	mov edi,bokArr
tab1:
	mov dword ptr[edi+12],esi   ; 每次都给结构体中的整型变量赋值,注意该赋值语句的位置
	invoke PrintBook,edi
	add edi,16
	inc esi
	cmp num,esi
	jne tab1
	pop edi
	pop esi
	ret
PrintBookArr endp 

PrintStudent proc stu:DWORD 
	mov eax,stu
	push dword ptr[eax+20]       ; 注意结构体中变量所在的偏移位置
	push dword ptr[eax+16]
	push dword ptr[eax+12]
	push eax
	push offset szBuffer3
	call printf
	add esp,20
	ret
PrintStudent endp

main proc   
	; 第一种赋值方法,要知道每个结构体的成员的大小,并注意结构体对齐
	mov eax,offset MyPoint	    ; 同lea eax,MyPoint
	mov dword ptr [eax+0],666
	mov dword ptr [eax+4],777
	invoke PrintPoint,offset MyPoint
	
	; 第二种赋值方法,直接用变量名赋值
    mov MyPoint.x,123
    mov MyPoint.y,456
	invoke PrintPoint,addr MyPoint ; addr同offset用于取变量的地址,但addr只能在invoke中使用

	invoke PrintBook,offset MyBook
	invoke PrintBookArr,offset MyBookArr,20

	invoke PrintStudent,offset MyStudent

	invoke ExitProcess,0  
main endp                 
end  

十四.其它位数的汇编

14.1 16汇编

assume cs:code,ds:data  ; 给cs寄存器取别名,用于设置代码区的区间
						; 给ds寄存器取别名,用于设置数据区的区间
                        
data segment            ; 数据区起始位置
	number dw 12138     ; 定义一个16位的名为number的变量,其值为12138
	szBuffe db 0bh,0ah,"hellowrold$"  ; $表示字符串的结束符
data ends               ; 数据区结束位置


code segment      ; 代码区起始位置		  
	func proc     ; 声明名为func的函数,并确定函数代码开始位置
		mov ax,4 
		mov bx,5  
		mov cx,6  
		ret       ; 退出函数
	func endp     ; func函数代码结束位置
				  
start:            ; start:用于指定程序运行的起始位置
	; mov ax,code 
	; mov ds,ax   ; 设置ds的起始位置,一般系统会自己找到 
	mov ax,number ; 使用number变量
	call func     ; 调用func函数,
	
	mov ah,09h       ; 通过给ax赋值09h,传给21中断,使其输出字符串
	lea dx,szBuffe   ; 还要用dx存字符串的地址
	; mov dx,offset szBuffe   ; 同上面的语句,用offset伪指令取到字符串的地址
	int 21h          ; 调用21中断,输出字符串
	
	mov ax,4c00h  ; 通过给ax赋值,来传21中断所需的参数,ah=4c表示程序结束,al=00表示程序返回值
	int 21h       ; 调用21号中断,用于退出程序
code ends         ; 代码区结束位置
end start         ; end start 汇编代码结束符符,并指定程序运行的结束位置

14.2 64位汇编

1.在64位下所有函数调用约定,都用 fastcall 调用约定
2.由于64位的指令比32位的长,因此为了优化,在实现功能相同的情况下,优先使用32位的指令和寄存器

includelib ucrt.lib
includelib legacy_stdio_definitions.lib  ; 载入C语言的stdio和stdilb库
option casemap:none      ; option用于启用和禁用汇编程序的功能,这里是启用名称要区分大小写

; includelib kernel32.lib  ; 一般不建议在64位中调用Win32,常常会出现奇怪的问题
; includelib user32.lib    ; 一般不建议在64位中调用Win32,常常会出现奇怪的问题
; extern MessageBoxA:proc  ; 调用Win32 API已不用再写出参数
extern printf:proc

.data
szBuffer1 db "asdafd",0
szBuffer2 db "%s",0dh,0ah,0
szBuffer3 db "%d",0dh,0ah,0

.code
func1 proc
	sub rsp,28h                 ; 开辟栈空间
	mov rdx,offset szBuffer1
	mov rcx,offset szBuffer2
	call printf   
	add rsp,28h                 ; 平栈
	ret
func1 endp

sum proc
	mov dword ptr [rsp+20h],r9d ; 一开头就要先将寄存器中的值,存到局部变量中,注意 rsp+20h 位置没有冲突,因为call的时候rsp-8了,注意寄存器的处理顺序与传入时的顺序一样
	mov dword ptr [rsp+18h],r8d  
	mov dword ptr [rsp+10h],edx  
	mov dword ptr [rsp+8],ecx  

	push rbp      
	mov rbp,rsp
	sub rsp,100h				; 继续开辟堆栈
	xor rax,rax					; 清空rax
	add eax,dword ptr [rbp+10h]
	add eax,dword ptr [rbp+18h]  
	add eax,dword ptr [rbp+20h]  
	add eax,dword ptr [rbp+28h]  
	add eax,dword ptr [rbp+30h]  
	add eax,dword ptr [rbp+38h] 

	add rsp,100h				; 平栈
	pop rbp
	ret
sum endp

main proc
	push rbp
	mov rbp,rsp
	sub rsp,100h                ; 开辟栈空间,主函数无局部变量,所以全部给子函数做参数区

	call func1                  ; 无参调用

	mov rdx,offset szBuffer1    ; 有参调用
	mov rcx,offset szBuffer2
	call printf	                ; 不用平栈

	mov dword ptr [rsp+28h],6   ; 参数超过4个的部分,用堆栈传递 (注意传递的顺序)
	mov dword ptr [rsp+20h],5  
	mov r9d,4                   ;注意寄存器的使用顺序
	mov r8d,3  
	mov edx,2  
	mov ecx,1  
	call sum                    ; 不用平栈
	mov rdx,rax                 ; 取出sum的返回值到rdx中
	mov rcx,offset szBuffer3
	call printf

	add rsp,100h		        ; 平栈
	pop rbp
	ret                         ; 用ret语句,即可退出程序
main endp
end

十五.混合编程

由于release模式下不会对内联汇编和汇编文件进行优化。可以有很多骚操作。

15.1 内联汇编

64位操作系统不支持,内联汇编。需用文件包含的方式调用汇编代码。

#include<stdio.h>
    
int add_fun(int a, int b)
{
	return a + b;
}

int main()
{
	char* s = "%d";
	_asm 
	{
		push 10
		push 20
		call add_fun
		add esp, 8
		push eax
		mov ecx,[s]  ; 注意s是指针不再用取地址了
		push ecx
		call printf
		add esp,8
	}
	return 0;
}

15.2 文件包含

; =============================== a.asm文件 ===============================
; 注意要设置:① 项目右键 --> 生成依赖项 --> 生成自定义 --> 选择masm并点击确定
; ② a.asm文件右键 --> 属性 --> 常规 --> 项类型 --> Microsoft Macro Assembler

includelib ucrt.lib
includelib legacy_stdio_definitions.lib  ; 载入C语言的stdio和stdilb库
option casemap:none      ; option用于启用和禁用汇编程序的功能,这里是启用名称要区分大小写

extern printf:proc

.data
szBiffer1 db "%s",0dh,0ah,0
szBiffer2 db "asdsad",0

.code
printfStr proc
	sub rsp,28h
	mov rdx,offset szBiffer2
	mov rcx,offset szBiffer1 
	call printf
	add rsp,28h
	mov eax,123
	ret 
printfStr endp
end
// =============================== b.c文件 ===============================
    
#include<stdio.h>

extern int printfStr(int num1, int num2);
// 如果是C++文件则用: extern "C" int printfStr(int num1, int num2);

int main()
{
	int b = printfStr(32, 232);
	printf("%d\n", b);
	return 0;
}

十六.常见错误及处理

发生的时机错误描述解决方法
尝试在release模式下运行asm无法生成 SAFESEH 映像项目的属性页 ==> 链接器 ==>命令行 ==> 在其它选项中输入 /SAFESEH:NO ==>点击应用。
尝试运行cmovCC语句。instruction or register not accepted in current CPU modeCUP指令集太低,使用cmovCC 时 CUP指令集必须高于或等于.686
使用 mov eax,ah 等invalid instruction operands无效的指令操作数或对象,仔细查看相关指令,了解其所支持的操作数和操作对象并更改
关键字冲突错误描述因发生的位置而不同,总之是莫名其妙改个名字,易冲突的关键字有str,name
运行了函数某函数后程序崩溃。XXXX位置发生访问冲突检测函数最后是否写了ret是否进行了平栈 ,注意程序程序崩溃的主要原因,通常是函数执行完成后栈不平衡

十七.其它

17.1 处理longlong类型

在32位的环境下,进行longlong类型的操作。由于32位寄存器最大占32位,以longlong类型的变量,本质上是“两个变量”,低32位数一般用eax保存,高32位数一般用ecx保存。因为是“两个变量”所以在调用参数时会push两次。在计算时先计算低32位,再用带进位计算指令操作高32位数在使用fastcall 时不管参数有几个,都不会用寄存器传longlong类型的数据。在返回变量时用eax保存低32位数,用edx保存高32位数。所以在32位下使用longlong类型,是一件低效的且麻烦的事。(在64下不会有该问题)

17.2 32位汇编输入与输出

32位汇编的输入与输出,通调用C语言的输入输出函数来实现

.586                     ; CUP指令集
.model flat,stdcall      ; 选择内存模式,调用约定 --> 平坦分段模式,基cs段和ds段共一个用4GB的空间。自己写的有参函数的调用约定用stdcall调用约定并自动套上stdcall的壳子 (无参的函数的调用约定自己写)
.stack 2048              ; 设置栈的大小,默认为1024KB
option casemap:none      ; option用于启用和禁用汇编程序的功能,这里是启用名称要区分大小写

includelib ucrt.lib      ; 载入C语言运行时库
includelib legacy_stdio_definitions.lib  ; 载入C语言的stdio和stdilb库
includelib kernel32.lib  ; 载入win32库(注意:即使只使用C语言库,也建议载入,防止出现奇怪的问题)
includelib user32.lib    ; 载入win32库(注意:即使只使用C语言库,也建议载入,防止出现奇怪的问题)

extern printf:proc       ; 声明printf函数,C语言库函数声明方式
extern scanf:proc        ; 声明scanf函数,C语言库函数声明方式
ExitProcess proto uCode:DWORD  ; win32函数用于退出程序,注意要声明函数的参数

.data             ; 数据区用于存变量(汇编代码一般分为.const常量区 .data数据区 .code代码区)
szBuffer1 db "%d",0dh,0ah,0    ; 注意0dh0ah指的是\r\n用于换行,注意直接写字符串中是无效的
szBuffer2 db "%d",0   
num dd ?                       ; ?表示不进行参数的初始化 
  
.code                          ; 代码区
PrintInt proc n1:DWORD         ; PrintInt函数代码的起始位置,用于输出int数据      
	push n1
	push offset szBuffer1      ; offset语句用于取变量的地址
	call printf
	add esp,8                  ; C语言函数,用cdecl调用约定,要进行平栈,其大小为参数个数*4
	ret
PrintInt endp   			   ; PrintInt函数代码的结束位置

ScnafInt proc n1:DWORD         ; ScnafInt函数代码的起始位置,用于输入int数据     
	push n1
	push offset szBuffer2 	   ; offset语句用于取变量的地址
	call scanf
	add esp,8                  ; 进行平栈
	ret
ScnafInt endp   			   ; ScnafInt函数代码的结束位置

main proc   
	invoke ScnafInt,offset num ; 注意要传num的地址
	invoke PrintInt,num
	invoke ExitProcess,0       ; 退出程序,win32函数都用stdcall调用约定,故不需要平栈
main endp				       ; main函数结束位置
end                            ; 汇编代码结束符符

17.3 未整理的知识

1.其它数据类型 REAL4,REAL8,REAL10,TBYTE,FWORD的操作。
2.特殊的有意思的指令TEXTEQU,TYPEDEF等。
3.浮点数的相关操作,包括MMX,AVX,SSE等指令集等。
4.各种中断指令。
5.32位下段寄存器的作用。

17.4 资料文档

资料名链接
masm汇编文档masm汇编指令参考
Intel白皮书英特尔® 64 位和 IA-32 架构开发人员手册
常用汇编指令及其影响的标志位常用汇编指令及其影响的标志位 - 2f28 - 博客园 (cnblogs.com)
汇编指令速查汇编指令速查 - lsgxeva - 博客园 (cnblogs.com)

十八.结语

1.本文探讨的汇编相关知识只是冰山一角,如果有时间我会对相关知识点进行补全。

2.汇编语言和操作系统强相关,学习操作系统后会有更深入的理解,由于篇幅有限,加之操作系统相关的知识难度较大,因此本文没有涉及。

作者:墨尘_MO
时间:2023年3月5日

  • 3
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值