Baike.dev
All toolsOnline toolsAI codingTrendingOpen sourceNewsSubmit
Log in
< Back to tools
C

cargo-call-stack

> 编程语言
Open source

Whole program static stack analysis

662 stars0 likes1 views
WebsiteGitHub

About

Whole program static stack analysis

cargo-call-stack

Static, whole program stack usage analyzer

Other examples: Embedded CoAP / IPv4 server (source) "Hello, world!"

HEADS UP: This tool relies on an experimental feature (-Z stack-sizes) and implementation details of rustc (like symbol mangling) and could stop working with a nightly toolchain at any time. You have been warned! Last tested nightly: 2023-11-13.

NOTE: This tool main use case are embedded (microcontroller) programs that lack, or have very little, indirect function calls and recursion. This tool is of very limited use -- specially its stack usage analysis -- on programs that link to the standard library as even the smallest program that links to the standard library will contain a large number of indirect function calls (like trait object dynamic dispatch specially in core::fmt) and potential recursion (specially in panicking branches).

Features

  • The tool produces the full call graph of a program as a dot file.
  • A start point can be specified to analyze only the call graph that begins at that function.

  • Each node (function) in the call graph includes the local stack usage of the function, if available (see -Z emit-stack-sizes).

  • The maximum stack usage of each function is also computed, or at least a lower bound is provided. Maximum stack usage of a function here refers to the stack usage that includes the stack used by functions that the function may invoke.

  • The tool has imperfect support for calls through function pointers (fn()) and dynamic dispatch (dyn Trait). You will get a call graph from programs that do indirect calls but it will likely be missing edges or contain incorrect edges. It's best to use this tool on programs that only do direct function calls.

Known limitations

  • Dynamically linked binaries are not supported. As a portion of the call graph is injected at runtime by the dynamic linker is not possible to produce a complete call graph or compute an upper bound of the program's stack usage.

Installation

bash
$ # NOTE always use the latest stable release
$ cargo +stable install cargo-call-stack

$ rustup +nightly component add rust-src

Example usage

NOTE this tool requires that your Cargo project is configured to use fat LTO when Cargo uses the release profile. This is not the default configuration (as of Rust 1.63) so you'll need to add this to your Cargo.toml:

toml
[profile.release]
# `lto = true` should also work
lto = 'fat'

The tool builds your program in release mode with LTO enabled, analyses it and then prints a dot file to stdout. See cargo call-stack -h for a list of build options (e.g. --features).

NOTE if you have not set a compilation target in e.g. .cargo/config.toml then you'll need to pass the --target flag to cargo-call-stack even if you are not cross compiling

NOTE the analysis corresponds to the newly produced binary, which won't be the same as the binary produced by cargo +nightly build --release

bash
$ 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's dot can then be used to generate an image from this dot file.

bash
$ dot -Tsvg cg.dot > cg.svg

Each node in this graph represents a function, which could be a free function, an inherent method or a trait method. Each directed edge indicates a "calls" relationship. For example, in the above graph Reset calls both main and DefaultPreInit.

Each node also contains its local stack usage in bytes and its max-imum stack usage, also in bytes. The maximum stack usage includes the stack usage of all the other functions that the function could invoke.

This is the no_std program used to generate the call graph shown above.

…

In the previous example the call graph contained disconnected subgraphs. The reason for that is exceptions (also known as interrupts). SysTick, for example, is an exception handler that can preempt any function called from Reset. This exception handler is never called from software but can be invoked by the hardware at any time. These exception handlers can appear as the roots of disconnected subgraphs.

Start point

In some cases you may be interested in the maximum stack usage of a particular function. The tool lets you specify a start point which will be used to filter the call graph to only include nodes reachable from that function.

If we invoke the tool on the previous program but select main as the start point we get this call graph:

bash
$ 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

Notice that SysTick and baz don't appear in this call graph since they are not reachable from main.

Cycles

The tool can, in some cases, compute the maximum stack usage of programs that involve recursion. Recursion appears as cycles in the call graph. Consider the following example:

…

It produces the following call graph:

The functions foo, bar and baz use zero stack space thus the cycle formed by them also uses zero stack space. In this particular case the maximum stack usage of main can be computed.

For the curious this is the disassembly of the "cyclic" program:

…

And yes, the estimated maximum stack usage is correct as shown in this debug session:

…

Trait object dispatch

NOTE as of ~nightly-2022-09-20 there's no distinction between function pointers and trait objects at the llvm-ir so the analysis can insert an edge from a dynamic dispatch call site (caller) to a regular function (callee)

In some cases the tool can produce correct call graphs for programs that use trait objects -- more details about where and how it fails in the "Known limitations" section. Here's an example:

…

The tool produces the following call graph:

Here i1 ({}*) denotes dynamic dispatch of a method with (Rust) signature fn(&[mut] self) -> bool. The dynamic dispatch can invoke either Bar.foo, which boils down to the default method implementation (app::Foo::foo in the graph), or Baz.foo (::foo in the graph). In this case the tool does not a draw an edge between i1 ({}*) and Quux::foo, whose signature is also fn(&self) -> bool, so the call graph is accurate.

If you are wondering why we use LLVM notation for the function signature of the trait method: that's because the tool operates on LLVM-IR where there's no bool primitive and most of Rust's type information has been erased.

Function pointers

NOTE as of ~nightly-2022-09-20 llvm discards type information associated to pointers and uses opaque pointers in llvm-ir so functions whose signature include references (and raw pointers) will include more callee edges that will never occur in practice.

In some cases the tool can produce correct call graphs for programs that invoke functions via pointers (e.g. fn()). Here's an example:

…

The tool produces the following call graph:

The node i1 ()* represents a call via function pointer -- the LLVM type i1 ()* is equivalent to Rust's fn() -> bool. This indirect call could invoke foo or bar, the only functions with signature fn() -> bool.

Known limitations

Lossy type information

To reason about indirect function calls the tool uses the type information available in the LLVM-IR of the program. This information does not exactly match Rust's type information and leads to mislabeling of functions. For example, consider this program:

…

The tool produces the following call graph:

Note that the node that represents the indirect function call has type i32 ()* (fn() -> i32), not u32 ()*. The reason is that there's no u32 type in LLVM, there are only signed integers. This leads the tool to wrongly add an edge between i32 ()* and baz. If the tool had Rust's type information then this edge would have not been added.

Opaque pointers in llvm-ir

As of ~nightly-2022-09-20 llvm is discarding type information associated to pointers and instead using opaque pointers (ptr) in llvm-ir. This results in even greater loss of type information, for example:

  • the signature of fn f(x: &i32) -> bool becomes fn(ptr) -> i1 in llvm-ir
  • the signature of impl Foo { fn f(&self) -> bool } also becomes fn(ptr) -> i1 in llvm-ir

so the two are callee candidates for both a function pointer call with signature fn(&T) -> bool and for dynamic dispatch of a method with signature fn(&self) -> bool

Miscellaneous

Inline assembly breaks LLVM's stack usage analysis. LLVM does not consider inline assembly in its analysis and reports an incorrect number. In this case, cargo-call-stack will use its own stack usage analysis based on machine code, which only supports the ARM Cortex-M architecture.

Hardware exceptions, like SysTick on Cortex-M devices, appear as disconnected nodes in the call graph. At the moment, cargo-call-stack cannot compute the whole program maximum stack usage when exceptions are present.

The tool only supports ELF binaries because -Z emit-stack-sizes only supports the ELF format.

License

Licensed under either of

  • Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
  • MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)

at your option.

Contribution

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.

Issues· 0 open

View all issuesOpen on GitHub

No open issues yet, or sync has not completed.

> Tags

Rust

No comments yet. Be the first to share.

> Details

PublishedAug 1, 2026
UpdatedSep 17, 2026
Category编程语言
PricingOpen source

> Related tools

T
TypeScript
JavaScript 的超集,为前端与全栈提供静态类型
P
Python
通用编程语言,广泛用于 Web、数据与 AI
G
Go
Google 推出的简洁高效系统语言