ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

函数指针和Trait对象也能分析?cargo-call-stack间接调用支持深度揭秘

函数指针和Trait对象也能分析?cargo-call-stack间接调用支持深度揭秘 函数指针和Trait对象也能分析cargo-call-stack间接调用支持深度揭秘【免费下载链接】cargo-call-stackWhole program static stack analysis项目地址: https://gitcode.com/gh_mirrors/ca/cargo-call-stackcargo-call-stack 是一个 Rust 静态栈用量分析工具它能对整个程序做静态分析生成完整调用图并计算每个函数的最大栈消耗。更厉害的是它还能分析函数指针fn()和 Trait 对象dyn Trait这类间接调用。今天这篇深度揭秘文章带你搞清楚它是如何做到的以及不完美支持背后的原理。先看效果一张调用图看懂栈分析运行工具后它会输出一个 Graphviz 的 dot 文件其中每个节点代表一个函数包含两个关键数据local函数自身的局部栈用量字节max最大栈用量含它可能调用的所有函数的栈消耗下图是一个典型的直接调用程序生成的调用图cargo-call-stack 程序静态栈分析结果比如图中Reset的max 16意味着从入口Reset出发的最深调用路径最多消耗 16 字节栈。对嵌入式微控制器开发来说这个数字直接决定了你的栈该开多大。为什么间接调用是静态分析的难点直接调用的目标在编译期就确定了画一条边就行。但间接调用不一样函数指针f()到底调谁取决于运行时指针指向Trait 对象dyn Trait的方法调用通过虚表跳转目标同样不确定对于最大栈用量这种安全相关的分析工具不能漏掉任何可能的调用目标——漏边可能低估栈用量导致真实运行时栈溢出。cargo-call-stack 的解法是在 LLVM-IR 层面根据函数签名把所有可能的候选目标全部连上边宁可多画不可漏画。函数指针调用按函数签名匹配所有候选工具会把通过函数指针的间接调用表示为一个特殊节点。比如一个签名是fn() - bool的间接调用会被表示为 LLVM 类型的i1 ()*节点i1对应 Rust 的bool返回值。然后所有签名与fn() - bool匹配的函数都会连到这个节点上——这个间接调用可能调到它们中的任何一个。项目里的示例 firmware/examples/function-pointer.rs 就是这种情况_start通过参数传入的函数指针调用而foo和bar都符合fn() - bool签名所以两者都会被连边。对应测试用例见 tests/firmware.rs 中的function_pointer测试它在多种目标架构上验证了这条边的正确生成。Trait 对象动态分发虚表调用也能连边Trait 对象dyn Foo的方法调用同样走间接调用。在 LLVM-IR 中动态分发调用点会被工具识别为i1 ({}*)这样的节点——表示对一个带隐式self参数、返回i1的方法做动态分发。此时所有能实现该 Trait 方法的函数包括默认实现和各个覆写实现都会成为候选目标被连上边。示例 firmware/examples/dynamic-dispatch.rs 展示了这个场景TraitFoo有一个默认foo方法Baz覆写它而一个签名相同但并非 Trait 方法的Quux::foo则不会被错误连边——说明匹配逻辑是基于 Trait 方法签名的而非名字相同就连。为什么不完美类型信息丢失的两个根源这里要诚实说明README 中明确标注了对函数指针和 Trait 对象是不完美imperfect支持。原因有两个1. LLVM-IR 的类型信息比 Rust 粗糙LLVM 没有u32这种无符号整数只有有符号整数。所以 Rust 里fn() - u32的函数指针在 IR 中变成了i32 ()*——这会错误地把签名是fn() - i32的函数也连上边。如果工具拥有 Rust 的完整类型信息这条错误的边就不会出现。2. 透明指针Opaque Pointers进一步抹掉类型新版 LLVM 用统一的ptr类型替代了带类型的指针于是fn f(x: i32) - bool变成fn(ptr) - i1impl Foo { fn f(self) - bool }也变成fn(ptr) - i1两者签名完全相同都会成为候选目标——调用图里就出现了实际永远不会发生的多余边。所以工具给出的最大栈用量更多是一个下界lower bound宁可高估候选保证分析偏安全但代价是调用图可能偏保守。这也是为什么官方建议该工具最适合间接调用少、无递归的嵌入式程序链接标准库的程序里比如core::fmt的大量动态分发分析结果参考价值有限。上手指南三步分析一个含间接调用的程序第 1 步安装工具务必用 stable 安装工具本身但分析需要 nightly 编译目标程序$ cargo stable install cargo-call-stack $ rustup nightly component add rust-src第 2 步在 Cargo.toml 的 release profile 启用 fat LTO非默认配置这是工具依赖的前提[profile.release] lto fat第 3 步运行分析把输出重定向为 dot 文件再用 Graphviz 渲染$ cargo nightly call-stack --example function-pointer cg.dot $ dot -Tsvg cg.dot cg.svg工具入口逻辑在 src/main.rsrun函数负责构建项目、触发编译并分析产物而间接调用的 IR 解析实现在 src/ir/define.rs 的indirect_call函数中——它专门识别call指令里目标是一个函数值的间接调用形态。什么时候该信这份分析结果场景可信度说明纯直接调用的嵌入式程序⭐⭐⭐最理想场景结果精确少量函数指针/Trait 对象⭐⭐调用图可能有冗余边max 值偏保守但安全链接标准库的普通程序⭐core::fmt、panic 分支带来大量间接调用和潜在递归参考意义有限其他值得注意的限制不支持动态链接的二进制运行时注入的代码无法静态分析硬件异常如 Cortex-M 的中断处理在调用图中是断开的子图无法合并计算全程序最大栈用量内联汇编会干扰 LLVM 自身的栈分析此时工具会回退到基于机器码的分析仅支持 ARM Cortex-M 架构一句话总结cargo-call-stack 对间接调用采用的是按签名匹配所有候选目标的保守策略——函数指针和dyn Trait的调用图能画出来、结果偏安全但可能冗余。把它用在间接调用少的嵌入式固件上你就能得到一份可靠的栈预算表反之如果项目里到处是core::fmt式的动态分发请把它当作辅助参考而非精确答案。【免费下载链接】cargo-call-stackWhole program static stack analysis项目地址: https://gitcode.com/gh_mirrors/ca/cargo-call-stack创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表