文章目录
序言
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字节中所包含的有效代码条数。注意,如果循环次数较少,反而会降低性能,万万不可强行盲目使用。
以下为的常用的用于代码对齐的指令。
指令 | 指令长度(字节) |
---|---|
nop | 1 |
xchg ax,ax | 2 |
nop dword ptr [eax] 或 xor ax,ax | 3 |
nop dword ptr [eax+00h] | 4 |
nop word ptr [eax+eax] | 5 |
注意:*1和+00h 自己在代码时会被优化掉,而在反汇编中也不会显示,所以具体指令的实际长度,请看反汇编中代码地址的变化。
1.3 其它要点
1.X86架构用的是小端序,即数据的低位放在低地址处,数据的高位放在高地址处。
2.X86架构读取和写入数据,都是从低地址到高地址。通常我们所说的地址,指的也是数据的低地址。
二.32位寄存器介绍
2.1 32位寄存器说明
![](https://img-blog.csdnimg.cn/073f1e5080c644aeb8a8c0ec400c1034.png)
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" |
offset | offset [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 ; 汇编代码结束符符
五.数据传输指令
指令 | 语法 | 描述 |
---|---|---|
mov | mov reg/mem , , , reg/mem/imm | ① mov a,b 相当于 a=b ② mov两边的对象的操作数要一样,且至少有一个reg ③ mov不会改变标志位 |
movzx/movsx | movzx/movsx reg , , , reg/mem | ① movzx和movsx要求左边对象的操作数,要大于右边对象的操作数 ② 赋值时movzx对左边对象进行低位赋值,高位置0。movsx 对左边对象进行低位赋值,高位置0还是1,由其最高位是0还是1决定 ③ 其它特性同mov |
lea | mov reg/mem , , , reg/mem | lea a,b 相当于 a=&b,把b的地址赋值给a |
xchg | xchg reg/mem , , , reg/mem | ① 用于交换两边的值 ② 两边的对象的操作数要一样 |
push 和 pop | ① push reg/mem/imm ② pop reg/mem | ① push 是先 esp-4,再把值压入栈。 ② pop 是先把值弹出栈,再esp+4。 ③ 32位下push 和 pop 操作数为32字节 |
lahf 和 sahf | 单独使用占一整行 | ① lahf用于把eflags寄存器前八位的值,放到ah寄存器中 ② sahf用于把ah中的值,放到eflags寄存器前八位中 |
cmovCC | cmovCC reg reg | ① 该指令只能对寄存器进行操作 ② 当满足某一条件时,该指令会把操作数2的值赋值给操作数1 ③ 使用cmovCC 时 CUP指令集必须高于或等于.686 |
六.数据计算指令
指令 | 语法 | 描述 |
---|---|---|
add | add reg/mem , , , reg/mem/imm | ①add a,b 相当于 a = a+b ② 会改变标志位 |
sub | sub reg/mem , , , reg/mem/imm | ①sub a,b 相当于 a = a-b ② 会改变标志位 |
mul 和 imul | mul/imul reg/mem , , , reg/mem/imm | ① mul用于操作无有符号数,imul用于操作有符号数 ② mul a,b 相当于 a = a*b ③ mul a,b,c 相当于 a = b*c |
div 和 idiv | div/idiv reg/mem , , , reg/mem/imm | ① div用于操作无有符号数,idiv用于操作有符号数 ② 32位中被除数必须是64位,可用 cdq 指令将eax与edx连接,使eax扩展到64位 ③ 余数会存入edx |
shl 和 shr | shl/shr reg/mem , , , reg/mem/imm | 对无符号数进行逻辑左移或逻辑右移 |
sal 和 sar | sal/sar reg/mem , , , reg/mem/imm | 对有符号数进行算数左移或算数右移 |
inc 和 dec | inc/dec reg/mem | 相当于+1和-1操作 |
cmp | cmp reg/mem , , , reg/mem/imm | 把两边的对象值相减比较,不会有结果,用于改变ZF和CF标志位 |
and 和 or 和 xor | and/or reg/mem , , , reg/mem | 与,或,异或运算 |
not | not 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 == bx | ZF=1 | je |
ax != bx | ZF=0 | jne |
ax < bx | CF=1 | jb |
ax > bx | ZF=0 && CF=0 | ja |
ax <= bx | ZF=1 ||CF=1 | jna |
ax >= bx | CF=0 | jnb |
八.函数
8.1 函数相关指令
指令 | 语法 | 描述 |
---|---|---|
call | call 函数名 | 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 函数 |
invoke | invoke 函数名 参数列表 | invoke 本质还是push 和call。其为了方便调用win32 函数或写有参数列表的函数。注意根据实际情况考虑是否平栈。 |
local | ① local 局部变量名:类型 ② local 标签名 | ① local在普通函数中使用时,用于定义局部变量 ② local在宏函数中使用时,用于定义唯一标签,即每次使用宏函数生成的标签都不一样 (供JCC语句使用) ③ 无论在哪使用,local语句都必须放在开头 |
8.2 函数调用约定
8.2.1 说明
1.通常在进行参数的传递时,不会使用ebx基地址寄存器,而是eax,ecx,edx三者循环使用。
2.函数内部处理的模板如下,注意对于不同的函数调用约定可能会有些许变化,但主体结构是不变的。
![](https://img-blog.csdnimg.cn/d7a711cea78f4dad8328509629b47391.png)
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具有极高的灵活性只要知道核心规则,都可以根据实际情况,进行改变。
![](https://img-blog.csdnimg.cn/a8984e1f35804402b890f8fd7df7733b.png)
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 == bx | ZF=1 | cmove |
ax != bx | ZF=0 | cmovne |
ax < bx | CF=1 | cmovb |
ax > bx | ZF=0 && CF=0 | cmova |
ax <= bx | ZF=1 ||CF=1 | cmovna |
ax >= bx | CF=0 | cmovnb |
.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可用参数替换 (一般&放在名称后面) |
% | %(计算表达式) | 用于给宏函数传参时,先计算出()里计算表达式的值,再用该值进行替换,而不是直接用计算表达式进行替换。 |
purge | purge宏函数名 | 取消宏函数的定义 |
rept | rept 次数 | 与endm配合使用,把区域内的代码复制n份,n可以自定义。可以用于定义变量或重复执行某一语句。 |
irp | irp 参数名,<参数1,…> | 与endm配合使用,把区域内的代码复制n份,每次复制都把对应参数替换成参数列表中的值。(注意:参数名后有逗号) |
irpc | irpc 参数名,字符串 | 与endm配合使用,把区域内的代码复制n份,每次复制都把对应参数替换成字符串中的一个字符(注意:参数名后有逗号) |
if 或 elseif 或 else 或ifndef 或 ifdef | 条件汇编语句 条件 | 条件汇编,用法与C语言中条件汇编的用法一样 |
ifb 或 ifnb | ifb <变量名> | ① ifb语句如果没有传指定的变量则为true。 ② ifnb语句如果传了指定的变量名则为true。 |
exitm | exitm宏函数名 | 终止当前重复块或宏块的展开,通常与条件伪指令一起使用 |
include | include 文件名 | 与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 mode | CUP指令集太低,使用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日