跳转至

控制流结构 — 循环与分支在机器层只是「跳转」的两种用法

这一章讲什么: 你在 C 里写的循环和分支,到机器层只剩下一种操作——改写指令指针 rip。 循环是「条件不满足就跳回前面」,分支是「条件满足就跳过一段」。本章把 C 的六种控制流结构 逐个对照到汇编,你会看到编译器的惯用手法,也会亲手写出一种编译器没选的写法(跳表)。 主线例子: 一个不用 printf、一次写一个字符的 "Hello, World!" 程序, 指针沿着字符串走到 NUL 为止——循环的每个零件都在它身上。

1. 先立框架:三种控制流,两类跳转

书里把编程范式先分了个类:指定语句执行顺序来编程,叫命令式编程(imperative programming); 只声明计算逻辑、由别的程序决定执行顺序,叫声明式编程——你写过的 makefile 就是声明式的1。 C 和汇编都是命令式的,而命令式的全部家当,就是三种控制流2:

控制流 你写的 机器层的动作
顺序 逐条语句 rip 自动 +1(取指后递增)
迭代 while / for / do-while 条件不满足就跳回代码块开头
选择 if / if-else / switch 条件不满足就跳过一个代码块

函数调用是第四种(「高级控制流操作」),第 14 章专讲。本章全部内容, 就是「跳转」这个单一动作在六种语法里的不同摆法——记住这个视角, 后面每种结构你只需要看「条件测试放在哪、往哪跳」。

2. 跳转指令:条件从哪来

无条件跳转 jmp

jmp label 直接把 rip 改成 label 的地址(距离照样是相对下一条指令的偏移量); jmp reg 把寄存器里的 64 位值复制进 rip3。它单独用的时候不多, Mostly 是搭在条件跳转旁边:跳过一个代码块、跳回一个代码块的开头。

条件跳转 jcc:读 rflags 的化验单

jcc 不比较任何数——它只读 rflags 状态标志的组合,组合为真就改 rip,否则不动。 所以它的铁律在第 12 章见过:必须紧跟那个「做运算、留化验单」的指令(cmp、test 等), 中间隔一行,化验单就被重写了4。

除 jcc 外还有一组小众的:jcxz / jecxz / jrcxz——不看 rflags, 只看 rcx/ecx 是不是 0,是就跳(限短跳转 −128~+127)5。

表 13-1:一张表管 30 个助记符

jcc 的 cc(条件)共 16 种组合,但有些组合有多个助记符,凑出 30 多个跳转指令。 书里的排法很讲究:左边一组管无符号值,右边一组管有符号值6:

无符号(高于/低于) 有符号(大于/小于) 都测什么
ja / jna(高于/不高于) jg / jng(大于/不大于) 大小
jae / jnae(高于或等于/否则) jge / jnge 大小或相等
jb / jnb(低于/不低于) jl / jnl(小于/不小于) 大小
jbe / jnbe jle / jnle 大小或相等
jc / jnc(进位/无进位) jo / jno(溢出/无溢出) CF / OF
jz / jnz(零/非零) je / jne(相等/不等) ZF
jp / jnp(奇偶) js / jns(负/非负) PF / SF

「高于」和「大于」的区别,是这张表的命门,也是第 3 章有符号/无符号的账在这里结的: 同一个位模式 0xffffffff,按无符号读是 4294967295,「高于」0x00000001; 按有符号读是 −1,「小于」+17。用错一组,程序不报错、结果静默地错—— 无符号计数器用了 jl,有符号温度用了 jb,是这类 bug 的标准长相。

作者的小窍门:忘了比较方向,就用 gdb

书里坦白:连作者自己用 jg/jl 时也经常忘「谁跟谁在比」。他的办法不是背, 而是在条件跳转处设断点,断下来看两个值,再单步看跳的方向8。 这个习惯值得原样收下:机器层的比较方向和 C 的阅读方向相反(cmp a, b 算的是 a−b), 不验证一次,谁也不敢说自己记对了。

3. 迭代:三种循环,一个模子

while 的完整语义,四个动作的循环

书里把 while 的执行顺序拆成四个动作,值得先建立这个坐标系9:

初始化循环控制变量          ← 只做一次
循环顶部:表达式求值         ← 每次循环都要来报到
    假 → 直接退出,执行循环后的语句
    真 → 执行循环体,改变循环控制变量,回到顶部

两个细节:一是「真/假」就是「非 0 / 0」,C 的布尔上下文就是按这个判的; 二是测试在顶部意味着空字符串之类「一上来就该结束」的情况能正确跳过循环体—— 这是 while 与 do-while 的分界线9。

主线例子登场:一次写一个字符的 Hello World

第 11 章我们用 write 输出字符串时,得先告诉它长度(14)。 本章的 C 程序(清单 13-1)改用一个循环绕开这个要求:指针从字符串头出发, 写一个字符、指针 +1,直到撞上 NUL 哨兵值为止9:

char *stringPtr = "Hello, World!\n";
while (*stringPtr != NUL) {
    write(STDOUT_FILENO, stringPtr, 1);
    stringPtr++;
}

哨兵值(sentinel)这个说法记一下:标记数据序列结束的唯一值。 用它的好处是循环对任意长度的字符串都成立,长度根本不用提前知道10。

while 在机器层的两种摆法

在摆汇编之前,先过一遍 C 版代码的两个语法点,它们后面都会在汇编里找到对应物:

  • #define NUL '\0' 给终止字符起了符号名。用符号不用裸 0,是为了让 「我们在跟 NUL 比较」这件事在字面上显形9;
  • stringPtr 是指针变量:编译器把字符串本体放进只读内存, 把首字符的地址存进这个 8 字节的栈上变量。stringPtr++ 加的不是 1 个字符, 是 1 个字节的地址——它让指针滑向串里的下一个字符9。

摆法一:编译器的。 清单 13-2 里 gcc 生成的代码看着别扭:它先 jmp .L2 跳到循环底部, 底部做 test al, al; jne .L3 测试,不为 NUL 就跳回顶部 .L3 执行循环体11。 一次循环里因此有两跳(jmp 下去 + jne 跳回来)。作者说明:现代 CPU 有分支预测技术, 猜中方向时跳转几乎不花代价,所以这一跳不心疼——不深讲,记住名词即可12。

把编译器版逐行读一遍(注释是作者加的):

main:
  push  rbp
  mov   rbp, rsp
  sub   rsp, 16                  # 给 stringPtr 变量开 16 字节房间
  lea   rax, .LC0[rip]           # "Hello, World!\n" 的地址
  mov   QWORD PTR -8[rbp], rax   # stringPtr = 那个地址
  jmp   .L2                      # ❶ 先跳到底部做测试
.L3:                             # ← 循环体从这里开始
  mov   rax, QWORD PTR -8[rbp]   # 取 stringPtr
  mov   edx, 1                   # write 的第 3 个参数:写 1 个字符
  mov   rsi, rax                 # 第 2 个参数:字符的地址
  mov   edi, 1                   # 第 1 个参数:标准输出
  call  write@PLT                # 发出这个字符
  add   QWORD PTR -8[rbp], 1     # stringPtr++(地址加 1,不是字符加 1)
.L2:                             # ← 测试点在底部
  mov   rax, QWORD PTR -8[rbp]   # 取 stringPtr
  movzx eax, BYTE PTR [rax]      # 读出它指向的那个字节
  test  al, al                   # 是 NUL 吗?
  jne   .L3                      # 不是,回顶部继续循环
  mov   eax, 0
  leave
  ret

三处和第 11 章直接呼应:sub rsp, 16 是开房间;lea …[rip] 是位置无关取址; call write@PLT 是过 PLT 调共享库。你在 11 章学的每个零件,在这里都是现成工具。

摆法二:手写的。 清单 13-3 把测试放回循环顶部,一次循环只跳一次:

whileLoop:
    cmp  byte ptr [rsi], 0   # 是 NUL 吗?
    je   allDone             # 是,收工
    ……write……
    inc  qword ptr aString[rbp]  # 指针 +1
    jmp  whileLoop           # 跳回顶部

主线例子走一遍: 第一次到 cmp 时,rsi 指着 'H'(0x48),不为 0 → write 把 H 搬上屏幕, inc 让指针挪到 'e'(0x65);……第 14 次测试时,指针已到 '\n'(0x0a),仍不为 0, write 输出换行,inc 挪到 NUL(0x00);第 15 次 cmp 终于相等,je 带走 rip,循环结束9。 14 个字符 = 14 次循环 + 15 次测试;哨兵值自己就是最后一次测试的判据。

这段代码带出新指令四条,都收进工具箱:

  • movzx(以零扩展移动):8 位值进 32/64 位寄存器,高位补 0。编译器用它把字符读进 eax13;
  • test(测试):按位 AND,只设标志、不改操作数。test al, al 是「这是不是 0」的标准写法14;
  • cmp(比较):算「第一个 − 第二个」的减法,只设标志、不改操作数。所有大小比较的源头15;
  • inc(递增):加 1,会改标志。stringPtr++ 和 i++ 都是它16。

test 和 cmp 的共同哲学值得点明:它们都是「只做化验不开药」—— 真正的「药」(改 rip)永远是下一条 jcc。

for 与 while:语法不同,汇编相同

for 循环把初始化、检查、更改三件事塞进一对括号,语义上和 while 完全等价—— 书里用 gcc 实证:同一个程序换成 for 写,生成的汇编一模一样17。 书里还交代了两个语法细节:括号里可以空着,但两个分号不能省; 以及作者的个人习惯——哪怕循环体只有一条语句也写花括号, 因为「随后修改程序时增添了其他语句,我经常会忘记加花括号」18。 这个习惯值得照搬:省两字符的代价是改代码时埋雷。

for 更适合「循环次数事先已知」的计数型循环;用哪个,书里给的答案很坦诚: 「使用高级语言的循环结构,这对解决问题是自然而然的……往往是一种主观选择」18。

主线例子的 for 版:一模一样的汇编

清单 13-4 是同一程序的 for 版:

for (stringPtr = "Hello, World!\n"; *stringPtr != NUL; stringPtr++)
    write(STDOUT_FILENO, stringPtr, 1);

书里让你自己用 gcc 验证,并且提前剧透了答案:得到的汇编与 while 版的清单 13-2 相同17。 这个「剧透」本身就是本章最重要的实证:语法是给人看的,机器只看跳转。

do-while:把测试挪到结尾

do-while 的唯一区别:测试在循环体末尾,所以循环体至少执行一次。 对照清单 13-6,它和 while 的汇编差一行——少掉开头的 jmp 和顶部测试, 只省「第一次循环的一次跳转」19。

代价要记牢:书里的警告说,空字符串会出问题——do-while 先把 NUL 写上了屏幕, 然后去读 NUL 后面的字节,那属于未定义行为20。「至少执行一次」是特性也是坑, 空输入就是它的试金石。

4. 选择:if、if-else 与 switch

if:一次条件跳转

清单 13-7 的抛硬币程序:for 循环 10 次,每次取随机数,小于 RAND_MAX/2 就打印 "heads"。 程序里有个值得停一下的写法:两个变量都声明成了 register int—— register 是 C 语言给编译器的建议:「这个变量很常用,尽量放寄存器」。 从本章的汇编回头看,这个建议被 gcc 完全采纳了:两个变量一个住 r12、一个住 rbx, 全程没有进过内存21。

它的汇编(清单 13-8)里 if 的全部实现是21::

cmp  r12d, 1073741822   # 和「一半最大值」比
jg   .L3                # 大了就跳过 if 块
lea  rdi, .LC0[rip]     # 否则:打印 heads
call puts@PLT
.L3:                    # if 之后的汇合点

注意编译器的翻译方向:C 写「小于则执行」,汇编写「大于则跳过」—— 机器层的 if 默认是「条件为假才跳」,想跳真分支得绕。这个「反转」贯穿全章。

顺带看序言里的新手势:push r12; push rbx。清单里两个计数/存随机数的变量被 gcc 放进了 r12 和 rbx——查第 11 章的表 11-2,它们都属于「被调方必须保存」的寄存器, 所以函数开头先压栈存起来,结尾弹回去22。第 11 章埋的那条伏笔,在这里第一次兑现。

手写版的硬币程序:一个容易踩的对齐坑

作者自己写的汇编版(清单 13-11)里藏着一个第 11 章规则的实战考验。 序言连压两个 8 字节寄存器(rbp、r12)之后,栈指针落到了 16 字节边界的错侧 (8 + 8 + 8 = 24,不对齐)。协议要求调用函数时 rsp 必须 16 字节对齐,于是30:

push  r12              # 保存(这时栈又不齐了)
sub   rsp, 8           # STACK_ALIGN:再挪 8 字节,凑回对齐
……函数体……
add   rsp, 8           # 撤掉那 8 字节
pop   r12              # 恢复(弹出前必须先把加回去的部分减掉)

书里警告:这是「在栈中保存寄存器时容易违反的规则之一」—— 压一个奇数个 8 字节,对齐就破了;忘了在 pop 前把临时偏移加回来,弹进 r12 的就是垃圾30。 对比编译器的做法(序言里根本不调 rsp,或一次 sub 到位),手写的自由度要用纪律换。

把这个补丁的完整数学走一遍,以后见到 STACK_ALIGN 就不用再想了:

call main      → 返回地址 8 字节(此时 rsp ≡ 8 mod 16,CPU 保证 call 前对齐)
push rbp       → 16(对齐)
push r12       → 24(❌ 破了)
sub rsp, 8     → 32(✓ 补回)
── 函数体里每次 call 前都是对齐的 ──
add rsp, 8     → 回到 24
pop r12        → 16
pop rbp        → 8
ret            → 弹出返回地址,rsp 复原

一个可以顺手记下的判据:序言里 push 的个数(含 rbp)若是奇数,就补 8; 偶数就不用——因为 call 已经贡献了那个"多余的 8"。作者把这个 8 起名 STACK_ALIGN 而不是 magic number 8,也是同一个纪律:让数字自己解释自己。

if-else:then 块末尾补一个无条件跳转

两个分支时,机器要多管一件事:then 块执行完,得跳过 else 块:

cmp  r12d, 1073741822
jg   .L3               # 条件假 → else 块
lea  rdi, .LC0[rip]    # then 块
call puts@PLT
jmp  .L4               # then 做完,跳过 else!
.L3:
lea  rdi, .LC1[rip]    # else 块
call puts@PLT
.L4:                   # 汇合点

那个 jmp .L4 是手写汇编最容易丢的一行23。丢了不会报错——只是 then 执行完 接着执行 else,两个都打印,而且你很难第一时间想到是少了一行跳转。

switch:阶梯 if-else,还是跳表

先说 switch 的语义,因为有一个行为初学者常忘:控制流跳进匹配的 case 后, 会接着把后面所有 case 的代码块都执行掉,直到 switch 结束—— 想让每个 case 只干自己的活,必须靠 break 提前退出31。 匹配不上任何 case 时,有 default 走 default,没有就整个跳过。

switch 按整数值选路。gcc 的做法朴实无华:编译成阶梯形 if-else 串 (逐值 cmp/je),完全不用新机制24。分支再多也一样,只是 cmp/je 更长。

作者的汇编版(清单 13-13)演示了另一种——跳表(jump table): 在只读数据区放一张表,每一项是一个代码块的地址(​.quad 指令,每项 8 字节), 选路变成三次算术加一次间接跳转25:

lea  rax, jumpTable[rip]   # 表的起始地址
mov  rbx, r12              # 索引值 i(1~10)
sub  rbx, 1                # 表从 0 开始数,减 1
shl  rbx, 3                # ×8:每项 8 字节(左移 3 位,第 16 章细讲)
add  rax, rbx              # 目标项的地址
mov  rax, [rax]            # 读出那一项:代码块的地址
jmp  rax                   # 跳!

主线例子的对照: 当 i = 2 时:rbx = 1,×8 = 8,rax 指到表的第二项, 取出 two 标签的地址,jmp 过去打印 "i = 2"26。三个要点:

  • 表里每种可能的取值都得有一项——i 从 1 到 10,表就有 10 项, 4 到 10 全指向同一个 over 块25;
  • 查表的总代价是固定的(四次算术 + 一次间接跳),与分支个数无关; 阶梯 if-else 最坏要一路 cmp 到底。分支多时跳表占优,分支少时阶梯更简单;
  • mov rax, [rax] 这行最容易看晕:rax 先是「表项的地址」,这一行之后变成 「代码块的地址」——同一寄存器,前后两个身份,读汇编时盯住值的变化27。

跳表是不是更快?书里很诚实:取决于缓存命中和 CPU 内部设计,同指令集的不同 CPU 上结论可能相反28。本节真正给的不是「标准答案」,而是「两种实现都见过」。

else-if 阶梯:分支再多也是同一招

两个分支不够时,C 允许把 if-else 串成阶梯(书给了完整的一般形式): 逐条测试 expression1、expression2……命中哪个走哪个块,全不命中走最后的 else32。 机器层的形状不用新画:就是第 4 节 if-else 结构的横向复制, 每个 else 里再嵌一套「测试 + 跳」,汇合点顺延到阶梯脚下。 这正好给跳表一个参照系:阶梯的代价随分支数线性涨,跳表恒定——分支数就是两者的分水岭。

do-while 的空串事故,逐字节走一遍

书里那个「空串会出问题」的警告,值得拿具体字节走一遍20。 输入是空串(第一个字符就是 NUL):

第 1 次进循环体:
  write 把 NUL(0x00)本身写上了屏幕          ← 已经错了
  指针 +1,指向 NUL 后面的那个字节
  test al, al …  al 是 NUL,循环条件为假,退出

第 2 步(退出后):字符串已经"完成"——
  但它以 NUL 开头、结尾没有 NUL,
  下一个读到它的函数会一直往后扫,直到撞上某个 0x00 才停。
  那可能是几字节后,也可能是几 KB 后——未定义行为,表现为随机崩溃。

对照 while:while 先测试再进体,空串一次循环体都不执行,天然安全; do-while 把测试挪到体后,等于先斩后奏。「至少执行一次」的语义, 在没有数据可处理时就是灾难——这不是 do-while 的设计缺陷,而是 用它之前必须先想清楚"零次数据"是否合法的契约问题。

跳表选路的逐格走读(i = 2 与 i = 10)

把跳表选路的全过程按格走一遍,每一步旁边都有具体的数 (基址 0x500000 是为演示编的,不是真实数值):

跳表(每项 8 字节,.quad):
  [0] → one   [1] → two   [2] → three   [3..9] → over

i = 2 时:
  lea  rax, jumpTable[rip]   rax = 0x500000    ← 表基址
  mov  rbx, r12              rbx = 2           ← 当前 i
  sub  rbx, 1                rbx = 1           ← 表从 0 数,i 从 1 数,减 1 对齐
  shl  rbx, 3                rbx = 8           ← ×8:每项 8 字节(左移 3 位)
  add  rax, rbx              rax = 0x500008    ← 指向表中第 2 项
  mov  rax, [rax]            rax = three 的地址 ← 取出那一项
  jmp  rax                                     ← 打印 "i = 3"

i = 10 时:唯一不同的是 rbx = 9、rax = 0x500048,指向 [9] → over。

两条路并排,跳表的真正卖点就显形了:不管选哪个分支,步数一模一样—— 四步算术加一次间接跳,恒定代价。对照阶梯 if-else:命中最后一个分支要 连 cmp 十次。分支少时这个差距无所谓;分支多时,这就是常数对线性。 书里练习 2、3 专门让你把循环改成 15 次再生成汇编对比:改动一行 C, 两种实现的清单各长一截——规模对两类代码的影响,亲眼看一次就记住了29。

5. 作者的判断与证据

说法 性质
六种结构在机器层都是「测试 + 跳转」,if 默认翻译成「假则跳」 由 gcc 输出实证(清单 13-2/13-6/13-8/13-10)11192123
30 余助记符 / 16 种条件;高于、低于管无符号 手册事实,书里给了完整对照表67
「分支预测让多一跳不心疼」 一句带过的事实陈述,未展开机制12
for 与 while 生成的汇编相同(该例中) 作者跑了 gcc 验证,但只是单版本单例子17
跳表与阶梯 if-else 谁快「很难说」 作者的诚实存疑,明说取决于缓存与 CPU28
do-while 对空字符串是未定义行为 书里明确警告20

判断(我们的,不是书里的): 本章有一个反复出现的「反转」,值得单独记住: C 的「真则执行」,到机器层一律翻译成「假则跳过」——if、if-else、while 全部如此。 这不是编译器恶趣味,而是硬件的性价比选择:条件跳转「为真才跳」与「为假才跳」都有 助记符,但编译器偏爱让「走直线」对应最常见路径(不跳的那条)。 如果错,会错在: 这个偏好不是协议,只是 gcc 在该版本下的习惯; 换编译器、换优化级别,分支的排布可能反过来——判读别人的汇编时,别拿这个当公理。

判断(我们的,不是书里的): 跳表一段是全书前半本「电路思维」的回响: 第 6 章的译码器就是「输入一个数,选一路输出」——跳表是它的软件版 (索引译码成地址)。「查表代替计算/比较」是贯穿硬件与软件的同一种优化。 如果错,会错在: 现代编译器对小 switch 也常编译成位图测试或算术比较, 「switch ⟹ 跳表」不是定律;它只是一种经典实现。

6. 边界与局限

  • 只讲了一个循环控制变量的情况。 书开篇就声明:多变量终止条件「为了清晰起见」不讨论10。
  • 分支预测只有一句名词解释。 现代 CPU 的分支预测器是性能的决定因素之一, 「让最常见路径不跳」的真正原因在这——本书不展开。
  • switch 的编译结果随编译器而变。 gcc 该版本选了阶梯 if-else;gcc 开 -O2、 或换 clang,同一段 switch 可能变成真正的跳表,或二分查找式的比较链。 书中练习也让你自己生成汇编对比29。
  • jcxz 一族限短跳转(−128~+127 字节),且容易和 loop 指令混淆——本书不用 loop,一笔带过5。

7. 可带走的

  1. 控制流三种:顺序、迭代、选择;机器层只剩「改 rip」一个动作;
  2. jcc 只读 rflags,必须紧跟产生标志的指令(cmp/test/算术);
  3. 无符号用 above/below,有符号用 greater/less——0xffffffff 高于 1,却小于 1;
  4. 记不准比较方向?在 jcc 设断点,看两个值、单步看跳向——作者也这么干;
  5. while/for 语义等价(该例中汇编相同);do-while 至少执行一次,空输入是它的坑;
  6. test al, al 判零、cmp a, b 比大小——都不改操作数,只留化验单;
  7. movzx 把窄值读进宽寄存器并补 0;inc 加 1;
  8. register 只是建议编译器用寄存器,采纳与否看它;住寄存器的变量在汇编里全程不进内存;
  9. if 的机器层是「假则跳过」;if-else 的 then 块末尾必须有无条件 jmp;
  10. switch 的 case 会贯穿执行,靠 break 收尾;两种实现:阶梯 if-else(编译器默认)与跳表(固定代价,分支多才划算);
  11. 跳表公式:表基址 + (索引 × 8),每项一个 .quad 地址,最后一跳是 jmp 寄存器;
  12. 压奇数个 8 字节会破坏 16 字节对齐,手写汇编要补 sub rsp, 8,pop 前记得加回来。

8. 原文地图

主题 原书章 原文位置
命令式/声明式、三种控制流 第13 章 控制流结构控制流结构 text/19-ch13.txt:9(搜「命令式编程」) · text/19-ch13.txt:15(搜「3 种基本的控制流结构」)
jmp 指令卡 第13 章 控制流结构控制流结构 text/19-ch13.txt:31(搜「jmp ——跳转」)
jcc / jcxz 家族 第13 章 控制流结构控制流结构 text/19-ch13.txt:48(搜「jcc ——」) · text/19-ch13.txt:55(搜「jcxz ——」)
高于/大于、无符号/有符号 第13 章 控制流结构控制流结构 text/19-ch13.txt:69(搜「无符号值」) · text/19-ch13.txt:74(搜「0xffffffff 高于」)
gdb 调试跳转方向 第13 章 控制流结构控制流结构 text/19-ch13.txt:97(搜「小窍门」)
while 循环 C 与汇编 第13 章 控制流结构控制流结构 text/19-ch13.txt:134(搜「清单 13-1」) · text/19-ch13.txt:169(搜「清单 13-2」) · text/19-ch13.txt:231(搜「清单 13-3」)
哨兵值 第13 章 控制流结构控制流结构 text/19-ch13.txt:291(搜「哨兵值」)
分支预测一句 第13 章 控制流结构控制流结构 text/19-ch13.txt:288(搜「每次迭代中,除了条件指令」)
movzx / test 指令卡 第13 章 控制流结构控制流结构 text/19-ch13.txt:208(搜「以零扩展移动」) · text/19-ch13.txt:218(搜「测试位」)
cmp / inc 指令卡 第13 章 控制流结构控制流结构 text/19-ch13.txt:274(搜「cmp ——比较」) · text/19-ch13.txt:282(搜「inc ——递增」)
for 与 while 汇编相同 第13 章 控制流结构控制流结构 text/19-ch13.txt:343(搜「相同的结果」)
do-while 与空串警告 第13 章 控制流结构控制流结构 text/19-ch13.txt:394(搜「清单 13-6」) · text/19-ch13.txt:437(搜「未指定的行为」)
if / if-else 汇编 第13 章 控制流结构控制流结构 text/19-ch13.txt:499(搜「清单 13-8」) · text/19-ch13.txt:598(搜「清单 13-10」)
push r12/rbx 保存寄存器 第13 章 控制流结构控制流结构 text/19-ch13.txt:668(搜「push r12」) · text/19-ch13.txt:693(搜「表 11-1」)
switch 阶梯与跳表 第13 章 控制流结构控制流结构 text/19-ch13.txt:783(搜「阶梯形式」) · text/19-ch13.txt:798(搜「清单 13-13」)
跳表三项操作与效率存疑 第13 章 控制流结构控制流结构 text/19-ch13.txt:885(搜「每一次可能的读取」) · text/19-ch13.txt:888(搜「很难说跳表是否」)

  1. 出处:「第13 章 控制流结构」第 8–14 段(text/19-ch13.txt:9,搜「命令式编程」;text/19-ch13.txt:13,搜「makefile」)。 ↩

  2. 出处:「第13 章 控制流结构」第 15–21 段(text/19-ch13.txt:15,搜「3 种基本的控制流结构」;text/19-ch13.txt:18,搜「调用函数」)。 ↩

  3. 出处:「第13 章 控制流结构」第 31–40 段(text/19-ch13.txt:31,搜「jmp ——跳转」;text/19-ch13.txt:39,搜「下一条指令」)。 ↩

  4. 出处:「第13 章 控制流结构」第 44–52 段(text/19-ch13.txt:44,搜「逻辑组合」;text/19-ch13.txt:51,搜「不会修改状态标志」)与第 95–96 段(text/19-ch13.txt:95,搜「必须紧接」)。 ↩

  5. 出处:「第13 章 控制流结构」第 53–66 段(text/19-ch13.txt:55,搜「jcxz ——」;text/19-ch13.txt:64,搜「不理会状态标志」)。 ↩↩

  6. 出处:「第13 章 控制流结构」第 67–71 段(text/19-ch13.txt:67,搜「16 种条件组合」;text/19-ch13.txt:69,搜「无符号值」;text/19-ch13.txt:76,搜「条件跳转」)。 ↩↩

  7. 出处:「第13 章 控制流结构」第 74–75 段(text/19-ch13.txt:74,搜「0xffffffff 高于」)。 ↩↩

  8. 出处:「第13 章 控制流结构」第 97–100 段(text/19-ch13.txt:97,搜「小窍门」;text/19-ch13.txt:99,搜「放置一个断点」)。 ↩

  9. 出处:「第13 章 控制流结构」第 134–166 段(text/19-ch13.txt:148,搜「stringPtr」;text/19-ch13.txt:150,搜「while」;text/19-ch13.txt:162,搜「NUL 字符」)。 ↩↩↩↩↩↩

  10. 出处:「第13 章 控制流结构」第 103–108 段(text/19-ch13.txt:108,搜「为了清晰起见」)与第 291–293 段(text/19-ch13.txt:291,搜「哨兵值」)。 ↩↩

  11. 出处:「第13 章 控制流结构」第 169–206 段(text/19-ch13.txt:185,搜「jmp .L2」;text/19-ch13.txt:205,搜「似乎是在循环结束时」)。 ↩↩

  12. 出处:「第13 章 控制流结构」第 287–290 段(text/19-ch13.txt:288,搜「每次迭代中,除了条件指令」;text/19-ch13.txt:289,搜「不会对此展开深入讨论」)。 ↩↩

  13. 出处:「第13 章 控制流结构」第 208–217 段(text/19-ch13.txt:208,搜「以零扩展移动」;text/19-ch13.txt:214,搜「高 32 位部分也全为 0」)。 ↩

  14. 出处:「第13 章 控制流结构」第 218–226 段(text/19-ch13.txt:218,搜「测试位」;text/19-ch13.txt:225,搜「按位与运算」)。 ↩

  15. 出处:「第13 章 控制流结构」第 274–281 段(text/19-ch13.txt:274,搜「cmp ——比较」;text/19-ch13.txt:280,搜「减去第一个操作数」)。 ↩

  16. 出处:「第13 章 控制流结构」第 282–286 段(text/19-ch13.txt:282,搜「inc ——递增」)。 ↩

  17. 出处:「第13 章 控制流结构」第 342–344 段(text/19-ch13.txt:343,搜「相同的结果」)。 ↩↩↩

  18. 出处:「第13 章 控制流结构」第 349–353 段(text/19-ch13.txt:349,搜「计数控制型循环」;text/19-ch13.txt:353,搜「主观选择」)。 ↩↩

  19. 出处:「第13 章 控制流结构」第 394–433 段(text/19-ch13.txt:431,搜「唯一的区别」;text/19-ch13.txt:433,搜「节省了第一次」)。 ↩↩

  20. 出处:「第13 章 控制流结构」第 435–437 段(text/19-ch13.txt:435,搜「至少执行一次循环体」;text/19-ch13.txt:437,搜「未指定的行为」)。 ↩↩↩

  21. 出处:「第13 章 控制流结构」第 467–539 段(text/19-ch13.txt:485,搜「randomNumber < RAND_MAX/2」;text/19-ch13.txt:520,搜「jg .L3」;text/19-ch13.txt:40,搜「跳过」)。 ↩↩↩

  22. 出处:「第13 章 控制流结构」第 667–700 段(text/19-ch13.txt:668,搜「push r12」;text/19-ch13.txt:694,搜「保留 r12 中的值」)。 ↩

  23. 出处:「第13 章 控制流结构」第 598–642 段(text/19-ch13.txt:625,搜「jmp .L4」;text/19-ch13.txt:642,搜「跳过 else-block」)。 ↩↩

  24. 出处:「第13 章 控制流结构」第 783–794 段(text/19-ch13.txt:783,搜「阶梯形式的 if-else」)。 ↩

  25. 出处:「第13 章 控制流结构」第 798–887 段(text/19-ch13.txt:816,搜「jumpTable」;text/19-ch13.txt:875,搜「每一项都是要执行的代码块」;text/19-ch13.txt:876,搜「分配 8 字节」)。 ↩↩

  26. 出处:「第13 章 控制流结构」第 838–848 段(text/19-ch13.txt:842,搜「lea rax, jumpTable」;text/19-ch13.txt:845,搜「shl rbx, 3」;text/19-ch13.txt:848,搜「jmp rax」)。 ↩

  27. 出处:「第13 章 控制流结构」第 882–886 段(text/19-ch13.txt:883,搜「我们想要的地址在表中的存」)。 ↩

  28. 出处:「第13 章 控制流结构」第 888–890 段(text/19-ch13.txt:888,搜「很难说跳表是否」;text/19-ch13.txt:889,搜「有所不同」)。 ↩↩

  29. 出处:「第13 章 控制流结构」第 892–899 段(text/19-ch13.txt:896,搜「使用编译器」)。 ↩↩

  30. 出处:「第13 章 控制流结构」第 698–700 段(text/19-ch13.txt:67,搜「jcc 指令的 cc 部」;text/19-ch13.txt:699,搜「减去 8」;text/19-ch13.txt:700,搜「加回去」)。 ↩↩

  31. 出处:「第13 章 控制流结构」第 740–746 段(text/19-ch13.txt:742,搜「所有剩余」;text/19-ch13.txt:743,搜「break 语句」)。 ↩

  32. 出处:「第13 章 控制流结构」第 701–717 段(text/19-ch13.txt:708,搜「else if」;text/19-ch13.txt:720,搜「离散值」)。 ↩