基础知识

存储单元

最小信息单位是bit(比特、二进制位),8bit为1Byte(字节)。存储单元以字节为单位计。128个存储单元的存储器可以存储128字节。

CPU对存储器的读写

读写时CPU与外部器件进行3类信息交互:

  • 存储单元的地址(地址信息)

  • 器件的选择,读或写的命令(控制信息)

  • 读或写的数据(数据信息)

读写过程

CPU从3号单元中读取数据过程

  1. CPU通过地址线将地址信息3发出。

  2. CPU通过控制线发出内存读取命令,选中存储芯片,并通知它,将要从中读取。

  3. 存储器将3号单元中的数据通过数据线送入CPU。

CPU向3号单元写入数据

  1. CPU通过地址线将地址信息3发出。

  2. CPU通过控制线发出内存写命令,选中存储器芯片,并通知它,要向其中写入数据。

  3. CPU通过数据线将数据送入内存的3号单元中。

地址总线

一个CPU有N根地址线,则可以立这个CPU的地址总线宽度为N。这样CPU可以最多寻找2的N次方个内存单元。

数据总线

CPU与内存或其它器件之间的数据传递是通过数据总线进行的。数据总线宽度决定了CPU和外界的数据传送速度(宽度不足时将需要进行多次传送)。8088CPU数据总线宽度为8,8086CPU的数据总线宽度为16。

控制总线

控制总线是一些不同控制线的集合。有多少根控制总线,就意味着CPU提供了对外部器件的多少种控制。它决定了CPU对外部器件的控制能力。

地址空间概述

主板上的核心器件通过总线(地址总线、数据总线、控制总线)相连。这些器件有:CPU、存储器、外围芯片组、扩展插槽(插有RAM内存条和各类接口卡)等。

PC上的存储器芯片从读写属性上分为两类:RAM和ROM。从功能和连接上分为:RAM、BIOS及接口卡上的RAM(例如:显存)。

这些存储器在物理上是独立的器件,但它们都和CPU的总线相连;CPU对它们进行读或写的时候都通过控制线发出内存读写命令。即CPU在操纵和控制它们的时候,把它们都当作内存来对侍,把它们总的看作一个由若干存储单元组成的逻辑存储器,这个逻辑存储器就是我们所说的内存地址空间。

所有物理存储器被看作由若干存储单元组成的逻辑存储器,每个物理存储器在这个逻辑存储器中占有一个地址段。CPU在这段地址空间中读写数据,实际上就是在对相应的物理存储器中读写数据。

内存地址空间的大小受CPU地址总线宽度的限制。8086CPU的地址总线宽度为20,可以传送2的20次方个不同的地址信息(内存单元),地址空间大小为1MB。80386CPU的地址总线宽度为32,则内在地址空间最大约为4GB。

寄存器(CPU工作原理)

CPU由运算器、控制器、寄存器等器件构成,这些器件靠内部总线相连。前一章所说的总线相对于CPU内部来说是外部总线。在CPU中:运算器进行信息处理;寄存器进行信息存储;控制器控制各种器件进行工作;内部总线连接各种器件,在它们之间进行数据的传送。

对于汇编程序员来说,CPU中的主要部件是寄存器。寄存器是CPU中程序员可以用指令读写的部件。程序员通过改变各种上寄存器中的内容来实现对CPU的控制。

8086CPU中有14个寄存器:AX、BX、CX、DX、SI、DI、SP、BP、IP、CS、SS、DS、ES、PSW。

通用寄存器

8086CPU的所有寄存器都是16位的,可以存储两个字节。AX、BX、CX、DX四个寄存器通常用来存放一般性的数据,被称为通用寄存器。

8086CPU的上代CPU中寄存器是8位的,为保证兼容性,8086CPU中的AX、BX、CX、DX四个寄存器都可以分为两个独立的8位寄存器来使用:

  • AX可分为AH和AL

  • BX可分为BH和BL

  • CX可分为CH和CL

  • DX可分为DH和DL

8086CPU可以一次性处理两种尺寸的数据:byte(字节,8位),可存在8位寄存器中,word(字,16位),可存在16位寄存器中。

物理地址

CPU访问内存单元时,要给出内存单元的地址。所有内存单元构成的存储空间是一个一维的线性空间,每个内存单元在这个空间中都有惟一的地址,这个地址被称为物理地址。

CPU通过地址总线卷入存储器的必须是一个内在单元的物理地址。在CPU向地址总线上发出物理地址之前,必须在内部先形成这个物理地址。

16位结构的CPU

16位结构的CPU有下面几方面的结构特性:

  • 运算器一次最多可以处理16位的数据

  • 寄存器的最大宽度为16位

  • 寄存器和运算器之间的通路为16位。

内存单元的地址在送上地址总线之前,必须在CPU中处理、传输、暂时存放,对于16位CPU,能一次处理、传输、暂存16位地址。

8086CPU给出物理地址的方法

8086CPU有20位地址总线,可以传送20位地址,达到1MB寻址能力。8086CPU又是16位结构,在内部一次性处理、传输、暂存的地址为16位。如果将地址从内部简单的发出,那就只能送出16位地址,表现出的寻址能力只有64KB。

8086CPU采用一种在内部用两个16位地址合成的方法来形成一个20位的物理地址。

当8086CPU要读写内存时:

  1. CPU中的相关部件提供两个16位的地址,一个称为段地址,另一个称为偏移地址

  2. 段地址和偏移地址通过内部总线送入一个称为地址加法器的部件

  3. 地址加法器将两个16位地址合并成为一个20位的物理地址

  4. 地址加法器通过内部总线将20位物理地址送入输入输出控制电路

  5. 输入输出控制电路将20位物理地址送上地址总线

  6. 20位物理地址被地址总线传送到存储器

地址加法器采用 物理地址=段地址X16+偏移地址 的方法用段地址和偏移地址合成物理地址。

段的概念

将若干地址连续内在单元看作一个段,用段地址X16的起始地址(基础地址),用领衔地址定位段中的内在单元。有两点需要注意:段地址X16必然是16的倍数,所以一个段的起始地址也一定是16的倍数;偏移地址为16位,16位的寻址能力为64KB,所以一个段的长度最大为64KB。

段寄存器

8086CPU有4个段寄存器:CS、DS、SS、ES。

CS和IP

CS和IP是8086CPU中两个最关键的寄存器,它们指示了CPU当前要读取指令的地址。CS主代码段寄存器,IP为指令指针寄存器。

在8086CPU中,任意时刻,设CS中内容为M,IP中的内容为N,8086CPU将从内存MX16+N单元开始,读取下一条指令并执行。

8086CPU加电启动或复位后CS和IP被设置为CS=F000H,IP=FFF0H,CPU从内存FFFF0H单元中读取指令执行。

修改CS、IP的指令

大部分寄存器的值,都可以用mov指令来改变,mov指令被称为传送指令。

但是mov指令不能用于设置CS、IP的值,因为8086CPU没有提供这样的功能。8086CPU提供了jmp指令来改变CS、IP的内容。

若想同时修改CS、IP的内容,可用指令“jmp 段地址: 偏移地址”完成,如:

1
2
jmp 2AE3:3
jmp 3:0B16

若想仅修改IP的内容,可以用指令“jmp 某一合法寄存器”完成,如:

1
jmp ax

代码段

在编程时,可根据需要,将一组内存单元定义为一个段。长度为N(N<=64KB)的一组代码,存在一组地址连续、起始地址为16的倍数的内存单元中,我们可以认为,这段内存是用来存放代码的,从而定义了一个代码段。

将一段内存当作代码段,仅仅是我们在编程时的一种安排,CPU并不会由于这种安排,就自动地将我们定义的代码段中的指令当作指令来执行。CPU只认被CS:IP指向的内存单元中的内容为指令。

Debug程序

  • R命令查看、改变CPU寄存器的内容,“r”查看,“r ax”修改ax的内容。

  • D命令查看内存中的内容,“d 段地址:偏移地址”将显示指定的内存单元开始的128个内存单元的内容。

  • E命令改写内存中的内容,“e 起始地址 数据 数据 数据……”,实际上也可以用它来直接写入机器码。

  • T指令逐条执行代码。

  • P执行中断调用。

  • A命令以汇编指令的形式在内存中写入机器指令。简单的A命令从一个预设的地址开始输入指令。“A 1000:0”从1000:0开始的内存中写入指令。

寄存器(内存访问)

内存中字的存储

CPU中,用16位寄存器来存储一个字。高8位存放高位字节,低8位存放低位字节。在内存中存储时,由于内存单元是字节单元,则一个字要用两个地址连续的内存单元来存放,这个字的低位字节放在低地址单元中,高位字节存放在高地址单元中。

字单元,即存放一个字型数据(16位)的内存单元,由两个地址连续的内存单元组成。

DS和[address]

CPu要读写一个内存单元的时候,必须先给出这个内存单元的地址,在8086PC中,内存地址由段地址和偏移地址组成。8086中有一个DS寄存器,通常用来存放要访问数据的段地址。

前面使用mov指令,可以完成两种传送:

  1. 将数据直接送入寄存器

  2. 将一个寄存器中的内容送入另一个寄存器

也可以使用mov将一个内存单元中的内容送入一个寄存器中。格式:mov 寄存器名,内在单元地址。

内存单元偏移地址放在[]中。段地址由CPU自动取ds中的数据。

不能使用类似mov ds, 1000h来将1000h送入ds。只能通过寄存器中转。

1
2
mov bx,1000h
mov ds,bx

从寄存器送入内存单元。例如,使用mov [0],al可以将al的数据传送到当前数据段中偏移地址为0的内存单元里。

字的传送

我们只要在mov指令中给出16位寄存器就可以进行16位数据的传送了。

mov、add、sub指令

mov指令形式:

  • mov 寄存器, 数据 如:mov ax, 8

  • mov 寄存器, 寄存器 如:mov ax,bx

  • mov 寄存器, 内存单元 如:mov ax,[0]

  • mov 内存单元, 寄存器 如:mov [0],bx

  • mov 段寄存器, 寄存器 如:mov ds,bx

  • mov 寄存器, 段寄存器 如:mov bx,ds

  • mov 内存单元, 段寄存器 如:mov [0],cs

  • mov 段寄存器, 内存单元 如:mov ds,[0]

  • 不能使用 mov 段寄存器, 数据

add和sub指令同mov一样,都操作两个对象。它们可以有以下几种形式:

  • add 寄存器, 数据 如:add ax,8

  • add 寄存器, 寄存器 如:add ax,bx

  • add 寄存器, 内存单元 如:add ax, [0]

  • add 内存单元, 寄存器 如:add [0],ax

  • sub 寄存器, 数据 如:sub ax,8

  • sub 寄存器, 寄存器 如:sub ax,bx

  • sub 寄存器, 内存单元 如:sub ax, [0]

  • sub 内在单元, 寄存器 如:sub [0],ax

数据段

将一段内存将作数据段,是编程时的一种安排,用ds存放数据段的段地址,再根据需要,用相关指令访问数据段中的具体单元。

栈是一种具有特殊的访问方式的存储空间。栈的操作规则为LIFO(Last In First Out),先进先出。

CPU提供的栈机制

8086CPU提供的入栈和出栈指令为PUSH和POP。

8086CPU中有两个寄存器来存放栈顶的地址,段寄存器SS和寄存器SP,栈顶的段地址放在SS中,偏移地址放在SP中。栈为空时,栈中没有元素,也就不存在栈顶的元素,所以SS:SP只指向栈的最底部单元下面的单元,该单元的偏移地址为栈最底部的字单元的偏移地址+2。栈操作时变化的是SP的值,内存中的数据不会被清空,只会被覆盖。

栈顶超界的问题

当栈满时再使用push指令,或栈空时再使用pop指令都将发生栈顶超界的问题,这将覆盖栈空间外的数据。

8086CPU不保证对栈的操作不会超界。

在编程的时候要自己操心栈顶超界的问题,要根据可能用到的最大栈空间,来安排栈的大小,执行出入栈的时候要注意防止超界。

push和pop指令

指令格式:

  • push 寄存器

  • pop 寄存器

  • push 段寄存器

  • pop 段寄存器

  • push 内存单元

  • pop 内存单元

指令执行时,可以只给出内存单元的偏移地址,段地址在指令执行时,CPU从ds中取得。

push和pop指令同mov指令不同,CPU执行mov指令时只需要一步操作,就是传送,而执行push、pop指令时需要两步操作。执行push时,先改变SP,然后向SS:SP处传送。执行pop时,先读取SS:SP的数据,然后改变SP。

栈段

栈段也仅仅是编程时的一种安排。我们可以将长度为N(N<=64K)的组地址连续,起始地址为16的倍数的内存单元当作栈空间来使用。

第一个程序

源程序

伪指令

汇编语言源程序中,包含两种指令,一种是汇编指令,一种是伪指令。汇编指令是有对应的机器码的指令,可以被编译的机器指令,是终为CPU所执行。而伪指令没有对应的机器指令,最终不被CPU所执行。伪指令由编译器来执行,编译器根据伪指令来进行相关的编译工作。

  1. segment和ends是一对成对使用的伪指令,这是在写可被编译的汇编程序时,必须要用到的一对伪指令。它的功能是定义一个段,segment说明段开始,ends说明段结束。使用格式为:
1
2
3
4
段名 segment
.
.
段名 ends

一个汇编程序由多个段组成,这些段被用来存放代码、数据或当作栈空间来使用。

一个有意义的汇编程序中至少有一个段,这个段用来存放代码。

  1. end是一个汇编程序的结束标记,

  2. assume这条伪指令的含义为“假设”。它假设某一段寄存器和程序中的某一个用segments…ends定义的段相关联。通过assume说明这种关联,在情况下,编译程序可以将段寄存器和某一个具体的段联系。assume并不是一条非要深入理解不可的伪指令,以后我们编程时,记着用assume将有特定用途的段和相关的段寄存器关联起来即可。

比如我们用codes segment … codesg ends定义了名为codseg的段。我们在程序开头,用assume cs:codeseg将用作代码段的段codesg和CPU中的段寄存器cs联系起来。

源程序中的“程序”

汇编语言写的源程序,包括伪指令和汇编指令,伪指令由编译器来处理,它们并不实现我们编程的最终目的。我们这里所说的程序是指源程序中最终由计算机执行、处理的指令或数据。

标号

汇编源程序中,除了汇编指令和伪指令外,还有一些标号,如“codesg”。一个标号指代了一个地址。比如codesg在segment的前面,作为一个段的名称,这个段的名称最终将被编译、连接程序处理为一个段的段地址。

程序的结构

源程序由一些段构成。我们可以在这些段中存放代码、数据、或将某个段当作栈空间。基本要素:

1
2
3
4
5
6
7
8
9
assume cs:abc
abc segment
  mov ax,2
  add ax,ax
  add ax,ax

abc ends

end

程序返回

通过下面的两条指令返回:

1
2
mov ax,4c00H
int 21H

语法错误和逻辑错误

编译源程序

编译过程中将得到3个输出文件:目标文件(.obj)、列表文件(.lst)、交叉引用文件(.crf)。目标文件是我们最终要得到的结果,另外两个只是中间结果。

连接器的简单解释

  • 当源程序很大时,可以将它分为多个源程序文件来编译,每个源程序编译成为目标文件后,再用连接程序将它们连接到一起,生成一个可执行文件

  • 程序中调用了某个库文件中的子程序,需要将这个库文件和该程序生成的目标文件连接到一起,生成一个可执行文件

  • 一个源文件编译后,得到了存有机器码的目标文件,目标文件中有些内容还不能直接用来生成可执行文件,连接程序将这些内容处理为最终的可执行信息。所以,在只有一个源程序文件,而又不需要调用某个库中的子程序的情况下,也必须用连接程序对目标文件进行处理,生成可执行文件。

以简化的方式进行编译和连接

编译和连接1.asm

1
2
masm 1;
link 1;

可执行文件中的程序将入内存并运行的原理

DOS中如果用户要执行一个程序,则输入该程序的可执行文件名,command首先根据文件名找到可执行文件,然后将这个可执行文件中的程序加载入内存,设置CS:IP指向程序的入口,此后,command暂停运行,CPU运行程序。程序运行结束后,返回到command中,command再次显示当前盘符和当前路径组成的提示符,等侍用户的输入。

程序执行过程的跟踪

用Debug将程序加载入内存。

DOS中EXE加载的过程。

  • 找到一段起始地址为SA:0000的容量足够的空闲内存区

  • 在这段内存区的前256字节中,创建一个称为程序段的前缀 (PSP)的数据区,DOS要利用PSP来和被加载的程序进行通信。

  • 从PSP的后面将程序装入,程序的地址被设置为SA+10H:0;(空闲的内存区从SA:0开始,0-255字节为PSP,从256字节处开始存放程序,为更好地区分PSP和程序。DOS一般将它们划分到不同的段中,所以,有了这样的地址安排:空闲内存区:SA:0,PSP区:SA:0,程序区SA+10H:0。注意:PSP区和程序区虽然物理地址连接,却有不同的段地址。)

  • 将内存区的段地址存入DS中,初始化其它相关寄存器后,设置CS:IP指向程序入口。

程序加载后,ds中存放着程序所在内存区的段地址,这个内存区的偏移地址为9,则程序所在的内存区的地址为ds:0

这个内存区的前256个字节中存放的是PSP,dos用来和程序进行通信。从256字节处向后的空间存放的是程序。

[bx]和loop指令

  1. [bx]和内存单元的描述

[bx]同样表示一个内存单元,它的偏移地址在bx中。

  1. loop

循环指令

  1. 我们定义的描述性的符号“()”

在下面的内容中,将使用“()”来表示一个寄存器或内存单元中的内容。它所表示的内容有两种类型:字节或字。是哪种类型将由寄存器名或具体的运算决定。

  1. 约定符号idata表示常量

后面,将使用idata表示常量。

[bx]

  • mov ax,[bx]

将bx中存放的数据作为一个偏移地址EA,段地址SA默认在ds中,将SA:EA处的数据送入ax中。

  • mov [bx],ax

bx中存放的数据作为一个偏移地址,段地址SA默认在ds中,将ax中的数据送入SA:EA处。

Loop指令

指令格式是:loop 标号。CPU执行loop指令时,要进行两步操作:

  • (cx)=(cx)-1

  • 判断cx中的值,不为零则转至标号处执行程序,如果为零则向下执行。

在Debug中跟踪用loop指令实现的循环程序

  • 在汇编程序中数据不能以字母开头。所以A000h要写为0A000h。

  • 在Debug中使用G来继续执行程序。或者g 0012之类的指令转到cs:0012处开始跟踪。

Debug和汇编编译器Masm对指令的不同处理

Debug和编译器masm对形如“mov ax,[0]”这类的指令在解释上不同。Debug会将[0]解释为一个内存单元。而0是内存单元的偏移地址;而编译器将[0]就解释为了0。

当前我们将偏移地址送入bx寄存器,用[bx]的方式来访问内存单元。如:

1
2
3
4
mov ax,2000h
mov ds,ax
mov bx,0
mov al,[bx]

如果希望像在Debug中那样,在“[]”中直接给出内存单元的偏移地址。只需要在“[]”的前面显式的给出段地址所在的寄存器。

1
2
3
mov ax,2000h
mov ds,ax
mov al,ds:[0]

段前缀

  1. mov ax,ds:[bx]

  2. mov ax,cs:[bx]

  3. mov ax,ss:[bx]

  4. mov as,es:[bx]

  5. mov ax,ss:[0]

  6. mov ax,cs:[0]

这些出现在访问内存单元的指令中,用于显式地指明内存单元的段地址的“ds:”、“cs”、“ss”或“es”,称为段前缀。

一段安全的空间

在8086模式中,随意向一段内存空间写入内容是很危险的,因为这段空间中可能存放着重要的系统数据或代码。

一般在PC机中,DOS和其他合法程序一般都不会使用0:200~0:300的256个字节的空间。所以我们使用这段空间是安全的。

包含多个段的程序

程序取得所需空间的方法有两种,一是在加载程序的时候为程序分配,再就是程序在执行过程中间向操作系统申请。

若要一个程序在加载的时候取得所需的空间,则必须要在源程序中做出说明。我们通过在源程序中定义段来进行内存空间的获取。

在代码段中使用数据

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
assume cs:code
code segment
  dw 0123H,0456H,0789H,0abcH,0defH,0fedH,0cbaH,0987h
  start:    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

这里代码段定义了一些字型数据。因此代码段的起始位置不再是代码,代码的起始位置由start来标明,代码结束由end start来标明。

在代码段中使用栈

将数据、代码、栈放入不同的段

更灵活的定位内存地址的方法

and和or指令

以字符形式给出数据

1
2
db 'unIX'   相当于 db 75H,6EH,49H,58H
mov al, 'a' 相当于 mov al,61H

[bx+idata]

它的偏移地址为(bx)+idata(bx中的数值加上idata)。也可以写作idata[bx]。

SI和DI

SI和DI是8086CPU中和bx功能相近的寄存器,SI和DI不能够分成两个8位寄存器来使用。

[bx+si]和[bx+di]

这两者的含义相似。表示一个内存单元,它的偏移地址为(bx)+(si),即bx中的数值加上si中的数值。

[bx+si+idata]和[bx+di+idata]

这两者的含义相似。表示一个内存单元,它的偏移地址为(bx)+(si)+idata,即bx中的数值加上si中的数值再加上idata。

不同的寻址方式的灵活应用

  1. [idata]用一个常量来表示地址,可用于直接定位一个内存单元;

  2. [bx]用一个变量来表示内存地址,可用于间接定位一个内存单元;

  3. [bx+idata]用一个变量和常量表示地址,可在一个起始地址的基础上用变量间接定位一个内存单元;

  4. [bx+si]用两个变量表示地址;

  5. [bx+si+idata]用两个变量和一个常量表示地址。

一般来说,在需要暂存数据的时候,我们都应该使用栈。

数据处理的两个基本问题

下面将使用reg来表示寄存器,sreg表示段寄存器。

reg包括:ax,bx,cx,dx,ah,al,bh,bl,ch,cl,dh,dl,sp,bp,si,di

sreg包括:ds,ss,cs,es

bx、si、di、bp

  1. 在8086CPU中,只有这4个寄存器可以用在“[…]”中来进行内存单元的寻址。

  2. 在[…]中,这4个寄存器可以单个出现,或只能以四种组合出现:bx和si、bx和di、bp和si、bp和di。

  3. 只要在[…]中使用寄存器bp,而指令没有显式的给出段地址,段地址默认在ss中。

机器指令处理的数据所在位置

绝大部分机器指令都是进行数据处理的指令,处理大致可分为三类:读取、写入、运算。在机器指令这一层来讲,关不关心数据的值是多少,而关心指令执行前一刻,它将要处理的数据所在的位置。指令在执行前,所要处理的数据可以在三个地方:CPU内部、内存、端口。

汇编语言中数据位置的表达

  • 立即数(idata)

对于直接包含在机器指令中的数据,在汇编指令中直接给出,如:

1
2
3
4
mov ax,1
add bx,2000h
or bx,00010000b
mov al,'a'
  • 寄存器

  • 段地址(SA)和偏移地址(EA)

指令要处理的数据在内存中,在汇编指令中可用[X]的格式给出EA,SA在某个段寄存器中。存放段寄存器可以是默认的,如:

1
2
3
4
5
mov ax,[0]
mov ax,[di]
mov ax,[bx+8]
mov ax,[bx+si]
mov ax[bx+si+8]

等指令,段地址默认在ds中:

1
2
3
4
mov ax,[bp]
mov ax,[bp+8]
mov ax,[bp+si]
mov ax,[bp+si+8]

等指令,段地址默认在ss中。

存放段地址的寄存器也可以显性的给出,如:

1
2
3
4
mov ax,ds:[bp]
mov ax,es:[bx]
mov ax,ss:[bx+si]
mov ax,cs:[bx+si+8]

寻址方式

当数据存放在内在中的时候,我们可以用多种方式来给定这个内存单元的偏移地 址,这种定位内存单元的方法一般被称为寻址方式。

寻址方式 含义 名称 常用格式举例
[idata] EA=idata;SA=(ds) 直接寻址 [idata]
[bx]
[si]
[di]
[bp]
EA=(bx);SA=(ds)
EA=(si);SA=(ds)
EA=(di);SA=(ds)
EA=(bp);SA=(ss)
寄存器相对寻址 用于结构体:
[bx].idata
用于数组:
idata[si],idata[di]
用于二维数组
[bx][idata]