CPU 的结构

16 位结构(类似其他说法如:16 位机、字长为 16 位)的 CPU 指:

  1. 运算器一次最多可以处理 16 位数据
  2. 寄存器的最大宽度为 16 位
  3. 寄存器和运算器之间的通路为 16 位

CPU 给出物理地址

8086CPU 有 20 位地址总线,可以传送 20 位地址,寻址能力为:$2 ^ {20}$,即:1 MB

但是 8086CPU 是 16 位结构,在内部一次性处理、传输、暂存的地址为 16 位,因此从内部结构来看,如果将地址从内部简单发出,就只能送出 16 位的地址,表现出的寻址能力只有:$2 ^ {16}$,即:64 KB

CPU 访问内存单元时,必须向内存提供内存单元的物理地址

  • 8086CPU 采用一种在内部用两个 16 位地址合成的方法来形成一个 20 位的物理地址

x86汇编_CPU给出物理地址1.png

  • 地址加法器合成物理地址的方法:物理地址 = 段地址 * 16 + 偏移地址16 = $2 ^ {4}$,表示左移 4 位

以 8086CPU 访问地址为 123C8h 的内存单元为例:

x86汇编_CPU给出物理地址2.png

不过,也可以将 段地址 * 16 看作基地址,那么:物理地址 = 基地址 + 偏移地址

一个数据的二进制形式,左移 1 位,相当于乘 2
一个数据的二进制形式,左移 N 位,相当于乘 $2 ^ {N}$
一个数据的十六进制形式,左移 N 位,相当于乘 $16 ^ {N}$


CPU 对内存分段

其实,内存并没有分段,段的划分来自于 CPU,可以使用分段的方式来管理内存

  • 分段的示例:

x86汇编_段1.png

  • 在编程时可以根据需要,将若干个地址连续的内存单元看作一个段:

    1. 段地址 * 16 来定位段的起始地址(基地址)
    2. 偏移地址 来定位段中的内存单元
  • 关于段需要注意的两点:

    1. 由于 段地址 * 16 必然是 16 的倍数,所以一个段的起始地址也一定是 16 的倍数
    2. 由于 偏移地址 只有 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)
  1. 这段长度为 10 字节的指令,存放在 123B0h ~ 123B9h 的一组内存单元,就可以认为这是一个代码段,段地址为 123Bh,长度为 10 个字节

  2. 若要让这段代码执行,可设置 CS = 123BhIP = 0000h


数据段

可以将一段长度小于 64KB、地址连续、起始地址为 16 的倍数的内存单元专门存储数据,称为数据段

段地址放在 DS 中,用 mov、add、sub 等访问内存单元时的指令时,CPU 就将我们定义的数据段中的内容作为数据来访问

  • 例如,将 123B0h ~ 123B9h 这段内存用于存放数据:

    1. 段地址为 123Bh,长度为 10 个字节
    2. 若要访问数据段中的数据,可以在 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 字节的内存空间当作栈来使用
    1. 段地址为 1001h,大小为 16 字节
    2. 以栈的方式来进行访问,这段空间就可以称为一个栈段

CPU 的寄存器

一个典型的 CPU 由运算器控制器寄存器等组成

8086CPU 的所有寄存器都是 16位 的,可以存放两个字节

  • 寄存器分类:

x86汇编_寄存器1.png

  1. 可见寄存器:编程中用到的寄存器,可以由指令指定,程序员可以感知
  2. 不可见寄存器:在程序中不可见,由系统来指定,程序员无法感知

通用寄存器

数据寄存器

AX/BX/CX/DX

用来存放一般性数据的寄存器,例如:AX、BX、CX、DX (在 32位的 CPU 中,更名为:EAX、EBX、ECX、EDX)

这四个通用寄存器都可以分成两个独立 8 位寄存器使用:AH、AL、BH、BL、CH、CL、DH、DL

  • 以 AX 寄存器为例:(小端序存放,高位存储在高地址)

x86汇编_通用寄存器1.png

  • 在 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,栈的结构如下:

x86汇编_栈结构1.png

x86汇编_栈结构2.png

  • 由于 pushpop 修改的是 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 寄存器图:

x86汇编_标志寄存器1.png

  • 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
  1. 对于无符号数 98 + 99 = 197 < 255 没有进位,所以 CF = 0
  2. 对于有符号数 98 + 99 发生溢出(正 + 正 = 负),所以 OF = 1
  • 例如:
mov al, 0F0h
add al, 88h
; add 指令执行后,CF = 1,OF = 1

$[0F0h]_补$ = 11110000
$[88h]_补$ = 10001000

   11110000
+  10001000
   --------
 1 01111000
  1. 对于无符号数 0F0h + 88h = 376 > 255 有进位,所以 CF = 1
  2. 对于有符号数 0F0h + 88h 发生溢出(负 + 负 = 正),所以 OF = 1
  • 例如:
mov al, 0F0h
add al, 78h
; add 指令执行后,CF = 1,OF = 0

$[0F0h]_补$ = 11110000
$[78h]_补$ = 01111000

   11110000
+  01111000
   --------
 1 01101000
  1. 对于无符号数 0F0h + 78h = 360 > 255 有进位,所以 CF = 1
  2. 对于有符号数 0F0h + 78h 没有发生溢出(最高位进位 1 ⊕ 次高位进位 1 = 0),所以 OF = 0
  • 仅当两个符号相同的数相加,或两个符号相异的数相减,才可能产生溢出
  • 进位是溢出的必要条件

计算机中补码运算的溢出判断方法:

  1. 假设 XfYf 分别为两个操作数的符号位,Zf 为运算结果的符号位
    ① 当 Xf = Yf = 0(两数同为正),而 Zf = 1(结果为负),正溢出
    ② 当 Xf = Yf = 1(两数同为负),而 Zf = 0(结果为正),负溢出
  2. 假设 Cs 表示符号位的进位,Cp 表示最高数值位进位, 表示异或(Cs 等价于进位标志位 CF
    ① 若 Cs ⊕ Cp = 0,无溢出
    ② 若 Cs ⊕ Cp = 1,有溢出
  3. 假设使用变形补码进行双符号位运算(正数符号为 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,那么:

    1. 8086CPU 将从内存的 M * 16 + N 单元开始,读取一条指令并执行
    2. 也可以说,8086CPU 将 CS:IP 指向的内容当作指令执行
    3. CS 中的内容作为段地址,IP 中的内容作为偏移地址
  • 8086CPU 只知道当前要执行的代码的位置(CS:IP),但不知道具体要执行的指令有多少

  • 以 8086CPU 读取、执行指令的工作原理为例:

x86汇编_段寄存器1.png

x86汇编_段寄存器2.png

x86汇编_段寄存器3.png

x86汇编_段寄存器4.png

x86汇编_段寄存器5.png

x86汇编_段寄存器6.png

x86汇编_段寄存器7.png

x86汇编_段寄存器8.png

x86汇编_段寄存器9.png

工作过程可以简要概括如下:

  1. CS:IP 指向的内存单元读取指令,读取的指令会进入指令缓冲器
  2. IP = IP + 所读取的指令的长度,指向下一条指令的地址
  3. 执行指令
  4. 跳转到步骤 1,重复这个流程

注意:

  1. 当 CPU 刚开始工作时,CS 被设置为 FFFFhIP 被设置为 0000h,因此 FFFF0h 单元中存放的是 CPU 开机后执行的第一条指令
  2. mov 指令不能用来修改 CS 和 IP 这两个寄存器的值
  3. 最简单的方法,可以通过 jmp 指令来修改 CS、IP 的值
    jmp 段地址:偏移地址
    jmp 2AE3:3,执行后:CS=2AE3h,IP=0003h
    jmp 3:0B16,执行后:CS=0003h,IP=0B16h
  4. 如果只想修改 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 指向栈顶元素

  • 当执行 pushpop 指令时,CPU 会从 SS 和 SP 中得到栈顶的地址

  • push ax 为例:

    1. 首先 SP = SP - 2,SS:SP 指向当前新的栈顶位置(原栈顶的上方)
    2. 将 ax 中的内容送入 SS:SP 所指向的内存单元处

x86汇编_SS和SP1.png

  • pop ax 为例:
    1. 将 SS:SP 指向的内存单元处的数据送入 ax 中
    2. 然后 SP = SP + 2,SS:SP 指向当前新的栈顶位置(原栈顶的下方)

x86汇编_SS和SP2.png

当栈为空时,例如栈空间为:10000h ~ 1000Fh

  1. 由于出栈、入栈以字(Word)为单位,因此当栈中只有一个元素时:SS = 1000h、SP = 0Eh
  2. 栈为空相当于唯一的元素出栈,出栈后 SP = SP + 2,SP = 0Eh + 2 = 10h,因此当栈为空时:SS = 1000h、SP = 10h(即:指向栈底下方的内存单元

注意:执行 pop 后,pop 操作前的栈顶元素依然存在于内存单元中,只是已经不在栈中
等再次执行 push 后,向该内存单元中送入新的数据时,即可将其覆盖


ES

附加段寄存器。存放当前执行程序中的一个辅助数据段的段地址