x86 32位汇编知识整理

文章目录

  • 序言
  • 一.必要知识
    • 1.1 反汇编的使用
    • 1.2 对齐操作
    • 1.3 其它要点
  • 二.32位寄存器介绍
    • 2.1 32位寄存器说明
    • 2.2 数据寄存器
    • 2.3 变址寄存器
    • 2.4 指针寄存器
    • 2.5 段寄存器
    • 2.6 指令指针寄存器
    • 2.7 标志寄存器
  • 三.基础知识
    • 3.1 常用数据类型
    • 3.2 常用符号
  • 四.32位汇编实例
  • 五.数据传输指令
  • 六.数据计算指令
  • 七.JCC指令
    • 7.1 根据地址值或标记
    • 7.2 根据eflags寄存器
  • 八.函数
    • 8.1 函数相关指令
    • 8.2 函数调用约定
      • 8.2.1 说明
      • 8.2.2 _cdecl
      • 8.2.3 _stdcall
      • 8.2.4 _fastcall 32位
      • 8.2.5 _fastcall 64位
      • 8.2.6 _thiscall
    • 8.3 不同函数的实现
  • 九.串的处理
    • 9.1 串处理的相关指令
    • 9.2 实现串的处理
  • 十.选择结构
  • 十一.循环结构
  • 十二.宏定义
  • 十三.结构体
  • 十四.其它位数的汇编
    • 14.1 16汇编
    • 14.2 64位汇编
  • 十五.混合编程
    • 15.1 内联汇编
    • 15.2 文件包含
  • 十六.常见错误及处理
  • 十七.其它
    • 17.1 处理longlong类型
    • 17.2 32位汇编输入与输出
    • 17.3 未整理的知识
    • 17.4 资料文档
  • 十八.结语

序言

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,64hpush eax mov ecx,offset szBuffer   ; 或用 lea ecx,[szBuffer]push ecxcall printfadd 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 0call 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,通常会打乱代码的执行顺序。用户在下调试断点时,只能下在函数的开头,不然会出现程序的崩溃。

#includeint main()
{char* s = "%d\n";_asm{mov eax,tab   ; 把标号放进eax寄存器中jmp eaxtab:push 30       mov ecx,[s]   ; 注意s是指针不再用取地址了push ecxcall printfadd esp,8}_asm{jmp $+7       ;使用$符号控制跳转的位置,跳转到 push 40push 30      push 40mov ecx, [s]  ; 注意s是指针不再用取地址了push ecxcall printfadd 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 procpush 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             ; 还原ebpret   
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 edimov eax,555mov dword ptr[ebp-4],eax    ; 用ebp-偏移量来获取局部变量,第一个局部变量在esp-4的位置mov ecx,666mov dword ptr[ebp-8],ecxpush 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 printfadd esp,20                  ; 内部调用了cdecl的函数,故要平栈(参数个数*4=20)pop edi                     ; 还原esi和edi寄存器(注意寄存器的出栈顺序)pop esiadd esp,8                   ; 也可用mov esp,ebp平栈,销毁用于保存局部变量而开辟的栈的空间pop ebp                     ; 还原ebpret
PrintInt endp  			main proc push 222push 111call 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 edimov eax,555mov dword ptr[ebp-4],eax    ; 用ebp-偏移量来获取局部变量,第一个局部变量在esp-4的位置mov ecx,666mov dword ptr[ebp-8],ecxpush 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 printfadd esp,20                  ; 内部调用了cdecl的函数,故要平栈(参数个数*4=20)pop edi                     ; 还原esi和edi寄存器(注意寄存器的出栈顺序)pop esileave  					    ; leave语句相当于先 mov esp,ebp平栈,销毁用于保存局部变量而开辟的栈的空间,再用 pop ebp,还原ebp ret 8						; ret 8 语句相当于先ret,再 add esp,8 实现函数内平栈
PrintInt endp  			main proc push 222push 111call 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 szBuffercall printfadd esp,28pop edi                     ; 还原esi和edi寄存器(注意寄存器的出栈顺序)pop esi   add esp,16                  ; 也可用mov esp,ebp平栈,用于销毁开辟的栈的空间pop ebp                     ; 还原ebpret 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],edxmov dword ptr [rsp+8],ecx   ; 注意 rsp+8 位置也没有冲突,因为rsp位置也能存数据push rbp  ; 保存rbppush rsi  ; 假设主函数和子函数共用了esi和edi寄存器push rdisub 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],eaxmov ecx,dword ptr[rbp+56]mov dword ptr [rsp+56],ecxmov edx,dword ptr[rbp+64]mov dword ptr [rsp+48],edxmov eax,dword ptr[rbp+72]mov dword ptr [rsp+40],eaxmov ecx,dword ptr[rbp+80]mov dword ptr [rsp+32],ecxmov 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 rsipop rbp         ret						
PrintInt endp  			main proc push rbpmov rbp,rspsub 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,333mov edx,222  mov ecx,111  call PrintInt   			  ; fastcall不需要,手动平栈add rsp,56pop rbpret        					  ; 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会被放到变量区

#includeclass 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 100lea 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 ebpmov ebp,esppush dword ptr[ebp+12]          ; 用ebp+偏移量,获取传入的参数push dword ptr[ebp+8]           ; 第一个参数必在ebp+8的位置push offset szBuffer1 	  call printfadd esp,12    pop ebpret
PrintInt1 endp  			; 2.======================= 定义有形参函数,系统自动实现stdcall ======================
PrintInt2 proc n1:DWORD,n2:DWORD      push n2                        ; 直接用形参名获取传入的参数push n1						   ; 直接用形参名获取传入的参数push offset szBuffer1 	  call printfadd esp,12                 ret
PrintInt2 endp   			 ; 3.======================= 定义有形参和局部变量的函数 ===============================
PrintInt3 proc n1:DWORD,n2:DWORD  sub esp,8					   ; 开辟8字节空间,存放2个dword变量mov eax,n1mov dword ptr[ebp-4],eax       ; 用ebp-偏移量,获取局部变量,第一个局部变量在ebp-4的位置mov ecx,n2mov dword ptr[ebp-8],ecx       push dword ptr[ebp-8]          ; 注意入栈的顺序                    push dword ptr[ebp-4]						   push offset szBuffer1 	  call printfadd 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,n1mov @num1,eax   mov ecx,n2mov @num2,ecxpush @num2                      push @num1					   push offset szBuffer1 	  call printfadd esp,12                 ret
PrintInt4 endp  ; 5.======================= 32位下局部变量的堆栈对齐 ==============================
PrintInt5 proc  push ebpmov ebp,espsub esp,16  ; 32位下的堆栈对齐的约定,任何类型的局部变量都要占4字节,这里使用参数的类型分别为,char,short,int,charmov al,102mov byte ptr[ebp-1],al        ; 0+1=1mov ax,123mov word ptr[ebp-6],ax        ; 4+2=6mov eax,num1mov dword ptr[ebp-12],eax     ; 4+4+4=12mov al,char1mov byte ptr[ebp-13],al       ; 4+4+4+1=13movzx eax,byte ptr[ebp-1]     ; 注意数据类型用movzx语句,因为在32位下push操作数为32字节push eaxmovzx ecx,word ptr[ebp-6]     ; 注意数据类型用movzx语句,因为在32位下push操作数为32字节push ecxmov edx,dword ptr[ebp-12]     ; 注意数据类型用movzx语句,因为在32位下push操作数为32字节push edx           movzx eax,byte ptr[ebp-13]push eaxpush offset szBuffer3 	  call printfadd esp,20add esp,16pop ebpret
PrintInt5 endp  main proc  push 456push num1call PrintInt1  ; 调用无参函数  add esp,8       ; 因为是cdecl,要手动平栈push 456push num1call PrintInt2  ; 调用有参函数 push 456push num1call PrintInt3  ; 调用有参有局部变量的函数push 456push num1call 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和ecxpush ecxpush n1push offset szBuffer6    call printfadd esp,8  pop ecx   ; 还原eax和ecxpop eaxret
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,0lea eax,szBuffer2 
tab1:push dword ptr[eax+ecx*4]; 循环输出串中元素的值call PrintIntadd ecx,1cmp ecx,20jne tab1; 2.============================ 串的比较 ====================================mov esi, offset szBuffer1	; 将原串szBuffer1的的地址放到esi中mov edi, offset szBuffer2	; 将目标串szBuffer2的的地址放到edi中cmpsd						; 把两个串中的元素一个个做减法,同时影响标志位lahf          ; lahf用于把eflags寄存器前八位的值,放到ah寄存器中and ah,64 movzx ecx,ahpush 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,所以要减1push ecx call PrintInt ; 输出该元素的下标; 4.============================ 串的填充 ====================================mov edi,offset szBuffer4  ; 把要填充的串放到edi中mov eax,103			      ; 填充用的字符放到al中,注意数据类型cldmov ecx,100               ; szBuffer4内元素有100个rep stosdmov ecx,0lea eax,szBuffer4 
tab2:push dword ptr[eax+ecx*4] ; 循环输出串中元素的值call PrintIntadd ecx,1cmp ecx,100jne tab2; 5.============================ 获取串中元素的值 ==============================mov esi, offset szBuffer5 ; 把要操作的串放到edi中mov edi,esicldmov ecx,10                ; szBuffer5一共与10个元素xor edx,edx
tab3:lodsd  					  ; 把取出的元素放到eax中,注意数据类型add edx, eax              ; 循环累加eaxloop tab3push edx	; 最后的输出值call PrintIntinvoke 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 n1push offset szBuffer   call printfadd esp,8ret
PrintInt endp      ; 1.===================== 用条件判断伪指令实现if... else if... else ===================
func1 proc num:DWORD               .if num == 10          add num,100	.elseif num == 20add num,200.else add num,300.endif         invoke PrintInt,numret
func1 endp; 2.===================== 用JCC指令实现if... else if... else  =======================
func2 proc num:DWORD  		     cmp num,10jne tab1add num,100jmp last                 ; 每个条件语句的结尾,都要有退出语句
tab1:cmp num,20jne tab2add num,200jmp last                 ; 每个条件语句的结尾,都要有退出语句
tab2:cmp num,30jne lastadd num,300
last:invoke PrintInt,numret
func2 endp; 3.===================== 用cmovCC指令实现简单的if... else if... else ================
func3 proc num:DWORD  		      cmp num,10mov ecx,100cmove eax,ecx            ; cmovCC指令只能对寄存器进行操作cmp num,20mov ecx,200cmove eax,ecxcmp num,30mov ecx,300cmove eax,ecxinvoke PrintInt,eaxret
func3 endp; 4.===================== 用JCC指令实现switch... case ===============================
func4 proc num:DWORD           		   cmp num,10               ; 在汇编中switch... case是先判断完所有条件再跳转je tab1cmp num,20je tab2cmp num,30je tab3jmp 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,numret
func4 endpmain proc   invoke func1,11invoke func2,20invoke func3,30invoke func4,40invoke 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 n1push offset szBuffer   call printfadd esp,8ret
PrintInt endp                  ; 1.===================== 用循环伪指令指令实现while循环 ==============================
func1 proc num:DWORD.while num > 0.if num == 5dec num.continue      ; 用法同continue.elseif num == 3.break         ; 用法同break.endifinvoke PrintInt,num	dec num.endw                  ; 循环判断结束符ret
func1 endp; 2.===================== 用JCC指令实现for循环 =====================================
func2 procpush ebpmov ebp,esppush esixor esi,esimov esi,0              ; 用当计数器esijmp tab1
tab2:inc esi                ; 每次esi加1
tab1:	cmp esi,100            ; 注意:for的条件判断放在中间jnb lastinvoke PrintInt,esi	jmp tab2
last:pop esi pop ebpret
func2 endp; 3.===================== 用JCC指令实现while循环 ====================================
func3 procpush ebpmov ebp,esppush esixor esi,esimov esi,0              ; 用当计数器esi
tab1:cmp esi,100			   ; 注意:while的条件判断放在开头jnb lastinvoke PrintInt,esi	inc esi                ; 每次esi加1jmp tab1
last:pop esipop ebpret
func3 endp; 4.===================== 用JCC指令实现do while循环 =================================
func4 procpush ebpmov ebp,esppush esixor esi,esimov esi,0              ; 用当计数器esi
tab1:invoke PrintInt,esi	inc esi                ; 每次esi加1	cmp esi,100            ; 注意:do while的条件判断放在最后jb tab1                ; 如果满足条件则跳转,并继续运行pop esi pop ebpret
func4 endp; 4.===================== 用loop指令实现简单的循环 ===================================
func5 proc push ebpmov ebp,espxor eax,eaxxor ecx,ecxmov ecx,100   ; loop会根据ecx中的值,跳转相应的次数,当ecx为0时停止跳转 (ecx的值必须大于0)
loop1:add eax,ecxloop loop1    ; 先ecx减1,再跳转invoke PrintInt,eaxpop ebpret
func5 endpmain proc   invoke func1,100call func2call func3call func4call func5invoke 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,eaxmov eax,n1add eax,n2
endm                 ; 宏函数结束位置 ; ================================= &符号的使用  ==================================
SelectString macro tabIDjmp tab&tabID
tab1:   push offset szBuffer3jmp tab3
tab2:push offset szBuffer4
tab3:push offset szBuffer1call PrintData
endm; ================================= 嵌套宏定义  ===================================
MyString1 macroMyString2 macropush offset szBuffer3push offset szBuffer1call PrintDataendmMyString2				   ; 使用子宏定义MyString2使其生效
endm; ================================= ifb语句  ======================================
CreateString MACRO S1,S2ifb S1 DB "only S1&",0elseS1 DB "S1& and S2&",0endif
ENDM; ================================= 可变参宏函数  ===================================
PrintfNumber MACRO Number1,Number2,Number3local last                             ; 在开头用local声明唯一标记sub esp,4                              ; 在宏函数中申请局部变量mov dword ptr[esp],100ifnb add eax,Number1add dword ptr[esp],1endififnb add eax,Number2add dword ptr[esp],1endififnb add eax,Number3add dword ptr[esp],1endifpush eaxpush offset szBuffer2call PrintDatacmp dword ptr[esp],103jne lastpush espcall printfadd 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 n2push n1 call printfadd esp,8                ret
PrintData endp   			   main proc ; 1.======================== 宏定义和宏函数的使用 ===============================mov eax,agepush eaxpush offset szBuffer2call PrintDatapush offset szBuffer3push offset szBuffer1call PrintDataMyAdd 100,50           ; 调用宏函数,返回值在eax中push eaxpush offset szBuffer2call PrintDataSelectString 2         ; 输入1或2选择分支(未用local指令。生成唯一标签,故只能调用一次)MyString1              ; 测试嵌套宏定义; 2.========================= 重复汇编  =======================================rept 5     ; 本质是把以下区域内的代码复制5份 MyString1  endmirp num,<1,2,3,4,5,number1,age>push numpush offset szBuffer2call PrintDataendm xor eax,eaxirpc ID,1ABadd eax,number&ID  ; 根据组合成的变量名,去取值endmpush eaxpush offset szBuffer2call PrintData; 3.========================= 测试宏函数  =======================================push offset szBuffer6  ; 测试条件汇编语句push offset szBuffer1call PrintDataPrintfNumber 123,100,100PrintfNumber 213,200,200invoke 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 structbname db "cat and bog",0   ; 也可以用dup定义字符串,大小为12字节(空格和最后的0也要算在内)price dd 100               ; 定义整型变量,大小为4字节
Book ends                      ; 总的大小为16+4=20,满足栈平衡Student structsname 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:DWORDmov eax,poitpush dword ptr[eax+4] push dword ptr[eax]push offset szBuffer1   call printfadd esp,12ret
PrintPoint endp PrintBook proc bok:DWORDmov eax,bok	push dword ptr[eax+12]push eaxpush offset szBuffer2   call printfadd esp,12ret
PrintBook endp PrintBookArr proc bokArr:DWORD,num:DWORDpush esipush edixor esi,esi                 ; esi用于计数xor edi,edi                 ; edi用于存每次Book结构体的地址mov edi,bokArr
tab1:mov dword ptr[edi+12],esi   ; 每次都给结构体中的整型变量赋值,注意该赋值语句的位置invoke PrintBook,ediadd edi,16inc esicmp num,esijne tab1pop edipop esiret
PrintBookArr endp PrintStudent proc stu:DWORD mov eax,stupush dword ptr[eax+20]       ; 注意结构体中变量所在的偏移位置push dword ptr[eax+16]push dword ptr[eax+12]push eaxpush offset szBuffer3call printfadd esp,20ret
PrintStudent endpmain proc   ; 第一种赋值方法,要知道每个结构体的成员的大小,并注意结构体对齐mov eax,offset MyPoint	    ; 同lea eax,MyPointmov dword ptr [eax+0],666mov dword ptr [eax+4],777invoke PrintPoint,offset MyPoint; 第二种赋值方法,直接用变量名赋值mov MyPoint.x,123mov MyPoint.y,456invoke PrintPoint,addr MyPoint ; addr同offset用于取变量的地址,但addr只能在invoke中使用invoke PrintBook,offset MyBookinvoke PrintBookArr,offset MyBookArr,20invoke PrintStudent,offset MyStudentinvoke ExitProcess,0  
main endp                 
end  

十四.其它位数的汇编

14.1 16汇编

assume cs:code,ds:data  ; 给cs寄存器取别名,用于设置代码区的区间; 给ds寄存器取别名,用于设置数据区的区间data segment            ; 数据区起始位置number dw 12138     ; 定义一个16位的名为number的变量,其值为12138szBuffe 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 procsub rsp,28h                 ; 开辟栈空间mov rdx,offset szBuffer1mov rcx,offset szBuffer2call printf   add rsp,28h                 ; 平栈ret
func1 endpsum procmov 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,rspsub rsp,100h				; 继续开辟堆栈xor rax,rax					; 清空raxadd 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 rbpret
sum endpmain procpush rbpmov rbp,rspsub rsp,100h                ; 开辟栈空间,主函数无局部变量,所以全部给子函数做参数区call func1                  ; 无参调用mov rdx,offset szBuffer1    ; 有参调用mov rcx,offset szBuffer2call 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 szBuffer3call printfadd rsp,100h		        ; 平栈pop rbpret                         ; 用ret语句,即可退出程序
main endp
end

十五.混合编程

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

15.1 内联汇编

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

#includeint add_fun(int a, int b)
{return a + b;
}int main()
{char* s = "%d";_asm {push 10push 20call add_funadd esp, 8push eaxmov ecx,[s]  ; 注意s是指针不再用取地址了push ecxcall printfadd esp,8}return 0;
}

15.2 文件包含

; =============================== a.asm文件 ===============================
; 注意要设置:① 项目右键 --> 生成依赖项 --> 生成自定义 --> 选择masm并点击确定
; ② a.asm文件右键 --> 属性 --> 常规 --> 项类型 --> Microsoft Macro Assemblerincludelib 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 procsub rsp,28hmov rdx,offset szBiffer2mov rcx,offset szBiffer1 call printfadd rsp,28hmov eax,123ret 
printfStr endp
end
// =============================== b.c文件 ===============================#includeextern 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 n1push offset szBuffer1      ; offset语句用于取变量的地址call printfadd esp,8                  ; C语言函数,用cdecl调用约定,要进行平栈,其大小为参数个数*4ret
PrintInt endp   			   ; PrintInt函数代码的结束位置ScnafInt proc n1:DWORD         ; ScnafInt函数代码的起始位置,用于输入int数据     push n1push offset szBuffer2 	   ; offset语句用于取变量的地址call scanfadd esp,8                  ; 进行平栈ret
ScnafInt endp   			   ; ScnafInt函数代码的结束位置main proc   invoke ScnafInt,offset num ; 注意要传num的地址invoke PrintInt,numinvoke 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日


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部