Zephyr Instrumentation 使用说明
概述 链接到标题
Zephyr Instrumentation 子系统利用 GCC -finstrument-functions 自动在每个函数入口/出口插桩,无需手动埋点即可实现函数调用图追踪与性能剖析。提供两种模式:
- Callgraph 模式用于时序追踪
- Statistical 模式用于热点分析
可以为 Instrumentation 指定启动和停止函数来限定检测窗口,默认以 main 为边界;配合 retained memory 可实现重启动态指定触发函数。提供 zaru.py 工具可以通过 UART 拿到 trace/profile 数据,trace 数据能够直接放到 Perfetto 可视化。
与 Tracing 子系统不一样,Tracing 关注内核对象操作,通过在内核对象内显式的插桩代码来完成数据收集。Instrumentation 则是通过编译器对所有的函数进行编译级别的插桩,消耗会更高,但数据会更丰富。
| 维度 | Instrumentation | Tracing |
|---|---|---|
| 层级 | 底层(函数级) | RTOS 感知(事件级) |
| 机制 | 编译器自动插桩 (-finstrument-functions) | 手动调用结构化 API |
| 适用场景 | 代码流程分析、性能瓶颈定位 | 线程切换、信号量等 RTOS 事件 |
| 开销 | 较高(所有函数) | 较低(仅标记点) |
使用配置 链接到标题
| 配置项 | 默认值 | 说明 | 典型取值示例 |
|---|---|---|---|
CONFIG_INSTRUMENTATION |
n |
启用 Instrumentation 子系统 | y |
CONFIG_INSTRUMENTATION_MODE_CALLGRAPH |
n |
启用调用图追踪模式 | y |
CONFIG_INSTRUMENTATION_MODE_STATISTICAL |
n |
启用统计剖析模式 | y |
CONFIG_INSTRUMENTATION_MODE_CALLGRAPH_TRACE_BUFFER_SIZE |
4096 |
调用图环形缓冲区大小(条目数) | 8192 / 16384 |
CONFIG_INSTRUMENTATION_MODE_STATISTICAL_MAX_NUM_FUNC |
256 |
统计模式最大追踪函数数 | 512 / 1024 |
CONFIG_INSTRUMENTATION_TRIGGER_FUNCTION |
main |
触发函数(开始记录) | get_sem_and_exec_function |
CONFIG_INSTRUMENTATION_STOPPER_FUNCTION |
main |
停止函数(结束记录) | main |
CONFIG_INSTRUMENTATION_DYNAMIC_TRIGGER |
n |
运行时动态配置触发/停止函数 | y |
CONFIG_INSTRUMENTATION_EXCLUDE_FUNCTION_LIST |
"" |
排除函数列表(逗号分隔) | k_busy_wait,memcpy |
CONFIG_INSTRUMENTATION_EXCLUDE_FILE_LIST |
"" |
排除文件列表(逗号分隔) | log_minimal.c |
调用图追踪模式 链接到标题
- 配置项:
CONFIG_INSTRUMENTATION_MODE_CALLGRAPH=y - 数据:函数进入/退出事件 + 时间戳 + 上下文信息
- 缓冲区:环形模式(默认覆盖)或固定模式(满则停)
CONFIG_INSTRUMENTATION_MODE_CALLGRAPH_TRACE_BUFFER_SIZE=8192 - 功能:
- 可重建完整的函数调用图
- 观察线程上下文切换
- 分析执行流程和时间关系
统计剖析模式 链接到标题
- 配置项:
CONFIG_INSTRUMENTATION_MODE_STATISTICAL=y - 数据:触发点至停止点间各函数累计执行时间
- 限制:最大追踪函数个数由
CONFIG_INSTRUMENTATION_MODE_STATISTICAL_MAX_NUM_FUNC控制 - 功能:按耗时排序给出函数热点列表
动态 Trigger 设置 链接到标题
- 启用
CONFIG_INSTRUMENTATION_DYNAMIC_TRIGGER=y - 依赖 Retained Memory 设备树节点
- 跨重启保留触发/停止地址,避免重新烧录
精细化排除 链接到标题
CONFIG_INSTRUMENTATION_EXCLUDE_FUNCTION_LIST:按函数名排除
CONFIG_INSTRUMENTATION_EXCLUDE_FILE_LIST:按源文件排除
主机交互工具 链接到标题
scripts/instrumentation/zaru.py 是宿主端控制工具,通过 UART 与目标板通信,依赖 ELF 文件做符号解析,支持 Instrumentation trace/profile 数据采集与可视化。
安装依赖 链接到标题
zaru.py 依赖 python3-bt2 不在 pip 包内,需要使用下面命令安装:
sudo apt-get install python3-bt2
安装完后需要将系统包设置给 Zephyr 编译虚拟环境,并重新激活:
deactivate
python3 -m venv --system-site-packages .venv
source .venv/bin/activate
执行命令时需要 ZEPHYR_BASE 环境变量,执行下面命令设置环境变量:
source zephyr/zephyr-env.sh
使用说明 链接到标题
全局参数 链接到标题
全局参数需放置在子命令之前,用于配置串口连接与编译输出目录。
| 参数 | 缩写 | 默认值 | 说明 |
|---|---|---|---|
--serial |
无 | /dev/ttyACM0 |
指定连接目标的串口设备(支持本地 /dev/ttyUSBx)。 |
--build-dir |
无 | 自动推导 | 指定编译输出目录(需包含 zephyr.elf 和 ctf_metadata)。 |
子命令 链接到标题
子命令及详细参数
reboot(重启目标设备):用于重启下位机目标设备并检测其连通状态。
status(查看系统状态):获取目标设备的 Instrumentation 状态(查看 Trace、Profile 及动态 Trigger 支持情况)。
trace(系统追溯分析):获取、配置或导出系统的函数调用 Trace 序列。
| 参数 | 缩写 | 参数值 | 说明 |
|---|---|---|---|
--trigger |
-t |
FUNC_NAME |
设置触发函数。进入该函数时开启 Trace(设为 0 可禁用)。 |
--stopper |
-s |
FUNC_NAME |
设置停止函数。退出该函数时关闭 Trace(设为 0 可禁用)。 |
--couple |
-c |
FUNC_NAME |
同时将 Trigger 和 Stopper 设为同一个函数(进入该函数开启,退出该函数停止)。 |
--list |
-l |
无 | 打印当前目标设备上设置的 Trigger 和 Stopper 函数名称。 |
--reboot |
-r |
无 | 在拉取 Trace 数据前自动重启目标设备。 |
--export-tef |
-e |
无 | 导出 Trace 为 Trace Event Format (JSON),可直接导入 Perfetto 查看。 |
--output |
-o |
文件名 | 指定导出 JSON 的文件名(默认为 ./trace_event.json)。 |
--demangle |
-d |
无 | 解析/还原 C++ 符号名(调用 c++filt)。 |
--annotation |
-a |
无 | 禁用函数返回(退出)处的名称注释(默认开启注释)。 |
--verbose |
-v |
无 | 开启详细调试输出。 |
profile(性能分析)测量并统计各个函数的执行耗时占比。
| 参数 | 缩写 | 参数值 | 说明 |
|---|---|---|---|
-n |
无 | 整数(默认 100) | 显示耗时最高的前 N 个函数。 |
--reboot |
-r |
无 | 在拉取 Profile 数据前自动重启目标设备。 |
--verbose |
-v |
无 | 开启详细输出。 |
使用示例 链接到标题
- 查看硬件支持状态:
python3 zaru.py --serial /dev/ttyUSB0 status
- 查询当前配置的 Trigger 和 Stopper:
python3 zaru.py --serial /dev/ttyUSB0 trace -l
- 设置触发/停止函数并重启拉取 Trace:
# 进入 main 时开启,退出 process_data 时停止,拉取前重启板子
python3 zaru.py --serial /dev/ttyUSB0 trace -t main -s process_data -r
- 追踪单个函数并导出为 Perfetto JSON 图表:
python3 zaru.py --serial /dev/ttyUSB0 trace -c my_function -r -e -o my_trace.json
- 获取耗时前 10 的函数(Profile):
python3 zaru.py --serial /dev/ttyUSB0 profile -n 10 -r
- 手动重启板子:
python3 zaru.py --serial /dev/ttyUSB0 reboot
ESP32S3 下示例 链接到标题
我使用的 ESP32S3,对 Instrumentation 的示例进行编译
west build -b esp32s3_touch_lcd_2/esp32s3/procpu zephyr/samples/subsys/instrumentation/ -- -DCONFIG_DEBUG_THREAD_INFO=y -DOPENOCD=/home/frank/workspace/tools/openocd-esp32/bin/openocd -DOPENOCD_DEFAULT_PATH=/home/frank/workspace/tools/openocd-esp32/share/openocd/scripts
非动态触发 链接到标题
编译时就确认好触发 INSTRUMENTATION 的函数,配置如下
# 基础启用
CONFIG_INSTRUMENTATION=y
# 双模式同时开启:调用图 + 统计剖析
CONFIG_INSTRUMENTATION_MODE_CALLGRAPH=y
CONFIG_INSTRUMENTATION_MODE_STATISTICAL=y
# 缓冲区调大,避免高频调用丢帧
CONFIG_INSTRUMENTATION_MODE_CALLGRAPH_TRACE_BUFFER_SIZE=112000
# 排除上下文切换函数
CONFIG_INSTRUMENTATION_EXCLUDE_FUNCTION_LIST="z_thread_mark_switched_in,z_thread_mark_switched_out,z_swap,do_swap"
非动态触发下默认在进入 main 函数时开启 Trace 开始收集数据,退出 main 函数时退出 Trace 收集数据,也可以通过 CONFIG_INSTRUMENTATION_TRIGGER_FUNCTION 和 CONFIG_INSTRUMENTATION_STOPPER_FUNCTION 来修改。
性能分析示例:
./zephyr/scripts/instrumentation/zaru.py --build-dir build/ --serial /dev/ttyUSB0 profile -n 10 -r 获取耗时前 10 的函数(Profile)
Using '/dev/ttyUSB0' to connect.
Connected to target.
Rebooting target... done!
Using ELF file: build/zephyr/zephyr.elf
Temporary dir: /tmp/tmpaaei0z39
Using ctf_metadata file: build/ctf_metadata
15.53% 1077396408 k_cpu_idle
15.52% 1077389376 arch_cpu_idle
6.16% 1107302792 thread_A
4.82% 1107302416 k_sem_take
4.81% 1077404436 z_impl_k_sem_take
3.59% 1107302552 k_msleep
3.58% 1107302484 k_sleep
3.57% 1077428856 z_impl_k_sleep
3.56% 1077428344 z_tick_sleep
2.70% 1077428288 arch_switch
Trace 抓取示例:
./zephyr/scripts/instrumentation/zaru.py --build-dir build/ --serial /dev/ttyUSB0 trace -r 获取 trace 信息
Using '/dev/ttyUSB0' to connect.
Connected to target.
Rebooting target... done!
Using ELF file: build/zephyr/zephyr.elf
Temporary dir: /tmp/tmpsme0zx80
Using ctf_metadata file: build/ctf_metadata
Thread Name Thread ID CPU Mode Timestamp Function(s)
------------------------------------------------------------------------------------------------
main 0x3fcc3ea0 13) 0 | 261620458 ns | main() {
main 0x3fcc3ea0 13) 0 | 261648383 ns | k_thread_create() {
main 0x3fcc3ea0 13) 0 | 261670529 ns | z_impl_k_thread_create() {
main 0x3fcc3ea0 13) 0 | 261690908 ns | z_setup_new_thread() {
main 0x3fcc3ea0 13) 0 | 261711383 ns | z_waitq_init() {
main 0x3fcc3ea0 13) 0 | 261731783 ns | sys_dlist_init();
main 0x3fcc3ea0 13) 0 | 261772545 ns | }; /* z_waitq_init */
main 0x3fcc3ea0 13) 0 | 261793275 ns | z_init_thread_base() {
main 0x3fcc3ea0 13) 0 | 261813775 ns | z_init_thread_timeout() {
main 0x3fcc3ea0 13) 0 | 261834437 ns | z_init_timeout() {
main 0x3fcc3ea0 13) 0 | 261854987 ns | sys_dnode_init();
main 0x3fcc3ea0 13) 0 | 261896104 ns | }; /* z_init_timeout */
main 0x3fcc3ea0 13) 0 | 261916700 ns | }; /* z_init_thread_timeout */
main 0x3fcc3ea0 13) 0 | 261937150 ns | }; /* z_init_thread_base */
main 0x3fcc3ea0 13) 0 | 261958000 ns | setup_thread_stack() {
main 0x3fcc3ea0 13) 0 | 261978637 ns | K_KERNEL_STACK_BUFFER();
main 0x3fcc3ea0 13) 0 | 262019904 ns | }; /* setup_thread_stack */
main 0x3fcc3ea0 13) 0 | 262040862 ns | arch_new_thread() {
main 0x3fcc3ea0 13) 0 | 262061583 ns | init_stack();
main 0x3fcc3ea0 13) 0 | 262103370 ns | }; /* arch_new_thread */
main 0x3fcc3ea0 13) 0 | 262124333 ns | k_spin_lock() {
main 0x3fcc3ea0 13) 0 | 262145054 ns | arch_irq_lock();
main 0x3fcc3ea0 13) 0 | 262186654 ns | z_spinlock_validate_pre();
main 0x3fcc3ea0 13) 0 | 262228387 ns | z_spinlock_validate_post();
./zephyr/scripts/instrumentation/zaru.py --build-dir build/ --serial /dev/ttyUSB0 trace -r -e -o test.json 将 trace 信息保存为 Perfetto 格式
Using '/dev/ttyUSB0' to connect.
Connected to target.
Rebooting target... done!
Using ELF file: build/zephyr/zephyr.elf
Temporary dir: /tmp/tmp7qzw55qc
Using ctf_metadata file: build/ctf_metadata
Found thread 'main'.
Found thread 'thread_A'.
Found thread 'input'.
Found thread 'idle'.
Found 864 event(s), 4 thread(s), 0 context switch(es).
76525 byte(s) written to 'test.json'.
在 ui.perfetto.dev 中打开 test.json 可以看到各个线程 trace 分析结果

动态触发 链接到标题
动态触发需要增加下面配置:
# 动态触发(需 retained_mem 驱动 + DT overlay)
CONFIG_INSTRUMENTATION_DYNAMIC_TRIGGER=y
# 排除引起递归的文件
CONFIG_INSTRUMENTATION_EXCLUDE_FILE_LIST="retention.c,retention.h,retained_mem.h,retained_mem_zephyr_ram.c"
由于配置了 CONFIG_INSTRUMENTATION_DYNAMIC_TRIGGER 需要使用 retained_mem,因此需要在 devicetree 中添加 instrumentation_triggers 节点,对于 esp32s3 将其放在 rtc 的 retained_mem 中,地址为 0x0,大小为 0x20 字节,prefix = [be ef] 用于启动时校验 retained 区是否有效
memory@50001f00 {
compatible = "zephyr,memory-region", "mmio-sram";
reg = <0x50001f00 0x100>;
zephyr,memory-region = "RTC_SLOW_RAM_RETAINED_MEM";
status = "okay";
retainedmem0: retainedmem {
compatible = "zephyr,retained-ram";
status = "okay";
#address-cells = <1>;
#size-cells = <1>;
instrumentation_triggers: retention@0 {
compatible = "zephyr,retention";
status = "okay";
reg = <0x0 0x20>;
prefix = [be ef];
};
};
};
配置触发函数为 z_init_static_threads,停止函数为 main:./zephyr/scripts/instrumentation/zaru.py --build-dir build/ --serial /dev/ttyUSB0 trace -t z_init_static_threads -s main
Using '/dev/ttyUSB0' to connect.
Connected to target.
Using ELF file: build/zephyr/zephyr.elf
trigger set at entry of 'z_init_static_threads' function, at address 0x4037c0d0.
New trigger set at entry of 'z_init_static_threads' function, at address 0x4037c0d0.
stopper set at entry of 'main' function, at address 0x42001a74.
New stopper set at exit of 'main' function, at address 0x1107303028.
启动重新抓取 trace 信息,可以看到是从 z_init_static_threads 开始抓取
Using '/dev/ttyUSB0' to connect.
Connected to target.
Rebooting target... done!
Using ELF file: build/zephyr/zephyr.elf
Temporary dir: /tmp/tmp2eyhg79i
Using ctf_metadata file: build/ctf_metadata
Thread Name Thread ID CPU Mode Timestamp Function(s)
------------------------------------------------------------------------------------------------
main 0x3fcc3ea0 13) 0 | 261510200 ns | z_init_static_threads() {
main 0x3fcc3ea0 13) 0 | 261532629 ns | z_setup_new_thread() {
main 0x3fcc3ea0 13) 0 | 261553029 ns | z_waitq_init() {
main 0x3fcc3ea0 13) 0 | 261573375 ns | sys_dlist_init();
main 0x3fcc3ea0 13) 0 | 261613916 ns | }; /* z_waitq_init */
main 0x3fcc3ea0 13) 0 | 261634570 ns | z_init_thread_base() {
main 0x3fcc3ea0 13) 0 | 261655012 ns | z_init_thread_timeout() {
main 0x3fcc3ea0 13) 0 | 261675579 ns | z_init_timeout() {
instrumentation 子系统的一些坑 链接到标题
以 Instrumentation 子系统目前的状态并不能开箱即用,并且还有设计缺陷,因此不太建议现阶段用于生产问题分析
Instrumetion本身的坑 链接到标题
由于设计上和实现上的问题有如下坑
- 递归调用:动态触发器与 retention 代码同时编译时会导致无限递归,需加入排除列表。
- 过早访问:系统初始化过早调用插桩时,retention 设备尚未就绪,需延迟加载机制
- 数据丢失:多线程下使用禁用/启用插桩来管理临界区会导致函数日志被绕过,造成追踪数据丢失
- 工具问题:无法正常设置stoper
已提 Issue 进行讨论 https://github.com/zephyrproject-rtos/zephyr/issues/116834
1,2 两点可以本地修改解决,3是设计缺陷不好修改,4已经提PR
https://github.com/zephyrproject-rtos/zephyr/pull/116836
ESP32S3 下的填坑 链接到标题
使用 Zephyr 下的子系统另一个大的问题就是不同的平台、不同的配置、不同的结果。instrumentation 示例代码是在 mps2_an385 和 b_u585i_iot02a 上跑,搬到 esp32s3 上需要填一些坑才能使用。
- 开机一直重启
- 问题原因:instrumentation 会对所有 Zephyr 的函数进行插桩,插桩代码被放到 flash 内,在 flash mmu 建立前的代码会调用 instrumentation 的代码访问 flash mmu 的总线会导致 exception 重启。
- 解决方法:通过修改 ld 文件将 instrumentation 及其关联代码放到 iram 内
- 开启
CONFIG_INSTRUMENTATION_DYNAMIC_TRIGGER后一直重启- 问题原因:retained text/rodata 都放到 flash 中,在 flash mmu 建立前的代码会调用 retained device 的代码访问 flash mmu 的总线会导致 exception 重启。
- 解决方法:延迟 retained 访问,在 flash mmu 建立并且 retention 设备驱动初始化完成后再进行访问。
具体的填坑和调试过程将在下一篇实现分析的博客中说明。