跳转至

中央处理单元 — 拆开这颗芯片:子系统、寄存器与取指节拍

这一章讲什么: 第 1 章把 CPU 画成一个方块,本章把方块拆开—— 里面是七个子系统和一个永不停歇的循环:取指令、译码、执行、地址加一。 然后落到 x86-64 的真实地面:16 个通用寄存器各有四五个别名 (rax/eax/ax/ah/al),一套标志位寄存器替第 3 章的进位溢出记账, 最后用调试器亲眼看见「C 变量 = 寄存器」这件事真的发生。 这是全书从「计算机是什么」转向「程序怎么跑」的转折点。

1. 顶层全景:CPU 的七个子系统和一个循环

正式拆箱之前,先交代型号:CPU 的两大制造商是 Intel 和 AMD;x86-64 的 CPU 可以跑在 32 位或 64 位模式——32 位模式叫兼容模式,能运行为 32 位或 16 位环境编译的程序;本书重点是 64 位模式(Intel 手册叫 IA-32e, AMD 手册叫长模式)22。两种模式不能在一个程序里混用,但 64 位操作系统 可以同时跑 32 位和 64 位程序。第 2 章说过 CPU 的两大职责——运算和 「根据结果决定下一步」——这一章就是这两大职责的内部解剖。

  ┌────────────────────────── CPU ──────────────────────────┐
  │  rip(指令指针)     缓存        指令寄存器                 │
  │      │              │              │                     │
  │      │  地址         │ 指令        ▼                     │
  │      ▼              ▼        控制单元(译码、发号施令)    │
  │  ┌──────────────── 总线接口 ────────────────┐            │
  │  │ 地址总线 / 数据总线 / 控制总线            │            │
  │  └──────────────────────────────────────────┘            │
  │  寄存器文件(16 × 64 位)←→ ALU ←→ 状态寄存器 rflags       │
  └──────────────────────────────────────────────────────────┘
       ▲ 指令执行周期:rip 给地址 → 取指 → 译码 → 执行
         → rip += 指令长度 → 回到开头(跳转指令例外:改写 rip)

  图说:上半部是「取指令的流水线」,下半部是「算数的工作台」;
  rflags 是工作台旁的黑板,ALU 每算一笔就在黑板上记一笔。

这个框图对应原书图 9-1,各子系统一句话身份1:

子系统 职责
指令指针(rip) 永远装「下一条指令的内存地址」
缓存 CPU 片上快速存储,提前养着将要用的指令
指令寄存器 装着当前正在执行的指令,位模式决定控制单元发什么命令
寄存器文件 一组用法相似的寄存器(x86-64 有整数文件和浮点文件)
控制单元 译码指令寄存器,产生信号指挥其他子系统(用有限状态机实现)
ALU 算术逻辑单元,执行算术和逻辑运算;CPU 自己算地址也用
状态寄存器 ALU 的运算副作用(进位、为零……)记在这里
总线接口 与内存、I/O 打交道的电路(地址/数据/控制三总线的 CPU 侧)

「高度简化的示意图。实际的 CPU 结构要复杂得多,不过本章中讨论的一般概念 适用于大多数 CPU」——原书的免责与定调2。注意「控制单元通常以 有限状态机的形式实现」:第 7 章的状态机,此刻坐在了 CPU 的驾驶座上。

2. 指令执行周期:软件的发动机

这是全书最重要的一段流程,把第 1 章的「CPU 循环取指令」具体化3:

  (1) 指令序列存在内存里
  (2) 第一条指令的地址复制进指令指针
  (3) rip 的地址送上地址总线
  (4) 控制总线发「读」信号
  (5) 内存把该地址的位状态副本送上数据总线,CPU 抄进指令寄存器
  (6) rip 自动增加 → 指向下一条指令
  (7) 执行指令寄存器里的指令
  (8) 回到 (3)

  (3)(4)(5) 合称「指令提取」(instruction fetch);
  (3)~(8) 合称「指令执行周期」。

这个循环每转一圈,世界就发生一件小事:一条指令被执行。转几百万圈, 一次键盘输入被处理;转几十亿圈,一帧游戏画面被渲染。理解这个循环 是理解一切软件行为的地基——第 13 章你会看到,「循环」「分支」「调用」 这些高级结构,全都是对这个循环中某一步的精心操纵。

而且循环里藏着程序员的默认工作模式(原书点破):「程序中的大多数指令 要用到至少一个寄存器文件中的至少一个寄存器。程序通常将数据从内存 加载到寄存器中,对数据进行操作,然后将结果存储在内存中。寄存器也 用于保存内存中数据的地址,从而作为指向数据或其他地址的指针。」24 ——load → 算 → store,这就是机器层程序的三段式;「变量」大部分时间 住在内存,干活时必须先搬到寄存器这个「工作台」。第 11 章的 mov 指令 会把这个模式演到你烦。

两个关键细节:

rip 的加法不是 +1,是「+这条指令的字节数」。 取指后,CPU 根据指令的 第一或第二字节判断它的长度,再加到 rip 上——x86-64 指令长短不一 (第 12 章细讲),rip 必须量体裁衣4。

跳转指令改写 rip。 「有些指令会改变 rip 中的地址,从而使程序从一个 位置跳转到另一个位置。在这种情况下,直到导致跳转的指令被实际执行时, 才知道下一条指令的地址。」——第 13 章的控制流和第 7 章的分支预测, 全部根系于这一句:CPU 在指令执行完之前,物理上无法预知下一条指令 在哪,所以才有「猜」的生意可做5。

补两个现代细节(原书交代为「执行细节」):大多数现代 CPU 有指令队列—— 取指电路提前把若干条指令排队备着,控制单元执行时队列不断被补满; 但这只是加速,「控制单元执行程序的本质可以用单指令寄存器模型来表示」6。 另外 x86-64 支持 rip 相对寻址:用「rip + 固定偏移」访问数据, 让程序可以装进内存任意位置执行,安全性也更好;第 11 章用它进函数内部7。

3. x86-64 寄存器:一张考古现场

3.1 全家桶与分工

x86-64 的寄存器按用途分类(表 9-1):16 个 64 位通用寄存器、1 个 标志寄存器、1 个指令指针、6 个段寄存器;另有浮点寄存器组(80 位数据 ×8 等)、 8 个 MMX、16 个 XMM、模型特定寄存器(MSR,操作系统专用)8。 本书的编程概念基本只用通用寄存器——它们处理整数类型(int、char、 布尔、地址);浮点寄存器留到第 19 章;MMX/XMM 超出范围。

3.1.1 寄存器和内存,在指令里看起来的差别

一个容易被滑过去的观察:原书说「机器指令通过地址访问 CPU 寄存器, 就像访问内存一样」——但两个地址空间是分开的:寄存器地址出现在 CPU 内部总线上,内存地址才走总线接口。从程序员视角,区别落在命名权上: 寄存器用汇编器预定义的名字(rax、rbx……),内存用程序员自己起的 符号名23。所以汇编程序里两种名字混在一起出现,而 register 变量不能有地址的原因也在这里——没有内存地址可寻。

3.2 通用寄存器的命名考古

每个 64 位寄存器的位从右到左编号 0~63。计算机指令把「若干位当一个单元」 处理,这套单元的老名字:字(word)= 16 位、双字(doubleword)= 32 位、 四字(quadword)= 64 位(从 16 位时代的 8086 一路沿用至今)9。

同一批位,按访问宽度有不同名字(表 9-2)——这是 x86 最劝退的一张表, 拆一个 rax 就懂了:

  位号:  63 ────────── 31 ── 15  8   7 ── 0
                 rax       │ ah │  al
                           │  ax(0~15)
                     eax(0~31)
          rax(0~63)

  rax  全部 64 位     eax  低 32 位    ax  低 16 位
  ah   位 8~15(仅 4 个老寄存器有)    al  位 0~7

  图说:r 前缀 = 64 位,r8~r15 是 x86-64 新增的 8 个;
  老的 8 个(ax/bx/cx/dx/si/di/sp/bp)从 8086 的 16 位、80386 的 32 位(e 前缀)
  一路加前缀而来——这就是「考古现场」:名字是地层,不是设计。

三条使用规则,每条都踩过坑才写得出10:

  1. 写 8/16 位部分不影响高位;写低 32 位会把高 32 位清零(原书括号: 「我也不知道 CPU 设计师为什么会选择这种做法,实在是很奇怪」—— 这句吐槽值得记住,读汇编时经常撞见);
  2. ah/bh/ch/dh 不能和 sil/dil/bpl/spl 出现在同一条指令里(老字节 vs 新字节 的编码冲突);
  3. 32 位模式的程序只能用老 8 个;64 位模式全开。

判断(我们的,不是书里的): 这套命名是「向后兼容税」的典型样本—— 每一代 CPU 都舍不得废掉上一代的汇编代码,于是名字越叠越多。 学习成本被转嫁给了程序员,但生态的连续性保住了(1980 年代的 DOS 程序 的算术部分今天还能跑)。读寄存器名的口诀:先找位号,再读前缀—— 名字会骗人(如 esp 在 64 位程序里是 rsp 的一部分),位号不会。 如果错,会错在: RISC 架构(ARM、RISC-V)的寄存器命名干净得多 (r0、r1……),x86 的混乱是历史偶然而非必然;别把「难记」当成「所有 CPU 都这样」(补充,不在书里,来自通用知识)。

3.3 两个不能乱动的寄存器:rsp 与 rbp

「尽管名为通用,某些指令以特殊的方式使用其中部分寄存器」——最重要的例外:

  • rsp:栈指针(stack pointer);
  • rbp:帧指针(frame pointer)。

两者跟踪调用栈(call stack)——程序运行期间函数之间传递信息的临时 存储区(第 11、14 章的主场)。「一些机器指令隐式用到了 rsp 寄存器」 (push/pop/call/ret 全自动动它);「rsp 和 rbp 寄存器的用法有一套非常 严格的协议……必须仔细遵循协议」——提前预告:汇编里一大半的谜之崩溃, 都是踩了这条协议11。

其他限制还有:乘除法指令硬绑定 rax/rdx;参数寄存器随环境而变—— GNU/Linux 下第一个参数走 rdi,Windows 下走 rcx(本书按 Linux 讲, 规则集中在第 11 章)12。

3.4 rflags:状态标志的注册表

状态寄存器 rflags:我们只关心被用作状态标志的几位(图 9-4 只画 低 12 位;高 32 位保留,12~31 是系统标志)13。六个标志,第 3 章的 老朋友在这里正式挂牌:

标志 名字 什么时候置 1
CF 进位标志 进位/借位溢出结果最高位(无符号装不下)
OF 溢出标志 有符号(补码)算术溢出
SF 符号标志 结果最高有效位的副本(即结果的符号)
ZF 零标志 结果为 0
AF 辅助进位标志 BCD 算术的进位/借位(第 2 章 BCD 的遗产)
PF 奇偶标志 结果最低字节里有偶数个 1

「有些专门测试状态标志的机器指令。例如,如果状态标志 ZF 为 1, 有一条指令可以跳转到程序中的另一个位置」——第 3 章的 CF/OF 分工表, 在这里长出了牙齿:if/while 的全部判断,就是查这些位再跳不跳14。 第 13 章讲条件跳转时会逐个对上号。

4. C 数据类型的大小:语言与硬件的接口

数据类型 = 取值范围 + 位模式 + 可执行的操作 + 语义用法。C 语言规范 只规定下限(int 至少能存 −32 768~+32 767,即至少 16 位),具体多大 由编译器设计者决定15。x86-64 下 gcc/g++ 的口径(表 9-4):

  char      1 字节        long    64 位模式 = 四字(32 位模式 = 双字!)
  int       双字(32 位)    float   双字
  long long 四字(64 位)    double  四字
  任意指针 *any  双字(32 位)/四字(64 位)  ← 指针跟着模式走

  图说:long 和指针是两种模式差一倍的类型——移植 32 位老代码时最常咬人。

「别指望这些大小总会相同」;C 标准库给了固定宽度的类型(int16_t、 u_int16_t)——「如果你的解决方案依赖于数据大小」,用它们16。

一个值可以有多种位模式身份,本章的例子值得带走:123 作为 int 是 0x0000007b;作为 C 字符串是 4 个字节 0x31 0x32 0x33 0x00("123\0")17—— 又是第 2 章那个对照(整数 vs 字符串),现在是「同一个数,两种类型,两套字节」。 类型不是值身上的标签,而是读写规则的选择器。

5. 主线例子:在 gdb 里看寄存器上班

程序 inches2feet.c:读入英寸数,换算成「几英尺几英寸」。作者给 feet 和 inchesRem 加了 register 修饰符❶,请求编译器把变量 放进寄存器(register 只是建议,标准不保证);inches 不行—— scanf 需要它的地址(指针)❸,而寄存器没有地址(第 8 章: 寄存器在 CPU 内部总线上寻址,不在内存地址空间里)18。

调试会话的关键几步(原书实录的浓缩)19:

  (gdb) b 17          ← 断点设在 printf 提示处
  (gdb) b 20          ← 断点设在计算处
  (gdb) r             ← 运行,停在断点 1
  (gdb) i r           ← 显示全部通用寄存器
      rax 0x7fffffffdeac ... rip 0x...<main+38> eflags 0x246 [PF ZF IF] ...

  (gdb) print &feet          ← 「Address requested for identifier "feet"
                                which is in register $r12」
  (gdb) print &inchesRem     ← 「... in register $rbx」
  (gdb) print &inches        ← $1 = (int *) 0x7fffffffdeac  ← 真内存地址

  图说:print &变量 是侦察器:变量在寄存器里,gdb 直接告诉你它占了哪个;
  变量在内存里,给出地址。register 请求有没有被满足,一 print 便知。

第一次 i r 的输出值得多看一眼,因为它把前三章的伏笔全收了:

  rip    0x5555555551af  <main+38>     ← 指令指针:停在 main 函数体内第 38 字节处
  rsp    0x7fffffffdea0                ← 栈指针:0x7fff... 开头的大地址(第 11 章展开)
  rbp    0x7fffffffded0                ← 帧指针,和 rsp 挨得很近(同一片栈区)
  eflags 0x246 [ PF ZF IF ]            ← 标志寄存器:gdb 把置 1 的标志名字列了出来
  cs/ss/ds/es/fs/gs                    ← 段寄存器:值是 0x33/0x2b 这类小数字,本章不讲

  图说:rip 指向代码段(0x5555... 低地址),rsp/rbp 指向栈(0x7fff... 高地址)——
  程序的两种「居所」在寄存器值上一眼可辨;eflags 里此刻 ZF=1 只是因为
  上一条指令恰好算出 0,标志位每算一笔就变一次,别当成常数读。

输入 123 后停在断点 2,走两条语句(n)再查:

  (gdb) i r rbx r12
  rbx   0x3    3     ← inchesRem = 123 % 12 = 3
  r12   0xa    10    ← feet = 123 / 12 = 10
  (gdb) print $rbx   → 3      (print 看寄存器要加 $ 前缀)
  (gdb) print feet   → 10     (print 变量名也行)
  程序输出:123" = 10' 3"      ✓

  图说:一次完整的「请求 → 落实 → 验证」闭环:register 建议 → 编译器分配
  r12/rbx → gdb 全程可见。C 变量的「居所」第一次被打开给你看。

三个顺手学下的 gdb 命令:n(next,过函数)、s(step,进函数)、 si(step instruction,一次一条机器指令——第 12 章的利器)、 i r [寄存器名](不指定就显示全部;注意 i r 中间有空格)20。

6. 作者的判断与证据

判断一:单指令寄存器模型是「本质」,指令队列是「细节」。 「控制单元执行程序的本质可以用单指令寄存器模型来表示,这正是我在这里 要描述的」——作者明确选择用简化的模型教本质,把队列、流水线的并行 全部标为「执行细节」6。这个取舍与第 7 章(主/次触发器是理解模型、 非工业实现)一脉相承:先给你能复述的模型,再告诉你真机哪里不同。

判断二:gdb 是学习工具,不只是排错工具。 本章是继第 2 章之后 第二次系统使用 gdb——这次的目的纯粹是「让看不见的东西显形」: 寄存器分配、变量居所、标志位。证据是例子的设计:没有 bug 要抓, 只有真相要看。「除了帮助查找 bug,gdb 还有助于学习各种概念」, 原书在小结里把这句话收进了官方结论21。

判断三:命名考古不用背,但要认得地层。 rax/eax/ax/ah/al 的来历 (8086 → 80386 加 e → x86-64 加 r 并新增 r8~r15)原书讲了整整一段, 还顺手吐槽了「写低 32 位清高位」的设计10。这种「知其所以然」的 讲法降低了记名字的成本:你不再背表,而是在读一部家族兴衰史。

判断四:本章是全书的枢纽章。 往回望,它把第 1 章的三总线、 第 3 章的标志位、第 7 章的有限状态机、第 8 章的寄存器文件全部收拢; 往前看,第 10~21 章的每一行汇编都默认你知道 rip 怎么走、rflags 谁在写、 rsp 为什么动不得。判断依据是本章小结的清单:通用寄存器、状态寄存器、 指令指针、指令寄存器、ALU、控制单元、总线接口、缓存、指令执行周期、 C 数据类型大小、调试器——后面十一章用到的每一个硬件名词,都在这张 清单里预先报了名25。

判断五:作者敢承认「我也不知道」。 「(我也不知道 CPU 设计师为什么 会选择这种做法,实在是很奇怪)」——这句括号吐槽放在教科书里罕见。 它的价值不在吐槽本身,而在示范了一种读规格书的健康姿态: 有些设计就是历史偶然,追不到为什么;记住行为(写低 32 位清高位), 放下执念(找深层理由),照样能写好程序。

7. 边界与局限

  • 流水线、乱序执行、多核一概未提——本章的 CPU 是「一次一条指令」 的串行模型;真实 CPU 的并行(第 7 章的流水线只是预告)在本书范围内 点到为止;
  • 段寄存器 cs/ss/ds 等只出现在 gdb 输出里,没有讲解(x86 分段的 历史包袱,64 位模式下基本闲置;第 21 章讲特权级时会再见 cs);
  • 浮点寄存器(x87 的 80 位栈、MMX、XMM)只列了清单,第 19 章才用;
  • rflags 只教了「读」,没教「写」——哪些指令会改哪些标志、 测试类指令(test/cmp)如何故意设置标志再让跳转消费,是第 12、13 章的内容; 本章只需记住「ALU 每算一笔,rflags 就被覆盖一笔,旧值不保留」;
  • gdb 显示的地址全部因机器而异,复现实验时数字几乎必然不同—— 看结构,别看数值;
  • register 修饰符在现代 C 里基本被编译器无视(优化器自己决定 哪些变量进寄存器);它的教学价值大于实用价值(补充,不在书里, 来自通用知识)。

8. 可带走的

全章主线例子,一行走完: inches2feet 程序 → register 请求 → gdb print &feet 暴露 feet 住在 r12、inchesRem 住在 rbx → 断点处 i r rbx r12 亲见 123 被除成 10 和 3 ——C 变量 = 寄存器, 从传闻变成亲见。 本章是全书的分水岭:前半部(1~8 章)讲「计算机怎么造」,后半部 (10~21 章)讲「程序怎么在上面跑」;分水岭本身,就是这条 「取指 → 译码 → 执行 → rip 前进」的循环。

  1. CPU 七子系统:rip、缓存、指令寄存器、寄存器文件、控制单元、ALU、 rflags、总线接口;控制单元是有限状态机(第 7 章的即视感);
  2. 指令执行周期:rip 给地址 → 取指(地址+读+抄进指令寄存器)→ 译码 → 执行 → rip += 指令长度 → 循环;
  3. 跳转指令在执行完之前,下一条指令的地址物理上未知——分支预测的 存在理由;
  4. 寄存器名按位号认:r 前缀 64、e 前缀 32、无前缀 16、h/l 是字节地层; 写低 32 位会清高位(官方吐槽过);
  5. rsp = 栈指针、rbp = 帧指针,有严格协议,不能当普通变量用(第 11 章详述);
  6. 乘除法绑 rax/rdx;参数寄存器 Linux 走 rdi、Windows 走 rcx—— 汇编规则随环境变,移植先看调用约定;
  7. rflags 六标志:CF(无符号装不下)、OF(有符号溢出)、SF(符号)、 ZF(结果为 0)、PF(奇偶)、AF(BCD);if/while 全靠查它们;
  8. C 类型大小由编译器定:long 和指针在 32/64 位模式差一倍; 依赖大小就用 int16_t 这类固定宽度类型;
  9. 同一个值按不同类型是不同位模式:123 → int 是 0x7b,char[] 是 "123\0"——类型是读写规则的选择器;
  10. 寄存器没有内存地址,所以 scanf 的目标必须住内存—— 「取地址 &」是内存公民的专属动作;
  11. gdb:n 过、s 进、si 逐指令、i r [名] 看寄存器、 print $rax 看单个寄存器值、print &变量 查变量居所;
  12. 数字会变、结构不变:gdb 里的地址每次都不同,但「变量 → 寄存器/栈」 的分配逻辑每次都一样;
  13. 第一次 i r 是伏笔回收现场:rip 指代码、rsp/rbp 指栈、eflags 报标志—— 前三章的概念在一张输出里全部到场;
  14. 「load → 算 → store」是机器层程序的三段式:变量住内存,干活先搬寄存器, 结果再搬回去——mov 指令存在的全部理由;
  15. 本章小结那张清单是全书后半部的「点名册」:后面十一章的每个硬件名词, 都已在第 9 章预先报到;
  16. 「我也不知道为什么」是健康的规格书阅读姿态:行为记住,执念放下;
  17. 跳转指令执行前地址未知 = 分支预测的存在理由——这个因果链背下来, 体系结构课的一半悬念提前解扣;
  18. 寄存器名按位号认、按地层记:r 前缀是新房,e 前缀是 386 加层, h/l 是 8086 的老家具——名字是历史,不是逻辑;
  19. gdb 侦察口诀:print &变量 看居所、i r 看全员、print $rax 看单个;
  20. scanf 的目标必须住内存(寄存器没地址)——「变量进不了寄存器」 的第一手案例,比任何说教都好记;
  21. rflags 的标志每算一笔就被覆盖一笔——想查就紧跟着查,隔一条指令就晚了;
  22. 64 位模式是本书的默认世界:所有寄存器、所有指针、所有地址,都按这个前提讲;
  23. 本章开始,每条 C 语句在你眼里都应该有一层汇编的影子—— 这是分水岭这一侧的基本功;
  24. 地址会变、结构不变,这条规矩从第 2 章一路用到本章的 gdb 输出。

9. 原文地图

本章地图覆盖第 9 章全文;行号以转码文本(一行 = 一个自然段)为准, 机器可用 book-verify --fix 校正漂移。

主题 原书章 原文位置
CPU 子系统概览 第9 章 中央处理单元 text/15-ch09.txt:36(搜「指令指针」)
「高度简化的示意图」 第9 章 中央处理单元 text/15-ch09.txt:30(搜「简化」)
指令执行周期八步 第9 章 中央处理单元 text/15-ch09.txt:88(搜「步骤」) · text/15-ch09.txt:101(搜「指令提取」)
rip 加指令长度 第9 章 中央处理单元 text/15-ch09.txt:73(搜「长度」)
「直到执行时才知道下一条地址」 第9 章 中央处理单元 text/15-ch09.txt:76(搜「跳转到另一个位置」)
rip 相对寻址 第9 章 中央处理单元 text/15-ch09.txt:78(搜「rip 相对寻址」)
指令队列是执行细节 第9 章 中央处理单元 text/15-ch09.txt:84(搜「指令队列」)
x86-64 寄存器分类(表 9-1) 第9 章 中央处理单元 text/15-ch09.txt:123(搜「表 9-1」)
字/双字/四字 第9 章 中央处理单元 text/15-ch09.txt:59(搜「9.1 CPU 概述」)
寄存器位组命名(表 9-2) 第9 章 中央处理单元 text/15-ch09.txt:172(搜「表 9-2」)
写低 32 位清高位(作者吐槽) 第9 章 中央处理单元 text/15-ch09.txt:56(搜「设置为 0」)
命名考古(8086/80386/r8~r15) 第9 章 中央处理单元 text/15-ch09.txt:199(搜「8086」) · text/15-ch09.txt:206(搜「e 前缀」)
rsp/rbp 栈指针与帧指针 第9 章 中央处理单元 text/15-ch09.txt:220(搜「栈指针」)
乘除绑 rax/rdx、参数寄存器因环境而异 第9 章 中央处理单元 text/15-ch09.txt:226(搜「乘法和除法指令」) · text/15-ch09.txt:231(搜「rdi」)
rflags 与六标志(表 9-3) 第9 章 中央处理单元 text/15-ch09.txt:237(搜「rflags」) · text/15-ch09.txt:246(搜「表 9-3」)
C 数据类型大小(表 9-4) 第9 章 中央处理单元 text/15-ch09.txt:270(搜「表 9-4」) · text/15-ch09.txt:14(搜「long」)
int16_t 固定宽度类型 第9 章 中央处理单元 text/15-ch09.txt:286(搜「int16_t」)
123 的两种位模式身份 第9 章 中央处理单元 text/15-ch09.txt:293(搜「0x0000007b」)
register 修饰符与 scanf 必须住内存 第9 章 中央处理单元 text/15-ch09.txt:332(搜「register」) · text/15-ch09.txt:144(搜「指针」)
n/s/si/i r 命令 第9 章 中央处理单元 text/15-ch09.txt:339(搜「next」) · text/15-ch09.txt:346(搜「info register」)
gdb 会话实录(r12/rbx 抓获现场) 第9 章 中央处理单元 text/15-ch09.txt:438(搜「in register $r12」) · text/15-ch09.txt:488(搜「0xa」)
小结:gdb 是学习工具 第9 章 中央处理单元 text/15-ch09.txt:561(搜「学习各种概念」)
兼容模式与 64 位模式 第9 章 中央处理单元 text/15-ch09.txt:11(搜「兼容模式」)
寄存器与内存是两套地址空间 第9 章 中央处理单元 text/15-ch09.txt:113(搜「地址访问」)
第一次 i r 输出(eflags/rsp/rbp) 第9 章 中央处理单元 text/15-ch09.txt:403(搜「i r」) · text/15-ch09.txt:423(搜「eflags」)
「load → 算 → store」三段式 第9 章 中央处理单元 text/15-ch09.txt:106(搜「寄存器」)
作者吐槽写低 32 位清高位 第9 章 中央处理单元 text/15-ch09.txt:194(搜「我也不知道 CPU 设计师」)
「直到执行时才知道」下一条地址 第9 章 中央处理单元 text/15-ch09.txt:76(搜「跳转到另一个位置」)
命名考古(8086 老名字) 第9 章 中央处理单元 text/15-ch09.txt:199(搜「8086」)

  1. 出处:「第9 章 中央处理单元」第 36 段(text/15-ch09.txt:36,搜「指令指针」)。原文:「指令指针:该寄存器始终包含待执行的下一条指令的内存地址」「寄存器文件是一组用法相似的寄存器」「控制单元通常以有限状态机的形式实现,包含译码器、复用器和其他逻辑元件」「ALU 用于执行程序中指定的算术和逻辑运算」「状态寄存器……ALU 完成可能产生进位的运算之后,状态寄存器中的某一位将被设置为 0(无进位)或 1(进位)」。 ↩

  2. 出处:「第9 章 中央处理单元」第 30 段(text/15-ch09.txt:30,搜「简化」)。原文:「记住,这是一个高度简化的示意图。实际的 CPU 结构要复杂得多,不过本章中讨论的一般概念适用于大多数 CPU。」 ↩

  3. 出处:「第9 章 中央处理单元」第 88 段(text/15-ch09.txt:88,搜「步骤」)。原文给出指令执行周期的 8 个步骤;「步骤(3)、(4)、(5)称为指令提取(instruction fetch)」「步骤(3)~(8)形成了一个周期,称为指令执行周期(instruction execution cycle)」见第 101~104 段(text/15-ch09.txt:101,搜「指令提取」)。 ↩

  4. 出处:「第9 章 中央处理单元」第 73 段(text/15-ch09.txt:73,搜「长度」)。原文:「根据指令的不同,第一或第二字节告诉 CPU 该指令的长度(字节数)。然后,CPU 将这个数字与 rip 寄存器的值相加,使得 rip 包含程序中下一条指令的地址。」 ↩

  5. 出处:「第9 章 中央处理单元」第 76 段(text/15-ch09.txt:76,搜「跳转到另一个位置」)。原文:「有些指令会改变 rip 中的地址,从而使程序从一个位置跳转到另一个位置。在这种情况下,直到导致跳转的指令被实际执行时,才知道下一条指令的地址。」 ↩

  6. 出处:「第9 章 中央处理单元」第 84 段(text/15-ch09.txt:84,搜「指令队列」)。原文:「大多数现代 CPU 都使用了指令队列。若干条指令在队列中等待执行。当常规控制单元执行指令时,由单独的电路保持指令队列满。但这只是一个执行细节,可以让控制单元运行得更快。控制单元执行程序的本质可以用单指令寄存器模型来表示,这正是我在这里要描述的。」 ↩↩

  7. 出处:「第9 章 中央处理单元」第 78 段(text/15-ch09.txt:78,搜「rip 相对寻址」)。原文:「x86-64 架构还支持 rip 相对寻址,允许程序访问距离 rip 中当前地址有一段固定距离的内存位置。这允许我们创建一个可以加载到内存任意位置执行的程序,获得更好的安全性。」 ↩

  8. 出处:「第9 章 中央处理单元」第 123 段(text/15-ch09.txt:123,搜「表 9-1」)。原书表 9-1 分类列出 x86-64 寄存器(通用 16×64 位、标志 1、指令指针 1、段 6×16 位、浮点 8×80 位等、MMX 8×64 位、XMM 16×128 位、MSR 视实现)。 ↩

  9. 出处:「第9 章 中央处理单元」第 59 段(text/15-ch09.txt:59,搜「9.1 CPU 概述」)。原文:「在早期,这个单元被称为字(word)。每种 CPU 架构都有特定的字长……因此,一个字为 16 位,双字为 32 位,四字为 64 位。」 ↩

  10. 出处:「第9 章 中央处理单元」第 56 段(text/15-ch09.txt:56,搜「设置为 0」)至第 209 段(text/15-ch09.txt:209,搜「r 前缀」)。原文:「当写入寄存器的 8 位或 16 位部分不会影响到余下的 56 位或 48 位。然而,当写入低 32 位时,高 32 位会被设置为 0。(我也不知道 CPU 设计师为什么会选择这种做法,实在是很奇怪。)」「80386 将寄存器的大小从 16 位增加到 32 位。没有增添新的寄存器,只是将旧的寄存器进行了简单的扩展。」 ↩↩

  11. 出处:「第9 章 中央处理单元」第 220 段(text/15-ch09.txt:220,搜「栈指针」)。原文:「rsp 寄存器被用作栈指针,rbp 寄存器被用作帧指针。两者用于跟踪数据项在调用栈中的位置」「rsp 和 rbp 寄存器的用法有一套非常严格的协议……在使用汇编语言编程时,必须仔细遵循协议。」 ↩

  12. 出处:「第9 章 中央处理单元」第 226 段(text/15-ch09.txt:226,搜「乘法和除法指令」)与第 231 段(text/15-ch09.txt:231,搜「rdi」)。原文:「无符号乘法和除法指令使用 rax 和 rdx 寄存器」「函数的第一个参数要放在 rdi 寄存器中传递,但如果是在 64 位 Windows 环境下使用 C 语言,第一个参数则要放在 rcx 寄存器中传递。」 ↩

  13. 出处:「第9 章 中央处理单元」第 237 段(text/15-ch09.txt:237,搜「rflags」)与第 240 段(text/15-ch09.txt:240,搜「保留」)。原文:「该寄存器被命名为 rflags。我们关注的是其中被用作状态标志的几个位」「高 32 位(32~63)保留用于其他用途……位 12~31,这些是系统标志位。」 ↩

  14. 出处:「第9 章 中央处理单元」第 256 段(text/15-ch09.txt:256,搜「ZF」)。原文:「有些专门测试状态标志的状态的机器指令。例如,如果状态标志 ZF 为 1,有一条指令可以跳转到程序中的另一个位置。」 ↩

  15. 出处:「第9 章 中央处理单元」第 59 段(text/15-ch09.txt:59,搜「9.1 CPU 概述」)。原文:「C 和 C++语言规范规定了每种数据类型的取值范围。例如,int 类型必须能够存储–32 768~+32 767 范围内的值」「编译器设计者可以自行决定是否超过语言规范中规定的最小值。」 ↩

  16. 出处:「第9 章 中央处理单元」第 271 段(text/15-ch09.txt:271,搜「别指望」)与第 286 段(text/15-ch09.txt:286,搜「int16_t」)。原文:「你应该注意,别指望这些大小总会相同」「如果你的解决方案依赖于数据大小,C 标准库通常定义了特定的大小。例如,GNU C 库将 16 位有符号整数定义为 int16_t。」 ↩

  17. 出处:「第9 章 中央处理单元」第 293 段(text/15-ch09.txt:293,搜「0x0000007b」)。原文:「在我们使用的 C/C++环境中,int 类型以双字存储,所以 123 的位模式为 0x0000007b。对于 C 风格的文本字符串,我们需要 4 字节的内存,但是位模式为 0x31、0x32、0x33、0x00,即字符 1、2、3、NUL。」 ↩

  18. 出处:「第9 章 中央处理单元」第 332 段(text/15-ch09.txt:332,搜「register」)与第 144 段(text/15-ch09.txt:144,搜「指针」)。原文:「register 修饰符仅仅是一种建议。C 语言标准并不要求编译器一定要满足请求」「变量 inches 必须位于内存中,因为 scanf 需要使用指向 inches 的指针来存储从键盘读入的值。」 ↩

  19. 出处:「第9 章 中央处理单元」第 438 段(text/15-ch09.txt:438,搜「in register $r12」)至第 496 段(text/15-ch09.txt:496,搜「print feet」)。gdb 实录:print &feet 报告变量在 $r12,print &inchesRem 报告在 $rbx;计算后 i r rbx r12 显示 0x3 和 0xa。 ↩

  20. 出处:「第9 章 中央处理单元」第 339 段(text/15-ch09.txt:339,搜「next」)、第 341 段(text/15-ch09.txt:341,搜「step」)、第 344 段(text/15-ch09.txt:344,搜「step instruction」)、第 346 段(text/15-ch09.txt:346,搜「info register」)。原文:「n(next):执行当前源代码语句;如果是函数调用,则执行整个函数」「si(step instruction):执行当前机器指令」「i r(info register):显示寄存器的内容」。 ↩

  21. 出处:「第9 章 中央处理单元」第 561 段(text/15-ch09.txt:561,搜「学习各种概念」)。原文:「调试器 除了帮助查找 bug,gdb 还有助于学习各种概念。」 ↩

  22. 出处:「第9 章 中央处理单元」第 11 段(text/15-ch09.txt:11,搜「兼容模式」)。原文:「x86-64 CPU 可以在 32 位或 64 位模式下运行。32 位模式称为兼容模式(compatibility mode)」「Intel 手册和 AMD 手册中分别称为 IA-32e 和长模式(long mode),本书中称为 64 位模式。」 ↩

  23. 出处:「第9 章 中央处理单元」第 113 段(text/15-ch09.txt:113,搜「地址访问」)。原文:「机器指令通过地址访问 CPU 寄存器,就像访问内存一样。当然,寄存器的寻址空间与内存寻址空间是分开的。寄存器地址出现在 CPU 内部总线上,而非总线接口的地址部分」「从程序员的角度来看,区别在于汇编器为寄存器预定义名称,而程序员则为内存地址创建符号名称。」 ↩

  24. 出处:「第9 章 中央处理单元」第 106 段(text/15-ch09.txt:106,搜「寄存器」)。原文:「程序中的大多数指令要用到至少一个寄存器文件中的至少一个寄存器。程序通常将数据从内存加载到寄存器中,对数据进行操作,然后将结果存储在内存中。寄存器也用于保存内存中数据的地址,从而作为指向数据或其他地址的指针。」 ↩

  25. 出处:「第9 章 中央处理单元」第 549 段(text/15-ch09.txt:549,搜「小结」)。原书 9.5 小结列出:通用寄存器、状态寄存器、指令指针、指令寄存器、算术逻辑单元、控制单元、总线接口、缓存、指令执行周期、C/C++数据类型大小、调试器。 ↩