本文是对基于IBM-PC汇编语言程序设计的一些笔记. 内容主要是汇编程序设计的基础知识和示例代码.

基本知识介绍

本文使用在dos环境下运行的MASM6.0. 为了需要运行dos环境, 首先需要下载dos的虚拟机. 这里推荐dosbox, 这是一个跨平台的dos虚拟机, 在其官网上可以下载到各个平台的程序.

下载了MASM6.0后直接解压, 可以看到如下的几个程序, 程序名和作用如下表所示

程序名 作用
MASM.exe 汇编主程序
Link.exe 链接器
ML.exe 汇编和链接
LIB.exe 相关库
DEBUG.exe 调试程序

其中, DEBUG.exe在前期使用较为频繁, 其相关指令较多, 因此下面给出DEBUG.exe的常见指令

命令名 作用 示例
r(Register) 显示或修改寄存器数 r / r ax
d(Dump) 显示指定位置的内存数据 d 1000:0004/d:1000:0004 38
e(Enter) 向指定位置写入数据 e 1000:0000 1 2 3 4 5 6
u(Unassmble) 显示指定位置对应的汇编代码 u 1000:0000
t(Trace) 执行一步 t
a(Assemble) 向指定位置输入汇编代码 a
q(quit) 退出程序 q

在DEBUG.exe输入r指令, 会显示所有寄存器的当前数值, 下面给出各个寄存器的含义

名称 作用 名称 作用
AX 累加器 CS 代码段
BX 基址变址 DS 数据段
CX 计数 ES 附加段
DX 数据 SS 堆栈段
SP 堆栈指针 DI 目的变址
BP 基址指针 SI 源变址
IP 指令指针

段的几点说明

  1. ax,bx,cx,dx除了作为16bit寄存器使用以外, 均可分为两个8bit的寄存器
  2. 由于偏移地址是一个16bit的寄存器, 所以一个段最多有64K的空间
  3. 一个段要求至少有16Byte的空间
  4. 段的起始位置和容量必须是16的整数倍
  5. 段实际地址 = 段地址x16+偏移地址
  6. 注意:不能使用立即数段寄存器赋值

数据和内存

数据在内存中的存放

对于大部分机器, 其内存结构均采用小端序, 即先存放低位数据, 再存放高位数据,例如数据0x1234 在内存中的实际存放顺序是34 12

内存寻址和ds寄存器

在汇编代码中, 可以使用如下的格式引用内存中的数据

1
mov ax [0]

上述代码实际是默认段寄存器为ds寄存器. 汇编代码中访问内存时, 总是默认从ds指定的位置开始读取数据

注意: 不可以使用mov指令在两个内存单元中直接移动数据

内存中的栈

在内存中, 有ss段寄存器和sp堆栈指针寄存器两者共同维护程序堆栈. 由于堆栈从高位开始, 向低位扩展, 所以当选择一块区域作为栈时, ss指向这一区域的开始位置, 而sp指向这一区域的结束位置.
在使用栈的过程中, sp始终指向当前的栈顶位置. 执行Push操作时, sp先减2, 然后写入数据. 执行Pop操作时, 先读取sp指向的位置的数据, 之后sp加2

注意事项

  1. 设置堆栈段寄存器和寄存器的时候, 两条指令必须连续执行
  2. si和di只能作为16bit寄存器使用
  3. 不能直接在两个内存单元中传递数据
  4. mov al [6]在asm文件中, 汇编器会将其汇编成mov al 6, 此时应该先将偏移地址放在某个寄存器中, 如bx, 使用mov al [bx]获得数据, 但是在DEBUG.exe程序中使用a指令逐行汇编时并没有这种问题

汇编程序结构

在使用汇编语言编写代码时, 会设计到两类指令, 第一类是汇编指令, 此类指令对应具体的机器代码, 在汇编后转换为CPU可以执行的二进制代码. 第二类是伪操作(伪指令), 此类指令由汇编器进行处理, 相当于汇编器提供的一些方便的函数调用.

一个完成的汇编程序的结构如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
assume cs:code,ds:data,ss:stack     ;关联自定义段, 将CS段与自定义的code段关联, 将DS段与自定义的data段关联
data segment ;定义(数据)段, 格式为: 段名 segment
...
data ends ;段结束, 格式为: 段名 ends
;------------------------
stack segment ;定义栈段
...
stack ends
;------------------------
code segment ;定义代码段
start: ; 定义标号, 相当于给此处的内存地址创建一个名称, 可被其他指令引用
mov ax,data
mov ds,ax ;初始段寄存器
...
mov ax, 4c00H ;这两句调用系统指令, 结束程序
int 21H
code ends
end start ;设置代码开始位置为start标号的位置
end ; 一个单独的end表示汇编程序结束

注意:

  1. 每个段结尾的endsend segment的含义
  2. 语句结束后没有分号, 在汇编代码中, 分号表示注释

数据声明方式

基本声明

数据声明的基本格式为

1
<标号> <数据类型> <数据1> [, <数据2>, <数据n>] 

其中<标号>是一个标识符, 表示这段数据在内存中的位置, 后续指令可通过标号引用数据. <数据类型>有三种取值, 分别是db(data byte), dw(data word), dd(data double word). 最后的数据部分可以填入若干数据.

1
2
3
DATA_BYTE db 10,4,10H
DATA_WORD dw 100,100H,-5
DATA_DW dd 3*20,0FFFDH ;所有的数值都必须是0-9开头, 因此使用16进制时, 高位使用0补齐

字符串

定义字符串与定义基本数据的格式相同, 数据部分可直接写ASCII字符. 在定义数据时可使用?进行占位, 占据的空间与数据类型需要的空间相同.

1
2
message1 db 'HELLO'
message2 dw 'AB',?

注意: 如果给定的数据类型足够存储字符串, 则按照小端序存储, 否则按照输入的顺序存储. 对于字符串而言, 通常仅希望其按照给定顺序存储, 因此声明为db类型

标号和地址

标号可以作为数据存入, 此时存入内存的是这个标号对应的内存地址. 一个标号等价于一个16bit的偏移地址, 和一个16bit的段地址.

1
2
3
PAR dw 100,200
ADDR_TABLE1 dd PAR ; 偏移地址在数据低位, 段地址在数据高
ADDR_TABLE2 dw PAR ; 只有偏移地址

如果被赋值的数据类型足够大, 则将一个地址赋给变量时, 偏移地址在数据低位, 段地址在数据高位. 如果被赋值的数据类型不够大, 则变量中只有偏移地址

大量数据分配

1
2
array db 100 dup(10)
db 2 dup(0,2 dup(1,2),3)
  • 第一行表示填充100个数据, 每个数据是10
  • 第二行是嵌套表示, 外部表示总体重复两次, 内部的表示将1,2重复2次
    • 即最后的序列为 0 1 2 1 2 3

类型转换

1
2
3
4
5
OPER1 db 1,2
OPER2 dw 1234H,5678H
...
mov ax, word ptr oper1+1
mov al, byte ptr oper2
  • 使用ptr关键字可以进行类型转化
  • 具体格式为 type ptr variable, 其中type可以为byte, word, dword
  • 先进行计算, 计算完成后, 将结果按照制定的格式进行转换
  • 其中的变量+1操作等于对应的内存地址+1

多类型

1
2
3
4
5
6
byte_array label byte
word_array dw 50 dup(?)
...
mov word_array + 2, 0
mov byte_array + 2, 0

  • 使用label关键字指定类型
  • 具体格式为 name label type, 其中type可选项与上一节相同
  • 相当于给一个内存指定了两个名字, 在代码中可以任意的使用

数据寻址方式

数据表达方式

汇编语言中数据有3中表达方式

  1. 立即数
    • 数据由字面值给出, 数据实际编码在机器指令中, 执行时, 保存在译码电路中
  2. 寄存器
    • 数据存放在寄存器中
  3. 段地址:偏移地址
    • 数据在内存中

寄存器间接寻址

使用形如[bx]的形式进行寄存器间接寻址, 表示将指定的寄存器的内容作为内存的地址, 取出相应地址上的数据

寄存器相对寻址

使用形如[bx+idata]的形式进行寄存器相对寻址, 表示将计算结果作为内存的地址, 取出相应地址上的数据
相对寻址的特点在于每次执行的时候idata是不变的立即数, 而每次改变寄存器的值, 从而对一组相对位置不变的数据操作

基址变址寻址

使用形如[si+bx]的形式进行基址变址寻址

相对基址变址寻址

使用形如[bx+si+idata]的形式进行相对基址变址寻址

一些限制

  1. 在8086CPU中, 只有bx, si, di, bp可以用于[…]
  2. 只能以bx+si,bx+di,bp+si,bp+di的形式出现, 其他组合都是非法的形式
  3. 如果在[…]中使用bp, 且没有指定段寄存器, 则默认段寄存器为ss, 即[bp+di+5] <=> (ss)x16+(bp)+(di)+5

循环程序设计

LOOP指令

  • 指令格式 LOOP 标号
  • 执行步骤
    1. (CX) = (CX) - 1
    2. 判断CX的值, 如果不为零, 则转至标号处执行, 否在继续向下执行

循环指令的例子

1
2
3
4
5
6
7
8
9
10
assume cs:codeseg
codeseg segment
mov ax, 2
mov cx, 11 ; 初始化循环变量, 执行11次
s: add ax, ax
loop s
mov ax 4c00H
int 21H
codeseg ends
end
  • 类似于高级语言中的循环结构, 汇编语言中的循环结构基本按照上述形式固定不变
  • 可以使用si寄存器和di寄存器作为一定数据的辅助段寄存器

分支程序设计

offset操作

offset是一个伪操作, 属于数值回送操作符, 作用是获得标号的偏移地址, 以下面的代码为例

1
2
3
4
5
6
7
8
assume sc:code
code segment
start: mov ax, offset start ;相当于 mov ax, 0
s: mov ax, offset s ;相当于 mov ax, 3
mov ax, 4C00H
int 21H
code ends
end

指令分类

8086CPU的跳转指令可以分成如下的几类

指令 效果
loop 循环
jmp 无条件跳转
jcxz 有条件跳转
call/ret 子程序调用
int 中断

无条件转移

接下来介绍几种常见的无条件转移指令

名称 指令格式 特点
段内短转移 jmp short 标号 指令中使用8bit保存IP的偏移量
段内近转移 jmp near ptr 标号 指令中使用16bit保存IP的偏移量
段间直接远转移 jmp far ptr 标号 使用标号所在的段和偏移地址修改CS和SP
段内间接近转移 jmp word ptr 内存单元 使用指定内存单元的字修改IP
段间间接远转移 jmp dword ptr 内存单元 使用指定内存的两个字, 低位字修改IP, 高位字修改CS
寄存器转移 jmp 寄存器 使用指定寄存器的值修改IP

几点补充

  1. 在执行跳转指令时, IP以及指向下一条指令, 因此所有的偏移都是相对于下一条指令的开始位置
  2. 因为段内短转移使用8bit保存偏移量, 所以只能向前跳转128字节或向后跳转127字节
  3. 因为段内近转移使用16bit保存偏移量, 所以只能向前跳转32768字节, 或者向后跳转32767字节

有条件转移

有条件转移根据之前的cmp指令计算结果决定是否转移, 且所有的转移都是短转移, 即只能在当前位置, 相对的跳转大约128个字节

有条件指令结构

有条件转移指令都是j开头, 根据转移条件不同跟上不同的后续符号, 后续符号可以分成如下几种情况

类型 无符号 有符号
相等 e(equal) e(equal)
大于 a(abve) l(less)
小于 b(below) g(greater)
否定 n(not) n(not)

例如, 无符号的大于指令是ja 有符号的大于指令是jg 有符号的小于等于指令是jle 或者jng

jcxz指令

从名字可以知道, 此指令是比较cx寄存器是否为0, 所以当cx为0跳转到标号, 否则指向下一条指令. 此指令跳转条件与loop正好相反

子程序设计

ret指令

利用栈中的数据, 修改ip寄存器的内容, 从而实现近转移, 等价于如下的代码

1
2
(ip) = ((ss)*16+sp)
(sp) = (sp) + 2;

retf指令

使用栈中的两个数据修改IP寄存器和CS寄存器, 用于实现远转移, 等价于如下的代码

1
2
3
4
(ip) = ((ss)*16+(sp)
(sp) = (sp) + 2;
(cs) = ((ss)*16+(sp))
(sp) = (sp) + 2;

注:

  1. 实际上所有的入栈操作时, 都是先压入段寄存器, 在压入偏移地址寄存器, 所以出栈操作正好相反
  2. retf即return far

call指令

将当前的IP或CS和IP压入栈中, 并根据指令格式中的目的地址进行转移, 各指令格式与等价操作如下所示

入栈操作
call 标号 call far ptr 标号 call 寄存器 call dword ptr 内存单元
(SP) = (SP) - 2 (SP) = (SP) - 2 (SP) = (SP) - 2 (SP) = (SP) - 2
((SS)*16+(SP)) = (IP) ((SS)*16+(SP)) = (CS) ((SS)*16+(SP)) = (IP) ((SS)*16+(SP)) = (CS)
                   |(SP) = (SP) - 2                |                     |(SP) = (SP) - 2  
                   |((SS)*16+(SP)) = (IP)          |                     |((SS)*16+(SP)) = (IP)  
跳转操作
call 标号 call far ptr 标号 call 寄存器 call dword ptr 内存单元
(IP) = (IP) + 16bit位移 (IP) = 目标标号所在段的偏移地址 (IP) = (16bit寄存器) (IP) = 内存单元地址
                   |(CS) = 目标标号所在段的段地址  |                      |(CS) = 内存单元地址+2

注:

  1. call 标号不能实现短转移, 因为是否为短转移是按照偏移量长度区分
  2. call 标号与jmp指令相同, call指令的二进制代码中保存的是标号相对于当前IP的偏移量, 而不是绝对地址
  3. call far ptr 标号类似于远转移指令, 但是在跳转前分别压入CS寄存器IP寄存器的值
  4. 所有CS和IP同时出现的地方(内存地址和栈),都是IP在低位,CS在高位

MUL指令

指令格式为:mul 寄存器/mul 内存单元
两个8bit数据或两个16bit数据相乘

  • 8bit数据使用al的值和指定的值相乘, 存放在ax中
  • 16bit数据使用ax的值和指定的值相乘, 高位存放在dx, 低位存放在ax

参数传递方式

  1. 利用寄存器传递少量参数
    • 在子程序的调用过程中, 如果不对寄存器做任何处理, 则寄存器中的值可以之间传递到子程序中
    • 但是寄存器数量有限, 不能大量传递数据
  2. 使用内存单元
    • 可以批量存放数据
    • 对于需要批量返回的结果, 也可以使用此方法

寄存器冲突

在调用子程序的时候, 由于寄存器数量有限, 因此当前程序和子程序可能使用了相同的寄存器. 可以在子程序中可以很使用如下的框架来解决寄存器冲突

1
2
3
4
5
6
子程序入口:子程序中用到的寄存器入栈
...
子程序内容
...
子程序中用到的寄存器出栈
子程序返回

标志位寄存器

标志位说明
标志名 解释 选项1 选项2 含义 针对数据类型
OF 溢出标志位 NV(未溢出) OV(溢出) 记录运算结果是否溢出 有符号数
DF 方向标志位 UP(递增) DN(递减) 控制串传送的增减方式 无关
IF 允许中断标志位 DI(禁止) EI(许可) …… ……
SF 符号标志位 PL(正) NG(负) 运算结果的符号状态 有符号数
ZF 零标志位 NZ(不等于零) ZF(等于零) 运算结果是否为零 全部类型
AF 辅助进位标志位 NA(无进位) AC(进位) …… ……
PF 奇偶标志位 PO(奇) PE(偶) 当前二进制数据1的个数 全部类型
CF 进位标志位 NC(无进位) CY(进位) 记录运算结果的最高有效位进位或借位 无符号数

注:

  1. 选项一对应为0,选项二对应为1
  2. 只有算数运算置标志位,数据移动运算不置标志位

CF与OF比较

  • CF是Carry Flag, 即进位标志位, 只针对无符号数
  • OF是Overflow Flag, 即溢出标志位, 只针对有符号数
  • 由于表示范围不一致,因此可以出现溢出但不进位
    • 例如对于有符号两个较大的数相加
    • 但无符号比有符号大一倍,没有进位
  • 由于对于正负的认识不同,因此也可能出现进位但不溢出
    • 例如有符号是负数+整数
    • 对于无符号就是两个正数相加
    • 此时无符号进位,有符号没有溢出

DF标志位

  • DF指示在进行串传递的时候, 每次执行si或di的变化
  • 置为递增
    • 指令格式cld
    • 将DF置为0
  • 置为递减
    • 指令格式std
    • 将DF置为1

串传送指令

字节传送

  • 指令格式 movsb
  • 以字节为单位传送指令, 将ds:si指向的内存单元的数据传输到es:di执行的内存单元
  • 执行过程如下
    1. ((es)*16+di) = ((ds)*16+si)
    2. 如果DF=0, (si) = (si) + 1, (di) = (di) + 1
    3. 如果DF=1, (si) = (si) - 1, (di) = (di) - 1

字传送

  • 指令格式 movsw
  • 以字为单位传送指令, 将ds:si指向的内存单元的数据传输到es:di执行的内存单元
  • 执行过程如下
    1. ((es)*16+di) = ((ds)*16+si)
    2. 如果DF=0, (si) = (si) + 2, (di) = (di) + 2
    3. 如果DF=1, (si) = (si) - 2, (di) = (di) - 2

串传送

  • 指令格式 rep movsbrep movsw
  • rep指令与movsb/movsw指令结合使用可用于串传送
  • rep movsb指令等价于
    1
    2
    s: 	movsb
    loop s
  • 将这样两条指令配合使用, 通过cx即可实现一段数据的传输

标志寄存器与栈操作

入栈操作

  • 指令格式: pushf
  • 将标志寄存器的值入栈

出栈操作

  • 指令格式:popf
  • 将标志寄存器的值出栈

中断程序设计

内中断与外中断

  • 由外设控制器或协处理器引起的中断称为硬件中断外中断
  • 由程序安排的中断指令INT产生的中断称为软件中断内中断

内中断的产生原因

  1. CPU内部错误, 如除数为零等
  2. 为调试程序设置的中断
  3. 执行into指令
  4. 执行int指令

常见中断类型

中断号 名称 作用
0 除法错误中断 执行除法指令时, 如果除数为零或者商操作寄存器范围, 立即产生此中断
1 单步执行中断 调试程序时, 使用此中断, 使得程序每次一条指令后立即中断
3 断点中断 当程序需要加入断点时, 使用此中断, 产生一个断点
4 溢出中断 程序产生溢出时, 产生此中断

注:

  1. 使用断点中断实际上就是在需要断点的地方插入一条int 3指令
  2. 产生溢出后, 可以使用into指令, 转入溢出中断处理, 如果没有溢出, 则into指令没有任何效果

中断向量表

  • 80x86系统可以处理256种中断类型
  • 中断向量表存放在内存单元0000:0000-0000:03FF的1024个内存单元中
  • 一个表项占两个字(4个字节), 其中低位存放偏移地址, 高位存放段地址

中断过程

根据中断类型码, 在中断向量表中获得中断向量并设置CS与IP称为中断过程, 此过程有硬件自动完成, 不能通过程序修改, 执行过程如下

  1. 获得中断类型码N
  2. 标志位寄存器入栈
  3. CS寄存器入栈
  4. IP寄存器入栈
  5. 设置TF标志位和IF标志位为0
  6. 从中断向量表获得数据, 设置CS和IP寄存器
  7. 跳转至中断程序

中断程序设计

由于中断随时都有可能发生, 因此中断处理程序必须存放内存中的特定位置. 中断程序步骤与子程序类似, 有如下几步

  1. 保存用到的寄存器
  2. 处理中断
  3. 恢复用到的寄存器
  4. 用iret返回

iret指令

在调用中断程序之前, 标志位寄存器, CS寄存器, IP寄存器依次入栈, 因此iret执行相反的出栈操作即可恢复到中断以前的状态, 即iret等价于以下代码

1
2
3
pop ip
pop cs
popf

DIV指令为

  • 指令格式: div 寄存器/DIV 内存单元
  • 指令含义
    1. 8bit除法, 被除数16bit存放在ax中, 计算后al保存商, ah保存余数
    2. 16bit除法, 被除数为32bit, 高位存放在dx, 低位存放在ax, 计算后ax保存商, dx保存余数
  • 如果商大于al或ax的保存范围则产生除法溢出

安装程序结构

编写一个安装程序可以分成如下的几个步骤

  1. 编写要被安装的程序, 并将代码置于安装程序中
  2. 设置ds:si指向被安装程序在安装程序中的位置, 将es:di执行被安装程序需要存在的位置
  3. 使用传输指令, 复制被安装程序
  4. 设置中断向量表
    以下是一个安装程序的示例
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    assume cs:codeseg
    codeseg segment
    start: mov ax, cs
    mov ds, ax
    mov si, offset dd0
    mov ax, 0
    mov es, ax
    mov di, 200h
    mov cx, offset dd0end - offset dd0
    cld
    rep movsb
    mov ax, 0
    mov es, ax
    mov word ptr es:[0*4],200h
    mov word ptr es:[0*4+2],0
    mov ax, 4C00H
    int 21H
    dd0: jmp short dd0start
    db "overflow!"
    dd0start:
    mov ax, cs
    mov ds, ax
    mov si, 202h
    mov ax, 0b800h
    mov es, ax
    mov di, 12*160+36*2
    mov cx, 9
    s: mov al, [si]
    mov es:[di], al
    inc si
    add di, 2
    loop s
    mov ax, 4c00h
    int 21h
    dd0end: nop
    codeseg ends
    end start
    说明:
  5. 上述代码中, 从start标号开始, 到dd0标号之前, 是安装程序, 可以看到此部分程序严格按照上述顺序完成了安装操作
  6. 从dd0标号到dd0end标号之间的代码是被安装程序
  7. 上述示例中, 将程序安装到了200H的位置, 此处通常为空, 但是正式程序中不建议这么使用
  8. mov cx, offset dd0end - offset dd0指令中, 使用两个标号的运算实现了被安装程序长度的可扩展性, 后续修改被安装程序的时候, 不需要修改安装程序
  9. 在dd0end标号对应的地方, 使用了一条nop指令占位, 从而可以添加一个标号, 之后用于计算程序长度

端口和外中断

IO设备的数据传输方式

  1. 程序控制方式(查询方式)
    • 在PC系统中, 除存储器外, 和CPU通过总线连接的各种输入输出设备
    • 每种IO设备都要通过一个硬件接口或控制器芯片和CPU相连
    • 这些接口或控制器芯片都能支持输入输出指令与外部设备交换信息
  2. 中断方式
  3. DMA方式

端口

  • 在各种硬件接口或控制器芯片中, 有一组可由CPU读写的寄存器
  • CPU将这些寄存器作为端口, 对它们统一进行编制
  • CPU对它们进行读写控制都是通过控制总线向芯片发出端口的读写指令

端口地址空间

  • 80x86系统允许设置64K个8bit端口或32K个16bit端口
  • 对端口的读写需要使用IN和OUT指令进行信息传输

端口指令

  • 格式 in al 60h
  • CPU通过地址总线将地址信息60H发出
  • CUP通过控制总线发出端口读指令
  • 端口所在芯片将60h端口中的数据通过数据总线送入CPU

端口指令的一些限制

  1. 对于端口号在0-255之间的端口, 可以直接访问
  2. 对于端口号在236-65535之间的端口, 端口地址需要放在dx寄存器中
  3. 访问8bit端口时, 数据只能存入al, 访问16bit端口时, 只能使用ax

通过端口访问CMOS RAM

  • 芯片由电池供电
  • 包含128个存储单元的RAM存储器
  • 内部实时时钟占用0-d单元保存系统时间
  • 内部有两个端口70H和71H, 通过这两个端口进行读写
    1. 70H端口为地址端口
    2. 71H端口为数据端口, 可以使用此端口读取或者写入数据
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      ; 读取CMOS RAM 2号单元
      assume cs:code
      code segment
      start:
      mov al, 2
      out 70h, al
      in al,71h
      mov ax,4C00h
      int 21h
      code ends
      end start
移位操作
  1. SHL指令
    • shl 寄存器,n / shl 内存单元,n
    • 逻辑左移
    • 最后移出的位置CF标志位
    • 如果移动次数大于1,移动次数存在cl中
  2. SHR指令
    • shr 寄存器,n / shr 内存单元,n
    • 逻辑右移
    • 最后移出的位置CF标志位
    • 如果移动次数大于1,移动次数存在cl中
  3. SAL指令
    • sal 寄存器,n / sal 内存单元,n
    • 算数左移
    • 最后移出的位置CF标志位
    • 如果移动次数大于1,移动次数存在cl中
  4. SAR指令
    • sar 寄存器,n / sar 内存单元,n
    • 算数右移
    • 使用符号位补全高位
    • 最后移出的位置CF标志位
    • 如果移动次数大于1,移动次数存在cl中

可屏蔽中断

  • 如果IF等于0,则不响应可屏蔽中断
  • 在中断处理过程中,将IF置为0,即可屏蔽其他可屏蔽中断
  • 几乎所有的外设引起的外中断都是可屏蔽中断

不可屏蔽中断

  • 是CPU必须响应的外中断
  • 中断码固定为2, 中断过程不需要取中断类型码
  • 是系统有必须处理的紧急情况发生时,用于通知CPU的中断信息

标号与直接定址表

地址标号

  • 地址标号代表一个内存单元地址
  • 地址标号后跟上一个冒号
  • 只能在代码段使用地址标号

数据标号

  • 数据标号直接跟上数据,不加冒号

  • 此标号除了代表内存地址以外,还隐含了此处数据的类型

  • 在使用此标号代表内存单元时,会进行检查数据类型是否匹配

  • 计算偏移的时候,类型信息没有影响

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    assume cs:codesg, ds:datasg     // 此信息仅用于编译,对程序不可见
    datasg segment
    a db 1,2,3,4
    b dw 0
    datasg segment

    codesg segment
    start:
    mov ax,datasg // 声明了数据段关联,但还是需要手动为ds赋值
    mov ds, ax
    mov si,0
    mov cx,4
    s: mov ax, a[si]
    mov ah, 0
    add b, ax
    inc si
    loop s
  • 数据标号可以作为数据被定义,标号表示此标号所表示的地址

1
2
3
4
5
datasg segment                  datasg segment
a db 1,2,3,4 a db 1,2,3,4
b dw 0 <==> b dw 0
c dw a, b c dw offset a, offset b
datasg segment datasg segment
1
2
3
4
5
datasg segment                  datasg segment
a db 1,2,3,4 a db 1,2,3,4
b dw 0 <==> b dw 0
c dw a, b c dw offset a,seg, offset b,seg
datasg segment datasg segment
  • 使用dw时,只保存标号的偏移地址,使用dd时,保存偏移地址和标号所在段的段地址

直接定址表

  • 用查表的方法的编程技巧

数值映射

0-9 数值+30h
10-15 数值+37h

使用直接定址表实现映射

  • 其实就是类似数组的操作

  • 可以提高算法的简洁性

  • 由于可以查表,从而提升了运算速度

  • 例子: 通过查表计算sin(x)

1
2
3
4
5
table db ag0 ag30 ag60 ag90
ag0 db '0',0
ag30 db '0.5', 0
ag60 db '0.866', 0
ag90 db '1',0
  • 使用两步查询获得数据
    1
    2
    3
    mov bx, 2
    mov bx, table[bx] ; 先取出偏移地址
    mov ah, cs:[bx] ; 再从偏移地址取出实际内容

例子:清屏程序

  1. 清屏
  2. 设置前景色
  3. 设置背景色
  4. 向上滚动一行
  • 先编写四个子函数,通过直接定址的方法,通过给定一个序号来指定调用的功能
  1. 清屏:将显存中当前屏幕字符设置为空格
  2. 设置前景色:设置属性字节(奇数字节)的0,1,2位
  3. 设置背景色:设置属性字节(奇数字节)的4,5,6位
  4. 向上滚动一行:依次将n+1行的内容复制到第n行,最后一行置为空

宏指令

宏定义

1
2
3
4
5
6
macro_name macro [dummy parameter list]
[local label1, label2 ...]



endm
  • dummy parameter list 相当于是高级语言中的形式参数列表
  • 由local引导的是本地标签,在不同的地方调用时,本地标号会被展开成不同的唯一标号
  • 标号必须是macro定义体的第一行,严格来说,之间包括注释也不能插入

宏调用

1
macro_name [actual parameter list]

宏展开

  • 宏定义必须出现在调用之前
  • 汇编过程中,将宏展开编程实际的代码

宏汇编操作符

  1. &
    • 拼接指令,与C语言宏的##类似
  2. ;;
    • 在宏中使用的注释
  3. %
    • 计算表达式
    • 在汇编过程中,计算%后的表达式,并将计算结果加入后续运算
1
2
3
4
5
6
7
8
9
10
strg macro string
db '&string&'
endm


strg 25-1
==> db '25-1'

strg % 25-1
==> db '24'

宏库的建立和调用

宏库
macro.mac

调用
include macro.mac

  • 引入macro,mac文件中全部的宏

排除
purge macroA …

  • 制定宏名,剔除不需要的宏

rept expression
… ;重复块
endm

  • 根据表达式的计算结果,重复制定次数

irp x, <1,2,3,4,5,6,7,8,9,10>
db x
endm

  • x会依次带入后面的值
  • 可以是数字或者寄存器,字符串等

显存操作

显存结构

  • 显存分为8页,每页4KB
  • 显示器规格是每屏25行,每行80个字符
  • 每个字符占2B,低位是对应ASCII码,高位是显示属性
  • 每屏实际占用4000B,剩余96字节无效果
  • 显存地址空间为B8000H-BFFFFH

字符属性

位数 含义 含义
7 BL 闪烁
6 R 背景色
5 G 背景色
4 B 背景色
3 I 高亮
2 R 前景色
1 G 前景色
0 B 前景色

最后更新: 2024年04月15日 23:40

版权声明:本文为原创文章,转载请注明出处

原始链接: https://lizec.top/2017/12/05/%E6%B1%87%E7%BC%96%E8%AF%AD%E8%A8%80%E7%AC%94%E8%AE%B0/