参考资料:
Assembly Language for x86 Processors.pdf
书籍知识大纲
1. 基础概念
- 汇编语言应用场景、虚拟机概念、数据表示(二进制、十六进制、字符存储)、布尔运算。
- 计算机体系结构基础(x86设计、指令执行周期、内存读取、程序运行原理)。
2. x86处理器架构
- 工作模式(实模式、保护模式)、内存管理(分段与分页)、浮点单元(FPU)、输入/输出系统(I/O层次)。
3. 汇编语言基础
- 数据类型定义(BYTE/DWORD/QWORD)、符号常量、宏指令、伪指令(如**
EQU**,TEXTEQU)、变量声明。 - 程序结构:数据段、代码段、堆栈段,以及汇编-链接-执行流程。
4. 核心指令集
- 数据传输 :
MOV,XCHG,PUSH,POP, 地址运算符(OFFSET,PTR)。 - 算术运算 :加减法(
ADD,SUB,NEG)、乘除法(MUL,IMUL,DIV)、扩展精度运算(ADC,SBB)。 - 位操作 :移位(
SHL,SHR)、循环移位(ROL,ROR)、逻辑运算(AND,OR,XOR,TEST)。 - 控制流 :条件跳转(
JZ,JG)、循环(LOOP,LOOPE)、过程调用(CALL,RET)。
5. 高级编程
- 过程与堆栈 :堆栈帧(
ENTER,LEAVE)、局部变量、递归实现。 - 字符串与数组 :字符串操作指令(
MOVS,SCAS,CMPS)、二维数组寻址(基址+变址+偏移)。 - 结构与联合 :结构体定义、嵌套结构、联合(Union)。
- 宏汇编 :宏定义与调用、条件汇编(
IF,REPEAT)、宏函数。
6. 系统编程
- Windows API编程 :控制台I/O(
ReadConsole,WriteConsole)、文件操作(CreateFile,ReadFile)、动态内存分配(堆操作)。 - MS-DOS编程 :中断调用(
INT 21h)、16位实模式编程、BIOS中断(**INT 13h**磁盘操作)。 - 异常与调试 :浮点异常处理、调试工具使用(如调试寄存器)。
7. 浮点与高级指令
IEEE 754浮点格式、FPU指令集(FLD,FADD,FCMP)。指令编码机制(机器码格式、ModR/M字节、地址模式编码)。
8. 高级语言接口
混合编程:C/C++与汇编接口(**__asm**内联汇编、外部函数调用)、调用约定(cdecl, stdcall)。保护模式与实模式下的链接差异。
9. 扩展主题
Java字节码与汇编对比、多模块程序设计(外部符号引用)、性能优化技巧(如利用CPU缓存)。
汇编基础

标识符
- 标识符允许1-247个字符,包括数字
- 是大小写不敏感的(汇编的大小写变量没有意义)
- 必须是字母,_,@,?,$开头的
字符常数的表达
- 与c语言是类似的,每一个字符表示一个byte(使用ASCII码)如’A’
- 引号里面使用引号只能单双引号进行间隔使用,没有转义字符/来进行相关的表述
符号常数
-
等于符号=:用来表示是一个32位的整数(被称为符号常量,因为可能多次使用到,但是在后续还可以进行更改重定义)
//应用举例 COUNT = 500//这个实际上就是将一个立即数改了一个名字为COUNT而已,所以在这里我们不能把COUNT当作一个变量!!! //所以COUNT = 500这一段代码是不可以在.data段里面的 mov ax,COUNT //这种常量往往还在.data段的前面 -
EQU:定义一个符号表示一个整数或者一段文本,但是后续不能进行更改和重定义,和C的const变量是类似的
PI EQU <3.1416> pressKey EQU <"Press any key to continue...",0> .data prompt BYTE pressKey //可以这样在data段中进行声明 -
TEXTEQU:是 文本等价宏,它不会分配内存,而是直接在汇编时文本替换,所以是可以进行重新定义的
//与C语言中的宏是很像的 continueMsg TEXTEQU <"Do you wish to continue (Y/N)?"> rowSize = 5 //它在汇编时会被替换为 5,不能更改运行时的值。 .data prompt1 BYTE continueMsg count TEXTEQU %(rowSize * 2) //%() 表示宏表达式求值,计算结果是 5 * 2 = 10,所以这里的count表示的内容成了10 setupAL TEXTEQU <mov al,count> .code setupAL //这里就是插入了一个mov al,count,由于count是10,所以实际是执行了mov al, 10 -
$:用来计算数组和字符串的大小
这个表示的内容是当前代码对应的内容的地址是多少!!!
list BYTE 10,20,30,40 ListSize = ($ - list) //这里完成的就是当前的地址(对应的是40那个BYTE,减去list对应的地址从而来得到这个list的总长度是4!!!)
声明变量的特殊情况
- 直接使用逗号来可以进行跨行连续声明
.data
array WORD 10,20,
30,40,
50,60
.code
mov eax,LENGTHOF array ; 6
mov ebx,SIZEOF array ; 12
//这个地方声明的所有内容都会被标记到array这个标签中去,所以对应LENGTHOF和SIZEOF都会出现正常的大小
- 多行出现多个相同的变量类型,但是没有对应的标签
.data
array WORD 10,20
WORD 30,40
WORD 50,60
.code
mov eax,LENGTHOF array ; 2
mov ebx,SIZEOF array ; 4
//这个地方就不一样了,声明的时候后面两组实际上是没有标签的,所以用LENGTHOF和SIZEOF只会识别第一行的部分,大小和上面的出现区别
基础数据类型
- BYTE,SBYTE:8位无符号,8位带符号→一个字节
- WORD,SWORD:16位无符号,16位带符号→两个字节
- DWORD,SDWORD:32位无符号,32位带符号→四个字节
- QWORD:64位整数→八个字节
- TBYTE:80位整数
- REAL4:4byte实数(对应32位float)→四个字节
- REAL8:8byte实数(对应64位double)→八个字节
- READ10:10byte实数(对应80位浮点数,与TBYTE有关系)
IA-32 基本的数据类型分为字节 (byte, 8 bit)、字 (word, 16 bit)、双字 (dword, 32 bit)、四字 (qword, 64 bit)、双四字 (double qword, 128 bit) 等:

数据都是在.data段进行定义的,不像C语言可以在code内进行定义
定义的方式【name】 【类型,上面8类】 【值】
- h-表示hex十六进制
- d-decimal表示十进制
- b-binary表示二进制
注意事项:
-
MASM不会对类型与值进行检查,所以可以像一个无符号整数比如BYTE赋值-128来表示0,但是这样不容易看懂代码
-
有关两种未定义数据的表示方式对比:
- .data?//声明没有经过初始化的数据段 - smallArray DWORD 10 DUP(?)//在段中声明没有初始化的变量 //两种形式下面的那种会等价节省空间 //这样不会在编译的时候进行空间的分配而是在运行的时候进行分配,所以对应的可执行文件的大小会小很多
引用数据类型
数组定义方式:
list1 BYTE 10,20,30,40
//这样就声明了一个list1变量来存四个BYTE的值
list2 BYTE 10,20,30,40
BYTE 20,30,50,70
BYTE 50,30,40,20
//这里是声明一个数组,数组里面存了三个数组,每个数组是原来数组的一部分
list3 BYTE ?,32,41h,00100101b
//可以有不确定的值?
//还可以有不同的进制同时存在
list4 BYTE 0Ah,20h,'A',22h
//可以有字符'A'这种表达方式
字符串定义方式:
str1 BYTE "Enter your name",0
str2 BYTE 'Error:halting program',0
//双引号和单引号是都可以的,MASM不会管这些
//没有这个0,MASM会把他自动认定位一个数组,就先C语言一样的
str3 BYTE 'A','E','I','O','U'
greeting BYTE "hello world"
BYTE "this is asm code",0
//字符串的标识就是最后面的这一个0,所以str3不是一个字符串而是一个BYTE的数组,这个0被称为空白符(null-terminated)
str4 BYTE "Checking Account",0dh,0ah,0dh,0ah,
//这里的0dh表示回车的意思,回车就是光标移到这一行最前面
//这个0ah表示换行的意思
"1. Create a new account",0dh,0ah,
"2. Open an existing account",0ah,0ah,
"3. Credit the account",0dh,0ah,
"4. Debit the account",0dh,0ah,
"5. Exit",0ah,0ah,//这里空了两行
"Choice>",0//只有在这里才结束
//实际的存储形式应该是这样的:
//Checking Account
//1. Create a new account
//2. Open an existing account
//3. Credit the account
//4. Debit the account
//5. Exit
// Choice>
//这里没有回到开头处补充"Choice>"内容
DUP伪指令来构建重复内容:
//`dup` 伪指令用于定义重复数据
var1 BYTE 20 DUP(0)
//这里创建了20个0
var2 BYTE 20 DUP(?)
//这里创建了20个没有存值的BYTE
var3 BYTE 4 DUP("STACK")
//创建4个STACK-》"STACKSTACKSTACKSTACK"共计20个BYTE
var4 BYTE 10,3 DUP(0),20
//10,0,0,0,20共计5个BYTE
注意事项:
- 即使是引用类型也是咋.data段中写的
寄存器

基础的寄存器ABCD后面全是跟的X表示的都是16bit的Reg,后续被拆分成两部分,高位为H(High)低位为L(Low),E 表示Extend拓展,就是将原来的16bit拓展到32bit了的
末尾位S的都是段寄存器(Segment,标志寄存器除外)
IA-32 指令集下,用于基本的程序执行的寄存器分为四个类型:
- 通用寄存器 (general-purpose registers)
- 段寄存器 (segment registers)
- 程序状态和控制寄存器 (program status and control register, EFLAGS register)
- 指令指针寄存器 (instruction pointer register, EIP register)
内存与地址
有关指针和C语言有很大的区别,C语言的数组+1对应的实际地址可以是+4,但是在这汇编中+1对应的就是一个BYTE,所以如果是一个WORD的数组,必须每一次都+2才可以正常的访问后面的元素,DWORD就要+4才可以,C是会进行智能识别种类从而无需考虑原来数据类型占用是多大的
可以使用**【】来去指定的地址取到对应的值,直接用标记Label的话实际只有地址**,所以直接写:
var WORD 023h
mov ax,var
mov ax,[var]
//这里认定的var是变量,所以在MASM中会智能识别出来进行取值,所以下面两行代码是等价的
IA-32 指令集支持三种基本的工作模式:保护模式 (protected mode),实模式 (real-address mode) 和系统管理模式 (system management mode, SMM)
IA-32 指令集属于 CISC (Complex Instruction Set Computer) 体系结构
一般忽略最后一种模式,将地址的存在认定为两种模式,IA32在8086等16位OS上采用的是实地址模式,但是在后续80386之后的32bitOS全部采用的都是保护模式,然后现代的OS也是采用的保护模式,32位的保护模式上面会有4G的逻辑地址空间

在保护模式下,处理器支持虚拟内存、分页等功能,也能提供更好的多任务性能。
**实模式下只支持 16 位的寄存器,也就是只有 20 位的寻址空间(16+4)即 1MB 的内存空间。**在加电或者重置后,处理器处于实模式,现在系统一开机就进入保护模式只有很短暂的加载启动程序部分时间会在实地址模式下面进行
平面内存模型中内存是单独一段的连续的地址空间。这个地址空间也被叫做线性地址空间 (linear address space)。代码、数据和栈都在这个空间内,这个空间按照字节来寻址,其中的一个地址就是线性地址 (linear address),这个也是我们这门课程的主要环境flat+386

在分段内存空间中,一个程序的内存地址被分为了若干段,代码、数据、栈等都分别存储在各自的段内。一个程序使用逻辑地址来编址一个段中的字节数据,这个地址包括了一个段基址和一个相对于基址的偏移。每个段可以有 232 字节的大小。而在处理器内部,所有的段都被映射到处理器的线性地址空间,对于某个内存位置的访问都由处理器内部转换完成,这些过程对程序是透明的。

所有的计算机底层实现中都使用了栈,这里的栈是遵循所有的数据结构中栈的特征,所有的内容都是被从top加入到栈里面的
函数(过程Procedures)
为了让汇编更加好管理进行了一层封装,就像C语言的函数一样,这种函数增加了代码的复用性
如何创建一个函数,和main函数的设置是一样的,下面的这个函数名字换成main就是main函数了,当然不一定汇编的启动是非要从main开始的,这个相关汇编器的设置有一定的关系的
SumOf PROC
;
;这里写代码!!!
;
SumOf ENDR
//这里完成了一个函数叫SumOf,相关的传参实现是需要我们进行手动压栈等等的操作的,这里的函数和C是不太一样的,内部的内容是可以影响外面的值的,但是我们C语言的设置保证每一次调用都实现了压栈和出栈因此没有影响的

CALL指令
用这个指令来进行函数的调用
调用是用了一系列的步骤的:
- 首先要获取当前调用call指令的下一个指令的地址,将其压入栈中,作为这一函数的栈底内容,到时候进行ret返回主函数的时候会进行出栈使用这个值的,类似一个push
- 然后会将call指令调用的标签对应的地址放到EIP中,然后就可以进行跳转到对应的函数中去了,类似一个goto,和一般编程语言中的函数不太一样
call SumOf
//这里调用的就是SumOf的Precedures
RET指令
用这个指令来进行出栈和产生返回值,关键是来返回到原来的过程中,实际上实现就是单纯的pop函数
//这里是在一个call后面才能进行ret退出函数
局部标签和全局标签
非常相似不同函数之间进行相互包裹的goto语句
全局标签的声明是使用两个:,局部标签是仅仅用一个:来进行声明的
main PROC
jmp L2 //这里无法实现,因为子啊main这个区域中找不到L2这个标签
**L1:: //这个L1是在全局都有效的**
exit
main ENDP
//这里有两个函数一个是mian一个是sub2
sub2 PROC
**L2: //这个L2标签是仅仅在sub2中有效的**
jmp L1 //这个是可行的,因为L1是一个全局的,所以是有效的
ret
sub2 ENDP
//综上这个函数是有问题的,所以没有办法实现在main中调用sub2函数,但是sub2函数的功能是正常的!!!
指令
运算类型格式转换类型指令:
| SHL | SHL des, count | 将操作数逻辑左移count位(相当于乘以2的count次方),高位补0,影响CF标志。 |
|---|---|---|
| SHR | SHR des, count | 将操作数逻辑右移count位(相当于除以2的count次方),低位补0,影响CF标志。 |
| SAL | SAL des, count | 算术左移(与SHL相同)。 |
| SAR | SAR des, count | 算术右移,符号位保持不变,影响CF标志。 |
| ROL | ROL des, count | 循环左移(不带进位),最高位移动到最低位,影响CF标志。 |
| ROR | ROR des, count | 循环右移(不带进位),最低位移动到最高位,影响CF标志。 |
| RCL | RCL des, count | 带进位循环左移,CF标志参与循环。 |
| RCR | RCR des, count | 带进位循环右移,CF标志参与循环。 |
| SHLD | SHLD des, src, count | 将src的位左移入des的高位,影响CF标志。 |
| SHRD | SHRD des, src, count | 将src的位右移入des的低位,影响CF标志。 |
| MUL | MUL op | 无符号乘法,结果存入AX(16位)或DX:AX(32位)。 |
| IMUL | IMUL op | 有符号乘法,结果存入AX(16位)或DX:AX(32位)。 |
| DIV | DIV op | 无符号除法,被除数在AX(16位)或DX:AX(32位),商存入AX,余数存入DX。 |
| IDIV | IDIV op | 有符号除法,规则与DIV类似。 |
| CBW | 无操作数 | 将AL扩展到AX(符号扩展)。 |
| CWD | 无操作数 | 将AX扩展到DX:AX(符号扩展)。 |
| CDQ | 无操作数 | 将AX扩展到DX:AX(符号扩展,32位环境)。 |
| ADC | ADC des, source | 加法并考虑进位标志CF,用于多精度加法。 |
| SBB | SBB des, source | 减法并考虑借位标志CF,用于多精度减法。 |
| BCD系列指令 | 处理BCD(十进制)数据,如AAA(校验AL)、AAM(ASCII调整)、DAA(十进制加法调整)等。 |
指令与汇编器的种类有关,MASM与NASM是不一样的,还有TASM
除此之外还有不同的指令集ISA,arm,amd64(x64),x86,RISC-V等等
汇编器和指令集的关系是0,但是汇编代码和汇编器是强相关的
一个指令包含:可选的Label+必要的助记符+相关的操作数+还可以选择有没有注释来解释指令
Label:
主要是在跳转和循环中使用这个
底层实现的原来就是一个Label对应的实际是代码段的对应的地址
操作数:
操作数一般是0~2个
主要是有三种类型:

- 立即数(8,16,32位的常数)
- 寄存器(仅仅使用指令来进行操作)
- 内存(用指令来进行表示或者使用寄存器中存的地址来进行表示)
注释:
- 单行注释:用;分号来进行开头
- 多行注释:使用相关的COMMENT指令
指令的格式:
- 没有操作数
- 一个操作数
- 两个操作数:add等运算指令
mov指令
注意事项:
- 操作数不可以是内存
- 不能将数据切断来进行mov
- 必须相同大小的区域进行mov
- 不可以对CS,EIP,IP这三个寄存器进行修改
movzx&movsx指令
movzx:将小的数移动到更大的寄存器中,可以使用movzx来进行高位缺失部分的0填充,这个函数的目的地必须是一个寄存器
mov bl,10001111b
movzx ax,bl
//这个地方的ax中存的就变成了0000000010001111
movsx:将小的数移动到更大的寄存器中,可以使用movsx来进行高位缺失部分的1填充,这个函数的目的地必须是一个寄存器
mov bl,10001111b
movsx ax,bl
//这个地方的ax中存的就变成了1111111110001111
xchg指令
用来进行两个操作数的交换,其中至少有一个是寄存器,两块内存被交换是不允许的,同时立即数也是不可以交换的,因为他本身就存在
//这里的交换也要是一样大小才可以哦
xchg var1,bx//交换内存和Reg也是可以的
xchg ax,bx//交换两个Reg
xchg var1,var2//这个是不被允许的两块内存是不能随便进行交换的
inc&dec指令
inc用来进行des++,dec用来进行des=des-1操作,就是对应C语言的递增和递减操作,可以对寄存器和内存中进行操作
.data
Myword WORD 1000h
.code
inc Myword
//1001h
dec Myword
//1000h
dec Myword
//999h
//这里的加减也是有溢出功能的00h-01h=FFh
add&sub指令
ADD指令des←des+source
SUB指令des←des-source
这两个指令是允许有立即数进行参与的
.data
var1 DWORD 10000h
var2 DWORD 20000h
.code
mov eax,var1
;00010000h
add eax,var2
;00030000h
add ax,0FFFFh
;0003FFFFh
add eax,1
;00040000h
sub ax,1
;0004FFFFh
neg指令
将des中的值来进行符号反转,正变负,负变正
.data
valB BYTE -1
valW WORD +32767
.code
mov al,valB ; AL = -1
neg al ; AL = +1
neg valW ; valW = -32767
//对于这个[valW]+1再进行反转会出现overflow标记从而表示产生的结果无效
//这个neg指令的内部实现实际上是sub 0,operand
//由于0作为des是不合法的但是实际上这个是可以实现的所以封装了一层位neg指令
OFFSET关键字
offset可以返回后面跟着的标签对应的偏移量,往往我们将这个直接当作是地址
.data
bVal BYTE ?
wVal WORD ?
dVal DWORD ?
dVal2 DWORD ?
.code
mov esi,OFFSET bVal ; ESI = 00404000
mov esi,OFFSET wVal ; ESI = 00404001
mov esi,OFFSET dVal ; ESI = 00404003
mov esi,OFFSET dVal2 ; ESI = 00404007
PTR关键字
对变量的初始类型进行重写,可以实现变量类型改变
.data
myDouble DWORD 12345678h
.code
mov ax,myDouble ; error – why?
mov ax,WORD PTR myDouble ; loads 5678h, ;读双字一半
mov WORD PTR myDouble,4321h ; saves 4321h
//可以用PTR来进行变量的改变但是不能用它来改变寄存器(怎么改????)
TYPE关键字
返回的是一个变量有多少个字节,有点类似是C的 sizeof
.data
var1 BYTE ?
var2 WORD ?
var3 DWORD ?
var4 QWORD ?
.code
mov eax,TYPE var1 ; 1
mov eax,TYPE var2 ; 2
mov eax,TYPE var3 ; 4
mov eax,TYPE var4 ; 8
LENGTHOF关键字
返回一个标签对应部分的长度(而且是已经赋值过了的),单位不是按照字节来的,而是按照对应的数据类型
.data LENGTHOF
byte1 BYTE 10,20,30 ; 3
array1 WORD 30 DUP(?),0,0 ; 32
array2 WORD 5 DUP(3 DUP(?)) ; 15
//这里的?都是没有算数的,所以这个LENGTHOF是求有效长度的,没有赋值的部分是不算的
array3 DWORD 1,2,3,4 ; 4
digitStr BYTE "12345678",0 ; 9
.code
mov ecx,LENGTHOF array1 ; 32
SIZEOF关键字
返回一个标签对应的全部长度,是包括没有赋值部分的,就是全部的长度,这里的单位是字节
.data SIZEOF
byte1 BYTE 10,20,30 ; 3
array1 WORD 30 DUP(?),0,0 ; 64
array2 WORD 5 DUP(3 DUP(?)) ; 30
array3 DWORD 1,2,3,4 ; 16
digitStr BYTE "12345678",0 ; 9
.code
mov ecx,SIZEOF array1 ; 64
LABEL关键字
实现的是同一组数据的不同解释方式,类似与联合体中的,不会创建新的数据
.data
dwList LABEL DWORD
wordList LABEL WORD//这个LABEL是用来表示后面声明的BYTE部分是可以用DWORD和WORD来进行解释的
intList BYTE 00h,10h,00h,20h//实际上只有这一行会出现对应的内存中
.code
mov eax,dwList ; 20001000h//这里用DWORD来进行解释数据
mov cx,wordList ; 1000h//这里用WORD来进行解释数据
mov dl,intList ; 00h
JMP指令
JMP是一种无条件跳转的指令
先声明一个标签——标签名+:号,在后续用jmp 标签名来进行跳转,这里是没有条件的跳转,类似C的goto语句
LOOP指令
也是需要标签的,调用仅仅需要将上面的jmp改成loop
这里的loop调用的是ecx这个计数器,开启循环对应的代码就是先要将循环的次数放到ecx中,然后每一次loop指令被调用都会对ecx进行-1的操作,知道ecx==0的时候才会停止循环,这里要注意最开始ecx=0的情况会先ecx-1=0FFFFFFFFH,从而导致循环了很多很多次
PUSH&POP指令
使用这两个指令进行对应的值的暂时存储,在函数中基本是必须使用的,而且由于IA-32中的通用寄存器比较少,所以,push和pop很常见的,但是在RISC-V等等很少使用这种,因为对应的寄存器比较多
//这里的pop和push是要求一一对应的
//下面先举例演示保存一些寄存器的值
push esi
push ecx
push ebx
mov esi,OFFSET dowrdVal
mov ecx,LENGTHOF dowrdVal
mov ebx,TYPE dwordVal
call DumpMem
//主要是这里的函数调用是要使用这些寄存器中的参数的,所以传入参数这一步骤要先清空原来的寄存器
pop ebx
pop ecx
pop esi
//这里对应关系要找好啊,这个地方类似那种括号是要求一一对应的
PUSHFD&POPFD指令
F表示flag,D表示double双字——因此没有操作数
这两个指令是用来进行push和pop这个EFLAGS这个状态寄存器的,由于原来的pop和push指令是只能对通用寄存器进行这些的处理的,因此我们必须使用新的指令来对这个特殊的标志寄存器来进行状态保存
PUSHAD&POPAD指令
A表示all,D表示double双字——因此没有操作数
这个是用来将所有的通用寄存器中的信息来进行压栈和出栈处理的,当然仅仅是32bit的寄存器才可以
这里的压栈顺序是:EAX,ECX,EDX,EBX,ESP,EBP,ESI,EBI
当然对应的出栈就是反过来的呗!
POPHA&PUSHA指令
A表示all——因此没有操作数
这两个指令是用来进行16bit的寄存器的压栈和出栈的
这里的压栈顺序是:AX,CX,DX,BX,SP,BP,SI,BI
当然对应的出栈就是反过来的呗!
USES指令
USES指令紧跟着PROC指令,后面紧跟着一串寄存器,uses运算符取代push pop,减少程序员工作量避免出错。
中间用空格或者TAB分开(不是其它符号),uses的作用是在过程的开始和结束的位置自动生成push和pop指令,避免程序员忘记写出入栈的代码。
Array PROC USES esi ecx
mov eax,0
//和下面的代码是等价的
Array PROC
push esi
push ecx
……
pop ecx
pop esi
Array ENDP
//就是一个自动进行pop和push的指令而已
BT(bit test)指令
语法:BT bitbase,n
bitbase是r/m16,r/m32,n可以是r16,r32,imm8
当然还有一个叫test的指令和这个是类似的,但是影响的标志位是不一样的!!!
TEST 指令对两个操作数执行按位 AND(与)运算 ,但不保存结果,只影响标志位(主要是 ZF、SF、PF)
//解释一下,这里的bt检测的是bitbase这个数据中的第n位是0还是1,是1的话会将CF=1,是0就把CF设为0
bt AX,9 ;对AX中第九位进行bittest
jc L1 ;是1的话跳转到L1标签处
SHL(Shift Left)指令

执行逻辑左移操作
如果溢出位是0的情况下是可以用移位来进行乘除运算的,左移1位就是乘以2,n位就是2大的n次方
SHR(Shift Right)指令

执行逻辑右移操作
右移是进行除法,而且除以2的n次方,这里要注意余数会和CF里面的数据有关,记录好移位操作中的所有CF状态值就可以得到对应的余数了
SAL(Shift Arithmetic Left)&SAR(Shift Arithmetic Right)指令

右移会出现补0和补1,左移就是正常的不会进行什么改变,后面最后一位也是直接填0
ROL(Rotate Left)指令
这个叫循环移位,是不会有数据进行丢失的

这里移出去还是会改变CF的,但是移出去的位还被重新放置到了最右边
ROR(Rotate Right)指令
这个循环右移和循环左移是类似的

RCL(Rotate Carry Left)指令

这个与前面的ROL&ROR有区别的,这个把CF也当作了这个数的一部分进行移位
RCR(Rotate Carry Right)指令

SHLD/SHLR指令——双精度左移
SHLD 指令将 dest 和 src 视为一个连续的 32 位(16 位模式下)或 64 位(32 位模式下)值,然后整体左移 count 位。
具体操作:
- 将
dest和src拼接成一个双倍长度的值 - 左移该值
count位 - 将结果的高位部分存回
dest,低位部分丢弃 - 最后移出的位存入 CF 标志
; 32位模式示例
mov eax, 0x11223344 ; EAX = 0x11223344
mov ebx, 0x55667788 ; EBX = 0x55667788
shld eax, ebx, 8 ; 将EAX:EBX左移8位,结果高32位存入EAX
; 执行后:
; EAX = 0x22334455
; EBX = 0x55667788 (不变)
; CF = 0 (最后移出的位是0)
MUL指令
用来进行无符号的惩罚的,允许8,16,32位运算的,其中有一个乘数是AL,AX,EAX
mov eax,60
mov ebx,40
mul ebx
//这里实现的是60*40的运算,注意mul的参数是不允许为立即数的

乘法运算的结果是需要两个寄存器来进行存储的,X*X会出来一个2X大小的结果
IMUL指令
带符号的惩罚,操作数也有一个默认的是AL,AX,EAX
IMUL 指令会影响以下标志位:
- CF 和 OF:当结果的高半部分不是低半部分的符号扩展时置1,否则清0
- SF、ZF、AF 和 PF:未定义
其实就是如果结果是正常的符号的话就是OF=0,反之就要设计OF=1特殊标记从而在后续环节中纠错
DIV指令

X/X仅仅只要一个寄存器就可以了,但是还要余数存储空间
mov dx,0
mov ax,8003h
mov cx,100h
div cx
//这里是16bit的除法运算,cx/ax,这里要注意英文表述方式:Divide 8003h by 100h(将8003除以100)
IDIV指令
和上面的DIV是一样的,但是有符号运算的,但是在除法操作执行前会先进行符号位的补位(这样才能一个存余数一个存原来的数),将高位全部用对应的符号位来补充
mov al,-48
CBW//将AL扩展到AH:AL
mov bl,5
IDIV bl//这里执行的是-48/5
//最后的结果是AL = -9,AH = -3,商是-9,余数是-3
💡 因为 IDIV 操作符要求被除数是 双倍长度的操作数 。
在做字节(byte)级别的 IDIV 时(即除数是 8 位寄存器如 BL),它会使用 16 位的 AX 寄存器 作为被除数。
也就是说:
IDIV bl的意思是:将AX中的值除以bl(即 BL 中的值)。- 所以,如果你想让
AL中的值参与一个完整的 16 位除法运算,你就必须把它的高 8 位(也就是AH)设置正确。 - 这个过程就叫:符号扩展(Sign Extension)
CBW&CWD&CDQ指令
这是三个进行类型转换的指令
- CBW(convert byte to word):这个是用来从AL→AH:AL
- CWD(convert word to doubleword):用来从AX→DX:AX
- CDQ(convert doubleword to quadword):用来从EAX→EDX:EAX
.data
dwordVal SDWORD -101//对应原来的数据是FFFFFF9Bh
.code
mov eax,dwordVal
CDQ
//这个时候的dwordVal是由两个寄存器构成的EDX:EAX = FFFFFFFFFFFFFF9Bh
IA32汇编环境配置
新手汇编程序
.386
//.386表示32位的应用程序
//如果不写或写成 .286、.186,汇编器默认只能生成对应 CPU 的指令子集。写 .386 就能使用包括 pushad、popad、32 位算术/逻辑指令等在内的全部 80386+ 指令。
.model flast,stdcall
//表示程序采用平坦内存模型,并遵循stdcall调用约定(支持MS-Windows的系统调用),其中参数从右向左压栈,被调用函数负责清理堆栈
//flat:取消了 DOS 时代的分段模型(code/data/stack 段),改用线性 32 位地址,和 Win32 平台一致。
//stdcall:决定了你在后面写 PROTO 或 INVOKE 时的默认调用规则,省得每次都要显式写 @stdcall。
.stack 4096
//为程序分配一个 4 096 字节的运行时栈,供函数调用时压参、保存返回地址、本地变量等
//一般只要几 KB 就够日常调用压栈了,调试时若参数/局部变量很多可适当加大。
ExitProcess PROTO,dwExitCode:DWORD
// 声明 Windows API 函数 ExitProcess 的接口:
// 名称 ExitProcess,接受一个 DWORD 类型参数 dwExitCode,返回时终止进程。
//这一行不生成代码,只告诉汇编器:“叫做 ExitProcess 的函数在别的库里有实现,它接受一个 32 位无符号参数”。
//后续你用 INVOKE ExitProcess,0 或者手动 push + call 时,汇编器会校验参数个数和类型。
.code
main PROC
// —— 这里开始你的主入口函数 main ——
mov eax,5
add eax,6
// ——— 简单示例:把 EAX 先赋 5,再加上 6,最后 EAX=11 ———
INVOKE ExitProcess,0
// 等价于:
// push 0
// call ExitProcess
// 宏 INVOKE 会帮你把参数按 stdcall 约定压栈,并发出 CALL 指令。
main ENDP
// 定义一个名为 main 的过程(也就是函数),编译器会在符号表里记录入口点。
//会自动把参数反序(从右向左)压栈,然后 call FuncName,并在调用后由 callee 清理栈空间。
END main
//END main 则告诉链接器程序的启动点是 main,最终它会转换为 Win32 的 CRT 入口(通常是 _mainCRTStartup 调用你的 main)。
相关代码
//基本的汇编模板————基础模板1
.386
.model flat,stdcall
.stack 4096
ExitProcess PROTO,dwExitCode:DWORD
.data
//这个地方用来声明变量
.code
//写代码的字段
main PROC
//写main函数
INVOKE ExitProcess,0
main ENDP
// main函数结束
END main
//汇编程序结束,并指定程序入口为 main
//包含库函数导入的举例————例1
INCLUDE Irvine32.inc
//引入 Irvine32.inc 文件,它包含了一些宏定义、常量、以及子程序,比如 DumpRegs 和 exit,这些在 Irvine 的开发环境中是必需的。相当于导入一个库。
.data
val1 DWORD 10000h
//DWORD 表示双字(4字节/32位)D就是Double的意思 后面是16进制的10000
val2 DWORD 20000h
val3 DWORD 30000h
finalVal DWORD ?
// ? 表示该变量还没有具体值。这里声明了一个没有初始化的DWORD
.code
main PROC
mov eax,val1
//将val1 放入到eax寄存器中
add eax,val2
//将val2和eax中存的val1相加,得到的结果存在eax中
sub eax,val3
//将eax中的值减去val3,得到的结果放在eax中,实际eax这个时候等于0
mov finalVal,eax
// 将eax寄存器中的值存到这个没有具体值的变量finalVal中
call DumpRegs
//调用 Irvine32 提供的 DumpRegs 子程序,用来显示当前寄存器的内容
exit
//调用退出程序的标准过程,程序结束
main ENDP
//主程序 main 的结尾
END main
//汇编程序结束,并指定程序入口为 main
此处涉及的关键语法:
- PROC和ENDP来实现一个过程的封装,这个在C中也叫函数
- END设置整个程序的入口,一般都是main,但是是可以更改的不像C语言默认只认main
- ?表示没有具体值的变量
- INCLUDE引入库函数,一般后面都是跟的inc文件(Irvine32.inc)
- 代码段在.code 后面,变量声明在.data后面
二进制程序的诞生

源文件-》经过汇编器-》目标文件(.o文件)-》进行链接-》可执行文件-》被操作系统加载从而允许成功
- Object File(目标文件):扩展名是
.obj,这是机器代码但还不能独立运行。 - Listing File(清单文件):通常是
.lst,用于调试和查看源码与机器码的对照。

-
Link Library(链接库):比如
Irvine32.lib,它包含标准函数的实现(如DumpRegs等)。 -
Linker(链接器) 把 Object File 和库文件合并,生成:
- Executable File(可执行文件):例如
.exe,这个是可以运行的程序。 - Map File(映射文件):用于调试,显示每个变量和函数在内存中的地址。

- Executable File(可执行文件):例如
-
OS Loader(操作系统加载器):加载可执行文件(.exe)到内存中,并运行。
Windows的特色
小端模式(小端字节序)Little Endian Order:
这是windows采用的数据存储方式,与linux采用的大端模式是有很大区别的
对于一个大于一个BYTE的数据类型会被拆分后按照倒过来的逻辑进行存储

有限状态机模型——Finite-State Machines
是一种基于某些输入改变状态的图结构,也叫状态转换图
状态机有3要素:状态、事件与响应
- 状态:系统处在什么状态?
- 事件:发生了什么事?
- 响应:此状态下发生了这样的事,系统要如何处理?
状态机编程主要有 3 种方法:switch-case 法、表格驱动法、函数指针法
用图形进行表述FSM的话:

有限状态机很容易进行追踪修改,由于其状态隔离的特性很容易使用汇编进行实现,所以大多数的游戏都是使用这种方式来进行架构的,而同样的很多游戏的性能优化需要到汇编层次中进行
移位运算
主要分两类,一种是逻辑移位运算一种是算术移位运算,Logical vs Arithmetic Shifts

这个是逻辑移位运算,特点就是直接进行移动,将新出现的都填0

这个是算术移位,特点是根据最高位原始的数据进行填充,上面这个本来是1开头的负数,所以移位后会填充为1,而且右移左移会将移出去的位置填写到CF标志位上去,所以有关移位运算循环的判断标准往往都是根据CF来的
移位运算的应用场景:
-
有的时候由于寄存器中的某些bit都有对应的含义,所以我们 再读取出对应bit上的信息,但是由于字节序和内存的结构都是基于字节的,所以读取某一个bit是困难的,需要进行移位拼接或者消除某些内容,比如这样一种编码1234,3456表示一个数123456,但是把他保存在两个字节中,这个时候我们取出来进行解析的时候就要将3456左移了,然后再和1234进行拼接

-
实现二进制乘法的底层运算方法
因为左移n位就是乘以2的n次方,这里的左移就是因为上面的123对应的数乘以下面36中有0的位都是0,所以仅仅乘以1的才有效,所以使用移位运算

标记位
是ALU中固有的一系列的标记位数常用来进行条件语句循环跳转等更高级功能实现
Zero flag:检测des上面的是不是0(ZF)
//这里进行sub后cx=0,然后同时ZF标志会被设置为1
mov cx,1
sub cx,1
//等于0的时候会被设置为1,反之默认为0
Sign flag:检测des上面的是不是负数(SF)
mov cx,0
sub cx,1
//cx是-1,这个时候的SF=1
add cx,2
//这里的cx是1,SF又被回归到0
//为负数被设置为1,否则是0
Carry flag:检测无符号的运算是不是越界了(CF)
CF仅仅在两个数相加出现比两个数都小,两个数相减出现比两个数都大的情况下被设置
mov al,0FFh
add al,1
//这个时候al里面按照计算结果应该是0100h,但是al只能存8bit,所以最后只剩下00h,这个时候就会触发CF=1的操作,这里是add后出现更小的数,所以才会设置的
mov al,0
sub al,1
//这个时候AL=0FFh,所以会设置CF=1,也是出现了比原来的al还大的值
//最重要的一点是这里的所有都是对应无符号数的,有符号数的不正常结果是设置OF
Overflow flag:检测有符号的运算是不是越界了(OF)
OF仅仅在正数相加出负数,负数相加出现正数的情况下进行设置
mov al,+127
add al,1
//这里OF=1,因为是127+1=128,但是128是超出了对应可以表示的最大数
mov al 7Fh
add al,1
//7Fh-1应该是等于80h,也会设置OF=1
mov指令不会影响这些,但是涉及运算的指令就会影响这些了
所有的CPU对运算操作是不分符号的,但是用户根据flag给tm指定了不同的意思,因此最基本的ALU仅仅只需要一个ADDER,但是剩下的其他功能都是需要
有关链接Link和库Lib
当然在asm汇编中也是有库文件的,如何构建一个library呢?
- 开始于一系列的ASM源代码
- 接下来将其进行编译成OBJ目标文件
- 然后创建一个空的库文件(用.LIB结尾的文件)
- 最后将这些OBJ文件加入到.LIB中,采用对应的工具(比如Microsoft LIB utility)
在这门课程中主要使用的就是Irvine32.lib文件

这里的两个文件,kernel32.lib是微软的32位软件开发工具的一部分——SDK(Software Development Kit)
这里最后会有一个ddl(动态链接库)
Irvine32.lib
如何导入一个库,这里用Irvine32库来做示例
INCLUDE Irvine32.inc//inc是什么意思?**包含文件(include file)**
.code
mov eax,1234h
call WriteHex //这个函数是用来展示一个hex的数字
call Crlf//这里是创建一个新的空行
以下是你提供的函数列表及其中文解释,以表格形式整理,便于查阅:
| 函数名 | 功能说明(中文) |
|---|---|
CloseFile | 关闭一个已打开的磁盘文件。 |
Clrscr | 清除控制台屏幕,并将光标定位到左上角。 |
CreateOutputFile | 创建一个新的磁盘文件,以输出模式写入。 |
Crlf | 向标准输出写入换行符(回车+换行)。 |
Delay | 暂停程序执行指定毫秒数。 |
DumpMem | 以十六进制格式将内存块写到标准输出。 |
DumpRegs | 显示通用寄存器和标志寄存器的值(十六进制)。 |
GetCommandtail | 将命令行参数复制到一个字节数组中。 |
GetDateTime | 从系统获取当前日期和时间。 |
GetMaxXY | 获取控制台窗口缓冲区的列数和行数。 |
GetMseconds | 返回自午夜以来经过的毫秒数。 |
GetTextColor | 返回当前控制台前景和背景文字颜色。 |
Gotoxy | 将光标定位到控制台指定的行和列。 |
IsDigit | 如果 AL 中是 ASCII 码表示的数字(0–9),则设置零标志位。 |
MsgBox, MsgBoxAsk | 显示弹出式消息框(或确认框)。 |
OpenInputFile | 打开一个已存在的文件用于输入。 |
ParseDecimal32 | 将无符号十进制字符串转换为二进制整数。 |
ParseInteger32 | 将有符号十进制字符串转换为二进制整数。 |
Random32 | 生成一个 32 位伪随机整数(范围为 0 到 FFFFFFFFh)。 |
Randomize | 初始化随机数生成器的种子。 |
RandomRange | 生成一个指定范围内的伪随机整数。 |
ReadChar | 从标准输入读取一个字符。 |
ReadDec | 从键盘读取一个 32 位无符号十进制整数。 |
ReadFromFile | 从磁盘文件读取内容到缓冲区。 |
ReadHex | 从键盘读取一个 32 位十六进制整数。 |
ReadInt | 从键盘读取一个 32 位有符号十进制整数。 |
ReadKey | 从键盘输入缓冲区读取一个字符。 |
ReadString | 从标准输入读取字符串,直到按下 [Enter]。 |
SetTextColor | 设置控制台后续文字输出的前景和背景颜色。 |
Str_compare | 比较两个字符串是否相等。 |
Str_copy | 将源字符串复制到目标字符串中。 |
Str_length | 返回字符串的长度,存于 EAX 寄存器中。 |
Str_trim | 移除字符串中不需要的字符(如空格、换行等)。 |
Str_ucase | 将字符串转换为全大写字母。 |
WaitMsg | 显示一条信息,并等待用户按下 [Enter]。 |
WriteBin | 以 ASCII 二进制格式写出一个无符号 32 位整数。 |
WriteBinB | 以字节、字或双字格式写出二进制整数。 |
WriteChar | 向标准输出写出一个字符。 |
WriteDec | 以十进制格式写出一个无符号 32 位整数。 |
WriteHex | 以十六进制格式写出一个无符号 32 位整数。 |
WriteHexB | 以字节、字或双字格式写出一个十六进制整数。 |
WriteInt | 以十进制格式写出一个有符号 32 位整数。 |
WriteStackFrame | 将当前过程的栈帧信息写入控制台。 |
WriteStackFrameName | 将当前过程的名称和栈帧信息写入控制台。 |
WriteString | 向控制台窗口输出一个以 null 结尾的字符串。 |
WriteToFile | 将缓冲区的内容写入输出文件。 |
WriteWindowsMsg | 显示由 Windows 系统生成的最近一条错误信息。 |

程序实例1——使用DumpReg输出寄存器信息
INCLUDE Irvine32.inc
.code
main PROC
call Clrscr//实现清楚屏幕输出的功能
mov eax,500
call Delay//根据eax的值来进行延迟,这里延迟的是500ms
call DumpRegs//这个函数是用来按照标准格式输出Reg的数据的
main ENDP
end main
//对应的输出
**EAX=000001F4 EBX=0099E000 ECX=00E510AA EDX=00E510AA
ESI=00E510AA EDI=00E510AA EBP=00B3FD30 ESP=00B3FD24
EIP=00E53674 EFL=00000283 CF=1 SF=1 ZF=0 OF=0 AF=0 PF=0**
程序实例2——用WriteString打印字符串函数的使用
include Irvine32.inc
.data
str1 BYTE "Assembly language is easy!",0
.code
main PROC
mov edx,OFFSET str1
call WriteString//调用 WriteString 打印字符串
call Crlf//打印换行符 (Carriage Return Line Feed)
main ENDP
end main
![]()
程序实例3——WriteBin类输出进制函数的使用
include Irvine32.inc
IntVal = 35//这个叫宏,它实际上不是一个变量,所以不能在data段,他实际上是一个立即数
main PROC
.code
mov eax,IntVal//传入的是立即数
call WriteBin
call Crlf
call WriteDec
call crlf
call WriteHex
call crlf
main ENDP
end main
0000 0000 0000 0000 0000 0000 0010 0011
35
00000023
程序实例4——ReadString读入函数的使用
include irvine32.inc
.data
filename BYTE 80 DUP(0)
.code
//main是在.code 后面的呀,所有的函数都是在.code 后面end main前面的
main PROC
mov edx,OFFSET filename//传入一个地址到edx中
mov ecx,SIZEOF filename//传入最大输入长度
call ReadString//这个函数是用来进行输入的等效于C中的scanf类似,这里传入的参数也是有一个地址,但是还多了一个最大的输入长度的参数
//ReadString 函数会读取用户输入并将其存储到 filename 数组中,最多读取 80 个字符。
main ENDP
end main
无输出,但是用户是可以进行输入的,但是最多只能输入80个字符,用回车键进行输入结束的确定,和C是很类似的
程序实例5——RandomRange函数的使用
include irvine32.inc
.code
main PROC
mov ecx,10
L1:
mov eax,100
call RandomRange//这里将100作为上界开始进行随机数的生成,生成的是0~99的随机数,循环了10次进行输出
call WriteInt//输出函数
call Crlf
loop L1
main ENDP
end main
+94
+2
+67
+57
+7
+40
+58
+48
+73
+94
程序实例6——控制台相关设置(非考点)
include Irvine32.inc
.data
str1 BYTE "Color output is easy!",0
.code
main PROC
mov eax,yellow + (blue * 16)
call SetTextColor
main ENDP
end main
//可以参考GPT的版本
INCLUDE Irvine32.inc ; 引入 Irvine32.inc 库
.data
str1 BYTE "Color output is easy!", 0
yellow DWORD 14 ; 定义黄色的控制台颜色代码(14)
blue DWORD 1 ; 定义蓝色的控制台颜色代码(1)
.code
main PROC
; 计算黄色 + 蓝色 * 16,并将结果传给 SetTextColor
mov eax, [yellow] ; 将黄色的值加载到 EAX
mov ebx, [blue] ; 将蓝色的值加载到 EBX
imul ebx, ebx, 16 ; 计算蓝色 * 16
add eax, ebx ; 将黄色 + 蓝色 * 16 加到 EAX
call SetTextColor ; 调用 SetTextColor 设置文本颜色
; 输出字符串
mov edx, OFFSET str1 ; 加载字符串地址到 EDX
call WriteString ; 输出字符串
main ENDP
end main

在控制台应用中,颜色常常是通过一个 16 进制代码来指定的,常见的颜色常量如下(这些常量在不同的环境中可能会有所不同):
0: 黑色
1: 蓝色
2: 绿色
3: 天蓝色
4: 红色
5: 紫色
6: 黄绿色
7: 白色
8: 灰色
9: 深蓝色
10: 浅绿色
11: 浅天蓝色
12: 浅红色
13: 浅紫色
14: 黄色
15: 亮白色
条件与分支
补充:
关于跳转386和x86会有不同的地方,.386仅仅允许跳转的是当前位置的基础上(-128~+127位置),但是后面的x86会出现允许在内存中任意的进行跳转,这是很大的进步
首先从最基础的CPU标记开始:
- ZF:当ALU结果是等于0的时候设置为1
- CF:当出现的结果是一个特别大(小)的数的时候设置为1
- SF:出现负号结果的时候设置为1
- OF:当负负得正,正正得负的时候设置为1,出现不合理的符号的时候
- PF:用来进行最近一次计算的结果的奇偶判别,就是判断最小的一位bit的0还是1,如果是1表示就是奇数,反之就是偶数了
- AF:
- AC(Auxiliary Carry Flag) 是处理器的一个标志位,用来指示在 二进制加法 或 减法 操作中,低 4 位的进位或借位。
- AC 是 仅在某些特定的加法/减法指令中设置的,它与 8 位的进位无关,而专门关注 4 位的进位。
- AC 常用于 BCD(Binary-Coded Decimal,二进制编码十进制) 运算中,因为 BCD 运算的补偿需要知道低 4 位的进位或借位
- 这些flag都是在条件判断中极其关键的元素
逻辑运算
| 汇编对应指令 | 在C语言中的符号 | 描述 | 运算规则 |
|---|---|---|---|
| AND | & | 与 | 两个位都为1时,结果才为1 |
| OR | 或 | ||
| XOR | ^ | 异或 | 两个位相同为0,相异为1 |
| NOT | ~ | 取反 | 0变1,1变0 |
| SHL | << | 左移 | 各二进位全部左移若干位,高位丢弃,低位补0 |
| SHR | >> | 右移 | 各二进位全部右移若干位,高位补0或符号位补齐 |
具体规则和运算可以参考外面计算机组成原理部分的数电知识和C语言的按位计算知识
有关逻辑运算符的应用:
-
进行小写和大写的转换,由于大小写之间相隔32(ASCII)
include Irvine32.inc .code main PROC mov al,'a' and al,11011111b call DumpRegs main ENDP end main -
这段代码的任务是开启键盘的 CapsLock 键,通过操作 BIOS 数据区域中的键盘标志字节来实现。下面是代码的解释:
- 打开键盘的 CapsLock 键,使用
OR指令设置 BIOS 数据区中0040:0017地址处键盘标志字节的第 6 位(即 CapsLock 状
mov ax, 40h ; 将 40h 加载到 AX 寄存器。40h 是 BIOS 数据段的地址,存储键盘的状态标志 mov ds, ax ; 将 BIOS 数据段加载到 DS,将 AX 寄存器中的值(40h)加载到 DS 寄存器,从而设置数据段为 BIOS 数据段 mov bx, 17h ; 键盘标志字节偏移量 or BYTE PTR [bx], 01000000b ; 设置第 6 位(CapsLock 打开),执行位运算 OR,将二进制 01000000b(即设置第 6 位)与 0040:0017 地址处的字节进行或运算。通过 OR 操作,确保第 6 位被设置为 1(开启 CapsLock),不管之前该位的值是什么。- 这段代码 仅在实模式下有效,并且 在 Windows NT、2000 或 XP 下无法使用。它适用于较早版本的 Windows(如 DOS)或实模式环境。
- 打开键盘的 CapsLock 键,使用
条件跳转
基于状态寄存器的跳转指令:

mov al, 5 ; AL = 00000101
shr al, 1 ; AL >> 1 = 00000010,移出的是1(LSB = 1),CF = 1
jnc L1 ; 因为 CF = 1,所以不跳转
//这里解释一下CF的作用,还是在移位运算将他理解为移位出去的那个数比较好理解
mov al, 4 ; AL = 00000100
shr al, 1 ; AL >> 1 = 00000010,移出的是0(LSB = 0),CF = 0
jnc L1 ; 因为 CF = 0,跳转到 L1
这一部分是Flag跳转,O表示overflow,P表示Parity(偶数),S表示signed是负数,C表示Carry进位
关于JC和JO的一些辩论
CF(进位/借位标志):
- 用途:反映 无符号(unsigned)运算的进位(加法)或借位(减法)情况。
- 何时置位:
- 加法(
ADD、ADC):如果最高位向更高位“进位”了,就将 CF 置 1。 - 减法(
SUB、SBB):如果被减数小于减数,需要“借位”才能完成运算,就将 CF 置 1。
- 加法(
- 意义:CF=1 表示运算结果超出了目标操作数的无符号表示范围(比如在 8 位无符号数中,加法结果 ≥256;减法结果 <0)。
示例(8 位无符号加法)
0xF0 + 0x20 = 0x10,且有一次进位(0xF0 + 0x20 = 0x110)
- 结果 AL = 0x10
- CF = 1
OF(溢出标志):
- 用途:反映 有符号(signed,两补码)运算是否发生溢出。
- 何时置位:当加减结果超出了有符号表示范围时(最高位符号位发生了错误进位)。
- 意义:OF=1 表示以有符号方式解释时,结果不再正确。
示例(8 位有符号加法)
0x7F (+127) + 0x01 (+1) = 0x80,按两补码解释 0x80 = –128
- 正确数学结果应该是 +128,但 +128 超出了 [-128, +127] 范围
- AL = 0x80
- CF = 0(无无符号进位,0x7F+0x01=0x80<0x100)
- OF = 1
基于等于的跳转指令:

基于不等号进行判断的跳转指令:(无符号操作数的比较)

基于不等号进行判断的跳转指令:(有符号操作数的比较)

这里跳转指令的J的意思都是Jump,G表示Greater,L表示Less,由于是有符号的所以没有上下之分,只有大小,所以是greater和less,A表示above,B表示below,E表示equal相等,有上下之分的原因是因为是无符号整数,N表示是not非,比较特殊的点是CX连在一起,表示对ECX和CX这两个特殊寄存器的判断(这里面涉及到了循环的条件判断,因为loop是没有条件的循环,这里相当是有条件的循环)。
条件循环
LOOPZ&LOOPE指令
与LOOP指令是类似的进行ECX=ECX-1的操作,但是关于跳转还需要ZF=1的条件
LOOPNZ&LOOPNE指令
与LOOP指令是类似的进行ECX=ECX-1的操作,但是关于跳转还需要ZF=0的条件,和上面的指令是相对的,差不多就是在loop进入下一个循环前会进行一个跳转的判断
.data
array SWORD -3,-6,-1,-10,10,30,40,4
sentinel SWORD 0
.code
mov esi,OFFSET array
mov ecx,LENGTHOF array
next:
test WORD PTR [esi],8000h
//进行检测esi中,这行代码测试当前 ESI 寄存器指向的数组元素的最高位(符号位)。在带符号数的 16 位数中,符号位是第 15 位,即 0x8000
//test 指令与操作数进行按位与运算,并设置标志寄存器,但不会修改操作数
//如果符号位为 1(即负数),那么 test 的结果会清除零标志 ZF;如果符号位为 0(即非负数),则零标志 ZF 会被设置为 1
pushfd
//存标志寄存器
add esi,TYPE array//移动指针到下一个,就像c中的a[i],然后i++一样
//这个地方本来会对EFlag有影响的,但是由于前面有pushfd,所以在后弦的loopnz判断的时候是用前面的test的flag来进行判断的
popfd
//输出标志寄存器内容
loopnz next
//仅仅当loop中出现0才会结束循环,由于这里上面的test中有对ZF的设置,所以就是说仅仅当出现
jnz quit
//循环完毕后如果还是没有等于的,那就进行quit
sub esi,TYPE array
//假设程序在数组中找到一个符号位为 1(即负数)的元素时,add esi, TYPE array 会让 ESI 指向下一个元素,即跳过了当前元素。如果此时程序进入了 quit 标签(即没有找到符合条件的元素)
//就需要执行 sub esi, TYPE array,让 ESI 回到当前元素的位置,以便程序能够根据需要做后续操作。
//换句话说,sub esi, TYPE array 是用来纠正 ESI 的位置,使得它再次指向最后一个测试的元素。这样就保证了程序在退出循环时,ESI 正确地指向当前元素,而不会错过它。
quit:
(break)//这里是伪代码,后面的实际上是有内容的但是没有写,所以用括号写一下表示后续的功能和break有关
条件结构
基本的条件语句
if(op1 != op2)
X = 1;
else
X = 2;
这里展示的是C代码和汇编的转换,这里要注意的点就是这个L2接后续代码的设计
mov eax,op1
cmp eax,op2
jne L1
mov X,1
jmp L2//这个地方用来衔接后续的代码
L1: mov X,2
L2:
//后续的代码
if(ebx <= ecx){
eax = 5;
edx = 6;
}
因为只有一个分支所以只会出现一个跳转语句,但是在if这个地方一定会出现一个cmp的,这是基本,而且要保证他的对应的Reg不会出现问题(如果结果入栈了要在跳转前及时出栈的)
cmp ebx,ecx
ja next
mov eax,5
mov edx,6
next:
多个条件的语句
对应的目标就是C中的if(a == b&& c >1)这种语句
//对应的伪代码是
if(al > bl) AND (bl > cl)X = 1;
//下面是汇编:
cmp al,bl//先比较前面的
ja L1//不符合就会跳到next,就直接退出判断了,所以C语言中为什么一个出现了不符合就不会继续执行后面的代码的原因就是这个
jmp next
L1:
cmp bl,cl//进行后面的比较
ja L2
jmp next//next对应的就是两个都不符合的情况,也就是不执行这个代码块中的内容直接执行后续代码
L2:
mov X,1
next:
//这里就是后续代码咯
//上面这种写法的标签还是太多了,有更好的设计,这个就是进行了一些等价变形,但是会简单很多
cmp al,bl
jbe next//当al <= bl退出
cmp bl,cl
jbe next//当bl <= cl退出
//这样判断否虽然不是那么直观,但是代码好看很多
mov X,1
next:
//当然一般都是采取下面这一种写法!!!
//这里展示OR
//对应的伪代码是
if(al > bl) OR (bl > cl)X = 1;
//转成汇编程序
cmp al,bl
ja L1//判断成功直接执行对应的语句
cmp bl,cl
jbe next//第二次还是失败才会进入到后续的代码段
L1:
mov X,1
next:
While条件循环
//本质上前面的LOOP指令就是JMP指令的一种,所以剩
while(eax < ebx)eax = eax +1;
//这个转换成汇编就是
top:
cmp eax,ebx
jae next
inc eax
jmp top//这个就是loop,有条件的loop没有专属的指令所以就直接用这个了
//这也是CISC的缺点,很多指令都是对几个基础指令的包装而已,是可以进行等价替换的,但是这样也会导致学习成本的提高(虽然会更加容易理解)
next:
表驱动Table-Driven Selection
类似C语言中的switch case语句,使用一个表来进行查阅从而来代替多路选择结构
- 创建一个表抱恨一系列进行查阅的值,和相关函数过程(用偏移值来进行机器码译码,标签是表示方式)
- 使用循环来进行表的搜索,所以需要循环的ecx基础步骤
- 适合很多数的比较操作
1.创建一个搜索表
.data
CaseTable BYTE 'A'
DWORD Process_A
EntrySize = ($ - CaseTable)//这里表示的是A这个过程结束之后到开始表的偏移量,对应一个case的大小
BYTE 'B'
DWORD Process_B
BYTE 'C'
DWORD Process_C
BYTE 'D'
DWORD Process_D
NumberOfENtries =($ - CaseTable)/EntrySize//对应整个表的每一小段的数量

2.进行遍历搜索
mov ebx,OFFSET CaseTable //把表的起始位置传入寄存器中
mov ecx,NumberOfEntries//总的段数
L1:cmp al,[ebx]//进行比较,然后进行跳转还是继续的选择
jne L2//没有匹配上继续
call NEAR PTR [ebx + 1]//开始移动指针
//NEAR 表示这是一个近调用(near call) ,即调用的目标地址在当前代码段内 (不切换段)。
//匹配玩之后根据数据段我们知道一个BYTE后就是对应DWORD的位置了,所以只用加一个1就可以了
call WriteString
call Crlf
//调用展示的模块
jmp L3
L2:add ebx,EntrySize
//到下一个段中去进行比较
loop L1
L3:
特殊的IF语段——条件跳转的简化表示
在汇编中有一些特殊的Tag用来进行更简单的表示
.IF,.ELSE,.ELSEIF,.ENDIF,这四个就是用来进行相关的表示的,但是实际上还是像上面一样的进行跳转的,不过省略了相关的实际汇编代码,其实MASM会进行自动的展开的
.IF eax > ebx && eax >ecx
mov edx,1
.ELSE
mov edx,2
.ENDIF

.data
vall DWORD 5
result DWORD ?
.code
mov eax,6
.IF eax > vall
mov result,1
.ENDIF
mov eax,6
cmp eax,vall
jbe @C001
mov result,1
@C001:
会进行左边到右边的转换,这个是MASM自动的,不受人为控制的,这里的jbe和jle和上面的类型是DWORD还是SDWORD有关,也是汇编器自动进行的
特殊的REPEAT语段——loop的简化表示
使用的相关的Tag是.REPEAT,.UNTIL XXXX
mov eax,0
.REPEAT
//这里是开始,repeat的中文就是重复(循环的意思)
inc eax
call WriteDec
call Crlf
.UNTIL eax == 10
//这是一个用来显示1~10的所有数的程序,使用这个不会再自动认定ecx是计数器了,可以根据UNTIL来进行自己的设置
特殊的WHILE语段——while循环的简化表示
使用配套.WHILE,.ENDW两个Tag,这种很类似shell编程中的封装方式
mov eax,0
.WHILE eax < 10
inc eax
call WriteDec
call Crlf
.ENDW
一些特殊内容(在64位中不会进行继承的一些指令,主要是拓展见识)
ADC指令
全称是 Add with Carry ,中文意思是 带进位加法
ADC 用于执行两个数的加法,并且加上 进位标志(CF) 的值。它常用于大整数加法、多精度计算等场景。
因为 CPU 的寄存器和内存操作通常是有限位宽的(比如 8 位、16 位、32 位、64 位),如果你要处理比这更大的数(例如 128 位整数),就需要分段加法,并使用 ADC 来处理每一步之间的进位。
destination = destination + source + CF(仅仅有有进位和没有进位两种状态)
这个就是java和C中某些库实现超大数加法的关键机器码内容
High: EAX = 0x12345678
Low : EDX = 0x9ABCDEF0
加上:
High: EBX = 0x87654321
Low : ECX = 0x0FEDCBA2
//
; 先加低 32 位
mov eax, 0x9ABCDEF0
mov edx, 0x12345678 ; 假设这是高 32 位
mov ebx, 0x0FEDCBA2
mov ecx, 0x87654321 ; 另一个数的高 32 位
; 先加低 32 位
add eax, ebx ; 加低 32 位
adc edx, ecx ; 加高 32 位,并加上可能的进位
SBB指令
全称是 Subtract with Borrow ,中文意思是 带借位减法
SBB 用于执行两个数的减法,并且减去进位标志(CF)的值 。它常用于大整数减法、多精度计算等场景。
destination = destination - source - CF
CF(Carry Flag)是 EFLAGS 寄存器中的一个标志位。- 在减法中,CF 表示是否有来自上一次运算的“借位”。
- 如果有借位(CF=1),则在减法时再减去 1;如果没有借位(CF=0),就只做普通减法。
这个就是java和C中某些库实现超大数减法的关键机器码内容
被减数:EAX = 0x9ABCDEF0 (低32位)
EDX = 0x12345678 (高32位)
减数: EBX = 0x0FEDCBA2 (低32位)
ECX = 0x87654321 (高32位)
//
; 先减去低 32 位
sub eax, ebx ; 低 32 位相减,可能会产生借位(CF 设置为 1 或 0)
; 再减去高 32 位,并考虑是否发生借位
sbb edx, ecx ; 高 32 位相减,如果有借位,则BCD再减 1
BCD指令
BCD(Binary-Coded Decimal) 是一种用二进制表示十进制数字的方法,有两种区别,一个是压缩BCD一种的不压缩BCD
| 十进制 | BCD 表示 |
|---|---|
| 0 | 0000 |
| 1 | 0001 |
| ... | ... |
| 9 | 1001 |
超过 1001 的值(如 1010 到 1111)不是合法的 BCD 编码。
不压缩 BCD(Unpacked BCD):
- 每个字节只存储一个十进制数字。
- 高 4 位通常为 0,低 4 位是有效的 BCD 值。
| 特点 | 描述 |
|---|---|
| ✅ 易于操作 | 每个字节只存一个数字,容易进行加减乘除调整 |
| ❌ 空间利用率低 | 一个字节只能存一个数字,浪费了一半的空间 |
| ✅ 用于 x86 BCD 指令 | 如 AAA, AAS, AAM, AAD 等指令处理的就是这种格式 |
| 特性 | Unpacked BCD | Packed BCD |
|---|---|---|
| 每个字节表示 | 一位十进制数字 | 两位十进制数字 |
| 格式 | 高 4 位为 0,低 4 位有效 | 高 4 位和低 4 位都有效 |
| 示例 | 0x04 表示 4,0x09 表示 9 | 0x45 表示 45,0x90 表示 90 |
| 使用指令 | AAA, AAS, AAM, AAD | DAA, DAS |
| 存储效率 | 较低 | 更高 |
| 处理复杂度 | 简单 | 稍微复杂一些 |
十进制数 45 可以表示为:
0100 0101(压缩 BCD,Packed BCD),即一个字节存储两个十进制数字。- 或者使用非压缩 BCD(Unpacked BCD),每个字节只存一位数字。
| 指令 | 全称 | 功能说明 |
|---|---|---|
| AAA | ASCII Adjust After Addition | 调整 AL 中的加法结果为 Unpacked BCD |
| AAS | ASCII Adjust After Subtraction | 调整 AL 中的减法结果为 Unpacked BCD |
| AAM | ASCII Adjust After Multiply | 调整 AX 中的乘法结果为 Unpacked BCD |
| AAD | ASCII Adjust Before Division | 将 AX 中的 Unpacked BCD 转换为二进制再除法前调整 |
| DAA | Decimal Adjust After Addition | 调整 AL 中的加法结果为 Packed BCD |
| DAS | Decimal Adjust After Subtraction | 调整 AL 中的减法结果为 Packed BCD |
这里的A表示的要么是ASCII,要么是Adjust变化,或者Addition加法结果
S表示Subtraction,M表示是乘法,D表示除法(仅仅在末尾的时候才是除法的意思),或者十进制(Decimal)

1323

被折叠的 条评论
为什么被折叠?



