受益匪浅,对cpu指令的执行,显存的运行过程,操作系统对程序的控制和运行,内存定位/寻址,递归/函数栈/子程序等等有了进一步的了解。
本书的课后检测点和实验很重要,日后需要用的时候应重点复习
重点:第九章,第十章
2021/4/19 暂停更新,有时间再看,10-12章均有未完成的部分
使用的教材:汇编语言(第3版)王爽
第1章 基础知识
汇编语言是直接在硬件之上工作的编程语言,需要有一定知识,但是在汇编课程中我们部队硬件系统进行全面和深入的研究,汇编课程的研究重点放在如何利用硬件系统的编程结构和指令集有效灵活地控制系统进行工作。
1.1 机器语言
机器语言是机器指令的集合,电子计算机的机器指令是一列二进制数字,每一种微处理器都有自己的机器指令集,也就是机器语言。
1.2 汇编语言的产生
汇编语言的主体是汇编指令,汇编指令是机器指令便于记忆的书写格式。
1.3 汇编语言的组成
(1)汇编指令:机器码的助记符,有相应的机器码。
(2)伪指令:没有对应的机器码,由编译器执行,计算机并不执行。
(3)其他符号:如+、-、*、/等,由编译器识别,没有对应的机器码。
汇编语言的核心是汇编指令,它决定了汇编语言的特性。
1.4 存储器
指令和数据在存储器中存放,也就是我们平时所说的内存。
1.5 指令和数据
指令和数据是应用上的概念。在内存或磁盘上,指令和数据没有任何区别,都是二讲制信息。CPU在工作的时候把有的信息看作指令,有的信息看作数据,为同样的信息赋予了不同的意义。
1.6 存储单元
微型机存储器的存储单元可以存储一个Byte,即8个二进制位。
1.7 CPU对存储器的读写
CPU要想进行数据的读写,必须和外部器件(标准的说法是芯片)进行下面3类信息的交互。
- 存储单元的地址(地址信息);
- 器件的选择,读或写的命令(控制信息);
- 读或写的数据(数据信息)。
在计算机中专门有连接CPU和其他芯片的导线,通常称为总线。总线从物理上来讲,就是一根根导线的集合。根据传送信息的不同,总线从逻辑上又分为3类,地址总线、控制总线和数据总线。
CPU从3号单元中读取数据的过程如下:

(1)CPU 通过地址线将地址信息3发出。
(2)CPU通过控制线发出内存读命令,选中存储器芯片,并通知它,将要从中读取数据。
(3)存储器将3号单元中的数据8通过数据线送入CPU。
1.8 地址总线
一个CPU有N跟地址总线,则可以说这个CPU的地址总线的宽度为N。这样的CPU最多可以寻找2的N次方个内存单元。
1.9 数据总线
数据总线的宽度决定了CPU和外界的数据传送速度。8 根数据总线一次可传送一个8位二进制数据(即一个字节)。16 根数据总线一次可传送两个字节。
1.10 控制总线
CPU对外部器件的控制是通过控制总线来进行的。在这里控制总线是个总称,控制总线是一些不同控制线的集合。有多少根控制总线,就意味着CPU提供了对外部器件的多少种控制。所以,控制总线的宽度决定了CPU对外部器件的控制能力。
前面所讲的内存读或写命令是由几根控制线综合发出的,其中有一根称为 “读信号输出”的控制线负贵由CPU向外传送读信号,CPU向该控制线上输出低电平表示将要读取数据;有一根称为“写信号输出”的控制线则负责传送写信号。
1.11 内存地址空间(概述)
举例来讲,一个CPU的地址总线宽度为10,那么可以寻址1024个内存单元,这1024 个可寻到的内存单元就构成这个CPU的内存地址空间。下面进行深入讨论。首先需要介绍两部分基本知识,主板和接口卡。
1.12 主板
在每一台PC机中,都有一个主板,主板上有核心器件和一些主要器件,这些器件通过总线(地址总线、数据总线、控制总线)相连。这些器件有CPU、存储器、外围芯片组(北桥负责高速信号,比如CPU与内存、显卡等设备的通信。南桥负责低速信号,比如PCI/PCIe、SATA、USB等外围设备通信。再后来北桥芯片逐渐被集成到了CPU里面。芯片组的主要差别就是CPU对外围设备和拓展支持情况的差别)、扩展插槽等。扩展插槽上一般插有RAM内存条和各类接口卡。
1.13 接口卡
CPU对外部设备都不能直接控制,直接控制这些设备进行工作的是插在扩展插槽上的接口卡。
简单地讲,就是CPU通过总线向接口卡发送命令,接口卡根据CPU的命令控制外设进行工作。
1.14 各类存储器芯片
一台PC机中,装有多个存储器芯片,这些存储器芯片从物理连接上看是独立的、不同的器件。从读写属性上看分为两类:随机存储器(RAM)和只读存储器(ROM)。 随机存储器可读可写,但必须带电存储,关机后存储的内容丢失;只读存储器只能读取不能写入,关机后其中的内容不丢失。这些存储器从功能和连接上又可分为以下几类。
随机存储器
用于存放供CPU使用的绝大部分程序和数据。装有BIOS(Basic Input/Output System,基本输入/输出系统)的ROM
BIOS是由主板和各类接口卡(如显卡、网卡等)厂商提供的软件系统,可以通过它利用该硬件设备进行最基本的输入输出。在主板和某些接口卡上插有存储相应BIOS的ROM。例如,主板上的ROM中存储着主板的BIOS(通常称为系统BIOS);显卡上的ROM中存储着显卡的BIOS;如果网卡上装有ROM,那其中就可以存储网卡的BIOS。- 接口卡上的RAM
某些接口卡需要对大批量输入、输出数据进行暂时存储,在其上装有RAM。最典型的是显示卡上的RAM,一般称为显存。显示卡随时将显存中的数据向显示器上输出。换句话说,我们将需要显示的内容写入显存,就会出现在显示器上。

1.15 内存地址空间
上述的那些存储器,在物理上是独立的器件,但是在以下两点上相同。
- 都和CPU的总线相连。
- CPU对它们进行读或写的时候都通过控制线发出内存读写命令。
这也就是说,CPU 在操控它们的时候,把它们都当作内存来对待,把它们总的看作一个由若干存储单元组成的逻辑存储器,这个逻辑存储器就是我们所说的内存地址空间。在汇编这门课中,我们所面对的是内存地址空间。

在图1.8中,所有的物理存储器被看作一个由若干存储单元组成的逻辑存储器,每个物理存储器在这个逻辑存储器中占有一个地址段,即一段地址空间。CPU在这段地址空间中读写数据,实际上就是在相对应的物理存储器中读写数据。
假设,图1.8中的内存地址空间的地址段分配如下。
地址0~7FFFH的32KB空间为主随机存储器的地址空间:
地址8000H~9FFFH的8KB空间为显存地址空间:
地址A000H~FFFFH的24KB空间为各个ROM的地址空间。
这样,CPU向内存地址为1000H的内存单元中写入数据,这个数据就被写入主随机存储器中;CPU向内存地址为8000H的内存单元中写入数据,这个数据就被写入显存中,然后会被显卡输出到显示器上;CPU向内存地址为C000H的内存单元中写入数据的操作是没有结果的,C000H 单元中的内容不会被改变,C000H 单元实际上就是ROM存储器中的一个单元。
内存地址空间的大小受CPU地址总线宽度的限制。80386CPU 的地址总线宽度为32,其内存地址空间最大为4GB。
我们在基于一个计算机硬件系统编程的时候,必须知道这个系统中的内存地址空间分配情况。不同的计算机系统的内存地址的分配情况是不同的。

第2章 寄存器
一个典型的CPU由运算器、控制器、寄存器等器件构成,这些器件靠内部总线相连。
对一个汇编程序员来说,CPU中的主要部件是寄存器。
2.1 通用寄存器
8086CPU的所以寄存器都是16位的,可以存放两个字节。AX、BX、CX、DX这4个寄存器通常用来存放一般性的数据,被称为通用寄存器。
为了向上一代的8位CPU寄存器兼容,这4个通用寄存器都可分为两个独立使用的8位寄存器来用。
例如:AX可分为AH(高8位)和AL(低8位)
2.2 字在寄存器中的存储
出于对兼容性的考虑,8086CPU可以一次性处理以下两种尺寸的数据
- 字节:记为byte,由8个bit组成
- 字:记为word,一个字由两个字节组成,这两个字节分别称为这个字的高位字节和低位字节
出于对数据直观分析的需要,我们多用十六进制来表示一个数据
2.3 几条汇编指令
| 程序段中的指令 | 指令执行后AX中的数据 | 指令执行后BX中的数据 |
|---|---|---|
| mov ax,4E20H | 4E20H | 0000H |
| add ax,106H | 6226H | 0000H |
| mov bx,2000H | 6226H | 2000H |
| add ax,bx | 8226H | 2000H |
| mov bx,ax | 8226H | 8226H |
| add ax,bx | 044CH | 8226H |
最后一条指令所得的值应该为1044CH,但是ax寄存器放不下,最高位的1不能保存
| 程序段中的指令 | 指令执行后AX中的数据 | 指令执行后BX中的数据 |
|---|---|---|
| mov ax,001AH | 001AH | 0000H |
| mov bx,0026H | 001AH | 0026H |
| add al,bl | 0040H | 0026H |
| add ah,bl | 2640H | 0026H |
| add bh,al | 2640H | 4026H |
| mov ah,0 | 0040H | 4026H |
| add al,85H | 00C5H | 4026H |
| add al,93H | 0058H | 4026H |
最后的ax应该为158H,但此时的al是作为一个独立的8位寄存器来使用的,和ah没有关系,所以最高位丢失
在进行数据传送或运算时,要注意指令的两个操作对象的位数应当是一致的。
2.4 物理地址
每个内存单元在存储空间内都有唯一的地址,称为物理地址。
在CPU向地址总线上发出物理地址之前,必须要在内部先形成这个物理地址。不同的CPU可以有不同的形成物理地址的方式。
2.5 16位结构的CPU
什么是16位结构的CPU呢?
- 运算器一次最多可以处理16位的数据
- 寄存器的最大宽度为16位
- 寄存器和运算器之间的通路为16位
内存单元的地址在送上地址总线之前,必须在CPU中处理、传输、暂时存放,对于16位CPU,能一次性处理,传输、暂时存储16位的地址。
2.6 8086CPU给出物理地址的方法
8086CPU有20位地址总线,可以传送20位地址,达到IMB寻址能力。8086CPU是16位结构,在内部一次性处理、 传输、暂时存储的地址为16 位。从8086CPU的内部结构来看,如果将地址从内部简单地发出,那么它只能送出16位的地址,表现出的寻址能力只有64KB。
8086CPU采用一种在内部用两个16位地址合成的方法来形成一个20位的物理地址,
当8086CPU要读写内存时:
(1) CPU中的相关部件提供两个16位的地址,一个称为段地址,另个称为偏移地址:
(2) 段地址和偏移地址通过内部总线送入一个称为地址加法器的部件;
(3) 地址加法器将两个16位地址合成为一个20位的物理地址;
(4) 地址加法器通过内部总线将20位物理地址送入输入输出控制电路:
(5) 输入输出控制电路将20位物理地址送上地址总线:
(6) 20位物理地址被地址总线传送到存储器。
地址加法器采用物理地址=段地址x16+偏移地址的方法用段地址和偏移地址合成物理(段地址左移四位)
地址。例如,8086CPU 要访问地址为123C8H的内存单元,此时,地址加法器的工作过程如图2.7所示(图中数据皆为十六进制表示)。

2.7 “段地址*16+偏移地址=物理地址”的本质含义
“段地址x16+偏移地址=物理地址”的本质含义是: CPU在访问内存时,用一个基础地址(段地址x16)和一个相对于基础地址的偏移地址相加,给出内存单元的物理地址。
更一般地说,8086CPU的这种寻址功能是“基础地址+偏移地址=物理地址”寻址模式的一种具体实现方案。8086CPU 中,段地址x16可看作是基础地址。
2.8 段的概念(内存地址小结)
(1)观察下面的地址
| 物理地址 | 段地址 | 偏移地址 |
|---|---|---|
| 21F60H | 2000H | 1F60H |
| 2100H | 0F60H | |
| 21F0H | 0060H | |
| 21F6H | 0000H | |
| 1F00H | 2F60H | |
| … | … |
结论:CPU可以用不同的段地址和偏移地址形成同一个物理地址
(2)如果给定一个段地址,仅通过变化偏移地址来进行寻址,最多可定位多少个内存单元?
结论:偏移地址16位,变化范围为0~FFFFH,仅用偏移地址来寻址最多可寻64KB个内存单元
在8086PC机中,存储单元的地址用两个元素来描述,即段地址和偏移地址。
“数据在 21F60H内存单元中。”这句话对于 8086PC机一般不这样讲, 取而代之的是两种类似的说法
①数据存在内存2000:1F60单元中;
②数据存在内存的2000H段中的1F60H单元中。
这两种描述都表示“数据在内存21F60H单元中”。
可以根据需要,将地址连续、起始地址为16的倍数的一组内存单元定义为一个段。
2.9 段寄存器(Segment Register)
段地址在8086CPU的段寄存器中存放。8086CPU有4个段寄存器:CS(Code Segment)、DS(Data Segment)、SS(Stack Segment)、ES(Extra Segment)、当8086CPU要访问内存时由这4个段寄存器提供内存单元的段地址。
本章中只看一下CS
2.10 CS和IP(指令的执行过程)
CS和IP是8086CPU中两个最关键的寄存器,它们指示了CPU当前要读取指令的地址。CS为代码段寄存器(Code Segment),IP 为指令指针寄存器(Instruction Pointer),从名称上我们可以看出它们和指令的关系。
在8086PC机中,任意时刻,设CS中的内容为M,IP中的内容为N, 8086CPU 将从内存Mx16+N单元开始,读取一条指令并执行。
也可以这样表述: 8086机中,任意时刻,CPU将CS:IP指向的内容当作指令执行。

读取一条指令后,IP中的值自动增加,以使CPU可以读取下一条指令,当前读入的指令B82301长度为3个字节,所以IP中的值增加3,后面的指令也依次如上运行
8086CPU的工作过程可以简要描述如下。
(1) 从CS:IP指向的内存单元读取指令,读取的指令进入指令缓冲器:
(2) IP=IP+所读取指令的长度, 从而指向下一条指令:
(3) 执行指令。转到步骤(1), 重复这个过程。
在8086CPU加电启动或复位后,CS和IP被设置为CS=FFFFH,IP=0000H,即在8086PC机刚启动时,CPU从内存FFFF0H单元中读取执行,FFFF0H单元中的指令是8086PC机开机后执行的第一条指令。
在内存中,指令和信息没有任何区别,CPU根据什么将内存中的信息看作指令?
CPU将CS:IP指向的内存单元中的内容看作指令。如果说,内存中的一段信息曾被CPU执行过的话,那么,它所在的内存单元必然被CS:IP指向过。
2.11 修改CS、IP的指令(jmp)
使用mov(传送指令)可以修改大部分寄存器的值,如AX,BX等,但不能设置CS,IP的值。
能够改变CS,IP的内容的指令被统称为转移指令,现在介绍一个最简单的:jmp指令
若想同时修改CS、IP的内容,可用形似”jmp 段地址:偏移地址“的指令完成
若想仅修改IP的内容,可直接用”jmp 某一合法寄存器“的指令完成
2.12 代码段
对于8086PC机,在编程时,可以根据需要,将一组内存单元定义为一个段,如果用于存代码,就可以认为是一个代码段。
将一段内存当作代码段,仅仅是我们在编程时的一种安排,CPU并不由于这种安排,就自动地将我们定义的代码段中的指令当作指令来执行。CPU只认可被CS:IP指向的内存单元中的内容为指令。
实验1 查看CPU和内存,用机器指令和汇编指令编程
1.预备知识:Debug的使用
(1)什么是Debug?
Debug是DOS、Windows 都提供的实模式(8086方式)程序的调试工具。使用它,可以查看CPU各种寄存器中的内容、内存的情况和在机器码级跟踪程序的运行。
实模式:16位模式,不同的程序可使用不同的分段策略,两个程序的不同逻辑地址,可能对应相同的物理地址,程序A可能修改程序B已经保存在内存里的值(游戏修改器)。
保护模式:32位模式,程序不能更改其他程序的内存。
(2)常用到的Debug功能。
- 用Debug的R命令查看、改变CPU寄存器的内容;
- 用Debug的D命令查看内存中的内容;
- 用Debug的E命令改写内存中的内容;
- 用Debug的U命令将内存中的机器指令翻译成汇编指令;
- 用Debug的T命令执行一条机器指令;
- 用Debug的A命令以汇编指令的格式在内存中写入一条机器指令。

也可以用R命令来改变寄存器中的内容,如”r ax” “r ip”等

使用D命令,Debug将输出3部分内容
(1)中间是从指定地址开始的128(16*8)个内存单元的内容,用十六进制的格式输出。
(2)左边是每行的起始地址
(3)右边是每个内存单元中的数据对应的可显示的ASCII码字符

也可直接写e 1000:0后逐个修改

如何向内存中写入机器码呢?我们知道,机器码也是数据,也可以用E命令将机器码写入内存。
| 机器码 | 对应的汇编指令 |
|---|---|
| b80100 | mov ax,0001 |
| b90200 | mov cx,0002 |
| 01c8 | add ax,cx |

U命令可以将内存单元中的内容翻译为汇编指令,并显示出来。
由此,我们可以再一次看到内存中的数据和代码没有任何区别,关键在于如何解释。
如何执行我们写入的机器指令呢?使用Debug的T命令可以执行一条或多条指令。

用E命令写入机器指令很不方便,为此,Debug提供了A命令。

2.实验任务
(1)使用Debug,将下面的程序段写入内存,逐条执行,观察每条指令执行后CPU中相关寄存器中内容的变化。(ez)
(2)将下面3条指令写入从2000:0开始的内存单元中,利用这3条指令计算2的8次方。(ez)
mov ax,1
add ax,ax
jmp 2000:0003
(3)主板上的ROM中写有一个生产日期,在内存FFF00H~FFFFFH的某几个单元中,请找到这个生产日期并试图改变它

rom修改无效 这个日期是由于win10不自带debug,所以使用DosBox来虚拟Dos环境,这个日期也是虚拟出来的,由于不是直接操作实模式,避免了一些误操作。
(4)向内存从B8100H开始的单元中填写数据


但是用d查看,内存中存储的数据会变化,因为显存中的内容和屏幕是一一对应的。在
输入的过程中屏幕上的内容变化了,显存中的内容也会跟着变。
第3章 寄存器(内存访问)
上一章中,我们主要从CPU如何执行指令的角度讲解了8086CPU的逻辑结构,形成物理地址的方法,相关的寄存器以及一些指令。
这一章中,我们从访问内存的角度继续学习几个寄存器。
3.1 内存中字的存储
CPU中,用16位寄存器来存储一个字。高8位高字节,低8位低字节。在内存中存储时,由于内存是字节单元,则一个字要用两个地址连续的内存单元来存放。
比如用0,1两个内存单元来存放数据20000(4E20H),则0号单元存储20H,1号单元存储4EH。
我们提出字单元的概念:字单元,即存放一个字型数据(16位)的内存单元,由两个地址连续的内存单元组成。高地址内存单元存放字型数据的高位字节,低地址内存单元中存放字型数据的低位字节。
3.2 DS和[address]
CPU要读写一个内存单元的时候,必须先给出这个内存单元的地址,8086CPU中有一个DS寄存器,通常用来存放要访问的数据的段地址。
mov bx,1000H
mov ds,bx
mov al,[0]
上面的3条指令将10000H(1000:0)中的数据读到al中。
mov除了前面用过的将数据送入寄存器,讲寄存器的内容送入另一个寄存器,还可以将一个内存单元的内容送入一个寄存器中。
[···]表示一个内存单元。[···]中的0表示内存单元的偏移地址,执行指令时,8086CPU自动取ds中的数据为内存单元的段地址。
由于8086CPU不支持将数据直接送入段寄存器的操作,所以只好用一个寄存器进行中转。
3.3 字的传送
因为8086CPU是16位结构,有16根数据线,所以,可以一次性传送16位的数据,也就是说可以一次性传送一个字。只要在mov指令中给出16位的寄存器就可以进行16位数据的传送了。
3.4 mov、add、sub指令
mov指令允许的形式:
- mov 寄存器,数据
- mov 寄存器,寄存器
- mov 寄存器,内存单元
- mov 内存单元,寄存器
- mov 段寄存器,寄存器
- mov 寄存器,段寄存器
- mov 内存单元,段寄存器
- mov 段寄存器,内存单元
数据其实也可以mov给内存单元,但是要加位宽修饰符
add和sub一样,都有两个操作对象,也可以有几种形式
- add 寄存器,数据
- add 寄存器,寄存器
- add 寄存器,内存单元
- add 内存单元,寄存器
3.5 数据段
前面讲过,对于8086pc机,在编程时,可以根据需要,将一组内存单元定义为一个段。所以我们也可以将一组长度小于64kb,地址连续,起始地址为16的倍数的内存单元当作专门存储数据的内存空间,从而定义了一个数据段。
将一段内存当作数据段,是我们在编程时的一种安排,可以在具体操作时,用ds存放数据段的段地址,再根据需要,用相关指令访问数据段中的具体单元。
数据和程序没有区别,段地址若是ds段,可看做是数据,段地址若是cs段,可看做是程序指令
3.6 栈
LIFO(Last In First Out)
3.7 CPU提供的栈机制
现今的CPU中都有栈的设计,8086CPU提供相关的指令来以栈的方式访问内存空间。这意味着,可以将一段内存当作栈来使用
下面举例说明,我们可以将10000H~1000FH这段内存当作栈来使用。

mov ax,0123H
push ax
mov bx,2266H
push bx
mov cx,1122H
push cx
pop ax
pop bx
pop cx
注意:字型数据用两个单元存放,高地址单元存放高8位,低地址单元存放低8位。
在8086CPU中,有两个寄存器,段寄存器SS (Stack Segment) 和寄存器SP (stack pointer) ,栈顶的段地址存放在SS中,偏移地址存放在SP中。任意时刻,SS:SP指向栈顶元素,push和pop指令执行时,CPU从SS和SP中得到栈顶的地址。
如上图,初试状态栈为空时,SS=1000H,SP=0010H
push ax 的执行,由以下两步完成。
- SP=SP-2,SS:SP 指向当前栈顶前面的单元,以当前栈顶前面的单元为新的栈顶
- 将ax中的内容送入SS:SP指向的内存单元处,SS:SP此时指向新栈顶
pop ax 的执行过程和 push ax 刚好相反,由以下两步完成。
- 将SS:SP 指向的内存单元处的数据送入ax中
- SP=SP+2,SS:SP 指向当前栈顶下面的单元,以当前栈顶下面的单元为新栈顶
3.8 栈顶超界的问题
栈顶超界是危险的,我们希望CPU可以帮我们解决这个问题,但实际的情况是,8086CPU中并没有这样的寄存器,我们在编程的时候要自己操心栈顶超界的问题。
3.9 push、pop指令
push和pop指令的格式可以是如下形式:
- push 寄存器
- push 段寄存器
- push 内存单元
push、pop实质上就是一种内存传送指令,可以在寄存器和内存之间传送数据,与mov不同的是,push和pop指令访问的内存单元的地址是由SS:SP 指出的。同时,push 和 pop 指令还要改变sp中的内容。
3.10 栈段
我们也可以将长度小于64kb的一组地址连续,起始地址为16的倍数的内存单元当作栈空间来用,从而定义了一个栈段。(段地址放在ss中)
一段内存,可以既是代码的存储空间,又是数据的存储空间,还可以是栈空间,也可以什么也不是。关键在于CPU中寄存器的设置,即CS , IP , SS , SP , DS的指向。
实验2 用机器指令和汇编指令编程
1.预备知识:Debug的使用
(1)关于D命令
D命令是查看内存单元的命令,CPU在访问内存单元的时候从段寄存器中得到内存单元的段地址,所以,Debug在其处理D命令的程序段中,必须有将段地址送入寄存器的代码。
D命令也提供了一种符合CPU机理的格式:“d 段寄存器:偏移地址”,以下是几个例子。
① -r ds
:1000
-d ds:0 ;查看从1000:0开始的内存
② -r ds
:1000
-d ds:10 18 ;查看1000:10~1000:18中的内容
③ -d cs:0 ;查看当前代码段中的指令代码
④ -d ss:0 ;查看当前栈段中的内容
(2)在E、A、U命令中使用寄存器
① -r ds
:1000
-e ds:0 11 22 33 44 55 66 ;查看从1000:0开始的内存
② -u cs:0 ;以汇编指令的形式,显示当然代码段中的代码
③ -r cs:0
:1000
-a ds:0 ;以汇编指令的形式,向从1000:0开始的内存单元中写入指令
(3)下一条指令执行了吗?
mov ax,2000
mov ss,ax
mov sp,10
mov ax,3123
...
在用T执行单步操作 mov ss,ax 后,下一条指令直接变成了mov ax,3123。
在用T命令执行 mov ss,ax的时候,它的下一条指令mov sp,10也紧接着执行了。
不单是mov ss,ax,对于如 mov ss,bx,mov ss,[0],pop ss等指令都会发生上面的情况,这些指令有哪些共性呢,它们都是修改栈段寄存器SS的指令。
为什么会这样呢,这涉及我们在以后的课程中要深入研究的内容:中断机制。
2.实验任务
(1) 使用Debug,将下面的程序段写入内存,逐条执行,根据指令执行后的实际运行情况填空。(ez)
(2) 仔细观察图3.19中的实验内容,然后分析:为什么2000:0~2000:f中的内容会发生改变。(书上)
第4章 第一个程序
现在我们将开始编写完整的汇编语言程序,用编译和连接程序将它们编译连接成为可执行文件
4.1 一个源程序从写出到执行的过程
- 编写源程序
- 对源程序进行编译连接(编译生成目标文件,连接生成可执行文件)
- 执行可执行文件中的程序
4.2 源程序
assume cs:codesg
codesg segment
mov ax,0123H
mov bx,0456H
add ax,bx
add ax,ax
mov ax,4c00H
int 21H
codesg ends
end
1. 伪指令
在汇编语言源程序中,包含两种指令,一种是汇编指令,一种是伪指令。汇编指令有对应的机器码,可以被编译为机器指令,最终为CPU所执行。而伪指令是由编译器来执行的指令,编译器根据伪指令来进行相关的编译工作。
上面的程序出现了3种伪指令
(1) XXX segment
…
XXX ends
segment 和 ends 是一对成对使用的伪指令,是在写可被编译器编译的汇编程序时,必须要用到的一对伪指令。功能是定义一个段,分别说明段的开始和结束。
一个汇编程序是由多个段组成的,这些段被用来存放代码,数据或当作栈空间来使用。
一个有意义的汇编程序中至少要有一个段,这个段用来存放代码。
(2) end
end是一个汇编程序的结束标记,汇编器在编译汇编程序的过程中,如果碰到了指令end,就结束对源程序的编译。
注意:不要把end和ends搞混
(3) assume
这条伪指令可以把某一段寄存器和程序中的某一个用segment…ends定义的段相关联。
2. 源程序中的”程序”
程序最先以汇编指令的形式存放在源程序中,经编译,连接后转变为机器码,存储在可执行文件中。(伪指令是由编译器来处理的)
3. 标号
如“codesg”。一个标号指代了一个地址,作为一个段的名称,这个段的名称最终将被编译,连接程序处理为一个段的段地址。
4. 程序的结构
如上程序
5. 程序返回
若想要一个程序p2运行,则必须有一个正在运行的程序p1,将p2从可执行文件中加载入内存后,将CPU的控制权交给p2,p2才能运行。p2开始运行后,p1暂停运行。
当p2运行完毕后,将CPU的控制权交还给使它得以运行的程序p1,此后,p1继续运行。
我们把交还CPU的控制权的过程称为程序返回
mov ax,4c00H
int 21H
这两条指令所实现的功能就是程序返回。
6. 语法错误和逻辑错误
语法错误可以在程序编译时被编译器发现。
4.3 编译源程序
可以用任意的文本编辑器来编辑源程序,只要最终将其存储为纯文本文件即可。(扩展名一般写.asm)
4.4 编译

4.5 连接

连接的作用有以下几个
- 当源程序很大时,可以将它分为多个源程序文件来编译,成为目标文件后,再用连接程序将它们连接到一起,生成一个可执行文件。
- 程序中调用了某个库文件中的子程序,需要将这个库文件和该程序生成的目标文件连接到一起,生成一个可执行文件。
- 一个源程序编译后,得到了存有机器码的目标文件,目标文件中的有些内容还不能直接用来生成可执行文件,连接程序将这些内容处理为最终的可执行信息。
4.6 以简化的方式进行编译和连接
4.7 1.exe的运行
4.8 谁将可执行文件中的程序装载进入内存并使它运行?
操作系统的外壳(shell)
任何通用的操作系统,都要提供一个称为shell(外壳)的程序,用户(操作人员)使用这个程序来操作计算机系统进行工作。
DOS中有一个程序command.com,这个程序在DOS中称为命令解释器,也就是DOS系统的shell。
DOS启动时,先完成其他重要的初始化工作,然后运行command.com,command.com运行后,执行完其他的相关任务后,在屏幕上显示出由当前盘符和当前路径组成的提示符,等待用户的输入。
用户可以输入所要执行的命令,如cd、dir等,这些命令由command执行,完成这些命令后,再次等待用户的输入。
如果用户要执行一个程序,则输入可执行文件的名称,command会找到文件,将这个可执行文件中的程序加载入内存,设置CS:IP指向程序的入口,此后,command暂停运行,CPU运行程序。程序运行结束后,返回到command中,再次等待用户输入。
刚才的汇编程序从写出到执行的过程:
| 编程 | 1.asm | 编译 | 1.obj | 连接 | 1.exe | 加载 | 内存中的程序 | 运行 |
|---|---|---|---|---|---|---|---|---|
| (Edit) | (masm) | (link) | (command) | (CPU) |
4.9 程序执行过程的跟踪
使用command不能逐条指令地看到程序的执行过程,因为command的程序加载,设置CS:IP指向程序的入口的操作是连续完成的,当CS:IP一指向程序的入口,command就放弃了CPU的控制权。
为了观察程序的运行过程,可以使用Debug,Debug可以将程序加载入内存,设置CS:IP指向程序的入口,但Debug并不放弃对CPU的控制,这样,我们就可以使用Debug的相关命令来单步执行程序,查看每一条指令的执行结果。

这里,需要讲解一下DOS系统中.EXE文件中的程序的加载过程。

(1)程序加载后,ds中存放着程序所在内存区的段地址,这个内存区的偏移地址为0,则程序所在的内存区的地址为ds:0;
(2)这个内存区的前256个字节中存放的是PSP,DOS用来和程序进行通信。从256字节处向后的空间存放的是程序。
实验3 编程、编译、连接、跟踪
书上
第5章 [BX]和loop指令
1.[bx]和内存单元的描述
[bx]同样也表示一个内存单元,它的偏移地址在bx中,比如下面的指令:
mov ax,[bx]
将一个内存单元的内容送入ax,这个内存单元的长度为2字节(字单元),存放一个字,偏移地址在bx中,段地址在ds中
2.loop
我们在这一章,讲解[bx]和loop指令的应用、意义和相关的内容。
3.我们定义的描述性的符号:“()”
为了描述上的简洁,使用一个描述性的符号 “()”来表示一个寄存器或一个丙存单元中的内容。比如:
(ax)表示ax中的内容、(al)表示al中的内容:
4.约定符号idata表示常量
5.1 [BX]
mov ax, [bx]
功能: bx中存放的数据作为一个偏移地址EA,段地址SA默认在ds中,将SA:EA处的数据送入ax中。即: (ax)=((ds)* 16+(bx))。
注意:inc bx的含义是bx中的内容加1。
5.2 Loop指令
loop指令的格式是: loop 标号,CPU执行loop指令的时候,要进行两步操作,
①(cx)=(cx)-1;
②判断cx中的值,不为零则转至标号处执行程序,如果为零则向下执行。
从上面的描述中,可以看到,cx中的值影响着loop指令的执行结果。通常我们用loop指令来实现循环功能,cx 中存放循环次数。
任务:编程计算$2^{12}$
分析:计算$2^{12}$需要11条重复的指令 add ax,ax。
assume cs:code
code segment
mov ax,2
mov cx,11
s: add ax,ax
loop s
mov ax,4c00h
int 21h
code ends;
end
用cx和loop指令相配合实现循环功能的程序框架如下
mov cx,循环次数
s:
循环执行的程序段
loop s
5.3 在Debug中跟踪用loop指令实现的循环程序

循环程序段从CS:0012开始,CS:0012前面的指令,我们不想一步步跟踪,可以使用g命令”g 0012”,它表示执行程序到当前代码段的0012h处。
当遇到loop指令时,可以使用p命令来执行,Debug就会自动重复执行循环中的指令,直到(cx)=0为止。
当然,也可以使用g命令来达到目的。
5.4 Debug和汇编编译器masm对指令的不同处理
在Debug中,mov ax,[0]表示将ds:0处的数据送入ax中。
但是在汇编源程序中,指令“mov ax,[0]”被编译器当作指令”mov ax,0处理”。
那么我们如何在源程序中实现将内存2000:0之类的数据送入al,bl,cl,dl呢?
可将偏移地址送入bx寄存器中,用[bx]的方式来访问内存单元。
也可以在[]的前面显式地给出段地址所在的段寄存器,如”mov al,ds:[0]”。
5.5 loop和[bx]的联合应用
计算ffff:0~ffff:b单元中的数据的和,结果存储在dx中。
assume cs:code
code segment
mov ax,0ffffh
mov ds,ax
mov bx,0 ;bx充当代表内存单元地址的变量
mov dx,0
mov cx,12
s: mov al,[bx]
mov ah,[0]
add,dx,ax
inc bx
loop s
mov ax,4c00h
int 21h
code ends
end
5.6 段前缀
我们可以在访问内存单元的指令中显示地给出内存单元的段地址所在的寄存器,比如”ds:” “cs:”等,这些在汇编语言中称为段前缀。
5.7 一段安全的空间
在8086模式中,随意向一段内存空间写入内容是很危险的,因为这段空间中可能存放着重要的系统数据或代码。
在一般的pc机中,DOS方法下,0:200~0:2ff的256个字节的空间一般不会被使用。
5.8 段前缀的使用
将内存ffff:0~ffff:b单元中的数据复制到0:200~0:20b单元中。
assume cs:code
code segment
mov ax,0ffffh
mov ds,ax
mov ax,0020h
mov es,ax
mov bx,0
mov cx,12
s: mov dl,[bx]
mov es:[bx],dl
inc bx
loop s
mov ax,4c00h
int 21h
code ends
end
实验4 [bx]和loop的使用
(1)、(2)编程,向内存0:200~0:23F依次传送数据0~63(3FH)。
assume cs:code
code segment
mov ax,20h
mov ds,ax
mov bx,0
mov cx,64
s: mov [bx],bx
inc bx
loop s
mov ax,4c00h
int 21h
code ends
end

第6章 包含多个段的程序
在前面的程序中,只有一个代码段,现在的问题是,如果程序需要用其他空间来存放数据,使用哪里呢?
合法地通过操作系统取得的空间都是安全的,程序取得空间的方法有两种,一是在加载程序的时候为程序分配,再就是程序在执行的过程中向系统申请。在我们的课程中,不讨论第二种方法。
6.1 在代码段中使用数据
从规范的角度来讲,我们是不能自已随便决定哪段空间可以使用的,应该让系统来为我们分配。
我们可以在程序中,定义我们希望处理的数据,这现数据就会被编译、连接程序作为程序的一部分载入内存。 与此同时,我们要处理的数据也就自然而然地获得了存储空间。
编程计算8个数据的和,结果存在ax寄存器中:
assume cs:code
code segment
dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
mov cx,0
...
code ends
end
dw(define word)的含义是定义字型数据,由于它们在代码段中,所以可以从cs中得到它们的段地址。用dw定义的数据处于代码段的最开始,所以偏移地址为0。
可是这样一来,我们就必须用Debug来执行程序,因为程序的入口处不是我们所希望执行的指令(指令在ip=10h处)。我们可以在源程序中指明程序的入口所在。
assume cs:code
code segment
dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
strat: mov bx,0
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除了通知编译器程序结束外,还可以通知编译器程序的入口在什么地方。在程序6.2中我们用end指令指明了程序的入口在标号start处。
在前面的课程中,我们已经知道在单任务系统中,可执行文件中的程序执行过程如下。
(1)由其他的程序(Debug、command 或其他程序)将可执行文件中的程序加载入内存:
(2)设置CS:IP指向程序的第一条要执行的指令(即程序的入口),从而使程序得以运行;
(3)程序运行结束后,返回到加载者。
现在的问题是,根据什么设置CPU的CS:IP 指向程序的第一条要执行的指令?
这一点,是由可执行文件中的描述信息指明的。我们知道可执行文件由描述信息和程序组成,程序来自于源程序中的汇编指令和定义的数据;描述信息则主要是编译、连接程序对源程序中相关伪指令进行处理所得到的信息。我们在程序6.2中,用伪指令end描述了程序的结束和程序的入口。在编译、连接后,由“end start” 指明的程序入口,被转化为一个入口地址,存储在可执行文件的描述信息中。在被加载到内存后,加载者从程序的可执行文件的描述信息中读到程序的入口地址,设置CS:IP。
6.2 在代码段中使用栈
完成下面的程序,利用栈,将程序中定义的数据逆序存放
assume cs:codesg
codesg segment
dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
?
codesg ends
end
可以在程序中通过定义数据来取得一段空间,然后将这段空间当作栈空间来用。
assume cs:codesg
codesg segment
dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
dw 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
;用dw定义16个字型数据,当栈来使用。
start: mov ax,cs
mov ss,ax
mov sp,30h
mov bx,0
mov cx,8
s: push cs:[bx]
add bx,2
loop s
mov bx,0
mov cx,8
s0: pop cs:[bx]
add bx,2
loop s0
mov ax,4c00h
int 21h
codesg ends
end start
ss:sp要指向栈底,使用设置ss:sp指向cs:30
6.3 将数据、代码、栈放入不同的栈
我们用和定义代码段一样的方法来定义多个段,然后在这些段里面定义需要的数据,或通过定义数据来取得栈空间。
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,0,0,0,0,0,0,0,0
stack ends
code segment
start: mov ax,stack
mov ss,ax
mov sp,20h
mov ax,data
mov ds,ax
mov bx,0
mov cx,8
s: push [bx]
add bx,2
loop s
mov bx,0
mov cx,8
s0: pop [bx]
add bx,2
loop s0
mov ax,4c00h
int 21h
code ends
end start
我们可以明显看出,定义一个段的方法和前面讲的定义代码段的方法没有区别。在程序中,段名就相当于一个标号,它代表了段地址。但是我们用伪指令”assume cs:code,ds:data,ss:stack”,CPU并没有真正将cs,ds,ss指向这3个地址,而是我们后来写的指令将其送入,因为assume是伪指令,是由编译器执行的,CPU并不知道它们,我们不必深究assume的作用。
实验5 编写、调试具有多个段的程序
重要,书上。
每个段的大小最低为16字节的倍数,向上取整。
第7章 更灵活的定位内存地址的方法
本章我们主要通过具体的问题来讲解一些更灵活的定位内存地址的方法和相关的编程方法
7.1 and 和 or 指令
(1) and指令:逻辑与指令,按位进行与运算。
mov al, 01100011B
and al, 00111011B
执行后: al=00100011B
通过该指令可将操作对象的相应位设为0,其他位不变。
(2) or指令:逻辑或指令,按位进行或运算。
mov al, 01100011B
or al, 00111011B
执行后: al=01111011B
通过该指令可将操作对象的相应位设为1,其他位不变。
7.2 关于ASCII码
ASCII码是一种编码方案,比如61H表示”a”,41H表示”A”
在文本编辑过程中,我们按下键盘的a键,这个按键的信息被送入计算机,计算机用ASCII码的规则对其进行编码,将其转化为61H存储在内存的指定空间中;文本编辑软件从内存中取出61H,将其送到显卡上的显存中;工作在文本模式下的显卡,用ASCII码的规则解释显存中的内容,61H被当作字符“a”,显卡驱动显示器,将字符“a”的图像画在屏幕上。
这也就是说,如果我们要想在显示器上看到“a”,就要给显卡提供“a”的ASCI码,61H。如何提供?当然是写入显存中。
7.3 以字符形式给出的数据
我们可以在汇编程序中,用’…’的方式指明数据是以字符的形式给出的,编译器将把它们转化为相对应的ASCII码。
assume cs:code ds:data
data segment
db 'unIX'
db 'foRK'
data ends
code segment
start: mov al,'a'
mov bl,'b'
mov ax,4c00h
int 21h
code ends
end start
7.4 大小写转换的问题
考虑这样一个问题,在codesg中填写代码,将datasg中的第一个字符串转化为大写,第二个字符串转化为小写。
datasg segment
db 'BaSiC'
db 'iNFOrMaTiOn'
datasg ends
但是,现实的问题却要求重新必须能区别对待大写字母和小写字母,我们还没有学习判断指令。
寻找新的规律可以看出,就ASCII码的二进制形式看,除第5位(位数从0开始计算)外,大写字母和小写字母的其他各位都一样。这样,我们就有了新的方法,一个字母,将它的第5位置0,它就必将变为大写字母;将它的第5位置1,它就必将变为小写字母。
这样就可以用or和and指令来将一个数据中的某一位置0或置1。
7.5 [bx+idata]
我们也可以用[bx+idata]表示一个内存单元,它的偏移地址为(bx)+idata
mov ax,[bx+200]
该指令也可以写成如下格式
mov ax,[200+bx]
mov ax,200[bx]
mov ax,[bx].200
7.6 用[bx+idata]的方式进行数组的处理
在codesg中填写代码,将datasg中的第一个字符串转化为大写,第二个字符串转化为小写
assume cs:code ds:data
datasg segment
db 'BaSiC'
db 'MinIX'
datasg ends
code segment
start: mov ax,datasg
mov ds,ax
mov bx,0
mov cx,5
s: mov al,[bx]
and al,11011111b
mov [bx],al
mov al,[5+bx]
or al,00100000b
mov [5+bx],al
int bx
loop s
code ends
end start
[bx+idata]的方式为高级语言实现数组提供了便利机制。
7.7 SI和DI
si和di是8086CPU中和bx功能相近的寄存器,但si和di不能够分成两个8位寄存器来使用。
7.8 [bx+si]和[bx+di]
mov ax,[bx+si]
该指令也可以写成如下格式
mov ax,[bx][si]
7.9 [bx+si+idata]和[bx+di+idata]
mov ax,[bx+si+idata]
该指令也可以写成如下格式(si好像都在后面)
mov ax,[bx+200+si]
mov ax,[200+bx+si]
mov ax,200[bx][si]
mov ax,[bx].200[si]
mov ax,[bx][si].200
7.10 不同的寻址方式的灵活应用(二重循环)
问题7.7:编程,将datasg段中每个单词改写成大写字母
assume cs:codesg,ds:datasg
datasg segment
db 'ibm '
db 'dec '
db 'dos '
db 'vax '
datasg ends
codesg segment
start:
codesg ends
end start
我们需要进行4*3次的二重循环,问题在于cx的使用,我们进行二重循环,却只用了一个循环计算器,造成在进行内存循环的时候,覆盖了外层循环的循环计数值。多用一个计算器又不可能,因为loop指令默认cx为循环计数器。
我们应该在每次内层循环的时候,将外层循环的cx中的数值保存起来,在执行外层循环的loop指令前,再恢复外层循环的cx数值。但是寄存器的数量有限,我们不应该选择寄存器,那么可以使用的就是内存了。可以考虑将需要暂存的数据放到内存单元中。
assume cs:codesg,ds:datasg
datasg segment
db 'ibm '
db 'dec '
db 'dos '
db 'vax '
dw 0 ;定义一个字,用来暂存cx
datasg ends
codesg segment
start:
mov ax,datasg
mov ds,ax
mov bx,0
mov cx,4
s0: mov [40h],cx
mov si,0
mov cx,3
s1: mov al,[bx+si]
and al,11011111b
mov [bx+si],al
inc si
loop s1
add bx 10h;
mov cx,[40h]
loop s0
mov ax,4c00h
int 21h
codesg ends
end start
一般来说,在需要暂存数据的时候,我们都应该使用栈。
assume cs:codesg,ds:datasg,ss:stacksg
datasg segment
db 'ibm '
db 'dec '
db 'dos '
db 'vax '
datasg ends
stacksg segment
dw 0,0,0,0,0,0,0,0
stacksg ends
codesg segment
start:
mov ax,datasg
mov ds,ax
mov ax,stacksg
mov ss,ax
mov sp,10h
mov bx,0
mov cx,4
s0: pash cx
mov si,0
mov cx,3
s1: mov al,[bx+si]
and al,11011111b
mov [bx+si],al
inc si
loop s1
add bx 10h;
pop cx
loop s0
mov ax,4c00h
int 21h
codesg ends
end start
实验6 实践课程中的程序
电脑上
第8章 数据处理的两个基本问题
本章对前面的所有内容是具有总结性的。我们知道,计算机是进行数据外理、运算的机器,那么有两个基本的问题就包含在其中:
(1)处理的数据在什么地方?
(2)要处理的数据有多长?
这两个问题,在机器指令中必须给以明确或隐含的说明,否则计算机就无法工作。
我们定义的描述性符号:
reg表示一个寄存器
sreg表示一个段寄存器
8.1 bx、si、di和bp
(1)在8086CPU中,只有这4个寄存器可以用在’[…]’中来进行内存单元的寻址。
(2)在[…]中,这4个寄存器可以单个出现,或只能以4种组合出现:
- bx和si
- bx和di
- bp和si
- bp和di
(3)只要在[…]中使用寄存器bp,而指令中没有显性地给出段地址,段地址就默认在ss中。
8.2 机器指令处理的数据在什么地方
绝大部分机器指令都是进行数据处理的指令,处理大致可分为3类:读取、写入、运算。在机器指令这一层来讲, 并不关心数据的值是多少,而关心指令执行前一刻,它将要处理的数据所在的位置。
指令在执行前,所要处理的数据可以在3个地方: CPU内部、内存、端口(端口将在后面的课程中进行讨论)
| 机器码 | 汇编指令 | 执行指令前数据的位置 |
|---|---|---|
| 8E1E0000 | mov bx,[0] | 内存,ds:0单元 |
| 89C3 | mov bx,ax | CPU内部,ax寄存器 |
| BB0100 | mov bx,1 | CPU内部,指令寄存器 |
8.3 汇编语言中数据位置的表达
(1)立即数(idata):执行前在CPU的指令缓冲器中
(2)寄存器
(3)段地址(SA)和偏移地址(EA):指令要处理的数据在内存中
8.4 寻址方式
当数据存放在内存中的时候,我们可以用多种方式来给定这个内存单元的偏移地址,这种定位内存单元的方法一般被称为寻址方式。(就是那些[bx+si+idata]什么的)
8.5 指令要处理的数据的长度(byte/word)
8086CPU的指令,可以处理两种尺寸的数据,byte 和word。所以在机器指令中要指明,指令进行的是字操作还是字节操作。对于这个问题,汇编语言中用以下方法处理。
(1)通过寄存器名指明要处理的数据的尺寸。(ax/al)
(2)在没有寄存器名存在的情况下,用操作符word/byte ptr指明内存单元的长度,例如
mov word ptr ds:[0],1
inc byte ptr [bx]
在没有寄存器参与的内存单元访问指令中,用word ptr 或byte ptr 显性地指明所要访问的内存单元的长度是很必要的。否则,CPU无法得知所要访问的单元是字单元,还是字节单元。
(3)有些指令默认了访问的是字单元还是字节单元,例如push指令只能进行字操作。
8.6 寻址方式的综合应用
书上
8.7 div指令
div是除法指令,使用div做除法的时候应注意以下问题。
(1)除数:有8位和16位两种,在一个reg或内存单元中。
(2)被除数:有16位和32位两种。如果除数为8位,被除数则为16位,默认在AX中存放;
如果除数为16位,被除数则为32位,在DX和AX中存放,DX存放高16位,AX存放低16位。
(3)结果:如果除数为8位,则AL存储除法操作的商,AH存储除法操作的余数:
如果除数为16位,则AX存储除法操作的商,DX存储除法操作的余数。
div byte ptr ds:[0]
含义:(al)=(ax)/((ds)*16+0)的商 (ah)=(ax)/((ds)*16+0)的余数
div word prt [bx+si+8]
含义:(ax)=[(dx)*10000H+(ax)]/((ds)*10H+(bx)+(si)+8)的商
(dx)=[(dx)*10000H+(ax)]/((ds)*10H+(bx)+(si)+8)的余数
8.8 伪指令dd
dd用来定义dword(双字)型数据
8.9 dup
dup也是由编译器识别处理的符号,用来进行数据的重复。
db 3 dup(0) 相当于db 0,0,0
db 3 dup(0,1,2) 相当于db 0,1,2,0,1,2,0,1,2
db 3 dup('abc','ABC') 相当于 db’abcABCabcABCabcABC’
实验7 寻址方式在结构化数据访问中的应用
重要,见代码
第9章 转移指令的原理
可以修改IP,或同时修改CS和IP的指令统称为转移指令。概括地讲,转移指令就是可以控制CPU执行内存中某处代码的指令。
8086CPU的转移行为有以下几类。
- 只修改IP时,称为段内转移,比如: jmp ax。
- 同时修改CS 和IP时,称为段间转移,比如: jmp 1000:0。
由于转移指令对IP的修改范围不同,段内转移又分为:短转移和近转移。
- 短转移IP的修改范围为 -128~127.
- 近转移IP的修改范围为 -32768~32767.
8086CPU的转移指令分为以下几类。
- 无条件转移指令(如: jmp)
- 条件转移指令
- 循环指令(如: loop)
- 过程
- 中断
这些转移指令转移的前提条件可能不同,但转移的基本原理是相同的。我们在这一章主要通过深入学习无条件转移指令jmp来理解CPU执行转移指令的基本原理。
9.1 操作符offset
操作符offset 在汇编语言中是由编译器处理的符号,它的功能是取得标号的偏移地址。
如:start:mov ax,offset start
9.2 jmp指令
jmp为无条件转移指令,可以只修改ip,也可以同时修改cs和ip。
指令要给出两种信息:
(1) 转移的目的地址
(2) 转移的距离(段间转移、段内短转移,段内近转移)
不同的给出目的地址的方法,和不同的转移位置,对应有不同格式的jmp指令。
9.3 依据位移进行转移的jmp指令
jmp short标号(转到标号处执行指令)
这种格式的jmp指令实现的是段内短转移,它对IP的修改范围为-128~127,也就是说,它向前转移时可以最多越过128 个字节,向后转移可以最多越过127个字节。
汇编指令jmp short s对应的机器指令应该是什么样的呢?我们先看一下别的汇编指令和其相对应的机器指令。
| 汇编指令 | 机器指令 |
|---|---|
| mov ax,0123h | B8 23 01 |
| mov ax, ds: [0123h] | A1 23 01 |
| push ds:[0123h] | FF 36 23 01 |
| jmp short s (0BBD:000B) | EB 03 |
| jmp fat ptr s (0BBD:010B) | EA 0B 01 BD 0B |
可以看到,在一般的汇编指令中,汇编指令中的idata(立即数), 不论它是表示一个数据还是内存单元的偏移地址,都会在对应的机器指令中出现。
而 jmp short 标号 的机器码却不包含转移的目的地址,而包含的是转移的位移(和目标标号的距离(由编译器计算))。
实际上,jmp short 标号 的功能为:(IP)=(IP)+8位位移(-128~127)
jmp near ptr 标号 的功能为:(IP)=(IP)+16位位移(-32768~32767)
9.4 转移的目的地址在指令中的jmp指令
前面讲的jmp指令是根据相对于当前ip的转移位移,jmp far ptr 标号 实现的是段间转移,又称远转移。
far ptr 指明了指令用标号的段地址和偏移地址修改CS和IP。
9.5 转移地址在寄存器中的jmp指令
指令格式:jmp 16位reg
功能:(IP)=(16 位reg)
这种指令我们在前面的内容(参见2.11节)中已经讲过,这里就不再详述。
9.6 转移地址和内存中的jmp指令
转移地址在内存中的jmp指令有两种格式:
(1) jmp word ptr 内存单元地址(段内转移)
功能:从内存单元地址处开始存放着一个字,是转移的目的偏移地址。
(2) jmp dword ptr 内存单元地址(段间转移)
功能:从内存单元地址处开始存放着两个字,高地址处的字是转移的目的段地址,低地址处是转移的目的偏移地址。
9.7 jcxz指令
jcxz指令为有条件转移指令,所有的有条件转移指令都是短转移,在对应的机器码中包含转移的位移,而不是目的地址。对IP的修改范围都为: - 128~127。
指令格式:jcxz 标号(如果(cx)=0,转移到标号处执行。)
“jcxz 标号” 的功能相当于 if((cx)==0) jmp short标号;
9.8 loop指令
loop指令为循环指令,所有的循环指令都是短转移。
9.9 根据位移进行转移的意义
这种设计,方便了程序段在内存中的浮动装配。
一段程序装在内存中的不同位置都可正确执行,因为他们根据位移进行转移,而不是根据地址。
9.10 编译器对转移位移超界的检测
如果在源程序中出现了转移范围超界的问题,在编译的时候,编译器将报错。
实验9 根据材料编程
80x25彩色字符模式显示缓冲区(以下简称为显示缓冲区)的结构:
内存地址空间中,B8000H~BFFFH共32KB的空间,为80X25彩色字符模式的显示缓冲区。向这个地址空间写入数据,写入的内容将立即出现在显示器上。
在80*25彩色字符模式下,显示器可以显示25行,每行80个字符,每个字符可以有256种属性(背景色、前景色、闪烁、高亮等组合信息)。
这样,一个字符在显示缓冲区中就要占两个字节,分别存放字符的ASCII码和属性。80x25模式下,一屏的内容在显示缓冲区中共占4000个字节。
$22580*8=32000=32kb$
显示缓冲区分为8页,每页4KB(≈4000B),显示器可以显示任意一页的 1内容。 一般情况下,显示第0页的内容。也就是说通常情况下,B8000H~B8F9FH 中的4000个字节的内容将出现在显示器上。

例:在显示器的0行0列显示红底高亮闪烁绿字’ABCDEF’(红底高亮闪烁绿字,属性字节为11001010B,CAH)
assume ds:data,cs:code
data segment
data ends
code segment
start: mov ax,data
mov ds,ax
mov ax,0B800H
mov es,ax
mov bx,0
mov ax,41h
mov cx,6
s: mov es:[bx],ax
inc ax
inc bx
mov dx,0CAH
mov es:[bx],dx
inc bx
loop s
mov ax,4c00h
int 21h
code ends
end start

第10章 CALL和RET指令
call和ret指令都是转移指令,它们都修改IP,或同时修改CS和IP。
10.1 ret和retf
ret指令用栈中的数据,修改IP的内容,从而实现近转移;
retf指令用栈中的数据,修改CS和IP的内容,从而实现远转移。
CPU执行ret指令时,相当于进行:pop IP
CPU执行retf指令时,相当于进行:pop IPpop CS
10.2 call指令
CPU执行call指令时,进行两步操作:
(1)将当前的IP或CS和IP压入栈中;
(2)转移。
call指令不能实现短转移,除此之外,call 指令实现转移的方法和jmp指令的原理相同,下面的几个小节中,我们以给出转移目的地址的不同方法为主线,讲解call指令的主要应用格式。
10.3 依据位移进行转移的call指令
call 标号 (将当前的IP压栈后,转到标号处执行指令)
CPU执行“call 标号”时,相当于进行:push IPjmp near ptr标号
| 内存地址 | 机器码 | 汇编指令 |
|---|---|---|
| 1000:0 | b8 00 00 | mov ax,0 |
| 1000:3 | e8 01 00 | call s |
| 1000:6 | 40 | inc ax |
| 1000:7 | 58 | S:pop ax |
上面的程序执行后,ax=6,因为在call s的时候,相当在读入指令后,ip便自动增加。
10.4 转移的目的地址在指令中的call指令
前面讲的call指令,其对应的机器指令中并没有转移的目的地址,而是相对于当前IP的转移位移。
call far ptr标号 实现的是段间转移。
CPU执行“call far ptr标号”时,相当于进行:push CSpush IPjmp far ptr标号
10.5 转移地址在寄存器中的call指令
指令格式: call 16位reg
CPU执行“call 16位reg”时,相当于进行:push IPjmp 16位reg
10.6 转移地址在内存中的call 指令
转移地址在内存中的call指令有两种格式。
(1)call word ptr 内存单元地址
CPU执行“call word ptr 内存单元地址”时,相当于进行:push IPjmp word ptr 内存单元地址
(2)call dword ptr 内存单元地址
CPU执行“call dword ptr 内存单元地址”时,相当于进行:push CSpush IPjmp dword ptr 内存单元地址
10.7 call和ret的配合使用(子程序/函数调用/函数栈/递归栈)
现在来看一下,如何将它们配合使用来实现子程序的机制。
可以写一个具有一定功能的程序段,我们称其为子程序,在需要的时候,用call 指令转去执行。call 指令转去执行子程序之前,call指令后面的指令的地址将存储在栈中,所以可在子程序的后面使用ret指令,用栈中的数据设置IP的值,从而转到call指令后面的代码处继续执行。
这样,我们可以利用call 和ret来实现子程序的机制。子程序的框架如下。
assume cs:code
code segment
main: ...
call subl ;调用子程序sub1
...
mov ax,4c00h
int 21h
sub1: ... ;子程序sub1开始
...
call sub2 ;调用子程序sub2
...
ret ;子程序返回
sub2: ... ;子程序sub2开始
...
ret ;子程序返回
code ends
end main
10.8 mul指令
mul 是乘法指令,使用mul做乘法的时候,注意以下两点。
(1)两个相乘的数:两个相乘的数,要么都是8位,要么都是16位。如果是8位,一个默认放在AL中,另一个放在8位reg或内存字节单元中;如果是16位,一个默认在AX中,另一个放在16位reg或内存字单元中。
(2)结果:如果是8位乘法,结果默认放在AX中;如果是16 位乘法,结果高位默认在DX中存放,低位在AX中放。
格式如下:mul regmul内存单元
10.9 模块化程序设计
从上面我们看到,call 与ret 指令共同支持了汇编语言编程中的模块化设计。在实际编程中,程序的模块化是必不可少的。因为现实的问题比较复杂,对现实问题进行分析时,把它转化成为相互联系、不同层次的子问题,是必须的解决方法。而call与ret指令对这种分析方法提供了程序实现上的支持。利用call 和ret 指令,我们可以用简捷的方法,实现多个相互联系、功能独立的子程序来解决一个复杂的问题。
10.10 参数和结果传递的问题
子程序一般都要根据提供的参数处理一定的事务, 处理后,将结果(返回值)提供给调用者。其实,我们讨论参数和返回值传递的问题,实际上就是在探讨,应该如何存储子程序需要的参数和产生的返回值。
例:编程,计算data段中第一组数据的3次方,结果保存在后面一组dword单元中。
assume cs:code
data segment
dw 1,2,3,4,5,6,7,8
dd 0,0,0,0,0,0,0,0
data ends
code segment
start: mov ax,data
mov ds,ax
mov si,0 ;ds:si指向第一组word单元
mov di,16 ;ds:di指向第二组dword单元
mov cx,8
s: mov bx,[si]
cal1 cube
mov [di],ax
mov [di].2, dx
add si,2 ;ds:si指向下一个word单元
add di,4 ;ds:di指向下一个dword单元
1oop s
mov ax, 4c00h
int 21h
cube: mov ax,bx
mul bx
mul bx
ret
code ends
end start
10.11 批量数据的传递
前面的例程中,子程序cube只有一-个参数,放在bx中。如果有两个参数,那么可以用两个寄存器来放,可是如果需要传递的数据有3个、4个或更多直至N个,在这种时候,我们将批量数据放到内存中,然后将它们所在内存空间的首地址放在寄字器中,传递给需要的子程序。对于具有批量数据的返回结果,也可用同样的方法。
编程,将data段中的字符串转化为大写。
assume cs:code
data segment
db 'conversation'
data ends
code segment
start: mov ax,data
mov ds,ax
mov si,0 ;ds:si指向字符串(批量数据)所在空间的首地址
mov cx,12 ;cx存放字符串的长度
call capital
mov ax, 4c00h
int 21h
capital:and byte ptr [si],11011111b
inc si
1oop capital
ret
code ends
end start
10.12 寄存器冲突的问题
设计一个子程序,功能:将-一个全是字母,以0结尾的字符串,转化为大写。(可以用jcxz来检测0)
capital:mov c1,[si]
mov ch,0
jcxz ok ;如果(cx)=0,结束;如果不是0,处理
and byte ptr [si],11011111b ;将ds:si所指单元中的字母转化为大写
inc si ;ds:si指向下一个单元
jmp short capital
ok: ret
但是,如果使用循环,重复调用子程序capital,完成对多个字符串的处理,就会重复使用cx,使得程序出错。
解决这个问题的简捷方法是,在子程序的开始将子程序中所有用到的寄存器中的内容都保存起来,在子程序返回前再恢复。可以用栈来保存寄存器中的内容。
以后,我们编写子程序的标准框架如下:
子程序开始: 子程序中使用的寄存器入栈
子程序内容
子程序中使用的寄存器出栈
返回(ret、retf)
我们改进一下子程序capital的设计:
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
ret
实验10 编写子程序
在这次实验中,我们将要编写3个子程序,通过它们来认识几个常见的问题和掌握解决这些问题的方法。同前面的所有实验y一样,这个实验是必须独立完成的,在后面的课程中,将要用到这个实验中编写的3个子程序。
1.显示字符串
问题:
显示字符串是现实工作中经常要用到的功能,应该编写一个通用的子程序来实现这个功能。我们应该提供灵活的调用接口,使调用者可以决定显示的位置(行、列)、内容和颜色。
子程序描述:
名称:show_ str
功能:在指定的位置,用指定的颜色,显示一个用0结束的字符串。
参数:(dh)= 行号(取值范围0~24), (dl)= 列号(取值范围0~79),(cl)=颜色,ds:si指向字符串的首地址
返回:无
应用举例:在屏幕的8行3列,用绿色显示data段中的字符串。
assume cs: code
data segment
db 'Welcome to masm !',0
data ends
code segment
start: mov dh,8 ;8行
mov dl,3 ;3列
mov cl,2 ;绿色
mov ax,data
mov ds,ax
mov si,0 ;si字符串首地址(ds里的)
call show_str
mov ax,4c00h
int 21h
show_str: push cx
push si
push dx
push bx
sub dh,1 ;dh=7
mov al,160
mul dh
mov bx,ax ;bx为行的偏移地址
mov al,2
mul dl
add bl,al ;bx为总偏移地址
mov ax,0B800H
mov es,ax ;es为显存显示在屏幕上的段地址
mov dl,cl
display: mov cl,ds:[si]
mov ch,0
jcxz ok
mov es:[bx],cl
inc bx
mov es:[bx],dl
inc bx
inc si
jmp short display
ok: pop bx
pop dx
pop si
pop cx
ret
code ends
end start

实验2和3暂时未做
第11章 标准寄存器
flag和其他寄存器不一样,其他寄存器是用来存放数据的,都是整个寄存器具有一个含义。而flag寄存器是按位起作用的,也就是说,它的每一位都有专门的含义,记录特定的信息。

在这一章中,我们学习标志寄存器中的CF、PF、ZF、SF、OF、DF标志位,以及些与其相关的典型指令。
11.1 ZF(Zero Flag)标志
flag的第6位是ZF,零标志位。它记录相关指令执行后,其结果是否为0。如果结果为0,那么zf=l;如果结果不为0,那么zf=0。
比如,指令:mov ax,1sub ax,1
执行后,结果为0,则zf=1。
注意:在8086CPU的指令集中,有的指令的执行是影响标志寄存器的,比如,add、sub、mul、 div、 inc、 or、 and 等,它们大都是运算指令(进行逻辑或算术运算);有的指令的执行对标志寄存器没有影响,比如,mov、push、 pop 等,它们大都是传送指令。在使用一条指令的时候,要注意这条指令的全部功能,其中包括,执行结果对标志寄存器的哪些标志位造成影响。
11.2 PF(Parity Flag)标志
flag的第2位是PF,奇偶标志位。它记录相关指令执行后,其结果的所有bit位中1的个数是否为偶数。如果1的个数为偶数,pf=1,如果为奇数,那么pf=0。
11.3 SF(Sign Flag)标志
flag的第7位是SF,符号标志位。它记录相关指令执行后,其结果是否为负。结果为负,sf=1;如果非负sf=0。
SF标志,就是CPU对有符号数运算结果的一种记录,它记录数据的正负。在我们将数据当作有符号数来运算的时候,可以通过它来得知结果的正负。如果我们将数据当作无符号数来运算,SF的值则没有意义,虽然相关的指令影响了它的值。
这也就是说,CPU在执行add等指令时,是必然要影响到SF标志位的值的。至于我们需不需要这种影响,那就看我们如何看待指令所进行的运算了。
11.4 CF(Carry Flag)标志
flag的第0位是CF,进位标志位。一般情况下,在进行无符号数运算的时候,它记录了运算结果的最高有效位向更高位的进位值,或从更高位的借位值。
我们知道,当两个数据相加的时候,有可能产生从最高有效位向更高位的进位。由于这个进位值在8位数中无法保存,我们在前面的课程中,就只是简单地说这个进位值丢失了。其实CPU在运算的时候,并不丢弃这个进位值。
而当两个数据做减法的时候,也有可能向更高位借位。比如,下面的指令:
mov al, 97H
sub al, 98H ;执行后: (al)=FFH, CF=1, CF记录了向更高位的借位值
sub al,al ;执行后: (al)=0, CF=0,CF记录了向更高位的借位值
11.5 OF(Overflow Flag)标志
在进行有符号数运算的时候,如结果超过了机器所能表示的范围称为溢出。
注意,这里所讲的溢出,只是对有符号数运算而言。如果在进行有符号数运算时发生溢出,那么运算的结果将不正确。
一定要注意 CF和OF的区别:CF是对无符号数运算有意义的标志位,而OF是对有符号数运算有意义的标志位。它们之间没有任何关系。
11.6 adc指令
adc是带进位加法指令,它利用了CF位上记录的进位值。
指令格式:adc 操作对象1,操作对象2.
功能:操作对象1=操作对象1+操作对象2 + CF
比如指令adc ax,bx实现的功能是: (ax)=(ax)+(bx)+ CF
CPU提供adc指令的目的,就是来进行加法的第二步运算的。adc指令和add指令相配合就可以对更大的数据进行加法运算。
编程,计算1EF0001000H+2010001EF0H,结果放在ax(最高16位),bx(次高16位),cx(低16位)中。
mov ax,001EH
mov bx,0F000H
mov cx,1000H
add cx,1EF0H
adc bx,1000H
adc ax,0020H
inc和loop指令不影响CF位
11.7 sbb指令
sbb是带借位减法指令,它利用了CF位上记录的借位值。
指令格式:sbb 操作对象1,操作对象2
功能:操作对象1=操作对象1-操作对象2-CF
比如指令sbb ax,bx实现的功能是: (ax)=(ax)-(bx)-CF
sbb和adc是基于同样的思想设计的两条指令,在应用思路上和adc类似。
11.8-11.12暂未看
第12章 内中断
任何一个通用的CPU,比如8086,都具备一种能力, 可以在执行完当前正在执行的指令之后,检测到从CPU外部发送过来的或内部产生的一种特殊信息, 并且可以立即对所接收到的信息进行处理。这种特殊的信息,我们可以称其为:中断信息。中断的意思是指,CPU不再接着(刚执行完的指令)向下执行,而是转去处理这个特殊信息。
中断信息可以来自CPU的内部和外部,这一章中,我们主要讨论来自于CPU内部的中断信息。
12.1 内中断的产生
当CPU的内部有什么事情发生的时候,将产生需要马上处理的中断信息呢?对于8086CPU,当CPU内部有下面的情况发生的时候,将产生相应的中断信息。
(1)除法错误, 比如,执行div指令产生的除法溢出;
(2)单步执行;
(3)执行into指令;
(4)执行int指令。
既然是不同的信息,就需要进行不同的处理。要进行不同的处理,CPU首先要知道,所接收到的中断信息的来源。8086CPU用称为中断类型码的数据来标识中断信息的来源。中断类型码为一个字节型数据,可以表示256种中断信息的来源。
上述的4种中断源,在8086CPU中的中断类型码如下。
(1)除法错误:0
(2)单步执行:1
(3)执行into指令:4
(4)执行int指令,该指令的格式为int n,指令中的n为字节型立即数,是提供给CPU的中断类型码。
12.2 中断处理程序
CPU收到中断信息后,需要对中断信息进行处理。而如何对中断信息进行处理,可以由我们编程决定。我们编写的,用来处理中断信息的程序被称为中断处理程序。一般来说,需要对不同的中断信息编写不同的处理程序。
12.3 中断向量表
CPU用8位的中断类型码通过中断向量表找到相应的中断处理程序的入口地址。那么什么是中断向量表呢?中断向量表就是中断向量的列表。那么什么又是中断向量呢?所谓中断向量,就是中断处理程序的入口地址。展开来讲,中断向量表,就是中断处理程序入口地址的列表。
中断向量表在内存中保存,其中存放着256个中断源所对应的中断处理程序的入口。CPU只要知道了中断类型码,就可以将中断类型码作为中断向量表的表项号,定位相应的表项,从而得到中断处理程序的入口地址。

中断向量表在内存中存放,对于8086PC机,中断向量表指定放在内存地址0处。从内存0000:0000到0000:03FF 的1024 个单元中存放着中断向量表。一个表项存放一个中断向量,也就是一个中断处理程序的入口地址,对于8086CPU,这个入口地址包括段地址和偏移地址,所以一个表项占两个字,高地址字存放段地址,低地址字存放偏移地址。
12.4 中断过程
可以用中断类型码,在中断向量表中找到中断处理程序的入口。找到这个入口地址的最终目的是用它设置CS和IP,使CPU执行中断处理程序。用中断类型码找到中断向量,并用它设置CS和IP,这个工作是由CPU的硬件自动
完成的。CPU硬件完成这个工作的过程被称为中断过程。
CPU收到中断信息后,要对中断信息进行处理,首先将引发中断过程。硬件在完成中断过程后,CS:IP 将指向中断处理程序的入口,CPU开始执行中断处理程序。
在中断过程中,在设置CS:IP之前,还要将原来的CS和IP的值保存起来。
下面是8086CPU在收到中断信息后,所引发的中断过程。
(1)(从中断信息中)取得中断类型码;
(2)标志寄存器的值入栈(在中断过程中会改变标志寄存器的值)
(3)设置标志寄存器的第8位TF和第9位IF的值为0(这一步的目的后面将介绍);
(4)CS的内容入栈;
(5) IP 的内容入栈;
(6)从内存地址为中断类型*4和中断类型码*4+2的两个字单元中读取中断处理程序的入口地址设置IP和CS。
CPU在收到中断信息之后,如果处理该中断信息,就完成一个由硬件自动执行的中断过程(程序员无法改变这个过程中所要做的工作)。
我们更简洁地描述中断过程,如下:
(1)取得中断类型码N;
(2)pushf
(3)TF=0,IF=0
(4)push CS
(5)push IP
(6)(IP)=(N*4),(CS)=(N*4+2)
在最后一步完成后,CPU开始执行由程序员编写的中断处理程序。
12.5 中断处理程序和iret指令
由于CPU随时都可能检测到中断信息,也就是说,CPU随时都可能执行中断处理程序,所以中断处理程序必须一直存储在内存某段空间之中。而中断处理程序的入口地址,即中断向量,必须存储在对应的中断向量表表项中。
中断处理程序的编写方法和子程序的比较相似,下面是常规的步骤:
(1)保存用到的寄存器;
(2)处理中断;
(3)恢复用到的寄存器;
(4)用iret指令返回。
iret指令的功能用汇编语法描述为:pop IPpop CSpopf