CPU与寄存器
CPU 的结构
16 位结构(类似其他说法如:16 位机、字长为 16 位)的 CPU 指:
- 运算器一次最多可以处理 16 位数据
- 寄存器的最大宽度为 16 位
- 寄存器和运算器之间的通路为 16 位
CPU 给出物理地址
8086CPU 有 20 位地址总线,可以传送 20 位地址,寻址能力为:$2 ^ {20}$,即:1 MB
但是 8086CPU 是 16 位结构,在内部一次性处理、传输、暂存的地址为 16 位,因此从内部结构来看,如果将地址从内部简单发出,就只能送出 16 位的地址,表现出的寻址能力只有:$2 ^ {16}$,即:64 KB
CPU 访问内存单元时,必须向内存提供内存单元的物理地址
- 8086CPU 采用一种在内部用两个 16 位地址合成的方法来形成一个 20 位的物理地址
- 地址加法器合成物理地址的方法:
物理地址 = 段地址 * 16 + 偏移地址
(16 = $2 ^ {4}$,表示左移 4 位)
以 8086CPU 访问地址为 123C8h
的内存单元为例:
不过,也可以将 段地址 * 16
看作基地址,那么:物理地址 = 基地址 + 偏移地址
一个数据的二进制形式,左移 1 位,相当于乘 2
一个数据的二进制形式,左移 N 位,相当于乘 $2 ^ {N}$
一个数据的十六进制形式,左移 N 位,相当于乘 $16 ^ {N}$
CPU 对内存分段
其实,内存并没有分段,段的划分来自于 CPU,可以使用分段的方式来管理内存
- 分段的示例:
在编程时可以根据需要,将若干个地址连续的内存单元看作一个段:
- 用
段地址 * 16
来定位段的起始地址(基地址) - 用
偏移地址
来定位段中的内存单元
- 用
关于段需要注意的两点:
- 由于
段地址 * 16
必然是 16 的倍数,所以一个段的起始地址也一定是 16 的倍数 - 由于
偏移地址
只有 16 位,而 16 位的寻址能力为 64 KB,所以一个段的最大长度只能是 64 KB
- 由于
一段内存,既可以是代码的存储空间,也可以是数据的存储空间,还可以是栈空间,甚至可以什么都不是,关键在于 CS、IP、SS、SP、DS 等寄存器的指向
代码段
可以将一段长度小于 64KB 的代码存放在地址连续、起始地址为 16 的倍数的内存单元中,称为代码段
段地址放在 CS 中,将段中第一条指令的偏移地址放在 IP 中,CPU 就将执行我们定义的代码段中的指令
- 例如:
mov ax, 0000 (B8 00 00)
add ax, 0123h (05 23 01)
mov bx, ax (8B D8)
jmp bx (FF E3)
这段长度为 10 字节的指令,存放在
123B0h ~ 123B9h
的一组内存单元,就可以认为这是一个代码段,段地址为123Bh
,长度为 10 个字节若要让这段代码执行,可设置
CS = 123Bh
,IP = 0000h
数据段
可以将一段长度小于 64KB、地址连续、起始地址为 16 的倍数的内存单元专门存储数据,称为数据段
段地址放在 DS 中,用 mov、add、sub 等访问内存单元时的指令时,CPU 就将我们定义的数据段中的内容作为数据来访问
例如,将
123B0h ~ 123B9h
这段内存用于存放数据:- 段地址为
123Bh
,长度为 10 个字节 - 若要访问数据段中的数据,可以在 DS 中存放数据段的段地址,再访问具体的单元
- 段地址为
比如,累加这个数据段中的前三个单元中的数据,代码如下:
mov ax, 123Bh
mov ds, ax ; 将123Bh送入ds,作为数据段的段地址
mov al, 0 ; 用al存放累加结果
add al, [0] ; 将数据段的第一个单元(偏移地址为0)中的数值加到al中
add al, [1] ; 将数据段的第二个单元(偏移地址为1)中的数值加到al中
add al, [2] ; 将数据段的第三个单元(偏移地址为2)中的数值加到al中
- 再比如,累加这个数据段中的前三个字型数据,代码如下:
mov ax, 123Bh
mov ds, ax ; 将123Bh送入ds,作为数据段的段地址
mov ax, 0 ; 用ax存放累加结果
add ax, [0] ; 将数据段的第一个字(偏移地址为0)加到ax中
add ax, [2] ; 将数据段的第二个字(偏移地址为2)加到ax中
add ax, [4] ; 将数据段的第三个字(偏移地址为4)加到ax中
栈段
可以将一段长度小于 64KB、地址连续、起始地址为 16 的倍数的内存单元当作栈空间来使用,称为栈段
段地址放在 SS 中,将栈顶单元的偏移地址放在 SP 中,用 push、pop 指令时,CPU 就将我们定义的栈段当作栈空间使用
- 例如,将
10010h ~ 1001Fh
这段长度为 16 字节的内存空间当作栈来使用- 段地址为
1001h
,大小为 16 字节 - 以栈的方式来进行访问,这段空间就可以称为一个栈段
- 段地址为
CPU 的寄存器
一个典型的 CPU 由运算器、控制器、寄存器等组成
8086CPU 的所有寄存器都是 16位 的,可以存放两个字节
- 寄存器分类:
- 可见寄存器:编程中用到的寄存器,可以由指令指定,程序员可以感知
- 不可见寄存器:在程序中不可见,由系统来指定,程序员无法感知
通用寄存器
数据寄存器
AX/BX/CX/DX
用来存放一般性数据的寄存器,例如:AX、BX、CX、DX (在 32位的 CPU 中,更名为:EAX、EBX、ECX、EDX)
这四个通用寄存器都可以分成两个独立 8 位寄存器使用:AH、AL、BH、BL、CH、CL、DH、DL
- 以 AX 寄存器为例:(小端序存放,高位存储在高地址)
- 在 8086CPU 中,一个字(Word)占 16bit,由两个字节(Byte)组成
寄存器 | 意义和用法 |
---|---|
AX | 累加器。算术运算的主要寄存器,乘、除指令必须使用它来存放操作数 |
BX | 基址寄存器。计算地址时,可用来存放一段内存的起始偏移地址 |
CX | 计数器。可记录重复操作次数的隐含计数器 |
DX | 数据寄存器。在存放 32 位数据时,可与 AX 组合使用,DX 存放高 16 位,AX 存放低 16 位;在 IO 操作中可存放 IO 端口地址 |
指针寄存器
SP/BP
寄存器 SP 和 BP 通常在栈中使用,分别指向栈顶和栈底
注意:SP 和 BP 不可以拆分为两个 8 位寄存器使用
寄存器 | 意义和用法 |
---|---|
SP | 堆栈指针寄存器。存放堆栈栈顶的偏移地址,总是指向堆栈段中的栈顶位置,专门用于数据进栈和出栈的位置指示,只能与 SS 配对使用 |
BP | 基址指针寄存器。存放堆栈基址的偏移地址,指向堆栈段中一个数据区的基址位置,通常与 SS 配对使用 |
栈结构
栈是一种具有特殊的访问方式的存储空间,后进先出
在基于 8086CPU 编程时,可以将一段内存当作栈来使用(高地址作为栈底,低地址作为栈顶)
8086CPU 的入栈和出栈都是以字(Word)为单位进行的
8086CPU 提供入栈和出栈指令:
push
(入栈) 和pop
(出栈)假设栈空间为
10000H ~ 10005H
,栈的结构如下:
- 由于
push
和pop
修改的是 SP,因此栈顶的变化范围最大为:0 ~ FFFFh
栈顶超界
在 8086CPU 中,没有用于记录栈顶上限和下限的寄存器,因此栈满时
push
、栈空时pop
都会发生栈顶越界问题
8086CPU 只知道栈顶的位置(SS:SP),但不知道栈的空间有多大
8086CPU 不保证栈的操作不会越界,因此需要程序员自己注意
变址寄存器
指针寄存器主要包括 SI(源变址寄存器)和 DI(目的变址寄存器)
注意:SI 和 DI 不可以拆分为两个 8 位寄存器使用
SI/DI
SI 和 DI 是 8086CPU 中与 BX 功能相近的寄存器,与 DS 联用可以用来确定数据段中某一存储单元的偏移地址
在串处理指令中 SI 和 DI 作为隐含的源变址寄存器和目的变址寄存器,此时 SI 和 DS 联用,DI 和 ES 联用,分别达到在数据段和附加段中寻址的目的
寄存器 | 意义和用法 |
---|---|
SI | 源变址寄存器。存放内存中源数据区的指针,在某些指令作用下可以自增或自减 |
DI | 目的变址寄存器。存放内存中目的数据区的指针,在某些指令作用下可以自增或自减 |
- 例如,以下三组指令实现的功能相同:
mov bx, 0
mov ax, [bx]
mov ax, [bx + 123]
mov si, 0
mov ax, [si]
mov ax, [si + 123]
mov di, 0
mov ax, [di]
mov ax, [di + 123]
专用寄存器
指令指针寄存器 IP
指令指针寄存器 IP 存放即将执行的指令的偏移地址
通常配合 CS 寄存器使用,指明代码段中即将要执行的一条指令
不能使用 mov 指令给 IP 寄存器赋值
标志寄存器 FLAG
标志寄存器 FLAG 主要存放 CPU 的两类标志:状态标志和控制标志
状态标志:反映处理器的当前状态,比如有无溢出、有无进位等
控制标志:用来控制处理器的工作方式,比如是否响应可屏蔽中断等
在 8086CPU 中,FLAG 的 1、3、5、12、13、14、15 位并没有使用,不具有任何含义;而剩下的 0、2、4、6、7、8、9、10、11 位都具有特殊含义
FLAG 寄存器是按位起作用的,16 位的 FLAG 寄存器图:
- FLAG 寄存器中主要标志位的作用:
标志 | 意义 | 值为 1 的意义 | 值为 0 的意义 |
---|---|---|---|
ZF | 零标志位 | 结果为 0 | 结果不为 0 |
PF | 奇偶标志位 | 结果中 1 的个数为偶数 | 结果中 1 的个数为奇数 |
SF | 符号标志位 | 结果为负 | 结果非负 |
CF | 进位标志位 | 进位或借位值为 1 | 进位或借位值为 0 |
OF | 溢出标志位 | 发生溢出 | 没有溢出 |
DF | 方向标志位 | 每次操作后 SI、DI 递减 | 每次操作后 SI、DI 递增 |
ZF
零标志位。用来记录相关指令执行后,结果是否为 0
如果结果为 0,则 ZF = 1;如果结果不为 0,则 ZF = 0
- 例如:
mov ax, 1
sub ax, 1
; 执行后,结果为 0,ZF = 1
mov ax, 2
sub ax, 1
; 执行后,结果不为 0,ZF = 0
PF
奇偶标志位。用来记录相关指令执行后,结果的所有 bit 位中 1 的个数是否为偶数
如果 1 的个数为偶数,则 PF = 1;如果不为偶数,则 PF = 0
- 例如:
mov al, 1
add al, 10
; 执行后,结果为 00001011b,有 3 个 1(奇数个 1),PF = 0
mov al, 1
or al, 2
; 执行后,结果为 00000011b,有 2 个 1(偶数个 1),PF = 1
sub al, al
; 执行后,结果为 00000000b,有 0 个 1(偶数个 1),PF = 1
SF
符号标志位。用来记录相关指令执行后,结果是否为负,是 CPU 对有符号数运算结果正负的一种记录
如果将数据当作有符号数,通过 SF 可以得知结果的正负;但如果将数据当作无符号数,那么 SF 的值就没有意义了
如果结果为负,则 SF = 1;如果结果非负,则 SF = 0
在计算机中,通常使用补码来表示有符号数
在计算机中,一个数据既可以看成是有符号数,也可以看成是无符号数。例如:
mov al, 10000001b
add al, 1
; 结果:(al) = 10000010b
; 如果将 add 指令的运算当作无符号数,那么相当于 129 + 1 = 130(10000010b)
; 如果将 add 指令的运算当作有符号数,那么相当于 -127 + 1 = -126(10000010b)
CPU 在执行 add 等指令时,必然会影响到 SF 的值,至于需不需要利用 SF 的值,就看我们如何看待指令所进行的运算
- 例如:
mov al, 10000001b
add al, 1
; 执行后,结果为 10000010b,SF = 1
; 说明:如果指令进行的是有符号数运算,那么结果为负
mov al, 10000001b
add al, 01111111b
; 执行后,结果为 00000000b,SF = 0
; 说明:如果指令进行的是有符号数运算,那么结果为非负
某些指令可能影响标志寄存器的多个标记位
例如:执行sub al, al
后,ZF、PF、SF 等标志位都会受到影响,ZF = 1,PF = 1,SF = 0
CF
进位标志位。在进行无符号数运算时,记录运算结果的最高有效位向更高位的进位值,或从更高位的借位值
CF 是对无符号数运算有意义的标志位,对于无符号数运算,CPU 用 CF 记录最高有效位是否进位
如果进位或借位值为 1,则 CF = 1;如果进位或借位值为 0,则 CF = 0
- 例如:
mov al, 98h
add al, al
; 执行后,CF = 1(向更高位进位)
add al, al
; 执行后,CF = 0
$[98h]_补$ = 10011000
10011000
+ 10011000
--------
1 00110000
00110000
+ 00110000
--------
01100000
- 例如:
mov al, 97h
sub al, 98h
; 执行后,CF = 1(向更高位借位)
sub al, al
; 执行后,CF = 0(向更高位借位)
$[-98h]_补$ = 01101000
$[97h]_补$ = 10010111
10010111
+ 01101000
--------
11111111
00000001
+ 11111111
--------
1 00000000
OF
溢出标志位。一般情况下,OF 记录了有符号数的运算结果是否发生溢出
OF 是对有符号数运算有意义的标志位,对于有符号数运算,CPU 用 OF 记录是否溢出
如果发生溢出,则 OF = 1;如果没有溢出,则 OF = 0
- 例如:
mov al, 98
add al, 99
; add 指令执行后,CF = 0,OF = 1
$[98]_补$ = 01100010
$[99]_补$ = 01100011
01100010
+ 01100011
--------
11000101
- 对于无符号数 98 + 99 = 197 < 255 没有进位,所以 CF = 0
- 对于有符号数 98 + 99 发生溢出(正 + 正 = 负),所以 OF = 1
- 例如:
mov al, 0F0h
add al, 88h
; add 指令执行后,CF = 1,OF = 1
$[0F0h]_补$ = 11110000
$[88h]_补$ = 10001000
11110000
+ 10001000
--------
1 01111000
- 对于无符号数 0F0h + 88h = 376 > 255 有进位,所以 CF = 1
- 对于有符号数 0F0h + 88h 发生溢出(负 + 负 = 正),所以 OF = 1
- 例如:
mov al, 0F0h
add al, 78h
; add 指令执行后,CF = 1,OF = 0
$[0F0h]_补$ = 11110000
$[78h]_补$ = 01111000
11110000
+ 01111000
--------
1 01101000
- 对于无符号数 0F0h + 78h = 360 > 255 有进位,所以 CF = 1
- 对于有符号数 0F0h + 78h 没有发生溢出(最高位进位 1 ⊕ 次高位进位 1 = 0),所以 OF = 0
- 仅当两个符号相同的数相加,或两个符号相异的数相减,才可能产生溢出
- 进位是溢出的必要条件
计算机中补码运算的溢出判断方法:
- 假设
Xf
、Yf
分别为两个操作数的符号位,Zf
为运算结果的符号位
① 当Xf = Yf = 0
(两数同为正),而Zf = 1
(结果为负),正溢出
② 当Xf = Yf = 1
(两数同为负),而Zf = 0
(结果为正),负溢出- 假设
Cs
表示符号位的进位,Cp
表示最高数值位进位,⊕
表示异或(Cs 等价于进位标志位 CF)
① 若Cs ⊕ Cp = 0
,无溢出
② 若Cs ⊕ Cp = 1
,有溢出- 假设使用变形补码进行双符号位运算(正数符号为 00,负数符号为 11)
① 若运算结果的双符号位为 01,正溢出
② 若运算结果的双符号位为 10,负溢出
③ 若运算结果的双符号位为 00 或 11,无溢出
DF
方向标志位。在串处理指令中,控制每次操作后 SI、DI 的增减
每次操作后 SI、DI 递增,则 DF = 0;每次操作后 SI、DI 递减,则 DF = 1
- 8086CPU 中,提供了两条指令对 DF 进行设置:
指令 | 意义 |
---|---|
cld | 将标志寄存器 DF 位设为 0 |
std | 将标志寄存器 DF 位设为 1 |
- 例如:使用串传送指令 movsb 将 data 段中的第一个字符串复制到其后的空间中
assume cs:code, ds:data
data segment
db 'Hello 4ss1du0us!'
db 16 dup (0)
data ends
code segment
main:
mov ax, data
mov ds, ax
mov si, 0 ; ds:si 指向 data:0
mov es, ax
mov di, 16 ; es:di 指向 data:0010
mov cx, 16 ; 设置 rep 循环 16 次
cld ; 设置 df = 0,使 si、di 递增,movsb 正向传送
rep movsb
mov ax, 4c00h
int 21h
code ends
end main
- 例如:使用串传送指令 movsb 将 F000h 段中的最后 16 个字符复制到 data 段中
assume cs:code, ds:data
data segment
db 16 dup (0)
data ends
code segment
main:
mov ax, 0F000h
mov ds, ax
mov si, 0FFFFh ; ds:si 指向 F000:FFFF(F000 段的最后一个字符处)
mov es, data
mov di, 15 ; es:di 指向 data:000F(data 段的最后一个字符处)
mov cx, 16 ; 设置 rep 循环 16 次
std ; 设置 df = 1,使 si、di 递减,movsb 反向传送
rep movsb
mov ax, 4c00h
int 21h
code ends
end main
段寄存器
用来提供段地址,段地址在 8086CPU 的段寄存器中存放,例如:CS、DS、SS、ES
CS 和 IP
CS 和 IP 是 8086CPU 中两个最关键的寄存器,它们指示了 CPU 当前要读取指令的地址
在 8086CPU 中的任意时刻,假设 CS 中的内容为:M,IP 中的内容为:N,那么:
- 8086CPU 将从内存的
M * 16 + N
单元开始,读取一条指令并执行 - 也可以说,8086CPU 将
CS:IP
指向的内容当作指令执行 - CS 中的内容作为段地址,IP 中的内容作为偏移地址
- 8086CPU 将从内存的
8086CPU 只知道当前要执行的代码的位置(CS:IP),但不知道具体要执行的指令有多少
以 8086CPU 读取、执行指令的工作原理为例:
工作过程可以简要概括如下:
- 从
CS:IP
指向的内存单元读取指令,读取的指令会进入指令缓冲器IP = IP + 所读取的指令的长度
,指向下一条指令的地址- 执行指令
- 跳转到步骤 1,重复这个流程
注意:
- 当 CPU 刚开始工作时,
CS
被设置为FFFFh
,IP
被设置为0000h
,因此FFFF0h
单元中存放的是 CPU 开机后执行的第一条指令mov
指令不能用来修改 CS 和 IP 这两个寄存器的值- 最简单的方法,可以通过 jmp 指令来修改 CS、IP 的值:
jmp 段地址:偏移地址
jmp 2AE3:3,执行后:CS=2AE3h,IP=0003h
jmp 3:0B16,执行后:CS=0003h,IP=0B16h- 如果只想修改 IP 的值:
jmp 通用寄存器
jmp ax
执行前:AX=1000h,CS=2000h,IP=0003h;
执行后:AX=1000h,CS=2000h,IP=1000h
DS 和 [address]
DS 寄存器通常用来存放要访问的数据的段地址
- 例如,读取
10000h(1000:0)
单元的内容,并存放到al
中:
mov bx, 1000h ; 8086CPU不支持直接将数据送入段寄存器,因此通过bx中转
mov ds, bx ; 数据段地址设为1000h
mov al, [0] //内存单元的偏移地址是0
- 再例如,将
al
中的内容存放到内存单元10000h(1000:0)
的位置:
mov bx, 1000h
mov ds, bx
mov [0], al
在
mov
指令中,*[]
表示一个内存单元,[0]
表示这个内存单元的偏移地址为 0,而这个内存单元的段地址默认存放在DS
中*8086CPU 不支持直接将数据送入段寄存器(硬件设计的问题),因此必须先将数据送入通用寄存器,然后再从通用寄存器送入段寄存器(数据 –> 通用寄存器 –> 段寄存器)
SS 和 SP
SS 用来存放栈顶的段地址,SP 用来存放栈顶的偏移地址,任意时刻:SS:SP 指向栈顶元素
当执行
push
和pop
指令时,CPU 会从 SS 和 SP 中得到栈顶的地址以
push ax
为例:- 首先
SP = SP - 2
,SS:SP 指向当前新的栈顶位置(原栈顶的上方) - 将 ax 中的内容送入 SS:SP 所指向的内存单元处
- 首先
- 以
pop ax
为例:- 将 SS:SP 指向的内存单元处的数据送入 ax 中
- 然后
SP = SP + 2
,SS:SP 指向当前新的栈顶位置(原栈顶的下方)
当栈为空时,例如栈空间为:
10000h ~ 1000Fh
:
- 由于出栈、入栈以字(Word)为单位,因此当栈中只有一个元素时:SS = 1000h、SP = 0Eh
- 栈为空相当于唯一的元素出栈,出栈后
SP = SP + 2
,SP = 0Eh + 2 = 10h,因此当栈为空时:SS = 1000h、SP = 10h(即:指向栈底下方的内存单元)
注意:执行
pop
后,pop
操作前的栈顶元素依然存在于内存单元中,只是已经不在栈中
等再次执行push
后,向该内存单元中送入新的数据时,即可将其覆盖
ES
附加段寄存器。存放当前执行程序中的一个辅助数据段的段地址