调试组件

开发软件意味着处理程序崩溃并发现 bug 的来源。Fuchsia 提供了一套工具来帮助识别和诊断组件中的问题,从分析崩溃日志到核心代码中的全面逐步调试,不一而足。

分析崩溃

Fuchsia 会在启动时启动一个名为 crashanalyzer 的程序,该程序会报告程序崩溃并将崩溃线程的回溯输出到系统日志。虽然您可以通过在运行时查看日志来直接探索这些内容,但回溯内容是使用堆栈内存地址引用进行编码的,而不是指向程序源文件中的相应行。

[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

这是因为,默认情况下,调试符号会在构建时从核心二进制文件中删除。为了正确分析崩溃日志,您需要将这些符号重新应用于回溯,以便根据源代码行号查看调用堆栈。调用 ffx log 命令时,开发者工具会通过一个名为 symbolizer 的附加二进制文件处理原始日志,该二进制文件会将本地 build 配置中的符号重新应用到日志中的所有回溯。

ffx log

您看到的输出包含重新应用于回溯的符号:

[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

通过经过适当符号化的回溯,您可以直接发现源代码中发生崩溃的位置。

分步调试

仅知道程序崩溃的位置可能不足以全面诊断问题。有时,您需要逐步编写代码,甚至检查内存中变量的状态。为了支持这一点,Fuchsia 针对核心代码提供一个名为 zxdb 的调试程序。

zxdb 工具是一个连接到目标设备上正在运行的 debug_agent 组件的客户端。您可以使用 zxdb 命令将 debug_agent 配置为附加到特定进程并设置断点。调试会话附加到正在运行的进程后,zxdb 可让您单步调试代码并检查堆栈帧。

显示 Fuchsia 调试程序 (zxdb) 如何与 Fuchsia 设备上运行的 debug_agent 服务交互以对进程执行交互式调试的示意图。

设置调试会话需要完成以下简要步骤:

  1. 在目标设备上运行 debug_agent 组件。
  2. 运行 zxdb 客户端并连接到目标设备。
  3. 设置 zxdb 的位置以查找调试符号。

启动调试会话的最简单方法是使用 ffx debug connect 命令,该命令可在本地 Fuchsia build 环境中执行所有这些操作。不过,如果您需要单独配置这些步骤,也可以手动执行这些步骤。

调试会话处于活动状态后,系统会将您转到 [zxdb] 提示,以发出调试程序命令。您可以使用 zxdbdebug_agent 配置为使用名称过滤器附加到进程,并设置待定断点(即使当前没有匹配的进程也是如此)。

以下示例在 main 代码中设置一个待处理断点,使其在执行开始时停止,并等待名为“hello-world”的进程启动:

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

将调试程序连接到进程后,您可以使用 zxdb 命令来控制和检查该进程的状态。下面简要列出了一些常用命令:

step 跳过线程中的下一行代码
next 进入线程中的下一行代码
continue 继续执行,直至下一个断点、异常或退出
frame 列出或从当前堆栈帧中进行选择
print 评估表达式并输出结果

练习:使用 Fuchsia 调试程序

在本练习中,您将使用 Fuchsia 调试程序 (zxdb) 检查 echo 组件的运行实例,并了解崩溃的原因。

启动模拟器

如果您尚未运行实例,请启动提供网络支持的 FEMU:

ffx emu start workstation_eng.x64 --headless

启动调试会话

模拟器启动后,使用 ffx debug connect 命令启动 zxdb 调试会话:

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

成功连接后,zxdb 提示符已准备好接受命令。

附加到组件

在启动组件之前,请将 zxdb 配置为附加到 echo 的实例。这样一来,调试程序就可以在进程开始后立即连接:

[zxdb] attach echo

greeting() 函数中设置断点:

[zxdb] break greeting

调试程序已准备就绪,启动一个新的 echo 组件实例:

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

探索调试会话

到达 greeting() 中的断点后,执行会停止,并且调试程序会等待新命令。使用 list 命令显示当前暂停执行的位置:

[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:

使用 next 命令进入 greeting() 函数:

[zxdb] next

print 命令将输出当前堆栈帧中任何变量的状态。输出 names 的当前值:

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

使用 next 多次逐步执行 greeting() 函数:

[zxdb] next

如需让程序继续执行,请使用 continue 命令:

[zxdb] continue

退出调试会话以返回到终端:

[zxdb] exit

引入一些崩溃代码

接下来,您需要向 main() 添加一些代码,导致组件崩溃(或 panic)。通过在收集参数后立即添加 strlen(nullptr) 引用来模拟此行为:

echo/main.cc

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

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

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

  return 0;
}

构建更新后的软件包并将其发布到 fuchsiasamples.com 代码库:

bazel run //fuchsia-codelab/echo:pkg.publish -- \
    --repo_name fuchsiasamples.com

使用 zxdb 启动新的调试会话:

ffx debug connect

调试崩溃的堆栈帧

将调试程序配置为连接到 echo 组件:

[zxdb] attach echo

启动组件的新实例:

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

这一次,调试程序检测到抛出了异常并停止执行:

Attached Process 1 state=Running koid=1164808 name=echo.cm
════════════════════════════════════════════════
 Page fault reading address 0x0 (second chance)
════════════════════════════════════════════════
 Process 1 (koid=1164808) thread 1 (koid=1164810)
 Faulting instruction: 0x43e0fd349210

🛑 strlen(const char*) • strlen.c:21
[zxdb]

使用 frame 命令检查崩溃点的堆栈轨迹:

[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

请注意,堆栈轨迹中的第 1 行表示 main.cc 中发生崩溃的点,与 nullptr 引用相对应。

当前堆栈帧(帧 0)位于系统库的深层,但您可以在命令前添加堆栈轨迹中的帧编号,以检查任何堆栈帧。

通过传递帧号来输出崩溃时的参数值,如下所示:

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

退出调试会话以返回到终端:

[zxdb] exit

销毁实例

使用以下命令清理 echo 实例:

ffx component destroy /core/ffx-laboratory:echo