第一章 基础知识
汇编语言的三类指令
1. 汇编指令
汇编指令是直接被CPU执行的指令,它们在汇编时被转换为对应的机器码。主要包括:
-
数据传送指令:如
MOV
,用于在寄存器、内存和I/O端口之间传送数据。MOV AX, BX ; 将BX中的数据传送到AX
-
算术与逻辑指令:如
ADD
、SUB
,用于进行数学计算和逻辑操作。ADD AX, BX ; 将BX中的数据加到AX中
-
控制转移指令:如
JMP
、CALL
,用于改变程序执行流程。JMP LABEL ; 无条件跳转到LABEL标签
2. 伪指令
伪指令是指导汇编器工作的指令,它们在汇编过程中被处理,但不生成机器码。主要包括:
-
段定义指令:如
SEGMENT
、ENDS
,用于定义和结束一个段。DATA SEGMENT
-
数据定义指令:如
DB
、DW
,用于定义字节或字数据。DB 0x55 ; 定义一个字节数据0x55
-
常量定义指令:如
EQU
,用于定义常量。MAXLEN EQU 255 ; 定义常量MAXLEN为255
3. 符号指令
符号指令用于标识变量、常量、代码段等,在汇编过程中被替换为具体的地址或值。它们没有对应的机器码。
-
标签:用于标识代码中的位置。
LABEL: ; 定义一个标签LABEL
-
宏定义:如
MACRO
,用于定义宏,简化代码编写。MYMACRO MACRO ; 宏定义内容 ENDM
CPU对存储器的读写
存储器结构与存储单元
存储器(Memory)由多个存储单元(Memory Cell)组成,每个存储单元都有唯一的地址(Address),用于存储数据(Data)。存储器可以分为两大类:
- 随机存取存储器(RAM):可读可写,用于存储临时数据。RAM是易失性存储器,断电后数据会丢失。
- 只读存储器(ROM):只能读取,用于存储固件和不可变数据。ROM是非易失性存储器,断电后数据仍然保存。
每个存储单元通常存储一个字节(8位)或一个字(16位)的数据,存储单元按照地址从零开始顺序编号。CPU通过地址访问这些存储单元中的数据。
总线系统
CPU通过总线(Bus)与存储器进行通信。总线系统包括:
地址总线
数据总线
数据总线是一组用于在CPU、内存和外设设备之间传输数据的信号线。数据总线的宽度决定了每次传输的数据量。例如,32位数据总线一次可以传输32位(4字节)的数据。
控制总线
控制总线是一组用于传输控制信号的信号线,这些信号用于协调和管理系统各部分之间的操作。控制信号包括读/写命令、时钟信号、中断请求等。
-
地址总线是一组用于传输存储单元地址的信号线。它的主要作用是指定内存或外设设备中的位置,以便CPU可以读写数据。地址总线的宽度决定了CPU可以访问的最大内存空间。例如,16位地址总线可以访问2^16 = 65,536个存储单元。
- 功能:传输内存地址。
- 影响:地址总线的宽度决定了CPU可以寻址的内存空间范围。例如,32位地址总线可以寻址4GB的内存空间。
- 方向:地址总线一般是单向的(寻址信息通常是从CPU发出)
- 功能:传输数据,包括指令和数据字。
- 影响:数据总线的宽度影响了系统的数据传输速率和性能。例如,64位数据总线相比于32位数据总线,可以一次传输更多的数据,从而提高系统性能。
- 方向:数据总线是双向的,因为数据既可以从CPU发送到内存或I/O设备,也可以从内存或I/O设备发送到CPU。
- 功能:传输控制信号以管理和协调系统操作。
- 影响:控制总线的效率和设计直接影响系统的整体性能和稳定性。它们确保数据和地址的传输按照正确的时序进行。
- 方向:控制总线是双向的,因为控制信号既可以从CPU发送到其他组件,也可以从其他组件发送到CPU。
读操作
读取存储器的过程可以分为以下几个步骤:
-
地址传输:
- CPU将要读取的数据地址放入地址总线。地址总线的宽度决定了CPU能够访问的最大内存地址范围。
MOV AX, [1234H] ; 读取内存地址1234H的数据到AX寄存器
- CPU将要读取的数据地址放入地址总线。地址总线的宽度决定了CPU能够访问的最大内存地址范围。
-
控制信号发送:
- CPU通过控制总线发送读命令(Read Command)到存储器。控制信号通知存储器准备好数据。
; 控制总线发送读信号
- CPU通过控制总线发送读命令(Read Command)到存储器。控制信号通知存储器准备好数据。
-
数据传输:
- 存储器接收到读命令后,将指定地址的数据放入数据总线。数据总线的宽度决定了一次能够传输的数据量。
; 数据总线传输数据
- 存储器接收到读命令后,将指定地址的数据放入数据总线。数据总线的宽度决定了一次能够传输的数据量。
-
数据接收:
- CPU从数据总线读取数据,并将其存入指定的寄存器或内存位置。
; CPU从数据总线接收数据并存入AX
- CPU从数据总线读取数据,并将其存入指定的寄存器或内存位置。
写操作
写入存储器的过程可以分为以下几个步骤:
-
地址传输:
- CPU将要写入数据的地址放入地址总线。
MOV [1234H], AX ; 将AX寄存器的数据写入内存地址1234H
- CPU将要写入数据的地址放入地址总线。
-
数据传输:
- CPU将要写入的数据放入数据总线。
; 数据总线传输要写入的数据
- CPU将要写入的数据放入数据总线。
-
控制信号发送:
- CPU通过控制总线发送写命令(Write Command)到存储器。控制信号通知存储器接收数据。
; 控制总线发送写信号
- CPU通过控制总线发送写命令(Write Command)到存储器。控制信号通知存储器接收数据。
-
数据存储:
- 存储器接收到写命令后,将数据总线上的数据写入指定地址的存储单元。
; 存储器将数据写入指定地址
- 存储器接收到写命令后,将数据总线上的数据写入指定地址的存储单元。
内存地址空间的分配
在计算机体系结构中,地址总线是单向的,即CPU向内存或I/O设备发送地址信息。CPU通过逻辑地址空间访问物理存储器,当CPU在某个地址空间中执行读写操作时,实际上是在相应的物理存储器单元中进行这些操作(对ROM写无效,ROM是只读)
8086PC内存地址空间的分配
-
主存储器地址空间 (RAM)
- 地址范围:00000h 到 9FFFFh
- 描述:这是系统的主内存区域,用于存放运行中的程序和数据。
-
显示地址空间
- 地址范围:A0000h 到 BFFFFh
- 描述:这是显示卡的显存地址空间。当向这个区域的内存单元中写数据时,实际上是向显示存储器中写入数据,这些数据会被显示卡输出到显示器上。
- 详细说明:在这个区域内,A0000h 到 AFFFFh 通常用于EGA/VGA显卡的显存,B0000h 到 B7FFFh 用于单色显示器(MDA),B8000h 到 BFFFFh 用于彩色显示器(CGA)。
-
各类ROM地址空间
- 地址范围:C0000h 到 FFFFFh
- 描述:这部分内存用于存储只读存储器(ROM)中的数据,包括BIOS程序和扩展卡的固件等。
- 详细说明:C0000h 到 C7FFFh 一般用于显示适配器的BIOS,E0000h 到 FFFFFh 则是系统BIOS区域。
CPU 与不同类型设备交互
“CPU 与所有设备(包括内存和外设)的交互,其主要区别在于访问的地址空间不同。CPU 通过不同的地址空间(内存地址空间、I/O 地址空间或内存映射 I/O 地址空间)与这些设备进行数据传输和控制操作。
示例一:CPU 与 RAM 交互
-
读操作:
- 发送地址:CPU 将要读取的内存地址(例如 0x0000)放在地址总线上。
- 控制信号:CPU 通过控制总线发送读命令。
- 数据传输:RAM 控制器读取地址 0x0000 处的数据,并通过数据总线传送给 CPU。假设返回的数据是 0x1234。
CPU -> 地址总线 -> 0x0000 CPU -> 控制总线 -> 读命令 RAM -> 数据总线 -> 0x1234
-
写操作:
- 发送地址和数据:CPU 将要写入的内存地址(例如 0x0000)和数据(例如 0x5678)放在地址总线和数据总线上。
- 控制信号:CPU 通过控制总线发送写命令。
- 数据写入:RAM 控制器将数据 0x5678 写入地址 0x0000。
CPU -> 地址总线 -> 0x0000 CPU -> 数据总线 -> 0x5678 CPU -> 控制总线 -> 写命令 RAM <- 数据总线 <- 0x5678
示例二:CPU 与显卡显存交互(内存映射 I/O)
-
写操作:
- 发送地址和数据:CPU 将要写入的显存地址(例如 0xA0000)和数据(例如图像数据)放在地址总线和数据总线上。
- 控制信号:CPU 通过控制总线发送写命令。
- 数据写入:显卡控制器将数据写入显存的指定地址 0xA0000。这些数据将被显示在屏幕上。
CPU -> 地址总线 -> 0xA0000 CPU -> 数据总线 -> 图像数据 CPU -> 控制总线 -> 写命令 显存 <- 数据总线 <- 图像数据
示例三:CPU 与键盘交互(I/O 端口映射)
-
读操作:
- 发送 I/O 端口地址:CPU 将要读取的 I/O 端口地址(例如 0x60)放在地址总线上。
- 控制信号:CPU 通过控制总线发送读命令。
- 数据传输:键盘控制器读取按键数据,并通过数据总线传送给 CPU。假设返回的按键数据是 0x1E(代表某个按键)。
CPU -> 地址总线 -> 0x60 CPU -> 控制总线 -> 读命令 键盘控制器 -> 数据总线 -> 0x1E
第2章寄存器
0. 典型的CPU结构
典型的CPU由运算器、控制器、寄存器等器件构成,这些器件通过片内总线相连。
- 运算器:进行信息处理
- 控制器:控制各种器件进行工作
- 寄存器:进行信息存储
1. 8086 CPU的寄存器
8086 CPU有14个16位寄存器:
- 通用寄存器:AX、BX、CX、DX
- 索引寄存器:SI、DI
- 指针寄存器:SP、BP、IP
- 段寄存器:CS、SS、DS、ES
- 状态寄存器:PSW
16位结构CPU的特性
- 运算器一次最多可以处理16位的数据
- 寄存器的最大宽度为16位
- 寄存器和运算器之间的通路为16位
数据处理
- 字节 (byte):8位,可以存在8位寄存器中
- 字 (word):16位,由两个字节组成,可以存在一个16位寄存器中
存储模式
8086采用小端模式:高地址存放高位字节,低地址存放低位字节。
2. 寄存器
16位寄存器
这些寄存器一次可以存储一个16位的数据(一个字)。
- AX、BX、CX、DX:这些是通用的16位寄存器。
- AX:累加器寄存器(Accumulator Register)
- BX:基址寄存器(Base Register)
- CX:计数器寄存器(Count Register)
- DX:数据寄存器(Data Register)
8位寄存器
这些寄存器是上面16位寄存器的一部分,可以分别存储8位的数据(一个字节)。
- AH 和 AL:高8位和低8位组成AX寄存器。
- BH 和 BL:高8位和低8位组成BX寄存器。
- CH 和 CL:高8位和低8位组成CX寄存器。
- DH 和 DL:高8位和低8位组成DX寄存器。
3. 物理地址生成
8086 CPU有20位地址总线,可以传送20位地址,达到1MB寻址能力。然而,内部一次性处理、传输、暂时存储的地址为16位。8086 CPU通过在内部用两个16位地址合成的方法来形成一个20位的物理地址:
- 物理地址 = 段地址 × 16 + 偏移地址
例如,8086 CPU要访问地址为123C8H的内存单元:
1230H左移一位(空出4位)加上00C8H合成123C8H。
4. 段寄存器
通过将内存划分为段,用段地址指示段,用偏移地址访问段内的单元,可以用分段的方式来管理内存。段寄存器有4个:CS、DS、SS、ES,提供内存单元的段地址。
段的定义
- 数据段:用来存放数据
- 代码段:用来存放代码
- 栈段:用来存放栈
段的起始地址
一个段的起始地址一定是16的倍数,偏移地址为16位,变化范围为0-FFFFH,所以一个段的长度最大为64KB。
5. 指令执行
CS和IP
CS为代码段寄存器,IP为指令指针寄存器。CPU将CS、IP中的内容当作指令的段地址和偏移地址,用它们合成指令的物理地址,然后执行指令。8086 CPU的工作过程简要描述如下:
- 从CS指向的内存单元读取指令,读取的指令进入指令缓冲器
- IP = IP + 读取指令的长度,从而指向下一条指令
- 执行指令
- 重复上述过程
JMP指令
能够改变CS、IP内容的指令被统称为转移指令,例如:
jmp 段地址:偏移地址
:用指令中给出的段地址修改CS,偏移地址修改IPjmp 某一合法寄存器
:仅修改IP的内容,如jmp ax
第三章 寄存器(内存访问)
3.1 内存中字的存储
内存中的字是如何存储的呢?这里举一个例子:
CPU中使用16位寄存器存储一个字单元,其中高八位存放高位字节,低八位存放低位字节。
我们拿0—3一共四个内存空间存放数据20000(4E20H)和18(0012H)
上图的0,1,2,
3……就是能访问到的地址,用16进制来表示一个字节,比如20H,4EH等等。 由于内存单元是字节单元,则一个字就要用两个连续的内存单元来存。 图中0、1两个内存单元存放数据4E20H,0是低地址单元,1是高地址单元。 高地址内存单元中存放字型数据的高位字节,低地址内存单元存放字型数据的低位字节。 以 起始地址为N的字单元简称N地址字单元。
3.2 DS和address
在8086CPU中,DS(数据段寄存器)和 CS(代码段寄存器)是两种重要的段寄存器
主要区别
-
DS(数据段寄存器):
- 用于存储数据段的基地址。
- 数据段用于存放程序的数据(如变量、数组等)。
- 内存访问指令默认使用DS寄存器来确定数据段的基地址。
-
CS(代码段寄存器):
- 用于存储代码段的基地址。
- 代码段用于存放程序的指令(代码)。
- CPU在执行指令时使用CS寄存器来确定代码段的基地址
3.3 字的传送
8086CPU是16位结构,有16根数据线,一次可以传送16位的数据,也就是一个字(word)。
- 一个字需要两个连续的内存单元来存储。
- 低位字节存放在低地址单元,高位字节存放在高地址单元。
- 字单元由两个连续的内存单元组成。例如,地址为N的字单元,N地址存放低位字节,N+1地址存放高位字节。
3.4 mov、add、sub指令
这三个指令都有两个操作对象。
mov
mov
指令不会将内存地址本身传送到寄存器,而是传送内存地址中存储的数据。
1. mov ax, [1234H]
- 作用:将内存地址
DS:1234H
中的数据传送到AX
寄存器。 - 解释:这条指令表示从内存地址
1234H
(相对于数据段寄存器DS
的偏移量)中读取数据,然后将这个数据存储到AX
寄存器中。 - 示例:
mov ax, [1234H] ; 将内存地址DS:1234H中的数据传送到AX寄存器
2. mov ax, 1234H
- 作用:将立即数
1234H
传送到AX
寄存器。 - 解释:这条指令表示将立即数
1234H
直接赋值给AX
寄存器,而不涉及任何内存访问。 - 示例:
mov ax, 1234H ; mov 寄存器, 数据
mov bx, ax ; mov 寄存器, 寄存器
mov ax, [1234H] ; mov 寄存器, 内存单元
mov [1234H], ax ; mov 内存单元, 寄存器
mov ds, ax ; mov 段寄存器, 寄存器
add
add ax, 10213 ; add 寄存器, 数据 (需要转化为十六进制再加)
add ax, bx ; add 寄存器, 寄存器
add ax, [1234H] ; add 寄存器, 内存单元
add [1234H], ax ; add 内存单元, 寄存器
sub
sub ax, 1 ; sub 寄存器, 数据
sub ax, bx ; sub 寄存器, 寄存器
sub ax, [1234H] ; sub 寄存器, 内存单元
sub [1234H], ax ; sub 内存单元, 寄存器
3.5 数据段
我们可以根据需要,将一组长度小于64KB、地址连续、起始地址为16的倍数的内存单元定义为一个数据段,用来作为专门存放数据的内存空间。
3.6 栈
栈是一种先进后出的存储数据结构。从程序的角度来说,有一个标记,一直指向栈顶元素。
3.7 CPU提供的栈机制
8086CPU提供入栈和出栈的指令,分别是push(入栈)和pop(出栈),在编程中可以将一个内存段看作栈来使用。
push ax
:将寄存器AX中的数据送入栈中。pop ax
:将栈顶的数据取出送入AX中。这两个操作的基本单位都是字(16位)。
在8086CPU中,段寄存器SS(栈段寄存器)和寄存器SP(栈指针)一起使用,SS存放栈段的基地址,SP存放栈顶的偏移地址,任意时刻,SS指向栈顶元素。
3.8 栈顶超界的问题
编程中根据可能用到的最大栈空间,来安排栈的大小,防止入栈的数据导致超界,出栈时也要注意,防止栈空时继续出栈导致的超界。
栈空间之外的空间里很可能存放了其他用途的数据、代码等,如果入栈出栈超界,导致这些代码或数据改变,会造成严重的后果。
3.9 push、pop指令
Push和pop指令后可以加寄存器、段寄存器、内存单元等。确定栈时需要初始化栈顶,使用mov ss, ax
指令。
Push和pop指令访问的内存空间的地址不是在指令中给出的,而是由SS
指出的,同时也修改SP(存储偏移地址)中的内容。
push
:SP = SP - 2pop
:SP = SP + 2
栈为空时,SS:SP指向最底部单元(eg. 10000H ~ 1000FH 初始SS:SP= 1000:0010)
3.10 栈段
问题一:
如果我们将10000H~1FFFFH这段空间当做栈段,初始状态是空的,此时,SS=1000H,SP=?
换位思考:
栈为空,相当于栈中唯一的元素出栈,出栈后,SP=SP+2。
SP原来为FFFEH,加2后SP=0,所以,当栈为空的时候,SS=1000H,SP=0。
问一个栈段最大可以设为多少?:
所以栈顶的变化范围是0~FFFFH,所以一个栈段的容量最大是64KB
(容量为10000H,即 2^16 btye = 65536 btye = 64KB)
第四章 第一个汇编程序
4.1 一个源程序从写出到执行的过程
1.编写汇编语言(用编辑器进行汇编语言的编写)
2.对源程序进行编译连接(对源程序进行编译,产生目标文件;再连接程序对目标文件进行连接,生产可执行文件)
3.执行可执行文件中的程序(执行文件需要将机器码和数据载入内存,然后CPU执行)
4.2 源程序
例如源程序:
1.伪指令
汇编语言中的两种指令:伪指令,汇编指令。
汇编指令有对应的机器码,伪指令没有对应机器码不被CPU执行而被编译器执行。
A.××× segment·····×××ends是成对出现的伪指令,用来定义一个代码段。
B.end(不是ends)是一个汇编程序结束的标志。
C.assume(假设)功能:将指定用途的段和相关的段寄存器关联起来。比如上面的代码将cs和定义的代码段codesg联系起来。
2.源程序中的“程序”
这里的源程序指最终由计算机执行、处理的指令或数据(最后的二进制机器码)。
3.程序的结构
1首先是定义段 abc segment ·····abc ends,
2在里面写汇编指令把·····换成汇编指令,
3指出何处结束end,
4用assume将段寄存器与abc联系起来。
4.程序的返回
举例:一个p2程序要执行必须有一个正在执行的p1程序把p2加载入内存后,才能将CPU交给p2,p2随之运行,p2结束后返回控制权给p1 这个返回过程就是:程序返回
mov ax,4c00H
int 21H
这两条指令实现的功能就是程序返回。
5.语法错误和逻辑错误
语法错误就是编译器发现的错误
运行的结果与预期的不同就是逻辑错误。
4.3 汇编程序从写出到执行过程
编程- >(.asm)
- 编译 -> (.obj)
- 连接(Link) -> (.exe)
- 加载(command) -> 内存中程序 -> CPU运行
4.4 跟踪程序执行过程
不同的加载方式:
command加载内存,运行程序(无法逐条查看指令执行过程,放弃了对CPU的控制)
利用debug加载内存,设置CS:IP指向程序入口(可以单步执行程序,不放弃对CPU的控制)
内存分布
PSP( 0 ~ 255): 程序段前缀的数据区(利用PSP与被加载的程序进行通信)
程序区(SA (DS)+ 10H:0 )
存入内存区段地址到DS,初始化其他相关寄存器后。设置CS:IP指向程序入口
第五章 [BX]和loop指令
1、[bx] 和 loop指令
[bx] 的含义:
[bx]同样表示一个内存单元,它的偏移地址在bx中,段地址默认在ds中
loop指令的格式是:loop 标号
CPU执行loop指令的时候,要进行两步操作:
(cx) = (cx) - 1;
判断 cx 中的值,不为零则转至标号处执行程序,如果为零则向下执行。
例如:计算
assume cs:code
code segment
mov ax, 2
mov cx, 11 ;循环次数
s: add ax, ax
loop s ;在汇编语言中,标号代表一个地址,标号s实际上标识了一个地址,
;这个地址处有一条指令:add ax,ax。
;执行loop s时,首先要将(cx)减1,然后若(cx)不为0,则向前
;转至s处执行add ax,ax。所以,可以利用cx来控制add ax,ax的执行次数。
mov ax,4c00h
int 21h
code ends
end
2,loop 和 [bx] 的联合应用
计算ffff:0 ~ ffff:b单元中的数据的和,结果存储在dx中
问题分析:
这些内存单元都是字节型数据范围0 ~ 255 ,12个字节数据和不会超过65535,dx可以存下
对于8位数据不能直接加到 dx
解决方案:
用一个16位寄存器来做中介。将内存单元中的8位数据赋值到一个16位寄存器a中,再将ax中的数据加到dx
assume cs:code
code segment
mov ax, 0ffffh ;在汇编源程序中,数据不能以字母开头,所以要在前面加0。
mov ds, ax
mov bx, 0 ;初始化ds:bx指向ffff:0
mov dx, 0 ;初始化累加寄存器dx,(dx)= 0
mov cx, 12 ;初始化循环计数寄存器cx,(cx)= 12
s: mov al, [bx]
mov ah, 0
add dx, ax ;间接向dx中加上((ds)* 16 +(bx))单元的数值
inc bx ;ds:bx指向下一个单元
loop s
mov ax, 4c00h
int 21h
code ends
end
3、段前缀
mov ax, ds:[bx]
mov ax, cs:[bx]
mov ax, ss:[bx]
mov ax, es:[bx]
mov ax, ss:[0]
mov ax, cs:[0]
这些出现在访问内存单元的指令中,用于显式地指明内存单元的段地址
的“ds:”,“cs:”,“ss:”,“es:”,在汇编语言中称为段前缀。
段前缀的使用
将内存ffff:0 ~ ffff:b单元中的数据复制到0:200 ~ 0:20b单元中。
1.DL和DS的关系
- EDX 是一个 32 位寄存器,可以分为多个部分:EDX(32 位),DX(16 位),DH(高 8 位)和 DL(低 8 位)。
- 操作 DL 只会影响 EDX 寄存器的低 8 位,不会影响 EDX 的其他部分,如 DH 或者整个 DX(16 位)。
2. ES寄存器
ES(Extra Segment)寄存器是 x86 架构中的一个 16 位段寄存器,主要用于字符串操作和数据传输。它通常与 DI(目的索引)寄存器结合使用,以指向附加的数据段。常见用法包括字符串指令(如 MOVSB
和 CMPSB
)中的目标段地址,帮助在内存中进行数据移动和比较。
assume cs:code
code segment
mov ax, 0ffffh
mov ds, ax ;(ds)= 0ffffh
mov ax, 0020h
mov es, ax ;(es)= 0020h 0:200 等效于 0020:0
mov bx, 0 ;(bx)= 0,此时ds:bx指向ffff:0,es:bx指向0020:0
mov cx,12 ;(cx)=12,循环12次
s: mov dl,[bx] ;(d1)=((ds)* 16+(bx)),将ffff:bx中的字节数据送入dl
mov es:[bx],dl ;((es)*16+(bx))=(d1),将dl中的数据送入0020:bx
inc bx ;(bx)=(bx)+1
loop s
mov ax,4c00h
int 21h
code ends
end
注意
1.汇编源程序中,数据不能以字母开头
如题如果在源码中的数据是以字母开头的数据,则编译器会报错
例如如下代码,执行编译
assume cs:code
code segment
start:
mov ax,ffffH
code ends
end start
执行编译会有如下报错:
第6章 包含多个段的程序
在代码段中使用数据
dw定义的数据位于代码段最开始,偏移地址为:0
这些数据位于cs:0 cs:2 ... cs:E
;计算 8 个数据的和存到 ax 寄存器
assume cs:code
code segment
dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h ;define word 定义8个字形数据
start: mov bx, 0 ;标号start
mov ax, 0
mov cx, 8
s: add ax, cs:[bx]
add bx, 2
loop s
mov ax, 4c00h
int 21h
code ends
end start ;end除了通知编译器程序结束外,还可以通知编译器程序的入口在什么地方
;用end指令指明了程序的入口在标号start处,也就是说,“mov bx,0”是程序的第一条指令。
在代码段中使用栈
;利用栈,将程序中定义的数据逆序存放。
assume cs:codesg
codesg segment
dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h ; 0-15单元
dw 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 ; 16-47单元作为栈使用
start: mov ax, cs
mov ss, ax
mov sp, 30h ;将设置栈顶ss:sp指向栈底cs:30。 30h = 48d
mov bx, 0
mov cx, 8
s: push cs:[bx]
add bx, 2
loop s ;以上将代码段0~15单元中的8个字型数据依次入栈
mov bx, 0
mov cx, 8
s0: pop cs:[bx]
add bx,2
loop s0 ;以上依次出栈8个字型数据到代码段0~15单元中
mov ax,4c00h
int 21h
codesg ends
end start ;指明程序的入口在start处
将数据、代码、栈放入不同的段
1.名称为“data”的段名,相当于标号
程序对段名的引用,eg. mov ds,data(错误的) data被编译器处理为一个表示段地址的数值
assume cs:code,ds:data,ss:stack
data segment
dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h ;0-15单元
data ends
stack segment
dw 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 ;0-31单元
stack ends
code segment
start: mov ax, stack;将名称为“stack”的段的段地址送入ax
mov ss, ax
mov sp, 20h ;设置栈顶ss:sp指向stack:20。 20h = 32d
mov ax, data ;将名称为“data”的段的段地址送入ax
mov ds, ax ;ds指向data段
mov bx, 0 ;ds:bx指向data段中的第一个单元
mov cx, 8
s: push [bx]
add bx, 2
loop s ;以上将data段中的0~15单元中的8个字型数据依次入栈
mov bx, 0
mov cx, 8
s0: pop [bx]
add bx, 2
loop s0 ;以上依次出栈8个字型数据到data段的0~15单元中
mov ax, 4c00h
int 21h
code ends
end start
;“end start”说明了程序的入口,这个入口将被写入可执行文件的描述信息,
;可执行文件中的程序被加载入内存后,CPU的CS:IP被设置指向这个入口,从而开始执行程序中的第一条指令
第七章
1.指令
and指令:逻辑与指令,按位进行与运算。
mov al, 01100011B and al, 00111011B
执行后:al=00100011B即都为1才为1
or指令:逻辑或指令,按位进行或运算。
mov al, 01100011B or al, 00111011B
执行后:al=01111011B 即只要有一个为1就为1
2.(ASCLL)以字符形式给出的数据
assume cs:code,ds:data
data segment
db 'unIx' ;相当于“db 75H,6EH,49H,58H”
db 'foRK'
data ends
code segment
start: mov al, 'a' ;相当于“mov al, 61H”,“a”的ASCI码为61H;
mov b1, 'b'
mov ax, 4c00h
int 21h
code ends
end start
大小写转换的问题
小写字母的ASCII码值比大写字母的ASCII码值大20H
大写字母ASCII码的第5位为0,小写字母的第5位为1(其他一致)
3、不同的寻址方式的灵活应用
1.[idata]用一个常量来表示地址,可用于直接定位一个内存单元;
2.[bx]用一个变量来表示内存地址,可用于间接定位一个内存单元;
3.[bx+idata]用一个变量和常量表示地址,可在一个起始地址的基础上用变量间接定位一个内存单元;
4.[bx+si]用两个变量表示地址;
5.[bx+si+idata]用两个变量和一个常量表示地址。
示例代码:将datasg段中每个单词改为大写字母
;将datasg段中每个单词改为大写字母
assume cs:codesg,ds:datasg,ss:stacksg
datasg segment
db 'ibm ' ;16
db 'dec '
db 'dos '
db 'vax ' ;看成二维数组
datasg ends
stacksg segment ;定义一个段,用来做栈段,容量为16个字节
dw 0, 0, 0, 0, 0, 0, 0, 0
stacksg ends
codesg segment
start: mov ax, stacksg
mov ss, ax
mov sp, 16
mov ax, datasg
mov ds, ax
mov bx, 0 ;初始ds:bx
;cx为默认循环计数器,二重循环只有一个计数器,所以外层循环先保存cx值,再恢复,我们采用栈保存
mov cx, 4
s0: push cx ;将外层循环的cx值入栈
mov si, 0
mov cx, 3 ;cx设置为内层循环的次数
s: mov al, [bx+si]
and al, 11011111b ;每个字符转为大写字母
mov [bx+si], al
inc si
loop s
add bx, 16 ;下一行
pop cx ;恢复cx值
loop s0 ;外层循环的loop指令将cx中的计数值减1
mov ax,4c00H
int 21H
codesg ends
end start
第八章 数据处理的两个基本问题
1、 bx、si、di和bp
在8086CPU中,只有这4个寄存器可以用在“[…]”中来进行内存单元的寻址。在[ ]中,这4个寄存器可以单个出现,
或只能以4种组合出现:bx和si、bx和di、bp和si、bp和di。
只要在[……]中使用寄存器bp,而指令中没有显性地给出段地址, 段地址就默认在ss中
2、机器指令处理的数据在什么地方
数据处理大致可分为3类:读取、写入、运算。
在机器指令这一层来讲,并不关心数据的值是多少,而关心指令执行前一刻,它将要处理的数据所在的位置。
指令在执行前,所要处理的数据可以在3个地方:CPU内部、内存、端口
3、汇编语言中数据位置的表达
汇编语言中用3个概念来表达数据的位置
立即数(idata)
mov ax, 1 ;对于直接包含在机器指令中的数据(执行前在CPU的指令缓冲器中)
add bx, 2000h ;在汇编语言中称为:立即数(idata)
or bx, 00010000b
mov al, 'a'
寄存器
mov ax, bx ;指令要处理的数据在寄存器中,在汇编指令中给出相应的寄存器名。
mov ds, ax
push bx
mov ds:[0], bx
push ds
mov ss, ax
mov sp, ax
段地址和偏移地址
;指令要处理的数据在内存中,在汇编指令中可用[X]的格式给出EA,SA在某个段寄存器中。
mov ax, [0]
mov ax, [di]
mov ax, [bx+8]
mov ax, [bx+si]
mov ax, [bx+si+8] ;以上段地址默认在ds中
mov ax, [bp]
mov ax, [bp+8]
mov ax, [bp+si]
mov ax, [bp+si+8] ;以上段地址默认在ss中
mov ax, ds:[bp]
mov ax, es:[bx]
mov ax, ss:[bx+si]
mov ax, cs:[bx+si+8] ;显式给出存放段地址的寄存器
寻址方式
4. 指令要处理的数据有多长
8086CPU的指令,可以处理两种尺寸的数据,byte和word
通过寄存器名指明要处理的数据的尺寸。
有些指令默认了访问的是字单元还是字节单元
例如,push [1000H],push 指令只进行字操作。
5、寻址方式的综合应用
mov ax, seg
mov ds, ax
mov bx, 60h ;确定记录地址,ds:bx
mov word ptr [bx+0ch], 38 ;排名字段改为38 [bx].0ch
add word ptr [bx+0eh], 70 ;收入字段增加70 [bx].0eh
mov si, 0 ;用si来定位产品字符串中的字符
mov byte ptr [bx+10h+si], 'V' ;[bx].10h[si]
inc si
mov byte ptr [bx+10h+si], 'A'
inc si
mov byte ptr [bx+10h+si], 'X'
/*定义一个公司记录的结构体*/
struct company
{
char cn[3];/*公司名称*/
char hn[9];/*总裁姓名*/
int pm;/*排名*/
int sr;/*收入*/
char cp[3];/*著名产品*/
};
//sizeof (struct company) == 24
int main()
{
/*定义一个公司记录的变量,内存中将存有一条公司的记录*/
struct company dec = {"DEC", "Ken Olsen", 137, 40, "PDP"};
int i;
dec.pm = 38;
dec.sr = dec.sr + 70;
i = 0;
dec.cp[i] = 'V'; //mov byte ptr [bx].10h[si], 'V'
i++;
dec.cp[i] = 'A';
i++;
dec.cp[i] = 'X';
return 0;
}
6、div指令、dd、dup、mul指令
div是除法指令
除数:有8位和16位两种,在一个寄存器或内存单元中。
被除数:默认放在AX或DX和AX中,
如果除数为8位,被除数则为16位,默认在AX中存放;
如果除数为16位,被除数则为32位,在DX和AX中存放,DX存放高16位,AX存放低16位。结果:
如果除数为8位,则AL存储除法操作的商,AH存储除法操作的余数;
如果除数为16位,则AX存储除法操作的商,DX存储除法操作的余数。
;利用除法指令计算100001/100。
;100001D = 186A1H
mov dx, 1
mov ax, 86A1H ;(dx)*10000H+(ax)=100001
mov bx, 100
div bx
;利用除法指令计算1001/100
mov ax, 1001
mov bl, 100
div b1
伪指令dd
db和dw定义字节型数据和字型数据。
dd是用来定义dword(double word,双字)型数据的伪指令
操作符dup
dup在汇编语言中同db、dw、dd等一样,也是由编译器识别处理的符号。
它和db、dw、dd等数据定义伪指令配合使用,用来进行数据的重复
db 3 dup (0) ;定义了3个字节,它们的值都是0,相当于db 0,0,0。
db 3 dup (0, 1, 2) ;定义了9个字节,它们是0、1、2、0、1、2、0、1、2,相当于db 0,1,2,0,1,2,0,1,2。
db 3 dup ('abc', 'ABC') ;定义了18个字节,它们是abcABCabcABCabcABCC,相当于db 'abc', 'ABC' ,'abc' , 'ABC, 'abc', 'ABC'。
第九章
可以修改IP或者同时修改CS和IP的指令统称为转移指令
只修改IP的为段内转移,如:jmp ax
同时修改CS和IP的为段间转移,如:jmp 1000:0
段内转移又分为:短转移和近转移
短转移IP的修改范围为-128~127
近转IP的修改范围为-32768~32767
8086转移指令分类
· 无条件转移指令(如:jmp)
· 条件转移指令
· 循环指令(如:loop)
· 过程
· 中断
9.1 操作符offset
offset在汇编语言中是由编译器处理的符号,功能是取得标号的偏移地址
assume cs:codesg
codesg segment
start: mov ax,offset start ; 相当于mov ax,0
s: mov ax,offset s ; 相当于mov ax,3
codesg ends
end start
有如下程序段,将该程序中s处的一条指令复制到s0处
assume cs:codesg
codesg segment
s: mov ax,bx ; 机器码占两个字节
mov si,offset s
mov di,offset s0
mov ax,cs:[si]
mov cs:[di],ax
s0: nop
nop ; nop的机器码占两个字节
codesg ends
end s
9.2 jmp指令
jmp为无条件转移,转到标号处执行指令可以只修改IP,也可以同时修改CS和IP;
jmp指令要给出两种信息:
转移的目的地址
转移的距离(段间转移、段内短转移,段内近转移)
jmp short 标号 jmp near ptr 标号 jcxz 标号 loop 标号 等几种汇编指令,它们对 IP的修改
是根据转移目的地址和转移起始地址之间的位移来进行的。在它们对应的机器码中不包含转移的目的地址,而包含的是到目的地址的位移距离。
1、依据位移进行转移的jmp指令
jmp short 标号(段内短转移)
指令“jmp short 标号”的功能为(IP)=(IP)+8位位移,转到标号处执行指令
(1)8位位移 = “标号”处的地址 - jmp指令后的第一个字节的地址;
(2)short指明此处的位移为8位位移;
(3)8位位移的范围为-128~127,用补码表示
(4)8位位移由编译程序在编译时算出。
assume cs:codesg
codesg segment
start:mov ax,0
jmp short s ;s不是被翻译成目的地址
add ax, 1
s:inc ax ;程序执行后, ax中的值为 1
codesg ends
end start
CPU不需要这个目的地址就可以实现对IP的修改。这里是依据位移进行转移
读取和执行过程:
(CS)=0BBDH,(IP)=0006,上一条指令执行结束后CS:IP指向EB 03(jmp short s的机器码);
读取指令码EB 03进入指令缓冲器;
(IP) = (IP) + 所读取指令的长度 = (IP) + 2 = 0008,CS:IP指向add ax,1;
CPU指行指令缓冲器中的指令EB 03;
指令EB 03执行后,(IP)=000BH,CS:IP指向inc ax
jmp near ptr 标号 (段内近转移)
指令“jmp near ptr 标号”的功能为:(IP) = (IP) + 16位位移。
2、转移的目的地址在指令中的jmp指令
jmp far ptr 标号(段间转移或远转移)
指令 “jmp far ptr 标号” 功能如下:
(CS) = 标号所在段的段地址;
(IP) = 标号所在段中的偏移地址。
far ptr指明了指令用标号的段地址和偏移地址修改CS和IP。
assume cs:codesg
codesg segment
start: mov ax, 0
mov bx, 0
jmp far ptr s ;s被翻译成转移的目的地址0B01 BD0B
db 256 dup (0) ;转移的段地址:0BBDH,偏移地址:010BH
s: add ax,1
inc ax
codesg ends
end start
3、转移地址在寄存器或内存中的jmp指令
jmp 16位寄存器
功能:IP =(16位寄存器)
转移地址在内存中的jmp指令有两种格式:
jmp word ptr 内存单元地址
(段内转移)
功能:从内存单元地址处开始存放着一个字,是转移的目的偏移地址。
mov ax, 0123H
mov ds:[0], ax
jmp word ptr ds:[0]
;执行后,(IP)=0123H
jmp dword ptr 内存单元地址
(段间转移)
功能:从内存单元地址处开始存放着两个字,高地址处的字是转移的目的段地址,低地址处是转移的目的偏移地址。
- (CS)=(内存单元地址+2)
- (IP)=(内存单元地址)
mov ax, 0123H
mov ds:[0], ax;偏移地址
mov word ptr ds:[2], 0;段地址
jmp dword ptr ds:[0]
;执行后,
;(CS)=0
;(IP)=0123H
;CS:IP 指向 0000:0123。
4、jcxz指令和loop指令
jcxz指令
jcxz指令为有条件转移指令,所有的有条件转移指令都是短转移,
在对应的机器码中包含转移的位移,而不是目的地址。对IP的修改范围都为-128~127。
指令格式:jcxz 标号(如果(cx)=0,则转移到标号处执行。)
当(cx) = 0时,(IP) = (IP) + 8位位移
8位位移 = “标号”处的地址 - jcxz指令后的第一个字节的地址;
8位位移的范围为-128~127,用补码表示;
8位位移由编译程序在编译时算出。
当(cx)!=0时,什么也不做(程序向下执行)
loop指令
loop指令为循环指令,所有的循环指令都是短转移,在对应的机器码中包含转移的位移,而不是目的地址。
对IP的修改范围都为-128~127。
指令格式:loop 标号 ((cx) = (cx) - 1,如果(cx) ≠ 0,转移到标号处执行)。
(cx) = (cx) - 1;如果 (cx) != 0,(IP) = (IP) + 8位位移。
8位位移 = 标号处的地址 - loop指令后的第一个字节的地址;
8位位移的范围为-128~127,用补码表示;
8位位移由编译程序在编译时算出。
如果(cx)= 0,什么也不做(程序向下执行)
第十章 call和ret指令
call和ret指令都是转移指令,它们都修改IP,或同时修改CS和IP。
1、ret 和 retf
ret指令用栈中的数据,修改IP的内容,从而实现近转移;
retf指令用栈中的数据,修改CS和IP的内容,从而实现远转移。
CPU执行ret指令时,相当于进行: pop IP:
(1)(IP) = ( (ss) * 16 + (sp) )
(2)(sp) = (sp) + 2
CPU执行retf指令时,相当于进行:pop IP, pop CS:
(1)(IP) = ( (ss) * 16 + (sp) )
(2)(sp) = (sp) + 2
(3)(CS) = ( (ss) * 16 + (sp) )
(4)(sp) = (sp) + 2
assume cs:code
stack seqment
db 16 dup (0)
stack ends
code segment
mov ax, 4c00h
int 21h
start: mov ax, stack
mov ss, ax
mov sp, 16
mov ax, 0
push ax ;ax入栈
mov bx, 0
ret ;ret指令执行后,(IP)=0,CS:IP指向代码段的第一条指令。可以push cs push ax retf
code ends
end start
2、call 指令
call指令经常跟ret指令配合使用,因此CPU执行call指令,进行两步操作:
(1)将当前的 IP 或 CS和IP 压入栈中;
(2)转移(jmp)。
call指令不能实现短转移,除此之外,call指令实现转移的方法和 jmp 指令的原理相同。
call 标号(近转移)
CPU执行此种格式的call指令时,相当于进行 push IP jmp near ptr 标号
call far ptr 标号(段间转移)
CPU执行此种格式的call指令时,相当于进行:push CS,push IP jmp far ptr 标号
call 16位寄存器
CPU执行此种格式的call指令时,相当于进行: push IP jmp 16位寄存器
call word ptr 内存单元地址
CPU执行此种格式的call指令时,相当于进行:push IP jmp word ptr 内存单元地址
mov sp, 10h
mov ax, 0123h
mov ds:[0], ax
call word ptr ds:[0]
;执行后,(IP)=0123H,(sp)=0EH
call dword ptr 内存单元地址
CPU执行此种格式的call指令时,相当于进行:push CS
push IP
jmp dword ptr 内存单元地址
mov sp, 10h
mov ax, 0123h
mov ds:[0], ax
mov word ptr ds:[2], 0
call dword ptr ds:[0]
;执行后,(CS)=0,(IP)=0123H,(sp)=0CH
3、call 和 ret 的配合使用
分析下面程序
assume cs:code
code segment
start: mov ax,1
mov cx,3
call s ;(1)CPU指令缓冲器存放call指令,IP指向下一条指令(mov bx, ax),执行call指令,IP入栈,jmp
mov bx,ax ;(4)IP重新指向这里 bx = 8
mov ax,4c00h
int 21h
s: add ax,ax
loop s;(2)循环3次ax = 8
ret;(3)return : pop IP
code ends
end start
4.mul 指令
mul是乘法指令,使用 mul 做乘法的时候:相乘的两个数:要么都是8位,要么都是16位。
8 位: AL中和 8位寄存器或内存字节单元中;
16 位: AX中和 16 位寄存器或内存字单元中。
结果:
8位:AX中;
16位:DX(高位)和 AX(低位)中。
格式:mul 寄存器 或 mul 内存单元
;计算100*10
;100和10小于255,可以做8位乘法
mov al,100
mov bl,10
mul bl
;结果: (ax)=1000(03E8H)
;计算100*10000
;100小于255,可10000大于255,所以必须做16位乘法,程序如下:
mov ax,100
mov bx,10000
mul bx
;结果: (ax)=4240H,(dx)=000FH (F4240H=1000000)
5.DIV指令:
div 指令是8086汇编中的除法运算指令,它的结果不是浮点数,而是两个整数:商和余数。
被除数:
默认放在AX或(DX和AX)中,如果除数为8位,被除数为16位,被除数默认在AX中存放,
如果除数为16位,被除数为32位,被 除数则在(DX和AX)中存放,DX存放高16位,AX存放低16位。
格式如下:
div reg
div 内存单元
结果:
如果除数 B 是8位,那么除法的结果AL保存商,AH保存余数,
如果除数 B 是16位,那么除法的结果 AX保存商,DX保存余数。
第十一章 标志寄存器
1.标志寄存器
CPU内部的寄存器中,有一种特殊的寄存器(对于不同的处理机,个数和结构都可能不同)具有以下3种作用。
(1)用来存储相关指令的某些执行结果;
(2)用来为CPU执行相关指令提供行为依据;
(3)用来控制CPU的相关工作方式。
这种特殊的寄存器在8086CPU中,被称为标志寄存器(flag)。
8086CPU的标志寄存器有16位,其中存储的信息通常被称为程序状态字
在8086CPU的指令集中,
(1)运算指令(进行逻辑或算术运算)的执行是影响标志寄存器的,比如,add、sub、mul、div、inc、or、and等
(2)传送指令的执行对标志寄存器没有影响,比如,mov、push、pop等
1、零标志位 (ZF)
零标志位(Zero Flag)。它记录相关指令执行后,其结果是否为0。
如果结果为0,那么zf = 1(表示结果是0);
如果结果不为0,那么zf = 0。
mov ax, 1
sub ax, 1 ;执行后,结果为0,则zf = 1
mov ax, 2
sub ax, 1 ;执行后,结果不为0,则zf = 0
2、奇偶标志位 (PF)
奇偶标志位(Parity Flag)。它记录相关指令执行后,其结果的所有bit位中1的个数是否为偶数。
如果1的个数为偶数,pf = 1,如果为奇数,那么pf = 0。
mov al, 1
add al, 10 ;执行后,结果为00001011B,其中有3(奇数)个1,则pf = 0;
mov al, 1
or al, 2 ;执行后,结果为00000011B,其中有2(偶数)个1,则pf = 1;
3、符号标志位(SF)
符号标志位(Symbol Flag)。它记录相关指令执行后,其结果是否为负。
如果结果为负,sf = 1;
如果非负,sf = 0。
计算机中通常用补码来表示有符号数据。
对于同一个二进制数据,计算机可以将它当作无符号数据来运算,也可以当作有符号数据来运算
00000001B,可以看作为无符号数1,或有符号数+1;
10000001B,可以看作为无符号数129,也可以看作有符号数-127。
SF标志,就是CPU对有符号数运算结果的一种记录,它记录数据的正负。在我们将数据当作有符号数来运算的时候,可以通过它来得知结果的正负。
如果我们将数据当作无符号数来运算,SF的值则没有意义,虽然相关的指令影响了它的值
mov al, 10000001B
add al, 1 ;执行后,结果为10000010B,sf = 1,
;表示:如果指令进行的是有符号数运算,那么结果为负;
mov al, 10000001B
add al, 01111111B ;执行后,结果为0,sf = 0,
;表示:如果指令进行的是有符号数运算,那么结果为非负
4、进位标志位(CF)
进位标志位(Carry Flag)。一般情况下,在进行无符号数运算的时候,它记录了运算结果的最高有效位向更高位的进位值,或从更高位的借位值
97H - 98H 产生借位CF = 1 ==>
(al) = 197H - 98H = FFH
5、溢出标志位(OF)
溢出标志位(Overflow Flag)。一般情况下,OF记录了有符号数运算的结果是否发生了溢出。
如果发生溢出,OF = 1;如果没有,OF = 0。
CF和OF的区别:CF是对无符号数运算有意义的标志位,而OF是对有符号数运算有意义的标志位
mov al, 98
add al, 99 ;执行后将产生溢出。因为进行的"有符号数"运算是:(al)=(al)+ 99 = 98 + 99=197 = C5H 为-59的补码
;而结果197超出了机器所能表示的8位有符号数的范围:-128 ~ 127。
;add 指令执行后:无符号运算没有进位CF=0,有符号运算溢出OF=1
;当取出的数据C5H按无符号解析C5H = 197, 当按有符号解析通过SP得知数据为负,即C5H为-59补码存储,
mov al,0F0H ;F0H,为有符号数-16的补码 -Not(F0 - 1)
add al,088H ;88H,为有符号数-120的补码 -Not(88- 1)
;执行后,将产生溢出。因为add al, 088H进行的有符号数运算结果是:(al)= -136
;而结果-136超出了机器所能表示的8位有符号数的范围:-128-127。
;add 指令执行后:无符号运算有进位CF=1,有符号运算溢出OF=1
2、adc指令和sbb指令
adb指令
adc是带进位加法指令,它利用了CF位上记录的进位值。
指令格式:adc 操作对象1, 操作对象2
功能:操作对象1 = 操作对象1 + 操作对象2 + CF
mov ax, 2
mov bx, 1
sub bx, ax ;无符号运算借位CF=1,有符号运算OF = 0
adc ax, 1 ;执行后,(ax)= 4。adc执行时,相当于计算:(ax)+1+CF = 2+1+1 = 4。
;计算1EF000H+201000H,结果放在ax(高16位)和bx(低16位)中。
;将计算分两步进行,先将低16位相加,然后将高16位和进位值相加。
mov ax, 001EH
mov bx, 0F000H
add bx, 1000H
adc ax, 0020H
sbb指令
sbb是带借位减法指令,它利用了CF位上记录的借位值。
指令格式:sbb 操作对象1, 操作对象2
功能:操作对象1 = 操作对象1 - 操作对象2 - CF
;计算 003E1000H - 00202000H,结果放在ax,bx中,程序如下:
mov bx, 1000H
mov ax, 003EH
sub bx, 2000H
sbb ax, 0020H
3、cmp指令
1.cmp指令
cmp是比较指令,cmp的功能相当于减法指令,只是不保存结果。cmp指令执行后,将对标志寄存器产生影响。
cmp指令格式:cmp 操作对象1,操作对象2
例如:
指令cmp ax, ax,做(ax)-(ax)的运算,结果为0,但并不在ax中保存,仅影响flag的相关各位。
指令执行后:zf=1,pf=1,sf=0,cf=0,of=0。
2.cmp进行无符号数比较
上面的表格可以正推也可以逆推(等价)
3.cmp进行有符号数比较
SF只能记录实际结果的正负,发生溢出的时候,实际结果的正负不能说明逻辑上真正结果的正负。
但是逻辑上的结果的正负,才是cmp指令所求的真正结果,所以我们在考察SF的同时考察OF,就可以得知逻辑上真正结果的正负,同时就知道比较的结果。
mov ah, 08AH ; -Not(8A-1) = -118 即当成有符号数时为-118
mov bh, 070H ; 有符号数时最高位为0为正数, 70H = 112
cmp ah, bh ;(ah)-(bh)实际得到的结果是1AH
; 在逻辑上,运算所应该得到的结果是:(-118)- 112 = -230
; sf记录实际结果的正负,所以sf=0
4.cmp ah, bh
(1)如果sf=1,而of=0 。
of=0说明没有溢出 => 逻辑上真正结果的正负=实际结果的正负;
sf=1,实际结果为负,=> 所以逻辑上真正的结果为负
所以(ah)<(bh)
(2)如果sf=1,而of=1:
of=1,说明有溢出, => 逻辑上真正结果的正负≠实际结果的正负;
sf=1,实际结果为负。 => 逻辑上真正的结果必然为正
说明了(ah)>(bh)。(3)如果sf=0,而of=1。
of=1,说明有溢出 => 逻辑上真正结果的正负≠实际结果的正负;
sf=0,实际结果非负。=> 那么逻辑上真正的结果必然为负。
说明了(ah)<(bh)。
(4)如果sf=0,而of=0
of=0,说明没有溢出 => 逻辑上真正结果的正负=实际结果的正负;sf=0,实际结果非负 => 所以逻辑上真正的结果非负,
所以(ah)≥(bh)。
4、检测比较结果的条件转移指令
可以根据某种条件,决定是否修改IP的指令,例:
jcxz它可以检测cx中的数值,如果(cx)=0,就修改IP,否则什么也不做。
所有条件转移指令的转移位移都是[-128,127]。
多数条件转移指令都检测标志寄存器的相关标志位= =>根据检测的结果来决定是否修改IP
这些条件转移指令通常都和cmp相配合使用,它们所检测的标志位,都是cmp指令进行无符号数比较的时记录比较结果的标志位
根据无符号数的比较结果进行转移的条件转移指令
(它们检测zf、cf的值)
指令 含义 检测的相关标志位
je 等于则转移 zf = 1
jne 不等于则转移 zf = 0
jb 低于则转移 cf = 1
jnb 不低于则转移 cf = 0
ja 高于则转移 cf = 0 且 zf = 0
jna 不高于则转移 cf = 1 且 zf = 1
j:jump,e:equal,
b:below,
a:above,
n:not
5、DF标志和串传送指令
方向标志位。在串处理指令中,控制每次操作后si、di的增减。
df = 0每次操作后si、di递增;
df = 1每次操作后si、di递减。
格式:movsb
功能:将ds:si指向的内存单元中的字节送入es:di中,然后根据标志寄存器df位的值,将si和di递增或递减
格式:movsw
功能:将ds:si指向的内存字单元中的字送入es:di中,然后根据标志寄存器df位的值,将si和di递增2或递减2。
格式:rep movsb
movsb和movsw进行的是串传送操作中的一个步骤,一般来说,movsb和movsw都和rep配合使用,
功能:rep的作用是根据cx的值,重复执行后面的串传送指令
8086CPU提供下面两条指令对df位进行设置。
cld指令:将标志寄存器的df位置0
std指令:将标志寄存器的df位置1
;将data段中的第一个字符串复制到它后面的空间中。
data segment
db 'Welcome to masm!'
db 16 dup (0)
data ends
mov ax, data
mov ds, ax
mov si, 0 ;ds:si 指向data:0
mov es, ax
mov di, 16 ;es:di指向data:0010
mov cx, 16 ;(cx)=16,rep循环16次
cld ;设置df=0,正向传送
rep movsb
6、pushf和popf
pushf的功能是将标志寄存器的值压栈,而popf是从栈中弹出数据,送入标志寄存器中
pushf和popf,为直接访问标志寄存器提供了一种方法。
7. 标志寄存器在Debug中的表示
实验
DEBUG 指令 (实验一)
A: 以汇编指令的格式在内存中写入一条机器指令
功能:允许用户以汇编语言的格式输入指令,DEBUG 程序会将这些汇编指令转化为机器码并存入内存。
语法:A 段地址:偏移地址
示例:
A 1000:0100
MOV AX, 4C00
INT 21
解释:在内存地址 1000:0100
处输入 MOV AX, 4C00
和 INT 21
两条指令。
D: 查看内存
功能:显示内存中的内容。
语法:D 段地址:起始偏移地址 [结束偏移地址]
示例:
D 1000:0100
解释:查看从内存地址 1000:0100
开始的内容。
E: 向内存中写入机器码
功能:向内存中写入数据,允许用户直接以机器码的形式向内存中写入数据。
语法:E 段地址:偏移地址 数据 [数据 [...]]
示例:
E 1000:0100 4C 00 21
解释:在内存地址 1000:0100
处写入数据 4C 00 21
。
G: 程序直接运行到指定地址位置
功能:将程序运行到指定的内存地址。
语法:G 偏移地址
示例:
G 0100
解释:从偏移地址 0100
处开始运行程序。
R: 查看, 改变寄存器
功能:显示或修改寄存器的内容。
语法:R [寄存器]
示例:
R AX
AX 0000
:1234
解释:查看 AX
寄存器的值,并将其修改为 1234
。
T: 执行一条机器指令
功能:单步执行一条机器指令。
语法:T
示例:
T
解释:执行当前内存位置的一条机器指令。
U: 查看内存指令翻译的汇编指令
功能:反汇编内存中的机器指令。
语法:U 段地址:偏移地址
示例:
U 1000:0100
解释:反汇编从内存地址 1000:0100
开始的指令。
P: 当遇到 int 21H 正常结束指令时使用P指令
功能:遇到 int 21H
结束指令或 loop
指令时执行到下一个非循环指令。
语法:P
示例:
P
解释:执行到 int 21H
或下一个非循环指令。
实验思考
(1)2000:0为什么不写为2000:0000
在DOS DEBUG工具中,段地址和偏移地址的表示法通常是简洁的,只需要指定实际有意义的部分。例如,2000:0
和2000:0000
确实表示的是同一个物理内存地址,但通常在写入时,只需要写出有意义的部分即可。
(2)2000H的H是?
在汇编语言和低级编程中,地址或数值后缀H
表示该数值是以十六进制(hexadecimal)表示的。
H
表示这是一个十六进制数。
(3)ROM无法修改
只读存储器(ROM),因此无法通过DEBUG工具直接修改它。这解释了为什么你的修改没有生效。ROM的内容在生产时已经固定,通常无法在运行时改变
实验内容
步骤3生产日期
步骤4
实验二
写入指令到内存(部分)
执行push [4]之后
2.
为什么栈结构指向内存出现数据?
可以发现初始没有执行这段代码时,我们使用d命令观察2000:00内存,都是00,怎么创建栈结构指向这段内存时,我们发现有些数据了。这些数据是什么?
可以看出这里面有cs值、ip值、ax值(这个容易看出来),"9D 05"是标志寄存器flag的值。
后面在将内中断的时候,就明白了。t命令实际是引发了单步中断,执行中断例程时,CPU会将一些中断例程使用的的寄存器变量自动压栈到栈中,此例中就包括了上述的寄存器变量的值。
单步中断P249
我们在使用Debug的t命令的时候,没有用想过这样的问题,Debug如何能让CPU在执行一条指令后,就显示各个寄存器的状态?我们知道,CPU在执行程序的时候是从CS:IP指向的某个地址的开始,自动向下读取指令执行。也就是说,如果CPU不提供其他功能的话, 就按这种方式工作,只要CPU一加电,它就从预设的地址开始一直执行下去,不可能有任何程序能控制它在执行完一条指令后停止,去做别的事情。可是,我们在Debug中看到的情况却是,Debug可以控制CPU执行被加载程序中的一条指令,然后让它停下来,显示寄存器的状态。
Debug有特殊的能力吗?我们只能说Debug利用了CPU提供的一种功能。只有CPU提供了在执行一条指令后就转去做其他事情的功能,Debug或是其他的程序才能利用CPU提供的这种功能做出我们使用T命令时的效果。
中断过程P238
8086CPU在收到中断信息后,所引发的中断过程。
(1)(从中断信息中)取得中断类型码;
(2)标志寄存器的值入栈(因为在中断过程中要改变标志寄存器的值,所以先将其保存在栈中);
(3)设置标志寄存器的第8位TF和第9位IF的值为0(这一步的目的后面将介绍);
(4)CS的内容入栈;
(5)IP的内容入栈;
(6)从内存地址为中断类型码4和中断类型码4+2的两个字单元中读取中断处理程序的入口地址设置IP和CS。
实验三
3.查看PSP的内容
PSP地址(ds 存放PSP的段地址:SA,PSP偏移地址为0)
程序地址(SA + 10H(CS): 0)
实验四 [bx]和loop的使用
(1)向内存0:200~0:23F 依次传送数据 0~63(3FH)
程序中只能用9条指令,包括mov ax,4c00h和int 21h
assume cs:code
code segment
mov ax,0020H ; 将数据段寄存器 DS 指向段地址 200h
mov ds,ax
mov bx,0 ; 初始化 BX 寄存器,作为内存偏移地址的起始值
mov cx,40H ; 初始化 CX 寄存器,循环计数器,设为 3Fh,即 63
s: mov [bx],bx ; 将 BX 的值存入 DS:BX 指向的内存地址
inc bx ; BX 增加 1,指向下一个内存地址
loop s ; CX 减 1,如果 CX 不为 0,则跳转到标号 s
mov ax,4c00h ; 终止程序
int 21h
code ends
end
(2)补全下面的程序,其功能是将mov ax,4c00
之前的指令复制到内存0:200处,补全程序,上机调试,跟踪运行结果
提示:
- 复制了什么?从哪里到哪里? :CS:0 (程序段起始) -> 0020:0
- 复制的是什么?有多少字节,你如何知道要复制的字节数量? 程序段,0~17H,u 0指令(直到int 32)
执行之前,内存状态:
执行之后,内存状态:
可见mov ax,4c00
之前的指令复制到内存0:200处,
代码:
assume cs:code
code segment
mov ax,cs
mov ds,ax
mov ax,0020h
mov es,ax
mov bx,0
mov cx,23
s: mov al,[bx]
mov es:[bx],al
inc bx
loop s
mov ax,4c00h
int 21h
code ends
end
实验五 编写,调试具有多个段的程序
一。将下面的程序编译连接,用Debug加载、跟踪,然后回答问题。
assume cs:code,ds:data,ss:stack
data segment
dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
data ends
stack segment
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 start
1.根据内存布局:ds:0为PSP,然后ds:100为程序段起始(故ds:100查看到dw数据)
故code段地址为076A(075A:100)
2.指向完mov ax,stack后,ax为stack段地址
故stack段地址为076B
3.执行完mov ax,data后,ax为data的段地址
故data的段地址为076A
故设程序加载后,CODE段的段地址为X,则DATA段的段地址为 X-2 ,STACK段的段地址为 X-1 。
4.程序返回前,各寄存器状态
②CPU执行程序,程序返回前,CS= 076C,SS= 076B,DS= 076A。
5.CPU执行程序,程序返回前data段中数据变化:
执行前:
执行后:
故data段中的数据 不变
二.类似
assume cs:code,ds:data,ss:stack
data segment
dw 0123h,0456h
data ends
stack segment
dw 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 start
1.该段实际占据空间为:
对于如下定义的段:
name segment
......
name ends
- 内存对齐:x86架构中,段的起始地址必须是16字节的倍数。
- 内存分配:段的大小必须是16字节的倍数,即使实际数据量小于16字节。
- 计算公式:实际占用空间 = (N / 16的整数部分 + 1)* 16字节。
具体规则
- N ≤ 16:实际占用16字节。
- N > 16:实际占用(N / 16的整数部分 + 1)* 16字节。
三.
assume cs:code,ds:data,ss:stack
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
data segment
dw 0123h,0456h
data ends
stack segment
dw 0,0
stack ends
end start
1.code段数据:cs:30
这次只不过是将data和stack段放到了code段后面了。那么就要注意它们段地址的变化了。
2.stack的段地址:076E
3. data的段地址:076D
4. CPU执行程序,程序返回前code段中数据变化:
执行前:
执行前:
5.程序返回前,各寄存器状态
CPU执行程序,程序返回前,CS= 076A,SS= 076E,DS= 076D。
6.程序加载后,段地址关系
故设程序加载后,CODE段(076A)的段地址为X,则DATA段(076D)的段地址为 X + 4,STACK段(076E )的段地址为 X + 4 。
四。 如果将(1)、(2)、(3)题中的最后一条伪指令“end start”改为“end”(也就是说不指明程序的入口),则那个程序仍然可以正确执行?请说明原因。
答案:如果不指名程序的(code段的)入口,并且使用end替换end start,都能正常运行。但只有(3)题中程序可以正确的执行(因为只有它是在内存中可执行代码在最前面)。
讲解:因为如果不指名入口,程序会从加载进内存的第一个单元起开始执行,前二个题中,定义的是数据,但CPU还是将数据当做指令代码执行了。只不过程序执行时逻辑上是错误了。但真的能执行的。
如果指明了程序的入口,CPU会直接从入口处开始执行真正的机器码,直到遇到中断指令返回。此种方式能够确保程序逻辑上的正确。因此有必要为程序来指明入口。
五 将a段和b段数据依次相加,结果存入c段
易错:
(1)mov ax,es:[bx] 会读取字型数据
(2)注释时使用中文:会出现out of memory的错误
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
c segment
db 0,0,0,0,0,0,0,0
c ends
code segment
start:
mov ax,c
mov ds,ax
mov ax,a
mov es,ax
mov bx,0
mov cx,8
s0: mov ax,es:[bx]
mov [bx], ax
inc bx
loop s0
mov ax,b
mov es,ax
mov bx,0
mov cx,8
s1: mov ax,es:[bx]
add [bx], ax
inc bx
loop s1
mov ax,4c00h
int 21h
code ends
end start
六 用push指令将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,a
mov ds,ax
mov ax,b
mov ss,ax
mov sp,10H
mov bx,0
mov cx,8
s: push [bx]
add bx,2
loop s
mov ax,4c00h
int 21h
code ends
end start
实验六 将datasg段中的每个单词的前4个字母改为大写字母
执行前
结果:
代码
assume cs:codesg,ss:stacksg,ds:datasg
stacksg segment
dw 0,0,0,0,0,0,0,0
stacksg ends
datasg segment
db '1. display '
db '2. brows '
db '3. replace '
db '4. modify '
datasg ends
codesg segment
start:
mov ax,stacksg
mov ss,ax
mov sp,16
mov ax,datasg
mov ds,ax
mov bx,0
mov cx,4
s0: push cx
mov cx,4
mov si,3
s1: mov al,[bx+si]
and al,11011111b
mov [bx+si],al
inc si
loop s1
add bx,16
pop cx
loop s0
codesg ends
end start
实验七 寻址方式在结构化数据访问中的应用
注意:
1.div word ptr es:[di],div 内存单元,需要指出大小 (word ptr和btye ptr)
结果:
代码:
assume cs:code,ds:data,ss:stack
stack segment stack
db 0,0,0,0,0,0,0,0
stack ends
data segment
;年份
db '1975', '1976', '1977', '1978', '1979', '1980', '1981', '1982', '1983'
db '1984', '1985', '1986', '1987', '1988', '1989', '1990', '1991', '1992'
db '1993', '1994', '1995'
;收入
dd 16, 22, 382, 1356, 2390, 8000, 16000, 24486, 50065, 97479, 140417, 197514
dd 345980, 590827, 803530, 1183000, 1843000, 2759000, 3753000, 4649000, 5937000
;员工
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 sumn ne ?? ')
table ends
code segment
start:
mov ax,data
mov es,ax ;设置年份段地址
mov ax,0
mov bp,ax ;设置年份偏移地址
add ax,54H
mov si,ax ;设置收入偏移地址
add ax,54H
mov di,ax ;设置员工偏移地址
mov ax,table
mov ds,ax ;设表格段地址
mov ax,stack
mov ss,ax
mov sp,16 ;设置栈顶
mov bx,0 ;定位结构数据
mov cx,21
s: mov ax,es:[bp]
mov [bx],ax
mov ax,es:[bp+2]
mov [bx+2],ax ;赋值年份
mov ax,es:[di]
mov [bx+10],ax ;赋值雇员
mov ax,es:[si+2]
mov [bx+7],ax ;赋值收入高16位
mov ax,es:[si]
mov [bx+5],ax ;赋值收入低16位,被除数低16位
mov dx,es:[si+2] ;被除数高16位
div word ptr es:[di]
mov [bx+13],ax ;赋值人均收入
add bp,4 ;增加年份偏移地址
add di,2 ;增加员工偏移地址
add si,4 ;增加收入偏移地址
add bx,16 ;增加结构数据偏移地址
loop s
;结束程序
mov ax,4c00h
int 21h
code ends
end start
实验八 奇怪的程序
是否可以运行?为什么运行结果是这样?
答: 可以,
assume cs:code
code segment
mov ax,4c00H
int 21H
start:
mov ax,0
s:
nop
nop
mov di,offset s
mov si,offset s2
mov ax,cs:[si]
mov cs:[di],ax
s0:
jmp short s
s1:
mov ax,0
int 21H
mov ax,0
s2:
jmp short s1
nop
code ends
end start
注意:
前提:
注意:
1.NOP占用1字节
2.将cs:[si]为s2处的jmp short s1指令赋值到 cs:[di]为start:s处3.
3.s1为0018,short s1为0(故jmp short s1为jmp 8,也就是jmp到s处)
结果:
assume cs:code
code segment
mov ax,4c00H
int 21H
start:
mov ax,0
s:
nop
nop
mov di,offset s
mov si,offset s2
mov ax,cs:[si]
mov cs:[di],ax
s0:
jmp short s
s1:
mov ax,0
int 21H
mov ax,0
s2:
jmp short s1
nop
code ends
end start
实验九
前置知识
- B8000H~BFFFFFH为 dosbox 显示字符区的总地址
- 显示字符窗口一共25行,每行 8 个字符,每个字符可有256种属性
- 一个字符在 dosbox 的显示字符区中就要占两个字节,分别存放字符的 ASCLL 码和属性
- 低位存放 ASCLL 码,高位存放属性
- 所以打开显示字符窗口的时候,只会显示 4000 个的内存单元,即,显示地址为 B8000H~B8F9F 的内存单元中的内容,回车可显示下一行
- 160个内存单元算一行,一行中两个内存单元组成一列,这一列前面为字符,后面为属性
debug 字符显示区相关知识
属性值
属性字节格式:按位设置字节属性,0是关闭,1是开启
7 6 5 4 3 2 1 0
闪烁 背景红 背景绿 背景蓝 高亮 字体红 字体绿 字体蓝
例子:
1. 绿色 :00000010b —> 2h ----> 2
2. 绿底红色:00100100b —> 24h ----> 36
3. 白底蓝色:01110001b ---> 71h ----> 113
代码
assume cs:code
data segment
db "welcome to masm!"
db 02H,24H,71H
data ends
code segment
start: mov ax,data
mov es,ax ;设置字符串段地址
mov bx,0 ;设置字符串偏移地址
mov ax,16
mov si,ax ;设置属性偏移地址
mov ax,0B800H
mov ds,ax ;设置缓冲区段地址
mov ax,07C0H
mov di,ax ;缓冲区开始偏移地址
mov cx,16
s: mov al,es:[bx]
mov ds:[di],al ;赋值字符
mov al,es:[si] ;赋值属性
mov ds:[di+1],al
add di,2
inc bx
loop s
mov ax,4c00H
int 21H
code ends
end start
实验 10
一、子程序1 显示字符串
1.实验任务
编写一个子程序,向外提供可以调用的接口,能够在指定的行号、列号,用指定的颜色,显示一个用0结束的字符串。
2.代码:
assume cs:code
data segment
db 'Welcome to masm!',0
data ends
code segment
start:
mov dh,8 ;指定行号
mov dl,3 ;指定列号
mov cl,2 ;指定颜色
mov ax,data ;设置ds
mov ds,ax
mov si,0 ;设置ds:[si]指向当前要显示的字符
call show_str ;调用子程序,显示字符串
mov ax,4c00H ;程序返回
int 21H
show_str:
;设置缓冲区段地址
mov ax,0B800H
mov es,ax
;设置并计算偏移地址
mov al,160
mul dh ;计算行偏移量
mov dh,0
add ax,dx
add ax,dx ;计算增加列偏移量
mov bx,ax
mov al,cl
mov cx,16
s: mov ah,[si]
mov es:[bx],ah ;设置字符
mov es:[bx+1],al ;设置属性
inc si
add bx,2
loop s
ret
code ends
end start
二、子程序2 解决除法溢出的问题
1.实验任务
(1)写一个子程序,功能是进行不会产生溢出的除法运算。被除数为dword型,除数为word型,结果为dword型。
参数:ax = dword型数据的低16位 ;dx = dword型数据的高16位 ;cx = 余数
返回: dx = 结果的高16位 ;ax = 结果的低16位 ; cx = 余数
(2)可以参考的公式如下。
X 为被除数,H 为X的高16位,L 为X的低16位 , N 除数。
int()表示取商,rem()表示取余数。
则 X/N = int( H/N ) * 65536 + [rem( H/N) * 65536 + L]/N 。
2.结果
3.代码:
assume cs:code,ss:stack
stack segment ;定义栈段
db 0
stack ends
code segment ;定义代码段
start:
mov ax,stack ;设置栈顶
mov ss,ax
mov sp,10H
mov ax,4240H ;被除数的低16位
mov dx,000FH ;被除数的高16位
mov cx,0AH ;除数
call divdw ;调用子程序
mov ax,4c00H ;程序返回
int 21H
divdw: ;功能:改进后的不会溢出的div指令(当前修改为16位除16位(32位除16位,dx = 0))
;参数: ax被除数低16位 , dx被除数高16位 , cx除数
;返回值 ax结果低16位 , dx结果高16位 , cx余数
;需要两个临时数据存放点(栈和)
push ax
push dx
;计算高位部分(32位(实际为16位)除16位)
mov dx,0
pop ax ;设置被除数(原DX)
div cx ;除法
mov bx,ax ;存储INT(H/N),返回值的高16位
; 计算低位部分(32位除16位)
;此时dx即为Rem(H/N)被除数高位
pop ax ;被除数低位(原AX)
div cx ;除法(此时ax已经为结果的低16位)
;返回值
mov cx,dx ;返回余数
mov dx,bx ;返回高16位
ret
code ends
end start
三、子程序3 数值显示
1.实验任务
(1)将data段中的数据以十进制的形式显示出来。应该编写一个通用的子程序,将一个给定的word型数据转为以十进制显示的字符串。而字符的显示功能,可以调用上边写的子程序1来完成。
子程序描述:
名称:dtoc
功能:将word型数据转变为表示十进制数的字符串,字符串以0为结尾符。
参数:ax = word型数据 ; ds:si 指向字符串的首地址
返回:无
2.遇到的问题:
(1)最后8,38两数的存储错误
(2)debug可以单步执行至完成,直接执行失败
原因:debug程序会初始化各个寄存器的值,而直接执行不会初始化各寄存器,我程序中bp没有初始化赋值为0
3.代码:
assume cs:code,es:data,ss:stack,ds:display
data segment ;定义数据段
dw 123,12666,1,8,3,38
data ends
display segment ;定义显示数据段
db 16 dup(0)
display ends
stack segment ;定义栈段
db 0
stack ends
temp_data segment ;定义临时数据段
db 16 dup(0)
temp_data ends
code segment ;定义代码段
start:
mov ax,stack ;设置栈顶
mov ss,ax
mov sp,10H
mov ax,data ;待转换数据段
mov es,ax
mov ax,display ;显示数据段
mov ds,ax
call dtoc
call show_str ;调用子程序,显示字符串
mov ax,4c00H ;程序返回
int 21H
dtoc: ;功能:word数据改为十进制数字符串,以'0'为结尾(将转换后的数存储到ds:si)
mov cx,6 ;存储待转换数据个数
mov di,0 ;存储显示数据偏移地址
mov bx,0 ;存储待转换数据偏移地址
words: ;全部数据转换
push cx
mov ax,es:[bx] ;被除数(低16位)
mov bp,0
word_: ;功能:单次数据转换时,cx不改变
mov dx,0 ;高十六位
mov cx,10 ;除数
call divdw
add cx,30H ;转字符(余数
push cx ;转换的字符
mov cx,ax ;判断是否完全转换(商为0
add cx,1
inc bp ;单次数据长度
loop word_
mov cx,bp
mov bp,0
add bx,2
store: ;功能: 单次数据存储
pop ax
mov [di],al
inc di
loop store
inc di
pop cx
loop words
ret
divdw: ;功能:改进后的不会溢出的div指令(当前修改为16位除16位(32位除16位,dx = 0))
;参数: ax被除数低16位 , dx被除数高16位 , cx除数
;返回值 ax结果低16位 , dx结果高16位 , cx余数
push dx
;计算低位部分(32位除16位),ax初始已经赋值
mov dx,0
div cx ;除法(此时ax已经为结果的低16位)
;返回值
mov cx,dx ;返回余数
pop dx
ret
show_str: ;功能:打印数据段中的字符串
;设置参数
mov dh,8 ;指定行号
mov dl,3 ;指定列号
mov cl,2 ;指定颜色
mov cx,24 ;打印的字数
mov ax,display ;待打印数据段地址
mov ds,ax
;执行过程
mov si,0 ;设置ds:[si]指向当前要显示的字符
;设置缓冲区段地址
mov ax,0B800H
mov es,ax
;设置并计算偏移地址
mov al,160
mul dh ;计算行偏移量
mov dh,0
add ax,dx
add ax,dx ;计算增加列偏移量
mov bx,ax
mov al,cl
s: mov ah,[si]
mov es:[bx],ah ;设置字符
mov es:[bx+1],al ;设置属性
inc si
add bx,2
loop s
ret
code ends
end start
课程设计一
一、课程设计任务
实验任务:将 实验7 寻址方式在结构化数据访问中的应用 中提供的公司数据,呈现在屏幕上,效果如下。
注意:
(1)由于之前的子程序不够互相独立,工作量主要在于完善子程序和一定程度上调整
(2)在调试时可以先申请一块普通内存,查看变化(debug显存实时在变化)
结果:
代码:
assume cs:code,ds:data,ss:stack
stack segment
db 0,0,0,0,0,0,0,0
stack ends
data segment
;年份
db '1975', '1976', '1977', '1978', '1979', '1980', '1981', '1982', '1983'
db '1984', '1985', '1986', '1987', '1988', '1989', '1990', '1991', '1992'
db '1993', '1994', '1995'
;收入
dd 16, 22, 382, 1356, 2390, 8000, 16000, 24486, 50065, 97479, 140417, 197514
dd 345980, 590827, 803530, 1183000, 1843000, 2759000, 3753000, 4649000, 5937000
;员工
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
display segment
db 1600 dup (0)
display ends
code segment
start:
mov ax,data
mov es,ax ;设置年份段地址
mov ax,0
mov bp,ax ;设置年份偏移地址
add ax,54H
mov si,ax ;设置收入偏移地址
add ax,54H
mov di,ax ;设置员工偏移地址
mov ax,0b800H
mov ds,ax ;设表格段地址
mov bx,0 ;定位结构数据
mov ax,stack
mov ss,ax
mov sp,16 ;设置栈顶
mov cx,21
data_store:
call store_year ; 存储年份
add bx,32
mov dx,es:[si+2] ;赋值收入高16位
mov ax,es:[si] ;赋值收入低16位
call dtoc
add bx,64
mov ax,es:[di] ;雇员数
mov dx,0
call dtoc
add bx,32
mov dx,es:[si+2] ;被除数高16位
mov ax,es:[si] ;被除数低16位
div word ptr es:[di]
mov dx,0
call dtoc ;赋值人均收入
add bp,4 ;增加年份偏移地址
add di,2 ;增加员工偏移地址
add si,4 ;增加收入偏移地址
add bx,32 ;增加结构数据偏移地址
loop data_store
mov ax,4c00H ;程序返回
int 21H
store_year: ;存储年份
mov al,es:[bp]
mov [bx],al
mov [bx+1],word ptr 2
mov al,es:[bp+1]
mov [bx+2],al
mov [bx+3],word ptr 2
mov al,es:[bp+2]
mov [bx+4],al
mov [bx+5],word ptr 2
mov al,es:[bp+3]
mov [bx+6],al
mov [bx+7],word ptr 2
ret
dtoc: ;功能:dword数据改为十进制数字符串,以'0'为结尾
;参数 ax字符低16位, dx字符高16位 ds:bx指向首地址
push cx
push bp
push bx
mov bp,0
word_: ;功能:单次数据转换时,cx不改变
mov cx,10 ;除数
call divdw
add cx,30H ;转字符(余数
push cx ;转换的字符
mov cx,ax ;判断是否完全转换(商为0
add cx,1
inc bp ;单次数据长度
loop word_
mov cx,bp
store: ;将数据写入显存
pop ax
mov [bx],al
mov [bx+1],word ptr 2
add bx,2
loop store
mov ax,0
pop bx
pop bp
pop cx
ret
divdw: ;功能:改进后的不会溢出的div指令(当前修改为16位除16位(32位除16位,dx = 0))
;参数: ax被除数低16位 , dx被除数高16位 , cx除数
;返回值 ax结果低16位 , dx结果高16位 , cx余数
;需要两个临时数据存放点(栈和)
push bx
push ax
push dx
;计算高位部分(32位(实际为16位)除16位)
mov dx,0
pop ax ;设置被除数(原DX)
div cx ;除法
mov bx,ax ;存储INT(H/N),返回值的高16位
; 计算低位部分(32位除16位)
;此时dx即为Rem(H/N)被除数高位
pop ax ;被除数低位(原AX)
div cx ;除法(此时ax已经为结果的低16位)
;返回值
mov cx,dx ;返回余数
mov dx,bx ;返回高16位
pop bx
ret
code ends
end start
实验11
注意:
(1)完成这个程序只需要熟悉cmp本质通过影响标志寄存器sf(溢出),of(为负),实现判断减法结果大小
(2)了解各种条件转移语句,jna(不等于),jb(小于),ja(大于)
j:jump,
e:equal,
b:below,
a:above,
n:not
代码:
assume cs:code,ds:data
data segment
db 'welcome to masm!haha,I am happy!',0
data ends
code segment
start:
mov ax,data
mov ds,ax
mov si,0
call letterc
mov ax,4c00H
int 21H
letterc:
push si
push ds
comp:
mov al,[si]
cmp al, 0 ;检测字符是否为0,字符串结尾检测
je exit ;如果为0,跳转到exit标号,退出
cmp al,'a'
jb next_char ;如果小于字符a,跳转到下个字符
cmp al,'z'
ja next_char ;如果大于字符z,跳转到下个字符
and al, 11011111B ;条件都不满足,开始转换为大写字母
mov [si],al
next_char:
inc si
jmp short comp
exit:
pop ds
pop si
ret
code ends
end start