一、寄存器及数据存储
寄存器是CPU
内部的信息存储单元,在8086CPU
中有14
个寄存器:
- 通用寄存器:
AX、BX、CX、DX
- 变址寄存器:
SI、DI
- 指针寄存器:
SP、BP
- 指令指针寄存器:
IP
- 段寄存器:
CS、SS、DS、ES
- 标志寄存器:
PSW
共性:8086CPU
中所有的寄存器都是16位
的,可以存放两个字节
1.1 通用寄存器(以AX为例)
1.1.1 对数据的存储
一个16
位寄存器存储一个16
位的数据,能存储的数据的最大值就是寄存器所有位都取1
的时候,此时值为2 ^ 16 - 1
,即65535D
;转成十六进制也就是FFFFH
。如下图所示:
例:在AX
中存储18D
,十六进制为12H
,二进制为10010B
,如下图所示
在AX
中存储20000D
,十六进制为4E20H
,二进制为0100111000100000B
,如下图所示!!]!
1.1.2 保证兼容性
8086CPU
的上一代CPU8088
的寄存器都是8
位的,为保证8086
的程序在原有8088
的平台上能兼容,在设计16
位通用寄存器时,会保证通用寄存器均可以分为两个独立的8
位寄存器使用。
AX
可以分为AH
和AL
,如下图所示
同样的:
BX
可以分为BH
和BL
CX
可以分为CH
和CL
DX
可以分为DH
和DL
在描述存储的数据时,用十六进制可以直观地看出高八位和低八位的组成,如下表所示:
寄存器 | 寄存器中的数据 | 所表示的值 |
---|---|---|
AX | 0100111000100000 | 20000(4E20H) |
AH | 01001110 | 78(4EH) |
AL | 00100000 | 32(20H) |
1.2 “字”在寄存器中的存储
8086
是16
位的CPU
,其字长(Word size)
为16bit
,一个字是由16
位构成的,占2
字节(1
字节永远是8bit
,N
位CPU
的字长为N bit
,字由N
位构成)
一个字(Word)
可以存在一个16
位的寄存器中,这个字的高位字节存在这个寄存器的高8位寄存器,这个字的低位字节存在这个寄存器的低8位寄存器
二、mov和add指令
2.1 指令简述
- mov 赋值操作
- add 相加运算
这两个指令很简单,如下表所示(汇编指令不区分大小写):
2.2 溢出问题
在8066
里面,溢出的位是会被寄存器丢失的(只是寄存器读不到,CPU并没有真正丢弃进位值,这个在后面再简述,下面的丢失同理),如执行下列程序段:
MOV AX, 0
MOV BX, 0
MOV AX, 8226H
ADD BX, AX
ADD AX, BX
执行ADD AX, BX
时AX
寄存器的值是8226H + 8226H = 1044CH
,但是由于AX
只有16
位,所以最高位的1
是会被丢失的,所以AX
寄存器的最终结果是044CH
如前面所说,通用寄存器的高位和低位是可以独立操作的,在对高八位和第八位独立操作时,如果遇到溢出也是会丢失进位值,低位寄存器不会向高位寄存器进位,如下列程序段:
MOV AX, 0
MOV AL, 82H
MOV BH, 82H
ADD AL, BH
由于82H + 82H = 104H
,但是AL
只有8
位,并不会向AH
进位,所以最终AL
中的数据时04H
,AX
中的数据也是04H
而不是104H
2.3 实战
要求:只用mov和add指令,用不超过四行的代码求2的4次方的值
我们可以先列个表
DEC | HEX | BIN |
---|---|---|
2 | 2H | 0010B |
4 | 4H | 0100B |
8 | 8H | 1000B |
16 | 10H | 00010000B |
我们可以发现每乘个2,就相当于二进制数向高位位移一次(进位),我们计算2的4次方只需要不断相加就行了,如下参考代码
MOV AX, 02H ;AX = 00000010B
ADD AX, AX ;AX = 00000100B
ADD AX, AX ;AX = 00001000B
ADD AX, AX ;AX = 00010000B
三、物理地址与分段管理
CPU在访问内存单元的时候需要给出内存单元的地址,所有的内存单元构成的存储空间是一个一维的线性空间,每一个内存单元在这个空间中都有唯一的地址,这个唯一的地址叫做物理地址,如下图所示
3.1 两个事实引出的矛盾
- 8086有20位地址总线,可传送20位地址,寻址能力为1M
- 8086是16位的CPU,运算器一次最多可以处理16位的数据,寄存器的最大宽度为16位。在8086内部处理、传输、暂存的地址也是16位,寻址能力也只有64KB
矛盾:8086如何处理在寻址空间上的这个矛盾?
3.2 8086给出物理地址的方法
8086CPU的地址加法器中将两个16位的地址(段地址、偏移地址)合成一个20位的物理地址,相关部件的逻辑结构如下图所示:
在地址加法器中做的运算为:
物理地址 = 段地址 * 16 + 偏移地址
如下图所示:
例:段地址为1230H
,偏移地址为00C8H
那么其物理地址为12300H + 00C8H = 123C8H
- M进制数向高位移N位(低位自动补零)代表这个数乘M的N次幂
3.3 段地址与偏移地址不唯一
当8086CPU访问一个内存单元中的地址时,其段地址与偏移地址并不是唯一的。也就是说CPU可以用不同的段地址和偏移地址形成同一个物理地址。如下所示,对于同一个物理地址21F60H
:
物理地址 | 段地址 | 偏移地址 |
---|---|---|
21F60H | 2000H | 1F60H |
21F60H | 2100H | 0F60H |
21F60H | 21F0H | 0060H |
21F60H | 21F6H | 0000H |
21F60H | 1F00H | 2F60H |
3.4 用分段的方式管理内存
内存并没有分段,段的划分来自CPU,我们在编程时可以根据需要,将若干地址连续的内存单元看做一个段,用段地址*16定位段的起始地址(基础地址),用偏移地址定位段中的内存单元。注意以下两点:
- 段地址*16必然是16的倍数,所以一个段的起始地址也一定是16的倍数
- 偏移地址为16位,变化范围为0~FFFFH,16位地址的寻址能力为64KB,所以一个段的最大长度为64KB
- 例:给定段地址2000H,则用偏移地址寻址的访问时20000H~2FFFFH,共64KB
3.5 对内存单元的描述
在8086PC机中,一般不会直接取说明内存单元的物理地址,比如说数据A在21F60H内存单元中通常用以下两种方式描述:
- 数据A存在内存2000:1F60单元中
- 数据存在内存的2000段中的1F60单元中
3.6 段寄存器
8086CPU在访问内存时需要由相关部件提供内存单元的段地址和偏移地址,从而送入地址加法器中合成物理地址。其中8086CPU中有4个专门的段寄存器存放段地址,分别是:
- CS:代码段寄存器
- DS:数据段寄存器
- SS:栈段寄存器
- ES:附加段寄存器
四、Debug的使用
4.1 用R命令查看、改变CPU寄存器的内容
R
(查看寄存器内容)R <寄存器名>
(改变指定寄存器的内容)
如,查看寄存器内容并将AX寄存器中的值改为1234:
4.2 用D命令查看内存中的内容
D
(列出预设地址内存处的128字节的内容)D <段地址:偏移地址>
(列出内存中指定地址处的内容)D <段地址:偏移地址 结尾偏移地址>
(列出内存中指定地址范围中的内容)
如下图所示:
4.3 用E命令改变内存中的内容
E <段地址:偏移地址 data1 data2 ...>
(依次写入数据1、数据2…(十六进制))E <段地址:偏移地址>
(逐个询问式修改)
<空格>
:接受并继续修改下一个<回车>
:结束修改
如下图所示:
4.4 用U命令将内存中的机器指令翻译成汇编指令
U <段地址:偏移地址>
(查看汇编代码)
如先将下述机器码写入到从地址2000:0000开始的一段内存中,最后用U
指令查看
B8 23 01
BB 03 00
89 D8
01 D8
我们可以看出其对应的汇编指令为:
MOV AX, 0123H
M0V BX, 0003H
MOV AX, BX
ADD AX, BX
4.5 用A命令以汇编指令的格式在内存中写入机器指令
A <段地址:偏移地址>
(写入汇编指令)
还是写入上面的那段指令,我们可以看到,机器码和汇编指令是一一对应的,如图所示:
4.6 用T命令执行机器指令
T
(单步执行CS:IP处的指令)
如图所示
每执行依据之后在最后面都会注明下一段内存中的程序,执行结果在寄存器中一目了然。
4.7 用Q命令退出Debug
五、CS与代码段及JMP指令
5.1 CS、IP简述
CS
:代码段寄存器IP
:指令指针寄存器
CS:IP
:CPU将内存中CS:IP指向的内容当做指令执行
5.2 8086PC工作过程
- 从CS:IP指向的内存单元中读取指令,读取的指令进入指令缓冲器
- IP = IP + 所读取的指令的长度,从而指向下一条指令
- 执行指令,转到步骤1,重复执行
5.3 jmp指令
8086CPU执行何处的指令,取决于CS:IP
的值,我们可以通过改变CS:IP
的内容来控制CPU要执行的目标。下面是一些看似可行的改变CS:IP
的方法
- Debug中的R命令可以改变寄存器的值
R SC
,R IP
.但是Debug是调试手段,并不是程序方式- 用MOV指令?先看测试:
我们可以发现以下语句都是ERROR
MOV CS,2000
MOV IP,0
这是因为8086不提供修改CS和IP的指令,下面这句没有报ERROR:
MOV AX, 2000
MOV CS, AX
这在语法上确实是可行的,但是一般不会这样使用,我们更希望有更高级的机制去完成这一特定的功能。到MOV IP,AX
又卡壳了,因为IP是自动增长的。那到底如何控制CPU执行的目标呢?其实有一个jmp
转移指令
JMP <段地址:偏移地址>
(用指令中给出的段地址修改CS,用偏移地址修改IP)JMP <某一合法寄存器>
(仅修改IP的内容)
5.4 问题分析:CPU运行的流程
如上图,从20000H开始,执行的序列依次是
MOV AX, 6622H
JMP 1000:3
MOV AX, 0000
MOV BX, AX
JMP BX
MOV AX, 0123H
MOV AX, 0000
;接下来就一直在10000H到10009H中死循环
以下我们到Debug里面去跟踪测试一下,先将机器码输入,如图:
从20000H处开始运行:如图所示,可以很清楚得看到CS和IP的跳变
六、内存中字的存储
对8086CPU,16位作为一个字。前面我们知道,字在16位寄存器中的存储是高8位放在高字节,低8位放在低字节。但是内存是连续分布的,16位的字在内存中需要两个连续的字节来储存。
6.1 小端储存
Intel公司采用的是小端模式来储存,即低位字节存在低地址单元,高位字节存放在高地址单元。
假设要将4E20H存放在0、1两个单元,则4EH是存放在单元1中,20H存放在单元0中;
将0012H存放在2、3两个单元,则将00H存放在单元3中,12H存放在单元2中,如下图所示:
6.2 8086中的字单元
由两个地址连续的内存单元组成,存放一个字型数据(16位)。在一个字单元中,低地址单元存放低位字节,高地址单元存放高位字节。
如6.1中的图:
- 在起始地址为0的单元中,存放的是4E20H
- 在起始地址为2的单元中,存放的是0012H
注意区分字节型数据和字型数据
- 0地址单元中存放的字节型数据是20H
- 0地址单元中存放的字型数据是4E20H
七、DS与数据段
7.1 用DS和[address]实现字的传送
CPU要读取内存单元的时候,必须先给出这个内存单元的地址,在8086PC中,内存地址由段地址和偏移地址组成,在写汇编指令的时候,用DS
寄存器存放要访问的数据的段地址,偏移地址用[address]
的形式直接给出,如下所示:
7.1.1 读取操作
将10000H(1000:0)中的数据读到AL中:
MOV BX, 1000H
MOV DS, BX
MOV AL, [0]
7.1.2 写入操作
将AL中的数据写到10000H中
MOV BX, 1000H
MOV DS, BX
MOV [0], AL
7.1.3 注意
- 8086CPU不支持直接将数据直接送入段寄存器(硬件设计的问题),所以都是先将数据写入通用寄存器,再通过通用寄存器写入到段寄存器中
MOV
指令访问内存单元时可以只给出单元的偏移地址,此时段地址默认在DS
寄存器中
7.1.4 字的传送
8086CPU可以一次传动一个字(16位的数据)
MOV BX, 1000H
MOV DS, BX
MOV AX, [0] ;1000:0处的字型数据送入AX
MOV [0], CX ;CX中的16位数据送到10000H处
7.2 DS与数据段
在8086PC机中,可以根据需要将一组内存单元定义为一个段,将哪段内存当做数据段、段地址如何定,都是由程序员在编程时自己安排的:
将一组长度为N(N≤64K)、地址连续、起始地址为16的倍数的内存单元当做专门储存数据的内存空间,从而就定义了一个数据段
如下所示,将123B0H~123BAH的内存单元定义为数据段:
MOV AX, 123BH
MOV DS, AX
累加该数据段中的前三个单元的数据
MOV AL, 0
MOV AL, [0]
MOV AL, [1]
MOV AL, [2]
累加该数据段中的前三个字型数据
MOV AX, 0
MOV AX, [0]
MOV AX, [2]
MOV AX, [4]
7.3 小结
MOV、ADD/SUB
是具有两个操作对象的指令,访问内存中的数据段JMP
是具有一个操作对象的指令,对应内存中的代码段
八、栈操作的实现
8.1 CPU提供的栈机制
现今的CPU都有栈的设计,8086CPU提供相关的指令,支持用栈的方式访问内存空间,基于8086的CPU编程,可以将一段内存当做栈来实现:
- PUSH :入栈/压栈
- POP : 出栈/弹栈
例:
PUSH AX ;将AX中的数据送入栈中
POP AX ;从栈顶取出数据送入AX中
需要注意的是,这里都是以字为单位对栈进行操作
8.2 与栈相关的寄存器
- 栈段寄存器
SS:
存放栈顶的段地址- 栈顶指针寄存器
SP
: 存放栈顶的偏移地址
任意时刻,SS:SP都是指向栈顶元素,SP决定了栈的大小,如以下定义:
MOV AX, 1000H
MOV SS, AX
MOV SP, 000FH
则栈为物理地址为10000H~1000FH的一段内存空间,如下所示:
8.3 栈的操作示例
MOV AX, 1000H
MOV SS, AX
MOV SP, 0010H
MOV AX, 001AH
MOV BX, 001BH
PUSH AX
PUSH BX
POP AX
POP BX
代码执行时寄存器及栈的状态如下图所示
我们可以发现,通过栈的机制,AX和BX的数值发生的交换
8.4 PUSH和POP的执行过程
PUSH AX
SP = SP - 2
- 将
AX
中的内容送入SS:SP
指向的内存单元处,SS:SP
此时指向新栈顶
POP AX
- 将
SS:SP
指向的内存单元处的数据送入AX
中SP = SP + 2
,SS:SP
指向当前栈顶下面的单元,以当前栈顶下面的单元为新的栈顶
PUSH和POP实质上就是一种内存传送指令,可以在寄存器和内存之间传递数据,与MOV指令不同的是,PUSH和POP指令访问的内存单元的地址不是在指令中给出的,而是由SS:SP指出的
8.5 爆栈
出现以下两种情况会发生栈顶超界的问题:
- 栈满的时候再使用
PUSH
指令入栈- 栈空的时候再使用
POP
指令出栈
对于8086这种低端的CPU,只知道栈顶的位置,并不知道程序安排的栈空间的大小,所以不会对程序中是否爆栈做出检查。程序员在编程的时候要根据可能用到的最大栈空间来安排栈的大小。
九、段的总结
9.1 三种段
数据段
- 将段地址放在
DS
中- 用
MOV/ADD/SUB
等访问内存单元的指令是,CPU将我们定义的数据段中的内容当做数据来访问- 无法明确得定义出数据段的大小
代码段
- 将段地址放在
CS
中,将段中的第一条指令的偏移地址放在IP
中- CPU将代码段的内容当在指令来执行
栈段
- 将段地址放在
SS
中,将栈顶单元的偏移地址放在SP中- CPU在序号执行栈操作(
PUSH/POP
)时,将定义的栈段当做栈空间来使用- 栈一旦定义,其大小就确定了