7. x86 汇编语言元素
现在我们带你讲解一下一个汇编语言程序是怎么写的。x86 汇编语言由语句 (statement) 构成,一条语句可能有以下元素:
7.1 常量整数和常量整数表达式
一个常量整数表示为 [{ + | - }] 数字 { 基数字符 },例如 -10、11010011b、0A3h。
标识说明:
[]的内容是可选的{ A | B }说明必须在A和B中选择一项
基数字符说明了此整数使用的数制,如下表所示:
| 基数字符 | 数制 |
|---|---|
空置/d |
十进制 (Dec) |
q/o |
八进制 (Oct) |
h |
十六进制 (Hex) |
b |
二进制 (Bin) |
如果一个十六进制值以字母开头,如 A4,则必须加上前缀 0 以免和变量名混淆。
整数字面量间的运算使用以下运算符:
| 运算符 | 优先级 | 备注 |
|---|---|---|
() |
1 | |
+, - |
2 | 正负号 |
*, / |
3 | |
mod |
4 | |
+, - |
5 | 加减号 |
每个表达式的计算结果必须为可存储为 32 位的整型(0 ~ FFFFFFFF),此结果在编译时确定,运行时不可变。
EQU 指令可以设置符号整数常量,相当于 C 中的 #define,格式为 标识符 EQU 常量整数表达式。EQU 定义的常量在汇编时直接替换为字面量,因此运行时固定不可变。更宽松的版本是 = 指令,允许常量重定义,但依然是汇编时确定,运行时不可更改。
7.2 其他常量
常量实数表示为 [{ + | - }]数字.[数字][指数],其中[指数]为科学记数法中的 部分,表示为 E[{ + | - }]integer。2.、-44.2E+05、26.E5 都是合法的十进制实数。常量实数按照 IEEE 浮点数格式转换存储。
编码实数 (encoded real) 以十六进制表示,用 r 结尾。对应的十六进制从 IEEE 浮点数格式的二进制数转换得到,如 +1.0 的 IEEE 浮点数格式为 0011 1111 1000 0000 0000 0000 0000 0000,编码实数表示为 3F800000r
字符和字符串常量以 ''/"" 包围,按照 ASCII 码存储。
7.3 保留字
保留字主要有以下类型:
- 指令助记符,如
mov、add、mul等 - 寄存器名称
- 汇编器指令
- 类型属性,如
byte和word - 运算符
- 预定义的符号
保留字大小写不敏感,如 MOV 和 mov 是一样的,但预定义符号除外。
7.4 标识符
标识符用于标识变量、常量、函数、标签,有以下规则:
- 长度在 1 ~ 247 之间
- 大小写不敏感(通过汇编器的
-Cp选项可使之大小写敏感) - 首字符必须是字母、下划线、
@、?或$,后续的字符才可以取数字。
作为建议,请尽量不要以下划线或者@开头,减少违反下一条规则的概率。 - 不能和保留字撞名
7.5 指令
指令在汇编时转换成运行时可执行的语句(机器语言字节)。一条指令包含四个部分:[标签:] [助记符] [操作数] [;注释]
-
标签
标签用于标识数据 / 代码的位置。 -
助记符
助记符标识指令功能。 -
操作数
操作数用于指令数据的输入输出。操作数可以是寄存器 (reg)、内存地址 (mem) 或者整数表达式 (imm)。一条指令可以有 0 到 3 个操作数。
对于有两个操作数的指令,第一个操作数作为目标操作数 (dest),第二个操作数作为源操作数 (src)。指令一般修改的是目标操作数。
对于有三个操作数的指令,后两个操作数作为源操作数执行指令,结果存储到目标操作数。
常见助记符及其操作数:助记符 操作数 说明 MOVdest, srcsrc的值赋值到destADDdest, srcdest = dest + srcINCopop递增 1DECopop递减 1SUBdest, srcdest = dest - srcMULop无符号乘法,结果储存在主寄存器中,即 EAX = EAX * opIMULdest, src1, src2有符号乘法, dest = src1 * src2JMPlabel无条件跳转至标签位置 CALLfunc调用函数 NOP无 无操作指令,占用 1 字节,常被汇编器用于字节对齐 Note无论何时,我们都建议优先以寄存器作为操作数,然后是常量值,最后才是内存地址标识,因为它们的读取效率是依次递减的。
-
注释
单行注释以;开头
多行注释使用COMMENT指令,语法为COMMENT 结束符。从COMMENT到第一个结束符之间的代码块将被忽略。
7.6 段式存储
正如 4.2 所述,x86 处理器底层依然属于段式存储模型。在大部分操作系统上,采用的模型和各位在 C / C++ 学的内存布局是一样的。
现在这个阶段,需要我们设置的内存段只有 .data(数据段)和 .code(代码段)。
7.7 示例 1
下面是一个进行加减运算的 MASM 汇编程序,操作数是写死在程序中的。
1 | INCLUDE Irvine32.inc |
我们详细分析一下这个程序。第一部分是汇编器指令:
INCLUDE Irvine32.inc调用了教学用库Irvine32.inc
我们要求:在本课程中,必须 INCLUDE Irvine32.inc,因为它预设置了有用的函数和汇编器指令。
接下来是代码区,我们在这里写函数代码:
.code表示下面的所有代码均属于代码段的内容main PROC定义main过程(Procedure,即函数)a:定义了一个标签a,指向mov eax, 30000hmov eax, 30000h将十六进制值30000h赋值给eaxadd eax, 80000h将80000h和eax的值相加,结果保存到eax。call DumpRegs调用Irvine32.inc的DumpRegs函数,作用是打印当前的寄存器数值jmp a表示无条件跳转到a指向的代码。由于有个 bug 故注释,注意到了么?exit是系统调用 (system call),表示程序的退出。注意不要对主函数使用ret,因为ret需要显式调用方main ENDP表示结束此过程的定义END main表示整个源文件结束,并且指定了入口点函数main。
7.8 数据类型与数据操作
MASM 内建以下数据类型:BYTE, SBYTE(有符号字节), WORD, SWORD, DWORD, SDWORD, FWORD(六字节), QWORD, TBYTE(十字节)。主要使用 BYTE, WORD, DWORD, QWORD 类型,可分别简写为 db, dw, dd, dq。
使用 TYPEDEF 可以定义新的类型,包括指针,语法为 类型名 TYPEDEF [PTR] 已注册的类型。已注册的类型包括内建类型和已使用 TYPEDEF 注册过的类型,例如:
1 | CHAR TYPEDEF BYTE |
使用数据类型在 .data 段定义数据的语法为 [标识符] 类型 [初始化值, ...]
1 | count dword 114514 |
数据定义时必须有至少一个初始化值,如果不想自己设定,可以使用 ? 表示随机赋值。所有的初始化值,无论其格式,都会被汇编器转换为二进制数据。如果初始化值是有规律重复的,可以使用 DUP (Duplicate) 命令:
1 | BYTE 20 DUP(0) ; 20 字节,均初始化为 0 |
可以有多个初始化值,这实际上就声明了一个数组:
1 | list byte 10,20,30,40 |
数据 10 位于 list 的偏移 0 位置,以此类推。
字符串也是一种数组,但作为例外,每个字节间不都需要逗号分隔。正如各位在 C 中所学的那样,所有的字符串都需要加一个 \0 终止符(当然,是手动的,没有编译器兜底了)。对于多行字符串,只需要一整块字符串的末尾终止即可,如果需要换行,需要使用 0dh, 0ah (CR/LF):
1 | greeting1 BYTE "Welcome to the Encryption Demo program " |
计算数组长度可以使用 $ - Array,例如:
1 | list BYTE 10,20,30,40 |
如果是字符串长度,需要减去使用的转义字符的长度:
1 | hello BYTE "Hello",0 |
注意采用这种方式计算的,表达式必须紧跟在数组之后,因为 ($ - list) 的语义是当前地址 ($) 减去 list 的地址。数据在段内是连续的,因此可以用偏移值计算元素个数。如果中间再插入了其他数据,偏移值就计算错误了。
BYTE 只占 1 字节,因此偏移值 = 元素数。如果是 WORD 等更大的数据类型,就需要除以它的单位长度,例如:
1 | data WORD 1,1,4,5,1,4 |
可以在代码段中间夹杂数据段。数据的作用域从声明位置开始:
1 |
|
在进行数据的转移时,数据的宽度必须匹配目标寄存器的宽度。由于寄存器的宽度可以 “分割”,会出现数值覆盖 (Overlapping Values) 的情况:
1 |
|
数值仅覆盖对应位数,而其他位不变。对于有符号数,这种情况更加复杂:
1 |
|
明明存储的是负数,结果读 ECX 的时候却读出了正数!除了尽量保证使用 CX,我们也可以将 ECX 赋值为 FFFFFFFFh,这样数值就是正确的 FFFFFFF0h 了,这被称为符号位扩展 (Sign Extension)。为了简便较小宽度的数据向叫大宽度的寄存器的转移, x86 设计了两个指令:
MOVZX(Move with Zero eXtension)
适用于无符号数,将扩展的位数设置为 0。
MOVSX(Move with Sign eXtension)
适用于有符号数,将扩展的位数设置为 1。
1 |
|
以上两个指令不得使用常量作为操作数。它们仅接受以下操作数组合:
1 | reg32, reg/mem8 |
对于两个内存数据的交换,需要使用寄存器中转:
1 | mov ax,val1 |
7.9 操作数和寻址方式
正如我们在 7.5 所述,操作数可以是寄存器、内存地址或者整数表达式。我们将这些操作数分类为:立即数、寄存器和内存,由此得到几种寻址模式:
| 寻址方式 | 典型语句 | 关键含义 | C 场景 | 记忆点 |
|---|---|---|---|---|
| 寄存器寻址 | mov ecx, eax |
寄存器中的数据传送 | 中间结果 | 无 [] |
| 立即数寻址 | add eax, 5 |
常量写在指令中 | 常量赋值 | 数直接给出 |
| 直接寻址 | mov ecx, count |
变量名给出地址 | 普通变量 | 名字就能定位 |
| 间接寻址 | mov edx, [ebx] |
按寄存器地址取值 | 指针 *p |
寄存器里装地址 |
| 相对寻址 | mov esi, [ebx+4] |
基址 + 常量偏移 | arr[1] |
偏移按字节 |
| 相对寻址 | mov eax, count[esi] |
符号地址 + 寄存器偏移 | count[i] |
基址 + 位移 |
| 变址寻址 | mov eax, [ebx+edx+80h] |
基址 + 变址 + 位移 | buf[32+i] |
多一个动态偏移 |
| 比例变址 | mov eax, [ebx+esi*4-80h] |
基址 + 下标 × 大小 + 位移 | arr[i-32] |
自动乘元素大小 |
- 立即数寻址
立即数就是内存中的字面量,因此立即数可以直接写在指令中:
1 | mov eax, 10h |
- 直接内存寻址
地址需要解引用以访问对应数据。不过相比直接写地址值,我们一般都使用变量名,因为变量名携带了相对与段基地址的偏移信息。
1 | mov eax, var1 |
发现了吗?同样是 mov,两者在 Hex dump 中的表示却不一样!我们将 B8、A1 称为操作码 (Opcode),可以参见 MOV – Move Data。
不同的 Opcode 对应不同的操作数类型,但在汇编中我们统一写成 mov。这就是上层虚拟机对底层虚拟机功能的封装。
B8 表示将 imm 复制到 reg,而 A1 表示将位于 seg:offset 的值复制到 EAX。这样你就明白为什么 mov eax, var1 是将 var1 指向的值复制到 eax,而不是将 var1 的地址赋值给 eax 了吧,因为此时操作码 A1 表示对随后的地址值解引用。
正如在 4.2 中所说的,x86 的大多数指令不支持两个内存操作数。每条 mov 指令的操作码中,用于指定操作数的位域有限,无法同时编码两个完整的内存地址;并且早期 CPU 的寄存器才是真正进行移动操作的场所,内存只是数据的 “仓库”,需要使用寄存器作为中转:
1 | mov eax, var1 |
现代 CPU 的存储转发机制可能会直接将 var1 的数据从 L1 数据缓存转发到 var2 的存储操作中,甚至不需要寄存器物理上 “持有” 数据。但指令集层面仍然要求程序员(或编译器)显式写出这个寄存器。
- 直接寄存器寻址
7.10 示例 2
1 | INCLUDE Irvine32.inc |
此处我调用了 Irvine32.inc 的 WriteString 函数。它以 edx 作为参数,接受字符串地址,将含终止符的字符串打印到标准输出。
7.11 基本运算
基本运算指令分为算术运算和逻辑运算。
8. 分支与循环程序设计
默认情况下,CPU 按顺序加载和执行程序。但条件指令会根据 CPU 状态标志的值(Zero、Sign、Carry 等)将控制权转移到程序中的新位置。控制转移分为两种:无条件转移和条件转移。
循环在这里视为分支,因为它也可以使用条件转移指令实现。
8.1 无条件转移
使用 JMP 指令:
1 | JMP label |
CPU 将自动将 label 转换成偏移值,赋值给 IP 以在新位置执行代码。
JMP 是创建循环的最简单方法,结合标签可以形成一个无限循环:
1 | top: |
8.2 条件转移
只有判断标志寄存器的值满足条件时,条件指令才跳转。由于标志寄存器不能直接设定,因此需要搭配 cmp (compare) 或 test 指令使用。
cmp 的底层逻辑是 A - B,用于判断两个数的大小。test 的底层逻辑是 A & B,用于检查特定位数的状态(test A,A 可以判断 A 是否为 0)。两个指令均只修改 FLAGS 寄存器而不破坏元数据。
常见条件转移指令:
| 指令 | 英文全称 | 含义 | 判断标志位 |
|---|---|---|---|
JE, JZ |
jump equal,jump zero | 结果为零则跳转 (相等时跳转) | ZF=1 |
JNE, JNZ |
jump not equal,jump not zero | 结果不为零则跳转 (不相等时跳转) | ZF=0 |
JS |
jump sign | 结果为负则跳转 | SF=1 |
JNS |
jump not sign | 结果为非负则跳转 | SF=0 |
JP, JPE |
jump parity,jump parity even | 结果中 1 的个数为偶数则跳转 | PF=1 |
JNP, JPO |
jump not parity,jump parity odd | 结果中 1 的个数为偶数则跳转 | PF=0 |
JO |
jump overflow | 结果溢出了则跳转 | OF=1 |
JNO |
jump not overflow | 结果没有溢出则跳转 | OF=0 |
JB, JNAE |
jump below,jump not above equal | 小于则跳转 (无符号数) | CF=1 |
JNB, JAE |
jump not below,jump above equal | 大于等于则跳转 (无符号数) | CF=0 |
JBE, JNA |
jump below equal,jump not above | 小于等于则跳转 (无符号数) | CF=1 or ZF=1 |
JNBE, JA |
jump not below equal,jump above | 大于则跳转 (无符号数) | CF=0 and ZF=0 |
JL, JNGE |
jump less,jump not greater equal | 小于则跳转 (有符号数) | SF≠ OF |
JNL, JGE |
jump not less,jump greater equal | 大于等于则跳转 (有符号数) | SF=OF |
JLE, JNG |
jump less equal,jump not greater | 小于等于则跳转 (有符号数) | ZF=1 or SF≠ OF |
JNLE, JG |
jump not less equal,jump greater | 大于则跳转 (有符号数) | ZF=0 and SF=OF |
其实记忆这些指令并不难,因为它们都是 JMP + 条件缩写的形式。
条件转移可以形成一个有限循环:
1 | ; ... |
有一个专门的指令 LOOP 同样实现了有限循环,以 ECX 作为循环次数,当其数值减少到 0 时,LOOP 指令就会停止,继续执行下面的指令。
1 | ; ... |
在进入 LOOP 循环体前,务必显式初始化 ECX!在循环体内,我们也不建议再修改 ECX。如果要修改,请先将 ECX 代表的循环次数保存,然后在退出一次循环前将次数恢复到 ECX。
对于嵌套循环,你同样需要先保存外层循环次数,再设置内层循环次数;内层循环结束后,及时将外层循环次数恢复到 ECX:
1 |
|
9. 函数
通俗地说,我们可以将函数(Procedure,本义为过程)定义为一个返回语句结束的语句块。函数是使用 PROC 与 ENDP 指令声明的。它必须被赋予一个合法的标识符。
对于非入口点函数(即 main),使用 RET 作为返回语句:
1 | sample PROC |
函数内标签的作用域仅限于此函数内,这主要影响跳转与循环指令。虽然可以通过标签名后标识 :: 来声明一个全局标签,但这样做容易破坏程序的运行时栈 (Runtime Stack)。关于运行时栈我们会更详细地介绍。
这是一个简单的加法函数,约定参数和返回值都通过寄存器传递,比较简单:
1 | SumOf PROC |
现在你可以使用 CALL 指令调用函数了!Irvine32.inc 实现了一些简单的输入输出函数。你可以直接打开此文件查看此库声明了哪些函数。
9.1 运行时栈
运行时栈用于存储函数执行期间的必要的临时信息。和数据结构中的 “栈” 一样,运行时栈同样遵循 LIFO 规则。运行时栈通过 ESP 寄存器管理地址。一般来讲,我们不会直接修改 ESP,而是通过 CALL、RET、PUSH 和 POP 指令间接修改。
ESP 永远指向最后一个压入栈的值,即栈顶。这里需要注意一下,在操作系统中,栈是 “向下生长” 的,即栈的地址是从高到低的。每当我们 PUSH 一个值进入栈,ESP 的值会递减,然后将操作数复制到栈里。一个 16 bits 操作数会使 ESP 递减 2,32 bits 则递减 4。PUSH 的操作数可以是寄存器、内存地址或立即数。
POP 会将 ESP 指向的栈内容复制到操作数中,然后递增 ESP。POP 的操作数可以是寄存器或内存地址。
9.2 函数调用
CALL 指令可以让 CPU 跳转到函数内存位置以执行指令。与之配套的是函数内的 RET 语句。
具体来讲,CALL 将返回地址压入栈中,然后将目标函数地址赋值给 EIP。返回地址指向 CALL 的下一条语句。当函数 RET 时,栈中存放的返回地址赋值给 EIP,之后弹出。
对于函数的嵌套调用同样如此。每 CALL 一个子函数,对应的返回地址就会压入栈中;然后 RET 时不断 “溯源”,直到返回主函数。
9.3 保存和恢复寄存器
现在我们准备写一个更复杂的函数:
1 | ;---------------------------------------------------- |
此函数通过寄存器传递参数和返回值,不使用特定标识符以提高兼容性。
不过我们不是想聊这个。看这个程序的开头,它将 ESI 和 ECX 的值压入栈中,而返回前又将其 POP 出来。这是大多数修改寄存器的函数的典型做法:始终保存并恢复被函数修改的寄存器,以确保调用者自身的寄存器值不会被覆盖。该规则的一个例外是用作返回值的寄存器,一般是 EAX。
你可以通过 USES 指令让汇编器自动生成这类代码:
1 | ArraySum PROC USES esi ecx |
注意 USES 紧跟在 PROC 之后,寄存器列表中间无逗号。
或者使用 pushad 和 popad (ad 代表 All Double)将所有的通用寄存器都 push / pop。
9.4 用栈传递参数
本节开头我们展示了如何用寄存器传递参数。然而,大多数现代语言中函数参数是通过堆栈传递的:在 32 位下,Windows API 的参数也通过栈传递。因此,必须学会如何用栈传递参数。
为什么不继续使用寄存器?
微软定义,使用寄存器传递参数的情况在 32 位下称为 fastcall 调用约定。顾名思义,和通过栈传递参数相比,寄存器传参更快。然而,一旦程序规模开始膨胀,寄存器传参出现 bug 的概率就越高,并且将寄存器反复弹压的操作所造成的代价也会抵消 fastcall 带来的效率优势。
9.4.1 栈帧
当函数被调用时,运行时栈会预留一块区域,称为栈帧(Stack Frame,或活动记录 Activation Record)。栈帧存放着函数的实参、返回地址、局部变量和保存的寄存器。一个栈帧由以下步骤建立:
- 传递的实参(如有)被压入堆栈。
- 调用函数,使函数返回地址被推送到栈上。
- 当函数开始执行时,
EBP会被推送到堆栈上。 EBP设置成和ESP相等。从此,EBP作为函数形参的基址参考。- 如果寄存器需要保存,会被推送到堆栈中。
- 如果存在局部变量,
ESP会递减以预留堆栈上的变量空间。
理想情况下,相关寄存器应在将 EBP 设置为 ESP 后、预留局部变量空间之前推送到栈中。这有助于避免更改现有栈参数的偏移量。即使使用 PUSHAD,我们依然建议在此之前先单独 push 和设置 EBP。
使用 ESP 实现栈上数据的存储和逻辑删除。EBP 在当前栈帧内位置固定,因此函数中对大部分数据的访问都基于 EBP 进行。
汇编的传参形式有两种:值传参和引用传参。
需要注意的是,两种传参的参数压栈顺序都是倒序的。参考等效 C 语言格式:int some_func(int var1, int var2),则 var2 先压入,var1 后压入,从右往左。
数组必定使用引用传参,因为值传参代表它需要将每个元素都压入栈。使用 OFFSET 指令。
如果压栈参数在寄存器里,将寄存器压入栈,那么由调用者恢复寄存器:
1 | mov ecx,eax |
9.4.2 栈参数的调用和清理
现在我们来一个示例:
1 | AddTwo PROC |
使用栈传参时,需要将 EBP 压入栈,ESP 由系统自动管理,不需要我们操心。
如果我们调用了 AddTwo(5,6),即:
1 | push 6 |
那么它的栈结构大概长这样:

如果我们想访问参数,那么用相对寻址。像 [ebp + 12] 这样用表达式引用实参的,我们称为显式栈参数 (Explicit Stack Parameter)。
有一个问题不知道你发现了没有:既然返回地址在实参下面,而当返回地址被弹出时,函数也退出了,那么剩下的实参怎么处理?
很遗憾,操作系统并不会在程序运行过程中自动回收它们。忽略它们?如果只用一层调用还好,但如果是嵌套调用呢?内层函数残留的栈实参在外层函数返回地址的下面,而 ESP 会读取实参作为地址!还记得我们在 C / C++ 所说的吗?标准结局就是内存访问冲突。
清理的理由有了,但怎么清理?POP 可不能把立即数 POP 出去啊。那么,既然栈实参不能移除,可不可以直接跳过它呢……
9.4.3 调用约定
调用约定 (Calling Convention) 确定了函数传参的传参形式、压栈顺序和清除栈方式。在 x86 Windows 下有两个主要的调用约定:C 调用约定 (CDECL) 和 STDCALL 调用约定。
C 调用约定和 STDCALL 的压栈顺序一致,即从右到左倒序压栈。而清除栈方式不一样:
- C 调用约定是调用者清除 (Caller Cleanup),在调用函数
CALL的下一行对ESP进行加法,其值等于被调用函数参数的总大小。 STDCALL是被调用者清除 (Callee Cleanup),给予被调用函数RET一个操作数,其值等于被调用函数参数的总大小。此时系统会自动向ESP增加值。
STDCALL 可以确保调用者不会忘记清理栈,而 C 调用约定允许可变数量参数。
x86 汇编允许混用寄存器传参和栈传参,即其他调用约定和 fastcall 的混合。但这不属于任何调用规定。
9.5 局部变量
和参数不一样,局部变量是顺序压栈的。比如,一个 C 函数:
1 | void MySub() |
在 x86 汇编中呈现为:
1 | MySub PROC |
栈帧大致表现为:

9.6 引用传参的使用
引用参数通常使用间接寻址访问。比如将一个数组的引用传递给 ESI:
1 | mov esi,[ebp+12] ; offset of array |
AP. CTF 中常见汇编知识
了解了一些基本的汇编指令后,下面我们来看看 CTF 中常见的一些指令:
loop循环指令:
1 | mov rax,0 |
上面的指令执行的是对 123 累加 236 次,从以上代码中我们可以看到,rcx 寄存器保存的是循环次数,loop 指令每执行一次,rcx 的数值就会减少 1,当其数值减少到 0 时,loop 指令就会停止,继续执行下面的指令。
- 无条件跳转指令:
1 | lable: |
无条件跳转指令为 jmp,其意思从字面上就可以看出来,只要是执行到 jmp 这里,无论什么情况,都会直接跳转到 lable 的代码块中继续往下执行指令。
- 条件跳转:
1 | lable: |
以上代码将 ebx 的值与 0 对比,如果相等,则会跳转到 lable 处,条件跳转指令 je 代表相等则跳转,还有其他与之条件不一样的条件跳转指令,一般条件跳转指令是与 cmp、test 指令混在一起用的。以上代码也可以用 test 来写:
1 | lable: |
test 是逻辑与指令,以上代表的即为 ebx&0,jnz 代表标志位为 0 就跳转,其实现效果与 cmp 相同。
下面是一些常见的条件跳转指令:
| 指令 | 英文全称 | 含义 | 判断标志位 |
|---|---|---|---|
JE, JZ |
jump equal,jump zero | 结果为零则跳转 (相等时跳转) | ZF=1 |
JNE, JNZ |
jump not equal,jump not zero | 结果不为零则跳转 (不相等时跳转) | ZF=0 |
JS |
jump sign | 结果为负则跳转 | SF=1 |
JNS |
jump not sign | 结果为非负则跳转 | SF=0 |
JP, JPE |
jump parity,jump parity even | 结果中 1 的个数为偶数则跳转 | PF=1 |
JNP, JPO |
jump not parity,jump parity odd | 结果中 1 的个数为偶数则跳转 | PF=0 |
JO |
jump overflow | 结果溢出了则跳转 | OF=1 |
JNO |
jump not overflow | 结果没有溢出则跳转 | OF=0 |
JB, JNAE |
jump below,jump not above equal | 小于则跳转 (无符号数) | CF=1 |
JNB, JAE |
jump not below,jump above equal | 大于等于则跳转 (无符号数) | CF=0 |
JBE, JNA |
jump below equal,jump not above | 小于等于则跳转 (无符号数) | CF=1 or ZF=1 |
JNBE, JA |
jump not below equal,jump above | 大于则跳转 (无符号数) | CF=0 and ZF=0 |
JL, JNGE |
jump less,jump not greater equal | 小于则跳转 (有符号数) | SF≠ OF |
JNL, JGE |
jump not less,jump greater equal | 大于等于则跳转 (有符号数) | SF=OF |
JLE, JNG |
jump less equal,jump not greater | 小于等于则跳转 (有符号数) | ZF=1 or SF≠ OF |
JNLE, JG |
jump not less equal,jump greater | 大于则跳转 (有符号数) | ZF=0 and SF=OF |
下面是一些常见的位运算指令:
AND:与运算,AND AX,BX将AX与BX进行逻辑与运算,并将结果保存到AX寄存器中OR:或运算,OR AX,BX将AX与BX进行逻辑或运算,并将结果保存到AX寄存器中XOR:异或运算,XOR AX,BX将AX与BX进行异或运算,并将结果保存到AX寄存器中NOT:取反操作,NOT CX将CX进行取反,并将结果保存到CX寄存器中TEST:逻辑与运算,TEST AX,BX将AX与BX进行与运算,并设置标志位,结果不保存
- 函数传参:
在汇编中,我们有时候需要知道参数被保存在那个寄存器里,这里我列举一般情况:
通常函数的返回值保存在 ax 寄存器里,比如 eax,rax 等; 函数的参数一般按顺序保存在 cx、dx、si 中,在 64 位汇编中,一般按 rcx, rdx, r8, r9, r10, r11, … 的顺序保存参数。
有时候根据函数调用约定以及编译器和平台的不同,这个储存规律也会发生改变,我们需要根据当时情况动态调整。
lea地址加载:
其使用格式为 lea rdx,[my_var],将 my_var 的地址而不是内容赋值给 rdx 寄存器。
xchg数值交换指令:
其使用格式为 xchg ax,bx 将 ax 和 bx 的数值交换。
