代码里明明注册了回调,调用图却在分发函数处断开;沿着基类接口往下找,也没看到预期的派生类实现。这类情况在阅读旧项目时并不少见。Understand显示的是当前工程配置下解析出的关系,源码是否纳入分析、条件编译走了哪条分支,都会影响图上的内容。排查时可以拿一个明确的调用位置作对照,沿着它检查,比盯着整张图寻找遗漏更容易发现原因。
一、Understand如何生成函数调用图
调用图需要以具体函数为起点。打开图形前,先核对函数定义和分析结果,避免后面追错对象。
1、确认选中的是目标函数
①点击【Project】→【Analyze Changed Files】,更新发生变化的源码。
②在编辑器中右键目标函数,选择【View Information】,核对所属文件、命名空间、类名和参数。
有重载函数时,名称相同的几个节点很容易混淆。例如接收字符串和接收整数的两个接口,可能分别进入不同处理流程,不能只凭短名称判断调用图是否正确。
2、按方向打开调用图
①右键函数,选择【Graphical Views】→【Calls】,查看该函数向下调用的对象。
②选择【Graphical Views】→【Called By】,追查它的上游调用者。
③需要围绕同一个函数同时查看两个方向时,选择【Graphical Views】→【Butterfly】。
这里的上下游关系来自静态分析。图中列出的候选函数,未必会在同一次运行中全部执行,也不能用节点排列顺序判断实际执行先后。
3、选择便于阅读的图形变体
①在工具栏【Variant】中选择【Simplified】,从较清晰的调用树开始阅读。
②节点较多时切换到【Cluster】,展开相关文件或类的分组。
③需要检查传统图形选项时,切换到【Classic】,右键图形空白区域查看【Level】等设置。
二、Understand间接调用关系未完整显示如何核查
先看缺失发生在哪里:是调用链更深处没有展开,还是到了函数指针、虚函数分派位置就无法继续。两者需要检查的内容不同。
1、排除显示层数和过滤影响
①在当前图形支持的菜单中调整【Level】或【Calls Depth】,增加显示深度。
②查看【Enable Graphic Filters】,临时关闭过滤;打开【Edit Graphic Filters】,核对是否排除了目标函数。
③使用【Open All Nodes】展开节点,或只展开出现疑问的那条分支。
入口经过调度层、适配层才到达业务函数时,显示层数不足就会截断后面的关系。若增加深度后目标出现,保留合适的局部范围即可,不必改动解析配置。
2、检查函数指针从哪里获得目标
①在间接调用的源码行右键指针变量,选择【View Information】,展开【References】,查看初始化、赋值和参数传递位置。
②点击相关引用,检查目标函数地址是否经过结构体成员、注册接口或中间变量传递。
③返回【Classic】调用图,检查右键菜单中的【Function Pointer】选项,核对当前显示设置。
注册位置只能说明函数地址曾被保存。例如处理函数被放进回调表后,还要查看分发代码读取的是哪个表项,以及该表项是否被重新赋值。赋值和调用之间隔着几层传递时,单看函数名搜索结果容易漏掉中间环节。
3、核对头文件、宏和分析模式
①点击【Project】→【Improve Project Accuracy】→【Missing Includes】,补齐声明函数指针类型、接口类等内容的头文件路径。
②打开【Project】→【Improve Project Accuracy】→【Undefined Macros】,核对控制注册和调用分支的宏定义。
③进入【Project】→【Configure Project】→【C++】,检查分析模式;现代C++项目优先采用【Strict】。调整配置后,执行【Project】→【Analyze All Files】。
如果预期参与编译的源码在编辑器中呈灰暗状态,要核对当前宏是否选中了其他分支。分析用的宏应与目标构建一致,不能为了让图上出现某个函数,就把互斥配置全部打开。
4、确认虚调用与运行时信息的边界
①在基类虚函数上右键选择【View Information】,检查【References】,打开关联源码核对派生实现的参数和限定符。
②在预期执行的派生函数上选择【Graphical Views】→【Called By】,从目标一侧反查入口。
静态分析可以给出已解析出的候选关系,但对象实际是什么类型,还受运行条件影响。通过插件或动态加载得到的目标,也可能缺少足够的源码信息。此时应结合调试器观察对象类型、函数地址和调用栈,不能仅凭图中没有连线就判定代码不会执行。
三、Understand调用关系调整后如何复核
复核时仍使用原来的调用者、目标函数和构建配置,这样才能知道哪项调整产生了作用。
1、从调用者和被调用者两侧核对
①全量分析后,在原调用者上打开【Graphical Views】→【Calls】,保持与调整前相同的【Variant】和显示深度。
②在目标函数上打开【Graphical Views】→【Called By】,查看对应入口。
③通过【View Information】检查引用位置,确认新增关系对应预期源码行。
节点增加并不一定说明问题解决。若新增关系来自另一套产品宏或另一个同名函数,还需要回到工程配置和函数定义继续核对。
2、继续追查仍然中断的传递位置
①对指针变量右键打开【View Information】,检查【References】中最后一个能够确认的赋值或传递位置。
②若该变量提供相应图形,选择【Graphical Views】→【Assigned To】,沿赋值方向辅助追踪,再打开相关源码核验。
对于仍不能确认的部分,记录调用行、代码版本和构建条件即可。后续补入插件源码、生成文件或其他配置时,可以从这个位置接着查,不必重新阅读整条链路。
总结
一张适合交接的调用图,需要让接手的人知道它对应哪套代码、哪些构建条件,以及哪些关系还缺少运行证据。回调、虚函数和动态加载越多,这些说明越有用。有人拿图评估接口修改范围,有人据此补测试入口,同一条未经确认的连线,可能影响不同工作的判断。希望这篇文章能帮助大家在阅读和核查调用关系时少走些弯路,也给后续复查留下一份看得懂的依据。如有相关需求,欢迎交流咨询。