news 2026/8/23 11:19:01

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/23 11:18:59

从数学建模到量化交易:基于MCM赛题的策略开发全流程解析

1. 项目概述&#xff1a;从数学建模到量化交易策略的实战跨越 2022年美国大学生数学建模竞赛&#xff08;MCM/ICM&#xff09;的C题&#xff0c;将一群数学、统计和计算机背景的学生&#xff0c;直接推向了金融市场的核心地带——量化交易策略。这个题目在当时引起了不小的讨论…

作者头像 李华
网站建设 2026/8/23 11:15:23

泰拉瑞亚灾厄Mod完整安装指南:从版本选择到汉化排错

如果你在《泰拉瑞亚》里已经通关了原版&#xff0c;觉得大师模式也不过如此&#xff0c;那么“灾厄”这个名字&#xff0c;你大概率听说过&#xff0c;甚至可能已经被它“折磨”过。这个以超高难度、海量内容和史诗级 Boss 战闻名的 Mod&#xff0c;早已成为硬核玩家证明自己的…

作者头像 李华
网站建设 2026/8/23 11:13:04

2026年硬盘盒选购指南:从SATA到NVMe协议,实测16款主流产品

最近在整理旧硬盘数据时&#xff0c;发现手头好几个闲置的2.5英寸和M.2固态硬盘&#xff0c;想买个硬盘盒把它们利用起来&#xff0c;结果一搜发现从十几块到上百块的产品琳琅满目&#xff0c;SATA、NVMe协议傻傻分不清&#xff0c;品牌更是五花八门。相信很多朋友也遇到过类似…

作者头像 李华