编译器中间端:控制流图基本块中函数调用的处理方式咨询
控制流图中函数调用的处理方式
我正在开发编译器的中间端,想了解如何在控制流图中处理函数调用。
示例三地址代码
LABEL label0 a = 1 b = 2 c = a + b IF c == g GOTO label1 ELSE GOTO label2 LABEL label1 d = c + 0 CALL func0 (d -> y) GOTO label0 LABEL label2 e = 8 f = 600 g = e + f CALL func0 (a -> y) CALL func1 END FUNC LABEL func0 PRINT y RETURN FUNC LABEL func1 result = "PROGRAM IS COMPLETE" PRINT result RETURN
我拆分的基本块
block1: LABEL label0 a = 1 b = 2 c = a + b IF c == g GOTO label1 ELSE GOTO label2 block2: LABEL label1 d = c + 0 CALL func0 (d -> y) block3: GOTO label0 block4: LABEL label2 e = 8 f = 600 g = e + f CALL func0 (a -> y) block5: CALL func1 block6: END block7: FUNC LABEL func0 PRINT y RETURN block8: FUNC LABEL func1 result = "PROGRAM IS COMPLETE" PRINT result RETURN
我假设函数调用应被视为跳转,因此会像GOTO或条件跳转语句一样强制开启新基本块。之后我将基本块嵌套进流图结构并定义连接:block1连接block2和block4,block3连接block1,block5连接block8,block4和block2分别连接block7;返回跳转方面,block7连接block5和block3,block8连接block6。
我的疑问是:将函数调用和返回集成到流图中并视为跳转的处理方式是否正确?还是拆分基本块时应忽略CALL语句,将函数体块作为独立子流图处理?
两种处理方式的合理性与适用场景
1. 将CALL视为跳转拆分基本块(你的当前做法)
这种处理方式是完全合理的,尤其是在需要做跨函数的控制流分析(比如全局死代码消除、跨函数数据流分析)时,这种集成式的流图能更直观地展示整个程序的控制流路径。
但要注意几个细节:
- 函数返回的目标块需要准确关联:比如
func0从block2调用后返回block3,从block4调用后返回block5,你当前的连接是对的,但要确保在后续分析中能区分不同调用点的返回路径——如果编译器支持尾调用优化,还需要特殊处理这类场景。 - 基本块拆分规则:CALL语句确实应该作为基本块的结尾,因为函数调用会改变程序的控制流(即使是普通函数调用,执行完后也会回到调用点的下一条指令),所以把CALL单独作为块的结尾、后续指令(比如你的
GOTO label0)拆成新块是符合基本块定义的(基本块是只有一个入口和一个出口的连续指令序列)。
2. 将函数体作为独立子流图处理
这种方式也是编译器中常用的实现方案,尤其是当编译器的分析/优化大多以单个函数为单位(比如局部优化、寄存器分配)时,独立子流图能简化处理逻辑:
- 每个函数对应自己独立的控制流图,主程序的流图中,CALL语句作为一个特殊节点,标记其调用的函数子图,以及返回后的目标块。
- 这种方式更符合模块化的设计思路,避免主流图因为大量函数调用变得过于复杂,也便于单独对每个函数进行优化。
总结
两种方式没有绝对的对错,取决于你的编译器的设计目标:
- 如果需要做全局跨函数分析,集成式的流图更合适;
- 如果优先考虑单函数的局部优化与模块化实现,独立子流图的方案更高效。
另外,你的基本块拆分是符合规范的——CALL作为控制流转移语句,确实应该触发新基本块的创建,这一点是正确的。
内容的提问来源于stack exchange,提问作者Jacob Bauer
相关产品推荐
相关产品推荐

