【2.0】访问寄存器和内存
前言
这部分集中在寄存器和内存的角度来访问和使用,CPU 和内存以及寄存器的交互命令。
寄存器
一个典型的 CPU 是由运算器,控制器,寄存器等器件构成的,这些器件由内部总线相连。
对于汇编程序员来说,CPU 中的主要部件就是寄存器。寄存器是 CPU 中程序员可以用指令读写的部件。程序员通过改变寄存器中的内容来实现对 CPU 的控制。
不同的 CPU ,寄存器的个数,结构是不相同的。8086 CPU有14个寄存器,每个寄存器都有一个名称,分别是:
- 通用寄存器:
AX
,BX
,CX
,DX
- 变址寄存器:
SI
,DI
- 指令寄存器:
SP
,BP
- 指令指针寄存器:
IP
- 段寄存器:
CS
,SS
,DS
,ES
- 标志寄存器:
PSW
其中 8086 所有寄存器都是 16 位的,这就意味着都可以存储两个字节的数据。
通用寄存器
以AX
为例,一个 16 位的寄存器存储一个 16 位的数据最大值是 $2^{16}-1$ 。
因为 8086 上一代 CPU 中的寄存器都是 8位 的,这样就会存在 8位的程序没有办法运行在 16位的 8086上,为了解决兼容性的问题,**8086给出的解决方案就是将通用寄存器分为两个独立的 8 位寄存器来使用,即AX
可以分为AH
和AL
**(高 8 位和低 8 位)。
从数据的角度来看,我们可以通过十六进制很容易的看出这个寄存器要表示的数据是什么。
由上述即可知,BX
也可以分为BH
和BL
,同理CX
,DX
也是如此。
字在寄存器中的存储
8086 是 16 位CPU,其字长为 16 bit。
一个字可以存在一个 16 位的寄存器中,这个字的高位字节存在这个寄存器的高 8 位寄存器,而其低位字节,存在这个寄存器的低 8 位寄存器。
几条汇编指令
汇编指令 | 操作 | 高级语言描述 |
---|---|---|
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 |
将BX 和AX 中的内容的内容相加,结果存在AX 中 |
AX = AX + BX |
注:汇编指令不区分大小写,上述 18 不指定进制的话,默认是十进制
物理地址
CPU 访问内存单元时要给出内存单元的地址,所有的内存单元构成的存储空间是一个一维的线性空间,每个存储单元在这个空间中有一个唯一的地址,这个唯一的的地址的就是物理地址。
我们知道 8086 有 20 位的地址总线,可以传送 20 位的地址,寻址能力为 1M。同时 8086 是 16 位的CPU,意味着它的运算器一次最多可以处理 16 位的数据,寄存器的最大宽度为 16位,在 8086 内部处理的,暂存的,传输的地址也是 16 位的,其寻址能力只有 64KB,这样就与它的地址总线的寻址能产生了冲突。、
为了解决这个寻址能力冲突的问题,8086 给出了它的解决方法,它将两个 16 位地址划分为段地址(基地址)和偏移地址,将两个 16 位的地址合成为一个 20 位的物理地址。
地址加法器合成物理地址的方法:$物理地址 = 段地址 \times 16 + 偏移地址$ ,例如:
上述例子左移了一位,因为它是十六进制,所以左移一位就是 4 个二进制比特位
用分段的方式管理内存
8086 CPU 用“$段地址 \times 16 + 偏移地址 = 物理地址$”的方法给出内存的物理地址。需要注意的是:内存实际上并没有分段,段的划分是来自于CPU内部。
通一段内存,多种分段方案
例如下图分段:
**它的物理地址(起始地址)为 10000H
,那么它的短地址为1000H
,大小为100H
**。
还可以采用如下的分段方式:
**它的物理地址(起始地址)为10000H
和10080H
,段地址为1000H
和1008H
,大小均为80H
**。
当然还有其他的分段方案,其原理也类似相同。不同的分段遵循如下规则:
- 段地址 $\times 16$ 必然是 16 的倍数,所以一个段的起始地址也一定是 16的倍数
- 偏移地址为 16 位,16位地址的寻址能力为 64K,所以一个段的长度最大为 64K
不同段地址和偏移地址形成同一个物理地址
物理地址 | 段地址 | 偏移地址 |
---|---|---|
21F60H |
2000H |
1F60H |
2100H |
0F60H |
|
21F0H |
0060H |
由上述可知同一个物理地址会根据段的划分不同而有不同的表示方法。例如:数据21F60H
在内存单元中,段地址是2000H
,其表示方法为:
- 数据存在内存
2000:1F60
单元中 - 数据存在内存的
2000H
段中的1F60H
单元中
因为段地址很重要,所以 CPU 中专门提供了 4 个段地址寄存器:
CS
:代码段寄存器DS
:数据段寄存器SS
:栈段寄存器ES
:附加段寄存器
CS
是英文 Code Segment 的缩写,意思是代码段,其他同理分别为:Data,Stack,Extra
Debug的使用
Debug相信学习过编程的都很了解,它是一种调试程序,在DOS系统中,使用命令debug
即可运行,例如:
使用Debug程序,可以查看CPU各种寄存器中的内容,内存的情况,并且在机器指令级跟踪程序的运行。
Debug 命令 |
功能 |
---|---|
R |
查看,改变CPU寄存器的内容 |
D |
查看内存中的内容 |
E |
改变内存中的内容 |
U |
将内存中的机器指令翻译成汇编指令 |
A |
以汇编指令的格式在内存中写入机器指令 |
T |
执行机器指令 |
R
命令——查看/改变寄存器内容
**R
命令改变寄存器的内容,命令格式:r 寄存器名称
**。输入完成后,系统会弹出ax
当前的值,以及等待你输入要改变的值的内容,改变完成后,使用r
命令来查看改变后的ax
寄存器的值。
R
是寄存器英文 register 的简写
D
命令——查看内存内容
D
命令列出预设地址内存处的 128 个字节的内容,如果连续输入d
,则继续向下查看 128 个字节的内容也可以使用
d 段地址:偏移地址
来查看你设定的地址的内存的内容。同样的使用命令
d 段地址:偏移地址 结尾偏移地址
会列出内存中指定地址范围的内容
D
是数据存储空间的英文 dump 的简写
E
命令——改变内存内容
E 段地址:偏移地址 数据1 数据2 ...
用来改变指定地址的内存中的内容
E 段地址:偏移地址
系统会逐个询问式修改,space(空格)表示继续,enter(回车)表示结束。
E
是输入的英文 enter 的简写
U
命令——将机器指令翻译成汇编命令
现在有汇编指令如下:
1 | mov ax,0123H |
其对应的机器码为:
1 | B8 23 01 |
现在我们使用E
命令将机器码写入,然后使用D
命令查看内容,再使用U 段地址:偏移地址
命令查看机器码对应的汇编。
我们可以把内存中的一段数据,可以看作数据内容,也可以看作命令
U
是反汇编英文 unassemble 的简写
A
命令——将汇编命令翻译成机器指令
现在我们继续使用上述的汇编指令,现在使用a 段地址:偏移地址
来输入汇编指令,将其转换为机器指令存入指定内存。
我们在Debug中默认就是十六进制,所以输入数值就不需要加
H
表示十六进制了
A
是汇编的英文 assemble 的简写
T
命令——执行机器指令
T
命令执行CS:IP
处的指令,我们可以通过修改 IP
和CS
的内容,即位置来让其执行指定位置的指令。
每次输入T
命令后逐条执行后面的命令。
T
是追踪的英文 trace 的简写
Q
命令——退出Debug
使用Q
命令退出Debug程序。
Q
是退出的英文 quit 的简写
CS,IP与代码段
CS(代码段地址)和 IP(指令指针寄存器)其CS:IP
指向的内容是 CPU 当作指令来执行的地址。示例:
当前,CS
中的内容为2000H
,IP
中的内容是0000H
,那么 CPU 通过地址加法器来计算出其 20 位的物理地址,然后在内存中找到物理地址其内容作为机器指令,然后将找到的指令通过数据总线传到 CPU 内的指令缓冲器中,进而由执行控制器进行执行。执行完成后,IP
会根据这个指令的内容大小,向下递增。
8086 工作过程简要描述:
- 从
CS:IP
指向的内存单元读取指令,读取的指令进入指令缓冲器。 IP
=IP
+ 所读取的指令的长度,从而指向下一条指令- 执行指令。转到第一步,重复这个过程。
JMP指令
我们知道 CPU 执行指令取决于 CS:IP
指向的内容,所以我们可以通过改变 CS:IP
中的内容,来控制 CPU 执行的目标指令。
我们可以通过使用Debug
程序的R
命令来改变寄存器的值,但是需要注意的是这是调试的手段,这并不是程序的方式。我们需要通过指令来修改CS
和IP
的值。
因为 8086 不支持直接使用如下代码修改寄存器的值:
1 | #这是错误的 |
我们需要使用转移指令jmp
来修改CS
和IP
的内容,代码示例:
1 | #同时修改CS,IP的内容,格式:jmp 段地址:偏移地址 |
内存中字的存储
对于 8096 CPU,16位作为一个字。一个 16 位的字,在寄存器中,高 8 位存放高字节,低 8 位存放低字节。
那么对于 16 位的字在内存中是如何存储的?内存中的解决方案是,低位字节存在低地址单元,高位字节存在高地址单元。例如4E20H
存放在内存中两个的地址0,1中,0 存放低地址20H
,1存放高地址4EH
。
需要注意的是:我们读取数据时高地址+低地址,但是数据的存放地址是低地址开始
这也就是大小端的由来
DS 和 [address]
8086 CPU 从内存中读取数据,需要利用DS
寄存器存放要访问的数据的段地址,偏移地址使用[]
的形式给出。代码示例:
1 | #将1000:0中的数据读取到al中 |
用[0]
表示其段地址默认就是DS
,其偏移地址就是0
,其中要首先把段地址存入DS
。另外需要注意:
1 | #这句是错误的,在设计的时候就不支持这种方式 |
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 ax
,将ax
中的数据送入栈中POP
(出栈):格式pop ax
,从栈顶取出数据送入ax
注意:栈的操作是以字为单位对栈进行操作,非字节;即 8086 是 16位为一次数据
现在对于内存 CPU 使用栈来使用,会产生下面两个疑问:
CPU是如何知道一段内存空间被当作栈来使用的?执行
push
和pop
的时候,如何知道哪个单元是栈顶单元?8086中存在两个与栈相关的寄存器,即:
SS
(栈段寄存器):存放栈顶的段地址SP
(栈顶指针寄存器):存放栈顶的偏移地址
即在任何时刻,**
SS:SP
永远指向栈顶元素**
从汇编的角度,我们可以利用栈来交换两个元素的值,代码示例:
1 | #修改栈的位置 |
需要注意的是:使用栈的时候可能会出现栈顶超界的问题,CPU不会进行检查,需要程序人员自己避免。
End
总结来说,汇编的段可以分为三种:
- 数据段:即将段地址存放在
DS
中,然后使用mov
,add
,sub
等指令将数据段的内容当作数据来访问 - 代码段:即将段地址存放在
CS
中,将段中第一条指令存放在偏移地址IP
中,CPU 将会执行我们定义的代码段中的指令 - 栈段:将段地址存放在
SS
中,栈顶元素的偏移地址存放在SP
中,CPU 进行栈操作的时候会将我们定义的栈段当作栈空间来使用。