程序中的 fall-through 行为分析

最近移指 dyninst 的时候,发现指令解析模块涉及到 fall-through 概念,查了些英文资料后,初步了解了这个概念。

wiki 上的解释

  • fall-through(fallthrough and fall through)

(programming) In certain programming constructs, the situation where execution passes to the next condition in a list unless explicitly redirected. (【编程】)在某些编程构造中,除非明确重定向,否则执行转移到列表中的下一个条件的情况。

1997, Bjarne Stroustrup, The C++ Programming Language: Language Libraries and Design:
It is a good idea to comment the (rare) cases in which a fall-through is intentional so that an uncommented fall-through can be assumed to be an error. 最好对故意失败的(罕见)情况进行注释,以便将未注释的失败视为错误。

2001, Graham M Seed, Barry J Cooper, An Introduction to Object-Oriented Programming in C++
If you place default elsewhere, then a break will be required to prevent fall-through. 如果您在其他地方设置了默认值,则需要中断以防止失败。

2008, Nagel et al, Professional C# 2008
Specifically, it prohibits fall-through conditions in almost all cases. 具体来说,它禁止在几乎所有情况下出现故障。

branch 指令中的体现

branch 指令包含两类:conditional 和 unconditional,分支指令中的指令流执行路径可分为两种:

  • target path:指的是将要跳转的位置,例如 C 中 goto 跳转的标签。

  • fall-through path:指的是紧跟分支指令之后的指令。

如下图所示,从 C 语言角度来看,当条件满足 a 等于 b 的时候,就会跳转到 target address,不满足则进入到 fall through address;从汇编角度来看,当判断条件寄存器 cr0 表示相等时,就跳转到 target address,不相等时则执行紧跟分支指令的下一条指令,即正常的 pc+4;

image-20230421100435020

switch 语句中的体现

fall-through 行为的示例

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int result = 5;
switch (result) {
case 1:
printf("The result is 1\n");
break;
case 5:
printf("The result is 5\n");
break;
case 10:
printf("The result is 10\n");
break;
default:
printf("The result does not exist.\n");
}

毫无疑问,上述代码块执行结果是:The result is 5,但如果删掉每条 case 里的 break 语句后,结果就不可控了,代码如下:

1
2
3
4
5
6
7
8
9
10
11
int result = 5;
switch (result) {
case 1:
printf("The result is 1\n");
case 5:
printf("The result is 5\n");
case 10:
printf("The result is 10\n");
default:
printf("The result does not exist.\n");
}

Case 5, case 10 和 default case 都会被执行,这就是 switch 语句中的 fall-through 行为表现。

fall-through 行为的好处

这里的 fall-through 行为看上去好像 bug,但事实上并不是,该行为也是有用处的,常用来分类一些相关联的 cases,例如下面代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int result = 10;
switch (result) {
case 1:
printf("The result is less than 10\n");
break;
case 2:
printf("The result is less than 10\n");
break;
case 5:
printf("The result is less than 10\n");
break;
case 10:
printf("The result is 10\n");
break;
default:
printf("The result does not exist.\n");
}

当 result 为 10 时,进入 case 10,打印:The result is 10。
当 result 为 1 时,进入 case 1,打印:The result is less than 10。
当 result 为 2 时,进入 case 2,打印:The result is less than 10。
当 result 为 5 时,进入 case 5,打印:The result is less than 10。

可以看出,后面三种情况是有关联的,并且打印内容是一样的,因此可以删掉 break,通过 fall-through 行为来归类相关联的 cases,这便是该行为的好处。修改后代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
int result = 10;
switch (result) {
case 1:
case 2:
case 5:
printf("The result is less than 10\n");
break;
case 10:
printf("The result is 10\n");
break;
default:
printf("The result does not exist.\n");
}