#call-stack #stack #call #graph #analysis #graph-node

no-std app cargo-call-stack

静态、整个程序栈使用分析器

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命令行工具

MIT/Apache

285KB
3.5K SLoC

cargo-call-stack

静态、整个程序栈使用分析器

Call graph with a cycle

其他示例: 嵌入式 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

Call graph with direct function calls

此图中的每个节点代表一个函数,可能是自由函数、固有方法或特质方法。每条有向边表示一个“调用”关系。例如,在上面的图中,Reset 调用了 mainDefaultPreInit

每个节点还包含其 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

Filtered call graph

请注意,SysTickbaz 不出现在此调用图中,因为它们无法从 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);
}

它生成了以下调用图

Call graph with a cycle

函数 foobarbaz 使用零堆栈空间,因此它们形成的循环也使用零堆栈空间。在这种情况下,可以计算出 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;
}

该工具生成了以下调用图

Dynamic dispatch

在这里,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);
}

该工具生成了以下调用图

Function pointers

节点 i1 ()* 代表通过函数指针的调用 -- LLVM 类型 i1 ()* 等同于 Rust 的 fn() -> bool。这种间接调用可以调用 foobar,这两个函数的签名是 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
}

该工具生成了以下调用图

Lossy types

请注意,表示间接函数调用的节点类型为 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-2.0许可定义,您有意提交的任何贡献,都将根据上述内容双重许可,不附加任何额外条款或条件。

依赖项

~8–17MB
~216K SLoC