计组回顾

地址线和数据线

先通过几道题回顾一下CPU的地址总线宽度和数据总线宽度的意义:

  1. 1GB,1MB,1KB分别是($2^{30}$bit,$2^{20}$bit,$2^{10}=1024$bit)。
  2. 一个CPU的寻址能力为8K,那么它的地址总线宽度为(13)。
  3. 8080,8088,80286,80386的地址总线宽度16根,20根,24根,32根,那么它们的寻址能力分别为($2^{16},2^{20},2^{24},2^{32}$)。
  4. 8080,8088,8086,80286,80386的数据总线宽度8根,8根,16根,16根,32根,则它们一次可以传输的数据为(1B,1B,2B,2B,4B)。
  5. 从内存中读取1024字节的数据,8086至少读取(512)次,80386字少读取(256)次。

从上面的题可以看出,地址总线宽度对应CPU的寻址能力,数据总线宽度对应CPU一次能最多够传输的数据(一根数据线传输1bit)。

存储器

但是,地址线仅仅体现的是CPU的寻址能力,实际的存储单元数还是要看内存的大小:

  1. 1KB的存储器有(1024)个存储单元,编号为(0到1023)。
  2. 1KB的存储器可以存储($2^{13}$)个bit,($2^{10}$)个byte。
  3. 在存储器中,数据和指令以(二进制)的形式存放。

寄存器

CPU由运算器、控制器、寄存器构成。

  1. 运算器进行信息的处理
  2. 控制器控制各种器件的工作
  3. 寄存器进行信息的存储
  4. CPU内部的总线连接各种器件,进行数据的传送。

写汇编代码时,我们需要主要关注CPU中的寄存器。程序员通过改变各种寄存器中的内容来实现对CPU的控制。

不同CPU,寄存器的个数和结构不同。8086CPU的14个寄存器如下:

AX,BX,CX,DX,SI,DI,SP,BP,IP,CS,SS,DS,ES,PSW。

通用寄存器

F4(AX,BX,CX,DX)是最常用的四个寄存器。它们用于存放一般性的数据,所以称为通用寄存器。且有且仅有它们四个可以分为高八位(H)和低八位(L)。

16为寄存器最大能存的数据为$2^{16}-1=65535$。

来看几个和寄存器有关的指令:

image-20220513160918903

指令右边的一般为源操作数,左边的一般为目标操作数或兼源操作数。

用于寄存器只有16位,所以计算的时候要注意溢出的问题:

image-20220513161504341

但是,如果我们的操作仅对寄存器的八位进行操作,当低八位溢出的时候,处理方式如下:

image-20220513161756145

即AL和AH分开使用时是独立的,AL的溢出不会对AH产生影响。

要注意指令的两个操作数位数是要相同的,这就是指令对象的一致性。

物理地址

CPU访问内存单元时,需要通过地址总线给出内存单元的物理地址。

所有内存单元构成的存储空间是一个唯一的线性空间,每一个内存单元在这个空间中都有唯一的地址,称这个地址位内存单元的物理地址。

不同CPU有不同的形成物理地址的方式,以8086为例。

image-20220513162705360

但是,8086的地址线有20根,寻址能力为$2^{20}$,这就与一次性最多操作16位(CPU内部最多产生16位的地址)产生了矛盾。Intel的做法是将一个地址拆分为段地址和偏移地址:

image-20220513163208826

这样使用两个寄存器通过地址加法器就可以获得20位的地址了。加法器中的计算规则为:物理地址=段地址<<4+偏移地址。

总结一下就是,CPU访问内存的时候,用一个基础的地址(段地址)和一个相对于基础地址的偏移地址相加,给出内存单元的物理地址。

可以将段地址<<4看作基础地址。

段的理解

一般的理解是这样的:我们将(段地址<<4)作为内存的基础地址,根据偏移地址在基础地址的基础上找到真实地址。这样就把内存根据段地址分为多个段,每个段最大为$2^{16}=64KB$(16指的是偏移地址的位数,64KB即为其最大值)。

但是,这个理解是片面的。段只是人为的划分,一个段的长度也不是固定的,而是最大为64KB。段的划分是视情况而定的,不唯一,如:

image-20220513165405244

一个物理地址可以有多种段地址和偏移地址的组合。

代码段CS和IP

段地址由段寄存器来装,段地址寄存器有4个:CS、DS、SS、ES。CPU就是通过段寄存器的种类来区分指令和数据的。CS即代码段寄存器,它指定的内存中的二进制串就是指令。

而偏移地址的存储则是比较复杂的。需要记住:当段寄存器为CS的时候,偏移地址寄存器一定为IP。<mark>CS:IP指向的内容就是当前指令执行的位置。</mark>

比如,一段程序的执行过程如下:

image-20220513171155911

  1. 先根据CS和IP定位到当前需要执行的指令的地址,到内存中取出指令,送入指令缓冲器中处理。
  2. IP加上刚才处理的指令的长度(CISC的不同指令长度不同),然后根据现在的CS和IP取出下一条需要执行的指令。
  3. ……

但是,处于安全性的考虑,mov是不能改变CS和IP的值的,这时需要jmp指令:

image-20220513171937634

如果jmp后面只有一个数,那么只改变IP。当然,立即数也可以改成另一个寄存器。

数据段DS

首先要知道8086CPU中一个字就是两个字节,高八位存放高位字节,低八位存放低位字节,所以一个字需要用两个存储单元来存放。对于小端机,低位字节放在低地址单元中,高地址放在高地址单元中。

DS用于存放要访问数据的段地址。

比如,对于程序:

1
2
mov ds,1000H
mov al,[0]

首先将数据段的段地址设置为1000H,下一条指令 mov al,[0]mov al,ds:[0]的省略,表示将数据段偏移地址为0的数据赋值给al。

如果[]前给出了具体的寄存器,就以那个寄存器为准;如果缺省则默认为ds。

知道了这个后,最后再来总结一下mov的操作数:

image-20220513185525457

栈与SS:SP

栈的操作

8086CPU提供了出栈和入栈的指令,这两个指令都是以一个字作为操作数的,比如只能为ax而不能用ah或al。

push ax表示将寄存器ax中的数据放入栈中,pop ax表示将栈顶数据取出并送入ax中。需要注意入栈和出栈一次操作的都是两个字节。

image-20220513190350122

内存中的栈是从高地址王低地址增长的,需要注意高位字节需要放到高位地址,低位字节放到低位地址。

image-20220513190455143

出栈后,实现了数据的逆序(高位字节和低位字节)

SS与SP

push和pop指令都需要知道当前栈顶的地址。和CS:IP一样,栈通过SS:SP来标志当前的栈顶元素——即任意时刻,SS:SP指向栈顶元素。

下面分析入栈和出栈SS和SP进行的操作:

  1. push ax:首先改变sp,使sp=sp-2,以当前栈顶作为新的栈顶;然后将ax中的数据送入当前SS:SP指向的地址(栈顶)。
  2. pop ax:和push的顺序相反,先将栈顶的数据送入ax,再使sp+=2作为新的栈顶。

编写汇编源程序

先要明白几个概念:

  1. 汇编语言
  2. 汇编语言源程序
  3. 汇编程序:将汇编语言翻译为机器语言的目标程序的工具软件,即编译器
  4. 汇编:动词,将汇编语言翻译为机器语言的过程

步骤

  1. 使用编辑器编写汇编语言源程序。
  2. 使用masm编译源程序,产生目标文件 .obj文件。
  3. 链接,生成可执行文件。其中可执行文件包含”程序”和”数据”以及相关的描述信息。
  4. 执行可执行文件。因为16位的程序不能在现在的电脑上运行,所以需要用到dosbox。
  5. 调试。

源程序示例

1
2
3
4
5
6
7
8
9
10
11
12
assume cs:codesg

codesg segment
mov ax,0123H
mov bx,0456H
add ax,bx

mov ax,4c00H
int 21H
codesg ends

end

可以把这个程序分为几个部分:

  1. 指令:4~9行的内容,其中8~9行是每个代码段结尾都需要加的,用于标志结尾。int不是整型而是中断的意思。
  2. 伪指令:上面的程序中有三种伪指令 assumexxx segment……xxx endsend
    1. xxx segment……xxx表示中间的部分为一个段,xxx为标号,用于标识一个段。标号是人为命名的。
    2. assume用于将我们定义的段与段寄存器挂钩。assume就是“令”的意思。
    3. end标志整个程序的结束,一般是 end start

实操

现在在dosbox里面来实操运行一下上面的源程序。

  1. 打开dosbox,将masm编译器所在目录挂载为虚拟磁盘c:盘(A盘、B盘也是可以的)
  2. 编译demo.asm:masm demo.asm,用于不需要lst和crf文件,所以字节回车即可。

    image-20220514114409963

  3. 链接,生成可执行文件。用于现在只有 demo.obj做一个文件,所以直接 link demo.obj即可。如果要实现一些高级功能,如输入输出,就还需要链接其他的库。

    image-20220514114658226

  4. 直接执行的话什么也看不到,所以通过调试来看程序的运行:

    image-20220514114805665

    每一行输入一个调试命令,t表示执行程序的下一条指令。可以看到,寄存器内容的变化和程序指定的是一样的。

总结一下整个流程:

image-20220514113406482

reference