汇编笔记-小甲鱼零基础汇编
汇编语言学习笔记(【汇编语言】小甲鱼零基础汇编)
[toc]
第一章 基础知识
学习汇编主要是:学习汇编的编程思想,掌握机器运行的思维 汇编语言是直接在硬件上工作的编程语言,首先要了解硬件系统的结构,才能有效的应用汇编语言对其编程。
- 汇编课程的研究重点 如何利用硬件系统的编程结构和指令集有效灵活的控制系统进行工作
- 汇编语言的主体是汇编指令
- 汇编指令和机器指令的差别在于指令的表示方法上 汇编指令是机器指令便于记忆的书写格式
- 汇编语言时机器指令的助记符
- 汇编语言的组成
- 汇编指令(机器码的助记符)
- 伪指令(由编译器执行)
- 其他符号(由编译器识别,如:+ - * /) 汇编语言的核心是汇编指令,他决定了汇编语言的特性
- CPU 对存储器的读写
CPU 要想进行数据的读写,必须和外部器件(即芯片)进行三类信息的交互
- 地址信息:存储单元的地址
- 控制信息:芯片的选择,读或写命令
- 数据信息:读或写的数据
第二章 寄存器(CPU 工作原理)
CPU=运算器+控制器+【寄存器】,器件之间通过总线相连 8086CPU 有 14 个寄存器,名称分别为: AX,BX,CX,DX,SI,DI,SP,BP,IP,CS,SS,DS,ES,PSW
1 通用寄存器
- 8086CPU 所有的寄存器都是 16 位的,可以存放 2 个字节
- AX、BX、CX、DX 通常用来存放一般性数据 被称为通用寄存器
- 8086 上一代 CPU 中的寄存器都是 8 位的,为了保证兼容性 这四个寄存器都是可以分为 2 个独立的 8 位寄存器使用 AX=AH+AL BX=BH+BL CX=CH+CL DX=DH+DL
- AX 的低 8 位(0-7)构成 AL 寄存器 高 8 位(8-15)构成了 AH 寄存器 AH 和 AL 寄存器是可以独立使用的 8 位寄存器
2 字在寄存器中的存储
8086 一个字 16 位
3 几条汇编指令
- 汇编指令不区分大小写
- 几条汇编指令 mov ax,18 ;AX=18 mov ah,78 ;AH=78 add ax,8 ;AX=AX+8 mov ax,bx ;AX=BX add ax,bx ;AX+=BX
- 用目前学过的汇编指令,最多使用四条指令,编程计算 2 的 4 次方 mov ax,2 ;ax=2 add ax,ax ;ax=4 add ax,ax ;ax=8 add ax,ax ;ax=16
4 物理地址
- CPU 访问内存单元时,要给出内存单元的地址。
- 所有的内存单元够成的存储空间是一个一维的线性空间
- 我们将这个唯一的地址称为物理地址
5 位结构的 CPU
16 位结构描述了一个淳朴具有以下几个方面特征:
- 运算器一次最多可以处理 16 位的数据
- 寄存器的最大宽度为 16 位
- 寄存器和运算器之间的通路是 16 位的
6 CPU 给出物理地址的方法
- 8086 有 20 位地址总线,可传送 20 位地址,实际上的寻址能力为 1M
- 8086 内部为 16 位结构,它只能传送 16 位的地址,理论上表现出的寻址能力却只有 64K
- 问题:8086CPU 如何用内部 16 位的数据转换成 20 位的地址?
- 8086CPU 采用一种在内部用两个 16 位地址合成的方法,来形成 20 位的物理地址 即:段地址+偏移地址=物理地址
- 地址加法器合成物理地址的方法: 物理地址=段地址 ×16+偏移地址
- “地址段 ×16”即是数据左移 4 位(二进制位的左移 4 位,十六进制的左移 1 位) 在地址加法器中,如何完成“段地址 ×16”? 二进制形式的段地址左移 4 位
7 “段地址 ×16+偏移地址=物理地址”的本质含义
- 即可以用两个 16 位的二进制数来表示一个 20 位的二进制数
- 8086CPU 中内部为 16 位结构,但地址线却是 20 位的,使用地址加法器可以把 16 位地址变成 20 位地址 具体操作就是:段地址 ×16+偏移地址
8 段的概念
- 内存并没有分段,段的划分来自于 CPU,由于 8086CPU 用“段地址 ×16+偏移地址=物理地址” 的方式给出内存单元的物理地址,使得我们可以用分段的方式来管理内存
- 以后,在编程时可以根据需要,将若干地址连续的内存单元看作一个段, 使用段地址 ×16 定位段的起始地址(基础地址),用偏移地址定位段中的内存单元
- 注意
- 段地址必然是 16 的倍数,即一个段的起始地址必然是 16 的倍数
- 偏移地址为 16 位,16 位地址的寻址能力为 64K,所以一个段的长度最大为 64K
- CPU 可以用不同的段地址和偏移地址形成同一个物理地址
9 段寄存器
- 段寄存器就是提供段地址的
8086CPU 有 4 个段寄存器:
- CS(code segment)
- DS(data segment)
- SS(stack segment)
- ES(extra segment)
- 当 8086CPU 要访问内存时,有这 4 个段寄存器提供内存单元的段地址
10 CS 和 IP
- CS 和 IP 时候 8086CPU 中最关键的寄存器 他们指示了 CPU 当前读取指令的地址。
- CS 和 IP 的含义 CS:代码段寄存器 IP:指令指针寄存器【专用寄存器】
- 8086CPU 工作过程的简要描述
- 从 CS:IP 指向内存单元,读取指令,读取的指令进入指令缓冲器
- IP=IP+所读取指令的长度,从而指向下一条指令
- 执行指令,转到步骤 1,重复这个过程
- 开机时的 CS 和 IP
- 在 8086CPU 加电启动或复位后(即 CPU 刚开始工作时)CS 和 IP 被设置为 CS=FFFFH,IP=0000H
- 即在 8086PC 机刚启动时,CPU 从内存 FFFF0H 单元中读取指令执行
- FFFF0H 单元中的指令是 8086PC 机开机后执行的第一条指令
- 修改 CS、IP 的指令
- 在 CPU 中,程序员能够【用指令读写】的部件只有【寄存器】, 程序员可以通过改变寄存器中的内容实现对 CPU 的控制
- CPU 从何处执行指令是由 CS、IP 中的内容决定的,程序员可以通过改变 CS、IP 中的内容 控制 CPU 执行目标指令
- 如何修改 CS 和 IP?
- 通过 mov 改变 AX 等,但是不能通过 mov 改变 CS 和 IP
- 【jmp 段地址:偏移地址】 可以用来同时修改 CS 和 IP 指令中的段地址修改 CS 偏移地址修改 IP
- 【jmp 某一合法的寄存器】 仅修改 IP 的内容 比如:jmp ax 或者 jmp bx(类似于 mov IP ax)
- jmp 是只具有一个操作对象的指令
11 代码段
- 可以将长度为 N(N<=64KB)的一组代码,存放在一组地址连续、其实地址为 16 的倍数的内存单元中 这段内存是用来存放代码的,从而定义了一个代码段
- CPU 中只认被 CS:IP 指向的内存单元中的内容为指令
12 实验一: 查看 CPU 和内存,用机器指令和汇编指令编程
- R 命令:查看、改变 CPU 寄存器的内容 r 后面加寄存器的名称可以改变 CPU 寄存器的内容
- D 命令:查看内存中的内容
- E 命令:改写内存中的内容
- U 命令:将内存汇总的机器指令翻译成汇编指令
- T 命令:执行一条机器指令
- A 命令:以汇编指令的格式在内存中写入一条机器指令
- debug 中输入的默认是 16 位数
- 空格数量任意
- 按 Q 可以退出
第三章 寄存器(内存访问)
13 内存中字的存储
- 任何两个地址连续的内存单元,N 号单元和 N+1 号单元,可以将他们看成两个存储单元 也可以看成一个地址为 N 的字单元中的高位字节单元和低位字节单元
- 注意:在内存的表示中,从高到低,是从 0 号单元开始,然后逐渐变大, 即在书写时,低位写在高的地方,高位写在低的地方, 如上图所示:4E20H 即是 0 号字节存储 20,1 号字节存储 4E
14 DS 和[address]
- 8086 中有一个 DS 寄存器,通常用来存放要访问的数据的段地址
- 例如:我们要读取 10000H 单元的内容可以用如下程序段进行:
mov bx,1000H
mov ds,bx
mov al,[0]
上面的三条指令将 10000H(1000:0)中的数据读到 al 中
- 复习:已知 mov 指令可以完成的两种传送功能
- 将数据直接送入寄存器
- 将一个寄存器中的内容送入另一个寄存器中
- 除此之外,mov 指令还可以将一个内存单元中的内容送入一个寄存器 mov 指令格式:mov 寄存器名,内存单元地址 [. . . ]表示一个内存单元,“[. . . ]”中的. . . 表示内存单元的【偏移地址】 执行指令时,8086CPU 自动取 DS 中的数据为内存单元的【段地址】
- 如何把 1000H 放入 DS 中? 要通过通用寄存器把段地址传入到 DS 中 8086CPU 不支持将数据直接送入段寄存器的操作,DS 是一个段寄存器 即:mov ds,1000H 是非法的 数据->通用寄存器->段寄存器
- 复习:已知 mov 指令可以完成的两种传送功能
- 写几条指令,将 AL 中的数据送入内存单元 10000H? mov bx,1000H mov ds,bx mov [0],al ;al 中的字节型数据送入到 1000H:0 中
15 字的传送
- 8086CPU 是 16 位结构,有 16 根数据线,所以可以一次性传送 16 位的数据 即:一次可以传送一个字
- 比如 mov bx,1000H mov ds,bx mov ax,[0] ;1000H:0 处的字型数据送入 ax 中 mov [0],cx ;cx 中的 16 位数据送入到 1000H:0 中
16 mov、add、sub 指令
- 复习:已学 mov 指令的几个形式
- mov 寄存器,数据 ;立即寻址
- mov 寄存器,寄存器 ;寄存器寻址
- mov 寄存器,内存单元 ;直接寻址
- mov 内存单元,寄存器 ;寄存器寻址?
- mov 段寄存器,寄存器 ;寄存器寻址
- mov 寄存器,段寄存器 ;寄存器寻址
- add、sub 同 mov 一样,都有两个操作对象
- add 的用法
- add 寄存器,数据 ;立即寻址
- add 寄存器,寄存器 ;寄存器寻址
- add 寄存器,内存单元 ;直接寻址
- add 内存单元,寄存器 ;
- sub 的用法
【不带借位的减法】
指令格式 sub op1,op2 ;意为:op1=op1-op2
- sub 寄存器,数据 ;立即寻址
- sub 寄存器,寄存器 ;寄存器寻址
- sub 寄存器,内存单元 ;直接寻址
- sub 内存单元,寄存器 ;
- add 的用法
17 数据段
- 如何访问数据段中的数据?
- 将一段内存当作数据段,是我们在编程时的一种安排
- 具体操作:用 DS 存放数据段的段地址,再根据需要,用相关指令访问数据段中的具体单元
18 栈
- 8086CPU 提供相关的指令来以栈的方式访问内存空间 这意味着,我们在基于 8086CPU 编程的时候,可以将一段内存当作栈来使用
- 8086CPU 提供入栈和出栈指令:(最基本的)
push(入栈)
pop(出栈)
- push ax:将寄存器 ax 中的数据送入栈中
- pop ax:从栈顶取出数据送入 ax
- 8086CPU 的入栈和出栈操作都是以【字(16 位)】为单位进行的
- pop 和 push 可以在寄存器和内存之间传送数据
- CPU 如何知道一段内存空间被当做栈使用?
- 8086CPU 中,有两个寄存器
- 段寄存器 SS:存放栈顶的段地址
- 寄存器 SP:存放栈顶的偏移地址【专用寄存器】
- 任意时刻 SS:SP 指向栈顶元素,当栈为空的时候,也就不存在栈顶元素 ss:sp 也就指向栈最高地址单元的下一个单元
- 8086CPU 中,有两个寄存器
- 执行 push 和 pop 的时候,如何知道哪个单元是栈顶单元?
- 执行 push ax 时
- sp=sp-2
- 将 ax 中的内容送入到 ss:sp 指向的内存单元 ss:sp 此时指向新栈顶
- 执行 pop ax 时
- 将 ss:sp 指向的内存单元的内容送入到 ax 中 注意:这里取出的内容在内存中还是存在的,并没有被重置 下一轮 push 会覆盖
- sp=sp+2
- 执行 push ax 时
- 如果栈是空的,sp 指向哪里? sp 指向最高地址单元的下一个单元
19 栈顶超界的问题
ss、sp 只记录了栈顶的地址,依靠 ss、sp 可以保证在入栈和出栈时找到栈顶 可以,如何能够保证在入栈、出栈时,栈顶不会超出栈空间?
- 8086CPU 不保证栈的操作不会越界
- 当栈空的时候,再执行 pop 出栈 或者 当栈满的时候再使用 push 入栈 都会发生栈顶超界问题,会操作到栈以外的数据, 这些数据可能是其他用途的数据或者代码 栈顶超界是危险的!!!
- 8086CPU 没有记录栈顶上下限的寄存器
20 栈段
- 将一段内存当做栈段,仅仅是我们在编程时的一种安排,
- ss:sp 指向我们定义的栈段的栈顶;
- 当栈空时,sp 指向最高地址的下一个单元
- 思考:一个栈段最大可以设为多少? 64KB
- 设栈顶的变化范围是 0-FFFFH,从栈空时 sp=0(最高地址单元 FFFFH 的下一个单元 0000H) 一直压栈,直到栈满,sp=0; 如果再次压栈,栈顶将环绕,覆盖原来栈中的内容
- 一段内存,既可以是代码的存储空间,又可以是数据的存储空间,还可以是栈空间 也可以是什么都属实。 关键在于 CPU 中寄存器的设置,即:cs、ip、ss、sp、ds 的设置 **可以通过 mov 直接给 sp 赋值【立即数寻址】,但是不能通过 mov 给 cs、ip、ss、ds 赋值 给 cs 和 ip 赋值需要使用 jum 指令 给 ss 和 ds 赋值需要使用 mov ss 或 ds,寄存器 ;【寄存器寻址】
第四章 第一个汇编程序
21 一个源程序从写出到执行的过程
1. 一个汇编语言程序从写出到最终执行的简要过程
编写->编译连接->执行
2. 对源程序进行编译连接
1. 使用汇编语言编译程序(MASM.EXE)对源程序文件中的源程序进行编译,产生目标文件【.obj文件】
2. 再用连接程序(LINK.EXE)对目标文件进行连接,生成可在操作系统中直接运行的可执行文件【.EXE文件】。
3. 可执行文件包含两部分内容
1. 程序(从源程序的汇编指令翻译过来的机器码)和数据(源程序中定义的数据)
2. 相关的描述信息(比如:程序有多大、要占多少内存空间等)
4. 执行可执行文件中的程序
1. 在操作系统(如:MSDOS)中,执行可执行文件中的程序
2. 操作系统依照可执行文件中的描述信息,将可执行文件中的机器码和数据加载入内存
并进行相关的初始化(比如:设置CS:IP指向第一条要执行的指令),然后由CPU执行程序
22 源程序的主要结构
源程序由 汇编指令+伪指令+宏指令 组成 伪指令:编译器处理 汇编指令:编译为机器码
- 伪指令
- 没有对应的机器码的指令,不能由 CPU 直接执行
- 伪指令是由编译器来执行的指令,编译器根据伪指令来进行相关的编译工作
- segment 和 ends【定义一个段】
- segment 和 ends 是一对成对使用的伪指令
- 编写汇编程序【必须】使用到的指令
- segment 和 ends 的功能是定义一个段 segment:说明一个段开始 ends:说明一个段结束
- 一个段必须有一个名称来标识,使用格式为 段名 segment 段名 ends
- 一个汇编程序由多个段组成 这些段用来存放【代码、数据、或当作栈空间】来使用 一个有意义的汇编程序至少要有一个段,这个段用来存放代码。
- end【真正的没了】
- end 是一个汇编程序的结束标记
- 编译器在编译汇编程序的过程中,如果碰到了伪指令 end,就结束对源程序的编译
- 如果程序写完了,要在结尾处加上伪指令 end 否则,编译器无法知道程序在何处结束
- 【切记】不要把 end 和 ends 搞混了 end:汇编程序的结束标记 ends:与 segment 成对出现
- assume【寄存器和段的关联假设】
- 它假设某一段寄存器和程序中的某一个用 segment…ends 定义的段相关联
- 通过 assume 说明这种关联,在需要的情况下, 编译程序可以将段寄存器和某一具体的段相联系
- 程序和源程序
- 我们将源程序文件中的所有内容称为【源程序】
- 将源程序中最终由计算机执行处理的指令或数据称为【程序】
- 程序最先以汇编指令的形式,存储在源程序中 然后经过编译、连接后转变为机器码,存储在可执行文件中
- 标号,标号与段名称有所区别
- 一个标号指代了一个地址,即是段名称。
- 段名称 放在 segment 的前面,作为一个段的名称 这个段的名称最终将被汇编、连接程序处理为一个段的段地址
- DOS 中的程序运行
- DOS 是一个单任务操作系统
- 一个程序结束后,将 CPU 的控制权交还给是他得以运行的程序 我们称这个过程为:程序返回
- 程序返回 mov ax,4c00H int 21H ;【中断机制】是 DOS 最伟大的机制,Windows 系统上是【消息机制】 这两条指令所实现的功能就是程序返回
- 几个和结束相关的内容
- 段结束:伪指令 通知编译器一个段的结束【ends】
- 程序结束:伪指令 通知编译器程序的结束【end】
- 程序返回:汇编指令 mov ax,4c00H int 21H
- 语法错误和逻辑错误
- 语法错误
- 程序在编译时被编译器发现的错误
- 容易发现
- 逻辑错误
- 在编写时不会表现出来的错误、在运行时会发生的错误
- 不容易发现
- 语法错误
23 以简化的方式进行汇编和连接
汇编使用的程序:masm. exe 连接使用的程序:link.exe 简化方式进行汇编和连接的程序:ml.exe
24 汇编和连接的作用
连接的作用
- 当源程序很大时,可以将他们分成多个源程序文件夹编译 每个源程序编译成为目标文件后,再用连接程序将它们连接在一起, 生成一个可执行文件
- 程序中调用了某个库文件中的子程序,需要将这个库文件和该程序生成的目标文件连接到一起 生成一个可执行文件
- 一个源程序编译后,得到了存有机器码的目标文件,目标文件中的有些内容还不能直接 用来生成可执行文件,连接程序将这些内容处理为最终的可执行信息。 所以在只有一个源程序文件,而又不需要调用某个库中的子程序的情况下,也必须用 连接程序对目标文件进行处理,生成可执行文件
25 可执行文件中的程序装入内存并运行的原理
- 在 DOS 中,可执行文件中的程序 P1 若要运行,必须有一个正在运行的程序 P2 将 P1 从可执行文件中加载入内存,将 CPU 的控制权交给 P1,P1 才能得以运行
- 当 P1 运行完毕后,应该将 CPU 的控制权交还给使他得以运行的程序
- 操作系统的外壳
- 操作系统是由多个功能模块组成的庞大、复杂的软件系统 任何通用的操作系统,都需要提供一个称为 shell(外壳)的程序, 用户(操作人员)使用这个程序来操作计算机系统工作
- DOS 中有一个程序 command.com,这个程序在 DOS 中称为命令解释器 也就是 DOS 系统的 shell
- 执行可执行文件 1.exe 时, (1)什么程序将 CPU 的控制权交给了 1.exe? (2)将程序 1.exe 加载入内存后,如何使程序得以运行? (3)1.exe 程序运行结束后,返回到了哪里? 1. 在 DOS 中直接执行 1.exe 时,是正在运行的 cmd.exe 将 1.exe 中的程序加载入内存 2. cmd.exe 设置 CPU 的 CS:IP 指向程序的第一条指令(即,程序的入口) 从而使程序得以运行 3. 程序运行结束后,返回 cmd.exe 中,CPU 继续运行 cmd.exe 【实验三】
第五章 【bx】和 loop 指令
26 [bx]
1. 和[0]类似,[0]表示内存单元,它的偏移地址是0;
2. [bx]同样也表示一个内存单元,它的段地址在DS中
它的偏移地址在bx中,至于是取字还是取字节,
要看他放入的寄存器是8位还是16位
3. 补充:inc指令:相当于C语言中的++运算符
27 Loop 指令
这个指令和循环有关
1. 指令格式:loop 标号
CPU执行loop指令的时候,要进行两步操作
1. (cx)=(cx)-1;
2. 判断cx中的值,若不为零,则转至标号处执行程序
若为零,则向下执行。
2. 通常,loop指令实现循环,cx中存放循环的次数
3. 标号
在汇编语言中,标号代表了一个地址,标号标识了一个地址
4. 使用cx和loop指令相配合实现循环功能的三个要点
1. 在cx中存放循环次数
2. loop指令中的标号所标识地址要在前面
3. 要循环执行的程序段,要写在标号和loop指令的中间
5. 用cx和loop指令相配合实现循环功能的程序框架
mov cx,循环次数
S:循环执行的程序段
loop s
28 在 Debug 中跟踪供 loop 指令实现的循环程序
**注意:在汇编程序中,数据不能以字母开头,如果要输入像FFFFH这样的数
则要在前面添加一个0
在debug程序中引入G命令和P命令
1. G命令
G命令如果后面不带参数,则一直执行程序,直到程序结束
G命令后面如果带参数,则执行到ip为那个参数地址停止
2. P命令
T命令相当于单步进入(step into)
P命令相当于单步通过(step over)
29 Debug 和汇编编译器 Masm 对指令的不同处理
1. 在debug中,可以直接用指令 mov ax,[0] 将偏移地址为0号单元的内容赋值给ax
2. 但通过masm编译器,mov ax,[0] 会被编译成 mov ax,0
1. 要写成这样才能实现:mov ax,ds:[0]
2. 也可以写成这样:
mov bx,0
mov ax,[bx] ;或者mov ax,ds:[bx]
30 loop 和[bx]的联合应用
- 计算 ffff:0~ffff:b 单元中的数据的和,结果存储在 dx 中
- 注意两个问题
- 12 个 8 位数据加载一起,最后的结果可能会超出 8 位(越界),故要用 16 位寄存器存放结果
- 将一个 8 位的数据加入到 16 位寄存器中,类型不匹配,8 位的数据不能与 16 位相加
- 【解决办法】 把原来 8 位的数据,先通过通用寄存器 ax,将它们转化成 16 位的
- 代码如下
- 注意两个问题
assume cs:codesg
codesg segment
start:
;指定数据段
mov ax,0ffffh
mov ds,ax
;初始化
mov ax,0
mov dx,0
mov bx,0
;指定循环次数,12次
mov cx,0ch
circ:
;把8位数据存入al中,即ax中存放的是[bx]转化之后的16位数据,前8位都是0
mov al,[bx]
;进行累加
add dx,ax
;bx自增,变化内存的偏移地址
inc bx
loop circ
;程序返回
mov ax,4c00h
int 21H
codesg ends
end start
31 段前缀
- 指令“mov ax,[bx]”中,内存单元的偏移地址由 bx 给出,而段地址默认在 ds 中
- 我们可以在访问内存单元的指令中显式地给出内存单元的段地址所在的段寄存器 比如 mov ax,ds:[0] mov ax,ds:[bx] 这里的 ds 就叫做【段前缀】
32 一段安全的空间
- 8086 模式中,随意向一段内存空间写入内容是很危险的 因为这段空间中可能存放着【重要的系统数据或代码】
- 在一般的 PC 机中,DOS 方式下,DOS 和其他合法的程序一般都不会使用【0:200~0:2FF】 的 256 个字节的空间。所以,我们使用这段空间是安全的
第六章 包含多个段的程序
33 在代码段中使用数据
- dw 的含义【定义字型数据:define word,16 字节】 在数据段中使用 dw 定义数据,则数据在数据段中 在代码段中使用 dw 定义数据,则数据在代码段中 堆栈段也是一样
- 在程序的第一条指令前加一个标号 start,并且这个标号在伪指令 end 后面出现 可以通知编译器程序在什么地方结束,并且也可以通知编译器程序的入口在哪里
34 在代码段中使用栈
**补充:如果题目要求【逆序】存放,就要想到栈(FILO) 使用 dw 向系统申请一段空间,然后把这个空间当做栈
35 将数据、代码、栈放入不同的段
- 在前面的 6. 1 和 6. 2 中,我们在程序中用到了数据和栈,我们在编程的时候要注意 何处是数据,何处是栈、何处是代码
- 这样做显然有两个问题
- 把他们放在一个段中是程序显得混乱
- 前面程序中处理的数据很少,用到的栈空间也小,放在一个段里面没有问题 但数据、栈、代码需要的空间超过 64KB,就不能放在一个段中 (8086 中一个段的容量不能大于 64KB)
- 我们可以和定义代码段一样的方法来定义多个段 然后在这些段里面定义需要的数据,或通过定义数据来取得栈空间
- 将数据、代码、栈放入不同的段
- 我们可以在源程序中为这三个段起具有含义的名称 用来存放数据的段,我们将其命名为“data” 用来存放代码的段,我们将其命名为“code” 用来作栈空间的段,我们将其命名为“stack” 但是 CPU 看得懂吗?【不能】
- 我们在源程序中用伪指令 “assume cs:code,ds:data,ss:stack”将 cs、ds 和 ss 分别和 code、data、stack 段相连 这样做了之后,CPU 是都就会将 cs 指向 code,ds 指向 data,ss 指向 stack 从而按照我们的意图来处理这些段呢?【不能】 伪指令 CPU 看不懂,伪指令是给编译器看的
- 若要 CPU 按照我们的安排行事,就要用机器指令控制它,源程序中的汇编指令 才是 CPU 要执行的内容 需在在 code 段中给 DS,CS、SS 设置相应的值才能让 CPU 识别出数据段、代码段、堆栈段 其中汇编程序开始的地方(即代码段开始的地方)由 end 后面的标号所指向的地方给出
- assume 指令不可省略,至于为什么,需要以后多多体会
36 【实验五】
- 如果段中的数据占 N 个字节,则程序加载后,这段实际占有的空间为:N%16==0?N:16×(N/16+1); 因为一个段最小占用 16 字节,即有 16 个字节只有这个段可以访问到
- 在编辑源程序的时候,如果调换各个段的编写位置,最后 CS、DS、SS 的值会发生变化
- 如果去掉 start,编译器会从上到下执行,如果第一个段是代码段,则可以正常运行 若第一个段不是代码段,则不会正常运行
- 代码示例 1
assume cs:code,ds:data,ss:stack
;数据段
data segment
;8个数据
dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
data ends
;栈段
stack segment
;8个数据
dw 0,0,0,0,0,0,0,0
stack ends
;代码段
code segment
start:
;栈空间初始化
mov ax,stack
mov ss,ax
mov sp,16
;数据段初始化
mov ax,data
mov ds,ax
push ds:[0];一个栈单元是一个字
push ds:[2]
;存放数据不会改变
pop ds:[2]
pop ds:[0]
;程序返回
mov ax,4c00h
int 21h
code ends
end
- 将 a,b 数据段中的内容分别相加,结果放入 data 数据段中
assume cs:code
;数据段
a segment
db 1,2,3,4,5,6,7,8
a ends
;数据段
b segment
db 1,2,3,4,5,6,7,8
b ends
;数据段
data segment
db 0,0,0,0,0,0,0,0
data ends
;代码段
code segment
start:
mov bx,0
mov ax,0
mov dx,a
mov ss,dx
mov dx,b
mov es,dx
mov dx,data
mov ds,dx
mov cx,8
circ:
add al,ss:[bx]
add al,es:[bx]
mov [bx],al
inc bx
mov al,0
loop circ
;程序返回
mov ax,4c00h
int 21h
code ends
end start
- 将 a 数据段中的前 8 个字型数据逆序存储到 b 段中
assume cs:code
a segment
dw 1,2,3,4,5,6,7,8,9,0ah,0bh,0ch,0dh,0eh,0fh,0ffh
a ends
b segment
dw 0,0,0,0,0,0,0,0
b ends
code segment
start:
mov ax,0
mov ax,a
mov ss,ax
mov sp,0
mov ax,0
mov ax,b
mov ds,ax
mov bx,0
mov cx,8
circ:
pop [bx]
add bx,2
loop circ
mov ax,4c00h
int 21h
code ends
end start
第七章 更灵活地定位内存地址
本章主要讲解一些更灵活的定位内存地址的方法和相关的编程方法
37 and 和 or 指令
- and 指令:逻辑与指令,按位进行与运算
- 如:mov al,01100011B and al,00111011B 执行后: al=00100011B
- 通过 and 指令可将操作对象的相应位设为 0,其他位保持不变 例如 al 的第 6 位设为 0:and al,10111111B 例如 al 的第 7 位设为 0:and al,01111111B 例如 al 的第 0 位设为 0:and al,11111110B
- or 指令,逻辑或运算,按位进行或运算
- 如:mov al,01100011B or al,00111011B 执行后: al=01111011B
- 通过该指令可将操作对象的相应位设为 1,其他位不变 or al,01000000B;将 al 的第 6 位设为 1 or al,10000000B;将 al 的第 7 位设为 1 or al,00000001B;将 al 的第 0 位设为 1
38 关于 ASCII 码
一种编码方案,在计算机系统中通常被采用,8 位
39 以字符形式给出的数据
1. 在汇编程序中,可以使用'×××'的方式指明数据是以字符的形式给出的
2. 编译器会将它们转化为相应的ASCII码
3. 例如
1. db 'unIX' ;相当于:db 75H,6EH,49H,58H
'u'、'n'、'I'、'X'的ASCII码分别为75H,6EH,49H,58H
2. mov al,'a' ;相当于:mov al,61H
'a'的ASCII码为61H
4. ASCII码中,大写字母和小写字母之间的规律
小写字母=大写字母+32
小写字母=大写字母+20H
大写字母从41H开始排,小写字母从61H开始排
大写 | 二进制 | 小写 | 二进制 |
---|---|---|---|
A | 01000001 | a | 01100001 |
B | 01000010 | b | 01100010 |
C | 01000011 | c | 01100011 |
D | 01000100 | d | 01100100 |
40 大小写转换的问题
1. 方案一:
1. 识别出是该字节是表示一个的大写英文字符,还是小写的
用于条件判断的汇编程序,目前还没有学到
2. 根据+20H 或者 -20H进行大小写转换
2. 方案二:
1. 若全部转化为大写,则将第5位置0
and al,11011111B
2. 若全部转化为小写,则将第5位置1
or al,00100000B
41 [bx+常数]
mov ax,[bx+200]的含义:
1. 将一个内存单元的内容送入ax,这个内存单元的长度为2字节,存放一个入一个子单元
该字单元的偏移地址为bx中的数值加上200,段地址在ds中
2. 也可以写成
1. mov ax,200[bx]
2. mov ax,[bx]. 200
42 用[bx+idata]的方式进行数组的处理
在codesg中填写代码,将datasg中定义的第一个字符串转化为大写,第二个字符串转化为小写
1. 我们观察datasg段中的两个字符串,一个的起始地址为0,另一个的起始地址为5
2. 我们可以将这两个字符串看作两个数组,一个从0地址开始存放,另一个从5开始存放
3. 我们可以用[0+bx]和[5+bx]的方式在同一个循环中定位这两个字符串中的字符
4. 注意这个数组的定位方式,对比C语言
C语言的数组定位方式:a[i],b[i], a、b是地址常量
汇编语言的数组定位方式:0[bx],5[bx]
所以:[bx+常数]的方式为高级语言实现数组提供了便利的机制
assume cs:codesg,ds:datasg
datasg segment
db 'BaSiC'
db 'MinIX'
datasg ends
codesg segment
start:
mov ax,datasg
mov ds,ax
mov bx,0
mov cx,5 ;做5次循环
circ:
mov al,[bx]
and al,11011111b
mov [bx],al
mov al,[bx+5];等价于mov al,5[bx];等价于mov al,[bx].5
or al,00100000b
mov 5[bx],al
inc bx
loop circ
mov ax,4c00h
int 21h
codesg ends
end start
43 SI 和 DI
已经学过的 10 个寄存器:AX、BX、CX、DX、DS、CS、SS、ES、IP、SP
- SI 和 DI 是 8086CPU 中和 bx 功能相近的寄存器 bx 不够用,所以引进了 SI 和 DI
- SI 和 DI(16 位)不能够分成两个 8 位寄存器来使用【和 bx 的区别】
- 下面三组指令实现了相同的功能
- mov bx,0 mov ax,[bx]
- mov si,0 mov ax,[si]
- mov di,0 mov ax,[di]
- 下面三组指令也实现了相同的功能
- mov bx,0 mov ax,[bx+123]
- mov si,0 mov ax,[si+123]
- mov di,0 mov ax,[di+123]
- 用寄存器 SI 和 DI 实现将字符串’welcome to masm!‘复制到它后面的数据区中 通常用 ds:si 指向要复制的源始字符串 通常用 ds:di 指向要复制的目的空间 **注意 si、di 是 16 位寄存器,循环中自增时,应该+2
assume cs:code,ds:data
data segment
db 'welcome to masm!'
db '................'
data ends
code segment
start:
mov ax,data
mov ds,ax
mov si,0
mov di,16
mov cx,8
circ:
mov ax,0[si]
mov [di],ax
inc di
inc di
inc si
inc si
loop circ
mov ax,4c00h
int 21h
code ends
end start
44 [bx+si]和[bx+di]
- [bx+si]和[bx+di]的含义类似,我们以[bx+si]为例进行讲解 [bx+si]表示一个内存单元,它的偏移地址为 bx 中的数值加上 si 中的数值 它的偏移地址在 ds 中
- [bx+si]也可以写成[bx][si]
45 [bx+si+常数]和[bx+di+常数]
- 以[bx+Si+常数]为例讲解 [bx+si+常量]表示一个内存单元,偏移地址为 bx 的值+si 的值+常数
- 指令 mov ax,[bx+si+常数]也可以写成如下形式
- mov ax,200[bx+si]
- mov ax,200[bx][si]
- mov ax,[bx]. 200[si]
46 不同的寻址方式的灵活应用
- 总结几种定位内存的方法
- ds:[常数] 【直接寻址】 用一个常量来表示地址,可用于直接定位一个内存单元
- [bx] 【寄存器间接寻址】 用一个寄存器的值来表示内存地址,可以间接定位一个内存单元
- [bx+常数] 【??】 用一节寄存器的值和常量表示内存地址,可在一个起始地址的基础上用变量间接定位一个内存单元
- [bx+si]
- [bx+si+常数]
- 编程,给定数据段 data,将 data 段中每个单词的头一个字母改写成大写字母
assume cs:code,ds:data
data segment
db '1. file '
db '2. edit '
db '3. search '
db '4. view '
db '5. options '
db '6. help '
data ends
code segment
start:
mov ax,data
mov ds,ax
mov bx,0
mov cx,6
circ:
mov al,[bx+3]
and al,11011111b
mov [bx+3],al
add bx,16
loop circ
mov ax,4c00h
int 21h
code ends
end start
- 编程,给定数据段 data,将 data 段中的每个单词改为大写字母
- 【loop 指令 cx-1 之后,在判断是否为 0】
- 双重循环用汇编怎么实现? 应该在每次开始内循环的时候,将外层循环的 cx 的值保存起来, 在执行外层循环的 loop 指令前,在恢复外层循环的 cx 数值。 **可以用寄存器来临时保存,也可以用栈空间(内存)保存【没有多余的寄存器】 更好的方法是使用:栈
46.1 使用寄存器实现
assume cs:code,ds:data
data segment
db 4,4,6,4,7,4;单词的字母数
db ' ';补齐
db '1. file '
db '2. edit '
db '3. search '
db '4. view '
db '5. options '
db '6. help '
data ends
code segment
start:
mov ax,data
mov ds,ax
mov bx,16
mov si,0
mov di,0
mov cx,6;外层循环6次
outer:;外层循环
mov dx,cx;用寄存器将外层循环的次数保存,C语言中是用栈来保存的
mov cx,0
mov cl,[di];内循环的次数
inner:;内层循环
mov al,[bx][si+3]
and al,11011111b
mov [bx][si+3],al
inc si
loop inner
add bx,16
mov si,0
inc di
mov cx,dx;恢复外层循环的次数
loop outer
mov ax,4c00h
int 21h
code ends
end start
46.2 使用栈实现【更好的方法】
assume cs:code,ds:data,ss:stack
data segment
db 4,4,6,4,7,4;单词的字母数
db ' ';补齐
db '1. file '
db '2. edit '
db '3. search '
db '4. view '
db '5. options '
db '6. help '
data ends
stack segment
dw 1,2,3,4,5,6,7,8
stack ends
code segment
start:
mov ax,data
mov ds,ax
mov ax,stack
mov ss,ax
mov sp,16
mov bx,16
mov si,0
mov cx,6;外层循环6次
outer:;外层循环
push cx;将外层循环的次数保存
mov cx,0
mov cl,[di];内循环的次数
inner:;内层循环
mov al,[bx][si+3]
and al,11011111b
mov [bx][si+3],al
inc si
loop inner
add bx,16
mov si,0
inc di
pop cx;恢复外层循环的次数
loop outer
mov ax,4c00h
int 21h
code ends
end start
第八章 数据处理的两个基本问题
本章对前面的所有内容是具有总结性的 计算机是进行数据处理、运算的机器,那么有两个基本的问题就包含在其中:
- 处理的数据在什么地方?
- 要处理的数据有多长? 这两个问题,在机器指令中必须给以明确或隐含的说明,否则计算机就无法工作
47 bx、si、di、bp
- 在 8086CPU 中,只有这 4 个寄存器(bx、bp、si、di)可以用在“[. . . ]” 中,用来进行内存单元的寻址
- 在“[. . . ]”中,这四个寄存器(bx、bp、si、di)可以单个出现,
或者只能以以下 4 种组合出现
- bx 和 si
- bx 和 di
- bp 和 si
- bp 和 di
- 错误的用法 mov ax,[bx+bp] mov ax,[si+di]
- 只要在[. . . ]中使用寄存器 bp,则指令中没有显性给出段地址,那么 段地址就默认在 ss 中,比如: mov ax,[bp] ax 的值为栈空间中,偏移地址为 bp 的内存单元 mov ax,[bp+常数] mov ax,[bp+si] mov ax,[bp+si+常数]
48 机器指令处理的数据所在的位置
- 绝大部分机器指令进行数据处理的指令大致可分为 3 大类 读取、写入、运算
- 在机器指令这一层,并不关心数据的值是多少,而关心指令执行前一刻 它将要处理的数据所在的位置
- 指令在执行前,所要处理的数据可以在三个地方 CPU 内部(寄存器)、内存、端口
49 汇编语言中数据位置的表达
汇编语言中用三个概念来表达数据的位置
- 立即数
- 寄存器
- 段地址(SA)和偏移地址(EA)
- 存放段地址的寄存器可以是默认的, 既可以是默认在 ds 中,也可以是在 ss 中(使用 bp 寄存器)
- 存放段地址的寄存器也可以显性的给出 mov ax,ds:[bp] mov ax,es:[bx] mov ax,ss:[bx+si] mov ax,cs:[bx+si+8]
50 寻址方式
51 指令要处理的数据有多长?
- 8086CPU 的指令,可以处理两种尺寸的数据,byte 和 word 所以在机器指令中要指明,指令进行的是字操作还是字节操作
- 8086CPU 确定数据长度的几种方法
- 通过寄存器名指明要处理的数据的尺寸 mov al,1 ;指明数据是字节型的 mov bx,ds:[0] ;指明数据是字型的
- 在没有寄存器名存在的情况下,用操作符 X ptr 指明内存单元的长度
X 在汇编指令中可以为 word 或 byte
- 下面的指令中,用 byte ptr 指明了指令访问的内存单元是字节型单元 mov byte ptr ds:[0],1 inc byte ptr [bx] inc byte ptr ds:[0] add byte ptr [bx],2
- 下面的指令中,用 word ptr 指明了指令访问的内存单元是字型单元 mov word ptr ds:[0],1 inc word ptr [bx] inc word ptr ds:[0] add word ptr [bx],2
- 其他方法 有些指令默认了访问的内存单元类型 pop、push 指令,一定是字型数据
- 在没有寄存器参与的内存单元访问指令中,用 word ptr 或者 byte ptr 显性地指明所要访问的内存单元的长度,是非常有必须要的 否则,CPU 无法得知所要访问的单元是字单元,还是字节单元
52 寻址方式的综合应用
53 div 指令
- div 是除法指令(division),使用 div 作除法的时候,要求
- 除数:8 位或 16 位,在寄存器或内存单元中
- 被除数:(默认)放在 AX 或 DX 和 AX 中
- 除数与被除数的相互关系 除数 被除数 8 位 16 位(AX) 16 位 32 位(DX+AX)
- 结果存放的位置 运算 8 位 16 位 商 AL AX 余数 AH DX
- div 指令格式
- div 寄存器
- div 内存单元 除数是寄存器或内存单元的内容
- div 指令示例
- div byte ptr ds:[0] ;被除数是 16 位,除数是 ds:[0]的内容(8 位) 含义:(al)=(ax)/((ds)*16+0)的商 (ah)=(ax)/((ds)*16+0)的余数
- div word ptr es:[0] ;被除数是 32 位,除数是 es:[0]的内容(16 位) 含义:(ax)=[(dx)*10000H+(ax)]/((es)*16+0)的商 (dx)=[(dx)*10000H+(ax)]/((es)*16+0)的余数
- 利用除法指令计算 100001/100
- 被除数 100001 大于 65535,要使用 dx 和 ax 两个寄存器联合存放 即说要进行的 16 位的除法
- 除数 100 小于 255,可以在一个 8 位寄存器中存放,但是,因为被除数是 32 位 除数应为 16 位,所以要用 16 位寄存器来存放除法 100
- 现将 100001 表示成十六进制数:186A1H,即 dx 中存放 1H,ax 中存放 86A1H
mov dx,1
mov ax,86A1H
mov bx,100
div bx ;默认除数是16位的
54 伪指令 dd
- dd 是用来定义双字型数据的
- 示例 data segment db 1 ;字节型数据 dw 1 ;字型数据 dd 1 ;双字型数据 data ends
- 已知 data 段数据,用 div 计算 data 中第一个数据除以第二个数据后的结果, 商存放在第 3 个数据的内存单元中
assume cs:code,ds:data
data segment
dd 100001
dw 100
dw 0
data ends
code segment
start:
mov ax,data
mov ds,ax
mov bx,0
mov ax,[bx] ;低位存放在ax中
mov dx,[bx+2] ;高位存放在dx中
div word ptr [bx+4]
mov [bx+6],ax ;商存放在ax中,把ax中的内容放入内存中
mov ax,4c00h
int 21h
code ends
end start
55 dup
- dup 是一个操作符,在汇编语言中,同 db、dw、dd 等一样,也是有编译器识别处理的符号
- dup 和 db、dw、dd 等数据定义伪指令配合使用的,用来进行数据的重复
- dup 示例
- db 3 dup(0) ;定义了 3 个字节,他们的值都是 0
- db 3 dup(0,1,2) ;定义了 9 个字节,他们是 0、1、2、0、1、2、0、1、2
- db 3 dup(‘abc’,‘ABC’) ;定义了 18 个字节,相当于 db’abcABCabcABCabcABC’
- dup 的使用格式 db 重复的次数 dup(重复的字节型数据) dw 重复的次数 dup(重复的字型数据) dd 重复的次数 dup(重复的双字型数据)
【实验七】 没调试成功
assume cs:code,ds:data,ss:stack,es:table
stack segment
;空栈时,sp指向16
dw 8 dup(0)
stack ends
data segment
;表示21年的21个字符串
;起始地址0,终止地址21*4-1:83
db '1975','1976','1977','1978','1979','1980','1981','1982','1983'
db '1984','1985','1986','1987','1988','1989','1990','1991','1992'
db '1993','1994','1995'
;表示21年公司总收入的21个双字型数据
;起始地址21*4:84,终止地址21*4+21*4-1:167
dd 16,22,382,1356,2390,8000,16000,24486,50065,97479,140417,197514
dd 345980,590827,803530,1183000,1843000,2759000,3753000,4649000,5937000
;表示21年公司雇员人数的21个字型数据
;起止地址21*8:168,终止地址21*8+21*2-1:209
dw 3,7,9,13,28,38,130,220,476,778,1001,1442,2258,2793,4037,5635,8226
dw 11542,14430,15257,17800
data ends
table segment
db 21 dup('year summ ne ?? ')
table ends
code segment
start:
mov ax,data
mov ds,ax
mov ax,table
mov es,ax
mov ax,stack
mov ss,ax
mov sp,16
mov si,0
mov di,0
mov bx,0
mov bp,0
mov cx,21
outer:
push si
add si,si
mov ax,ds:[bp]
mov es:[bx][di],ax
mov ax,ds:84[bp]
mov es:[bx][di+5],ax
pop si
mov al,168[si]
mov es:[bx][di+10],al
inc si
add di,2
push si
add si,si
mov ax,ds:[bp]
mov es:[bx][di],ax
mov ax,ds:84[bp]
mov es:[bx][di+5],ax
pop si
mov al,168[si]
mov es:[bx][di+10],al
inc si
add di,2
add bx,16
loop outer
mov ax,4c00h
int 21h
code ends
end start
第九章 转移指令的原理
8086CPU 的转移指令分为以下几类:
- 无条件跳转指令(如:jmp)
- 条件跳转指令
- 循环指令(如:loop)
- 过程,就像 C 语言中的函数
- 中断
56 操作符 offset
操作符 offset 在汇编语言中由编译器处理,它的功能是取标号的偏移地址 如:s:mov ax,offset s
57 jmp 指令
- 无条件转移,可以只修改 ip,也可以同时修改 cs 和 ip
- 【jmp 段地址:偏移地址】 可以用来同时修改 CS 和 IP 指令中的段地址修改 CS 偏移地址修改 IP 这种用法编译器不认识,只能做在 debug 中使用
- 【jmp 某一合法的寄存器】 仅修改 IP 的内容 比如:jmp ax 或者 jmp bx(类似于 mov IP ax)
- jmp 指令要给出两种信息:
- 转移的目的地址
- 转移的距离(段间转移、段内短转移、段内近转移)
58 依据位移进行转移的 jmp 指令
- jmp short 标号【转到标号处执行指令,段内短转移】 此格式实现的是:段内短转移,它对 ip 的修改范围为-128~127
- 也就是说,它向前转移时可以最多越过 128 个字节,负数使用补码表示 向后转移可以最多越过 127 个字节
- CPU 不需要目的地址就可以实现对 ip 的修改 jmp 指令的机器码中不包含目的地址,但是可以实现跳转 实现的方式,是在原地址的基础上进行一个偏移量,即位移
- 还有一种和指令“jmp short 标号”功能类似的指令格式: jmp near ptr 标号,它实现的是段内近转移 功能为:(ip)=(ip)+16 位位移 jmp short 标号是 8 位的位移,而 jmp near ptr 标号是 16 位位移
59 转移的目的地址在指令中的 jmp 指令
前面讲的 jmp 指令,其对应的机器码中并没有转移的目的地址,而是相对于当前 ip 的转移位移
- 指令“jmp far ptr 标号” 实现的是段间转移,又称为远转移,这时机器码中应该明确给出【段地址】
- 指令“jmp far ptr 标号”功能如下: (CS)=标号所在段的段地址 (IP)=标号所在段中的偏移地址 far ptr 指明了指令用标号的段地址和偏移地址修改 cs 和 ip
60 转移地址在寄存器中的 jmp 指令
指令格式:jmp 16 位寄存器 功能:修改 ip 寄存器中的值,把 16 位寄存器中的值送入到 ip 寄存器中
61 转移地址在内存中的 jmp 指令
转移地址在内存中的 jmp 指令有两种格式:
- jmp word ptr 内存单元地址(段内转移) 功能:将内存中的那个字视为一个偏移地址,然后跳转到那个偏移地址 与【jmp 寄存器】功能相似 内存单元地址可用寻址方式的任意格式给出
- jmp dword ptr 内存单元地址(段间转移) (ip)=(内存单元地址) ;双字中的低位字是给 ip 的 (cs)=(内存单元地址+2) ;双字中的高位字是给 cs 的 跟【jmp 段地址:偏移地址】功能类似 内存单元地址可用寻址方式的任意格式给出 **补充:不能直接向内存单元中加入立即数 要通过寄存器,把立即数加进去
62 jcxz 指令
- 有条件跳转指令,所有的有条件跳转指令都是短转移 对应的机器码中包含转移的位移,而不是目的地址。对 ip 的修改范围都为:-128~127 **另一个有条件跳转指令【loop 指令】
- 指令格式:jcxz 标号 如果(cx)=0,则跳转到标号处执行
- jcxz 标号 指令的操作:
- 当(cx)=0 时,(ip)=(ip)+8 位位移
- 当(cx)!=0 时,什么也不做(程序继续向下执行)
63 loop 指令
- 循环指令,所有的循环指令都是短转移,在对应的机器码中包含转移的位移
- 指令格式:loop 标号
- 指令的内部操作
- cx=cx-1
- 如果 cx!=0,(ip)=(ip)+8 位位移,跳转
- (cx)=0,什么也不做,程序向下执行 cx 用来控制循环的次数
64 根据位移进行转移的意义
- 根据位移进行转移,这样设计,方便了程序段在内存中的浮动装配 可以实现代码的复用
- 如果在机器码中直接给出【段地址:偏移地址】, 这段程序在内存中换一个位置,则会运行不正确
- 段内近转移、段内短转移都是根据位移进行转移,一共有四种方式
- jmp short ptr 标号
- jmp near ptr 标号
- jcxz 标号
- loop 标号
65 编译器对转移位移超界的检测
注意,根据位移进行转移的指令,他们的转移范围会受到限制 如果在源程序中出现了转移范围超界的问题,在编译的时候,编译器将报错 【实验八、九】【这个实验要重点看】
第十章 call 和 ret 指令
call 和 ret 指令都是转移指令,它们都能修改 ip,或同时修改 cs 和 ip
66 ret 和 ref
- ret 指令用栈中的数据,修改 ip 的内容,从而实现【近转移】
CPU 执行 ret 指令时,进行下面两步操作:
- (ip)=((ss)*16+(sp)) ;ip 的值修改为栈顶的内容
- (sp)=(sp)+2 ;栈顶移动
- retf 指令用栈中的数据,修改 cs 和 ip 的内容,从而实现【远转移】
CPU 执行 retf 指令时,进行下面四步操作
- (ip)=((ss)*16+(sp)) ;ip 的内容修改为栈顶的内容
- (sp)=(sp)+2 ;栈顶移动
- (cs)=((ss)*16+(sp)) ;cs 的内容修改为栈顶移动之后,栈顶的内容
- (sp)=(sp)+2 ;栈顶移动 栈顶的两个字,低位字修改为 ip,高位字修改为 cs
- 可以看出,如果我们用汇编语法来解释 ret 和 retf 指令,则
- CPU 执行 ret 指令,相当于 pop ip
- 执行 retf 指令时,相当于 pop ip pop cs
67 call 指令
- call 指令经常跟 ret 指令配合使用,因此 CPU 执行 call 指令,进行两步操作:
- 将当前的 ip 或 cs 和 ip 压入栈中
- 转移
- call 指令不能实现短转移,除此之外, call 指令实现转移的方法和 jmp 指令的原理相同 【依据位移进行转移的 call 指令】
- CPU 执行“call 标号”这种格式的 call 指令时,进行如下操作:
- (sp)=(sp)-2 ;栈顶移动
- ((ss)*16+(sp))=(ip) ;当前 ip 内容压栈
- (ip)=(ip)+16 位位移 ;跳转到标号处
- call 指令格式:call 标号 相当于执行: push ip jmp near ptr 标号
68 转移的目的地址在指令中的 call 指令
- 指令格式:call far ptr 标号 实现的是段间转移
- 执行这种格式的 call 指令时 CPU 的操作
- (sp)=(sp)-2 ;栈顶移动
- ((ss)×16+(sp))=(cs) ;先把 cs 压栈
- (sp)=(sp)-2 ;栈顶移动
- ((ss)×16+(sp))=(ip) ;然后把 ss 压栈
- CPU 执行“call far ptr 标号”时,相当于进行 push cs push ip jmp far ptr 标号
69 转移地址在寄存器中的 call 指令
- 指令格式:call 16 位寄存器
- 执行这种指令时,在 CPU 中的操作
- (sp)=(sp)-2
- ((ss)×16+(sp))=(ip)
- (ip)=(16 位寄存器)
- 相当于 push ip jmp 16 位寄存器
70 转移地址在内存中的 call 指令
转移地址在内存中的 call 指令有两种格式:
- call word ptr 内存单元地址 汇编语法解释 push ip jmp word ptr 内存单元地址
- call dword ptr 内存单元地址 汇编语法解释 push cs ;cs 存放在高位 push ip ;ip 存放在低位 jmp dword ptr 内存单元地址
71 call 和 ret 的配合使用
72 mul 指令
相乘的两个数;要么都是 8 位,要么都是 16 位
- 8 位:AL 中和 8 位寄存器或内存字节单元中 AL 中的内容作为被乘数 结果放在 AX 中
- 16 位:AX 中和 16 位寄存器或内存字单元中 AX 中的内容作为被乘数 结果放在 DX(高位)和 AX(低位)中。
- 格式如下: mul 寄存器 mul 内存单元(byte ptr 或 word ptr 指明是字还是字节)
73 模块化程序设计
74 参数和结果传递的问题
【编程】计算 data 段中第一组数据的 3 次方,结果保存在后面一组 dword 单元中
data sgement
dw 1,2,3,4,5,6,7,8
dd 0,0,0,0,0,0,0,0
data ends
75 批量数据的传递
使用寄存器、内存、栈传递数据 【编程】将一个全是字母,以 0 结尾的字符串,转化为大写 【实验十 编写子程序】 1.显示字符串 2.解决除法溢出问题 3.数值显示 【课程设计 1】
第十一章 标志寄存器
8086CPU 的标志寄存器有 16 位,其中存储的信息通常被称为程序状态字(PSW) 本章中的标志寄存器(以下简称为 flag)是我们要学习的最有一个寄存器 flag 寄存器是按位起作用的,也就是说,它的每一位都有专门的含义,记录特定的信息 8086CPU 的 flag 寄存器的结构:
- flag 的 1、3、4、12、13、14、15 位共 7 位在 8086CPU 中没有使用,不具有任何含义 而 0、2、4、6、7、8、9、10、11 位共 9 位都具有特殊的含义
- 示意图 ![avatar](./11. 1. png)
76 ZF 标志
- flag 的第 6 位是 ZF,零标志位。 它记录相关指令执行后, 1. 结果为 0,ZF=1 2. 结果不为 0,ZF=0
- 示例: mov ax,1 sub ax,1 指令执行后,结果为 0,则 ZF=1 mov ax,2 sub ax,1 指令执行后,结果不为 0,则 ZF=0
- 注意,在 8086CPU 的指令集中,有的指令的执行会影响标志寄存器 比如:add、sub、mul、div、inc、or、and 等 他们大都是运算指令(逻辑运算或者算术运算) 有的指令的执行对标志寄存器没有影响, 比如:mov、push、pop 等,他们大都是传送指令
77 PF 标志
flag 的第 2 位是 PF,奇偶标志位 它记录指令执行后,结果的所有二进制位中 1 的个数
- 为偶数,PF=1
- 为奇数,PF=0
78 SF 标志
- flag 的第 7 位是 SF,符号标志位
- 它记录指令执行后
- 结果为负。sf=1
- 结果为正,sf=0 sf 标志,就是 CPU 对有符号数运算结果的一种记录,它记录数据的正负 sf 标志把所有数当作有符号数 如果把数据当作无符号数运算,sf 的值则没有意义,虽然相关指令会影响它的值
- 也就是说,CPU 在执行 add 等指令时,是必然要影响 sf 标志位的值 至于我们需不需要这种影响,那就看我们如何看待指令所进行的运算
79 CF 标志
-
flag 的第 0 位是 CF,进位标志位 一般请况下,在进行无符号数运算的时候, 它记录了运算结果的最高有效位向更高位的进位值, 或从更高位的借位值 代表假想的更高位
-
CPU 在运算时,不会丢弃进位值,而是记录在一个特殊的寄存器的某一位上 8086CPU 就用 flag 的 cf 为来记录这个进位值,借位也一样
-
在 debug 中的显示 ![avatar](./11. 2. png)
-
无符号的时候产生的结果
80 OF 标志
flag 中的第 11 位 进行有符号数运算的时候,如果结果超过了机器所能表示的范围称为溢出
- 这里所讲的溢出,只是对有符号数运算而言 就像进位只是相对于无符号数而言!
- 一定要注意 cf 和 of 的区别 当需要把机器码看成有符号数则使用 of 当需要把机器码看成无符号数则使用 cf
81 adc 标志
adc 是带进位的加法指令,他利用了 cf 上记录的进位值
- 格式:adc 操作对象 1,操作对象 2
- 功能:操作对象 1=操作对象 1+操作对象 2+cf 比如:adc ax,bx 实现的功能是: (ax)=(ax)+(bx)+cf
- 执行 adc 指令的时候,加上的 cf 的值的含义,由 adc 指令前的指令决定 也就是说,关键在于所加上的 cf 值是被什么指令设置的
- 如果 cf 是被 sub 指令设置的,那么他的含义就是借位值 如果是被 add 指令设置的,那么它的含义就是进位值
- 下面的指令和 add ax,bx 具有相同的结果 add al,bl adc ah,bh CPU 提供 adc 指令的目的,就是来进行加法的第二步运算的 adc 指令和 add 指令相配合就可以对更大的数据进行加法运算 【实验:编程计算 1EF000H+201000H,结果放在 ax(高 16 位)和 bx(低 16 位)中】
82 sbb 标志
sbb 是带借位减法指令,他利用了 cf 位上记录的借位值
- 格式:sbb 操作对象 1,操作对象 2
- 功能:操作对象 1=操作对象 1-操作对象 2-cf
- 利用 sbb 指令,我们可以对任意大的数据进行减法运算
- sbb 和 adc 是基于相同的思想设计的两条指令, 在应用思路上和 adc 类似
83 cmp 标志
-
cmp 是比较指令,功能相当于减法指令,只是不保存结果
-
cmp 指令执行后,将对标志寄存器产生影响
-
其他相关指令通过识别这些被影响的标志寄存器,来得知比较结果
-
cmp 指令格式:cmp 操作对象 1,操作对象 2
-
功能:计算操作对象 1-操作对象 2,但并不保存结果,仅仅根据计算结果对标志寄存器进行设置
-
比如:cmp ax,ax 做(ax)-(ax)的运算,结果为 0,但并不在 ax 中保存,仅影响 flag 的相关位 指令执行后 zf=1 ;结果为 0 pf=1 ;结果的 1 的个数为偶数 sf=0 ;结果为正号 cf=0 ;结果没有产生进位或借位 of=0 ;结果没有溢出
-
根据 flag,判断 cmp 指令的结果(无符号数) ![avatar](./11. 3. png)
-
cmp 既可以对无符号数进行比较,也可以对有符号数进行比较 cmp 操作数 1,操作数 2 ;操作数 1、操作数 2 都是有符号数
- of=0,说明没有溢出,逻辑上真正结果的正负=实际结果的正负 of=0,sf=1 则 操作数 1 比操作数 2 小 of=0,sf=0 则 操作数 1 比操作数 2 大
- of=1,说明有溢出,逻辑上真正结果的正负与实际结果的正负相反 of=1,sf=1 则 操作数 1 比操作数 2 大 of=1,sf=0 则 操作数 1 比操作数 2 小
84 检测比较结果的条件转移指令
- 这些条件转移指令通常和 cmp 相配合使用
- 因为 cmp 指令可以同时进行两种比较,无符号数和有符号数的比较
所以,这些转移指令也分为两种,即:
- 根据【无符号数】的比较结果进行转移的条件转移指令, 他们检测 zf、cf 的值
- 根据【有符号数】的比较结果进行转移的条件转移指令 他们检测 sf、of 和 zf 的值
- 无符号比较,条件转移指令小结【无符号,6 个】
- je 等于则转移 zf=1
- jne 不等于则转移 zf=0
- jb 低于则转移 cf=1 【b 表示 below】
- jnb 不低于则转移 cf=0
- ja 高于则转移 cf=0,zf=0【a 表示 above】
- jna 不高于则转移 cf=1 或 zf=1
85 DF 标志和串传送指令
- flag 的第 10 位 DF,方向标志位 在串处理指令(movsb,movsw)中,控制每次操作后 si、di 的增减 df=0:每次操作后 si,di 递增 df=1:每次操作后 si,di 递减
- 格式:movsb
- 功能:(以字节为单位传送)
- ((es)*16+(di))=((ds)*16+(si))
- 如果 df=0,则:(si)=(si)+1 (di)=(di)+1 如果 df=1,则:(si)=(si)-1 (di)=(di)-1
- 功能文字描述 movsb 的功能是将 ds:si 指向的内存单元中的字节 送入 es:di 中,然后根据标志寄存器 df 位的值, 将 si 和 di 递增或递减
- movsw 传送一个字
- movsb 和 movsw 都和 rep 配合使用 格式:rep movsb rep 的作用根据 cx 的值,重复执行后面的串传送指令
- cld 指令和 std 指令 cld 指令:将标志寄存器的 df 置为 0【c:clear】 std 指令:将标志寄存器的 df 置为 1【s:set】
86 pushf 和 popf
pushf:将标志寄存器的值压栈 popf:从栈中弹出数据,送入标志寄存器中 pushf 和 popf 为直接访问标志寄存器提供了一种方法
87 标志寄存器在 debug 中的表示
第十二章 内中断
引言和简介
- 中断是 CPU 处理外部突发事件的一个重要技术
- 它能使 CPU 在运行过程中对外部事件发出的中断请求及时地进行处理,处理完成后 又立即返回断点,继续进行 CPU 原来的工作。
- 引起中断的原因【即:发出中断请求的来源叫作中断源】
- 根据中断源的不同,可以把中断分为:【软件中断】和【硬件中断】两大类 而硬件中断又可以分为【外部中断】和【内部中断】两类
88 内中断的产生
- 外部中断一般是指计算机外设发出的中断请求,如:键盘中断、打印机中断、定时器中断。 外部中断是可以屏蔽的中断,也就是说,利用中断控制器可以屏蔽这些外部设备的中断请求。
- 内部中断是指因硬件出错(如突然掉电、奇偶校验错等)或运算出错(除数为零、运算溢出、单步中断)所引起的中断。 内部中断是不可屏蔽的中断
- 软件中断其实并不是真正的中断,他们只是可被调用执行的一般程序, DOS 的系统功能调用(int 21h)都是软件中断
- CPU 为了处理并发的中断请求,规定了中断的优先权,优先权由高到低的顺序是:
- 除法错、溢出中断、软件中断
- 不可屏蔽中断
- 可屏蔽中断
- 单步中断
89 中断处理程序简介
- CPU 的设计者必须在中断信息和其处理程序的入口地址之间建立某种联系 使得 CPU 根据中断信息可以找到要执行的处理程序。
- 中断信息中包含有表示中断的类型码。根据 CPU 的设计,中断类型码的作用就是用来定位中断处理程序的。
- CPU 用 8 位的中断类型码通过中断向量表找到相应的中断处理程序的入口地址 即中断类型码是中断向量在中断向量表中的索引
90 中断向量表【中断向量表就是中断向量的列表】
- 中断向量表在内存中保存,其中存放着 256 个【2^8,8 位中断类型码】中断源所对应的中断处理程序的入口 对于 8086PC 机,中断向量表指定放在内存地址 0 处
- 从 0:0-0:03ffh 的 1024 个字节【256*4,物理地址使用段地址和偏移地址存放,需要 4 个字节】中存放着中断向量表
91 中断过程
- 可以用中断类型码,在中断向量表中找到中断处理程序的入口 找到这个入口地址的最终目的是用它设置 cs 和 ip,使 CPU 执行中断处理程序
- 用中断类型码找到中断向量,并用它设置 cs 和 ip,这个工作时由 CPU 的硬件自动完成的 CPU 硬件完成这个工作的过程被称为【中断过程】
- 中断过程
8086CPU 的中断过程
- (从中断信息中)取得中断类型码
- 标志寄存器的值入栈(保护标志位)
- 设置标志寄存器的第 8 位 TF 和第 9 位 IF 设置为 0(后面讲解本步的目的)
- cs 内容入栈
- ip 内容入栈
- 从内存地址为中断类型码4 和中断类型码4+2 的两个子单元中 读取中断处理程序的入口地址设置 cs 和 ip
- 使用汇编语言描述中断过程,如下
- 取得中断类型码 N
- pushf
- TF=0,IF=0
- push cs
- push ip
- (ip)=(N4),(cs)=(N4+2)
92 中断处理程序
- 由于 CPU 随时都可能检测到中断信息,也就是说,CPU 随时都可能执行中断处理程序, 所以,中断处理程序必须一致存储在内存某段空间中
- 而中断处理程序的入口地址,即【中断向量】,必须存储在对应的中断向量表表项中
- 中断处理程序的编写方法和子程序的比较类似,下面是常规的步骤
- 保存用到的寄存器
- 处理中断
- 恢复用到的寄存器
- 用 iret 指令返回 **iret 指令的功能用汇编语法描述为 pop ip pop cs popf iret 通常和硬件自动完成的中断过程配合使用 iret 指令执行后,CPU 回到执行中断处理程序前的执行点继续执行程序
93 除法错误中断的处理
当 CPU 执行 div 等除法指令的时候,如果发生了除法溢出错误,将产生中断类型码为 0 的终端信息 CPU 将检测到这个信息,然后引发中断程序,转去执行 0 号中断对应的中断处理程序 例如: mov ax 1000h mov bh,1 div bh 此程序会产生溢出 运行之后,会显示
94 编程处理 0 号中断
现在重新编写一个 0 号中断处理程序,它的功能是在屏幕中间显示“Welcome to here!”的广告词,然后返回到操作系统 把中断处理程序放到安全空间中 中断程序的框架
95 安装
计算中断程序的长度:offset 标号 1-offset 标号 2 在代码段中存放数据
96 do0
97 设置中断向量
98 单步中断
如果检测到标志寄存器的 tf 位为 1,则产生单步中断,引发中断过程
99 响应中断的特殊情况
第十三章 int 指令
100 int 指令
- int 格式:int n ;n 为中断类型码 它的功能是引发中断过程
- CPU 执行 int n 指令,相当于引发一个 n 号中断的中断过程,执行过程如下
- 取中断类型码
- 标志寄存器入栈,if=0,tf=0
- cs,ip 入栈
- 从此处转去执行 n 号中断的中断处理过程
- 可以在程序中使用 int 指令调用任何一个中断的中断处理程序 可以用 int 指令调用这些子程序,也可以自己编写一些中断处理程序供别人使用
101 编写供应用程序调用的中断例程
【实例 1】编写、安装中断 7ch 的中断例程,实现求一个 word 型数据的平方
- 功能:求一 word 型数据的平方
- 参数:(ax)=要计算的数据
- 返回值:dx、ax 中存放结果的高 16 位和低 16 位
- 应用举例:求 2*3456^2
101.1 程序 1:调用中断程序计算平方
code segment
assume cs: code
start:
mov ax,3456; (ax)=3456
int 7ch;调用中断7ch的中断例程,计算ax中的数据的平方
add ax,ax
adc dx,dx ;存放结果,讲结果乘以2
mov ax,4c00h
int 21h
code ends
end start
101.2 程序 2:编写中断程序
程序 2 中要做三部分工作
- 编程实现求平方功能的程序
- 安装程序,我们将其安装在 0:200 处
- 设置中断向量表,将程序的入口地址保存在 7ch 表项中,使其成为中断 7ch 的中断例程。
code segment
assume cs:code
start:
mov ax,cs
mov ds,ax
mov si,offset sqr ;设置ds:si指向源地址
mov ax,0
mov es,ax
mov di,200h ;设置es:di指向目的地址
mov cx,offset sqrend - offset sqr ;设置cx为传输长度
cld ;设置传输方向为正
rep movsb
mov ax,0
mov es,ax
mov word ptr es:[7ch*4],200h ;设置中断向量地址,偏移地址
mov word ptr es:[7ch*4+2],0 ;设置中断向量地址,段地址
mov ax,4c00h
int 21h
sqr:
mul ax
iret
sqrend: nop
code ends
end start
101.3 【实例 2】编写、安装中断 7ch 的中断例程,实现将一个全是字母,以 0 结尾的字符串,转化为大写。
code segment
assume cs:code
start:
mov ax,cs
mov ds,ax
mov si,offset capital
mov ax,0
mov es,ax
mov di,200h
mov cx,offset capitalend - offset capital
cld
rep movsb
mov ax,0
mov es,ax
mov word ptr es:[7ch*4],200h
mov word ptr es:[7ch*4+2],0
mov ax,4c00h
int 21h
capital:
push cx
push si
change:
mov cl,[si]
mov ch,0
jcxz ok
and byte ptr [si],11011111b
inc si
jmp short change
ok:
pop si
pop cx
iret
capitalend:
nop
code ends
end start
102 对 int、iret 和栈的深入理解
【问题】用 7ch 中断例程完成 loop 指令的功能 不要随便修改 sp,可以使用 bp 进行间接访问
103 BIOS 和 DOS 所提供的中断例程
104 BIOS 和 DOS 中断例程的安装过程
- 开机后,CPU 一加电,初始化(cs)=0ffffh,ip=0,自动从 ffff:0 单元开始执行程序 ffff:0 处有一条跳转指令,CPU 执行该指令后,转去执行 bios 中的硬件系统的检测和初始化程序。
- 初始化程序将建立 bios 所支持的中断向量,即将 bios 提供的中断例程的入口地址登记在中断向量表中。
- 硬件系统检测和初始化完成后,调用 19h 进行操作系统的引导。从此将计算机交由操作系统控制。
- DOS 启动后,除完成其他工作外,还将它所提供的中断例程装入内存,并建立相应的中断向量
105 BIOS 中断例程的应用
- int 10h 中断例程是 bios 提供的中断例程,其中包含了多个和屏幕输出相关的子程序 一般来说,一个供程序员调用的中断例程中,往往包括多个子程序,中断例程内部用传递进来的参数来决定执行哪个子程序
- bios 和 dos 提供的中断例程,都用 ah 来传递内部子程序的编号
106 DOS 中断例程应用
int 21h 中断例程是 dos 提供的中断例程,其中包含了 dos 提供给程序员造编程时调用的子程序 【实验 13】 介绍一本汇编语言的书《The Art of Assembly Language》
第十四章 端口
CPU 可以直接读写 3 个地方的数据
- CPU 内部的寄存器
- 内存单元
- 端口
107 端口的读写
- 对端口的读写不能用 mov、push、pop 等内存读写指令 端口的读写指令只有两条:【in】和【out】分别用于从端口读取数据和往端口写入数据
- CPU 执行内存访问指令和端口访问指令时,总线上的信息:
- 访问内存
mov ax,ds:[8];
假设执行前(ds)=0
执行时,与总线相关的操作:
- CPU 通过地址线将地址信息 8 发出
- CPU 通过控制线发出内存读命令,选中存储器芯片,并通知它,将要从中读取数据
- 存储器将 8 号单元中的数据通过数据线送入 CPU
- 访问端口
这里的【端口】是对硬件开放的端口
in al,60h; 从 60h 号端口读入一个字节
执行时与总线相关的操作
- CPU 通过地址线将地址信息 60h 发出
- CPU 通过控制线发出端口读命令,选中端口所在的芯片,并通知它,将要从中读取数据
- 端口所在的芯片将 60h 端口中的数据通过数据线送入 CPU **注意:在 in 和 out 指令中,只能使用 ax 或 al 来存放从端口中读入的数据或要发送到端口中的数据 访问 8 位端口时用 al,访问 16 位端口时用 ax
- 对 0-255 以内的端口进行读写 in al,20h ;从 20h 端口读一个字节 out 20h,al ;往 20h 端口写一个字节
- 对 256-65535 的端口进行读写时,端口号放在【dx】中 mov dx,3f8h ;将端口号 3f8 送入 dx in al,dx ;从 3f8h 端口读一个字节 out dx,al ;从 3f8h 端口写一个字节
- 访问内存
mov ax,ds:[8];
假设执行前(ds)=0
执行时,与总线相关的操作:
108 CMOS RAM 芯片
- PC 机中有一个 CMOS RAM 芯片,其有如下特征
- 包含一个实时钟和一个有 128 个存储单元的 RAM 存储器。(早期的计算机为 64 字节)
- 该芯片靠电池供电。因此,关机后其内部的实时钟仍可以正常工作,RAM 中的信息不丢失
- 128 字节的 RAM 中,内部实时钟占用 0-0dh 单元来保存时间信息,其余大部分分单元用于 保存系统配置信息,供系统启动时 bios 程序读取 bios 也提供了相关的程序,使我们可以在开机的时候配置 CMOS RAM 中的系统信息 **补充:BIOS BIOS 是英文"Basic Input Output System"的缩略词,直译过来后中文名称就是"基本输入输出系统"。 在 IBM PC 兼容系统上,是一种业界标准的固件接口。BIOS 这个字眼是在 1975 年第一次由 CP/M 操作系统中出现。 BIOS 是个人电脑启动时加载的第一个软件
- 该芯片内部有两个端口,端口地址为 70h 和 71h。CPU 通过这两个端口读写 CMOS RAM。
- 70h 为地址端口,存放要访问的 CMOS RAM 单元的地址;71h 为数据端口,存放从选定的 CMOS RAM 单元中读取的数据 或要写入到其中的数据
- 比如:读 CMOS RAM 的 2 号单元:
- 将 2 送入端口 70h
- 从 71h 读取 2 号单元的内容
109 shl 和 shr 指令
shl 和 shr 是逻辑移位指令,后面的课程中我们要用到移位指令
- shl 逻辑左移指令,功能为:
- 将一个寄存器或内存单元中的数据向左移位
- 将最后移出的移位写入 cf 中
- 最低位用 0 补充 例如有如下指令: mov al,01001000b shl al,1 ;将 al 中的数据左移一位 执行后(al)=100100000b,cf=0. 如果移动位数大于 1 时,必须将移动位数放在 cl 中
- shr 逻辑右移指令,与 shl 刚好相反
110 CMOS RAM 中存储的时间信息
在 CMOS RAM 中存放着当前时间 秒:00h 分:02h 时:04h 日:07h 月:08h 年:09h 这 6 个信息的长度都为 1 个字节 这些数据以 BCD 码的方式存放,一个字节可以表示两个 BCD 码 CMOS RAM 存储时间信息的单元中存储了用两个 BCD 码表示的两个十进制数 高 4 位的 BCD 码表示十位,低四位的 BCD 码表示个位 【编程】:在屏幕中间显示当前的月份 1. CMOS RAM 芯片回顾: 1. 70h 为地址端口,存放要访问的 CMOS RAM 单元的地址 2. 71h 为数据端口,存放从选定的 CMOS RAM 单元中【读取】的数据,或【写入】其中的数据 2. 分析 这个程序主要做两部分工作 1. 从 CMOS RAM 的 8 号单元读取当前月份的 BCD 码 要读取 CMOS RAM 的信息,我们首先要向地址端口 70h 写入要访问的单元的地址 mov al,8 out 70h,al 然后从数据端口 71h 中取得指定单元中的数据 in al,71h 2. 将用 BCD 码表示的月份以十进制的形式显示到屏幕上
编程:在屏幕中间显示当前的月份
code segment
assume cs:code
start:
mov al,8
out 70h,al
in al,71h
mov ah,al
mov cl,4
shr ah,cl
and al,00001111b
add ah,30h
add al,30h
mov bx,0b800h ;显存
mov es,bx
mov byte ptr es:[160*12+40*2],ah ;显示月份的十位数码
mov byte ptr es:[160*12+40*2+2],al ;显示月份的个位数码
mov ax,4c00h
int 21h
code ends
end start
110.1 【实验十四】编程:以“年/月/日 时:分:秒”的格式,显示当前日期和时间
第十五章 外中断
CPU 除了有运算能力,还有 I/O 能力
111 接口芯片和端口
- 在 PC 系统的接口卡和主板上,装有各种接口芯片,这些外设接口芯片的内部装有若干寄存器 CPU 将这些寄存器当做【端口】访问
- 外设的输入不直接送入内存和 CPU,而是送入相关的接口芯片的【端口】中
- CPU 向外设的输出也是要先送入【端口】中,再由相关芯片送入到外设
- CPU 可以向外设输出控制命令,这些控制命令也是先送到【端口】中,然后相关芯片根据命令进行相关工作
- 可见:CPU 与外部设备的交流是通过【端口】进行的 CPU 在执行完当前指令后,可以检测到发送过来的中断信息,引发中断过程,处理外设的输入
112 外中断信息
- 在 PC 系统中,外中断源一共有两类
- 可屏蔽中断
- 不可屏蔽中断
- 可屏蔽中断是 CPU 可以不响应的外中断。CPU 是否响应可屏蔽中断
要看标志寄存器的 IF 位的设置
当 CPU 检测到可屏蔽中断信息时:
- 若 IF=1,则 CPU 在执行完当前指令后相应中断,引发中断过程
- 若 IF=0,则不响应可屏蔽中断
- 可屏蔽中断所引发的中断过程,除在第一步的实现上与内中断有所不同外,基本上和内中断的中断过程相同
- 因为可屏蔽中断信息来自于 CPU 外部,中断类型码是通过数据总线送入 CPU 的 而内中断的中断码是在 CPU 内部产生的
- IF 设置为 0 的原因:在进入中断处理程序后,禁止其他的可屏蔽中断 当然,如果中断处理程序中需要处理可屏蔽中断,可以用指令将 IF 设置为 1
- 8086CPU 提供的设置 IF 的指令如下: sti ;用于设置 IF=1 cli ;用于设置 IF=0
- 不可屏蔽中断是 CPU 必须相应的外中断。 当 CPU 检测到不可屏蔽中断信息时,则在执行完当前指令后 立即响应,应发中断过程
- 8086CPU 不可屏蔽中断的中断类型码固定为 2,所以中断过程中,不需要取中断类型码
- 不可屏蔽中断的中断过程
- 标志寄存器入栈,IF=0,TF=0
- CS,IP 入栈
- (IP)=(8),(CS)=(0AH) ;固定地址
- 几乎所有外中断,都是可屏蔽中断。当外设有需要处理的事件发生时 相关芯片向 CPU 发出可屏蔽中断信息。 不可屏蔽中断是系统中有必须处理的紧急情况发生时用来通知 CPU 的中断信息,本门课程中,主要讨论可屏蔽中断
113 PC 机键盘的处理过程
- 下面看一个键盘输入的处理过程,并以此来体会 PC 机处理外设输入的基本方法
- 键盘输入
- 引发 9 号中断
- 执行 int 9 中断例程
- PC 机键盘的处理过程
- 键盘上每一个键相当于一个开关,键盘中有一个芯片对键盘上的每一触键的开关状态进行扫描。
- 按下一个键时,开关接通,该芯片就产生一个扫描码,扫描码说明按下的键在键盘上的位置 扫描码被送入主板上的相关接口芯片的寄存器中,该寄存器的端口地址为 60H
- 松开控下的键时,也产生一个扫描码,扫描码说明了松开的键在键盘上的位置,松开按键时
产生的扫描码也被送入 60H 端口中。
一般按下一个键时,产生的扫描码称为通码,松开一个键产生的扫描码称为断码
扫描码长度为一个字节,通码的第七位为 0,断码的第七位为 1
即:断码=通码+80H
**BIOS 提供了 int9 中断例程,用来进行基本的键盘输入处理,主要的工作如下:
- 读出 60H 端口中的扫描码
- 如果是字符键的扫描码,将该扫描码对应的字符码(即:ASCII 码)送入内存中的 BIOS 键盘缓冲区 3,如果是控制键和切换键的扫描码,则将其转变为状态字节,写入内存中存储状态字节的单元
- 键盘的输入到达 60H 端口时,相关的芯片会向 CPU 发出中断类型码为 9 的可屏蔽中断信息。
- CPU 检测到中断信息后,如果 IF=1,则相应中断,同时将 IF 设置为 0(不让其他可屏蔽中断进行干扰),引发中断过程,转去执行 int9 中断例程
- BIOS 键盘缓冲区是系统启动后,BIOS 用于存放 int9 中断例程所接受的键盘输入的内存区
- 该内存区可以存储 15 个键盘输入,int9 中断例程除了接收扫描码外,还要产生和扫描码对应的字符码, 所以在 BIOS 键盘缓冲区中,一个键盘输入用一个字单元存放,高字节存放扫描码,低字节存放字符码
- 0040:17 单元存储键盘状态字节,该字节记录了控制键和切换键的状态。键盘状态字节各位记录的信息如下:
114 编写 int9 中断例程,并安装
梳理键盘输入的处理过程
- 键盘产生扫描码
- 扫描码送入 60H 端口
- 一旦侦测到 60H 端口有动静,引发 9 号中断
- CPU 执行 int9 中断例程处理输入 以上的过程,前三步都由硬件系统自动完成,能够修改的只有第四步,修改 int9 中断程序
【任务演示】在屏幕中依次显示“a”~“z”并可以让人看清。在显示过程中,按下 Esc 键后,该表显示的颜色
程序 1:实现连续显示“a”~“z” 编程:在屏幕中间依次显示“a”~“z”,并可以让人看清。在显示的过程中,按下’Esc’键后,改变显示的颜色。 部分功能代码:
stack segment
db 128 dup (0)
stack ends
code segment
assume cs:code
start:
mov ax,stack
mov ss,ax
mov sp,128
mov ax,0b800h
mov es,ax
mov ah,'a'
s: mov es:[160*12+40*2],ah
call delay
inc ah
cmp ah,'z'
jna s
mov ax,4c00h
int 21h
delay:
push ax
push dx
mov dx,0010h ;循环10000000h次
mov ax,0
s1:
sub ax,1
sbb dx,0
cmp ax,0
jne s1
cmp dx,0
jne s1
pop dx
pop ax
ret
code ends
end start
114.1 程序 2:实现改变颜色
编程:在屏幕中间依次显示“a”~“z”,并可以让人看清。在显示的过程中,按下’Esc’键后,改变显示的颜色。
stack segment
db 128 dup (0)
stack ends
data segment
dw 0,0
data ends
code segment
assume cs:code
start:
mov ax,stack
mov ss,ax
mov sp,128
mov ax,data
mov ds,ax
mov ax,0
mov es,ax
push es:[9*4]
pop ds:[0]
push es:[9*4+2]
pop ds:[2] ;将原来的int 9中断例程的入口地址保存在ds:0、ds:2单元中
mov word ptr es:[9*4],offset int9
mov es:[9*4+2],cs ;在中断向量表中设置新的int 9中断例程的入口地址
mov ax,0b800h
mov es,ax
mov ah,'a'
s:
mov es:[160*12+40*2],ah
call delay
inc ah
cmp ah,'z'
jna s
mov ax,0
mov es,ax
push ds:[0]
pop es:[9*4]
push ds;[2]
pop es;[9*4+2] ;将中断向量表中int 9中断例程的入口恢复为原来的地址
mov ax,4c00h
int 21h
delay:
push ax
push dx
mov dx,0010h
mov ax,0
s1:
sub ax,1
sbb dx,0
cmp ax,0
jne s1
cmp dx,0
jne s1
pop dx
pop ax
ret
——以下为新的 int 9 中断例程——————– int9 中断例程是在进行键盘输入之后,由系统自动调用
int9:
push ax
push bx
push es
in al,60h
pushf
pushf
pop bx
and bh,11111100b
push bx
popf
call dword ptr ds:[0] ;对int指令进行模拟,调用原来的int 9中断例程
cmp al,1
jne int9ret
mov ax,0b800h
mov es,ax
inc byte ptr es:[160*12+40*2+1] ;属性增加1,改变颜色
int9ret:
pop es
pop bx
pop ax
iret
code ends
end start
第十六章 直接定址表
115 描述了单元长度的标号
- 本章讨论如何有效合理地组织数据,以及相关的编程技术
- 前面的课程中,我们一直在代码段中使用标号来标记指令、数据、段的起始地址
- 还可以使用一种标号,这种标号不但可以表示内存单元的地址,还表示了内存单元的长度 即:表示在此标号处的单元,是一个字节单元,还是字单元还是双字单元
- 例如
- 标号 1 a : db 1,2,3,4,5,6,7,8 b : dw 0 此种标号只能标记地址 此种加有“:”的地址标号,只能在代码段中使用,不能在其他段中使用
- 标号 2 a db 1,2,3,4,5,6,7,8 ;标号 a,描述了地址 code:0,和从这个地址开始,以后的内存单元都是字节单元 b dw 0 ;标号 b 描述了地址 code:8,和从这个地址开始,以后的内存单元都是字单元 此种标号既可以标记地址,也可以表示此标号处的单元
- 使用这种包含单元长度的标号,可以使我们以简洁的形式访问内存中的数据 这种标号此后称为数据标号,它标记了存储数据的单元的地址和长度
- 数据标号的用法 指令:mov ax,b ;相当于:mov ax,cs:[8] 指令:mov b,2 ;相当于:mov word ptr cs:[8],2 指令:inc b ;相当于:inc word ptr cs:[8] 指令:mov al,a [si] ;相当于:mov al,cs:0[si] 指令:mov al,a[3] ;相当于:mov al,cs:0[3] 指令:mov al,a[bx+si+3] ;相当于:mov al,cs:0[bx+si+3]
116 在其他段中使用数据标号
- 注意,如果想在代码段中,直接用数据标号访问数据, 则需要用伪指令 assume 将标号所在的段和一个段寄存器联系起来。 否则编译器在编译的时候,无法确定标号的段地址在哪一个寄存器中。
- 当然,这种联系是编译器需要的,但绝对不是说,我们因为编译器的工作需要, 用 assume 指令将段寄存器和某个段相联系,段寄存器中就会真的存放该段的地址。
- 我们可以将数据标号当作数据来定义,此时,编译器将标号所表示的地址当作数据的值。
- 把数据标号当做数据来定义时,使用【dw】定义数据 比如: data segment a db 1,2,3,4,5,6,7,8 b dw 0 c dw a,b ;数据标号 c 处存储的两个字型数据为标号 a、b 的偏移地址。 data ends 数据标号 c 处存储的两个字型数据为标号 a、b 的偏移地址。 相当于: data segment a db 1,2,3,4,5,6,7,8 b dw 0 c dw offset a, offset b data ends
- 把数据标号当做数据来定义时,使用【dd】定义数据 再比如: data segment a db 1,2,3,4,5,6,7,8 b dw 0 c dd a,b ;数据标号 c 处存储的两个双字型数据为标号 a 的偏移地址和段地址、标号 b 的偏移地址和段地址。 data ends 数据标号 c 处存储的两个双字型数据为标号 a 的偏移地址和段地址、标号 b 的偏移地址和段地址。 相当于: data segment a db 1,2,3,4,5,6,7,8 b dw 0 c dw offset a, seg a, offset b, seg b ;seg 操作符,功能为取得某一标号的段地址。 data ends seg 操作符,功能为取得某一标号的段地址。
117 直接定址表
本节课,我们将使用“查表”的方法,编写相关程序的技巧 【任务】编写子程序,以十六进制的形式在屏幕中间显示给定的 byte 型数据
code segment
assume cs:code
start:
mov al,0eh ;al中存放了byte型数据
call showbyte
mov ax,4c00h
int 21h
;子程序:
;用al传送要显示的数据
showbyte:
jmp short show
table db '0123456789ABCDEF' ;字符表
show: push bx ;保护现场
push es
mov ah,al
shr ah,1
shr ah,1
shr ah,1
shr ah,1 ;右移4位,ah中得到高4位的值
and al,00001111b ;al中为低4位的值
mov bl,ah
mov bh,0
mov ah,table[bx] ;用高4位的值作为相对于table的偏移,取得对应的字符
mov bx,0b800h
mov es,bx
mov es:[160*12+40*2],ah
mov bl,al
mov bh,0
mov al,table[bx] ;用低4位的值作为相对于table的偏移,取得对应的字符
mov es:[160*12+40*2+2],al
pop es
pop bx
ret
code ends
end start
118 程序入口地址的直接定址表
【编程】实现一个子程序 setscreen,为显示输出提供如下功能: 1. 清屏 2. 设置前景色 3. 设置背景色 4. 向上滚动一行
- 入口参数说明:
- 用 ah 寄存器传递功能号 0:清屏;1:设置前景色;2:设置背景色;3:向上滚动一行
- 对于 2、3 号功能,用 al 传递颜色值 al∈{0,1,2,3,4,5,6,7}
- 各种功能如何实现
- 清屏: 将显存中当前屏幕中的字符设为空格符;
- 设置前景色: 设置显存中当前屏幕中处于奇地址的属性字节的第 0、1、2 位; 012 位存放前景色
- 设置背景色: 设置显存中当前屏幕中处于奇地址的属性字节的第 4、5、6 位; 456 位存放背景色
- 向上滚动一行: 依次将第 n+1 行的内容复制到第 n 行处:最后一行为空。
;功能子程序1:清屏
sub1: push bx ;保护现场,调用子程序的时候,注意要保护现场,运行子程序的时候,可能会修改一些寄存器的值
push cx
push es
mov bx,0b800h
mov es,bx
mov bx,0
mov cx,2000
sub1s: mov byte ptr es:[bx],' ' ;循坏2000次
add bx,2
loop sub1s
pop es ;恢复现场
pop cx
pop bx
ret
;功能子程序2:设置前景
sub2: push bx
push cx
push es
mov bx,0b800h
mov es,bx
mov bx,1
mov cx,2000
sub2s: and byte ptr es:[bx],11111000b
or es:[bx],al
add bx,2
loop sub2s
pop es
pop cx
pop bx
ret
;功能子程序3:设置背景色
sub3: push bx
push cx
push es
mov cl,4
shl al,cl
mov bx,0b800h
mov es,bx
mov bx,1
mov cx,2000
sub3s: and byte ptr es:[bx],10001111b
or es:[bx],al
add bx,2
loop sub2s
pop es
pop cx
pop bx
ret
;功能子程序4:向上滚动一行
sub4:
push cx
push si
push di
push es
push ds
mov si,0b800h
mov es,si
mov ds,si
mov si,160 ;ds:si指向第n+1行,第1行
mov di,0 ;es:di指向第n行,第0行
cld
mov cx,24;共复制24行
sub4s:
push cx
mov cx,160
rep movsb ;复制
pop cx
loop sub4s
mov cx,80
mov si,0
sub4s1:
mov byte ptr es:[160*24+si],' ' ;最后一行清空
add si,2
loop sub4s1
pop ds
pop es
pop di
pop si
pop cx
ret ;sub4 ends
- 可以将这些功能子程序的入口地址存储在一个表中,他们在表中的位置和功能号相对应
编程:实现一个子程序 setscreen,为显示输出提供如下功能: (1) 清屏。 (2) 设置前景色。 (3) 设置背景色。 (4) 向上滚动一行。
入口参数说明: (1) 用 ah 寄存器传递功能号:0 表示清屏,1 表示设置前景色,2 表示设置背景色,3 表示向上滚动一行; (2) 对于 2、3 号功能,用 al 传送颜色值,(al) ∈{0,1,2,3,4,5,6,7}
setscreen: jmp short set
table dw sub1,sub2,sub3,sub4
set:
push bx
cmp ah,3 ;判断传递的是否大于 3
ja sret
mov bl,ah
mov bh,0
add bx,bx ;根据ah中的功能号计算对应子程序的地址在table表中的偏移
call word ptr table[bx] ;调用对应的功能子程序,学会本句代码,是本章节的【精髓】
sret:
pop bx
iret
;功能子程序1:清屏
sub1:
push bx
push cx
push es
mov bx,0b800h
mov es,bx
mov bx,0
mov cx,2000
sub1s:
mov byte ptr es:[bx],' '
add bx,2
loop sub1s
pop es
pop cx
pop bx
ret ;sub1 ends
;功能子程序2:设置前景色
sub2:
push bx
push cx
push es
mov bx,0b800h
mov es,bx
mov bx,1
mov cx,2000
sub2s:
and byte ptr es:[bx],11111000b
or es:[bx],al
add bx,2
loop sub2s
pop es
pop cx
pop bx
ret ;sub2 ends
;功能子程序3:设置背景色
sub3:
push bx
push cx
push es
mov cl,4
shl al,cl
mov bx,0b800h
mov es,bx
mov bx,1
mov cx,2000
sub3s:
and byte ptr es:[bx],10001111b
or es:[bx],al
add bx,2
loop sub2s
pop es
pop cx
pop bx
ret ; sub3 ends
;功能子程序4:向上滚动一行
sub4:
push cx
push si
push di
push es
push ds
mov si,0b800h
mov es,si
mov ds,si
mov si,160 ;ds:si指向第n+1行
mov di,0 ;es:di指向第n行
cld
mov cx,24;共复制24行
sub4s:
push cx
mov cx,160
rep movsb ;复制
pop cx
loop sub4s
mov cx,80
mov si,0
sub4s1:
mov byte ptr es:[160*24+si],' ' ;最后一行清空
add si,2
loop sub4s1
pop ds
pop es
pop di
pop si
pop cx
ret ;sub4 ends
第十七章 使用 BIOS 进入键盘输入和磁盘读写
引言
- 大多数有用的程序都需要处理用户的输入,键盘输入是最基本的输入。
- 程序和数据通常需要长期存储,磁盘是最常用的存储设备。
- BIOS 为这两种外设的 I/O 提供了最基本的中断例程,在本章中,我们对它们的应用和相关的问题进行讨论。
119 int9 中断例程对键盘输入的处理
CPU 在 9 号中断发生后,执行 int 9 中断例程,从 60h 端口读出扫描码, 并将其转化为相应的 ASCII 码或状态信息,存储在内存的指定空间(键盘缓冲区或状态字节)中。 17. 2 使用 int16h 中断例程读取键盘缓冲区
- BIOS 提供了 int 16h 中断例程供程序员调用。
- int 16h 中断例程中包含的一个最重要的功能是从键盘缓冲区中读取一个键盘输入,该功能的编号为 0。
- 下面的指令从键盘缓冲区(缓冲区的最低位)中读取一个键盘输入,并且将其从缓冲区中删除: mov ah,0 int 16h 结果:(ah)=扫描码,(al)=ASCII 码。
- int 16h 中断例程的 0 号功能,进行如下的工作: (1)检测键盘缓冲区中是否有数据; (2)没有则继续做第 1 步;(缓冲区随时有可能输入数据) (3)读取缓冲区第一个字单元中的键盘输入; (4)将读取的扫描码送入 ah,ASCII 码送入 al; (5)将己读取的键盘输入从缓冲区中删除。
- 可见,B1OS 的 int 9 中断例程和 int 16h 中断例程是一对相互配合的程序, int 9 中断例程向键盘缓冲区中写入, int 16h 中断例程从缓冲区中读出。 它们写入和读出的时机不同,int 9 中断例程在有键按下的时候向键盘缓冲区中写入数据; 而 int 16h 中断例程是在应用程序对其进行调用的时候,将数据从键盘缓冲区中读出。 【编程】接收用户的键盘输入,输入“r”,将屏幕上的字符设置为红色:输入“g”, 将屏幕上的字符设置为绿色;输入“b ”,将屏幕上的字符设置为蓝色。
;编程:
;接收用户的键盘输入,输入“r”,将屏幕上的字符设置为红色:输入“g”,
;将屏幕上的字符设置为绿色;输入“b ”,将屏幕上的字符设置为蓝色。
;A、B、C处的程序指令比较有技巧,请读者自行分析
code segment
assume cs:code
start:
mov ah,0
int 16h ;int 16h 0号功能实现从键盘缓冲区读取一个键盘输入
mov ah,1 ;A
cmp al,'r'
je red
cmp al,'g'
je green
cmp al,'b'
je blue
jmp short sret
red:
shl ah,1 ;B
green:
shl ah,1 ;C
blue:
mov bx,0b800h
mov es,bx
mov bx,1
mov cx,2000
s: and byte ptr es:[bx],11111000b ;设置颜色
or es:[bx],ah ;设置颜色
add bx,2
loop s
sret:
mov ax,4c00h
int 21h
code ends
end start
120 字符串的输入
int 21h 的 0a 号功能可以实现字符串的输入 也可以用 int 16h,通过显示键盘缓冲区中的内容,实现字符串的显示
- 使用 int 16h 显示字符串程序的处理过程如下 ① 调用 int 16h 读取键盘输入; ② 如果是字符,进入字符栈,显示字符栈中的所有字符;继续执行 ① ; ③ 如果是退格键,从字符栈中弹出一个字符,显示字符栈中的所有字符;继续执行 ① ; ④ 如果是 Enter 键,向字符栈中压入 0,返回。
- 子程序:字符栈的入栈、出栈和显示 参数说明 (ah)=功能号,0 表示入栈,1 表示出栈,2 表示显示; ds : si 指向字符栈空间; 对于 0 号功能:(al)=入栈字符; 对于 1 号功能:(al)=返回的字符; 对于 2 号功能:(dh)、(dl) =字符串在屏幕上显示的行、列位置。
;使用int 16h显示字符串的子程序:字符栈
;最基本的字符串输入程序,需要具备下面的功能:
;(1) 在输入的同时需要显示这个字符串;
;(2)一般在输入回车符后,字符串输入结束;
;(3)能够删除已经输入的字符。
;编写一个接收字符串的输入子程序,实现上面三个基本功能。
;因为在输入的过程中需要显示,子程序的参数如下:
; (dh)、(dl)=字符串在屏幕上显示的行、列位置;
; ds:si 指向字符串的存储空间,字符串以O 为结尾符。
;功能子程序实现
charstack:
jmp short charstart
table dw charpush,charpop,charshow
top dw 0 ;栈顶
charstart:
push bx
push dx
push di
push es
cmp ah,2
ja sret
mov bl,ah
mov bh,0
add bx,bx
jmp word ptr table[bx] ;使用直接定址表
charpush:
mov bx,top
mov [si][bx],al
inc top
jmp sret
charpop:
cmp top,0
je sret
dec top
mov bx,top
mov al,[si][bx]
jmp sret
charshow:
mov bx,0b800h
mov es,bx
mov al,160
mov ah,0
mul dh
mov di,ax
add dl,dl
mov dh,0
add di,dx
mov bx,0
charshows:
cmp bx,top
jne noempty
mov byte ptr es:[di],' '
jmp sret
noempty:
mov al,[si][bx]
mov es:[di],al
mov byte ptr es:[di+2],' '
inc bx
add di,2
jmp charshows
sret:
pop es
pop di
pop dx
pop bx
ret
121 应用 int13h 中断例程对键盘进行读写
- 磁盘的实际访问由磁盘控制器进行,我们可以通过控制磁盘控制器来访问磁盘。
- 注意,我们只能以扇区为单位对磁盘进行读写。 在读写扇区的时候,要给出面号、磁道号和扇区号。面号和磁道号从 0 开始,而扇区号从 1 开始。
- BIOS 提供了对扇区进行读写的中断例程,这些中断例程完成了许多复杂的和硬件相关的工作。
- 我们可以通过调用 BIOS 中断例程来访问磁盘。 BIOS 提供的访问磁盘的中断例程为 int 13h 。 如下,读取 0 面 0 道 1 扇区的内容到 0:200:
返回参数: 操作成功:(ah)=0,(al)=读入的扇区数 操作失败:(ah)=出错代码 将 0:200 中的内容写入 0 面 0 道 1 扇区示例 返回参数:
操作成功: (ah)=0,(al)=写入的扇区数
操作失败: (ah)=出错代码
5.注意:使用 int 13h 中断例程对软盘进行读写。直接向磁盘扇区写入数据是很危险的, 很可能覆盖掉重要的数据。 【编程】将当前屏幕的内容保存在磁盘上 分析:1 屏的内容占 4000 个字节,需要 8 个扇区(一个扇区 512B),我们用 0 面 0 道的 1~8 扇区存储显存中的内容。
code segment
assume cs:code
start:
mov ax,0b800h
mov es,ax
mov bx,0 ;es:bx 指向将写入磁盘的数据的内存区
mov al,8 ;写入的扇区数
mov ch,0 ;磁道号,从0开始
mov cl,1 ;扇区号 从1开始
mov dl,0 ;驱动器号0:软驱A, 1:软驱B,硬盘从80h开始, 80h:硬盘C,81h:硬盘D
mov dh,0 ;磁头号,(对于软盘即面号,因为一个面用一个磁头来读写)
mov ah,3 ;传递 int 13h 写入数据的功能号
int 13h
;返回参数
;操作成功:(ah) = 0,(al) = 写入的扇区数
;操作失败:(ah) = 出错代码
return:
mov ax,4c00h
int 21h
code ends
end start
【实验 17 和课程设计 2】 课程设计 1 在第十章
综合研究
- 研究试验 1 搭建一个精简的 C 语言开发环境
- 研究试验 2 使用寄存器
- 研究试验 3 使用内存空间
- 研究试验 4 不用 main 函数编程
- 研究试验 5 函数如何接受不定数量的参数