Debugging components

Developing software means dealing with program crashes and uncovering the source of bugs. Fuchsia has a suite of tools to help identify and diagnose issues in your components from analyzing crash logs to full step-through debugging in core code.

Analyzing crashes

Fuchsia starts a program at boot called crashanalyzer that reports program crashes and prints a backtrace of the crashing thread to the system log. While you can explore these directly by reviewing the logs at runtime, the backtrace content is encoded using the stack memory address references rather than pointing to the corresponding lines in the program source files.

[klog][I] devmgr: crash_analyzer_listener: analyzing exception type 0x108
[klog][I] <== fatal exception: process crasher[42410] thread initial-thread[42424]
[klog][I] <== fatal page fault, PC at 0x1e1888dbbbd7
[klog][I]  CS:                   0 RIP:     0x1e1888dbbbd7 EFL:            0x10246 CR2:                  0
[klog][I]  RAX:                  0 RBX:                0x1 RCX:     0x721ad98697c6 RDX:     0x77accb36f264
[klog][I]  RSI:                  0 RDI:                  0 RBP:     0x2781c4816f90 RSP:     0x2781c4816f80
[klog][I]   R8:                  0  R9:                  0 R10:                  0 R11:              0x246
[klog][I]  R12:     0x773bf11dcda0 R13:     0x773bf11dcdd0 R14:               0x16 R15:         0x78050d69
[klog][I]  errc:               0x6
[klog][I] bottom of user stack:
[klog][I] 0x00002781c4816f80: f11dcda0 0000773b 9ccd2b38 000039b2 |....;w..8+...9..|
[klog][I] 0x00002781c4816f90: c4816fd0 00002781 88dbbba7 00001e18 |.o...'..........|
[klog][I] 0x00002781c4816fa0: 00000008 00000000 9ccd2b38 000039b2 |........8+...9..|
[klog][I] 0x00002781c4816fb0: f11dcf70 0000773b f11dcf70 0000773b |p...;w..p...;w..|
[klog][I] 0x00002781c4816fc0: cb36f570 000077ac f11dcdd0 0000773b |p.6..w......;w..|
[klog][I] 0x00002781c4816fd0: c4816ff0 00002781 cb2b0d0f 000077ac |.o...'....+..w..|
[klog][I] 0x00002781c4816fe0: 00000054 00000000 f11dcf70 0000773b |T.......p...;w..|
[klog][I] 0x00002781c4816ff0: f11dcfe0 0000773b 00000000 00000000 |....;w..........|
[klog][I] arch: x86_64
[klog][I] dso: id=a94c78564173530d51670b6586b1aa471e004f06 base=0x7d3506a49000 name=libfdio.so
[klog][I] dso: id=a61961ba9776a67a00fb322af9ebbdcfd1ce3f62 base=0x77accb297000 name=libc.so
[klog][I] dso: id=760f1e6e47d3dd8b6a19150aa47241279ec75a9c base=0x721ad9863000 name=<vDSO>
[klog][I] dso: id=b18462140c6784a53736105bbf3021852eeda68c base=0x1e1888dbb000 name=app:crasher
[klog][I] bt#01: pc 0x1e1888dbbbd7 sp 0x2781c4816f80 (app:crasher,0xbd7)
[klog][I] bt#02: pc 0x1e1888dbbba7 sp 0x2781c4816fa0 (app:crasher,0xba7)
[klog][I] bt#03: pc 0x77accb2b0d0f sp 0x2781c4816fe0 (libc.so,0x19d0f)
[klog][I] bt#04: pc 0 sp 0x2781c4817000
[klog][I] bt#05: end

This is because the debug symbols are stripped out of the core binaries by default at build time. To properly analyze the crash log, you need to reapply those symbols to the backtrace to see the call stack in terms of source code line numbers. When you call the ffx log command, the developer tools process the raw log through an additional binary called symbolizer that reapplies the symbols from your local build configuration to any backtraces in the log.

ffx log

The output you see includes the symbols reapplied to the backtrace:

[klog][I] devmgr: crash_analyzer_listener: analyzing exception type 0x108
... same output as "raw" backtrace ...
start of symbolized stack:
[klog][I] #01: blind_write at ../../src/developer/forensics/crasher/cpp/crasher.c:21
[klog][I] #02: main at ../../src/developer/forensics/crasher/cpp/crasher.c:137
[klog][I] #03: start_main at ../../zircon/third_party/ulib/musl/src/env/__libc_start_main.c:49
[klog][I] #04: unknown, can't find pc, sp or app/library in line
end of symbolized stack

With a properly symbolized backtrace, you can directly discover the site of a crash in your source code.

Step-through debugging

Just knowing where a program crashed may not be enough information to fully diagnose the issue. Sometimes it's necessary to walk through the code step-by-step and even inspect the state of variables in memory. To support this, Fuchsia has a debugger for core code called zxdb.

The zxdb tool is a client that connects to a running debug_agent component on the target device. You can use the zxdb commands to configure the debug_agent to attach to specific processes and set breakpoints. Once a debug session is attached to a running process, zxdb allows you to step through the code and inspect the stack frames.

Diagram showing how the Fuchsia debugger (zxdb) interacts with the
debug_agent service running on a Fuchsia device to perform interactive
debugging of a process.

Setting up the debug session requires the following high-level steps:

  1. Run the debug_agent component on the target device.
  2. Run the zxdb client and connect to the target device.
  3. Set the location for zxdb to find debug symbols.

The simplest method to start a debug session is to use the ffx debug connect command, which does all of these in the context of your local Fuchsia build. However, these steps can also be performed manually if you need to configure them separately.

Once the debug session is active, you are taken to a [zxdb] prompt to issue debugger commands. You can use zxdb to configure the debug_agent to attach to a process using a name filter and set pending breakpoints even if no matching process is currently running.

The following example sets a pending breakpoint on main to stop at the beginning of execution, and waits for a process called "hello-world" to start:

[zxdb] attach hello-world
Waiting for process matching "hello-world"

[zxdb] break main
Breakpoint 1 (Software) on Global, Enabled, stop=All, @ main
Pending: No matches for location, it will be pending library loads.

Once the debugger is attached to a process, you can use zxdb commands to control and inspect the state of the process. Here is a short collection of common commands:

step Step over the next line of code in the thread
next Step into the next line of code in the thread
continue Continue execution until the next breakpoint, exception, or exit
frame List or select from the current stack frames
print Evaluate an expression and print the result

Exercise: Using the Fuchsia debugger

In this exercise, you'll use the Fuchsia debugger (zxdb) to inspect a running instance of the echo-args component and understand the cause of a crash.

Start the emulator

If you do not already have an instance running, start the emulator:

  1. Start a new emulator instance:

    ffx emu start --headless

    When startup is complete, the emulator prints the following message and returns:

    Logging to "$HOME/.local/share/Fuchsia/ffx/emu/instances/fuchsia-emulator/emulator.log"
    Waiting for Fuchsia to start (up to 60 seconds)........
    Emulator is ready.
    
  2. Start a package server to enable the emulator to load software packages:

    fx serve

Start a debug session

Once the emulator has started up, start a zxdb debugging session with the ffx debug connect command:

ffx debug connect
Connecting (use "disconnect" to cancel)...
Connected successfully.
👉 To get started, try "status" or "help".
[zxdb]

After successfully connecting, the zxdb prompt is ready to accept commands.

Attach to the component

Before launching the component, configure zxdb to attach to an instance of echo-args. This enables the debugger to attach as soon as the process starts:

[zxdb] attach echo-args

Set a breakpoint on the greeting() function:

[zxdb] break greeting

With the debugger ready, start a new echo-args component instance:

ffx component run /core/ffx-laboratory:echo-args fuchsia-pkg://fuchsia.com/echo-args#meta/echo-args.cm

Explore the debug session

Upon reaching the breakpoint in greeting(), execution stops and the debugger waits for a new command. Use the list command to show where execution is currently paused:

Rust

[zxdb] list
  18
  19 // Return a proper greeting for the list
  20 fn greeting(names: &Vec<String>) -> String {
  21     // Join the list of names based on length
▶ 22     match names.len() {
  23         0 => String::from("Nobody"),
  24         1 => names.join(""),
  25         2 => names.join(" and "),
  26         _ => names.join(", "),
  27     }
  28 }
  29

C++

[zxdb] list
  17 
  18 // Return a proper greeting for the list
▶ 19 std::string greeting(std::vector<std::string>& names) {
  20   // Join the list of names based on length
  21   auto number_of_names = names.size();
  22   switch (number_of_names) {
  23     case 0:
  24       return "Nobody!";
  25     case 1:
  26       return join(names, "");
  27     case 2:
  28       return join(names, " and ");
  29     default:

The print command will output the state of any variables in the current stack frame. Print the current value of names:

Rust

[zxdb] print names
vec!["Alice", "Bob", "Spot"]

C++

[zxdb] print names
{"Alice", "Bob", "Spot"}

Step through the greeting() function a few times using the next command:

[zxdb] next

To let the program continue to completion, use the continue command:

[zxdb] continue

Exit the debugging session to return to the terminal:

[zxdb] exit

Introduce some crashing code

Next, you'll add some code to src/main.rs to cause the component to crash (or panic). Simulate this behavior by adding an assert!(false) macro just after the arguments are collected:

Rust

echo-args/src/main.rs:

#[fuchsia::main(logging = true)]
async fn main() -> Result<(), anyhow::Error> {
    // ...

    // Simulate a crash 
    assert!(false, "fake crash");

    // Print a greeting to syslog
    info!("Hello, {}!", greeting(&args));

    Ok(())
}

C++

echo-args/main.cc:

int main(int argc, const char* argv[], char* envp[]) {
  // ...

  // Simulate a crash 
  std::strlen(nullptr);

  // Print a greeting to syslog
  FX_LOGS(INFO) << "Hello, " << echo::greeting(arguments) << "!" << std::endl;

  return 0;
}

Run fx build again to rebuild the component:

fx build

Start a new debug session with zxdb:

ffx debug connect

Debug the crashing stack frame

Configure the debugger to attach to the echo-args component:

[zxdb] attach echo-args

Start a new instance of the component:

ffx component run /core/ffx-laboratory:echo-args fuchsia-pkg://fuchsia.com/echo-args#meta/echo-args.cm

This time, the debugger detects that an exception was thrown. Use the frame command to inspect the stack trace at the point of the crash:

Rust

[zxdb] frame
▶ 0 abort() • abort.c:7
  1 panic_abort::__rust_start_panic::abort() • panic_abort/src/lib.rs:43
  2 panic_abort::__rust_start_panic(…) • panic_abort/src/lib.rs:38
  3 std::panicking::rust_panic(…) • library/std/src/panicking.rs:672
  4 std::panicking::rust_panic_with_hook(…) • library/std/src/panicking.rs:642
  5 std::panicking::begin_panic::$({closure#0}<&str>)() • rust/library/std/src/panicking.rs:544
  6 std::sys_common::backtrace::$(__rust_end_short_backtrace<std::panicking::begin_panic::{closure#0}, !>)(…) • rust/library/std/src/sys_common/backtrace.rs:144
  7 std::panicking::begin_panic<…>(…) • rust/library/std/src/panicking.rs:543
  8 echo_args::main::component_entry_point::$({generator#0})(…) • main.rs:18
  9 core::future::from_generator::$({impl#1})::$(poll<echo_args::main::component_entry_point::{generator#0}>)(…) • rust/library/core/src/future/mod.rs:80
  10 core::future::future::$({impl#1})::$(poll<&mut core::future::from_generator::GenFuture<echo_args::main::component_entry_point::{generator#0}>>)(…) • future/future.rs:119
  11 futures_util::future::future::FutureExt::$(poll_unpin<core::pin::Pin<&mut core::future::from_generator::GenFuture<echo_args::main::component_entry_point::{generator#0}>>>)(…) • future/future/mod.rs:562
  12 fuchsia_async::runtime::fuchsia::executor::local::MainTask::$(poll<core::pin::Pin<&mut core::future::from_generator::GenFuture<echo_args::main::component_entry_point::{generator#0}>>>)(…) • fuchsia/src/lib/fuchsia-async/src/runtime/fuchsia/executor/local.rs:444
  13 fuchsia_async::runtime::fuchsia::executor::local::LocalExecutor::$(run_singlethreaded<core::future::from_generator::GenFuture<echo_args::main::component_entry_point::{generator#0}>>)(…) • fuchsia/src/lib/fuchsia-async/src/runtime/fuchsia/executor/local.rs:73
  14 fuchsia::$(main_singlethreaded<fuchsia::init_logging_for_component_with_executor::{closure#0}, core::future::from_generator::GenFuture<echo_args::main::component_entry_point::{generator#0}>, core::result::Result<(), anyhow::Error>>)(…) • fuchsia/src/lib/fuchsia/src/lib.rs:152
  15 echo_args::main() • main.rs:7
  16 core::ops::function::FnOnce::call_once<…>(…) • /b/s/w/ir/x/w/rust/library/core/src/ops/function.rs:227
  17 std::sys_common::backtrace::__rust_begin_short_backtrace<…>(…) • rust/library/std/src/sys_common/backtrace.rs:125
  18 std::rt::lang_start::$({closure#0}<core::result::Result<(), anyhow::Error>>)() • rust/library/std/src/rt.rs:63
  19 core::ops::function::impls::$({impl#2})::call_once<…>(…) • /b/s/w/ir/x/w/rust/library/core/src/ops/function.rs:259 (inline)
  20 std::panicking::try::do_call<…>(…) • library/std/src/panicking.rs:403 (inline)
  21 std::panicking::try<…>(…) • library/std/src/panicking.rs:367 (inline)
  22 std::panic::catch_unwind<…>(…) • library/std/src/panic.rs:129 (inline)
  23 std::rt::lang_start_internal::$({closure#2})() • library/std/src/rt.rs:45 (inline)
  24 std::panicking::try::$(do_call<std::rt::lang_start_internal::{closure#2}, isize>)(…) • library/std/src/panicking.rs:403 (inline)
  25 std::panicking::$(try<isize, std::rt::lang_start_internal::{closure#2}>)(…) • library/std/src/panicking.rs:367 (inline)
  26 std::panic::$(catch_unwind<std::rt::lang_start_internal::{closure#2}, isize>)(…) • library/std/src/panic.rs:129 (inline)
  27 std::rt::lang_start_internal(…) • library/std/src/rt.rs:45
  28 std::rt::lang_start<…>(…) • rust/library/std/src/rt.rs:62
  29 $elf(main) + 0x1f
  30 «libc startup» (-r expands)
  31 «libc startup» (-r expands)
  32 $elf(_start) + 0x11

Notice line 8 in the stack trace indicates the point in src/main.rs where the crash happened, corresponding to the assert!() macro line of code.

C++

[zxdb] frame
▶ 0 strlen(…) • strlen.c:21
  1 main(…) • main.cc:27
  2 «libc startup» (-r expands)
  3 «libc startup» (-r expands)
  4 $elf(_start) + 0x11

Notice line 1 in the stack trace indicates the point in main.cc where the crash happened, corresponding to the nullptr reference.

The current stack frame (frame 0) is deep within the system library, but you can inspect any stack frame by prefixing the command with the frame number from the stack trace.

Print the value of the arguments at the point of the crash by passing the frame number as follows:

Rust

[zxdb] frame 8 print args
vec!["Alice", "Bob", "Spot"]

C++

[zxdb] frame 1 print arguments
{"Alice", "Bob", "Spot"}

Exit the debugging session to return to the terminal:

[zxdb] exit

Destroy the instance

Clean up the echo-args instance using the following command:

ffx component destroy /core/ffx-laboratory:echo-args