通俗易懂和你聊聊寄存器那些事(精美图文)

下面我们就来介绍一下关于寄存器的相关内容。我们知道,寄存器是CPU内部的构造,它主要用于信息的存储。除此之外,CPU内部还有运算器,负责处理数据;控制器控制其他组件;外部总线连接CPU和各种部件,进行数据传输;内部总线负责CPU内部各种组件的数据处理。

为什么会出现寄存器?因为我们知道,程序在内存中装载,由CPU来运行,CPU的主要职责就是用来处理数据。那么这个过程势必涉及到从存储器中读取和写入数据,因为它涉及通过控制总线发送数据请求并进入存储器存储单元,通过同一通道获取数据,这个过程非常的繁琐并且会涉及到大量的内存占用,而且有一些常用的内存页存在,其实是没有必要的,因此出现了寄存器,存储在CPU内部。

认识寄存器

寄存器的官方叫法有很多,Wiki上面的叫法是ProcessingRegister,也可以称为CPURegister,计算机中经常有一个东西多种叫法的情况,反正你知道都说的是寄存器就可以了。

认识寄存器之前,我们首先先来看一下CPU内部的构造。


CPU从逻辑上可以分为3个模块,分别是控制单元、运算单元和存储单元,这三部分由CPU内部总线连接起来。

几乎所有的冯·诺伊曼型计算机的CPU,其工作都可以分为5个阶段:取指令、指令译码、执行指令、访存取数、结果写回。

取指令阶段是将内存中的指令读取到CPU中寄存器的过程,程序寄存器用于存储下一条指令所在的地址

指令译码阶段,在取指令完成后,立马进入指令译码阶段,在指令译码阶段,指令译码器按照预定的指令格式,对取回的指令进行拆分和解释,识别区分出不同的指令类别以及各种获取操作数的方法。

执行指令阶段,译码完成后,就需要执行这一条指令了,此阶段的任务是完成指令所规定的各种操作,具体实现指令的功能。

访问取数阶段,根据指令的需要,有可能需要从内存中提取数据,此阶段的任务是:根据指令地址码,得到操作数在主存中的地址,并从主存中读取该操作数用于运算。

结果写回阶段,作为最后一个阶段,结果写回(WriteBack,WB)阶段把执行指令阶段的运行结果数据写回到CPU的内部寄存器中,以便被后续的指令快速地存取;

计算机架构中的寄存器

寄存器是一块速度非常快的计算机内存,下面是现代计算机中具有存储功能的部件比对,可以看到,寄存器的速度是最快的,同时也是造价最高昂的。


我们以intel8086处理器为例来进行探讨,8086处理器是x86架构的前身。在8086后面又衍生出来了8088。

在8086CPU中,地址总线达到20根,因此最大寻址能力是2^20次幂也就是1MB的寻址能力,8088也是如此。

在8086架构中,所有的内部寄存器、内部以及外部总线都是16位宽,可以存储两个字节,因为是完全的16位微处理器。8086处理器有14个寄存器,每个寄存器都有一个特有的名称,即

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

这14个寄存器有可能进行具体的划分,按照功能可以分为三种

通用寄存器

控制寄存器

段寄存器

下面我们分别介绍一下这几种寄存器

通用寄存器

通用寄存器主要有四种,即AX、BX、CX、DX同样的,这四个寄存器也是16位的,能存放两个字节。AX、BX、CX、DX这四个寄存器一般用来存放数据,也被称为数据寄存器。它们的结构如下


8086CPU的上一代寄存器是8080,它是一类8位的CPU,为了保证兼容性,8086在8080上做了很小的修改,8086中的通用寄存器AX、BX、CX、DX都可以独立使用两个8位寄存器来使用。

在细节方面,AX、BX、CX、DX可以再向下进行划分

AX(AccumulatorRegister):累加寄存器,它主要用于输入/输出和大规模的指令运算。

BX(BaseRegister):基址寄存器,用来存储基础访问地址

CX(CountRegister):计数寄存器,CX寄存器在迭代的操作中会循环计数

DX(dataRegister):数据寄存器,它也用于输入/输出操作。它还与AX寄存器以及DX一起使用,用于涉及大数值的乘法和除法运算。

这四种寄存器可以分为上半部分和下半部分,用作八个8位数据寄存器

AX寄存器可以分为两个独立的8位的AH和AL寄存器;

BX寄存器可以分为两个独立的8位的BH和BL寄存器;

CX寄存器可以分为两个独立的8位的CH和CL寄存器;

DX寄存器可以分为两个独立的8位的DH和DL寄存器;

除了上面AX、BX、CX、DX寄存器以外,其他寄存器均不可以分为两个独立的8位寄存器

如下图所示。


合起来就是


AX的低位(0-7)位构成了AL寄存器,高8位(8-15)位构成了AH寄存器。AH和AL寄存器是可以使用的8位寄存器,其他同理。

在认识了寄存器之后,我们通过一个示例来看一下数据的具体存储方式。

比如数据19,它在16位存储器中所存储的表示如下

寄存器的存储方式是先存储低位,如果低位满足不了就存储高位,如果低位能够满足,高位用0补全,在其他低位能满足的情况下,其余位也用0补全。

8086CPU可以一次存储两种类型的数据

字节(byte):一个字节由8bit组成,这是一种恒定不变的存储方式

字(word):字是由指令集或处理器硬件作为单元处理的固定大小的数据,对于intel来说,一个字长就是两个字节,字是计算机一个非常重要的特征,针对不同的指令集架构来说,计算机一次处理的数据也是不同的。也就是说,针对不同指令集的机器,一次能处理不用的字长,有字、双字(32位)、四字(64位)等。

AX寄存器

我们上面探讨过,AX的另外一个名字叫做累加寄存器或者简称为累加器,其可以分为2个独立的8位寄存器AH和AL;在编写汇编程序中,AX寄存器可以说是使用频率最高的寄存器。

下面是几段汇编代码

movax,20/*将20送入寄存器AX*/movah,80/*将80送入寄存器AH*/addax,10/*将寄存器AX中的数值加上8*/

AX相比于其他通用寄存器来说,有一点比较特殊,AX具有一种特殊功能的使用,那就是使用DIV和MUL指令式使用。

BX寄存器

BX被称为数据寄存器,即表明其能够暂存一般数据。同样为了适应以前的8位CPU,而可以将BX当做两个独立的8位寄存器使用,即有BH和BL。BX除了具有暂存数据的功能外,还用于寻址,即寻找物理内存地址。BX寄存器中存放的数据一般是用来作为偏移地址使用的,因为偏移地址当然是在基址地址上的偏移了。偏移地址是在段寄存器中存储的,关于段寄存器的介绍,我们后面再说。

CX寄存器

CX也是数据寄存器,能够暂存一般性数据。同样为了适应以前的8位CPU,而可以将CX当做两个独立的8位寄存器使用,即有CH和CL。除此之外,CX也是有其专门的用途的,CX中的C被翻译为Counting也就是计数器的功能。当在汇编指令中使用循环LOOP指令时,可以通过CX来指定需要循环的次数,每次执行循环LOOP时候,CPU会做两件事

一件事是计数器自动减1

还有一件就是判断CX中的值,如果CX中的值为0则会跳出循环,而继续执行循环下面的指令,当然如果CX中的值不为0,则会继续执行循环中所指定的指令。

DX寄存器

DX也是数据寄存器,能够暂存一般性数据。同样为了适应以前的8位CPU,DX的用途其实在前面介绍AX寄存器时便已经有所介绍了,那就是支持MUL和DIV指令。同时也支持数值溢出等。

段寄存器

CPU包含四个段寄存器,用作程序指令,数据或栈的基础位置。实际上,对IBMPC上所有内存的引用都包含一个段寄存器作为基本位置。

段寄存器主要包含

CS(CodeSegment):代码寄存器,程序代码的基础位置

DS(DataSegment):数据寄存器,变量的基本位置

SS(StackSegment):栈寄存器,栈的基础位置

ES(ExtraSegment):其他寄存器,内存中变量的其他基本位置。

索引寄存器

索引寄存器主要包含段地址的偏移量,索引寄存器主要分为

BP(BasePointer):基础指针,它是栈寄存器上的偏移量,用来定位栈上变量

SP(StackPointer):栈指针,它是栈寄存器上的偏移量,用来定位栈顶

SI(SourceIndex):变址寄存器,用来拷贝源字符串

DI(DestinationIndex):目标变址寄存器,用来复制到目标字符串

状态和控制寄存器

就剩下两种寄存器还没聊了,这两种寄存器是指令指针寄存器和标志寄存器:

IP(InstructionPointer):指令指针寄存器,它是从CodeSegment代码寄存器处的偏移来存储执行的下一条指令

FLAG:Flag寄存器用于存储当前进程的状态,这些状态有位置(Direction):用于数据块的传输方向,是向上传输还是向下传输中断标志位(Interrupt):1-允许;0-禁止陷入位(Trap):确定每条指令执行完成后,CPU是否应该停止。1-开启,0-关闭进位(Carry):设置最后一个无符号算术运算是否带有进位溢出(Overflow):设置最后一个有符号运算是否溢出符号(Sign):如果最后一次算术运算为负,则设置1=负,0=正零位(Zero):如果最后一次算术运算结果为零,1=零辅助进位(AuxCarry):用于第三位到第四位的进位奇偶校验(Parity):用于奇偶校验

物理地址

我们大家都知道,CPU访问内存时,需要知道访问内存的具体地址,内存单元是内存的基本单位,每一个内存单元在内存中都有唯一的地址,这个地址即是物理地址。而CPU和内存之间的交互有三条总线,即数据总线、控制总线和地址总线。

CPU通过地址总线将物理地址送入存储器,那么CPU是如何形成的物理地址呢?这将是我们接下来的讨论重点。

现在,我们先来讨论一下和8086CPU有关的结构问题。

cxuan和你聊了这么久,你应该知道8086CPU是16位的CPU了,那么,什么是16位的CPU呢?

你可能大致听过这个回答,16位CPU指的是CPU一次能处理的数据是16位的,能回答这个问题代表你的底层还不错,但是不够全面,其实,16位的CPU指的是

CPU内部的运算器一次最多能处理16位的数据

寄存器的最大宽度为16位

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

好了,现在你应该知道为什么叫做16位CPU了吧。

在你知道上面这个问题的答案之后,我们下面就来聊一聊如何计算物理地址。

8086CPU有20位地址总线,每一条总线都可以传输一位的地址,所以8086CPU可以传送20位地址,也就是说,8086CPU可以达到2^20次幂的寻址能力,也就是1MB。8086CPU又是16位的结构,从8086CPU的结构看,它只能传输16位的地址,也就是2^16次幂也就是64KB,那么它如何达到1MB的寻址能力呢?

原来,8086CPU的内部采用两个16位地址合成的方式来传输一个20位的物理地址,如下图所示

叙述一下上图描述的过程

CPU中相关组件提供两个地址:段地址和偏移地址,这两个地址都是16位的,他们经由地址加法器变为20位的物理地址,这个地址即是输入输出控制电路传递给内存的物理地址,由此完成物理地址的转换。

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

下面是地址加法器的工作流程

其实段地址*16,就是左移4位。在上面的叙述中,物理地址=段地址*16+偏移地址,其实就是基础地址+偏移地址=物理地址寻址模式的一种具体实现方案。基础地址其实就等于段地址*16。

你可能不太清楚段的概念,下面我们就来探讨一下。

什么是段

段这个概念经常出现在操作系统中,比如在内存管理中,操作系统会把不同的数据分成段来存储,比如代码段、数据段、bss段、rodata段等。

但是这些的划分并不是内存干的,cxuan告诉你是谁干的,这其实是幕后BossCPU搞的,内存当作了声讨的对象。

其实,内存没有进行分段,分段完全是由CPU搞的,上面聊过的通过基础地址+偏移地址=物理地址的方式给出内存单元的物理地址,使得我们可以分段管理CPU。

如图所示

这是两个16KB的程序分别被装载进内存的示意图,可以看到,这两个程序的段地址的大小都是16380。

段寄存器

cxuan在上面只是简单为你介绍了一下段寄存器的概念,介绍的有些浅,而且介绍段寄存器不介绍段也有不知庐山真面目的感觉,现在为你详细的介绍一下,相信看完上面的段的概念之后,段寄存器也是手到擒来。

我们在合成物理地址的那张图提到了相关部件的概念,这个相关部件其实就是段寄存器,即CS、DS、SS、ES。8086的CPU在访问内存时,由这四个寄存器提供内存单元的段地址。

CS寄存器

要聊CS寄存器,那么IP寄存器是你绕不过去的曾经。CS和IP都是8086CPU非常重要的寄存器,它们指出了CPU当前需要读取指令的地址。

在8086CPU中,由CS:IP指向的内容当作指令执行。如下图所示

说明一下上图

在CPU内部,由CS、IP提供段地址,由加法器负责转换为物理地址,输入输出控制电路负责输入/输出数据,指令缓冲器负责缓冲指令,指令执行器负责执行指令。在内存中有一段连续存储的区域,区域内部存储的是机器码、外面是地址和汇编指令。

上面这幅图的段地址和偏移地址分别是2000和0000,当这两个地址进入地址加法器后,会由地址加法器负责将这两个地址转换为物理地址

然后地址加法器负责将指令输送到输入输出控制电路中

输入输出控制电路将20位的地址总线送到内存中。

然后取出对应的数据,也就是B8、23、01,图中的B8、BB都是操作数。

控制输入/输出电路会将B82301送入指令缓存器中。

此时这个指令就已经具备执行条件,此时IP也就是指令指针会自动增加。我们上面说到IP其实就是从CodeSegment也就是CS处偏移的地址,也就是偏移地址。它会知道下一个需要读取指令的地址,如下图所示

在这之后,指令执行执行取出的B82301这条指令。

然后下面再把2000和0003送到地址加法器中再进行后续指令的读取。后面的指令读取过程和我们上面探讨的如出一辙,这里cxuan就不再赘述啦。

通过对上面的描述,我们能总结一下8086CPU的工作过程

段寄存器提供段地址和偏移地址给地址加法器

由地址加法器计算出物理地址通过输入输出控制电路将物理地址送到内存中

提取物理地址对应的指令,经由控制电路取回并送到指令缓存器中

IP继续指向下一条指令的地址,同时指令执行器执行指令缓冲器中的指令

什么是CodeSegment

CodeSegment即代码段,它就是我们上面聊到就是CS寄存器中存储的基础地址,也就是段地址,段地址其本质上就是一组内存单元的地址,例如上面的movax,0123H、movbx,0003H。我们可以将长度为N的一组代码,存放在一组连续地址、其实地址为16的倍数的内存单元中,我们可以认为,这段内存就是用来存放代码的。

DS寄存器

CPU在读写一个内存单元的时候,需要知道这个内存单元的地址。在8086CPU中,有一个DS寄存器,通常用来存放访问数据的段地址。如果你想要读取一个10000H的数据,你可能会需要下面这段代码

movbx,10000Hmovds,bxmova1,[0]

上面这三条指令就把10000H读取到了a1中。

在上面汇编代码中,mov指令有两种传送方式

一种是把数据直接送入寄存器

一种是将一个寄存器的内容送入另一个寄存器

但是不仅仅如此,mov指令还具有下面这几种表达方式

描述举例mov寄存器,数据比如:movax,8mov寄存器,寄存器比如:movax,bxmov寄存器,内存单元比如:movax,[0]mov内存单元,寄存器比如:mov[0],axmov段寄存器,寄存器比如:movds,ax

栈我相信大部分小伙伴已经非常熟悉了,栈是一种具有特殊的访问方式的存储空间。它的特殊性就在于,先进入栈的元素,最后才出去,也就是我们常说的先入后出。

它就像一个大的收纳箱,你可以往里面放相同类型的东西,比如书,最先放进收纳箱的书在最下面,最后放进收纳箱的书在最上面,如果你想拿书的话,必须从最上面开始取,否则是无法取出最下面的书籍的。

栈的数据结构就是这样,你把书籍压入收纳箱的操作叫做压入(push),你把书籍从收纳箱取出的操作叫做弹出(pop),它的模型图大概是这样

入栈相当于是增加操作,出栈相当于是删除操作,只不过叫法不一样。栈和内存不同,它不需要指定元素的地址。它的大概使用如下

//压入数据Push(123);Push(456);Push(789);//弹出数据j=Pop();k=Pop();l=Pop();

在栈中,LIFO方式表示栈的数组中所保存的最后面的数据(LastIn)会被最先读取出来(FirstOut)。

栈和SS寄存器

下面我们就通过一段汇编代码来描述一下栈的压入弹出的过程

8086CPU提供入栈和出栈指令,最基本的两个是PUSH(入栈)和POP(出栈)。比如pushax会把ax寄存器中的数据压入栈中,popax表示从栈顶取出数据送入ax寄存器中。

我这里首先有一个初始的栈,没有任何指令和数据。

然后我们向栈中push数据后,栈中数据如下

涉及的指令有

movax,2345Hpushax

再向栈中push数据

其中涉及的指令有

movbx,0132Hpushbx

现在栈中有两条数据,现在我们执行出栈操作

其中涉及的指令有

popax/*ax=0132H*/

再继续取出数据

涉及的指令有

popbx/*bx=*/

完整的push和pop过程如下

现在cxuan问你一个问题,我们上面描述的是10000H~1000FH这段空间来作为push和pop指令的存取单元。但是,你怎么知道这个栈单元就是10000H~1000FH呢?也就是说,你如何选择指定的栈单元进行存取?

事实上,8086CPU有一组关于栈的寄存器SS和SP。SS是段寄存器,它存储的是栈的基础位置,也就是栈顶的位置,而SP是栈指针,它存储的是偏移地址。在任意时刻,SS:SP都指向栈顶元素。push和pop指令执行时,CPU从SS和SP中得到栈顶的地址。

现在,我们可以完整的描述一下push和pop过程了,下面cxuan就给你推导一下这个过程。

上面这个过程主要涉及到的关键变化如下。

当使用PUSH指令向栈中压入1个字节单元时,SP=SP-1;即栈顶元素会发生变化;

而当使用PUSH指令向栈中压入2个字节的字单元时,SP=SP–2;即栈顶元素也要发生变化;

当使用POP指令从栈中弹出1个字节单元时,SP=SP+1;即栈顶元素会发生变化;

当使用POP指令从栈中弹出2个字节单元的字单元时,SP=SP+2;即栈顶元素会发生变化;

栈顶越界问题

现在我们知道,8086CPU可以使用SS和SP指示栈顶的地址,并且提供PUSH和POP指令实现入栈和出栈,所以,你现在知道了如何能够找到栈顶位置,但是你如何能保证栈顶的位置不会越界呢?栈顶越界会产生什么影响呢?

比如如下是一个栈顶越界的示意图

栈顶越界是危险的,因为我们既然将一块区域空间安排为栈,那么在栈空间外部也可能存放了其他指令和数据,这些指令和数据有可能是其他程序的,所以如此操作会让计算机懵逼。

我们希望8086CPU能自己解决问题,毕竟8086CPU已经是个成熟的CPU了,要学会自己解决问题了。

然鹅(故意的),这对于8086CPU来说,这可能是它一辈子的夙愿了,真实情况是,8086CPU不会保证栈顶越界问题,也就是说8086CPU只会告诉你栈顶在哪,并不会知道栈空间有多大,所以需要程序员自己手动去保证。。。

另外,我输出了六本PDF,已免费提供下载,如下所示

发布于 2025-04-12
166
目录

    推荐阅读