当前位置: 首页 > news >正文

cargo-call-stack 源码解析指南:用 nom 手写 LLVM IR 解析器,构建全程序调用图

cargo-call-stack 源码解析指南:用 nom 手写 LLVM IR 解析器,构建全程序调用图

【免费下载链接】cargo-call-stackWhole program static stack analysis项目地址: https://gitcode.com/gh_mirrors/ca/cargo-call-stack

cargo-call-stack是一个 Rust 编写的全程序静态栈使用分析工具,它最核心的引擎,就是一个用nom组合子库手写的 LLVM IR 解析器。工具先用 nightly 编译器生成 LLVM IR(.ll文本),再用这个解析器把每个函数定义、调用语句"读"成内存数据结构,最终借助petgraph组装出全程序调用图,输出给 Graphviz 渲染。

本文带你潜入 Cargo.toml 声明的nom = "7.1.3"之下,看看这个不到 2000 行的解析器是如何分层的、有哪些巧妙(和"偷懒")的设计。💡

为什么不用现成的解析库?

先看工具的数据流水线:

  1. cargo +nightly call-stack以 fat LTO 重新构建你的程序,拿到合并后的 LLVM IR 文本
  2. src/ir.rs的入口函数parse()把整篇 IR 解析为Vec<Item>
  3. Item中抽取函数名与签名,从函数体中抽取调用边
  4. 调用边装入petgraph有向图,输出.dot文件

为什么不直接用inkwell或反汇编二进制?因为工具依赖的符号表、-Z stack-sizes信息都以文本形式存在,而.ll文件恰好是结构非常规则的文本——手写一个"够用"的解析器比引入重量级 LLVM 绑定更轻、更可控。这也是本项目最值得关注的设计决策:它不需要解析完整的 LLVM IR,只需要解析出"构建调用图"所需的最小子集。

解析器分层设计:Item → Define → Stmt

解析器按 IR 的结构分成三个模块,层级关系一目了然:

模块职责关键类型
src/ir.rs顶层入口、基础词法解析parse()FnSig
src/ir/item.rs顶层项(行级结构)Item枚举,11 个变体
src/ir/define.rs函数定义与函数体内语句DefineStmt
src/ir/ty.rs类型系统Type枚举

Item枚举(item.rs)覆盖了 IR 文件的全部顶层结构:AliasGlobalTypeDefineDeclareMetadataModuleAsm等。顶层item()解析器就是对这些解析函数的一次alt分支尝试:

pub fn item(i: &str) -> IResult<&str, Item> { alt(( comment, source_filename, target, type_, global, alias, map(super::define::parse, Item::Define), declare, attributes, metadata, module_asm, ))(i) }

真正有价值的是Define变体。函数体内,Stmt枚举(define.rs)只保留了 7 种语句,其中对调用图分析至关重要的是这三种:

  • DirectCall(&str)—— 直接调用,直接给出被调函数名,一条确定的边
  • IndirectCall(FnSig)—— 间接调用(函数指针、trait 对象动态分发),只给出签名
  • Asm(&str)—— 内联汇编,LLVM 的栈分析对它"失明",工具需要另行处理

也就是说,解析阶段就已经完成了"降噪":解析器只关心会形成调用边的语句,其余语句统统归入Stmt::Other丢弃。

手写 nom 解析器的几个关键技巧

1. 用"有意的失败"划清词法边界

attribute解析器(ir.rs)处理internalfastcc等函数属性时,遇到doublefloatbitcastalias这类关键词会故意返回错误:

// have this branch always error because this is not an attribute but part of a type "double" | "float" | "void" | "ptr" => { return Err(nom::Err::Error(error_position!(i, ErrorKind::Switch))); }

这样alt组合子会自动回退,让类型解析器或语句解析器接手——用解析失败代替显式判断,是 nom 中处理"词法歧义"的惯用手法

2. "够用就好"的快捷跳过

解析器大量使用not_line_ending直接吞掉整行剩余内容,例如declare遇到llvm.开头的内建函数(item.rs):

if name.starts_with("llvm.") { // llvm intrinsic; we don't care about these let i = not_line_ending(i)?.0; Ok((i, Item::Declare(Declare { name, sig: None }))) }

内建函数不会出现在调用图里,所以整行跳过,签名记为None。这种"局部精确、全局粗略"的取舍,是嵌入式小工具写解析器的典型思路——不为完备性买单,只为下游需求买单

3. 宽松匹配:loosely_equal应对类型信息丢失

Rust 有u32,但 LLVM 只有无符号整数(i32);新版 LLVM 还使用不透明指针ptr,把fn f(x: &i32)impl Foo { fn f(&self) }都压成fn(ptr) -> i1。因此Type不能简单==比较,而是实现了loosely_equal()(ty.rs):

pub fn loosely_equal(&self, other: &Self) -> bool { match (self, other) { (Self::OpaquePointer, Self::OpaquePointer) => true, (Self::OpaquePointer, Self::Pointer(_)) => true, (Self::Pointer(_), Self::OpaquePointer) => true, // ... } }

这正是工具处理间接调用时的核心逻辑:把IndirectCall(FnSig)与所有已解析函数的签名做宽松比较,把"签名兼容"的函数全部连边。代价是可能出现多余边(详见 README.md 的 "Lossy type information" 章节),收益是不依赖 Rust 类型信息也能工作。

4. 精确的报错定位

parse()(ir.rs)把 nom 返回的失败偏移量换算回行号再报错,而不是抛出一个无法定位的偏移值——毕竟.ll文件动辄上万行。

5. 用真实 IR 片段做测试

src/ir/define/目录下有 11 个真实编译器输出的测试文件(parse1.ll 到 parse11.ll),覆盖内联汇编、间接调用、packed struct 等边缘场景。写解析器时,来自真实编译器的测试样例比手写字符串可靠得多

从调用边到全程序调用图

解析完成后,工具把每条Stmt翻译成图论操作:DirectCall连一条确定边;IndirectCallloosely_equal向所有签名匹配的函数连边;再结合-Z emit-stack-sizes提供的每个函数本地栈用量,做全图最大栈使用量计算,最后输出 dot 格式。以示例程序为例,工具产出的全程序调用图如下(每个节点同时标注localmax栈用量):

图中Reset同时调用mainDefaultPreInit,这正是解析器从define体里抽出的DirectCall边。

给新手的可借鉴清单

  • 组合子思维tag/char拼词法,alt/many0/separated_list0/delimited搭结构,解析器就是"可组合的小函数"
  • 为下游需求裁剪文法:不解析完整 IR,只保留调用图所需的最小子集,用not_line_ending大胆跳过
  • 失败也是信息:用"故意返回错误"引导alt回退,解决词法歧义
  • 测试用真实输入:保存真实编译器输出作为回归测试语料

如果你想继续深挖,建议按 src/ir.rs → src/ir/item.rs → src/ir/define.rs → src/ir/ty.rs 的顺序通读源码,再跑一遍 tests/firmware.rs 的固件级集成测试,即可完整复现"文本 IR → 全程序调用图"的全过程。🚀

【免费下载链接】cargo-call-stackWhole program static stack analysis项目地址: https://gitcode.com/gh_mirrors/ca/cargo-call-stack

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

http://www.cnnetsun.cn/news/4172983.html

相关文章:

  • 从数学建模到量化交易:基于MCM赛题的策略开发全流程解析
  • 泰拉瑞亚灾厄Mod完整安装指南:从版本选择到汉化排错
  • 2026年硬盘盒选购指南:从SATA到NVMe协议,实测16款主流产品
  • unicode-segmentation如何实现UAX29标准:剖析GraphemeCursor状态机与GB规则判定逻辑
  • personal-jekyll-theme源码架构全解析:Jekyll布局、Liquid模板与组件化设计实战
  • HyperRogue的.tes镶嵌文件格式完全指南:定义并加载你的自定义几何
  • 计算机考研408核心考点:虚拟内存地址转换机制深度解析与真题实战
  • 机器人应用泛化:从汽车产线到千行百业的技术变革与实践指南
  • ESP-FC 低成本飞行控制器完整指南:约 5 美元打造自己的 ESP32 四轴飞控
  • Spec4j:基于Java注解的REST API文档自动化生成方案
  • AI招聘技术:原生智能体如何重塑人才选拔流程
  • pypdf 完整指南:合并、拆分、水印等 6 个常用操作一次讲清
  • 网站链接检查神器:broken-link-checker 帮你 5 分钟扫完整站 404
  • AltTab 使用指南:macOS 上的窗口切换技巧
  • C++运算符重载与函数模板:从语法特性到工程实践的核心设计工具
  • Czkawka 跨平台视频查重:从安装到批量清理的完整指南
  • C++模板编程中typename关键字的深度解析与应用实践
  • 数维杯数学建模竞赛:A/B/C三类赛题通用破题思路与实战建模指南
  • TimeSage-MT:构建多轮对话时间序列智能体的评测基准与工程实践
  • andrej-karpathy-skills:把 AI 的“顺手重构“管住
  • 视频标题 - BV1xx411c7mX
  • palera1n 越狱工具:从连上设备到完成越狱的完整指南
  • 5分钟清干净满地物品:流放之路过滤器 NeverSink Filter 快速上手指南
  • 告别手动整理!FileMover开源工具实现文件批量自动化管理
  • wewe-rss RSS订阅管理前端错误监控完整指南:从白屏到分层防御
  • 数学建模中的概率模型:从随机变量到蒙特卡洛模拟的完整指南
  • Auto.js 简介与避坑指南
  • 工业AGV多车路径规划:从轻量级算法到仓库落地实践
  • 从文件到屏幕:Python/java 字符编码、解码、文本处理的底层逻辑解析
  • OPTEE 3.15运行在QEMU ARMv8上