Whole program static stack analysis

Overview

cargo-call-stack

Static, whole program stack analysis

Call graph with a cycle

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!

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.

Installation

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

$ rustup +nightly component add rust-src

Example usage

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).

IMPORTANT: the analysis corresponds to the newly produced binary, which won't be the same as the binary produced by 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's dot can then be used to generate an image from this dot file.

$ dot -Tsvg cg.dot > cg.svg

Call graph with direct function calls

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.

#![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);
    }

}

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:

$ 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

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:

#![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);
}

It produces the following call graph:

Call graph with a cycle

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:

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>

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

(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

Trait object dispatch

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:

#![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;
}

The tool produces the following call graph:

Dynamic dispatch

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 (<app::Baz as app::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

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

#![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);
}

The tool produces the following call graph:

Function pointers

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:

#![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
}

The tool produces the following call graph:

Lossy types

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.

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

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.

Comments
  • Unexpected difference in LLVM and cargo-call-stack size for untyped functions

    Unexpected difference in LLVM and cargo-call-stack size for untyped functions

    I have an application that triggers this assert in cargo-call-stack:

    thread 'main' panicked at 'assertion failed: `(left == right)`
      left: `0`,
     right: `8`: BUG: LLVM reported that `OUTLINED_FUNCTION_13` uses 0 bytes of stack but this doesn't match our analysis
    

    originating from this code

                                // in all other cases our results should match
    
                                assert_eq!(
                                    *llvm_stack, stack,
                                    "BUG: LLVM reported that `{}` uses {} bytes of stack but \
                                     this doesn't match our analysis",
                                    canonical_name, llvm_stack
                                );
    

    Running objdump reveals the instructions for OUTLINED_FUNCTION_13:

    0003fe96 <OUTLINED_FUNCTION_13>:
       3fe96: 4d f8 08 ed  	str	lr, [sp, #-8]!
       3fe9a: 50 46        	mov	r0, r10
       3fe9c: f0 f7 1a f8  	bl	0x2fed4 <core::cell::RefCell$LT$T$GT$::borrow_mut::hd9b9aa1e3adf77aa> @ imm = #-65484
       3fea0: 05 46        	mov	r5, r0
       3fea2: 04 30        	adds	r0, #4
       3fea4: 0e 46        	mov	r6, r1
       3fea6: ef f7 33 fa  	bl	0x2f310 <core::ptr::drop_in_place$LT$core..option..Option$LT$drogue_device..drivers..ble..mesh..config..network..Network$GT$$GT$::h7eb090b9876c8058> @ imm = #-68506
       3feaa: 28 46        	mov	r0, r5
       3feac: 59 46        	mov	r1, r11
       3feae: 4f f4 dc 72  	mov.w	r2, #440
       3feb2: 5d f8 08 eb  	ldr	lr, [sp], #8
       3feb6: 01 f0 27 b8  	b.w	0x40f08 <__aeabi_memcpy4> @ imm = #4174
    

    And it looks like it is supposed to use 8 bytes of stack.

    Is the correct way forward to modify the thumb.rs to catch this sp modification so that the calculated value is correct?

    opened by lulf 6
  • Fix parsing

    Fix parsing

    This commit fixes some parsing errors I had. Maybe this fixes #22, #28, #32 and #33.

    Parsing is fixed for my own project, though I still have logged errors (at least the dot file is generated):

    [2022-04-28T15:23:33Z ERROR cargo_call_stack] BUG? no callees for `void ({}*)`
    [2022-04-28T15:23:33Z ERROR cargo_call_stack] BUG? no callees for `void ({}*, void ({}*)*)`
    
    opened by thvdveld 6
  • failed to parse .ll file

    failed to parse .ll file

    When I run

    cargo +nightly call-stack --bin my-binary
    

    I get:

    error: BUG: failed to parse .ll file; please submit a bug report. Details: Error(Code(CompleteStr("…

    followed by 177 megabytes of code. Pasting that much into a bug report probably wouldn't be helpful. Besides, this project contains some proprietary code, so I'm not allowed to disclose all of it.

    Perhaps you could change this error case to quote the unparsable input more precisely?

    opened by kornelski 6
  • assertion failure on ARM thumb

    assertion failure on ARM thumb

    [2022-11-02T16:09:07Z WARN cargo_call_stack] assuming that asm!("push {lr}\0Asub sp, sp, #4\0Amov r2, sp\0Abl __udivmodsi4\0Aldr r1, [sp]\0Aadd sp, sp, #4\0Apop {pc}") does not use the stack in __aeabi_uidivmod thread 'main' panicked at 'assertion failed: (left != right) left: 14, right: 14', /Users/aholtzma/.cargo/registry/src/github.com-1ecc6299db9ec823/cargo-call-stack-0.1.12/src/thumb.rs:136:13 stack backtrace: 0: rust_begin_unwind at /rustc/59eed8a2aac0230a8b53e89d4e99d55912ba6b35/library/std/src/panicking.rs:517:5 1: core::panicking::panic_fmt at /rustc/59eed8a2aac0230a8b53e89d4e99d55912ba6b35/library/core/src/panicking.rs:101:14 2: core::panicking::assert_failed_inner 3: core::panicking::assert_failed 4: cargo_call_stack::thumb::analyze 5: cargo_call_stack::run 6: cargo_call_stack::main

    opened by aholtzma 5
  • BUG: failed to parse LLVM IR

    BUG: failed to parse LLVM IR

    Hello,

    Error

    I get the following message on my project

    error: failed to parse application's LLVM IR from `project/target/x86_64-unknown-linux-gnu/release/deps/bin-0d269819aa1d3063.ll`: BUG: failed to parse LLVM IR; please submit a cargo-call-stack bug report and attach the `.ll` file: Failure("Eof in line 880")
    

    Info

    $ cargo +nightly call-stack  --version
    cargo-call-stack 0.1.7
    $ cargo --version
    cargo 1.61.0 (a028ae4 2022-04-29)
    $ rustc --version
    rustc 1.61.0 (fe5b13d68 2022-05-18)
    

    bin-0d269819aa1d3063.ll.txt

    Let me know if I can help figuring it out.

    thanks!

    opened by forty 5
  • Fix parser error on `undef` type in argument list of tail call.

    Fix parser error on `undef` type in argument list of tail call.

    LLVM output was generated by rustc nightly-2022-11-16.

    I ran into an issue with the parser in the tool and found and 'fixed' the issue.

    Note: I wasn't able to run the integration tests, they ALL fail on my machine, but I don't think the code change I made is the cause. I have an underlying issue with the rustc linker failing to link targets due to an bug that is fixed, but still unreleased. (See https://github.com/rust-lang/rust/issues/88704)

    The specific line that fails to parse is line 3 in the example.

    tail call void %1(i32 noundef 0, i32 undef) #39, !dbg !104902
    

    Specifically, it's missing handling for undef after the i32 type.

    opened by hydra 4
  • add heuristics to deal with outlined functions

    add heuristics to deal with outlined functions

    as of version 14.0.6, the outlined functions produced by one of LLVM's optimization passes do not get assigned a correct stack size by LLVM's emit-stack-sizes pass

    to work around the issue use the stack usage obtained from analyzing the machine code (ARM Cortex-M only) instead of the stack usage reported by LLVM

    also, avoid producing warnings about not having type information for outlined functions. so far all these outlined functions appear to be called directly and never via a function pointer

    closes #65

    opened by japaric 3
  • build failure on stm32

    build failure on stm32

    While trying to run this on an STM32L0x project we get the following error:

    $ cargo  +nightly call-stack --bin STM32_controller
       Compiling gimli v0.25.0
    error[E0432]: unresolved import `alloc::sync`
     --> /Users/aholtzma/.cargo/registry/src/github.com-1ecc6299db9ec823/gimli-0.25.0/src/read/dwarf.rs:2:12
      |
    2 | use alloc::sync::Arc;
      |            ^^^^ could not find `sync` in `alloc`
    
    error[E0282]: type annotations needed
      --> /Users/aholtzma/.cargo/registry/src/github.com-1ecc6299db9ec823/gimli-0.25.0/src/read/dwarf.rs:97:18
       |
    97 |             sup: None,
       |                  ^^^^ cannot infer type for type parameter `T` declared on the enum `Option`
    
    error[E0283]: type annotations needed
      --> /Users/aholtzma/.cargo/registry/src/github.com-1ecc6299db9ec823/gimli-0.25.0/src/read/dwarf.rs:60:5
       |
    21 | #[derive(Debug, Default)]
       |                 ------- in this derive macro expansion
    ...
    60 |     pub sup: Option<Arc<Dwarf<R>>>,
       |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ cannot infer type
       |
       = note: cannot satisfy `_: Default`
       = note: this error originates in the derive macro `Default` (in Nightly builds, run with -Z macro-backtrace for more info)
    
    Some errors have detailed explanations: E0282, E0283, E0432.`
    

    It's not clear what even uses gimli, so I'm a bit at a loss where to go from here.

    opened by aholtzma-am 3
  • assertion with 64-bit division

    assertion with 64-bit division

    Source code

    static X: AtomicUsize = AtomicUsize::new(0);
    
    #[entry]
    fn main() -> ! {
        X.store(div64 as usize, Ordering::Relaxed);
    
        loop {}
    }
    
    fn div64(x: u64, y: u64) -> u64 {
        x / y
    }
    
    #[exception]
    fn SysTick() {
        X.fetch_add(1, Ordering::Relaxed);
    }
    

    Error message

    2021-09-17T10:55:53Z WARN  cargo_call_stack] no type information for `_ZN17compiler_builtins3int19specialized_div_rem11u64_div_rem17hfa5cc727e5163f40E`
    thread 'main' panicked at 'assertion failed: `(left == right)`
      left: `8`,
     right: `24`: BUG: LLVM reported that `__aeabi_uldivmod` uses 8 bytes of stack but this doesn't match our analysis', src/main.rs:962:29
    

    Machine code

    000004ec <__aeabi_uldivmod>:
     4ec:   b510        push    {r4, lr}
     4ee:   b084        sub sp, #16
     4f0:   ac02        add r4, sp, #8
     4f2:   9400        str r4, [sp, #0]
     4f4:   f000 f925   bl  742 <__udivmoddi4>
     4f8:   9a02        ldr r2, [sp, #8]
     4fa:   9b03        ldr r3, [sp, #12]
     4fc:   b004        add sp, #16
     4fe:   bd10        pop {r4, pc}
    
    bug 
    opened by japaric 3
  • error: options `-C embed-bitcode=no` and `-C lto` are incompatible

    error: options `-C embed-bitcode=no` and `-C lto` are incompatible

    I wanted to try this out, but after compiling for a while, it exits with:

    error: options `-C embed-bitcode=no` and `-C lto` are incompatible
    

    I'm using nightly-2020-06-08. Is that the problem?

    A quick search turned up https://github.com/rust-lang/cargo/pull/8066

    opened by WyseNynja 3
  • Panic!() Function in Analyzed Code causes Assertion Errors in Cargo-Call-Stack

    Panic!() Function in Analyzed Code causes Assertion Errors in Cargo-Call-Stack

    When I was trying Cargo-Call-Stack in my project, it failed with the following error message:

    ...
    thread 'main' panicked at 'assertion failed: `(left == right)`
      left: `56`,
     right: `52`: BUG: LLVM reported that `_ZN50_$LT$$RF$mut$u20$W$u20$as$u20$core..fmt..Write$GT$9write_fmt17h0763000cb7e29fb5E` uses 52 bytes of stack but this doesn't match our analysis', .../.cargo/registry/src/github.com-1ecc6299db9ec823/cargo-call-stack-0.1.4/src/main.rs:1001:29
    

    I could narrow down this problem and found that it seems to be related to the panic-handler implementations that several crates offer for embedded development. Comparing the four options listed in the Embedded Rust Book, I got the results that I documented in the following example code:

    #![no_main]
    #![no_std]
    
    use cortex_m_rt::entry;
    extern crate nucleo_f401re;
    
    use panic_semihosting as _; // cargo-call-stack assertion error
    //use panic_itm as _;       // cargo-call-stack assertion error
    //use panic_halt as _;      // OK
    //use panic_abort as _;     // OK
    
    #[entry]
    fn main() -> ! {
        panic!("Test panic behavior.");
    }
    

    I do not know if there is a simple explanation why this has to fail there or if that is a bug, I just wanted to let you know. If you need further information, please let me know. If it is of any help, my current setup is:

    • rustc 1.45.0-nightly (769d12eec 2020-05-12) OR rustc 1.43.1 (8d69840ab 2020-05-04)
    • cargo 1.45.0-nightly (cb06cb269 2020-05-08) OR cargo 1.43.0 (2cbe9048e 2020-05-03)
    • cargo-call-stack 0.1.4
    opened by DrTobe 3
  • Support Raspberry RP2040 target?

    Support Raspberry RP2040 target?

    For example: https://github.com/embassy-rs/cyw43/tree/master/firmware this repo.

    $ cargo call-stack --example rpi-pico-w --target RP2040 > cg.dot
    error: failed to run `rustc` to learn about target-specific information
    
    Caused by:
      process didn't exit successfully: `/home/aaron/.cargo/bin/cargo-call-stack rustc - --crate-name ___ --print=file-names --target RP2040 --crate-type bin --crate-type rlib --crate-type dylib --crate-type cdylib --crate-type staticlib --crate-type proc-macro --print=sysroot --print=cfg` (exit status: 1)
      --- stderr
      error: Error loading target specification: Could not find specification for target "RP2040". Run `rustc --print target-list` for a list of built-in targets
    
    opened by overheat 0
  • Allow analysis of debug builds, not just release builds.

    Allow analysis of debug builds, not just release builds.

    I have a use-case where I need to know how much MCU memory to devote to stack usage for code compiled in debug mode.

    The tool currently always builds in release mode as it adds the --release argument to the rustc invocation.

    Please can this restriction be changed? Maybe by way of a new command line argument that defaults to release mode? More specifically the cargo profile needs to be supported along with custom profiles.

    See: https://doc.rust-lang.org/cargo/reference/profiles.html#custom-profiles

    It seems there is some conditional code already that adds the --release argument, but the value it depends on is hard-coded.

    See: https://github.com/japaric/cargo-call-stack/blob/main/src/main.rs#L120 and https://github.com/japaric/cargo-call-stack/blob/main/src/main.rs#L174-L176

    opened by hydra 0
  • BUG? no symbol at address <xyz>

    BUG? no symbol at address

    Analyzing an example built from a stm32f4 project. It requires linking to a C library, which I mocked out by declaring void symbol_name_here(void) {} for every needed symbol (because the real thing links the other way, using the crate as a staticlib -- and my attempts to make Rust link everything correctly have failed). So that could plausibly have something to do with it.

    cargo +nightly call-stack --example ex --features=... --target=thumbv7em-none-eabihf returns:

    thread 'main' panicked at 'BUG? no symbol at address 146572', /Users/matejcik/.cargo/registry/src/github.com-1ecc6299db9ec823/cargo-call-stack-0.1.11/src/main.rs:970:48
    stack backtrace:
       0: rust_begin_unwind
                 at /rustc/263edd43c5255084292329423c61a9d69715ebfa/library/std/src/panicking.rs:584:5
       1: core::panicking::panic_fmt
                 at /rustc/263edd43c5255084292329423c61a9d69715ebfa/library/core/src/panicking.rs:142:14
       2: cargo_call_stack::run::{{closure}}
       3: cargo_call_stack::run
       4: cargo_call_stack::main
    

    binary is here: https://easyupload.io/dj57kq output of objdump -D is here: https://pastebin.mozilla.org/E4DoW44B

    it appears that there is indeed no symbol at address 146572, or 0x23c8c:

    00023c54 <_ZN4core3str19slice_error_fail_rt17h8803d315cd910966E>:
    (...)
       23c86:	2401      	movgt	r4, #1
       23c88:	f104 0cfd 	add.w	ip, r4, #253	; 0xfd
       23c8c:	f910 400c 	ldrsb.w	r4, [r0, ip]
       23c90:	f114 0f41 	cmn.w	r4, #65	; 0x41
    (...)
    

    i can do more debugging if you give me some pointers, at this point I have zero idea where to start.

    opened by matejcik 1
  • intrinsic to direct call assumption could be less pessimistic

    intrinsic to direct call assumption could be less pessimistic

    for this code:

    #![no_main]
    #![no_std]
    
    use core::{cmp::Ordering, panic::PanicInfo};
    
    #[no_mangle]
    fn _start() -> (usize, usize) {
        (yes as usize, no as usize)
    }
    
    fn no(a: &str, b: &str) -> bool {
        if a.len() == 4 && b.len() == 4 {
            a.cmp(b) == Ordering::Equal
        } else {
            false
        }
    }
    
    fn yes(a: &str, b: &str) -> bool {
        a.cmp(b) == Ordering::Equal
    }
    
    #[panic_handler]
    fn panic(_: &PanicInfo) -> ! {
        loop {}
    }
    

    call-stack v0.1.11 produces the following call graph

    cg

    if you look at the machine code, the function no does not call the memcmp function

    000200f6 <app::no>:
       200f6:       4684            mov     ip, r0
       200f8:       2000            movs    r0, #0
       200fa:       2904            cmp     r1, #4
       200fc:       bf01            itttt   eq
       200fe:       2b04            cmpeq   r3, #4
       20100:       6810            ldreq   r0, [r2, #0]
       20102:       f8dc 1000       ldreq.w r1, [ip]
       20106:       1a08            subeq   r0, r1, r0
       20108:       bf04            itt     eq
       2010a:       fab0 f080       clzeq   r0, r0
       2010e:       0940            lsreq   r0, r0, #5
       20110:       4770            bx      lr
    

    the LLVM IR does contain a call @memcmp and that's why call-stack adds that edge

    ; app::no
    define internal noundef zeroext i1 @_ZN3app2no17hfff1cfbfed4433e3E ; etc.
      ; ..
      %_19.i.i.i = tail call i32 @memcmp ; etc.
      ; ..
    

    in the particular case of Cortex-M where call-stack analyzes the machine code and sees no 'branch' instruction, call-stack should not add the edge

    cc #63

    enhancement 
    opened by japaric 0
  • thread 'main' panicked at 'BUG: callee `XYZ` is unknown' / (minimally) handle dynamically linked binaries

    thread 'main' panicked at 'BUG: callee `XYZ` is unknown' / (minimally) handle dynamically linked binaries

    dynamically linked binaries contain 'undefined' symbols

    $ file hello
    hello: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2
    
    $ nm -CSn hello
                     U abort@GLIBC_2.2.5
                     U bcmp@GLIBC_2.2.5
                     U calloc@GLIBC_2.2.5
    (..)
    

    these symbols are provide at runtime by a dynamic linker (e.g. /lib/ld-linux.so.2). Therefore cargo-call-stack, a static analysis tool, cannot provide a complete call graph for these kind of binaries.

    Currently cargo-call-stack panics when it sees these kind of binaries. We should more gracefully handle these binaries by adding a dummy node that represents the dynamic libraries provided by the environment (dynamic linker) to the call graph and having all calls into 'undefined' symbols be connected to that dummy node.

    It's not possible to provide a max stack usage number for dynamically linked binaries so that should also be reported to the console.

    Workaround

    If you are hitting the "thread 'main' panicked at 'BUG: callee XYZ is unknown'" error message try using a compilation target that produces a statically linked binary. If you were using x86_64-unknown-linux-gnu then use x86_64-unknown-linux-musl.

    enhancement 
    opened by japaric 0
Owner
Jorge Aparicio
@ferrous-systems engineer. @rust-embedded WG core member. He|they
Jorge Aparicio
Binary Analysis Framework in Rust

Welcome to Falcon Falcon is a formal binary analysis framework in Rust. Expression-based IL with strong influences from RREIL and Binary Ninja's LLIL.

Falcon Binary Analysis Framework 489 Dec 18, 2022
Linux anti-debugging and anti-analysis rust library

DebugOff Library Linux anti-analysis Rust library The goal of this library is to make both static and dynamic (debugging) analysis more difficult. The

null 65 Jan 7, 2023
Finds imports that could be exploited, still requires manual analysis.

drv-vuln-scanner Vulnerable driver scanning tool for win64, put drivers to scan in drv/. Finds imports that could be exploited, still requires manual

selene 24 Dec 10, 2022
Rust-verification-tools - RVT is a collection of tools/libraries to support both static and dynamic verification of Rust programs.

Rust verification tools This is a collection of tools/libraries to support both static and dynamic verification of Rust programs. We see static verifi

null 253 Dec 31, 2022
irulescan is a static security analyzer for iRules

irulescan is a tool to scan iRules for unexpected/unsafe expressions that may have undesirable effects like double substitution.

Simon Kowallik 2 Dec 18, 2022
Adds zero-cost stack overflow protection to your embedded programs

flip-link adds zero-cost stack overflow protection to your embedded programs The problem Bare metal Rust programs may not be memory safe in presence o

Knurling 151 Dec 29, 2022
A rust program to bruteforce ZIP, PDF and some popular hashes.

Veldora A program to bruteforce zips, pdfs and some popular hashes. This is basically a rust version of bruttle, but a lot faster. Installation: git c

Aquib 30 Dec 28, 2022
A tiny program that locates and extracts public save files from Windows to your local directory!

Save Game Extractor | Download Save Game Extractor is a tool that automatically locates and copies save files for Windows games in public directories.

popcar2 6 Dec 23, 2021
A Rust program to control bias lighting on Linux and Windows.

displaylight_rs This Rust workspace is a rewrite of my DisplayLight project. It colors leds mounted behind the monitor with the colors shown on the di

Ivor Wanders 2 Sep 25, 2022
A new shellcode injection technique. Given as C++ header, standalone Rust program or library.

FunctionStomping Description This is a brand-new technique for shellcode injection to evade AVs and EDRs. This technique is inspired by Module Stompin

Ido Veltzman 608 Jan 4, 2023
Whole program static stack analysis

cargo-call-stack Static, whole program stack analysis Other examples: Embedded CoAP / IPv4 server (source) "Hello, world!" HEADS UP: This tool relies

Jorge Aparicio 457 Dec 22, 2022
Retina is a network analysis framework that supports 100+ Gbps traffic analysis on a single server with no specialized hardware.

Retina Retina is a network analysis framework that enables operators and researchers to ask complex questions about high-speed (>100gbE) network links

Stanford Security Research 73 Jun 21, 2023
Xori is an automation-ready disassembly and static analysis library for PE32, 32+ and shellcode

Xori - Custom disassembly framework Xori is an automation-ready disassembly and static analysis library that consumes shellcode or PE binaries and pro

ENDGAME 712 Nov 28, 2022
Xori is an automation-ready disassembly and static analysis library for PE32, 32+ and shellcode

Xori - Custom disassembly framework Xori is an automation-ready disassembly and static analysis library that consumes shellcode or PE binaries and pro

ENDGAME 712 Nov 28, 2022
Shellcheck - a static analysis tool for shell scripts

ShellCheck - A shell script static analysis tool ShellCheck is a GPLv3 tool that gives warnings and suggestions for bash/sh shell scripts: The goals o

Vidar Holen 31.1k Jan 9, 2023
⚙️ A curated list of static analysis (SAST) tools for all programming languages, config files, build tools, and more.

This repository lists static analysis tools for all programming languages, build tools, config files and more. The official website, analysis-tools.de

Analysis Tools 10.7k Jan 2, 2023
Rust-based static analysis for TypeScript projects

Fast TypeScript Analyzer FTA (Fast TypeScript Analyzer) is a super-fast TypeScript static analysis tool written in Rust. It captures static informatio

Sam Brown 4 May 23, 2023
A pure, low-level tensor program representation enabling tensor program optimization via program rewriting

Glenside is a pure, low-level tensor program representation which enables tensor program optimization via program rewriting, using rewriting frameworks such as the egg equality saturation library.

Gus Smith 45 Dec 28, 2022
Docker images for compiling static Rust binaries using musl-libc and musl-gcc, with static versions of useful C libraries. Supports openssl and diesel crates.

rust-musl-builder: Docker container for easily building static Rust binaries Source on GitHub Changelog UPDATED: Major updates in this release which m

Eric Kidd 1.3k Jan 1, 2023
Hot reload static web server for deploying mutiple static web site with version control.

SPA-SERVER It is to provide a static web http server with cache and hot reload. 中文 README Feature Built with Hyper and Warp, fast and small! SSL with

null 7 Dec 18, 2022