前言

这部分集中在寄存器和内存的角度来访问和使用,CPU 和内存以及寄存器的交互命令。

寄存器

一个典型的 CPU 是由运算器,控制器,寄存器等器件构成的,这些器件由内部总线相连。

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

不同的 CPU ,寄存器的个数,结构是不相同的。8086 CPU有14个寄存器,每个寄存器都有一个名称,分别是:

  • 通用寄存器:AXBXCXDX
  • 变址寄存器:SIDI
  • 指令寄存器:SPBP
  • 指令指针寄存器:IP
  • 段寄存器:CSSSDSES
  • 标志寄存器:PSW
image-20230106012617899

其中 8086 所有寄存器都是 16 位的,这就意味着都可以存储两个字节的数据。

通用寄存器

AX为例,一个 16 位的寄存器存储一个 16 位的数据最大值是 $2^{16}-1$ 。

image-20230106012849579

因为 8086 上一代 CPU 中的寄存器都是 8位 的,这样就会存在 8位的程序没有办法运行在 16位的 8086上,为了解决兼容性的问题,**8086给出的解决方案就是将通用寄存器分为两个独立的 8 位寄存器来使用,即AX可以分为AHAL**(高 8 位和低 8 位)。

image-20230106013550936

从数据的角度来看,我们可以通过十六进制很容易的看出这个寄存器要表示的数据是什么。

image-20230106013607520

由上述即可知,BX也可以分为BHBL,同理CX,DX也是如此。

字在寄存器中的存储

8086 是 16 位CPU,其字长为 16 bit。

一个可以存在一个 16 位的寄存器中,这个字的高位字节存在这个寄存器的高 8 位寄存器,而其低位字节,存在这个寄存器的低 8 位寄存器。

image-20230106014201334

几条汇编指令

汇编指令 操作 高级语言描述
mov ax,18 将 18 送入AX AX = 18
mov ah,78 将 78 送入AH AH = 78
add ax,8 将寄存器中AX中的数值加上 8 AX = AX +8
mov ax,bx 将寄存器BX中的数据送入寄存器AX AX = BX
add ax,bx BXAX中的内容的内容相加,结果存在AX AX = AX + BX

注:汇编指令不区分大小写,上述 18 不指定进制的话,默认是十进制

物理地址

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

我们知道 8086 有 20 位的地址总线,可以传送 20 位的地址,寻址能力为 1M。同时 8086 是 16 位的CPU,意味着它的运算器一次最多可以处理 16 位的数据,寄存器的最大宽度为 16位,在 8086 内部处理的,暂存的,传输的地址也是 16 位的,其寻址能力只有 64KB,这样就与它的地址总线的寻址能产生了冲突。、

为了解决这个寻址能力冲突的问题,8086 给出了它的解决方法,它将两个 16 位地址划分为段地址(基地址)和偏移地址,将两个 16 位的地址合成为一个 20 位的物理地址

地址加法器合成物理地址的方法:$物理地址 = 段地址 \times 16 + 偏移地址$ ,例如:

image-20230106172248311

上述例子左移了一位,因为它是十六进制,所以左移一位就是 4 个二进制比特位

用分段的方式管理内存

8086 CPU 用“$段地址 \times 16 + 偏移地址 = 物理地址$”的方法给出内存的物理地址。需要注意的是:内存实际上并没有分段,段的划分是来自于CPU内部

image-20230106173314679

通一段内存,多种分段方案

例如下图分段:

image-20230106173705951

**它的物理地址(起始地址)为 10000H,那么它的短地址为1000H,大小为100H**。

还可以采用如下的分段方式:

image-20230106173822276

**它的物理地址(起始地址)为10000H10080H,段地址为1000H1008H,大小均为80H**。

当然还有其他的分段方案,其原理也类似相同。不同的分段遵循如下规则:

  1. 段地址 $\times 16$ 必然是 16 的倍数,所以一个段的起始地址也一定是 16的倍数
  2. 偏移地址为 16 位,16位地址的寻址能力为 64K,所以一个段的长度最大为 64K

不同段地址和偏移地址形成同一个物理地址

物理地址 段地址 偏移地址
21F60H 2000H 1F60H
2100H 0F60H
21F0H 0060H

由上述可知同一个物理地址会根据段的划分不同而有不同的表示方法。例如:数据21F60H在内存单元中,段地址是2000H,其表示方法为:

  1. 数据存在内存2000:1F60单元中
  2. 数据存在内存的2000H段中的1F60H单元中

因为段地址很重要,所以 CPU 中专门提供了 4 个段地址寄存器

  • CS:代码段寄存器
  • DS:数据段寄存器
  • SS:栈段寄存器
  • ES:附加段寄存器

CS是英文 Code Segment 的缩写,意思是代码段,其他同理分别为:DataStackExtra

Debug的使用

Debug相信学习过编程的都很了解,它是一种调试程序,在DOS系统中,使用命令debug即可运行,例如:

image-20230106175814665

使用Debug程序,可以查看CPU各种寄存器中的内容,内存的情况,并且在机器指令级跟踪程序的运行。

Debug命令 功能
R 查看,改变CPU寄存器的内容
D 查看内存中的内容
E 改变内存中的内容
U 将内存中的机器指令翻译成汇编指令
A 以汇编指令的格式在内存中写入机器指令
T 执行机器指令

R命令——查看/改变寄存器内容

**R命令改变寄存器的内容,命令格式:r 寄存器名称**。输入完成后,系统会弹出ax当前的值,以及等待你输入要改变的值的内容,改变完成后,使用r命令来查看改变后的ax寄存器的值。

image-20230106182339854

R是寄存器英文 register 的简写

D命令——查看内存内容

  • D命令列出预设地址内存处的 128 个字节的内容,如果连续输入d,则继续向下查看 128 个字节的内容

  • 也可以使用d 段地址:偏移地址来查看你设定的地址的内存的内容。

  • 同样的使用命令d 段地址:偏移地址 结尾偏移地址会列出内存中指定地址范围的内容

image-20230106184817521

D是数据存储空间的英文 dump 的简写

E命令——改变内存内容

  • E 段地址:偏移地址 数据1 数据2 ...用来改变指定地址的内存中的内容
image-20230106185410960
  • E 段地址:偏移地址系统会逐个询问式修改,space(空格)表示继续,enter(回车)表示结束。
image-20230106185547233

E是输入的英文 enter 的简写

U命令——将机器指令翻译成汇编命令

现在有汇编指令如下:

1
2
3
4
mov ax,0123H
mov bx,0003H
mov ax,bx
add ax,bx

其对应的机器码为:

1
2
3
4
B8 23 01
BB 03 00
89 D8
01 D8

现在我们使用E命令将机器码写入,然后使用D命令查看内容,再使用U 段地址:偏移地址命令查看机器码对应的汇编

image-20230106190525880

我们可以把内存中的一段数据,可以看作数据内容,也可以看作命令

U是反汇编英文 unassemble 的简写

A命令——将汇编命令翻译成机器指令

现在我们继续使用上述的汇编指令,现在使用a 段地址:偏移地址来输入汇编指令,将其转换为机器指令存入指定内存。

image-20230106191927894

我们在Debug中默认就是十六进制,所以输入数值就不需要加H表示十六进制了

A是汇编的英文 assemble 的简写

T命令——执行机器指令

T命令执行CS:IP处的指令,我们可以通过修改 IPCS的内容,即位置来让其执行指定位置的指令。

每次输入T命令后逐条执行后面的命令。

image-20230106193618428

T是追踪的英文 trace 的简写

Q命令——退出Debug

使用Q命令退出Debug程序

Q是退出的英文 quit 的简写

CS,IP与代码段

CS(代码段地址)和 IP(指令指针寄存器)其CS:IP指向的内容是 CPU 当作指令来执行的地址。示例:

当前,CS中的内容为2000HIP中的内容是0000H,那么 CPU 通过地址加法器来计算出其 20 位的物理地址,然后在内存中找到物理地址其内容作为机器指令,然后将找到的指令通过数据总线传到 CPU 内的指令缓冲器中,进而由执行控制器进行执行。执行完成后,IP会根据这个指令的内容大小,向下递增。

image-20230109162527783

8086 工作过程简要描述:

  1. CS:IP指向的内存单元读取指令,读取的指令进入指令缓冲器。
  2. IP=IP + 所读取的指令的长度,从而指向下一条指令
  3. 执行指令。转到第一步,重复这个过程。

JMP指令

我们知道 CPU 执行指令取决于 CS:IP指向的内容,所以我们可以通过改变 CS:IP中的内容,来控制 CPU 执行的目标指令

我们可以通过使用Debug程序的R命令来改变寄存器的值,但是需要注意的是这是调试的手段,这并不是程序的方式。我们需要通过指令来修改CSIP的值。

因为 8086 不支持直接使用如下代码修改寄存器的值:

1
2
3
#这是错误的
mov cs 2000H
mov ip 0000H

我们需要使用转移指令jmp来修改CSIP的内容,代码示例:

1
2
3
4
5
6
#同时修改CS,IP的内容,格式:jmp 段地址:偏移地址
jmp 2AE3:3
#仅修改IP的内容,格式jmp 某一合法寄存器
jmp ax
jmp bx
#如上指令类似于 mov IP,ax 和 mpv IP,bx

内存中字的存储

对于 8096 CPU,16位作为一个字。一个 16 位的字,在寄存器中,高 8 位存放高字节,低 8 位存放低字节

image-20230109170524793

那么对于 16 位的字在内存中是如何存储的?内存中的解决方案是,低位字节存在低地址单元,高位字节存在高地址单元。例如4E20H存放在内存中两个的地址0,1中,0 存放低地址20H,1存放高地址4EH

需要注意的是:我们读取数据时高地址+低地址,但是数据的存放地址是低地址开始

这也就是大小端的由来

DS 和 [address]

8086 CPU 从内存中读取数据,需要利用DS寄存器存放要访问的数据的段地址,偏移地址使用[]的形式给出。代码示例:

1
2
3
4
5
6
7
8
9
#将1000:0中的数据读取到al中
mov bx,1000H
mov ds,bx
mov al,[0]

#将al中的数据写入到1000:0
mov bx,1000H
mov ds,bx
mov [0],al

[0] 表示其段地址默认就是DS,其偏移地址就是0,其中要首先把段地址存入DS。另外需要注意:

1
2
3
4
5
6
#这句是错误的,在设计的时候就不支持这种方式
mov ds 1000H

#这句是正确的,即借助通用寄存器来赋值
mov bx,1000H
mov ds,bx

8086 可以一次性传送一个字(16位的数据),同理也是一次写 16 位的数据。

区别每次取的数据大小,可以看取数据要往寄存器哪里放,如果是AX这种通用寄存器,按照 8086 就是 16位的数据;如果是AL或者AH就表示每次自取/写 8位数据。

指令说明

  • mov指令:不支持mov ds,8,即数据不可以直接存放在段寄存器中
  • add指令:不支持add ds,ax,即通用寄存器和段寄存器相加;不支持add [1],[2],即两个内存单元的相加
  • sub指令:(减法指令)同加法

栈以及栈操作的实现

栈是一种特殊的数据结构,它是一种操作受限的线性表,它只允许在一端进行插入或者删除操作。关于栈数据结构的详情可以查看【3.1】栈和队列 ,此处不再过多赘述。

现在的 CPU 中会直接提供栈的设计。8086 支持用栈的方式访问内存空间,基于 8086 CPU编程可以将一段内存当作栈来使用。其指令如下:

  • PUSH(入栈):格式push axax中的数据送入栈中
  • POP(出栈):格式pop ax从栈顶取出数据送入ax

注意:栈的操作是以字为单位对栈进行操作,非字节;即 8086 是 16位为一次数据

现在对于内存 CPU 使用栈来使用,会产生下面两个疑问:

  1. CPU是如何知道一段内存空间被当作栈来使用的?执行pushpop的时候,如何知道哪个单元是栈顶单元?

    8086中存在两个与栈相关的寄存器,即:

    • SS(栈段寄存器):存放栈顶的段地址
    • SP(栈顶指针寄存器):存放栈顶的偏移地址

    即在任何时刻,**SS:SP永远指向栈顶元素**

从汇编的角度,我们可以利用栈来交换两个元素的值,代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#修改栈的位置
mov ax,1000H
mov ss,ax
mov sp,0010H

#元素赋值
mov ax,001AH
mov bx,001BH

#元素入栈
push ax
push bx

#元素出栈
pop ax
pop bx

需要注意的是:使用栈的时候可能会出现栈顶超界的问题,CPU不会进行检查,需要程序人员自己避免。

End

总结来说,汇编的段可以分为三种:

  • 数据段:即将段地址存放在DS中,然后使用mov,add,sub等指令将数据段的内容当作数据来访问
  • 代码段:即将段地址存放在CS中,将段中第一条指令存放在偏移地址IP中,CPU 将会执行我们定义的代码段中的指令
  • 栈段:将段地址存放在SS中,栈顶元素的偏移地址存放在SP中,CPU 进行栈操作的时候会将我们定义的栈段当作栈空间来使用。