寄存器基础知识

CPU和计算器的其它部件通过外部总线(包括地址总线、控制总线、数据总线)相连。CPU内部由运算器、控制器、寄存器等部件组成,这些CPU内部的部件之间使用内部总线相连:

  • 运算器处理数据
  • 寄存器存储数据
  • 控制器控制各种部件
  • 内部总线连接各部件

程序员可直接操作的是寄存器。不同CPU的寄存器个数、结构不同,8086 CPU的寄存器是16位,每个寄存器可存放2字节数据。

8086 CPU总共有14个寄存器:AX BX CX DX SI DI SP BP IP CS SS DS ES PSW。其中AX BX CX DX是通用寄存器,可由程序员决定在通用寄存器中存放哪些数据,其它寄存器都有特殊意义,修改特殊寄存器可能会影响CPU的工作流程。

例如,AX寄存器的逻辑结构:

8086 CPU的上一代寄存器都是8位,为兼容原来基于上代寄存器编写的程序能直接在8086上运行,8086CPU的AX BX CX DX这四个寄存器可分为为两个可独立的8位寄存器来使用:

  • AX可分为AH和AL(H表示AX中的高位8bit,L表示低位的8bit)
  • BX可分为BH和BL
  • CX可分为CH和CL
  • DX可分为DH和DL

例如,AX寄存器中存储十进制数值20000(对应的二进制为:0100 1110 0010 0000):

如果存储了20000的AX寄存器看作是AH和AL两个独立的寄存器,那么AH中的二进制位0100 1110表示十进制数值78,AL存储的二进制位0010 0000表示十进制数值32。

关于机器的位数和字长(word)这个单位

数据总线宽度决定一次性最多能传输多少数据。如果CPU有16根数据总线,那么它一次性可以传输2个字节数据,如果有32根数据总线,则一次性可传输4字节,64根数据总线,则一次性可传输8字节。

以数据总线一次性传输的数据量为传输单位,这个单位称为字(word),能传输的数据量称为字长。所以,数据总线的宽度决定了字长

内存的操作单位是字节,为什么要用字长做CPU的数据单位?主要是为了CPU在和外界传输信息时不浪费任何一根数据总线。

既然CPU和外界部件通信时的数据单位是字长,那么CPU的寄存器大小也应当是一个字长大小,这样才能直接容下一次数据传输事务中涉及的数据量。

此外,为了能尽可能对内存地址空间进行大范围寻址,或者说为了识别更大的内存,地址总线的宽度通常也会设计为字长大小,即地址总线宽度和数据总线宽度一样大。比如数据总线宽度32,字长是4字节,地址总线最多能携带4字节的地址信息,能寻址的内存范围为0-(2^32-1)个内存单元,即只识别4G内存。

编程语言中的指针存储的就是内存地址,所以通常来说一个指针的大小就是一个字长大小

这样的话,关于多少位的说法就统一了:64位机器,数据总线64根,地址总线64根,寄存器大小8字节,指针8字节。现代64位机器基本都这样。但是以前的机器就不一定遵守这样的规律了。

物理寻址的方式

以前有些CPU数据总线宽度和地址总线宽度并不一致。

比如8086 CPU是16位的,它的数据总线是16根,寄存器大小2字节,但是地址总线是20根。这样的话,8086的可寻址范围就比16根地址总线大一些。如果是16根地址总线,最多可寻址64KB大小的内存,而20根地址总线,可寻址1MB大小的内存。只是要注意,寄存器大小只有16位,无法存放20位的地址信息,所以要描述20位的地址信息,需要使用两个16位地址合成一个20位大地址,即需要两个寄存器的参与。

实际上,对物理内存寻址的方式是物理地址=段地址 * 16 + 偏移地址。CPU将段寄存器中的段地址和偏移地址寄存器中的偏移地址取出来,通过地址加法器按照上面的公式计算出真实的物理内存地址。

比如你距离图书馆2826米,有人问你图书馆在哪里,你可以告诉它在前方2826米处。但加上限制条件,只给两张纸条,且每张纸条最多只能写三位数,这时两张纸条可以分别写200和826,如果两者此前已经协商好计算方式200 * 10 + 826,经过运算就得到了4位数的地址。

段地址 * 16中乘16而不是乘其它数值也是有原因的,因为给出的地址来自于16位的寄存器,是16位的,乘16的效果是直接补一个0,相当于直接16进制左移一位,即进一位,相当于十进制的乘10。

例如,要寻址5位3字节的123C8(Hex),超出了2字节大小,它等价于12300 + 00C8,所以给出段地址1230,再给出基于该段的偏移00C8,即可寻址到123C8。

的概念并不是真的对内存分了段,而是CPU为了描述地址而使用的一个逻辑概念。一个逻辑地址段代表一个逻辑地址范围,在这个范围内,以段首为参照进行偏移,可描述出这个段中的所有地址。所以,只要给出段首地址,再给出该段的偏移地址,就可以计算出实际的物理地址。

因为偏移地址也是16位的(假设寄存器的大小是2字节),它能描述的最大偏移值为65535,即偏移的变化范围是0-FFFF(Hex)。换句话说,每个段最大的范围是64KB。

CPU可以使用不同的段地址和偏移地址形成同一个物理地址。例如:

1
2
3
物理地址(Hex)    段地址(Hex)   偏移地址(Hex)
21F60 2000 1F60
2100 0F60

因为段地址*16是16的倍数,段的范围是64KB,也是16的倍数,所以每个段的段首地址是16的倍数,即段地址用16进制描述时必须是0结尾的。

所以,可根据需要,将起始地址为16的倍数的地址,且地址连续的一组内存单元定义为一个段。

读取指令并执行的过程

CPU计算物理地址的方式为物理地址 = 段地址 * 16 + 偏移地址。段地址来自于段地址寄存器,偏移地址来自于偏移地址寄存器。

指令也存放在内存中,所以CPU要执行指令需要先寻址并从内存中读取到寄存器中。对指令寻址时,指令的段地址保存在CS寄存器中,指令的偏移地址保存在IP中。所以在任意时刻,这两个寄存器中的值CS:IP都可以描述CPU将要执行的指令

例如,CPU当前的状态:

要执行第一条指令mov ax,0123(Hex),CPU将通过地址加法器根据公式2000(Hex) * 16(Dec) + 0(Hex)运算得到地址20000,于是将地址20000送入输入输出电路并通过地址总线发送出去,然后从内存中读取3个字节(B8 23 01,即mov ax,0123(Hex))放入数据总线传输到输入输出电路,并将其缓存在指令缓冲器中。于此同时,IP寄存器的偏移值随之增加3字节。所以得到如下状态:

最后,执行控制器从指令缓冲器中取得指令并执行。执行完成后,AX寄存器中将存放数据0123(Hex)。

所以,CPU执行指令的工作过程为:根据CS:IP计算将要执行的指令所在的地址,读取指令后将其放入指令缓冲器,同时更新IP寄存器的值,最后执行指令。并重复上述过程。

之后继续按照这个逻辑取指并执行。比如接下来,计算得到20003地址放入地址总线,并读取3字节的指令通过数据总线存入指令缓冲器等待执行,此时IP寄存器的偏移值变成6,指令执行后,BX寄存器的值为0003(Hex)。再随后读取2字节指令mov ax,bx,读取完成后IP寄存器的值为5,执行完成后AX寄存器将保存BX寄存器中的值0003(Hex),最后读取2字节指令add ax,bx,读取完成后IP寄存器的值为7,执行后完成后AX寄存器的值为0006(Hex)。

修改CS、IP寄存器的值

mov指令可以修改通用寄存器,但是无法修改CS、IP寄存器的值。要修改CS和IP,需使用jmp指令:

  • jmp 段地址:偏移地址:同时修改CS和IP寄存器的值
  • jmp 通用寄存器(ax/bx...):只修改IP寄存器的值

例如:

1
2
3
4
5
jmp 2AE3:3  # 执行后,CS=2AE3(Hex),IP=0003(Hex)
jmp 3:0B16 # 执行后,CS=0003(Hex),IP=0B16(Hex)

jmp ax # 执行后,IP的值为ax寄存器中的值
jmp bx # 执行后,IP的值为bx寄存器中的值

内存中字的存储:大端和小端

在一个16位的CPU寄存器中,字长为2字节。寄存器在存放一个字长数据时,高8位存放高位字节,低8位存放低位字节。

但是在内存中,内存被划分成存储单元,每个存储单元只能存放1字节数据。所以,存放一个字需要2个存储单元。在存放时,有两种存放方式:

  • 小端(little endian):字的高位字节–>高位地址单元,字的低位字节–>低位地址单元中(小端高对高)
  • 大端(big endian):字的高位字节–>低位地址单元,字的低位字节–>高位地址单元中

比如,16位寄存器中存放一个16进制为0x0123的2字节int数据,该值在寄存器中的存放方式为:

1
2
3
       高位字节   低位字节
Bin 0000 0001 0010 0011
Hex 01 23

如果要将这2个字节存放到内存,假设从内存单元0开始存放,那么按照这两种存放方式存储时,内存中的存放结果为:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 内存编址方式
低位 高位
---------------------->

######### 小端法 ###########
低位地址 高位地址
Bin 0010 0011 0000 0001
Hex 23 01

######### 大端法 ###########
低位地址 高位地址
Bin 0000 0001 0010 0011
Hex 01 23

人在阅读或书写一个二进制或十六进制数字时,总是高位在左边,低位在右边,这符合寄存器中的高低位顺序,但是内存编址方式(或数组索引)是低位在前,高位在后,两者相反的顺序使得高低位交叉的大端存储法更适合人类阅读,而小端则『反人类』。

采用哪种存储方式并不影响计算机的运行速度,只是影响人的理解,也影响跨主机数据通信。因为不同CPU在将寄存器数据存放到内存(或从内存读取到寄存器)时可能采用不同的方式,所以如果数据跨主机后将可能会出现反序现象。

为了保证跨主机网络通信不会出现数据反序问题,TCP/IP规定了网络字节序为大端方式,即数据在经过网络传输出去时和接收进来时必须采用大端方式。

读写内存单元

CPU要读写内存单元,需要先确定该内存单元的地址。寻址方式仍然是物理地址=段地址*16 + 偏移地址

8086CPU中,对数据寻址时的段地址保存在DS寄存器中,偏移地址通过索引方式[N]来指定。

注意,前面曾使用过的CS寄存器和IP寄存器是用来对指令进行寻址的。此处的DS寄存器是用来对数据进行寻址的,是准备读写内存单元的。

例如,从10000H(即1000:0)处读取数据保存到al中,并将cx的数据写入内存单元10003H处:

1
2
3
4
mov bx, 1000H    # 将段地址保存在中间寄存器bx
mov ds, bx # 将bx数据保存到ds寄存器
mov al, [0] # 从10000H处读取1字节数据保存到al
mov [3], cx # 将cx寄存器的数据写入内存单元

上面的一系列mov指令说明,可以将数据送入寄存器,将寄存器A数据送入寄存器B,将内存单元的数据送入寄存器以及将寄存器数据写入内存。

为什么不直接将数据1000H送入ds段地址寄存器?这是CPU是否支持决定的,8086 CPU不支持这种操作。

所以,对于8086CPU,如果想要读写内存单元,总会有如下两个指令来设置ds寄存器中的段地址:

1
2
mov bx, 1000H
mov ds, bx

问题(1):

1
2
3
4
5
6
7
8
9
10
11
12
13
# 内存单元数据
10000H 10001H 10002H 10003H
23 11 22 66

# 执行以下指令后,寄存器ax bx cx的值分别是多少
# ax,bx,cx都是2字节,所以每次从内存单元读取2字节
mov ax, 1000H # ax=1000H
mov ds, ax # ds=1000H
mov ax, [0] # ax=1123H
mov bx, [2] # bx=6622H
mov cx, [1] # cx=2211H
add bx, [1] # bx=8833H
add cx, [2] # cx=8833H

问题(2):

1
2
3
4
5
6
7
8
9
10
11
12
# 内存单元数据
10000H 10001H 10002H 10003H
23 11 22 11

# 执行以下指令后,内存中的值分别是多少
mov ax, 1000H # ax=1000H
mov ds, ax # ds=1000H
mov ax, 11316 # ax=2C34H,十进制11316=2C34(Hex)
mov [0], ax # 内存单元:34 2C 22 11
mov bx, [0] # bx=2C34
sub bx, [2] # bx=2C34-1122=1B12H
mov [2], bx # 内存单元:34 2C 12 1B

CPU的栈

现在的CPU都提供了栈,8086CPU也提供了栈,这意味着在基于8086CPU编程的时候,可以将一段内存当作栈来使用(栈也在内存,只不过可以用特殊方式访问这部分内存单元)。

8086CPU提供了push入栈指令和pop出栈指令,它们都以字为单位。

1
2
3
4
5
6
push ax    # 将ax中的数据送入栈中
pop ax # 从栈顶取出数据送入ax中
push 段寄存器
pop 段寄存器
push 内存单元 # 在栈内存单元和普通内存单元间传输数据
pop 内存单元

例如:

1
2
3
4
5
6
7
8
9
mov ax, 0123H
push ax
mov bx, 2266H
push bx
mov cx, 1122H
push cx
pop ax # 栈顶数据(原来cx的数据)保存到ax
pop bx
pop cx # 栈底数据(原理ax的数据)保存到cx

从图可知,一段当作栈的连续内存,栈顶在低位,栈底在高位,且栈顶可能是空余一部分数据的,即栈顶是从高位向低位增长的。

如何知道这段连续内存是一个栈空间,如何知道当前的栈顶位置?

8086CPU使用两个寄存器来描述栈:SS寄存器保存栈的段地址,SP寄存器保存栈顶的偏移

所以,**SS寄存器中的值指向的地址就是栈空间的段首地址(低位),SP寄存器中的值对应的就是从高位不断扩展到低位的偏移地址(空栈时在最高位)**。

因为push和pop以字为操作单位,所以每次push压栈时,SP寄存器的值都会先减2再向内存单元写入数据,每次pop弹栈时,都会先读取栈顶数据再将SP寄存器的值加2

例如,将10000H-1000FH这段空间当作栈,初始化为栈的过程:

1
2
3
mov ax, 1000H
mov ss, ax
mov sp, 0010H # 000F的下一个单元地址