想象你是一位新上任的餐厅后厨经理,面前摆着一份厚厚的菜谱——这就是程序员常说的“源码”。光看文字,你能一眼看出哪道菜先做、哪道菜后做、盐从哪里取、又要加进哪口锅吗?很难。所以有经验的经理会先请人画三张图纸:第一张是厨房布局图,标出冰箱、灶台、烤箱的位置与嵌套关系;第二张是出菜流程图,画清从接单到装盘的顺序,以及“如果客人忌辣就走清淡路线”这类分支;第三张是食材追踪表,记录每一份食材从入库、加工到上桌的每一站。 这三张图纸,对应程序分析中的三个核心概念:AST 抽象语法树、CFG 控制流图和 DFG 数据流图。它们分别回答:代码“长什么样”、“怎么执行”、“数据怎么流动”。本文就用这个厨房比喻,带你从零看懂这三张图,并了解 AI 如何借助它们理解代码、查找缺陷甚至挖掘漏洞。
掌握这三种图结构,是你进入代码智能领域的“三张入场券”——它串起了语法分析、程序分析、图神经网络与各类工程工具,也是 AI 程序员与普通开发者拉开差距的分水岭。
可视化演示
展示AST抽象语法树节点层级结构、CFG控制流图基本块分支路径、DFG数据流图定义使用依赖及三图派生关联与跨图融合,含流动波形背景动画。
本节配有交互动效(进入视口后自动加载,约 3KB)。
AST抽象语法树
新经理拿到菜谱后,第一件事不是读文字,而是请人画出厨房布局图:灶台在哪个区域、烤箱属于哪个工作站、备菜区和出菜区如何嵌套。这张图有意忽略菜谱上的装饰性文字和多余空格,只保留“谁包含谁”的结构。AST 抽象语法树就是代码的厨房布局图——它按语法规则把源码组织成一棵层次分明的树。
编译器在做这件事时分成两步:先做词法分析,把字符流切成一个个 token(关键字、变量名、运算符),再做语法分析,按文法规则将 token 拼成一棵树。树的内部节点是“表达式”“赋值语句”“条件语句”这样的语法结构,叶子节点则是变量名、数字字面量等最小的单元。比如 a + b * c,因为乘法优先级高于加法,* 会先组成 b * c 子树,+ 成为整棵树的根节点。
AST 丢弃了空格、换行、多余括号等与语义无关的格式噪声——括号只影响树的形状,本身不出现在树中。这一特性让 AST 成为编译器前端、IDE 语法高亮、代码格式化工具共用的基础结构,也是后续构建 CFG 和 DFG 的起点。
有了这张布局图,不管菜谱多厚,你都能迅速看清哪些“设备”先组合、哪些后组合。对程序员而言,读 AST 就是戴上“结构眼镜”看代码,复杂逻辑瞬间变成一棵可以遍历的树。
- 非终结符 EE 代表“表达式”,对应 AST 中的内部节点,它可以继续向下展开成更具体的结构。
- 加法展开规则E 展开为
E + T,表示一个表达式由“表达式”和“项”通过加号组合,对应 AST 中+节点下挂着两个子树。 - 项的进一步展开T 表示“项”,继续展开为
T × F,体现乘法优先级高于加法——乘号先结合成子树。 - 叶子节点落地F 最终展开为变量标识符或数字字面量,对应 AST 中不可再分的叶子节点。
CFG控制流图
布局图告诉你厨房里有哪些设备,却没告诉你一道菜如何在各设备间流转。CFG 控制流图是后厨的出菜流程图:从接单开始,先备菜还是先热锅,条件分支怎么走,循环回去要重复几次,最终在哪里装盘出餐。它回答的问题是“程序实际往哪儿走”。
构建 CFG 时,分析器先把代码切成基本块——一段“单入口、单出口、中间没有跳转”的连续语句;再把块与块之间的跳转画成有向边。一个 if-else 语句会让某一块分裂出两条出边,一条指向 then 分支,一条指向 else 分支;一个 while 循环则会让某个块的出边绕回它自己,形成环。
有了基本块和边,CFG 变成一张标准的图结构。分析者可以在上面做路径可达性分析(某段代码是否永远走不到)、死代码检测、分支覆盖率计算,甚至判断程序会不会陷入死循环。编译器优化中的“常量传播”“公共子表达式消除”也都以 CFG 为舞台。
出菜流程图让学徒一眼看清整条动线,CFG 则让程序分析器可以回答“执行到底会不会走到这一行”。在你接手陌生代码时,CFG 就是那张帮你避开“死胡同”的动线图。
- 顶点集合 VV 是所有基本块的集合,每个基本块是一段顺序执行的连续语句,内部不存在跳转。
- 边集合 E每条有向边 e=(u,v) 表示执行完基本块 u 后,控制流可能进入基本块 v。
- 入口节点v_entry 是函数的唯一入口,对应函数被调用时执行的第一段代码。
- 出口节点v_exit 是程序的正常结束位置,所有 return 路径都会在图中汇聚到这里。
DFG数据流图
前两张图看完,你还想知道:盐从哪个架子被取走、加进了哪口锅、最终装进哪道菜?DFG 数据流图就是这份食材追踪表,它不关心执行顺序,只关心数据从哪里来、到哪里去。
DFG 的节点是变量的“定义点”和“使用点”:在某个语句中给变量赋值,就是一个定义点;在另一个语句中读取这个变量,就是一个使用点。只要定义点的值有可能流到使用点,就在两者之间连一条有向边。比如 c = a + b; d = c; e = a:变量 c 在第二行被 d 使用,变量 a 的定义同时被 c 和 e 两处消费。
这种“定值-引用”信息威力巨大:如果一个变量的定义没有任何使用点,说明这个赋值是死代码,可以安全删除;如果从“用户输入”到一个“危险操作”之间存在一条 DFG 路径,说明可能存在安全漏洞。数据依赖分析因此成为编译优化与安全审计的共同底座。
有了食材追踪表,一旦某批食材“变质”(比如来自用户的恶意输入),你就能顺着表一路查到所有被污染的菜品。这正是漏洞挖掘中污点分析的基本思路。
- 定义点 def(u)变量 x 在语句 u 中被赋值,u 就是 x 的一个“定义点”——食材在这里从“无”变“有”。
- 使用点 use(v)变量 x 在语句 v 中被读取,v 称为“使用点”——食材在这里被取用。
- 可行路径连接只有从 u 到 v 存在一条执行路径且 x 未被中途重赋值时,才生成一条定值-引用边。
三图构建方法
三张图纸不是凭空画出来的,而是同一位“老厨师”按固定流程依次绘制。这条流水线就是编译器与代码分析工具的标准工作流程,每一步的输出恰好是下一步的输入。
第一步词法分析把源代码字符流切成 token 序列,相当于把菜谱拆成“食材”和“动作”的清单;第二步语法分析按文法规则把 token 组装成 AST,相当于根据清单画出厨房布局图;第三步控制流分析遍历 AST,把顺序语句切分为基本块并用跳转边相连,得到 CFG;第四步数据流分析在 CFG 上扫描每个变量的定值与引用,生成 DFG。
实际工程中,LLVM、GCC 等编译器内部都维护着这样的多级中间表示:源码先变成 AST(或更底层的中间码),再生成 CFG 供优化器使用,数据流分析则按需在 CFG 上跑固定点迭代算法。库如 Python 的 ast 模块、JavaScript 的 Babel,都能一键输出 AST,而 CFG/DFG 通常由静态分析框架(如 CodeQL、Soot)进一步加工而来。
这位“老厨师”就是编译器的化身。理解了这套流水线,你就能明白为什么 IDE 能在你打字的同时给出语法错误提示——它一直在后台默默绘制这三张图。
- 词法分析把源码的字符流切成具有类型标注的 token 序列,空白符和注释在这一步被丢弃。
- 语法分析按文法规则把 token 递归组合成树状结构,生成 AST。
- 控制流分析遍历 AST,将顺序语句切分为基本块,把 if/while 等结构转换为跳转边,得到 CFG。
- 数据流分析在 CFG 上扫描每个变量的定值和引用位置,连接出数据依赖边,生成 DFG。
应用场景
图纸画好不是挂在墙上看的,而是用来做“后厨审计”的。三张图各有分工,组合起来能覆盖代码智能的大部分关键场景。
代码理解是最直接的场景:接手陌生项目时,AST 帮你快速浏览模块的层级结构,CFG 帮你梳理一个函数从进入到返回的所有执行路径,DFG 帮你定位某个变量的值究竟来自何处。缺陷检测则更依赖后两张图:在 CFG 上做可达性分析,能找出永远不会执行的代码块;在 DFG 上检查定值-引用链,能发现从未被读取的赋值、空指针解引用、资源泄漏等问题。
漏洞挖掘把 DFG 用到极致:把“用户输入”标记为污染源,沿着数据依赖边追踪到危险函数(如 SQL 拼接、命令执行),一旦路径存在就有漏洞风险,这就是业界常说的污点分析。此外,语义搜索与克隆检测把两段代码分别建模成图,再按结构相似度打分——比纯文本比对更能识别“写法不同但功能相同”的代码。
后厨审计越早发现隐患,修复成本越低。三张图纸就是代码世界的安全监控系统——从“看得懂”到“查得出”,再到“抓得准”。
- 公共边数|E1 ∩ E2| 表示两张图中同时出现的有向边数量,公共边越多,说明两段代码的执行骨架越一致。
- 并集边数|E1 ∪ E2| 表示两图所有边的去重总数,作为归一化分母,避免图越大分数越占便宜。
- 相似度取值两者之比落在 [0,1],越接近 1 表示结构越相似,常用于代码查重与检索排序。
图神经网络
传统分析靠人工规则,而现代 AI 想让模型自己“看图学艺”,像一位新厨师大量观摩后厨图纸后逐渐形成直觉。图神经网络(GNN)就是让机器学会读图的模型家族。
GNN 把图的每个节点表示成一个向量(嵌入),通过消息传递机制迭代更新:每轮中,每个节点收集邻居节点的向量,加权聚合后与自己上一轮的向量合并,再过激活函数得到新向量。经过若干轮迭代,每个节点的向量就“吸收”了周围结构的信息——+ 节点知道自己连接着两个变量,if 节点知道自己通向两个分支。
更前沿的做法是跨图融合:把同一份代码的 AST、CFG、DFG 放进同一个模型,让三类图的信息互相传递,模型同时感知语法结构、执行顺序和数据依赖。在缺陷预测、代码克隆检测、代码摘要生成等任务上,这种多图融合模型明显超过纯文本模型(比如只看源码字符串的 LSTM)。需要注意的是:GNN 的输入是图,不是文本,所以前置的图构建环节质量直接决定模型上限。
新厨师不需要背下每本菜谱,只需把图纸内化成脑中的连接模式。GNN 做的正是把图结构变成可计算的向量,让机器“看一眼图就懂代码”。
- 初始向量每个节点 v 拥有一个初始嵌入 h_v^(0),通常由节点类型(“if 节点”“赋值节点”等)经编码得到。
- 邻居集合N(v) 是与 v 直接相连的所有节点,消息传递的第一层只收集一跳邻居。
- 聚合与更新邻居向量经权重矩阵 W_agg 变换后求和,与本身上一轮向量经 W_self 变换后相加,再过激活函数得到新表示。
- 多轮迭代重复 T 轮后,每个节点的向量包含 T 跳邻域的结构信息,送入分类器即可完成缺陷预测等任务。
验证与部署
新厨师学成后能不能上岗?要先试菜、测时间、收集客人反馈,再决定是否转正。AI 代码分析系统从论文到生产线,同样要走完“验证 → 评估 → 部署”这条路。
可行性验证是第一步:在小规模数据集上检验图构建流程是否跑得通、GNN 是否学得动。这个阶段最常踩的坑是“图建得慢、模型训不动”,所以要先用小模型快速迭代。随后进入性能评估:用精确率、召回率、F1 等指标衡量检测准确度,同时统计图构建耗时、显存占用、单次推理延迟这些工程指标。
落地形态多种多样:可以是 IDE 插件(写代码时实时提示)、CI 流水线(每次提交自动扫描)、或者仓库级批量扫描服务。部署后还要建立回归测试集,防止模型更新后“修复一个缺陷、引入十个误报”。
试菜通过、客人满意,新厨师才算正式上岗。AI 代码分析工具也是如此——从实验到上线,是一段用数据说话、用指标验收的完整旅程。
- 精确率 P预测为缺陷的样本中真正是缺陷的比例,衡量“报出来的问题是不是真问题”。
- 召回率 R真实缺陷中被模型找出的比例,衡量“真问题被漏掉了多少”。
- F1 调和平均P 和 R 冲突时(调高阈值 P 升 R 降),F1 给出综合得分,只有两者都高时 F1 才高。
AI 应用场景
智能缺陷检测系统
阶段:代码理解 → 多图构建 → GNN 推理 → 持续集成反馈
解决的问题:传统 lint 工具靠人工规则只能识别已知模式,遇到空指针解引用、资源泄漏这种跨语句、跨函数的缺陷就束手无策。AI 将代码转成 AST、CFG、DFG 三图融合的结构,把缺陷检测转化为图分类问题,让模型自己学习“什么样的图形结构意味着有 bug”,大幅降低漏报。
具体案例:某互联网公司支付团队在 CI 流水线接入基于 CodeQL + GNN 的缺陷扫描服务。每次提交代码,系统自动构建三图并预测高危缺陷,上线三个月后缺陷漏报率降低约 40%,单条缺陷的平均确认时间从 40 分钟缩短到 6 分钟。
漏洞挖掘与污点分析
阶段:DFG 数据流建模 → 污点传播追踪 → 风险链排序
解决的问题:安全工程师手动追踪“用户输入 → 危险函数”的路径极其耗时,且容易遗漏间接调用。基于 DFG 的污点分析自动沿着数据依赖边传播污染标记;再用 GNN 学习“哪种传播链在真实漏洞中最常见”,对告警做优先级排序,减少安全团队的无效工作量。
具体案例:某安全公司定期扫描开源项目,用 DFG 追踪 request.getParameter 到 SQL 执行处的污染链,再用图分类模型过滤低风险路径,每周产出一份可直接分发给开发者的漏洞报告,误报率比纯规则方案降低一半以上。
代码语义搜索与克隆检测
阶段:AST+CFG 图嵌入 → 向量检索 → 相似度排序
解决的问题:传统代码搜索靠关键词匹配,找不到“写法不同但作用相同”的代码。把 AST 与 CFG 融合后做图嵌入,语义相近的代码会在向量空间里彼此靠近,实现真正的结构级搜索。
具体案例:某大型软件企业搭建内部代码检索服务,开发者贴入一段代码,系统返回仓库中功能相似的实现,帮助团队复用成熟逻辑、发现重复代码。上线后代码重复率下降约 18%,新项目的人均开发效率提升明显。
Python 演示
# -*- coding: utf-8 -*-"""用三个小型实现理解 AST/CFG/DFG 的核心思想""" class Node: # AST 节点: 一个结构名 + 若干子节点 def __init__(self, name, *kids): self.name = name self.kids = kids def parse_expr(tokens): # 递归下降解析表达式并生成 AST pos = 0 def expr(): nonlocal pos node = term() while pos < len(tokens) and tokens[pos] == '+': pos += 1 node = Node('+', node, term()) return node def term(): nonlocal pos node = factor() while pos < len(tokens) and tokens[pos] == '*': pos += 1 node = Node('*', node, factor()) return node def factor(): nonlocal pos t = tokens[pos] pos += 1 return Node('id' if t.isalpha() else 'num', t) return expr() def build_cfg(): # 返回基本块列表和跳转边列表 blocks = ['entry', 'cond: a>0', 'then: c=a+b', 'else: c=a*b', 'merge: return c', 'exit'] edges = [(0, 1), (1, 2), (1, 3), (2, 4), (3, 4), (4, 5)] return blocks, edges def build_dfg(assignments): # assignments: [(变量, 依赖元组), ...] # 返回 (上游行号, 下游行号, 依赖变量, 目标变量) last_def = {} edges = [] for line_no, (var, deps) in enumerate(assignments): for dep in deps: if dep in last_def: edges.append((last_def[dep], line_no, dep, var)) last_def[var] = line_no return edges def dump(node, depth=0): # 用缩进打印一棵 AST out = ' ' * depth + node.name if len(node.kids) == 1 and isinstance(node.kids[0], str): out += ' (' + node.kids[0] + ')' print(out) for kid in node.kids: if isinstance(kid, Node): dump(kid, depth + 1) def main(): print('==== AST: a+b*c ====') ast_tree = parse_expr(list('a+b*c')) dump(ast_tree) print('\n==== CFG: if-else ====') blocks, edges = build_cfg() for i, blk in enumerate(blocks): print('块%d: %s' % (i, blk)) print('跳转边:', edges) print('\n==== DFG: a=x; c=a+b; d=c; e=a ====') assigns = [('a', ('x',)), ('c', ('a', 'b')), ('d', ('c',)), ('e', ('a',))] for up, down, dep, var in build_dfg(assigns): print('行%d 定义 %s -> 行%d 使用 (%s 依赖)' % (up, dep, down, var)) if __name__ == '__main__': main()
class Node:定义通用的语法树节点,名字表示结构类型,kids存放子节点。def parse_expr(tokens):递归下降解析器,用expr/term/factor三层函数体现运算符优先级。while pos < len(tokens) and tokens[pos] == '+':循环处理多个加号,构造左结合的加法子树。factor()` 返回 `Node('id' , t)标识符变成叶子节点,缩进打印时显示具体变量名。def build_cfg():用硬编码的基本块和边表演示 if-else 的控制流结构。edges = [(0, 1), (1, 2), (1, 3), ...]第 1 块 if 指向 2(then)和 3(else)两条出边,正是分支的图表达。def build_dfg(assignments):维护last_def字典记录每个变量最近一次定义的行号。if dep in last_def:只有当依赖变量存在“活跃定义”时才生成数据依赖边。last_def[var] = line_no更新当前变量的定义点,供后续语句引用。if len(node.kids) == 1 and isinstance(node.kids[0], str):叶子节点(只有一个字符串孩子)打印为id (a)的形式。
常见误区
误区“AST 就是把源码按照缩进整理成一棵树,跟 IDE 左侧的文件夹导航差不多,只是换了个展示方式而已。”
正解AST 是按形式文法生成的抽象结构,只保留语法本质——表达式如何嵌套、运算符优先级如何体现、语句如何组合——并丢弃空格、注释等与语义无关的符号。缩进在 Python 里虽是语法的一部分,但 AST 会把缩进结构化为显式的子树关系,而不是“长得像缩进”的树。文件夹导航是“文件系统视图”,AST 是“语言结构视图”,两者层次完全不同。
为何常见:IDE 的大纲视图、代码折叠功能给人“按结构排布代码”的第一印象,容易让初学者误以为 AST 只是对源码的视觉重组。
误区“CFG 不就是把 if/else 画成好看的流程图吗?我直接在源码里看分支不就行了,何必多此一举?”
正解CFG 的价值在于把控制流变成可计算的图结构:基本块单入口单出口,边代表跳转。有了它,编译器才能做路径可达性、死代码检测、分支覆盖率计算;AI 模型才能判断“这段代码到底有没有执行死路”。肉眼只能看到局部分支,而 CFG 把全局路径分析变成了图算法问题,这是任何静态分析框架的基石。
为何常见:初学者只看到 CFG 的“可视化”表面,看不到它作为中间表达被下游分析引擎大量消费的事实。
误区“CFG 和 DFG 都是图,节点差不多,边也差不多,应该可以互换吧?反正都是代码的一种图形化表示。”
正解两者的边在语义上完全不同。CFG 的边表示“执行权转移”——下一步执行哪个基本块;DFG 的边表示“数据依赖”——这个值从哪个定义点来。同一个变量赋值既会出现在 CFG 的某个基本块里,也可能成为 DFG 的一个定义节点;但 CFG 边不代表任何数据流动,DFG 边也不代表执行顺序。它们是正交的两个分析维度,缺一不可。
为何常见:两者都以有向图形式呈现,且构建流程有先后关系(DFG 往往基于 CFG 计算),初学者很容易把“长得像”当成“一回事”。
误区“把代码当作文本直接丢给大语言模型不就行了?它什么都能懂,为什么还要费劲去建图?”
正解文本序列是一维的,而代码的结构——谁嵌套在谁里、谁跳转到谁、谁依赖谁——本质上是多维的。GNN 在图结构上做消息传递,天然利用这些结构约束;语言模型则需要从大量文本中隐式“猜测”结构,在长代码、深层依赖上容易丢上下文。实际工业方案常把两者结合:用 LLM 生成初步解释,用 GNN + 图结构做精确的缺陷定位。结构信息依然是专业分析器的“骨相”。
为何常见:大模型时代“万物皆文本”的直觉很强,容易让人忽略结构敏感任务中显式图带来的稳定归纳偏置。
面试题
Q1. 在 AST 抽象语法树中,叶子节点通常代表什么?
- A.运算符与关键字
- B.变量与字面量节点← 正确答案
- C.基本块与跳转边
- D.数据流与依赖链
答案:B。AST 的叶子对应语法上不可再分的单元——变量名、数字、字符串等字面量。运算符如 +、* 是内部节点,因为它们还要连接两个子树;基本块属于 CFG 的概念,依赖链属于 DFG 的概念,与 AST 叶子无关,就像厨房布局图的“抽屉”是最小的存放单元一样。
Q2. 下列哪个图最能直接回答“这段代码中哪些行永远不会被执行”?
- A.抽象语法树的完整结构
- B.词法符号表的字典
- C.控制流图的可达性分析← 正确答案
- D.类型推导树的全部叶节点
答案:C。不可达代码检测本质上是在 CFG 上做可达性分析——从入口节点出发,能遍历到的节点就是可执行的,遍历不到的就是死代码。AST 只描述语法结构,词法符号表只记录 token,类型推导树关注类型关系,它们都无法直接回答“会不会执行到”,如同只看菜谱的食材清单无法知道哪道工序永远不会被客人点到。
Q3. 在控制流图 CFG 中,一个“基本块”的定义是什么?
- A.能并行执行的语句集合
- B.单入口单出口的连续语句块← 正确答案
- C.包含循环结构的代码段
- D.整个函数体的完整代码
答案:B。基本块是 CFG 的最小单元:一段连续语句,只有一个入口和一个出口,内部不存在任何跳转或分支目标。循环结构由多个基本块配合回边表达,函数体则包含多个基本块。它就像后厨出菜流程中“连续完成、中间不停顿”的一道工序。
Q4. 构建 DFG 时,一条定值-引用边的生成条件是什么?
- A.变量的定义点与使用点之间有边← 正确答案
- B.变量在任何位置被读取过
- C.两个基本块在源码中相邻
- D.函数在同一代码块调用两次
答案:A。DFG 的边表示“变量 x 在 u 处定义,在 v 处被读取,且存在一条从 u 到 v 的可行路径且路径上 x 未被重赋值”。只有“被读取过”还不够,必须存在数据传播路径;相邻基本块与函数调用次数都不产生数据依赖,就像食材必须真实传到下一道工序才算“流动”。
Q5. 对于表达式 a + b * c,在对应的 AST 中下列说法正确的是?
- A.乘号比加号更接近根节点
- B.加号比乘号更接近根节点← 正确答案
- C.两者距离根节点相同
- D.取决于变量的定义位置
答案:B。由于乘法优先级高于加法,b * c 先结合成子树,+ 成为整棵树的根节点,* 节点是根的孩子,距离根更远。操作数优先级越高,在 AST 中结合得越紧密、位置越靠下,这与变量的定义位置无关,如同油烟重先爆炒、装盘最后完成。
Q6. 用图神经网络 GNN 处理代码图时,消息传递机制的主要作用是什么?
- A.把全部源代码直接替换成嵌入
- B.聚合邻居信息迭代更新节点向量← 正确答案
- C.消除图中多余的控制流边
- D.降低图的节点总数便于计算
答案:B。GNN 的核心是消息传递:每轮让每个节点收集邻居节点的向量,加权聚合后与自身向量合并,再经激活函数更新。迭代多轮后,节点向量蕴含了其邻域结构信息。它不替换源码、不消除边、也不减少节点,而是让信息在图上“流动”,正如厨师之间的低声沟通让整个后厨的配合越来越默契。
Q7. 相比只用 AST 建模,跨图融合(同时使用 AST、CFG、DFG)的主要收益有哪些?
- A.同时捕获语法、执行与数据依赖← 正确答案
- B.图规模变得更小而更易训练
- C.不再需要词法分析阶段
- D.提供更丰富的代码语义特征← 正确答案
答案:AD。三种图各看一个维度:AST 给语法结构,CFG 给执行顺序,DFG 给数据流动,融合后模型能得到更完整的代码语义,这正是选项 A 和 D 正确的原因。但融合并不会让图变小,多图建模往往会增加节点与边的总量;词法分析仍然是构建 AST 的前置步骤,无法省去。三图融合更像把布局图、流程图、食材表一起摆在桌上,信息更全面而不是更少。
Q8. 以下哪一项是 CFG 的直接应用?
- A.变量类型自动推导
- B.语法错误定位
- C.不可达代码检测← 正确答案
- D.token 类别标注
答案:C。不可达代码检测依赖 CFG 的可达性分析:从入口节点遍历图,遍历不到的基本块就是死代码。变量类型推导主要在类型系统与 AST 上进行,语法错误定位发生在语法分析阶段,token 标注是词法分析的任务。把“哪块代码永远走不到”这个问题放到出菜流程图上一看便知。
Q9. 关于 DFG 与污点分析的关系,下列说法正确的是?
- A.污点传播沿数据流边进行← 正确答案
- B.污点分析可以脱离 CFG 独立完成
- C.数据依赖边不能跨函数传播
- D.污点源一定是变量定义处
答案:A。污点分析正是沿着 DFG 的数据依赖边,把“污点”从污染源传播到危险函数;而要精确计算数据依赖,必须先有 CFG 提供的路径信息,所以“脱离 CFG”不准确。过程间分析中数据依赖边可以跨函数传递(通过调用关系),污点源既可以是变量定义,也可以是函数参数、I/O 入口等。因此只有 A 正确。
Q10. 三图构建的正确流水线顺序是?
- A.词法分析 → 语法分析 → 控制流分析 → 数据流分析← 正确答案
- B.语法分析 → 词法分析 → 数据流分析 → 控制流分析
- C.词法分析 → 控制流分析 → 语法分析 → 数据流分析
- D.数据流分析 → 控制流分析 → 语法分析 → 词法分析
答案:A。标准流程是先词法分析切出 token,再语法分析生成 AST,然后做控制流分析得到 CFG,最后在 CFG 基础上进行数据流分析得到 DFG。每一步依赖上一步的输出,顺序不可颠倒。这就像后厨必须先备菜、再按流程炒制、最后才能追踪每样食材去了哪道菜。