Skip to content

feat(starry): perf call-graph capture (PERF_SAMPLE_CALLCHAIN) - #1

Open
JosephJoshua wants to merge 6 commits into
feat/perf-multicore-biglittlefrom
feat/perf-callchain
Open

feat(starry): perf call-graph capture (PERF_SAMPLE_CALLCHAIN)#1
JosephJoshua wants to merge 6 commits into
feat/perf-multicore-biglittlefrom
feat/perf-callchain

Conversation

@JosephJoshua

Copy link
Copy Markdown

概述

在多核硬件 PMU perf(rcore-os#1577)之上,为 PMU 采样补齐调用图捕获PERF_SAMPLE_CALLCHAIN),使 perf record -g 能采集每个样本的栈回溯(内核 + 用户,基于帧指针),从而在主机侧用 perf report / stackcollapse 生成火焰图——这是当前 perf 功能集中最大的缺口。此前采样只有一个叶子 IP(hw.rs 在 open 时显式拒绝 PERF_SAMPLE_CALLCHAIN)。

主要内容

  • 中断帧管脚(keystone):PMU 溢出处理程序运行在 dispatch_irq 内,能实时读中断 PC/EL(ELR_EL1/SPSR_EL1),但被中断的 x29(帧指针)已被处理程序自身的栈帧覆盖。在两个 IRQ 入口(EL1 内核中断 / EL0 用户中断)以 per-CPU 指针发布被中断的 TrapFramedispatch_irq 前置、返回后即清;新增 interrupted_fp()/interrupted_sp()axcpu)。
  • 无分配 FP 回溯引擎axbacktrace::walk_fp):读取器注入、固定缓冲、镜像 unwind_core 的护栏(单调递增 fp、8 MiB 跳变、字对齐、深度/总步数上限),可在硬中断上下文使用。
  • 记录格式sampling.rs):新增 PERF_SAMPLE_CALLCHAIN / PERF_CONTEXT_KERNEL(-128) / PERF_CONTEXT_USER(-512),加入 SUPPORTED_SAMPLE_TYPE(两处 hw open 校验自动放行),按 Linux 字段序在 build_sample 末尾发出 u64 nr + 各条目,固定 SAMPLE_RECORD_MAX_LEN 覆盖最坏记录。
  • 内核 + 用户回溯:内核帧经 always-mapped 内核空间回溯;用户帧经新的 IRQ-safe 无缺页读取器(手工遍历 TTBR0/TTBR1,只经直映射访问页表帧与解析出的叶子物理页,从不解引用输入 VA)。对每个将要解引用的物理地址都用 phys_ram_ranges() 做 RAM 边界检查,因此一个合法但指向设备 MMIO(用户 mmap 的 RGA/NPU/JPU 寄存器窗口)或另一核并发改表得到的撕裂表项,都不会在硬中断里触发 MMIO 副作用读或缺页 panic。

验证

  • QEMU(smp4):新增两个 grouped-C 自测。perf-hw-callchain-kernel 校验内核区记录格式(含 PERF_CONTEXT_KERNEL 标记 + 叶子 IP,CI 安全:默认内核未开帧指针,故内核区只有叶子——与 Linux 未开 CONFIG_FRAME_POINTER 一致)。perf-hw-callchain-user 以已知嵌套链 outer→mid→inner→busy-fno-omit-frame-pointer)断言 PERF_CONTEXT_USER 后 ≥4 个用户 IP,多次运行稳定回溯出 8 层,证明无缺页读取器确实走通了用户栈。
  • 对抗式代码审查:两轮多视角审查(IRQ 安全 / 无分配-无缺页 / 记录格式-vs-Linux ABI / 缓冲越界 / FP 播种 / 页表遍历正确性 / 用户走查 DoS)。确认的问题均已修复:内核直解引用改为无缺页读取器(避免坏帧指针 panic);对叶子与表帧加 RAM 边界检查(避免 MMIO 副作用/缺页);walk_fp 增加总步数上限(避免构造的全跳过链在硬中断里空转成 DoS)。

说明

  • perf 采样代码均以 #[cfg(target_arch = "aarch64")] 门控;axbacktrace::walk_fp/范围 getter 为纯 core,不影响其它架构。
  • 内核深层回溯需以 -Cforce-frame-pointersBACKTRACE=y)构建内核(同 Linux CONFIG_FRAME_POINTER);用户回溯只要被采样二进制保留帧指针即可。
  • 基于 feat(starry): multicore + big.LITTLE hardware-PMU perf rcore-os/tgoskits#1577(多核 + big.LITTLE 硬件 PMU perf);待其合入后 rebase 到 dev。
  • 已过 cargo xtask clippycargo fmt --all -- --check

The PMU overflow handler runs inside dispatch_irq and can read the
interrupted PC/EL live (ELR_EL1/SPSR_EL1), but the interrupted frame
pointer (x29) survives only in the saved TrapFrame — the handler's own
frames have already clobbered the GPR. Call-graph sampling
(PERF_SAMPLE_CALLCHAIN) needs that x29.

Publish a per-CPU *const TrapFrame at the two IRQ-entry sites (EL1
kernel interrupt in trap.rs, EL0 user interrupt in UserContext::run)
immediately before dispatch_irq and clear it immediately after, both
inside the IRQ-masked window so no nested IRQ observes a stale frame.
Add interrupted_fp()/interrupted_sp() accessors alongside the existing
interrupted_pc()/interrupted_is_user(). The publish/clear is a single
per-CPU store each (TPIDR-relative, cannot fault) and does not reorder
the register save/restore path.

Pulls ax-percpu into the aarch64 dependency set (already present for
x86_64) for the per-CPU cell.
…e getters

Add walk_fp(pc, fp, ip_range, fp_range, read, out) -> usize: an
allocation-free frame-pointer walk into a caller-supplied buffer that
reads memory through an injected closure instead of dereferencing
directly, so a hard-IRQ caller (PMU-sampling call-graph capture) can
supply a fault-safe reader. Mirrors unwind_core's guards (monotonic fp,
8 MiB jump cap, word alignment, depth cap via out.len()) but never
allocates and is not behind the alloc feature, so it is available in the
default (no-alloc) kernel build.

Add ip_range()/fp_range() getters over the ranges init() records, each a
lock-free spin::Once load safe to call from IRQ context, so the sampling
unwinder can reuse the kernel text / address-space bounds.

Serialize the depth-sensitive tests: set_max_depth mutates a process
global, so init_for_tests now returns a guard that holds a shared lock
for the test body, removing a latent flake under cargo test's parallel
harness (surfaced by the new walk_fp tests raising thread contention).
Emit PERF_SAMPLE_CALLCHAIN so perf record -g captures per-sample kernel
stack backtraces (frame-pointer), rendering as flamegraphs off-target.

- perf/unwind.rs: kernel-side integration over axbacktrace::walk_fp —
  kernel_ranges() reuses the boot-time backtrace ranges, a direct-deref
  reader for always-mapped kernel frames, and kernel_callchain() that
  walks the interrupted x29 chain into a fixed buffer.
- sampling.rs: define PERF_SAMPLE_CALLCHAIN / PERF_CONTEXT_KERNEL /
  PERF_CONTEXT_USER, add CALLCHAIN to SUPPORTED_SAMPLE_TYPE (both hw.rs
  open sites accept it via the mask), grow SAMPLE_RECORD_MAX_LEN to bound
  the callchain block, emit 'u64 nr + entries' in build_sample in Linux
  field order, and build the chain in the overflow handler from the
  interrupted frame pointer (leaf-only fallback when none is published,
  so a sample is never dropped).

Kernel frames only in this pass; the user region is a leaf IP with a
PERF_CONTEXT_USER marker (the no-fault user FP unwind is M4b). Sampling
stays aarch64-only and allocation-free in IRQ context.
…d bound

Follow-up hardening from adversarial review of the PMU-sampling callers:

- Bound TOTAL iterations (a small multiple of out.len()), not just recorded
  frames. A return address outside ip_range is skipped without consuming a
  recorded slot, so the depth cap alone did not bound the loop; a crafted
  all-skip user chain (every [fp+8] out of range, monotonic small-gap [fp])
  could otherwise spin the walk — each step doing real work (two no-fault
  page-table walks) — into an IRQ-latency DoS. Apply the 8 MiB-gap check on
  every fp advance (both the skip and record paths), not only after recording.
- Bound the whole frame record [fp, fp+16) inside fp_range (not just fp), so
  the saved-LR read at fp+8 can never touch one word past fp_range.end.

Adds a test that would hang without the step cap (an ever-advancing all-skip
chain) and confirms it terminates recording only the leaf.
Add user-space callchains (PERF_SAMPLE_CALLCHAIN user region) and make the
whole FP unwind fault-proof from hard-IRQ context.

- perf/nofault.rs: read_user_word_nofault / read_kernel_word_nofault walk
  TTBR0 / TTBR1 by hand (4-level, 48-bit VA) against the always-mapped direct
  map, never dereferencing the input VA. Every table frame and the resolved
  leaf is bound-checked against phys_ram_ranges() before deref, so a valid PTE
  whose output is device MMIO (a user mmap of an RGA/NPU/JPU register window,
  or a concurrently-torn table on another CPU) can never trigger a live MMIO
  read (device side effect) or a data abort (panic) in the overflow handler.
- perf/unwind.rs: route the kernel reader through the no-fault path too, so a
  corrupt/stale in-kernel frame pointer that slips past the range guards yields
  None instead of faulting; add user_callchain, bounding the walk to a window
  above the interrupted SP_EL0.
- perf/sampling.rs: build_callchain now unwinds the user region for user
  samples (deep [PERF_CONTEXT_USER, ip, ra…]) via the no-fault reader, seeded
  from the interrupted x29 + SP.

Kernel deep frames still require an FP-enabled kernel (-Cforce-frame-pointers);
user frames unwind whenever the sampled binary keeps frame pointers.
Two grouped-C qemu/system cases exercising PERF_SAMPLE_CALLCHAIN end to end
(open sampling event with IP|TID|TIME|CALLCHAIN, mmap ring, parse the callchain
block, classify by PERF_CONTEXT_* markers):

- perf-hw-callchain-kernel: asserts a well-formed callchain record with a
  PERF_CONTEXT_KERNEL region + leaf IP. CI-safe: does NOT require a deep kernel
  chain, since the default kernel is built without frame pointers (like Linux
  without CONFIG_FRAME_POINTER), so the kernel region is leaf-only.
- perf-hw-callchain-user: builds a known nested chain outer->mid->inner->busy
  with -fno-omit-frame-pointer and asserts >= 4 user IPs after PERF_CONTEXT_USER
  — proving the kernel unwinds several USER frames via the no-fault TTBR0 reader.
  Uses a read(/dev/zero) workload: QEMU-TCG's cycle counter barely advances on a
  pure-ALU loop, so the sampling counter would seldom overflow.

Both skip-as-pass off aarch64. Validated in QEMU (smp4): kernel record
well-formed; user unwinds 8 frames deep across repeated runs.
Copilot AI review requested due to automatic review settings July 11, 2026 17:47

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants