Skip to Content

内核 PTE 断点

单文件实现 /root/shadow/shadow_ce/server/kern_ptebp.h,共 ~992 行,任何 ko 只要先 include kern_kpatch.h 即可直接 ptebp_init() 启用。

不是硬件断点 (HWBP 见 用户态硬件断点极致优化硬件断点),也不是 BRK 断点。PTE 断点靠故意把页表项改坏让 MMU 抛 page fault,在 do_page_fault 的 B-detour hook 里识别”是我设的陷阱”,抓现场,单步走一条,再把 PTE 改回去。


概念

为什么需要 PTE 断点

ARM64 硬件断点 (HWBP / WP) 资源极其稀缺:

资源Cortex-X925 (SM8750) 通常数量
DBGBVR_EL1 (instruction BP)6
DBGWVR_EL1 (watch point)4

一个游戏业务线程 + 4 个采样点就把硬件槽位用完。Cheat Engine 类工具经常需要几十个断点同时命中(子弹、血量、坐标、技能、…),硬件不够用。

PTE 断点换一个维度:页粒度的 R/W/X 拦截

  • 粒度粗:最小单位是 4KB 页(需要做 sub-page 过滤补偿)
  • 数量无限:只受内核内存限制 (当前上限 PTEBP_MAX_SLOTS=64 / PTEBP_MAX_PAGES=32,纯软件配额)
  • 不占硬件资源:和 HWBP、VBAR 极致断点可以并存

核心原理

MMU 在 stage-1 翻译时会检查 PTE 的权限位。把 PTE 改坏 → 下一次该页的访存就 fault → 走 do_page_fault → 我们 hook 了这个函数 → 识别出是自己设的陷阱 → 抓寄存器 → 恢复 PTE → 打开 MDSCR.SS 单步 → 那条指令走完 → 在 step handler 里再把 PTE 改回陷阱态。

user code ──┐ │ load/store/exec ┌──── MMU 翻译 ─────┐ │ PTE 被 ptebp 改坏 │ └────┬──────────────┘ │ data/instr abort VBAR_EL1 + 0x400 ─→ kernel vector ─→ do_page_fault ┌── B-detour hook ──┘ ptebp_fault_handler ├── 匹配 pg / sub-page 过滤 ├── 记录 hit (pt_regs × 33) ├── 尝试 LL/SC emulate (跳过单步) ├── 还原 PTE (saved or step_pte) ├── flush_page (tlbi vae1is + ASID) ├── TIF_SINGLESTEP + SPSR.SS + MDSCR.SS └── return 0 → eret back to userspace user code 跑一条 ──→ step exception (EC=0x32) ptebp_step_fn (注册的 step_hook) ├── 找 stepping 条目 ├── 清 TIF / MDSCR.SS ├── 把 PTE 改回 trap_pte └── flush_page → 继续跑

HWBP vs PTE BP 对比

维度硬件断点 (HWBP)PTE 断点
粒度byte 精确4KB 页 (需软件过滤)
数量BP=6 / WP=464 slots / 32 pages
资源争抢和 gdb/ptrace 共用 DBGBVR/DBGWVR只占 PTE + 若干 RAM 结构
命中延迟~163 ticks (perf_event_open 路径)较 HWBP 慢,走 do_page_fault
类型R/W/RW/XR/W/RW/X
单步恢复内核框架自动 (WP) / 手写 (BP)手写 step_hook + MDSCR.SS
同页 N 个点的代价每个都占槽同一 ptebp_page_trap 合并,0 增量

PTE 操纵

ARM64 stage-1 PTE 关键位

bit 含义 ptebp 会动的 --- ----- ----------- 0 Valid ✓ (WRITE 组合时用) 2:4 AttrIndx (S1PIE: 重映射表索引) 6:7 AP[2:1] ✓ wrprotect / clear USER 10 AF (Access Flag) (可选方案,没采用) 11 nG (ASID 相关) 52 Contiguous (PTE_CONT) ✓ 必须清,否则 16PTE 合一 53 PXN 54 UXN (Unprivileged eXecute Never)

几种 trap PTE 的生成方式

ptebp_compute_trap_pte() 按当前 slot 组合生成一个聚合 trap PTE(kern_ptebp.h:294):

组合结果 PTE
多类型混合 / 含 RW / 含 READ__pte(0) — 直接让页”不存在”,任何访问都 fault
纯 WRITEpte_wrprotect(saved) — 清 AP[2],只拦写
纯 EXEC__pte(0)S1PIE 下必须清 PIIndex,不是设 UXN
无 slot保持 saved

为什么 EXEC 不能只设 UXN? 参见 project_pte_s1pie_analysis:SM8750 启用 Stage-1 Permission Indirection Extension,老路子 AP[1] / UXN 会被 PIRE0_EL1/PIR_EL1 的 PI Index 重映射改写。把 PTE 整个清零等价于无效化该页,MMU 一定会 fault。

Sub-page 过滤(4KB 粗粒度的补救)

PTE 只能拦到 “这一页发生了访问”,具体命中的 byte 需要在 fault handler 里二次过滤:

// kern_ptebp.h:574-583 — fault handler 内 if (slot->bp_type == PTEBP_TYPE_EXEC) { if (regs->pc != slot->watch_addr) match = 0; /* PC 不等就不算命中 */ } else { unsigned long w_start = slot->watch_addr; unsigned long w_end = w_start + slot->watch_size; if (far < w_start || far >= w_end) match = 0; /* FAR 不在区间就不算命中 */ }

watch_size 由客户端传(常见 1/2/4/8 byte),这是”现象1”修复的直接产物(参见 session_2026_04_03_phenomenon1 / dev-history)。没做过滤时,同页上的其它访问会触发一堆假命中,UI 刷屏。

修改流程(含 contpte 展开)

static void ptebp_pte_set_trap(struct mm_struct *mm, unsigned long addr, pte_t *ptep, pte_t trap) { pte_t cur = __ptep_get(ptep); if (pte_valid(cur) && (pte_val(cur) & PTE_CONT)) contpte_try_unfold(mm, addr, ptep, cur); /* 必须先 unfold */ __set_pte(ptep, trap); }

为什么要 contpte_try_unfold?contpte 优化把 16 个连续 4KB PTE 合并成一个 TLB 条目,单独改其中一个 PTE 不会让硬件 TLB 失效。历史上这是把我们卡住很久的 Bug(见下文”已知问题”第 1 条)。

此外 第一次 arm 的时候 就把 PTE_CONTsaved_pte 里剥掉:

// ptebp_set: kern_ptebp.h:829-833 if (pg->slot_count == 1) { saved = __ptep_get(ptep); saved = clear_pte_bit(saved, __pgprot(PTE_CONT)); /* 防止 restore 时又聚合 */ pg->saved_pte = saved; }

单页 TLB flush

拦截修改完必须立刻 flush TLB,否则硬件仍用旧映射:

// kern_ptebp.h:195-202 static void ptebp_flush_page(struct mm_struct *mm, unsigned long va) { unsigned long asid = ASID(mm); unsigned long tlbi_val = (va >> 12) | (asid << 48); dsb(ishst); asm volatile("tlbi vae1is, %0" :: "r"(tlbi_val) : "memory"); dsb(ish); }

选择 tlbi vae1is(VA + ASID, inner-shareable):

  • 单 VA 粒度:只刷目标页,不炸全 TLB
  • 带 ASID:跨进程安全,不影响其它 mm
  • IS 后缀:广播到 inner-shareable 域内所有 CPU
  • dsb(ishst) / dsb(ish) 组 fence — 保证 PTE 写入对 TLBI 可见、TLBI 对后续访存可见

历史上 vmalle1is(全局 flush)也可以,但延迟比单页版高数倍。TLBI 细节见 project_tlbi_optimization


do_page_fault hook

Hook 机制

shadow_ce 用 B-detour (一跳 26-bit B 指令) 覆盖 do_page_fault 前几条指令。具体由 kern_kpatch.h 提供:

// kern_shadow_ce.c:48-49 #define KPATCH_TAG TAG #include "kern_kpatch.h" // kern_ptebp.h:677-680 ret = kpatch_hook_install(&ptebp_g_dpf_hook, "do_page_fault", ptebp_fault_handler);
  • 符号解析走 kallsyms(kpatch 用 kprobe bootstrap 拿到 kallsyms_lookup_name,绕过 CFI)
  • 写 .text 靠 aarch64_insn_patch_text_nosync(内核自带,临时关 CONFIG_STRICT_KERNEL_RWX
  • trampoline 页用 module_alloc 分配(确保在 ±128MB B 范围内)
  • 原指令搬到 trampoline,末尾加 B back+N*4 跳回

为何不用 kprobes? 详细讨论见 kprobes vs B-detourproject_cfi_hook_research。简短答案:do_page_fault 是 direct call,B-detour 能跑;但某些 CFI 环境下别的 hook 点 (例如 syscall_trace_exit) 是 indirect call 被 KCFI 拦住 → 要上 kprobes。PTE 断点走的是前者。

Handler 流程(核心路径)

ptebp_fault_handlerkern_ptebp.h:527

FSC 过滤(Fault Status Code)

ESR[5:0] 里的 FSC 告诉你到底是哪种 fault:

FSC含义ptebp 会接管?
0x04-0x07Translation fault L0-L3✓ (PTE=0 时)
0x08-0x0BAccess flag fault✗ (如果改 AF 才会走到)
0x0C-0x0FPermission fault L0-L3✓ (WRITE 组合 wrprotect)
0x00-0x03Address size fault✗ 透传
0x10+Sync external abort / TLB conflict✗ 透传
// kern_ptebp.h:540-542 fsc = esr & 0x3F; if (!((fsc >= 0x04 && fsc <= 0x07) || (fsc >= 0x0C && fsc <= 0x0F))) goto call_orig;

不在这个范围就立刻透传,避免把正常 COW / 缺页 / 外部错误吞掉导致应用或内核挂。

EC 判断 write / exec

// kern_ptebp.h:559-561 ec = (esr >> 26) & 0x3F; is_write = (ec == 0x24 || ec == 0x25) && (esr & (1 << 6)); // WnR bit is_exec = (ec == 0x20 || ec == 0x21);
EC含义走哪条路
0x20Instruction abort from lower ELEXEC 断点
0x21Instruction abort from same EL(不应在 user path 出现)
0x24Data abort from lower ELR/W 断点
0x25Data abort from same EL同上
0x32Software step走 step_hook 不走 fault
0x34/0x35Watchpoint EL0/EL1不是 PTE BP 的事

关于 EC 如何路由到 VBAR+0x200 / VBAR+0x400 / VBAR+0x000,以及为什么 syscall 里的 kernel uaccess 会走出 EL1 路径,参见 project_wp_ec_routing

误判回退

所有”不是我们想管的 fault”一律 call_orig(调 trampoline 执行原 do_page_fault):

call_orig: return ((ptebp_dpf_t)kpatch_trampoline(&ptebp_g_dpf_hook))(far, esr, regs);

kpatch_trampoline() 返回被我们搬走的原前几条指令 + 跳回 do_page_fault+N*4 的那块 module_alloc 页。走这条路用户看不出任何区别——不会干扰任何合法的 COW、stack grow、mmap populate。


单步 (stepping)

ARM64 单步不是 x86 的 EFLAGS.TF 一键开关,而是 MDSCR_EL1.SS + SPSR_EL1.SS 的两级状态机 + Debug Monitor Exception (DME)。

两级状态机

MDSCR.SSSPSR.SS (eret 回来后)行为
0ignored正常执行
10下一条指令 eret 后触发 software step exception (EC=0x32)
11先允许执行一条,然后触发

fault handler 把二者都设为 1(代码一见上方 Mermaid 图的 R 节点,实际代码 kern_ptebp.h:623-630):

set_ti_thread_flag(task_thread_info(current), TIF_SINGLESTEP); regs->pstate |= DBG_SPSR_SS; { u64 mdscr_val; asm volatile("mrs %0, mdscr_el1" : "=r"(mdscr_val)); mdscr_val |= DBG_MDSCR_SS; asm volatile("msr mdscr_el1, %0" :: "r"(mdscr_val)); }

注意 我们自己直写 MDSCR,没调 enable_debug_monitors — 因为 DME 全局启用早已在 ptebp_init 阶段完成(下面”MDE 全局启用”)。fault → step 切换路径上再调那个 API 会 WARN_ON(preemptible())

step exception 处理

单步异常走的不是 do_page_fault,而是内核的 user_step_hook 列表:

// kern_ptebp.h:521 static struct step_hook ptebp_g_step_hook = { .fn = ptebp_step_fn }; // kern_ptebp.h:690 call_register_user_step_hook(&ptebp_g_step_hook);

ptebp_step_fn (kern_ptebp.h:476):

  1. currentptebp_g_stepping[64] 里找 entry;找不到就是 “context switch 时泄漏过来的 MDSCR.SS”,手动把 SS bit 清掉,return DBG_HOOK_HANDLED(不 panic,静默吞)
  2. 找到就 clear_ti_thread_flag(TIF_SINGLESTEP) + 清 MDSCR.SS
  3. 如果 mm_users==0(进程已退)就直接返回
  4. 用缓存的 ptep(或重新 walk)把 trap PTE 装回去
  5. flush 单页 TLB
  6. return,eret 回到用户态继续跑

为什么 enable/disable_debug_monitors 不能随便调

enable_debug_monitors(DBG_ACTIVE_EL0) 是内核维护的 per-CPU refcount。gdb、kprobe、uprobe、perf HWBP 都在争这个全局 enable。

PTE 断点的做法是只在 init/exit 时调一次,中间不碰

// kern_ptebp.h:654-667 static void ptebp_enable_mde_cb(void *info) { call_enable_debug_monitors(DBG_ACTIVE_EL0); } static void ptebp_disable_mde_cb(void *info) { call_disable_debug_monitors(DBG_ACTIVE_EL0); } static int ptebp_cpu_online(unsigned int cpu) { call_enable_debug_monitors(DBG_ACTIVE_EL0); return 0; } // init: on_each_cpu(ptebp_enable_mde_cb, NULL, 1); ptebp_g_cpuhp_state = cpuhp_setup_state(CPUHP_AP_ONLINE_DYN, "ptebp:online", ptebp_cpu_online, NULL);

参见 project_mdscr_refcount_research。关键结论:

  • MDSCR 有全局 refcount,乱 inc/dec 会和其他子系统冲突
  • 直接 msr mdscr_el1 绕过 refcount 是安全的,因为 DME 已经在 init 时拉起来了
  • TIF_SINGLESTEP 是跨 context switch 保持 SS bit 的关键——没它的话切到别的任务就丢了

SVC 路径的 TIF_SINGLESTEP 陷阱

已知死坑(已修复,见 project_ptebp_svc_fix):

单步时如果那条指令正好是 SVC (系统调用),内核 syscall 路径会检查 TIF_SINGLESTEP,走完 syscall 就给进程发 SIGTRAP 杀掉

修复方法:hook syscall_trace_exit,在它被调用前暂时清 TIF,调完再设回来:

// kern_ptebp.h:641-650 static __nocfi notrace void ptebp_ste_handler(struct pt_regs *regs) { if (test_ti_thread_flag(task_thread_info(current), TIF_SINGLESTEP)) { clear_ti_thread_flag(task_thread_info(current), TIF_SINGLESTEP); ((ptebp_ste_t)kpatch_trampoline(&ptebp_g_ste_hook))(regs); set_ti_thread_flag(task_thread_info(current), TIF_SINGLESTEP); } else { ((ptebp_ste_t)kpatch_trampoline(&ptebp_g_ste_hook))(regs); } }

不修这个 bug,任何用 PTE 断点调试的进程只要一 syscall 就挂。


VBAR 协议与 EC routing

ARM64 异常入口走 VBAR_EL1 + 偏移

偏移用途
Current EL using SP0, Synchronous0x000kernel 内 EL1 SP_EL0 同步异常(罕见)
Current EL using SPx, Synchronous0x200kernel 内 EL1 同步异常 → 这里包含 uaccess WP
Lower EL using AArch64, Synchronous0x400userspace → kernel 的 user fault / svc / BRK

几乎所有 user-mode 触发的 PTE BP 异常都走 +0x400(EC=0x20/0x21/0x24/0x25/0x32)。do_page_fault 是这条路径上的分派点之一,所以 PTE BP 的 hook 放在 do_page_fault 就够了。

特殊情况 — syscall 内 kernel 侧 uaccess 触发 EL1 WP+0x200(EC=0x34/0x35)。shadow_ce 的 Extreme HWBP 走这条路径(见 极致优化硬件断点kern_vbar_engine.h)。PTE BP 目前不接管 EL1 侧,所以不碰 +0x200。

路径细节和为什么 0x34 (EL0) 走 +0x400、0x35 (EL1) 走 +0x200,见 project_wp_ec_routing


并发与多线程安全

Slot / page 表的锁

kern_ptebp.h:145 声明:

static DEFINE_SPINLOCK(ptebp_g_lock);

几乎所有修改 ptebp_g_pages[] / ptebp_g_slots[] / ptebp_g_stepping[] 的路径都 spin_lock_irqsave。事件 ring buffer 走 atomic 无锁:

static atomic_t ptebp_g_write_idx; static atomic_t ptebp_g_read_idx;

SPSC 语义:fault handler (任意 CPU) 写 idx,userspace poll 线程读 idx。

mm 生命周期

每个 slot 在 ptebp_setget_task_mm 拿引用,ptebp_clearexitmmput

fault handler 在跑的时候 mm 一定还活着(current->mm 就是 target)。但额外做了一道保护:

// kern_ptebp.h:551-557 if (atomic_read(&current->mm->mm_users) == 0) { spin_lock_irqsave(&ptebp_g_lock, flags); pg->active = 0; pg->slot_count = 0; spin_unlock_irqrestore(&ptebp_g_lock, flags); goto call_orig; }

遇到正在 teardown 的 mm 立刻弃置 page trap,避免去 walk 一个半崩的 mm 的页表。

mmu_notifier vs GUP reconnect

两个更深层的问题:

  1. 内核本身改了 PTE 怎么办?(migration、numa balance、madvise、compaction…) 我们 cache 的 ptep 指向的 PTE 可能被内核从下面改走,trap 丢了。 对策:mmu_notifier(参见 project_mmu_notifier_retrap)。当前 kern_ptebp.h 的实现没有注册 mmu_notifier,依赖 fault handler 重新 walk 兜底。

  2. GUP / swap 把页换出去再换回来,PFN 变了 参见 project_shadow_pg_a16GUP reconnect 方案 — A16 稳定版的做法。kern_ptebp.h 目前每次 ptep_walk_pte 都从 pgd 重新 walk 一遍,避免缓存 ptep 变脏;代价是比缓存方案慢(延迟分析见下文”性能”)。

  3. 多线程 stepping 窗口的同页噪音 线程 A 命中 → PTE 被改回 saved → 开 MDSCR.SS → 返回用户态;此时线程 B 也在同一页执行 → 不 fault 直接跑过去了(这是 bug 吗?不是——本来就是想在 step 窗口让 A 往前走,B 顺便搭车)。但如果 B 在 A 的 step 窗口内执行到 A 想监控的地址,就会”漏命中”。参见 dev-history / session_2026_04_05_changeling2,这是 PTE 断点相对 HWBP 的本质劣势。

ptebp_register_stepping 幂等

如果 nested fault(极偶尔发生:单步本身又 fault),先找现有 entry 删掉再注册新的:

// kern_ptebp.h:598-604 struct ptebp_stepping *old = ptebp_find_stepping(current); was_stepping = (old != NULL); if (old) ptebp_unregister_stepping(old); // ... ptebp_register_stepping(current, page_idx);

was_stepping 被记住,决定 restore 哪个 PTE:

if (was_stepping) restore = pg->saved_pte; /* 第 N 层 nested:放开继续跑 */ else if (is_exec) restore = ptebp_compute_step_pte(pg); /* 首次 exec:允许 exec 禁数据 */ else restore = pg->saved_pte; /* 首次 data:完全放开 */

ptebp_compute_step_pte() 只在 EXEC 路径上用 — step 窗口里还想继续拦同页的 data 访问(否则 R/W 断点在 step 窗口会漏命中)。


客户端管理 (bp_engine.py)

Python 端封装在 /root/shadow/shadow_ce/client/bp_engine.py

BreakpointEngine 单例

# bp_engine.py:13 class BreakpointEngine: _instance = None _lock = threading.Lock()

两条 TCP:control (self._ctl) 给 UI 主线程下 set/clear/query,poll (self._poll_client) 给后台 bp-poll daemon 线程做 50ms 节奏的 ptebp_poll

设置协议

# bp_engine.py:75-98 _type_map = {'r': 1, 'w': 2, 'rw': 3, 'x': 4} elif self._backend == "extreme_hwbp": result, slot_id, tc = self._ctl.ehwbp_set(...) else: # "kernel_fault" (PTE BP) result, slot_id = self._ctl.ptebp_set(self._pid, addr, bp_type, size)

三种后端切换hwbp(用户态 perf_event_open)、kernel_fault(PTE BP,本章主题)、extreme_hwbp(VBAR 汇编)。UI 的 Settings 下拉框选哪个就用哪个,同一个 BreakpointEngine 只绑一种。

poll 与分发

# bp_engine.py:166-182 hits = self._poll_client.ptebp_poll(512) # 每次最多拉 512 条 if not hits: time.sleep(0.05); continue buckets = {} for hit in hits: sid = hit[2] # slot_id buckets.setdefault(sid, []).append(hit) for sid, slot_hits in buckets.items(): cb = self._callbacks.get(sid) if cb: cb(slot_hits) # HwbpWindow 注册的回调

slot_id 分桶,每个 Found Code 窗口只看自己那条断点的命中(一页多断点时很有用)。

ioctl 协议 (shadow_ce.h)

CMD编号用途
SHADOW_CE_PTEBP_SET_IOWR('C', 10, ce_ptebp_set_req)加断点
SHADOW_CE_PTEBP_CLEAR_IOWR('C', 11, ce_ptebp_clear_req)删断点(slot_id=-1 清全部)
SHADOW_CE_PTEBP_POLL_IOWR('C', 12, ce_ptebp_poll_req)拉事件
SHADOW_CE_PTEBP_QUERY_IOWR('C', 13, ce_ptebp_query_req)查当前 slot 列表

对应 CE 协议 CMD 0xD2-0xD5struct ce_ptebp_hit 里一次带满 33 个寄存器 (x0..x28, fp, lr, sp, pc) — 对着 disasm view 直接能回放那条指令的上下文。


已知问题与修复

下面列出开发过程中实际踩过的 10 个坑,每条都在 dev-history / memory 有详细记录。

1. ✅ contpte 吃掉 PTE=0

  • 现象ptebp_set 返回成功,pr_info 确认 PTE: 0x20000bcb50afc3 → 0x0,但 3 秒后 poll 返回 0 hits
  • 根因:ARM64 contpte 把 16 个连续 PTE 折叠进一个 TLB 条目,单改其中一个 PTE 硬件视而不见
  • 修复ptebp_pte_set_trap()contpte_try_unfold 展开,ptebp_set() 首次 arm 时从 saved_pte 里剥掉 PTE_CONT bit
  • 文件kern_ptebp.h:168-175, kern_ptebp.h:829-833
  • 来源dev-history/ptebp-issues.md 问题 7、8

2. ✅ Kernel .text 写保护 panic

  • 现象:insmod 直接 panic
  • 根因:自己 *(u32*)addr = insndo_page_fault 前 4 字节触发 CONFIG_STRICT_KERNEL_RWX
  • 修复:改用 aarch64_insn_patch_text_nosync(内核 API,内部临时关写保护)
  • 来源dev-history/ptebp-issues.md 问题 1

3. ✅ 未导出符号 + CFI 拦截

  • 现象set_memory_x / register_user_step_hook 在设备上没 EXPORT_SYMBOL_GPL
  • 根因Module.symvers 有但 runtime vmlinux 没
  • 修复:全部走 kallsyms 解析 + __nocfi 包装 (kern_kpatch.h 提供的 call_* 系列);参见 project_cfi_hook_research
  • 来源dev-history/ptebp-issues.md 问题 2、3、5

4. ✅ PACIASP 首指令

  • 现象:dmesg 显示 first insn: 0xd503233f 不是期待的 SCS push
  • 根因:SM8750 有 PAC 硬件,should_patch_pac_into_scs() 返回 false
  • 修复:trampoline 运行时读实际字节,别硬编码任何指令
  • 来源dev-history/ptebp-issues.md 问题 4

5. ⚠️ cpuhp 回调 WARN_ON(preemptible())

  • 现象ptebp_cpu_online 执行时 dmesg 刷 Call trace
  • 根因enable_debug_monitors 内部检查 !preemptible(),而 cpuhp callback 可能 preemptible
  • 影响:只是 WARN,功能正常,不 panic
  • 状态:已知未修,可以加 preempt_disable() 包一层
  • 来源dev-history/ptebp-issues.md 问题 6

6. ✅ SVC 导致进程被 SIGTRAP 杀死

  • 现象:PTE 断点的进程一做 syscall 就挂
  • 根因:单步 + TIF_SINGLESTEP 会让 syscall exit path 给进程发 SIGTRAP
  • 修复:hook syscall_trace_exit,进入时暂时清 TIF,退出时设回
  • 文件kern_ptebp.h:641-650, 684-688
  • 来源project_ptebp_svc_fix / session_2026_04_05_changeling2

7. ✅ WRITE BP 卡死无限 fault

  • 现象:纯 WRITE 断点在同页有其他 WRITE 访问时进程挂死
  • 根因:step 窗口结束后回 fault 时 trap_pte 不同步,无限 fault 循环
  • 修复compute_step_pte 正确处理 WRITE-only 情况,step 窗口保留 wrprotect 但允许 exec;fault handler 用 was_stepping 识别 nested fault
  • 文件kern_ptebp.h:321-341, 597-609
  • 来源project_ptebp_write_hang

8. ✅ EXEC 只捕 1 条指令(“onehit” bug)

  • 现象:在某函数首条指令设 EXEC 断点,只报一次 hit,之后再也不来
  • 根因:stepping 清除时机竞态 — step handler 没来得及把 trap PTE 改回去,下次执行就跳过了
  • 修复:step handler 走完才 flush + 重装 trap;ptebp_register_stepping 加 nested detection
  • 来源project_ptebp_onehit_bug

9. ✅ 多类型混合同页误判

  • 现象:同一页上既有 WRITE 断点又有 EXEC 断点,WRITE 命中时也报 EXEC
  • 根因:未做细分,见到 fault 就按所有 slot 匹配
  • 修复ptebp_fault_handleris_write / is_exec + bp_type 做双向过滤(kern_ptebp.h:563-590)
  • 来源dev-history/ptebp-issues.md + 测试 ptebp_mixed_stress.c

10. ✅ Sub-page 精度(“现象 1”)

  • 现象:watch 了 0x1234(4 字节),同页上 0x1238 的访问也算命中
  • 根因:fault handler 只比较 FAR & PAGE_MASK,没看具体 offset
  • 修复:增加 watch_addr + watch_size 字段,handler 里 far < w_start || far >= w_end 就跳过
  • 文件kern_ptebp.h:574-583(代码片段见前文 Sub-page 过滤一节)
  • 来源session_2026_04_03_phenomenon1

11. ✅ EXEC miss(libtersafe 高频场景)

  • 现象:libtersafe 的 EXEC 断点偶发 miss
  • 根因:PC 精确匹配失败 + ASLR 过期
  • 修复 / 验证:多套方案在 session_2026_04_03_ptebp_exec_miss 展开;PTE BP 侧保留目前的 PC 精确匹配
  • 状态:已记录,短期绕道是用 HWBP / extreme_hwbp 替代

压力测试

/root/shadow/shadow_ce/tests/ 里 6 个 stress / sanity 测试覆盖所有 bp_type 与并发场景:

文件场景行数
test_ptebp.c最小 sanity — ioctl 直接调用,不走 TCP121
ptebp_stress.c20 线程 × 60Hz × 2 页,每页 10 个 inc/ldxr 函数200
ptebp_r_stress.c20 线程纯 READ (10 plain ldr + 10 ldxr)134
ptebp_w_stress.c20 线程纯 WRITE (inc counter,测 wrprotect 路径)130
ptebp_rw_stress.c20 线程 RW (ldxr/stxr atomic inc) — 测 LL/SC emulation153
ptebp_mixed_stress.c5 EXEC + 5 READ + 5 WRITE + 5 RW 全在同一页172
ptebp_v2_samepage.cEXEC + READ + WRITE 全部落在 1 张 4KB 页218

示例场景 — ptebp_mixed_stress.c

Page layout (4KB): [0x000..0x09F] func 0-4: EXEC 目标 (ldr/add/str/ret) [0x0A0..0x13F] func 5-9: READ 目标 (ldr/mov/ret) [0x140..0x1DF] func 10-14: WRITE 目标 [0x1E0..0x2FF] func 15-19: RW 目标 (ldxr/stxr) [0x800..0x89F] 20 × uint64_t counters

所有 20 个函数在同一 4KB 页,20 个 pthread 每 60Hz 各调自己那一个。这就是”N 个 slot 合并到 1 个 ptebp_page_trap”的最极端场景。

通过率

当前主线(A16 kernel 6.6.89):28/28 ALL PASS(参见 memory project_shadow_pg_a16session_2026_03_28_ptrscan)。回到 A15 6.6.66 也是 26/26 稳定。

怎么跑

# 编译 aarch64-linux-gnu-gcc -O2 -static -o ptebp_mixed_stress ptebp_mixed_stress.c -lpthread # 推到机器 adb.exe push ptebp_mixed_stress /data/local/tmp/ adb.exe shell "chmod 755 /data/local/tmp/ptebp_mixed_stress" # 启动 target,拿 pid adb.exe shell "su -c '/data/local/tmp/ptebp_mixed_stress &'" adb.exe shell "pidof ptebp_mixed_stress" # 在 client 里连上该 pid,用 PTE BP 设 20 个断点,看 Found Code 刷命中

test_ptebp.c 是最纯粹的 sanity 二进制,直接打开 /dev/cx_xxxxxx(ko 注册的 misc device)调 ioctl,不走 TCP、不起 ce_server,用来 bisect 内核侧 bug 最方便。


性能

命中延迟分解 (Cortex-X925 @ 4.32GHz, 1 tick ≈ 0.23ns)

路径ticks备注
硬件 fault → vector → kernel entry~50ARM64 eret/eret 最小开销
do_page_fault → B-detour 跳转~15trampoline + 重建栈
FSC/EC 过滤 + find_page~30线性扫 32 entries,cache-hot 后 ~10
sub-page 过滤 + store_hit~40memcpy 33 regs
restore PTE + flush_page~80tlbi 是大头 (~50)
TIF + SPSR + MDSCR 配置~20msr fence cost
eret back + user 执行 1 条 + step exc~50同上
step handler → restore trap~70再来一次 tlbi
总计~283首次命中

vs 对比:

方案单次 hit ticks
perf_event_open HWBP~163
PTE BP (当前)~283
PTE BP (优化路线目标)~110-60
Extreme HWBP (VBAR 汇编)~12

优化路线 (project_ptebp_283_to_50_ticks / project_ptebp_optimization)

3 阶段:

  1. Slot union cache + 线性扫改 hash — 省 ~40 ticks
  2. 预计算 trap/step PTE — set 时算好,fault 路径直接 load,省 ~30 ticks
  3. VBAR 拦截 + mmu_notifier 预 walk — 省 ~80 ticks,需要和 extreme_hwbp 融合

详见 project_ptebp_cache_researchsession_2026_04_03_gamma_research


限制

  1. 粒度是页 — byte 精确完全靠软件过滤补救(watch_addr + watch_size),且每页最多 16 个 slotPTEBP_MAX_SLOTS_PER_PAGE
  2. 同页不同线程 step 窗口漏命中 — 如前文”并发”节描述,这是架构必然的权衡
  3. kernel 改 PTE 时需要协同 — 当前没接 mmu_notifier,遇到 migration / swap 只靠重新 walk 兜底;重 trap 有延迟
  4. contpte 必须展开 — 任何 PTE 修改前必走 contpte_try_unfold,否则 TLB 无视你的修改
  5. DME 全局 refcount 不可乱动 — init 时 enable_debug_monitors 一次到底,中间路径只能 msr mdscr_el1 直写。和 gdb、kprobe 共用有风险
  6. 部分 syscall 场景需要 syscall_trace_exit hook 配合,否则 SIGTRAP 杀进程
  7. CFI_CLANG 下 hook 点选择受限 — direct call 才能 B-detour;indirect call 必须换 kprobes,参见 project_cfi_hook_research
  8. 上限写死PTEBP_MAX_SLOTS=64 / PTEBP_MAX_PAGES=32 / PTEBP_MAX_EVENTS=4096,高负载下 ring overflow(lose events)只是 wrap-around,不会 panic

参考

  • 源码: /root/shadow/shadow_ce/server/kern_ptebp.h
  • ioctl 协议: /root/shadow/shadow_ce/server/shadow_ce.h:110-170
  • 整合点: /root/shadow/shadow_ce/server/kern_shadow_ce.c:142-183, 290-302
  • 客户端: /root/shadow/shadow_ce/client/bp_engine.py
  • 测试: /root/shadow/shadow_ce/tests/ptebp_*.c
  • 开发史: dev-history/ptebp-issues.md
  • 相关章节: 用户态硬件断点 · 极致优化硬件断点
Last updated on