16 个版本
0.1.15 | 2023年2月27日 |
---|---|
0.1.14 | 2022年11月24日 |
0.1.12 | 2022年10月17日 |
0.1.11 | 2022年7月7日 |
0.1.0 | 2018年12月3日 |
#28 在 命令行工具
285KB
3.5K SLoC
cargo-call-stack
静态、整个程序栈使用分析器
其他示例: 嵌入式 CoAP / IPv4 服务器 (源代码) "Hello, world!"
注意:此工具依赖于实验性功能(-Z stack-sizes
)和 rustc
的实现细节(如符号混淆)并且可能随时停止工作。已警告!最后一次测试的夜间版本:2022-09-20。
注意:此工具的主要用例是缺乏或只有少量间接函数调用和递归的嵌入式(微控制器)程序。对于链接到标准库的程序,此工具非常有限,特别是在栈使用分析方面——即使是链接到标准库的最小程序也将包含大量间接函数调用(如特例对象动态调度,特别是在 core::fmt
中)和可能的递归(特别是在恐慌分支中)。
功能
-
该工具以 dot 文件 的形式生成程序的完整调用图。
-
可以指定 起点 以仅分析从该函数开始的调用图。
-
调用图中的每个节点(函数)都包含函数的本地栈使用情况,如果 可用(参见
---stack-sizes
)。 -
每个函数的最大栈使用量也计算出来,或者至少提供一个下限。此处函数的最大栈使用量是指包括函数可能调用的函数所使用的栈。
-
此工具对通过函数指针(
fn()
)和动态派发(dyn Trait
)进行的调用支持不完善。您将从一个执行间接调用的程序中获得调用图,但它可能缺少边或包含错误的边。最好在仅执行直接函数调用的程序上使用此工具。
已知限制
- 不支持动态链接的二进制文件。由于动态链接器在运行时注入调用图的一部分,因此无法生成完整的调用图或计算程序堆栈使用的上界。
安装
$ # NOTE always use the latest stable release
$ cargo +stable install cargo-call-stack
$ rustup +nightly component add rust-src
示例用法
注意 此工具要求您的 Cargo 项目配置为在 Cargo 使用发布配置时使用 fat LTO。这并非默认配置(截至 Rust 1.63),因此您需要将其添加到您的
Cargo.toml
[profile.release]
# `lto = true` should also work
lto = 'fat'
该工具以发布模式启用 LTO 构建您的程序,分析它,然后将点文件打印到标准输出。有关构建选项(例如 --features
)的列表,请参阅 cargo call-stack -h
。
注意 如果您在例如
.cargo/config.toml
中未设置编译目标,则即使在执行交叉编译时,也需要将--target
标志传递给cargo-call-stack
。
注意 分析对应于新生成的二进制文件,它不会与由
cargo +nightly build --release
生成的二进制文件相同。
$ cargo +nightly call-stack --example app > cg.dot
warning: assuming that llvm_asm!("") does *not* use the stack
warning: assuming that llvm_asm!("") does *not* use the stack
然后可以使用 Graphviz 的 dot
从此点文件生成图像。
$ dot -Tsvg cg.dot > cg.svg
此图中的每个节点代表一个函数,可能是自由函数、固有方法或特质方法。每条有向边表示一个“调用”关系。例如,在上面的图中,Reset
调用了 main
和 DefaultPreInit
。
每个节点还包含其 local
堆栈使用量(以字节为单位)和其 max
最大堆栈使用量,也以字节为单位。最大堆栈使用量包括函数可能调用的所有其他函数的堆栈使用量。
这是用于生成上述调用图的 no_std
程序。
#![feature(llvm_asm)]
#![no_main]
#![no_std]
extern crate panic_halt;
use core::ptr;
use cortex_m_rt::{entry, exception};
#[entry]
fn main() -> ! {
foo();
bar();
loop {}
}
#[inline(never)]
fn foo() {
// spill variables onto the stack
unsafe { llvm_asm!("" : : "r"(0) "r"(1) "r"(2) "r"(3) "r"(4) "r"(5)) }
}
#[inline(never)]
fn bar() {
unsafe { llvm_asm!("" : : "r"(0) "r"(1) "r"(2) "r"(3) "r"(4) "r"(5) "r"(6) "r"(7)) }
}
#[exception]
fn SysTick() {
bar();
}
#[inline(never)]
fn baz() {
let x = 0;
unsafe {
// force `x` to be on the stack
ptr::read_volatile(&&x);
}
}
在上一个示例中,调用图包含不连接的子图。这种情况的原因是 异常(也称为中断)。例如,
SysTick
是一个可以中断从Reset
调用的任何函数的异常处理程序。这个异常处理程序永远不会从软件中调用,但可以在任何时候由硬件调用。这些异常处理程序可以出现在不连接的子图的根处。
起点
在某些情况下,您可能对特定函数的最大堆栈使用量感兴趣。该工具允许您指定一个 起点,它将用于过滤调用图,只包括从该函数可到达的节点。
如果我们对上一个程序调用此工具,但选择 main
作为起点,我们得到此调用图
$ cargo +nightly call-stack --example app main > cg.dot
warning: assuming that llvm_asm!("") does *not* use the stack
warning: assuming that llvm_asm!("") does *not* use the stack
请注意,SysTick
和 baz
不出现在此调用图中,因为它们无法从 main
访达。
循环
在某些情况下,该工具可以计算涉及递归的程序的最大堆栈使用量。递归在调用图中表现为循环。以下是一个例子:
#![feature(llvm_asm)]
#![no_main]
#![no_std]
extern crate panic_halt;
use core::sync::atomic::{AtomicBool, Ordering};
use cortex_m_rt::{entry, exception};
static X: AtomicBool = AtomicBool::new(true);
#[inline(never)]
#[entry]
fn main() -> ! {
foo();
quux();
loop {}
}
// these three functions form a cycle that breaks when `SysTick` runs
#[inline(never)]
fn foo() {
if X.load(Ordering::Relaxed) {
bar()
}
}
#[inline(never)]
fn bar() {
if X.load(Ordering::Relaxed) {
baz()
}
}
#[inline(never)]
fn baz() {
if X.load(Ordering::Relaxed) {
foo()
}
}
#[inline(never)]
fn quux() {
// spill variables onto the stack
unsafe { llvm_asm!("" : : "r"(0) "r"(1) "r"(2) "r"(3) "r"(4) "r"(5)) }
}
#[exception]
fn SysTick() {
X.store(false, Ordering::Relaxed);
}
它生成了以下调用图
函数 foo
、bar
和 baz
使用零堆栈空间,因此它们形成的循环也使用零堆栈空间。在这种情况下,可以计算出 main
的最大堆栈使用量。
对好奇的人来说,这是“循环”程序的反汇编
08000400 <app::foo>:
8000400: f240 0000 movw r0, #0
8000404: f2c2 0000 movt r0, #8192 ; 0x2000
8000408: 7800 ldrb r0, [r0, #0]
800040a: 0600 lsls r0, r0, #24
800040c: bf18 it ne
800040e: f000 b801 bne.w 8000414 <app::bar>
8000412: 4770 bx lr
08000414 <app::bar>:
8000414: f240 0000 movw r0, #0
8000418: f2c2 0000 movt r0, #8192 ; 0x2000
800041c: 7800 ldrb r0, [r0, #0]
800041e: 0600 lsls r0, r0, #24
8000420: bf18 it ne
8000422: f000 b801 bne.w 8000428 <app::baz>
8000426: 4770 bx lr
08000428 <app::baz>:
8000428: f240 0000 movw r0, #0
800042c: f2c2 0000 movt r0, #8192 ; 0x2000
8000430: 7800 ldrb r0, [r0, #0]
8000432: 0600 lsls r0, r0, #24
8000434: bf18 it ne
8000436: f7ff bfe3 bne.w 8000400 <app::foo>
800043a: 4770 bx lr
0800043c <app::quux>:
800043c: b580 push {r7, lr}
800043e: f04f 0c00 mov.w ip, #0
8000442: f04f 0e01 mov.w lr, #1
8000446: 2202 movs r2, #2
8000448: 2303 movs r3, #3
800044a: 2004 movs r0, #4
800044c: 2105 movs r1, #5
800044e: bd80 pop {r7, pc}
08000450 <main>:
8000450: f7ff ffd6 bl 8000400 <app::foo>
8000454: f7ff fff2 bl 800043c <app::quux>
8000458: e7fe b.n 8000458 <main+0x8>
是的,根据此调试会话,估计的最大堆栈使用量是正确的
(gdb) b app::foo
(gdb) b app::bar
(gdb) b app::baz
(gdb) c
Continuing.
Breakpoint 3, main () at src/main.rs:16
16 foo();
(gdb) p $sp
$1 = (void *) 0x20005000
(gdb) c
Continuing.
halted: PC: 0x08000400
Breakpoint 4, app::foo () at src/main.rs:31
31 if X.load(Ordering::Relaxed) {
(gdb) p $sp
$2 = (void *) 0x20005000
(gdb) c
Continuing.
halted: PC: 0x0800040c
Breakpoint 5, app::bar () at src/main.rs:38
38 if X.load(Ordering::Relaxed) {
(gdb) p $sp
$3 = (void *) 0x20005000
(gdb) c
Continuing.
halted: PC: 0x08000420
Breakpoint 6, app::baz () at src/main.rs:45
45 if X.load(Ordering::Relaxed) {
(gdb) p $sp
$4 = (void *) 0x20005000
(gdb) c
Continuing.
halted: PC: 0x08000434
Breakpoint 4, app::foo () at src/main.rs:31
31 if X.load(Ordering::Relaxed) {
(gdb) p $sp
$5 = (void *) 0x20005000
特质对象调度
注意,截至 ~nightly-2022-09-20,在 llvm-ir 中没有区分函数指针和特质对象,因此分析可以从动态调度调用点(调用者)插入到常规函数(被调用者)的边。
在某些情况下,该工具可以为使用特质对象的程序生成正确的调用图(更多关于在哪里以及如何失败的信息,请参阅“已知限制”部分)。以下是一个例子:
#![feature(llvm_asm)]
#![no_main]
#![no_std]
extern crate panic_halt;
use cortex_m_rt::{entry, exception};
use spin::Mutex; // spin = "0.5.0"
static TO: Mutex<&'static (dyn Foo + Sync)> = Mutex::new(&Bar);
#[entry]
#[inline(never)]
fn main() -> ! {
// trait object dispatch
(*TO.lock()).foo();
Quux.foo();
loop {}
}
trait Foo {
// default implementation of this method
fn foo(&self) -> bool {
// spill variables onto the stack
unsafe { llvm_asm!("" : : "r"(0) "r"(1) "r"(2) "r"(3) "r"(4) "r"(5)) }
false
}
}
struct Bar;
// uses the default method implementation
impl Foo for Bar {}
struct Baz;
impl Foo for Baz {
// overrides the default method
fn foo(&self) -> bool {
unsafe { llvm_asm!("" : : "r"(0) "r"(1) "r"(2) "r"(3) "r"(4) "r"(5) "r"(6) "r"(7)) }
true
}
}
struct Quux;
impl Quux {
// not a trait method!
#[inline(never)]
fn foo(&self) -> bool {
// NOTE(llvm_asm!) side effect to preserve function calls to this method
unsafe { llvm_asm!("NOP" : : : : "volatile") }
false
}
}
// this handler can change the trait object at any time
#[exception]
fn SysTick() {
*TO.lock() = &Baz;
}
该工具生成了以下调用图
在这里,i1 ({}*)
表示具有(Rust)签名的动态调度方法的调用fn(&[mut] self) -> bool
。动态调度可以调用 Bar.foo
,这归结为默认方法实现(图中的 app::Foo::foo
),或者 Baz.foo
(图中的 <app::Baz as app::Foo>::foo
)。在这种情况下,工具在 i1 ({}*)
和 Quux::foo
之间没有画边,其签名也是 fn(&self) -> bool
,因此调用图是准确的。
如果您想知道为什么我们使用 LLVM 符号来表示特质方法的函数签名:这是因为工具在 LLVM-IR 上操作,其中没有 bool
原始类型,并且 Rust 的大多数类型信息已经被删除。
函数指针
注意,截至 ~nightly-2022-09-20,llvm 丢弃了与指针关联的类型信息,并在 llvm-ir 中使用不透明指针,因此签名包含引用(和原始指针)的函数将包含更多实际不会发生的被调用者边。
在某些情况下,该工具可以为通过指针调用函数的程序生成正确的调用图(例如 fn()
)。以下是一个例子
#![feature(llvm_asm)]
#![no_main]
#![no_std]
extern crate panic_halt;
use core::sync::atomic::{AtomicPtr, Ordering};
use cortex_m_rt::{entry, exception};
static F: AtomicPtr<fn() -> bool> = AtomicPtr::new(foo as *mut _);
#[inline(never)]
#[entry]
fn main() -> ! {
if let Some(f) = unsafe { F.load(Ordering::Relaxed).as_ref() } {
// call via function pointer
f();
}
loop {}
}
fn foo() -> bool {
// spill variables onto the stack
unsafe { llvm_asm!("" : : "r"(0) "r"(1) "r"(2) "r"(3) "r"(4) "r"(5)) }
false
}
fn bar() -> bool {
unsafe { llvm_asm!("" : : "r"(0) "r"(1) "r"(2) "r"(3) "r"(4) "r"(5) "r"(6) "r"(7)) }
true
}
// this handler can change the function pointer at any time
#[exception]
fn SysTick() {
F.store(bar as *mut _, Ordering::Relaxed);
}
该工具生成了以下调用图
节点 i1 ()*
代表通过函数指针的调用 -- LLVM 类型 i1 ()*
等同于 Rust 的 fn() -> bool
。这种间接调用可以调用 foo
或 bar
,这两个函数的签名是 fn() -> bool
。
已知限制
类型信息丢失
为了推理间接函数调用,工具使用程序 LLVM-IR 中的类型信息。这些信息并不完全匹配 Rust 的类型信息,导致函数被错误标记。例如,考虑这个程序
#![feature(llvm_asm)]
#![no_main]
#![no_std]
extern crate panic_halt;
use core::{
ptr,
sync::atomic::{AtomicPtr, Ordering},
};
use cortex_m_rt::{entry, exception};
static F: AtomicPtr<fn() -> u32> = AtomicPtr::new(foo as *mut _);
#[inline(never)]
#[entry]
fn main() -> ! {
if let Some(f) = unsafe { F.load(Ordering::Relaxed).as_ref() } {
// call via function pointer
f();
}
let x = baz();
unsafe {
// NOTE(volatile) used to preserve the return value of `baz`
ptr::read_volatile(&x);
}
loop {}
}
// this handler can change the function pointer at any time
#[exception]
fn SysTick() {
F.store(bar as *mut _, Ordering::Relaxed);
}
fn foo() -> u32 {
// spill variables onto the stack
unsafe { llvm_asm!("" : : "r"(0) "r"(1) "r"(2) "r"(3) "r"(4) "r"(5)) }
0
}
fn bar() -> u32 {
1
}
#[inline(never)]
fn baz() -> i32 {
unsafe { llvm_asm!("" : : "r"(0) "r"(1) "r"(2) "r"(3) "r"(4) "r"(5) "r"(6) "r"(7)) }
F.load(Ordering::Relaxed) as usize as i32
}
该工具生成了以下调用图
请注意,表示间接函数调用的节点类型为 i32 ()*
(fn() -> i32
),而不是 u32 ()*
。原因是 LLVM 中没有 u32
类型,只有有符号整数。这导致工具错误地将 i32 ()*
和 baz
之间添加边。如果工具有了 Rust 的类型信息,则不会添加此边。
LLVM-IR 中的不可见指针
截至 ~nightly-2022-09-20,LLVM 正在丢弃与指针关联的类型信息,并在 llvm-ir 中使用不可见指针(ptr
)。这导致类型信息丢失更加严重,例如
fn f(x: &i32) -> bool
的签名在 llvm-ir 中变为fn(ptr) -> i1
impl Foo { fn f(&self) -> bool }
的签名在 llvm-ir 中也变为fn(ptr) -> i1
因此,这两个函数都是具有签名 fn(&T) -> bool
的函数指针调用和具有签名 fn(&self) -> bool
的方法动态调用的候选者。
杂项
内联汇编破坏了LLVM的栈使用分析。LLVM在其分析中 不 考虑内联汇编,并报告了错误的数据。在这种情况下,cargo-call-stack
将使用基于机器码的自己的栈使用分析,它仅支持ARM Cortex-M架构。
硬件异常,如Cortex-M设备上的 SysTick
,在调用图中表现为断开的节点。目前,当存在异常时,cargo-call-stack
无法计算整个程序的最大栈使用量。
该工具仅支持ELF二进制文件,因为 -Z emit-stack-sizes
仅支持ELF格式。
许可
在以下任一许可下授权:
- Apache License,版本2.0 (LICENSE-APACHE 或 https://apache.ac.cn/licenses/LICENSE-2.0)
- MIT许可 (LICENSE-MIT 或 http://opensource.org/licenses/MIT)
由您选择。
贡献
除非您明确声明,否则根据Apache-2.0许可定义,您有意提交的任何贡献,都将根据上述内容双重许可,不附加任何额外条款或条件。
依赖项
~8–17MB
~216K SLoC