底层心法
摘录自《汇编语言》(第四版) — 王爽
该篇章着重描述 CPU 是如何通过地址总线和数据总线与内存打交道的。
电子计算机能处理、传输的信息都是电信号,这些信号需要导线传送。因此计算机中有专门连接 CPU 和其他芯片的导线,通常成为总线。总线从物理上讲,就是一根导线的集合。
一根导线能传送的稳定状态只有两种:高电平或低电平。二进制表示则为 1 或 0。
-
地址总线
一个 CPU 有 N根地址线,则可以说该 CPU 的地址总线宽度为 N。这样的 CPU 最多可以寻找 2 的 N次方个内存单元。
如,具有 10根地址线的 CPU可以传送 10位二进制数,即 2 的 10次方,最小数为 0,最大数为 1023。
-
数据总线
数据总线的宽度决定了 CPU 和外界的数据的传送速度。8 根数据总线一次可传送 8位二进制数据(一个字节)。
如,8088微处理器只有 8根数据线,向内存写入 98D9H 时需要进行两次数据传送。
-
控制总线
CPU 对外部器件的控制是通过控制总线来进行的,这里控制总线是个总称(不同控制线的集合)。有多少根控制总线,意味着 CPU 提供了对外部器件的多少种控制。
如,“读信号输出“的控制线负责由 CPU 向外传送读信号,CPU 像该控制线上输出低电平表示要读取数据;有一根”写信号输出“的控制线负责传送写信号。
1 寄存器
开始进入正题了,一个典型的 CPU 由运算器、控制器、寄存器等器性构成,这些器件靠内部总线相连,在CPU中:
- 运算器进行信息处理
- 寄存器进行信息存储
- 控制器控制各种器件进行工作
- 内部总线连接各种器件,在它们之间进行数据的传送。
对于一个汇编程序员来说,CPU 中的主要部件是寄存器,程序员通过改变各种寄存器中的内容来实现对 CPU 的控制。不同的 CPU,寄存器的个数、结构是不相同的,下面以 8086CPU 为例进行逐一介绍。
1.1 通用寄存器
8086CPU 的所有寄存器都是 16位的,可以存放两个字节。AX、BX、CX、DX 这4个通用寄存器通常用来存放一般性数据,被称为通用寄存器。为了兼容上一代CPU,这 4个寄存器可分为两个独立使用的 8位寄存器来用:
- AX:分为 AH 和 AL
- BX:分为 BH 和 BL
- CX:分为 CH 和 CL
- DX:分为 DH 和 DL
以 AX 为例,低 8位(0~7位)构成了 AL寄存器,高 8位(8~15位)构成了 AH寄存器。
那么接下来学习几条汇编指令,查看以下的表格:
| 汇编指令 | 控制CPU完成的操作 | 高级语言的语法描述 |
|---|---|---|
| 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 | 将 AX 和 BX 中数值相加,结果存入 AX | AX=AX+BX |
指令 mov 和 add 的基本用法掌握后,考虑以下的场景寄存器值的变化:
AH=12H AL=FFH
add AL,01H
上述指令执行后,寄存器 AH、AL 的值是多少?由于AL 只能容纳 2 位十六进制数,FFH + 01H = 100H 运算后会进位,但 AH=12H 完全不会变化。在汇编中,运算的进位逻辑严格受限于指令所指定的“操作对象宽度”。它们的进位只会影响标志寄存器,绝对不会影响其对应的高位或低位寄存器。
1.2 物理地址
汇编语言、组成原理或操作系统 课程中,内存最小可寻址单位是字节(1 Byte = 8 bit)
所有的内存单元构成的存储空间是一个一维的线性空间,每一个内存单元在这个空间中都有唯一的地址,称为物理地址。
CPU 通过地址总线送入存储器的,必须是一个内存单元的物理地址。以 8086CPU 为例,该 CPU 有 20位地址总线,可以传送 20位地址,达到 1MB寻址能力。那么问题来了!8086CPU 是 16位结构,内存一次性处理、传输、暂时存储的地址为 16位,表现出的寻址能力只有 64KB,那么它是如何达成 1MB寻址能力的呢?

如图所示,当 CPU 读写内存时通过内部总线,将 段地址 和 偏移地址 送入地址加法器,采用 段地址x16+偏移地址 的方法合成 20位物理地址(x16 在二进制中也称为左移 4位)。
1.3 段与段寄存器
首先需要清晰的认识到,内存并没有分段,段的划分来自于 CPU 内部地址加法器的计算逻辑 以及 段寄存器的设计局限。
由于 8086CPU 的物理计算方式为:段地址x16+偏移地址,意味着一个段的起始物理地址的最后一位(十六进制)必须是0,偏移地址用一个 16位寄存器表示,因此段的最大长度限制为 64KB。
如,
10000H、1F030H,而10001H一定是非法的,因为没有地址左移后能得到末尾是 1的数。
这种方式被称为实模式寻址,随后的 x86 保护模式引入了 GDT(全局描述符表)查表机制 以增强安全性,而现代 64 位架构则进一步弱化了分段,转而通过**多级页表映射(分页机制)**来实现更高效的虚拟内存管理。
1.3.1 段寄存器
在 8086CPU 中有 4个段寄存器:CS、DS、SS、ES,指令指针寄存器为 IP。

在任意时刻,CPU 将 CS:IP 指向的内容当作指令执行。8086CPU 的工作过程可以简要描述如下:
- 初始状态,CS:2000H,IP:0000H,CPU将从内存 2000H×16+0000H处读取指令执行
- CS、IP中的内容送入地址加法器(地址加法器完成:物理地址=段地址×16+偏移地址)
- 地址加法器将物理地址送入输入输出控制电路,再将物理地址 20000H送上地址总线
- 从内存 20000H单元开始存放的机器指令 B82301通过数据总线被送入CPU
- 输入输出控制电路将机器指令 B82301送入指令缓冲器
- 读取一条指令后,IP中的值自动增加,以使CPU可以读取下一条指令。(因当前读入的指令 B82301长度为 3个字节,所以 IP中的值加 3。此时,CS:IP指向内存单元2000:0003)
- 执行控制器执行指令B82301(即mov ax,0123H)
- 指令 B82301被执行后 AX中的内容为 0123H。(此时,CPU将从内存单元2000:0003处读取指令)
- 执行指令。转到步骤(1),重复这个过程
在内存中,指令和数据没有任何区别,都是二进制信息,CPU 根据什么将内存中的信息看作指令?如果 CPU 通过 CS:IP 去读取它,它就是指令。
1.3.2 修改 CS、IP 指令
在通用寄存器章节里,我们可以通过 mov 指令设置寄存器的值,但不能用于设置 CS、IP 的值。能够修改 CS、IP 内容的指令被统称为转移指令:jmp,jmp 2AE3:3,执行后,CS=2AE3H,IP=0003H,CPU 将从 2AE33H读取指令。
如果仅修改 IP的内容,可用 jmp 某一合法寄存器 的指令完成,如:
jmp ax
执行前:ax=1000H, CS=2000H, IP=0003H
执行后:ax=1000H, CS=2000H, IP=1000H
在含义上好似 mov IP,ax(请注意并不能这样做,仅仅做出解释好理解)。
2 内存访问
在段寄存器小节结束时,我们学习到 CPU 将通过 CS寄存器读取的内存信息当作是指令执行。那么相应的,通常以 DS寄存器存放要访问的数据的段地址。
假设需要读取 10000H单元的内容:
mov bx,1000H
mov ds,bx
mov al,[0]
指令将 10000H(1000:0)中的数据读到 AL 中。这里需要注意两点:
-
指令
mov ds,1000H是非法的,需要一个寄存器中转1000H 被称为立即数,它是静态的。为了避免控制线增加,电路设计上该通路不被支持
-
新的语法
mov al,[0],其中[]表示一个内存单元,而0表示内存单元的偏移地址。然而只有偏移地址是不能定位到具体的内存单元的,因此 8086CPU 自动取 ds 中的数据为内存单元的段地址。随着现代寻址的进化,寄存器宽度(32/64位)已经足以覆盖全部内存空间,偏移地址 = 完整的虚拟地址
案例中,寄存器和内存间进行字节型的数据传送,而 8086CPU是 16位结构,是可以一次性传送 16位数据的。操作非常简单,使用 16位寄存器就能进行 16位数据传送了,如:mov ax,[0],mov [0],cx。
2.1 栈
栈的概念不必赘述,现今 CPU 中都有栈的设计,指令为 push 入栈和 pop 出栈。在进行编程时,将一段内存当作栈使用,那么 push ax 则表示将寄存器 AX中的数据送入栈中。
若我们将 10000H~1000FH 这段内存当作栈使用,这里会引出两个问题:
- 这段栈空间的地址范围如何被 CPU 所知晓?
- 当需要从栈顶取出、放入元素时,CPU 如何知道栈顶单元地址?
下面介绍 8086CPU 中两个新的寄存器,段寄存器SS 和寄存器SP,栈顶段地址存放在SS中,偏移地址存放在SP中。任意时刻,SS:SP指向栈顶元素。那么:
① 在程序启动时,SS=10000H、SP=01H 都会被校准为预设值,那么栈空间的范围能被确定下来。
② 当放入元素时,SP = SP - 机器字长;同理弹出元素时,SP = SP + 机器字长,栈顶元素永远能被确定。
在 8086CPU 以及后续 16/32/64位架构中,为了保持栈的整齐和处理速度,Intel 规定:栈操作必须是以机器字长为单位。因此,push al 是非法指令哦 ~
机器字长:16位=2、32位=4、64位=8
使用 push 和 pop 还能在内存单元之间传送数据,如:
mov ax,1000H
mov ds,ax ;内存单元地址放在ds中
push [0] ;将1000:0处的字压入栈中
pop [2] ;出栈数据送入1000:2处
从某种程度上说,这种操作能够节省寄存器,不需要占用任何通用寄存器;同时绕过 mov 指令不支持直接把一个内存单元的内容传给另一个内存单元的限制(必须一个中转站,寄存器或栈)。但他并不是没有缺点,例如性能的下降。