内核 PTE 断点
单文件实现
/root/shadow/shadow_ce/server/kern_ptebp.h,共 ~992 行,任何 ko 只要先 includekern_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=4 | 64 slots / 32 pages |
| 资源争抢 | 和 gdb/ptrace 共用 DBGBVR/DBGWVR | 只占 PTE + 若干 RAM 结构 |
| 命中延迟 | ~163 ticks (perf_event_open 路径) | 较 HWBP 慢,走 do_page_fault |
| 类型 | R/W/RW/X | R/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 |
| 纯 WRITE | pte_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_CONT 从 saved_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-detour 及
project_cfi_hook_research。简短答案:do_page_fault是 direct call,B-detour 能跑;但某些 CFI 环境下别的 hook 点 (例如syscall_trace_exit) 是 indirect call 被 KCFI 拦住 → 要上 kprobes。PTE 断点走的是前者。
Handler 流程(核心路径)
ptebp_fault_handler 在 kern_ptebp.h:527。
FSC 过滤(Fault Status Code)
ESR[5:0] 里的 FSC 告诉你到底是哪种 fault:
| FSC | 含义 | ptebp 会接管? |
|---|---|---|
| 0x04-0x07 | Translation fault L0-L3 | ✓ (PTE=0 时) |
| 0x08-0x0B | Access flag fault | ✗ (如果改 AF 才会走到) |
| 0x0C-0x0F | Permission fault L0-L3 | ✓ (WRITE 组合 wrprotect) |
| 0x00-0x03 | Address 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 | 含义 | 走哪条路 |
|---|---|---|
| 0x20 | Instruction abort from lower EL | EXEC 断点 |
| 0x21 | Instruction abort from same EL | (不应在 user path 出现) |
| 0x24 | Data abort from lower EL | R/W 断点 |
| 0x25 | Data abort from same EL | 同上 |
| 0x32 | Software step | 走 step_hook 不走 fault |
| 0x34/0x35 | Watchpoint 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.SS | SPSR.SS (eret 回来后) | 行为 |
|---|---|---|
| 0 | ignored | 正常执行 |
| 1 | 0 | 下一条指令 eret 后触发 software step exception (EC=0x32) |
| 1 | 1 | 先允许执行一条,然后触发 |
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):
- 用
current在ptebp_g_stepping[64]里找 entry;找不到就是 “context switch 时泄漏过来的 MDSCR.SS”,手动把 SS bit 清掉,return DBG_HOOK_HANDLED(不 panic,静默吞) - 找到就
clear_ti_thread_flag(TIF_SINGLESTEP)+ 清 MDSCR.SS - 如果
mm_users==0(进程已退)就直接返回 - 用缓存的
ptep(或重新 walk)把 trap PTE 装回去 - flush 单页 TLB
- 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, Synchronous | 0x000 | kernel 内 EL1 SP_EL0 同步异常(罕见) |
| Current EL using SPx, Synchronous | 0x200 | kernel 内 EL1 同步异常 → 这里包含 uaccess WP |
| Lower EL using AArch64, Synchronous | 0x400 | userspace → 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_set 时 get_task_mm 拿引用,ptebp_clear 或 exit 时 mmput。
fault handler 在跑的时候 mm 一定还活着(current->mm 就是 target)。但额外做了一道保护:
// kern_ptebp.h:551-557
if (atomic_read(¤t->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
两个更深层的问题:
-
内核本身改了 PTE 怎么办?(migration、numa balance、madvise、compaction…) 我们 cache 的
ptep指向的 PTE 可能被内核从下面改走,trap 丢了。 对策:mmu_notifier(参见project_mmu_notifier_retrap)。当前 kern_ptebp.h 的实现没有注册 mmu_notifier,依赖 fault handler 重新 walk 兜底。 -
GUP / swap 把页换出去再换回来,PFN 变了 参见
project_shadow_pg_a16的 GUP reconnect 方案 — A16 稳定版的做法。kern_ptebp.h 目前每次ptep_walk_pte都从 pgd 重新 walk 一遍,避免缓存 ptep 变脏;代价是比缓存方案慢(延迟分析见下文”性能”)。 -
多线程 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-0xD5。struct 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_CONTbit - 文件:
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 = insn写do_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_handler用is_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 直接调用,不走 TCP | 121 |
ptebp_stress.c | 20 线程 × 60Hz × 2 页,每页 10 个 inc/ldxr 函数 | 200 |
ptebp_r_stress.c | 20 线程纯 READ (10 plain ldr + 10 ldxr) | 134 |
ptebp_w_stress.c | 20 线程纯 WRITE (inc counter,测 wrprotect 路径) | 130 |
ptebp_rw_stress.c | 20 线程 RW (ldxr/stxr atomic inc) — 测 LL/SC emulation | 153 |
ptebp_mixed_stress.c | 5 EXEC + 5 READ + 5 WRITE + 5 RW 全在同一页 | 172 |
ptebp_v2_samepage.c | EXEC + 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_a16 与 session_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 | ~50 | ARM64 eret/eret 最小开销 |
| do_page_fault → B-detour 跳转 | ~15 | trampoline + 重建栈 |
| FSC/EC 过滤 + find_page | ~30 | 线性扫 32 entries,cache-hot 后 ~10 |
| sub-page 过滤 + store_hit | ~40 | memcpy 33 regs |
| restore PTE + flush_page | ~80 | tlbi 是大头 (~50) |
| TIF + SPSR + MDSCR 配置 | ~20 | msr 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 阶段:
- Slot union cache + 线性扫改 hash — 省 ~40 ticks
- 预计算 trap/step PTE — set 时算好,fault 路径直接 load,省 ~30 ticks
- VBAR 拦截 + mmu_notifier 预 walk — 省 ~80 ticks,需要和 extreme_hwbp 融合
详见 project_ptebp_cache_research、session_2026_04_03_gamma_research。
限制
- 粒度是页 — byte 精确完全靠软件过滤补救(
watch_addr+watch_size),且每页最多 16 个 slot(PTEBP_MAX_SLOTS_PER_PAGE) - 同页不同线程 step 窗口漏命中 — 如前文”并发”节描述,这是架构必然的权衡
- kernel 改 PTE 时需要协同 — 当前没接 mmu_notifier,遇到 migration / swap 只靠重新 walk 兜底;重 trap 有延迟
- contpte 必须展开 — 任何 PTE 修改前必走
contpte_try_unfold,否则 TLB 无视你的修改 - DME 全局 refcount 不可乱动 — init 时
enable_debug_monitors一次到底,中间路径只能msr mdscr_el1直写。和 gdb、kprobe 共用有风险 - 部分 syscall 场景需要
syscall_trace_exithook 配合,否则 SIGTRAP 杀进程 - CFI_CLANG 下 hook 点选择受限 — direct call 才能 B-detour;indirect call 必须换 kprobes,参见
project_cfi_hook_research - 上限写死 —
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
- 相关章节: 用户态硬件断点 · 极致优化硬件断点