控制流结构 — 循环与分支在机器层只是「跳转」的两种用法¶
这一章讲什么: 你在 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. 可带走的¶
- 控制流三种:顺序、迭代、选择;机器层只剩「改 rip」一个动作;
- jcc 只读 rflags,必须紧跟产生标志的指令(cmp/test/算术);
- 无符号用 above/below,有符号用 greater/less——0xffffffff 高于 1,却小于 1;
- 记不准比较方向?在 jcc 设断点,看两个值、单步看跳向——作者也这么干;
- while/for 语义等价(该例中汇编相同);do-while 至少执行一次,空输入是它的坑;
test al, al判零、cmp a, b比大小——都不改操作数,只留化验单;- movzx 把窄值读进宽寄存器并补 0;inc 加 1;
register只是建议编译器用寄存器,采纳与否看它;住寄存器的变量在汇编里全程不进内存;- if 的机器层是「假则跳过」;if-else 的 then 块末尾必须有无条件 jmp;
- switch 的 case 会贯穿执行,靠 break 收尾;两种实现:阶梯 if-else(编译器默认)与跳表(固定代价,分支多才划算);
- 跳表公式:
表基址 + (索引 × 8),每项一个.quad地址,最后一跳是jmp 寄存器; - 压奇数个 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(搜「很难说跳表是否」) |
-
出处:「第13 章 控制流结构」第 8–14 段(
text/19-ch13.txt:9,搜「命令式编程」;text/19-ch13.txt:13,搜「makefile」)。 ↩ -
出处:「第13 章 控制流结构」第 15–21 段(
text/19-ch13.txt:15,搜「3 种基本的控制流结构」;text/19-ch13.txt:18,搜「调用函数」)。 ↩ -
出处:「第13 章 控制流结构」第 31–40 段(
text/19-ch13.txt:31,搜「jmp ——跳转」;text/19-ch13.txt:39,搜「下一条指令」)。 ↩ -
出处:「第13 章 控制流结构」第 44–52 段(
text/19-ch13.txt:44,搜「逻辑组合」;text/19-ch13.txt:51,搜「不会修改状态标志」)与第 95–96 段(text/19-ch13.txt:95,搜「必须紧接」)。 ↩ -
出处:「第13 章 控制流结构」第 53–66 段(
text/19-ch13.txt:55,搜「jcxz ——」;text/19-ch13.txt:64,搜「不理会状态标志」)。 ↩↩ -
出处:「第13 章 控制流结构」第 67–71 段(
text/19-ch13.txt:67,搜「16 种条件组合」;text/19-ch13.txt:69,搜「无符号值」;text/19-ch13.txt:76,搜「条件跳转」)。 ↩↩ -
出处:「第13 章 控制流结构」第 74–75 段(
text/19-ch13.txt:74,搜「0xffffffff 高于」)。 ↩↩ -
出处:「第13 章 控制流结构」第 97–100 段(
text/19-ch13.txt:97,搜「小窍门」;text/19-ch13.txt:99,搜「放置一个断点」)。 ↩ -
出处:「第13 章 控制流结构」第 134–166 段(
text/19-ch13.txt:148,搜「stringPtr」;text/19-ch13.txt:150,搜「while」;text/19-ch13.txt:162,搜「NUL 字符」)。 ↩↩↩↩↩↩ -
出处:「第13 章 控制流结构」第 103–108 段(
text/19-ch13.txt:108,搜「为了清晰起见」)与第 291–293 段(text/19-ch13.txt:291,搜「哨兵值」)。 ↩↩ -
出处:「第13 章 控制流结构」第 169–206 段(
text/19-ch13.txt:185,搜「jmp .L2」;text/19-ch13.txt:205,搜「似乎是在循环结束时」)。 ↩↩ -
出处:「第13 章 控制流结构」第 287–290 段(
text/19-ch13.txt:288,搜「每次迭代中,除了条件指令」;text/19-ch13.txt:289,搜「不会对此展开深入讨论」)。 ↩↩ -
出处:「第13 章 控制流结构」第 208–217 段(
text/19-ch13.txt:208,搜「以零扩展移动」;text/19-ch13.txt:214,搜「高 32 位部分也全为 0」)。 ↩ -
出处:「第13 章 控制流结构」第 218–226 段(
text/19-ch13.txt:218,搜「测试位」;text/19-ch13.txt:225,搜「按位与运算」)。 ↩ -
出处:「第13 章 控制流结构」第 274–281 段(
text/19-ch13.txt:274,搜「cmp ——比较」;text/19-ch13.txt:280,搜「减去第一个操作数」)。 ↩ -
出处:「第13 章 控制流结构」第 282–286 段(
text/19-ch13.txt:282,搜「inc ——递增」)。 ↩ -
出处:「第13 章 控制流结构」第 342–344 段(
text/19-ch13.txt:343,搜「相同的结果」)。 ↩↩↩ -
出处:「第13 章 控制流结构」第 349–353 段(
text/19-ch13.txt:349,搜「计数控制型循环」;text/19-ch13.txt:353,搜「主观选择」)。 ↩↩ -
出处:「第13 章 控制流结构」第 394–433 段(
text/19-ch13.txt:431,搜「唯一的区别」;text/19-ch13.txt:433,搜「节省了第一次」)。 ↩↩ -
出处:「第13 章 控制流结构」第 435–437 段(
text/19-ch13.txt:435,搜「至少执行一次循环体」;text/19-ch13.txt:437,搜「未指定的行为」)。 ↩↩↩ -
出处:「第13 章 控制流结构」第 467–539 段(
text/19-ch13.txt:485,搜「randomNumber < RAND_MAX/2」;text/19-ch13.txt:520,搜「jg .L3」;text/19-ch13.txt:40,搜「跳过」)。 ↩↩↩ -
出处:「第13 章 控制流结构」第 667–700 段(
text/19-ch13.txt:668,搜「push r12」;text/19-ch13.txt:694,搜「保留 r12 中的值」)。 ↩ -
出处:「第13 章 控制流结构」第 598–642 段(
text/19-ch13.txt:625,搜「jmp .L4」;text/19-ch13.txt:642,搜「跳过 else-block」)。 ↩↩ -
出处:「第13 章 控制流结构」第 783–794 段(
text/19-ch13.txt:783,搜「阶梯形式的 if-else」)。 ↩ -
出处:「第13 章 控制流结构」第 798–887 段(
text/19-ch13.txt:816,搜「jumpTable」;text/19-ch13.txt:875,搜「每一项都是要执行的代码块」;text/19-ch13.txt:876,搜「分配 8 字节」)。 ↩↩ -
出处:「第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」)。 ↩ -
出处:「第13 章 控制流结构」第 882–886 段(
text/19-ch13.txt:883,搜「我们想要的地址在表中的存」)。 ↩ -
出处:「第13 章 控制流结构」第 888–890 段(
text/19-ch13.txt:888,搜「很难说跳表是否」;text/19-ch13.txt:889,搜「有所不同」)。 ↩↩ -
出处:「第13 章 控制流结构」第 892–899 段(
text/19-ch13.txt:896,搜「使用编译器」)。 ↩↩ -
出处:「第13 章 控制流结构」第 698–700 段(
text/19-ch13.txt:67,搜「jcc 指令的 cc 部」;text/19-ch13.txt:699,搜「减去 8」;text/19-ch13.txt:700,搜「加回去」)。 ↩↩ -
出处:「第13 章 控制流结构」第 740–746 段(
text/19-ch13.txt:742,搜「所有剩余」;text/19-ch13.txt:743,搜「break 语句」)。 ↩ -
出处:「第13 章 控制流结构」第 701–717 段(
text/19-ch13.txt:708,搜「else if」;text/19-ch13.txt:720,搜「离散值」)。 ↩