汇编
前言
汇编是个干黑客的好东西,可惜我已经没时间好好学了😭,本文以8086典中典cpu,MASM和王爽经典教材进行学习。
环境配置
古老的是通过dosbox,然后从网上找到古老的并非自带的MASM的工具,然后挂载,通过外部编辑器来写代码,dosbox只负责编译和运行。(但是dosbox明明是用来玩dos游戏的)
现在也可以使用vscode配置对应的环境,但是我还不会。TODO
引入
8086是16位处理器,一共只有14个寄存器虽然还是记不住,但是比64位的那么多寄存器好记多了。
字(word)代表着处理器一次性处理数据的能力,在16位中 1个字=2个字节(Byte)=16位(Bit), 而在常见的64位中 1个字 = 8个字节 = 64位
通用寄存器
8086提供四个16位通用寄存器:AX、BX、CX 和 DX。每个寄存器可拆分为两个8位部分:高字节(H)和低字节(L),例如 AX = AH + AL(其中+表示拼接)。 8086采用小端(little-endian)字节序:当将16位值存入内存时,低字节(如 AL)存储在较低的内存地址,高字节(如 AH)存储在相邻的较高地址。 但是说是通用其实还是有一定特定的用处的(约定俗成那种)只不过是还可以随便用,详见英文名字。
- AX = AH + AL :Accumulator
- BX = BH + BL:Base
- CX = CH + CL:Count
- DX = DH + DL:Data
简单的指令
在汇编中,预留关键词不区分大小写,但是用户自定义的是区分的。
例题:四条指令 计算2的4次方
寻址
不巧8086的地址总线是20位,cpu却只有16位,那么怎么办呢。当我们学这个的时候有一种先射箭后画靶的感觉,当初设计师肯定是先设计的寻址再让地址总线是20位。而20位支持1MiB的内存大小,当年还是很🐮的。 至于为什么不直接把地址总线设置成32位,那鬼才知道他脑袋一热怎么想的。
以上是废话,我们只需要知道在8086中,地址 = 段地址 x 16 + 偏移地址,段地址 x 16 又称之为基础地址。(注意,如果逆向过植物大战僵尸的修改阳光,肯定会遇到过基址这个东西,但是其实并不是一个东西,现代的基址是用来内存保护的。)
寄存器(寻址)
使用CS和IP,作为上面提到的段地址和偏移地址。即 地址 = CS x 16 + IP。(由于IP是16位,所以我们可知一个地址有很多种表示方式)
至于怎么修改这个这两个的值呢,不巧还真不能用mov,而是使用jmp CS:IP来修改,想要只修改IP,那就jmp 地址,至于只修改CS正常人类好像是没这个需求。
小于等于64KiB的一组代码可以视作一个代码段,如123B0H~123B9H,可以视作段地址位123BH的一个代码段,我们通过jmp 123BH:0就是可以实现开始执行这一堆,注意如果一个指令执行完了,IP会自动跳转到下一条指令的位置(相当于IP ← IP + 当前指令长度),但是指令既可以是2个字节,也可以是三个3字节
寄存器(内存访问)
对于两个连续的内存字节,我们既可以视作两个独立的字节,也可以视作一个字,但是要注意由于是小端,所以如果内存是12H 34H,对应的字其实是3412H。
想要内存访问,也是需要通过16位的CPU来访问20位的地址总线,不过此时我们用DS寄存器来表示段地址,再加[偏移],来表示。
注意:ds不能直接用数字赋值,必须通过寄存器来间接赋值,就是这么设计的。
类似于之前的代码段,我们称从ds开始这64KiB位为数据段
补充:
MASM的逆天设计,导致上面的mov ax, [10H]其实是错误的,在debug中该语句时按照预期执行的,但是在MASM中实际上它是ax = 10H。(早期简单汇编器(如 Intel ASM)中,[100h] 本就是 “带括号的立即数” 写法(括号仅为视觉区分,无内存含义),MASM 作为商用汇编器,必须兼容这种行业惯例,否则开发者迁移成本极高。???)
如果想要实现这个功能
- 使用寄存器中转
- 显式加上段前缀
内存访问补充(段前缀)
在指令前,你可以使用以下四个段前缀之一来覆盖默认的段寄存器:
CS:(代码段前缀)DS:(数据段前缀)ES:(附加段前缀)SS:(堆栈段前缀)
我们先暂时了解:在 8086/8085 实模式下,内存地址 0:0200h到 0:02FFh(对应的物理地址是 00200h到 002FFh)这段 256 字节的空间,通常被 DOS 和其他合法程序刻意避开,主要是因为它是 BIOS 数据区 的重要组成部分,用于存储系统关键的运行时数据。
寄存器(栈)
在栈中,数据的操作单元是字,同样的小端存储。通过SS:SP来确定哪个地址是栈顶,注意在8086中是没有栈底这个概念的,处理器不会进行是否越界的判断,你需要自己保证SP不会超出SS段边界。
需要记住并理解的一件事情是8086中栈是从高地址到低地址的,所以初始化的时候SP应当为FFFEH(因为这个段最大是FFFFH,但是由于一次是移动两个字节,所以说要凑成偶数地址从而字对齐)(注意dosbox中经测试,sp在自动初始化的时候为0)
上面是等价写法,助于我们理解其实际操作过程,和ds不一样,ss和sp是可以直接赋值的。(备注mov ax, [sp] 在汇编中默认是 DS:[SP],这是常见陷阱。必须显式写 ss:[sp] 或依赖 pop/push 自动用 SS。)
然后需要注意的一点,当你的SP为0000H的时候,你再次压入数据,会变成FFFEH,但是这不是循环栈或者地址回绕,这只是单纯的硬件无符号加减法导致的溢出,导致的环形相接。
程序
很不幸,学汇编的时候并不能很轻松的写出第一个hello,world。
但是我们要先背诵记住这个框架
|
|
或者使用伪指令.STACK [大小]来定义栈大小
|
|
在 8086 + MASM 的 EXE 程序中,使用 .STACK 时不需要手动初始化 SS 和 SP
伪指令
汇编语言中的指令分为伪指令和汇编指令,伪指令的作用和Java中的注解类似,并非是直接用来编译的,但是编译器可以根据其做出一些处理,而汇编指令则被编译成机器码。
对于不同的编译器伪指令也是不同的,我们只学习MASM的。
- 段
段是用来存储数据、代码或者当成栈的。一个汇编语言至少有一个代码段。
- 结束
end写所有的最后,表示程序的结束,注意和ends的区分
- assume
关联,将自己定义的段和系统的寄存器关联
在8086中,MASM 汇编器需要明确知道自定义的变量或者段应该用那哪个寄存器访问他,没有智能推断
返回
就像C语言中的return 0;一样(古老的标准中也是需要显式返回的,虽然现在可以不写),汇编需要对程序进行显式返回。
这个先记下来,可以先知道这是调用中断,至于中断如果接触过单片机的话会有所了解。
LOOP循环
其格式为
当走到loop这一行时,先执行cx–,然后再判断cx是否为0,如果不为0,再跳转到标号。用起来像C语言当中的goto一样。
我们以计算3*16为例
跳转
在汇编中,我们相当于使用goto来实现一切的if语句种种,没想到啊,判断语句比循环语句还复杂。
8086 的条件跳转指令(Jcc)依赖 CPU 标志寄存器 FLAGS 中的 6 个关键标志位:
| 标志位 | 名称 | 含义 |
|---|---|---|
CF |
Carry Flag | 进位/借位(无符号数运算) |
ZF |
Zero Flag | 结果为零 |
SF |
Sign Flag | 结果符号位(最高位),负数则 SF=1 |
OF |
Overflow Flag | 有符号数溢出 |
PF |
Parity Flag | 结果低8位中 1 的个数为偶数则 PF=1 |
AF |
Auxiliary Flag | 辅助进位(BCD 运算用) |
跳转表格如下
| 类型 | 指令 | 条件 | 适用场景 |
|---|---|---|---|
| 无符号大于 | JA |
CF=0, ZF=0 | A > B (unsigned) |
| 无符号小于 | JB |
CF=1 | A < B (unsigned) |
| 有符号大于 | JG |
(SF XOR OF)=0, ZF=0 | A > B (signed) |
| 有符号小于 | JL |
(SF XOR OF)=1 | A < B (signed) |
| 等于 | JE/JZ |
ZF=1 | A == B |
| 不等于 | JNE/JNZ |
ZF=0 | A != B |
| 为负 | JS |
SF=1 | A < 0 |
| 为正 | JNS |
SF=0 | A >= 0 |
| 进位 | JC |
CF=1 | 加法进位/减法借位 |
| 溢出 | JO |
OF=1 | 有符号数溢出 |
| 等于0 | JCXZ |
CX=0 | 循环前检查计数器是否为0,或字符串/数组边界处理 |
程序框架如下
|
|
**注意,汇编中的数字存储本身不分有无符号,只是在读取和运算的时候按照规则看待的,有符号的按照补码的方式存储。**这有助于我们理解溢出
标记位。
函数
| 指令 | 作用 |
|---|---|
PROC |
定义一个过程(子程序),类似高级语言的函数/方法 |
CALL |
调用一个过程(近调用或远调用) |
RET |
从过程返回到调用点(必须与 CALL 成对) |
形如
距离属性:默认为NEAR段内调用,也可为FAR段见调用
调用 CALL 函数名
-
NEAR CALL:PUSH IP -
FAR CALL:PUSH CS和PUSH IP
返回RET
- 从栈顶弹出返回地址到 IP
NEAR RET:POP IPFAR RET:POP IP→POP CS
- 继续执行
CALL之后的指令
代码示例:
无参数,段内调用
|
|
从这个程序要学到以下内容:
- END后面标签指明了程序开始的地方,若无标签则从代码段第一行开始执行
- 保护调用者寄存器,在子程序用到的寄存器需要备份一下,防止在子程序中被更改,进而有可能对主程序造成影响(子程序中 修改了哪些寄存器,就保护哪些)
寄存器传参
这种比较简便和方便,但是传参数目收到限制
栈传参
我们要回忆一下,压栈的时候SP是减去对应的参数大小的。
也可以 RET 4为:stdcall / Pascal / Windows API → 被调用者清理
当然MASM有相应的语法糖
但是要标好语言约定格式
|
|
或者采用STDCALL,连ret几都自动帮你算好(就是proc后面的参数个数),缺点就是无法实现变长参数,但是都已经用参数了其实也无所谓。
|
|
需要注意一下参数的顺序
最后要注意访问值的时候的规则
| 寻址模式 | 默认段寄存器 | 示例 |
|---|---|---|
[BX], [SI], [DI] |
DS |
mov ax, [bx] → DS:BX |
[BP], [BP+偏移] |
SS |
mov ax, [bp+6] → SS:BP+6 |
| 直接寻址 | DS |
mov ax, [1234h] → DS:1234h |
对于如ADD_TWO PROC stdcall a:word, b:word的参数,其实是相当于bp这一种,所以想要修改外部的值的时候,记住通过中间寄存器来修改,否则修改的是栈上的值。
远调用
简单了解一下
|
|
更多的运算
mul
MUL是无符号乘法,它的行为和结果存放位置依赖于操作数的大小(8 位或 16 位)。
MUL 会根据结果设置 CF(进位标志)和 OF(溢出标志):
- 如果结果 超出目标寄存器的大小 → CF = 1,OF = 1
- 如果结果可以完全放入目标寄存器 → CF = 0,OF = 0
div
DIV 是 无符号除法指令
除数是显式提供的操作数,被除数隐含在 AX 或 DX:AX,取决于操作数大小。
| 操作数大小 | 被除数 | 商 | 余数 |
|---|---|---|---|
| 8 位 | AX (16 位) | AL | AH |
| 16 位 | DX:AX (32 位) | AX | DX |
中断
中断(Interrupt) 是一种软硬件机制,用于响应事件(如 I/O 操作、定时器、异常等)。我们在 DOS 或 BIOS 编程中,会用 INT n 来调用系统提供的服务。
DOS系统调用中断
INT 21H
注意是AH,看的是高8位
| 功能类别 | AX 功能号 / 说明 | 说明/用途 |
|---|---|---|
| 字符输出 | AH = 02h | 输出字符到标准输出(DL = 字符) |
| 字符输入 | AH = 01h | 从标准输入读取字符(回显到屏幕)读到AL里面 |
| 直接输入 | AH = 08h | 从标准输入读取字符(不回显) |
| 输出字符串 | AH = 09h | 输出以 $ 结尾的字符串,DX = 地址 |
| 读磁盘文件 | AH = 3Fh | DX = 缓冲区, BX = 文件句柄, CX = 字节数 |
| 写磁盘文件 | AH = 40h | DX = 缓冲区, BX = 文件句柄, CX = 字节数 |
| 打开文件 | AH = 3Dh | DX = 文件名, AL = 0(只读)、1(写) |
| 关闭文件 | AH = 3Eh | BX = 文件句柄 |
| 创建文件 | AH = 3Ch | DX = 文件名, CX = 属性 |
| 程序退出 | AH = 4Ch | AL = 返回码, 退出程序 |
常用到的换行是0Ah,回车是0Dh
还有一个是常用但是比较复杂的,从键盘输入字符串AH = 0Ah
需要先准备一个 输入缓冲区,代码示例如下:
|
|
BIOS中断
用于 硬件底层操作,如显示、键盘、磁盘操作。略
INT 10H - 视频服务
| AH 功能号 | 功能说明 |
|---|---|
| 00h | 设置显示模式 |
| 01h | 设置光标属性 |
| 02h | 设置光标位置 |
| 06h | 向屏幕滚动 |
| 0Eh | TTY 输出字符(带滚动) |
INT 16H - 键盘服务
| AH 功能号 | 功能说明 |
|---|---|
| 00h | 等待键盘按键,返回 ASCII + 扫描码 |
| 01h | 检测按键(非阻塞) |
| 02h | 获取键盘状态 |
IINT 13H - 磁盘服务
| AH 功能号 | 功能说明 |
|---|---|
| 00h | 重置磁盘 |
| 02h | 读扇区 |
| 03h | 写扇区 |
| 08h | 获取磁盘参数 |
寄存器一览
一、通用寄存器(8 个 16 位寄存器)
通用寄存器用于临时存储数据和地址,支持 16 位整体操作或拆分为两个 8 位寄存器独立操作,是编程中最常用的寄存器组。
-
数据寄存器(4 个)
-
AX (Accumulator):累加器,多用于算术运算(加减乘除)、数据传输,是汇编指令的默认操作数寄存器。
- 拆分:
AH(高 8 位)、AL(低 8 位)
- 拆分:
-
BX (Base):基址寄存器,常与段寄存器配合存放内存地址(基址寻址)。
- 拆分:
BH(高 8 位)、BL(低 8 位)
- 拆分:
-
CX (Count):计数寄存器,用于循环(LOOP 指令)、字符串操作的计数器。
- 拆分:
CH(高 8 位)、CL(低 8 位)
- 拆分:
-
DX (Data):数据寄存器,用于存放乘法运算的高位结果、除法运算的余数,或扩展地址总线宽度。
- 拆分:
DH(高 8 位)、DL(低 8 位)
- 拆分:
-
-
指针与变址寄存器(4 个)这类寄存器不能拆分为 8 位,专门用于内存寻址,存放偏移地址。
- SP (Stack Pointer):栈指针寄存器,指向栈顶的偏移地址,配合
SS段寄存器使用,用于栈操作(PUSH/POP)。 - BP (Base Pointer):基址指针寄存器,指向栈底或栈内某位置的偏移地址,配合
SS段寄存器访问栈内数据。 - SI (Source Index):源变址寄存器,字符串操作中存放源数据的偏移地址,配合
DS段寄存器使用。 - DI (Destination Index):目的变址寄存器,字符串操作中存放目的数据的偏移地址,配合
ES段寄存器使用。
- SP (Stack Pointer):栈指针寄存器,指向栈顶的偏移地址,配合
二、段寄存器(4 个 16 位寄存器)
8086 是 16 位处理器、20 位地址总线,通过段地址 + 偏移地址的方式生成 20 位物理地址(物理地址 = 段地址 × 16 + 偏移地址)。段寄存器专门存放段的起始地址(高 16 位)。
- CS (Code Segment):代码段寄存器,存放程序指令所在段的段地址,配合
IP寄存器定位下一条要执行的指令。 - DS (Data Segment):数据段寄存器,存放程序数据所在段的段地址,默认用于访问内存数据。
- SS (Stack Segment):栈段寄存器,存放栈空间所在段的段地址,配合
SP/BP寄存器访问栈数据。 - ES (Extra Segment):附加段寄存器,额外的数据段寄存器,常用于字符串操作的目的段地址。
三、控制寄存器(2 个 16 位寄存器)
控制寄存器用于控制 CPU 的运行状态和指令执行顺序,程序员只能间接修改,不能直接赋值。
-
IP (Instruction Pointer):指令指针寄存器,存放当前代码段中下一条要执行指令的偏移地址,与CS配合确定指令的物理地址。
- CPU 执行完一条指令后,会自动修改
IP的值,指向下一条指令,程序员无法通过MOV指令直接修改。
- CPU 执行完一条指令后,会自动修改
-
FLAGS
:标志寄存器(也叫程序状态字寄存器 PSW),存放 CPU 执行指令后的状态标志和控制标志,共 16 位,仅 9 位有效(6 个状态标志 + 3 个控制标志)。
-
状态标志(反映运算结果特征):
CF(进位标志):无符号数运算进位 / 借位时置 1。PF(奇偶标志):运算结果低 8 位中 1 的个数为偶数时置 1。AF(辅助进位标志):低 4 位向高 4 位进位 / 借位时置 1(用于 BCD 码运算)。ZF(零标志):运算结果为 0 时置 1。SF(符号标志):运算结果最高位为 1 时置 1(对应有符号数的负数)。OF(溢出标志):有符号数运算溢出时置 1。
-
控制标志(控制 CPU 运行方式):
TF(陷阱标志):置 1 时 CPU 进入单步执行模式(调试用)。IF(中断允许标志):置 1 时允许 CPU 响应可屏蔽中断,置 0 时禁止。DF(方向标志):字符串操作的方向控制,置 0 时正向(地址递增),置 1 时反向(地址递减)。
-
栈补充
栈和字符串结合起来就容易晕头转向。
首先要搞清楚OFFSET的用途,它的核心作用是:在汇编时获取某个标号(label)、变量或过程(procedure)在它所在段内的偏移地址(offset address),即段内偏移量(以字节为单位)。
而与其功能类似的是LEA(Load Effective Address): LEA reg16, mem
目标操作数 reg16:只能是 16 位通用寄存器:AX, BX, CX, DX, SI, DI, BP, SP
源操作数 mem:必须是内存寻址表达式(即含 [...] 的地址表达式),如:
[BX][SI+4][BX+SI+10h][BP+DI-2][DI]
注:可是明明有lea后面跟着标号的写法,但这其实只是MASM将label隐含成[label]了,对于lea的[],这个并非是取内存的值,只是对于框里面的表达式进行运算来求值而已。
然后我们就考虑如何处理栈必须是word为单位,而字符串确实用byte存储的问题。解决方式就是尽量不用,或者用八位的凑合用一下。
debug
MASM 的 debug 指令用于在 DOS 下调试可执行文件。常用指令如下:
d:显示内存内容(dump),如d 100。u:反汇编(unassemble),如u 100。e:编辑内存(enter),如e 100。g:运行程序(go),如g。t:单步执行(trace),如t。r:显示/修改寄存器(register),如r。q:退出 debug(quit)。
对于下断点:
在 DOS debug 里这样做:
-
载入程序
-
反汇编找到该指令偏移(例:
u 100,记下lea ax,[buffer+2]前的偏移,如 0123)。 -
设断点并运行
到达断点后用 r 看寄存器,t 单步继续
或者可以在那行前插入软件断点 int 3,重新运行,使用g即可在此处停下。
停下后你就会发现确实是停下了,但也动不了了,此时需要R IP → 输入 当前IP+1;2. G
MASM 6.11邪教
.IF、.WHILE、.REPEAT 是 MASM(Microsoft Macro Assembler) 从 **v6.0+**开始支持的伪代码
- 大小写不敏感:
.if,.If,.IF均可(但推荐大写保持风格统一)。 - 运算符空格:
eax==0必须写成eax == 0(MASM 6.11 要求空格)。 - 寄存器 vs 内存:条件中尽量用寄存器,内存操作(如
[var] == 5)可能效率低或需mov中转。 - 嵌套支持:可任意嵌套(
.IF里套.WHILE等),但注意.ENDIF/.ENDW配对。 - 现代替代:
- 在 x64 MASM(ml64.exe) 中,这些指令仍支持(Visual Studio 自带)。
- 但纯汇编项目更倾向手写
cmp/jcc以精确控制性能。
- 坑爹:默认是无符号的比较,想要有符号还是老老实实的cmp吧
八股取士补充
寻址
-
立即寻址
mov ax, 3064h -
寄存器寻址
mov ax, bx -
直接寻址
mov ax, [1234h]注意:在 MASM 中,mov ax, [1234h]默认被解释为mov ax, 1234h(立即数寻址) -
寄存器间接寻址
mov ax, [bx] -
寄存器相对寻址
mov ax, count[SI]等价于mov ax, [count + SI]count为提前定义好的符号地址 -
基址变址寻址方式
mov ax, [bx][di]等价于mov ax, [BX + DI] -
相对基址变址寻址方式 相当于二维数组
mov ax, MASK[bx][si]等价于mov ax, MASK[bx+si]等价于mov ax, [MASK + bx + si]
然后是8086不支持的比例变址寻址方式,就是允许在寻址的时候进行乘法运算
跳转
近跳转只改 IP
段间跳转 = 修改 CS(代码段) 和 IP(指令指针)
NEXTROUTINE在同模块内,形如
INTERS 必须是一个内存变量(标号),指向一个 FAR 指针表(每个元素 4 字节:IP + CS)。
|
|
冷门指令
冷门且无用
8086有都没有的
movsx 带符号扩展的数据传送。将小尺寸有符号整数扩展为大尺寸(高位用符号位填充:正数补 0,负数补 1)。
movzx带零扩展的数据传送。将小尺寸无符号整数扩展为大尺寸(高位补 0)。
|
|
没人用过的
XCHG ax, bx交换
考试有用的
累加器专用传送指令
8086中I/O端口与CPU通信使用IN和OUT指令,端口的范围为0~FFFFH,当端口号小于256时(0~255),可以使用立即数
eg对于读取时间
XALT:用 AL 作为索引,从一张字节表中取出对应字节,放回 AL
|
|
地址传送指令
LEA
LEA reg16, memory_operand
⚠️ reg16 只能是 16 位通用寄存器(AX/BX/CX/DX/SI/DI/BP/SP)
⚠️ memory_operand 必须是内存操作数(不能是立即数或寄存器)
LDS、LES、LFS、LGS、LSS
8086 只支持 LDS 和 LES
标志寄存器
LAHF: AH <- 标志低字节送到AH SAHF: AH -> 标志 PUSHF: 标志进栈 POPF: 标志出栈
CBW: 字节转为字 AL → AX 符号扩展 CWD: 字转为双字 AX → DX:AX 符号扩展 可视作 负数 扩展全为1,正数扩展全为0,这样维持了补码形式的大小不变和符号的正负 注意上述操作对象只能是AL和AX,这是无参数的指令
加法带进位、减法带借位
ADC dst, src : dst <- dst + src + cf
SBB dest, src: dst -< dst - src - cf
这个是用来进行32位加减法的
乘除法符号
imul bx AX * BX → DX:AX idiv bx DX:AX(32 位)/ bx -> 商AX 余数 DX
必考之加减乘除 $$ \text{Result} = \left\lfloor \frac{A \times B + 2026}{C} \right\rfloor + D \times E $$
最终 Result 存放在:DX:AX (32 位有符号整数)
中断
中断 = 外部/内部事件“打断”CPU当前工作,让它去处理更紧急的事。
中断分为可屏蔽中断和非屏蔽中断。
CPU通过IF位(Interrupt Flag,中断允许标志位)
- IF = 1 → 允许响应可屏蔽中断
- IF = 0 → 屏蔽(忽略)可屏蔽中断
所以 IF位和中断屏蔽寄存器用来实现两级控制是否允许中断。
非屏蔽中断(NMI) = 不能被忽略的超级紧急事件NMI 。无视IF位,CPU一定会响应。
中断屏蔽寄存器的IO端口是21H,通过八位来对应控制八个外部设备的是否允许中断,1表示禁止,0表示允许
| 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
|---|---|---|---|---|---|---|---|
| 打印机 | 软盘 | 硬盘 | 串行通信口1 | 串行通信口2 | 保留 | 键盘 | 定时器 |
编写中断程序时,需要在主程序配置好中断屏蔽寄存器和IF并设置中断向量,所以需要先知道什么是中断向量表
每个中断都有一个编号,叫 中断号(Interrupt Vector Number) 中断向量表(IVT) 就是一个数组,有 256 个中断向量。当触发中断时可以高效地跳转到对应的中断程序地址。
以覆写定时中断为例(假设已经有一个countdown proc)
|
|
注意中断的函数返回是iret
通过1CH中断进行定时操作
串子
方向标志 DF(Direction Flag):
DF = 0:正向(递增) —— 字符串操作时,SI/DI 自动 +1(字节)或 +2(字)DF = 1:反向(递减) —— SI/DI 自动 –1(字节)或 –2(字)
8086 提供 5 条隐式操作寄存器的字符串指令:
| 指令 | 操作 | 源地址 | 目标地址 | 说明 |
|---|---|---|---|---|
MOVSB / MOVSW |
移动字节/字 | [DS:SI] → [ES:DI] |
自动更新 SI/DI | 内存复制(需 DS→ES) |
CMPSB / CMPSW |
比较字节/字 (SI- ) | [DS:SI] vs [ES:DI] |
更新 SI/DI,影响 FLAGS(ZF, CF 等) | 可用于 repe CMPSB 实现 memcmp |
SCASB / SCASW |
扫描(搜索)字节/字 | AL/AX vs [ES:DI] |
更新 DI,影响 FLAGS | 常用于 repne SCASB 搜索字符(如 strlen) |
STOSB / STOSW |
存储字节/字 | AL/AX → [ES:DI] |
更新 DI | 常用于 rep STOSB 填充内存(如 memset) |
LODSB / LODSW |
加载字节/字 | [DS:SI] → AL/AX |
更新 SI | 较少与 REP 连用(因无目标存储) |
这些是重复前缀,作用于带 CX 计数的字符串指令,相当于“硬件循环”。
| 前缀 | 全称 | 条件 | 常用于 |
|---|---|---|---|
REP |
Repeat | CX ≠ 0 |
无条件重复(MOVSB, STOSB) |
REPE / REPZ |
Repeat while Equal / Zero | CX ≠ 0 且 ZF = 1 |
CMPSB, SCASB(如比较相等时继续) |
REPNE / REPNZ |
Repeat while Not Equal / Not Zero | CX ≠ 0 且 ZF = 0 |
SCASB, CMPSB(如搜索不等时继续) |